5.0 KiB
Complement Race State Adapter Solution
Problem
The existing single-player UI components were deeply coupled to a specific state shape that differed from the new multiplayer state structure:
Old Single-Player State:
currentQuestion- single question object at root levelcorrectAnswers,streak,score- at root levelgamePhase: 'intro' | 'controls' | 'countdown' | 'playing' | 'results'- Config fields at root:
mode,style,complementDisplay
New Multiplayer State:
currentQuestions: Record<playerId, question>- per playerplayers: Record<playerId, PlayerState>- stats nested in player objectsgamePhase: 'setup' | 'lobby' | 'countdown' | 'playing' | 'results'- Config nested:
config.{mode, style, complementDisplay}
Solution: State Adapter Layer
Created a compatibility transformation layer in the Provider that:
- Transforms multiplayer state to look like single-player state
- Maintains local UI state (currentInput, isPaused, etc.) separately from server state
- Provides compatibility dispatch that maps old reducer actions to new action creators
Key Implementation Details
1. Compatible State Interface (CompatibleGameState)
Defined an interface that matches the old single-player GameState shape, allowing existing UI components to work without modification.
2. Local UI State
Uses useState to track local UI state that doesn't need server synchronization:
currentInput- what user is typingpreviousQuestion- for animationsisPaused- local pause stateshowScoreModal- modal visibilityactiveSpeechBubbles- AI commentaryadaptiveFeedback- difficulty feedbackdifficultyTracker- adaptive difficulty data
3. State Transformation (compatibleState useMemo hook)
Transforms multiplayer state into compatible single-player shape:
const compatibleState = useMemo((): CompatibleGameState => {
const localPlayer = localPlayerId ? multiplayerState.players[localPlayerId] : null
// Map gamePhase: setup/lobby -> controls
let gamePhase = multiplayerState.gamePhase
if (gamePhase === 'setup' || gamePhase === 'lobby') {
gamePhase = 'controls'
}
return {
// Extract config fields to root level
mode: multiplayerState.config.mode,
style: multiplayerState.config.style,
// Extract local player's question
currentQuestion: localPlayerId
? multiplayerState.currentQuestions[localPlayerId] || null
: null,
// Extract local player's stats
score: localPlayer?.score || 0,
streak: localPlayer?.streak || 0,
// Map AI opponents to old aiRacers format
aiRacers: multiplayerState.aiOpponents.map(ai => ({
id: ai.id,
name: ai.name,
position: ai.position,
// ... etc
})),
// Include local UI state
currentInput: localUIState.currentInput,
adaptiveFeedback: localUIState.adaptiveFeedback,
// ... etc
}
}, [multiplayerState, localPlayerId, localUIState])
4. Compatibility Dispatch
Maps old reducer action types to new action creators:
const dispatch = useCallback((action: { type: string; [key: string]: any }) => {
switch (action.type) {
case 'START_COUNTDOWN':
case 'BEGIN_GAME':
startGame()
break
case 'SUBMIT_ANSWER':
const responseTime = Date.now() - multiplayerState.questionStartTime
submitAnswer(action.answer, responseTime)
break
// Local UI state actions
case 'UPDATE_INPUT':
setLocalUIState(prev => ({ ...prev, currentInput: action.input }))
break
// ... etc
}
}, [startGame, submitAnswer, multiplayerState.questionStartTime])
Benefits
✅ Preserves all existing UI components - No need to rebuild the beautiful train animations, railroad tracks, passenger mechanics, etc.
✅ Enables multiplayer - Uses the standard useArcadeSession pattern for real-time synchronization
✅ Maintains compatibility - Existing components work without any changes
✅ Clean separation - Local UI state (currentInput, etc.) is separate from server-synchronized state
✅ Type-safe - Full TypeScript support with proper interfaces
Files Modified
src/arcade-games/complement-race/Provider.tsx- Added state adapter layersrc/app/arcade/complement-race/components/*.tsx- Updated imports to use new Provider
Testing
Type Checking
- ✅ No TypeScript errors in new code
- ✅ All component files compile successfully
- ✅ Only pre-existing errors remain (known @soroban/abacus-react issue)
Format & Lint
- ✅ Code formatted with Biome
- ✅ No new lint warnings
- ✅ All style guidelines followed
Next Steps
- Test in browser - Load the game and verify UI renders correctly
- Test game flow - Verify controls → countdown → playing → results
- Test multiplayer - Join with two players and verify synchronization
- Add ghost train visualization - Show opponent trains at 30-40% opacity
- Test passenger mechanics - Verify shared passenger board works
- Performance testing - Ensure smooth animations with state updates