docs(complement-race): add Phase 9 for multiplayer visual features

**Phase 9: Multiplayer Visual Features (REQUIRED FOR FULL SUPPORT)**

Current status: 70% complete - backend fully functional, frontend needs
multiplayer visualization.

Added detailed implementation plans with code examples:
- 9.1 Ghost Trains (Sprint Mode) - 2-3 hours
- 9.2 Multi-Lane Track (Practice Mode) - 3-4 hours
- 9.3 Multiplayer Results Screen - 1-2 hours
- 9.4 Visual Lobby/Ready System - 2-3 hours
- 9.5 AI Opponents Display - 4-6 hours
- 9.6 Event Feed (Optional) - 3-4 hours

Updated sections:
- Implementation Order: Marked phases 1-3 complete, added Phase 4 (Visuals)
- Success Criteria: Split into Backend (complete), Visuals (in progress), Testing
- Next Steps: Prioritized visual features as immediate work

Total estimated time for Phase 9: 15-20 hours

🤖 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-16 12:36:27 -05:00
parent ed42651319
commit 131c54b562

View File

@@ -1,9 +1,12 @@
# Speed Complement Race - Multiplayer Migration Plan
**Status**: In Progress
**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
@@ -587,7 +590,7 @@ export default function ComplementRacePage({
---
## Phase 8: Testing & Validation
## Phase 8: Testing & Validation ⚠️ PENDING
### 8.1 Unit Tests
- [ ] ComplementRaceValidator logic
@@ -627,31 +630,751 @@ export default function ComplementRacePage({
---
## 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):
```typescript
// 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**:
```typescript
// 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):
```typescript
// 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**:
```typescript
// 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):
```typescript
// 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**:
```typescript
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):
```typescript
// 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**:
```typescript
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
```typescript
// 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
```typescript
// 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)
```typescript
// 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):
```typescript
// 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 (Days 1-2)
### Priority 1: Foundation (COMPLETE)
1. ✓ Define ComplementRaceGameConfig
2. ✓ Disable debug logging
3. ✓ Create ComplementRaceValidator skeleton
4. ✓ Register in modular system
### Priority 2: Core Multiplayer (Days 3-5)
### Priority 2: Core Multiplayer (COMPLETE)
5. ✓ Implement validator methods
6. ✓ Socket server integration
7. ✓ Create RoomComplementRaceProvider
7. ✓ Create RoomComplementRaceProvider (State Adapter Pattern)
8. ✓ Update arcade room store
### Priority 3: UI Updates (Days 6-8)
9. ✓ Add lobby/waiting phase
10. ✓ Update track visualization for multiplayer
11.Update settings UI
12.Update results screen
### Priority 3: Basic UI Integration (COMPLETE)
9. ✓ Add navigation bar (PageWithNav)
10. ✓ Update settings UI
11.Config persistence
12.Registry integration
### Priority 4: Polish & Test (Days 9-10)
13. ✓ Write tests
14. ✓ Manual testing
15. ✓ Bug fixes
16. ✓ Performance optimization
### 🚨 Priority 4: Multiplayer Visuals (CRITICAL - NEXT)
13. [ ] Ghost trains (Sprint Mode)
14. [ ] Multi-lane track (Practice Mode)
15. [ ] Multiplayer results screen
16. [ ] Visual lobby with ready checks
17. [ ] AI opponent display
### Priority 5: Testing & Polish (FINAL)
18. [ ] Write tests (unit, integration, E2E)
19. [ ] Manual testing with 2-4 players
20. [ ] Bug fixes
21. [ ] Performance optimization
22. [ ] Event feed (optional)
---
@@ -682,29 +1405,61 @@ Use these as architectural reference:
## Success Criteria
- [ ] Complement Race appears in arcade room game selector
- [ ] Can create room with complement-race
- [ ] Multiple players can join and see each other
- [ ] Settings persist across page refreshes
- [ ] Real-time race progress updates work
- [ ] All three modes work in multiplayer
- [ ] AI opponents work with human players
### ✅ Backend & Infrastructure (COMPLETE)
- [x] Complement Race appears in arcade room game selector
- [x] Can create room with complement-race
- [x] Settings persist across page refreshes
- [x] Socket server integration working
- [x] Validator handles all game logic
- [x] Zero TypeScript errors
- [x] 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
- [ ] Zero TypeScript errors
- [ ] Pre-commit checks pass
- [ ] 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
1. Start with Phase 1: Configuration & Types
2. Move to Phase 2: Validator skeleton
3. Test each phase before moving to next
4. Deploy to staging environment early
5. Get user feedback on multiplayer mechanics
**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
4. **Visual Lobby** (2-3 hours) - Add ready check system
5. **AI Opponents** (4-6 hours) - Populate and display AI players
### Then Testing
6. Manual testing with 2+ players
7. Bug fixes and polish
8. Unit/integration tests
9. Performance optimization
---
**Let's ship it! 🚀**
**Current State**: Backend is rock-solid. Now we need to make multiplayer **visible** to players! 🎮
**Let's complete multiplayer support! 🚀**