37 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
useReducerfor 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):
ahead- AI is winning (AI progress > player progress)behind- AI is losing (AI progress < player progress)adaptive_struggle- Player struggling (success rate < 60%, triggered periodically)adaptive_mastery- Player dominating (success rate > 85%, triggered periodically)player_passed- Player just overtook AI (position change detected)ai_passed- AI just overtook player (position change detected)lapped- Player lapped AI in circular mode (lap difference >= 1)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.4per 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
- Create directory structure
- Set up TypeScript types (gameTypes.ts)
- Create ComplementRaceContext with useReducer
- Implement page.tsx wrapper with PageWithNav
- Basic GameIntro component
- Basic GameControls component (mode/timeout/style buttons)
Week 2: Game Mechanics
- Implement useGameLoop hook
- Question generation with repeat avoidance
- Timer management
- Answer validation
- Score calculation
- Implement GameDisplay component
- Implement GameTimer component
- Implement GameHeader component
- Implement keyboard input capture
- Wire up game flow: intro → controls → countdown → playing
Week 3: AI System
- Implement useAIRacers hook
- Position updates (200ms interval)
- Rubber-banding logic
- Passing detection
- Port all commentary messages to aiCommentary.ts
- Implement SpeechBubble component
- Implement AIRacer component
- Wire up commentary triggering
- Test all 8 commentary contexts
Week 4: Adaptive Difficulty
- Implement useAdaptiveDifficulty hook
- Per-pair performance tracking
- Learning mode detection
- Time limit calculation
- Implement AI speed adaptation
- Wire up adaptive feedback messages
- Test difficulty scaling
Week 5: Visualizations Part 1 (Linear & Circular)
- Implement LinearTrack component
- Horizontal track layout
- Position calculations
- Finish line
- Implement CircularTrack component
- SVG circle
- Trigonometry positioning
- Lap tracking
- Celebration effects
- Test both visualizations with AI movement
Week 6: Visualizations Part 2 (Steam Train)
- Implement useSteamJourney hook
- Momentum system
- Decay calculations
- Train position updates
- Implement SteamTrainJourney component
- Dynamic sky gradients
- SVG railroad track
- Locomotive animation
- Pressure gauge
- Momentum bar
- Test time of day progression
- Test momentum decay at all skill levels
Week 7: Scoring & Results
- Implement scoring system (scoringSystem.ts)
- Score calculation
- Medal determination
- Speed rating
- Implement ScoreModal component
- Implement end game flow
- Test all scoring scenarios
Week 8: Polish & Testing
- Implement sound effects system
- Add all animations (Panda CSS)
- Unit tests for critical functions
- Integration tests for game flow
- Manual testing (full checklist)
- Performance optimization
- Bug fixes
- 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
-
Timing Precision:
- Use
useReffor all intervals/timeouts - Clear all timers in cleanup functions
- Test with React.StrictMode enabled
- Use
-
State Complexity:
- Thoroughly test reducer with all action types
- Add logging for state transitions in development
- Consider using Immer for complex state updates
-
Animation Performance:
- Use CSS transforms instead of position properties
- Avoid layout thrashing (batch DOM reads/writes)
- Use
requestAnimationFramefor smooth animations
-
Speech Bubble Positioning:
- Test on various screen sizes (mobile, tablet, desktop)
- Use viewport-relative units where appropriate
- Handle edge cases (bubbles near screen edges)
-
Circular Track Math:
- Write unit tests for position calculations
- Verify with multiple progress values
- Test lap detection edge cases
-
Memory Leaks:
- Ensure all intervals are cleared on unmount
- Remove event listeners properly
- Test for memory leaks with React DevTools Profiler
-
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
- Start Simple: Begin with practice mode (linear track) before tackling sprint/survival modes
- Test Incrementally: Test each feature as you build it, don't wait until the end
- Reference Original Often: Keep web_generator.py open and cross-reference constantly
- Preserve Comments: Port over helpful comments from original code
- Document Deviations: If you must deviate from original, document why
- Performance First: Profile early and often, don't wait for performance issues
- Mobile Matters: Test on actual mobile devices, not just browser DevTools
- Accessibility: Add ARIA labels and keyboard navigation support
Where to Resume After Disconnection
- Check this file:
apps/web/COMPLEMENT_RACE_PORT_PLAN.md - Check todo list in Claude Code session
- Check git status for what's been created
- Look for work-in-progress files in
apps/web/src/app/games/complement-race/ - 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/andmatching/ - 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)