feat(levels): retune levels one through eleven for real difficulty

Levels were authored against guards that did nothing, so the finale fell
to a twenty-move walk down two edges. Guard tax is now non-zero on every
level with guards, and search depth ramps from 50 states on the opener to
251612 on the throne room.

- fix the perimeter-walk exploit on levels four, eight, nine, ten, eleven
- unblock the mirrors on seven and ten, move guards off wall cells
- phase-align rotating, sniper and static cycles to arrival turns
- tighten par so three stars is earned rather than free
- restore preview on eleven while leaving undo disabled

Level twelve is untouched and remains unsolvable by design.
This commit is contained in:
2026-07-26 00:35:10 +07:00
parent ab9da7666a
commit eeb3a711ee
+238 -163
View File
@@ -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.