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

40 KiB

Speed Complement Race - Multiplayer Migration Plan

Status: Phase 1-8 Complete (70%) - Multiplayer Visuals Remaining Created: 2025-10-16 Updated: 2025-10-16 (Post-Review) Goal: Migrate Speed Complement Race from standalone single-player game to modular multiplayer arcade room game

Current State: Backend/Server Complete | ⚠️ Frontend Needs Multiplayer UI


Overview

Speed Complement Race is currently a sophisticated single-player game with three modes (Practice, Sprint, Survival). This plan outlines the migration to the modular arcade room system to support:

  • Multiplayer gameplay (up to 4 players)
  • Room-based configuration persistence
  • Socket-based real-time synchronization
  • Consistent architecture with other arcade games

Current State Analysis

What We Have

  • Complex single-player game with 3 modes
  • Advanced adaptive difficulty system
  • AI opponent system with personalities
  • Rich UI components and animations
  • Comprehensive state management (useReducer + Context)
  • 8 specialized custom hooks
  • Sound effects and visual feedback

What's Missing

  • Multiplayer support (max players: 1)
  • Socket integration
  • Validator registration in modular system
  • Persistent configuration (uses placeholder config)
  • Room-based settings
  • Real-time state synchronization
  • ⚠️ Debug logging enabled in production

Architecture Goals

Target Architecture

Arcade Room System
  ↓
Socket Server (socket-server.ts)
  ↓
Validator (ComplementRaceValidator.ts)
  ↓
Provider (RoomComplementRaceProvider.tsx) → merges saved config with defaults
  ↓
Game Components (existing UI)

Key Principles

  1. Preserve existing gameplay - Keep all three modes working
  2. Maintain UI/UX quality - All animations, sounds, visuals stay intact
  3. Support both single and multiplayer - AI opponents + human players
  4. Follow existing patterns - Use matching-game and number-guesser as reference
  5. Type safety - Comprehensive TypeScript coverage

Migration Phases

Phase 1: Configuration & Type System ✓

1.1 Define ComplementRaceGameConfig

File: src/lib/game-configs.ts

export interface ComplementRaceGameConfig {
  // Game Style (which mode)
  style: "practice" | "sprint" | "survival";

  // Question Settings
  mode: "friends5" | "friends10" | "mixed";
  complementDisplay: "number" | "abacus" | "random";

  // Difficulty
  timeoutSetting:
    | "preschool"
    | "kindergarten"
    | "relaxed"
    | "slow"
    | "normal"
    | "fast"
    | "expert";

  // AI Settings
  enableAI: boolean;
  aiOpponentCount: number; // 0-2 for multiplayer, 2 for single-player

  // Multiplayer Settings
  maxPlayers: number; // 1-4
  competitiveMode: boolean; // true = race against each other, false = collaborative

  // Sprint Mode Specific
  routeDuration: number; // seconds per route (default 60)
  enablePassengers: boolean;

  // Practice/Survival Mode Specific
  raceGoal: number; // questions to win (default 20)
}

export const DEFAULT_COMPLEMENT_RACE_CONFIG: ComplementRaceGameConfig = {
  style: "practice",
  mode: "mixed",
  complementDisplay: "random",
  timeoutSetting: "normal",
  enableAI: true,
  aiOpponentCount: 2,
  maxPlayers: 1,
  competitiveMode: true,
  routeDuration: 60,
  enablePassengers: true,
  raceGoal: 20,
};

1.2 Disable Debug Logging

File: src/app/arcade/complement-race/hooks/useSteamJourney.ts

Change:

const DEBUG_PASSENGER_BOARDING = false; // was true

Phase 2: Validator Implementation ✓

2.1 Create ComplementRaceValidator

File: src/lib/validators/ComplementRaceValidator.ts

Responsibilities:

  • Validate player answers
  • Generate questions
  • Manage game state
  • Handle scoring
  • Synchronize multiplayer state

Key Methods:

class ComplementRaceValidator {
  getInitialState(config: ComplementRaceGameConfig): GameState;
  getNewQuestion(state: GameState): ComplementQuestion;
  validateAnswer(
    state: GameState,
    playerId: string,
    answer: number,
  ): ValidationResult;
  updatePlayerProgress(
    state: GameState,
    playerId: string,
    correct: boolean,
  ): GameState;
  checkWinCondition(state: GameState): {
    winner: string | null;
    gameOver: boolean;
  };
  updateAIPositions(state: GameState, deltaTime: number): GameState;
  serializeState(state: GameState): SerializedState;
  deserializeState(serialized: SerializedState): GameState;
}

State Structure:

interface MultiplayerGameState {
  // Configuration
  config: ComplementRaceGameConfig;

  // Players
  players: Map<playerId, PlayerState>;

  // Current Question (shared in competitive, individual in practice)
  currentQuestions: Map<playerId, ComplementQuestion>;

  // AI Opponents
  aiRacers: AIRacer[];

  // Sprint Mode State
  sprint: {
    momentum: Map<playerId, number>;
    trainPosition: Map<playerId, number>;
    passengers: Passenger[];
    currentRoute: number;
    elapsedTime: number;
  } | null;

  // Race Progress
  progress: Map<playerId, number>; // 0-100% or lap count

  // Game Status
  phase: "waiting" | "countdown" | "playing" | "finished";
  winner: string | null;
  startTime: number | null;

  // Difficulty Tracking (per player)
  difficultyTrackers: Map<playerId, DifficultyTracker>;
}

interface PlayerState {
  id: string;
  name: string;
  score: number;
  streak: number;
  bestStreak: number;
  correctAnswers: number;
  totalQuestions: number;
  position: number; // track position
  isReady: boolean;
}

Phase 3: Socket Server Integration ✓

3.1 Register Game Handler

File: src/services/socket-server.ts

Add to game session management:

case 'complement-race':
  validator = new ComplementRaceValidator();
  break;

3.2 Socket Events

Client → Server:

  • game:answer - Submit answer for current question
  • game:ready - Player ready to start
  • game:settings-change - Update game config (host only)

Server → Client:

  • game:state-update - Full state sync
  • game:question-new - New question generated
  • game:answer-result - Answer validation result
  • game:player-progress - Player moved/scored
  • game:ai-update - AI positions updated
  • game:game-over - Game finished with winner

3.3 Real-time Synchronization Strategy

State Updates:

  • Full state broadcast every 200ms (AI updates)
  • Instant broadcasts on player actions (answers, ready status)
  • Delta compression for large states (sprint mode passengers)

Race Condition Handling:

  • Server is source of truth
  • Client predictions for smooth animations
  • Rollback on server correction

Phase 4: Room Provider & Configuration ✓

4.1 Create RoomComplementRaceProvider

File: src/app/arcade/complement-race/context/RoomComplementRaceProvider.tsx

Similar to existing ComplementRaceProvider but:

  • Accepts roomCode prop
  • Loads saved config from arcade room state
  • Merges saved config with defaults
  • Emits socket events on state changes
  • Listens for socket events to update state
export function RoomComplementRaceProvider({
  roomCode,
  children
}: {
  roomCode: string;
  children: React.ReactNode;
}) {
  // Load saved config
  const savedConfig = useArcadeRoom((state) =>
    state.games['complement-race']?.config as ComplementRaceGameConfig
  );

  // Merge with defaults
  const config = useMemo(() => ({
    ...DEFAULT_COMPLEMENT_RACE_CONFIG,
    ...savedConfig,
  }), [savedConfig]);

  // Initialize state with config
  const [state, dispatch] = useReducer(gameReducer, getInitialState(config));

  // Socket integration
  useSocketSync(roomCode, state, dispatch);

  return (
    <ComplementRaceContext.Provider value={{ state, dispatch }}>
      {children}
    </ComplementRaceContext.Provider>
  );
}

4.2 Update Arcade Room Store

File: src/app/arcade/stores/arcade-room-store.ts

Ensure complement-race config is saved:

updateGameConfig: (gameName: string, config: Partial<GameConfig>) => {
  set((state) => {
    const game = state.games[gameName];
    if (game) {
      game.config = { ...game.config, ...config };
    }
  });
},

Phase 5: Multiplayer Game Logic ✓

5.1 Sprint Mode: "Passenger Rush" - Shared Universe Design

Core Concept: ONE railroad with ONE set of passengers. Players compete to pick them up and deliver them first.

Shared Game Board

  • All players see the SAME track with SAME stations
  • 6-8 passengers spawn per route at various stations
  • Once a player picks up a passenger, it disappears for EVERYONE
  • Real competition for limited resources

Visual Design: Ghost Trains

Your train: 🚂🟦 Full opacity (100%), prominent
Other players: 🚂🟢🟡🟣 Low opacity (30-40%), "ghost" effect

Benefits:
- See your position clearly
- Track opponents without visual clutter
- Classic racing game pattern (ghost racers)
- No collision confusion

Gameplay Mechanics

Movement:

  • Answer complement questions to build momentum
  • Correct answer → +15 momentum → train speed increases
  • Each player has independent momentum/speed
  • Trains can pass through each other (no collision)

Pickup Rules:

When train reaches station (within 5% position):
  IF passenger waiting at station:
     First player to arrive claims passenger
    📢 Broadcast: "🟦 Player 1 picked up 👨‍💼 Bob!"
    🚫 Passenger removed from board (no longer available)
  ELSE:
    ⏭️ Train passes through empty station

Delivery Rules:

When train with passenger reaches destination station:
   Auto-deliver
  🎯 Award points: 10 (normal) or 20 (urgent)
  🚃 Free up car for next passenger
  📢 Broadcast: "🟦 Player 1 delivered 👨‍💼 Bob! +10pts"

Capacity:

  • Each train: 3 passenger cars = max 3 concurrent passengers
  • Must deliver before picking up more
  • Strategic choice: quick nearby delivery vs. valuable long-distance

Resource Competition:

  • 6-8 passengers per route
  • 4 players competing
  • Not enough for everyone to get all passengers
  • Creates natural urgency and strategy

Win Conditions (Host Configurable)

Route-based (default):

  • Play 3 routes (3 minutes)
  • Most passengers delivered wins
  • Tiebreaker: total points

Score-based:

  • First to 100 points
  • Urgent passengers (20pts) are strategic targets

Time-based:

  • 5-minute session
  • Most deliveries at time limit

5.2 Practice/Survival Mode Multiplayer

Practice Mode: Linear race track with multiple lanes

  • 2-4 horizontal lanes stacked vertically
  • Each player in their own lane
  • AI opponents fill remaining lanes (optional)
  • Same questions for all players simultaneously
  • First correct answer gets position boost + bonus points
  • First to 20 questions wins

Survival Mode: Circular track with lap counting

  • Players race on shared circular track
  • Lap counter instead of finish line
  • Infinite laps, timed rounds
  • Most laps in 5 minutes wins

5.3 Practice Mode: Simultaneous Questions

Question Flow:

1. Same question appears for all players: "7 + ? = 10"
2. Players race to answer (optional: show "🤔" indicator)
3. First CORRECT answer:
   - Advances 1 full position
   - Gets +100 base + +50 "first" bonus
4. Other correct answers:
   - Advance 0.5 positions
   - Get +100 base (no bonus)
5. Wrong answers:
   - Momentum penalty (-10)
   - No position change
6. Next question after 3 seconds OR when all answer

Strategic Tension:

  • Rush to be first (more reward) vs. take time to be accurate
  • See opponents' progress in real-time
  • Dramatic overtaking moments

5.4 AI Opponent Scaling

function getAICount(
  config: ComplementRaceGameConfig,
  humanPlayers: number,
): number {
  if (!config.enableAI) return 0;

  const totalRacers = humanPlayers + config.aiOpponentCount;
  const maxRacers = 4; // UI limitation

  return Math.min(config.aiOpponentCount, maxRacers - humanPlayers);
}

AI Behavior in Multiplayer:

  • Optional (host configurable)
  • Fill empty lanes in practice/survival modes
  • Act as ghost trains in sprint mode
  • Same speech bubble personalities
  • Adaptive difficulty (don't dominate human players)

5.5 Live Updates & Broadcasts

Event Feed (shown to all players):

• 🟦 Player 1 delivered 👨‍💼 Bob! +10 pts
• 🟢 Player 2 picked up 👩‍🎓 Alice at Hillside
• 🟡 Player 3 answered incorrectly! -10 momentum
• 🟣 Player 4 took the lead! 🏆

Tension Moments (sprint mode):

When 2+ players approach same station:
"🚨 Race for passenger at Riverside!"
"🟦 You: 3% away"
"🟢 Player 2: 5% away"

Result:
"🟦 Player 1 got there first! 👨‍💼 claimed!"

Scoreboard (always visible):

🏆 LEADERBOARD:
1. 🟣 Player 4: 4 delivered (50 pts)
2. 🟦 Player 1: 3 delivered (40 pts)
3. 🟢 Player 2: 2 delivered (30 pts)
4. 🟡 Player 3: 1 delivered (10 pts)

Phase 6: UI Updates for Multiplayer ✓

6.1 Track Visualization Updates

Practice/Survival Mode:

  • Stack up to 4 player tracks vertically
  • Show player names/avatars
  • Color-code each player's lane
  • Show AI opponents in separate lanes

Sprint Mode:

  • Show multiple trains on same track OR
  • Picture-in-picture mini views OR
  • Leaderboard overlay with positions

6.2 Settings UI

Add to GameControls.tsx:

  • Max Players selector (1-4)
  • Enable AI toggle
  • AI Opponent Count (0-2)
  • Competitive vs Collaborative toggle

6.3 Lobby/Waiting Room

Add GameLobby.tsx phase:

  • Show connected players
  • Ready check system
  • Host can change settings
  • Countdown when all ready

6.4 Results Screen Updates

Show multiplayer results:

  • Leaderboard with all player scores
  • Individual stats per player
  • Replay button (returns to lobby)
  • "Play Again" resets with same players

Phase 7: Registry & Routing ✓

7.1 Update Game Registry

File: src/lib/validators/index.ts

import { ComplementRaceValidator } from "./ComplementRaceValidator";

export const GAME_VALIDATORS = {
  matching: MatchingGameValidator,
  "number-guesser": NumberGuesserValidator,
  "complement-race": ComplementRaceValidator, // ADD THIS
} as const;

7.2 Update Game Config

File: src/lib/game-configs.ts

export type GameConfig =
  | MatchingGameConfig
  | NumberGuesserConfig
  | ComplementRaceGameConfig; // ADD THIS

7.3 Update GameSelector

File: src/components/GameSelector.tsx

GAMES_CONFIG = {
  "complement-race": {
    name: "Speed Complement Race",
    fullName: "Speed Complement Race 🏁",
    maxPlayers: 4, // CHANGE FROM 1
    url: "/arcade/complement-race",
    chips: [
      "🤖 AI Opponents",
      "🔥 Speed Challenge",
      "🏆 Three Game Modes",
      "👥 Multiplayer",
    ],
    difficulty: "Intermediate",
    available: true,
  },
};

7.4 Update Routing

File: src/app/arcade/complement-race/page.tsx

Add room-based routing:

// Support both standalone and room-based play
export default function ComplementRacePage({
  searchParams,
}: {
  searchParams: { room?: string };
}) {
  const roomCode = searchParams.room;

  return (
    <PageWithNav>
      {roomCode ? (
        <RoomComplementRaceProvider roomCode={roomCode}>
          <ComplementRaceGame />
        </RoomComplementRaceProvider>
      ) : (
        <ComplementRaceProvider>
          <ComplementRaceGame />
        </ComplementRaceProvider>
      )}
    </PageWithNav>
  );
}

Phase 8: Testing & Validation ⚠️ PENDING

8.1 Unit Tests

  • ComplementRaceValidator logic
  • Question generation
  • Answer validation
  • Win condition detection
  • AI position updates

8.2 Integration Tests

  • Socket event flow
  • State synchronization
  • Room configuration persistence
  • Multi-player race logic

8.3 E2E Tests

  • Single-player mode (backward compatibility)
  • Multiplayer with 2 players
  • Multiplayer with 4 players
  • AI opponent behavior
  • All three game modes (practice, sprint, survival)
  • Settings persistence across sessions

8.4 Manual Testing Checklist

  • Create room with complement-race
  • Join with multiple clients
  • Change settings (host only)
  • Start game with countdown
  • Answer questions simultaneously
  • Verify real-time position updates
  • Complete game and see results
  • Play again functionality
  • AI opponent behavior correct
  • Sprint mode passengers work
  • Sound effects play correctly
  • Animations smooth
  • No console errors

Phase 9: Multiplayer Visual Features ⚠️ REQUIRED FOR FULL SUPPORT

Status: Backend complete, frontend needs multiplayer visualization

Goal: Make multiplayer visible to players - currently only local player is shown on screen

9.1 Ghost Trains (Sprint Mode) 🚨 HIGH PRIORITY

File: src/app/arcade/complement-race/components/SteamTrainJourney.tsx

Current State: Only the local player's train is rendered

Required Change: Render all other players' trains with ghost effect

Implementation (Est: 2-3 hours):

// In SteamTrainJourney.tsx
import { useComplementRace } from '@/arcade-games/complement-race/Provider'

export function SteamTrainJourney() {
  const { state } = useComplementRace()
  const { localPlayerId } = useArcadeSession()

  // Existing local player train (keep as-is)
  const localPlayer = state.players[localPlayerId]

  return (
    <div className="steam-track">
      {/* Existing local player train - keep full opacity */}
      <Train
        position={localPlayer.position}
        momentum={localPlayer.momentum}
        passengers={localPlayer.claimedPassengers}
        color="blue"
        opacity={1.0}
        isLocalPlayer={true}
      />

      {/* NEW: Ghost trains for other players */}
      {Object.entries(state.players)
        .filter(([playerId]) => playerId !== localPlayerId)
        .map(([playerId, player]) => (
          <GhostTrain
            key={playerId}
            position={player.position}
            color={player.color}
            opacity={0.35}
            name={player.name}
            passengerCount={player.claimedPassengers?.length || 0}
          />
        ))}

      {/* Existing stations and passengers */}
      <Stations />
      <Passengers />
    </div>
  )
}

New Component: GhostTrain:

// src/app/arcade/complement-race/components/GhostTrain.tsx
interface GhostTrainProps {
  position: number // 0-100%
  color: string    // player color
  opacity: number  // 0.35 for ghost effect
  name: string     // player name
  passengerCount: number
}

export function GhostTrain({ position, color, opacity, name, passengerCount }: GhostTrainProps) {
  return (
    <div
      className="ghost-train"
      style={{
        position: 'absolute',
        left: `${position}%`,
        opacity,
        filter: 'blur(1px)', // subtle blur for ghost effect
        transform: 'translateX(-50%)',
      }}
    >
      <div className={css({ fontSize: '2rem' })}>🚂</div>
      <div className={css({
        fontSize: '0.7rem',
        color,
        fontWeight: 'bold'
      })}>
        {name}
        {passengerCount > 0 && ` (${passengerCount}👥)`}
      </div>
    </div>
  )
}

Visual Design:

  • Local player: Full opacity (100%), vibrant colors, clear
  • Other players: 30-40% opacity, subtle blur, labeled with name
  • Show passenger count on ghost trains
  • No collision detection needed (trains pass through each other)

Checklist:

  • Create GhostTrain component
  • Update SteamTrainJourney to render all players
  • Test with 2 players (local + 1 ghost)
  • Test with 4 players (local + 3 ghosts)
  • Verify position updates in real-time
  • Verify ghost effect (opacity, blur)

9.2 Multi-Lane Track (Practice Mode) 🚨 HIGH PRIORITY

File: src/app/arcade/complement-race/components/LinearTrack.tsx

Current State: Single horizontal lane showing only local player

Required Change: Stack 2-4 lanes vertically, one per player

Implementation (Est: 3-4 hours):

// In LinearTrack.tsx
import { useComplementRace } from '@/arcade-games/complement-race/Provider'
import { useArcadeSession } from '@/lib/arcade/game-sdk'

export function LinearTrack() {
  const { state } = useComplementRace()
  const { localPlayerId } = useArcadeSession()

  const players = Object.entries(state.players)
  const laneHeight = 120 // pixels per lane

  return (
    <div className="track-container">
      {players.map(([playerId, player], index) => {
        const isLocalPlayer = playerId === localPlayerId

        return (
          <Lane
            key={playerId}
            yOffset={index * laneHeight}
            isLocalPlayer={isLocalPlayer}
          >
            {/* Player racer */}
            <Racer
              position={player.position}
              color={player.color}
              name={player.name}
              opacity={isLocalPlayer ? 1.0 : 0.35}
              isLocalPlayer={isLocalPlayer}
            />

            {/* Track markers (start/finish) */}
            <StartLine />
            <FinishLine position={state.raceGoal} />

            {/* Progress bar */}
            <ProgressBar
              progress={player.position}
              color={player.color}
            />
          </Lane>
        )
      })}
    </div>
  )
}

New Component: Lane:

// src/app/arcade/complement-race/components/Lane.tsx
interface LaneProps {
  yOffset: number
  isLocalPlayer: boolean
  children: React.ReactNode
}

export function Lane({ yOffset, isLocalPlayer, children }: LaneProps) {
  return (
    <div
      className={css({
        position: 'relative',
        height: '120px',
        width: '100%',
        transform: `translateY(${yOffset}px)`,
        borderBottom: '2px dashed',
        borderColor: isLocalPlayer ? 'blue.500' : 'gray.300',
        backgroundColor: isLocalPlayer ? 'blue.50' : 'gray.50',
        transition: 'all 0.3s ease',
      })}
    >
      {children}
    </div>
  )
}

Layout Design:

┌──────────────────────────────────────────┐
│ 🏁 [========🏃‍♂️=========>      ] Player 1 │  ← Local player (highlighted)
├──────────────────────────────────────────┤
│ 🏁 [=======>🏃‍♀️             ] Player 2 │  ← Ghost (low opacity)
├──────────────────────────────────────────┤
│ 🏁 [===========>🤖           ] AI Bot 1 │  ← AI (low opacity)
├──────────────────────────────────────────┤
│ 🏁 [=====>🏃                 ] Player 3 │  ← Ghost (low opacity)
└──────────────────────────────────────────┘

Features:

  • Each lane is color-coded per player
  • Local player's lane has brighter background
  • Progress bars show position clearly
  • Names/avatars next to each racer
  • Smooth position interpolation for animations

Checklist:

  • Create Lane component
  • Create Racer component (or update existing)
  • Update LinearTrack to render multiple lanes
  • Test with 2 players
  • Test with 4 players (2 human + 2 AI)
  • Verify position updates synchronized
  • Verify local player lane is emphasized

9.3 Multiplayer Results Screen 🚨 HIGH PRIORITY

File: src/app/arcade/complement-race/components/GameResults.tsx

Current State: Shows only local player stats

Required Change: Show leaderboard with all players

Implementation (Est: 1-2 hours):

// In GameResults.tsx
import { useComplementRace } from '@/arcade-games/complement-race/Provider'
import { useArcadeSession } from '@/lib/arcade/game-sdk'

export function GameResults() {
  const { state, playAgain } = useComplementRace()
  const { localPlayerId, isMultiplayer } = useArcadeSession()

  // Calculate leaderboard
  const leaderboard = Object.entries(state.players)
    .map(([id, player]) => ({ id, ...player }))
    .sort((a, b) => b.score - a.score)

  const winner = leaderboard[0]
  const localPlayerRank = leaderboard.findIndex(p => p.id === localPlayerId) + 1

  return (
    <div className="results-container">
      {/* Winner Announcement */}
      <div className="winner-banner">
        <h1>🏆 {winner.name} Wins!</h1>
        <p>{winner.score} points</p>
      </div>

      {/* Full Leaderboard */}
      {isMultiplayer && (
        <div className="leaderboard">
          <h2>Final Standings</h2>
          {leaderboard.map((player, index) => (
            <LeaderboardRow
              key={player.id}
              rank={index + 1}
              player={player}
              isLocalPlayer={player.id === localPlayerId}
            />
          ))}
        </div>
      )}

      {/* Local Player Summary */}
      <div className="player-summary">
        <h3>Your Performance</h3>
        <StatCard label="Rank" value={`${localPlayerRank} / ${leaderboard.length}`} />
        <StatCard label="Score" value={state.players[localPlayerId].score} />
        <StatCard label="Accuracy" value={`${calculateAccuracy(state.players[localPlayerId])}%`} />
        <StatCard label="Best Streak" value={state.players[localPlayerId].bestStreak} />
      </div>

      {/* Actions */}
      <div className="actions">
        <Button onClick={playAgain}>Play Again</Button>
        <Button onClick={exitToLobby}>Back to Lobby</Button>
      </div>
    </div>
  )
}

New Component: LeaderboardRow:

interface LeaderboardRowProps {
  rank: number
  player: PlayerState
  isLocalPlayer: boolean
}

export function LeaderboardRow({ rank, player, isLocalPlayer }: LeaderboardRowProps) {
  const medalEmoji = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : ''

  return (
    <div
      className={css({
        display: 'flex',
        alignItems: 'center',
        padding: '1rem',
        backgroundColor: isLocalPlayer ? 'blue.100' : 'white',
        borderLeft: isLocalPlayer ? '4px solid blue.500' : 'none',
        borderRadius: '8px',
        marginBottom: '0.5rem',
      })}
    >
      <div className="rank">{medalEmoji || rank}</div>
      <div className="player-name">{player.name}</div>
      <div className="score">{player.score} pts</div>
      <div className="stats">
        {player.correctAnswers}/{player.totalQuestions} correct
      </div>
    </div>
  )
}

Checklist:

  • Update GameResults.tsx to show leaderboard
  • Create LeaderboardRow component
  • Add winner announcement
  • Highlight local player in leaderboard
  • Show individual stats per player
  • Test with 2 players
  • Test with 4 players
  • Verify "Play Again" works in multiplayer

9.4 Visual Lobby/Ready System ⚠️ MEDIUM PRIORITY

File: src/app/arcade/complement-race/components/GameLobby.tsx (NEW)

Current State: Game auto-starts, no visual ready check

Required Change: Show lobby with player list and ready indicators

Implementation (Est: 2-3 hours):

// NEW FILE: src/app/arcade/complement-race/components/GameLobby.tsx
import { useComplementRace } from '@/arcade-games/complement-race/Provider'
import { useArcadeSession } from '@/lib/arcade/game-sdk'

export function GameLobby() {
  const { state, setReady } = useComplementRace()
  const { localPlayerId, isHost } = useArcadeSession()

  const players = Object.entries(state.players)
  const allReady = players.every(([_, p]) => p.isReady)
  const canStart = players.length >= 1 && allReady

  return (
    <div className="lobby-container">
      <h1>Waiting for Players...</h1>

      {/* Player List */}
      <div className="player-list">
        {players.map(([playerId, player]) => (
          <PlayerCard
            key={playerId}
            player={player}
            isLocalPlayer={playerId === localPlayerId}
            isReady={player.isReady}
          />
        ))}

        {/* Empty slots */}
        {Array.from({ length: state.config.maxPlayers - players.length }).map((_, i) => (
          <EmptySlot key={`empty-${i}`} />
        ))}
      </div>

      {/* Ready Toggle */}
      <div className="ready-controls">
        <Button
          onClick={() => setReady(!state.players[localPlayerId].isReady)}
          variant={state.players[localPlayerId].isReady ? 'success' : 'default'}
        >
          {state.players[localPlayerId].isReady ? '✓ Ready' : 'Ready Up'}
        </Button>
      </div>

      {/* Start Game (host only) */}
      {isHost && (
        <div className="host-controls">
          <Button
            onClick={startGame}
            disabled={!canStart}
          >
            {canStart ? 'Start Game' : 'Waiting for all players...'}
          </Button>
        </div>
      )}

      {/* Countdown when starting */}
      {state.gamePhase === 'countdown' && (
        <Countdown seconds={state.countdownSeconds} />
      )}
    </div>
  )
}

Component: PlayerCard:

interface PlayerCardProps {
  player: PlayerState
  isLocalPlayer: boolean
  isReady: boolean
}

export function PlayerCard({ player, isLocalPlayer, isReady }: PlayerCardProps) {
  return (
    <div
      className={css({
        display: 'flex',
        alignItems: 'center',
        padding: '1rem',
        borderRadius: '8px',
        backgroundColor: isLocalPlayer ? 'blue.100' : 'gray.100',
        border: isReady ? '2px solid green.500' : '2px solid gray.300',
      })}
    >
      <Avatar color={player.color} />
      <div className="player-info">
        <div className="name">
          {player.name}
          {isLocalPlayer && ' (You)'}
        </div>
        <div className="status">
          {isReady ? '✓ Ready' : '⏳ Not ready'}
        </div>
      </div>
    </div>
  )
}

Integration: Update Provider to handle lobby phase

// In Provider.tsx - add to context
const setReady = useCallback((ready: boolean) => {
  emitMove({
    type: 'set-ready',
    ready,
  })
}, [emitMove])

return (
  <ComplementRaceContext.Provider value={{
    state,
    // ... other methods
    setReady,
  }}>
    {children}
  </ComplementRaceContext.Provider>
)

Checklist:

  • Create GameLobby.tsx component
  • Create PlayerCard component
  • Add setReady to Provider context
  • Update Validator to handle 'set-ready' move
  • Show lobby before game starts
  • Test ready/unready toggling
  • Test "Start Game" (host only)
  • Verify countdown before game starts

9.5 AI Opponents Display ⚠️ MEDIUM PRIORITY

Current State: AI opponents defined in types but not populated

Files to Update:

  1. src/arcade-games/complement-race/Validator.ts - AI logic
  2. Track components (LinearTrack, SteamTrainJourney) - AI rendering

Implementation (Est: 4-6 hours):

Step 1: Populate AI Opponents in Validator

// In Validator.ts - validateStartGame method
validateStartGame(config: ComplementRaceConfig) {
  const humanPlayerCount = this.activePlayers.length
  const aiCount = config.enableAI
    ? Math.min(config.aiOpponentCount, config.maxPlayers - humanPlayerCount)
    : 0

  // Create AI players
  const aiOpponents: AIOpponent[] = []
  const aiPersonalities = ['speedy', 'steady', 'chaotic']

  for (let i = 0; i < aiCount; i++) {
    const aiId = `ai-${i}`
    aiOpponents.push({
      id: aiId,
      name: `Bot ${i + 1}`,
      color: ['purple', 'orange', 'pink'][i],
      personality: aiPersonalities[i % aiPersonalities.length],
      difficulty: config.timeoutSetting,
    })

    // Add to players map
    state.players[aiId] = {
      id: aiId,
      name: `Bot ${i + 1}`,
      score: 0,
      streak: 0,
      bestStreak: 0,
      position: 0,
      isReady: true, // AI always ready
      correctAnswers: 0,
      totalQuestions: 0,
      isAI: true,
    }
  }

  state.aiOpponents = aiOpponents
  return state
}

Step 2: Update AI Positions (per frame)

// In Validator.ts - new method
updateAIPositions(state: ComplementRaceState, deltaTime: number) {
  state.aiOpponents.forEach((ai) => {
    const aiPlayer = state.players[ai.id]

    // AI answers questions at interval based on difficulty
    const answerInterval = this.getAIAnswerInterval(ai.difficulty, ai.personality)
    const timeSinceLastAnswer = Date.now() - (aiPlayer.lastAnswerTime || 0)

    if (timeSinceLastAnswer > answerInterval) {
      // AI answers question
      const correct = this.shouldAIAnswerCorrectly(ai.personality)

      if (correct) {
        aiPlayer.score += 100
        aiPlayer.streak += 1
        aiPlayer.position += 1
        aiPlayer.correctAnswers += 1
      } else {
        aiPlayer.streak = 0
      }

      aiPlayer.totalQuestions += 1
      aiPlayer.lastAnswerTime = Date.now()

      // Generate new question for AI
      state.currentQuestions[ai.id] = this.generateQuestion(state.config)
    }
  })

  return state
}

// Helper: AI answer timing based on difficulty
private getAIAnswerInterval(difficulty: string, personality: string) {
  const baseInterval = {
    'preschool': 8000,
    'kindergarten': 6000,
    'relaxed': 5000,
    'slow': 4000,
    'normal': 3000,
    'fast': 2000,
    'expert': 1500,
  }[difficulty] || 3000

  // Personality modifiers
  const modifier = {
    'speedy': 0.8,  // 20% faster
    'steady': 1.0,  // Normal
    'chaotic': 0.9 + Math.random() * 0.4, // Random 90-130%
  }[personality] || 1.0

  return baseInterval * modifier
}

// Helper: AI accuracy based on personality
private shouldAIAnswerCorrectly(personality: string): boolean {
  const accuracy = {
    'speedy': 0.85,  // Fast but less accurate
    'steady': 0.95,  // Very accurate
    'chaotic': 0.70, // Unpredictable
  }[personality] || 0.85

  return Math.random() < accuracy
}

Step 3: Render AI in UI

Already handled by 9.1 and 9.2 - Since AI opponents are in state.players, they'll render automatically as ghost trains/lanes!

Checklist:

  • Implement AI population in validateStartGame
  • Implement updateAIPositions logic
  • Add AI answer timing system
  • Add AI personality behaviors
  • Test with 1 human + 2 AI
  • Test with 2 human + 1 AI
  • Verify AI appears in results screen
  • Verify AI doesn't dominate human players

9.6 Event Feed (Optional Polish)

Priority: LOW (nice to have)

File: src/app/arcade/complement-race/components/EventFeed.tsx (NEW)

Implementation (Est: 3-4 hours):

// NEW FILE: EventFeed.tsx
interface GameEvent {
  id: string
  type: 'passenger-claimed' | 'passenger-delivered' | 'wrong-answer' | 'overtake'
  playerId: string
  playerName: string
  playerColor: string
  timestamp: number
  data?: any
}

export function EventFeed() {
  const [events, setEvents] = useState<GameEvent[]>([])

  // Listen for game events
  useEffect(() => {
    // Subscribe to validator broadcasts
    socket.on('game:event', (event: GameEvent) => {
      setEvents((prev) => [event, ...prev].slice(0, 10)) // Keep last 10
    })
  }, [])

  return (
    <div className="event-feed">
      {events.map((event) => (
        <EventItem key={event.id} event={event} />
      ))}
    </div>
  )
}

Checklist:

  • Create EventFeed component
  • Update Validator to emit events
  • Add event types (claim, deliver, overtake)
  • Position feed in UI (corner overlay)
  • Auto-dismiss old events
  • Test with multiple players

Phase 9 Summary

Total Estimated Time: 15-20 hours

Priority Breakdown:

  • 🚨 HIGH (8-9 hours): Ghost trains, multi-lane track, results screen
  • ⚠️ MEDIUM (8-12 hours): Lobby system, AI opponents
  • LOW (3-4 hours): Event feed

Completion Criteria:

  • Can see all players' trains/positions in real-time
  • Multiplayer leaderboard shows all players
  • Lobby shows player list with ready indicators
  • AI opponents appear and compete
  • All animations smooth with multiple players
  • Zero visual glitches with 4 players

Once Phase 9 is complete:

  • Multiplayer will be FULLY functional
  • Overall implementation: 100% complete
  • Ready for Phase 8 (Testing & Validation)

Implementation Order

Priority 1: Foundation (COMPLETE)

  1. ✓ Define ComplementRaceGameConfig
  2. ✓ Disable debug logging
  3. ✓ Create ComplementRaceValidator skeleton
  4. ✓ Register in modular system

Priority 2: Core Multiplayer (COMPLETE)

  1. ✓ Implement validator methods
  2. ✓ Socket server integration
  3. ✓ Create RoomComplementRaceProvider (State Adapter Pattern)
  4. ✓ Update arcade room store

Priority 3: Basic UI Integration (COMPLETE)

  1. ✓ Add navigation bar (PageWithNav)
  2. ✓ Update settings UI
  3. ✓ Config persistence
  4. ✓ Registry integration

🚨 Priority 4: Multiplayer Visuals (CRITICAL - NEXT)

  1. Ghost trains (Sprint Mode)
  2. Multi-lane track (Practice Mode)
  3. Multiplayer results screen
  4. Visual lobby with ready checks
  5. AI opponent display

Priority 5: Testing & Polish (FINAL)

  1. Write tests (unit, integration, E2E)
  2. Manual testing with 2-4 players
  3. Bug fixes
  4. Performance optimization
  5. Event feed (optional)

Risk Mitigation

Risk 1: Breaking Existing Single-Player

Mitigation: Keep existing Provider, add new RoomProvider, support both paths

Risk 2: Complex Sprint Mode State Sync

Mitigation: Start with Practice mode, add Sprint later, use delta compression

Risk 3: Performance with 4 Players

Mitigation: Optimize rendering, use React.memo, throttle updates, profile early

Risk 4: AI + Multiplayer Complexity

Mitigation: Make AI optional, test with AI disabled first, add AI last


Reference Games

Use these as architectural reference:

  • Matching Game (src/lib/validators/MatchingGameValidator.ts) - Room config, socket integration
  • Number Guesser (src/lib/validators/NumberGuesserValidator.ts) - Turn-based logic
  • Game Settings Docs (.claude/GAME_SETTINGS_PERSISTENCE.md) - Config patterns

Success Criteria

Backend & Infrastructure (COMPLETE)

  • Complement Race appears in arcade room game selector
  • Can create room with complement-race
  • Settings persist across page refreshes
  • Socket server integration working
  • Validator handles all game logic
  • Zero TypeScript errors
  • Pre-commit checks pass

⚠️ Multiplayer Visuals (IN PROGRESS - Phase 9)

  • Sprint Mode: Can see other players' trains (ghost effect)
  • Practice Mode: Multi-lane track shows all players
  • Survival Mode: Circular track with multiple players
  • Real-time position updates visible on screen
  • Multiplayer results screen shows full leaderboard
  • Visual lobby with player list and ready indicators
  • AI opponents visible in all game modes

Testing & Polish (PENDING)

  • 2-player multiplayer test (all 3 modes)
  • 4-player multiplayer test (all 3 modes)
  • AI + human players test
  • Single-player mode still works (backward compat)
  • All animations and sounds intact
  • No console errors in production
  • Smooth performance with 4 players
  • Event feed for competitive tension (optional)

Current Status: 70% Complete

What Works: Backend, state management, config persistence, navigation What's Missing: Multiplayer visualization (ghost trains, multi-lane tracks, lobby UI)


Next Steps

Immediate Priority: Phase 9 - Multiplayer Visual Features

Quick Wins (Do These First)

  1. Ghost Trains (2-3 hours) - Make Sprint mode multiplayer visible
  2. Multi-Lane Track (3-4 hours) - Make Practice mode multiplayer visible
  3. Results Screen (1-2 hours) - Show full leaderboard

After Quick Wins

  1. Visual Lobby (2-3 hours) - Add ready check system
  2. AI Opponents (4-6 hours) - Populate and display AI players

Then Testing

  1. Manual testing with 2+ players
  2. Bug fixes and polish
  3. Unit/integration tests
  4. Performance optimization

Current State: Backend is rock-solid. Now we need to make multiplayer visible to players! 🎮

Let's complete multiplayer support! 🚀