diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 00000000000..0d791c7a2ae
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,15 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(mkdir:*)",
+ "Bash(pnpm --filter perseus test:*)",
+ "Bash(pnpm test:*)",
+ "Bash(git mv:*)",
+ "Bash(for file in sky.png city-far.png city-semi-far.png city-semi-close.png city-close.png streetlamp.png lamplight.png)",
+ "Bash(do git mv packages/perseus/src/__docs__/$file packages/perseus/src/games/crash-course/assets/backgrounds/)",
+ "Bash(done)"
+ ],
+ "deny": [],
+ "ask": []
+ }
+}
diff --git a/CAR_BONUS_CONCEPT.md b/CAR_BONUS_CONCEPT.md
new file mode 100644
index 00000000000..f14daffcee8
--- /dev/null
+++ b/CAR_BONUS_CONCEPT.md
@@ -0,0 +1,78 @@
+# Concept: Car Bonus Scene (Game Over Sequence)
+
+## What We're Building
+
+A comedic "bonus level" that plays when the user runs out of lives in the Math Blaster game. Inspired by the Street Fighter 2 car-smashing bonus level, but with a twist: the car self-destructs without any user interaction. It's a quick visual gag that serves as a humorous transition to the game over screen.
+
+## Why
+
+- **Add personality**: Makes the game over experience more memorable and fun
+- **Reduce frustration**: Lightens the mood when players lose all their lives
+- **Homage**: A playful reference to a classic video game moment
+- **Polish**: Adds a unique touch that makes the Math Blaster game feel more complete
+
+## Key Components
+
+1. **Car Bonus Scene Component**: A new React component that displays the car sequence
+ - Shows intact car (initial state)
+ - Transitions to destroyed car (imploded state)
+ - Simple two-frame "animation" (before → after)
+
+2. **Transition Logic**: Integration into existing game flow
+ - Triggered when lives reach zero
+ - Smoothly transitions from main game to car scene
+ - After completion, proceeds to existing game over screen
+
+3. **Visual Assets**: Two car images
+ - Before: Intact car
+ - After: Imploded/destroyed car
+
+4. **Timing System**: Controls the sequence duration
+ - Total duration: 3-5 seconds
+ - Possible breakdown:
+ - Show intact car: ~1-2 seconds
+ - Implode transition: ~0.5 seconds
+ - Show destroyed car: ~1-2 seconds
+ - Optional: Brief shake/anticipation effect before implode
+
+## How It Works (High Level)
+
+**User Flow:**
+
+1. Player is playing an endless runner type game with math questions
+2. Player loses their last life
+3. Game transitions to car bonus scene
+4. Car appears intact on screen
+5. Brief pause (build anticipation)
+6. Car suddenly implodes (image switches from before → after)
+7. Brief pause (appreciate the destruction)
+8. Transition to existing game over screen
+
+**No user interaction required** - it's a passive, timed sequence.
+
+## Technical Considerations
+
+- **React Component**: Create a self-contained component for the car scene
+- **State Management**: Needs to know when game is over (lives === 0)
+- **Timing**: Use CSS transitions or React state + setTimeout for image swap
+- **Assets**: Need to source/create the two car images (before/after)
+- **Integration Point**: Hook into existing game over logic in Math Blaster
+- **Storybook Story**: Create a story to demo and test the sequence in isolation
+
+## Success Criteria
+
+- [ ] Car bonus scene triggers when player runs out of lives
+- [ ] Sequence displays intact car, then destroyed car
+- [ ] Total duration is 3-5 seconds
+- [ ] After sequence completes, game over screen appears
+- [ ] Transition feels smooth and polished
+- [ ] Works in Storybook for easy demonstration
+- [ ] Visual gag lands (it's funny!)
+- [ ] No user interaction required (truly passive)
+
+## Open Questions
+
+- Should there be sound effects? (Currently not planned, but could add later)
+- Do we want any text on screen during the sequence? (e.g., "BONUS!" or "NICE TRY!")
+- Should there be any particle effects or additional visual polish?
+- What should the background look like? (Solid color, street scene, etc.)
diff --git a/CAR_BONUS_PLAN.md b/CAR_BONUS_PLAN.md
new file mode 100644
index 00000000000..440c5ecdf57
--- /dev/null
+++ b/CAR_BONUS_PLAN.md
@@ -0,0 +1,33 @@
+# Implementation Plan: Car Bonus Scene
+
+Based on: [CAR_BONUS_CONCEPT.md](./CAR_BONUS_CONCEPT.md)
+
+## Implementation Tasks
+
+1. **Create CarBonusScene component** (`packages/perseus/src/__docs__/car-bonus-scene.tsx`)
+ - Functional component with `onComplete` callback prop
+ - State management for intact → destroyed → complete
+ - Use setTimeout for 3-5 second timing
+ - Two car images (before/after)
+ - Basic CSS styling (centered, responsive)
+
+2. **Create Storybook story** (`packages/perseus/src/__docs__/car-bonus-scene.stories.tsx`)
+ - Demo the standalone component
+ - Test timing and transitions in isolation
+
+3. **Integrate into Math Blaster game**
+ - Add car bonus scene state to game flow
+ - Trigger when lives reach 0
+ - Transition: game → car scene → game over screen
+
+4. **Polish**
+ - Smooth fade transitions
+ - Fine-tune timing
+ - Test on different screen sizes
+ - Run lint/format/type-check
+
+## Notes
+
+- Start with placeholder images if needed
+- Keep it simple - just two images and timing
+- Total estimated time: 2-4 hours
diff --git a/PHASE_1_setup_assets.md b/PHASE_1_setup_assets.md
new file mode 100644
index 00000000000..bb2f75413af
--- /dev/null
+++ b/PHASE_1_setup_assets.md
@@ -0,0 +1,313 @@
+# Phase 1: Setup and Asset Organization
+
+**Part of**: [CRASH_COURSE_IMPLEMENTATION_PLAN.md](./CRASH_COURSE_IMPLEMENTATION_PLAN.md)
+
+**Goal**: Create the new directory structure and organize all 40+ asset files into proper subdirectories with consistent naming.
+
+## Tasks
+
+### Task 1: Create Directory Structure
+- **What**: Set up the new `games/crash-course/` folder hierarchy
+- **Why**: Establishes the foundation for all subsequent work
+- **Implementation notes**:
+ - Create base directory at `packages/perseus/src/games/crash-course/`
+ - Create asset subdirectories for organization
+ - Create placeholder files to prevent empty directory issues
+- **Files affected**:
+ - New: `packages/perseus/src/games/crash-course/` (directory)
+ - New: `packages/perseus/src/games/crash-course/assets/` (directory)
+ - New: `packages/perseus/src/games/crash-course/assets/sprites/` (directory)
+ - New: `packages/perseus/src/games/crash-course/assets/backgrounds/` (directory)
+ - New: `packages/perseus/src/games/crash-course/assets/ui/` (directory)
+ - New: `packages/perseus/src/games/crash-course/assets/audio/` (directory)
+ - New: `packages/perseus/src/games/crash-course/assets/story/` (directory)
+- **Acceptance criteria**:
+ - Directory structure exists
+ - All asset subdirectories created
+ - Structure follows Perseus conventions
+
+### Task 2: Move and Organize Sprite Assets
+- **What**: Move character, alien, and car sprites to the sprites folder
+- **Why**: Groups related visual assets together
+- **Implementation notes**:
+ - Move character animation frames (run1-6, impact, idle)
+ - Move alien sprites (alien1-3)
+ - Move car sprites (car1-2)
+ - Move special effects (beam)
+ - Keep original names or use kebab-case consistently
+- **Files affected**:
+ - Move from: `packages/perseus/src/__docs__/*.png`
+ - Move to: `packages/perseus/src/games/crash-course/assets/sprites/`
+ - Specific files:
+ - `run1.png` → `sprites/run1.png`
+ - `run2.png` → `sprites/run2.png`
+ - `run3.png` → `sprites/run3.png`
+ - `run4.png` → `sprites/run4.png`
+ - `run5.png` → `sprites/run5.png`
+ - `run6.png` → `sprites/run6.png`
+ - `impact.png` → `sprites/impact.png`
+ - `idle.png` → `sprites/idle.png`
+ - `alien1.png` → `sprites/alien1.png`
+ - `alien2.png` → `sprites/alien2.png`
+ - `alien3.png` → `sprites/alien3.png`
+ - `car1.png` → `sprites/car1.png`
+ - `car2.png` → `sprites/car2.png`
+ - `beam.png` → `sprites/beam.png`
+- **Acceptance criteria**:
+ - All sprite files moved to correct directory
+ - No duplicate files remain in __docs__
+
+### Task 3: Move and Organize Background Assets
+- **What**: Move parallax layers and environment graphics
+- **Why**: Separates background elements from interactive sprites
+- **Implementation notes**:
+ - Move sky, city layers, and ground elements
+ - Move street lamp and lamp light
+- **Files affected**:
+ - Move from: `packages/perseus/src/__docs__/*.png`
+ - Move to: `packages/perseus/src/games/crash-course/assets/backgrounds/`
+ - Specific files:
+ - `sky.png` → `backgrounds/sky.png`
+ - `city-far.png` → `backgrounds/city-far.png`
+ - `city-semi-far.png` → `backgrounds/city-semi-far.png`
+ - `city-semi-close.png` → `backgrounds/city-semi-close.png`
+ - `city-close.png` → `backgrounds/city-close.png`
+ - `streetlamp.png` → `backgrounds/streetlamp.png`
+ - `lamplight.png` → `backgrounds/lamplight.png`
+- **Acceptance criteria**:
+ - All background files moved
+ - Logical organization by depth/layer
+
+### Task 4: Move and Organize UI Assets
+- **What**: Move buttons, screens, and UI elements
+- **Why**: Keeps user interface assets separate from game graphics
+- **Implementation notes**:
+ - Move all button images (start, next, mute, unmute)
+ - Move screen images (title, victory, lose)
+ - Move bonus scene images
+- **Files affected**:
+ - Move from: `packages/perseus/src/__docs__/*.png`
+ - Move to: `packages/perseus/src/games/crash-course/assets/ui/`
+ - Specific files:
+ - `title.png` → `ui/title.png`
+ - `start.png` → `ui/start.png`
+ - `next.png` → `ui/next.png`
+ - `mute.png` → `ui/mute.png`
+ - `unmute.png` → `ui/unmute.png`
+ - `victory.png` → `ui/victory.png`
+ - `lose.png` → `ui/lose.png`
+ - `bonus1.png` → `ui/bonus1.png`
+ - `bonus2.png` → `ui/bonus2.png`
+ - `skid.png` → `ui/skid.png`
+- **Acceptance criteria**:
+ - All UI assets moved
+ - Easy to find buttons vs screens
+
+### Task 5: Move and Organize Story Assets
+- **What**: Move all 7 story page images
+- **Why**: Keeps narrative content separate and organized
+- **Implementation notes**:
+ - Move story1 through story7 images
+ - Keep sequential naming for easy identification
+- **Files affected**:
+ - Move from: `packages/perseus/src/__docs__/story*.png`
+ - Move to: `packages/perseus/src/games/crash-course/assets/story/`
+ - Specific files:
+ - `story1.png` → `story/story1.png`
+ - `story2.png` → `story/story2.png`
+ - `story3.png` → `story/story3.png`
+ - `story4.png` → `story/story4.png`
+ - `story5.png` → `story/story5.png`
+ - `story6.png` → `story/story6.png`
+ - `story7.png` → `story/story7.png`
+- **Acceptance criteria**:
+ - All story images in story/ folder
+ - Sequential numbering preserved
+
+### Task 6: Move and Organize Audio Assets
+- **What**: Move all audio files (.ogg)
+- **Why**: Separates audio from visual assets
+- **Implementation notes**:
+ - Move all 4 audio files
+ - Consider renaming for clarity (optional)
+- **Files affected**:
+ - Move from: `packages/perseus/src/__docs__/*.ogg`
+ - Move to: `packages/perseus/src/games/crash-course/assets/audio/`
+ - Specific files:
+ - `alexbouncymix2.ogg` → `audio/menu-theme.ogg` (or keep original name)
+ - `Zodik - Tedox.ogg` → `audio/game-theme-1.ogg` (or keep original)
+ - `Zodik - Neon Owl.ogg` → `audio/game-theme-2.ogg` (or keep original)
+ - `Game Over II.ogg` → `audio/game-over-theme.ogg` (or keep original)
+- **Acceptance criteria**:
+ - All audio files moved
+ - Names are clear and descriptive
+
+### Task 7: Move TypeScript/CSS Files
+- **What**: Move the game components and utilities to the new location
+- **Why**: Consolidates all game code in one place
+- **Implementation notes**:
+ - Move main story file, utils, CSS, and car bonus scene
+ - Rename files from "math-blaster" to "crash-course"
+ - Update internal references
+- **Files affected**:
+ - Move and rename:
+ - `__docs__/math-blaster-game.stories.tsx` → `games/crash-course/crash-course.stories.tsx`
+ - `__docs__/math-blaster-game.module.css` → `games/crash-course/crash-course.module.css`
+ - `__docs__/math-blaster-utils.ts` → `games/crash-course/crash-course-utils.ts`
+ - `__docs__/car-bonus-scene.tsx` → `games/crash-course/car-bonus-scene.tsx`
+ - `__docs__/car-bonus-scene.module.css` → `games/crash-course/car-bonus-scene.module.css`
+ - `__docs__/car-bonus-scene.stories.tsx` → `games/crash-course/car-bonus-scene.stories.tsx` (or delete if not needed)
+- **Acceptance criteria**:
+ - All code files in games/crash-course/
+ - Consistent "crash-course" naming
+ - No "math-blaster" references in filenames
+
+### Task 8: Update Import Paths in Main Component
+- **What**: Update all asset import statements to point to new locations
+- **Why**: Make the code work after moving assets
+- **Implementation notes**:
+ - Update image imports to use new asset paths
+ - Update audio imports to use new paths
+ - Update relative imports for CSS and utilities
+ - Search for `from "./` and update paths
+- **Files affected**:
+ - `games/crash-course/crash-course.stories.tsx` (main file)
+ - `games/crash-course/car-bonus-scene.tsx`
+- **Acceptance criteria**:
+ - All imports resolve correctly
+ - No import errors when building
+ - TypeScript doesn't complain
+
+### Task 9: Update Import Paths in CarBonusScene
+- **What**: Update asset imports in the CarBonusScene component
+- **Why**: Ensure CarBonusScene works after asset reorganization
+- **Implementation notes**:
+ - Update any image imports (bonus1, bonus2, skid)
+ - Update CSS import path if needed
+- **Files affected**:
+ - `games/crash-course/car-bonus-scene.tsx`
+- **Acceptance criteria**:
+ - CarBonusScene imports work
+ - No broken image references
+
+### Task 10: Update Storybook Story Metadata
+- **What**: Change Storybook title and component references
+- **Why**: Make it appear under "Games" section with correct name
+- **Implementation notes**:
+ - Change `title: "Games/Crash Course"` in story metadata
+ - Update component name references
+ - Update description to reflect new name
+- **Files affected**:
+ - `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Story appears under "Games" in Storybook
+ - Title is "Crash Course" not "Math Blaster"
+ - Description is accurate
+
+## Technical Details
+
+### Import Path Examples
+
+**Before (in __docs__):**
+```typescript
+import run1Img from "./run1.png";
+import styles from "./math-blaster-game.module.css";
+```
+
+**After (in games/crash-course/):**
+```typescript
+import run1Img from "./assets/sprites/run1.png";
+import styles from "./crash-course.module.css";
+```
+
+### Directory Structure Result
+```
+packages/perseus/src/games/crash-course/
+├── assets/
+│ ├── sprites/
+│ │ ├── run1.png
+│ │ ├── run2.png
+│ │ ├── run3.png
+│ │ ├── run4.png
+│ │ ├── run5.png
+│ │ ├── run6.png
+│ │ ├── impact.png
+│ │ ├── idle.png
+│ │ ├── alien1.png
+│ │ ├── alien2.png
+│ │ ├── alien3.png
+│ │ ├── car1.png
+│ │ ├── car2.png
+│ │ └── beam.png
+│ ├── backgrounds/
+│ │ ├── sky.png
+│ │ ├── city-far.png
+│ │ ├── city-semi-far.png
+│ │ ├── city-semi-close.png
+│ │ ├── city-close.png
+│ │ ├── streetlamp.png
+│ │ └── lamplight.png
+│ ├── ui/
+│ │ ├── title.png
+│ │ ├── start.png
+│ │ ├── next.png
+│ │ ├── mute.png
+│ │ ├── unmute.png
+│ │ ├── victory.png
+│ │ ├── lose.png
+│ │ ├── bonus1.png
+│ │ ├── bonus2.png
+│ │ └── skid.png
+│ ├── story/
+│ │ ├── story1.png
+│ │ ├── story2.png
+│ │ ├── story3.png
+│ │ ├── story4.png
+│ │ ├── story5.png
+│ │ ├── story6.png
+│ │ └── story7.png
+│ └── audio/
+│ ├── menu-theme.ogg (or original name)
+│ ├── game-theme-1.ogg
+│ ├── game-theme-2.ogg
+│ └── game-over-theme.ogg
+├── crash-course.stories.tsx (main game component)
+├── crash-course.module.css
+├── crash-course-utils.ts
+├── car-bonus-scene.tsx
+└── car-bonus-scene.module.css
+```
+
+## Testing Considerations
+
+After this phase:
+1. **Build Check**: Run `pnpm tsc` - should have no errors
+2. **Lint Check**: Run `pnpm lint` - should pass
+3. **Storybook Check**:
+ - Run `pnpm storybook`
+ - Navigate to "Games/Crash Course"
+ - Verify game loads and displays start screen
+ - All images should load correctly
+ - Audio should be available (test mute button)
+
+## Potential Issues
+
+### Import Resolution
+- **Issue**: Webpack/Vite may need configuration to resolve asset imports
+- **Mitigation**: Perseus likely already configured for this, but verify
+- **How to detect**: Build errors about unresolved imports
+
+### Asset Loading
+- **Issue**: Some assets might not load if paths are incorrect
+- **Mitigation**: Systematic testing of each screen
+- **How to detect**: Console errors, broken images in Storybook
+
+### File System Case Sensitivity
+- **Issue**: macOS is case-insensitive but Linux is case-sensitive
+- **Mitigation**: Use consistent casing (lowercase with hyphens)
+- **How to detect**: Works locally but fails in CI
+
+### Git History
+- **Issue**: Moving files loses git history
+- **Mitigation**: Use `git mv` instead of regular `mv` to preserve history
+- **How to detect**: Check `git log --follow` on moved files
diff --git a/PHASE_2_ui_components.md b/PHASE_2_ui_components.md
new file mode 100644
index 00000000000..5d7168b684f
--- /dev/null
+++ b/PHASE_2_ui_components.md
@@ -0,0 +1,348 @@
+# Phase 2: Extract UI Components
+
+**Part of**: [CRASH_COURSE_IMPLEMENTATION_PLAN.md](./CRASH_COURSE_IMPLEMENTATION_PLAN.md)
+
+**Goal**: Break out all screen and overlay components from the monolithic 1673-line file into separate, focused components.
+
+## Tasks
+
+### Task 1: Create Types File
+- **What**: Extract all TypeScript types and interfaces into a shared types file
+- **Why**: Allows components to import types without circular dependencies
+- **Implementation notes**:
+ - Create `types.ts` in crash-course directory
+ - Export all game-related types: GameState, CharacterState, SpriteFrame, Obstacle
+ - Import from crash-course-utils if needed
+- **Files affected**:
+ - New: `games/crash-course/types.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - All types exported from central location
+ - No type duplication between files
+ - TypeScript compilation succeeds
+
+### Task 2: Extract StartScreen Component
+- **What**: Create standalone StartScreen component
+- **Why**: Reduces main file by ~50 lines, makes start screen independently testable
+- **Implementation notes**:
+ - Extract JSX from gameState === "start" block
+ - Accept props: imagesLoaded, titleImage, startButton, onStart
+ - Handle loading state internally
+ - Apply styles from crash-course.module.css
+- **Files affected**:
+ - New: `games/crash-course/components/StartScreen.tsx`
+ - Modified: `games/crash-course/crash-course.stories.tsx` (remove inline JSX)
+- **Acceptance criteria**:
+ - StartScreen renders independently
+ - Start button click triggers callback
+ - Loading state displayed correctly
+ - Images load properly
+
+### Task 3: Extract StoryScreen Component
+- **What**: Create standalone StoryScreen component with page navigation
+- **Why**: Isolates story functionality, makes it reusable
+- **Implementation notes**:
+ - Extract JSX from gameState === "story" block
+ - Accept props: currentPage (1-7), storyImages, nextButton, startButton, onNext
+ - Handle page transitions
+ - Show appropriate button (next vs start) based on page
+- **Files affected**:
+ - New: `games/crash-course/components/StoryScreen.tsx`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Story pages display correctly
+ - Page navigation works
+ - Final page shows start button
+ - Images load for all 7 pages
+
+### Task 4: Extract GameOverScreen Component
+- **What**: Create standalone game over screen component
+- **Why**: Separates end state UI from main game logic
+- **Implementation notes**:
+ - Extract JSX from gameState === "gameover" block
+ - Accept props: loseImage, startButton, onPlayAgain
+ - Simple presentation component
+- **Files affected**:
+ - New: `games/crash-course/components/GameOverScreen.tsx`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Game over screen displays
+ - Play again button works
+ - Styling preserved
+
+### Task 5: Extract VictoryScreen Component
+- **What**: Create standalone victory screen component
+- **Why**: Separates victory UI from main game
+- **Implementation notes**:
+ - Extract JSX from gameState === "victory" block
+ - Accept props: victoryImage, startButton, onPlayAgain
+ - Simple presentation component
+- **Files affected**:
+ - New: `games/crash-course/components/VictoryScreen.tsx`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Victory screen displays
+ - Play again button works
+ - Styling preserved
+
+### Task 6: Extract HUD Component
+- **What**: Create heads-up display component showing score, time, lives
+- **Why**: Separates UI overlay from game canvas logic
+- **Implementation notes**:
+ - Extract JSX from gameState === "playing" HUD blocks
+ - Accept props: score, gameTime, lives
+ - Include the HUD background gradient
+ - Self-contained styling
+- **Files affected**:
+ - New: `games/crash-course/components/HUD.tsx`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - HUD displays all info correctly
+ - Lives indicators show/fade properly
+ - Background gradient applies
+ - Positioning correct
+
+### Task 7: Extract QuestionOverlay Component
+- **What**: Create component for displaying Perseus questions with timer
+- **Why**: Complex component deserves its own file, easier to maintain
+- **Implementation notes**:
+ - Extract JSX from Question Overlay block (lines ~1396-1481)
+ - Accept props: obstacle, remainingDistance, onCheckAnswer, answerFeedback
+ - Include timer bar logic and calculation
+ - Include ServerItemRenderer integration
+ - Forward ref to itemRenderer
+- **Files affected**:
+ - New: `games/crash-course/components/QuestionOverlay.tsx`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Questions display correctly
+ - Timer bar animates properly
+ - Check answer button works
+ - Feedback displays correctly
+ - Perseus integration maintained
+
+### Task 8: Extract MuteButton Component
+- **What**: Create simple mute/unmute button component
+- **Why**: Reusable UI element, simple extraction
+- **Implementation notes**:
+ - Extract JSX from mute button block
+ - Accept props: isMuted, muteImage, unmuteImage, onToggle
+ - Simple presentational component
+- **Files affected**:
+ - New: `games/crash-course/components/MuteButton.tsx`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Button displays correct icon
+ - Click toggles mute state
+ - Positioning preserved
+ - Hover effects work
+
+### Task 9: Extract BenevolenceMessage Component
+- **What**: Create component for "BENEVOLENCE" message animation
+- **Why**: Self-contained animation, can be reused
+- **Implementation notes**:
+ - Extract JSX from benevolenceMessage block
+ - Accept props: show (boolean)
+ - Handle animation automatically when shown
+ - Include CSS animation
+- **Files affected**:
+ - New: `games/crash-course/components/BenevolenceMessage.tsx`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Message displays when triggered
+ - Animation plays correctly
+ - Fades out after 2 seconds
+ - Centered properly
+
+### Task 10: Create Components Directory and Index
+- **What**: Organize all components in a subdirectory with barrel export
+- **Why**: Clean imports, clear organization
+- **Implementation notes**:
+ - Create `games/crash-course/components/` directory
+ - Move all component files into components/
+ - Create `index.ts` with re-exports
+ - Update imports in main file
+- **Files affected**:
+ - New: `games/crash-course/components/index.ts`
+ - All component files moved to components/
+ - Modified: `games/crash-course/crash-course.stories.tsx` (update imports)
+- **Acceptance criteria**:
+ - All components in components/ folder
+ - Barrel export works
+ - Main file imports simplified
+
+### Task 11: Update Main Component to Use Extracted Components
+- **What**: Replace inline JSX with component imports
+- **Why**: Dramatically reduces main file size
+- **Implementation notes**:
+ - Replace each game state's inline JSX with component usage
+ - Pass appropriate props to each component
+ - Maintain existing behavior
+ - Verify callbacks work correctly
+- **Files affected**:
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Main file reduced by 400-500 lines
+ - All screens still work
+ - No functionality lost
+ - Props passed correctly
+
+## Technical Details
+
+### Component Props Pattern
+
+**Example: StartScreen**
+```typescript
+// components/StartScreen.tsx
+import * as React from "react";
+import styles from "../crash-course.module.css";
+
+type StartScreenProps = {
+ imagesLoaded: boolean;
+ titleImage?: HTMLImageElement;
+ startButton?: HTMLImageElement;
+ onStart: () => void;
+};
+
+export const StartScreen = ({
+ imagesLoaded,
+ titleImage,
+ startButton,
+ onStart,
+}: StartScreenProps): React.ReactElement => {
+ if (!imagesLoaded) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {titleImage && (
+

+ )}
+ {startButton && (
+

+ )}
+
+ );
+};
+```
+
+### Barrel Export Pattern
+
+**components/index.ts**
+```typescript
+export {StartScreen} from "./StartScreen";
+export {StoryScreen} from "./StoryScreen";
+export {GameOverScreen} from "./GameOverScreen";
+export {VictoryScreen} from "./VictoryScreen";
+export {HUD} from "./HUD";
+export {QuestionOverlay} from "./QuestionOverlay";
+export {MuteButton} from "./MuteButton";
+export {BenevolenceMessage} from "./BenevolenceMessage";
+```
+
+### Usage in Main Component
+
+**Before:**
+```typescript
+{gameState === "start" && (
+
+ {!imagesLoaded &&
Loading sprites...
}
+ {imagesLoaded && (/* 20+ lines of JSX */)}
+
+)}
+```
+
+**After:**
+```typescript
+import {StartScreen} from "./components";
+
+{gameState === "start" && (
+
+)}
+```
+
+## Component Organization
+
+```
+games/crash-course/
+├── components/
+│ ├── StartScreen.tsx (~40 lines)
+│ ├── StoryScreen.tsx (~50 lines)
+│ ├── GameOverScreen.tsx (~35 lines)
+│ ├── VictoryScreen.tsx (~35 lines)
+│ ├── HUD.tsx (~60 lines)
+│ ├── QuestionOverlay.tsx (~120 lines)
+│ ├── MuteButton.tsx (~25 lines)
+│ ├── BenevolenceMessage.tsx (~20 lines)
+│ └── index.ts (~10 lines)
+├── crash-course.stories.tsx (now ~1200 lines, down from 1673)
+└── ...
+```
+
+## Testing Considerations
+
+After this phase:
+1. **Visual Testing**: Each component should display correctly in Storybook
+2. **Interaction Testing**:
+ - Start screen → Story → Game → End screens flow
+ - Buttons trigger correct callbacks
+ - Question overlay displays and accepts answers
+ - HUD updates correctly
+3. **Regression Testing**: Game plays identically to before
+
+## Potential Issues
+
+### Prop Drilling
+- **Issue**: May need to pass many props through components
+- **Mitigation**: Use composition, consider context for deeply nested state
+- **How to detect**: Components with 10+ props
+
+### Ref Forwarding
+- **Issue**: QuestionOverlay needs ref to ServerItemRenderer
+- **Mitigation**: Use React.forwardRef properly
+- **How to detect**: getUserInput() calls fail
+
+### CSS Module Scope
+- **Issue**: Components need access to parent CSS module
+- **Mitigation**: Import from parent, or create component-specific styles
+- **How to detect**: Missing styles in components
+
+### State Management
+- **Issue**: Components need access to game state
+- **Mitigation**: Pass state as props, prepare for Phase 4 refactor
+- **How to detect**: Components can't access needed state
+
+## Benefits After This Phase
+
+- Main component reduced from 1673 to ~1200 lines
+- 8 new reusable, testable components
+- Clearer separation of concerns
+- Easier to understand code structure
+- Individual components can be documented/tested
+- Foundation for Phase 3 (logic extraction)
diff --git a/PHASE_3_game_logic.md b/PHASE_3_game_logic.md
new file mode 100644
index 00000000000..9f0ad7389cc
--- /dev/null
+++ b/PHASE_3_game_logic.md
@@ -0,0 +1,535 @@
+# Phase 3: Extract Game Logic and Hooks
+
+**Part of**: [CRASH_COURSE_IMPLEMENTATION_PLAN.md](./CRASH_COURSE_IMPLEMENTATION_PLAN.md)
+
+**Goal**: Move game loop, audio management, sprite loading, and utilities into custom hooks and utility functions.
+
+## Tasks
+
+### Task 1: Create Constants File
+- **What**: Extract all magic numbers and configuration into a constants file
+- **Why**: Centralized configuration, easier to tune game behavior
+- **Implementation notes**:
+ - Extract all CAPS constants from main file
+ - Group by category (canvas, physics, timing, spawning)
+ - Export as named constants
+- **Files affected**:
+ - New: `games/crash-course/constants.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - All magic numbers moved to constants
+ - Constants organized by category
+ - Main file imports constants
+ - Game behavior unchanged
+
+**Example:**
+```typescript
+// constants.ts
+export const CANVAS = {
+ WIDTH: 800,
+ HEIGHT: 600,
+} as const;
+
+export const PHYSICS = {
+ GROUND_Y: 450,
+ SCROLL_SPEED: 2,
+ JUMP_HEIGHT: 140,
+ JUMP_DURATION: 1000,
+} as const;
+
+export const GAME_CONFIG = {
+ DURATION: 300000, // 5 minutes
+ OBSTACLE_SPAWN_INTERVAL: 5000,
+ COOL_MODE_DURATION: 2000,
+ LAMP_SPACING: 500,
+} as const;
+```
+
+### Task 2: Extract Drawing Utilities
+- **What**: Move all canvas drawing functions to utilities file
+- **Why**: Separates rendering logic from game logic
+- **Implementation notes**:
+ - Extract drawGame function and helper drawing code
+ - Create functions for: drawBackground, drawCharacter, drawObstacles, drawAlien, drawLamps
+ - Accept canvas context and necessary state as parameters
+ - Keep pure functions (no side effects)
+- **Files affected**:
+ - New: `games/crash-course/utils/drawing.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - All drawing code in utilities
+ - Functions are pure (testable)
+ - Game renders identically
+ - Main file 200-300 lines shorter
+
+**Example:**
+```typescript
+// utils/drawing.ts
+export function drawBackground(
+ ctx: CanvasRenderingContext2D,
+ spriteImages: Map,
+ bgOffsets: number[],
+): void {
+ // Background drawing logic
+}
+
+export function drawCharacter(
+ ctx: CanvasRenderingContext2D,
+ spriteImages: Map,
+ characterState: CharacterState,
+ frame: number,
+ x: number,
+ y: number,
+ shake: {x: number; y: number},
+): void {
+ // Character drawing logic
+}
+```
+
+### Task 3: Extract Physics Utilities
+- **What**: Move physics calculations to utilities file
+- **Why**: Makes physics logic testable and reusable
+- **Implementation notes**:
+ - Extract jump calculation logic
+ - Extract collision detection logic
+ - Extract parallax calculation logic
+ - Create pure functions that return new values
+- **Files affected**:
+ - New: `games/crash-course/utils/physics.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Physics calculations extracted
+ - Functions are pure and testable
+ - Game physics unchanged
+ - Unit tests can be added easily
+
+**Example:**
+```typescript
+// utils/physics.ts
+export function calculateJumpY(
+ startTime: number,
+ currentTime: number,
+ groundY: number,
+ jumpHeight: number,
+ jumpDuration: number,
+): {y: number; isComplete: boolean} {
+ const jumpProgress = currentTime - startTime;
+ if (jumpProgress >= jumpDuration) {
+ return {y: groundY, isComplete: true};
+ }
+
+ const progress = jumpProgress / jumpDuration;
+ const height = Math.sin(progress * Math.PI) * jumpHeight;
+ return {y: groundY - height, isComplete: false};
+}
+
+export function checkCollision(
+ obstacleX: number,
+ obstacleWidth: number,
+ collisionZoneX: number,
+ characterX: number,
+): boolean {
+ return obstacleX < collisionZoneX && obstacleX + obstacleWidth > characterX;
+}
+```
+
+### Task 4: Create useSpriteLoader Hook
+- **What**: Extract sprite loading logic into a custom hook
+- **Why**: Reusable image loading, cleaner separation
+- **Implementation notes**:
+ - Create hook that loads all game images
+ - Return {images: Map, isLoaded: boolean}
+ - Handle loading errors gracefully
+ - Use useEffect for loading
+- **Files affected**:
+ - New: `games/crash-course/hooks/useSpriteLoader.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Hook loads all sprites
+ - Returns loaded state
+ - Handles errors
+ - Main file simplified
+
+**Example:**
+```typescript
+// hooks/useSpriteLoader.ts
+export function useSpriteLoader(): {
+ images: Map;
+ isLoaded: boolean;
+} {
+ const [images, setImages] = useState(new Map());
+ const [isLoaded, setIsLoaded] = useState(false);
+
+ useEffect(() => {
+ // Image loading logic
+ }, []);
+
+ return {images, isLoaded};
+}
+```
+
+### Task 5: Create useAudioManager Hook
+- **What**: Extract audio playback logic into a custom hook
+- **Why**: Centralizes audio management, easier to debug
+- **Implementation notes**:
+ - Create hook that manages all 4 audio tracks
+ - Handle play/pause/stop/volume
+ - Handle music transitions (tedox → neon owl)
+ - Accept gameState and isMuted as inputs
+ - Return toggleMute function
+- **Files affected**:
+ - New: `games/crash-course/hooks/useAudioManager.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Audio plays correctly per game state
+ - Transitions work smoothly
+ - Mute/unmute works
+ - Cleanup on unmount
+
+**Example:**
+```typescript
+// hooks/useAudioManager.ts
+export function useAudioManager(
+ gameState: GameState,
+ isMuted: boolean,
+): {
+ toggleMute: () => void;
+} {
+ const menuAudioRef = useRef(null);
+ const gameAudioRef = useRef(null);
+ // ... other refs
+
+ useEffect(() => {
+ // Setup audio
+ return () => {
+ // Cleanup
+ };
+ }, []);
+
+ useEffect(() => {
+ // Handle audio based on gameState and isMuted
+ }, [gameState, isMuted]);
+
+ const toggleMute = useCallback(() => {
+ // Toggle logic
+ }, []);
+
+ return {toggleMute};
+}
+```
+
+### Task 6: Create useGameTimer Hook
+- **What**: Extract game time tracking into a hook
+- **Why**: Separates time management from game loop
+- **Implementation notes**:
+ - Track elapsed time from game start
+ - Calculate display time (11:55:00 → 00:00:00)
+ - Check for victory condition (5 minutes elapsed)
+ - Return {gameTime: string, isComplete: boolean}
+- **Files affected**:
+ - New: `games/crash-course/hooks/useGameTimer.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Timer updates correctly
+ - Victory detected at 5 minutes
+ - Time display formatted correctly
+ - Hook is reusable
+
+**Example:**
+```typescript
+// hooks/useGameTimer.ts
+export function useGameTimer(
+ gameStartTime: number,
+ isPlaying: boolean,
+): {
+ gameTime: string;
+ isComplete: boolean;
+} {
+ const [gameTime, setGameTime] = useState("11:55:00");
+ const [isComplete, setIsComplete] = useState(false);
+
+ useEffect(() => {
+ if (!isPlaying) return;
+
+ const interval = setInterval(() => {
+ const elapsed = Date.now() - gameStartTime;
+ // Calculate and update time
+ }, 100);
+
+ return () => clearInterval(interval);
+ }, [gameStartTime, isPlaying]);
+
+ return {gameTime, isComplete};
+}
+```
+
+### Task 7: Extract Game Loop Core Logic
+- **What**: Create a hook for the main game loop
+- **Why**: Most complex piece, needs careful extraction
+- **Implementation notes**:
+ - Create useGameLoop hook
+ - Accept necessary state and refs as parameters
+ - Handle: obstacle movement, collision detection, spawning, animation updates
+ - Return updated state values
+ - Use requestAnimationFrame internally
+ - Keep drawGame call separate (rendering stays in component)
+- **Files affected**:
+ - New: `games/crash-course/hooks/useGameLoop.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Game loop runs at 60fps
+ - All game mechanics work
+ - State updates correctly
+ - No performance regression
+
+**Example:**
+```typescript
+// hooks/useGameLoop.ts
+export function useGameLoop(
+ gameState: GameState,
+ canvasRef: React.RefObject,
+ spriteImages: Map,
+ // ... other parameters
+): {
+ obstacles: Obstacle[];
+ walkFrame: number;
+ characterY: number;
+ // ... other game state
+} {
+ // Game loop implementation
+ // Uses useEffect with requestAnimationFrame
+ // Updates and returns game state
+}
+```
+
+### Task 8: Create Obstacle Management Utilities
+- **What**: Extract obstacle-related functions to utilities
+- **Why**: Obstacle logic is complex and can be isolated
+- **Implementation notes**:
+ - Keep createObstacle from crash-course-utils
+ - Add: updateObstaclePositions, checkObstacleCollisions, removeOffscreenObstacles
+ - Add: findCurrentObstacle
+ - Pure functions that operate on obstacle arrays
+- **Files affected**:
+ - Modified: `games/crash-course/crash-course-utils.ts`
+ - New: `games/crash-course/utils/obstacles.ts` (or extend existing utils)
+- **Acceptance criteria**:
+ - Obstacle functions extracted
+ - Functions are pure and testable
+ - Game obstacle behavior unchanged
+
+**Example:**
+```typescript
+// utils/obstacles.ts
+export function updateObstaclePositions(
+ obstacles: Obstacle[],
+ scrollSpeed: number,
+ currentTime: number,
+): Obstacle[] {
+ return obstacles.map((obs) => {
+ let speed = scrollSpeed;
+ if (obs.racing && obs.racingStartTime) {
+ const racingDuration = currentTime - obs.racingStartTime;
+ const acceleration = Math.min(racingDuration / 500, 5);
+ speed = scrollSpeed * (1 + acceleration * 2);
+ }
+ return {...obs, x: obs.x - speed};
+ });
+}
+
+export function removeOffscreenObstacles(
+ obstacles: Obstacle[],
+): Obstacle[] {
+ return obstacles.filter((obs) => obs.x + obs.width >= 0);
+}
+```
+
+### Task 9: Create Alien Animation Utilities
+- **What**: Extract alien behavior logic to utilities
+- **Why**: Alien has complex animation states (floating, blinking, abducting)
+- **Implementation notes**:
+ - Create functions for: calculateAlienPosition, updateAlienBlinkState
+ - Handle floating motion calculations
+ - Handle blink timing and state transitions
+ - Handle abduction and return animations
+- **Files affected**:
+ - New: `games/crash-course/utils/alien.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Alien behavior extracted
+ - Animations work identically
+ - Functions are testable
+
+### Task 10: Create Hooks Directory and Index
+- **What**: Organize all hooks with barrel export
+- **Why**: Clean imports, clear organization
+- **Implementation notes**:
+ - Create `games/crash-course/hooks/` directory
+ - Move all hook files into hooks/
+ - Create index.ts with re-exports
+ - Update imports in main file
+- **Files affected**:
+ - New: `games/crash-course/hooks/index.ts`
+ - All hook files in hooks/
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - All hooks in hooks/ folder
+ - Barrel export works
+ - Imports simplified
+
+### Task 11: Create Utils Directory and Index
+- **What**: Organize all utilities with barrel export
+- **Why**: Clean organization, discoverable utilities
+- **Implementation notes**:
+ - Create `games/crash-course/utils/` directory if not exists
+ - Organize: drawing.ts, physics.ts, obstacles.ts, alien.ts
+ - Create index.ts with re-exports
+ - Move crash-course-utils.ts content to appropriate files
+- **Files affected**:
+ - New: `games/crash-course/utils/index.ts`
+ - All utility files in utils/
+ - Modified imports
+- **Acceptance criteria**:
+ - All utilities organized
+ - Barrel export works
+ - No duplicate code
+
+### Task 12: Update Main Component to Use Hooks
+- **What**: Replace inline logic with hook calls
+- **Why**: Dramatically reduces main file complexity
+- **Implementation notes**:
+ - Replace sprite loading useEffect with useSpriteLoader
+ - Replace audio useEffects with useAudioManager
+ - Replace game loop useEffect with useGameLoop
+ - Replace timer logic with useGameTimer
+ - Pass hook results to components
+- **Files affected**:
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Main file reduced to ~600-800 lines
+ - All hooks integrated
+ - Game plays identically
+ - No functionality lost
+
+## Technical Details
+
+### Hook Organization
+
+```
+games/crash-course/
+├── hooks/
+│ ├── useSpriteLoader.ts (~80 lines)
+│ ├── useAudioManager.ts (~150 lines)
+│ ├── useGameTimer.ts (~50 lines)
+│ ├── useGameLoop.ts (~300-400 lines)
+│ └── index.ts (~5 lines)
+├── utils/
+│ ├── drawing.ts (~200 lines)
+│ ├── physics.ts (~100 lines)
+│ ├── obstacles.ts (~150 lines)
+│ ├── alien.ts (~100 lines)
+│ └── index.ts (~5 lines)
+├── constants.ts (~50 lines)
+└── crash-course.stories.tsx (~600-800 lines, down from 1200)
+```
+
+### Main Component After Extraction
+
+```typescript
+// crash-course.stories.tsx (simplified)
+const CrashCourseGame = (): React.ReactElement => {
+ const canvasRef = useRef(null);
+ const itemRendererRef = useRef(null);
+
+ // Basic state
+ const [gameState, setGameState] = useState("start");
+ const [storyPage, setStoryPage] = useState(1);
+ const [isMuted, setIsMuted] = useState(false);
+
+ // Custom hooks
+ const {images: spriteImages, isLoaded} = useSpriteLoader();
+ const {toggleMute} = useAudioManager(gameState, isMuted);
+ const {gameTime, isComplete: timeUp} = useGameTimer(gameStartTime, gameState === "playing");
+ const gameLoopState = useGameLoop(
+ gameState,
+ canvasRef,
+ spriteImages,
+ // ... other params
+ );
+
+ // Handle time up
+ useEffect(() => {
+ if (timeUp) setGameState("victory");
+ }, [timeUp]);
+
+ // Render
+ return (
+
+
+
+
+ setIsMuted(!isMuted)}
+ />
+
+ {gameState === "start" && (
+
+ )}
+
+ {gameState === "playing" && (
+ <>
+
+ {currentObstacle && }
+ >
+ )}
+
+ {/* Other game states */}
+
+
+ );
+};
+```
+
+## Testing Considerations
+
+After this phase:
+1. **Functionality Testing**: Game must play identically
+2. **Performance Testing**: Verify 60fps maintained
+3. **Hook Testing**: Hooks can be tested in isolation
+4. **Utility Testing**: Utils can be unit tested
+5. **Regression Testing**: Full game playthrough
+
+## Potential Issues
+
+### Hook Dependencies
+- **Issue**: Complex dependency arrays in useEffect
+- **Mitigation**: Use useCallback/useMemo, careful dependency management
+- **How to detect**: Infinite re-render loops, stale closures
+
+### Game Loop Performance
+- **Issue**: Hook extraction might impact performance
+- **Mitigation**: Profile before/after, use refs for values needed in game loop
+- **How to detect**: FPS drops, stuttering animation
+
+### State Synchronization
+- **Issue**: Hooks updating state independently might cause conflicts
+- **Mitigation**: Careful state design, consider useReducer for complex state
+- **How to detect**: Inconsistent state, race conditions
+
+### requestAnimationFrame Cleanup
+- **Issue**: Animation frame must be canceled properly
+- **Mitigation**: Return cleanup function from useEffect
+- **How to detect**: Memory leaks, continued animation after unmount
+
+## Benefits After This Phase
+
+- Main component reduced from 1200 to ~600-800 lines
+- Game logic testable in isolation
+- Hooks are reusable
+- Utilities can be unit tested
+- Clear separation of concerns
+- Easier to debug and maintain
+- Foundation for Phase 4 optimization
diff --git a/PHASE_4_state_management.md b/PHASE_4_state_management.md
new file mode 100644
index 00000000000..97c25b0bf2d
--- /dev/null
+++ b/PHASE_4_state_management.md
@@ -0,0 +1,466 @@
+# Phase 4: Refactor State Management
+
+**Part of**: [CRASH_COURSE_IMPLEMENTATION_PLAN.md](./CRASH_COURSE_IMPLEMENTATION_PLAN.md)
+
+**Goal**: Simplify the dual state/ref pattern, consolidate related state, and optimize re-renders.
+
+## Tasks
+
+### Task 1: Audit Current State Usage
+- **What**: Document all state variables and their usage patterns
+- **Why**: Understand what can be consolidated before making changes
+- **Implementation notes**:
+ - List all useState variables (currently 30+)
+ - List all useRef variables
+ - Identify which state has corresponding refs
+ - Identify which state is used only in game loop
+ - Identify which state triggers renders
+ - Create a spreadsheet or document mapping state usage
+- **Files affected**:
+ - Documentation only (or comments in code)
+- **Acceptance criteria**:
+ - Complete inventory of state/refs
+ - Clear understanding of duplication
+ - Identified candidates for consolidation
+
+### Task 2: Consolidate Jump-Related State
+- **What**: Merge jump state and refs into single source of truth
+- **Why**: Currently has both isJumping/isJumpingRef, jumpStartTime/jumpStartTimeRef, characterY/characterYRef
+- **Implementation notes**:
+ - Keep refs for values needed in game loop (performance)
+ - Remove duplicate state variables where possible
+ - Use refs as source of truth, derive display state if needed
+ - Or use state and update in batches
+- **Files affected**:
+ - Modified: `games/crash-course/hooks/useGameLoop.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Reduced from 6 jump-related variables to 3-4
+ - No duplicate tracking
+ - Jump behavior unchanged
+
+**Before:**
+```typescript
+const [isJumping, setIsJumping] = useState(false);
+const [jumpStartTime, setJumpStartTime] = useState(0);
+const [characterY, setCharacterY] = useState(GROUND_Y);
+const isJumpingRef = useRef(false);
+const jumpStartTimeRef = useRef(0);
+const characterYRef = useRef(GROUND_Y);
+```
+
+**After (Option 1 - Ref-only):**
+```typescript
+const jumpStateRef = useRef({
+ isJumping: false,
+ startTime: 0,
+ y: GROUND_Y,
+});
+// Derive display state when needed for UI
+const [characterY, setCharacterY] = useState(GROUND_Y);
+```
+
+**After (Option 2 - Grouped State):**
+```typescript
+const [jumpState, setJumpState] = useState({
+ isJumping: false,
+ startTime: 0,
+ y: GROUND_Y,
+});
+// Only ref for game loop access
+const jumpStateRef = useRef(jumpState);
+useEffect(() => {
+ jumpStateRef.current = jumpState;
+}, [jumpState]);
+```
+
+### Task 3: Consolidate Alien Animation State
+- **What**: Merge alien-related state variables
+- **Why**: Currently has 10+ alien-related state/ref pairs
+- **Implementation notes**:
+ - Group alien state: frame, time, blinkState, blinkTimer, isAbducting, isReturning, isFlyingAway, etc.
+ - Create AlienState type
+ - Use single state object with ref for game loop
+ - Reduce variable count significantly
+- **Files affected**:
+ - New type in: `games/crash-course/types.ts`
+ - Modified: `games/crash-course/hooks/useGameLoop.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Alien state consolidated into 1-2 variables
+ - Alien animations work identically
+ - Code is clearer
+
+**Before:**
+```typescript
+const [alienFrame, setAlienFrame] = useState(1);
+const [alienTime, setAlienTime] = useState(0);
+const [alienBlinkState, setAlienBlinkState] = useState("idle");
+const [isAlienAbducting, setIsAlienAbducting] = useState(false);
+const [isAlienReturning, setIsAlienReturning] = useState(false);
+const [isAlienFlyingAway, setIsAlienFlyingAway] = useState(false);
+// ... plus corresponding refs
+```
+
+**After:**
+```typescript
+type AlienState = {
+ frame: number;
+ time: number;
+ blinkState: "idle" | "blink1" | "blink2";
+ blinkTimer: number;
+ mode: "floating" | "abducting" | "returning" | "flyingAway";
+ returnStartTime?: number;
+ returnStartPos?: {x: number; y: number};
+ flyAwayStartTime?: number;
+};
+
+const [alienState, setAlienState] = useState({
+ frame: 1,
+ time: 0,
+ blinkState: "idle",
+ blinkTimer: 2000,
+ mode: "floating",
+});
+const alienStateRef = useRef(alienState);
+```
+
+### Task 4: Consolidate Visual Effect State
+- **What**: Group shake, parallax, and other visual effects
+- **Why**: These are related rendering concerns
+- **Implementation notes**:
+ - Group: shakeOffset, isShaking, parallaxOffsets
+ - Create VisualEffectsState type
+ - Single state object for all visual effects
+ - Simplifies render logic
+- **Files affected**:
+ - New type in: `games/crash-course/types.ts`
+ - Modified: `games/crash-course/hooks/useGameLoop.ts`
+ - Modified: `games/crash-course/utils/drawing.ts`
+- **Acceptance criteria**:
+ - Visual effects grouped
+ - Rendering unchanged
+ - Clearer code organization
+
+**After:**
+```typescript
+type VisualEffectsState = {
+ shake: {x: number; y: number};
+ isShaking: boolean;
+ parallaxOffsets: number[];
+};
+
+const [visualEffects, setVisualEffects] = useState({
+ shake: {x: 0, y: 0},
+ isShaking: false,
+ parallaxOffsets: [0, 0, 0, 0, 0],
+});
+```
+
+### Task 5: Use useReducer for Game State
+- **What**: Convert complex game state to useReducer pattern
+- **Why**: Complex state transitions are easier to manage with reducer
+- **Implementation notes**:
+ - Create game reducer with actions: START_GAME, SPAWN_OBSTACLE, ANSWER_CORRECT, ANSWER_INCORRECT, etc.
+ - Move state update logic into reducer
+ - Centralize state transitions
+ - Makes state changes predictable and testable
+- **Files affected**:
+ - New: `games/crash-course/hooks/useGameReducer.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Complex state managed by reducer
+ - State transitions clearer
+ - Game behavior unchanged
+ - Easier to debug state changes
+
+**Example:**
+```typescript
+// hooks/useGameReducer.ts
+type GameAction =
+ | {type: "START_GAME"}
+ | {type: "NEXT_STORY_PAGE"}
+ | {type: "SPAWN_OBSTACLE"; obstacle: Obstacle}
+ | {type: "ANSWER_CORRECT"; obstacleId: string; points: number}
+ | {type: "ANSWER_INCORRECT"}
+ | {type: "LOSE_LIFE"}
+ | {type: "GAME_OVER"}
+ | {type: "VICTORY"};
+
+type GameReducerState = {
+ gameState: GameState;
+ storyPage: number;
+ score: number;
+ lives: number;
+ obstacles: Obstacle[];
+ currentObstacle: Obstacle | null;
+};
+
+function gameReducer(
+ state: GameReducerState,
+ action: GameAction,
+): GameReducerState {
+ switch (action.type) {
+ case "START_GAME":
+ return {
+ ...state,
+ gameState: "story",
+ storyPage: 1,
+ score: 0,
+ lives: 3,
+ obstacles: [],
+ };
+ case "ANSWER_CORRECT":
+ return {
+ ...state,
+ score: state.score + action.points,
+ obstacles: state.obstacles.map((obs) =>
+ obs.id === action.obstacleId
+ ? {...obs, answered: true, correct: true}
+ : obs,
+ ),
+ };
+ // ... other cases
+ default:
+ return state;
+ }
+}
+
+export function useGameReducer() {
+ return useReducer(gameReducer, initialState);
+}
+```
+
+### Task 6: Optimize Re-renders with useMemo/useCallback
+- **What**: Memoize expensive calculations and callback functions
+- **Why**: Prevent unnecessary re-renders, improve performance
+- **Implementation notes**:
+ - Identify expensive calculations (e.g., obstacle distance calculations)
+ - Wrap in useMemo with proper dependencies
+ - Wrap callbacks in useCallback to prevent re-creation
+ - Profile before/after to verify improvement
+- **Files affected**:
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+ - Modified: `games/crash-course/hooks/useGameLoop.ts`
+- **Acceptance criteria**:
+ - Key calculations memoized
+ - Callbacks stable across renders
+ - Measurable performance improvement
+ - No stale closure bugs
+
+**Example:**
+```typescript
+// Memoize obstacle distance calculation
+const currentObstacleDistance = useMemo(() => {
+ if (!currentObstacle) return Infinity;
+ const liveObstacle = obstacles.find((o) => o.id === currentObstacle.id);
+ return liveObstacle ? liveObstacle.x - COLLISION_ZONE_X : Infinity;
+}, [currentObstacle, obstacles]);
+
+// Stable callback
+const handleCheckAnswer = useCallback(() => {
+ if (!currentObstacle) return;
+ // ... answer checking logic
+}, [currentObstacle, /* other dependencies */]);
+```
+
+### Task 7: Extract Separate State Hooks
+- **What**: Create custom hooks for independent state domains
+- **Why**: Further separation of concerns
+- **Implementation notes**:
+ - Create useCharacterState hook (position, state, animation)
+ - Create useObstacleManager hook (spawning, movement, collision)
+ - Create useAlienBehavior hook (animation, abduction, return)
+ - Each hook manages its own domain
+- **Files affected**:
+ - New: `games/crash-course/hooks/useCharacterState.ts`
+ - New: `games/crash-course/hooks/useObstacleManager.ts`
+ - New: `games/crash-course/hooks/useAlienBehavior.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - State organized by domain
+ - Hooks are focused and testable
+ - Main component cleaner
+ - Game works identically
+
+### Task 8: Remove Unnecessary State Synchronization
+- **What**: Eliminate redundant state updates and synchronization
+- **Why**: Reduces complexity and potential bugs
+- **Implementation notes**:
+ - Identify where same value is stored in both state and ref
+ - Choose one source of truth (usually ref for game loop, state for rendering)
+ - Remove synchronization code
+ - Use derived values where possible
+- **Files affected**:
+ - Modified: `games/crash-course/hooks/useGameLoop.ts`
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - No redundant updates
+ - Simpler code
+ - Game behavior unchanged
+
+### Task 9: Implement Batch State Updates
+- **What**: Batch multiple setState calls into single update
+- **Why**: Reduces re-renders, improves performance
+- **Implementation notes**:
+ - Use React 18 automatic batching
+ - Group related setState calls
+ - Use setState callback form for dependent updates
+ - Consider using unstable_batchedUpdates if needed (older React)
+- **Files affected**:
+ - Modified: `games/crash-course/hooks/useGameLoop.ts`
+- **Acceptance criteria**:
+ - Multiple updates batched
+ - Fewer re-renders
+ - Better performance
+
+**Example:**
+```typescript
+// Before: Multiple separate updates
+setScore(score + 10);
+setCharacterState("coolMode");
+setCoolModeEndTime(Date.now() + 2000);
+setAnswerFeedback({show: true, correct: true, message: "Correct!"});
+
+// After: Batched (React 18 does this automatically)
+// Or use a single state update with reducer
+dispatch({
+ type: "ANSWER_CORRECT",
+ points: 10,
+ feedback: "Correct!",
+});
+```
+
+### Task 10: Add State Performance Monitoring
+- **What**: Add dev-mode logging to track state updates
+- **Why**: Helps identify performance issues during development
+- **Implementation notes**:
+ - Use React DevTools Profiler
+ - Add useEffect with logging in dev mode
+ - Track render counts for expensive components
+ - Add performance marks for game loop
+- **Files affected**:
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+ - Modified: `games/crash-course/hooks/useGameLoop.ts`
+- **Acceptance criteria**:
+ - Can identify excessive re-renders
+ - Performance data available in dev mode
+ - No performance impact in production
+
+**Example:**
+```typescript
+if (process.env.NODE_ENV === "development") {
+ useEffect(() => {
+ console.log("Component rendered", {
+ gameState,
+ obstacles: obstacles.length,
+ score,
+ });
+ });
+}
+```
+
+## Technical Details
+
+### State Organization After Phase 4
+
+```typescript
+// Main component state structure
+const CrashCourseGame = () => {
+ // Core game state (managed by reducer)
+ const [gameState, dispatch] = useGameReducer();
+
+ // UI state
+ const [storyPage, setStoryPage] = useState(1);
+ const [isMuted, setIsMuted] = useState(false);
+ const [benevolenceMessage, setBenevolenceMessage] = useState(false);
+
+ // Domain-specific hooks
+ const characterState = useCharacterState(gameState);
+ const obstacles = useObstacleManager(gameState);
+ const alienState = useAlienBehavior(gameState, characterState);
+
+ // Visual effects (grouped)
+ const [visualEffects, setVisualEffects] = useState({
+ shake: {x: 0, y: 0},
+ isShaking: false,
+ parallaxOffsets: [0, 0, 0, 0, 0],
+ });
+
+ // Infrastructure hooks
+ const {images, isLoaded} = useSpriteLoader();
+ const {toggleMute} = useAudioManager(gameState.gameState, isMuted);
+ const {gameTime, isComplete} = useGameTimer(gameStartTime, gameState.gameState === "playing");
+
+ // Game loop (orchestrates everything)
+ useGameLoop({
+ gameState,
+ characterState,
+ obstacles,
+ alienState,
+ visualEffects,
+ dispatch,
+ });
+
+ // Render...
+};
+```
+
+### Before vs After Comparison
+
+| Metric | Before Phase 4 | After Phase 4 |
+|--------|----------------|---------------|
+| State variables | 30+ | 10-15 |
+| Ref variables | 20+ | 5-10 |
+| Duplicate state/ref | 15+ pairs | 0-3 pairs |
+| Main component lines | 600-800 | 400-500 |
+| State updates per frame | 10-15 | 3-5 (batched) |
+| Re-renders per frame | 5-10 | 1-2 |
+
+## Testing Considerations
+
+After this phase:
+1. **Performance Testing**:
+ - Measure FPS before/after
+ - Check render counts with React DevTools Profiler
+ - Verify no performance regression
+2. **Functionality Testing**:
+ - Full playthrough
+ - All game mechanics work
+ - State transitions correct
+3. **State Debugging**:
+ - Use React DevTools to inspect state
+ - Verify state structure is clearer
+ - Check for memory leaks
+
+## Potential Issues
+
+### Over-optimization
+- **Issue**: Premature optimization can make code harder to read
+- **Mitigation**: Only optimize where there's measurable benefit
+- **How to detect**: Code is complex but performance unchanged
+
+### Stale Closures
+- **Issue**: useMemo/useCallback can capture stale values
+- **Mitigation**: Careful dependency management, ESLint rules
+- **How to detect**: Unexpected behavior, old values being used
+
+### useReducer Complexity
+- **Issue**: Reducer can become large and complex
+- **Mitigation**: Split into multiple reducers or use context
+- **How to detect**: Reducer file > 300 lines
+
+### State Synchronization Bugs
+- **Issue**: Removing synchronization might break things
+- **Mitigation**: Thorough testing, careful refactoring
+- **How to detect**: State inconsistencies, race conditions
+
+## Benefits After This Phase
+
+- Cleaner, more maintainable state management
+- Better performance (fewer re-renders)
+- Easier to debug (centralized state logic)
+- More testable (reducers are pure functions)
+- Foundation for future enhancements
+- Clear separation of state concerns
+- Reduced cognitive load
diff --git a/PHASE_5_documentation.md b/PHASE_5_documentation.md
new file mode 100644
index 00000000000..be48d76a7f7
--- /dev/null
+++ b/PHASE_5_documentation.md
@@ -0,0 +1,578 @@
+# Phase 5: Documentation and Polish
+
+**Part of**: [CRASH_COURSE_IMPLEMENTATION_PLAN.md](./CRASH_COURSE_IMPLEMENTATION_PLAN.md)
+
+**Goal**: Add comprehensive documentation, finalize naming, verify everything works, and clean up.
+
+## Tasks
+
+### Task 1: Create Main README
+- **What**: Write comprehensive README for the crash-course game
+- **Why**: Documents purpose, architecture, how to run, and how to extend
+- **Implementation notes**:
+ - Create README.md in games/crash-course/
+ - Include: overview, features, how to play, architecture, development guide
+ - Add ASCII architecture diagram
+ - Link to Perseus documentation
+ - Document known issues/limitations
+- **Files affected**:
+ - New: `games/crash-course/README.md`
+- **Acceptance criteria**:
+ - README covers all key aspects
+ - Clear instructions for developers
+ - Architecture diagram included
+ - Well-formatted and readable
+
+**README Outline:**
+```markdown
+# Crash Course
+
+An educational endless runner game demonstrating Perseus widget integration.
+
+## Overview
+[What it is, purpose, proof of concept status]
+
+## Features
+[List of game features]
+
+## How to Play
+[User instructions]
+
+## How to Run
+[Developer instructions]
+
+## Architecture
+[Component breakdown, data flow, hooks]
+[ASCII diagram]
+
+## Project Structure
+[Directory tree with explanations]
+
+## Key Technologies
+[React, Perseus, Canvas, etc.]
+
+## Development Guide
+[How to modify, extend, debug]
+
+## Testing
+[How to test]
+
+## Known Issues
+[Current limitations]
+
+## Future Enhancements
+[Ideas for improvement]
+
+## Credits
+[Asset sources, music credits]
+```
+
+### Task 2: Add JSDoc Comments to Functions
+- **What**: Add JSDoc documentation to all exported functions and hooks
+- **Why**: Improves discoverability and IDE support
+- **Implementation notes**:
+ - Document all exported functions in utils/
+ - Document all custom hooks
+ - Include: description, parameters, return values, examples
+ - Use TypeScript types in JSDoc
+- **Files affected**:
+ - Modified: All files in `games/crash-course/utils/`
+ - Modified: All files in `games/crash-course/hooks/`
+ - Modified: `games/crash-course/components/` files
+- **Acceptance criteria**:
+ - All public APIs documented
+ - JSDoc shows in IDE tooltips
+ - Examples included where helpful
+
+**Example:**
+```typescript
+/**
+ * Calculates the character's Y position during a jump using a parabolic arc.
+ *
+ * The jump follows a sine wave pattern for smooth, natural-looking motion.
+ *
+ * @param startTime - When the jump started (timestamp)
+ * @param currentTime - Current time (timestamp)
+ * @param groundY - Y coordinate of the ground
+ * @param jumpHeight - Maximum height of the jump in pixels
+ * @param jumpDuration - Total duration of the jump in milliseconds
+ * @returns Object containing the current Y position and whether jump is complete
+ *
+ * @example
+ * ```typescript
+ * const {y, isComplete} = calculateJumpY(
+ * Date.now() - 500, // Started 500ms ago
+ * Date.now(),
+ * 450, // Ground at y=450
+ * 140, // Jump 140 pixels high
+ * 1000 // 1 second total
+ * );
+ * ```
+ */
+export function calculateJumpY(
+ startTime: number,
+ currentTime: number,
+ groundY: number,
+ jumpHeight: number,
+ jumpDuration: number,
+): {y: number; isComplete: boolean} {
+ // Implementation
+}
+```
+
+### Task 3: Add Inline Code Comments
+- **What**: Add explanatory comments for complex logic
+- **Why**: Makes code easier to understand and maintain
+- **Implementation notes**:
+ - Add comments for non-obvious logic
+ - Explain "why" not "what" (code shows what)
+ - Document complex algorithms (jump physics, collision detection)
+ - Explain magic numbers if any remain
+ - Add TODO comments for known issues
+- **Files affected**:
+ - Modified: All TypeScript files with complex logic
+- **Acceptance criteria**:
+ - Complex sections have explanatory comments
+ - Code intent is clear
+ - Future maintainers can understand logic
+
+**Example:**
+```typescript
+// Calculate parabolic jump arc using sine wave for smooth motion.
+// Progress goes from 0 to 1 over JUMP_DURATION, sine gives us
+// smooth acceleration at start/end of jump.
+const progress = jumpProgress / JUMP_DURATION;
+const jumpHeight = Math.sin(progress * Math.PI) * JUMP_HEIGHT;
+const newY = GROUND_Y - jumpHeight;
+```
+
+### Task 4: Document Component Props
+- **What**: Add comments/documentation for complex component props
+- **Why**: Makes components easier to use
+- **Implementation notes**:
+ - Add JSDoc to component prop types
+ - Document callback signatures
+ - Explain non-obvious prop requirements
+ - Add usage examples in comments
+- **Files affected**:
+ - Modified: All files in `games/crash-course/components/`
+- **Acceptance criteria**:
+ - All props documented
+ - Callback signatures clear
+ - Components easy to use
+
+**Example:**
+```typescript
+/**
+ * Props for the QuestionOverlay component.
+ */
+type QuestionOverlayProps = {
+ /** The current obstacle/question to display */
+ obstacle: Obstacle;
+
+ /**
+ * Remaining distance in pixels before collision.
+ * Used to calculate timer bar progress.
+ */
+ remainingDistance: number;
+
+ /**
+ * Callback fired when user clicks "Check Answer".
+ * Should validate the answer and update game state.
+ */
+ onCheckAnswer: () => void;
+
+ /**
+ * Feedback to display after answer submission.
+ * If show is false, no feedback is displayed.
+ */
+ answerFeedback: {
+ show: boolean;
+ correct: boolean;
+ message: string;
+ };
+};
+```
+
+### Task 5: Update Storybook Story Documentation
+- **What**: Enhance the Storybook story with controls and documentation
+- **Why**: Makes the demo more interactive and informative
+- **Implementation notes**:
+ - Update story metadata with detailed description
+ - Add Storybook controls for configurable parameters (if applicable)
+ - Add links to source code
+ - Document how to use in Storybook
+- **Files affected**:
+ - Modified: `games/crash-course/crash-course.stories.tsx`
+- **Acceptance criteria**:
+ - Story has detailed description
+ - Controls work (if applicable)
+ - Easy to explore in Storybook
+
+**Example:**
+```typescript
+const meta: Meta = {
+ title: "Games/Crash Course",
+ component: CrashCourseGame,
+ parameters: {
+ docs: {
+ description: {
+ component: `
+# Crash Course
+
+An endless runner game showcasing Perseus widget integration in an educational gaming context.
+
+## Features
+- **Educational Content**: Integrates real Perseus math questions
+- **Progressive Difficulty**: Questions spawn every 5 seconds
+- **Time-Based Challenge**: Survive for 5 minutes to win
+- **Multiple Question Types**: Addition, subtraction, multiplication, division
+- **Dynamic Animations**: Parallax backgrounds, alien behaviors, character states
+- **Story Mode**: 7-page narrative introduction
+
+## Technical Highlights
+- Canvas-based rendering at 60fps
+- Custom React hooks for game logic
+- Perseus ServerItemRenderer integration
+- Audio management with multiple tracks
+- Complex state management
+
+## How to Play
+1. Click "Start" on the title screen
+2. Watch the story sequence
+3. Answer math questions to jump over obstacles
+4. Survive until midnight (5 minutes) to win
+
+## Architecture
+See /games/crash-course/README.md for detailed architecture documentation.
+ `,
+ },
+ },
+ layout: "fullscreen",
+ },
+};
+```
+
+### Task 6: Add Architecture Diagram
+- **What**: Create ASCII art or diagram showing component relationships
+- **Why**: Visual aid for understanding the system
+- **Implementation notes**:
+ - Create clear ASCII diagram
+ - Show data flow
+ - Show component hierarchy
+ - Include in README and code comments
+- **Files affected**:
+ - Modified: `games/crash-course/README.md`
+- **Acceptance criteria**:
+ - Diagram is clear and accurate
+ - Shows key relationships
+ - Easy to understand
+
+**Example:**
+```
+┌─────────────────────────────────────────────────────────────┐
+│ CrashCourseGame │
+│ (Main Orchestrator) │
+└─────────────┬───────────────────────────────────────────────┘
+ │
+ ┌─────────┼─────────┬──────────┬──────────┬──────────┐
+ │ │ │ │ │ │
+ ▼ ▼ ▼ ▼ ▼ ▼
+┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
+│Sprite │ │Audio │ │Game │ │Game │ │Character│ │Obstacle│
+│Loader │ │Manager │ │Timer │ │Loop │ │State │ │Manager │
+└────────┘ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘
+ │
+ ┌─────────────┼─────────────┐
+ ▼ ▼ ▼
+ ┌────────┐ ┌────────┐ ┌────────┐
+ │Drawing │ │Physics │ │Collision│
+ │Utils │ │Utils │ │Detection│
+ └────────┘ └────────┘ └────────┘
+
+Game State Flow:
+START → STORY (7 pages) → PLAYING → [VICTORY | CAR_BONUS → GAMEOVER]
+```
+
+### Task 7: Document Known Issues and Limitations
+- **What**: Create a list of current limitations and technical debt
+- **Why**: Transparency about what needs improvement
+- **Implementation notes**:
+ - List performance limitations
+ - Document browser compatibility issues
+ - Note areas that could be improved
+ - Explain proof-of-concept limitations
+- **Files affected**:
+ - Modified: `games/crash-course/README.md`
+ - New: `games/crash-course/TECHNICAL_DEBT.md` (optional)
+- **Acceptance criteria**:
+ - Known issues documented
+ - Workarounds noted
+ - Future work identified
+
+**Example:**
+```markdown
+## Known Issues
+
+### Performance
+- Game loop may stutter on older devices (requires 60fps)
+- Large number of obstacles can impact performance
+- No frame-skipping or adaptive quality
+
+### Browser Compatibility
+- Requires HTML5 Canvas support
+- Audio autoplay blocked until user interaction
+- Not tested in all browsers
+
+### Accessibility
+- No keyboard-only navigation
+- Screen reader support limited
+- No visual settings (contrast, motion reduction)
+
+### Mobile
+- Not optimized for touch controls
+- On-screen keyboard may cover game area
+- Performance varies significantly
+
+### Technical Debt
+- Some duplicate state/ref patterns remain (performance trade-offs)
+- Game loop could be further optimized
+- Asset loading could use better error handling
+- No unit tests yet
+```
+
+### Task 8: Add Development Guide
+- **What**: Document how to modify and extend the game
+- **Why**: Helps future developers work on the codebase
+- **Implementation notes**:
+ - Explain how to add new question types
+ - Document how to modify game parameters
+ - Show how to add new screens/states
+ - Explain testing approach
+- **Files affected**:
+ - Modified: `games/crash-course/README.md`
+- **Acceptance criteria**:
+ - Common modifications documented
+ - Extension points identified
+ - Examples provided
+
+**Example:**
+```markdown
+## Development Guide
+
+### Adding a New Question Type
+
+1. Add generator function in `crash-course-utils.ts`:
+```typescript
+function generateNewTypeQuestion(): PerseusItem {
+ // Create Perseus item
+}
+```
+
+2. Update `generateQuestion()` to include new type:
+```typescript
+const types = ["addition", "subtraction", "multiplication", "division", "newType"];
+```
+
+### Modifying Game Parameters
+
+All game parameters are in `constants.ts`. Common modifications:
+
+- **Game Speed**: Adjust `PHYSICS.SCROLL_SPEED`
+- **Difficulty**: Adjust `GAME_CONFIG.OBSTACLE_SPAWN_INTERVAL`
+- **Game Length**: Adjust `GAME_CONFIG.DURATION`
+
+### Adding a New Screen
+
+1. Create component in `components/NewScreen.tsx`
+2. Add state to GameState type: `type GameState = ... | "newScreen"`
+3. Add rendering in main component:
+```typescript
+{gameState === "newScreen" && }
+```
+```
+
+### Task 9: Verify All Naming is Consistent
+- **What**: Final pass to ensure no "math-blaster" references remain
+- **Why**: Professional polish, consistency
+- **Implementation notes**:
+ - Search codebase for "math-blaster", "mathblaster", "MathBlaster"
+ - Update any remaining references to "crash-course", "CrashCourse"
+ - Check comments, variable names, class names
+ - Update any console.log statements
+- **Files affected**:
+ - Any files with remaining old naming
+- **Acceptance criteria**:
+ - No "math-blaster" references
+ - Consistent "crash-course" naming throughout
+ - Professional appearance
+
+### Task 10: Final Testing and Cleanup
+- **What**: Comprehensive testing and code cleanup
+- **Why**: Ensure everything works before completion
+- **Implementation notes**:
+ - Full playthrough of the game
+ - Test all screens and transitions
+ - Verify assets load correctly
+ - Check audio playback
+ - Run linting: `pnpm lint`
+ - Run type checking: `pnpm tsc`
+ - Test in Storybook
+ - Remove any commented-out code
+ - Remove debug/console.log statements
+ - Remove unused imports
+- **Files affected**:
+ - All files in `games/crash-course/`
+- **Acceptance criteria**:
+ - Game works perfectly
+ - No console errors
+ - Lint passes
+ - Type check passes
+ - No debug code remains
+ - Code is clean and professional
+
+### Task 11: Create Migration Guide
+- **What**: Document the changes made during refactoring
+- **Why**: Helps team understand what changed
+- **Implementation notes**:
+ - Summarize major changes
+ - Document new file structure
+ - Note breaking changes (if any)
+ - Provide before/after comparisons
+- **Files affected**:
+ - New: `games/crash-course/MIGRATION.md`
+- **Acceptance criteria**:
+ - Changes clearly documented
+ - Team can understand refactoring
+ - Before/after comparisons included
+
+**Example:**
+```markdown
+# Migration Guide
+
+This document summarizes the refactoring from math-blaster to crash-course.
+
+## What Changed
+
+### File Organization
+- **Before**: All files in `packages/perseus/src/__docs__/`
+- **After**: Organized in `packages/perseus/src/games/crash-course/`
+
+### Component Structure
+- **Before**: 1 monolithic 1673-line file
+- **After**: 20+ focused files with clear responsibilities
+
+### Asset Organization
+- **Before**: 40+ assets scattered in `__docs__/`
+- **After**: Assets organized in subdirectories by type
+
+### State Management
+- **Before**: 30+ state variables, 20+ refs, heavy duplication
+- **After**: Consolidated state with hooks and reducer pattern
+
+## File Mapping
+
+| Old Location | New Location |
+|--------------|--------------|
+| `__docs__/math-blaster-game.stories.tsx` | `games/crash-course/crash-course.stories.tsx` |
+| `__docs__/math-blaster-utils.ts` | `games/crash-course/crash-course-utils.ts` |
+| ... | ... |
+
+## Breaking Changes
+
+None - this is an internal refactoring. The Storybook story moved from "Math Blaster Game" to "Games/Crash Course".
+```
+
+### Task 12: Optional - Clean Up Original Files
+- **What**: Remove old files from __docs__ once verified
+- **Why**: Prevent confusion, clean up codebase
+- **Implementation notes**:
+ - **ONLY DO THIS AFTER PHASE 5 IS COMPLETE AND TESTED**
+ - Delete math-blaster files from __docs__
+ - Delete asset files that were moved
+ - Update any references
+ - Commit as separate change
+- **Files affected**:
+ - Deleted: All math-blaster files in `__docs__/`
+ - Deleted: All moved asset files
+- **Acceptance criteria**:
+ - Old files removed
+ - No broken references
+ - Game still works
+ - Git history preserved (if using git mv)
+
+## Technical Details
+
+### Documentation Standards
+
+Follow these guidelines:
+- Use Markdown for all documentation
+- Keep paragraphs short (3-4 sentences max)
+- Use code examples liberally
+- Include visual diagrams where helpful
+- Link between documents
+- Use proper heading hierarchy
+- Include table of contents for long docs
+
+### JSDoc Standards
+
+```typescript
+/**
+ * Brief one-line description.
+ *
+ * Longer description if needed. Can span multiple lines.
+ * Explain the "why" and important context.
+ *
+ * @param paramName - Description of parameter
+ * @param anotherParam - Another description
+ * @returns Description of return value
+ * @throws {ErrorType} When this error occurs
+ *
+ * @example
+ * ```typescript
+ * // Usage example
+ * const result = functionName(arg1, arg2);
+ * ```
+ *
+ * @see {@link RelatedFunction} for related functionality
+ */
+```
+
+## Testing Considerations
+
+After this phase:
+1. **Documentation Review**: Have someone else read the docs
+2. **Final Playthrough**: Complete game session
+3. **Storybook Check**: Verify story displays correctly
+4. **Build Verification**: `pnpm build` succeeds
+5. **Link Checking**: All documentation links work
+
+## Benefits After This Phase
+
+- Comprehensive documentation for future developers
+- Clear understanding of system architecture
+- Easy to onboard new contributors
+- Professional, polished codebase
+- Clear path for future enhancements
+- Well-documented APIs and components
+- Project is "complete" and ready for use/extension
+
+## Project Completion Checklist
+
+- [ ] Main README created and comprehensive
+- [ ] All public APIs have JSDoc documentation
+- [ ] Complex logic has inline comments
+- [ ] Component props documented
+- [ ] Storybook story enhanced
+- [ ] Architecture diagram included
+- [ ] Known issues documented
+- [ ] Development guide written
+- [ ] All naming consistent (no "math-blaster")
+- [ ] Full testing completed successfully
+- [ ] Migration guide created
+- [ ] Original files cleaned up (optional)
+- [ ] Lint passes
+- [ ] Type check passes
+- [ ] Game works perfectly in Storybook
+- [ ] Team review completed
+- [ ] Ready for production use
diff --git a/packages/perseus/src/__docs__/car-bonus-scene.module.css b/packages/perseus/src/__docs__/car-bonus-scene.module.css
new file mode 100644
index 00000000000..48ffbb10e2d
--- /dev/null
+++ b/packages/perseus/src/__docs__/car-bonus-scene.module.css
@@ -0,0 +1,142 @@
+@import url('https://fonts.googleapis.com/css2?family=Quantico:wght@400;700&display=swap');
+
+.overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none;
+ background: #000;
+}
+
+.backgroundImage {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.text {
+ position: absolute;
+ top: 20%;
+ left: 50%;
+ transform: translateX(-50%);
+ font-family: 'Quantico', sans-serif;
+ font-size: 48px;
+ font-weight: bold;
+ color: #ffffff;
+ text-shadow: 3px 3px 0px #000000;
+ letter-spacing: 2px;
+ z-index: 10;
+}
+
+.cursor {
+ animation: blink 0.5s infinite;
+}
+
+.carContainer {
+ position: absolute;
+ bottom: 22px; /* Ground level: 600px canvas - 578px ground = 22px (GROUND_Y 450 + CHARACTER_HEIGHT 128) */
+ left: 50%;
+}
+
+.carContainer.sliding {
+ animation: slideInObstacle 0.8s ease-out forwards;
+}
+
+.carContainer.stopped {
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.carImage {
+ width: 256px;
+ height: 256px;
+ image-rendering: pixelated;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: crisp-edges;
+}
+
+@keyframes slideInObstacle {
+ from {
+ left: 100%;
+ transform: translateX(0);
+ }
+ to {
+ left: 50%;
+ transform: translateX(-50%);
+ }
+}
+
+@keyframes blink {
+ 0%,
+ 50% {
+ opacity: 1;
+ }
+ 51%,
+ 100% {
+ opacity: 0;
+ }
+}
+
+.bonusLevelText {
+ position: absolute;
+ top: 30%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-family: 'Quantico', sans-serif;
+ font-size: 72px;
+ font-weight: bold;
+ color: #ffff00;
+ text-shadow: 4px 4px 0px #000000, 0 0 20px #ffff00;
+ letter-spacing: 4px;
+ z-index: 20;
+ white-space: nowrap;
+}
+
+.nevermindText {
+ position: absolute;
+ top: 30%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-family: 'Quantico', sans-serif;
+ font-size: 72px;
+ font-weight: bold;
+ color: #ffff00;
+ text-shadow: 4px 4px 0px #000000, 0 0 20px #ffff00;
+ letter-spacing: 4px;
+ z-index: 20;
+ white-space: nowrap;
+}
+
+.nextButton {
+ position: absolute;
+ bottom: 20px;
+ right: 20px;
+ cursor: pointer;
+ pointer-events: all;
+ transition: transform 0.2s ease;
+ z-index: 30;
+ filter: drop-shadow(0 0 10px rgba(255, 0, 255, 0.5));
+ animation: buttonShine 2s ease-in-out infinite;
+}
+
+.nextButton:hover {
+ transform: scale(1.05);
+ filter: drop-shadow(0 0 20px rgba(255, 0, 255, 0.8));
+}
+
+@keyframes buttonShine {
+ 0%, 100% {
+ filter: drop-shadow(0 0 10px rgba(255, 0, 255, 0.5));
+ }
+ 50% {
+ filter: drop-shadow(0 0 20px rgba(0, 255, 255, 0.8));
+ }
+}
diff --git a/packages/perseus/src/__docs__/car-bonus-scene.stories.tsx b/packages/perseus/src/__docs__/car-bonus-scene.stories.tsx
new file mode 100644
index 00000000000..0cfed786522
--- /dev/null
+++ b/packages/perseus/src/__docs__/car-bonus-scene.stories.tsx
@@ -0,0 +1,40 @@
+import * as React from "react";
+
+import {CarBonusScene} from "./car-bonus-scene";
+
+import type {Meta, StoryObj} from "@storybook/react-vite";
+
+const meta: Meta = {
+ title: "Games/Car Bonus Scene",
+ component: CarBonusScene,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "A comedic bonus scene that plays when the player runs out of lives in Math Blaster. " +
+ "The car self-destructs in a humorous reference to the Street Fighter 2 bonus level.",
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ onComplete: () => {
+ // Car bonus scene completed
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
diff --git a/packages/perseus/src/__docs__/car-bonus-scene.tsx b/packages/perseus/src/__docs__/car-bonus-scene.tsx
new file mode 100644
index 00000000000..5f13520ee2a
--- /dev/null
+++ b/packages/perseus/src/__docs__/car-bonus-scene.tsx
@@ -0,0 +1,202 @@
+import * as React from "react";
+import {useEffect, useRef, useState} from "react";
+
+import bonusGameAudio from "../games/crash-course/assets/audio/bonusgame.wav";
+import bonus1Img from "../games/crash-course/assets/ui/bonus1.png";
+import bonus2Img from "../games/crash-course/assets/ui/bonus2.png";
+import styles from "./car-bonus-scene.module.css";
+import explosionAudio from "../games/crash-course/assets/audio/explosion.wav";
+import nextImg from "../games/crash-course/assets/ui/next.png";
+import skidImg from "../games/crash-course/assets/ui/skid.png";
+import tireSquealAudio from "../games/crash-course/assets/audio/tires_squal_loop.wav";
+
+type BonusState = "skid" | "bonusLevel" | "explosion" | "complete";
+
+type CarBonusSceneProps = {
+ onComplete: () => void;
+};
+
+export const CarBonusScene = ({onComplete}: CarBonusSceneProps) => {
+ const [bonusState, setBonusState] = useState("skid");
+ const [displayText, setDisplayText] = useState("");
+ const [flashVisible, setFlashVisible] = useState(true);
+ const [nevermindFlashVisible, setNevermindFlashVisible] = useState(true);
+ const [showSkidImage, setShowSkidImage] = useState(false);
+
+ const tireSquealRef = useRef(null);
+ const bonusGameRef = useRef(null);
+ const explosionRef = useRef(null);
+ const nevermindIntervalRef = useRef | null>(null);
+
+ useEffect(() => {
+ // Initialize audio
+ const tireSqueal = new Audio(tireSquealAudio);
+ tireSqueal.loop = false;
+ tireSqueal.volume = 0.7;
+ tireSquealRef.current = tireSqueal;
+
+ const bonusGame = new Audio(bonusGameAudio);
+ bonusGame.loop = false;
+ bonusGame.volume = 0.7;
+ bonusGameRef.current = bonusGame;
+
+ const explosion = new Audio(explosionAudio);
+ explosion.loop = false;
+ explosion.volume = 0.7;
+ explosionRef.current = explosion;
+
+ // Sequence timing
+ const timers: ReturnType[] = [];
+
+ // Phase 1a: Black screen + tire squeal (play only 1.5 seconds)
+ tireSqueal.play().catch((error) => {
+ console.log("Tire squeal audio play failed:", error);
+ });
+
+ // Stop tire squeal after 1.5 seconds
+ timers.push(
+ setTimeout(() => {
+ if (tireSquealRef.current) {
+ tireSquealRef.current.pause();
+ tireSquealRef.current.currentTime = 0;
+ }
+ }, 1500),
+ );
+
+ // Phase 1b: Show skid image after black screen
+ timers.push(
+ setTimeout(() => {
+ setShowSkidImage(true);
+ setDisplayText("Oh. I guess it stopped.");
+ }, 1500),
+ );
+
+ // Note: Phase 2 (Bonus Level) is now triggered by user clicking next button on skid screen
+ // No automatic transition from skid to bonus level
+
+ // Cleanup
+ return () => {
+ timers.forEach((timer) => clearTimeout(timer));
+ if (nevermindIntervalRef.current) {
+ clearInterval(nevermindIntervalRef.current);
+ }
+ if (tireSquealRef.current) {
+ tireSquealRef.current.pause();
+ tireSquealRef.current.currentTime = 0;
+ }
+ if (bonusGameRef.current) {
+ bonusGameRef.current.pause();
+ bonusGameRef.current.currentTime = 0;
+ }
+ if (explosionRef.current) {
+ explosionRef.current.pause();
+ explosionRef.current.currentTime = 0;
+ }
+ };
+ }, [onComplete]);
+
+ // Determine which image to show based on state
+ let backgroundImage: string | null = null;
+ if (bonusState === "skid" && showSkidImage) {
+ backgroundImage = skidImg;
+ } else if (bonusState === "bonusLevel") {
+ backgroundImage = bonus1Img;
+ } else if (bonusState === "explosion") {
+ backgroundImage = bonus2Img;
+ }
+
+ const handleSkidNext = () => {
+ // Transition from skid to bonus level
+ setBonusState("bonusLevel");
+ setDisplayText(""); // Clear previous text
+
+ // Start bonus level music
+ if (bonusGameRef.current) {
+ bonusGameRef.current.play().catch((error) => {
+ console.log("Bonus game audio play failed:", error);
+ });
+ }
+
+ // Flash "BONUS LEVEL" continuously for the entire duration
+ const flashInterval = setInterval(() => {
+ setFlashVisible((prev) => !prev);
+ }, 500); // 500ms on, 500ms off
+
+ // After 7 seconds, transition to explosion
+ setTimeout(() => {
+ clearInterval(flashInterval);
+
+ // Abruptly stop bonus music
+ if (bonusGameRef.current) {
+ bonusGameRef.current.pause();
+ bonusGameRef.current.currentTime = 0;
+ }
+
+ // Play explosion
+ if (explosionRef.current) {
+ explosionRef.current.play().catch((error) => {
+ console.log("Explosion audio play failed:", error);
+ });
+ }
+
+ setBonusState("explosion");
+ setFlashVisible(false);
+
+ // Start flashing "NEVERMIND" text
+ nevermindIntervalRef.current = setInterval(() => {
+ setNevermindFlashVisible((prev) => !prev);
+ }, 500);
+ }, 7000); // 7 seconds for bonus level
+ };
+
+ const handleExplosionNext = () => {
+ // Clear the nevermind flashing interval
+ if (nevermindIntervalRef.current) {
+ clearInterval(nevermindIntervalRef.current);
+ }
+ setBonusState("complete");
+ onComplete();
+ };
+
+ return (
+
+ {backgroundImage && (
+

+ )}
+
+ {displayText && (
+
{displayText}
+ )}
+
+ {bonusState === "bonusLevel" && flashVisible && (
+
BONUS LEVEL
+ )}
+
+ {bonusState === "explosion" && nevermindFlashVisible && (
+
NEVERMIND
+ )}
+
+ {bonusState === "skid" && showSkidImage && (
+

+ )}
+
+ {bonusState === "explosion" && (
+

+ )}
+
+ );
+};
diff --git a/packages/perseus/src/__docs__/idle.png b/packages/perseus/src/__docs__/idle.png
new file mode 100644
index 00000000000..8334bba1bb7
Binary files /dev/null and b/packages/perseus/src/__docs__/idle.png differ
diff --git a/packages/perseus/src/__docs__/math-blaster-game.module.css b/packages/perseus/src/__docs__/math-blaster-game.module.css
new file mode 100644
index 00000000000..b8bd66837fe
--- /dev/null
+++ b/packages/perseus/src/__docs__/math-blaster-game.module.css
@@ -0,0 +1,456 @@
+@import url('https://fonts.googleapis.com/css2?family=Quantico:wght@400;700&display=swap');
+
+.gameContainer {
+ position: relative;
+ width: 800px;
+ height: 600px;
+ margin: 20px auto;
+ border: 2px solid #333;
+ background: #87ceeb;
+ overflow: hidden;
+ font-family: 'Quantico', sans-serif;
+}
+
+.gameCanvas {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.muteButton {
+ position: absolute;
+ top: 12px;
+ right: 10px;
+ width: 40px;
+ height: 40px;
+ cursor: pointer;
+ z-index: 100;
+ pointer-events: all;
+ transition: transform 0.2s ease;
+}
+
+.muteButton:hover {
+ transform: scale(1.1);
+}
+
+.hudBackground {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 64px;
+ background: linear-gradient(
+ 180deg,
+ rgba(0, 0, 0, 0.7) 0%,
+ rgba(0, 0, 0, 0.4) 70%,
+ rgba(0, 0, 0, 0) 100%
+ );
+ z-index: 4;
+ pointer-events: none;
+}
+
+.questionOverlay {
+ position: absolute;
+ background: rgba(0, 0, 0, 0.92);
+ border: 3px solid #00ffff;
+ border-radius: 8px;
+ padding: 16px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 0 20px rgba(0, 255, 255, 0.4);
+ max-width: 400px;
+ z-index: 10;
+ color: #fff;
+ pointer-events: all;
+}
+
+.questionOverlay :global(.perseus-widget-container),
+.questionOverlay :global(p),
+.questionOverlay :global(div),
+.questionOverlay :global(label),
+.questionOverlay :global(span),
+.questionOverlay :global(.katex),
+.questionOverlay :global(.katex *) {
+ color: #fff !important;
+}
+
+.hud {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ right: 60px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ z-index: 5;
+ pointer-events: none;
+}
+
+.score {
+ background: rgba(0, 0, 0, 0.85);
+ padding: 10px 16px;
+ border-radius: 4px;
+ font-size: 18px;
+ font-weight: bold;
+ color: #00ffff;
+ font-family: 'Quantico', sans-serif;
+ text-shadow: 0 0 8px #00ffff, 0 0 16px #00ffff;
+ border: 2px solid #00ffff;
+ box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.8), 0 0 10px rgba(0, 255, 255, 0.4);
+ min-height: 44px;
+ display: flex;
+ align-items: center;
+}
+
+.gameTime {
+ background: #1a1a1a;
+ padding: 10px 16px;
+ border-radius: 4px;
+ font-size: 20px;
+ font-weight: bold;
+ color: #ff0000;
+ font-family: "Courier New", "Lucida Console", monospace;
+ letter-spacing: 3px;
+ text-shadow: 0 0 8px #ff0000, 0 0 16px #ff0000;
+ border: 2px solid #333;
+ box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.8), 0 0 8px rgba(255, 0, 0, 0.3);
+ min-height: 44px;
+ display: flex;
+ align-items: center;
+}
+
+.lives {
+ background: rgba(0, 0, 0, 0.85);
+ padding: 10px 16px;
+ border-radius: 4px;
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ font-family: 'Quantico', sans-serif;
+ color: #ff00ff;
+ font-weight: bold;
+ font-size: 18px;
+ text-shadow: 0 0 8px #ff00ff, 0 0 16px #ff00ff;
+ border: 2px solid #ff00ff;
+ box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.8), 0 0 10px rgba(255, 0, 255, 0.4);
+ min-height: 44px;
+}
+
+.alien {
+ font-size: 18px;
+}
+
+.alienLost {
+ opacity: 0.3;
+ text-decoration: line-through;
+}
+
+.gameOver {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #000;
+ color: white !important;
+ text-align: center;
+ z-index: 20;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.gameOverButton {
+ position: absolute;
+ bottom: 20px;
+ right: 20px;
+ cursor: pointer;
+ pointer-events: all;
+ transition: transform 0.2s ease;
+ z-index: 10;
+ filter: drop-shadow(0 0 10px rgba(255, 0, 255, 0.5));
+ animation: buttonShine 2s ease-in-out infinite;
+}
+
+.gameOverButton:hover {
+ transform: scale(1.05);
+ filter: drop-shadow(0 0 20px rgba(255, 0, 255, 0.8));
+}
+
+.startScreen {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #000;
+ color: white !important;
+ text-align: center;
+ z-index: 20;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.startScreen h1 {
+ font-size: 48px;
+ margin: 0 0 20px 0;
+ color: white !important;
+ font-family: 'Quantico', sans-serif;
+}
+
+.startScreen p {
+ font-size: 18px;
+ margin: 10px 0;
+ line-height: 1.5;
+ color: white !important;
+ font-family: 'Quantico', sans-serif;
+}
+
+.startScreen button {
+ margin-top: 20px;
+ padding: 12px 24px;
+ font-size: 18px;
+ background: #4a90e2;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ pointer-events: all;
+ font-family: 'Quantico', sans-serif;
+}
+
+.startScreen button:hover {
+ background: #357abd;
+}
+
+.startButton {
+ position: absolute;
+ bottom: 20px;
+ right: 20px;
+ cursor: pointer;
+ pointer-events: all;
+ transition: transform 0.2s ease;
+ z-index: 10;
+ filter: drop-shadow(0 0 10px rgba(255, 0, 255, 0.5));
+ animation: buttonShine 2s ease-in-out infinite;
+}
+
+.startButton:hover {
+ transform: scale(1.05);
+ filter: drop-shadow(0 0 20px rgba(255, 0, 255, 0.8));
+}
+
+@keyframes buttonShine {
+ 0%, 100% {
+ filter: drop-shadow(0 0 10px rgba(255, 0, 255, 0.5));
+ }
+ 50% {
+ filter: drop-shadow(0 0 20px rgba(0, 255, 255, 0.8));
+ }
+}
+
+.checkButton {
+ margin-top: 12px;
+ padding: 8px 16px;
+ font-size: 16px;
+ background: #00ffff;
+ color: #000;
+ border: 2px solid #00ffff;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: bold;
+ font-family: 'Quantico', sans-serif;
+ box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
+ transition: all 0.2s ease;
+}
+
+.checkButton:hover {
+ background: #00cccc;
+ box-shadow: 0 0 20px rgba(0, 255, 255, 0.8);
+ transform: translateY(-1px);
+}
+
+.checkButton:disabled {
+ background: #333;
+ color: #666;
+ border-color: #333;
+ cursor: not-allowed;
+ box-shadow: none;
+}
+
+.feedback {
+ margin-top: 12px;
+ padding: 12px 16px;
+ border-radius: 4px;
+ font-weight: bold;
+ font-size: 16px;
+ text-align: center;
+ font-family: 'Quantico', sans-serif;
+}
+
+.feedbackCorrect {
+ background: rgba(0, 255, 100, 0.15);
+ color: #00ff66;
+ border: 2px solid #00ff66;
+ text-shadow: 0 0 8px rgba(0, 255, 100, 0.8);
+ box-shadow: 0 0 15px rgba(0, 255, 100, 0.4);
+}
+
+.feedbackIncorrect {
+ background: rgba(255, 0, 100, 0.15);
+ color: #ff0066;
+ border: 2px solid #ff0066;
+ text-shadow: 0 0 8px rgba(255, 0, 100, 0.8);
+ box-shadow: 0 0 15px rgba(255, 0, 100, 0.4);
+ animation: pulse 0.5s ease-in-out;
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.05);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+.debugInfo {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+ background: rgba(0, 0, 0, 0.8);
+ color: #0f0;
+ padding: 10px;
+ border-radius: 4px;
+ font-family: monospace;
+ font-size: 12px;
+ max-width: 400px;
+ z-index: 15;
+ white-space: pre-wrap;
+ word-break: break-all;
+}
+
+.timerContainer {
+ position: relative;
+ width: 100%;
+ height: 24px;
+ background: #e0e0e0;
+ border-radius: 4px;
+ margin-bottom: 12px;
+ overflow: hidden;
+}
+
+.timerBar {
+ height: 100%;
+ transition: width 0.1s linear, background-color 0.3s ease;
+ border-radius: 4px;
+}
+
+.timerText {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-weight: bold;
+ font-size: 14px;
+ color: #000;
+ text-shadow: 0 0 2px rgba(255, 255, 255, 0.8);
+ z-index: 1;
+ font-family: 'Quantico', sans-serif;
+}
+
+.benevolenceMessage {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 72px;
+ font-weight: bold;
+ color: #ff00ff;
+ text-shadow: 0 0 20px rgba(255, 255, 255, 0.8), 0 0 40px
+ rgba(255, 0, 255, 0.6);
+ z-index: 30;
+ animation: benevolencePulse 2s ease-in-out;
+ pointer-events: none;
+ font-family: 'Quantico', sans-serif;
+}
+
+@keyframes benevolencePulse {
+ 0% {
+ transform: translate(-50%, -50%) scale(0.5);
+ opacity: 0;
+ }
+ 50% {
+ transform: translate(-50%, -50%) scale(1.2);
+ opacity: 1;
+ }
+ 100% {
+ transform: translate(-50%, -50%) scale(1);
+ opacity: 0;
+ }
+}
+
+.storyScreen {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 20;
+}
+
+.storyNextButton {
+ position: absolute;
+ bottom: 20px;
+ right: 20px;
+ cursor: pointer;
+ pointer-events: all;
+ transition: transform 0.2s ease;
+ z-index: 10;
+ filter: drop-shadow(0 0 10px rgba(255, 0, 255, 0.5));
+ animation: buttonShine 2s ease-in-out infinite;
+}
+
+.storyNextButton:hover {
+ transform: scale(1.05);
+ filter: drop-shadow(0 0 20px rgba(255, 0, 255, 0.8));
+}
+
+.victoryScreen {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 20;
+}
+
+.victoryButton {
+ position: absolute;
+ bottom: 20px;
+ right: 20px;
+ cursor: pointer;
+ pointer-events: all;
+ transition: transform 0.2s ease;
+ z-index: 10;
+ filter: drop-shadow(0 0 10px rgba(255, 0, 255, 0.5));
+ animation: buttonShine 2s ease-in-out infinite;
+}
+
+.victoryButton:hover {
+ transform: scale(1.05);
+ filter: drop-shadow(0 0 20px rgba(255, 0, 255, 0.8));
+}
+
diff --git a/packages/perseus/src/__docs__/math-blaster-game.stories.tsx b/packages/perseus/src/__docs__/math-blaster-game.stories.tsx
new file mode 100644
index 00000000000..3535ab532a9
--- /dev/null
+++ b/packages/perseus/src/__docs__/math-blaster-game.stories.tsx
@@ -0,0 +1,1672 @@
+import {scorePerseusItem} from "@khanacademy/perseus-score";
+import {View} from "@khanacademy/wonder-blocks-core";
+import * as React from "react";
+import {useEffect, useRef, useState} from "react";
+
+import {storybookDependenciesV2} from "../../../../testing/test-dependencies";
+import {ServerItemRenderer} from "../server-item-renderer";
+
+import gameOverAudio from "../games/crash-course/assets/audio/Game Over II.ogg";
+import neonOwlAudio from "../games/crash-course/assets/audio/Zodik - Neon Owl.ogg";
+import tedoxAudio from "../games/crash-course/assets/audio/Zodik - Tedox.ogg";
+import alexBouncyMixAudio from "../games/crash-course/assets/audio/alexbouncymix2.ogg";
+import alien1Img from "../games/crash-course/assets/sprites/alien1.png";
+import alien2Img from "../games/crash-course/assets/sprites/alien2.png";
+import alien3Img from "../games/crash-course/assets/sprites/alien3.png";
+import beamImg from "../games/crash-course/assets/sprites/beam.png";
+import {CarBonusScene} from "./car-bonus-scene";
+import car1Img from "../games/crash-course/assets/sprites/car1.png";
+import cityCloseImg from "../games/crash-course/assets/backgrounds/city-close.png";
+import cityFarImg from "../games/crash-course/assets/backgrounds/city-far.png";
+import citySemiCloseImg from "../games/crash-course/assets/backgrounds/city-semi-close.png";
+import citySemiFarImg from "../games/crash-course/assets/backgrounds/city-semi-far.png";
+import impactImg from "../games/crash-course/assets/sprites/impact.png";
+import lampLightImg from "../games/crash-course/assets/backgrounds/lamplight.png";
+import loseImg from "../games/crash-course/assets/ui/lose.png";
+import styles from "./math-blaster-game.module.css";
+import {createObstacle} from "./math-blaster-utils";
+import muteImg from "../games/crash-course/assets/ui/mute.png";
+import nextImg from "../games/crash-course/assets/ui/next.png";
+import run1Img from "../games/crash-course/assets/sprites/run1.png";
+import run2Img from "../games/crash-course/assets/sprites/run2.png";
+import run3Img from "../games/crash-course/assets/sprites/run3.png";
+import run4Img from "../games/crash-course/assets/sprites/run4.png";
+import run5Img from "../games/crash-course/assets/sprites/run5.png";
+import run6Img from "../games/crash-course/assets/sprites/run6.png";
+import skyImg from "../games/crash-course/assets/backgrounds/sky.png";
+import startImg from "../games/crash-course/assets/ui/start.png";
+import story1Img from "../games/crash-course/assets/story/story1.png";
+import story2Img from "../games/crash-course/assets/story/story2.png";
+import story3Img from "../games/crash-course/assets/story/story3.png";
+import story4Img from "../games/crash-course/assets/story/story4.png";
+import story5Img from "../games/crash-course/assets/story/story5.png";
+import story6Img from "../games/crash-course/assets/story/story6.png";
+import story7Img from "../games/crash-course/assets/story/story7.png";
+import streetLampImg from "../games/crash-course/assets/backgrounds/streetlamp.png";
+import titleImg from "../games/crash-course/assets/ui/title.png";
+import unmuteImg from "../games/crash-course/assets/ui/unmute.png";
+import victoryImg from "../games/crash-course/assets/ui/victory.png";
+
+import type {Obstacle} from "./math-blaster-utils";
+import type {PerseusScore} from "@khanacademy/perseus-core";
+import type {Meta, StoryObj} from "@storybook/react-vite";
+
+// Game constants
+const CANVAS_WIDTH = 800;
+const CANVAS_HEIGHT = 600;
+const GROUND_Y = 450;
+const SCROLL_SPEED = 2; // Slowed down from 3
+const CHARACTER_X = 100;
+const SPRITE_SIZE = 128; // Updated to 128x128 for new sprite sheet
+const CHARACTER_WIDTH = SPRITE_SIZE;
+const CHARACTER_HEIGHT = SPRITE_SIZE;
+const JUMP_HEIGHT = 140; // Increased from 150 to clear obstacles better
+const JUMP_DURATION = 1000; // ms
+const OBSTACLE_SPAWN_INTERVAL = 5000; // Increased from 3000ms to give more time
+const COLLISION_ZONE_X = CHARACTER_X + CHARACTER_WIDTH + 20;
+const COOL_MODE_DURATION = 2000; // How long cool mode lasts after correct answer
+const LAMP_SPACING = 500; // Distance between street lamps
+const GAME_DURATION = 300000; // 5 minutes in milliseconds (11:55 to midnight)
+
+type GameState =
+ | "start"
+ | "story"
+ | "playing"
+ | "carBonus"
+ | "gameover"
+ | "victory";
+type CharacterState = "running" | "coolMode" | "impact" | "loss";
+
+type SpriteFrame = {
+ imageUrl?: string;
+ color?: string;
+ label: string;
+};
+
+// Sprite frame definitions
+const SPRITE_FRAMES: Record = {
+ running: [
+ {imageUrl: run1Img, label: "Run 1"},
+ {imageUrl: run2Img, label: "Run 2"},
+ {imageUrl: run3Img, label: "Run 3"},
+ {imageUrl: run4Img, label: "Run 4"},
+ {imageUrl: run5Img, label: "Run 5"},
+ {imageUrl: run6Img, label: "Run 6"},
+ ],
+ coolMode: [
+ {imageUrl: run1Img, color: "#9370DB", label: "Cool 1"}, // Tinted purple (will add guitar overlay)
+ {imageUrl: run2Img, color: "#8A2BE2", label: "Cool 2"},
+ {imageUrl: run3Img, color: "#9400D3", label: "Cool 3"},
+ {imageUrl: run4Img, color: "#9370DB", label: "Cool 4"},
+ {imageUrl: run5Img, color: "#8A2BE2", label: "Cool 5"},
+ {imageUrl: run6Img, color: "#9400D3", label: "Cool 6"},
+ ],
+ impact: [{imageUrl: impactImg, label: "Impact"}],
+ loss: [{imageUrl: impactImg, label: "Loss"}],
+};
+
+const MathBlasterGame = (): React.ReactElement => {
+ const canvasRef = useRef(null);
+ const itemRendererRef = useRef(null);
+ const animationFrameRef = useRef();
+ const lastSpawnTimeRef = useRef(0);
+ const gameLoopRef = useRef<() => void>();
+ const spriteImagesRef = useRef