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:
@@ -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! 🚀**
|
||||
|
||||
Reference in New Issue
Block a user