soroban-abacus-flashcards/apps/web/COMPLEMENT_RACE_PORT_PLAN.md

36 KiB

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)

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

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)

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)

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)

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)

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:

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)

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)

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):

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):

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):

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):

// 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.

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):

// 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)

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):

.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

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:

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:

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):

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)