# Speed Complement Race - Port to Next.js Technical Plan

**Date Created**: 2025-09-30
**Source**: `packages/core/src/web_generator.py` (lines 10956-15113)
**Target**: `apps/web/src/app/games/complement-race/`
**Status**: Planning Complete, Ready to Implement

---

## 📋 **Project Overview**

**Goal**: Port the Speed Complement Race game from `packages/core/src/web_generator.py` (standalone HTML) to `apps/web/src/app/games/complement-race` (Next.js + React + TypeScript)

**Critical Requirement**: Preserve ALL gameplay mechanics, AI personalities, adaptive systems, and visual polish from the original

**Original Game Features**:

- 3 game modes: Endurance Race (20 answers), Lightning Sprint (60 seconds), Survival Mode (infinite)
- 2 AI opponents with distinct personalities: Swift AI (competitive), Math Bot (analytical)
- Adaptive difficulty system that tracks per-pair performance
- 3 distinct visualizations: Linear track, Circular track, Steam train journey
- Speech bubble commentary system (266 lines per AI personality)
- Momentum-based steam train system with pressure gauge
- Dynamic day/night cycle visualization
- Lap tracking and celebration system
- Rubber-banding AI catchup mechanics
- Complex scoring with medals and speed ratings

---

## **Phase 1: Architecture & Setup** ⚙️

### **1.1 Directory Structure**

```
apps/web/src/app/games/complement-race/
├── page.tsx                           # Main page wrapper
├── context/
│   └── ComplementRaceContext.tsx      # Game state management
├── components/
│   ├── ComplementRaceGame.tsx         # Main game orchestrator
│   ├── GameIntro.tsx                  # Welcome screen with instructions
│   ├── GameControls.tsx               # Mode/timeout/style selection
│   ├── GameCountdown.tsx              # 3-2-1-GO countdown
│   ├── GameDisplay.tsx                # Question display area
│   ├── GameTimer.tsx                  # Timer bar component
│   ├── GameHeader.tsx                 # Sticky header with stats
│   ├── RaceTrack/
│   │   ├── LinearTrack.tsx            # Endurance mode visualization
│   │   ├── CircularTrack.tsx          # Survival mode visualization
│   │   └── SteamTrainJourney.tsx      # Sprint mode visualization
│   ├── AISystem/
│   │   ├── AIRacer.tsx                # Individual AI racer component
│   │   ├── SpeechBubble.tsx           # AI commentary display
│   │   └── aiCommentary.ts            # Commentary logic & messages
│   ├── ScoreModal.tsx                 # End game results
│   └── VisualFeedback.tsx             # Correct/incorrect animations
├── hooks/
│   ├── useGameLoop.ts                 # Core game loop logic
│   ├── useAIRacers.ts                 # AI movement & adaptation
│   ├── useAdaptiveDifficulty.ts       # Per-pair performance tracking
│   ├── useSteamJourney.ts             # Steam train momentum system
│   └── useKeyboardInput.ts            # Keyboard capture
├── lib/
│   ├── gameTypes.ts                   # TypeScript interfaces
│   ├── aiPersonalities.ts             # AI personality definitions
│   ├── scoringSystem.ts               # Scoring & medal calculations
│   └── circularMath.ts                # Trigonometry for circular track
└── sounds/
    └── (audio files for sound effects)
```

### **1.2 Core Types** (gameTypes.ts)

```typescript
export type GameMode = "friends5" | "friends10" | "mixed";
export type GameStyle = "practice" | "sprint" | "survival";
export type TimeoutSetting =
  | "preschool"
  | "kindergarten"
  | "relaxed"
  | "slow"
  | "normal"
  | "fast"
  | "expert";

export interface ComplementQuestion {
  number: number;
  targetSum: number;
  correctAnswer: number;
}

export interface AIRacer {
  id: string;
  position: number;
  speed: number;
  name: string;
  personality: "competitive" | "analytical";
  icon: string;
  lastComment: number;
  commentCooldown: number;
  previousPosition: number;
}

export interface DifficultyTracker {
  pairPerformance: Map<string, PairPerformance>;
  baseTimeLimit: number;
  currentTimeLimit: number;
  difficultyLevel: number;
  consecutiveCorrect: number;
  consecutiveIncorrect: number;
  learningMode: boolean;
  adaptationRate: number;
}

export interface PairPerformance {
  attempts: number;
  correct: number;
  avgTime: number;
  difficulty: number;
}

export interface GameState {
  // Game configuration
  mode: GameMode;
  style: GameStyle;
  timeoutSetting: TimeoutSetting;

  // Current question
  currentQuestion: ComplementQuestion | null;
  previousQuestion: ComplementQuestion | null;

  // Game progress
  score: number;
  streak: number;
  bestStreak: number;
  totalQuestions: number;
  correctAnswers: number;

  // Game status
  isGameActive: boolean;
  isPaused: boolean;
  gamePhase: "intro" | "controls" | "countdown" | "playing" | "results";

  // Timing
  gameStartTime: number | null;
  questionStartTime: number;

  // Race mechanics
  raceGoal: number;
  timeLimit: number | null;
  speedMultiplier: number;
  aiRacers: AIRacer[];

  // Adaptive difficulty
  difficultyTracker: DifficultyTracker;

  // Survival mode specific
  playerLap: number;
  aiLaps: Map<string, number>;
  survivalMultiplier: number;

  // Sprint mode specific
  momentum: number;
  trainPosition: number;
  lastCorrectAnswerTime: number;

  // Input
  currentInput: string;

  // UI state
  showScoreModal: boolean;
}
```

---

## **Phase 2: State Management** 🔄

### **2.1 Context Pattern**

Follow existing pattern from memory-quiz:

- Use `useReducer` for complex game state
- Create ComplementRaceContext with provider
- Export custom hooks for game actions

### **2.2 Game Actions**

```typescript
type GameAction =
  | { type: "SET_MODE"; mode: GameMode }
  | { type: "SET_STYLE"; style: GameStyle }
  | { type: "SET_TIMEOUT"; timeout: TimeoutSetting }
  | { type: "START_RACE" }
  | { type: "START_COUNTDOWN" }
  | { type: "BEGIN_GAME" }
  | { type: "NEXT_QUESTION" }
  | { type: "SUBMIT_ANSWER"; answer: number }
  | { type: "UPDATE_INPUT"; input: string }
  | {
      type: "UPDATE_AI_POSITIONS";
      positions: Array<{ id: string; position: number }>;
    }
  | {
      type: "TRIGGER_AI_COMMENTARY";
      racerId: string;
      message: string;
      context: string;
    }
  | { type: "UPDATE_MOMENTUM"; momentum: number }
  | { type: "UPDATE_TRAIN_POSITION"; position: number }
  | { type: "COMPLETE_LAP"; racerId: string }
  | { type: "PAUSE_RACE" }
  | { type: "RESUME_RACE" }
  | { type: "END_RACE" }
  | { type: "SHOW_RESULTS" }
  | { type: "RESET_GAME" };
```

---

## **Phase 3: Core Game Logic** 🎯

### **3.1 Game Loop Hook** (useGameLoop.ts)

```typescript
export function useGameLoop() {
  // Manages:
  // - Question generation (with repeat avoidance)
  // - Timer management
  // - Answer validation
  // - Score calculation
  // - Streak tracking
  // - Race completion detection
  // Returns:
  // - nextQuestion()
  // - submitAnswer()
  // - pauseGame()
  // - resumeGame()
  // - endGame()
}
```

**Critical Details to Preserve**:

- Avoid repeating same question consecutively
- Safety limit of 10 attempts when generating questions
- Exact timer calculations based on timeout setting
- Streak bonus system
- Score formula: `correctAnswers * 100 + streak * 50 + speedBonus`
- Speed bonus: `max(0, 300 - (avgTime * 10))`

### **3.2 AI Racers Hook** (useAIRacers.ts)

```typescript
export function useAIRacers() {
  // Manages:
  // - AI position updates (200ms intervals)
  // - Rubber-banding catchup system
  // - Passing event detection
  // - Commentary trigger logic
  // - Speed adaptation
  // Returns:
  // - aiRacers state
  // - updateAIPositions()
  // - triggerCommentary()
  // - checkForPassingEvents()
}
```

**Critical Details to Preserve**:

- Swift AI: speed = 0.25 \* multiplier
- Math Bot: speed = 0.15 \* multiplier
- AI updates every 200ms
- Random variance in AI progress (0.6-1.4 range via `Math.random() * 0.8 + 0.6`)
- Rubber-banding: AI speeds up 2x when >10 units behind
- Passing detection with tolerance = 0.1 for floating point
- Commentary cooldown (2-6 seconds random via `Math.random() * 4000 + 2000`)

### **3.3 Adaptive Difficulty Hook** (useAdaptiveDifficulty.ts)

```typescript
export function useAdaptiveDifficulty() {
  // Manages:
  // - Per-pair performance tracking
  // - Time limit adaptation
  // - Difficulty level calculation
  // - Learning mode detection
  // - Adaptive feedback messages
  // Returns:
  // - currentTimeLimit
  // - updatePairPerformance()
  // - getAdaptiveFeedback()
  // - calculateDifficulty()
}
```

**Critical Details to Preserve**:

- Pair key format: `"${number}_${complement}_${targetSum}"`
- Base time limit: 3000ms
- Difficulty scale: 1-5
- Learning mode: first 10-15 questions
- Adaptation rate: 0.1 (gradual changes)
- Success rate thresholds:
  - > 85% → adaptiveMultiplier = 1.6x
  - > 75% → 1.3x
  - > 60% → 1.0x
  - > 45% → 0.75x
  - <45% → 0.5x
- Response time factors:
  - <1500ms → 1.2x
  - <2500ms → 1.1x
  - > 4000ms → 0.9x
- Streak bonuses:
  - 8+ streak → 1.3x
  - 5+ streak → 1.15x
- Bounds: min 0.3x, max 2.0x

---

## **Phase 4: Visualization Components** 🎨

### **4.1 Linear Track** (LinearTrack.tsx)

```typescript
export function LinearTrack({
  playerProgress,
  aiRacers,
  raceGoal,
  showFinishLine,
}) {
  // Renders:
  // - Horizontal track with background
  // - Player racer at left% position
  // - AI racers at left% positions
  // - Finish line (conditional)
  // - Speech bubbles attached to racers
}
```

**Position Calculation**:

```typescript
const leftPercent = Math.min(98, (progress / raceGoal) * 96 + 2);
// 2% minimum (start), 98% maximum (near finish), 96% range for race
```

### **4.2 Circular Track** (CircularTrack.tsx)

```typescript
export function CircularTrack({ playerProgress, playerLap, aiRacers, aiLaps }) {
  // Renders:
  // - Circular SVG track
  // - Racers positioned using trigonometry
  // - Lap counter display
  // - Celebration effects on lap completion
  // Math:
  // progressPerLap = 50
  // currentLap = Math.floor(progress / 50)
  // angle = (progress / 50) * 360
  // angleRad = (angle * Math.PI) / 180
  // x = radius * cos(angleRad - π/2)
  // y = radius * sin(angleRad - π/2)
  // rotation = angle degrees
}
```

**Critical Details**:

- Track radius: (trackWidth / 2) - 20
- Start at top of circle (offset by -π/2)
- Counter-rotate speech bubbles: `--counter-rotation: ${-angle}deg`
- Lap detection: `Math.floor(progress / 50)`
- Celebration on lap completion with cooldown to prevent duplicates
- Track lap counts per racer in Map

### **4.3 Steam Train Journey** (SteamTrainJourney.tsx)

```typescript
export function SteamTrainJourney({
  momentum,
  trainPosition,
  timeElapsed,
  correctAnswers,
}) {
  // Renders:
  // - Dynamic sky gradient (6 time periods)
  // - SVG curved railroad track
  // - Animated steam locomotive
  // - Steam puff effects
  // - Coal shoveler animation
  // - Station markers
  // - Pressure gauge with PSI
  // - Momentum bar
  // Systems:
  // - Momentum decay (1% per second base)
  // - Accelerated decay if no answers (>5s)
  // - Train movement (momentum * 0.4 per 200ms)
  // - Time of day progression (60s = full cycle)
}
```

**Time of Day Gradients** (from web_generator.py lines 4344-4351):

```typescript
const timeOfDayGradients = {
  dawn: "linear-gradient(135deg, #ffb347 0%, #ffcc5c 30%, #87ceeb 70%, #98d8e8 100%)",
  morning:
    "linear-gradient(135deg, #87ceeb 0%, #98d8e8 30%, #b6e2ff 70%, #cce7ff 100%)",
  midday:
    "linear-gradient(135deg, #87ceeb 0%, #a8d8ea 30%, #c7e2f7 70%, #e3f2fd 100%)",
  afternoon:
    "linear-gradient(135deg, #ffecd2 0%, #fcb69f 30%, #ff8a65 70%, #ff7043 100%)",
  dusk: "linear-gradient(135deg, #ff8a65 0%, #ff7043 30%, #8e44ad 70%, #5b2c87 100%)",
  night:
    "linear-gradient(135deg, #2c3e50 0%, #34495e 30%, #1a252f 70%, #0f1419 100%)",
};

// Time progression (from line 13064-13088):
if (gameProgress < 0.17) return "dawn";
else if (gameProgress < 0.33) return "morning";
else if (gameProgress < 0.67) return "midday";
else if (gameProgress < 0.83) return "afternoon";
else if (gameProgress < 0.92) return "dusk";
else return "night";
```

**Momentum Decay Config by Skill Level** (calibrated for different ages):

```typescript
const momentumConfigs = {
  preschool: {
    baseDecay: 0.3, // Very gentle decay
    highSpeedDecay: 0.5, // >75% momentum
    mediumSpeedDecay: 0.4, // >50% momentum
    starvationThreshold: 10, // Seconds before extra decay
    starvationRate: 2, // Divisor for extra decay calculation
    maxExtraDecay: 2.0, // Maximum extra decay per second
    warningThreshold: 8, // When to log warnings
  },
  kindergarten: {
    baseDecay: 0.4,
    highSpeedDecay: 0.6,
    mediumSpeedDecay: 0.5,
    starvationThreshold: 8,
    starvationRate: 2,
    maxExtraDecay: 2.5,
    warningThreshold: 6,
  },
  relaxed: {
    baseDecay: 0.6,
    highSpeedDecay: 0.9,
    mediumSpeedDecay: 0.7,
    starvationThreshold: 6,
    starvationRate: 1.8,
    maxExtraDecay: 3.0,
    warningThreshold: 5,
  },
  slow: {
    baseDecay: 0.8,
    highSpeedDecay: 1.2,
    mediumSpeedDecay: 1.0,
    starvationThreshold: 5,
    starvationRate: 1.5,
    maxExtraDecay: 3.5,
    warningThreshold: 4,
  },
  normal: {
    baseDecay: 1.0,
    highSpeedDecay: 1.5,
    mediumSpeedDecay: 1.2,
    starvationThreshold: 5,
    starvationRate: 1.5,
    maxExtraDecay: 4.0,
    warningThreshold: 4,
  },
  fast: {
    baseDecay: 1.2,
    highSpeedDecay: 1.8,
    mediumSpeedDecay: 1.5,
    starvationThreshold: 4,
    starvationRate: 1.2,
    maxExtraDecay: 5.0,
    warningThreshold: 3,
  },
  expert: {
    baseDecay: 1.5,
    highSpeedDecay: 2.5,
    mediumSpeedDecay: 2.0,
    starvationThreshold: 3,
    starvationRate: 1.0,
    maxExtraDecay: 6.0,
    warningThreshold: 2,
  },
};

// Decay calculation (from line 13036-13046):
let decayRate =
  momentum > 75
    ? config.highSpeedDecay
    : momentum > 50
      ? config.mediumSpeedDecay
      : config.baseDecay;

if (timeSinceLastCoal > config.starvationThreshold) {
  const extraDecay = Math.min(
    config.maxExtraDecay,
    timeSinceLastCoal / config.starvationRate,
  );
  decayRate += extraDecay;
}

momentum = Math.max(0, momentum - decayRate);
```

**Pressure Gauge Calculation** (lines 13118-13146):

```typescript
const pressure = Math.round(momentum); // 0-100
const psi = Math.round(pressure * 1.5); // Scale to 0-150 PSI

// Arc progress (circumference = 251.2 pixels)
const circumference = 251.2;
const offset = circumference - (pressure / 100) * circumference;

// Needle rotation (-90 to +90 degrees for half-circle)
const rotation = -90 + (pressure / 100) * 180;

// Gauge color
let gaugeColor = "#ff6b6b"; // Coral red for low pressure
if (pressure > 70)
  gaugeColor = "#4ecdc4"; // Turquoise for high pressure
else if (pressure > 40) gaugeColor = "#feca57"; // Sunny yellow for medium pressure
```

**Train Movement** (line 13057-13058):

```typescript
// Updates 5x per second (200ms intervals)
trainPosition += momentum * 0.4; // Continuous movement rate
```

---

## **Phase 5: AI Personality System** 🤖

### **5.1 Commentary System** (aiCommentary.ts)

**IMPORTANT**: Lines 11768-11909 contain ALL commentary messages. Must port exactly.

```typescript
export const swiftAICommentary = {
  ahead: [
    "💨 Eat my dust!",
    "🔥 Too slow for me!",
    "⚡ You can't catch me!",
    "🚀 I'm built for speed!",
    "🏃‍♂️ This is way too easy!",
  ],
  behind: [
    "😤 Not over yet!",
    "💪 I'm just getting started!",
    "🔥 Watch me catch up to you!",
    "⚡ I'm coming for you!",
    "🏃‍♂️ This is my comeback!",
  ],
  adaptive_struggle: [
    "😏 You struggling much?",
    "🤖 Math is easy for me!",
    "⚡ You need to think faster!",
    "🔥 Need me to slow down?",
  ],
  adaptive_mastery: [
    "😮 You're actually impressive!",
    "🤔 You're getting faster...",
    "😤 Time for me to step it up!",
    "⚡ Not bad for a human!",
  ],
  player_passed: [
    "😠 No way you just passed me!",
    "🔥 This isn't over!",
    "💨 I'm just getting warmed up!",
    "😤 Your lucky streak won't last!",
    "⚡ I'll be back in front of you soon!",
  ],
  ai_passed: [
    "💨 See ya later, slowpoke!",
    "😎 Thanks for the warm-up!",
    "🔥 This is how it's done!",
    "⚡ I'll see you at the finish line!",
    "💪 Try to keep up with me!",
  ],
  lapped: [
    "😡 You just lapped me?! No way!",
    "🤬 This is embarrassing for me!",
    "😤 I'm not going down without a fight!",
    "💢 How did you get so far ahead?!",
    "🔥 Time to show you my real speed!",
    "😠 You won't stay ahead for long!",
  ],
  desperate_catchup: [
    "🚨 TURBO MODE ACTIVATED! I'm coming for you!",
    "💥 You forced me to unleash my true power!",
    "🔥 NO MORE MR. NICE AI! Time to go all out!",
    "⚡ I'm switching to MAXIMUM OVERDRIVE!",
    "😤 You made me angry - now you'll see what I can do!",
    "🚀 AFTERBURNERS ENGAGED! This isn't over!",
  ],
};

export const mathBotCommentary = {
  ahead: [
    "📊 My performance is optimal!",
    "🤖 My logic beats your speed!",
    "📈 I have 87% win probability!",
    "⚙️ I'm perfectly calibrated!",
    "🔬 Science prevails over you!",
  ],
  behind: [
    "🤔 Recalculating my strategy...",
    "📊 You're exceeding my projections!",
    "⚙️ Adjusting my parameters!",
    "🔬 I'm analyzing your technique!",
    "📈 You're a statistical anomaly!",
  ],
  adaptive_struggle: [
    "📊 I detect inefficiencies in you!",
    "🔬 You should focus on patterns!",
    "⚙️ Use that extra time wisely!",
    "📈 You have room for improvement!",
  ],
  adaptive_mastery: [
    "🤖 Your optimization is excellent!",
    "📊 Your metrics are impressive!",
    "⚙️ I'm updating my models because of you!",
    "🔬 You have near-AI efficiency!",
  ],
  player_passed: [
    "🤖 Your strategy is fascinating!",
    "📊 You're an unexpected variable!",
    "⚙️ I'm adjusting my algorithms...",
    "🔬 Your execution is impressive!",
    "📈 I'm recalculating the odds!",
  ],
  ai_passed: [
    "🤖 My efficiency is optimized!",
    "📊 Just as I calculated!",
    "⚙️ All my systems nominal!",
    "🔬 My logic prevails over you!",
    "📈 I'm at 96% confidence level!",
  ],
  lapped: [
    "🤖 Error: You have exceeded my projections!",
    "📊 This outcome has 0.3% probability!",
    "⚙️ I need to recalibrate my systems!",
    "🔬 Your performance is... statistically improbable!",
    "📈 My confidence level just dropped to 12%!",
    "🤔 I must analyze your methodology!",
  ],
  desperate_catchup: [
    "🤖 EMERGENCY PROTOCOL ACTIVATED! Initiating maximum speed!",
    "🚨 CRITICAL GAP DETECTED! Engaging catchup algorithms!",
    "⚙️ OVERCLOCKING MY PROCESSORS! Prepare for rapid acceleration!",
    "📊 PROBABILITY OF FAILURE: UNACCEPTABLE! Switching to turbo mode!",
    "🔬 HYPOTHESIS: You're about to see my true potential!",
    "📈 CONFIDENCE LEVEL: RISING! My comeback protocol is online!",
  ],
};

export function getAICommentary(
  racer: AIRacer,
  context: CommentaryContext,
  playerProgress: number,
  aiProgress: number,
): string | null {
  // Check cooldown (line 11759-11761)
  const now = Date.now();
  if (now - racer.lastComment < racer.commentCooldown) {
    return null;
  }

  // Select message set based on personality and context
  const messages =
    racer.personality === "competitive"
      ? swiftAICommentary[context]
      : mathBotCommentary[context];

  if (!messages || messages.length === 0) return null;

  // Return random message
  return messages[Math.floor(Math.random() * messages.length)];
}
```

**Commentary Contexts** (8 total):

1. `ahead` - AI is winning (AI progress > player progress)
2. `behind` - AI is losing (AI progress < player progress)
3. `adaptive_struggle` - Player struggling (success rate < 60%, triggered periodically)
4. `adaptive_mastery` - Player dominating (success rate > 85%, triggered periodically)
5. `player_passed` - Player just overtook AI (position change detected)
6. `ai_passed` - AI just overtook player (position change detected)
7. `lapped` - Player lapped AI in circular mode (lap difference >= 1)
8. `desperate_catchup` - AI is >30 units behind (emergency catchup mode)

**Commentary Trigger Logic** (line 11911-11942):

```typescript
// Triggers every 4 questions
if (totalQuestions % 4 !== 0) return;

// Check each AI racer
aiRacers.forEach((racer) => {
  const playerProgress = correctAnswers;
  const aiProgress = Math.floor(racer.position);

  let context = "";

  // Determine context based on positions
  if (aiProgress > playerProgress + 5) {
    context = "ahead";
  } else if (playerProgress > aiProgress + 5) {
    context = "behind";
  }

  // Trigger commentary
  const message = getAICommentary(racer, context, playerProgress, aiProgress);
  if (message) {
    showAICommentary(racer, message, context);
  }
});
```

### **5.2 Speech Bubble Component** (SpeechBubble.tsx)

```typescript
export function SpeechBubble({
  racerId,
  message,
  isVisible,
  position,
  counterRotation,
}) {
  // Renders:
  // - Bubble with content
  // - Tail pointing to racer
  // - Fade in/out animation
  // - Auto-hide after 3.5s (line 11752)
  // - Position near racer
  // - Counter-rotation for circular track

  // Auto-hide implementation (line 11749-11752):
  useEffect(() => {
    if (isVisible) {
      const timer = setTimeout(() => {
        setIsVisible(false);
      }, 3500);
      return () => clearTimeout(timer);
    }
  }, [isVisible]);

  // Cooldown setting (line 11746-11747):
  // racer.lastComment = Date.now()
  // racer.commentCooldown = Math.random() * 4000 + 2000 // 2-6 seconds
}
```

**CSS Styles** (lines 5864-5977):

```css
.speech-bubble {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
  background: white;
  border-radius: 15px;
  padding: 10px 15px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  font-size: 14px;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.3s ease;
  z-index: 10;
  pointer-events: none;
}

.speech-bubble.visible {
  opacity: 1;
}

/* Circular track counter-rotation */
.race-track.circular .racer .speech-bubble {
  transform: translateX(-50%) rotate(var(--counter-rotation));
}
```

---

## **Phase 6: Sound & Polish** 🎵

### **6.1 Sound System**

```typescript
export function useSoundEffects() {
  const sounds = useMemo(
    () => ({
      correct: new Audio("/sounds/correct.mp3"),
      incorrect: new Audio("/sounds/incorrect.mp3"),
      countdown: new Audio("/sounds/countdown.mp3"),
      raceStart: new Audio("/sounds/race_start.mp3"),
      victory: new Audio("/sounds/victory.mp3"),
      defeat: new Audio("/sounds/defeat.mp3"),
    }),
    [],
  );

  return {
    playSound: useCallback(
      (type: keyof typeof sounds, volume = 0.5) => {
        sounds[type].volume = volume;
        sounds[type].currentTime = 0; // Reset to start
        sounds[type].play().catch((err) => {
          console.warn("Sound play failed:", err);
        });
      },
      [sounds],
    ),
  };
}
```

**Sound Triggers** (from original):

- Countdown: 0.4 volume (line 11186)
- Race start: 0.6 volume (line 11196)
- Correct answer: full volume
- Incorrect answer: full volume
- Victory: when player wins race
- Defeat: when AI wins race

### **6.2 Animation Classes** (Panda CSS)

**Correct Answer Animation**:

```typescript
const correctAnimation = css({
  background: "linear-gradient(45deg, #d4edda, #c3e6cb)",
  border: "2px solid #28a745",
  boxShadow: "0 0 20px rgba(40, 167, 69, 0.3)",
  animation: "correctPulse 0.3s ease",

  "@keyframes correctPulse": {
    "0%": { transform: "scale(1)", backgroundColor: "white" },
    "50%": { transform: "scale(1.05)", backgroundColor: "#d4edda" },
    "100%": { transform: "scale(1)", backgroundColor: "white" },
  },
});
```

**Incorrect Answer Animation**:

```typescript
const incorrectAnimation = css({
  background: "linear-gradient(45deg, #f8d7da, #f1b0b7)",
  border: "2px solid #dc3545",
  boxShadow: "0 0 20px rgba(220, 53, 69, 0.3)",
  animation: "incorrectShake 0.3s ease",

  "@keyframes incorrectShake": {
    "0%, 100%": { transform: "translateX(0)" },
    "25%": { transform: "translateX(-10px)" },
    "75%": { transform: "translateX(10px)" },
  },
});
```

**Racer Bounce Animation** (lines 5596-5612):

```typescript
const racerBounce = css({
  animation: "racerBounce 0.3s ease-out",

  "@keyframes racerBounce": {
    "0%": { transform: "translateY(0) scale(1)" },
    "30%": { transform: "translateY(-8px) scale(1.1)" },
    "50%": { transform: "translateY(-12px) scale(1.15)" },
    "70%": { transform: "translateY(-8px) scale(1.1)" },
    "100%": { transform: "translateY(0) scale(1)" },
  },
});

// Special bounce for circular track (preserves rotation)
const circularBounce = css({
  "@keyframes circularBounce": {
    "0%": { transform: "rotate(var(--racer-rotation)) translateY(0) scale(1)" },
    "30%": {
      transform: "rotate(var(--racer-rotation)) translateY(-8px) scale(1.1)",
    },
    "50%": {
      transform: "rotate(var(--racer-rotation)) translateY(-12px) scale(1.15)",
    },
    "70%": {
      transform: "rotate(var(--racer-rotation)) translateY(-8px) scale(1.1)",
    },
    "100%": {
      transform: "rotate(var(--racer-rotation)) translateY(0) scale(1)",
    },
  },
});
```

---

## **Phase 7: Testing Strategy** ✅

### **7.1 Unit Tests**

- [ ] Question generation (no repeats, 10 attempt safety)
- [ ] Score calculation formulas (base + streak + speed bonus)
- [ ] AI speed adaptation logic (all threshold values)
- [ ] Difficulty tracking per pair (Map operations)
- [ ] Circular position calculations (trigonometry)
- [ ] Momentum decay system (all skill levels)
- [ ] Commentary selection logic (cooldowns, context matching)
- [ ] Passing event detection (tolerance for floating point)
- [ ] Lap tracking and celebration cooldown

### **7.2 Integration Tests**

- [ ] Game flow: intro → controls → countdown → playing → results
- [ ] AI movement synchronized with game timer (200ms updates)
- [ ] Speech bubbles appear/disappear correctly (3.5s auto-hide)
- [ ] Mode switching (practice/sprint/survival)
- [ ] Adaptive difficulty adjusts properly (learning mode → adaptive)
- [ ] Lap tracking in circular mode (50 progress per lap)
- [ ] Momentum system in sprint mode (decay + gain)
- [ ] Time of day progression (6 periods over 60s)

### **7.3 Manual Testing Checklist**

- [ ] All 3 game modes work correctly
- [ ] Both AI personalities have distinct commentary
- [ ] All 8 commentary contexts trigger appropriately
- [ ] Adaptive difficulty responds to performance
- [ ] Steam train visualization smooth (5 updates/second)
- [ ] Circular track positioning correct (racers follow circle)
- [ ] Speech bubbles positioned properly on both tracks
- [ ] Speech bubbles counter-rotate on circular track
- [ ] Medals awarded correctly (Gold/Silver/Bronze)
- [ ] Speed ratings calculate correctly
- [ ] Sound effects play at right times
- [ ] Responsive on mobile/tablet/desktop
- [ ] Keyboard input captures properly
- [ ] No memory leaks (intervals cleaned up)
- [ ] Timer accuracy (countdown, question timer, game timer)
- [ ] Pause/resume functionality
- [ ] Modal animations smooth
- [ ] Racer bounce animations work
- [ ] Progress bar updates smoothly
- [ ] Pressure gauge animates correctly

---

## **Phase 8: Migration Checklist** 📋

### **Must Preserve - Exact Values**:

- ✅ Swift AI speed: 0.25x base multiplier
- ✅ Math Bot speed: 0.15x base multiplier
- ✅ AI update interval: 200ms
- ✅ AI variance: `Math.random() * 0.8 + 0.6` (0.6-1.4 range)
- ✅ AI rubber-banding: 2x speed when >10 units behind
- ✅ AI passing tolerance: 0.1
- ✅ Commentary cooldown: 2-6s (`Math.random() * 4000 + 2000`)
- ✅ Speech bubble duration: 3.5s
- ✅ Countdown timing: 1s per number
- ✅ Race goal (practice): 20 answers
- ✅ Time limit (sprint): 60 seconds
- ✅ Circular lap length: 50 progress units
- ✅ Base time limit: 3000ms
- ✅ Difficulty scale: 1-5
- ✅ Adaptation rate: 0.1
- ✅ Success rate thresholds: 85%, 75%, 60%, 45%
- ✅ Response time thresholds: 1500ms, 2500ms, 4000ms
- ✅ Streak bonus thresholds: 8, 5
- ✅ Adaptive multiplier bounds: 0.3-2.0
- ✅ Score formula: `correctAnswers * 100 + streak * 50 + speedBonus`
- ✅ Speed bonus: `max(0, 300 - (avgTime * 10))`
- ✅ Train movement rate: `momentum * 0.4` per 200ms
- ✅ Momentum decay base: skill-level dependent (see config)
- ✅ Pressure gauge PSI: `momentum * 1.5` (0-150 range)
- ✅ Time of day thresholds: 0.17, 0.33, 0.67, 0.83, 0.92
- ✅ Question repeat safety: 10 attempts max

### **Must Preserve - All Commentary**:

- ✅ All 41 Swift AI messages across 8 contexts
- ✅ All 41 Math Bot messages across 8 contexts
- ✅ Exact emoji and wording for each message
- ✅ Message randomization (no patterns)
- ✅ Context-appropriate triggering

### **Must Preserve - Visual Polish**:

- ✅ All time of day gradients (6 periods)
- ✅ Momentum gauge colors (red/yellow/turquoise thresholds)
- ✅ Racer bounce animation (with circular variant)
- ✅ Speech bubble styling and positioning
- ✅ Correct/incorrect feedback animations
- ✅ Countdown animation (scale + color)
- ✅ Modal entrance/exit animations
- ✅ Progress bar smooth transitions

---

## **Implementation Order**

### **Week 1: Core Infrastructure**

1. Create directory structure
2. Set up TypeScript types (gameTypes.ts)
3. Create ComplementRaceContext with useReducer
4. Implement page.tsx wrapper with PageWithNav
5. Basic GameIntro component
6. Basic GameControls component (mode/timeout/style buttons)

### **Week 2: Game Mechanics**

1. Implement useGameLoop hook
   - Question generation with repeat avoidance
   - Timer management
   - Answer validation
   - Score calculation
2. Implement GameDisplay component
3. Implement GameTimer component
4. Implement GameHeader component
5. Implement keyboard input capture
6. Wire up game flow: intro → controls → countdown → playing

### **Week 3: AI System**

1. Implement useAIRacers hook
   - Position updates (200ms interval)
   - Rubber-banding logic
   - Passing detection
2. Port all commentary messages to aiCommentary.ts
3. Implement SpeechBubble component
4. Implement AIRacer component
5. Wire up commentary triggering
6. Test all 8 commentary contexts

### **Week 4: Adaptive Difficulty**

1. Implement useAdaptiveDifficulty hook
   - Per-pair performance tracking
   - Learning mode detection
   - Time limit calculation
2. Implement AI speed adaptation
3. Wire up adaptive feedback messages
4. Test difficulty scaling

### **Week 5: Visualizations Part 1 (Linear & Circular)**

1. Implement LinearTrack component
   - Horizontal track layout
   - Position calculations
   - Finish line
2. Implement CircularTrack component
   - SVG circle
   - Trigonometry positioning
   - Lap tracking
   - Celebration effects
3. Test both visualizations with AI movement

### **Week 6: Visualizations Part 2 (Steam Train)**

1. Implement useSteamJourney hook
   - Momentum system
   - Decay calculations
   - Train position updates
2. Implement SteamTrainJourney component
   - Dynamic sky gradients
   - SVG railroad track
   - Locomotive animation
   - Pressure gauge
   - Momentum bar
3. Test time of day progression
4. Test momentum decay at all skill levels

### **Week 7: Scoring & Results**

1. Implement scoring system (scoringSystem.ts)
   - Score calculation
   - Medal determination
   - Speed rating
2. Implement ScoreModal component
3. Implement end game flow
4. Test all scoring scenarios

### **Week 8: Polish & Testing**

1. Implement sound effects system
2. Add all animations (Panda CSS)
3. Unit tests for critical functions
4. Integration tests for game flow
5. Manual testing (full checklist)
6. Performance optimization
7. Bug fixes
8. Final verification against original

---

## **Critical Reference Points in Original Code**

### **Source File**: `packages/core/src/web_generator.py`

**Key Line Ranges**:

- Class definition: 10956-10957
- Constructor & state: 10957-11030
- Game configuration: 11098-11161 (startRace)
- Question generation: 11214-11284 (nextQuestion)
- Timer system: 11286-11364 (startQuestionTimer, getTimerDuration)
- Answer handling: 11366-11500+ (handleKeydown, submitAnswer)
- AI initialization: 11001-11024 (aiRacers array)
- AI commentary system: 11723-11909 (showAICommentary, getAICommentary)
- AI movement: 12603-12850+ (updateAIRacers, startAIRacers)
- Circular track: 12715-12752 (updateCircularPosition)
- Lap tracking: 12754-12782 (checkForLappingCelebration)
- Passing detection: 12678-12713 (checkForPassingEvents)
- Race initialization: 12251-12349 (initializeRace)
- Steam journey init: 13006-13062 (initializeSteamJourney)
- Momentum system: 13029-13053 (momentum decay interval)
- Time of day: 13064-13095 (updateTimeOfDay)
- Pressure gauge: 13118-13146 (updatePressureGauge)
- Train movement: 13465-13572 (updateTrainPosition)
- Adaptive difficulty: 14607-14738 (adaptAISpeeds, showAIAdaptationFeedback)
- Difficulty tracking: 14740-14934 (getAdaptiveTimeLimit, updatePairDifficulty)
- Scoring system: 12140-12238 (calculateResults)

---

## **Risk Mitigation Strategies**

1. **Timing Precision**:
   - Use `useRef` for all intervals/timeouts
   - Clear all timers in cleanup functions
   - Test with React.StrictMode enabled

2. **State Complexity**:
   - Thoroughly test reducer with all action types
   - Add logging for state transitions in development
   - Consider using Immer for complex state updates

3. **Animation Performance**:
   - Use CSS transforms instead of position properties
   - Avoid layout thrashing (batch DOM reads/writes)
   - Use `requestAnimationFrame` for smooth animations

4. **Speech Bubble Positioning**:
   - Test on various screen sizes (mobile, tablet, desktop)
   - Use viewport-relative units where appropriate
   - Handle edge cases (bubbles near screen edges)

5. **Circular Track Math**:
   - Write unit tests for position calculations
   - Verify with multiple progress values
   - Test lap detection edge cases

6. **Memory Leaks**:
   - Ensure all intervals are cleared on unmount
   - Remove event listeners properly
   - Test for memory leaks with React DevTools Profiler

7. **AI Behavior Consistency**:
   - Compare AI speeds between original and port
   - Verify rubber-banding triggers correctly
   - Test commentary triggers in all contexts

---

## **Success Criteria**

### **Functional**:

- ✅ All 3 game modes (practice/sprint/survival) working
- ✅ All 8 commentary contexts triggering appropriately
- ✅ Adaptive difficulty responding to player performance
- ✅ Circular track lap tracking accurate
- ✅ Steam train momentum system working
- ✅ Scoring and medals calculating correctly

### **Quality**:

- ✅ No regressions from original gameplay
- ✅ Smooth animations (60fps target)
- ✅ Responsive on all screen sizes
- ✅ No memory leaks or performance issues
- ✅ TypeScript strict mode compliance
- ✅ Unit test coverage for critical logic

### **Preservation**:

- ✅ All 82 commentary messages preserved exactly
- ✅ All numerical values match original
- ✅ All formulas calculate identically
- ✅ All timing behaviors match original
- ✅ Visual polish matches or exceeds original

---

## **Notes for Implementation**

1. **Start Simple**: Begin with practice mode (linear track) before tackling sprint/survival modes
2. **Test Incrementally**: Test each feature as you build it, don't wait until the end
3. **Reference Original Often**: Keep web_generator.py open and cross-reference constantly
4. **Preserve Comments**: Port over helpful comments from original code
5. **Document Deviations**: If you must deviate from original, document why
6. **Performance First**: Profile early and often, don't wait for performance issues
7. **Mobile Matters**: Test on actual mobile devices, not just browser DevTools
8. **Accessibility**: Add ARIA labels and keyboard navigation support

---

## **Where to Resume After Disconnection**

1. Check this file: `apps/web/COMPLEMENT_RACE_PORT_PLAN.md`
2. Check todo list in Claude Code session
3. Check git status for what's been created
4. Look for work-in-progress files in `apps/web/src/app/games/complement-race/`
5. Review recent commits to see what phase was being worked on

**Key Context**:

- Original source: `packages/core/src/web_generator.py` (lines 10956-15113)
- Generated output example: `packages/core/src/flashcards_en.html`
- Implementation follows pattern from existing games: `memory-quiz/` and `matching/`
- Using: Next.js 14, React 18, TypeScript, Panda CSS, @soroban/abacus-react

---

**Last Updated**: 2025-09-30 by Claude Code
**Original Python Code**: 4,157 lines (class + commentary + systems)
**Estimated TypeScript**: ~5,000 lines (with proper separation of concerns)
**Complexity**: ⭐⭐⭐⭐⭐ (5/5 - Very Complex, Many Nuances)
