CompletePuzzle Game

Photon Forge

A light-beam puzzle game built in 9 iterations.

9
Iterations
330
Tests

Test Growth

v1███░░░░░░░░░░░░░░░░░57Initial game build
v2██████░░░░░░░░░░░░░░9120 campaign levels
v3████████░░░░░░░░░░░░124Solver algorithm
v5█████████████░░░░░░░215Timer + UI polish
v6███████████████████░320Endless mode
v8████████████████████330Mobile polish

Try It

$ play photon-forgeOpen in new tab →

The Prompt

v1 Prompt
# Photon Forge - Ralph Wiggum Prompt

## Your Mission

Make all tests pass.

## Success Criteria

You are DONE when ALL of these commands pass:

```bash
cd ./code
npm run build && npm run lint && npm run test:unit && npm run test:e2e
```

When all pass, output exactly: **RALPH_WIGGUM_COMPLETE**

## Project Location

`./code`

## Workflow

1. Run `npm run test:unit` to see what's failing
2. Read the failing test files to understand requirements
3. Fix the code to satisfy the tests
4. Iterate until all tests pass
5. Run full suite: `npm run build && npm run lint && npm run test:unit && npm run test:e2e`

## Completion

When all tests pass, output:

**RALPH_WIGGUM_COMPLETE**
v6 Prompt (233 lines)
# Photon Forge v6 - Endless Mode + Procedural Generation

## Your Mission

Add a dual-mode game structure: Campaign (existing 20 levels) and Endless (procedurally generated levels with scaling difficulty). This is a significant feature - take your time to understand the existing codebase before making changes.

## Success Criteria

```bash
cd ./code
npm run build && npm run lint && npm run test:unit && npm run test:e2e
```

When all pass, output: **RALPH_WIGGUM_COMPLETE**

## Architecture Overview

### Mode Structure
```
Main Menu
├── Campaign Mode → Level Select (20 levels) → Game
└── Endless Mode → Game (procedural) → Game Over Screen
```

### New Files to Create
- `src/game/procedural.ts` - Level generation
- `src/game/endless.ts` - Endless run state management

### Files to Modify
- `src/game/progress.ts` - Add endless high score persistence
- `src/components/Menu.tsx` - Mode selection + campaign level select
- `src/components/Game.tsx` - Support endless mode UI

---

## Feature 1: Procedural Level Generation

Create `src/game/procedural.ts`:

```typescript
interface DifficultyConfig {
  gridSize: number;      // 8, 10, or 12
  minObstacles: number;
  maxObstacles: number;
  minSources: number;
  maxSources: number;
  minTargets: number;
  maxTargets: number;
}

// Returns config for difficulty level
function getDifficultyConfig(difficulty: number): DifficultyConfig;

// Generates a solvable level at the given difficulty
// CRITICAL: Must ALWAYS return a solvable level (verify with solver!)
function generateLevel(difficulty: number, options?: { seed?: number }): Level;
```

### Difficulty Scaling
| Difficulty | Grid Size | Obstacles | Sources | Targets |
|------------|-----------|-----------|---------|---------|
| 1-5        | 8x8       | 0-3       | 1       | 1       |
| 6-10       | 10x10     | 3-6       | 1-2     | 1-2     |
| 11+        | 12x12     | 5-10      | 1-3     | 1-3     |

### Generation Algorithm (Suggested)
1. Create empty grid of appropriate size
2. Place sources on edges (random positions, random directions)
3. Place targets (must be reachable by beam paths)
4. Place obstacles (random, but don't block all paths)
5. **VERIFY with solver** - if unsolvable, regenerate
6. Use negative IDs for procedural levels (e.g., -1, -2, etc.)

---

## Feature 2: Endless Mode State

Create `src/game/endless.ts`:

```typescript
interface EndlessRun {
  wave: number;           // Current wave (starts at 1)
  score: number;          // Cumulative score
  currentLevel: Level;    // Current procedural level
  startTime: number;      // Run start timestamp
  isActive: boolean;      // Is run in progress
}

function createEndlessRun(): EndlessRun;
function advanceEndlessRun(run: EndlessRun, stars: number, mirrors: number, time: number): EndlessRun;
function calculateEndlessScore(stars: number, time: number): number;
```

### Score Calculation
- Same as campaign: stars (100/200/300) + time bonus (0/25/50/100)
- No maximum score - accumulates infinitely
- Optional: Wave multiplier for high waves

---

## Feature 3: Endless High Score Persistence

Add to `src/game/progress.ts`:

```typescript
interface EndlessHighScore {
  score: number;
  wave: number;
  date: number;
}

function saveEndlessHighScore(score: number, wave: number): void;
function loadEndlessHighScore(): EndlessHighScore | null;
function clearEndlessHighScore(): void;
```

- Only save if new score beats existing high score
- Store in localStorage with different key than campaign progress

---

## Feature 4: UI Changes

### Main Menu (Mode Selection)
- Show two buttons: Campaign and Endless
- Campaign button shows "20 Levels" or similar
- Endless button shows "Endless" or "∞"
- Show endless high score preview
- `data-testid="campaign-mode"`, `data-testid="endless-mode"`
- `data-testid="endless-high-score-preview"`

### Campaign Level Select
- Same as current, but accessed via Campaign button
- Back button returns to mode selection
- `data-testid="back-to-modes"`
- Keep existing `data-testid="level-grid"`, `data-testid="level-button"`

### Endless In-Game UI
- Wave indicator: `data-testid="endless-wave"`
- Score display: `data-testid="endless-score"`
- Quit button: `data-testid="endless-quit"`
- Timer and help button (same as campaign)

### Endless Game Over Screen
- `data-testid="endless-game-over"`
- Final score: `data-testid="endless-final-score"`
- Waves reached: `data-testid="endless-waves-reached"`
- High score: `data-testid="endless-high-score"`
- Play again: `data-testid="endless-play-again"`
- Back to menu: `data-testid="endless-back-to-menu"`

---

## Required data-testid Summary

### Mode Selection
- `campaign-mode` - Campaign mode button
- `endless-mode` - Endless mode button
- `endless-high-score-preview` - High score on main menu

### Campaign
- `back-to-modes` - Return to mode selection
- (Keep existing: `level-grid`, `level-button`, `total-score`)

### Endless In-Game
- `endless-wave` - Current wave display
- `endless-score` - Current score display
- `endless-quit` - Quit/exit button
- (Keep existing: `timer`, `game-help-button`)

### Endless Game Over
- `endless-game-over` - Game over container
- `endless-final-score` - Final score display
- `endless-waves-reached` - Wave count reached
- `endless-high-score` - High score display
- `endless-play-again` - Restart button
- `endless-back-to-menu` - Menu button

---

## Key Files to Read First

1. `tests/unit/procedural.test.ts` - Generation requirements
2. `tests/unit/endless.test.ts` - Endless mode requirements
3. `tests/e2e/endless.spec.ts` - UI requirements
4. `src/game/solver.ts` - Use this to verify generated levels
5. `src/game/types.ts` - Level interface
6. `src/game/levels.ts` - Example level structure
7. `src/game/scoring.ts` - Score calculation reference

---

## Critical Requirements

1. **Generated levels MUST be solvable** - Use solver.ts to verify
2. **Generated levels MUST be non-trivial** - Solutions must require at least 1 mirror (reject levels where beam naturally hits target)
3. **Grid size MUST scale** - 8x8 → 10x10 → 12x12
4. **No score ceiling in endless** - Can exceed 8000
5. **High score persists** - Separate from campaign progress
6. **All existing tests must still pass** - Don't break campaign mode
7. **useGame hook MUST respond to level changes** - THIS IS A KNOWN BUG THAT MUST BE FIXED:

   In `src/hooks/useGame.ts`, line 21 currently has:
   ```typescript
   const [level] = useState<Level>(initialLevel);  // BUG: Never updates!
   ```

   This MUST be changed to use the prop directly:
   ```typescript
   const level = initialLevel;  // CORRECT: Updates when prop changes
   ```

   AND mirrors must reset when level changes. Add this useEffect:
   ```typescript
   useEffect(() => {
     setMirrors([]);
   }, [initialLevel.id]);
   ```

   Without this fix, endless mode will show stale level data after advancing waves.

8. **Add test for useGame level transitions** - Create `tests/unit/useGame.test.ts` that verifies the hook responds to level changes:
   ```typescript
   // Test that level prop changes are reflected
   // When initialLevel changes, the hook should use the new level
   // Mirrors should reset when level changes
   ```

---

## Completion

When all tests pass: **RALPH_WIGGUM_COMPLETE**

The Journey

I wanted to test if the Ralph Loop methodology could build a complete game. Not a toy example, but something with real mechanics: physics, color mixing, level progression, scoring.

The first iteration was rough. E2E tests passed but the game logic was buggy. Beams reflected wrong, colors didn't mix properly. The tests said 'canvas visible' but didn't verify the actual behavior.

By v3, I had unit tests for every physics rule, color combination, and edge case. The AI couldn't get away with shallow implementations anymore. If a beam should reflect north, the test proved it.

The solver pattern was the breakthrough. Instead of manually checking if levels were solvable, I built a solver and tested every level against it. The AI could generate content freely, and the tests would catch anything invalid.

v6 taught me about rejection criteria. The generator made 'solvable' levels, but some were trivial (beam hit target directly). I had to explicitly test that levels require at least one mirror placement.

By v8, the game was polished. 330 tests covering physics, UI, progression, scoring, and procedural generation.

v9 was a humbling lesson. The game passed all tests on desktop but was broken on mobile. Mirror removal required right-click (impossible on touch), and endless mode had timing issues on mobile browsers. I fixed mirror removal with a tap-to-cycle pattern that works everywhere. For endless mode, I decided to just cut it rather than sink more time into debugging. The core methodology learning was already done, and I think I was chasing polish at that point. Lesson learned: test on actual phones earlier.

Lessons Learned

  • Tests define done, but only as well as you write them
  • Unit tests are essential for logic-heavy code (physics, color mixing)
  • Test negative cases to catch bugs positive tests miss
  • Fixture tests lock critical behavior and prevent regressions
  • The Solver Pattern validates generated content automatically
  • Generators need rejection criteria, not just acceptance
  • Design constraints can drive UI quality
  • Test on real mobile devices, not just browser devtools
  • Know when to cut features vs. debug complex platform-specific issues