Compare commits

...

5 Commits

Author SHA1 Message Date
semantic-release-bot
fabb33252c chore(release): 3.17.4 [skip ci]
## [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](00dcb872b7))
2025-10-15 15:16:36 +00:00
Thomas Hallock
00dcb872b7 fix(matching): add settings persistence to matching game
The matching game was not saving settings to the database at all.
When you changed gameType, difficulty, or turnTimer, it only sent
a move to the arcade session but never saved to the database.

This adds the same persistence logic that memory-quiz uses:

**On Load:**
- Reads settings from gameConfig['matching'] in the database
- Merges with initialState
- Passes to useArcadeSession

**On Change:**
- Sends SET_CONFIG move (for real-time sync)
- Saves to gameConfig['matching'] via updateGameConfig
- Updates TanStack Query cache

Changes:
- Import useUpdateGameConfig hook
- Add mergedInitialState with settings from database
- Save settings in setGameType, setDifficulty, setTurnTimer

Now settings persist when switching between games!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 10:15:42 -05:00
semantic-release-bot
ea23651cb6 chore(release): 3.17.3 [skip ci]
## [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](2273c71a87))

### Code Refactoring

* remove verbose console logging for cleaner debugging ([9cb5fdd](9cb5fdd2fa))
2025-10-15 15:13:35 +00:00
Thomas Hallock
2273c71a87 fix(arcade): preserve gameConfig when switching games
**ROOT CAUSE:**
When switching games, setRoomGame was called with gameConfig: {},
which OVERWROTE the entire gameConfig in the database, destroying
all saved settings for ALL games.

**THE FIX:**
Remove gameConfig parameter from setRoomGame call - only change the
game name, preserve all existing settings.

**ADDED DEBUG LOGGING:**
Added detailed logging in RoomMemoryQuizProvider to help diagnose
settings persistence issues:
- Log gameConfig on component init
- Log what settings are being loaded
- Log what settings are being saved
- Log the full updated gameConfig

Changes:
- src/app/arcade/room/page.tsx: Don't pass gameConfig when switching games
- src/app/arcade/memory-quiz/context/RoomMemoryQuizProvider.tsx: Added debug logs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 10:12:33 -05:00
Thomas Hallock
9cb5fdd2fa refactor: remove verbose console logging for cleaner debugging
Removed excessive console.log statements from:
- RoomMemoryQuizProvider.tsx: Removed ~14 verbose logs related to player
  metadata, scores, and move processing
- useRoomData.ts: Removed logs for moderation events and player updates

Kept critical logs for debugging settings persistence:
- Loading saved game config
- Saving game config
- Room game changed
- Cache updates

This cleanup makes console output much more manageable when debugging
settings persistence issues.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 10:04:24 -05:00
6 changed files with 127 additions and 88 deletions

View File

@@ -1,3 +1,22 @@
## [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)

View File

@@ -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,24 @@ 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
// 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
if (!gameConfig) return initialState
// Get settings for this specific game (matching)
const savedConfig = gameConfig['matching'] as Record<string, any> | null | undefined
if (!savedConfig) return initialState
return {
...initialState,
// Restore settings from saved config
gameType: savedConfig.gameType ?? initialState.gameType,
difficulty: savedConfig.difficulty ?? initialState.difficulty,
turnTimer: savedConfig.turnTimer ?? initialState.turnTimer,
}
}, [roomData?.gameConfig])
// Arcade session integration WITH room sync
const {
@@ -259,7 +276,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,
})
@@ -506,8 +523,26 @@ 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,
},
}
updateGameConfig({
roomId: roomData.id,
gameConfig: updatedConfig,
})
}
},
[activePlayers, sendMove, viewerId]
[activePlayers, sendMove, viewerId, roomData?.id, roomData?.gameConfig, updateGameConfig]
)
const setDifficulty = useCallback(
@@ -519,8 +554,26 @@ 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,
},
}
updateGameConfig({
roomId: roomData.id,
gameConfig: updatedConfig,
})
}
},
[activePlayers, sendMove, viewerId]
[activePlayers, sendMove, viewerId, roomData?.id, roomData?.gameConfig, updateGameConfig]
)
const setTurnTimer = useCallback(
@@ -532,8 +585,26 @@ 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,
},
}
updateGameConfig({
roomId: roomData.id,
gameConfig: updatedConfig,
})
}
},
[activePlayers, sendMove, viewerId]
[activePlayers, sendMove, viewerId, roomData?.id, roomData?.gameConfig, updateGameConfig]
)
const goToSetup = useCallback(() => {

View File

@@ -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,
@@ -251,15 +210,23 @@ export function RoomMemoryQuizProvider({ children }: { children: ReactNode }) {
// 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
if (!gameConfig) return initialState
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
if (!savedConfig) return initialState
console.log('[RoomMemoryQuizProvider] Loading saved config for memory-quiz:', savedConfig)
console.log('[RoomMemoryQuizProvider] Loading saved game config for memory-quiz:', savedConfig)
if (!savedConfig) {
console.log('[RoomMemoryQuizProvider] No saved config for memory-quiz, using initialState')
return initialState
}
return {
const merged = {
...initialState,
// Restore settings from saved config
selectedCount: savedConfig.selectedCount ?? initialState.selectedCount,
@@ -267,6 +234,14 @@ export function RoomMemoryQuizProvider({ children }: { children: ReactNode }) {
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
@@ -310,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])
@@ -336,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
@@ -381,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
@@ -400,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
@@ -438,6 +386,8 @@ 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,
@@ -449,8 +399,11 @@ export function RoomMemoryQuizProvider({ children }: { children: ReactNode }) {
// 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,
@@ -459,11 +412,13 @@ export function RoomMemoryQuizProvider({ children }: { children: ReactNode }) {
[field]: value,
},
}
console.log('[RoomMemoryQuizProvider] Saving game config for memory-quiz:', updatedConfig)
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, roomData?.id, roomData?.gameConfig, updateGameConfig]

View File

@@ -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: {},
})
}

View File

@@ -353,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: {
@@ -367,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: {
@@ -391,7 +389,6 @@ export function useRoomData() {
createdAt: Date
}
}) => {
console.log('[useRoomData] New report submitted:', data)
setModerationEvent({
type: 'report',
data: {
@@ -416,7 +413,6 @@ export function useRoomData() {
createdAt: Date
}
}) => {
console.log('[useRoomData] Room invitation received:', data)
setModerationEvent({
type: 'invitation',
data: {
@@ -439,7 +435,6 @@ export function useRoomData() {
createdAt: Date
}
}) => {
console.log('[useRoomData] New join request submitted:', data)
setModerationEvent({
type: 'join-request',
data: {
@@ -496,7 +491,6 @@ export function useRoomData() {
// 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])

View File

@@ -1,6 +1,6 @@
{
"name": "soroban-monorepo",
"version": "3.17.2",
"version": "3.17.4",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [