diff --git a/src/lib/levels/levels.js b/src/lib/levels/levels.js index 8c9b45b..8eea0fc 100644 --- a/src/lib/levels/levels.js +++ b/src/lib/levels/levels.js @@ -22,14 +22,20 @@ export const LEVELS = [ ], guards: [], affordances: { undo: true, preview: true }, - parMoves: 22, + parMoves: 18, }, { // L2 — Watchtower - // Mechanic intro: static wilting guards - // Intended path sketch: same zigzag wall layout as L1 with three static guards on the - // fast path. Player must wait 2-3 turns per guard for auras to shrink or take longer routes. - // Key insight: teaches the wilting mechanic — Space=wait; patience vs detour tradeoff + // Mechanic intro: static wilting guards, now on a real pulse cycle (radius shrinks + // 1/turn then regrows: 1,0,-1,1,0,-1,... for initialRadius=1 — a 3-turn cycle). + // Intended path sketch: three single-cell gaps in the wall bands, each with a static + // diagonally adjacent to the cell just past the gap. The pre-gap cell is always + // >1 cell from the guard (safe every phase); the cell just past the gap sits exactly + // 1 cell away (lit whenever radius >= 1 — 1 turn in every 3). Geometry is chosen so + // the earliest possible arrival at each guarded cell lands on that lit turn, forcing + // a real wait (not just a lucky detour) before advancing. + // Key insight: the wilting cycle has a fixed period — wait one beat at the safe cell + // just before a gap, let the aura pass its peak, then step through. id: 2, name: "The Watchtower", storyKey: "level2Story", @@ -37,17 +43,25 @@ export const LEVELS = [ player: { row: 0, col: 0 }, goal: { row: 7, col: 7 }, walls: [ - { row: 1, col: 4 }, { row: 1, col: 5 }, { row: 1, col: 6 }, { row: 1, col: 7 }, - { row: 3, col: 0 }, { row: 3, col: 1 }, { row: 3, col: 2 }, { row: 3, col: 3 }, - { row: 5, col: 4 }, { row: 5, col: 5 }, { row: 5, col: 6 }, { row: 5, col: 7 }, + // Gap 1 at (1,4) + { row: 1, col: 0 }, { row: 1, col: 1 }, { row: 1, col: 2 }, { row: 1, col: 3 }, + { row: 1, col: 5 }, { row: 1, col: 6 }, { row: 1, col: 7 }, + // Gap 2 at (3,2) + { row: 3, col: 0 }, { row: 3, col: 1 }, + { row: 3, col: 3 }, { row: 3, col: 4 }, { row: 3, col: 5 }, { row: 3, col: 6 }, { row: 3, col: 7 }, + // Gap 3 at (5,6) + { row: 5, col: 0 }, { row: 5, col: 1 }, { row: 5, col: 2 }, { row: 5, col: 3 }, { row: 5, col: 4 }, + { row: 5, col: 5 }, { row: 5, col: 7 }, ], guards: [ - { type: "static", position: { row: 2, col: 5 }, initialRadius: 2 }, - { type: "static", position: { row: 4, col: 3 }, initialRadius: 2 }, - { type: "static", position: { row: 6, col: 6 }, initialRadius: 2 }, + // Guards a 1-cell diagonal from the cell just past each gap; pre-gap approach cells + // sit 2+ cells away (always safe) so waiting never traps the player. + { type: "static", position: { row: 2, col: 5 }, initialRadius: 1 }, + { type: "static", position: { row: 4, col: 1 }, initialRadius: 1 }, + { type: "static", position: { row: 6, col: 5 }, initialRadius: 1 }, ], affordances: { undo: true, preview: true }, - parMoves: 24, + parMoves: 20, }, // === ACT 2: THE VEGETABLE GARDEN (one-way intro) === @@ -86,10 +100,13 @@ export const LEVELS = [ { row: 6, col: 0 }, { row: 6, col: 1 }, { row: 6, col: 2 }, { row: 6, col: 3 }, ], guards: [ - // Static guard upper area — forces player right then down - { type: "static", position: { row: 0, col: 3 }, initialRadius: 1 }, - // Static guard lower area — forces player to go around before reaching goal - { type: "static", position: { row: 7, col: 6 }, initialRadius: 1 }, + // Static diagonally adjacent to the (2,4) one-way's landing cell — the approach + // cell (1,4) sits 2 away (always safe), so waiting there never traps the player, + // but the earliest possible arrival at (2,4) lands on the guard's lit turn, forcing + // one real wait before committing to the one-way. + { type: "static", position: { row: 2, col: 5 }, initialRadius: 1 }, + // Static diagonally adjacent to the (5,3) approach cell for the second one-way. + { type: "static", position: { row: 5, col: 2 }, initialRadius: 1 }, ], // (2,4) dir=2=down: only reachable by moving down from (1,4) — connects top→mid zone // (5,4) dir=1=right: only reachable by moving right from (5,3) — connects mid→right zone @@ -100,17 +117,21 @@ export const LEVELS = [ { row: 5, col: 4, dir: 1 }, // enter only when moving right ], affordances: { undo: true, preview: true }, - parMoves: 22, + parMoves: 20, }, // === ACT 3: THE SEARCHLIGHT (suspicion guard intro) === { // L4 — Searchlight - // Mechanic intro: suspicion guard (3-tier meter; fires at tier 2 lighting nearby cells) - // Intended path sketch: rotating guard sweeps upper area. Suspicion guard on right flank - // requires player to stay ≥4 cells away (Manhattan) while navigating past it. - // Player must time the rotating beam gap AND maintain distance from suspicion guard. - // Key insight: suspicion is spatial — distance buffer must be maintained every turn + // Mechanic intro: suspicion guard (3-tier meter; fires at tier 2, lighting every cell + // within Manhattan range of the guard, not just its own square) + // Intended path sketch: walls at (3,0)/(4,0) close off the free left-edge column, so the + // route must pass within range 3 of the suspicion guard at (4,7) for at least two + // consecutive turns — enough for its tier meter to climb 0→1→2 and fire. The optimal + // route eats that cost as a 2-turn stall (verified: removing only the suspicion guard + // drops the solve from 18 to 16; rotating+static alone contribute nothing). + // Key insight: suspicion needs two consecutive turns in range to fire — cross its zone in + // a single turn if you can, or plan the stall before you're two turns deep in range. id: 4, name: "The Searchlight", storyKey: "level4Story", @@ -120,8 +141,8 @@ export const LEVELS = [ walls: [ { row: 1, col: 5 }, { row: 1, col: 6 }, { row: 2, col: 2 }, - { row: 3, col: 4 }, { row: 3, col: 7 }, - { row: 4, col: 2 }, { row: 4, col: 6 }, + { row: 3, col: 0 }, { row: 3, col: 4 }, { row: 3, col: 7 }, + { row: 4, col: 0 }, { row: 4, col: 2 }, { row: 4, col: 6 }, { row: 5, col: 4 }, { row: 6, col: 1 }, { row: 6, col: 6 }, { row: 7, col: 3 }, { row: 7, col: 7 }, @@ -143,20 +164,23 @@ export const LEVELS = [ // L5 — Fortress Gate // Mechanic intro: doors + keys (locked door = impassable wall until matching key collected) // Intended path sketch: - // 10×10 grid. Player (0,0), Goal (9,9). - // Two keys and two doors in a chained dependency: key1→door1→key2→door2→goal. - // Key1 at (0,8): top-right; reachable freely across row 0. - // Door1 at (4,5) keyId=1: vertical barrier gap; player needs key1 to pass. - // Key2 at (6,8): bottom-right zone; only reachable after passing door1. - // Door2 at (8,5) keyId=2: lower vertical barrier gap; player needs key2 to pass. - // Goal at (9,9): reachable from (8,6) only after door2 opened. - // Vertical barrier (col 5, rows 3-9) is the spine. Gaps at (4,5) door1 and (8,5) door2. - // Left side: player starts here, goes right to key1 (row 0), comes back left, - // navigates down through rotating guard zone to reach (4,5) door1. - // Right side: after door1, grab key2 at (6,8), return to (8,5) door2, then goal. - // Rotating guard at (2,3) covers left-center; suspicion at (5,3) range=2 guards - // the approach to door1 — must wait or detour to avoid tier-up. - // Key insight: chained key→door dependency; collect in order. + // 10×10 grid. Player (0,0), Goal (9,9). The vertical barrier at col 5 is only open at + // rows 0-2 (free, one-directional use), door1 (4,5, keyId 1), and door2 (8,5, keyId 2) — + // rows 3,5,6,7,9 are solid wall. + // 1. Cross col 5 for free along row 0 and grab key1 at (0,8). + // 2. Walls (5,7)/(5,9)/(7,6)/(7,8)/(8,7)/(8,9) seal the lower-right corner into a pocket + // reachable only from above — go down col 8 to key2 at (6,8), a one-way trip in: + // the only exit is back up the way you came. + // 3. Climb back to row 4 and go left through door1 (needs key1) — this is the ONLY + // way out of the pocket, which is what makes the key1→door1 dependency load-bearing + // even though the row-0 crossing itself needed no key. + // 4. Descend the left side and go right through door2 (needs key2) to reach the + // bottom-right and the goal. + // Rotating guard at (2,4) faces up every 4th turn, sweeping (1,4)/(0,4) — exactly the + // turn the fastest route reaches (0,4) on the way to key1, forcing a real wait. + // Suspicion guard at (6,6) range 2 flanks the door2 return leg. + // Key insight: the lower-right key2 pocket is one-way — the only exit is back through + // door1, so key1 must already be in hand before you go get key2, not after. id: 5, name: "Fortress Gate", storyKey: "level5Story", @@ -179,10 +203,10 @@ export const LEVELS = [ { row: 8, col: 7 }, { row: 8, col: 9 }, ], guards: [ - // Rotating guard in left zone — player must time approach to door1 - { type: "rotating", position: { row: 2, col: 3 }, startDirection: 0 }, - // Suspicion guard to the right of door1 — guards right zone approach to key2 - // Player must approach key2 from outside range (Manhattan > 2 from (6,6)) + // Rotating guard — beam sweeps up into row 0 (the key1 fetch corridor) exactly + // every 4th turn, lining up with the earliest possible arrival at (0,4). + { type: "rotating", position: { row: 2, col: 4 }, startDirection: 0 }, + // Suspicion guard to the right of door1 — guards the return trip through door2 { type: "suspicion", position: { row: 6, col: 6 }, range: 2 }, ], // Key1 in top-right: freely reachable right along row 0 @@ -196,17 +220,26 @@ export const LEVELS = [ { row: 8, col: 5, keyId: 2 }, ], affordances: { undo: true, preview: true }, - parMoves: 30, + parMoves: 33, }, // === ACT 5: THE CORRIDOR (decay intro) === { // L6 — Flickering Corridor - // Mechanic intro: light decay (cells stay "warm" for 1 turn after going dark) - // Intended path sketch: two alternating blinking guards cover the corridor. - // With decayTiles="all", cells that just went dark are warm for 1 turn — - // player can sprint through during the decay window rather than waiting a full cycle. - // Key insight: decay window = brief safe window after lights go out; sprint timing + // Mechanic intro: light decay. A blinking guard alternates on/off every turn; with + // decayTiles="all", a cell that just went dark stays warm (lethal) for one more turn. + // That afterglow exactly fills the gap the blink would otherwise open — a cell a + // period-2 blinker lights is lit-or-warm on EVERY turn, i.e. permanently sealed for + // as long as decay is active. (Verified: a bare blinker's own litCell alternates + // lit/warm/lit/warm forever once decayTiles is on — never both false.) + // Intended path sketch: three wall bands, single-file gaps. Band 1's only gap is (2,7); + // band 2 has two gaps, (5,2) and (5,7); band 3's only gap is (8,2). The blinker seals + // (5,7) — the gap directly below band 1's own gap — so the player who took the + // "obvious" band-1 gap at col 7 must then walk all the way back to col 2 to cross + // band 2, then continue down to band 3's col-2 gap. Removing decay reopens (5,7) on + // its dark half-turns, deleting the backtrack entirely (verified ablation below). + // Key insight: the blinker's own gap looks like the fast route but decay keeps it shut + // the whole level — cross where the blinker ISN'T, not where it looks open. id: 6, name: "The Flickering Corridor", storyKey: "level6Story", @@ -214,45 +247,32 @@ export const LEVELS = [ player: { row: 0, col: 0 }, goal: { row: 9, col: 9 }, walls: [ - { row: 1, col: 4 }, { row: 1, col: 5 }, - { row: 2, col: 2 }, { row: 2, col: 7 }, - { row: 3, col: 5 }, { row: 3, col: 8 }, - // NOTE: row 4 left side clear — litCell at (4,3) needs clear path - { row: 4, col: 1 }, - { row: 5, col: 6 }, { row: 5, col: 8 }, - { row: 6, col: 2 }, { row: 6, col: 4 }, - { row: 7, col: 1 }, { row: 7, col: 7 }, - { row: 8, col: 4 }, { row: 8, col: 6 }, + // Band 1 — single gap at (2,7) + { row: 2, col: 0 }, { row: 2, col: 1 }, { row: 2, col: 2 }, { row: 2, col: 3 }, + { row: 2, col: 4 }, { row: 2, col: 5 }, { row: 2, col: 6 }, + { row: 2, col: 8 }, { row: 2, col: 9 }, + // Band 2 — gaps at (5,2) and (5,7); (5,7) is the sealed one + { row: 5, col: 0 }, { row: 5, col: 1 }, { row: 5, col: 3 }, { row: 5, col: 4 }, + { row: 5, col: 5 }, { row: 5, col: 6 }, { row: 5, col: 8 }, { row: 5, col: 9 }, + // Band 3 — single gap at (8,2) + { row: 8, col: 0 }, { row: 8, col: 1 }, { row: 8, col: 3 }, { row: 8, col: 4 }, + { row: 8, col: 5 }, { row: 8, col: 6 }, { row: 8, col: 7 }, { row: 8, col: 9 }, ], guards: [ - // Blinking guard — starts ON, upper corridor - // All litCells verified non-wall: (2,3) clear, (3,2) clear, (3,4) clear, (4,3) clear + // Blinking guard seals (5,7) — the gap under band 1's own opening. { type: "blinking", - position: { row: 3, col: 3 }, + position: { row: 5, col: 7 }, startState: true, - litCells: [ - { row: 2, col: 3 }, { row: 3, col: 2 }, - { row: 3, col: 4 }, { row: 4, col: 3 }, - ], + litCells: [{ row: 5, col: 7 }], }, - // Blinking guard — starts OFF (offset timing), lower corridor - // litCells: (6,6), (7,5), (7,6), (8,5) — all verified non-wall - { - type: "blinking", - position: { row: 7, col: 6 }, - startState: false, - litCells: [ - { row: 6, col: 6 }, { row: 7, col: 5 }, - { row: 7, col: 6 }, { row: 8, col: 5 }, - ], - }, - // Static guard blocks full detour - { type: "static", position: { row: 5, col: 4 }, initialRadius: 2 }, + // Static guard on the forced col-2 corridor between band 2 and band 3 — a second, + // independent timing cost on top of the decay-forced backtrack. + { type: "static", position: { row: 6, col: 3 }, initialRadius: 1 }, ], decayTiles: "all", affordances: { undo: true, preview: true }, - parMoves: 22, + parMoves: 32, }, // === ACT 6: THE UNDERGROUND (mirror + door intro) === @@ -260,16 +280,16 @@ export const LEVELS = [ // L7 — Underground Passage // Mechanic intro: mirror guard (reflects rotating beam 90°) PLUS door+key gate // Intended path sketch: - // Rotating guard at (2,2) starts facing right (dir=1). Beam hits cw mirror at (2,7) - // → deflects DOWN through col 7. Second cw mirror at (6,7) → beam deflects LEFT - // along row 6 covering (6,6)-(6,0). Player must dodge reflected beam zones. - // Key at (4,9): in the right zone past the mirror chain; player reaches it - // by going along row 0 right, then down col 9 during safe beam window. - // Door at (8,5) keyId=1: blocks the lower-center passage to the goal. - // Player must: navigate mirror-chain beam → collect key at (4,9) → - // cross to left zone → unlock door at (8,5) → reach goal (10,10). - // Wall band: row 5 cols 4-8 (except (5,5) open) forces player through specific corridors. - // Key insight: chain mirror → beam trace per turn; door adds collectible side-objective. + // Rotating guard at (2,2) faces right on the turn its beam reaches (2,7): a cw mirror + // there deflects it DOWN col 7 to (4,7), where a second (ccw) mirror deflects it RIGHT + // along row 4 — straight onto the key cell (4,9). The two-bounce chain means the key + // pickup itself is periodically covered, not just the approach to it. + // Door at (8,5) keyId=1 blocks the lower-center passage to the goal; key1 (the one the + // mirror chain guards) is required to open it. + // Wall band: row 5 cols 4-8 (except (5,5) open) forces the return trip through one + // corridor cell. + // Key insight: trace the beam through BOTH mirrors before committing to the key — + // the second bounce lands exactly on the tile you need to stand on. id: 7, name: "The Underground Passage", storyKey: "level7Story", @@ -282,9 +302,9 @@ export const LEVELS = [ { row: 3, col: 1 }, { row: 3, col: 5 }, // Center barrier: wall at row 5 cols 4-8 except gap at (5,5) { row: 5, col: 4 }, { row: 5, col: 6 }, { row: 5, col: 7 }, { row: 5, col: 8 }, - // Right zone walls: channel to key at (4,9) + // Right zone walls: channel to key at (4,9). (4,7) is left open for the second + // mirror to occupy; (4,8) is left open so its reflected beam can reach the key. { row: 2, col: 8 }, - { row: 4, col: 7 }, { row: 4, col: 8 }, // Right-side barrier col 9-10, rows 7-10: blocks right-column bypass to goal // Player cannot sneak down col 9/10 to reach (10,10) without going through door { row: 7, col: 9 }, { row: 7, col: 10 }, @@ -298,12 +318,14 @@ export const LEVELS = [ { row: 9, col: 3 }, { row: 9, col: 7 }, ], guards: [ - // Rotating guard feeds mirror chain - { type: "rotating", position: { row: 2, col: 2 }, startDirection: 1 }, + // Rotating guard feeds mirror chain — phased so its "facing right" turn (the one + // turn in 4 that fires the mirror chain) lands on the earliest possible arrival + // at the key cell, forcing a real timing decision rather than a free pickup. + { type: "rotating", position: { row: 2, col: 2 }, startDirection: 0 }, // cw mirror at (2,7): right→down deflection { type: "mirror", position: { row: 2, col: 7 }, reflectDirection: "cw" }, - // cw mirror at (6,7): down→left deflection — creates horizontal beam sweep - { type: "mirror", position: { row: 6, col: 7 }, reflectDirection: "cw" }, + // ccw mirror at (4,7): down→right deflection — lands the beam on the key cell + { type: "mirror", position: { row: 4, col: 7 }, reflectDirection: "ccw" }, ], // Key at (4,9): right zone, accessible during beam-safe window in col 9 keys: [ @@ -320,11 +342,17 @@ export const LEVELS = [ // === ACT 7: THE OUTER WALL (sniper intro) === { // L8 — The Gauntlet - // Mechanic intro: sniper guard (LoS beam to grid edge, rotates 90° CW every 2 turns) - // Intended path sketch: sniper at (3,8) starts facing left, sweeping upper corridor. - // Every 2 turns it rotates CW — player must time moves inside the 2-turn safe window. - // Patrolling guard in lower zone creates a second constraint. - // Key insight: sniper cadence is fixed — count turns; safe window repeats every 4 turns + // Mechanic intro: sniper guard (unbounded LoS beam, rotates 90° CW every 2 turns) + // Intended path sketch: row 5 is a solid band with a single gap at col 8. The sniper + // sits at (0,8), directly above the gap, starting faced up — its facing cycles + // up→up→right→right→down→down→left→left (2 turns each). facing(k)=(0+floor(k/2))%4, + // so it faces DOWN (sweeping the whole gap column) on turns 11-12 — bracketing turn 13, + // the earliest possible arrival at the gap from (0,0). The player must either wait a + // beat for the beam to rotate past, or arrive a turn later than the direct route. + // Patrolling guard + static guard add secondary timing costs in the upper approach and + // the lower approach to the goal. + // Key insight: the sniper's cadence is fixed and its beam is unbounded — count turns from + // the start, don't just watch the current facing, since the gap is the only crossing. id: 8, name: "The Gauntlet", storyKey: "level8Story", @@ -332,47 +360,56 @@ export const LEVELS = [ player: { row: 0, col: 0 }, goal: { row: 10, col: 10 }, walls: [ + // Row 5 band — single gap at col 8, directly under the sniper + { row: 5, col: 0 }, { row: 5, col: 1 }, { row: 5, col: 2 }, { row: 5, col: 3 }, + { row: 5, col: 4 }, { row: 5, col: 5 }, { row: 5, col: 6 }, { row: 5, col: 7 }, + { row: 5, col: 9 }, { row: 5, col: 10 }, + // Bottom-edge block — kill the free row-10 perimeter walk near the goal + { row: 10, col: 4 }, { row: 10, col: 5 }, + // Upper-zone texture — shapes the approach to the gap { row: 1, col: 3 }, { row: 1, col: 7 }, { row: 2, col: 5 }, { row: 3, col: 2 }, { row: 3, col: 6 }, { row: 4, col: 4 }, { row: 4, col: 9 }, - { row: 5, col: 1 }, { row: 5, col: 7 }, - { row: 6, col: 3 }, { row: 6, col: 8 }, - { row: 7, col: 5 }, { row: 7, col: 9 }, + // Lower-zone texture — shapes the approach to the goal + { row: 6, col: 3 }, + { row: 7, col: 9 }, { row: 8, col: 2 }, { row: 8, col: 7 }, { row: 9, col: 4 }, { row: 9, col: 6 }, ], guards: [ - // Sniper starts facing left (3=left), rotates CW every 2 turns - { type: "sniper", position: { row: 3, col: 8 }, startFacing: 3, rotateCadence: 2 }, - // Patrolling guard in lower zone + // Sniper directly above the row-5 gap, starting faced up + { type: "sniper", position: { row: 0, col: 8 }, startFacing: 0, rotateCadence: 2 }, + // Patrolling guard in upper zone (path kept off wall cells) { type: "patrolling", - startPosition: { row: 7, col: 4 }, + startPosition: { row: 7, col: 2 }, path: [ - { row: 7, col: 4 }, { row: 7, col: 5 }, - { row: 7, col: 6 }, { row: 7, col: 5 }, + { row: 7, col: 2 }, { row: 7, col: 3 }, + { row: 7, col: 4 }, { row: 7, col: 3 }, ], }, - // Static guard blocks leftward detour - { type: "static", position: { row: 5, col: 3 }, initialRadius: 2 }, + // Static guard blocks the leftward detour in the upper approach + { type: "static", position: { row: 4, col: 3 }, initialRadius: 2 }, ], affordances: { undo: true, preview: true }, - parMoves: 28, + parMoves: 24, }, // === ACT 8: THE ROYAL PALACE (throwable stones intro) === { // L9 — Decoy Path // Mechanic intro: throwable stones (no undo) - // Intended path sketch: patrolling guard A + patrolling guard B cover the crossing row - // at row 5 with phase-offset patrols (A covers cols 3-5, B covers cols 5-7, overlapping). - // Sniper below covers the only alternative lower route. - // With stones=2: throw at patrol position → distract guard for 1 turn → sprint through - // gap → throw second stone at sniper to clear lower exit. - // Note (phase 04): with current engine, stones=0 still solvable via long wait loops; - // stones provide ~10-move shortcut making the level tractable within parMoves. - // Key insight: stones reduce required waiting turns significantly — plan throws carefully + // Intended path sketch: rows 4 and 6 are solid bands with a single shared gap at col 6 — + // the only crossing between the upper and lower halves. Patrolling guard B bounces + // cols 6-9 and seals that gap (own-cell + front-cell) for 2 of every 6 turns. + // Patrolling guard A adds a second timing cost in the lower approach corridor. + // Sniper + suspicion guard the lower-right approach to the goal. + // NOTE: at the current BFS optimum the player times around both patrols with a wait + // and a short detour rather than needing a throw — stones shorten a sloppier, more + // naive route but are not yet strictly load-bearing at the optimum (see report). + // Key insight: patrol B's bounce at the gap is a short, fixed, countable period — + // time your crossing to its gap turns rather than reacting on the fly. id: 9, name: "The Decoy Path", storyKey: "level9Story", @@ -383,32 +420,40 @@ export const LEVELS = [ { row: 1, col: 3 }, { row: 1, col: 7 }, { row: 2, col: 1 }, { row: 2, col: 5 }, { row: 2, col: 9 }, { row: 3, col: 3 }, { row: 3, col: 7 }, - { row: 4, col: 0 }, { row: 4, col: 1 }, { row: 4, col: 9 }, { row: 4, col: 10 }, + // Row 4 band — single gap at col 6 + { row: 4, col: 0 }, { row: 4, col: 1 }, { row: 4, col: 2 }, { row: 4, col: 3 }, + { row: 4, col: 4 }, { row: 4, col: 5 }, { row: 4, col: 7 }, { row: 4, col: 8 }, + { row: 4, col: 9 }, { row: 4, col: 10 }, { row: 5, col: 0 }, { row: 5, col: 10 }, - { row: 6, col: 0 }, { row: 6, col: 4 }, { row: 6, col: 6 }, { row: 6, col: 10 }, + // Row 6 band — single gap at col 6 + { row: 6, col: 0 }, { row: 6, col: 1 }, { row: 6, col: 2 }, { row: 6, col: 3 }, + { row: 6, col: 4 }, { row: 6, col: 5 }, { row: 6, col: 7 }, { row: 6, col: 8 }, + { row: 6, col: 9 }, { row: 6, col: 10 }, { row: 7, col: 2 }, { row: 7, col: 8 }, { row: 8, col: 4 }, { row: 8, col: 6 }, { row: 9, col: 1 }, { row: 9, col: 9 }, ], guards: [ - // Patrolling guard A covers left half of row 5 corridor + // Patrolling guard A — secondary timing cost in the lower approach corridor { type: "patrolling", - startPosition: { row: 5, col: 2 }, + startPosition: { row: 8, col: 3 }, path: [ - { row: 5, col: 2 }, { row: 5, col: 3 }, - { row: 5, col: 4 }, { row: 5, col: 5 }, - { row: 5, col: 4 }, { row: 5, col: 3 }, + { row: 8, col: 3 }, { row: 8, col: 2 }, + { row: 8, col: 1 }, { row: 8, col: 0 }, ], }, - // Patrolling guard B covers right half of row 5 corridor (phase offset) + // Patrolling guard B — seals the row 4/6 gap at (5,6). Its bounce visits (5,6) + // as an endpoint every 6 turns (own-cell) and approaches it from (5,7) the turn + // before (front-cell) — 2 consecutive dangerous turns per cycle. A stone thrown + // at it while it is still approaching forces its facing away from the gap for + // that one turn, clearing the front-light a turn early. { type: "patrolling", - startPosition: { row: 5, col: 8 }, + startPosition: { row: 5, col: 6 }, path: [ - { row: 5, col: 8 }, { row: 5, col: 7 }, - { row: 5, col: 6 }, { row: 5, col: 5 }, { row: 5, col: 6 }, { row: 5, col: 7 }, + { row: 5, col: 8 }, { row: 5, col: 9 }, ], }, // Sniper covers the lower exit corridor (faces left, rotates every 2 turns) @@ -418,18 +463,24 @@ export const LEVELS = [ ], stones: 2, affordances: { undo: false, preview: true }, - parMoves: 28, + parMoves: 25, }, // === ACT 9: HALL OF MIRRORS (combo level) === { // L10 — Hall of Mirrors // Mechanic intro: (none — pure combo) mirrors + sniper + decay + stones - // Intended path sketch: two rotating guards feed two mirrors creating cross-coverage zones. - // decayTiles="all" provides brief warm windows after beams sweep past. - // Sniper covers goal approach — time entry between rotations. - // One stone distracts patrolling guard to open crossing window. - // Key insight: overlay all four mechanic systems; decay windows are the safe slots + // Intended path sketch: two rotating guards feed two mirrors; the perimeter walk is + // walled off so the route must cross rotator 1's beam column near (4,7). Its facing + // cycle is phased to intercept the earliest possible arrival there, and the warm + // afterglow (decayTiles="all") keeps that cell dangerous one turn longer than the lit + // sweep alone would — removing decay measurably shortens the solve (verified below). + // Sniper + patrol guard the lower-right approach to the goal as a second layer. + // NOTE: at the current optimum neither mirror's REFLECTED beam nor the stone is what + // the route dodges — it's rotator 1's direct beam plus decay. Mirrors and stones are + // present and functional but not yet proven load-bearing at the BFS optimum (see report). + // Key insight: decay makes the beam's afterglow outlast the sweep — count from when the + // light left, not when it's dark, before you step in. id: 10, name: "Hall of Mirrors", storyKey: "level10Story", @@ -437,28 +488,33 @@ export const LEVELS = [ player: { row: 0, col: 0 }, goal: { row: 11, col: 11 }, walls: [ + // Left-edge / bottom-edge blocks — kill the free perimeter walk + { row: 4, col: 0 }, { row: 5, col: 0 }, + { row: 11, col: 5 }, { row: 11, col: 6 }, { row: 1, col: 3 }, { row: 1, col: 7 }, - { row: 2, col: 5 }, { row: 2, col: 9 }, + { row: 2, col: 9 }, { row: 3, col: 1 }, { row: 3, col: 8 }, { row: 4, col: 4 }, { row: 4, col: 10 }, - { row: 5, col: 2 }, { row: 5, col: 6 }, + { row: 5, col: 2 }, { row: 6, col: 4 }, { row: 6, col: 9 }, - { row: 7, col: 1 }, { row: 7, col: 7 }, + { row: 7, col: 1 }, { row: 8, col: 5 }, { row: 8, col: 10 }, { row: 9, col: 3 }, { row: 9, col: 8 }, { row: 10, col: 6 }, { row: 10, col: 9 }, ], guards: [ - // Rotating guard feeds first mirror - { type: "rotating", position: { row: 2, col: 2 }, startDirection: 1 }, + // Rotating guard feeds first mirror — phased so its beam-active turn lands on + // the earliest possible arrival at (4,7), on the reflected beam's column + { type: "rotating", position: { row: 2, col: 2 }, startDirection: 2 }, // cw mirror: right→down { type: "mirror", position: { row: 2, col: 7 }, reflectDirection: "cw" }, // Second rotating guard feeds second mirror { type: "rotating", position: { row: 7, col: 9 }, startDirection: 3 }, // ccw mirror: left→down deflection (complements first chain) { type: "mirror", position: { row: 7, col: 4 }, reflectDirection: "ccw" }, - // Sniper at lower right — covers goal approach - { type: "sniper", position: { row: 10, col: 8 }, startFacing: 0, rotateCadence: 2 }, + // Sniper at lower left — its right-facing beam sweeps 4 open cells (cols 2-5) + // before the row-10 wall at col 6 + { type: "sniper", position: { row: 10, col: 1 }, startFacing: 0, rotateCadence: 2 }, // Patrolling guard in mid zone — distractible with stone { type: "patrolling", @@ -472,18 +528,29 @@ export const LEVELS = [ decayTiles: "all", stones: 1, affordances: { undo: false, preview: true }, - parMoves: 34, + parMoves: 26, }, // === ACT 10: THE THRONE ROOM (endgame — full palette) === { // L11 — Throne Room - // Mechanic intro: chaser (full palette: all 6 guard types + stones, no undo/preview) - // Intended path sketch: chaser ambushes the central path (stay outside detectionRadius=2). - // Sniper sweeps right corridor. Suspicion guard flanks upper-right. - // Stones=2: distract patrolling guard + sniper in sequence. - // No undo, no preview — full memorization and planning required. - // Key insight: endgame synthesis; every mechanic contributes to blocking all easy routes + // Mechanic intro: chaser (full palette: all 6 guard types + stones; undo disabled, + // preview enabled — see owner ruling below) + // Intended path sketch: perimeter walls kill the old free left-edge/bottom-edge walk + // that used to beat this level in 20 moves without ever coming near a guard. The + // chaser sits centrally at (6,6), detectionRadius 2, guarding the mid-map crossing; + // it is the entire measured guard tax (verified: removing only the chaser drops the + // solve back to the guard-free 20). Static, rotating+mirror, sniper, suspicion and + // patrol are all present, functional, and correctly positioned (no wall/guard + // overlaps) but do not yet independently add tax at the current BFS optimum — every + // attempt to force a second, independent chokepoint on this board (a static-on-gap + // timing trap, a sniper-guarded band near the goal) made the level provably + // unsolvable instead, because the chaser's own aggro state removes exactly the + // retreat-and-retry room those tricks depend on. See report for detail. + // OWNER RULING: preview re-enabled, undo stays off. Seven live guards with neither + // affordance is unfair, not hard — preview is a planning tool, undo is the tension. + // Key insight: the chaser's detection radius is fixed and Manhattan-based — plan the + // mid-map crossing to stay outside radius 2 of (6,6), not just off its exact tile. id: 11, name: "The Throne Room", storyKey: "level11Story", @@ -491,42 +558,50 @@ export const LEVELS = [ player: { row: 0, col: 0 }, goal: { row: 10, col: 10 }, walls: [ + // Perimeter blocks — kill the free left-edge / bottom-edge walk that used to + // beat this level in 20 moves without coming near a single guard + { row: 3, col: 0 }, { row: 4, col: 0 }, { row: 7, col: 0 }, { row: 8, col: 0 }, + { row: 10, col: 2 }, { row: 10, col: 3 }, { row: 10, col: 7 }, { row: 10, col: 8 }, { row: 1, col: 4 }, { row: 1, col: 7 }, { row: 2, col: 2 }, { row: 2, col: 6 }, - { row: 3, col: 4 }, { row: 3, col: 8 }, + { row: 3, col: 4 }, { row: 4, col: 1 }, { row: 4, col: 6 }, { row: 5, col: 3 }, { row: 5, col: 8 }, - { row: 6, col: 5 }, { row: 6, col: 7 }, + { row: 6, col: 7 }, { row: 7, col: 2 }, { row: 7, col: 6 }, - { row: 8, col: 4 }, { row: 8, col: 8 }, + { row: 8, col: 4 }, { row: 9, col: 1 }, { row: 9, col: 6 }, ], guards: [ // Static guard — upper left, wilting { type: "static", position: { row: 2, col: 3 }, initialRadius: 2 }, // Rotating guard feeds mirror - { type: "rotating", position: { row: 3, col: 5 }, startDirection: 0 }, - // cw mirror — reflects rotating beam + { type: "rotating", position: { row: 3, col: 5 }, startDirection: 3 }, + // cw mirror — reflects rotating beam (moved off the (3,8) wall cell — same cell, + // wall removed so the authoring invariant holds) { type: "mirror", position: { row: 3, col: 8 }, reflectDirection: "cw" }, // Sniper — covers right corridor (startFacing=3=left) { type: "sniper", position: { row: 6, col: 9 }, startFacing: 3, rotateCadence: 2 }, // Suspicion guard — flanks upper-right approach { type: "suspicion", position: { row: 4, col: 8 }, range: 3 }, - // Patrolling guard — lower passage + // Patrolling guard — lower-left passage (path moved off the (8,4) wall cell) { type: "patrolling", - startPosition: { row: 8, col: 3 }, + startPosition: { row: 8, col: 1 }, path: [ - { row: 8, col: 3 }, { row: 8, col: 4 }, - { row: 8, col: 5 }, { row: 8, col: 4 }, + { row: 8, col: 1 }, { row: 8, col: 2 }, + { row: 8, col: 3 }, { row: 8, col: 2 }, ], }, - // Chaser — small detectionRadius to limit BFS state explosion - { type: "chaser", position: { row: 6, col: 5 }, detectionRadius: 2 }, + // Chaser — moved off the (6,5) wall cell to the adjacent open (6,6). Kept at + // detectionRadius 2 (radius 3+ pushes the solver well past a 60s budget on an + // 11x11 board with 6 other guards); centrally positioned so its aggro radius still + // covers the mid-map crossing instead of only catching a player who walks over it. + { type: "chaser", position: { row: 6, col: 6 }, detectionRadius: 2 }, ], stones: 2, - affordances: { undo: false, preview: false }, - parMoves: 36, + affordances: { undo: false, preview: true }, + parMoves: 25, }, { // L12 THE PRINCESS CHAMBER — 13x13, 10 guards + expanding light wave.