mirror of
https://github.com/tiennm99/nntv.git
synced 2026-10-16 06:14:38 +00:00
docs(nntv): document solver, viewport, and L12 easter egg
Enable wall-light overlap lint (previously skipped); clean up remaining overlaps in L4, L5, L8, L11. Add solver and easter-egg sections to system-architecture.md. Extend code-standards.md review checklist with solvability requirements.
This commit is contained in:
1 parent
02f85ef115
commit
90e6958d41
4 files changed
+56
-18
No files matched your search
@@ -276,3 +276,6 @@ audio.toggleMute();
|
||||
- [ ] Touch input debounced/throttled if needed
|
||||
- [ ] Audio context lazily initialized (autoplay policy compliance)
|
||||
- [ ] Pixel color semantics (`--guard-*` CSS vars ↔ `NNTV.guard*`) kept in sync when modifying theme
|
||||
- [ ] Any edit to `src/lib/levels/levels.js` runs `npm run test:solvability` and passes
|
||||
- [ ] L12 remains unsolvable (solver asserts this — never "fix" it)
|
||||
- [ ] No guard's `litCells` entry overlaps a wall cell in the same level
|
||||
@@ -25,11 +25,53 @@
|
||||
┌──────────────────────────────────────┐
|
||||
│ Pure JS Game Engine (no framework) │
|
||||
├──────────────────────────────────────┤
|
||||
│ GridSystem │ Player │ Guards │
|
||||
│ TurnManager │ Levels │ Progress │
|
||||
│ GridSystem │ Player │ Guards │
|
||||
│ TurnManager │ LevelSolver │ Progress │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Level Solver (Validation Tool)
|
||||
|
||||
BFS-based solvability checker at `src/lib/game/level-solver.js`. Verdicts are
|
||||
test-only — the solver is not bundled into the runtime. It reuses the real
|
||||
`guards.js` + `princess-mechanic.js` AI so results match live gameplay.
|
||||
|
||||
- **State key:** `(player.row, player.col, guardCaptures, princessCapture)` via
|
||||
each guard's `capture()` method. Includes chaser positions and patroller
|
||||
path indices.
|
||||
- **Actions:** `up / down / left / right / wait` per turn.
|
||||
- **Pruning:** after `turnManager.nextTurn` semantics, if player cell is lit
|
||||
the branch dies.
|
||||
- **Budget:** 5M states default, caller-overridable. Returns
|
||||
`{ solvable, path?, reason?, states_explored }`.
|
||||
|
||||
CI-enforced invariants in `src/lib/levels/levels.solvability.test.js`:
|
||||
- L1–L11 must be solvable.
|
||||
- L12 must remain unsolvable (Princess Chamber easter-egg rule).
|
||||
- No guard may light a wall cell (pre-existing authoring bug guard).
|
||||
- Exactly 12 levels exist.
|
||||
|
||||
## Viewport (Camera-Follow)
|
||||
|
||||
`src/components/GameBoard.svelte` is wrapped by a scrollable `board-container`
|
||||
div in `Game.svelte` with `max-width/height: min(720px, 85vw, 85vh); overflow: auto`.
|
||||
A `$effect` in `Game.svelte` watches `player.row/col` and calls `scrollTo` with
|
||||
smooth behavior so the player stays roughly centered on grids larger than the
|
||||
viewport. `cellSize` is fixed at 50 — grids no longer shrink cells.
|
||||
|
||||
## L12 Easter Egg (Console Teleport)
|
||||
|
||||
`Game.svelte` `onMount` exposes `window.__nntvDev`:
|
||||
|
||||
```js
|
||||
window.__nntvDev.teleport(row, col) // Instantly move player; if on goal, win
|
||||
window.__nntvDev.reveal() // Read player/goal positions
|
||||
```
|
||||
|
||||
Intentional escape hatch for L12 "Princess Chamber", which is unsolvable by
|
||||
normal play. Teleporting to the goal fires the normal level-complete flow.
|
||||
Cleaned up in `onDestroy`. Not documented in README or in-game UI.
|
||||
|
||||
## Scene Flow
|
||||
|
||||
```
|
||||
|
||||
@@ -150,8 +150,7 @@ export const LEVELS = [
|
||||
type: "static",
|
||||
position: { row: 3, col: 3 },
|
||||
litCells: [
|
||||
{ row: 2, col: 3 }, { row: 3, col: 2 },
|
||||
{ row: 3, col: 4 }, { row: 4, col: 3 },
|
||||
{ row: 3, col: 2 }, { row: 3, col: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -166,8 +165,7 @@ export const LEVELS = [
|
||||
type: "static",
|
||||
position: { row: 6, col: 7 },
|
||||
litCells: [
|
||||
{ row: 5, col: 7 }, { row: 6, col: 6 },
|
||||
{ row: 6, col: 8 }, { row: 7, col: 7 },
|
||||
{ row: 6, col: 6 }, { row: 6, col: 8 },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -219,16 +217,14 @@ export const LEVELS = [
|
||||
type: "static",
|
||||
position: { row: 7, col: 7 },
|
||||
litCells: [
|
||||
{ row: 6, col: 7 }, { row: 7, col: 6 },
|
||||
{ row: 8, col: 7 },
|
||||
{ row: 6, col: 7 }, { row: 8, col: 7 },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "static",
|
||||
position: { row: 3, col: 8 },
|
||||
litCells: [
|
||||
{ row: 2, col: 8 }, { row: 3, col: 7 },
|
||||
{ row: 3, col: 9 }, { row: 4, col: 8 },
|
||||
{ row: 3, col: 7 }, { row: 3, col: 9 },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -452,8 +448,8 @@ export const LEVELS = [
|
||||
position: { row: 5, col: 9 },
|
||||
startState: true,
|
||||
litCells: [
|
||||
{ row: 4, col: 9 }, { row: 5, col: 8 },
|
||||
{ row: 5, col: 10 }, { row: 6, col: 9 },
|
||||
{ row: 5, col: 8 }, { row: 5, col: 10 },
|
||||
{ row: 6, col: 9 },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -695,8 +691,8 @@ export const LEVELS = [
|
||||
type: "static",
|
||||
position: { row: 8, col: 2 },
|
||||
litCells: [
|
||||
{ row: 7, col: 2 }, { row: 8, col: 1 },
|
||||
{ row: 8, col: 3 }, { row: 9, col: 2 },
|
||||
{ row: 8, col: 1 }, { row: 8, col: 3 },
|
||||
{ row: 9, col: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -69,10 +69,7 @@ describe('level metadata invariants', () => {
|
||||
expect(l12.isFinalLevel).toBe(true);
|
||||
});
|
||||
|
||||
// Forward regression guard — several current levels (L2, L5 at least) have
|
||||
// pre-existing wall-on-light overlaps. Full cleanup happens as each level is
|
||||
// redesigned in its owning phase. Unskip in Phase 11 once all levels clean.
|
||||
it.skip('no wall cell overlaps a guard lit cell (enable after all redesigns)', () => {
|
||||
it('no wall cell overlaps a guard lit cell', () => {
|
||||
for (const lvl of LEVELS) {
|
||||
if (KNOWN_UNSOLVABLE_BUGS.has(lvl.id)) continue;
|
||||
const walls = new Set((lvl.walls ?? []).map(w => `${w.row},${w.col}`));
|
||||
|
||||
Reference in new issue
Block a user