refactor: properly separate LocalMemoryPairsProvider and RoomMemoryPairsProvider

Clean separation of concerns between arcade sessions and room-based play:

ARCHITECTURE:
- LocalMemoryPairsProvider: Arcade sessions (roomId: undefined)
  * Uses useArcadeRedirect for canModifyPlayers
  * No room sync, local-only play
  * Used by /arcade/matching

- RoomMemoryPairsProvider: Room-based multiplayer (roomId: roomData.id)
  * Hard-coded canModifyPlayers: false (always show buttons)
  * Network sync across all room members
  * Used by /arcade/room

CHANGES:
- Added canModifyPlayers to LocalMemoryPairsProvider (uses useArcadeRedirect)
- Added hoverCard action to LocalMemoryPairsProvider
- Added HOVER_CARD case to LocalMemoryPairsProvider optimistic updates
- Added playerHovers to LocalMemoryPairsProvider initial state
- Removed isInRoom conditional from RoomMemoryPairsProvider (cleaner)
- Updated /arcade/matching to use LocalMemoryPairsProvider (was incorrectly using Room)
- Added clear doc comments explaining each provider's purpose

FIXES:
- /arcade/matching no longer behaves like a room
- Each provider has single, clear responsibility
- No more cross-cutting concerns or conditional logic based on room state

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-09 15:44:41 -05:00
parent db9f9096b4
commit 98822ecda5
3 changed files with 42 additions and 14 deletions

View File

@@ -1,6 +1,7 @@
'use client'
import { type ReactNode, useCallback, useEffect, useMemo } from 'react'
import { useArcadeRedirect } from '@/hooks/useArcadeRedirect'
import { useArcadeSession } from '@/hooks/useArcadeSession'
import { useViewerId } from '@/hooks/useViewerId'
import type { GameMove } from '@/lib/arcade/validation'
@@ -38,6 +39,8 @@ const initialState: MemoryPairsState = {
originalConfig: undefined,
pausedGamePhase: undefined,
pausedGameState: undefined,
// HOVER: Initialize hover state
playerHovers: {},
}
/**
@@ -193,6 +196,17 @@ function applyMoveOptimistically(state: MemoryPairsState, move: GameMove): Memor
}
}
case 'HOVER_CARD': {
// Update player hover state for networked presence
return {
...state,
playerHovers: {
...state.playerHovers,
[move.playerId]: move.data.cardId,
},
}
}
default:
return state
}
@@ -204,6 +218,9 @@ export function LocalMemoryPairsProvider({ children }: { children: ReactNode })
// NOTE: We deliberately do NOT call useRoomData() for local play
const { activePlayerCount, activePlayers: activePlayerIds, players } = useGameMode()
// Use arcade redirect to determine button visibility for arcade sessions
const { canModifyPlayers } = useArcadeRedirect({ currentGame: 'matching' })
// Get active player IDs directly as strings (UUIDs)
const activePlayers = Array.from(activePlayerIds)
@@ -494,7 +511,7 @@ export function LocalMemoryPairsProvider({ children }: { children: ReactNode })
}, [canResumeGame, activePlayers, state.currentPlayer, sendMove])
const goToSetup = useCallback(() => {
// Send GO_TO_SETUP move - synchronized across all room members
// Send GO_TO_SETUP move
const playerId = activePlayers[0] || state.currentPlayer || ''
sendMove({
type: 'GO_TO_SETUP',
@@ -503,6 +520,22 @@ export function LocalMemoryPairsProvider({ children }: { children: ReactNode })
})
}, [activePlayers, state.currentPlayer, sendMove])
const hoverCard = useCallback(
(cardId: string | null) => {
// HOVER: Send hover state for networked presence
// Use current player as the one hovering
const playerId = state.currentPlayer || activePlayers[0] || ''
if (!playerId) return // No active player to send hover for
sendMove({
type: 'HOVER_CARD',
playerId,
data: { cardId },
})
},
[state.currentPlayer, activePlayers, sendMove]
)
// NO MORE effectiveState merging! Just use session state directly with gameMode added
const effectiveState = { ...state, gameMode } as MemoryPairsState & { gameMode: GameMode }
@@ -517,6 +550,7 @@ export function LocalMemoryPairsProvider({ children }: { children: ReactNode })
currentGameStatistics,
hasConfigChanged,
canResumeGame,
canModifyPlayers, // Arcade sessions: use arcade redirect logic
startGame,
resumeGame,
flipCard,
@@ -525,6 +559,7 @@ export function LocalMemoryPairsProvider({ children }: { children: ReactNode })
setGameType,
setDifficulty,
setTurnTimer,
hoverCard,
exitSession,
gameMode,
activePlayers,

View File

@@ -214,18 +214,13 @@ function applyMoveOptimistically(state: MemoryPairsState, move: GameMove): Memor
}
// Provider component for ROOM-BASED play (with network sync)
// NOTE: This provider should ONLY be used for room-based multiplayer games.
// For arcade sessions without rooms, use LocalMemoryPairsProvider instead.
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()
// Determine if we're in a room vs arcade session
const isInRoom = !!roomData?.id
// For arcade sessions (not in room), use arcade redirect logic
// For rooms, we ignore this and always show buttons
const arcadeRedirect = useArcadeRedirect({ currentGame: 'matching' })
// Get active player IDs directly as strings (UUIDs)
const activePlayers = Array.from(activePlayerIds)
@@ -565,9 +560,7 @@ export function RoomMemoryPairsProvider({ children }: { children: ReactNode }) {
currentGameStatistics,
hasConfigChanged,
canResumeGame,
// Room-based: always show buttons (false = show buttons)
// Arcade session: use arcade redirect logic to determine button visibility
canModifyPlayers: isInRoom ? false : arcadeRedirect.canModifyPlayers,
canModifyPlayers: false, // Room-based games: always show buttons (false = show buttons)
startGame,
resumeGame,
flipCard,

View File

@@ -1,13 +1,13 @@
import { ArcadeGuardedPage } from '@/components/ArcadeGuardedPage'
import { MemoryPairsGame } from './components/MemoryPairsGame'
import { RoomMemoryPairsProvider } from './context/RoomMemoryPairsProvider'
import { LocalMemoryPairsProvider } from './context/LocalMemoryPairsProvider'
export default function MatchingPage() {
return (
<ArcadeGuardedPage>
<RoomMemoryPairsProvider>
<LocalMemoryPairsProvider>
<MemoryPairsGame />
</RoomMemoryPairsProvider>
</LocalMemoryPairsProvider>
</ArcadeGuardedPage>
)
}