Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f7eebce4b | ||
|
|
94ef39234d | ||
|
|
6d14dd8b47 | ||
|
|
0ee7739091 | ||
|
|
5c135358fc | ||
|
|
74554c3669 | ||
|
|
a89d3a9701 | ||
|
|
180e213d00 | ||
|
|
c33698ce52 | ||
|
|
5b4cb7d35a | ||
|
|
eacbafb1ea | ||
|
|
08fe4326a6 | ||
|
|
fabb33252c | ||
|
|
00dcb872b7 | ||
|
|
ea23651cb6 | ||
|
|
2273c71a87 | ||
|
|
9cb5fdd2fa | ||
|
|
73c54a7ebc | ||
|
|
7cea297095 | ||
|
|
019d36a0ab | ||
|
|
1922b2122b | ||
|
|
3dfe54f1cb | ||
|
|
5f04a3b622 | ||
|
|
05a8e0a842 | ||
|
|
9dac9b7a36 | ||
|
|
b99e754395 |
83
CHANGELOG.md
83
CHANGELOG.md
@@ -1,3 +1,86 @@
|
||||
## [3.17.9](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.17.8...v3.17.9) (2025-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **arcade:** read nested gameConfig correctly when creating sessions ([94ef392](https://github.com/antialias/soroban-abacus-flashcards/commit/94ef39234d362b82e032cb69d3561b9fcb436eaf))
|
||||
|
||||
## [3.17.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.17.7...v3.17.8) (2025-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **arcade:** preserve game settings when returning to game selection ([0ee7739](https://github.com/antialias/soroban-abacus-flashcards/commit/0ee7739091d60580d2f98cfe288b8586b03348f3))
|
||||
|
||||
## [3.17.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.17.6...v3.17.7) (2025-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **arcade:** prevent gameConfig from being overwritten when switching games ([a89d3a9](https://github.com/antialias/soroban-abacus-flashcards/commit/a89d3a970137471e2652de992c45370dbb97416d))
|
||||
|
||||
## [3.17.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.17.5...v3.17.6) (2025-10-15)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **logging:** use JSON.stringify for all object logging ([c33698c](https://github.com/antialias/soroban-abacus-flashcards/commit/c33698ce52ebdc18ce3a0d856f9241c7389ed651))
|
||||
|
||||
## [3.17.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.17.4...v3.17.5) (2025-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **arcade:** implement settings persistence for matching game ([08fe432](https://github.com/antialias/soroban-abacus-flashcards/commit/08fe4326a6a7c484b9058a241f4ff79b3fb5125f))
|
||||
|
||||
## [3.17.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.17.3...v3.17.4) (2025-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **matching:** add settings persistence to matching game ([00dcb87](https://github.com/antialias/soroban-abacus-flashcards/commit/00dcb872b7e70bdb7de301b56fe42195e6ee923f))
|
||||
|
||||
## [3.17.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.17.2...v3.17.3) (2025-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **arcade:** preserve gameConfig when switching games ([2273c71](https://github.com/antialias/soroban-abacus-flashcards/commit/2273c71a872a5122d0b2023835fe30640106048e))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* remove verbose console logging for cleaner debugging ([9cb5fdd](https://github.com/antialias/soroban-abacus-flashcards/commit/9cb5fdd2fa43560adc32dd052f47a7b06b2c5b69))
|
||||
|
||||
## [3.17.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.17.1...v3.17.2) (2025-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **room-data:** update query cache when gameConfig changes ([7cea297](https://github.com/antialias/soroban-abacus-flashcards/commit/7cea297095b78d74f5b77ca83489ec1be684a486))
|
||||
|
||||
## [3.17.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.17.0...v3.17.1) (2025-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **arcade-rooms:** navigate to invite link after room creation ([1922b21](https://github.com/antialias/soroban-abacus-flashcards/commit/1922b2122bb1bc4aeada7526d8c46aa89024bb00))
|
||||
* **memory-quiz:** scope game settings by game name for proper persistence ([3dfe54f](https://github.com/antialias/soroban-abacus-flashcards/commit/3dfe54f1cb89bd636e763e1c5acb03776f97c011))
|
||||
|
||||
## [3.17.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.16.0...v3.17.0) (2025-10-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **memory-quiz:** persist game settings per-game across sessions ([05a8e0a](https://github.com/antialias/soroban-abacus-flashcards/commit/05a8e0a84272c6c45a4014413ee00726eb88b76a))
|
||||
|
||||
## [3.16.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.15.2...v3.16.0) (2025-10-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **arcade:** broadcast game selection changes to all room members ([b99e754](https://github.com/antialias/soroban-abacus-flashcards/commit/b99e7543952bb0d47f42e79dc4226b3c1280a0ee))
|
||||
|
||||
## [3.15.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v3.15.1...v3.15.2) (2025-10-15)
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,36 @@ export async function PATCH(req: NextRequest, context: RouteContext) {
|
||||
const viewerId = await getViewerId()
|
||||
const body = await req.json()
|
||||
|
||||
console.log(
|
||||
'[Settings API] PATCH request received:',
|
||||
JSON.stringify(
|
||||
{
|
||||
roomId,
|
||||
body,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
// Read current room state from database BEFORE any changes
|
||||
const [currentRoom] = await db
|
||||
.select()
|
||||
.from(schema.arcadeRooms)
|
||||
.where(eq(schema.arcadeRooms.id, roomId))
|
||||
|
||||
console.log(
|
||||
'[Settings API] Current room state in database BEFORE update:',
|
||||
JSON.stringify(
|
||||
{
|
||||
gameName: currentRoom?.gameName,
|
||||
gameConfig: currentRoom?.gameConfig,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
// Check if user is the host
|
||||
const members = await getRoomMembers(roomId)
|
||||
const currentMember = members.find((m) => m.userId === viewerId)
|
||||
@@ -97,6 +127,11 @@ export async function PATCH(req: NextRequest, context: RouteContext) {
|
||||
updateData.gameConfig = body.gameConfig
|
||||
}
|
||||
|
||||
console.log(
|
||||
'[Settings API] Update data to be written to database:',
|
||||
JSON.stringify(updateData, null, 2)
|
||||
)
|
||||
|
||||
// If game is being changed (or cleared), delete the existing arcade session
|
||||
// This ensures a fresh session will be created with the new game settings
|
||||
if (body.gameName !== undefined) {
|
||||
@@ -111,6 +146,45 @@ export async function PATCH(req: NextRequest, context: RouteContext) {
|
||||
.where(eq(schema.arcadeRooms.id, roomId))
|
||||
.returning()
|
||||
|
||||
console.log(
|
||||
'[Settings API] Room state in database AFTER update:',
|
||||
JSON.stringify(
|
||||
{
|
||||
gameName: updatedRoom.gameName,
|
||||
gameConfig: updatedRoom.gameConfig,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
// Broadcast game change to all room members
|
||||
if (body.gameName !== undefined) {
|
||||
const io = await getSocketIO()
|
||||
if (io) {
|
||||
try {
|
||||
console.log(`[Settings API] Broadcasting game change to room ${roomId}: ${body.gameName}`)
|
||||
const broadcastData: {
|
||||
roomId: string
|
||||
gameName: string | null
|
||||
gameConfig?: Record<string, unknown>
|
||||
} = {
|
||||
roomId,
|
||||
gameName: body.gameName,
|
||||
}
|
||||
|
||||
// Only include gameConfig if it was explicitly provided
|
||||
if (body.gameConfig !== undefined) {
|
||||
broadcastData.gameConfig = body.gameConfig
|
||||
}
|
||||
|
||||
io.to(`room:${roomId}`).emit('room-game-changed', broadcastData)
|
||||
} catch (socketError) {
|
||||
console.error('[Settings API] Failed to broadcast game change:', socketError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If setting to retired, expel all non-owner members
|
||||
if (body.accessMode === 'retired') {
|
||||
const nonOwnerMembers = members.filter((m) => !m.isCreator)
|
||||
|
||||
@@ -28,6 +28,19 @@ export async function GET() {
|
||||
return NextResponse.json({ error: 'Room not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
console.log(
|
||||
'[Current Room API] Room data READ from database:',
|
||||
JSON.stringify(
|
||||
{
|
||||
roomId,
|
||||
gameName: room.gameName,
|
||||
gameConfig: room.gameConfig,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
// Get members
|
||||
const members = await getRoomMembers(roomId)
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function RoomBrowserPage() {
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
router.push(`/arcade-rooms/${data.room.id}`)
|
||||
router.push(`/join/${data.room.code}`)
|
||||
} catch (err) {
|
||||
console.error('Failed to create room:', err)
|
||||
showError('Failed to create room', err instanceof Error ? err.message : undefined)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { type ReactNode, useCallback, useEffect, useMemo } from 'react'
|
||||
import { useArcadeSession } from '@/hooks/useArcadeSession'
|
||||
import { useRoomData } from '@/hooks/useRoomData'
|
||||
import { useRoomData, useUpdateGameConfig } from '@/hooks/useRoomData'
|
||||
import { useViewerId } from '@/hooks/useViewerId'
|
||||
import {
|
||||
buildPlayerMetadata as buildPlayerMetadataUtil,
|
||||
@@ -240,6 +240,7 @@ export function RoomMemoryPairsProvider({ children }: { children: ReactNode }) {
|
||||
const { data: viewerId } = useViewerId()
|
||||
const { roomData } = useRoomData() // Fetch room data for room-based play
|
||||
const { activePlayerCount, activePlayers: activePlayerIds, players } = useGameMode()
|
||||
const { mutate: updateGameConfig } = useUpdateGameConfig()
|
||||
|
||||
// Get active player IDs directly as strings (UUIDs)
|
||||
const activePlayers = Array.from(activePlayerIds)
|
||||
@@ -247,8 +248,77 @@ export function RoomMemoryPairsProvider({ children }: { children: ReactNode }) {
|
||||
// Derive game mode from active player count
|
||||
const gameMode = activePlayerCount > 1 ? 'multiplayer' : 'single'
|
||||
|
||||
// NO LOCAL STATE - Configuration lives in session state
|
||||
// Changes are sent as moves and synchronized across all room members
|
||||
// Track roomData.gameConfig changes
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
'[RoomMemoryPairsProvider] roomData.gameConfig changed:',
|
||||
JSON.stringify(
|
||||
{
|
||||
gameConfig: roomData?.gameConfig,
|
||||
roomId: roomData?.id,
|
||||
gameName: roomData?.gameName,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
}, [roomData?.gameConfig, roomData?.id, roomData?.gameName])
|
||||
|
||||
// Merge saved game config from room with initialState
|
||||
// Settings are scoped by game name to preserve settings when switching games
|
||||
const mergedInitialState = useMemo(() => {
|
||||
const gameConfig = roomData?.gameConfig as Record<string, any> | null | undefined
|
||||
console.log(
|
||||
'[RoomMemoryPairsProvider] Loading settings from database:',
|
||||
JSON.stringify(
|
||||
{
|
||||
gameConfig,
|
||||
roomId: roomData?.id,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
if (!gameConfig) {
|
||||
console.log('[RoomMemoryPairsProvider] No gameConfig, using initialState')
|
||||
return initialState
|
||||
}
|
||||
|
||||
// Get settings for this specific game (matching)
|
||||
const savedConfig = gameConfig.matching as Record<string, any> | null | undefined
|
||||
console.log(
|
||||
'[RoomMemoryPairsProvider] Saved config for matching:',
|
||||
JSON.stringify(savedConfig, null, 2)
|
||||
)
|
||||
|
||||
if (!savedConfig) {
|
||||
console.log('[RoomMemoryPairsProvider] No saved config for matching, using initialState')
|
||||
return initialState
|
||||
}
|
||||
|
||||
const merged = {
|
||||
...initialState,
|
||||
// Restore settings from saved config
|
||||
gameType: savedConfig.gameType ?? initialState.gameType,
|
||||
difficulty: savedConfig.difficulty ?? initialState.difficulty,
|
||||
turnTimer: savedConfig.turnTimer ?? initialState.turnTimer,
|
||||
}
|
||||
console.log(
|
||||
'[RoomMemoryPairsProvider] Merged state:',
|
||||
JSON.stringify(
|
||||
{
|
||||
gameType: merged.gameType,
|
||||
difficulty: merged.difficulty,
|
||||
turnTimer: merged.turnTimer,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
return merged
|
||||
}, [roomData?.gameConfig])
|
||||
|
||||
// Arcade session integration WITH room sync
|
||||
const {
|
||||
@@ -259,7 +329,7 @@ export function RoomMemoryPairsProvider({ children }: { children: ReactNode }) {
|
||||
} = useArcadeSession<MemoryPairsState>({
|
||||
userId: viewerId || '',
|
||||
roomId: roomData?.id, // CRITICAL: Pass roomId for network sync across room members
|
||||
initialState,
|
||||
initialState: mergedInitialState,
|
||||
applyMove: applyMoveOptimistically,
|
||||
})
|
||||
|
||||
@@ -498,6 +568,8 @@ export function RoomMemoryPairsProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const setGameType = useCallback(
|
||||
(gameType: typeof state.gameType) => {
|
||||
console.log('[RoomMemoryPairsProvider] setGameType called:', gameType)
|
||||
|
||||
// Use first active player as playerId, or empty string if none
|
||||
const playerId = activePlayers[0] || ''
|
||||
sendMove({
|
||||
@@ -506,12 +578,45 @@ export function RoomMemoryPairsProvider({ children }: { children: ReactNode }) {
|
||||
userId: viewerId || '',
|
||||
data: { field: 'gameType', value: gameType },
|
||||
})
|
||||
|
||||
// Save setting to room's gameConfig for persistence
|
||||
if (roomData?.id) {
|
||||
const currentGameConfig = (roomData.gameConfig as Record<string, any>) || {}
|
||||
const currentMatchingConfig = (currentGameConfig.matching as Record<string, any>) || {}
|
||||
|
||||
const updatedConfig = {
|
||||
...currentGameConfig,
|
||||
matching: {
|
||||
...currentMatchingConfig,
|
||||
gameType,
|
||||
},
|
||||
}
|
||||
console.log(
|
||||
'[RoomMemoryPairsProvider] Saving gameType to database:',
|
||||
JSON.stringify(
|
||||
{
|
||||
roomId: roomData.id,
|
||||
updatedConfig,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
updateGameConfig({
|
||||
roomId: roomData.id,
|
||||
gameConfig: updatedConfig,
|
||||
})
|
||||
} else {
|
||||
console.warn('[RoomMemoryPairsProvider] Cannot save gameType - no roomData.id')
|
||||
}
|
||||
},
|
||||
[activePlayers, sendMove, viewerId]
|
||||
[activePlayers, sendMove, viewerId, roomData?.id, roomData?.gameConfig, updateGameConfig]
|
||||
)
|
||||
|
||||
const setDifficulty = useCallback(
|
||||
(difficulty: typeof state.difficulty) => {
|
||||
console.log('[RoomMemoryPairsProvider] setDifficulty called:', difficulty)
|
||||
|
||||
const playerId = activePlayers[0] || ''
|
||||
sendMove({
|
||||
type: 'SET_CONFIG',
|
||||
@@ -519,12 +624,45 @@ export function RoomMemoryPairsProvider({ children }: { children: ReactNode }) {
|
||||
userId: viewerId || '',
|
||||
data: { field: 'difficulty', value: difficulty },
|
||||
})
|
||||
|
||||
// Save setting to room's gameConfig for persistence
|
||||
if (roomData?.id) {
|
||||
const currentGameConfig = (roomData.gameConfig as Record<string, any>) || {}
|
||||
const currentMatchingConfig = (currentGameConfig.matching as Record<string, any>) || {}
|
||||
|
||||
const updatedConfig = {
|
||||
...currentGameConfig,
|
||||
matching: {
|
||||
...currentMatchingConfig,
|
||||
difficulty,
|
||||
},
|
||||
}
|
||||
console.log(
|
||||
'[RoomMemoryPairsProvider] Saving difficulty to database:',
|
||||
JSON.stringify(
|
||||
{
|
||||
roomId: roomData.id,
|
||||
updatedConfig,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
updateGameConfig({
|
||||
roomId: roomData.id,
|
||||
gameConfig: updatedConfig,
|
||||
})
|
||||
} else {
|
||||
console.warn('[RoomMemoryPairsProvider] Cannot save difficulty - no roomData.id')
|
||||
}
|
||||
},
|
||||
[activePlayers, sendMove, viewerId]
|
||||
[activePlayers, sendMove, viewerId, roomData?.id, roomData?.gameConfig, updateGameConfig]
|
||||
)
|
||||
|
||||
const setTurnTimer = useCallback(
|
||||
(turnTimer: typeof state.turnTimer) => {
|
||||
console.log('[RoomMemoryPairsProvider] setTurnTimer called:', turnTimer)
|
||||
|
||||
const playerId = activePlayers[0] || ''
|
||||
sendMove({
|
||||
type: 'SET_CONFIG',
|
||||
@@ -532,8 +670,39 @@ export function RoomMemoryPairsProvider({ children }: { children: ReactNode }) {
|
||||
userId: viewerId || '',
|
||||
data: { field: 'turnTimer', value: turnTimer },
|
||||
})
|
||||
|
||||
// Save setting to room's gameConfig for persistence
|
||||
if (roomData?.id) {
|
||||
const currentGameConfig = (roomData.gameConfig as Record<string, any>) || {}
|
||||
const currentMatchingConfig = (currentGameConfig.matching as Record<string, any>) || {}
|
||||
|
||||
const updatedConfig = {
|
||||
...currentGameConfig,
|
||||
matching: {
|
||||
...currentMatchingConfig,
|
||||
turnTimer,
|
||||
},
|
||||
}
|
||||
console.log(
|
||||
'[RoomMemoryPairsProvider] Saving turnTimer to database:',
|
||||
JSON.stringify(
|
||||
{
|
||||
roomId: roomData.id,
|
||||
updatedConfig,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
updateGameConfig({
|
||||
roomId: roomData.id,
|
||||
gameConfig: updatedConfig,
|
||||
})
|
||||
} else {
|
||||
console.warn('[RoomMemoryPairsProvider] Cannot save turnTimer - no roomData.id')
|
||||
}
|
||||
},
|
||||
[activePlayers, sendMove, viewerId]
|
||||
[activePlayers, sendMove, viewerId, roomData?.id, roomData?.gameConfig, updateGameConfig]
|
||||
)
|
||||
|
||||
const goToSetup = useCallback(() => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useGameMode } from '@/contexts/GameModeContext'
|
||||
import { useArcadeSession } from '@/hooks/useArcadeSession'
|
||||
import { useRoomData } from '@/hooks/useRoomData'
|
||||
import { useRoomData, useUpdateGameConfig } from '@/hooks/useRoomData'
|
||||
import { useViewerId } from '@/hooks/useViewerId'
|
||||
import type { GameMove } from '@/lib/arcade/validation'
|
||||
import { TEAM_MOVE } from '@/lib/arcade/validation/types'
|
||||
@@ -55,35 +55,21 @@ function applyMoveOptimistically(state: SorobanQuizState, move: GameMove): Sorob
|
||||
const activePlayers = move.data.activePlayers || []
|
||||
const playerMetadata = move.data.playerMetadata || {}
|
||||
|
||||
console.log('🎯 [START_QUIZ] Initializing player scores:', {
|
||||
activePlayers,
|
||||
playerMetadata,
|
||||
})
|
||||
|
||||
// Extract unique userIds from playerMetadata
|
||||
const uniqueUserIds = new Set<string>()
|
||||
for (const playerId of activePlayers) {
|
||||
const metadata = playerMetadata[playerId]
|
||||
console.log('🎯 [START_QUIZ] Processing player:', {
|
||||
playerId,
|
||||
metadata,
|
||||
hasUserId: !!metadata?.userId,
|
||||
})
|
||||
if (metadata?.userId) {
|
||||
uniqueUserIds.add(metadata.userId)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎯 [START_QUIZ] Unique userIds found:', Array.from(uniqueUserIds))
|
||||
|
||||
// Initialize scores for each userId
|
||||
const playerScores = Array.from(uniqueUserIds).reduce((acc: any, userId: string) => {
|
||||
acc[userId] = { correct: 0, incorrect: 0 }
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
console.log('🎯 [START_QUIZ] Initialized playerScores:', playerScores)
|
||||
|
||||
return {
|
||||
...state,
|
||||
quizCards,
|
||||
@@ -122,12 +108,6 @@ function applyMoveOptimistically(state: SorobanQuizState, move: GameMove): Sorob
|
||||
const foundNumbers = state.foundNumbers || []
|
||||
const numberFoundBy = state.numberFoundBy || {}
|
||||
|
||||
console.log('✅ [ACCEPT_NUMBER] Before update:', {
|
||||
moveUserId: move.userId,
|
||||
currentPlayerScores: playerScores,
|
||||
number: move.data.number,
|
||||
})
|
||||
|
||||
const newPlayerScores = { ...playerScores }
|
||||
const newNumberFoundBy = { ...numberFoundBy }
|
||||
|
||||
@@ -139,15 +119,6 @@ function applyMoveOptimistically(state: SorobanQuizState, move: GameMove): Sorob
|
||||
}
|
||||
// Track who found this number
|
||||
newNumberFoundBy[move.data.number] = move.userId
|
||||
|
||||
console.log('✅ [ACCEPT_NUMBER] After update:', {
|
||||
userId: move.userId,
|
||||
newScore: newPlayerScores[move.userId],
|
||||
allScores: newPlayerScores,
|
||||
numberFoundBy: move.data.number,
|
||||
})
|
||||
} else {
|
||||
console.warn('⚠️ [ACCEPT_NUMBER] No userId in move!')
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
@@ -162,11 +133,6 @@ function applyMoveOptimistically(state: SorobanQuizState, move: GameMove): Sorob
|
||||
// Defensive check: ensure state properties exist
|
||||
const playerScores = state.playerScores || {}
|
||||
|
||||
console.log('❌ [REJECT_NUMBER] Before update:', {
|
||||
moveUserId: move.userId,
|
||||
currentPlayerScores: playerScores,
|
||||
})
|
||||
|
||||
const newPlayerScores = { ...playerScores }
|
||||
if (move.userId) {
|
||||
const currentScore = newPlayerScores[move.userId] || { correct: 0, incorrect: 0 }
|
||||
@@ -174,13 +140,6 @@ function applyMoveOptimistically(state: SorobanQuizState, move: GameMove): Sorob
|
||||
...currentScore,
|
||||
incorrect: currentScore.incorrect + 1,
|
||||
}
|
||||
console.log('❌ [REJECT_NUMBER] After update:', {
|
||||
userId: move.userId,
|
||||
newScore: newPlayerScores[move.userId],
|
||||
allScores: newPlayerScores,
|
||||
})
|
||||
} else {
|
||||
console.warn('⚠️ [REJECT_NUMBER] No userId in move!')
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
@@ -238,6 +197,7 @@ export function RoomMemoryQuizProvider({ children }: { children: ReactNode }) {
|
||||
const { data: viewerId } = useViewerId()
|
||||
const { roomData } = useRoomData()
|
||||
const { activePlayers: activePlayerIds, players } = useGameMode()
|
||||
const { mutate: updateGameConfig } = useUpdateGameConfig()
|
||||
|
||||
// Get active player IDs as array
|
||||
const activePlayers = Array.from(activePlayerIds)
|
||||
@@ -246,6 +206,44 @@ export function RoomMemoryQuizProvider({ children }: { children: ReactNode }) {
|
||||
// This prevents sending a network request for every keystroke
|
||||
const [localCurrentInput, setLocalCurrentInput] = useState('')
|
||||
|
||||
// Merge saved game config from room with initialState
|
||||
// Settings are scoped by game name to preserve settings when switching games
|
||||
const mergedInitialState = useMemo(() => {
|
||||
const gameConfig = roomData?.gameConfig as Record<string, any> | null | undefined
|
||||
console.log('[RoomMemoryQuizProvider] Initializing - gameConfig:', gameConfig)
|
||||
|
||||
if (!gameConfig) {
|
||||
console.log('[RoomMemoryQuizProvider] No gameConfig, using initialState')
|
||||
return initialState
|
||||
}
|
||||
|
||||
// Get settings for this specific game (memory-quiz)
|
||||
const savedConfig = gameConfig['memory-quiz'] as Record<string, any> | null | undefined
|
||||
console.log('[RoomMemoryQuizProvider] Loading saved config for memory-quiz:', savedConfig)
|
||||
|
||||
if (!savedConfig) {
|
||||
console.log('[RoomMemoryQuizProvider] No saved config for memory-quiz, using initialState')
|
||||
return initialState
|
||||
}
|
||||
|
||||
const merged = {
|
||||
...initialState,
|
||||
// Restore settings from saved config
|
||||
selectedCount: savedConfig.selectedCount ?? initialState.selectedCount,
|
||||
displayTime: savedConfig.displayTime ?? initialState.displayTime,
|
||||
selectedDifficulty: savedConfig.selectedDifficulty ?? initialState.selectedDifficulty,
|
||||
playMode: savedConfig.playMode ?? initialState.playMode,
|
||||
}
|
||||
console.log('[RoomMemoryQuizProvider] Merged state:', {
|
||||
selectedCount: merged.selectedCount,
|
||||
displayTime: merged.displayTime,
|
||||
selectedDifficulty: merged.selectedDifficulty,
|
||||
playMode: merged.playMode,
|
||||
})
|
||||
|
||||
return merged
|
||||
}, [roomData?.gameConfig])
|
||||
|
||||
// Arcade session integration WITH room sync
|
||||
const {
|
||||
state,
|
||||
@@ -255,7 +253,7 @@ export function RoomMemoryQuizProvider({ children }: { children: ReactNode }) {
|
||||
} = useArcadeSession<SorobanQuizState>({
|
||||
userId: viewerId || '',
|
||||
roomId: roomData?.id, // CRITICAL: Pass roomId for network sync across room members
|
||||
initialState,
|
||||
initialState: mergedInitialState,
|
||||
applyMove: applyMoveOptimistically,
|
||||
})
|
||||
|
||||
@@ -287,19 +285,8 @@ export function RoomMemoryQuizProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// Build player metadata from room data and player map
|
||||
const buildPlayerMetadata = useCallback(() => {
|
||||
console.log('🔍 [buildPlayerMetadata] Starting:', {
|
||||
roomData: roomData?.id,
|
||||
activePlayers,
|
||||
viewerId,
|
||||
playersMapSize: players.size,
|
||||
})
|
||||
|
||||
const playerOwnership = buildPlayerOwnershipFromRoomData(roomData)
|
||||
console.log('🔍 [buildPlayerMetadata] Player ownership:', playerOwnership)
|
||||
|
||||
const metadata = buildPlayerMetadataUtil(activePlayers, playerOwnership, players, viewerId)
|
||||
console.log('🔍 [buildPlayerMetadata] Built metadata:', metadata)
|
||||
|
||||
return metadata
|
||||
}, [activePlayers, players, roomData, viewerId])
|
||||
|
||||
@@ -313,13 +300,6 @@ export function RoomMemoryQuizProvider({ children }: { children: ReactNode }) {
|
||||
// Build player metadata for multiplayer
|
||||
const playerMetadata = buildPlayerMetadata()
|
||||
|
||||
console.log('🚀 [startQuiz] Sending START_QUIZ move:', {
|
||||
viewerId,
|
||||
activePlayers,
|
||||
playerMetadata,
|
||||
numbers,
|
||||
})
|
||||
|
||||
sendMove({
|
||||
type: 'START_QUIZ',
|
||||
playerId: TEAM_MOVE, // Team move - all players act together
|
||||
@@ -358,11 +338,6 @@ export function RoomMemoryQuizProvider({ children }: { children: ReactNode }) {
|
||||
// Clear local input immediately
|
||||
setLocalCurrentInput('')
|
||||
|
||||
console.log('🚀 [acceptNumber] Sending ACCEPT_NUMBER move:', {
|
||||
viewerId,
|
||||
number,
|
||||
})
|
||||
|
||||
sendMove({
|
||||
type: 'ACCEPT_NUMBER',
|
||||
playerId: TEAM_MOVE, // Team move - can't identify specific player
|
||||
@@ -377,10 +352,6 @@ export function RoomMemoryQuizProvider({ children }: { children: ReactNode }) {
|
||||
// Clear local input immediately
|
||||
setLocalCurrentInput('')
|
||||
|
||||
console.log('🚀 [rejectNumber] Sending REJECT_NUMBER move:', {
|
||||
viewerId,
|
||||
})
|
||||
|
||||
sendMove({
|
||||
type: 'REJECT_NUMBER',
|
||||
playerId: TEAM_MOVE, // Team move - can't identify specific player
|
||||
@@ -415,14 +386,42 @@ export function RoomMemoryQuizProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const setConfig = useCallback(
|
||||
(field: 'selectedCount' | 'displayTime' | 'selectedDifficulty' | 'playMode', value: any) => {
|
||||
console.log(`[RoomMemoryQuizProvider] setConfig called: ${field} = ${value}`)
|
||||
|
||||
sendMove({
|
||||
type: 'SET_CONFIG',
|
||||
playerId: TEAM_MOVE,
|
||||
userId: viewerId || '',
|
||||
data: { field, value },
|
||||
})
|
||||
|
||||
// Save setting to room's gameConfig for persistence
|
||||
// Settings are scoped by game name to preserve settings when switching games
|
||||
if (roomData?.id) {
|
||||
const currentGameConfig = (roomData.gameConfig as Record<string, any>) || {}
|
||||
console.log('[RoomMemoryQuizProvider] Current gameConfig:', currentGameConfig)
|
||||
|
||||
const currentMemoryQuizConfig =
|
||||
(currentGameConfig['memory-quiz'] as Record<string, any>) || {}
|
||||
console.log('[RoomMemoryQuizProvider] Current memory-quiz config:', currentMemoryQuizConfig)
|
||||
|
||||
const updatedConfig = {
|
||||
...currentGameConfig,
|
||||
'memory-quiz': {
|
||||
...currentMemoryQuizConfig,
|
||||
[field]: value,
|
||||
},
|
||||
}
|
||||
console.log('[RoomMemoryQuizProvider] Saving updated gameConfig:', updatedConfig)
|
||||
updateGameConfig({
|
||||
roomId: roomData.id,
|
||||
gameConfig: updatedConfig,
|
||||
})
|
||||
} else {
|
||||
console.warn('[RoomMemoryQuizProvider] No roomData.id, cannot save config')
|
||||
}
|
||||
},
|
||||
[viewerId, sendMove]
|
||||
[viewerId, sendMove, roomData?.id, roomData?.gameConfig, updateGameConfig]
|
||||
)
|
||||
|
||||
// Merge network state with local input state
|
||||
|
||||
@@ -111,13 +111,13 @@ export default function RoomPage() {
|
||||
console.log('[RoomPage] Calling setRoomGame with:', {
|
||||
roomId: roomData.id,
|
||||
gameName: internalGameName,
|
||||
gameConfig: {},
|
||||
preservingGameConfig: true,
|
||||
})
|
||||
|
||||
// Don't pass gameConfig - we want to preserve existing settings for all games
|
||||
setRoomGame({
|
||||
roomId: roomData.id,
|
||||
gameName: internalGameName,
|
||||
gameConfig: {},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface RoomData {
|
||||
name: string
|
||||
code: string
|
||||
gameName: string | null // Nullable to support game selection in room
|
||||
gameConfig?: Record<string, unknown> | null // Game-specific settings
|
||||
accessMode: 'open' | 'password' | 'approval-only' | 'restricted' | 'locked' | 'retired'
|
||||
members: RoomMember[]
|
||||
memberPlayers: Record<string, RoomPlayer[]> // userId -> players
|
||||
@@ -71,6 +72,7 @@ async function fetchCurrentRoom(): Promise<RoomData | null> {
|
||||
name: data.room.name,
|
||||
code: data.room.code,
|
||||
gameName: data.room.gameName,
|
||||
gameConfig: data.room.gameConfig || null,
|
||||
accessMode: data.room.accessMode || 'open',
|
||||
members: data.members || [],
|
||||
memberPlayers: data.memberPlayers || {},
|
||||
@@ -105,6 +107,7 @@ async function createRoomApi(params: CreateRoomParams): Promise<RoomData> {
|
||||
name: data.room.name,
|
||||
code: data.room.code,
|
||||
gameName: data.room.gameName,
|
||||
gameConfig: data.room.gameConfig || null,
|
||||
accessMode: data.room.accessMode || 'open',
|
||||
members: data.members || [],
|
||||
memberPlayers: data.memberPlayers || {},
|
||||
@@ -141,6 +144,7 @@ async function joinRoomApi(params: {
|
||||
name: data.room.name,
|
||||
code: data.room.code,
|
||||
gameName: data.room.gameName,
|
||||
gameConfig: data.room.gameConfig || null,
|
||||
accessMode: data.room.accessMode || 'open',
|
||||
members: data.members || [],
|
||||
memberPlayers: data.memberPlayers || {},
|
||||
@@ -183,6 +187,7 @@ async function getRoomByCodeApi(code: string): Promise<RoomData> {
|
||||
name: data.room.name,
|
||||
code: data.room.code,
|
||||
gameName: data.room.gameName,
|
||||
gameConfig: data.room.gameConfig || null,
|
||||
accessMode: data.room.accessMode || 'open',
|
||||
members: data.members || [],
|
||||
memberPlayers: data.memberPlayers || {},
|
||||
@@ -348,7 +353,6 @@ export function useRoomData() {
|
||||
|
||||
// Moderation event handlers
|
||||
const handleKickedFromRoom = (data: { roomId: string; kickedBy: string; reason?: string }) => {
|
||||
console.log('[useRoomData] User was kicked from room:', data)
|
||||
setModerationEvent({
|
||||
type: 'kicked',
|
||||
data: {
|
||||
@@ -362,7 +366,6 @@ export function useRoomData() {
|
||||
}
|
||||
|
||||
const handleBannedFromRoom = (data: { roomId: string; bannedBy: string; reason: string }) => {
|
||||
console.log('[useRoomData] User was banned from room:', data)
|
||||
setModerationEvent({
|
||||
type: 'banned',
|
||||
data: {
|
||||
@@ -386,7 +389,6 @@ export function useRoomData() {
|
||||
createdAt: Date
|
||||
}
|
||||
}) => {
|
||||
console.log('[useRoomData] New report submitted:', data)
|
||||
setModerationEvent({
|
||||
type: 'report',
|
||||
data: {
|
||||
@@ -411,7 +413,6 @@ export function useRoomData() {
|
||||
createdAt: Date
|
||||
}
|
||||
}) => {
|
||||
console.log('[useRoomData] Room invitation received:', data)
|
||||
setModerationEvent({
|
||||
type: 'invitation',
|
||||
data: {
|
||||
@@ -434,7 +435,6 @@ export function useRoomData() {
|
||||
createdAt: Date
|
||||
}
|
||||
}) => {
|
||||
console.log('[useRoomData] New join request submitted:', data)
|
||||
setModerationEvent({
|
||||
type: 'join-request',
|
||||
data: {
|
||||
@@ -446,6 +446,25 @@ export function useRoomData() {
|
||||
})
|
||||
}
|
||||
|
||||
const handleRoomGameChanged = (data: {
|
||||
roomId: string
|
||||
gameName: string | null
|
||||
gameConfig?: Record<string, unknown>
|
||||
}) => {
|
||||
console.log('[useRoomData] Room game changed:', data)
|
||||
if (data.roomId === roomData?.id) {
|
||||
queryClient.setQueryData<RoomData | null>(roomKeys.current(), (prev) => {
|
||||
if (!prev) return null
|
||||
return {
|
||||
...prev,
|
||||
gameName: data.gameName,
|
||||
// Only update gameConfig if it was provided in the broadcast
|
||||
...(data.gameConfig !== undefined ? { gameConfig: data.gameConfig } : {}),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
socket.on('room-joined', handleRoomJoined)
|
||||
socket.on('member-joined', handleMemberJoined)
|
||||
socket.on('member-left', handleMemberLeft)
|
||||
@@ -455,6 +474,7 @@ export function useRoomData() {
|
||||
socket.on('report-submitted', handleReportSubmitted)
|
||||
socket.on('room-invitation-received', handleInvitationReceived)
|
||||
socket.on('join-request-submitted', handleJoinRequestSubmitted)
|
||||
socket.on('room-game-changed', handleRoomGameChanged)
|
||||
|
||||
return () => {
|
||||
socket.off('room-joined', handleRoomJoined)
|
||||
@@ -466,13 +486,13 @@ export function useRoomData() {
|
||||
socket.off('report-submitted', handleReportSubmitted)
|
||||
socket.off('room-invitation-received', handleInvitationReceived)
|
||||
socket.off('join-request-submitted', handleJoinRequestSubmitted)
|
||||
socket.off('room-game-changed', handleRoomGameChanged)
|
||||
}
|
||||
}, [socket, roomData?.id, queryClient])
|
||||
|
||||
// Function to notify room members of player updates
|
||||
const notifyRoomOfPlayerUpdate = useCallback(() => {
|
||||
if (socket && roomData?.id && userId) {
|
||||
console.log('[useRoomData] Notifying room of player update')
|
||||
socket.emit('players-updated', { roomId: roomData.id, userId })
|
||||
}
|
||||
}, [socket, roomData?.id, userId])
|
||||
@@ -567,13 +587,20 @@ async function setRoomGameApi(params: {
|
||||
gameName: string
|
||||
gameConfig?: Record<string, unknown>
|
||||
}): Promise<void> {
|
||||
// Only include gameConfig in the request if it was explicitly provided
|
||||
// Otherwise, we preserve the existing gameConfig in the database
|
||||
const body: { gameName: string; gameConfig?: Record<string, unknown> } = {
|
||||
gameName: params.gameName,
|
||||
}
|
||||
|
||||
if (params.gameConfig !== undefined) {
|
||||
body.gameConfig = params.gameConfig
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/arcade/rooms/${params.roomId}/settings`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
gameName: params.gameName,
|
||||
gameConfig: params.gameConfig || {},
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -607,6 +634,8 @@ export function useSetRoomGame() {
|
||||
|
||||
/**
|
||||
* Clear/reset game for a room (host only)
|
||||
* This only clears gameName (returns to game selection) but preserves gameConfig
|
||||
* so settings persist when the user selects a game again.
|
||||
*/
|
||||
async function clearRoomGameApi(roomId: string): Promise<void> {
|
||||
const response = await fetch(`/api/arcade/rooms/${roomId}/settings`, {
|
||||
@@ -614,7 +643,7 @@ async function clearRoomGameApi(roomId: string): Promise<void> {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
gameName: null,
|
||||
gameConfig: null,
|
||||
// DO NOT send gameConfig: null - we want to preserve settings!
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -646,3 +675,65 @@ export function useClearRoomGame() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update game config for current room (game-specific settings)
|
||||
*/
|
||||
async function updateGameConfigApi(params: {
|
||||
roomId: string
|
||||
gameConfig: Record<string, unknown>
|
||||
}): Promise<void> {
|
||||
console.log(
|
||||
'[updateGameConfigApi] Sending PATCH to server:',
|
||||
JSON.stringify(
|
||||
{
|
||||
url: `/api/arcade/rooms/${params.roomId}/settings`,
|
||||
gameConfig: params.gameConfig,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
const response = await fetch(`/api/arcade/rooms/${params.roomId}/settings`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
gameConfig: params.gameConfig,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
console.error('[updateGameConfigApi] Server error:', JSON.stringify(errorData, null, 2))
|
||||
throw new Error(errorData.error || 'Failed to update game config')
|
||||
}
|
||||
|
||||
console.log('[updateGameConfigApi] Server responded OK')
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook: Update game config for current room
|
||||
* This allows games to persist their settings (e.g., difficulty, card count)
|
||||
*/
|
||||
export function useUpdateGameConfig() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: updateGameConfigApi,
|
||||
onSuccess: (_, variables) => {
|
||||
// Update the cache with the new gameConfig
|
||||
queryClient.setQueryData<RoomData | null>(roomKeys.current(), (prev) => {
|
||||
if (!prev) return null
|
||||
return {
|
||||
...prev,
|
||||
gameConfig: variables.gameConfig,
|
||||
}
|
||||
})
|
||||
console.log(
|
||||
'[useUpdateGameConfig] Updated cache with new gameConfig:',
|
||||
JSON.stringify(variables.gameConfig, null, 2)
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -84,16 +84,20 @@ export function initializeSocketServer(httpServer: HTTPServer) {
|
||||
// Different games have different initial configs
|
||||
let initialState: any
|
||||
if (room.gameName === 'matching') {
|
||||
// Access nested gameConfig: { matching: { gameType, difficulty, turnTimer } }
|
||||
const matchingConfig = (room.gameConfig as any)?.matching || {}
|
||||
initialState = validator.getInitialState({
|
||||
difficulty: (room.gameConfig as any)?.difficulty || 6,
|
||||
gameType: (room.gameConfig as any)?.gameType || 'abacus-numeral',
|
||||
turnTimer: (room.gameConfig as any)?.turnTimer || 30,
|
||||
difficulty: matchingConfig.difficulty || 6,
|
||||
gameType: matchingConfig.gameType || 'abacus-numeral',
|
||||
turnTimer: matchingConfig.turnTimer || 30,
|
||||
})
|
||||
} else if (room.gameName === 'memory-quiz') {
|
||||
// Access nested gameConfig: { 'memory-quiz': { selectedCount, displayTime, selectedDifficulty } }
|
||||
const memoryQuizConfig = (room.gameConfig as any)?.['memory-quiz'] || {}
|
||||
initialState = validator.getInitialState({
|
||||
selectedCount: (room.gameConfig as any)?.selectedCount || 5,
|
||||
displayTime: (room.gameConfig as any)?.displayTime || 2.0,
|
||||
selectedDifficulty: (room.gameConfig as any)?.selectedDifficulty || 'easy',
|
||||
selectedCount: memoryQuizConfig.selectedCount || 5,
|
||||
displayTime: memoryQuizConfig.displayTime || 2.0,
|
||||
selectedDifficulty: memoryQuizConfig.selectedDifficulty || 'easy',
|
||||
})
|
||||
} else {
|
||||
// Fallback for other games
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "3.15.2",
|
||||
"version": "3.17.9",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user