soroban-abacus-flashcards/apps/web/.claude/COMPLEMENT_RACE_STATE_ADAPT...

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 level
  • correctAnswers, streak, score - at root level
  • gamePhase: 'intro' | 'controls' | 'countdown' | 'playing' | 'results'
  • Config fields at root: mode, style, complementDisplay

New Multiplayer State:

  • currentQuestions: Record<playerId, question> - per player
  • players: Record<playerId, PlayerState> - stats nested in player objects
  • gamePhase: 'setup' | 'lobby' | 'countdown' | 'playing' | 'results'
  • Config nested: config.{mode, style, complementDisplay}

Solution: State Adapter Layer

Created a compatibility transformation layer in the Provider that:

  1. Transforms multiplayer state to look like single-player state
  2. Maintains local UI state (currentInput, isPaused, etc.) separately from server state
  3. 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 typing
  • previousQuestion - for animations
  • isPaused - local pause state
  • showScoreModal - modal visibility
  • activeSpeechBubbles - AI commentary
  • adaptiveFeedback - difficulty feedback
  • difficultyTracker - 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 layer
  • src/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

  1. Test in browser - Load the game and verify UI renders correctly
  2. Test game flow - Verify controls → countdown → playing → results
  3. Test multiplayer - Join with two players and verify synchronization
  4. Add ghost train visualization - Show opponent trains at 30-40% opacity
  5. Test passenger mechanics - Verify shared passenger board works
  6. Performance testing - Ensure smooth animations with state updates