fix: convert LocalMemoryPairsProvider to pure client-side with useReducer
Replace arcade session-based provider with pure client-side reducer for local-only games at /arcade/matching. This eliminates "No active session" errors when starting non-networked games. Changes: - Replace useArcadeSession with useReducer for local state management - Implement complete game logic client-side (match validation, turn switching) - Remove dependency on socket/server for local games - Add MATCH_FOUND, MATCH_FAILED, SWITCH_PLAYER actions to reducer - Auto-detect matches and switch players using useEffect hooks Local games now run entirely in the browser without requiring an arcade session or network connection. Fixes: Starting game at /arcade/matching no longer shows session errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,8 @@
|
||||
"Bash(git pull:*)",
|
||||
"Bash(git stash:*)",
|
||||
"Bash(npm run format:*)",
|
||||
"Bash(npm run pre-commit:*)"
|
||||
"Bash(npm run pre-commit:*)",
|
||||
"Bash(npm run type-check:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { type ReactNode, useCallback, useEffect, useMemo } from 'react'
|
||||
import { type ReactNode, useCallback, useEffect, useMemo, useReducer } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useArcadeRedirect } from '@/hooks/useArcadeRedirect'
|
||||
import { useArcadeSession } from '@/hooks/useArcadeSession'
|
||||
import { useViewerId } from '@/hooks/useViewerId'
|
||||
import type { GameMove } from '@/lib/arcade/validation'
|
||||
import { useGameMode } from '../../../../contexts/GameModeContext'
|
||||
import { generateGameCards } from '../utils/cardGeneration'
|
||||
import { validateMatch } from '../utils/matchValidation'
|
||||
import { MemoryPairsContext } from './MemoryPairsContext'
|
||||
import type { GameMode, GameStatistics, MemoryPairsContextValue, MemoryPairsState } from './types'
|
||||
|
||||
// Initial state
|
||||
// Initial state for local-only games
|
||||
const initialState: MemoryPairsState = {
|
||||
cards: [],
|
||||
gameCards: [],
|
||||
@@ -19,13 +19,13 @@ const initialState: MemoryPairsState = {
|
||||
difficulty: 6,
|
||||
turnTimer: 30,
|
||||
gamePhase: 'setup',
|
||||
currentPlayer: '', // Will be set to first player ID on START_GAME
|
||||
currentPlayer: '',
|
||||
matchedPairs: 0,
|
||||
totalPairs: 6,
|
||||
moves: 0,
|
||||
scores: {},
|
||||
activePlayers: [],
|
||||
playerMetadata: {}, // Player metadata for cross-user visibility
|
||||
playerMetadata: {},
|
||||
consecutiveMatches: {},
|
||||
gameStartTime: null,
|
||||
gameEndTime: null,
|
||||
@@ -35,38 +35,43 @@ const initialState: MemoryPairsState = {
|
||||
isProcessingMove: false,
|
||||
showMismatchFeedback: false,
|
||||
lastMatchedPair: null,
|
||||
// PAUSE/RESUME: Initialize paused game fields
|
||||
originalConfig: undefined,
|
||||
pausedGamePhase: undefined,
|
||||
pausedGameState: undefined,
|
||||
// HOVER: Initialize hover state
|
||||
playerHovers: {},
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimistic move application (client-side prediction)
|
||||
* The server will validate and send back the authoritative state
|
||||
*/
|
||||
function applyMoveOptimistically(state: MemoryPairsState, move: GameMove): MemoryPairsState {
|
||||
switch (move.type) {
|
||||
// Action types for local reducer
|
||||
type LocalAction =
|
||||
| { type: 'START_GAME'; cards: any[]; activePlayers: string[]; playerMetadata: any }
|
||||
| { type: 'FLIP_CARD'; cardId: string }
|
||||
| { type: 'MATCH_FOUND'; cardIds: [string, string]; playerId: string }
|
||||
| { type: 'MATCH_FAILED'; cardIds: [string, string] }
|
||||
| { type: 'CLEAR_MISMATCH' }
|
||||
| { type: 'SWITCH_PLAYER' }
|
||||
| { type: 'GO_TO_SETUP' }
|
||||
| { type: 'SET_CONFIG'; field: string; value: any }
|
||||
| { type: 'RESUME_GAME' }
|
||||
| { type: 'HOVER_CARD'; playerId: string; cardId: string | null }
|
||||
| { type: 'END_GAME' }
|
||||
|
||||
// Pure client-side reducer with complete game logic
|
||||
function localMemoryPairsReducer(state: MemoryPairsState, action: LocalAction): MemoryPairsState {
|
||||
switch (action.type) {
|
||||
case 'START_GAME':
|
||||
// Generate cards and initialize game
|
||||
return {
|
||||
...state,
|
||||
gamePhase: 'playing',
|
||||
gameCards: move.data.cards,
|
||||
cards: move.data.cards,
|
||||
gameCards: action.cards,
|
||||
cards: action.cards,
|
||||
flippedCards: [],
|
||||
matchedPairs: 0,
|
||||
moves: 0,
|
||||
scores: move.data.activePlayers.reduce((acc: any, p: string) => ({ ...acc, [p]: 0 }), {}),
|
||||
consecutiveMatches: move.data.activePlayers.reduce(
|
||||
(acc: any, p: string) => ({ ...acc, [p]: 0 }),
|
||||
{}
|
||||
),
|
||||
activePlayers: move.data.activePlayers,
|
||||
playerMetadata: move.data.playerMetadata || {}, // Include player metadata
|
||||
currentPlayer: move.data.activePlayers[0] || '',
|
||||
scores: action.activePlayers.reduce((acc: any, p: string) => ({ ...acc, [p]: 0 }), {}),
|
||||
consecutiveMatches: action.activePlayers.reduce((acc: any, p: string) => ({ ...acc, [p]: 0 }), {}),
|
||||
activePlayers: action.activePlayers,
|
||||
playerMetadata: action.playerMetadata,
|
||||
currentPlayer: action.activePlayers[0] || '',
|
||||
gameStartTime: Date.now(),
|
||||
gameEndTime: null,
|
||||
currentMoveStartTime: Date.now(),
|
||||
@@ -74,7 +79,6 @@ function applyMoveOptimistically(state: MemoryPairsState, move: GameMove): Memor
|
||||
isProcessingMove: false,
|
||||
showMismatchFeedback: false,
|
||||
lastMatchedPair: null,
|
||||
// PAUSE/RESUME: Save original config and clear paused state
|
||||
originalConfig: {
|
||||
gameType: state.gameType,
|
||||
difficulty: state.difficulty,
|
||||
@@ -85,8 +89,7 @@ function applyMoveOptimistically(state: MemoryPairsState, move: GameMove): Memor
|
||||
}
|
||||
|
||||
case 'FLIP_CARD': {
|
||||
// Optimistically flip the card
|
||||
const card = state.gameCards.find((c) => c.id === move.data.cardId)
|
||||
const card = state.gameCards.find((c) => c.id === action.cardId)
|
||||
if (!card) return state
|
||||
|
||||
const newFlippedCards = [...state.flippedCards, card]
|
||||
@@ -94,15 +97,69 @@ function applyMoveOptimistically(state: MemoryPairsState, move: GameMove): Memor
|
||||
return {
|
||||
...state,
|
||||
flippedCards: newFlippedCards,
|
||||
currentMoveStartTime:
|
||||
state.flippedCards.length === 0 ? Date.now() : state.currentMoveStartTime,
|
||||
isProcessingMove: newFlippedCards.length === 2, // Processing if 2 cards flipped
|
||||
currentMoveStartTime: state.flippedCards.length === 0 ? Date.now() : state.currentMoveStartTime,
|
||||
isProcessingMove: newFlippedCards.length === 2,
|
||||
showMismatchFeedback: false,
|
||||
}
|
||||
}
|
||||
|
||||
case 'MATCH_FOUND': {
|
||||
const [id1, id2] = action.cardIds
|
||||
const updatedCards = state.gameCards.map((card) =>
|
||||
card.id === id1 || card.id === id2
|
||||
? { ...card, matched: true, matchedBy: action.playerId }
|
||||
: card
|
||||
)
|
||||
|
||||
const newMatchedPairs = state.matchedPairs + 1
|
||||
const newScores = {
|
||||
...state.scores,
|
||||
[action.playerId]: (state.scores[action.playerId] || 0) + 1,
|
||||
}
|
||||
const newConsecutiveMatches = {
|
||||
...state.consecutiveMatches,
|
||||
[action.playerId]: (state.consecutiveMatches[action.playerId] || 0) + 1,
|
||||
}
|
||||
|
||||
// Check if game is complete
|
||||
const gameComplete = newMatchedPairs >= state.totalPairs
|
||||
|
||||
return {
|
||||
...state,
|
||||
gameCards: updatedCards,
|
||||
cards: updatedCards,
|
||||
flippedCards: [],
|
||||
matchedPairs: newMatchedPairs,
|
||||
moves: state.moves + 1,
|
||||
scores: newScores,
|
||||
consecutiveMatches: newConsecutiveMatches,
|
||||
lastMatchedPair: action.cardIds,
|
||||
isProcessingMove: false,
|
||||
showMismatchFeedback: false,
|
||||
gamePhase: gameComplete ? 'results' : state.gamePhase,
|
||||
gameEndTime: gameComplete ? Date.now() : null,
|
||||
// Player keeps their turn on match
|
||||
}
|
||||
}
|
||||
|
||||
case 'MATCH_FAILED': {
|
||||
// Reset consecutive matches for current player
|
||||
const newConsecutiveMatches = {
|
||||
...state.consecutiveMatches,
|
||||
[state.currentPlayer]: 0,
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
moves: state.moves + 1,
|
||||
showMismatchFeedback: true,
|
||||
isProcessingMove: true,
|
||||
consecutiveMatches: newConsecutiveMatches,
|
||||
// Don't clear flipped cards yet - CLEAR_MISMATCH will do that
|
||||
}
|
||||
}
|
||||
|
||||
case 'CLEAR_MISMATCH': {
|
||||
// Clear mismatched cards and feedback
|
||||
return {
|
||||
...state,
|
||||
flippedCards: [],
|
||||
@@ -111,14 +168,24 @@ function applyMoveOptimistically(state: MemoryPairsState, move: GameMove): Memor
|
||||
}
|
||||
}
|
||||
|
||||
case 'SWITCH_PLAYER': {
|
||||
const currentIndex = state.activePlayers.indexOf(state.currentPlayer)
|
||||
const nextIndex = (currentIndex + 1) % state.activePlayers.length
|
||||
const nextPlayer = state.activePlayers[nextIndex]
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentPlayer: nextPlayer,
|
||||
currentMoveStartTime: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
case 'GO_TO_SETUP': {
|
||||
// Return to setup phase - pause game if coming from playing/results
|
||||
const isPausingGame = state.gamePhase === 'playing' || state.gamePhase === 'results'
|
||||
|
||||
return {
|
||||
...state,
|
||||
gamePhase: 'setup',
|
||||
// PAUSE: Save game state if pausing from active game
|
||||
pausedGamePhase: isPausingGame ? state.gamePhase : undefined,
|
||||
pausedGameState: isPausingGame
|
||||
? {
|
||||
@@ -128,12 +195,11 @@ function applyMoveOptimistically(state: MemoryPairsState, move: GameMove): Memor
|
||||
moves: state.moves,
|
||||
scores: state.scores,
|
||||
activePlayers: state.activePlayers,
|
||||
playerMetadata: state.playerMetadata,
|
||||
playerMetadata: state.playerMetadata || {},
|
||||
consecutiveMatches: state.consecutiveMatches,
|
||||
gameStartTime: state.gameStartTime,
|
||||
}
|
||||
: undefined,
|
||||
// Reset visible game state
|
||||
gameCards: [],
|
||||
cards: [],
|
||||
flippedCards: [],
|
||||
@@ -155,16 +221,12 @@ function applyMoveOptimistically(state: MemoryPairsState, move: GameMove): Memor
|
||||
}
|
||||
|
||||
case 'SET_CONFIG': {
|
||||
// Update configuration field optimistically
|
||||
const { field, value } = move.data as { field: string; value: any }
|
||||
const clearPausedGame = !!state.pausedGamePhase
|
||||
|
||||
return {
|
||||
...state,
|
||||
[field]: value,
|
||||
// Update totalPairs if difficulty changes
|
||||
...(field === 'difficulty' ? { totalPairs: value } : {}),
|
||||
// Clear paused game if config changed
|
||||
[action.field]: action.value,
|
||||
...(action.field === 'difficulty' ? { totalPairs: action.value } : {}),
|
||||
...(clearPausedGame
|
||||
? { pausedGamePhase: undefined, pausedGameState: undefined, originalConfig: undefined }
|
||||
: {}),
|
||||
@@ -172,9 +234,8 @@ function applyMoveOptimistically(state: MemoryPairsState, move: GameMove): Memor
|
||||
}
|
||||
|
||||
case 'RESUME_GAME': {
|
||||
// Resume paused game
|
||||
if (!state.pausedGamePhase || !state.pausedGameState) {
|
||||
return state // No paused game, no-op
|
||||
return state
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -190,140 +251,123 @@ function applyMoveOptimistically(state: MemoryPairsState, move: GameMove): Memor
|
||||
playerMetadata: state.pausedGameState.playerMetadata,
|
||||
consecutiveMatches: state.pausedGameState.consecutiveMatches,
|
||||
gameStartTime: state.pausedGameState.gameStartTime,
|
||||
// Clear paused state
|
||||
pausedGamePhase: undefined,
|
||||
pausedGameState: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
case 'HOVER_CARD': {
|
||||
// Update player hover state for networked presence
|
||||
return {
|
||||
...state,
|
||||
playerHovers: {
|
||||
...state.playerHovers,
|
||||
[move.playerId]: move.data.cardId,
|
||||
[action.playerId]: action.cardId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
case 'END_GAME': {
|
||||
return {
|
||||
...state,
|
||||
gamePhase: 'results',
|
||||
gameEndTime: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// Provider component for LOCAL play (no network sync)
|
||||
// Provider component for LOCAL-ONLY play (no network, no arcade session)
|
||||
export function LocalMemoryPairsProvider({ children }: { children: ReactNode }) {
|
||||
const router = useRouter()
|
||||
const { data: viewerId } = useViewerId()
|
||||
// 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
|
||||
// Use arcade redirect to determine button visibility
|
||||
const { canModifyPlayers } = useArcadeRedirect({ currentGame: 'matching' })
|
||||
|
||||
// Get active player IDs directly as strings (UUIDs)
|
||||
// Get active player IDs as array
|
||||
const activePlayers = Array.from(activePlayerIds)
|
||||
|
||||
// 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 (even in local mode for consistency)
|
||||
// Pure client-side state with useReducer
|
||||
const [state, dispatch] = useReducer(localMemoryPairsReducer, initialState)
|
||||
|
||||
// Arcade session integration WITHOUT room sync
|
||||
const {
|
||||
state,
|
||||
sendMove,
|
||||
connected: _connected,
|
||||
exitSession,
|
||||
} = useArcadeSession<MemoryPairsState>({
|
||||
userId: viewerId || '',
|
||||
roomId: undefined, // CRITICAL: No roomId means no network sync
|
||||
initialState,
|
||||
applyMove: applyMoveOptimistically,
|
||||
})
|
||||
|
||||
// Handle mismatch feedback timeout
|
||||
// Handle mismatch feedback timeout and player switching
|
||||
useEffect(() => {
|
||||
if (state.showMismatchFeedback && state.flippedCards.length === 2) {
|
||||
// After 1.5 seconds, send CLEAR_MISMATCH
|
||||
// Server will validate that cards are still in mismatch state before clearing
|
||||
const timeout = setTimeout(() => {
|
||||
sendMove({
|
||||
type: 'CLEAR_MISMATCH',
|
||||
playerId: state.currentPlayer,
|
||||
data: {},
|
||||
})
|
||||
dispatch({ type: 'CLEAR_MISMATCH' })
|
||||
// Switch to next player after mismatch
|
||||
dispatch({ type: 'SWITCH_PLAYER' })
|
||||
}, 1500)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}
|
||||
}, [state.showMismatchFeedback, state.flippedCards.length, sendMove, state.currentPlayer])
|
||||
}, [state.showMismatchFeedback, state.flippedCards.length])
|
||||
|
||||
// Handle automatic match checking when 2 cards flipped
|
||||
useEffect(() => {
|
||||
if (state.flippedCards.length === 2 && !state.showMismatchFeedback) {
|
||||
const [card1, card2] = state.flippedCards
|
||||
const isMatch = validateMatch(card1, card2)
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (isMatch.isValid) {
|
||||
dispatch({
|
||||
type: 'MATCH_FOUND',
|
||||
cardIds: [card1.id, card2.id],
|
||||
playerId: state.currentPlayer,
|
||||
})
|
||||
// Player keeps turn on match - no SWITCH_PLAYER
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'MATCH_FAILED',
|
||||
cardIds: [card1.id, card2.id],
|
||||
})
|
||||
// SWITCH_PLAYER will happen after CLEAR_MISMATCH timeout
|
||||
}
|
||||
}, 600) // Small delay to show both cards
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}
|
||||
}, [state.flippedCards, state.showMismatchFeedback, state.currentPlayer])
|
||||
|
||||
// Computed values
|
||||
const isGameActive = state.gamePhase === 'playing'
|
||||
|
||||
const canFlipCard = useCallback(
|
||||
(cardId: string): boolean => {
|
||||
console.log('[LocalProvider][canFlipCard] Checking card:', {
|
||||
cardId,
|
||||
isGameActive,
|
||||
isProcessingMove: state.isProcessingMove,
|
||||
currentPlayer: state.currentPlayer,
|
||||
flippedCardsCount: state.flippedCards.length,
|
||||
})
|
||||
|
||||
if (!isGameActive || state.isProcessingMove) {
|
||||
console.log('[LocalProvider][canFlipCard] Blocked: game not active or processing')
|
||||
return false
|
||||
}
|
||||
|
||||
const card = state.gameCards.find((c) => c.id === cardId)
|
||||
if (!card || card.matched) {
|
||||
console.log('[LocalProvider][canFlipCard] Blocked: card not found or already matched')
|
||||
return false
|
||||
}
|
||||
|
||||
// Can't flip if already flipped
|
||||
if (state.flippedCards.some((c) => c.id === cardId)) {
|
||||
console.log('[LocalProvider][canFlipCard] Blocked: card already flipped')
|
||||
return false
|
||||
}
|
||||
|
||||
// Can't flip more than 2 cards
|
||||
if (state.flippedCards.length >= 2) {
|
||||
console.log('[LocalProvider][canFlipCard] Blocked: 2 cards already flipped')
|
||||
return false
|
||||
}
|
||||
|
||||
// In local play, we allow the current player to flip
|
||||
// Authorization is simpler - just check if it's this player's turn
|
||||
// In local play, all local players can flip during their turn
|
||||
const currentPlayerData = players.get(state.currentPlayer)
|
||||
console.log('[LocalProvider][canFlipCard] Authorization check:', {
|
||||
currentPlayerId: state.currentPlayer,
|
||||
currentPlayerFound: !!currentPlayerData,
|
||||
currentPlayerIsLocal: currentPlayerData?.isLocal,
|
||||
})
|
||||
|
||||
// Block if current player is explicitly marked as remote (shouldn't happen in local play)
|
||||
if (currentPlayerData && currentPlayerData.isLocal === false) {
|
||||
console.log(
|
||||
'[LocalProvider][canFlipCard] BLOCKED: Current player is remote (unexpected in local play)'
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
console.log('[LocalProvider][canFlipCard] ALLOWED: All checks passed')
|
||||
return true
|
||||
},
|
||||
[
|
||||
isGameActive,
|
||||
state.isProcessingMove,
|
||||
state.gameCards,
|
||||
state.flippedCards,
|
||||
state.currentPlayer,
|
||||
players,
|
||||
]
|
||||
[isGameActive, state.isProcessingMove, state.gameCards, state.flippedCards, state.currentPlayer, players]
|
||||
)
|
||||
|
||||
const currentGameStatistics: GameStatistics = useMemo(
|
||||
@@ -341,7 +385,6 @@ export function LocalMemoryPairsProvider({ children }: { children: ReactNode })
|
||||
[state.moves, state.matchedPairs, state.totalPairs, state.gameStartTime, state.gameEndTime]
|
||||
)
|
||||
|
||||
// PAUSE/RESUME: Computed values for pause/resume functionality
|
||||
const hasConfigChanged = useMemo(() => {
|
||||
if (!state.originalConfig) return false
|
||||
return (
|
||||
@@ -355,16 +398,13 @@ export function LocalMemoryPairsProvider({ children }: { children: ReactNode })
|
||||
return !!state.pausedGamePhase && !!state.pausedGameState && !hasConfigChanged
|
||||
}, [state.pausedGamePhase, state.pausedGameState, hasConfigChanged])
|
||||
|
||||
// Action creators - send moves to arcade session
|
||||
// Action creators
|
||||
const startGame = useCallback(() => {
|
||||
// Must have at least one active player
|
||||
if (activePlayers.length === 0) {
|
||||
console.error('[LocalMemoryPairs] Cannot start game without active players')
|
||||
return
|
||||
}
|
||||
|
||||
// Capture player metadata from local players map
|
||||
// This ensures all room members can display player info even if they don't own the players
|
||||
const playerMetadata: { [playerId: string]: any } = {}
|
||||
for (const playerId of activePlayers) {
|
||||
const playerData = players.get(playerId)
|
||||
@@ -379,56 +419,31 @@ export function LocalMemoryPairsProvider({ children }: { children: ReactNode })
|
||||
}
|
||||
}
|
||||
|
||||
// Use current session state configuration (no local state!)
|
||||
const cards = generateGameCards(state.gameType, state.difficulty)
|
||||
// Use first active player as playerId for START_GAME move
|
||||
const firstPlayer = activePlayers[0]
|
||||
sendMove({
|
||||
dispatch({
|
||||
type: 'START_GAME',
|
||||
playerId: firstPlayer,
|
||||
data: {
|
||||
cards,
|
||||
activePlayers,
|
||||
playerMetadata,
|
||||
},
|
||||
cards,
|
||||
activePlayers,
|
||||
playerMetadata,
|
||||
})
|
||||
}, [state.gameType, state.difficulty, activePlayers, players, viewerId, sendMove])
|
||||
}, [state.gameType, state.difficulty, activePlayers, players, viewerId])
|
||||
|
||||
const flipCard = useCallback(
|
||||
(cardId: string) => {
|
||||
console.log('[LocalProvider] flipCard called:', {
|
||||
cardId,
|
||||
viewerId,
|
||||
currentPlayer: state.currentPlayer,
|
||||
activePlayers: state.activePlayers,
|
||||
gamePhase: state.gamePhase,
|
||||
canFlip: canFlipCard(cardId),
|
||||
})
|
||||
|
||||
if (!canFlipCard(cardId)) {
|
||||
console.log('[LocalProvider] Cannot flip card - canFlipCard returned false')
|
||||
return
|
||||
}
|
||||
|
||||
const move = {
|
||||
type: 'FLIP_CARD' as const,
|
||||
playerId: state.currentPlayer, // Use the current player ID from game state (database player ID)
|
||||
data: { cardId },
|
||||
}
|
||||
console.log('[LocalProvider] Sending FLIP_CARD move via sendMove:', move)
|
||||
sendMove(move)
|
||||
dispatch({ type: 'FLIP_CARD', cardId })
|
||||
},
|
||||
[canFlipCard, sendMove, viewerId, state.currentPlayer, state.activePlayers, state.gamePhase]
|
||||
[canFlipCard]
|
||||
)
|
||||
|
||||
const resetGame = useCallback(() => {
|
||||
// Must have at least one active player
|
||||
if (activePlayers.length === 0) {
|
||||
console.error('[LocalMemoryPairs] Cannot reset game without active players')
|
||||
return
|
||||
}
|
||||
|
||||
// Capture player metadata from local players map
|
||||
const playerMetadata: { [playerId: string]: any } = {}
|
||||
for (const playerId of activePlayers) {
|
||||
const playerData = players.get(playerId)
|
||||
@@ -443,114 +458,71 @@ export function LocalMemoryPairsProvider({ children }: { children: ReactNode })
|
||||
}
|
||||
}
|
||||
|
||||
// Use current session state configuration (no local state!)
|
||||
const cards = generateGameCards(state.gameType, state.difficulty)
|
||||
// Use first active player as playerId for START_GAME move
|
||||
const firstPlayer = activePlayers[0]
|
||||
sendMove({
|
||||
dispatch({
|
||||
type: 'START_GAME',
|
||||
playerId: firstPlayer,
|
||||
data: {
|
||||
cards,
|
||||
activePlayers,
|
||||
playerMetadata,
|
||||
},
|
||||
cards,
|
||||
activePlayers,
|
||||
playerMetadata,
|
||||
})
|
||||
}, [state.gameType, state.difficulty, activePlayers, players, viewerId, sendMove])
|
||||
}, [state.gameType, state.difficulty, activePlayers, players, viewerId])
|
||||
|
||||
const setGameType = useCallback(
|
||||
(gameType: typeof state.gameType) => {
|
||||
// Use first active player as playerId, or empty string if none
|
||||
const playerId = activePlayers[0] || ''
|
||||
sendMove({
|
||||
type: 'SET_CONFIG',
|
||||
playerId,
|
||||
data: { field: 'gameType', value: gameType },
|
||||
})
|
||||
},
|
||||
[activePlayers, sendMove]
|
||||
)
|
||||
const setGameType = useCallback((gameType: typeof state.gameType) => {
|
||||
dispatch({ type: 'SET_CONFIG', field: 'gameType', value: gameType })
|
||||
}, [])
|
||||
|
||||
const setDifficulty = useCallback(
|
||||
(difficulty: typeof state.difficulty) => {
|
||||
const playerId = activePlayers[0] || ''
|
||||
sendMove({
|
||||
type: 'SET_CONFIG',
|
||||
playerId,
|
||||
data: { field: 'difficulty', value: difficulty },
|
||||
})
|
||||
},
|
||||
[activePlayers, sendMove]
|
||||
)
|
||||
const setDifficulty = useCallback((difficulty: typeof state.difficulty) => {
|
||||
dispatch({ type: 'SET_CONFIG', field: 'difficulty', value: difficulty })
|
||||
}, [])
|
||||
|
||||
const setTurnTimer = useCallback(
|
||||
(turnTimer: typeof state.turnTimer) => {
|
||||
const playerId = activePlayers[0] || ''
|
||||
sendMove({
|
||||
type: 'SET_CONFIG',
|
||||
playerId,
|
||||
data: { field: 'turnTimer', value: turnTimer },
|
||||
})
|
||||
},
|
||||
[activePlayers, sendMove]
|
||||
)
|
||||
const setTurnTimer = useCallback((turnTimer: typeof state.turnTimer) => {
|
||||
dispatch({ type: 'SET_CONFIG', field: 'turnTimer', value: turnTimer })
|
||||
}, [])
|
||||
|
||||
const resumeGame = useCallback(() => {
|
||||
// PAUSE/RESUME: Resume paused game if config unchanged
|
||||
if (!canResumeGame) {
|
||||
console.warn('[LocalMemoryPairs] Cannot resume - no paused game or config changed')
|
||||
return
|
||||
}
|
||||
|
||||
const playerId = activePlayers[0] || state.currentPlayer || ''
|
||||
sendMove({
|
||||
type: 'RESUME_GAME',
|
||||
playerId,
|
||||
data: {},
|
||||
})
|
||||
}, [canResumeGame, activePlayers, state.currentPlayer, sendMove])
|
||||
dispatch({ type: 'RESUME_GAME' })
|
||||
}, [canResumeGame])
|
||||
|
||||
const goToSetup = useCallback(() => {
|
||||
// Send GO_TO_SETUP move
|
||||
const playerId = activePlayers[0] || state.currentPlayer || ''
|
||||
sendMove({
|
||||
type: 'GO_TO_SETUP',
|
||||
playerId,
|
||||
data: {},
|
||||
})
|
||||
}, [activePlayers, state.currentPlayer, sendMove])
|
||||
dispatch({ type: 'GO_TO_SETUP' })
|
||||
}, [])
|
||||
|
||||
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
|
||||
if (!playerId) return
|
||||
|
||||
sendMove({
|
||||
dispatch({
|
||||
type: 'HOVER_CARD',
|
||||
playerId,
|
||||
data: { cardId },
|
||||
cardId,
|
||||
})
|
||||
},
|
||||
[state.currentPlayer, activePlayers, sendMove]
|
||||
[state.currentPlayer, activePlayers]
|
||||
)
|
||||
|
||||
// NO MORE effectiveState merging! Just use session state directly with gameMode added
|
||||
const exitSession = useCallback(() => {
|
||||
router.push('/arcade')
|
||||
}, [router])
|
||||
|
||||
const effectiveState = { ...state, gameMode } as MemoryPairsState & { gameMode: GameMode }
|
||||
|
||||
const contextValue: MemoryPairsContextValue = {
|
||||
state: effectiveState,
|
||||
dispatch: () => {
|
||||
// No-op - replaced with sendMove
|
||||
console.warn('dispatch() is deprecated in arcade mode, use action creators instead')
|
||||
// No-op - local provider uses action creators instead
|
||||
console.warn('dispatch() is not available in local mode, use action creators instead')
|
||||
},
|
||||
isGameActive,
|
||||
canFlipCard,
|
||||
currentGameStatistics,
|
||||
hasConfigChanged,
|
||||
canResumeGame,
|
||||
canModifyPlayers, // Arcade sessions: use arcade redirect logic
|
||||
canModifyPlayers,
|
||||
startGame,
|
||||
resumeGame,
|
||||
flipCard,
|
||||
|
||||
Reference in New Issue
Block a user