docs(plans): add level redesign brainstorm and 11-phase plan

Fixes L2 unsolvable bug and widens grids across all 12 levels.
L12 preserved as unsolvable-by-normal-play (easter egg).
This commit is contained in:
tiennm99 committed 2026-04-21 21:19:56 +07:00
1 parent 692d90cafb
commit 6e2114162f
13 files changed
+772

No files matched your search

@@ -0,0 +1,64 @@
# Phase 1: Scrollable Viewport
## Context
- Brainstorm: `plans/reports/brainstorm-260421-2042-level-redesign-solvability.md` §5
- Board component: `src/components/GameBoard.svelte` (90 LOC)
- Game scene: `src/scenes/Game.svelte` (409 LOC)
## Overview
- **Priority:** CRITICAL — blocks all grid >9x9 redesigns
- **Status:** pending
- Prereq for Acts 3–6 (grids 10x10–13x13). Desktop-first; no mobile handling.
## Key Insights
- Current rendering assumes full grid fits on screen → unplayable at 12x12+.
- Simple solution: CSS scroll container + `scrollIntoView` on player cell each turn.
- Svelte 5 runes: `$effect(() => ...)` to react to player position.
## Requirements
- Viewport shows ~9x9 cells. Grid larger than viewport scrolls.
- After each player move, scroll so player cell is visible (prefer centered).
- Smooth scroll (CSS `scroll-behavior: smooth`).
- No layout break at grids ≤8x8 (viewport = grid size, no scroll).
## Architecture
- Wrap `GameBoard.svelte` grid in a scrollable container `<div class="viewport">`.
- Fixed viewport size (e.g., `min(80vh, 720px)` square).
- Inner grid expands naturally based on rows × cols × cellSize.
- `$effect` triggers `cell_element.scrollIntoView({ block: 'center', inline: 'center', behavior: 'smooth' })` on `player.row`/`player.col` change.
## Related Files
- **Modify:** `src/components/GameBoard.svelte` (add viewport wrapper, scroll effect)
- **Modify:** `src/styles/theme.css` (viewport CSS vars if needed)
- **Read for context:** `src/scenes/Game.svelte`, `src/lib/game/player.js`
## Implementation Steps
1. Add `<div class="viewport">` wrapping the `.grid` element in `GameBoard.svelte`.
2. CSS: `.viewport { max-width: 80vh; max-height: 80vh; overflow: auto; scroll-behavior: smooth; }`.
3. Bind `ref` to the player's cell element (use Svelte action or id).
4. Add `$effect` watching `player.row`, `player.col` → call `scrollIntoView` on the player cell.
5. Test with temporary 12x12 dummy level (don't commit dummy level).
6. Verify no regression at 6x6, 8x8.
7. Run `npm run build` to check compile.
## Todo List
- [ ] Add viewport wrapper + CSS in `GameBoard.svelte`
- [ ] Wire player cell ref + `$effect` scrollIntoView
- [ ] Manual test: 6x6 (no scroll), 12x12 (scroll follows)
- [ ] `npm run build` passes
- [ ] `npm run dev` smoke test on L1 + temp 12x12
## Success Criteria
- 12x12 grid fully playable; camera follows player.
- 6x6/8x8 grids render identically to before (no scrollbars when grid fits).
- Smooth scroll animation on each move.
## Risk Assessment
- **Risk:** `scrollIntoView` with smooth animation may lag on fast moves. **Mitigation:** test with held arrow key; switch to instant scroll if lag visible.
- **Risk:** Svelte 5 reactivity on ref may not fire. **Mitigation:** use `bind:this` + derived runes or simple DOM query.
## Security Considerations
N/A — pure UI.
## Next Steps
Phase 2 (solver) can run in parallel with this phase — no file overlap.
@@ -0,0 +1,79 @@
# Phase 2: Level Solver (BFS)
## Context
- Brainstorm: `plans/reports/brainstorm-260421-2042-level-redesign-solvability.md` §4
- Existing engine: `src/lib/game/grid-system.js`, `guards.js`, `turn-manager.js`, `player.js`
## Overview
- **Priority:** CRITICAL — blocks Phase 3 (tests) and all redesigns.
- **Status:** pending
- Pure-JS BFS solver that replays the real turn simulation to prove reachability.
## Key Insights
- **Must import guard AI from `guards.js`** — do NOT reimplement, or solver disagrees with runtime.
- Chaser is stateful → include positions in state key.
- Mirror + rotating beams are pure function of rotating direction + mirror positions → not part of state key (recomputed each turn).
- Cycle length = `LCM(4 [rotating], 2 [blinking], patrol_path_lengths...)`. Typical 4–24.
- L12 expanding-wave mechanic lives in `princess-mechanic.js` — solver must hook in `waveRadius` as state dim.
## Requirements
- Function `solveLevel(levelDef, { maxStates = 10_000_000 } = {})` → `{ solvable: boolean, path?: string[], states_explored: number }`.
- `path` is array of actions: `'up' | 'down' | 'left' | 'right' | 'wait'`.
- BFS guarantees shortest path.
- Hard cap on state count; returns `solvable: false, reason: 'budget_exhausted'` if exceeded.
- Returns `solvable: false, reason: 'no_path'` if BFS completes without reaching goal.
## Architecture
State key:
```
key = `${pRow},${pCow}|${chaserPositions.map(c => c.row+','+c.col).join(';')}|${turn % cycle}|${waveRadius ?? 0}`
```
Per step:
1. Apply action (move or wait); validate against walls + bounds.
2. Run `turnManager.nextTurn()` equivalent on a cloned grid + guards.
3. If player cell is lit after guard update → prune.
4. If player at goal → return path.
5. Enqueue new state if unseen.
## Related Files
- **Create:** `src/lib/game/level-solver.js` (main BFS + state cloning helpers)
- **Create:** `src/lib/game/level-solver.test.js` (unit tests for solver itself)
- **Read for context:** `src/lib/game/guards.js`, `turn-manager.js`, `grid-system.js`, `princess-mechanic.js`
## Implementation Steps
1. Extract or reuse guard instantiation from `level-manager.js` so solver can build guards from a level def.
2. Write `cloneState(grid, guards, player)` — deep copies cells, guard positions, player.
3. Write `stateKey(player, guards, turn, cycle, waveRadius)`.
4. Write `computeCycle(level)` → LCM of per-guard periods.
5. Implement BFS loop with visited Set and state budget.
6. Hook expanding-wave: include `waveRadius` in state when level has `isFinalLevel` + princess mechanic.
7. Return `{ solvable, path, states_explored, par }`.
## Todo List
- [ ] Create `level-solver.js` scaffolding + exports
- [ ] Implement state cloning helpers
- [ ] Implement `computeCycle`
- [ ] Implement BFS core loop with state budget
- [ ] Wire chaser positions into state key
- [ ] Wire princess-mechanic `waveRadius` into state key
- [ ] Unit tests: toy 3x3 level (trivial solve), 4x4 blocked level (no path), cycle computation
- [ ] `npm test` passes
- [ ] Solver finishes L1 in <5s; L11 in <60s on dev machine
## Success Criteria
- Solver correctly solves L1 (current) returning a path ≤ par.
- Solver returns `solvable: false` for current L2 (matches known bug).
- Solver returns `solvable: false` for current L12 (preserves invariant).
- Unit tests pass.
## Risk Assessment
- **Risk:** State explosion on 12x12 + 2 chasers. **Mitigation:** profile; if >10M states, tighten cycle (shorter patrol paths) or add heuristic dedup (e.g., ignore chaser when >N cells away).
- **Risk:** Chaser simulation non-deterministic (random tiebreak in pathfinding). **Mitigation:** ensure `guards.js` chaser AI is deterministic; if not, make it so (sorted tiebreak).
## Security Considerations
N/A — pure compute in test harness.
## Next Steps
Phase 3 writes the Vitest suite using this solver.
@@ -0,0 +1,53 @@
# Phase 3: Solvability Test Suite + CI Wiring
## Context
- Brainstorm §4.2
- Depends on: Phase 2 (solver)
## Overview
- **Priority:** HIGH — gate for all redesign phases.
- **Status:** pending
- Vitest suite that asserts invariants on every PR touching `levels.js`.
## Requirements
- For L1, L3–L11: assert `solveLevel(level).solvable === true`.
- For L2: known fail (baseline); test marked `.skip` with TODO to unskip after Phase 4.
- For L12: assert `solveLevel(level).solvable === false` (unsolvable invariant).
- Each passing level also asserts `par = path.length + 2` (or logs mismatch for manual par tuning).
## Architecture
- One describe block per level; `test.each(LEVELS)` pattern.
- Long-running: set Vitest timeout to 120s per test.
- Separate suite file so dev can skip fast unit tests vs full solvability run.
## Related Files
- **Create:** `src/lib/levels/levels.solvability.test.js`
- **Modify:** `package.json` — add script `"test:solvability": "vitest run src/lib/levels/levels.solvability.test.js"`
- **Modify:** `.github/workflows/*.yml` if CI exists (else note for later)
## Implementation Steps
1. Create test file, import `LEVELS` and `solveLevel`.
2. Loop levels, assert expected solvability per hard rules.
3. Add par validation (warn only, don't fail yet — par tuning happens in redesign phases).
4. Add `test.each` table for grid size validation (L1-2 = 8x8, L3-4 = 9x9, etc.) — initially all fail; phase-by-phase unskips.
5. Add npm script.
6. If `.github/workflows` exists, add step: `npm run test:solvability`.
## Todo List
- [ ] Write solvability test file
- [ ] Skip L2 initially with TODO
- [ ] Add par-delta warning (console.warn, not fail)
- [ ] Add grid-size table (initially all skipped; unskip per phase)
- [ ] Add npm script
- [ ] CI integration (if CI present)
- [ ] Confirm local: L1/3-11/12 assertions pass
## Success Criteria
- `npm run test:solvability` passes with L2 skipped and L12 asserted-unsolvable.
- CI (if present) runs solvability on PRs.
## Risk Assessment
- **Risk:** Solver too slow for CI. **Mitigation:** run solvability only on PRs that touch `levels.js` or `level-solver.js` (path-filter in CI).
## Next Steps
Phase 4 redesigns L1-L2. After L2 passes, unskip its assertion here.
@@ -0,0 +1,46 @@
# Phase 4: Act 1 Redesign (L1–L2, 8x8)
## Context
- Brainstorm §3.1, §6 (migration)
- Depends on: Phase 3 (tests)
## Overview
- **Priority:** HIGH (fixes L2 unsolvability bug).
- **Status:** pending
- Act 1 teaches movement + static guards.
## Requirements
- Both levels: 8x8 grid, preserved `id`/`name`/`storyKey`.
- **L1 "Garden Path":** 0 guards (gentle intro, pure movement/walls); par ~12.
- **L2 "The Watchtower":** 3 static guards, ≥2 viable paths; par ~16.
- L2 must be SOLVABLE this time.
## Implementation Steps
1. Sketch L1 8x8 grid: start (0,0), goal (7,7), ~10 walls forming winding but connected path.
2. Run solver on L1 → assert `solvable`; set par = path.length + 2.
3. Sketch L2 8x8 grid: 3 static guards placed so ≥2 distinct paths exist; walls widen the arena.
4. Run solver on L2 → assert solvable; set par = path.length + 2.
5. Update `src/lib/levels/levels.js` — replace L1, L2 entries in place.
6. Unskip L2 assertion in `levels.solvability.test.js`.
7. Run `npm run test:solvability` → both pass.
8. Manual playthrough via `npm run dev`.
## Related Files
- **Modify:** `src/lib/levels/levels.js` (L1, L2 entries only)
- **Modify:** `src/lib/levels/levels.solvability.test.js` (unskip L2, enable L1-2 grid-size check)
## Todo List
- [ ] Draft L1 layout, verify with solver
- [ ] Draft L2 layout with 3 guards + 2 paths, verify with solver
- [ ] Replace entries in `levels.js`
- [ ] Unskip L2 test
- [ ] Solvability suite green
- [ ] Manual playthrough L1 + L2 via `npm run dev`
## Success Criteria
- L1 and L2 solvable per solver.
- L2 has ≥2 distinct solutions (verify by running solver with different seeds or via manual BFS count).
- Par values match `solver_optimal + 2`.
## Risks
- **Overlapping wall+light** (original L2 bug). **Mitigation:** lint via assertion in solver — no wall cell should appear in any guard's `litCells` array.
@@ -0,0 +1,41 @@
# Phase 5: Act 2 Redesign (L3–L4, 9x9)
## Context
- Brainstorm §3.1
- Depends on: Phase 4 (Act 1 done; solver + tests trusted)
## Overview
- **Priority:** MEDIUM
- **Status:** pending
- Act 2 introduces **rotating** + **blinking** (blinking promoted from old Act 3).
## Requirements
- Both levels: 9x9 grid, preserved metadata.
- **L3 "Vegetable Patrol":** 4 guards total — mix of static + rotating (≥1 rotating); par ~18.
- **L4 "The Searchlight":** 5 guards — static + rotating + 1 blinking; timing window required; par ~22.
## Implementation Steps
1. Draft L3 9x9 with 4 guards; verify solver solvable + ≥2 paths.
2. Draft L4 9x9 with 5 guards including blinking (introduce teaching level); verify solvable; ensure `wait` action is useful (solver path includes at least one `wait`).
3. Replace L3, L4 in `levels.js`.
4. Enable L3-4 grid-size assertion in test.
5. `npm run test:solvability` green.
6. Manual playthrough.
## Related Files
- **Modify:** `src/lib/levels/levels.js` (L3, L4)
- **Modify:** `src/lib/levels/levels.solvability.test.js`
## Todo List
- [ ] L3 drafted + solver-verified
- [ ] L4 drafted + solver-verified (blinking intro)
- [ ] Entries replaced
- [ ] Tests green
- [ ] Manual playthrough
## Success Criteria
- Solver proves both solvable with pars reasonable.
- L4 solution requires a `wait` action (teaches mechanic).
## Risks
- **Rotating guard cycle + blinking cycle interaction** may create tight windows. **Mitigation:** solver par check; if par >30, simplify.
@@ -0,0 +1,41 @@
# Phase 6: Act 3 Redesign (L5–L6, 10x10)
## Context
- Brainstorm §3.1
- Depends on: Phase 1 (viewport for 10x10), Phase 5
## Overview
- **Priority:** MEDIUM
- **Status:** pending
- Act 3 introduces **patrolling** (promoted from old Act 4).
## Requirements
- Both levels: 10x10 grid, preserved metadata.
- **L5 "Fortress Gate":** 6 guards — static + rotating + blinking + 1 patrolling (intro); par ~22.
- **L6 "The Flickering Corridor":** 7 guards — mix, ≥2 patrollers OR 1 patroller with longer path; par ~28.
## Implementation Steps
1. Draft L5 10x10 with 6 guards (1 patrolling); solver-verify.
2. Draft L6 10x10 with 7 guards; solver-verify.
3. Replace in `levels.js`.
4. Enable 10x10 grid-size checks.
5. Test green.
6. Manual playthrough — verify viewport scrolls correctly (phase-1 integration smoke).
## Related Files
- **Modify:** `src/lib/levels/levels.js` (L5, L6)
- **Modify:** `src/lib/levels/levels.solvability.test.js`
## Todo List
- [ ] L5 drafted + verified (patrolling intro)
- [ ] L6 drafted + verified
- [ ] Entries replaced
- [ ] Tests green
- [ ] Manual playthrough confirms viewport scroll works
## Success Criteria
- Both solvable; ≥2 paths per level.
- Patroller path length ≥4 cells (meaningful patrol).
## Risks
- **Patrol path length inflates cycle** → solver state blowup. **Mitigation:** cap patrol path ≤6 cells in Act 3.
@@ -0,0 +1,41 @@
# Phase 7: Act 4 Redesign (L7–L8, 11x11)
## Context
- Brainstorm §3.1
- Depends on: Phase 1 (viewport), Phase 6
## Overview
- **Priority:** MEDIUM
- **Status:** pending
- Act 4 introduces **mirror** guards (promoted from old Act 5).
## Requirements
- Both levels: 11x11 grid, preserved metadata.
- **L7 "The Underground Passage":** 7 guards — mix + 1 mirror (intro); par ~26.
- **L8 "The Gauntlet":** 8 guards — ≥2 mirrors + ≥2 patrolling + rotating; par ~32.
## Implementation Steps
1. Draft L7 11x11 with mirror intro; solver-verify (requires solver to correctly model mirror deflection).
2. Draft L8 11x11 with complex mirror setup; solver-verify.
3. Replace in `levels.js`.
4. Tests green.
5. Manual playthrough.
## Related Files
- **Modify:** `src/lib/levels/levels.js` (L7, L8)
- **Modify:** `src/lib/levels/levels.solvability.test.js`
## Todo List
- [ ] L7 drafted (mirror intro) + verified
- [ ] L8 drafted + verified
- [ ] Entries replaced
- [ ] Tests green
- [ ] Manual playthrough
## Success Criteria
- Both solvable; mirror deflection creates a hazard that player must account for.
- L8 has ≥3 guards of same dynamic type (patrol/rotating) — "gauntlet" flavor.
## Risks
- **Solver mirror logic mismatch with runtime.** **Mitigation:** Phase 2 solver imports guards.js — single source of truth.
- **11x11 + mirrors + many guards → state blowup.** **Mitigation:** profile; may need state cap bump or heuristic pruning.
@@ -0,0 +1,40 @@
# Phase 8: Act 5 Redesign (L9–L10, 12x12)
## Context
- Brainstorm §3.1
- Depends on: Phase 1 (viewport), Phase 7
## Overview
- **Priority:** MEDIUM
- **Status:** pending
- Act 5 introduces **chaser** guards.
## Requirements
- Both levels: 12x12 grid, preserved metadata.
- **L9 "The Decoy Path":** 8 guards — mix + 1 chaser (intro); par ~34.
- **L10 "Hall of Mirrors":** 9 guards — chaser + multiple mirrors + rotating; par ~38.
## Implementation Steps
1. Draft L9 12x12 with chaser intro; solver-verify (longest run; may take minutes).
2. Draft L10 12x12 with mirror-heavy layout + chaser; solver-verify.
3. Replace in `levels.js`.
4. Tests green.
5. Manual playthrough — verify viewport + camera follow smooth at 12x12.
## Related Files
- **Modify:** `src/lib/levels/levels.js` (L9, L10)
- **Modify:** `src/lib/levels/levels.solvability.test.js`
## Todo List
- [ ] L9 drafted (chaser intro) + verified
- [ ] L10 drafted + verified
- [ ] Entries replaced
- [ ] Tests green
- [ ] Manual playthrough at 12x12
## Success Criteria
- Both solvable; chaser creates genuine threat (solver proves shortest path avoids chaser).
- Solver completes within 10min per level (or raise state cap).
## Risks
- **Solver budget exhausted on 12x12 + chaser + long cycle.** **Mitigation:** shorten patrol paths; cap chaser detection radius at 3.
@@ -0,0 +1,42 @@
# Phase 9: L11 Redesign — Throne Room (12x12)
## Context
- Brainstorm §3.1
- Depends on: Phase 1 (viewport), Phase 8
## Overview
- **Priority:** MEDIUM
- **Status:** pending
- L11 is the climax before the impossible finale. All guard types combined.
## Requirements
- 12x12 grid, preserved metadata (`id: 11`, `name: "The Throne Room"`, `storyKey: level11Story`).
- 9 guards: ≥1 of each type (static, rotating, blinking, patrolling, mirror, chaser).
- Must be solvable but hard (par ~42).
- Multiple paths; each path tests a different mix of skills learned.
## Implementation Steps
1. Draft 12x12 with all 6 guard types represented; solver-verify.
2. Ensure par > L10 par (difficulty climb).
3. Replace L11 in `levels.js`.
4. Tests green.
5. Manual playthrough — note difficulty feel; adjust if solver par drastically exceeds 42.
## Related Files
- **Modify:** `src/lib/levels/levels.js` (L11)
- **Modify:** `src/lib/levels/levels.solvability.test.js`
## Todo List
- [ ] L11 drafted with all 6 guard types
- [ ] Solver-verified
- [ ] Par sensible (≤~50)
- [ ] Tests green
- [ ] Manual playthrough
## Success Criteria
- Solvable.
- Uses every guard type at least once.
- Feels like a climactic combo of all mechanics.
## Risks
- **Mirror + chaser + rotating interaction too chaotic.** **Mitigation:** if solver par >50 or feels unfair, reduce to 8 guards (drop one redundant type).
@@ -0,0 +1,66 @@
# Phase 10: L12 Redesign — Princess Chamber + Console Easter Egg (13x13)
## Context
- Brainstorm §6, §10
- Memory: `project_level12_unsolvable.md`
- Depends on: Phase 9
## Overview
- **Priority:** HIGH (preserves core narrative invariant).
- **Status:** pending
- L12 MUST remain unsolvable by normal play. Only console-based teleport can trigger the win screen.
## Hard Invariant
Solver MUST return `solvable: false` for L12 after redesign. Test asserts this; CI blocks any change that makes L12 solvable.
## Requirements
- 13x13 grid (biggest), preserved metadata (`id: 12`, `isFinalLevel: true`).
- 10 guards + expanding-wave mechanic from `princess-mechanic.js`.
- Console easter-egg hook exposed on `window` so dev-tools users can teleport to goal.
- Normal win flow triggers regardless of how goal was reached.
## Architecture
- **Guards:** all 6 types represented, packed aggressively around goal.
- **Expanding wave:** intact from current impl — lights ring around goal after player within distance 4.
- **Console hook:** expose `window.__nntvDev = { teleport(row, col) { ... }, reveal() { ... } }`.
- `teleport` mutates the live game state via existing reactive store.
- Documented in code comments only; NOT in README, NOT in guide UI.
- **Win flow unchanged:** existing `isAtGoal()` check + `LevelCompletePopup.svelte` fire normally when player position = goal.
## Implementation Steps
1. Draft 13x13 layout with 10 guards + wave mechanic.
2. Solver-verify `solvable: false` within 10M state budget.
3. Replace L12 in `levels.js`.
4. Add `window.__nntvDev` hook in `src/main.js` (or `Game.svelte`) — gated behind Svelte store reference.
5. Manual test:
- Normal play → impossible confirmed.
- `window.__nntvDev.teleport(goal.row, goal.col)` in console → win flow triggers.
6. Add test in `levels.solvability.test.js`: assert L12 unsolvable.
7. Manual playthrough.
## Related Files
- **Modify:** `src/lib/levels/levels.js` (L12)
- **Modify:** `src/main.js` or `src/scenes/Game.svelte` (expose dev hook)
- **Modify:** `src/lib/levels/levels.solvability.test.js`
- **Read:** `src/lib/game/princess-mechanic.js`
## Todo List
- [ ] L12 drafted (13x13, 10 guards + wave)
- [ ] Solver confirms unsolvable
- [ ] Dev hook (`window.__nntvDev.teleport`) wired
- [ ] Manual: normal play impossible, console win works
- [ ] Solvability test asserts `!solvable`
- [ ] Code comment documents easter egg
## Success Criteria
- Solver asserts `solvable: false` — test green.
- Pressing every direction + wait indefinitely never reaches goal (manual spot-check).
- `window.__nntvDev.teleport(12, 12)` from console → LevelCompletePopup fires.
- No in-game UI/readme hint about the easter egg.
## Risks
- **Dev hook security.** **Mitigation:** non-sensitive (just game state); no auth/data exposure. Fine for a client-only game.
- **Wave mechanic solvability.** **Mitigation:** solver includes `waveRadius` in state key (Phase 2).
## Security Considerations
- `window.__nntvDev` is intentionally world-accessible in-browser. Acceptable — no server, no user data.
@@ -0,0 +1,53 @@
# Phase 11: Docs + Full Smoke Test
## Context
- Brainstorm §11
- Depends on: all prior phases
## Overview
- **Priority:** LOW–MEDIUM (final polish, CI gate).
- **Status:** pending
- Update `docs/`, verify full playthrough, tune pars if needed.
## Requirements
- Update `docs/system-architecture.md` — add solver + viewport sections.
- Update `docs/code-standards.md` — note "all level edits must run solvability suite".
- README unchanged re: easter egg (keep hidden).
- Full manual playthrough L1→L11; verify L12 is impossible but console-teleport wins.
## Implementation Steps
1. Run full test suite: `npm test && npm run test:solvability`.
2. Check `docs/` files exist — if not, create.
3. Append solver architecture section to `docs/system-architecture.md`.
4. Append level-authoring rule to `docs/code-standards.md`: "never commit `levels.js` without running `npm run test:solvability`".
5. Add codebase-summary.md entry for `level-solver.js`.
6. Manual playthrough L1 → L11 (screenshot key moments for regression visibility).
7. Par tuning pass: if solver_par + 2 feels too loose/tight per actual play, adjust.
8. `npm run build` produces clean dist.
## Related Files
- **Modify:** `docs/system-architecture.md`
- **Modify:** `docs/code-standards.md`
- **Modify:** `docs/codebase-summary.md` (if exists)
- **Modify (maybe):** `src/lib/levels/levels.js` (par tuning)
## Todo List
- [ ] Full test suite green
- [ ] docs/system-architecture.md updated
- [ ] docs/code-standards.md updated
- [ ] Manual playthrough L1→L11 complete, no blockers
- [ ] L12 console win confirmed
- [ ] Par tuning (if any) complete
- [ ] `npm run build` clean
## Success Criteria
- All 11 solvable levels playable end-to-end without test failures.
- L12 impossible by normal play; console win works.
- Docs reflect new solver + viewport architecture.
## Risks
- **Par values feel off in real play.** **Mitigation:** dedicated tuning pass here; adjust before merge.
## Next Steps
- Archive plan via `/ck:plan archive`.
- Merge to main, tag release if versioned.
@@ -0,0 +1,61 @@
---
name: Level Redesign & Solvability
slug: level-redesign-solvability
created: 2026-04-21
status: pending
blockedBy: []
blocks: []
source: plans/reports/brainstorm-260421-2042-level-redesign-solvability.md
---
# Level Redesign & Solvability — Implementation Plan
Redesign all 12 levels of Night Ninja: Twilight Voyage. Fix L2 unsolvability. Widen grids (8x8→13x13). Add more guards per act. Introduce automated BFS solver + CI-enforced solvability tests. Preserve L12 as unsolvable (console easter-egg win only).
## Hard Rules
1. Exactly 12 levels, 6 acts. No adds/removes/renumbers.
2. L12 must remain unsolvable by normal play.
3. Desktop-first. Large grids via scrollable viewport that follows player.
4. Preserve `id`, `name`, `storyKey`, `isFinalLevel` per level (no progress migration).
## Progression Target
| Act | Levels | Grid | Guard types (cumulative) | Guard count |
|-----|--------|------|--------------------------|-------------|
| 1 | 1–2 | 8x8 | static | 0 / 3 |
| 2 | 3–4 | 9x9 | + rotating + blinking | 4 / 5 |
| 3 | 5–6 | 10x10 | + patrolling | 6 / 7 |
| 4 | 7–8 | 11x11 | + mirror | 7 / 8 |
| 5 | 9–10 | 12x12 | + chaser | 8 / 9 |
| 6 | 11 | 12x12 | all | 9 |
| 6 | 12 | 13x13 | all + expanding wave | 10 (unsolvable) |
## Phases
| # | File | Status | Description |
|---|------|--------|-------------|
| 1 | [phase-01-scrollable-viewport.md](phase-01-scrollable-viewport.md) | pending | Camera-follow viewport for grids >9x9 |
| 2 | [phase-02-level-solver.md](phase-02-level-solver.md) | pending | BFS solver in `src/lib/game/level-solver.js` |
| 3 | [phase-03-solvability-tests.md](phase-03-solvability-tests.md) | pending | Vitest suite + CI wiring; assert L2 fails as baseline |
| 4 | [phase-04-redesign-act1-l1-l2.md](phase-04-redesign-act1-l1-l2.md) | pending | Act 1 (L1–L2, 8x8, static) |
| 5 | [phase-05-redesign-act2-l3-l4.md](phase-05-redesign-act2-l3-l4.md) | pending | Act 2 (L3–L4, 9x9, + rotating + blinking) |
| 6 | [phase-06-redesign-act3-l5-l6.md](phase-06-redesign-act3-l5-l6.md) | pending | Act 3 (L5–L6, 10x10, + patrolling) |
| 7 | [phase-07-redesign-act4-l7-l8.md](phase-07-redesign-act4-l7-l8.md) | pending | Act 4 (L7–L8, 11x11, + mirror) |
| 8 | [phase-08-redesign-act5-l9-l10.md](phase-08-redesign-act5-l9-l10.md) | pending | Act 5 (L9–L10, 12x12, + chaser) |
| 9 | [phase-09-redesign-l11.md](phase-09-redesign-l11.md) | pending | L11 Throne Room (12x12, all guards) |
| 10 | [phase-10-redesign-l12-easter-egg.md](phase-10-redesign-l12-easter-egg.md) | pending | L12 Princess Chamber (13x13, unsolvable) + console win hook |
| 11 | [phase-11-docs-and-smoke-test.md](phase-11-docs-and-smoke-test.md) | pending | Update docs/ + manual playthrough QA |
## Dependencies
- Phase 1 (viewport) blocks Phases 6–10 (grids ≥10x10 unplayable without it).
- Phase 2 (solver) blocks Phase 3 (tests need solver).
- Phase 3 (tests) blocks Phases 4–10 (each redesign validated by tests).
- Phases 4–9 independent of each other but best done in order (progression curve).
- Phase 10 depends on Phase 9 (L11 finalized before L12).
- Phase 11 depends on all previous.
## Source
Brainstorm summary: `plans/reports/brainstorm-260421-2042-level-redesign-solvability.md`
@@ -0,0 +1,145 @@
# Level Redesign & Solvability — Brainstorm Summary
**Date:** 2026-04-21
**Scope:** Redesign all 12 levels of Night Ninja: Twilight Voyage. Fix L2 unsolvability. Expand grid sizes. Add more guards. Preserve L12 as intentionally unsolvable.
**Status:** Design approved — ready for implementation plan.
---
## 1. Problem Statement
- **L2 unsolvable** — BFS from `(0,0)` cannot reach `(5,5)`. Right-side component `{(3,5),(4,4),(4,5),(5,3),(5,4),(5,5)}` is disconnected by permanent light walls from static guards at `(2,4)` and `(4,2)`.
- **No solvability gate** — levels authored manually; no automated proof. Risk of regressions on future edits.
- **Levels feel narrow** — current grids 6x6–9x9 have single corridors; user wants wider, more complex puzzles.
- **Progression imbalanced** — mirror/chaser/patrolling introduced too late (Acts 4–5); early acts feel sparse.
## 2. Hard Rules (invariants)
1. **Exactly 12 levels, 6 acts.** No adds, removes, renumbers.
2. **L12 must remain unsolvable by normal play.** Only console easter-egg win (user teleports player to goal via dev tools). Win condition / congratulations screen still fires when goal reached by any means.
3. **Desktop-first.** No mobile responsive work. Large grids handled via scrollable viewport that follows the player.
4. **Preserve** `id`, `name`, `storyKey`, `isFinalLevel` per level — zero progress-persistence migration.
## 3. Solution — Redesign Philosophy
### 3.1 Progression (rebalanced acts)
| Act | Levels | Grid | Guard types (cumulative) | Guard count |
|-----|--------|------|--------------------------|-------------|
| 1 | 1–2 | 8x8 | static | 0 / 3 |
| 2 | 3–4 | 9x9 | + rotating + **blinking** (promoted from Act 3) | 4 / 5 |
| 3 | 5–6 | 10x10 | + **patrolling** (promoted from Act 4) | 6 / 7 |
| 4 | 7–8 | 11x11 | + **mirror** (promoted from Act 5) | 7 / 8 |
| 5 | 9–10 | 12x12 | + chaser | 8 / 9 |
| 6 | 11 | 12x12 | all types combined | 9 |
| 6 | 12 | 13x13 | all types + expanding-wave mechanic | 10 (unsolvable) |
### 3.2 Design Rules Per Level
- **Connectivity gate:** at least one reachable path from start→goal in solver (except L12).
- **Widen, don't narrow:** add 2–3 alternative routes so guards force *decisions*, not single chokepoints.
- **More guards ≠ more walls.** Keep wall density ~20–25% of grid area; increase guard count per act tier.
- **Par = solver_optimal + 2** (small slack for undo experimentation).
- **No redundant wall-on-light** (L2 had wall `(3,4)` over guard light `(3,4)` — sloppy overlap, remove).
- **Guard beam pruning:** rotating beams stop at walls — use walls strategically to create safe windows.
## 4. Solvability Solver (automated validation)
### 4.1 State & Search
**State key:** `(player_row, player_col, chaser_positions_tuple, turn_mod_cycle)`
- `cycle = LCM(4 [rotating], 2 [blinking], patrol_path_length_i...)`. Typical cycles 4–24.
- **Chaser** is stateful — include each chaser's position in state key. State space stays tractable (<10M for 12x12 + 2 chasers + cycle 24).
- **Mirror beams** are pure function of rotating-guard directions + mirror positions each turn — not part of state.
**Transitions:** 5 actions per turn — `up / down / left / right / wait`.
**Pruning:** after applying action + running guard update for next turn, if player cell `isLight` → dead branch.
**Algorithm:** BFS (guarantees shortest). Bail out at state-count cap (~10M) with "unsolvable within budget" → manual check.
### 4.2 Location & Modules
- `src/lib/game/level-solver.js` — pure BFS solver, reuses existing `grid-system.js`, `guards.js`, `turn-manager.js`.
- `src/lib/levels/levels.solvability.test.js` — Vitest suite asserting:
- For L1–L11: solver returns a valid path.
- For L12: solver returns no path within budget (unsolvable invariant).
- Par values match `solver_optimal + 2`.
- CI hook: run solvability suite on every PR that touches `levels.js`.
### 4.3 Known Limitations
- Chaser path depends on guard AI implementation — solver must use *identical* chaser logic as runtime (import from `guards.js`, don't reimplement).
- Cycle computation must account for patrol path length (may be odd / prime).
- L12's expanding-wave mechanic needs the solver to model princess-mechanic.js state — include `waveRadius` in state key.
## 5. Scrollable Viewport (prereq for large grids)
- Current rendering assumes full grid fits on screen.
- Need: camera-follow the player so 12x12 / 13x13 grids are playable on 1080p screens.
- Implementation touch points:
- `src/components/GameBoard.svelte` (or equivalent) — CSS `overflow: auto` + `scroll-behavior: smooth` + programmatic `scrollIntoView` on player position change.
- Viewport: show ~9x9 cells visible; pan on player move.
- Out-of-scope for this redesign brainstorm but **blocker** for Acts 4–6 playability.
## 6. L12 Easter Egg — Console Win
- Expose a minimal dev hook on `window` (e.g., `window.__nntvDev = { teleportToGoal() { ... } }`) OR rely on existing game-state reactivity so user can directly mutate player position via devtools.
- Reaching goal by any means → normal win flow (congratulations, progress saved).
- Document the easter egg ONLY in code comments, not in-game UI or README.
## 7. Migration & Rollout
- **In-place rewrite** of `src/lib/levels/levels.js`. No new file.
- Preserve each level's `id`, `name`, `storyKey`, `isFinalLevel`, array position. No changes to `src/lib/localization/*` story keys needed.
- Update `parMoves` from solver output for all levels.
- Tests first: land solver + solvability suite **before** level content changes, so each new level design is validated as it's written.
## 8. Evaluated Alternatives
| Approach | Pros | Cons | Verdict |
|----------|------|------|---------|
| Fix L2 only, leave others | Minimal churn | Doesn't address "narrow levels" complaint; no regression protection | **Rejected** |
| Keep grids, add guards only | No viewport work | Crowded; still narrow corridors; low decision value | **Rejected** |
| Aggressive grid expansion + more guards + solver | Solves all issues; prevents future regressions | Requires viewport work; solver effort | **Chosen** |
| Expand to 15–18 levels | Richer progression | Breaks progress persistence, story keys, count invariant | **Rejected** (violates hard rule) |
## 9. Success Criteria
- [ ] Vitest suite: 11/11 levels solvable, L12 unsolvable. Passes on CI.
- [ ] Every level has ≥2 distinct BFS-discovered paths OR clear single-path justification in code comment.
- [ ] All acts visually wider than before (row/col counts match Section 3.1).
- [ ] Scrollable viewport works on 1080p / 1440p screens for 12x12 grids.
- [ ] Win condition fires when L12 goal is reached via console manipulation.
- [ ] No changes to story keys, level IDs, or progress-persistence schema.
## 10. Next Steps (Implementation Plan Phases)
1. **Phase 1:** Implement scrollable viewport (prereq for >9x9 grids).
2. **Phase 2:** Build `level-solver.js` + unit tests on current L1, L3–L11 (should solve) and L12 (should fail). Accept that L2 will fail — that's the baseline bug.
3. **Phase 3:** Solvability test suite — wire into CI.
4. **Phase 4:** Redesign L1–L2 (Act 1, 8x8). Run solver, lock par values.
5. **Phase 5:** Redesign L3–L4 (Act 2, 9x9, introduce blinking earlier).
6. **Phase 6:** Redesign L5–L6 (Act 3, 10x10, introduce patrolling).
7. **Phase 7:** Redesign L7–L8 (Act 4, 11x11, introduce mirror).
8. **Phase 8:** Redesign L9–L10 (Act 5, 12x12, introduce chaser).
9. **Phase 9:** Redesign L11 (Act 6, 12x12, all guards combined).
10. **Phase 10:** Redesign L12 (Act 6, 13x13, preserve unsolvable invariant; expose console win hook).
11. **Phase 11:** Update `docs/` (code-standards, system-architecture) with solver + easter-egg notes. Smoke-test full playthrough.
## 11. Risks
| Risk | Mitigation |
|------|------------|
| Solver too slow on chaser+long-cycle levels | State cap 10M; if exceeded, reduce cycle via shorter patrol paths |
| Viewport work reveals rendering bugs | Isolate in Phase 1; land before level changes |
| Solver disagrees with runtime (chaser logic) | Import guard logic from `guards.js` — single source of truth |
| L12 accidentally solvable after redesign | Solver test asserts unsolvability; fails CI |
| Parsing console easter egg as "cheating" concern | Document in code comments only; feature is intentional |
---
## Unresolved Questions
1. **Console easter-egg API shape** — expose `window.__nntvDev.teleport()` explicitly, or rely on Svelte 5 reactive store mutation? Simplest path TBD in Phase 10.
2. **Viewport implementation owner** — is scrollable viewport a separate brainstorm/plan, or bundled into this redesign's Phase 1?
3. **Story text updates** — do any `storyKey` copy lines need rewriting to match new grid sizes/themes (e.g., "narrow corridor" lines on now-wider levels)? Decision deferred to QA after redesigns land.
4. **Chaser count in Act 5+** — cap at 1 per level, or allow 2 in L10/L11? Affects solver state-space budget.