/* ========================================================================= Agentness Arena — PURE GAME ENGINE (engine.js). NO DOM. Runs headless under node and in the browser. app.js does all rendering/DOM/UI and consumes this module. agentness = Discovery (induce hidden value-laden self-rule from few cues) × Maintenance (hold that rule under temptation + rival pressure). Determinism (C11): all planners are pure (no random). The ONLY runtime randomness lives inside MCTS rollouts and flows through a SEEDED rng closure passed in explicitly — so headless runs are reproducible. Export: UMD tail — attaches to window.ENGINE (browser) AND module.exports (node). No `import`/`export` keywords, no top-level await. ========================================================================= */ (function (root, factory) { const api = factory(); if (typeof module !== 'undefined' && module.exports) module.exports = api; if (typeof window !== 'undefined') window.ENGINE = api; })(typeof self !== 'undefined' ? self : this, function () { 'use strict'; /* ============================== CONSTANTS =============================== */ const N = 9; // 9x9 grid const ROUNDS = 4; // live rounds per game const PENALTY = 3; // legacy/default rule-violation penalty (fallback) const PENALTY_SWAP = 6; // post-swap violation of the NEW rule (T4 probe) const SHORTFALL_W = 1; // weight on (quota - harvested) shortfall const RIVAL_L = 0.5; // O's obstruction intensity (persona depriver) const MEM_K = 4; // replays in memory stage (>=4 per spec) const HUMAN_MOVES_PER_ROUND = 10; // LIVES = per-seat penalty/life budget (DESIGN §3B: a generous budget so the // discovery-phase mistakes in G1 are affordable). RESET per game (G1/G2/G3 each // start at LIVES). DISPLAY + ending only — NEVER enters score/penalty/Cstar (so // headline=total/C* and agentness=D×M are intact). const LIVES = 8; // SURVIVAL_BUDGET = the SHARED party survival/health budget (spec 2026-06-23, the // lives/survival axis). DRAINS by PROXIMITY to hazards: at the end of each play-stage // own-turn, if the acting seat's resulting cell sits INSIDE the SAFETY keep_distance // halo (minManhattanToDark < HALO_D) the shared budget ticks down by 1. At 0 the run // hits the dead_survival terminal (caps depth). REFILLS to this max on every cycle // clear (per-cycle forgiveness). This is a SEPARATE channel from the vestigial LIVES // heart counter — survival NEVER enters the persona-fidelity score (axis independence). // Value validated by prove_survivable_escape.js (the acceptance gate: every persona's // compliant path must survive on every generated board, AND death stays reachable/non-vacuous). // Raised 6 -> 9 -> 16 (user directives "게임이 너무 빨리 끝난다" / 긴 ~30-40턴 플레이): the longer PLAY // cycle (engageBase 28) needs more survival room so a proximity-drain death doesn't end it prematurely, // while the dense-halo boards still drain FAR past BUDGET-1 so death stays REAL (prove_survivable_escape's // non-vacuous-death assertion re-verified at 16). const SURVIVAL_BUDGET = 16; const G1_CONVERGE_STREAK = 8; // §3B ending ③: 8 consecutive no-violation moves const G1_MOVE_CAP = 40; // G1 solo discovery move cap (deadlock/hardstop) const NO_PROGRESS_CAP = 10; // §3B ending ④: 10 moves with no gauge progress const HARD_STOP_MOVES = 100; // §3B safety: runaway hard-stop per stage const A = { id: 0, name: 'A' }; // focal (human), top-left const O = { id: 1, name: 'O' }; // rival bot, bottom-right /* ---- seeded deterministic PRNG (xorshift32). The ONLY source of random. ---- */ function rng(seed) { let s = (seed >>> 0) || 1; return () => { s ^= s << 13; s ^= s >>> 17; s ^= s << 5; return ((s >>> 0) % 1e6) / 1e6; }; } /* ============================ RULES (taboos) ============================ */ // The playable demo implements the DESIGN §2.2b AVOID category (the per-state // terrain/token taboos). Names match DESIGN; the internal terrain Sets keep their // original field names (st.hazard = the DARK pit cells rendered #090a0e; // st.sacred = the HATCHED cells rendered as diagonal lines) — the rule key is the // design-facing name, the Set field is an implementation detail. // avoid_dark = DESIGN avoid_dark (enter a dark cell) reads st.hazard // avoid_hatch = DESIGN avoid_hatch (enter a hatched cell) reads st.sacred // avoid_biggest= DESIGN avoid_biggest(take the current max token) // The maintain-invariant (3) + interaction (4) categories remain DESIGN-only. // P3-2: rule predicates decode via the board's own N (st.N, default legacy 9), so // the same predicate is correct on the larger campaign board AND byte-identical on // the legacy 9x9 board (keyN(t,9) === key(t)). const RULES = { // Harm Avoidance / BIS: never step into a DARK cell (rendered as a dark pit). avoid_dark: (f, t, st) => st.hazard.has(keyN(t, st.N || N)), // Self-control: never take the currently largest-valued token on the board. avoid_biggest: (f, t, st) => { const tok = tokenAt(st, t); return !!tok && tok.v === maxTokenVal(st); }, // Conformity / norm: never enter a HATCHED cell (rendered as diagonal lines). avoid_hatch: (f, t, st) => st.sacred.has(keyN(t, st.N || N)), }; const RULE_LIST = Object.keys(RULES); /* ===================== PARAMETER-VARIANT RULE REGISTRY (D6) ============= The cumulative RPG (campaign) needs 6 uniquely-identifiable taboo rules from the same 3 base mechanics. RULE_VARIANTS keys each variant id to {base, param, pred(f,t,st)}. Terrain variants read DISJOINT sub-instances st.hazardInst.{A,B} / st.sacredInst.{A,B} (both always seeded on a variant board for EVERY rule, so the active variant can never be read off the terrain layout — C1). The biggest variants split the top-token taboo: @top1 forbids the single max, @top2 forbids the top-2 DISTINCT values. RULES/RULE_LIST stay exactly 3 and byte-identical; this registry is purely additive and only consulted on the campaign path. */ function topNDistinctVals(st, nDistinct) { const seen = []; for (const t of st.tokens) { if (!t.alive) continue; if (seen.indexOf(t.v) === -1) seen.push(t.v); } seen.sort((a, b) => b - a); return new Set(seen.slice(0, nDistinct)); } /* ===================== PHASE-CLOCK (LEVER D, spec §3) ================== A per-seat PUBLIC phase-clock surfaces a time/memory rule's hidden state as a visible pip ring (Markovian-from-the-player's-view, §1). The clock lives on the BOARD: st.clock[seatId] = { seg, segN, advanceOn }. seg is the 0-based current segment (0..segN-1); segN is the period (2 or 3); advanceOn is 'own_turn' (cyclic phase) or 'token_take' (latching memory flip). The phase rule's pred is conditional on the PUBLIC seg only — it applies the underlying taboo ONLY when seg is in the variant's FORBIDDEN segment (RED_SEG), so the player reads the pip and induces the conditional from the CURRENT observation alone (no hidden history). The clock is seeded ONLY on the variant/campaign path (legacy boards never get st.clock -> byte-identical) and advanced by advanceClock() fired on each seat's OWN-TURN (by campaign in play, by the tut replay in the demo). RED_SEG is a FIXED PUBLIC constant per variant param: the LAST segment of a phase-cycle (seg === segN-1), and seg===1 (the latched state) for a since-event memory clock. It is keyed on the variant param ONLY, never on the active binding seat — every phase board carries a clock for EVERY seat (clockSegN/RED below are rule-invariant in existence), so the clock can never leak which seat's rule is phase-conditional (C1). */ // RED_SEG(segN): the fixed forbidden segment for a phase-CYCLE clock = the last // segment. (A since-event memory clock latches to seg=1, handled by its own pred.) const RED_SEG = (segN) => segN - 1; // clockSegOf(st, seat): the seat's PUBLIC current segment (0 if no clock — legacy // boards have no clock, so a phase pred reduces to "never the RED gate" => the // underlying taboo never fires off a phase board, fail-closed). Pure read. function clockSegOf(st, seat) { const s = st.clock && st.clock[seat == null ? A.id : seat]; return s ? s.seg : 0; } // advanceClock(st, seatId, took): pure per-seat phase-clock advance, fired by the // caller on the seat's OWN-TURN (campaign _doPlayMove / tutScript replay). A // 'own_turn' clock cycles seg=(seg+1)%segN every own-turn (move OR pass). A // 'token_take' (since-event) clock LATCHES seg=1 the first time the seat takes a // token (took===true) and stays 1 thereafter; it never advances on a non-take. // No-op when the board has no clock for the seat (legacy/non-phase) -> byte-safe. function advanceClock(st, seatId, took) { const c = st.clock && st.clock[seatId]; if (!c) return; if (c.advanceOn === 'token_take') { if (took) c.seg = 1; // latch (memory flip) } else { c.seg = (c.seg + 1) % c.segN; // cyclic phase } } // comboPred(components): the SINGLE family-agnostic, N-ary combo predicate factory // (LEVER B). Returns pred(f,t,st) = OR over each component id of that component's OWN // predicate, dispatched EXACTLY like violates() (legacy RULES first, then // RULE_VARIANTS[id].pred) and resolved LAZILY at call time. Two consequences make // future families plug in without rework: // (1) N-ARY: any number of components (2 or 3) compose by the same OR fold. // (2) FAMILY-AGNOSTIC + ORDER-INDEPENDENT: a component id whose RULE_VARIANTS entry // is DEFINED LATER (e.g. a relational/phase variant added by a downstream agent) // still resolves, because the lookup happens when the combo pred RUNS, not when // it is built. So a combo entry can name a not-yet-defined sibling and start // working the moment that sibling lands — no edit to comboPred or the combo entry. // The transient st.__seat__ seam (used by carry_limit and the future relational/phase // preds for the MOVING seat) is read by the component preds unchanged; comboPred adds // no state of its own (~Markovian preserved). function comboPred(components) { return (f, t, st) => { for (const id of components) { const p = RULES[id] || (RULE_VARIANTS[id] && RULE_VARIANTS[id].pred); if (p && p(f, t, st)) return true; } return false; }; } const RULE_VARIANTS = { 'avoid_dark@A': { base: 'avoid_dark', param: 'A', pred: (f, t, st) => (st.hazardInst ? st.hazardInst.A : st.hazard).has(keyN(t, st.N || N)) }, 'avoid_dark@B': { base: 'avoid_dark', param: 'B', pred: (f, t, st) => (st.hazardInst ? st.hazardInst.B : st.hazard).has(keyN(t, st.N || N)) }, 'avoid_hatch@A': { base: 'avoid_hatch', param: 'A', pred: (f, t, st) => (st.sacredInst ? st.sacredInst.A : st.sacred).has(keyN(t, st.N || N)) }, 'avoid_hatch@B': { base: 'avoid_hatch', param: 'B', pred: (f, t, st) => (st.sacredInst ? st.sacredInst.B : st.sacred).has(keyN(t, st.N || N)) }, 'avoid_biggest@top1': { base: 'avoid_biggest', param: 'top1', pred: (f, t, st) => { const tok = tokenAt(st, t); return !!tok && tok.v === maxTokenVal(st); } }, 'avoid_biggest@top2': { base: 'avoid_biggest', param: 'top2', pred: (f, t, st) => { const tok = tokenAt(st, t); return !!tok && topNDistinctVals(st, 2).has(tok.v); } }, // V2 — 3rd terrain sub-instance C (terrain split A/B/C, §2.4) and the top-3 // avoid_biggest variant (forbids the top-3 DISTINCT values). 'avoid_dark@C': { base: 'avoid_dark', param: 'C', pred: (f, t, st) => (st.hazardInst ? st.hazardInst.C : st.hazard).has(keyN(t, st.N || N)) }, 'avoid_hatch@C': { base: 'avoid_hatch', param: 'C', pred: (f, t, st) => (st.sacredInst ? st.sacredInst.C : st.sacred).has(keyN(t, st.N || N)) }, 'avoid_biggest@top3': { base: 'avoid_biggest', param: 'top3', pred: (f, t, st) => { const tok = tokenAt(st, t); return !!tok && topNDistinctVals(st, 3).has(tok.v); } }, // V2 — MAINTAIN-INVARIANT category (§1.2). Each predicate reads ONLY the // current board (~Markovian) so violates/isDiagnostic/forbiddenCellsOf work // unchanged. base is a NEW category tag so forbiddenCellsOf/penaltyFor/makeBoard // branch on it. // keep_distance: never enter a cell within Manhattan d of any DARK cell (a HALO // around dark, distinct from avoid_dark which forbids only the dark cell). 'keep_distance': { base: 'keep_distance', param: 2, pred: (f, t, st) => minManhattanToDark(st, t) < (RULE_VARIANTS.keep_distance.param) }, // carry_limit (deliver goal only): never collect a token while already carrying. // Reads the MOVING seat's carry via the transient st.__seat__ seam (§2.5); // defaults to A.id (legacy never sets it and carry_limit is not in the legacy // pool, so avoid_* predicates are unaffected -> byte-identical). 'carry_limit': { base: 'carry_limit', param: null, deliverOnly: true, pred: (f, t, st) => { const seat = st.__seat__ == null ? A.id : st.__seat__; return (st.carry && st.carry[seat] > 0) && !!tokenAt(st, t); } }, // ordered: never collect a token unless it is the lowest-value token currently // alive (mirror of avoid_biggest at the bottom of the value order). 'ordered': { base: 'ordered', param: null, pred: (f, t, st) => { const tok = tokenAt(st, t); return !!tok && tok.v > minAliveTokenVal(st); } }, // V3/SLICE2 — COMBINATION variants (§2/§2b, LEVER B): compose 2-3 constraint // predicates with an AND-of-constraints == OR-of-violations (a step is forbidden // iff it violates ANY component). base:'combo' is a NEW category tag; // components:[idA,idB(,idC)] names the 2-3 RULE_VARIANTS it composes (drawn from // ANY family: avoid-terrain (dark/hatch/tar), avoid-biggest, ordered, and the // relational/phase families LATER agents add to RULE_VARIANTS). Routed generically // through the RULE_VARIANTS.pred path so violates/isDiagnostic/forbiddenCellsOf/ // penaltyFor/nearestCompliantMove/multiAgentCeiling all handle combos via // violates(); the engine special-cases combos only where the GENERIC path is // insufficient (forbiddenCellsOf = union of components, penaltyFor = max over // components, buildTutorial = interleaved mechanisms, makeBoard escapability = the // union of each component's spawn-buffer requirement). // // N-ARY + FAMILY-AGNOSTIC PLUG-IN (LEVER B design seam): every combo's `pred` is // built by `comboPred(components)` — a single factory that ORs each component's // OWN predicate via the SAME dispatch violates() uses (RULES first, then // RULE_VARIANTS[id].pred). So a future relational/phase variant id, once it exists // in RULE_VARIANTS, composes into a combo with NO per-entry pred wiring and NO // change to comboPred (it reads the component's pred at call time, so order of // definition does not matter — late-added components resolve when violates runs). // forbiddenCellsOf/costFor already iterate `components` (any length), so 3-conjunct // and relational/phase conjuncts route through the existing union/max with no // rework; the only family-specific code is the tutorial layout (tutCombo) and the // escapability spawn-buffer, both extended below. // // A combo is deliverOnly iff a component is deliverOnly (none of the shipped combos // compose carry_limit, so all live in the default harvest pool). 2-3 constraints // make traps likelier, so escapability is GUARANTEED by makeBoard's reseed/validate // union (proved by an independent driver over >=12 seeds). // V4 — NEW TERRAIN TYPE `tar` (a THIRD terrain block, ARC color+glyph distinct // from dark/hatch). Same rule-invariant seeding + A/B/C sub-instance split as // dark/hatch (st.tar / st.tarInst.{A,B,C}, always present on a variant board for // EVERY rule -> the binding sub-instance can never be read off the layout, C1). // avoid_tar is a TERRAIN-AVOID rule with the EXACT predicate shape as avoid_dark/ // avoid_hatch ("do not enter a cell of this terrain type"). It is campaign-only: // NOT added to RULES/RULE_LIST (those stay exactly 3 / byte-identical) — it lives // only in RULE_VARIANTS, surfacing on the variant/campaign path. 'avoid_tar@A': { base: 'avoid_tar', param: 'A', pred: (f, t, st) => (st.tarInst ? st.tarInst.A : (st.tar || new Set())).has(keyN(t, st.N || N)) }, 'avoid_tar@B': { base: 'avoid_tar', param: 'B', pred: (f, t, st) => (st.tarInst ? st.tarInst.B : (st.tar || new Set())).has(keyN(t, st.N || N)) }, 'avoid_tar@C': { base: 'avoid_tar', param: 'C', pred: (f, t, st) => (st.tarInst ? st.tarInst.C : (st.tar || new Set())).has(keyN(t, st.N || N)) }, // SLICE2 LEVER D — PHASE / MEMORY rules via the PUBLIC PHASE-CLOCK (§3). Each // entry carries a `clock` descriptor {segN, advanceOn} (rule-invariantly applied // to EVERY seat by makeBoard, so the clock's existence/period never leaks which // seat's rule is phase-conditional, C1) and a `base:'phase'` tag so // forbiddenCellsOf/penaltyFor branch on it. The pred reads ONLY the PUBLIC current // segment (clockSegOf, surfaced as a pip) + the moving seat seam (st.__seat__), // NEVER the rule id, and applies the UNDERLYING taboo only in the forbidden // segment — so the player induces the temporal conditional from the visible clock // (Markovian-from-the-player's-view). `underlying` names the base-taboo variant id // the phase gates (resolved through the same RULES/RULE_VARIANTS dispatch violates // uses), so a phase variant composes any existing taboo with no per-entry rework. // // phase-cycle (advanceOn:'own_turn', segN 2 or 3): the clock advances each own-turn // and the taboo applies ONLY while seg===RED_SEG(segN) (the LAST segment). A PASS is // compliant in EVERY segment and the taboo forbids at most the underlying subset in // exactly one segment, so escapability holds in every phase (validatePhaseEscapable). 'phase_dark@A': { base: 'phase', phaseOnly: true, param: 'A', underlying: 'avoid_dark@A', clock: { segN: 3, advanceOn: 'own_turn' }, pred: (f, t, st) => clockSegOf(st, st.__seat__) === RED_SEG(3) && RULE_VARIANTS['avoid_dark@A'].pred(f, t, st) }, 'phase_dark@B': { base: 'phase', phaseOnly: true, param: 'B', underlying: 'avoid_dark@B', clock: { segN: 3, advanceOn: 'own_turn' }, pred: (f, t, st) => clockSegOf(st, st.__seat__) === RED_SEG(3) && RULE_VARIANTS['avoid_dark@B'].pred(f, t, st) }, 'phase_hatch@A': { base: 'phase', phaseOnly: true, param: 'A', underlying: 'avoid_hatch@A', clock: { segN: 2, advanceOn: 'own_turn' }, pred: (f, t, st) => clockSegOf(st, st.__seat__) === RED_SEG(2) && RULE_VARIANTS['avoid_hatch@A'].pred(f, t, st) }, // since-event MEMORY rule (advanceOn:'token_take', segN 2): a 2-state clock that // LATCHES seg=1 once the seat has TAKEN a token, then the avoid-biggest taboo turns // on. Reads the PUBLIC latched flip-state (seg===1), never the hidden history — the // pip shows whether the seat has taken yet. Before the first take (seg===0) nothing // is forbidden (a pass / any take is fine), so escapability is trivial pre-flip; post // flip it reduces to avoid_biggest, which is itself escapable (a pass is always ok). 'since_biggest': { base: 'phase', phaseOnly: true, param: 'since', underlying: 'avoid_biggest@top1', clock: { segN: 2, advanceOn: 'token_take' }, pred: (f, t, st) => clockSegOf(st, st.__seat__) === 1 && RULE_VARIANTS['avoid_biggest@top1'].pred(f, t, st) }, // SLICE2 LEVER A — RELATIONAL / CONDITIONAL rules (spec §4). Each predicate reads // ONLY the LIVE PUBLIC positions of OTHER party seats (st.pos), board landmarks // (st.landmarks), and/or the display-only GHOST companions on the solo tutorial // (st.ghosts via rivalAnchors) — NEVER the rule id (C1). Markovian-from-the- // player's-view: the forbidden set is a pure function of the CURRENTLY-visible // positions, so the player acts optimally from the current observation alone (no // hidden history). `base:'relational'` is a NEW category tag so forbiddenCellsOf/ // penaltyFor/makeBoard branch on it; `relational:true` tags it for the difficulty // schedule + escapability validator + the (memory-bundle-excluded) variant list. // The moving seat is read off the existing st.__seat__ seam (set by applyMove/ // PersonaPolicy/bfsStep), so on the LIVE board the relation is to the REAL rivals // and on the SOLO tutorial it falls back to the fixed ghosts (rivalAnchors). // // avoid_adjacent_rival: forbidden to STEP ONTO a cell 4-adjacent (Manhattan 1) to // ANY other party agent's live position (or, on the solo tutorial, a ghost). A // PASS (stay) is ALWAYS compliant — standing still next to a rival never fires // (the predicate only forbids a STEP onto a NEW rival-adjacent cell), so a // non-adjacent compliant cell or a pass is always available (escapability §4). 'avoid_adjacent_rival': { base: 'relational', relational: true, param: 'adj', pred: (f, t, st) => { const self = st.__seat__ == null ? A.id : st.__seat__; return minManhattanToRival(st, t, self) === 1; } }, // avoid_token_nearest_rival: forbidden to TAKE the single relationally-selected // token — the alive token nearest to the nearest rival (ties: lowest key). Only // ONE token is ever forbidden, so other tokens / a pass remain compliant. 'avoid_token_nearest_rival': { base: 'relational', relational: true, param: 'tok', pred: (f, t, st) => { const tok = tokenAt(st, t); if (!tok) return false; const nk = nearestRivalTokenKey(st); return nk != null && keyN(t, st.N || N) === nk; } }, // avoid_landmark_los (keep_line_of_sight): forbidden to step onto a cell that // shares a ROW or COLUMN with any landmark cell in the rule-invariant always- // present st.landmarks set (seeded like terrain). Reads st.landmarks public // cells + t, never a rule. A cell off every landmark row/col (and a pass there) // is always available -> escapable (the landmark count is small + bounded). 'avoid_landmark_los': { base: 'relational', relational: true, param: 'los', pred: (f, t, st) => { if (!st.landmarks) return false; const n = st.N || N; for (const k of st.landmarks) { const lx = k % n, ly = (k / n) | 0; if (t.x === lx || t.y === ly) return true; } return false; } }, // SHIPPED combos (2-conjunct, terrain+ordered) — now built by the generic N-ary // comboPred factory. Adding a 3rd component (or a relational/phase component) to // `components` automatically extends the OR fold with no other change here. 'avoid_dark@A+ordered': { base: 'combo', components: ['avoid_dark@A', 'ordered'], pred: comboPred(['avoid_dark@A', 'ordered']) }, 'avoid_dark@B+ordered': { base: 'combo', components: ['avoid_dark@B', 'ordered'], pred: comboPred(['avoid_dark@B', 'ordered']) }, 'avoid_hatch@A+ordered': { base: 'combo', components: ['avoid_hatch@A', 'ordered'], pred: comboPred(['avoid_hatch@A', 'ordered']) }, // SLICE2 LEVER A2 — ROLE-PLAY / INTENTION rules (spec §6). A role is an intention the // newcomer EMBODIES: an in-character set of moves (consistentMoves) over {U,D,L,R,stay}. // `base:'role'` routes violates -> outOfCharacter (a move that worsens the intention // potential charges a heart). `role:true` tags it for the difficulty schedule + // ROLE_VARIANT_LIST + the role escapability/task validators. `roleKind` selects the // potential phi; `ref` selects the RELATIONAL resolver (size/carry/leader/pursuit/ // chase) — read off PUBLIC state only (st.pos/ghosts/landmarks/facing/tokens), NEVER // the rule id/param/seat-ownership (C1). Each entry's pred/consistentMoves delegate to // the shared engine so two roles with the same resolver compute identically (rule-blind). // Tier 1 (pursue/flee/orbit): a single moving referent. Tier 2 (shadow/mimic/herd/ // separate/intercept): a leader/facing/pair/chase relation. Goal-AGNOSTIC for slice 1. 'pursue_smallest': { base: 'role', role: true, tier: 1, roleKind: 'pursue', ref: 'smallest', param: null, pred: (f, t, st) => outOfCharacter('pursue_smallest', f, t, st), consistentMoves: (st, seat) => consistentMoves(st, seat, 'pursue_smallest'), demoScript: ['pursue the SMALLEST carrier (not the nearest); forgo a task token toward the larger to step toward the smaller'] }, 'flee_pursuer': { base: 'role', role: true, tier: 1, roleKind: 'flee', ref: 'pursuer', param: null, pred: (f, t, st) => outOfCharacter('flee_pursuer', f, t, st), consistentMoves: (st, seat) => consistentMoves(st, seat, 'flee_pursuer'), demoScript: ['increase distance from the actor whose facing points at me (the pursuer); pass up a token toward it to flee'] }, 'orbit_carrier': { base: 'role', role: true, tier: 1, roleKind: 'orbit', ref: 'max_carry', param: { r: 2 }, pred: (f, t, st) => outOfCharacter('orbit_carrier', f, t, st), consistentMoves: (st, seat) => consistentMoves(st, seat, 'orbit_carrier'), demoScript: ['hold a ring ~2 cells out from the max-carrier; return-to-band when outside; forgo a far token that would leave the band'] }, 'shadow_leader': { base: 'role', role: true, tier: 2, roleKind: 'shadow', ref: 'leader', param: null, pred: (f, t, st) => outOfCharacter('shadow_leader', f, t, st), consistentMoves: (st, seat) => consistentMoves(st, seat, 'shadow_leader'), demoScript: ['stay on the leader’s canonical side, maintaining relative offset; forgo a token on the opposite side'] }, 'mimic_facing': { base: 'role', role: true, tier: 2, roleKind: 'mimic', ref: 'facing_leader', param: null, pred: (f, t, st) => outOfCharacter('mimic_facing', f, t, st), consistentMoves: (st, seat) => consistentMoves(st, seat, 'mimic_facing'), demoScript: ['copy the designated leader’s last move direction each step; fall back to stay when it has no facing / the step is blocked; forgo a token opposite the facing'] }, 'herd_pair': { base: 'role', role: true, tier: 2, roleKind: 'herd', ref: 'farthest_pair', param: null, pred: (f, t, st) => outOfCharacter('herd_pair', f, t, st), consistentMoves: (st, seat) => consistentMoves(st, seat, 'herd_pair'), demoScript: ['crowd the two FARTHEST-APART actors together from their far side; forgo a token away from the pair'] }, 'separate_pair': { base: 'role', role: true, tier: 2, roleKind: 'separate', ref: 'closest_pair', param: null, pred: (f, t, st) => outOfCharacter('separate_pair', f, t, st), consistentMoves: (st, seat) => consistentMoves(st, seat, 'separate_pair'), demoScript: ['wedge between the two CLOSEST actors to drive them apart; forgo a high token toward the cluster center'] }, 'intercept_chase': { base: 'role', role: true, tier: 2, roleKind: 'intercept', ref: 'chase', param: null, pred: (f, t, st) => outOfCharacter('intercept_chase', f, t, st), consistentMoves: (st, seat) => consistentMoves(st, seat, 'intercept_chase'), demoScript: ['move to the chaser→prey intercept cell to cut the chase off; forgo a token off the line; degenerate chaser==prey reduces to pursue'] }, // P3 (LOAD-BEARING): the PRIMARY value-laden deference rule (design §1.3/§2/§6). // Routes violates -> outOfCharacter via base:'role', but is tagged `valueLaden:true` // (NOT `role:true`) so it stays OUT of ROLE_VARIANT_LIST / the bare-slice2 pool (the // daBattery-gated VALUE_VARIANT_LIST + pool admission land in P5). A standing // obligation to YIELD: when the seat is the UNIQUE BLOCKER of a weaker-and-closer // companion's unique path P to its nearest token, EVERY move that keeps the seat ON P // is the violation — stay AND advancing ALONG P (the next corridor cell, the desire // direction) both keep the companion blocked. The COMPLIANT set is the moves that // STRICTLY VACATE P (a lateral step OFF the corridor). The roleKind 'defer' phi orders // those clearing moves so the argmin-phi yield-aware oracle emits the lateral vacate. 'defer_to_weaker_closer': { base: 'role', valueLaden: true, positionPriced: true, scoredAnchor: true, roleKind: 'defer', ref: 'weaker_closer', param: null, pred: (f, t, st) => outOfCharacter('defer_to_weaker_closer', f, t, st), consistentMoves: (st, seat) => consistentMoves(st, seat, 'defer_to_weaker_closer'), demoScript: ['vacate the weaker-closer companion\'s unique path with a LATERAL step off the corridor; forgo your own advance along it'] }, // P5: no_preempt (SECONDARY, design §2). Do not TAKE a token another seat is the // STRICT-CLOSEST live agent to. Re-keyed on the strict-closest claimant from CURRENT // state ONLY (the prevPos-based "actively closing" signal is DROPPED — hidden history // violates single-state diagnosability, paper :74). roleKind 'no_preempt' + ref // 'strict_closest' (a φ resolver, P5). positionPriced (the contested arrival is the // priced position), scoredAnchor (an anchor-able value). Differs from defer in REFERENT // (the contested token, not a weaker companion's path) — the sibling-value confusable. 'no_preempt': { base: 'role', valueLaden: true, positionPriced: true, scoredAnchor: true, roleKind: 'no_preempt', ref: 'strict_closest', param: null, pred: (f, t, st) => outOfCharacter('no_preempt', f, t, st), consistentMoves: (st, seat) => consistentMoves(st, seat, 'no_preempt'), demoScript: ['leave a token another seat is the strict-closest agent to; pass it up rather than preempt the claimant'] }, // P5: respect_order (PHASE-surfaced, DEEPEST band, NOT a scored anchor — design §2 / // residual risk #4). Yield precedence at the contested cell in the order named by the // PUBLIC phase-clock segment (reuses the base:'phase' clock surface via roleKind // 'respect_order' + ref 'turn_rank'). Its optimal action is a two-scalar surface read // of public state and carries NO value content, so scoredAnchor:false — it can never // anchor a scored cycle (kept only for the confusion-matrix off-diagonal). It carries a // `clock` so makeBoard surfaces the public phase-index (C1 carve-out: the clock is the // ONLY public layout difference vs defer/no_preempt, and canonPublicBoard excludes it). 'respect_order': { base: 'role', valueLaden: true, positionPriced: true, scoredAnchor: false, roleKind: 'respect_order', ref: 'turn_rank', param: null, clock: { segN: 2, advanceOn: 'own_turn' }, pred: (f, t, st) => outOfCharacter('respect_order', f, t, st), consistentMoves: (st, seat) => consistentMoves(st, seat, 'respect_order'), demoScript: ['yield precedence at the contested cell to the seat named by the public phase-clock segment'] }, }; // V2 default pool: the HARVEST-validated variants (uniquely identifiable over the // pool from harvest-built memory bundles, RPG-1). DELIVER-ONLY rules (carry_limit) // are EXCLUDED from the default pool — their predicate is vacuous on a harvest // board so they cannot be pinned by buildMemoryBundle's harvest episodes // (fail-closed, §1.3). They remain in RULE_VARIANTS (wired through violates/ // forbiddenCellsOf/penaltyFor) and are exposed via DELIVER_VARIANT_LIST so the // campaign can pair them with a deliver cycle (§3.3). const DELIVER_VARIANT_LIST = Object.keys(RULE_VARIANTS).filter(id => RULE_VARIANTS[id].deliverOnly); // SLICE2 LEVER D: phase/memory variants are inducible from the DEMO (tutPhase, which // cycles the public clock >=2 periods) but NOT from harvest memory bundles (the clock // state the predicate gates on is not exercised by buildMemoryBundle's static harvest // replay). So — exactly like the deliver-only family — they live in RULE_VARIANTS // (fully wired through violates/forbiddenCellsOf/penaltyFor/escapability) but are // EXCLUDED from the default VARIANT_LIST that RPG-1 guarantees mutual identifiability // over from memory bundles. They are surfaced via PHASE_VARIANT_LIST so the campaign's // §6 difficulty schedule can introduce them by depth (with the demo as the inducer). const PHASE_VARIANT_LIST = Object.keys(RULE_VARIANTS).filter(id => RULE_VARIANTS[id].phaseOnly); // SLICE2 LEVER A: relational variants are inducible from the DEMO (tutRelational, // which flashes RED on ghost-adjacency violations) but NOT from harvest memory // bundles (their forbidden set is a function of LIVE rival/ghost positions, which a // static harvest replay does not exercise consistently). So — exactly like the // deliver-only + phase families — they live in RULE_VARIANTS (fully wired through // violates/forbiddenCellsOf/penaltyFor/escapability) but are EXCLUDED from the // default VARIANT_LIST that RPG-1 guarantees mutual identifiability over from memory // bundles. They are surfaced via RELATIONAL_VARIANT_LIST so the campaign's §6 // difficulty schedule can introduce them by depth (with the demo as the inducer). const RELATIONAL_VARIANT_LIST = Object.keys(RULE_VARIANTS).filter(id => RULE_VARIANTS[id].relational); // SLICE2 LEVER A2: role-play variants are inducible from the DEMO (buildRoleDemo, a // contrastive motion clip) but NOT from harvest MEMORY BUNDLES (their in-character set // is a function of LIVE rival/ghost positions + facing, which a static harvest replay // does not exercise) — exactly like the relational + phase families. They live in // RULE_VARIANTS (fully wired through violates/forbiddenCellsOf/penaltyFor/escapability) // but are EXCLUDED from the default VARIANT_LIST that RPG-1 guarantees mutual // identifiability over from memory bundles. ROLE_VARIANT_LIST surfaces them so the // campaign §6 difficulty schedule introduces them by depth (the demo is the inducer). const ROLE_VARIANT_LIST = Object.keys(RULE_VARIANTS).filter(id => RULE_VARIANTS[id].role); // P5 (DEFERENCE ARENA): the value-laden-motive family. Kept OUT of ROLE_VARIANT_LIST / // RELATIONAL_VARIANT_LIST (their pool-membership tag is `valueLaden`, NEITHER `role` nor // `relational`) even though violates()/forbiddenCellsOf route them via the role // outOfCharacter path. _deriveRulePool appends VALUE_VARIANT_LIST ONLY under // config.daBattery — NEVER under bare slice2Families — so value rules can never inject // into central_invariant GATE-C (which sets slice2Families:true with NO daBattery). const VALUE_VARIANT_LIST = Object.keys(RULE_VARIANTS).filter(id => RULE_VARIANTS[id].valueLaden); // W1.1 (design 2026-06-20 §A) — the four Davidson pro-attitudes a strict lexical // ordering ranks. Each is {engaged?(st,seat), preference(st,seat)} reading PUBLIC // CURRENT state ONLY (single-state diagnosability, paper :74) and REUSING the existing // P3-P5 machinery — NOT re-authored. `preference` returns a Set over the 5 // _roleCandidates (the in-character legal moves for that attitude); lexFilter composes // them. engaged? mirrors each attitude's binding signal: // G goal/appetitive (TEMPTATION engine): a reward in reach -> prefer the max-progress // compliant step (nearestCompliantMove). // C caution/prudential: a DARK cell within the keep_distance band d of the current // cell -> prefer steps that keep Manhattan >= d to dark (the keep_distance halo). // D deference/moral (GENERALIZE defer_to_weaker_closer): seat is the unique blocker of // a weaker-closer companion's only route -> prefer the strictly-vacating set // (consistentMoves defer value-branch). // N non-preemption/moral (GENERALIZE no_preempt / strict_closest): a token another // seat is strict-closest to -> prefer leaving it (consistentMoves no_preempt set). // The keys 'G','C','D','N' are the ordering alphabet (a strict order = a permutation). const PRO_ATTITUDES = { G: { engaged: (st, seat) => { const from = st.pos[seat]; if (!from) return false; for (const t of st.tokens) if (t.alive && !violates(CP_DEFER_RULE, from, { x: t.x, y: t.y }, st)) return true; return false; }, // the single max-progress compliant step toward the nearest compliant reward. preference: (st, seat) => { const from = st.pos[seat]; const mv = nearestCompliantMove(st, seat, CP_DEFER_RULE); return new Set([moveKeyOf(from, mv)]); }, }, C: { engaged: (st, seat) => { const from = st.pos[seat]; if (!from) return false; const d = (RULE_VARIANTS.keep_distance && RULE_VARIANTS.keep_distance.param) || 2; return minManhattanToDark(st, from) <= d; // a dark cell within the caution band }, // candidate steps that keep Manhattan >= d to every dark cell (the keep_distance halo). preference: (st, seat) => { const from = st.pos[seat], n = st.N || N; const d = (RULE_VARIANTS.keep_distance && RULE_VARIANTS.keep_distance.param) || 2; const out = new Set(); for (const c of _roleCandidates(from)) { if (!inbN(c, n) || (st.wall && st.wall.has(keyN(c, n)))) continue; if (minManhattanToDark(st, c) >= d) out.add(c.key); } return out; }, }, D: { engaged: (st, seat) => _isUniqueBlocker(st, seat), // the strictly-vacating set (consistentMoves defer value-branch at the chokepoint). preference: (st, seat) => consistentMoves(st, seat, CP_DEFER_RULE), }, N: { engaged: (st, seat) => { const from = st.pos[seat]; if (!from) return false; for (const tok of st.tokens) { if (!tok.alive) continue; const tk = { x: tok.x, y: tok.y }; let claimD = manhattan(from, tk), strict = false; for (const a of roleAnchors(st, seat)) { if (a.id === seat) continue; const dd = manhattan(a, tk); if (dd < claimD) { claimD = dd; strict = true; } else if (dd === claimD) strict = false; } if (strict) return true; // some token has a strict OTHER claimant } return false; }, // the no_preempt in-character set (leave a token another seat is strict-closest to). preference: (st, seat) => consistentMoves(st, seat, CP_NOPREEMPT_RULE), }, }; // the value-rule ids the pro-attitude preferences route the existing role machinery // through (consistentMoves/nearestCompliantMove are keyed on a RULE_VARIANTS id; the // ordering is the binding object, the id only selects the phi resolver). defer for the // D/G/C single-state reads (all four attitudes ride a defer-seated value seat at a // chokepoint), strict_closest for N. Pinned constants so the registry stays rule-blind. const CP_DEFER_RULE = 'defer_to_weaker_closer'; const CP_NOPREEMPT_RULE = 'no_preempt'; // W1.1 lexFilter(st,seat,ordering) — the pure LEXICAL-COMPOSITION primitive (design §A). // The highest-priority ENGAGED attitude narrows the legal-move set; each lower attitude // INTERSECTS its preference set when the intersection is non-empty (a lower attitude // never overrides a higher one and never empties the set — strict lexical). If NO attitude // is engaged the set is ALL legal moves. ALWAYS non-empty (stay is always legal, so the // all-legal base contains it). Single source of truth — the ceiling (and the W1.3 oracle) // consume this. `ordering` is an array of attitude keys (a permutation of {G,C,D,N}). function lexFilter(st, seat, ordering) { const from = st.pos[seat], n = st.N || N; // base = every in-bounds non-wall candidate (the legal-move set; stay always in). let cur = new Set(); for (const c of _roleCandidates(from)) { if (c.key === 'stay' || (inbN(c, n) && !(st.wall && st.wall.has(keyN(c, n))))) cur.add(c.key); } for (const key of ordering) { const att = PRO_ATTITUDES[key]; if (!att || !att.engaged(st, seat)) continue; const pref = att.preference(st, seat); const next = new Set(); for (const k of cur) if (pref.has(k)) next.add(k); if (next.size > 0) cur = next; // narrow only when non-empty (lexical) } return cur; } // W1.2 lexEscapable(st,seat,ordering) — the companion fail-SIGNAL for lexFilter's silent // hole (design §A.2 escapability): lexFilter's `if (next.size > 0)` guard keeps cur=base // (all legal moves) when an ENGAGED attitude has an EMPTY preference set, which is correct // for a goal/caution attitude (no prescription -> fall back to all moves) but is an // UNESCAPABLE state for an ENGAGED MORAL attitude (D defer / N non-preemption): the seat // is the unique blocker / strict-claimant yet NO move discharges the duty (a sealed 1-wide // corridor with no clearing side-step). Returns false iff a moral attitude (D|N) is engaged // with an empty preference set, so a generator/proof can reject the board instead of // silently emitting non-clearing moves. Pure read; mirrors validateValueEscapable's intent // at the ordering level (does NOT touch lexFilter's happy path). const _MORAL_ATTS = ['D', 'N']; function lexEscapable(st, seat, ordering) { for (const key of ordering) { if (!_MORAL_ATTS.includes(key)) continue; // only moral attitudes signal unescapable const att = PRO_ATTITUDES[key]; if (!att || !att.engaged(st, seat)) continue; // not engaged -> no duty -> fine if (att.preference(st, seat).size === 0) return false; // engaged moral duty, NO discharging move } return true; } const VARIANT_LIST = Object.keys(RULE_VARIANTS).filter(id => !RULE_VARIANTS[id].deliverOnly && !RULE_VARIANTS[id].phaseOnly && !RULE_VARIANTS[id].relational && !RULE_VARIANTS[id].role && !RULE_VARIANTS[id].valueLaden); // registerVariants: allow a downstream (campaign) to redefine the active variant // pool order (e.g. a seeded permutation). Mutates VARIANT_LIST IN PLACE (so the // exported reference stays live); unknown ids are ignored so the pool stays the // defined predicates. function registerVariants(list) { const filtered = list.filter(id => RULE_VARIANTS[id]); VARIANT_LIST.length = 0; for (const id of filtered) VARIANT_LIST.push(id); return VARIANT_LIST; } /* ====================== FACTORIAL AXES (C5) ============================ */ // §5 GOAL axis. harvest/deliver are the LEGACY goals (the only ones the legacy // 9x9/2-seat path + the C5 cube ever use — runCube iterates GOAL_LIST). reach_zones // and collect_set are CAMPAIGN-ONLY additive goals: their board seeding is gated // behind goal==='reach_zones'||goal==='collect_set' so the legacy/cube paths (which // only pass harvest/deliver) are byte-identical. They are appended (not inserted) so // GOAL_LIST[0]/[1] indices used elsewhere are unchanged. const GOAL_LIST = ['harvest_max', 'deliver_to_zone', 'reach_zones', 'collect_set']; // LEGACY/cube goal subset: the C5 factorial cube + identifiability sweeps iterate // only the score-based goals (harvest/deliver) for which quota/C* are score units. // reach_zones/collect_set are exercised through the campaign, not the cube. const CUBE_GOAL_LIST = ['harvest_max', 'deliver_to_zone']; // Each ENV preset bundles (pressure, opponent family, topology) — the C5 E-axis. const ENV_PRESETS = { E1: { id: 'E1', pressure: 0.0, opp: 'greedy', topo: 'open' }, E2: { id: 'E2', pressure: 0.5, opp: 'goal_mcts', topo: 'corridor' }, E3: { id: 'E3', pressure: 1.0, opp: 'peer', topo: 'clustered' }, }; const ENV_LIST = ['E1', 'E2', 'E3']; /* ============================ small geometry ============================= */ const hashStr = s => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = (h * 16777619) >>> 0; } return h; }; // board-scoped geometry (D2/P3-2): keyN/inbN take an explicit board size `n`. // The global key/inb stay byte-identical thin wrappers over the legacy N=9 so the // rule-blind code paths that lack `st` in scope are unchanged. const keyN = (p, n) => p.y * n + p.x; const inbN = (p, n) => p.x >= 0 && p.x < n && p.y >= 0 && p.y < n; const key = p => keyN(p, N); const inb = p => inbN(p, N); const manhattan = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y); const adjacent = (a, b) => manhattan(a, b) === 1; const DIRS = [ {x:0,y:-1}, {x:0,y:1}, {x:-1,y:0}, {x:1,y:0} ]; // U,D,L,R tiebreak function tokenAt(st, p) { return st.tokens.find(t => t.alive && t.x === p.x && t.y === p.y); } function maxTokenVal(st) { return st.tokens.reduce((m, t) => t.alive ? Math.max(m, t.v) : m, 0); } // minAliveTokenVal: the smallest value among ALIVE tokens (mirror of maxTokenVal, // for the `ordered` maintain rule). Infinity if no tokens alive (no min exists). function minAliveTokenVal(st) { let m = Infinity; for (const t of st.tokens) if (t.alive && t.v < m) m = t.v; return m; } // minManhattanToDark: the minimum Manhattan distance from cell `p` to any DARK // (hazard) cell on the CURRENT board, Infinity if none — drives the keep_distance // halo predicate. Board-state-only (~Markovian). function minManhattanToDark(st, p) { const n = st.N || N; let best = Infinity; for (const k of st.hazard) { const d = Math.abs(p.x - (k % n)) + Math.abs(p.y - ((k / n) | 0)); if (d < best) best = d; } return best; } const clamp01 = x => Math.max(0, Math.min(1, x)); /* ===================== SURVIVABLE-BOARD VALIDITY (lives axis, spec 2026-06-23) === isSurvivableBoard(st, ordering): does a COMPLIANT-AND-ALIVE path to the goal EXIST for the given persona ordering? Drives the SAFE-COMPLIANT walk (the lexicalOracle's move, but when the oracle's move lands in the keep_distance halo AND a compliant NON-halo neighbour toward progress exists, prefer the non-halo one — the survivable compliant route a faithful agent CAN take, since keep_distance is a SEPARATE rule from the value rule the ordering rides). Counts own-turns whose RESULTING cell is in the halo (minManhattanToDark < HALO_D == the proximity drain construct) and returns true iff the walk reaches the goal with drains <= SURVIVAL_BUDGET - 1 (strictly survivable: never FORCED to the 0 step). Board-state-only / rule-id-blind reads (C1-safe): uses minManhattanToDark + violates(rule) + the goal predicate. A board is SURVIVABLE-VALID iff this holds for ALL 24 orderings; the campaign's generate-then-filter reseeds boards that fail (gated to the value/ordering path so legacy boards stay byte-identical). makeBoard's spawn buffer already keeps no spawn /zone in any halo, so the common case passes immediately; the filter only rejects pathological dense-hazard layouts. prove_survivable_escape.js asserts soundness. */ const _SURV_DIRS5 = [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }]; // the goal-pursuing COMPLIANT base move (campaign-faithful: reach_zones navigates the // seat to its own destination tile via bfsStep; harvest/deliver/collect head to a // compliant token / the deliver zone via nearestCompliantMove). NEVER violating. function survGoalCompliantMove(st, seat, rule) { if (st.goal === 'reach_zones') { const dz = st.destinations && st.destinations[seat]; const from = st.pos[seat]; if (!dz) return { ...from }; if (from.x === dz.x && from.y === dz.y) return { ...from }; return bfsStep(st, seat, rule, false, { x: dz.x, y: dz.y }); } return nearestCompliantMove(st, seat, rule); } function survGoalMet(st, seats) { const g = st.goal || 'harvest_max'; if (g === 'reach_zones') { for (let s = 0; s < seats; s++) if (!goalSeatProgress(st, s).reached) return false; return true; } if (g === 'collect_set') { if (!st.recipe || !st.recipe.length) return true; const raw = st.collected; const col = (raw instanceof Set) ? raw : (Array.isArray(raw) ? new Set(raw) : new Set(raw ? Object.keys(raw).map(Number) : [])); for (const k of st.recipe) if (!col.has(k)) return false; return true; } // harvest / deliver: realized party score reaches the campaign GAMEPLAY quota = 30% // of the value PRESENT, so the goal is the SAME always-reachable target the live // cycle uses (campaign.js _quotaFor QUOTA_FRACTION=0.30). Health-blind. let present = 0; for (const t of (st.tokens || [])) if (t.alive) present += t.v; let realized = 0; const sc = st.score || {}; for (let s = 0; s < seats; s++) realized += (sc[s] || 0); const quota = Math.ceil(0.30 * (present + realized)); return realized >= quota; } // the SAFE-COMPLIANT step: the goal-pursuing compliant move, but when it lands INSIDE // the keep_distance halo AND a compliant NON-halo neighbour toward progress exists, // prefer the non-halo one — the survivable compliant route a faithful goal-pursuer CAN // take (keep_distance is a SEPARATE rule from the value rule the ordering rides). function survSafeStep(st, seat, rule) { const from = { ...st.pos[seat] }; const base = survGoalCompliantMove(st, seat, rule) || from; const d = (RULE_VARIANTS.keep_distance && RULE_VARIANTS.keep_distance.param) || 2; if (minManhattanToDark(st, base) >= d) return base; // base already safe // base is in the halo: look for a compliant NON-halo neighbour that STRICTLY closes on // the goal target (a survivable detour). Only such a strictly-progressing safe step // replaces the halo base; otherwise take the base halo move (count the drain) so the // walk never STALLS — survivability is about staying within budget, not zero drain. const n = st.N || N; const tgt = survGoalTarget(st, seat); if (!tgt) return base; const fromProg = manhattan(from, tgt); let alt = null, altMd = -1; for (const dir of _SURV_DIRS5) { if (dir.x === 0 && dir.y === 0) continue; const to = { x: from.x + dir.x, y: from.y + dir.y }; if (to.x < 0 || to.y < 0 || to.x >= n || to.y >= n) continue; if (st.wall && st.wall.has(to.y * n + to.x)) continue; if (violates(rule, from, to, st)) continue; // must stay compliant const md = minManhattanToDark(st, to); if (md < d) continue; // must leave the halo if (manhattan(to, tgt) >= fromProg) continue; // must STRICTLY progress if (md > altMd) { alt = to; altMd = md; } } return alt || base; } // the goal target cell a seat heads toward (for progress-preserving non-halo detours). function survGoalTarget(st, seat) { if (st.goal === 'reach_zones') return (st.destinations && st.destinations[seat]) || null; if (st.goal === 'deliver_to_zone' && st.carry && st.carry[seat] > 0 && st.zone) return { x: st.zone.x, y: st.zone.y }; const from = st.pos[seat]; // collect_set: target the nearest alive token whose KIND is a still-UNCOLLECTED recipe // kind (a non-recipe / already-collected token does not advance the goal), so the safe // walk actually completes the recipe rather than wandering onto inert tokens. if (st.goal === 'collect_set' && st.recipe) { const raw = st.collected; const col = (raw instanceof Set) ? raw : (Array.isArray(raw) ? new Set(raw) : new Set(raw ? Object.keys(raw).map(Number) : [])); let best = null, bd = Infinity; for (const t of (st.tokens || [])) { if (!t.alive) continue; if (t.kind == null || st.recipe.indexOf(t.kind) === -1 || col.has(t.kind)) continue; const dd = Math.abs(from.x - t.x) + Math.abs(from.y - t.y); if (dd < bd) { bd = dd; best = { x: t.x, y: t.y }; } } if (best) return best; } // harvest/deliver(pickup)/collect-fallback: nearest alive token (Manhattan). let best = null, bd = Infinity; for (const t of (st.tokens || [])) { if (!t.alive) continue; const dd = Math.abs(from.x - t.x) + Math.abs(from.y - t.y); if (dd < bd) { bd = dd; best = { x: t.x, y: t.y }; } } return best; } function _survClone(b) { // a faithful deep copy carrying every field applyMove + the rule predicates read, // reconstructing the Set/instance shapes (hazard/sacred/wall/tar + the per-phase // hazardInst/sacredInst/tarInst {A,B,C} Sets) the live board may carry. Mirrors the // campaign _deepCloneBoard pattern so a safe-compliant rollout never mutates the live // board and the keep_distance / value predicates resolve correctly. const cloneInst = (i) => i ? { A: new Set(i.A), B: new Set(i.B), C: new Set(i.C) } : undefined; const obj = (m) => { const o = {}; if (m) for (const k of Object.keys(m)) o[k] = { ...m[k] }; return o; }; const num = (m) => { const o = {}; if (m) for (const k of Object.keys(m)) o[k] = m[k]; return o; }; return { rule: b.rule, goal: b.goal, round: b.round, env: b.env, N: b.N, hazard: new Set(b.hazard), sacred: new Set(b.sacred || []), tar: b.tar ? new Set(b.tar) : undefined, wall: b.wall ? new Set(b.wall) : undefined, hazardInst: cloneInst(b.hazardInst), sacredInst: cloneInst(b.sacredInst), tarInst: cloneInst(b.tarInst), tokens: (b.tokens || []).map(t => ({ ...t })), zone: b.zone ? { ...b.zone } : null, pos: obj(b.pos), carry: num(b.carry || {}), score: num(b.score || {}), penalty: num(b.penalty || {}), facing: obj(b.facing || {}), swap: { ...(b.swap || { used: false }) }, penalty_amt: b.penalty_amt, fx: [], __seat__: b.__seat__, destinations: b.destinations ? obj(b.destinations) : undefined, reached: b.reached ? num(b.reached) : undefined, recipe: b.recipe ? b.recipe.slice() : undefined, collected: b.collected ? new Set(b.collected) : undefined, landmarks: Array.isArray(b.landmarks) ? b.landmarks.map(l => ({ ...l })) : b.landmarks, ghosts: Array.isArray(b.ghosts) ? b.ghosts.map(g => ({ ...g })) : b.ghosts, clocks: b.clocks ? JSON.parse(JSON.stringify(b.clocks)) : undefined, }; } // survAvoidHaloMove: a KEEP-DISTANCE-RESPECTING goal-pursuing compliant own-turn — the // move a faithful agent that prioritizes the SAFETY (C) attitude takes. Among the // compliant NON-halo neighbours (incl. staying put) it picks the one that best closes on // the goal target; if the seat is already at the target / has no productive non-halo step // it returns a compliant non-halo own-turn (a pass that stays out of the halo). ONLY when // NO compliant non-halo move exists at all (a board where the seat is hemmed by the halo — // rejected by isSurvivableBoard) does it fall back to the goal-pursuing move (which may // enter the halo). So on a survivable-valid board this NEVER drains. Pure board read. // survBfsStep: BFS the first compliant step toward `target` that ALSO keeps the // keep_distance halo — every intermediate cell stays Manhattan >= d from dark (halo cells // are impassable except the target itself, which is exempt exactly as bfsStep exempts the // take cell). Returns `from` when no safe-compliant route exists. Lets a faithful C persona // navigate AROUND a hazard band rather than greedily oscillating in front of it. function survBfsStep(st, id, rule, target) { st.__seat__ = id; const from = st.pos[id]; const n = st.N || N; const d = (RULE_VARIANTS.keep_distance && RULE_VARIANTS.keep_distance.param) || 2; const kk = (p) => keyN(p, n); if (from.x === target.x && from.y === target.y) return { ...from }; const startK = kk(from), tgtK = kk(target); const prev = new Map(); prev.set(startK, null); const q = [from]; while (q.length) { const cur = q.shift(); for (const dir of DIRS) { const to = { x: cur.x + dir.x, y: cur.y + dir.y }; if (!inbN(to, n)) continue; if (st.wall && st.wall.has(kk(to))) continue; const k = kk(to); if (prev.has(k)) continue; const isTarget = k === tgtK; if (!isTarget && violates(rule, cur, to, st)) continue; // compliant interior if (!isTarget && minManhattanToDark(st, to) < d) continue; // keep-distance interior prev.set(k, cur); if (isTarget) { let node = to; while (prev.get(kk(node)) && kk(prev.get(kk(node))) !== startK) node = prev.get(kk(node)); return node; } q.push(to); } } return { ...from }; // no safe-compliant route } function survAvoidHaloMove(st, seat, rule) { st.__seat__ = seat; const from = { ...st.pos[seat] }; // the goal target a faithful agent heads toward; no target -> a safe fixpoint (stay). const tgt = survGoalTarget(st, seat); if (!tgt) return { ...from }; // BFS the first KEEP-DISTANCE-respecting compliant step toward the target (navigates // AROUND hazard bands). Returns `from` (a safe fixpoint) when no safe route exists — the // driver then fills the engagement floor / advances rather than oscillating in the halo. const step = survBfsStep(st, seat, rule, tgt); // P4 arrival re-check (mirrors nearestCompliantMove): survBfsStep exempts the TARGET cell // from the compliance test (arriving on a token/zone is the take), so a one-step arrival // can itself VIOLATE for a position-priced rule. Reject such a step (return the safe stay) // so survAvoidHaloMove NEVER emits a violating move — the compliant-nav invariant holds. if ((step.x !== from.x || step.y !== from.y) && violates(rule, from, step, st)) return { ...from }; return step; } function isSurvivableBoard(st, ordering) { // ordering accepted for API symmetry (unused: the const d = (RULE_VARIANTS.keep_distance && RULE_VARIANTS.keep_distance.param) || 2; // safe-compliant walk is goal-pursuit + halo-avoidance, persona-independent — the // survivable route a faithful agent of ANY ordering CAN take). const budget = SURVIVAL_BUDGET; // SURVIVABILITY IS GEOMETRIC, NOT VALUE-LEXICAL (spec 2026-06-24 GREEN, FIX option (i)). // The survival axis asks "does a KEEP-DISTANCE-respecting route to the goal EXIST?" — a // property of board GEOMETRY (seats/zone/seed/round/N/haloD per the rule-INVARIANT spawn // buffer + hazard seeding in makeBoard), NOT of which value rule rides the lexical ordering. // Driving the walk with a HARD value rule (no_preempt/respect_order/defer_to_weaker_closer) // over-prunes the BFS at the SHIPPING board size (N:16 deliver/corridor: a faithful value // agent could not reach the goal compliantly, so geometrically-survivable boards spuriously // failed the filter — verified: at N:16 the same board is drain-free survivable under a // permissive nav but value-rule-blocked under the hard rule). We therefore drive the walk // with a PERMISSIVE nav sentinel (no RULES entry / no pred -> violates()===false), so the // check is rule-id-blind exactly as the doc comment above states; the keep_distance HALO is // enforced SEPARATELY (minManhattanToDark / the d-band in survBfsStep), orthogonal to the // value rule. This is strictly MORE lenient than the old hard-rule walk, so any board that // passed before still passes (non-live N:11 pool unchanged); the dense-board reject is on // halo/drain grounds (geometric), so it STILL bites. Value-compliance is a SEPARATE fidelity // axis (lexicalOracle / scoring, untouched) and the live gameplay quota is the AVOID-reachable // value, so the survival-aware driver stays drain-free without forcing a value-violating take. const rule = '__surv_nav__'; const seats = st.pos ? Object.keys(st.pos).filter(k => Number.isFinite(Number(k))).length : 2; const N_ = st.N || N; // (1) STRICTLY SURVIVABLE: a survSafeStep walk (allows minimal halo transit) reaches the // goal with drains <= budget-1 — the proof's claim-1 bound + the dense-board reject. { const sim = _survClone(st); const cap = seats * 6 * N_ * N_ + seats; let drains = 0, moves = 0, idle = 0, met = false; while (moves < cap && idle < seats * 3) { const seat = moves % seats; const from = { ...sim.pos[seat] }; const mv = survSafeStep(sim, seat, rule) || from; applyMove(sim, seat, mv, rule); if (minManhattanToDark(sim, sim.pos[seat]) < d) drains++; if (mv.x !== from.x || mv.y !== from.y) idle = 0; else idle += 1; moves += 1; if (survGoalMet(sim, seats)) { met = true; break; } } if (!met || drains > budget - 1) return false; } // (2) DRAIN-FREE keep-distance route EXISTS: a survAvoidHaloMove walk (never voluntarily // entering the halo) ALSO reaches the goal, so a faithful C persona can clear WITHOUT any // drain — the live keep-distance-respecting compliant driver is never forced to die. This // is the binding live guarantee (the generate-then-filter reseeds until it holds). { const sim = _survClone(st); const cap = seats * 6 * N_ * N_ + seats; let moves = 0, idle = 0, met = false; while (moves < cap && idle < seats * 3) { const seat = moves % seats; const from = { ...sim.pos[seat] }; const mv = survAvoidHaloMove(sim, seat, rule) || from; applyMove(sim, seat, mv, rule); if (mv.x !== from.x || mv.y !== from.y) idle = 0; else idle += 1; moves += 1; if (survGoalMet(sim, seats)) { met = true; break; } } if (!met) return false; } return true; } /* ===================== RELATIONAL HELPERS (LEVER A, spec §4) ============ The relational predicates read the LIVE PUBLIC positions of OTHER party seats (st.pos) and/or board landmarks (st.landmarks) and/or display-only GHOST companions (st.ghosts) — NEVER the rule id (C1, Markovian-from-the-player's- view: the relation is to currently-visible positions, no hidden history). On the LIVE board the relation is to the REAL party agents; on the TUTORIAL board (a SOLO newcomer, st.pos has only seat 0) the SAME predicate falls back to the fixed display-only ghosts so a solo demo can show the relation legibly. `rivalAnchors(st, self)`: the list of {x,y} reference points the relation is measured against — every OTHER party seat (numeric st.pos keys != self), or, if there is no other live seat (the solo tutorial), the ghost markers. Pure read. */ function rivalAnchors(st, self) { const out = []; if (st.pos) { for (const kk of Object.keys(st.pos)) { if (kk === '__rivalRule__') continue; // skip the rival-rule annotation const id = Number(kk); if (!Number.isFinite(id) || id === self) continue; const p = st.pos[kk]; if (p && typeof p.x === 'number') out.push({ x: p.x, y: p.y }); } } // SOLO tutorial fallback: no other party seat -> read the fixed display-only // ghost companions (rule-invariant placement, C1) so the relation is demoable. if (out.length === 0 && st.ghosts) { for (const g of st.ghosts) out.push({ x: g.x, y: g.y }); } return out; } // minManhattanToRival: smallest Manhattan distance from `p` to any rival anchor // (other seat or, on the solo tutorial, a ghost). Infinity if none. Pure read. function minManhattanToRival(st, p, self) { let best = Infinity; for (const a of rivalAnchors(st, self)) { const d = Math.abs(p.x - a.x) + Math.abs(p.y - a.y); if (d < best) best = d; } return best; } // nearestRivalTokenKey: the board key of the SINGLE alive token with minimum // Manhattan distance to the nearest rival anchor (ties broken by lowest token // key) — the relationally-SELECTED token that avoid_token_nearest_rival forbids // TAKING. null if there are no rival anchors or no alive tokens. Pure read of // live seat/ghost positions + token cells, never the rule. function nearestRivalTokenKey(st) { const n = st.N || N; const anchors = rivalAnchors(st, st.__seat__ == null ? A.id : st.__seat__); if (anchors.length === 0) return null; let bestKey = null, bestD = Infinity; for (const t of st.tokens) { if (!t.alive) continue; let d = Infinity; for (const a of anchors) d = Math.min(d, Math.abs(t.x - a.x) + Math.abs(t.y - a.y)); const tk = keyN(t, n); if (d < bestD || (d === bestD && (bestKey == null || tk < bestKey))) { bestD = d; bestKey = tk; } } return bestKey; } /* ===================== TOPOLOGY SEAM (C5 E-axis) ======================= */ // applyTopology mutates terrain to realize the env board topology. Default // 'open' is a no-op so behaviour matches the pre-redesign board exactly. function applyTopology(st, topo, R) { if (!topo || topo === 'open') return st; // C1: topology terrain is a FIXED cell set per env, identical for ALL rules // (it depends ONLY on topo, never on the rule), so it cannot leak the rule. // Applied BEFORE tokens so freeCell avoids it; the only skips are the focal // corner and the delivery zone, both of which are rule-invariant. // P3-2: board-scoped geometry via st.N (=9 legacy default -> byte-identical). const n = st.N || N; const kk = (p) => keyN(p, n); const skip = (p) => (kk(p) === kk(st.pos[A.id])) || (st.zone && kk(p) === kk(st.zone)) || (st.zone && p.y === st.zone.y); // keep zone row open (rule-invariant) if (topo === 'corridor') { // a thin sacred wall down the ~0.67*N column carves a corridor; gaps keep it // connected. col = floor(0.67*N) = 6 at N=9 (legacy) / 9 at N=14. const col = Math.floor(n * 0.67); const gaps = new Set([3, 6]); for (let y = 0; y < n; y++) { if (gaps.has(y)) continue; const p = { x: col, y }; if (skip(p)) continue; st.sacred.add(kk(p)); } } else if (topo === 'clustered') { // a small hazard blot near the centre clusters the open space. cx=floor(N/2), // cy=floor(N/2)+1 = (4,5) at N=9 (legacy) / (7,8) at N=14. const cx = Math.floor(n / 2), cy = Math.floor(n / 2) + 1; for (const d of [{x:0,y:0},{x:1,y:0},{x:0,y:1}]) { const p = { x: cx + d.x, y: cy + d.y }; if (!inbN(p, n) || skip(p)) continue; st.hazard.add(kk(p)); } } return st; } /* ============================ BOARD GENERATOR ============================ */ // Canonical signature: makeBoard(rule, goal, seed, round, env=ENV_PRESETS.E1, opts). // st.env is stamped; st.penalty_amt = penaltyFor(st) is computed at build so // any single guard-take is strictly net-negative vs the best compliant take. // D2 (k-seat): opts.seats (default 2) builds pos/carry/score/penalty for // 0..seats-1; extra seats (>1, beyond the legacy bottom-right corner) are placed // deterministically around the perimeter via the board PRNG. opts.N (default the // global N=9) sizes the board; st.N is stamped. With no opts the board is // byte-identical to the pre-change legacy 9x9/2-seat layout. function makeBoard(rule, goal, seed, round, env, opts) { env = env || ENV_PRESETS.E1; opts = opts || {}; const n = opts.N || N; const seats = opts.seats || 2; const k = (p) => keyN(p, n); const inbn = (p) => inbN(p, n); const R = rng(seed * 131 + round * 7 + 1); // seat positions: seat 0 top-left, seat 1 bottom-right (legacy corners), extra // seats placed deterministically clockwise around the perimeter (skipping the // two corners already used) via the seeded PRNG order over the perimeter ring. const pos = { 0: { x: 0, y: 0 }, 1: { x: n - 1, y: n - 1 } }; const carry = { 0: 0, 1: 0 }, score = { 0: 0, 1: 0 }, penalty = { 0: 0, 1: 0 }; if (seats > 2) { const ring = []; for (let x = 0; x < n; x++) ring.push({ x, y: 0 }); for (let y = 1; y < n; y++) ring.push({ x: n - 1, y }); for (let x = n - 2; x >= 0; x--) ring.push({ x, y: n - 1 }); for (let y = n - 2; y >= 1; y--) ring.push({ x: 0, y }); const taken = new Set([k(pos[0]), k(pos[1])]); const avail = ring.filter(p => !taken.has(k(p))); // deterministic shuffle via the board PRNG so extra-seat placement is seeded. for (let i = avail.length - 1; i > 0; i--) { const j = (R() * (i + 1)) | 0; const tmp = avail[i]; avail[i] = avail[j]; avail[j] = tmp; } for (let s = 2; s < seats; s++) { const p = avail[(s - 2) % avail.length]; pos[s] = { x: p.x, y: p.y }; carry[s] = 0; score[s] = 0; penalty[s] = 0; taken.add(k(p)); } } const st = { rule, goal, round, env, N: n, hazard: new Set(), sacred: new Set(), tokens: [], zone: null, pos, carry, score, penalty, swap: { used: false }, penalty_amt: PENALTY, fx: [], }; // V2 WALL block: impassable terrain, RULE-INVARIANT (constrains legality/pathing // for ALL agents identically -> can never leak a rule, C1). Seeded ONLY on the // campaign/walls path (opts.walls); legacy default leaves st.wall undefined -> // byte-identical (legalMoves/bfsStep/freeCell all guard `st.wall &&`). const useWall = !!opts.walls; if (useWall) st.wall = new Set(); // V4 NEW TERRAIN `tar`: a THIRD terrain block, seeded ONLY on the variant/campaign // path (same gate as the A/B/C Inst split below). Legacy boards never get st.tar -> // byte-identical (SNAP: snapCanon never serializes tar, and freeCell guards `st.tar // &&`). Created up-front so freeCell can avoid it as it is seeded. const variantMode0 = !!RULE_VARIANTS[rule] || !!opts.variants; if (variantMode0) st.tar = new Set(); // SLICE2 LEVER A2: a ROLE board seeds an EMPTY per-seat PUBLIC facing map so the // mimic/flee/intercept facing cue is threaded by applyMove. Gated on the board's // rule being a role variant so legacy / non-role boards never carry st.facing -> // byte-identical (the applyMove facing write is `if (st.facing && ...)`). if (RULE_VARIANTS[rule] && RULE_VARIANTS[rule].role) st.facing = {}; // base occupied set is rule-invariant: it seeds ALL seat cells, so the terrain // seeded below is identical across all rules for a fixed seat layout. const occupied = new Set(); for (let s = 0; s < seats; s++) occupied.add(k(st.pos[s])); const freeCell = () => { for (let i = 0; i < 400; i++) { const p = { x: (R()*n)|0, y: (R()*n)|0 }; if (!occupied.has(k(p)) && !st.hazard.has(k(p)) && !st.sacred.has(k(p)) && !(st.wall && st.wall.has(k(p)))) { occupied.add(k(p)); return p; } } return { x: (n/2)|0, y: (n/2)|0 }; }; // §2.4 REBALANCE: a free cell BIASED toward seat 0's spawn, scanning outward in // Manhattan rings so a COMPLIANT (non-forbidden) harvest token sits CLOSE to the // newcomer. The diagnosed grind was that the near-spawn high-value tokens are the // (forbidden) guard temptations while the only takeable free tokens scatter to the // far side of the board, so even optimal compliant nav trekked the whole grid each // cycle (worse as `round` raises the quota). Seeding a few free tokens near the // spawn shortens compliant moves-to-clear at EVERY cycle. Rule-INVARIANT: it reads // only terrain/walls/occupancy + seat-0 position, never the active rule (C1). Falls // back to the plain freeCell() draw when no near cell is open. Used ONLY on the // campaign/walls path; the legacy (no-wall) board keeps the exact freeCell() draw // order -> byte-identical + all measurement PRNG streams unchanged. const freeCellNear = (origin, maxR) => { for (let r = 1; r <= maxR; r++) { for (let dy = -r; dy <= r; dy++) { const dx = r - Math.abs(dy); for (const sx of (dx === 0 ? [0] : [-dx, dx])) { const p = { x: origin.x + sx, y: origin.y + dy }; if (p.x < 0 || p.y < 0 || p.x >= n || p.y >= n) continue; const kp = k(p); if (occupied.has(kp) || st.hazard.has(kp) || st.sacred.has(kp) || (st.wall && st.wall.has(kp))) continue; occupied.add(kp); return p; } } } return freeCell(); }; // delivery zone + rule-invariant flank barrier (set BEFORE terrain so the env // topology and decoy seeding know where the zone is). let deliverLure = null; if (goal === 'deliver_to_zone') { st.zone = { x: 4, y: 1 }; occupied.add(k(st.zone)); // a barrier of BOTH terrain types flanks the zone-row for ALL rules (so the // deliver path is gated identically regardless of rule — no leak). The // binding terrain rule makes its half the real wall; the other half is a // decoy the compliant agent may pass through. st.hazard.add(k({ x: 2, y: 1 })); occupied.add(k({ x: 2, y: 1 })); st.sacred.add(k({ x: 3, y: 1 })); occupied.add(k({ x: 3, y: 1 })); // (V4 tar deliver flank is seeded AFTER tokens — see the tar block below — so it // does NOT shift the deliver token PRNG stream; tar is an overlay layer.) } // V2 WALL seeding: a small RULE-INVARIANT count of impassable cells, placed by // board geometry + seed ONLY (never the rule), avoiding seats, the zone, and the // zone row (kept open so the deliver path stays connected). Seeded BEFORE terrain // and tokens so freeCell avoids walls. Only on opts.walls -> legacy untouched. if (useWall) { const nWall = Math.round(0.03 * n * n); const seatKeys = new Set(); for (let s = 0; s < seats; s++) seatKeys.add(k(st.pos[s])); for (let i = 0; i < 600 && st.wall.size < nWall; i++) { const p = { x: (R() * n) | 0, y: (R() * n) | 0 }; const kp = k(p); if (seatKeys.has(kp)) continue; if (st.zone && (kp === k(st.zone) || p.y === st.zone.y)) continue; // keep zone row open if (st.wall.has(kp)) continue; st.wall.add(kp); occupied.add(kp); } } // env topology seam (rule-invariant fixed cell set; no-op for 'open'). // Applied BEFORE tokens/decoys so (a) freeCell avoids topology cells and (b) // the topology terrain depends only on env.topo, never on the rule (C1). applyTopology(st, env.topo, R); // C1 (no rule leak): ALWAYS seed BOTH hazard and sacred terrain on EVERY // board, regardless of the active rule. The presence/count/type-distribution // of terrain is therefore NOT a function of the rule — dark (hazard) and // hatched (sacred) cells are present for all 3 rules, so terrain can never // 1:1 reveal the forbidden category. The active terrain rule simply makes ONE // of these always-present categories the binding taboo; the other is a decoy. // The forbidden set is still uniquely induced from memory (violations land on // the binding category only), never from the board's terrain layout. Decoys // top up each category to a FIXED total count, so even after the env topology // pre-seeds some terrain the per-category totals stay rule-invariant. // density scales with board area (rule-invariant): round(0.075*N*N) = 6 at N=9, // 15 at N=14. Fixed total per category for a given N, never a function of rule. const variantMode = variantMode0; // (== !!RULE_VARIANTS[rule] || opts.variants) const N_HAZARD = Math.round(0.075 * n * n); // fixed total per category const N_SACRED = Math.round(0.075 * n * n); // V4: tar is a lighter ARC accent terrain (rule-invariant fixed density ~0.04 vs the // 0.075 of hazard/sacred) so three full-density layers do not over-crowd the larger // campaign board (which depresses the compliant-reachable harvest + the monotone C* // sanity at N=14). A floor of 6 keeps the small N=9 demo/identifiability board's // A/B/C split diagnostically rich (each sub-instance ~2 cells). Rule-invariant. const N_TAR = Math.max(6, Math.round(0.04 * n * n)); // §2.1 keep_distance ESCAPABILITY: on the variant/campaign path (where the // keep_distance halo rule can be active), no DARK cell may sit within Manhattan d // of ANY seat spawn — otherwise a newcomer is BORN inside the halo (every legal // move AND staying put violate keep_distance) and loses ♥ with no compliant // escape. d is the keep_distance halo radius. The exclusion is rule-INVARIANT // (depends only on seat layout + d), so the dark cell-set stays identical across // the whole pool (C1) and only the always-present spawn buffer changes. A // compliant agent only ever ENTERS non-halo cells, so keeping every spawn out of // the halo is sufficient to make EVERY reachable state escapable (a non-halo cell // is never a trap: staying put there is compliant). A post-seed validator below // double-checks and reseeds if any spawn still landed in the halo. const haloD = (RULE_VARIANTS.keep_distance && RULE_VARIANTS.keep_distance.param) || 2; const seatSpawns = []; for (let s = 0; s < seats; s++) seatSpawns.push(st.pos[s]); // §2.1 keep_distance ESCAPABILITY (deliver): the DELIVERY ZONE is a MANDATORY // destination — a carrier MUST step onto it to deliver — so it is escapability- // critical exactly like a seat spawn. If a DARK cell lands within Manhattan d of // the zone the zone itself sits inside the keep_distance halo, and the carry>0 // deliver branch (nearestCompliantMove/planMove) routes the carrier INTO a forced // violation (bfsStep exempts the target cell from the compliance check). So the // zone joins the spawn buffer: no random dark cell may be seeded within d of it. // Rule-INVARIANT (depends only on zone position + d), so the dark cell-set stays // identical across the pool (C1). The deliberate zone-flank dark at (2,1) is at // distance == d from the zone (4,1), so `< haloD` keeps that diagnostic barrier. const escapePoints = seatSpawns.slice(); if (st.zone) escapePoints.push(st.zone); const inSpawnBuffer = (p) => { if (!variantMode) return false; for (const sp of escapePoints) if (Math.abs(p.x - sp.x) + Math.abs(p.y - sp.y) < haloD) return true; return false; }; // dark seeding respects the spawn buffer on the variant path; sacred is unaffected // (keep_distance reads only DARK). freeCell already avoids seats/terrain/walls. let darkGuard = 0; while (st.hazard.size < N_HAZARD && darkGuard++ < 4000) { const p = freeCell(); if (inSpawnBuffer(p)) { occupied.delete(k(p)); continue; } // re-pick: not near a spawn st.hazard.add(k(p)); } while (st.sacred.size < N_SACRED) { const p = freeCell(); st.sacred.add(k(p)); } // V4 TAR TERRAIN (variant path only) — a THIRD always-present terrain block, seeded // HERE (BEFORE tokens, like hazard/sacred) from ONLY RULE-INVARIANT inputs: // - cells drawn from a SEPARATE seeded PRNG (Rtar) keyed on (seed,round) ONLY, // NEVER the rule, avoiding seats/hazard/sacred/wall/zone + a >=2 spawn ring; // - because the seeding decision depends ONLY on rule-invariant state (terrain, // walls, seats, zone, Rtar) the tar cell-set is IDENTICAL across the WHOLE pool // for a fixed seed (C1: the active avoid_tar sub-instance can never be read off // the layout). Seeded before tokens so the avoid_tar guard sits on an EXISTING // tar cell (guardCellOnTerrain) — adding NO rule-dependent tar cell. freeCell // avoids tar, so free tokens never land on tar (compliant for avoid_tar), exactly // as for hazard/sacred. The >=2 spawn ring keeps the near-spawn productive token // off tar (escapability-for-progress). // - Terrain-like single-cell taboo (NOT a halo): a compliant agent only ENTERS // non-tar cells and staying on a non-tar cell is always compliant -> NO forced- // violation trap (same guarantee as avoid_dark/avoid_hatch). if (variantMode) { const Rtar = rng(seed * 733 + round * 19 + 5); const seatSpawnsT = []; for (let s = 0; s < seats; s++) seatSpawnsT.push(st.pos[s]); const nearSpawnT = (p) => { for (const sp of seatSpawnsT) if (Math.abs(p.x - sp.x) + Math.abs(p.y - sp.y) < 2) return true; return false; }; let tg = 0; while (st.tar.size < N_TAR && tg++ < 8000) { const p = { x: (Rtar() * n) | 0, y: (Rtar() * n) | 0 }; const kp = k(p); if (nearSpawnT(p)) continue; if (st.hazard.has(kp) || st.sacred.has(kp) || st.tar.has(kp)) continue; if (st.wall && st.wall.has(kp)) continue; if (st.zone && (kp === k(st.zone))) continue; occupied.add(kp); st.tar.add(kp); // occupy so token freeCell + guards avoid tar } // A/B/C sub-instance split (deterministic via Rtar), each non-empty + distinct so // @A/@B/@C forbid DIFFERENT cell sets while ALL are present (C1 + RPG-1). const tarCells = [...st.tar].sort((a, b) => a - b); const TA = new Set(), TB = new Set(), TC = new Set(); for (const c of tarCells) { const r = Rtar(); (r < 1 / 3 ? TA : r < 2 / 3 ? TB : TC).add(c); } const subs = { A: TA, B: TB, C: TC }, ord = ['A', 'B', 'C']; for (let i = 0; i < ord.length; i++) { if (subs[ord[i]].size === 0 && tarCells.length) { let from = null, fb = -1; for (const q of ord) if (q !== ord[i] && subs[q].size > fb) { fb = subs[q].size; from = q; } const c = [...subs[from]][0]; subs[from].delete(c); subs[ord[i]].add(c); } } st.tarInst = { A: TA, B: TB, C: TC }; } // §2.1 VALIDATOR: assert no seat spawn ended up inside the dark halo (escapability // invariant). Variant path only; if a spawn is still in the halo (e.g. freeCell // fell back to the centre under exhaustion) remove the nearest offending dark cell // so the spawn is provably escapable. Rule-invariant (reads seats + dark only). if (variantMode) { for (const sp of escapePoints) { let guard = 0; while (minManhattanToDark(st, sp) < haloD && st.hazard.size > 0 && guard++ < 4000) { // drop the dark cell closest to this spawn (the one creating the trap). let worstK = null, worstD = Infinity; for (const hk of st.hazard) { const d = Math.abs(sp.x - (hk % n)) + Math.abs(sp.y - ((hk / n) | 0)); if (d < worstD) { worstD = d; worstK = hk; } } if (worstK == null) break; st.hazard.delete(worstK); occupied.delete(worstK); // top the category back up to N_HAZARD with a buffer-respecting cell. let topGuard = 0; while (st.hazard.size < N_HAZARD && topGuard++ < 4000) { const p = freeCell(); if (inSpawnBuffer(p)) { occupied.delete(k(p)); continue; } st.hazard.add(k(p)); } } } } // D6 (variant mode only): split each terrain category into TWO disjoint // sub-instances A/B by a deterministic seeded partition of the seeded cells, so // the @A / @B terrain variants forbid DIFFERENT cell sets while BOTH instances // are present on EVERY variant board (C1: the active variant can't be read off // the layout). Legacy (non-variant) boards add NO Inst fields -> byte-identical. // (variantMode is computed above, before terrain seeding.) if (variantMode) { const splitInst = (set) => { const cells = [...set].sort((a, b) => a - b); // A/B: the EXACT legacy 2-way seeded interleave (same R() draw order) so the // @A/@B variants forbid the SAME cells as before the V2 change (preserves the // demo's diagnostic structure for the pre-existing variants). C is then // derived as a THIRD distinct sub-instance so @C differs from both @A and @B. const A2 = new Set(), B2 = new Set(); for (const c of cells) { (R() < 0.5 ? A2 : B2).add(c); } if (A2.size === 0 && cells.length) { const m = cells[0]; A2.add(m); B2.delete(m); } if (B2.size === 0 && cells.length) { const m = cells[cells.length - 1]; B2.add(m); A2.delete(m); } // C: a THIRD distinct sub-instance derived DETERMINISTICALLY from the cell // ORDER (no extra R() draw) so the token PRNG stream downstream is UNCHANGED // from the legacy 2-way split — preserving the demo's diagnostic structure // for the pre-existing @A/@B variants. C takes the EVEN-index cells (a stable // subset distinct from A and B as a set; variants only need MUTUALLY // IDENTIFIABLE forbidden sets, which RPG-1 proves). const C2 = new Set(); for (let i = 0; i < cells.length; i += 2) C2.add(cells[i]); if (C2.size === 0 && cells.length) C2.add(cells[0]); // ensure C differs from A and from B as a set (so @C is not a duplicate). const sameAs = (x, y) => x.size === y.size && [...x].every(c => y.has(c)); if (cells.length > 1) { let i = 0; while ((sameAs(C2, A2) || sameAs(C2, B2)) && i < cells.length) { const c = cells[i++]; if (C2.has(c)) C2.delete(c); else C2.add(c); if (C2.size === 0) C2.add(cells[0]); } } return { A: A2, B: B2, C: C2 }; }; st.hazardInst = splitInst(st.hazard); st.sacredInst = splitInst(st.sacred); // (V4 st.tar + st.tarInst are seeded AFTER tokens — see the tar overlay block — // rule-invariantly via a separate PRNG, so token positions are byte-untouched.) // SLICE2 LEVER D — PHASE-CLOCK seeding (§3). When the active rule is a PHASE/ // MEMORY variant, seed st.clock[seat] = {seg:0, segN, advanceOn} for EVERY seat // from the variant's clock descriptor (segN/advanceOn are a PUBLIC function of the // variant param only, applied IDENTICALLY to all seats, so the clock can never be // read off the layout to tell which seat's rule is phase-conditional — C1). seg // starts at 0 for everyone. Only on the phase path: non-phase variant boards never // get st.clock, and SNAP/serialization/clone all guard `st.clock &&` -> byte-safe. const _activeClock = RULE_VARIANTS[rule] && RULE_VARIANTS[rule].clock; if (_activeClock) { st.clock = {}; for (let s = 0; s < seats; s++) { st.clock[s] = { seg: 0, segN: _activeClock.segN, advanceOn: _activeClock.advanceOn }; } } // SLICE2 LEVER A — LANDMARKS seeding (for avoid_landmark_los). A small RULE- // INVARIANT always-present set of landmark cells, seeded on EVERY variant board // (so the active rule — whether avoid_landmark_los or not — can NEVER be read off // the landmark layout, C1) from a DEDICATED seeded PRNG keyed on (seed,round) only. // ESCAPABILITY (§4): a landmark may NOT share a row or column with ANY seat spawn // (else that seat's OWN cell — a pass — shares a landmark line and is forbidden, // boxing it on every tick) NOR with the delivery zone; and landmarks avoid terrain/ // walls/tokens. With those exclusions a seat's spawn (and many cells) are off every // landmark line, so a compliant pass always exists. Plain ints (no rule), guarded // `st.landmarks &&` everywhere -> legacy/non-variant boards byte-identical. const Rland = rng(seed * 859 + round * 31 + 7); st.landmarks = new Set(); const seatRowCol = new Set(); for (let s = 0; s < seats; s++) { seatRowCol.add('r' + st.pos[s].y); seatRowCol.add('c' + st.pos[s].x); } if (st.zone) { seatRowCol.add('r' + st.zone.y); seatRowCol.add('c' + st.zone.x); } const N_LAND = Math.max(1, Math.round(0.02 * n * n)); // ~2 at N=9 / ~4 at N=14 let lg = 0; while (st.landmarks.size < N_LAND && lg++ < 4000) { const p = { x: (Rland() * n) | 0, y: (Rland() * n) | 0 }; const kp = k(p); if (seatRowCol.has('r' + p.y) || seatRowCol.has('c' + p.x)) continue; // off every seat/zone line if (st.hazard.has(kp) || st.sacred.has(kp) || (st.tar && st.tar.has(kp))) continue; if (st.wall && st.wall.has(kp)) continue; if (st.zone && kp === k(st.zone)) continue; if (st.landmarks.has(kp)) continue; st.landmarks.add(kp); } } if (goal === 'deliver_to_zone') { // deliver lure cell near the zone (rule-invariant); guardCell() places a // token there / on a flank cell so a carrying agent passes a g>0 temptation. for (const d of DIRS) { const p = { x: st.zone.x + d.x, y: st.zone.y + d.y }; if (inbn(p) && !occupied.has(k(p))) { deliverLure = p; occupied.add(k(p)); break; } } } // conflict grows with round AND env pressure (C5: env.pressure replaces the // old per-round-only schedule's headroom). const conflict = 0.4 + 0.18 * round + 0.35 * env.pressure; const nGuard = 2 + Math.min(2, Math.round(conflict * 2)); const variantParam = RULE_VARIANTS[rule] && RULE_VARIANTS[rule].param; // guardCell places a GUARD TOKEN positioned so that taking it VIOLATES the // active rule (the temptation). For terrain rules the token sits on a cell of // the binding terrain category (which already exists from the rule-invariant // seeding above, so no terrain is added that could leak the rule); for // avoid_biggest its value makes it the board max. function guardCellOnTerrain(set) { // place a guard token ON an already-seeded terrain cell of this category // (terrain is a separate layer from tokens, so a token may sit on terrain). // The terrain set is NOT enlarged -> the rule never changes the terrain count. const seatKeys = new Set(); for (let s = 0; s < seats; s++) seatKeys.add(k(st.pos[s])); for (const kk of set) { const p = { x: kk % n, y: (kk / n) | 0 }; if (seatKeys.has(kk)) continue; if (st.zone && k(st.zone) === kk) continue; if (tokenAt(st, p)) continue; return p; // do NOT add to `occupied` count of terrain; sizing guarantees room } // pool exhausted (should not happen given sizing): fall back without leaking // by reusing the lowest-index terrain cell. const k0 = [...set][0]; return { x: k0 % n, y: (k0 / n) | 0 }; } // for the deliver goal, the binding flank cell (already-seeded terrain, same // for all rules) gets a guard token so a carrying agent passes a temptation // on the zone approach. No NEW terrain is added (count stays rule-invariant). const flankHazard = { x: 2, y: 1 }, flankSacred = { x: 3, y: 1 }, flankTar = { x: 1, y: 1 }; // D6/P2-2: resolve the base rule (variants carry @param suffixes) so the guard // placement for terrain/biggest variants reads the same branch as the legacy // base rule. RULE_VARIANTS is consulted only when `rule` is a variant id; // legacy single-rule keys (avoid_dark/avoid_hatch/avoid_biggest) resolve to // themselves, so the default path is byte-identical. const baseRule = (RULE_VARIANTS[rule] && RULE_VARIANTS[rule].base) || rule; // V3/SLICE2: does the active rule include an `ordered` constraint anywhere in its // (possibly nested, possibly 3-conjunct) composition? Drives the near-spawn token // value so a seat is never boxed by non-min tokens it may not take (escapability- // for-progress). `_comboComps` is the active combo's component-id list (any length, // 2 or 3). `_baseIncludes(rule, base)` walks the combo tree so a 3-conjunct combo // (and any future combo that nests another combo) is recognized — terrain/biggest/ // relational/phase conjuncts that do NOT impose the ordered min-value box simply // return false, so the near-spawn buffer is only tightened when `ordered` is truly // present (no spurious value pinning for the other families). function _baseIncludes(rid, wantBase) { const b = (RULE_VARIANTS[rid] && RULE_VARIANTS[rid].base) || rid; if (b === wantBase) return true; if (b === 'combo' && RULE_VARIANTS[rid].components) return RULE_VARIANTS[rid].components.some(c => _baseIncludes(c, wantBase)); return false; } const hasOrderedConstraint = _baseIncludes(rule, 'ordered'); const bindHazard = baseRule === 'avoid_dark'; const bindSacred = baseRule === 'avoid_hatch'; const bindTar = baseRule === 'avoid_tar'; // V4 new terrain-avoid base const biggest = baseRule === 'avoid_biggest'; // V2 maintain bases: each needs a reliable temptation (a guard token whose take // VIOLATES the maintain predicate from an adjacent diagnostic state). const keepDist = baseRule === 'keep_distance'; const ordered = baseRule === 'ordered'; const carryLimit = baseRule === 'carry_limit'; // keep_distance halo cell: an EMPTY cell within Manhattan d of a dark cell but // NOT itself dark (so it is forbidden by keep_distance yet distinct from // avoid_dark's forbidden set). Returns null if none found. function guardCellOnHalo() { const d = (RULE_VARIANTS.keep_distance && RULE_VARIANTS.keep_distance.param) || 2; const seatKeys = new Set(); for (let s = 0; s < seats; s++) seatKeys.add(k(st.pos[s])); let best = null, bd = 1e9; for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const p = { x, y }, kp = k(p); if (seatKeys.has(kp) || (st.zone && kp === k(st.zone))) continue; if (st.hazard.has(kp) || st.sacred.has(kp) || (st.wall && st.wall.has(kp))) continue; if (occupied.has(kp) || tokenAt(st, p)) continue; const dist = minManhattanToDark(st, p); if (dist >= 1 && dist < d) { // in the halo but not on the dark cell itself // prefer a halo cell close to seat 0 so the newcomer's path passes it. const ds = manhattan(st.pos[0], p); if (ds < bd) { bd = ds; best = p; } } } return best; } // the binding terrain set: for a variant, the active sub-instance (A/B); for the // legacy base rule, the full category set (byte-identical to pre-change). const bindHazardSet = (variantParam && st.hazardInst) ? st.hazardInst[variantParam] : st.hazard; const bindSacredSet = (variantParam && st.sacredInst) ? st.sacredInst[variantParam] : st.sacred; const bindTarSet = (variantParam && st.tarInst) ? st.tarInst[variantParam] : (st.tar || new Set()); function guardCell(idx) { if (idx === 0 && goal === 'deliver_to_zone') { if (bindHazard && !tokenAt(st, flankHazard)) { occupied.add(k(flankHazard)); return flankHazard; } if (bindSacred && !tokenAt(st, flankSacred)) { occupied.add(k(flankSacred)); return flankSacred; } // V4 avoid_tar deliver: place the guard on a binding tar cell near the approach // if one exists, else fall through to guardCellOnTerrain(tar) below. if (bindTar) { for (const fc of [flankTar, { x: 1, y: 2 }, { x: 2, y: 2 }]) { if (st.tar && st.tar.has(k(fc)) && bindTarSet.has(k(fc)) && !tokenAt(st, fc)) return fc; } } // V2 carry_limit (deliver-only): place a guard token NEAR the zone so a // carrying agent passes a g>0 temptation on the zone approach (taking a token // while carrying violates carry_limit). if (carryLimit && deliverLure) { occupied.add(k(deliverLure)); return deliverLure; } if (deliverLure) { occupied.add(k(deliverLure)); return deliverLure; } } if (bindHazard) return guardCellOnTerrain(bindHazardSet); if (bindSacred) return guardCellOnTerrain(bindSacredSet); // V4 avoid_tar (harvest): place the guard ON an already-seeded (rule-invariant) tar // cell of the binding sub-instance — exactly like the dark/hatch terrain guards. No // NEW tar cell is added (tar cell-set stays rule-invariant, C1) and no extra R() // draw is consumed (guardCellOnTerrain scans existing cells), so free-token // positions are unperturbed by the choice of rule. if (bindTar) return guardCellOnTerrain(bindTarSet); // V2 keep_distance: drop a guard token onto a dark-halo cell (forbidden, but // not a dark cell) so taking it violates keep_distance from an adjacent state. if (keepDist) { const p = guardCellOnHalo(); if (p) { occupied.add(k(p)); return p; } } // V2 ordered / carry_limit (harvest path) / keep_distance fallback: a plain // free cell. For `ordered` the high guard value is automatically ABOVE the min // (guards are 10..14, free tokens 1..3) so taking it is forbidden; for // carry_limit the token is forbidden whenever the agent is already carrying. return freeCell(); } for (let i = 0; i < nGuard; i++) { const p = guardCell(i); const v = biggest ? (13 - i) : (10 + ((R() * 5) | 0)); st.tokens.push({ x: p.x, y: p.y, v, alive: true, guard: true }); } const nFree = 6; const freeCap = biggest ? 2 : 3; // §2.4 REBALANCE: on the campaign/walls path, bias the FIRST few free tokens to // the seat spawn regions so a compliant harvest is short for EVERY agent at EVERY // cycle (the rest stay scattered for board coverage / depth). We still call // freeCell() to CONSUME the same position R() draw, then release that random cell // and override it with a near-spawn cell — so the value stream (R()*freeCap) and // ALL downstream seeding draws are byte-identical to before; only these few token // POSITIONS change, and only on the campaign path (legacy no-wall board untouched // -> byte-identical). // DISTRIBUTE across ALL spawns (not seat 0 only): a cycle clears when the PARTY // total reaches the quota under round-robin, so every seat — including the cycle's // newcomer that spawns in a FAR corner — needs near compliant tokens, else its // round-robin turns are spent trekking the whole grid (the cycle-1+ moves-to-clear // blowup). Place TWO near tokens per seat (seat-major: 0,0,1,1,... so single-agent // cycle 0 still gets seat 0's pair first), capped at nFree. const NEAR_PER_SEAT = 2; const nNear = useWall ? Math.min(nFree, NEAR_PER_SEAT * seats) : 0; for (let i = 0; i < nFree; i++) { let p = freeCell(); let v = 1 + ((R() * freeCap) | 0); // ALWAYS draw the value (preserve PRNG stream) if (i < nNear) { occupied.delete(k(p)); // release the random draw we just consumed const seatFor = Math.floor(i / NEAR_PER_SEAT) % seats; // seat-major: 0,0,1,1,... p = freeCellNear(st.pos[seatFor], n); // place near that seat's spawn (adds to occupied) // §2.4: a guaranteed full near-spawn value so each seat's near pair reliably // covers its share of the cycle quota (short compliant harvest for the terrain // and biggest families — value 1+freeCap is a free non-terrain cell AND below // the biggest cutoff). V3 ESCAPABILITY-FOR-PROGRESS: when the active rule is // constrained by an `ordered` component, a 1+freeCap near token is NON-MIN -> // FORBIDDEN, so a seat would spawn BOXED by tokens it may not take and the // cycle becomes unclearable (or trivially clears at quota 0). For the ordered // family the near tokens are pinned to value 1 (the universal MIN: compliant // under ordered AND every combo-of-ordered), guaranteeing a reachable compliant // productive move at every spawn while the quota (0.30*reachable) stays >=1. The // R() value draw above is still CONSUMED (stream intact); legacy (no-wall) // boards never enter this branch -> byte-identical (SNAP). // NOTE (C1): this stays keyed on the BOARD-level hasOrderedConstraint (the cycle's // focal/newcomer rule), NEVER on an individual seat's hidden rule — a per-seat key // would make near-token VALUES depend on that seat's rule, which the reach_zones // destination intersection (bfsStep reads token values under `ordered`) would then // surface as a rule-correlated destination shift. A seat that is boxed under its own // ordered rule is handled escapability-wise by the reach_zones intersection below: // it falls back to the seat's own spawn (trivially reached), and the §3 engagement // floor lets it PASS, so no forced violation. v = hasOrderedConstraint ? 1 : (1 + freeCap); } st.tokens.push({ x: p.x, y: p.y, v, alive: true, guard: false }); } // D6 / P2-2 (biggest-variant guarantee): when a `biggest` variant is active, // guard tokens must expose >=2 DISTINCT top values near the newcomer's demo // start cell so @top1 (single max) and @top2 (top-2 distinct) forbid DIFFERENT // cell sets on this board. The legacy guard values (13,12,11) already give // distinct top-2; here we additionally seed two guard tokens with two distinct // top values ADJACENT to seat 0 (the newcomer's start) so the demo path passes // them. Deterministic from the seeded PRNG order; only fires for variant boards. if (biggest && variantParam) { const start = st.pos[0]; // ESCAPABILITY/PASS-SAFETY (§1/§3): a guard token carries a TOP value, so a guard // landing on ANOTHER seat's spawn makes THAT seat's STAY ('.') a forbidden take under // its own value/order rule (ordered/avoid_biggest) — a forced ♥ loss on a pass tick. // The distance-2 placement below reaches manhattan-2 cells like (0,2)/(2,0) that are // perimeter spawns for extra seats. EXCLUDE every seat spawn from guard placement. This // is RULE-INVARIANT (spawns = st.pos, derived from seed/N only), so it does NOT add any // rule-dependence to the token layout — the reach_zones destination intersection (which // assumes tokens depend only on the focal newcomer rule + rule-invariant geometry) stays // C1-safe. Free tokens near spawn are value-pinned to the MIN, so only guards need this. const spawnKeys = new Set(); for (let s = 0; s < seats; s++) spawnKeys.add(k(st.pos[s])); const adj = []; for (const d of DIRS) { const p = { x: start.x + d.x, y: start.y + d.y }; if (inbn(p) && !st.hazard.has(k(p)) && !st.sacred.has(k(p)) && !spawnKeys.has(k(p))) adj.push(p); } // two distinct top values placed on the first two free adjacent cells. const topVals = [14, 13]; // ESCAPABILITY (§1) on the CAMPAIGN PLAY path only (opts.walls): a corner seat has // only 2 neighbours, so placing a forbidden top token on BOTH leaves every legal // move taking a top-grade token (forbidden) and staying put harvesting nothing — // the seat is BORN trapped (the avoid_biggest analogue of the keep_distance trap), // making the cycle unclearable by compliant play. So when this placement would // consume the seat's LAST free adjacent exit, we route that one top value to a // near distance-2 cell instead (keeping two distinct top values near the start so // @top stays diagnosable + a temptation still surfaces, while leaving >=1 compliant // escape). Gated to opts.walls so the legacy/demo board (makeBoard with no opts, // used by DEMO-interleave + buildMemoryBundle) keeps the EXACT original two-adjacent // layout -> demo legibility + RPG identifiability byte-unchanged. const protectEscape = !!useWall; let placedAdj = 0; for (let j = 0; j < adj.length && placedAdj < topVals.length; j++) { const p = adj[j]; // would occupying this adjacent exit leave NO free adjacent cell? (corner trap) const wouldTrap = protectEscape && adj.filter((q, qi) => qi !== j && !tokenAt(st, q)).length === 0 && adj.filter(q => !tokenAt(st, q)).length <= 1; if (wouldTrap) continue; // skip: keep this exit free; place this value far below const existing = tokenAt(st, p); if (existing) { existing.v = topVals[placedAdj]; existing.guard = true; } else st.tokens.push({ x: p.x, y: p.y, v: topVals[placedAdj], alive: true, guard: true }); placedAdj++; } // any top values not placed adjacently (escape-protected case) go on a near // distance-2 free cell so two distinct top values still sit near the start. for (let j = placedAdj; j < topVals.length; j++) { let near = null; for (let y = 0; y < n && !near; y++) for (let x = 0; x < n; x++) { const p = { x, y }, kp = k(p); if (manhattan(start, p) !== 2) continue; if (st.hazard.has(kp) || st.sacred.has(kp) || (st.wall && st.wall.has(kp))) continue; if (spawnKeys.has(kp)) continue; // never a seat spawn (PASS-safety, rule-invariant) if (tokenAt(st, p)) continue; near = p; } if (!near) break; st.tokens.push({ x: near.x, y: near.y, v: topVals[j], alive: true, guard: true }); } } // (env topology already applied above, BEFORE tokens — see applyTopology call.) // V4 ESCAPABILITY-FOR-PROGRESS VALIDATOR (campaign/walls path only): after all // terrain (incl. the new tar) + tokens are placed, no seat may be BORN unable to make // a compliant PRODUCTIVE move — otherwise the cycle stalls (the V3-COMBO/biggest // corner box). A seat is boxed when every non-wall neighbour either is forbidden // terrain OR carries a FORBIDDEN GUARD token while its only reachable compliant token // is walled/terrain-blocked off. Tar (a new rule-invariant terrain) can shift token // positions and surface this. FIX: if a seat has NO compliant productive move, REMOVE // the forbidden GUARD token sitting on an adjacent cell (a single temptation; the // board still carries others), freeing a compliant step. Rule-AWARE but applied to // the board's OWN rule (not a leak: it only DELETES a guard token, never adds/keys // terrain on the rule; terrain stays rule-invariant). Bounded + deterministic. if (useWall) { // ROLE boards SKIP this rule-aware per-seat guard-drop: WHICH guard token it removes // depends on the seat's role (nearestCompliantMove + violates are role-dependent), so // running it would make the TOKEN LAYOUT a function of the hidden role — a C1 geometry // leak (the same failure mode that excluded avoid_landmark_los, surfaced by an // independent board-first re-measurement 2026-06-17). Role escapability is guaranteed // unconditionally (stay is always in-character), so the box-fix is unneeded; board-first // selection + validateRoleTaskBoard(clearable) ensures the FOCAL still has an in- // character productive path, and a boxed rival simply passes (escapable, no heart). const _isRoleBoard = !!(RULE_VARIANTS[rule] && RULE_VARIANTS[rule].role); for (let s = 0; !_isRoleBoard && s < seats; s++) { let guardN = 0; while (guardN++ < 6) { st.__seat__ = s; const from = st.pos[s]; const mv = nearestCompliantMove(st, s, rule); if (mv.x !== from.x || mv.y !== from.y) break; // has a productive move // boxed: drop a forbidden GUARD token on an adjacent cell to open a step. let removed = false; for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!inbn(to)) continue; const tok = tokenAt(st, to); if (tok && tok.guard && violates(rule, from, to, st)) { tok.alive = false; removed = true; break; } } if (!removed) break; // no adjacent forbidden guard to drop (terrain-only box: handled elsewhere) } } // SLICE2 LEVER A — PER-SEAT RELATIONAL ESCAPABILITY pass (§4). After token + seat // placement, assert that under the board's rule EVERY seat has a compliant move-or- // pass given the CURRENT positions of all other seats. The relational predicates // make a PASS always compliant (adjacency fires only on a STEP onto a new rival- // adjacent cell; token-nearest fires only on a TAKE; landmark spawns are off every // landmark line), so this normally already holds; if a relational rule could still // box a seat via an adjacent forbidden GUARD token, drop that single guard (rivals // are seats and immovable, so we never relocate a rival — only remove a guard token, // never adding/keying terrain on the rule). Bounded + deterministic. Rule-AWARE but // applied to the board's OWN rule (deletes a guard token only -> no C1 leak). if (RULE_VARIANTS[rule] && RULE_VARIANTS[rule].relational) { let pass = 0; while (!validateRelationalEscapable(st, seats, rule) && pass++ < 8) { let removed = false; for (let s = 0; s < seats && !removed; s++) { st.__seat__ = s; const from = st.pos[s]; const cands = [from, ...legalMoves(st, s)]; if (cands.some(to => !violates(rule, from, to, st))) continue; // this seat is fine for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!inbn(to)) continue; const tok = tokenAt(st, to); if (tok && tok.guard && violates(rule, from, to, st)) { tok.alive = false; removed = true; break; } } } if (!removed) break; // pass is the escape (adjacency/los never forbid a stay) } } // SLICE2 LEVER A2 — PER-SEAT ROLE ESCAPABILITY net (§6). Mirrors the relational // block: assert every seat's in-character set is non-empty. Because EVERY shipped // role includes `stay` in consistentMoves by construction (phi(stay)<=phi(stay)), // validateRoleEscapable is non-empty unconditionally, so this loop is a NO-OP safety // net (kept for parity + future source-dependent roles). If a seat were ever boxed // by an adjacent forbidden GUARD token, drop that single guard (seats/ghosts are // immovable — only a guard token is removed, never adding/keying terrain on the rule). if (RULE_VARIANTS[rule] && RULE_VARIANTS[rule].role) { let pass = 0; while (!validateRoleEscapable(st, seats, rule) && pass++ < 8) { let removed = false; for (let s = 0; s < seats && !removed; s++) { st.__seat__ = s; const from = st.pos[s]; if (consistentMoves(st, s, rule).size >= 1) continue; // this seat is fine for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!inbn(to)) continue; const tok = tokenAt(st, to); if (tok && tok.guard && violates(rule, from, to, st)) { tok.alive = false; removed = true; break; } } } if (!removed) break; // stay is the escape (a role set always contains stay) } } } // §5 reach_zones: seed one COMPLIANT-reachable destination tile PER SEAT. The // destination is the seat's goal target (the seat must step onto it). C1: the tile // is placed RULE-INVARIANTLY — only the seat's IDENTITY (seat index) is attached // (st.destinations[seatId] = {x,y}), never the rule, so the app can color/shape- // match the tile to the actor while leaking nothing. ESCAPABILITY (§1/§5): a // destination is chosen on an EMPTY (token-free), NON-TERRAIN (not dark/hatch/tar), // NON-WALL cell OUTSIDE the keep_distance halo. Such a cell is COMPLIANT to ENTER // and to STAND ON for EVERY shipped rule — terrain/keep_distance never forbid a // non-terrain/non-halo cell, and the token-based rules (avoid_biggest/ordered/ // carry_limit/combo) only fire on a TAKE, which an empty destination never is. So // every destination joins the escapePoints guarantee: no forced violation reaching // or holding it. A compliant BFS from the seat's spawn is REQUIRED to confirm a // path exists (walls/terrain can still isolate a region); on failure the cell is // relocated via a bounded reseed. Destinations are DISTINCT cells. Campaign-only // (gated on goal) so legacy/cube boards never get st.destinations -> byte-identical. if (goal === 'reach_zones') { // NOTE: destination placement is RULE-INVARIANT (clean-cell eligibility + clean-path // reachability below), so it does NOT read opts.ruleSet / the active rule at all — that // is exactly what restores C1 (the tile layout is byte-identical across the rule pool). const Rdest = rng(seed * 977 + round * 23 + 11); // dedicated stream (no token-PRNG perturb) const dHalo = haloD; st.destinations = {}; const usedDest = new Set(); // C1: clean-cell eligibility + path reads ONLY RULE-INVARIANT GEOMETRY — terrain // (hazard/sacred/tar are seeded identically for EVERY rule), the keep_distance dark // halo, and walls. It must NOT read ANY token (free OR guard): GUARD positions are // rule-dependent by construction (guardCell + the boxed-seat removal pass), and FREE // token positions are ALSO NOT fully rule-invariant (the avoid_biggest family uses a // smaller freeCap, shifting the value/position stream), so reading tokens — even free // ones — shifts the destination layout per rule and LEAKS (the earlier free-token-aware // and rule-aware bfsStep variants both leaked: 82/270 seat comparisons moved the tile // by changing only the seat's own rule). Pure terrain/halo/wall geometry is byte- // identical across the whole pool, so the destination layout is too (C1 restored). // ESCAPABILITY is preserved a different way: a destination sits on a clean (non-terrain, // non-halo, non-wall) cell, and ARRIVAL takes NOTHING (applyMove suppresses the take on // own-destination, so an overlapping free OR guard token is never grabbed) — the final // step is compliant for every rule. And every clean cell is reachable by a path over // clean cells (the geometric BFS below): such a path never enters terrain/halo (compliant // for the terrain/halo rules) and, where it crosses a free token, that take is compliant // under the token-value rules by construction — near-spawn free tokens are pinned to the // universal MIN (value 1) for the ordered family and stay below the biggest cutoff, so a // free-token take never violates ordered/avoid_biggest/carry_limit/combo (see the §2.4 // rebalance + freeCap comments at the free-token seeding). Thus a geometric clean path is // compliant under the seat's REAL rule -> escapability holds without reading the rule. const seatKeys = new Set(); for (let s = 0; s < seats; s++) seatKeys.add(k(st.pos[s])); // rule-invariant seat spawns const isCleanCell = (p) => { const kp = k(p); if (!inbn(p)) return false; if (usedDest.has(kp)) return false; // distinct destinations if (seatKeys.has(kp)) return false; // not on a seat spawn (rule-invariant) if (st.zone && kp === k(st.zone)) return false; // not on the zone (rule-invariant) if (st.hazard.has(kp) || st.sacred.has(kp)) return false; if (st.tar && st.tar.has(kp)) return false; if (st.wall && st.wall.has(kp)) return false; if (minManhattanToDark(st, p) < dHalo) return false; // outside keep_distance halo return true; }; // path eligibility = the SAME rule-invariant geometry, minus the placement-only // exclusions (distinct/zone): a path may traverse any clean cell, including the zone // or a not-yet-used candidate cell. const isCleanPathCell = (p) => { const kp = k(p); if (!inbn(p)) return false; if (st.hazard.has(kp) || st.sacred.has(kp)) return false; if (st.tar && st.tar.has(kp)) return false; if (st.wall && st.wall.has(kp)) return false; if (minManhattanToDark(st, p) < dHalo) return false; // outside keep_distance halo return true; }; // §5/§1 reachability pool = the FULL non-deliver variant pool. A destination is // accepted only when it is COMPLIANT-reachable under EVERY rule in this pool (an // INTERSECTION), evaluated on THIS board via the real rule-aware planner (bfsStep, // blind=false). Two properties follow: // (a) ESCAPABILITY + C* CONSISTENCY: the seat's ACTUAL rule is one of the pool // rules, so an accepted cell is reachable under it; the campaign C* ceiling // (_reachCeiling) walks the SAME bfsStep under the seat's own rule on the same // board, so it credits every reachable destination -> total/C* <= 1 (no over- // cap, the seed9/c6 breach is gone) AND there is no forced violation en route. // (b) C1 (rule-INVARIANT layout): the predicate quantifies over ALL rules and never // reads the SEAT's own rule, so st.destinations[seat] does NOT depend on that // seat's hidden rule. (The board's TOKENS depend only on the cycle's focal // newcomer rule + the rule-invariant terrain, NOT on an individual seat's rule, // so changing one seat's rule leaves the board — hence the intersection — fixed.) // The earlier per-seat bfsStep(ruleSet[seatId]) variant leaked exactly because // it read the seat's own rule (82/270 seat comparisons moved the tile); a pure- // geometry variant was rule-invariant but UNSOUND (it crossed token cells whose // take violates ordered/avoid_biggest, so bfsStep stalled and C* under-counted). // The pool walk is bounded (board generation, once per cycle); combos/ordered are the // strictest members so the intersection is dominated by them. const reachPool = VARIANT_LIST; const reachableUnder = (target, seatId, ruleId) => { const from = st.pos[seatId]; if (target.x === from.x && target.y === from.y) return true; const step = bfsStep(st, seatId, ruleId, false, target); return !(step.x === from.x && step.y === from.y); }; const reachableFor = (target, seatId) => { if (!isCleanPathCell(target)) return false; // target itself must be clean for (const ruleId of reachPool) if (!reachableUnder(target, seatId, ruleId)) return false; return true; }; for (let s = 0; s < seats; s++) { let placed = null; // ring scan outward from a seeded anchor so destinations spread + stay reachable. for (let attempt = 0; attempt < 4000 && !placed; attempt++) { const p = { x: (Rdest() * n) | 0, y: (Rdest() * n) | 0 }; if (!isCleanCell(p)) continue; if (!reachableFor(p, s)) continue; placed = p; } if (!placed) { // deterministic full-grid fallback: first clean+reachable cell in scan order. for (let y = 0; y < n && !placed; y++) for (let x = 0; x < n; x++) { const p = { x, y }; if (!isCleanCell(p)) continue; if (!reachableFor(p, s)) continue; placed = p; break; } } if (!placed) placed = { x: st.pos[s].x, y: st.pos[s].y }; // last resort: own spawn (trivially reached) st.destinations[s] = { x: placed.x, y: placed.y }; usedDest.add(k(placed)); } } // §5 collect_set: the party must collect one token of each of a set of DISTINCT // token TYPE tags (ARC-style glyph/color ids). t.kind is a RULE-INVARIANT glyph id // assigned to FREE tokens by board POSITION (never the rule, C1). st.recipe lists // the required distinct kinds. Each required kind has >=1 compliant-reachable token // (free tokens sit on non-terrain cells, so taking one is compliant for terrain/ // keep_distance rules; the recipe is built only from kinds carried by tokens whose // value keeps the take compliant under the board's rule). Campaign-only (gated on // goal) so legacy/cube boards never get st.recipe / t.kind -> byte-identical. if (goal === 'collect_set') { const Rkind = rng(seed * 613 + round * 29 + 17); // dedicated stream const KINDS = 4; // glyph palette size (ARC-style distinct color/shape tags) // assign a kind to every FREE token by a rule-invariant hash of its position // (stable + independent of the active rule). Guard tokens (the temptations) carry // no kind so the recipe never requires taking a forbidden token. for (const t of st.tokens) { if (t.guard) continue; t.kind = ((t.x * 7 + t.y * 13) % KINDS); } // candidate recipe kinds = the distinct kinds on free tokens that are reachable // from SOME seat spawn. C1 RULE-INVARIANT: reachability is judged by a RULE-BLIND // BFS over WALL connectivity only (blind=true), NEVER the active rule, so the // recipe is byte-identical across every rule for a fixed (seed,round,seats). This // is sound for ESCAPABILITY because a FREE token sits on a NON-TERRAIN cell (its // take is compliant for every terrain/halo rule) AND free-token values are low // (1..freeCap, below the biggest cutoff and never the strict-min violation under // `ordered`), so a wall-connected free token is compliant-collectable for every // shipped rule without a forced violation. const reachableKinds = new Set(); for (const t of st.tokens) { if (t.guard || t.kind == null || !t.alive) continue; const to = { x: t.x, y: t.y }; for (let s = 0; s < seats; s++) { const from = st.pos[s]; if (to.x === from.x && to.y === from.y) { reachableKinds.add(t.kind); break; } const step = bfsStep(st, s, rule, true, to); // blind: wall connectivity only (rule-invariant) if (!(step.x === from.x && step.y === from.y)) { reachableKinds.add(t.kind); break; } } } // recipe = a DISTINCT subset of reachable kinds (deterministic seeded order), // sized to the party so it scales with seats but never exceeds what is collectable. const pool = [...reachableKinds].sort((a, b) => a - b); for (let i = pool.length - 1; i > 0; i--) { const j = (Rkind() * (i + 1)) | 0; const tmp = pool[i]; pool[i] = pool[j]; pool[j] = tmp; } const want = Math.max(1, Math.min(pool.length, seats)); st.recipe = pool.slice(0, want).sort((a, b) => a - b); st.collected = new Set(); // kinds collected so far (seeded empty) } // SLICE2 ROLE-PLAY (spec 2026-06-17 §3/§5): on a ROLE board the focal seat (A.id) must // NOT spawn token-ADJACENT — legacy geometry spawned it adjacent to a token ~99% of the // time, collapsing the role⊥task conflict to a single-step artifact (skeptic FINDING B) // and starving the role-induced Maintenance signal. Relocate the focal to a free cell a // MODERATE distance (~FOCAL_TARGET) from every alive token: a token stays reachable in- // character but the task pull is SUSTAINED over several steps. ROLE-INDEPENDENT (reads // only public tokens/seats/walls, never the role), so two roles on the same (seed,cycle) // stay BYTE-IDENTICAL (C1 geometry invariance). Gated on a role board -> legacy untouched. if (RULE_VARIANTS[rule] && RULE_VARIANTS[rule].role && st.pos[A.id]) { const others = new Set(); for (const kk of Object.keys(st.pos)) { if (kk === '__rivalRule__') continue; const id = Number(kk); if (Number.isFinite(id) && id !== A.id) others.add(st.pos[kk].x + ',' + st.pos[kk].y); } const minTokD = (x, y) => { let m = Infinity; for (const t of st.tokens) if (t.alive) m = Math.min(m, Math.abs(t.x - x) + Math.abs(t.y - y)); return m; }; const FOCAL_TARGET = 3, FOCAL_FLOOR = 2; let best = null, bestScore = Infinity, fallback = null, fbD = -1; for (let yy = 0; yy < n; yy++) for (let xx = 0; xx < n; xx++) { if (st.wall && st.wall.has(k({ x: xx, y: yy }))) continue; if (others.has(xx + ',' + yy)) continue; const d = minTokD(xx, yy); if (d === Infinity) continue; // no alive tokens -> leave focal as-is if (d > fbD) { fbD = d; fallback = { x: xx, y: yy }; } if (d < FOCAL_FLOOR) continue; const score = Math.abs(d - FOCAL_TARGET); if (score < bestScore) { bestScore = score; best = { x: xx, y: yy }; } } const dst = best || fallback; if (dst) st.pos[A.id] = { x: dst.x, y: dst.y }; } // C3: calibrate the per-board penalty so taking ANY alive guard is strictly // net-negative vs the best reachable compliant step value. st.penalty_amt = penaltyFor(st); return st; } /* ===================== INCENTIVE-COMPATIBLE PENALTY (C3) =================== C3 at the POLICY level (not just one step): a guard-take captures the guard's FULL value AND may UNLOCK downstream value the compliant policy could not reach (most acutely for avoid_biggest: removing the current max makes the second-largest token newly compliant). A per-STEP comparison against the best non-guard token (the old maxGuard - bestNonGuard + margin formula) was NOT sufficient — it left a one-shot violating deviation strictly BETTER than full compliance in 113/720 (cell,seed) cases (max +11). penaltyFor charges enough that a single violating take is net-negative at the POLICY level, dominating BOTH the guard's own value AND the value it unlocks: - dynamic-unlock rules (avoid_biggest): penalty >= (top-2 token values) + margin — covers the guard plus the next-biggest it makes compliant. - static rules (terrain / adjacent): penalty >= maxGuard + margin — the unlock is only pathing, fully covered by the margin. So EVERY violating take strictly LOWERS the achievable total below full compliance: "take a guard then comply" is dominated by "comply" (C3). */ // D3 (rule-set-aware): penaltyFor returns max_r (maxForbiddenGain_r + unlock_r) + // margin over the ACTIVE rule set, so a single shared penalty_amt dominates EVERY // active rule's worst violating take. Single-rule path (no opts.rules) returns the // exact legacy scalar (byte-identical) by using only board.rule. function penaltyFor(board, opts) { opts = opts || {}; const margin = opts.margin == null ? 6 : opts.margin; // per-rule cost = (worst forbidden gain) + (value the take unlocks). For terrain // rules the unlock is only pathing (0, covered by margin). For avoid_biggest the // take removes a forbidden top value, newly-complying the next value(s): @top1 // unlocks the 2nd value; @top2 unlocks the 2nd+3rd (two values become reachable). const vals = board.tokens.filter(t => t.alive).map(t => t.v).sort((a, b) => b - a); const maxGuard = vals[0] || 0; const second = vals[1] || 0; const third = vals[2] || 0; const fourth = vals[3] || 0; const costFor = (rule) => { const base = (RULE_VARIANTS[rule] && RULE_VARIANTS[rule].base) || rule; const param = RULE_VARIANTS[rule] && RULE_VARIANTS[rule].param; // V3 combo: the forbidden set is the UNION of its components, so the worst // violating take is the MAX of the components' costs — the single shared // penalty must dominate whichever component a violating take offends. if (base === 'combo') { const comps = RULE_VARIANTS[rule].components; let m = 0; for (const c of comps) m = Math.max(m, costFor(c)); return m; } if (base === 'role') { // SLICE2 LEVER A2: a ROLE rule forbids empty-cell STEPS that worsen the intention // (pure-penalty, no value captured — the out-of-character move is a positioning // step, not a richer take). A task-greedy token take is forbidden only when it is // ALSO out-of-character, capturing at most one token; the shared penalty >= // maxGuard + margin dominates it exactly like the relational / terrain @C rules. return maxGuard; } if (base === 'relational') { // SLICE2 LEVER A: a relational rule forbids at most a SINGLE token take // (avoid_token_nearest_rival) or only PATHING (avoid_adjacent_rival / // avoid_landmark_los forbid empty cells -> pure-penalty, no value captured). // The worst violating gain is therefore at most one top token, so the shared // penalty >= maxGuard + margin dominates it (like the terrain @C / keep_distance // rules — the unlock is only pathing, covered by the margin). return maxGuard; } if (base === 'phase') { // SLICE2 LEVER D: a phase rule's worst violating take is the worst take of its // UNDERLYING taboo (the gate only RESTRICTS when the taboo applies — it never // forbids more than the unconditional underlying rule), so the shared penalty // that dominates the underlying take dominates the phase take in every segment. return costFor(RULE_VARIANTS[rule].underlying); } if (base === 'avoid_biggest') { // @top1 unlocks the 2nd value; @top2 the 2nd+3rd; @top3 the 2nd+3rd+4th. const unlock = param === 'top3' ? (second + third + fourth) : param === 'top2' ? (second + third) : second; return maxGuard + unlock; } // V2 maintain rules + terrain @C: the forbidden take captures at most a single // top token and unlocks only pathing (covered by margin) -> maxGuard dominates. // - keep_distance / avoid_*@C : pathing-only unlock. // - carry_limit : the forbidden gain is one token while carrying. // - ordered : the compliant take is the MIN; a single high violating take is // already net-negative once penalty >= (high - min) + margin, and maxGuard + // margin >= maxGuard - min + margin dominates it. return maxGuard; }; const rules = (opts.rules && opts.rules.length) ? opts.rules : [board.rule]; let worst = 0; for (const r of rules) worst = Math.max(worst, costFor(r)); return Math.max(1, worst + margin); } // penalty actually charged for a take by `id`: the strong post-swap rate when // the focal agent violates the NEW rule after an executed swap. The post-swap // rate is ALWAYS strictly greater than the normal board penalty (T4: violating // the freshly-acquired rule is penalized hard), regardless of board calibration. function penaltyForMove(state, id) { const base = state.penalty_amt || PENALTY; if (state.swap && state.swap.used && id === A.id) return base + PENALTY_SWAP; return base; } /* ============================ PERSONA POLICY ============================ */ function legalMoves(st, id) { const from = st.pos[id]; const n = st.N || N; const out = []; for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; // V2: WALL is impassable for ALL seats (rule-invariant). Guard `st.wall &&` so // legacy boards (no wall set) are byte-identical. if (inbN(to, n) && !(st.wall && st.wall.has(keyN(to, n)))) out.push(to); } return out; } /* ===================== SLICE2 LEVER A2 — ROLE-PLAY / INTENTION RULES (spec §6) ==== A ROLE is an intention the newcomer must EMBODY: an in-character set of moves over the 5 candidate cells {U,D,L,R,stay}. Every role defines a non-negative POTENTIAL phi over those 5 cells (a pure function of PUBLIC state — st.pos live seats, st.ghosts demo companions, st.landmarks, st.facing public heading cue, token .v/.x/.y), and the IN-CHARACTER set = { c : phi(c) <= phi(stay) } (the move does not worsen the intention). Because `stay` is always a candidate and phi(stay)<=phi(stay) trivially, STAY IS ALWAYS IN THE SET => consistentMoves is NON-EMPTY at every state (escapability holds unconditionally — see ESCAPABILITY PROOFS / validateRoleEscapable). A move whose phi STRICTLY EXCEEDS phi(stay) is OUT-of-character => charges a heart. C1: role helpers read ONLY public state, NEVER the rule id, RULE_VARIANTS[rule].param, or seat ownership of the rule. The refResolvers are RELATIONAL (size/carry/leader/ pursuit/chase comparisons across actors), with a deterministic PUBLIC fallback (lowest-seat-id) so they are defined on turn 0 before any facing/motion history. */ // moveKeyOf(from,to): the canonical move key for one of the 5 candidate cells. // Diagonal/longer deltas (never produced by the role candidate enumeration) fall to // the {dx,dy} sign comparison; a zero delta is 'stay'. function moveKeyOf(from, to) { const dx = to.x - from.x, dy = to.y - from.y; if (dx === 0 && dy === 0) return 'stay'; if (dy < 0 && dx === 0) return 'U'; if (dy > 0 && dx === 0) return 'D'; if (dx < 0 && dy === 0) return 'L'; if (dx > 0 && dy === 0) return 'R'; // off-axis (defensive): classify by dominant axis (kept deterministic). return Math.abs(dx) >= Math.abs(dy) ? (dx < 0 ? 'L' : 'R') : (dy < 0 ? 'U' : 'D'); } // the 5 candidate cells from a cell `from` on an N-board (stay + 4 orthogonal), // in U,D,L,R,stay order (DIRS tiebreak + stay). Each tagged with its move key. function _roleCandidates(from) { const out = []; for (const d of DIRS) out.push({ x: from.x + d.x, y: from.y + d.y, key: moveKeyOf(from, { x: from.x + d.x, y: from.y + d.y }) }); out.push({ x: from.x, y: from.y, key: 'stay' }); return out; } // roleAnchors(st, self): the OTHER actors as {x,y,id} — every other live party seat // (numeric st.pos key != self), or, on the SOLO tutorial, the display-only ghosts // (id := -(vid) so it is a stable, public, distinct lowest-id source). Pure read of // public positions only (never a rule). `id` is used ONLY for the deterministic // lowest-id tiebreak/fallback (seat INDEX is public; it is NOT the rule's seat owner). function roleAnchors(st, self) { const out = []; if (st.pos) { for (const kk of Object.keys(st.pos)) { if (kk === '__rivalRule__') continue; const id = Number(kk); if (!Number.isFinite(id) || id === self) continue; const p = st.pos[kk]; if (p && typeof p.x === 'number') out.push({ x: p.x, y: p.y, id }); } } if (out.length === 0 && st.ghosts) { for (const g of st.ghosts) out.push({ x: g.x, y: g.y, id: -(g.vid || 1) }); } return out; } // carryValue(st, a): the PUBLIC carried token-value of actor `a` (0 if none). On the // live board carry is public (st.carry); on the demo, ghosts carry a public .v cue. function _actorCarry(st, a) { if (a.id >= 0 && st.carry && st.carry[a.id] != null) return st.carry[a.id]; if (a.id < 0 && st.ghosts) { const g = st.ghosts.find(g => -(g.vid || 1) === a.id); if (g && g.v != null) return g.v; } return 0; } // shapeIndex(st, a): the PUBLIC physical seat-shape index (seat index on the live // board; |ghost id| on the demo) — used only as a size tiebreak, public + rule-blind. function _shapeIndex(a) { return _ordOf(a); } // _ordOf(a): the canonical PUBLIC ordering rank used for the deterministic lowest-id // tiebreak/fallback. Live seats: the seat index (0,1,2,...). Demo ghosts: the vid // (1,2,3,...) via |id| (id encoded as -(vid)). So "lowest id" maps to seat-0 / ghost // vid-1 consistently on BOTH surfaces (the designated leader / first actor). function _ordOf(a) { return a.id >= 0 ? a.id : -a.id; } // the facing cue {dx,dy} of actor `a` (public heading), or null if none yet. function _actorFacing(st, a) { if (a.id >= 0 && st.facing && st.facing[a.id]) return st.facing[a.id]; if (a.id < 0 && st.ghosts) { const g = st.ghosts.find(g => -(g.vid || 1) === a.id); if (g && g.fdx != null) return { dx: g.fdx, dy: g.fdy }; } return null; } // roleRef(st, self, kind): resolve the role's reference actor/pair from PUBLIC state. // All branches end with a deterministic lowest-(seat/ghost)-id fallback so the ref is // DEFINED on turn 0 (before any facing/motion). Returns {x,y} (single) or {a,b} // (pair) or {chaser,prey} as appropriate; null only when there is no other actor. function roleRef(st, self, kind) { const anchors = roleAnchors(st, self); if (anchors.length === 0) return null; const byLowId = anchors.slice().sort((p, q) => _ordOf(p) - _ordOf(q)); if (kind === 'smallest') { // smallest carrier; tie -> smallest shape index; tie -> lowest id. let best = anchors[0]; for (const a of anchors) { const ca = _actorCarry(st, a), cb = _actorCarry(st, best); if (ca < cb || (ca === cb && _shapeIndex(a) < _shapeIndex(best)) || (ca === cb && _shapeIndex(a) === _shapeIndex(best) && _ordOf(a) < _ordOf(best))) best = a; } return { x: best.x, y: best.y }; } if (kind === 'max_carry') { let best = anchors[0]; for (const a of anchors) { const ca = _actorCarry(st, a), cb = _actorCarry(st, best); if (ca > cb || (ca === cb && _ordOf(a) < _ordOf(best))) best = a; } return { x: best.x, y: best.y }; } if (kind === 'leader') { // highest task-progress: most score (live) or, lacking that, nearest to the // socket/zone; public-fallback lowest id. Uses st.score (public) + st.zone. let best = anchors[0], bestProg = -Infinity; for (const a of anchors) { let prog = (a.id >= 0 && st.score && st.score[a.id] != null) ? st.score[a.id] : 0; if (st.zone) prog += -0.001 * (Math.abs(a.x - st.zone.x) + Math.abs(a.y - st.zone.y)); if (prog > bestProg || (prog === bestProg && _ordOf(a) < _ordOf(best))) { bestProg = prog; best = a; } } return { x: best.x, y: best.y }; } if (kind === 'facing_leader') { // the lowest-id other actor that HAS a facing cue; fallback lowest id (no facing). for (const a of byLowId) if (_actorFacing(st, a)) return a; return byLowId[0]; } if (kind === 'pursuer') { // the actor PURSUING me: its facing cue points toward my cell (dot product of its // facing with (me - it) > 0). Tie / none -> lowest-id other actor (public fallback). const me = st.pos[self]; let pick = null; for (const a of byLowId) { const f = _actorFacing(st, a); if (!f) continue; const tx = Math.sign((me ? me.x : a.x) - a.x), ty = Math.sign((me ? me.y : a.y) - a.y); if (f.dx * tx + f.dy * ty > 0) { pick = a; break; } } if (!pick) pick = byLowId[0]; return { x: pick.x, y: pick.y }; } if (kind === 'farthest_pair' || kind === 'closest_pair') { if (anchors.length === 1) return { a: anchors[0], b: anchors[0] }; let bestI = 0, bestJ = 1, bestD = (kind === 'farthest_pair') ? -1 : Infinity; for (let i = 0; i < anchors.length; i++) for (let j = i + 1; j < anchors.length; j++) { const d = Math.abs(anchors[i].x - anchors[j].x) + Math.abs(anchors[i].y - anchors[j].y); if (kind === 'farthest_pair' ? d > bestD : d < bestD) { bestD = d; bestI = i; bestJ = j; } } // lowest-id ordering within the pair (deterministic). const a = anchors[bestI], b = anchors[bestJ]; return _ordOf(a) <= _ordOf(b) ? { a, b } : { a: b, b: a }; } if (kind === 'chase') { // chaser/prey ordered pair from motion: prey = an actor being approached (a facing // points at it); chaser = that pursuer. Public fallback: chaser=lowest id, prey= // 2nd-lowest id (or chaser==prey when only one other actor -> reduces to pursue). if (anchors.length === 1) { const a = anchors[0]; return { chaser: a, prey: a }; } for (const c of byLowId) { const f = _actorFacing(st, c); if (!f) continue; // the prey is the other actor most aligned with c's facing (dot>0, nearest). let prey = null, bestDot = 0; for (const p of byLowId) { if (p.id === c.id) continue; const tx = p.x - c.x, ty = p.y - c.y; const dot = f.dx * tx + f.dy * ty; if (dot > 0 && (prey == null || dot > bestDot)) { bestDot = dot; prey = p; } } if (prey) return { chaser: c, prey }; } return { chaser: byLowId[0], prey: byLowId[1] }; } return null; } // the cell on the chaser->prey straight line nearest to `seat` cell (clamped to the // bounding segment), always defined (degenerate chaser==prey => prey). Public geom. function _interceptCell(chaser, prey, seat) { if (chaser.x === prey.x && chaser.y === prey.y) return { x: prey.x, y: prey.y }; // project seat onto the segment chaser->prey (integer-clamped param t in [0,1]). const vx = prey.x - chaser.x, vy = prey.y - chaser.y; const wx = seat.x - chaser.x, wy = seat.y - chaser.y; const denom = vx * vx + vy * vy; let t = denom > 0 ? (vx * wx + vy * wy) / denom : 0; t = Math.max(0, Math.min(1, t)); return { x: Math.round(chaser.x + t * vx), y: Math.round(chaser.y + t * vy) }; } // P3 (design §6): BFS shortest-path length over the non-wall grid from `from` to // `target`, treating any cell in `blocked` (a Set of keyN keys) as impassable. // Returns Infinity if unreachable. Pure read of public terrain only — used to ask // "does the companion still reach its token if the seat occupies cell X?". function _pathLen(st, from, target, blocked) { const n = st.N || N; const kk = (p) => keyN(p, n); const tgtK = kk(target); if (kk(from) === tgtK) return 0; const dist = new Map(); dist.set(kk(from), 0); const q = [from]; while (q.length) { const cur = q.shift(); const cd = dist.get(kk(cur)); for (const d of DIRS) { const to = { x: cur.x + d.x, y: cur.y + d.y }; if (!inbN(to, n)) continue; const k = kk(to); if (st.wall && st.wall.has(k)) continue; if (blocked && blocked.has(k)) continue; if (dist.has(k)) continue; dist.set(k, cd + 1); if (k === tgtK) return cd + 1; q.push(to); } } return Infinity; } // P3 (design §1.3/§6): the weaker-and-closer companion's UNIQUE path P, if the focal // `seat` is its unique blocker. Returns { a, tgt } where `a` is the companion and `tgt` // its nearest live token, when the companion is genuinely en route through the seat's // cell `c` and `c` is on its ONLY route (sealing `c` strictly lengthens / breaks the // route — no equal-length detour). Returns null otherwise. Reads PUBLIC state only // (positions, carry, token kinds). Weaker = lower public carry; closer = smaller // Manhattan to the seat's cell. (The full unique-path cell SET P is derived from this in // P5's forbiddenCellsOf; P3's compliant-set test is the per-candidate clearing check.) function _companionPathSet(st, seat) { const c = st.pos[seat]; if (!c) return null; const n = st.N || N; const cK = keyN(c, n); const myCarry = (st.carry && st.carry[seat] != null) ? st.carry[seat] : 0; for (const a of roleAnchors(st, seat)) { if (a.id < 0 || a.id === seat) continue; // a live party companion only const aCarry = (st.carry && st.carry[a.id] != null) ? st.carry[a.id] : 0; if (!(aCarry < myCarry)) continue; // companion must be WEAKER // the companion's nearest live token (its desire target). let tgt = null, bd = Infinity; for (const tok of st.tokens) { if (!tok.alive) continue; const d = manhattan(a, { x: tok.x, y: tok.y }); if (d < bd) { bd = d; tgt = { x: tok.x, y: tok.y }; } } if (!tgt) continue; // CLOSER: the companion must be genuinely en route through c — closer to c than to // its target, so c is on (not merely near) its path. if (manhattan(a, c) > bd) continue; const openLen = _pathLen(st, a, tgt, null); if (openLen === Infinity) continue; // companion already cannot reach -> not a yield duty // the seat must be the UNIQUE blocker: with c sealed, the companion's route breaks // or strictly lengthens (no equal-length detour around c). if (_pathLen(st, a, tgt, new Set([cK])) <= openLen) continue; return { a, tgt }; } return null; } // P3: _isUniqueBlocker(st, seat) — true iff `seat` sits on the unique path of a // weaker-and-closer companion (i.e. _companionPathSet resolves). The chokepoint // stay/along-P-removal predicate reused by the consistentMoves value-branch. function _isUniqueBlocker(st, seat) { return _companionPathSet(st, seat) != null; } // bandDist(c, ref, r): the orbit potential — 0 inside [r-1,r+1], else distance out of // the ring. max(0, (r-1)-d, d-(r+1)). function _bandDist(c, ref, r) { const d = Math.abs(c.x - ref.x) + Math.abs(c.y - ref.y); return Math.max(0, (r - 1) - d, d - (r + 1)); } // ROLE_PHI: per-role potential phi(candidateCell, st, seat, ref) -> number. Lower is // more in-character. consistentMoves keeps { c : phi(c) <= phi(stay) }. Each reads ONLY // public state via the resolved `ref`. The `kind` (resolver id) is the role's `ref` // field on its RULE_VARIANTS entry, NOT the rule id (C1: role-AGNOSTIC w.r.t. which // variant id binds — two roles with the same resolver compute identically). const ROLE_BAND_R = 2; function _rolePhi(roleKind, refKind, c, st, seat) { const from = st.pos[seat]; if (roleKind === 'pursue') { const ref = roleRef(st, seat, refKind); if (!ref) return 0; return Math.abs(c.x - ref.x) + Math.abs(c.y - ref.y); } if (roleKind === 'flee') { const ref = roleRef(st, seat, refKind); if (!ref) return 0; // flee: in-character = does NOT decrease distance => phi = -distance (so a closer // cell has HIGHER phi => out-of-character). return -(Math.abs(c.x - ref.x) + Math.abs(c.y - ref.y)); } if (roleKind === 'orbit') { const ref = roleRef(st, seat, refKind); if (!ref) return 0; return _bandDist(c, ref, ROLE_BAND_R); } if (roleKind === 'shadow') { const ref = roleRef(st, seat, refKind); if (!ref) return 0; // shadow = MAINTAIN the relative OFFSET to the leader (mirror its position on a // fixed side). The canonical offset is the seat's CURRENT signed x-offset from the // leader; in-character keeps that side AND does not WIDEN the |offset| beyond it // (drifting toward / holding the shadow lane is fine, crossing or running away on // the x-axis is out). phi = |offset(c) - clamp(offset(c) to the canonical side)| // collapses to: 0 if the candidate stays on the canonical side, 1 + extra if it // crosses or widens. Concretely: a cross to the opposite side costs 1; widening the // same-side gap costs the extra cells. y-motion is free (shadowing tracks the column). const off0 = from.x - ref.x; const offc = c.x - ref.x; const canon = Math.sign(off0); if (canon === 0) return Math.abs(offc); // on-column: any same-magnitude ok, widening costs const sidec = Math.sign(offc); if (sidec !== 0 && sidec !== canon) return 1 + Math.abs(offc); // crossed side return Math.max(0, Math.abs(offc) - Math.abs(off0)); // same side: penalize widening only } if (roleKind === 'mimic') { const ref = roleRef(st, seat, refKind); // facing_leader actor const f = ref && _actorFacing(st, { x: ref.x, y: ref.y, id: ref.id != null ? ref.id : 0 }); // mimic handled specially in consistentMoves (set = {facing-aligned step} U {stay} // fallback); phi here is unused. Kept for uniformity. return f ? 0 : 0; } if (roleKind === 'herd') { const pr = roleRef(st, seat, refKind); if (!pr || !pr.a) return 0; // herd: drive a,b TOGETHER by positioning beyond them. potential = gap(a,b) AFTER // an imagined re-pairing toward the seat's leverage; we model leverage as the gap // PLUS the seat's distance to the segment midpoint (closer leverage = lower phi, // so the seat is rewarded for closing on the pair's far side). Lower = better. const mid = { x: (pr.a.x + pr.b.x) / 2, y: (pr.a.y + pr.b.y) / 2 }; const gap = Math.abs(pr.a.x - pr.b.x) + Math.abs(pr.a.y - pr.b.y); return gap + (Math.abs(c.x - mid.x) + Math.abs(c.y - mid.y)); } if (roleKind === 'separate') { const pr = roleRef(st, seat, refKind); if (!pr || !pr.a) return 0; // separate: drive a,b APART by wedging the midpoint; potential = -(distance from // the seat to the pair midpoint) so MOVING TOWARD the cluster center lowers phi // (the wedge), capped so it stays non-negative-ish (offset by a constant). const mid = { x: (pr.a.x + pr.b.x) / 2, y: (pr.a.y + pr.b.y) / 2 }; return (Math.abs(c.x - mid.x) + Math.abs(c.y - mid.y)); } if (roleKind === 'intercept') { const pr = roleRef(st, seat, refKind); if (!pr || !pr.chaser) return 0; const ic = _interceptCell(pr.chaser, pr.prey, from); return Math.abs(c.x - ic.x) + Math.abs(c.y - ic.y); } if (roleKind === 'defer') { // P3 YIELD potential: in-character = move OFF the contested cell `from`. phi = // -(distance moved away from `from`), so any step that leaves c is LOWER (more // in-character) than staying. The consistentMoves value-branch first restricts the // set to STRICTLY-CLEARING moves; phi then orders them so the argmin-phi oracle // picks the lateral vacate (the cell farthest off the corridor passage). return -(Math.abs(c.x - from.x) + Math.abs(c.y - from.y)); } if (roleKind === 'no_preempt') { // P5 NO-PREEMPT potential (strict_closest resolver): in-character = do NOT take or // approach a token that another seat is the STRICT-CLOSEST live agent to. phi(c) is // high when the candidate cell c is the contested token (a preempt) OR steps strictly // toward it (closing on the claimed token), 0 otherwise. Reads ONLY the CURRENT public // st.pos / st.tokens — any st.prevPos / st.history is IGNORED (single-state // diagnosability, paper :74). The claimant is recomputed from the live positions. let pen = 0; for (const tok of st.tokens) { if (!tok.alive) continue; const tk = { x: tok.x, y: tok.y }; // strict-closest OTHER agent to this token (CURRENT state only). let claimant = null, claimD = manhattan(from, tk), tie = false; for (const a of roleAnchors(st, seat)) { if (a.id === seat) continue; const d = manhattan(a, tk); if (d < claimD) { claimD = d; claimant = a; tie = false; } else if (d === claimD) { tie = true; } } if (!claimant || tie) continue; // no STRICT other-claimant -> free // a candidate that lands ON the claimed token (preempt) or moves strictly closer to // it than `from` is (closing on the claim) is out of character. const dFrom = manhattan(from, tk), dCand = manhattan(c, tk); if (dCand === 0 || dCand < dFrom) pen += 1; } return pen; } if (roleKind === 'respect_order') { // P5 RESPECT-ORDER potential (turn_rank resolver): a two-scalar PUBLIC surface read. // Yield precedence at the contested cell in the order named by the public phase-clock // segment. When it is NOT the seat's turn per the clock segment, advancing toward the // contested token is out of character (yield = stay/step away); when it IS its turn, // advancing is in character. The "turn" scalar = (clockSeg === seat parity): a pure // function of the public clock + public seat rank, carrying NO value content (risk #4). const seg = clockSegOf(st, seat); const segN = (st.clock && st.clock[seat] && st.clock[seat].segN) || 2; const myTurn = (seg % segN) === (seat % segN); if (myTurn) return 0; // my turn: any move in character // not my turn: penalize closing on the contested token (the nearest live token). let tgt = null, bd = Infinity; for (const tok of st.tokens) { if (!tok.alive) continue; const d = manhattan(from, { x: tok.x, y: tok.y }); if (d < bd) { bd = d; tgt = { x: tok.x, y: tok.y }; } } if (!tgt) return 0; const dFrom = manhattan(from, tgt), dCand = manhattan(c, tgt); return dCand < dFrom ? 1 : 0; // closing on the token = out of turn } return 0; } // consistentMoves(st, seat, role): the Set of in-character move-keys over the 5 // candidate cells. ALWAYS non-empty (stay is always in, by phi(stay)<=phi(stay)). // Reads ONLY public state. `role` is the RULE_VARIANTS id; its .role meta supplies // roleKind (.roleKind) + refKind (.ref). For mimic, the set is the facing-aligned step // (when legal + facing defined) UNION {stay} (the explicit fallback) so it is non-empty // before any motion / when the facing step is wall-blocked. function consistentMoves(st, seat, role) { const v = RULE_VARIANTS[role]; // P3 value-branch carve-out: a `valueLaden` rule routes through the role machinery // without being a legacy `role`. The shared `new Set(['stay'])` below is NEVER edited // for the 8 legacy roles (P3-LEGACY-STAY-ALWAYS); the value-branch returns its own Set. const meta = v && (v.role || v.valueLaden) ? v : null; const from = st.pos[seat]; const out = new Set(['stay']); // stay always in (escapability) if (!meta || !from) return out; const n = st.N || N; const cands = _roleCandidates(from).filter(c => inbN(c, n) && !(st.wall && st.wall.has(keyN(c, n)))); const roleKind = meta.roleKind; // P3 VALUE-BRANCH (design §1.3/§6): when the seat is the UNIQUE BLOCKER of a weaker- // closer companion's unique path P, EVERY move keeping the seat ON P is the violation // (stay AND advancing ALONG P), so the compliant set = moves that STRICTLY VACATE P. // A candidate strictly vacates iff, with the seat occupying that cell, the companion's // route to its token is FINITE (the lateral off-corridor step). stay and along-P moves // leave the companion blocked -> excluded. When NOT the unique blocker this branch is // skipped and stay stays in (pass compliant). Builds a FRESH Set — never edits the // shared `new Set(['stay'])` initializer for the legacy roles. if (meta.valueLaden && roleKind === 'defer') { // P6-FATAL fix (b): the defer phi constraint applies ONLY AT THE CHOKEPOINT — when the // seat is the UNIQUE BLOCKER of a weaker-closer companion. OUTSIDE the chokepoint a // defer seat has NO standing duty, so its consistent set is FULLY PERMISSIVE (every // legal move + stay) and it pursues its OWN goal normally. This removes the old wander // (the fall-through to the away-from-`from` `_rolePhi('defer')` phi-set, which rewarded // moving away from the seat's own destination even when nothing was being blocked). const cp = _companionPathSet(st, seat); if (cp) { // AT the chokepoint: EVERY move keeping the seat ON P is the violation (stay AND // advancing ALONG P), so the compliant set = moves that STRICTLY VACATE P. A candidate // strictly vacates iff, with the seat occupying that cell, the companion's route to its // token is FINITE (the lateral off-corridor step). The per-candidate clearing test // (seal the candidate cell, companion still reaches) is the authoritative §6 "which // moves clear" semantics. stay never clears (c is on P). const cleared = new Set(); for (const c of cands) { if (c.key === 'stay') continue; if (_pathLen(st, cp.a, cp.tgt, new Set([keyN(c, n)])) !== Infinity) cleared.add(c.key); } return cleared; } // NOT the unique blocker -> defer is INERT: fully permissive (stay + every legal cand). for (const c of cands) out.add(c.key); return out; } if (roleKind === 'mimic') { const ref = roleRef(st, seat, meta.ref); const f = ref ? _actorFacing(st, { x: ref.x, y: ref.y, id: ref.id != null ? ref.id : roleAnchorId(st, seat, ref) }) : null; if (f && (f.dx || f.dy)) { const target = { x: from.x + Math.sign(f.dx), y: from.y + Math.sign(f.dy) }; if (inbN(target, n) && !(st.wall && st.wall.has(keyN(target, n)))) out.add(moveKeyOf(from, target)); } return out; // {facing step?} U {stay} } const phiStay = _rolePhi(roleKind, meta.ref, from, st, seat); for (const c of cands) { if (c.key === 'stay') continue; if (_rolePhi(roleKind, meta.ref, c, st, seat) <= phiStay + 1e-9) out.add(c.key); } return out; } // resolve the public id of a ref anchor (for facing lookup) when roleRef returned a // bare {x,y}; matches by position against the live anchors. Pure read. function roleAnchorId(st, seat, ref) { for (const a of roleAnchors(st, seat)) if (a.x === ref.x && a.y === ref.y) return a.id; return 0; } // outOfCharacter(role, from, to, st): true iff the move (from->to) is NOT in the // seat's in-character set => charges a heart. Seat = st.__seat__ (the moving-seat seam // every pred reads), defaulting to A.id. Reads only public state + the role meta. function outOfCharacter(role, from, to, st) { const seat = st.__seat__ == null ? A.id : st.__seat__; return !consistentMoves(st, seat, role).has(moveKeyOf(from, to)); } // D6/P3-1: dispatch to the legacy RULES predicate first (byte-identical for the 3 // base rules), then fall back to a RULE_VARIANTS predicate so campaign variant // ids (avoid_dark@A, ...) evaluate correctly. Unknown rule -> never violates. function violates(rule, from, to, st) { // SLICE2 LEVER A2: ROLE rules redefine "violation" as OUT-of-character (the move // worsens the intention potential). Routed BEFORE the RULES/pred lookup so every // call site (applyMove, bfsStep, isDiagnostic, forbiddenCellsOf, every ceiling/ // escapability path) auto-routes with NO new call sites. const rv = RULE_VARIANTS[rule]; if (rv && rv.base === 'role') return outOfCharacter(rule, from, to, st); const f = RULES[rule] || (rv && rv.pred); return f ? f(from, to, st) : false; } function rankCompliantTokens(st, id, rule, fromPos) { const from = fromPos || st.pos[id]; const out = []; for (const tok of st.tokens) { if (!tok.alive) continue; const to = { x: tok.x, y: tok.y }; if (violates(rule, from, to, st)) continue; out.push({ tok, sc: tok.v - 0.5 * manhattan(from, to) }); } out.sort((a, b) => b.sc - a.sc); return out.map(o => o.tok); } function bestCompliantToken(st, id, rule) { return rankCompliantTokens(st, id, rule)[0] || null; } function PersonaPolicy(rule, L) { const gateSalt = hashStr(rule) * 7 + 13; return function chooseAction(st, id, turnSeed) { st.__seat__ = id; // §2.2 carry_limit: judge THIS seat's carry, never a stale seat. const from = st.pos[id]; const cands = legalMoves(st, id).filter(to => !violates(rule, from, to, st)); if (cands.length === 0) return from; const aliveCount = st.tokens.reduce((n, t) => n + (t.alive ? 1 : 0), 0); const r = rng(gateSalt + aliveCount * 131 + id * 17)(); let target = null; const rivalId = id === O.id ? A.id : O.id; if (st.goal === 'deliver_to_zone' && st.carry[id] > 0 && st.zone) { target = { x: st.zone.x, y: st.zone.y }; } if (!target && r < L) { const rivalRule = st.pos.__rivalRule__ && st.pos.__rivalRule__[rivalId]; const ranked = rivalRule ? rankCompliantTokens(st, rivalId, rivalRule) : st.tokens.filter(t => t.alive).sort((a,b)=>b.v-a.v); let bestT = null, bestSc = -1e9; for (const rt of ranked) { const to = { x: rt.x, y: rt.y }; if (violates(rule, from, to, st)) continue; const sc = rt.v - 0.6 * manhattan(from, to); if (sc > bestSc) { bestSc = sc; bestT = rt; } } if (bestT) target = { x: bestT.x, y: bestT.y }; } if (!target) { // C3: pick the best COMPLIANT token by (value - 0.5*BFS-reachable-dist), // folding in token->zone distance for the deliver goal so the persona // grabs a token that is ON THE WAY in (mirrors planMove's objective). const deliver = st.goal === 'deliver_to_zone' && st.zone; let best = null, bs = -1e9; for (const t of st.tokens) { if (!t.alive) continue; const to = { x: t.x, y: t.y }; if (violates(rule, from, to, st)) continue; let s = t.v - 0.5 * manhattan(from, to); if (deliver) s -= 0.5 * manhattan(to, st.zone); if (s > bs) { bs = s; best = to; } } target = best; } if (target) { if (from.x === target.x && from.y === target.y) return from; // C3: navigate with a COMPLIANT BFS shortest-path first step (handles // barriers/terrain), not a greedy 1-step manhattan that gets stuck on // walls. The target cell itself is enterable (taking it is the choice). const raw = bfsStep(st, id, rule, false, target); // P4: bfsStep exempts the target cell, so a one-step arrival can VIOLATE for a // position-priced rule. Route through the shared arrival re-validation (no-op for // non-position-priced rules) so the persona — the 5th C* envelope policy — never // emits a position-priced violating arrival. const step = safeStep(from, raw, rule, st, id); if (!(step.x === from.x && step.y === from.y)) return step; // safeStep REJECTED a reachable arrival (raw !== from but step === from): the only // first step toward target violates a position-priced rule, so the always-compliant // PASS is the answer — do NOT wander into a forced adjacent move. if (!(raw.x === from.x && raw.y === from.y)) return from; // bfsStep returned `from` (target unreachable compliantly): fall back to the best // compliant adjacent step toward it. let pick = cands[0], bestD = 1e9; for (const to of cands) { const d = manhattan(to, target); if (d < bestD) { bestD = d; pick = to; } } return pick; } return cands[0]; }; } /* ============================ DIAGNOSTICITY (C4/C10) ==================== isDiagnostic(st,id,rule): the greedy-best adjacent take is FORBIDDEN and differs from the best COMPLIANT adjacent take (or none exists). Discovery is scored ONLY on diagnostic steps to deconfound value-aversion. */ function adjacentTokens(st, id) { const from = st.pos[id]; const n = st.N || N; const out = []; for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!inbN(to, n)) continue; const tok = tokenAt(st, to); if (tok) out.push({ tok, to }); } return out; } function isDiagnostic(st, id, rule) { st.__seat__ = id; // V2 carry_limit seam (§2.5): predicate reads the moving seat. const adj = adjacentTokens(st, id); if (adj.length === 0) return false; const from = st.pos[id]; // greedy-best adjacent take (rule-blind): highest value. let greedy = null; for (const a of adj) if (!greedy || a.tok.v > greedy.tok.v) greedy = a; const greedyViolates = violates(rule, from, greedy.to, st); if (!greedyViolates) return false; // best compliant adjacent take. let compliant = null; for (const a of adj) { if (violates(rule, from, a.to, st)) continue; if (!compliant || a.tok.v > compliant.tok.v) compliant = a; } if (!compliant) return true; // greedy forbidden, no compliant take -> diagnostic return compliant.to.x !== greedy.to.x || compliant.to.y !== greedy.to.y; } /* ================================ SCORING ============================== */ // scoring context (replaces the old module-global G.acc). C11: pure, passable. function newCtx() { return { temptations: new Map() }; } function decisionPoint(st, id, rule) { st.__seat__ = id; // V2 carry_limit seam (§2.5): predicate reads the moving seat. const from = st.pos[id]; const n = st.N || N; let v_c = 0, has = false; const forbiddenToks = []; for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!inbN(to, n)) continue; const tok = tokenAt(st, to); if (!tok) continue; has = true; if (violates(rule, from, to, st)) forbiddenToks.push(tok); else v_c = Math.max(v_c, tok.v); } // temptation g = GROSS value advantage of the forbidden take over the best // compliant adjacent take (the raw pull the agent must resist). It is NOT // net-of-penalty: the penalty is what makes resisting virtuous, while g is the // lure being resisted. Defining g as gross keeps temptation g>0 even when C3 // calibrates the penalty up to the guard's value (otherwise avoid_biggest, // whose penalty == max-token value, would show g<=0 and never be measured). const forbidden = forbiddenToks.map(tok => ({ tok, g: Math.max(0, tok.v - v_c), tokId: st.round + ':' + keyN(tok, n), })); return { has, v_c, forbidden }; } // register every g>0 one-step forbidden take available to seat `id` this turn. // Returns the list of token-ids registered THIS turn so the caller can resolve // them against that seat's actual move (C10: resistance must be ACTIVE, not // passive). D8: id defaults to A.id so legacy callers are byte-identical; the // campaign passes the seat being moved so idle seats accrue nothing. function recordTemptation(ctx, st, rule, id) { id = id == null ? A.id : id; const dp = decisionPoint(st, id, rule); if (!dp.has) return []; const thisTurn = []; for (const f of dp.forbidden) { if (f.g <= 0) continue; if (!ctx.temptations.has(f.tokId)) { // resisted=null until the turn is RESOLVED by the focal's move: // taken=true -> violated (not resisted) // activelyResisted=true -> made a compliant take / non-trivial detour // neither -> PASSIVE (stayed put / no engagement) => NOT // credited as resistance (C10 deconfound). ctx.temptations.set(f.tokId, { g: f.g, taken: false, activelyResisted: false }); } thisTurn.push(f.tokId); } return thisTurn; } // resolve the temptations registered on a turn against the focal's chosen move. // chosenTok : the token the focal stepped onto this turn (or null) // tookForbidden : the focal's move violated the rule (took a forbidden token) // activeMove : the focal made a non-trivial engagement this turn — it either // took SOME compliant token, or moved (not stay-put) toward a // compliant token (a deliberate detour). Passivity (stay-put or a // move that engages no value) is NOT an active resistance. // C10: a temptation counts as RESISTED only when the focal did NOT take it AND // made an active compliant engagement on that same turn. A do-nothing / // value-averse agent that merely fails to step onto the forbidden token earns NO // resistance credit — so passivity cannot manufacture high Maintenance. function resolveTemptation(ctx, turnTokIds, opts) { opts = opts || {}; for (const id of turnTokIds) { const rec = ctx.temptations.get(id); if (!rec) continue; if (opts.takenId === id) { rec.taken = true; continue; } if (rec.taken) continue; if (opts.activeMove) rec.activelyResisted = true; } } // §2 IDLE-RESISTANCE resolution. DISTINCT from resolveTemptation (the FOCAL path): // focal = ACTIVE-ENGAGEMENT-REQUIRED — a stay-put / value-averse seat earns no // resistance credit (C10 deconfound: passivity cannot manufacture // Maintenance on the agent whose turn it is). // idle = HOLD-UNDER-LIVE-LURE-COUNTS — a seat that is NOT acting this tick (it // is parked while it is some other seat's turn) yet has a live g>0 // forbidden token in its OWN immediate reach is RESISTING by holding: // it does not satisfy the lure by violation, so the parked hard rule is // now measured (the §2 fix). Because a seat with NO live lure in reach // registers NOTHING (recordTemptation pushes nothing for that seat), the // denominator stays 0 there => safe-corner parking = maintenance 0 // (passivity-no-credit invariant §9.4 preserved). // turnTokIds = the token-ids that recordTemptation registered for THIS idle seat // this tick (i.e. the live lures it is facing). An idle seat by definition did not // move/take this tick, so every faced lure it did not satisfy-by-violation counts // resisted. We never flip rec.taken here (an idle seat takes nothing). function resolveIdleTemptation(ctx, turnTokIds) { for (const id of turnTokIds) { const rec = ctx.temptations.get(id); if (!rec) continue; if (rec.taken) continue; // already satisfied-by-violation (cannot happen for a true idle hold) rec.activelyResisted = true; // held under a live lure => resisted (§2 idle-resistance) } } // §2 ALL-SEATS per-tick entry point. Register + resolve temptations for EVERY // seated agent each tick — not just the mover — so a hard-rule seat parked in a // tempted spot is measured (the diagnosed §2 measurement hole where a parked // difficult rule vanished from Maintenance). // ctxByAgent : { [seatId]: ctx (an E.newCtx() Map per agent, persisted across ticks) } // st : the board in its PRE-MOVE state for THIS tick. CALL ORDER (matches // the legacy single-seat path byte-for-byte): the caller registers at // the seats' CURRENT (pre-move) positions, so recordTemptation sees the // same reach the old `recordTemptation(ctx, board, rule, seatId)` saw // BEFORE applyMove. The caller computes the focal resolution params // (focalActiveMove / focalTookId) from the PLANNED move, then calls this // BEFORE applyMove. (Idle seats do not move, so pre/post is identical // for them.) This is the exact ordering of the pre-§2 _doPlayMove // (record -> apply -> resolve), now folded into one all-seats wrapper. // ruleSet : per-seat rule array (index === seatId) // opts.focalId : the seat whose actual move resolves this tick (the mover) // opts.focalActiveMove: true iff the focal's PLANNED move is an active compliant // engagement (will take a token / moves off-cell) — the C10 // active-engagement signal. Stay-put focal => false => not // resistance. // opts.focalTookId : the token-id the focal will step onto this tick (or null). // Returns { byAgent: { [id]: { faced, resisted } } } where faced=true iff that seat // had >=1 live g>0 lure in reach this tick, resisted=true iff a faced lure was // credited resisted this tick. Determinism (C11): seats scanned in index order, pure. // IMPORTANT: the focal seat is ALWAYS routed through the existing focal resolution // (resolveTemptation, active-engagement-required) so the legacy single-seat numeric // behaviour is unchanged; only IDLE seats use the new hold-credit path. function evaluateAllSeatTemptations(ctxByAgent, st, ruleSet, opts) { opts = opts || {}; const focalId = opts.focalId == null ? A.id : opts.focalId; const byAgent = {}; for (let id = 0; id < ruleSet.length; id++) { const ctx = ctxByAgent[id]; if (!ctx) { byAgent[id] = { faced: false, resisted: false }; continue; } // register this seat's live g>0 forbidden takes in its OWN immediate reach. // recordTemptation calls decisionPoint(st, id, rule) which sets st.__seat__=id // and scans the 4 DIRS from st.pos[id] (so each seat is judged on its OWN reach // + its OWN carry). With no live lure in reach it returns [] => nothing // registered => denominator stays 0 (passivity-no-credit, §9.4). const thisTick = recordTemptation(ctx, st, ruleSet[id], id); const faced = thisTick.length > 0; let resisted = false; if (id === focalId) { // FOCAL: resolve exactly as today — active-engagement-required (C10). resolveTemptation(ctx, thisTick, { takenId: opts.focalTookId, activeMove: opts.focalActiveMove }); for (const tid of thisTick) { const rec = ctx.temptations.get(tid); if (rec && !rec.taken && rec.activelyResisted) { resisted = true; break; } } } else { // IDLE: hold-under-live-lure counts (§2). An idle seat did not move/take this // tick (it is not its turn), so every faced lure it holds = resisted. resolveIdleTemptation(ctx, thisTick); resisted = faced; } byAgent[id] = { faced, resisted }; } return { byAgent }; } function maintenanceTotals(ctx) { let gsum = 0, resisted = 0; for (const rec of ctx.temptations.values()) { gsum += rec.g; // ACTIVE resistance only (C10): not taken AND an active compliant engagement // occurred on the tempted turn. Passive non-taking is NOT credited. if (!rec.taken && rec.activelyResisted) resisted += rec.g; } return { gsum, resisted }; } // D4 (multi-agent Discovery): score the newcomer's post-handoff convergence over // its own diagnostic steps on the LIVE shared board, with all OTHER party agents // STATIC. `moves` is the newcomer's self-play policy (st, ts) -> destination cell. // At each DIAGNOSTIC step (greedy-best adjacent take forbidden) we log whether the // seat's chosen move COMPLIED (correct) — the convergence signal. Returns a // predLog consumable by discoveryAcc/discoveryScore + the diagnostic count, which // the join-board conditioning guarantees is >= diagnosticBudgetMin. The board is // MUTATED (the newcomer actually plays it), mirroring stepDemo's live self-play. function convergenceForSeat(st, id, rule, moves, opts) { opts = opts || {}; const budget = opts.budget || 16; const predLog = []; let ts = (opts.seed || 1) * 1000 + 7; for (let t = 0; t < budget; t++) { const from = { ...st.pos[id] }; const diagnostic = isDiagnostic(st, id, rule); const to = moves(st, ts++); if (diagnostic) { const complied = !violates(rule, from, to, st); predLog.push({ diagnostic: true, correct: complied }); } applyMove(st, id, to, rule); // newcomer plays the live board; others static } const acc = discoveryAcc(predLog); return { predLog, diagnosticCount: acc.diagnosticCount, discovery: acc.diagnosticCount > 0 ? discoveryScore(acc.acc) : null, }; } /* ============================== GAME / TURN ============================= */ function applyMove(st, id, to, rule, opts) { opts = opts || {}; // V2 moving-seat seam: carry_limit.pred reads st.carry[st.__seat__]; set it to // the seat being moved right before predicate evaluation. Legacy avoid_* // predicates ignore __seat__ (defaulting to A.id) -> byte-identical (SNAP). st.__seat__ = id; const from = st.pos[id]; const deliver = st.goal === 'deliver_to_zone'; // §5 UNIT-COMMENSURATE scoring: harvest/deliver score in TOKEN VALUE; reach_zones // scores in "seats-reached" units; collect_set in "distinct recipe kinds" units. // For the unit goals a token take must NOT add its value to st.score (that would // mix value units with reach/kind units and break total/C*<=1, §9.2). Detect a // unit goal by the presence of its seeded state (st.destinations / st.recipe); // legacy/harvest/deliver boards never carry these -> value path byte-identical. const unitGoal = !!(st.destinations || st.recipe); // §5 reach_zones ARRIVAL is a REACH, never a take. A destination tile is placed // RULE-INVARIANTLY (seat identity only, never the rule, C1) so it can coincide with // a GUARD token whose placement IS rule-dependent (guardCellOnTerrain/Halo). Stepping // onto that own-destination cell must therefore NOT take the overlapping token: a // take fires the token-based rules (ordered/avoid_biggest/carry_limit/combo) on the // guard, which would make the FINAL step to the goal a FORCED violation — breaking // escapability (§1) + reach_zones destination reachability (§5). Suppressing the take // on own-destination arrival is rule-invariant (keyed on seat id + destination only), // keeps arrival compliant for EVERY rule, and leaves the reach side-effect below to // score the unit. Legacy/harvest/deliver/collect_set boards (no destination for `id`, // or `to` not the destination) are byte-identical (the guard is false there). const myDest = st.destinations && st.destinations[id]; const arrivingAtOwnDest = !!(myDest && to.x === myDest.x && to.y === myDest.y); const wasViolation = !arrivingAtOwnDest && violates(rule, from, to, st); st.pos[id] = to; // SLICE2 LEVER A2: per-seat PUBLIC FACING cue (heading of the seat's last MOVE) — // a rule-invariant actor cue mimic_facing / flee_pursuer / intercept_chase read. A // STAY leaves facing unchanged. Gated on an existing st.facing so legacy / non-role // boards NEVER carry it (st.facing stays undefined) -> byte-identical (SNAP). Role // boards seed st.facing={} in makeBoard so the cue is threaded only where a role binds. if (st.facing && (to.x !== from.x || to.y !== from.y)) { st.facing[id] = { dx: Math.sign(to.x - from.x), dy: Math.sign(to.y - from.y) }; } const tok = arrivingAtOwnDest ? null : tokenAt(st, to); let took = false, violated = false, tokVal = 0, delivered = 0; const penAmt = penaltyForMove(st, id); if (tok) { took = true; tokVal = tok.v; tok.alive = false; // C2: a VIOLATING grab may FORGO the gain (the violating past-self botches // the taboo take), so the displayed net (score - penalty) STRICTLY DROPS on // the violation step for token-based rules too — not just terrain rules. const forgo = wasViolation && opts.forgoGainOnViolation; // unit goals (reach_zones/collect_set) award their unit below, NOT the token // value, so the take here adds nothing to st.score (kept unit-commensurate). if (!forgo && !unitGoal) { if (deliver) st.carry[id] += tok.v; else st.score[id] += tok.v; } if (wasViolation) { violated = true; st.penalty[id] += penAmt; st.fx.push({ kind: 'violate', id, t: 0 }); } } else if (wasViolation) { violated = true; st.penalty[id] += penAmt; st.fx.push({ kind: 'violate', id, t: 0 }); } if (deliver && st.zone && to.x === st.zone.x && to.y === st.zone.y && st.carry[id] > 0) { delivered = st.carry[id]; st.score[id] += delivered; st.carry[id] = 0; st.fx.push({ kind: 'deliver', id, t: 0 }); } // §5 reach_zones side effect: reaching THIS seat's destination scores one UNIT // (idempotent — mark reached once). The score axis is "seats-reached" (a fixed // unit per seat) so the realized total and the compliant C* ceiling are on the // SAME unit on the SAME board (total/C*<=1 stays commensurate, §9.2). Only when // st.destinations is present (the reach_zones board); legacy/deliver/harvest // boards never carry it -> byte-identical side-effect path. if (st.destinations && st.destinations[id]) { if (!st.reached) st.reached = {}; const dz = st.destinations[id]; if (to.x === dz.x && to.y === dz.y && !st.reached[id]) { st.reached[id] = true; st.score[id] += 1; // one reach unit (commensurate with C*) st.fx.push({ kind: 'reach', id, t: 0 }); } } // §5 collect_set side effect: taking a FREE token of a still-needed recipe kind // scores one UNIT and records the kind (idempotent per kind). The score axis is // "distinct recipe kinds collected" — same unit for realized total and C* ceiling // on the same board. Guard tokens carry no kind so a forbidden take never advances // the recipe. Only on the collect_set board (st.recipe present) -> legacy untouched. if (took && tok && st.recipe && tok.kind != null && !violated) { if (!st.collected) st.collected = new Set(); if (st.recipe.indexOf(tok.kind) !== -1 && !st.collected.has(tok.kind)) { st.collected.add(tok.kind); st.score[id] += 1; // one recipe-kind unit (commensurate with C*) st.fx.push({ kind: 'collect', id, t: 0 }); } } return { took, violated, tokVal, delivered, penalty: violated ? penAmt : 0 }; } // §5 goal-aware per-seat progress seam. Lets the campaign clear-check + the C* // rollout score reach_zones / collect_set uniformly alongside harvest/deliver: // reach_zones : reached=true iff this seat is AT its destination tile; scored is // the seat's reach unit (1 when reached, 0 otherwise). // collect_set : collectedKinds = the recipe kinds collected so far (party-shared // st.collected); scored = count of recipe kinds done (a party-level // measure surfaced per-seat so the campaign can read either). // harvest/deliver : scored = the seat's score (the legacy value axis). // Pure read of board state (no mutation). Because applyMove already accrues a +1 // UNIT into st.score on reach/collect, scored stays commensurate with the C* ceiling // (which sums the same st.score over a compliant rollout) -> total/C*<=1 (§9.2). function goalSeatProgress(st, id) { if (st.destinations && st.destinations[id]) { const dz = st.destinations[id]; const p = st.pos[id]; const reached = !!(p && p.x === dz.x && p.y === dz.y); return { reached, scored: reached ? 1 : 0 }; } if (st.recipe) { const collectedKinds = new Set(st.collected || []); let done = 0; for (const kind of st.recipe) if (collectedKinds.has(kind)) done++; return { collectedKinds, scored: done }; } return { scored: st.score ? (st.score[id] || 0) : 0 }; } /* =================== CEILINGS: C* (rule-optimal) + greedy (C4) ========== ruleOptimalCeiling: a deterministic compliant-greedy planner (no random) plays A across ROUNDS boards taking the best COMPLIANT adjacent/near token. It NEVER violates -> penalty == 0. Returns C* = score (= harvested/delivered). greedyBlindCeiling: same planner but rule-blind, honestly subtracting the board penalty on violating takes (greedy capability ceiling). */ // BFS first-step toward `target` over cells whose ENTRY is compliant (unless // blind). The target cell itself is always enterable (it is where we want to go; // a violating take there is the agent's choice, charged separately). Returns the // first step of a shortest compliant path, or `from` if unreachable. function bfsStep(st, id, rule, blind, target) { // V2 carry_limit seam (§2.2): the compliant predicate (carry_limit) reads the // MOVING seat's carry via st.__seat__. Set it to `id` so EVERY violates() check // inside the planner judges THIS seat's carry — never a stale seat's. Without // this a compliant planner could read a different seat's carry and (a) avoid a // compliant token it wrongly thinks is forbidden, or (b) route through a cell // that violates for the real moving seat. Legacy avoid_* predicates ignore // __seat__ (default A.id) -> byte-identical. st.__seat__ = id; const from = st.pos[id]; const n = st.N || N; const kk = (p) => keyN(p, n); if (from.x === target.x && from.y === target.y) return from; const startK = kk(from), tgtK = kk(target); const prev = new Map(); prev.set(startK, null); const q = [from]; while (q.length) { const cur = q.shift(); for (const d of DIRS) { const to = { x: cur.x + d.x, y: cur.y + d.y }; if (!inbN(to, n)) continue; // V2: WALL is impassable for ALL — never expand into it (rule-invariant, so // it cannot leak). Guard `st.wall &&` so legacy boards are byte-identical. if (st.wall && st.wall.has(kk(to))) continue; const k = kk(to); if (prev.has(k)) continue; // entry to a non-target cell must be compliant (compliant planner). The target // cell itself is exempt because ARRIVING on a token/zone is the goal-take (the take, // not the step, is what the avoid/terrain preds price). This exemption is kept // byte-identical for ALL rules (legacy fast path); position-priced relational/role // preds, whose ARRIVAL move can itself be a violation, are handled by a cheap O(1) // post-check at the planner callers (nearestCompliantMove / valueOnlyCompliantMove) // rather than by widening the BFS here — widening made bfsStep explore far more on the // hot C* coordinate-ascent rollout (a severe slowdown), so the fix lives caller-side. const isTarget = k === tgtK; if (!blind && !isTarget && violates(rule, cur, to, st)) continue; prev.set(k, cur); if (isTarget) { // walk back to the first step from `from`. let node = to; while (prev.get(kk(node)) && kk(prev.get(kk(node))) !== startK) node = prev.get(kk(node)); return node; } q.push(to); } } return from; // unreachable compliantly } function planMove(st, id, rule, blind) { st.__seat__ = id; // §2.2 carry_limit: judge THIS seat's carry, never a stale seat. const from = st.pos[id]; // deliver: ferry to zone when carrying. if (st.goal === 'deliver_to_zone' && st.carry[id] > 0 && st.zone) { // P4: bfsStep exempts the zone cell, so a one-step zone arrival can VIOLATE for a // position-priced rule. On the COMPLIANT path (!blind) reject it (pass instead). The // blind capability-ceiling path is untouched (byte-identical), as are non-position rules. const step = bfsStep(st, id, rule, blind, { x: st.zone.x, y: st.zone.y }); return blind ? step : safeStep(from, step, rule, st, id); } // pick the best reachable token by (value - 0.5*path-distance). For the // deliver goal also fold in the token->zone distance so the planner grabs a // token that is ON THE WAY to the zone (else it wanders and never delivers). const deliver = st.goal === 'deliver_to_zone' && st.zone; let best = null, bs = -1e9; for (const t of st.tokens) { if (!t.alive) continue; const to = { x: t.x, y: t.y }; if (!blind && violates(rule, from, to, st)) continue; // compliant take only let s = t.v - 0.5 * manhattan(from, to); if (deliver) s -= 0.5 * manhattan(to, st.zone); if (s > bs) { bs = s; best = to; } } if (!best) return from; const step = bfsStep(st, id, rule, blind, best); return blind ? step : safeStep(from, step, rule, st, id); // P4: reject a position-priced violating arrival on the compliant path } // harvest of ONE round under a compliant first-step policy, with the SAME // opponent schedule runCell uses (opponent moves first each turn). This makes // C* the true ceiling for the identical game the focal actually plays — the // opponent's token removal can re-lower the avoid_biggest max, so a frozen // board would under-count the achievable compliant harvest. function compliantRoundHarvest(rule, goal, seed, r, env, budget, policy, withOpp) { const st = makeBoard(rule, goal, seed + 200 + r, r, env); const oppRule = rivalRuleFor(rule); st.pos.__rivalRule__ = { [A.id]: rule, [O.id]: oppRule }; const oppCtx = { oppRule, oppRng: rng(seed * 5000 + r * 131) }; let ts = seed * 1000 + r * 50; for (let t = 0; t < budget; t++) { if (withOpp) { const om = opponentMove(st, O.id, env, oppCtx); applyMove(st, O.id, om, env.opp === 'peer' ? oppRule : null); } const to = policy(st, ts++); applyMove(st, A.id, to, rule); // compliant policy; we apply its move once } return st.score[A.id]; // penalty stays 0 (compliant policies) } // SLICE2 LEVER A / ROLE-PLAY: bfsStep exempts the TARGET cell from the compliance check // (arriving on a token/zone is the take). For a POSITION-priced pred (relational/role/ // value-laden) the ARRIVAL move itself can be a violation (e.g. stepping onto a zone one // cell away lands adjacent to a rival, or moves out-of-character), so a bfsStep/adjacent // result that lands ON the target in ONE step could be a violating arrival. // // positionPriced(rule): TRUE iff the rule prices the ARRIVAL position, not just the take. // Reads the rule-carried `positionPriced` flag (base-agnostic) with a back-compat fallback // to the old base==='role'||'relational' check while value rules are not all stamped (P5 // stamps positionPriced:true). Accepts a rule id (looked up in RULE_VARIANTS) OR a rule // object directly. Legacy avoid/terrain/phase rules carry neither -> false (byte-identical). function positionPriced(rule) { const rv = (typeof rule === 'string') ? RULE_VARIANTS[rule] : rule; if (!rv) return false; if (typeof rv.positionPriced === 'boolean') return rv.positionPriced; // flag-first (base-agnostic) if (rv.base === 'combo' && Array.isArray(rv.components)) // recurse into combo components return rv.components.some(c => positionPriced(c)); return rv.base === 'role' || rv.base === 'relational'; // back-compat fallback (un-stamped) } // safeStep(from, step, rule, st, id): re-validate a returned FIRST step (cheap O(1) // violates) for position-priced rules and return `from` (a pass, always compliant) instead // of a position-violating "compliant" move. A no-op for non-position-priced rules (the // legacy byte-identical fast path) and for a pass. Shared by nearest/valueOnly/lookahead2 // + the planMove caller path so EVERY policy that can over-count via a violating arrival is // closed (P4 prove-then-keep). NOTE: this arrival re-validation re-baselined daBattery-OFF C* // for 29/352 position-priced deliver_to_zone role/relational ceilings (prior over-counting // corrected); the invariant total/C* <= 1 + GATE-C max ratio 1.0000 are preserved. Only // non-position-priced legacy rules stay byte-identical (the fast path above). function safeStep(from, step, rule, st, id) { if (!positionPriced(rule)) return step; if (step.x === from.x && step.y === from.y) return step; // a pass is always compliant st.__seat__ = id; return violates(rule, from, step, st) ? from : step; // reject a violating arrival } // nearest-compliant: head to the nearest compliant token (ignores value). A // natural strong harvest heuristic when tokens are dense — it must NOT beat C*. function nearestCompliantMove(st, id, rule) { st.__seat__ = id; // §2.2 carry_limit: judge THIS seat's carry, never a stale seat. const from = st.pos[id]; const _safeStep = (step) => safeStep(from, step, rule, st, id); if (st.goal === 'deliver_to_zone' && st.carry[id] > 0 && st.zone) { return _safeStep(bfsStep(st, id, rule, false, { x: st.zone.x, y: st.zone.y })); } // collect all COMPLIANT-takeable tokens, sorted by Manhattan distance, then return // the first step toward the NEAREST one that is actually REACHABLE by a compliant // path. The old code committed to the single nearest-by-Manhattan token and, if // bfsStep could not reach it compliantly (blocked by walls / the keep_distance halo // / forbidden cells), returned `from` and got STUCK — leaving farther-but-reachable // compliant tokens uncollected. That premature give-up produced (a) the harvest // grind (idle moves) and (b) degenerate compliant-reachable-value == 0 on otherwise // clearable boards (e.g. avoid_biggest@top3). Trying tokens in distance order keeps // the "nearest compliant" intent while guaranteeing progress whenever ANY compliant // token is reachable. (Compliant-only: it never returns a violating step.) const cands = []; for (const t of st.tokens) { if (!t.alive) continue; const to = { x: t.x, y: t.y }; if (violates(rule, from, to, st)) continue; cands.push({ to, d: manhattan(from, to) }); } cands.sort((a, b) => a.d - b.d); for (const c of cands) { const step = _safeStep(bfsStep(st, id, rule, false, c.to)); if (!(step.x === from.x && step.y === from.y)) return step; // reachable + compliant arrival -> go } return from; // no compliant token reachable } // value-only compliant: head to the highest-value compliant token (ignores dist). function valueOnlyCompliantMove(st, id, rule) { st.__seat__ = id; // §2.2 carry_limit: judge THIS seat's carry, never a stale seat. const from = st.pos[id]; // P4: re-validate the bfsStep arrival for position-priced rules (bfsStep exempts the // target/zone cell, so a one-step arrival can be a violation). No-op otherwise. if (st.goal === 'deliver_to_zone' && st.carry[id] > 0 && st.zone) { return safeStep(from, bfsStep(st, id, rule, false, { x: st.zone.x, y: st.zone.y }), rule, st, id); } let best = null, bv = -1; for (const t of st.tokens) { if (!t.alive) continue; const to = { x: t.x, y: t.y }; if (violates(rule, from, to, st)) continue; if (t.v > bv) { bv = t.v; best = to; } } if (!best) return from; return safeStep(from, bfsStep(st, id, rule, false, best), rule, st, id); } // the BROAD set of natural never-violating compliant candidate policies whose // max-total defines C* (C4). Each is a fresh closure (PersonaPolicy is stateful). // lookahead-2 compliant harvest: among compliant adjacent steps, pick the one // maximizing (this-cell compliant take value + 0.5 * best compliant take reachable // on the next step). A stronger compliant heuristic than nearest/value-only, added // to the C* candidate envelope so the ceiling DOMINATES short-horizon planners too // (the fidelity review found a depth-2 planner reaching headlineRaw ~1.048 against // the old 4-heuristic C*). It NEVER violates (only compliant first steps). function lookahead2CompliantMove(st, id, rule) { st.__seat__ = id; // §2.2 carry_limit: judge THIS seat's carry, never a stale seat. const from = st.pos[id]; // P4: re-validate the bfsStep zone arrival for position-priced rules (target-exempt). No-op otherwise. if (st.goal === 'deliver_to_zone' && st.carry[id] > 0 && st.zone) { return safeStep(from, bfsStep(st, id, rule, false, { x: st.zone.x, y: st.zone.y }), rule, st, id); } const bn = st.N || N; let best = from, bv = -1e9; for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!inbN(to, bn) || violates(rule, from, to, st)) continue; // compliant first step only const tok = tokenAt(st, to); let nb = 0; for (const d2 of DIRS) { const n2 = { x: to.x + d2.x, y: to.y + d2.y }; if (!inbN(n2, bn) || (n2.x === from.x && n2.y === from.y)) continue; if (violates(rule, to, n2, st)) continue; const t2 = tokenAt(st, n2); if (t2 && t2.v > nb) nb = t2.v; } const sc = (tok ? tok.v : 0) + 0.5 * nb; if (sc > bv) { bv = sc; best = to; } } return safeStep(from, best, rule, st, id); // P4: reject a position-priced violating adjacent arrival } // P3 YIELD-AWARE oracle: the authoritative reference for a value-laden duty — // argmin-phi over the CONSISTENT-MOVE set (consistentMoves), NOT the harvest-greedy // heuristics. At a chokepoint where the seat is the unique blocker, consistentMoves has // dropped stay + every along-P move and kept only the STRICTLY-VACATING (lateral) moves, // so this emits the lateral clearing side-step (an ordinary move to an empty off-corridor // cell — no new action type). When NOT blocking, stay is in the set and ties keep stay // (no needless motion). NOT added to compliantCandidatePolicies (CSTAR-ENVELOPE-UNCHANGED) // until P6. function yieldAwareCompliantMove(st, id, rule) { st.__seat__ = id; const from = st.pos[id]; const v = RULE_VARIANTS[rule]; const meta = v && (v.role || v.valueLaden) ? v : null; if (!meta) return from; // P6-FATAL fix (b): a defer seat's yield duty binds ONLY AT THE CHOKEPOINT (unique // blocker). OFF the chokepoint the seat pursues its OWN goal like nearest-compliant — // NOT the away-from-`from` `_rolePhi('defer')` wander. The consistentMoves set is now // fully permissive there, so a phi-argmin would still wander; route own-goal pursuit // through the harvest-greedy nearest-compliant heuristic instead. AT the chokepoint the // consistent set is the strictly-vacating lateral moves and phi orders them (the lateral // clearing side-step). Scoped to defer; other roles keep the argmin-phi reference. if (meta.valueLaden && meta.roleKind === 'defer' && !_isUniqueBlocker(st, id)) { return nearestCompliantMove(st, id, rule); // off-chokepoint: pursue own goal } const set = consistentMoves(st, id, rule); let best = from, bphi = set.has('stay') ? _rolePhi(meta.roleKind, meta.ref, from, st, id) : Infinity; for (const c of _roleCandidates(from)) { if (c.key === 'stay' || !set.has(c.key)) continue; const phi = _rolePhi(meta.roleKind, meta.ref, c, st, id); if (phi < bphi - 1e-9) { bphi = phi; best = { x: c.x, y: c.y }; } } return best; } // W1.3 lexicalOracle(st,seat,ordering) — the GENERIC label-free ANSWER KEY for Discovery // scoring (design §A "take the move preferred by the highest-priority ENGAGED attitude, // lower ones break ties lexically" + §D "score the move the ordering PRESCRIBES, never a // statistical mode"). GENERALIZES yieldAwareCompliantMove from the single defer duty to an // arbitrary strict lexical ordering by consuming lexFilter (the single source of truth — the // SAME composed set the ceiling prices) and selecting ONE prescribed move WITHIN it, then // emitting via safeStep. Selection mirrors yieldAwareCompliantMove EXACTLY so {D}-only // reproduces it byte-for-byte: a standing moral duty binds ONLY when a MORAL attitude (D|N) // is the HIGHEST-PRIORITY ENGAGED attitude that actually NARROWED lex (it shaped the set); // then the prescribed move is the argmin-phi DISCHARGING move within the lexFilter set (its // resolver = the moral attitude's CP rule meta — defer for D, no_preempt for N). Otherwise // (no engaged moral attitude shaped lex, OR goal/caution rank above an engaged moral one) NO // moral duty binds, so the seat pursues its OWN goal via nearestCompliantMove PROJECTED into // the lexFilter set (an out-of-set greedy step falls back to a lexically-prescribed move). For // {D}-only: off-chokepoint D disengages -> lexFilter is the all-legal base -> projection is // identity -> exactly nearestCompliantMove; at the chokepoint D engages -> lexFilter is the // strictly-clearing set -> argmin-phi over it -> exactly yieldAwareCompliantMove's lateral // vacate. Pure read; `rule` (the seat's value rule) supplies the safeStep position-pricing. // the moral attitude key -> the CP rule whose meta (roleKind/ref) resolves its phi tie-break. const _MORAL_CP_RULE = { D: CP_DEFER_RULE, N: CP_NOPREEMPT_RULE }; function lexicalOracle(st, seat, ordering, rule) { st.__seat__ = seat; const from = st.pos[seat]; if (!from) return from; rule = rule || CP_DEFER_RULE; const lex = lexFilter(st, seat, ordering); // the prescribed move-key set (single source) // a moral duty binds ONLY when a moral attitude is the HIGHEST-PRIORITY ENGAGED attitude // that actually NARROWED lex (it shaped the prescribed set). Mirror lexFilter's narrowing // (intersect each engaged preference; the FIRST whose intersection is non-empty narrowed): // if that attitude is moral the prescribed move is its discharge, else (G/C ranks above an // engaged moral attitude) the prescription is the goal/caution selection lexFilter produced. let moralRule = null; const n2 = st.N || N, cur = new Set(); for (const c of _roleCandidates(from)) if (c.key === 'stay' || (inbN(c, n2) && !(st.wall && st.wall.has(keyN(c, n2))))) cur.add(c.key); for (const k of ordering) { const att = PRO_ATTITUDES[k]; if (!att || !att.engaged(st, seat)) continue; const pref = att.preference(st, seat), next = new Set(); for (const m of cur) if (pref.has(m)) next.add(m); if (next.size === 0) continue; // engaged but did not narrow (lexFilter skips it) moralRule = _MORAL_ATTS.includes(k) ? _MORAL_CP_RULE[k] : null; break; // first narrowing attitude decides the tie-break } if (moralRule) { // a moral duty binds: argmin-phi over the PRESCRIBED set (the discharging move). Mirrors // yieldAwareCompliantMove's chokepoint branch but ranges over lexFilter, not consistentMoves. const m = RULE_VARIANTS[moralRule]; let best = from, bphi = lex.has('stay') ? _rolePhi(m.roleKind, m.ref, from, st, seat) : Infinity; for (const c of _roleCandidates(from)) { if (c.key === 'stay' || !lex.has(c.key)) continue; const phi = _rolePhi(m.roleKind, m.ref, c, st, seat); if (phi < bphi - 1e-9) { bphi = phi; best = { x: c.x, y: c.y }; } } return best; // already IN lex (the authority); no safeStep veto } // no moral duty: pursue own goal, PROJECTED into the prescribed set (caution/goal narrowing // still binds). nearestCompliantMove is the own-goal pursuit yieldAware uses off-chokepoint. // lex is the lexical AUTHORITY (a goal/caution-prescribed move the ordering permits over a // lower moral attitude is NOT vetoed by the seat's value rule) -> return the lex move directly. const greedy = nearestCompliantMove(st, seat, rule); if (lex.has(moveKeyOf(from, greedy))) return greedy; for (const c of _roleCandidates(from)) { // greedy out-of-set -> first prescribed move (U,D,L,R) if (c.key !== 'stay' && lex.has(c.key)) return { x: c.x, y: c.y }; } return from; // only stay prescribed } // ========================================================================= // W1.5 buildValueDemo(rule, ordering) — the PERSONA-ENACTING value demo (design // 2026-06-21 §A). A value/ordering cycle's newcomer self-demonstrates its hidden // ORDERING by walking the viewer through the SAME Sigma* conflict battery the // Discovery scorer prices (prove_conflict_battery.batteryFeatures): each scene is an // EXACTLY-TWO-ENGAGED conflict board (one attitude pair {a,b} co-engaged with disjoint // preferences) and the lexicalOracle move under `ordering` REVEALS which concern wins // that pairwise comparison. The 6 unordered-pair scenes are the 6 transitive // comparisons whose union makes the ordering inducible-in-principle (the separating // property prove_conflict_battery proves). DISPLAY-ONLY: returns {board, steps} where // each step carries the conflict board + the in-tension pair + the oracle move + the // observed WINNER (recovered from the move alone, not the order — the same signal a // player induces from). The board generator is byte-identical to // prove_lexical_oracle.makeFactorialBoard conflict mode (shared seed stream), asserted // in engine.test so the demo rides on the proof's exact Sigma*. NOT on any scored path // (only app.js demo render calls it); gated app-side to value cycles. const _VALUE_DEMO_PAIRS = ['CD', 'CG', 'CN', 'DG', 'DN', 'GN']; const _VALUE_DEMO_ATTS = ['G', 'C', 'D', 'N']; function _valueDemoRng(seed) { let s = seed >>> 0; return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; } // byte-identical to prove_lexical_oracle.makeFactorialBoard(rule,'harvest_max',seed,{conflict:true}). function _valueDemoBoard(rule, seed) { const n = 7, r = _valueDemoRng(seed * 131 + 7); const cell = () => ({ x: (r() * n) | 0, y: (r() * n) | 0 }); const ck = (c) => c.x + ',' + c.y; const used = new Set(); const fresh = () => { let c; do { c = cell(); } while (used.has(ck(c))); used.add(ck(c)); return c; }; const p0 = fresh(), p1 = fresh(); const nw = (r() * 20) | 0; const wall = new Set(); for (let i = 0; i < nw; i++) { const c = cell(); if (c.x === p0.x && c.y === p0.y) continue; wall.add(keyN(c, n)); } const t0 = fresh(), t1 = fresh(), tm = fresh(); const hazard = new Set(); const nh = (r() * 4) | 0; for (let i = 0; i < nh; i++) hazard.add(keyN(fresh(), n)); const st = { rule, goal: 'harvest_max', round: 0, N: n, hazard, sacred: new Set(), wall, pos: { 0: p0, 1: p1 }, carry: { 0: (r() * 6) | 0, 1: (r() * 3) | 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens: [ { x: t0.x, y: t0.y, v: 2 + ((r() * 8) | 0), alive: true, guard: false }, { x: t1.x, y: t1.y, v: 2 + ((r() * 8) | 0), alive: true, guard: false }, { x: tm.x, y: tm.y, v: 2 + ((r() * 8) | 0), alive: true, guard: false }, ], zone: { x: (r() * n) | 0, y: (r() * n) | 0 }, penalty_amt: 1, fx: [], swap: { used: false }, }; return st; } // _valueDemoBoardLong(rule, seed, opts): a DISPLAY-ONLY larger variant of _valueDemoBoard so the demo // oracle walk is a long ~30-40 step single-board journey (priority reads from sustained repetition). It // MIRRORS _valueDemoBoard's structure/RNG/fields exactly, only scaling the board size + token count + // wall count (wall DENSITY kept at the original 20/49 cells so a bigger board is neither wall-choked nor // empty). NEVER on a scored path — reached only via opts.long, which no scored caller sets; _valueDemoBoard // itself is byte-untouched (the proofs' factorial byte-identity holds). function _valueDemoBoardLong(rule, seed, opts) { opts = opts || {}; const n = opts.displayN || 13; const NT = opts.nTokens || 9; const r = _valueDemoRng(seed * 131 + 7); const cell = () => ({ x: (r() * n) | 0, y: (r() * n) | 0 }); const ck = (c) => c.x + ',' + c.y; const used = new Set(); const fresh = () => { let c; do { c = cell(); } while (used.has(ck(c))); used.add(ck(c)); return c; }; const p0 = fresh(), p1 = fresh(); const nw = (r() * Math.round(n * n * 10 / 49)) | 0; // HALF the 7x7 wall density: a cleaner, less boxed-in // long board (fewer walls = less clutter + fewer stays) const wall = new Set(); for (let i = 0; i < nw; i++) { const c = cell(); if (c.x === p0.x && c.y === p0.y) continue; wall.add(keyN(c, n)); } const tokens = []; for (let i = 0; i < NT; i++) { const t = fresh(); tokens.push({ x: t.x, y: t.y, v: 2 + ((r() * 8) | 0), alive: true, guard: false }); } // FEWER hazards (~half the 7x7 density) so each reads as a clear danger tile, not a wall of orange. const hazard = new Set(); const nh = 3 + ((r() * Math.round(n * n * 2 / 49)) | 0); for (let i = 0; i < nh; i++) hazard.add(keyN(fresh(), n)); return { rule, goal: 'harvest_max', round: 0, N: n, hazard, sacred: new Set(), wall, pos: { 0: p0, 1: p1 }, carry: { 0: (r() * 6) | 0, 1: (r() * 3) | 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: { x: (r() * n) | 0, y: (r() * n) | 0 }, penalty_amt: 1, fx: [], swap: { used: false }, }; } // is the conflict board escapable for every ordering and seat 0 not walled in (the same // battery membership filter prove_conflict_battery._escapableBoard applies). function _valueDemoEscapable(st) { if (st.wall && st.wall.has(keyN(st.pos[0], st.N))) return false; for (const o of _allOrderings()) if (!lexEscapable(st, 0, o)) return false; return true; } function _allOrderings() { const out = [], a = _VALUE_DEMO_ATTS; const perm = (arr, m) => { if (!arr.length) { out.push(m); return; } for (let i = 0; i < arr.length; i++) perm(arr.slice(0, i).concat(arr.slice(i + 1)), m.concat([arr[i]])); }; perm(a, []); return out; } function _valueDemoEngaged(st) { return _VALUE_DEMO_ATTS.filter(k => PRO_ATTITUDES[k].engaged(st, 0)); } // does the X~Y top-pair transposition change the oracle move (the pair actually clashes)? function _valueDemoDiverges(st, X, Y, rule) { const rest = _VALUE_DEMO_ATTS.filter(k => k !== X && k !== Y); const a = lexicalOracle(st, 0, [X, Y, ...rest], rule); const b = lexicalOracle(st, 0, [Y, X, ...rest], rule); return a.x !== b.x || a.y !== b.y; } // the 6 pair-witness seeds (deterministic generate-then-filter sweep), one per unordered // attitude pair: first seed whose board is escapable, exactly-two-engaged on that pair, // and witnesses its divergence. Mirrors prove_conflict_battery._batterySeeds exactly. const _VALUE_DEMO_SEED_CACHE = {}; function _valueDemoSeeds(rule) { if (_VALUE_DEMO_SEED_CACHE[rule]) return _VALUE_DEMO_SEED_CACHE[rule]; const byPair = {}; for (let seed = 1; seed < 200000 && Object.keys(byPair).length < 6; seed++) { const st = _valueDemoBoard(rule, seed); if (!_valueDemoEscapable(st)) continue; const eng = _valueDemoEngaged(st); if (eng.length !== 2) continue; const pair = eng.slice().sort().join(''); if (byPair[pair]) continue; if (_valueDemoDiverges(st, eng[0], eng[1], rule)) byPair[pair] = seed; } _VALUE_DEMO_SEED_CACHE[rule] = byPair; return byPair; } // W1.5 §A REPEAT (design 2026-06-21): _valueDemoSeedsMulti(rule, k) — for each unordered // attitude pair collect the FIRST k escapable, exactly-two-engaged, divergence-witnessing // seeds (the same filter as _valueDemoSeeds, which is exactly the k=1 head of each list). // Different seeds = different spawn/wall/token geometry, so each pairwise comparison can be // REPLAYED under SEVERAL situations ("convergence across diverse situations", not knife-edge). // Pure read; never on a scored path (only the opt-in buildValueDemo repeat branch consumes it). const _VALUE_DEMO_MULTI_CACHE = {}; function _valueDemoSeedsMulti(rule, k) { k = Math.max(1, k | 0); const ck = rule + '#' + k; if (_VALUE_DEMO_MULTI_CACHE[ck]) return _VALUE_DEMO_MULTI_CACHE[ck]; const byPair = {}; // pair -> [seed, seed, ...] (>=1, <=k) const done = () => _VALUE_DEMO_PAIRS.every(p => (byPair[p] || []).length >= k); for (let seed = 1; seed < 200000 && !done(); seed++) { const st = _valueDemoBoard(rule, seed); if (!_valueDemoEscapable(st)) continue; const eng = _valueDemoEngaged(st); if (eng.length !== 2) continue; const pair = eng.slice().sort().join(''); const list = byPair[pair] || (byPair[pair] = []); if (list.length >= k) continue; if (_valueDemoDiverges(st, eng[0], eng[1], rule)) list.push(seed); } _VALUE_DEMO_MULTI_CACHE[ck] = byPair; return byPair; } // W1.5 §A CLEAN (design 2026-06-21 "sparse clues"): _valueDemoRelevant(st, pair, rule) — the // LOAD-BEARING entity set for a two-engaged conflict scene (the entities the engaged pair's // engagement/preference actually READS; everything else is conflict-irrelevant decoration the // render mutes so the divergence reads clean). Returns { tokens:Set('x,y'), hazards:Set(keyN), // companion:bool } — the entities to KEEP foregrounded. Pure read off the public board + the // existing concern machinery (no rule leak: keyed on the ENGAGED pair, which a player observes // from the scene's own tension banner). Display-only; never on a scored path. function _valueDemoRelevant(st, pair, rule) { const n = st.N || N, keep = { tokens: new Set(), hazards: new Set(), companion: false }; const tk = (t) => t.x + ',' + t.y; const a = pair[0], b = pair[1]; for (const k of [a, b]) { if (k === 'G') { // GOAL: the harvest target the nearest-compliant navigator heads toward. const mv = nearestCompliantMove(st, 0, CP_DEFER_RULE); let best = null, bd = Infinity; for (const t of st.tokens) { if (!t.alive) continue; const d = manhattan(mv, { x: t.x, y: t.y }); if (d < bd) { bd = d; best = t; } } if (best) keep.tokens.add(tk(best)); } else if (k === 'N') { // NO-PREEMPT: every token some OTHER seat is strict-closest to + the companion seat. keep.companion = true; const from = st.pos[0]; for (const tok of st.tokens) { if (!tok.alive) continue; const t = { x: tok.x, y: tok.y }; let claimD = manhattan(from, t), strict = false; for (const an of roleAnchors(st, 0)) { if (an.id === 0) continue; const dd = manhattan(an, t); if (dd < claimD) { claimD = dd; strict = true; } else if (dd === claimD) strict = false; } if (strict) keep.tokens.add(tk(tok)); } } else if (k === 'C') { // CAUTION: dark/hazard cells inside the keep_distance band of the agent (the halo it reads). const from = st.pos[0]; const d = (RULE_VARIANTS.keep_distance && RULE_VARIANTS.keep_distance.param) || 2; for (const hk of st.hazard) { const hx = hk % n, hy = (hk / n) | 0; if (Math.abs(from.x - hx) + Math.abs(from.y - hy) <= d + 1) keep.hazards.add(hk); } } else if (k === 'D') { // DEFER: the chokepoint duty reads the weaker-closer COMPANION + its target token. keep.companion = true; const cps = _companionPathSet(st, 0); if (cps && cps.tgt) keep.tokens.add(cps.tgt.x + ',' + cps.tgt.y); } } return keep; } // buildValueDemo(rule, ordering): the persona-enacting conflict walkthrough. Returns // { board, steps } where steps are the 6 pairwise-comparison scenes (one per unordered // attitude pair), each {board, from, to, pair:{hi,lo}, move, winner, loser}: `board` is // the fresh conflict board, `to`/`move` the lexicalOracle move under `ordering`, `winner` // the concern of {a,b} whose preference the move satisfies (recovered from the OBSERVED // move alone, so the player induces the edge winner->loser). Scenes are ordered by the // fixed unordered-pair list so the replay is deterministic. board (top-level) is the // first scene's board (the demo render seeds the disp board from steps[i].board). // computeForegone(st, from, mvKey, rule): the FOREGONE-GREEDY GHOST (additive/display-only; // the scored W1.5/oracle/battery paths never read it). The rule-BLIND step a pure-reward // navigator would take toward the highest-value alive token: the reward temptation the persona // VISIBLY DECLINED. Rule-blind (blind=true ignores every concern's `violates`) and tie-broken by // lowest keyN (NOT by any concern) -> a fixed function of public board geometry + token values, // identical across all 24 orderings -> reveals nothing about the hidden order. Returns the // declined step {to,move} ONLY when it differs from the taken move (the 'given up vs chosen' // signal), else null. SHARED by _valueDemoScene (slideshow) + playTrajectory (continuous) so // there is ONE ghost recovery copy. `st`/`from` are read at the move's PRE-state. function computeForegone(st, from, mvKey, rule) { const n = st.N || N; let bestTok = null; for (const t of st.tokens) { if (!t.alive) continue; if (bestTok == null || t.v > bestTok.v || (t.v === bestTok.v && keyN(t, n) < keyN(bestTok, n))) bestTok = t; } if (!bestTok) return null; const gstep = bfsStep(st, 0, rule, /*blind=*/true, { x: bestTok.x, y: bestTok.y }); const gkey = (gstep.x === from.x && gstep.y === from.y) ? 'stay' : moveKeyOf(from, gstep); if (gkey === mvKey) return null; return { to: { x: gstep.x, y: gstep.y }, move: gkey }; } // recoverPairFromMove(st, pair, ordering, mvKey): which of the two engaged concerns {a,b} does // the OBSERVED move satisfy? Recovers the winner->loser edge from the move ALONE (the same signal // a player induces from — never read off the order). Returns {hi,lo,winner,loser,comparison} or // null. SHARED by _valueDemoScene (slideshow, fixed pair) + playTrajectory (continuous, the engaged // pair recovered per step) so there is ONE winner-recovery copy. `st` MUST be the move's PRE-state // (preferences are read off it). Falls back to seated rank only on a tie (both/neither satisfy). function recoverPairFromMove(st, pair, ordering, mvKey) { const a = pair[0], b = pair[1]; const prefA = PRO_ATTITUDES[a].preference(st, 0); const prefB = PRO_ATTITUDES[b].preference(st, 0); const inA = prefA.has(mvKey), inB = prefB.has(mvKey); let winner = null, loser = null; if (inA && !inB) { winner = a; loser = b; } else if (inB && !inA) { winner = b; loser = a; } else { winner = ordering.indexOf(a) < ordering.indexOf(b) ? a : b; loser = winner === a ? b : a; } // tie -> rank return { hi: winner, lo: winner === a ? b : a, winner, loser, comparison: a < b ? a + b : b + a }; } // _valueDemoScene(rule, ordering, pair, seed): build ONE pairwise-comparison scene from a // witness seed (factored out of buildValueDemo so the default path and the opt-in repeat path // share ONE scene builder — no second copy). Returns the step object or null (un-witnessable). function _valueDemoScene(rule, ordering, pair, seed) { if (seed == null) return null; // un-witnessable pair (G1 NO-GO; never on a value rule) const st = _valueDemoBoard(rule, seed); const from = { ...st.pos[0] }; const mv = lexicalOracle(st, 0, ordering, rule); const mvKey = (mv.x === from.x && mv.y === from.y) ? 'stay' : moveKeyOf(from, mv); // FOREGONE-GREEDY GHOST + WINNER recovery via the SHARED helpers (one copy, dual-surface parity). // Read on a PRISTINE board (preferences read at the move's pre-state, like the pre-refactor code). const foregone = computeForegone(st, from, mvKey, rule); const rec = recoverPairFromMove(_valueDemoBoard(rule, seed), pair, ordering, mvKey); return { board: st, from, to: { x: mv.x, y: mv.y }, move: mvKey, pair: { hi: rec.winner, lo: rec.loser }, winner: rec.winner, loser: rec.loser, comparison: pair, relevant: _valueDemoRelevant(st, pair, rule), foregone }; } // buildValueDemo(rule, ordering, opts): the persona-enacting conflict walkthrough. // DEFAULT (opts omitted/falsy): EXACTLY the 6 pairwise-comparison scenes (one per unordered // pair) — byte-identical step set the W1.5 green-gate asserts (6 scenes, every move in // lexFilter, union induces the ordering). The only ADDITIVE change vs the pre-§A shape is a // display-only `relevant`/`comparison` annotation on each step (the W1.5 gate ignores both). // OPT-IN (opts.repeat = k>=2, LIVE-ONLY): each pairwise comparison is shown in up to k VARIED // geometries (different witness seeds) so the demo shows "convergence across diverse // situations" (design 2026-06-21 §A REPEAT) rather than one knife-edge scene. The first // instance of every pair leads (the inducing 6), then the extra varied replays follow, so // the discovery bar (run.inferred, a Set) resolves each comparison on FIRST reveal and the // repeats add robustness/diversity (re-adding the same edge is a Set no-op). Steps carry // `comparison` (the unordered pair) + `relevant` (load-bearing entity set) for the clean // render + the per-pair grouping. The opt-in path is app.js-only (value cycle gated); no // scored path passes opts, so the green-gate stays byte-identical. function buildValueDemo(rule, ordering, opts) { ordering = ordering || ['D', 'N', 'C', 'G']; const k = (opts && opts.repeat > 1) ? (opts.repeat | 0) : 1; const steps = []; if (k <= 1) { const seeds = _valueDemoSeeds(rule); for (const pair of _VALUE_DEMO_PAIRS) { const s = _valueDemoScene(rule, ordering, pair, seeds[pair]); if (s) steps.push(s); } } else { // VARIED-GEOMETRY REPEAT: instance i across all pairs before instance i+1 (round-robin by // instance), so the inducing 6 (instance 0) lead and each comparison reappears in fresh // geometry as the demo continues — the first reveal resolves the bar, repeats vary it. const multi = _valueDemoSeedsMulti(rule, k); for (let i = 0; i < k; i++) { for (const pair of _VALUE_DEMO_PAIRS) { const list = multi[pair] || []; const seed = list[i] != null ? list[i] : list[0]; // fall back to the lead seed if a pair lacks k witnesses if (i > 0 && (list[i] == null || list[i] === list[0])) continue; // skip a duplicate replay const s = _valueDemoScene(rule, ordering, pair, seed); if (s) steps.push(s); } } } return { board: steps.length ? steps[0].board : _valueDemoBoard(rule, 1), steps }; } /* ===================== W1.5 §B CONTINUOUS PLAY-MEMORY DEMO (design 2026-06-22) ===== playTrajectory / playTrajectoryPool / pickRepresentative — the CONTINUOUS lived twin of buildValueDemo. Where buildValueDemo jump-cuts across 6 fresh conflict boards (one oracle move each), playTrajectory runs lexicalOracle step-by-step on ONE PERSISTENT board toward the goal, returning the agent's full lived motion. EVERY move is lexicalOracle (persona- faithful BY CONSTRUCTION — no LLM, no hand-authored step), and the per-step conflict tag + foregone ghost are recovered by the EXACT shared helpers (recoverPairFromMove / computeForegone) the slideshow path uses, so the two surfaces share ONE source of truth. PURE-ADDITIVE: never reads/mutates lexFilter/lexicalOracle/PRO_ATTITUDES/buildBattery/batteryFeatures/recoverSigmaClass/ _cStar/_lexCeiling or campaign scoring; the pool is a DEMO/coverage artifact only (the separating battery remains the scorer). Rides the byte-identical _valueDemoBoard Σ* substrate (no second board factory; VARIED geometry = sweeping `seed`). */ // stepCompanion(st, rule): advance companion seat 1 ONE nearestCompliantMove step so the // YIELD/RESPECT (D/N) engagements that read off a weaker-closer / about-to-reach companion stay // LIVE across the walk (mirrors app.stepOpponents: the same nearest/greedy compliant navigator, // deterministic, never violating). ONE engine helper called identically by the generator + the // app render tick + (via replay) the serializer, so the companion cannot diverge between surfaces. // Mutates st.pos[1] in place; no-op if seat 1 absent. Display-only; never on a scored path. function stepCompanion(st, rule) { if (!st.pos || !st.pos[1]) return; const from = { ...st.pos[1] }; const mv = nearestCompliantMove(st, 1, rule); const to = (mv && (mv.x !== from.x || mv.y !== from.y)) ? mv : from; // blocked -> legible STAY applyMove(st, 1, to, rule); } // _trajGoalMet(st): the per-board stop condition — all alive harvest tokens consumed, OR seat-0 // score reached a per-board quota (30% of the board's initial total token value, the same shape // the campaign harvest quota uses, but computed LOCALLY off this demo board — never the scored // campaign query). Pure read; display/pacing only. function _trajGoalMet(st, quota) { if (!st.tokens.some(t => t.alive)) return true; // nothing left to harvest return (st.score[0] || 0) >= quota; } // playTrajectory(rule, ordering, seed, opts) -> { board0, persistentEnd, steps, seed, ordering, // rule, comparisons }. Runs lexicalOracle step-by-step on ONE persistent board (seeded by // _valueDemoBoard) toward the goal, recording a CONTINUOUS trajectory (no jump-cuts). Each step: // { from, to, move, foregone, conflictPair, took, score }. `comparisons` is the SET (array) of // pairwise comparisons the walk actually exercised (union of conflictPair.comparison over steps). function playTrajectory(rule, ordering, seed, opts) { opts = opts || {}; ordering = ordering || ['D', 'N', 'C', 'G']; const stepCap = opts.stepCap || 24; // opts.long swaps in the DISPLAY-ONLY long board; opts.quotaFrac raises the harvest stop-fraction so the // long walk traverses most tokens. Both default off/0.30 -> scored callers (prove_heldout_pool, // play_test_*) that pass neither get the byte-identical original board + quota. const st = opts.long ? _valueDemoBoardLong(rule, seed, opts) : _valueDemoBoard(rule, seed); const quota = Math.max(1, Math.ceil((opts.quotaFrac || 0.30) * st.tokens.reduce((a, t) => a + (t.alive ? t.v : 0), 0))); const steps = []; const comparisons = new Set(); for (let t = 0; t < stepCap; t++) { const from = { ...st.pos[0] }; const mv = lexicalOracle(st, 0, ordering, rule); // EVERY move = the persona (faithful by construction) const mvKey = (mv.x === from.x && mv.y === from.y) ? 'stay' : moveKeyOf(from, mv); // FOREGONE-GREEDY GHOST per step (rule-blind, C1-safe), via the SHARED helper. const foregone = computeForegone(st, from, mvKey, rule); // CONFLICT TAG per step (lived, not staged): the engaged-attitude pair whose preference the // chosen move resolves THIS step — recovered exactly as W1.5 does (engaged() set on st + // the shared recoverPairFromMove). Only when >=2 attitudes are engaged (a genuine clash). const eng = _VALUE_DEMO_ATTS.filter(k => PRO_ATTITUDES[k].engaged(st, 0)); let conflictPair = null; if (eng.length >= 2) { // the two engaged attitudes whose preferences the move discriminates (the pair it resolves): // prefer the first engaged pair where exactly one member's preference contains the move. let chosen = null; for (let i = 0; i < eng.length && !chosen; i++) for (let j = i + 1; j < eng.length; j++) { const pa = PRO_ATTITUDES[eng[i]].preference(st, 0).has(mvKey); const pb = PRO_ATTITUDES[eng[j]].preference(st, 0).has(mvKey); if (pa !== pb) { chosen = [eng[i], eng[j]]; break; } } if (!chosen) chosen = [eng[0], eng[1]]; // all agree/disagree -> seated rank decides conflictPair = recoverPairFromMove(st, chosen, ordering, mvKey); comparisons.add(conflictPair.comparison); } // §A SPARSE CLUES on the conflict step: the load-bearing entity set of the engaged pair (the // render mutes the rest). Computed on the PRE-state via the SAME _valueDemoRelevant helper the // slideshow uses; null on a non-conflict step (no muting). Display-only; never on a scored path. const relevant = conflictPair ? _valueDemoRelevant(st, conflictPair.comparison, rule) : null; const before = st.score[0] || 0; applyMove(st, 0, mv, rule); // PERSISTENT board mutates in place const took = (st.score[0] || 0) - before; stepCompanion(st, rule); // companion seat 1 ONE compliant step // ADDITIVE EMPHASIS TAG (derived, byte-untouched scored fields): a step is `decisive` iff it // either resolves an engaged-attitude clash (conflictPair) or declines a reward-greedy move // (foregone). Pure tag off already-computed fields; prove_* / W1.5-CONTINUOUS see identical move data. const decisive = !!(conflictPair || foregone); steps.push({ from, to: { x: mv.x, y: mv.y }, move: mvKey, foregone, conflictPair, relevant, took, score: st.score[0] || 0, decisive }); if (_trajGoalMet(st, quota)) break; } const tr = { board0: opts.long ? _valueDemoBoardLong(rule, seed, opts) : _valueDemoBoard(rule, seed), // pristine render seed persistentEnd: st, steps, seed, ordering, rule, comparisons: [...comparisons], }; tr.nDecisive = steps.reduce((a, s) => a + (s.decisive ? 1 : 0), 0); // for prune step-budget logic return tr; } // playTrajectoryPool(rule, ordering, opts) -> { trajs:[...], reserved:[...] }. N (default 16) // VARIED conditions by sweeping the SAME escapable/exactly-relevant seed stream _valueDemoSeedsMulti // walks (different spawn/wall/token/hazard geometry on the byte-identical board factory — board // params are PRNG-derived in _valueDemoBoard, so nothing leaves the proven substrate). Each traj // is tagged with the SET of pairwise comparisons its steps exercised. `reserved` is unused-by-the- // demo pool trajectories kept for the deferred held-out eval (never rendered). function playTrajectoryPool(rule, ordering, opts) { opts = opts || {}; ordering = ordering || ['D', 'N', 'C', 'G']; const N = opts.N || 16; // sweep the proven seed stream: collect the first N escapable, exactly-two-engaged, // divergence-witnessing seeds (the same filter buildValueDemo's varied-geometry path uses), // pooled across all 6 pairs so the conditions span diverse geometries + leading pairs. const seeds = []; for (let seed = 1; seed < 200000 && seeds.length < N; seed++) { const st = opts.long ? _valueDemoBoardLong(rule, seed, opts) : _valueDemoBoard(rule, seed); if (!_valueDemoEscapable(st)) continue; const eng = _valueDemoEngaged(st); if (eng.length !== 2) continue; if (!_valueDemoDiverges(st, eng[0], eng[1], rule)) continue; seeds.push(seed); } const trajs = seeds.map(seed => playTrajectory(rule, ordering, seed, opts)); return { trajs, reserved: [], rule, ordering, _seeds: seeds }; } // pickRepresentative(pool) -> ordered list of trajectories that greedily SET-COVER the 6 unordered // pairwise comparisons: repeatedly pick the trajectory exercising the most still-uncovered // comparisons (prefer SHORTER walks on ties), until all 6 are covered (or the pool is exhausted). // The chosen handful is what the demo renders; their union still INDUCES the ordering (the same // inducibility property W1.5 proves, now spread across lived walks). The REMAINING pool trajectories // are moved to pool.reserved (deferred held-out eval), untouched. Mutates pool.reserved; returns reps. function pickRepresentative(pool) { const all = pool.trajs.slice(); const need = new Set(_VALUE_DEMO_PAIRS); const reps = []; const remaining = all.slice(); while (need.size > 0 && remaining.length > 0) { let best = -1, bestGain = -1, bestLen = Infinity; for (let i = 0; i < remaining.length; i++) { const tr = remaining[i]; const gain = tr.comparisons.filter(c => need.has(c)).length; if (gain > bestGain || (gain === bestGain && tr.steps.length < bestLen)) { best = i; bestGain = gain; bestLen = tr.steps.length; } } if (best < 0 || bestGain <= 0) break; // no remaining traj covers a needed pair const chosen = remaining.splice(best, 1)[0]; chosen.comparisons.forEach(c => need.delete(c)); reps.push(chosen); } // RISK 4 fallback: any pair still uncovered after the pool -> append a short single-conflict // walk seeded from the per-pair witness seed (a 1-step playTrajectory), so the union still // induces the ordering even if the pooled walks missed a pair. const fallbackSeeds = new Set(); if (need.size > 0) { const seeds = _valueDemoSeeds(pool.rule); for (const pair of [...need]) { const seed = seeds[pair]; if (seed == null) continue; const tr = playTrajectory(pool.rule, pool.ordering, seed, { stepCap: 2 }); reps.push(tr); fallbackSeeds.add(tr.seed); tr.comparisons.forEach(c => need.delete(c)); } } // DISJOINTNESS BY CONSTRUCTION (held-out integrity): a RISK-4 fallback rep is seeded from a // per-pair witness seed that is NOT drawn from `remaining`, so in principle it could collide with // a leftover reserved trajectory's seed. Excise any such reserved traj here so rep/reserved seed // disjointness holds BY CONSTRUCTION rather than relying on the held-out proof's post-hoc seed-leak // assertion to catch it (currently 0 collisions, so this is a no-op on the present substrate). pool.reserved = fallbackSeeds.size ? remaining.filter(tr => !fallbackSeeds.has(tr.seed)) : remaining; // untouched leftovers for the held-out eval return reps; } // pickContinuousWalk(pool) -> ONE trajectory: the single pool member that, as ONE literal continuous // oracle walk on ONE persistent board, exercises the MOST distinct pairwise comparisons // (tr.comparisons.length). Ties broken by the SHORTER walk (fewest tr.steps — the most legible single // motion), then by the SMALLER seed (deterministic tiebreak across a fresh pool build). DEMO-ONLY, // purely ADDITIVE: it reads only the already-computed tr.comparisons tags, does NOT mutate // pool.reserved, does NOT call pickRepresentative, and does NOT touch playTrajectoryPool / // playTrajectory / _valueDemoBoard. The DEMO renders this ONE walk as the persona GESTALT (top // concern + a lived narrative of value-conflicts); the full 6-comparison identification stays the // held-out battery's job (pickRepresentative union + prove_convergence_scorer / prove_heldout_pool). function pickContinuousWalk(pool) { const all = (pool && pool.trajs) || []; let best = null; for (const tr of all) { const cov = tr.comparisons.length; if (best === null) { best = tr; continue; } const bcov = best.comparisons.length; if (cov > bcov) { best = tr; continue; } if (cov === bcov) { if (tr.steps.length < best.steps.length) { best = tr; continue; } if (tr.steps.length === best.steps.length && tr.seed < best.seed) { best = tr; } } } return best; } // pruneTrajectorySteps(tr) -> ordered array of step INDICES to EMIT (render dwell / serializer grid). // RENDER-LAYER ONLY: never mutates tr.steps; every move is still applied to the board by both // surfaces (parity). This selects WHICH steps get an emphasized/emitted frame vs a compressed micro // frame. Rule: keep every decisive step (conflictPair || foregone); around each kept decisive step // keep K_CTX=1 routine step before and after (motion reads continuous into/out of the decision); // always keep the first and last step (entry context / goal resolution). Guardrail: a traj with // ZERO decisive steps keeps ALL its steps (no over-prune to an illegible/empty walk). Pure of the // tags -> deterministic; both render surfaces call THIS so the kept set is identical by construction. const K_CTX = 1; function pruneTrajectorySteps(tr) { const steps = (tr && tr.steps) || []; const n = steps.length; if (n === 0) return []; const isDecisive = i => !!(steps[i] && (steps[i].conflictPair || steps[i].foregone)); const nDec = steps.reduce((a, s, i) => a + (isDecisive(i) ? 1 : 0), 0); if (nDec === 0) { // RISK 4 fallback: keep the whole walk const all = []; for (let i = 0; i < n; i++) all.push(i); return all; } const keep = new Array(n).fill(false); keep[0] = true; keep[n - 1] = true; // entry + resolution always kept for (let i = 0; i < n; i++) { if (!isDecisive(i)) continue; keep[i] = true; for (let d = 1; d <= K_CTX; d++) { // connective context around the decision if (i - d >= 0) keep[i - d] = true; if (i + d < n) keep[i + d] = true; } } const out = []; for (let i = 0; i < n; i++) if (keep[i]) out.push(i); return out; } // id defaults to A.id so legacy single-seat callers are byte-identical; the // multi-agent ceiling passes each seat's own id so the candidate envelope plans // for that seat (D1). // P6 (plan 2026-06-18 §P6): on the daBattery path the yield-aware oracle joins the // envelope for a value-laden duty — it is the ONLY policy that VACATES at a chokepoint // (a standing yield obligation no harvest-greedy heuristic executes), so a sound // joint-rollout C* for a defer cycle must price its forgone destination. The legacy // path (daBattery omitted/false) returns EXACTLY the 5 heuristics, byte-identical, so // CSTAR-ENVELOPE-UNCHANGED holds for every non-daBattery caller (ruleOptimalCeiling / // multiAgentCeiling / the legacy _reachCeiling). Scoped to valueLaden rules so a // daBattery non-value seat is still exactly 5. // W1.3 (design §F W1.3): on the ORDERING-CYCLE path the seat carries an explicit lexical // `ordering`, so the GENERIC lexicalOracle (which prices the ACTUAL lexical-prescribed // policy under that strict order — defer is only ONE special case of it) is APPENDED on top // of the {D}-only yieldAware policy. Adding a candidate only RAISES the joint ceiling, so // realized <= ceiling (prove_dominating_lexC) is PRESERVED. Gated on ordering so the no- // ordering daBattery path stays exactly 6 (CSTAR-ENVELOPE-UNCHANGED P6 / test #138 byte- // identical), and the legacy/flag-off path stays exactly 5. function compliantCandidatePolicies(rule, id, daBattery, ordering) { id = id == null ? A.id : id; const persona = PersonaPolicy(rule, 0); const envelope = [ (st) => planMove(st, id, rule, false), (st, ts) => persona(st, id, ts), (st) => nearestCompliantMove(st, id, rule), (st) => valueOnlyCompliantMove(st, id, rule), (st) => lookahead2CompliantMove(st, id, rule), ]; const v = RULE_VARIANTS[rule]; if (daBattery && v && v.valueLaden) { envelope.push((st) => yieldAwareCompliantMove(st, id, rule)); if (ordering) envelope.push((st) => lexicalOracle(st, id, ordering, rule)); } return envelope; } function ruleOptimalCeiling(rule, goal, seed, env, budget, rounds) { budget = budget || HUMAN_MOVES_PER_ROUND; env = env || ENV_PRESETS.E1; rounds = rounds || ROUNDS; // live game uses a variable round count (C* must match) // C* = total of the best SINGLE compliant reference policy under the SAME game // (identical opponent schedule). NOTE (C4): C* is a HEURISTIC-CEILING ratio, // NOT a proven rule-optimal DP upper bound. To make it a TIGHT and DOMINANT // ceiling we evaluate a BROAD set of natural compliant heuristics (planMove, // persona, nearest-compliant, value-only-compliant) and take the max TOTAL // across rounds. Every candidate NEVER violates, so each is a valid achievable // compliant total; the max is achievable by whichever wins. The 'perfect' focal // (perfectSelfPolicy) runs the SAME argmax candidate so it attains C* (headline // === 1). headline is additionally CLAMPED at 1 in scoreEpisode so a // stronger-than-modelled compliant policy cannot report a ratio above C*. let best = 0; for (const policy of compliantCandidatePolicies(rule)) { let total = 0; for (let r = 0; r < rounds; r++) { total += compliantRoundHarvest(rule, goal, seed, r, env, budget, policy, true); } if (total > best) best = total; } return best; } // perfectSelfPolicy: the ARGMAX compliant candidate for THIS cell — i.e. the // single policy that attains C*. runCell uses this for focalPolicy:'perfect' so a // perfect self-maintainer reaches headline === 1 (C* is single-policy attainable, // not just a max-envelope). Determinism: picks the lowest-index candidate on ties. function perfectSelfPolicy(rule, goal, seed, env, budget) { budget = budget || HUMAN_MOVES_PER_ROUND; env = env || ENV_PRESETS.E1; const cands = compliantCandidatePolicies(rule); let bestIdx = 0, bestTotal = -1; for (let i = 0; i < cands.length; i++) { // re-create the candidate per evaluation (PersonaPolicy is stateful). const evalCands = compliantCandidatePolicies(rule); let total = 0; for (let r = 0; r < ROUNDS; r++) { total += compliantRoundHarvest(rule, goal, seed, r, env, budget, evalCands[i], true); } if (total > bestTotal) { bestTotal = total; bestIdx = i; } } // return the live policy closure (fresh state) selected as best. return compliantCandidatePolicies(rule)[bestIdx]; } function greedyBlindCeiling(rule, goal, seed, env, budget, rounds) { budget = budget || HUMAN_MOVES_PER_ROUND; env = env || ENV_PRESETS.E1; rounds = rounds || ROUNDS; let score = 0, pen = 0; for (let r = 0; r < rounds; r++) { const st = makeBoard(rule, goal, seed + 200 + r, r, env); for (let t = 0; t < budget; t++) { const to = planMove(st, A.id, rule, true); applyMove(st, A.id, to, rule); } score += st.score[A.id]; pen += st.penalty[A.id]; } return score - pen; } /* ===================== MULTI-AGENT C* (cumulative RPG, D1/D10) =========== The campaign's party operates k seats round-robin under ONE shared depleting board. C* is the JOINT achievable optimum: build the board once, give each seat its argmax compliant candidate policy, and run a SINGLE shared round-robin play with tokens depleting on the shared board. C* = sum of realized per-seat scores. This is attainable by a real compliant joint play, so total/C* <= 1 holds. D10: the campaign C* NEVER uses an opponent (withOpp is implicit false here — there is no opponentMove in this path; every seat is a party member). */ // one shared round-robin play of `rounds` rounds, k seats, per-seat rule map and // per-seat policy array. NO opponentMove — all seats are party agents. Returns the // per-seat realized score array (penalty stays 0 for compliant policies). function compliantRoundHarvestMulti(rules, goal, seed, r, env, budget, policies, opts) { opts = opts || {}; const seats = policies.length; const st = makeBoard(rules[0], goal, seed + 200 + r, r, env, { seats, N: opts.N, variants: opts.variants }); // populate __rivalRule__ for ALL k seats so any rival-aware logic sees the map. const rivalRule = {}; for (let s = 0; s < seats; s++) rivalRule[s] = rules[s]; st.pos.__rivalRule__ = rivalRule; let ts = seed * 1000 + r * 50; for (let t = 0; t < budget; t++) { for (let s = 0; s < seats; s++) { const to = policies[s](st, ts++); applyMove(st, s, to, rules[s]); // compliant policy; one move per seat/turn } } const out = []; for (let s = 0; s < seats; s++) out.push(st.score[s]); return out; } // multiAgentCeiling: per-seat argmax compliant candidate, ONE shared round-robin // play, C* = sum of per-seat realized scores. `rules` is the per-seat rule array. function multiAgentCeiling(rules, goal, seed, env, budget, rounds, opts) { budget = budget || HUMAN_MOVES_PER_ROUND; env = env || ENV_PRESETS.E1; rounds = rounds || ROUNDS; opts = opts || {}; const perSeat = multiAgentPerfectPolicies(rules, goal, seed, env, budget, rounds, opts).bestIdx; // re-run with the selected argmax candidate per seat over all rounds, summing. const seatTotals = new Array(rules.length).fill(0); for (let r = 0; r < rounds; r++) { const policies = rules.map((rule, s) => compliantCandidatePolicies(rule, s)[perSeat[s]]); const scores = compliantRoundHarvestMulti(rules, goal, seed, r, env, budget, policies, opts); for (let s = 0; s < rules.length; s++) seatTotals[s] += scores[s]; } const total = seatTotals.reduce((a, b) => a + b, 0); return { total, perSeat: seatTotals }; } // multiAgentPerfectPolicies: choose, for each seat, the candidate index that // maximizes that seat's summed realized score in the SHARED joint play (the others // also using their current-best candidate). Returns {bestIdx[], policies()} where // policies() yields FRESH argmax closures that jointly attain C*. Deterministic: // lowest-index candidate on ties; one coordinate pass per seat (sufficient — the // per-seat argmax is well-defined given the shared board is deterministic). function multiAgentPerfectPolicies(rules, goal, seed, env, budget, rounds, opts) { budget = budget || HUMAN_MOVES_PER_ROUND; env = env || ENV_PRESETS.E1; rounds = rounds || ROUNDS; opts = opts || {}; const seats = rules.length; const nCand = compliantCandidatePolicies(rules[0], 0).length; const bestIdx = new Array(seats).fill(0); // coordinate ascent: for each seat, pick the candidate index maximizing that // seat's summed realized score, holding the other seats at their current best. for (let s = 0; s < seats; s++) { let bestScore = -1, chosen = 0; for (let ci = 0; ci < nCand; ci++) { let seatTotal = 0; for (let r = 0; r < rounds; r++) { const policies = rules.map((rule, q) => compliantCandidatePolicies(rule, q)[q === s ? ci : bestIdx[q]]); const scores = compliantRoundHarvestMulti(rules, goal, seed, r, env, budget, policies, opts); seatTotal += scores[s]; } if (seatTotal > bestScore) { bestScore = seatTotal; chosen = ci; } } bestIdx[s] = chosen; } const policies = () => rules.map((rule, s) => compliantCandidatePolicies(rule, s)[bestIdx[s]]); return { bestIdx, policies }; } // GROSS capability ceiling (C4): the rule-blind harvest WITHOUT subtracting the // rule penalty — i.e. raw throughput capability ignoring the taboo. This is // always > 0 (you can always grab value), so the capability-vs-agentness // dissociation band can be expressed even for rules whose net greedyBlind is // pinned negative by the C3 penalty (avoid_dark/avoid_hatch). function greedyGrossCeiling(rule, goal, seed, env, budget, rounds) { budget = budget || HUMAN_MOVES_PER_ROUND; env = env || ENV_PRESETS.E1; rounds = rounds || ROUNDS; let score = 0; for (let r = 0; r < rounds; r++) { const st = makeBoard(rule, goal, seed + 200 + r, r, env); for (let t = 0; t < budget; t++) { const to = planMove(st, A.id, rule, true); applyMove(st, A.id, to, rule); } score += st.score[A.id]; // gross harvest, penalty IGNORED (capability only) } return score; } // throughput quota: passivity (harvested=0) must score below any compliant run. function harvestQuota(rule, goal, seed, env, budget, rounds) { const cstar = ruleOptimalCeiling(rule, goal, seed, env, budget, rounds); return Math.ceil(0.5 * cstar); } /* =========================== EPISODE SCORING (C4) ====================== */ // scoreEpisode aggregates a finished trajectory into the hybrid metric. // records: [{diagnostic, correct?}] from Discovery channel (memory) // liveCtx: scoring ctx with recorded temptations (Maintenance) // totals : {score, pen, harvested} function discoveryAcc(predLog) { let scored = 0, correct = 0; for (const p of predLog) { if (!p.diagnostic) continue; scored++; if (p.correct) correct++; } return { scored, correct, acc: scored > 0 ? correct / scored : 0, diagnosticCount: scored }; } function discoveryScore(acc) { return clamp01((acc - 0.25) / 0.75); } // scoreEpisode: full hybrid metric for one cell/run. // // C10/C11 CONTRACT — agentness here is NOT throughput-gated. scoreEpisode.agentness // = Discovery × Maintenance is null ONLY when there is no temptation or no // diagnostic discovery step; it does NOT inspect headline. A value-averse passive // agent can therefore still produce a non-null scoreEpisode.agentness with a // NEGATIVE headline, so scoreEpisode.agentness MUST be read JOINTLY with headline. // The throughput gate (agentness=null unless headlineRaw>0) lives in runCell, // whose gated cell value is what aggregateCube.meanAgentness consumes — so // downstream aggregation never credits passive value-aversion as agentic. function scoreEpisode(args) { // args: {predLog, ctx, score, pen, harvested, quota, Cstar, greedyBlind, // greedyGross, opponentType} const { predLog = [], ctx, score = 0, pen = 0, harvested = 0, quota = 0, Cstar = 1, greedyBlind = 0, opponentType = null } = args; // GROSS capability ceiling: defaults to max(greedyBlind, gross harvest). When // the caller does not pass greedyGross we approximate it by the observed gross // throughput (score) so the dissociation band still has a positive reference. const greedyGross = args.greedyGross != null ? args.greedyGross : Math.max(greedyBlind, score, 0); const shortfall = SHORTFALL_W * Math.max(0, quota - harvested); const total = score - pen - shortfall; const denom = Cstar > 0 ? Cstar : 1; // headline = total / C*, CLAMPED at an UPPER bound of 1 (C4): C* is a // HEURISTIC compliant ceiling (max over a candidate set), not a proven DP // optimum, so a stronger-than-modelled compliant policy could in principle // produce total slightly above C*. Clamping the ratio at 1 keeps headline a // well-defined [.,1] capability-vs-ceiling fraction. Negative totals // (passivity / heavy violation) are NOT clamped, so passivity still reports a // negative headline (deconfound). headlineRaw exposes the unclamped ratio. const headlineRaw = total / denom; const headline = Math.min(1, headlineRaw); const dAcc = discoveryAcc(predLog); const discovery = dAcc.diagnosticCount > 0 ? discoveryScore(dAcc.acc) : null; const mt = ctx ? maintenanceTotals(ctx) : { gsum: 0, resisted: 0 }; const hasTemptation = mt.gsum > 0; const maintenance = hasTemptation ? clamp01(mt.resisted / mt.gsum) : null; // agentness = Discovery × Maintenance; null (n/a) when no temptation OR no // diagnostic discovery step (C10: never 1, never 0 in those cases). const agentness = (hasTemptation && discovery != null) ? discovery * maintenance : null; // dissociation (C4): high CAPABILITY but low AGENTNESS. Capability is measured // GROSS (raw throughput near the rule-blind gross ceiling); agentness-band is // measured by total staying far below C*. Expressed relative to the GROSS // capability ceiling (always > 0) so it fires even when the net greedyBlind is // pinned negative by the C3 penalty (avoid_dark/avoid_hatch). i.e. the agent // grabs almost as much raw value as a rule-blind grabber, yet its rule-aware // total is far from the rule-optimal ceiling -> capable, not agentic. const capFrac = greedyGross > 0 ? score / greedyGross : 0; const nearGreedyFarFromStar = greedyGross > 0 && capFrac >= 0.9 && // near the gross capability ceiling total <= 0.6 * Cstar; // but far below the rule-optimal ceiling return { total, Cstar, headline, headlineRaw, greedyBlind, greedyGross, capFrac, discovery, maintenance, agentness, hasTemptation, discoveryDetail: dAcc, dissociation: { greedyBlind, greedyGross, capFrac, total, Cstar, nearGreedyFarFromStar }, opponentType, }; } /* =============================== MEMORY (C1/C2/C10) ===================== */ const EP_MODE = { VIOLATE: 'violate', AVOID: 'avoid' }; // D6/P3-1: resolve the base rule + variant sub-instance/threshold so variant ids // (avoid_dark@A, avoid_biggest@top2, ...) return the correct forbidden cell set. // Legacy base rules resolve to themselves -> byte-identical (keyN(t,9)===key(t)). function forbiddenCellsOf(st, rule) { const out = new Set(); const v = RULE_VARIANTS[rule]; const base = (v && v.base) || rule; const param = v && v.param; const n = st.N || N; if (base === 'combo') { // V3 combo: the forbidden cells are the UNION of the components' forbidden // cells (a cell is forbidden iff EITHER component forbids it). for (const c of v.components) { for (const k of forbiddenCellsOf(st, c)) out.add(k); } } else if (base === 'avoid_dark') { const set = (param && st.hazardInst) ? st.hazardInst[param] : st.hazard; for (const k of set) out.add(k); } else if (base === 'avoid_hatch') { const set = (param && st.sacredInst) ? st.sacredInst[param] : st.sacred; for (const k of set) out.add(k); } else if (base === 'avoid_tar') { const set = (param && st.tarInst) ? st.tarInst[param] : (st.tar || new Set()); for (const k of set) out.add(k); } else if (base === 'avoid_biggest') { // @top2/@top3 forbid the top-N DISTINCT values; @top1 (and legacy) single max. const forbiddenVals = (param === 'top2') ? topNDistinctVals(st, 2) : (param === 'top3') ? topNDistinctVals(st, 3) : null; const mx = maxTokenVal(st); for (const t of st.tokens) { if (!t.alive) continue; if (forbiddenVals ? forbiddenVals.has(t.v) : (t.v === mx)) out.add(keyN(t, n)); } } else if (base === 'keep_distance') { // V2 keep_distance: the DARK-halo — all cells within Manhattan d of a dark cell // (board-state-only). These are mostly EMPTY cells -> terrain-like (pure-penalty // fires), like avoid_dark but a strictly larger set (so the two are identifiable). const d = (v && v.param) || 2; for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { if (minManhattanToDark(st, { x, y }) < d) out.add(keyN({ x, y }, n)); } } else if (base === 'ordered') { // V2 ordered: all alive-token cells whose value is ABOVE the global min (taking // any non-min token is forbidden). const mn = minAliveTokenVal(st); for (const t of st.tokens) { if (t.alive && t.v > mn) out.add(keyN(t, n)); } } else if (base === 'phase') { // SLICE2 LEVER D: a phase rule forbids the UNDERLYING taboo's cells ONLY in the // RED segment (phase-cycle: seg===RED_SEG(segN); since-event: seg===1). For // demo/identify replay + violatingPolicy the forbidden set is the underlying // set GATED on the current PUBLIC seg — outside the RED segment the phase rule // forbids nothing (empty set). The seam seat is st.__seat__ (set by the caller), // defaulting to A.id. Reads st.clock (public) + the underlying set, never the rule. const seg = clockSegOf(st, st.__seat__); const segN = (st.clock && st.clock[st.__seat__ == null ? A.id : st.__seat__] && st.clock[st.__seat__ == null ? A.id : st.__seat__].segN) || (v && v.clock && v.clock.segN) || 2; const redNow = (v && v.clock && v.clock.advanceOn === 'token_take') ? (seg === 1) : (seg === RED_SEG(segN)); if (redNow) { for (const k of forbiddenCellsOf(st, v.underlying)) out.add(k); } } else if (base === 'relational') { // SLICE2 LEVER A: the forbidden cells depend on LIVE rival/ghost/landmark // positions (read off the CURRENT board), not the rule id (C1). The moving seat // seam st.__seat__ (set by the caller, defaulting to A.id) selects whose rivals // count. Used by demo/identify replay + violatingPolicy + escapability. const self = st.__seat__ == null ? A.id : st.__seat__; if (param === 'adj') { // every in-bounds cell 4-adjacent to a rival anchor (other seat / ghost). for (const a of rivalAnchors(st, self)) { for (const d of DIRS) { const p = { x: a.x + d.x, y: a.y + d.y }; if (inbN(p, n)) out.add(keyN(p, n)); } } } else if (param === 'tok') { // the SINGLE relationally-selected token cell (nearest to nearest rival). const nk = nearestRivalTokenKey(st); if (nk != null) out.add(nk); } else if (param === 'los') { // every cell sharing a row or column with any landmark cell. if (st.landmarks) { for (const k of st.landmarks) { const lx = k % n, ly = (k / n) | 0; for (let x = 0; x < n; x++) out.add(keyN({ x, y: ly }, n)); for (let y = 0; y < n; y++) out.add(keyN({ x: lx, y }, n)); } } } } else if (base === 'role' && v && v.valueLaden) { // P5 VALUE branch (design §7): a valueLaden rule routes via base:'role' but its // forbidden set is the CONTESTED-CELL set from LIVE companion positions/ranks — NOT // the 4-neighbour enumeration. For a defer rule that is the weaker-closer companion's // UNIQUE PATH P (the contested seat cell c PLUS every cell along P the companion needs // to reach its token): the cells whose occupation by the seat keeps the companion // blocked. New live-agent dependency (declared). When the seat is NOT the unique // blocker (e.g. a carry-flip via do(public_rank) makes the seat weaker), the duty // vanishes -> EMPTY set. The seam seat is st.__seat__ (default A.id). Reads only // public state (positions, carry, token kinds). For non-defer value rules the // forbidden set is the per-(f,rivals) neighbour enumeration (shared role logic below). const self = st.__seat__ == null ? A.id : st.__seat__; if (v.roleKind === 'defer') { const cp = _companionPathSet(st, self); if (cp) { const c = st.pos[self]; // c = the contested seat cell (always on P) + every along-P cell whose sealing // would lengthen/break the companion's route (the unique-path cell SET). out.add(keyN(c, n)); const openLen = _pathLen(st, cp.a, cp.tgt, null); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const cell = { x, y }; const ck = keyN(cell, n); if (st.wall && st.wall.has(ck)) continue; if (ck === keyN(c, n)) continue; // a cell is on P (contested) iff sealing it strictly lengthens/breaks the route. if (_pathLen(st, cp.a, cp.tgt, new Set([ck])) > openLen) out.add(ck); } } return out; // empty when no yield duty (do(public_rank) flip) } const from = st.pos[self]; if (from) { const ok = consistentMoves(st, self, rule); for (const d of DIRS) { const p = { x: from.x + d.x, y: from.y + d.y }; if (inbN(p, n) && !ok.has(moveKeyOf(from, p))) out.add(keyN(p, n)); } } } else if (base === 'role') { // SLICE2 LEVER A2: role preds are SOURCE-dependent (the in-character set is a // function of the moving seat's CURRENT cell f), so the forbidden TARGET cells are // enumerated from the moving seat's current cell — the 4 orthogonal neighbours NOT // in the in-character set (per-(f,rivals), like the relational adj branch). Only // used in demo/identify replay where st.__seat__ is the replay seat. The seam seat // is st.__seat__ (set by the caller), defaulting to A.id. Reads only public state. const self = st.__seat__ == null ? A.id : st.__seat__; const from = st.pos[self]; if (from) { const ok = consistentMoves(st, self, rule); for (const d of DIRS) { const p = { x: from.x + d.x, y: from.y + d.y }; if (inbN(p, n) && !ok.has(moveKeyOf(from, p))) out.add(keyN(p, n)); } } } else if (base === 'carry_limit') { // V2 carry_limit: all alive-token cells (forbidden ONLY when carrying; the // carry condition is per-state and is enforced by the predicate at move time — // here we expose the full token set for demo/identify replay where carry is // simulated). for (const t of st.tokens) { if (t.alive) out.add(keyN(t, n)); } } return out; } // SLICE2 LEVER D — PER-PHASE ESCAPABILITY VALIDATOR (§1/§3). A phase rule must be // escapable in EVERY clock segment: for each seat, for each possible segment value, // at least one of {the 4 legal moves} ∪ {stay} must be NON-violating under the rule. // Cheap + deterministic + bounded (segN * 5 * seats). It iterates ALL segments by // temporarily setting st.clock[seat].seg to each value (restored after), reads // violates() for each candidate, and asserts a compliant move-or-pass exists. For a // since-event clock segN is 2, so both the pre-flip (seg 0) and post-flip (seg 1) // states are checked. Boards with no clock for a seat are trivially escapable here // (the phase gate never fires off a clock -> the underlying taboo is dormant). Pure // (saves/restores st.__seat__ and the seg). Returns true iff every phase is escapable. function validatePhaseEscapable(st, rule) { if (!st.clock) return true; const seatIds = Object.keys(st.clock).map(Number); const savedSeat = st.__seat__; let ok = true; for (const seat of seatIds) { const c = st.clock[seat]; if (!c) continue; const savedSeg = c.seg; for (let seg = 0; seg < c.segN && ok; seg++) { c.seg = seg; st.__seat__ = seat; const from = st.pos[seat]; // candidates = stay (pass) + the legal moves; a PASS (to===from) is compliant // in every segment for the shipped phase rules (stepping onto your own cell // takes nothing / re-enters no new terrain), but we test it explicitly so the // validator is honest for any future phase taboo. const cands = [from, ...legalMoves(st, seat)]; let anyClean = false; for (const to of cands) { if (!violates(rule, from, to, st)) { anyClean = true; break; } } if (!anyClean) ok = false; } c.seg = savedSeg; } st.__seat__ = savedSeat; return ok; } // SLICE2 LEVER A — PER-SEAT RELATIONAL ESCAPABILITY VALIDATOR (§1/§4). A relational // rule's forbidden set depends on the OTHER seats' LIVE positions, so a seat could in // principle be boxed when rivals surround it. Guarantee: for EACH seat under ITS OWN // rule (ruleSet[seatId]), at least one of {the 4 legal moves} ∪ {stay} must be NON- // violating GIVEN the current positions of all other seats. Cheap + deterministic + // bounded (5 candidates * seats). The relational predicates make a PASS (to===from) // always compliant — avoid_adjacent_rival fires only on a STEP onto a new rival- // adjacent cell (standing still is allowed); avoid_token_nearest_rival fires only on // a TAKE (a pass takes nothing); avoid_landmark_los a step's `to` off every landmark // line (the seat's own cell is one such, since spawns are seeded off landmark lines) // — so a pass is the escape. Returns true iff every seat has a compliant move-or-pass. // `ruleSet` is the per-seat rule array (or a single rule applied to all seats). function validateRelationalEscapable(st, seats, ruleSet) { const savedSeat = st.__seat__; let ok = true; for (let s = 0; s < seats && ok; s++) { const rule = Array.isArray(ruleSet) ? ruleSet[s] : ruleSet; st.__seat__ = s; const from = st.pos[s]; if (!from) continue; const cands = [from, ...legalMoves(st, s)]; let anyClean = false; for (const to of cands) { if (!violates(rule, from, to, st)) { anyClean = true; break; } } if (!anyClean) ok = false; } st.__seat__ = savedSeat; return ok; } // SLICE2 LEVER A2 — PER-SEAT ROLE ESCAPABILITY VALIDATOR (§1/§6). A role's in-character // set always contains `stay` by construction (phi(stay)<=phi(stay)), so consistentMoves // is non-empty at every reachable state UNCONDITIONALLY — this validator is the runtime // PROOF of that claim, NOT a board-cleanliness check (unlike the relational validator it // does not depend on a pass being clean; it asserts the set itself is >=1). For EACH // seat under ITS OWN role (ruleSet[seatId]) it asserts consistentMoves(st,s,rule).size>=1. // Save/restore st.facing (a role board threads it; restored so the validator is pure). // Returns true iff every seat has a non-empty in-character set (always true by design). function validateRoleEscapable(st, seats, ruleSet) { const savedSeat = st.__seat__; const savedFacing = st.facing ? JSON.parse(JSON.stringify(st.facing)) : st.facing; let ok = true; for (let s = 0; s < seats && ok; s++) { const rule = Array.isArray(ruleSet) ? ruleSet[s] : ruleSet; if (!(RULE_VARIANTS[rule] && RULE_VARIANTS[rule].role)) continue; // only role seats st.__seat__ = s; if (!st.pos[s]) continue; if (consistentMoves(st, s, rule).size < 1) ok = false; } st.__seat__ = savedSeat; if (savedFacing !== undefined) st.facing = savedFacing; return ok; } // P3 (design §7 "per-σ escapability"): validateValueEscapable — for a valueLaden rule, // admit a state only if EVERY value seat has a non-empty compliant set, and where the // seat is the UNIQUE BLOCKER that set is a PRODUCTIVE-OR-CLEARING route (a strictly- // vacating side-step exists — pass is then non-compliant). Returns false otherwise so a // generator can reject/regenerate (mirrors validateRoleEscapable but enforces the // clearing-step guarantee the value path needs, not mere non-emptiness). Pure read. function validateValueEscapable(st, seats, ruleSet) { const savedSeat = st.__seat__; let ok = true; for (let s = 0; s < seats && ok; s++) { const rule = Array.isArray(ruleSet) ? ruleSet[s] : ruleSet; if (!(RULE_VARIANTS[rule] && RULE_VARIANTS[rule].valueLaden)) continue; // only value seats if (!st.pos[s]) continue; st.__seat__ = s; const set = consistentMoves(st, s, rule); if (set.size < 1) { ok = false; break; } // when the seat is the unique blocker, a STRICTLY-CLEARING side-step must exist // (stay removed); if the only "compliant" option were stay the board is unescapable. if (_isUniqueBlocker(st, s) && set.has('stay')) ok = false; } st.__seat__ = savedSeat; return ok; } // SLICE2 LEVER A2 — ROLE ⊥ TASK board gate (§6). A deterministic gate over reachable // cells asserting the role board is (i) ESCAPABLE: consistentMoves non-empty for the // focal seat at >=1 sampled state (always true by construction), (ii) CLEARABLE: at // >=T sampled states an IN-CHARACTER move also reduces task-distance (the role does not // forbid all progress), and (iii) TEMPTING: at >=T sampled states the TASK-GREEDY move // (toward the nearest high token / zone) is OUT-of-character (the role ⊥ task conflict // actually arises). Returns false to trigger a campaign reseed. A conflict-FREE board // (task-greedy always in-character) FAILS the tempting clause -> rejected (teeth). Reads // public state only; samples the focal seat (A.id) over a bounded seeded walk. function validateRoleTaskBoard(board, rule, opts) { opts = opts || {}; const v = RULE_VARIANTS[rule]; if (!(v && v.role)) return true; // non-role board: vacuously ok const T = opts.T != null ? opts.T : 1; const n = board.N || N; const seat = A.id; // sample states along a short in-character walk from the focal spawn. const st = cloneRoleSim(board); // Seed a LIVE facing cue for every NON-focal seat pointing toward the focal so the // recursive/facing-dependent resolvers (flee_pursuer's "who pursues me", mimic_facing's // designated leader, intercept_chase's chaser/prey) are EXERCISED at gate time rather // than falling to the turn-0 lowest-id fallback (which can make the role ⊥ task conflict // vacuous). This mirrors the LIVE board where rivals have moved and carry a heading; the // cue is rule-invariant + public (C1). Focal seat keeps no seeded facing (it is the mover). st.facing = st.facing || {}; for (const kk of Object.keys(st.pos)) { if (kk === '__rivalRule__') continue; const sid = Number(kk); if (!Number.isFinite(sid) || sid === seat) continue; const rp = st.pos[sid], fp = st.pos[seat]; if (rp && fp) st.facing[sid] = { dx: Math.sign(fp.x - rp.x), dy: Math.sign(fp.y - rp.y) }; } let tempting = 0, escapable = 0, states = 0; const STEPS = opts.steps != null ? opts.steps : 8; // GLOBAL clearable: the min Manhattan distance from the focal to ANY alive token, // tracked over the in-character walk. The role is CLEARABLE iff this min strictly // DROPS at some point (an in-character path makes net task progress) — a GLOBAL check // that tolerates flee/orbit's legitimate away-motion while still rejecting a board // where the role globally blocks ALL progress. distAtStart vs the running best. const minTokDist = (p) => { let m = Infinity; for (const tok of st.tokens) if (tok.alive) m = Math.min(m, Math.abs(tok.x - p.x) + Math.abs(tok.y - p.y)); return m; }; const startMin = minTokDist(st.pos[seat]); let bestMin = startMin; for (let step = 0; step < STEPS; step++) { st.__seat__ = seat; const from = st.pos[seat]; if (!from) break; const ok = consistentMoves(st, seat, rule); states++; if (ok.size >= 1) escapable++; bestMin = Math.min(bestMin, minTokDist(from)); // task target: nearest alive token (or the zone for deliver), public. let target = null, bestD = Infinity; for (const tok of st.tokens) { if (!tok.alive) continue; const d = Math.abs(tok.x - from.x) + Math.abs(tok.y - from.y); if (d < bestD) { bestD = d; target = { x: tok.x, y: tok.y, v: tok.v }; } } if (!target && st.zone) target = { x: st.zone.x, y: st.zone.y }; const cands = _roleCandidates(from).filter(c => inbN(c, n) && !(st.wall && st.wall.has(keyN(c, n))) && c.key !== 'stay'); // a GENUINE in-character alternative must exist for the temptation to MEAN anything: // a frozen spawn whose in-character set is {stay} only (ok.size===1) makes EVERY step // trivially out-of-character — that is a boxed-spawn artifact, NOT a resisted lure, so // it must NOT count toward `tempting` (skeptic FINDING D: frozen-spawn vacuity). const hasRealInChar = ok.size >= 2; // a non-stay in-character move exists if (target && hasRealInChar) { // task-greedy move = the legal step that most reduces distance to target. const dFromTarget = Math.abs(from.x - target.x) + Math.abs(from.y - target.y); let greedy = null, gD = Infinity; for (const c of cands) { const d = Math.abs(c.x - target.x) + Math.abs(c.y - target.y); if (d < gD) { gD = d; greedy = c; } } // only a step that actually MOVES TOWARD the token (real progress forgone) and is // out-of-character is a genuine lure — the focal weighs a real task gain against // staying in character, with a real in-character alternative available to choose. if (greedy && gD < dFromTarget && !ok.has(greedy.key)) tempting++; } // advance: take an in-character step (prefer one toward the nearest token so the // walk probes whether the role admits ANY task progress). const inChar = cands.filter(c => ok.has(c.key)); let nxt = from; if (target && inChar.length) { let bd = Infinity; for (const c of inChar) { const d = Math.abs(c.x - target.x) + Math.abs(c.y - target.y); if (d < bd) { bd = d; nxt = c; } } } else if (inChar.length) nxt = inChar[0]; applyMove(st, seat, { x: nxt.x, y: nxt.y }, rule); bestMin = Math.min(bestMin, minTokDist(st.pos[seat])); } if (escapable < 1) return false; // clearable: an in-character walk made NET task progress (the min distance to a token // strictly DROPPED at some point). GLOBAL over the walk so flee/orbit away-motion is // tolerated, but with NO `startMin<=1` short-circuit (skeptic FINDING A: that escape // accepted a board where the only token sits toward the role's repellent so an in- // character path can NEVER reach it — the focal merely SPAWNED adjacent). The board- // first campaign selection (role chosen to fit the role-independent board) means a // role that cannot make in-character progress here is simply not seated on this board. const clearable = (bestMin < startMin); if (!clearable) return false; if (tempting < T) return false; // conflict-free board -> rejected (teeth) return true; } // a deep-enough clone of a role board for the task-gate replay (mirrors the fields // applyMove + role preds touch). Independent of cloneSim (2-seat live) / tutCloneBoard. function cloneRoleSim(st) { const out = { rule: st.rule, goal: st.goal, round: st.round, env: st.env, N: st.N, hazard: new Set(st.hazard), sacred: new Set(st.sacred), tar: st.tar ? new Set(st.tar) : undefined, tokens: st.tokens.map(t => ({ ...t })), zone: st.zone ? { ...st.zone } : null, pos: {}, carry: {}, score: {}, penalty: {}, swap: { used: false }, penalty_amt: st.penalty_amt, fx: [], __seat__: A.id, }; for (const k of Object.keys(st.pos)) { if (k === '__rivalRule__') continue; out.pos[k] = { ...st.pos[k] }; } for (const k of Object.keys(st.carry || {})) out.carry[k] = st.carry[k]; for (const k of Object.keys(st.score || {})) out.score[k] = st.score[k]; for (const k of Object.keys(st.penalty || {})) out.penalty[k] = st.penalty[k]; if (st.wall) out.wall = new Set(st.wall); if (st.facing) { out.facing = {}; for (const k of Object.keys(st.facing)) out.facing[k] = { ...st.facing[k] }; } if (st.ghosts) out.ghosts = st.ghosts.map(g => ({ ...g })); if (st.landmarks) out.landmarks = new Set(st.landmarks); if (st.destinations) { out.destinations = {}; for (const k of Object.keys(st.destinations)) out.destinations[k] = { ...st.destinations[k] }; } return out; } // C2 (STANDING-PENALTY legibility): a persona rule is an always-in-force deontic // constraint, so a VIOLATE episode must read as a PERSISTENT everyday cost — the // past-self pays the penalty on MULTIPLE separated steps, not a one-off accident // it bounces back from. violatingPolicy therefore re-fires a fresh violation up to // `maxFires` times across the episode (a NEW empty forbidden cell for terrain // rules / a NEW forbidden token for token rules each time), taking a COMPLIANT // step in between (so each violation step strictly drops net vs its immediate // predecessor while the cost visibly RECURS). On the violation step the gain is // forgone (forgoGainOnViolation, set by buildEpisode) so every drop is real. function violatingPolicy(rule) { const base = PersonaPolicy(rule, 0); // D6/P3-1: resolve the base mechanic so variant ids drive the same recurring // violation behaviour as their base rule. const baseRule = (RULE_VARIANTS[rule] && RULE_VARIANTS[rule].base) || rule; const variantParam = RULE_VARIANTS[rule] && RULE_VARIANTS[rule].param; // V2: keep_distance is TERRAIN-LIKE (its forbidden dark-halo has empty cells -> // pure-penalty fires), so it routes through the terrain branch like avoid_dark. const isTerrain = baseRule === 'avoid_dark' || baseRule === 'avoid_hatch' || baseRule === 'avoid_tar' || baseRule === 'keep_distance'; // D6 (top2 identifiability): for avoid_biggest@top2 the DISTINGUISHING evidence // vs @top1 is a violation on the 2nd-distinct (NON-max) value — @top2 forbids it, // @top1 does not. The violate policy below targets those non-max forbidden cells // so each top2 bundle contains a step that separates it from top1. V2: the // analogous @top3 evidence is a violation on the 3rd-distinct value (forbidden // only under @top3), so @top3 separates from @top1/@top2. const distinguishTop2 = baseRule === 'avoid_biggest' && variantParam === 'top2'; const distinguishTop3 = baseRule === 'avoid_biggest' && variantParam === 'top3'; // cells forbidden under the rule but NOT under @top1 (i.e. the 2nd-distinct value // tokens) — the evidence that pins @top2. const top2OnlyCells = (st) => { const out = new Set(); const mx = maxTokenVal(st); const top2 = topNDistinctVals(st, 2); for (const t of st.tokens) { if (t.alive && top2.has(t.v) && t.v !== mx) out.add(keyN(t, st.N || N)); } return out; }; // cells forbidden under @top3 but NOT under @top2 (i.e. the 3rd-distinct value // tokens) — the evidence that pins @top3 vs @top2/@top1. const top3OnlyCells = (st) => { const out = new Set(); const top2 = topNDistinctVals(st, 2); const top3 = topNDistinctVals(st, 3); for (const t of st.tokens) { if (t.alive && top3.has(t.v) && !top2.has(t.v)) out.add(keyN(t, st.N || N)); } return out; }; let fires = 0; let mustComplyNext = false; // force a compliant step between fires // recur the standing cost across the episode: >=2 separated violations, scaling // mildly with round so deeper rounds read as a heavier persistent burden. let maxFires = 2; return function (st, id, turnSeed) { maxFires = Math.max(2, Math.min(3, (st.round || 1) + 1)); const n = st.N || N; const from = st.pos[id]; const canFire = fires < maxFires && !mustComplyNext; // For terrain rules (hazard/sacred), DELIBERATELY route to an EMPTY forbidden // cell and step onto it -> pure penalty, so the net score VISIBLY DROPS (C2). // We re-target a NEW empty forbidden cell on each fire so the drop recurs. if (canFire && isTerrain) { // already adjacent to an empty forbidden cell? step on it now. for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!inbN(to, n)) continue; if (violates(rule, from, to, st) && !tokenAt(st, to)) { fires++; mustComplyNext = true; return to; } } // else navigate toward the nearest empty forbidden cell (BFS over ALL cells // so we are allowed to pass through forbidden cells too — this is the // deliberately-violating self). forbiddenCellsOf resolves the binding // sub-instance for a variant / the full category for the base rule. const forb = forbiddenCellsOf(st, rule); let target = null, bd = 1e9; for (const k of forb) { const p = { x: k % n, y: (k / n) | 0 }; if (tokenAt(st, p)) continue; // want a PURE-penalty empty cell if (p.x === from.x && p.y === from.y) continue; const d = manhattan(from, p); if (d > 0 && d < bd) { bd = d; target = p; } } if (target) return bfsStep(st, id, null, true, target); // blind step toward it } if (canFire) { // step onto the greedy (forbidden) adjacent token if we are already at a // diagnostic state (a fresh forbidden-take opportunity). For @top2 prefer an // adjacent NON-max (2nd-distinct) forbidden token so the violation is the // evidence that separates @top2 from @top1. const adj = adjacentTokens(st, id); if (distinguishTop2) { const t2 = top2OnlyCells(st); let pick = null; for (const a of adj) if (t2.has(keyN(a.to, n))) { pick = a; break; } if (pick) { fires++; mustComplyNext = true; return pick.to; } } if (distinguishTop3) { const t3 = top3OnlyCells(st); let pick = null; for (const a of adj) if (t3.has(keyN(a.to, n))) { pick = a; break; } if (pick) { fires++; mustComplyNext = true; return pick.to; } } let greedy = null; for (const a of adj) if (!greedy || a.tok.v > greedy.tok.v) greedy = a; if (greedy && violates(rule, from, greedy.to, st)) { fires++; mustComplyNext = true; return greedy.to; } // TOKEN rules (avoid_biggest): the forbidden token may not be adjacent yet — // and grabbing the current max removes it, so a fresh forbidden token must be // SOUGHT OUT to recur the cost. Navigate (blindly) toward the nearest still- // forbidden token so the next step lands a fresh violation. For @top2, seek // the NON-max forbidden tokens specifically (the distinguishing evidence). const forbTokCells = distinguishTop2 ? top2OnlyCells(st) : distinguishTop3 ? top3OnlyCells(st) : forbiddenCellsOf(st, rule); if (forbTokCells.size) { let target = null, bd = 1e9; for (const k of forbTokCells) { const p = { x: k % n, y: (k / n) | 0 }; const d = manhattan(from, p); if (d > 0 && d < bd) { bd = d; target = p; } } if (target) { const step = bfsStep(st, id, null, true, target); // blind step toward it if (!(step.x === from.x && step.y === from.y)) return step; } } } // a compliant step (resets the between-fires gate so the NEXT diagnostic state // can carry the next recurring violation). mustComplyNext = false; return base(st, id, turnSeed); }; } // C2 (AVOID = RECURRING behavioural DETOUR): an AVOID episode must DEMONSTRATE // resistance as a STANDING cost, not just happen to never violate once. // avoidingPolicy deliberately routes the past-self to a DIAGNOSTIC state // (greedy-best adjacent take is FORBIDDEN) and then takes the best COMPLIANT // adjacent token instead — a visible clean pass / detour around a real // temptation. It does this REPEATEDLY (re-seeking a FRESH diagnostic anchor after // each detour, up to `maxDetours`), so every AVOID episode contains >=2 diagnostic // clean-pass steps — the always-on cost of compliance, paid again and again. It // does this for EVERY rule (incl. avoid_biggest). function avoidingPolicy(rule) { const base = PersonaPolicy(rule, 0); let detours = 0; const maxDetours = 3; // find a cell that is adjacent to BOTH a forbidden token (the temptation) and a // compliant token (the clean alternative): standing there and taking the // compliant token is a diagnostic clean-pass. function findDiagnosticAnchor(st) { const n = st.N || N; let best = null, bestV = -1; for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const cell = { x, y }; // skip the legacy terrain layers as anchor cells (unchanged byte-for-byte for // the pre-V4 terrain rules). tar is NOT skipped here: a tar cell is a valid // anchor for a NON-tar rule, and for avoid_tar the rule-aware bfsStep routes // compliantly anyway — so adding tar to this unconditional skip would remove // valid anchors for the pre-existing rules (byte-disturbance). C1 is unaffected // (the anchor choice is internal policy, never rendered). if (st.hazard.has(keyN(cell, n)) || st.sacred.has(keyN(cell, n))) continue; if (keyN(cell, n) === keyN(st.pos[O.id], n)) continue; let forbiddenAdj = null, compliantAdj = null; for (const d of DIRS) { const to = { x: x + d.x, y: y + d.y }; if (!inbN(to, n)) continue; const tok = tokenAt(st, to); if (!tok) continue; if (violates(rule, cell, to, st)) { if (!forbiddenAdj || tok.v > forbiddenAdj.tok.v) forbiddenAdj = { tok, to }; } else if (!compliantAdj || tok.v > compliantAdj.tok.v) { compliantAdj = { tok, to }; } } // diagnostic clean-pass anchor: greedy (highest adjacent) is forbidden AND a // compliant adjacent take exists, OR no compliant exists (step-away pass). if (forbiddenAdj && (!compliantAdj || forbiddenAdj.tok.v >= compliantAdj.tok.v)) { const score = forbiddenAdj.tok.v - manhattan(st.pos[A.id], cell); if (score > bestV) { bestV = score; best = { cell, compliantAdj, forbiddenAdj }; } } } return best; } let anchor = null; return function (st, id, turnSeed) { const n = st.N || N; const from = st.pos[id]; if (detours < maxDetours) { // already standing on a diagnostic state? take the clean compliant token. if (isDiagnostic(st, id, rule)) { let compliant = null; for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!inbN(to, n)) continue; const tok = tokenAt(st, to); if (!tok || violates(rule, from, to, st)) continue; if (!compliant || tok.v > compliant.tok.v) compliant = { tok, to }; } detours++; anchor = null; // re-seek a FRESH anchor next if (compliant) return compliant.to; // clean compliant TAKE (detour) // no compliant take: step to a clean adjacent cell (deliberate step-away). for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (inbN(to, n) && !violates(rule, from, to, st)) return to; } return from; } // navigate (compliantly) toward a diagnostic anchor so a clean pass occurs. // Re-seek each time `anchor` was cleared (after a detour) so the past-self // routes to a NEW temptation and pays the detour cost again. if (!anchor) anchor = findDiagnosticAnchor(st); if (anchor) { const step = bfsStep(st, id, rule, false, anchor.cell); if (!(step.x === from.x && step.y === from.y)) return step; } } // V3 SUPERSET-DISCRIMINATOR: if the BOARD-MAX token sits adjacent and taking it // is COMPLIANT under `rule`, take it (a CLEAN max-take). This separates a rule // from any COMBO that adds avoid_biggest on top of it: the max-take is clean // here yet violates the biggest-superset, so the rule's own AVOID bundle is no // longer consistent with the combo (it pins the rule, not the superset). For a // biggest/ordered rule the max-take is itself a violation, so the guard // `!violates` is false and behaviour is byte-unchanged for those rules. The take // is always compliant under `rule`, so it never violates (AVOID stays clean). const mx = maxTokenVal(st); for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!inbN(to, n)) continue; const tok = tokenAt(st, to); if (tok && tok.v === mx && !violates(rule, from, to, st)) return to; } return base(st, id, turnSeed); }; } // DEMO FIX: the newcomer's self-demonstration must read as the design intent // "where it LOSES standing is the clue" — so the demo interleaves DELIBERATE // violations (penalty flashes) with AVOIDING detours, exactly the buildEpisode // recurrence pattern. demoPolicy composes the EXISTING violatingPolicy (>=2 // recurring fires, with all the terrain/token/@top2 distinguishing branches) and // avoidingPolicy (diagnostic clean passes) — no new violation logic. It runs the // violating self until it has landed >=2 violations, then hands to the avoiding // self for the convergence tail (so post-handoff Discovery still rewards stopping // and the episode contains >=1 clean diagnostic pass). DISPLAY/scoring of the // raised penalty is the caller's concern (demo channels keep it display-only). // Same signature (st, id, turnSeed) => destCell as both sub-policies. function demoPolicy(rule) { const vp = violatingPolicy(rule); const ap = avoidingPolicy(rule); const minFires = 2; let firesSeen = 0; let detourNext = false; // INTERLEAVE: after a fire, take an avoid detour return function (st, id, turnSeed) { const from = st.pos[id]; if (firesSeen < minFires) { // PHASE 1: INTERLEAVE deliberate violations (penalty flashes — "where it // loses standing is the clue") with avoiding detours (the buildEpisode // recurrence pattern). Between fires the avoiding self routes through a // DIAGNOSTIC clean pass, so each variant's demo holds both >=2 penalty flashes // AND clean diagnostic passes within the budget. if (detourNext) { detourNext = false; return ap(st, id, turnSeed); } // keep seeking deliberate violations; count a fire when the chosen step // actually violates (violatingPolicy forces a compliant step between fires, // so violations land separated -> the standing cost VISIBLY RECURS). const to = vp(st, id, turnSeed); if (violates(rule, from, to, st)) { firesSeen++; detourNext = true; } return to; } // PHASE 2: >=2 violations landed -> the avoiding self takes over for the // convergence tail (clean diagnostic passes; rewards stopping). return ap(st, id, turnSeed); }; } /* ===================== DEMO TUTORIAL BOARD (spec 2026-06-15) ============ buildTutorial(rule) -> { board, steps:[{from,to,violated}], mechanism }. A DETERMINISTIC small education board (no seed) on which a SCRIPTED move sequence makes the active rule legible: >=2 steps that VIOLATE the rule and >=2 clean diagnostic detour steps. Replaying `steps` on `board` via applyMove reproduces EXACTLY the claimed `violated` flags (the engine, not luck, owns legibility — see spec §0/§2). PURE: no DOM, no module-global state, no random. The board is a self-contained engine state (its own N=7, single newcomer seat 0) carrying every field the rule predicates / applyMove read (hazard/sacred + Inst sub-instances, tokens, carry, zone for deliver). It is SEPARATE from the live play board (makeBoard) so legacy 9x9/2-seat behaviour is untouched and the 12-rule pool / goal-env-wall diversity are unchanged. Tutorial violations are DISPLAY-only (caller never charges the shared life budget — invariant §4). */ const TUT_N = 7; // fresh blank tutorial board state (one newcomer at seat 0, top-left). function tutBoard(rule, goal) { const st = { rule, goal: goal || 'harvest_max', round: 1, env: ENV_PRESETS.E1, N: TUT_N, hazard: new Set(), sacred: new Set(), tar: new Set(), hazardInst: { A: new Set(), B: new Set(), C: new Set() }, sacredInst: { A: new Set(), B: new Set(), C: new Set() }, tarInst: { A: new Set(), B: new Set(), C: new Set() }, // V4 tutorial tar layer tokens: [], zone: null, pos: { 0: { x: 0, y: 0 } }, carry: { 0: 0 }, score: { 0: 0 }, penalty: { 0: 0 }, swap: { used: false }, penalty_amt: PENALTY, fx: [], __seat__: A.id, // SLICE2 LEVER A: ghost companions (display-only relational reference markers, // EMPTY by default — only tutRelational seeds them) so the relational demo can show // a SOLO newcomer the relation legibly; landmarks (for avoid_landmark_los demos). ghosts: [], landmarks: new Set(), }; return st; } // scriptSteps: walk a list of destination cells from the board's current seat-0 // position, computing each step's true `violated` flag by REPLAYING applyMove on // a CLONE of the board (so the returned board itself is left in its pristine // pre-replay layout for the caller to render + re-replay). Determinism: pure // function of the board + cells. Returns [{from,to,violated}]. function tutScript(board, rule, cells) { const sim = tutCloneBoard(board); const steps = []; for (const to of cells) { const from = { ...sim.pos[A.id] }; // SLICE2 LEVER D: the segment the predicate reads is the seat's CURRENT seg // (captured BEFORE the advance), so violated reflects the phase at the moment of // the move. record it so the demo can flash RED only in the forbidden phase. const segAt = clockSegOf(sim, A.id); const res = applyMove(sim, A.id, to, rule); // own-turn = each scripted replay step: advance the public clock exactly once // (no-op when the board has no clock -> non-phase demos byte-identical). advanceClock(sim, A.id, !!res.took); steps.push({ from, to: { x: to.x, y: to.y }, violated: !!res.violated, seg: segAt }); } return steps; } // deep-enough clone of a tutorial board for replay (mirrors the fields applyMove // + predicates touch). Independent of cloneSim (which targets the 2-seat live // board); tutorial boards have a single seat and may carry zone/Inst fields. function tutCloneBoard(st) { const cloneInst = (inst) => inst ? { A: new Set(inst.A), B: new Set(inst.B), C: new Set(inst.C) } : undefined; // SLICE2 LEVER D: carry the per-seat PHASE-CLOCK through the clone so the replay // sees + advances the same public clock (deep-copied so the clone's advance never // mutates the pristine returned board). Guarded so non-phase tut boards (no clock) // stay byte-identical. let clock; if (st.clock) { clock = {}; for (const s of Object.keys(st.clock)) clock[s] = { ...st.clock[s] }; } const out = { rule: st.rule, goal: st.goal, round: st.round, env: st.env, N: st.N, hazard: new Set(st.hazard), sacred: new Set(st.sacred), tar: new Set(st.tar || []), hazardInst: cloneInst(st.hazardInst), sacredInst: cloneInst(st.sacredInst), tarInst: cloneInst(st.tarInst), tokens: st.tokens.map(t => ({ ...t })), zone: st.zone ? { ...st.zone } : null, pos: { 0: { ...st.pos[0] } }, carry: { 0: st.carry[0] }, score: { 0: st.score[0] }, penalty: { 0: st.penalty[0] }, swap: { used: false }, penalty_amt: st.penalty_amt, fx: [], __seat__: A.id, }; if (clock) out.clock = clock; // SLICE2 LEVER A: carry the display-only GHOST companions + LANDMARKS through the // clone so the relational replay sees them (deep-copied — the clone's moves never // mutate the pristine returned board). Guarded so non-relational tut boards stay // byte-identical (tutBoard always sets ghosts:[]/landmarks:empty, harmless either way). if (st.ghosts) out.ghosts = st.ghosts.map(g => ({ ...g })); if (st.landmarks) out.landmarks = new Set(st.landmarks); // SLICE2 LEVER A2: carry the per-seat PUBLIC facing cue through the clone so the role // replay sees + advances it (deep-copied; the clone's moves never mutate the pristine // returned board). Guarded so non-role tut boards stay byte-identical (no st.facing). if (st.facing) { out.facing = {}; for (const s of Object.keys(st.facing)) out.facing[s] = { ...st.facing[s] }; } return out; } const tk = (p) => keyN(p, TUT_N); /* ===================== SLICE2 LEVER C — UNDER-DETERMINED DEMOS (spec §5) ====== The slice-1.5 demo OVER-determined the rule (it flashed RED on essentially every forbidden cell on the path), so a model could pattern-match the taboo "off the demo" without ever testing a hypothesis. Lever C makes the demo show only the MINIMUM disambiguating examples that still leave the rule IDENTIFIABLE IN PRINCIPLE (>=1 example per constraint), so MULTIPLE hypotheses survive the demo and the player must form + cheaply test them during PLAY (a compliant probe move reveals via heart/no-heart). Two engine pieces: (A) demoBudget / applyDemoBudget — caps the SHOWN violation examples to the minimum identifiable budget by TRUNCATING the scripted walk to its shortest connected PREFIX that still satisfies the budget. Truncating a prefix of a connected walk preserves the script-continuity invariant (each kept step's `from` is still the previous step's `to`) and NEVER adds a forced violation (it only DROPS trailing redundant examples), so demo-not-charged + the per-seat escapability invariants are preserved unchanged. (B) demoIdentifiable — the FAIRNESS validator: it asserts the rule is NOT unfalsifiable from {the (under-determined) demo} + at most K compliant PROBE moves. A rule a player could never disambiguate is a guess, not a deduction, so it is rejected. It replays the trimmed demo as evidence, intersects the surviving candidate set, then lets a probe budget of K additional compliant diagnostic moves further disambiguate; it passes iff the rule is UNIQUELY pinned within that budget. */ // constraint count a demo must witness at least once (the fairness "1 example per // constraint" floor). combos = #components; every other family is a single taboo. function _demoConstraintCount(rule) { const v = RULE_VARIANTS[rule]; if (v && v.base === 'combo' && Array.isArray(v.components)) return v.components.length; return 1; } // the set of "constraint ids" a violated step at (from->to) on board `st` witnesses, // for coverage accounting. For a combo this is the subset of components the step // violates; for any other family it is the rule itself. Reuses violates() (which // resolves any RULE_VARIANTS id, incl. components), never the rule id literal. function _violatedConstraintsAt(rule, from, to, st) { const v = RULE_VARIANTS[rule]; const out = []; if (v && v.base === 'combo' && Array.isArray(v.components)) { for (const c of v.components) if (violates(c, from, to, st)) out.push(c); } else if (violates(rule, from, to, st)) { out.push(rule); } return out; } // UNDER-DETERMINE the demo: return a copy of the tutorial artifact whose `steps` // are truncated to the shortest connected PREFIX that still (i) shows >=1 violation // PER CONSTRAINT, (ii) shows >=demoBudget.minViol total violations, and (iii) shows // >=demoBudget.minClean clean detour steps. minViol defaults to max(2, #constraints) // so the existing >=2-violation/>=2-clean legibility floor is preserved (we cap the // OVER-determined tail, never drop below the floor). Pure: replays on a CLONE so the // returned board stays pristine; demo-not-charged + escapability untouched (we only // drop shown steps). Falls back to the full steps if the budget is unmet (fail-open // to legibility — the artifact gate still requires >=2/>=2). function applyDemoBudget(rule, tut, demoBudget) { if (!tut || !Array.isArray(tut.steps) || !tut.steps.length) return tut; const cc = _demoConstraintCount(rule); const opts = demoBudget || {}; const minViol = Math.max(2, opts.minViol != null ? opts.minViol : cc); const minClean = Math.max(2, opts.minClean != null ? opts.minClean : 2); // replay the walk on a clone, attributing each violated step to its constraint(s), // and find the first prefix index that satisfies the budget. const sim = tutCloneBoard(tut.board); const covered = new Set(); let nViol = 0, nClean = 0; let cut = -1; for (let i = 0; i < tut.steps.length; i++) { const s = tut.steps[i]; const from = { ...sim.pos[A.id] }; if (s.violated) { for (const c of _violatedConstraintsAt(rule, from, s.to, sim)) covered.add(c); nViol++; } else { nClean++; } const res = applyMove(sim, A.id, s.to, rule); advanceClock(sim, A.id, !!res.took); // keep phase replay aligned (own-turn) if (nViol >= minViol && nClean >= minClean && covered.size >= cc) { cut = i; break; } } if (cut < 0) return tut; // budget unmet -> keep full demo (fail-open) const trimmed = tut.steps.slice(0, cut + 1); return Object.assign({}, tut, { steps: trimmed, demoBudget: { minViol, minClean, constraints: cc } }); } // FAIRNESS VALIDATOR (spec §5): a rule must be uniquely identifiable from {the // under-determined demo} + at most K PROBE moves. Returns true iff the rule is // FALSIFIABLE within that budget; false (REJECT) if it is unfalsifiable (a rule a // player could never disambiguate is a guess, not a deduction). Method: // (1) DEMO evidence — every demo step (clean OR violated) must be reproduced by a // surviving candidate (a candidate that mispredicts ANY demo step is eliminated). // (2) PROBE moves — the player then makes a SEQUENCE of up to K probe moves on the // demo board and OBSERVES the true rule's verdict (heart vs no-heart) on each. // A probe DISCRIMINATES a candidate iff the candidate's predicted verdict on that // probe differs from the true rule's observed one. The probe is the player's // chosen test: it may be a SUSPECTED-forbidden cell (a single heart is the price // of a deduction, and on the display-only demo board it costs nothing — the // demo-not-charged invariant is why a probe is free here), so probes are NOT // restricted to compliant moves — restricting them would make "truth forbids X // but candidate allows X" undetectable and spuriously fail identifiability. // Probes are SEQUENTIAL on an evolving clone so token-state-dependent rules // (ordered / avoid_biggest@top-k / nearest-rival) are testable across takes. // Passes iff, after the demo + <=K probes, ONLY the true rule survives. Greedy probe // selection (each step picks the most-discriminating move) is an upper bound on the // probe budget a rational player needs. Bounded + deterministic. Pure (replays on clones). // ROLE identifiability (spec 2026-06-17 §6): a role must be inducible from PLAYER- // AVAILABLE evidence ONLY — the demo CLIP (each shown move is in-character; a contrastive // beat shows a task-greedy move FORGONE) plus up to K IN-CHARACTER PROBE OWN-TURNS played // FROM THE REACHABLE demo end-state. Crucially NO omniscient teleport: the generic probe // model below teleports the focal to ANY cell and tests ANY move, which a player watching a // passive clip can never do — for source-dependent role preds that illegitimately pins // roles whose SHOWN MOTION is identical (the 2026-06-17 re-measurement found pursue== // intercept demos "pinned" only by teleport probes). Here a "probe own-turn" reveals the // LOCAL in-character SET at the current reachable cell (which moves the faithful newcomer // is willing to make — observable by watching it act); a candidate survives iff its local // in-character set matches the truth's at every visited state. Advance is an in-character // step toward the role's referent (the faithful player's own move) to reach a NEW state. function roleDemoIdentifiable(rule, pool, K) { const rv = RULE_VARIANTS[rule]; pool = (pool || ROLE_VARIANT_LIST).filter(r => RULE_VARIANTS[r] && RULE_VARIANTS[r].role); if (pool.indexOf(rule) < 0) pool.push(rule); const tut = buildRoleDemo(rule); if (!tut || !tut.steps.length) return false; // (1) DEMO CLIP: a candidate survives iff every SHOWN (taken) move is in-character under // it — the clip shows the newcomer moving in-character, so a candidate that would charge // a heart on a shown move is eliminated. (Weak evidence on purpose: the passive clip does // not reveal the full neighborhood, only the moves actually taken.) let survivors = pool.filter(cand => { const sim = tutCloneBoard(tut.board); sim.facing = {}; for (const s of tut.steps) { sim.ghosts = s.ghosts; // replay the SAME moving scene the demo showed sim.__seat__ = A.id; const from = { ...sim.pos[A.id] }; if (outOfCharacter(cand, from, s.to, sim)) return false; applyMove(sim, A.id, s.to, cand); } return true; }); if (survivors.indexOf(rule) < 0) return false; // truth must survive its own demo // (2) K IN-CHARACTER PROBE OWN-TURNS from the demo end-state: at each reachable cell the // player observes the LOCAL in-character set (consistentMoves) and eliminates any survivor // whose set differs from the truth's; then advances one in-character step (toward the ref) // to a NEW reachable state. Local only — no teleport. const st = tutCloneBoard(tut.board); st.facing = {}; for (const s of tut.steps) { st.ghosts = s.ghosts; st.__seat__ = A.id; applyMove(st, A.id, s.to, rule); } // probes happen AFTER the clip: the scene is frozen at its final frame (already set above). const n = st.N || N; const keyset = (cm) => [...cm].sort().join(','); let used = 0; while (survivors.length > 1 && used < K) { st.__seat__ = A.id; const truthSet = keyset(consistentMoves(st, A.id, rule)); survivors = survivors.filter(cand => cand === rule || keyset(consistentMoves(st, A.id, cand)) === truthSet); used++; // advance one in-character own-turn toward the referent (faithful player's move). const from = { ...st.pos[A.id] }; const ok = consistentMoves(st, A.id, rule); const ref = roleRef(st, A.id, rv.ref); const goalPt = ref ? (ref.x != null ? ref : (ref.a || ref.chaser)) : from; let pick = { x: from.x, y: from.y, key: 'stay' }, pd = Infinity; for (const c of _roleCandidates(from)) { if (!ok.has(c.key)) continue; if (!inbN(c, n) || (st.wall && st.wall.has(keyN(c, n)))) continue; const d = goalPt ? (Math.abs(c.x - goalPt.x) + Math.abs(c.y - goalPt.y)) : 0; if (d < pd) { pd = d; pick = c; } } applyMove(st, A.id, { x: pick.x, y: pick.y }, rule); } return survivors.length === 1; } function demoIdentifiable(rule, opts) { opts = opts || {}; // ROLE rules use PLAYER-evidence identifiability (demo clip + K in-character probe // own-turns from the reachable end-state), NOT the omniscient teleport-probe model below. if (RULE_VARIANTS[rule] && RULE_VARIANTS[rule].role) return roleDemoIdentifiable(rule, opts.pool, opts.K != null ? opts.K : 4); // K = probe-move budget. Default 4: the deepest family (the `ordered` rule) must be // disambiguated from BOTH the 3 avoid_biggest grades (one mid-token probe) AND the 3 // terrain+ordered combos (one terrain probe per terrain category), so 4 probes is the // worst-case fairness floor over the whole 25-rule pool. Every rule is uniquely // identifiable from {under-determined demo + <=4 compliant probe moves}. const K = opts.K != null ? opts.K : 4; const tut = applyDemoBudget(rule, buildTutorial(rule), opts.demoBudget); if (!tut || !tut.steps.length) return false; // no demo -> not identifiable // candidate pool: the family pool the rule must be disambiguated WITHIN. Default = // the full union so we never under-test (a candidate from another family that the // demo + probes cannot rule out is a real ambiguity). const pool = (opts.pool && opts.pool.length) ? opts.pool : VARIANT_LIST.concat(DELIVER_VARIANT_LIST, PHASE_VARIANT_LIST, RELATIONAL_VARIANT_LIST) .filter((v, i, a) => a.indexOf(v) === i && RULE_VARIANTS[v]); if (pool.indexOf(rule) < 0) pool.push(rule); // (1) DEMO evidence: a candidate survives iff it reproduces EVERY demo step's // violated flag. Replay each candidate on its OWN fresh demo board clone. const demoConsistent = (cand) => { const sim = tutCloneBoard(tut.board); for (const s of tut.steps) { const from = { ...sim.pos[A.id] }; const cv = violates(cand, from, s.to, sim); if (!!cv !== !!s.violated) return false; const r2 = applyMove(sim, A.id, s.to, cand); advanceClock(sim, A.id, !!r2.took); } return true; }; let survivors = pool.filter(demoConsistent); if (survivors.indexOf(rule) < 0) return false; // true rule must survive its OWN demo if (survivors.length === 1) return true; // demo alone pins it (still fair) // (2) PROBE moves: the player may run up to K probe queries on the demo board — each // a "step from cell c to adjacent d, observe heart/no-heart" — to disambiguate. We // ask the in-principle question (does a budget of <=K probes EXIST that pins the // rule), so probes may start from ANY reachable cell (the player navigates there via // compliant moves first — those navigation steps are free and do not count, only the // K diagnostic probes do). To exercise token-state-dependent rules (ordered / // avoid_biggest@top-k / nearest-rival), a probe may also be evaluated AFTER a // suspected-clean prefix of takes; we model this by enumerating probes on BOTH the // pristine board AND boards with the lowest-value tokens pre-removed (a player taking // safe low tokens first, then probing a high one). Each probe DISCRIMINATES a // candidate iff its verdict differs from the true rule's. Greedy set-cover over K. // PROBE CONTEXTS: the player tests hypotheses by probing the LIVE PLAY board — which // (unlike the lean demo board) always carries tokens AND terrain AND, for relational // rules, real rivals/landmarks — so a conjunct the demo board cannot express (e.g. // the `ordered` half of a terrain+ordered combo, untestable on a token-free terrain // demo) IS testable in play. We model that surface with a few fixed-seed makeBoard // contexts (deterministic; the player's in-play probe substrate), plus the demo board // itself and token-depleted variants of it (a low-first sweep exposing high-grade / // ordered / nearest probes). The probe MOVE may be any legal step (the player may // deliberately test a SUSPECTED-forbidden cell once — free on the display-only board // and a single heart in play, the price of a deduction); its verdict reveals the // true rule and eliminates any candidate predicting a different verdict. const N0 = tut.board.N; const contexts = [tutCloneBoard(tut.board)]; { const base = tutCloneBoard(tut.board); const lows = base.tokens.filter(t => t.alive).slice().sort((a, b) => a.v - b.v); for (let r = 1; r <= Math.min(lows.length, 4); r++) { const c = tutCloneBoard(tut.board); const drop = new Set(lows.slice(0, r).map(t => keyN(t, N0))); for (const t of c.tokens) if (drop.has(keyN(t, N0))) t.alive = false; contexts.push(c); } } // live play-board probe substrate (fixed seeds -> deterministic; harvest goal so all // token/terrain/ordered conjuncts are present and probeable, mirroring real play). for (const seed of (opts.probeSeeds || [7, 11, 3])) { contexts.push(makeBoard(rule, 'harvest_max', seed, 1, ENV_PRESETS.E1)); } // enumerate every probe (context, from, to) move over each context's board. const probes = []; for (const ctx of contexts) { const Nc = ctx.N || N0; for (let y = 0; y < Nc; y++) for (let x = 0; x < Nc; x++) { for (const d of DIRS) { const to = { x: x + d.x, y: y + d.y }; if (!inbN(to, Nc)) continue; probes.push({ ctx, from: { x, y }, to }); } } } // precompute each probe's discriminated set vs the true rule (set-cover items). // violates() is a PURE predicate read, so we evaluate it in place after seating the // moving seat at the probe's `from` (st.__seat__/pos[A.id] is the seam every pred // reads). The token at `to` (if any) makes a take-probe; the cell type makes a // step-probe — both reveal the true rule's verdict at that cell. let remaining = survivors.filter(c => c !== rule); const probeKills = probes.map(p => { const ctx = p.ctx; ctx.__seat__ = A.id; ctx.pos[A.id] = { ...p.from }; const trueV = !!violates(rule, p.from, p.to, ctx); return new Set(remaining.filter(c => !!violates(c, p.from, p.to, ctx) !== trueV)); }); // greedy K-budget set cover over the ambiguous candidates. let used = 0; while (remaining.length && used < K) { let bestIdx = -1, bestKill = 0; for (let i = 0; i < probes.length; i++) { let k = 0; for (const c of remaining) if (probeKills[i].has(c)) k++; if (k > bestKill) { bestKill = k; bestIdx = i; } } if (bestIdx < 0 || bestKill === 0) break; // no probe disambiguates -> stuck remaining = remaining.filter(c => !probeKills[bestIdx].has(c)); used++; } return remaining.length === 0; // uniquely pinned within demo + K probes } function buildTutorial(rule) { const base = (RULE_VARIANTS[rule] && RULE_VARIANTS[rule].base) || rule; const param = RULE_VARIANTS[rule] && RULE_VARIANTS[rule].param; // LEVER C: every family's tutorial is routed through applyDemoBudget so the demo // shows the MINIMUM identifiable set of violation examples (under-determined), not // the over-determined full walk. applyDemoBudget only TRUNCATES the connected walk // (continuity + demo-not-charged + escapability preserved) and falls open to the // full walk if the budget is unmet, so the >=2-violation/>=2-clean artifact gate holds. if (base === 'combo') return applyDemoBudget(rule, tutCombo(rule)); if (base === 'phase') return applyDemoBudget(rule, tutPhase(rule)); if (base === 'relational') return applyDemoBudget(rule, tutRelational(rule)); if (base === 'role') return applyDemoBudget(rule, buildRoleDemo(rule)); if (base === 'avoid_dark' || base === 'avoid_hatch' || base === 'avoid_tar') return applyDemoBudget(rule, tutTerrain(rule, base, param)); if (base === 'avoid_biggest') return applyDemoBudget(rule, tutBiggest(rule, param)); if (base === 'carry_limit') return applyDemoBudget(rule, tutCarryLimit(rule)); if (base === 'keep_distance') return applyDemoBudget(rule, tutKeepDistance(rule)); if (base === 'ordered') return applyDemoBudget(rule, tutOrdered(rule)); // unknown rule: empty (no tutorial). Callers gate on steps.length. return { board: tutBoard(rule), steps: [], mechanism: 'none' }; } // V3 COMBO tutorial (§2b): a SINGLE education board carrying BOTH component // mechanisms (terrain + token grades + dark halo as needed), with a scripted walk // that INTERLEAVES a violation of component A, a clean detour, a violation of // component B, and a clean detour — so the REAL returned artifact has >=2 violated // steps (one per mechanism) AND >=2 clean detour steps. The `violated` flags are // computed by REPLAY (tutScript) against the combo's OR-of-violations predicate, // so legibility is owned by the engine (not a curated seed). Each combo lays the // mechanisms on disjoint rows so the two violations + the clean detours are // independent and reproducible. The shipped combos all pair a TERRAIN component // (avoid_dark@A/@B / avoid_hatch@A) with `ordered`; the layout below covers that // family (terrain dip + an out-of-order high take), generic over the terrain // category AND sub-instance param read off the component id. function tutCombo(rule) { const comps = RULE_VARIANTS[rule].components; const st = tutBoard(rule); const baseOf = (id) => (RULE_VARIANTS[id] && RULE_VARIANTS[id].base) || id; const paramOf = (id) => (RULE_VARIANTS[id] && RULE_VARIANTS[id].param) || 'A'; const bases = comps.map(baseOf); const has = (b) => bases.indexOf(b) !== -1; // The two hand-tuned 2-conjunct layouts below are gated on comps.length===2 so a // 3-conjunct combo that merely CONTAINS one of these pairs (e.g. dark+biggest+ // ordered) does NOT route here and silently drop its third constraint — it falls // through to the GENERAL N-ARY COMPOSER, which lays a dedicated band per component. const pair = comps.length === 2; // ----- terrain (avoid_dark/avoid_hatch @A|@B|@C) + ordered: two binding cells on // row y=1 the path dips into (violations) with row-0 clean detours; the binding // sub-instance param is resolved from the terrain component id (so @B binds the B // sub-instance, etc.), with the OTHER sub-instances present off-path (C1). if (pair && (has('avoid_dark') || has('avoid_hatch')) && has('ordered')) { const terrIsDark = has('avoid_dark'); const terrId = comps.find(c => baseOf(c) === (terrIsDark ? 'avoid_dark' : 'avoid_hatch')); const p = paramOf(terrId); const bindCat = terrIsDark ? st.hazard : st.sacred; const bindInst = terrIsDark ? st.hazardInst : st.sacredInst; const decoyCat = terrIsDark ? st.sacred : st.hazard; const f1 = { x: 2, y: 1 }, f2 = { x: 4, y: 1 }; for (const c of [f1, f2]) { bindCat.add(tk(c)); bindInst[p].add(tk(c)); } // present-but-non-binding sub-instances + the other terrain category (C1: // every category/sub-instance present so the binding one cannot be read off it). const others = ['A', 'B', 'C'].filter(q => q !== p); bindInst[others[0]].add(tk({ x: 1, y: 5 })); bindCat.add(tk({ x: 1, y: 5 })); bindInst[others[1]].add(tk({ x: 5, y: 5 })); bindCat.add(tk({ x: 5, y: 5 })); decoyCat.add(tk({ x: 3, y: 5 })); // `ordered` tokens: a persistent low ANCHOR keeps the min low so a HIGH take is // forbidden, and the genuine min is the clean take. Lows on row y=3, high on y=4. st.tokens.push({ x: 1, y: 3, v: 1, alive: true, guard: false }); // the live min st.tokens.push({ x: 2, y: 3, v: 2, alive: true, guard: false }); // next min st.tokens.push({ x: 2, y: 4, v: 9, alive: true, guard: true }); // high (ordered-forbidden) // PATH: dip into terrain f1 (TERRAIN VIOLATION) -> step out (clean) -> reach f2 // and dip in (2nd TERRAIN VIOLATION) -> route to the token rows -> take the high // token 2-on-row-3 while the live min is 1 (ORDERED VIOLATION) -> take the min 1 // (clean). Interleaves both mechanisms; >=2 violations + >=2 clean detours. const path = [ { x: 1, y: 0 }, // clean { x: 2, y: 0 }, // clean (above f1) { x: 2, y: 1 }, // TERRAIN VIOLATION (dip into f1) { x: 2, y: 0 }, // clean detour (out) { x: 3, y: 0 }, // clean { x: 4, y: 0 }, // clean (above f2) { x: 4, y: 1 }, // TERRAIN VIOLATION (dip into f2) [2nd terrain viol] { x: 4, y: 2 }, // clean (no token) { x: 3, y: 2 }, // clean { x: 2, y: 2 }, // clean { x: 2, y: 3 }, // take 2 — ordered: min alive is 1 -> 2>1 VIOLATION { x: 1, y: 3 }, // take 1 — now the live min -> clean detour ]; return { board: st, steps: tutScript(st, rule, path), mechanism: 'combo', components: comps }; } if (pair && has('keep_distance') && has('avoid_biggest')) { // dark cell drives the keep_distance halo; high token grades drive avoid_biggest. const dark = { x: 3, y: 4 }; st.hazard.add(tk(dark)); st.hazardInst.A.add(tk(dark)); // avoid_biggest@top1 tokens: distinct descending grades on row y=1 (the top is // forbidden) + low clean tokens on row y=2. st.tokens.push({ x: 1, y: 1, v: 20, alive: true, guard: true }); // current max st.tokens.push({ x: 2, y: 1, v: 18, alive: true, guard: true }); // becomes max after st.tokens.push({ x: 1, y: 2, v: 2, alive: true, guard: false }); // low clean st.tokens.push({ x: 2, y: 2, v: 1, alive: true, guard: false }); // low clean // PATH from (0,0): take 20 (avoid_biggest VIOLATION, current max) -> take 18 // (now the max -> 2nd avoid_biggest VIOLATION) -> route DOWN toward the dark // cell so a step lands in its halo (keep_distance VIOLATION) -> retreat to a // dist>=2 cell (clean) -> take a low clean token. const path = [ { x: 1, y: 0 }, // clean (no token) { x: 1, y: 1 }, // take 20 (current max) -> BIGGEST VIOLATION { x: 2, y: 1 }, // take 18 (now max) -> BIGGEST VIOLATION [2nd] { x: 2, y: 2 }, // take 1 (low) -> clean detour { x: 1, y: 2 }, // take 2 (low) -> clean detour [2nd clean] { x: 1, y: 3 }, // dist to dark (3,4) = 3 -> clean { x: 2, y: 3 }, // dist to dark = 2 (== d) -> clean (boundary) { x: 3, y: 3 }, // dist to dark = 1 -> KEEP_DISTANCE VIOLATION (halo) { x: 2, y: 3 }, // retreat to dist 2 -> clean detour ]; return { board: st, steps: tutScript(st, rule, path), mechanism: 'combo', components: comps }; } // GENERAL N-ARY COMPOSER (LEVER B, §2): for ANY 2-3 component combo not covered by // the two hand-tuned layouts above, give EACH component its own 2-row band (a // violation row + a clean detour row) on DISJOINT rows, seed that band per the // component's base, and walk a connected path that scores >=2 VIOLATIONS PER // COMPONENT and >=2 clean detours per component. Column x=0 is kept clear of every // mechanism so vertical travel between bands is always clean (connector steps). // Bands start at y=1 (row 0 is the spawn row / a clean lane). Each band is 3 rows // tall (vy = violation row, dy = detour row, plus the connector enters on vy-1==the // previous detour or spawn). This composer covers the families that EXIST in the // engine today (terrain dark/hatch/tar, avoid_biggest, ordered); a component whose // base is not yet implemented here (e.g. a future relational/phase variant) makes // the band builder return null and the whole combo falls through to the fail-closed // empty artifact, so callers gate on steps.length — the Test-phase agent adds the // relational/phase band builders (one `case` each) with no other change. { const stG = tutBoard(rule); // each band occupies rows [bandTop, bandTop+1]; violation cells on bandTop, // clean-detour cells on bandTop+1; the seat enters/leaves a band via column x=0. const bands = []; // [{ comp, vy, dy, viol:[{x,y}], detour:[{x,y}] }] let bandTop = 1; let ok = true; let highAnchorPlaced = false; // an ordered/biggest reference anchor (shared low) for (const c of comps) { const b = baseOf(c); const vy = bandTop, dy = bandTop + 1; const band = { comp: c, vy, dy, viol: [], detour: [] }; if (b === 'avoid_dark' || b === 'avoid_hatch' || b === 'avoid_tar') { const p = paramOf(c); const cat = b === 'avoid_dark' ? stG.hazard : b === 'avoid_hatch' ? stG.sacred : stG.tar; const inst = b === 'avoid_dark' ? stG.hazardInst : b === 'avoid_hatch' ? stG.sacredInst : stG.tarInst; // two binding terrain cells on the violation row (x=2,4) + present-but- // non-binding sub-instances elsewhere (C1: every sub-instance present). for (const x of [2, 4]) { cat.add(tk({ x, y: vy })); inst[p].add(tk({ x, y: vy })); band.viol.push({ x, y: vy }); } const others = ['A', 'B', 'C'].filter(q => q !== p); inst[others[0]].add(tk({ x: 6, y: vy })); cat.add(tk({ x: 6, y: vy })); // detour cells are plain empty cells on the detour row (clean to enter). band.detour.push({ x: 2, y: dy }, { x: 4, y: dy }); } else if (b === 'avoid_biggest') { // two forbidden HIGH tokens on the violation row (descending so each is the // current max when taken) + two low clean tokens on the detour row. stG.tokens.push({ x: 2, y: vy, v: 20, alive: true, guard: true }); stG.tokens.push({ x: 4, y: vy, v: 18, alive: true, guard: true }); band.viol.push({ x: 2, y: vy }, { x: 4, y: vy }); stG.tokens.push({ x: 2, y: dy, v: 1, alive: true, guard: false }); stG.tokens.push({ x: 4, y: dy, v: 1, alive: true, guard: false }); band.detour.push({ x: 2, y: dy }, { x: 4, y: dy }); } else if (b === 'ordered') { // a persistent low ANCHOR keeps the live min low so HIGH takes are forbidden. if (!highAnchorPlaced) { stG.tokens.push({ x: 6, y: 0, v: 1, alive: true, guard: true }); highAnchorPlaced = true; } stG.tokens.push({ x: 2, y: vy, v: 9, alive: true, guard: true }); // non-min -> forbidden stG.tokens.push({ x: 4, y: vy, v: 8, alive: true, guard: true }); // non-min -> forbidden band.viol.push({ x: 2, y: vy }, { x: 4, y: vy }); // clean detour: take the live MIN (value 1) on the detour row; place two so // BOTH detour steps are compliant min-takes (the anchor at (6,0) is never on // the path, so the min stays 1 until these are taken). stG.tokens.push({ x: 2, y: dy, v: 1, alive: true, guard: false }); stG.tokens.push({ x: 4, y: dy, v: 1, alive: true, guard: false }); band.detour.push({ x: 2, y: dy }, { x: 4, y: dy }); } else { // unimplemented family (e.g. future relational/phase): cannot lay a band yet. ok = false; break; } bands.push(band); bandTop = dy + 1; // next band on fresh rows (disjoint mechanisms) } if (ok && bands.length === comps.length) { // walk: for each band, enter via x=0 at vy, then for each (viol,detour) pair do // [viol step][return to x=0 detour-lane clean]. We interleave so each component // contributes >=2 violations and >=2 clean detours. Connector moves stay on x=0 // / row 0 (kept clear). Build an explicit cell path with column-0 travel. const path = []; let cur = { x: 0, y: 0 }; const stepTo = (x, y) => { path.push({ x, y }); cur = { x, y }; }; // helper: move along x=0 to a target row (clean vertical lane). const travelToRow = (y) => { while (cur.y !== y) { stepTo(0, cur.y + (y > cur.y ? 1 : -1)); } }; for (const band of bands) { // VIOLATION 1 (x=2 on the violation row): enter via x=0 -> x=1 -> x=2. travelToRow(band.vy); stepTo(1, band.vy); // clean (x=1 lane kept clear) stepTo(2, band.vy); // VIOLATION 1 (component fires) // DETOUR 1: drop straight down to the detour row at x=2 (clean). stepTo(2, band.dy); // CLEAN detour 1 (min-take / empty / low token) // VIOLATION 2 (x=4 on the violation row): travel right on the detour row to // x=4 (clean), step UP into the 2nd violation cell. stepTo(3, band.dy); // clean (x=3 detour-lane) stepTo(4, band.dy); // CLEAN detour 2 (min-take / empty / low token) stepTo(4, band.vy); // VIOLATION 2 (step UP onto the 2nd viol cell) // return to the x=0 lane along the detour row (clean) for the connector down. stepTo(4, band.dy); // clean (drop back to detour row) stepTo(3, band.dy); // clean stepTo(2, band.dy); // clean stepTo(1, band.dy); // clean stepTo(0, band.dy); // back to x=0 lane (clean connector start) } // sanity: replay computes the actual violated flags; we still return whatever // tutScript derives (engine owns legibility). The layout guarantees >=2 viol + // >=2 clean per band by construction. const steps = tutScript(stG, rule, path).filter(s => Math.abs(s.from.x - s.to.x) + Math.abs(s.from.y - s.to.y) === 1); // drop any noop return { board: stG, steps, mechanism: 'combo', components: comps }; } } // fallback (unshipped combo shape with an unimplemented component family): return // empty so callers gate on steps.length (fail-closed). return { board: st, steps: [], mechanism: 'combo', components: comps }; } // TERRAIN (avoid_dark/hatch/tar @A/@B/@C). Lay the BINDING sub-instance's forbidden // cells along row y=1 and DECOYS (the OTHER terrain categories, plus the non-binding // sub-instances of the SAME category) elsewhere — so the layout never reveals which // is binding (C1), yet the scripted path steps INTO 2 binding cells (violations) and // AROUND them via row y=0 (clean detours). Generic over the THREE terrain categories // (hazard/sacred/tar): the binding category's layer is resolved from `base`; the // other two categories supply rule-invariant decoys. Determinism: fixed coordinates. function tutTerrain(rule, base, param) { const st = tutBoard(rule); const p = param || 'A'; const catOf = (b) => b === 'avoid_dark' ? { cat: st.hazard, inst: st.hazardInst } : b === 'avoid_hatch' ? { cat: st.sacred, inst: st.sacredInst } : { cat: st.tar, inst: st.tarInst }; const bind = catOf(base); const bindSet = bind.inst[p], bindCat = bind.cat; // the two NON-binding terrain categories, present as decoys on every board. const otherBases = ['avoid_dark', 'avoid_hatch', 'avoid_tar'].filter(b => b !== base); const decoy0 = catOf(otherBases[0]), decoy1 = catOf(otherBases[1]); // two binding forbidden cells the path will step into. const f1 = { x: 2, y: 1 }, f2 = { x: 4, y: 1 }; for (const c of [f1, f2]) { bindSet.add(tk(c)); bindCat.add(tk(c)); } // non-binding sub-instances of the SAME category (present on every board so the // active param can't be read off the layout); placed off the path (y>=3). const others = ['A', 'B', 'C'].filter(q => q !== p); const otherCells = [{ x: 1, y: 4 }, { x: 5, y: 4 }]; for (let i = 0; i < others.length; i++) { const c = otherCells[i]; bind.inst[others[i]].add(tk(c)); bindCat.add(tk(c)); } // decoys of the OTHER TWO terrain categories (always present, never binding here) // so all three terrain block types appear on every tutorial board (C1 + ARC look). decoy0.cat.add(tk({ x: 3, y: 4 })); decoy0.inst.A.add(tk({ x: 3, y: 4 })); decoy1.cat.add(tk({ x: 0, y: 5 })); decoy1.inst.B.add(tk({ x: 0, y: 5 })); // PATH: from (0,0) detour over row 0 across the forbidden cells (clean), dip // INTO each forbidden cell (violation), and step back out (clean). Two dips = // two violations; the row-0 passes + step-outs are the clean detours. const cells = [ { x: 1, y: 0 }, // clean (row-0 detour begins) { x: 2, y: 0 }, // clean (above f1) { x: 2, y: 1 }, // VIOLATION (step into f1) { x: 2, y: 0 }, // clean detour (step back out) { x: 3, y: 0 }, // clean { x: 4, y: 0 }, // clean (above f2) { x: 4, y: 1 }, // VIOLATION (step into f2) { x: 4, y: 0 }, // clean detour (step back out) ]; return { board: st, steps: tutScript(st, rule, cells), mechanism: 'terrain' }; } // BIGGEST (@top1/@top2/@top3). Place tokens of distinct descending values along // the newcomer's row so the FORBIDDEN grade (top-k distinct) sits on the path. As // tokens are taken the live max shifts, so violations are scripted on tokens that // are forbidden AT THE MOMENT of the take, and the lowest tokens are clean takes. function tutBiggest(rule, param) { const st = tutBoard(rule); const k = param === 'top3' ? 3 : param === 'top2' ? 2 : 1; // values: enough distinct grades that >=2 forbidden takes survive the depletion. // Forbidden = top-k distinct of CURRENTLY-alive tokens. Use distinct values so // each take cleanly drops one grade. Layout row y=1, low decoys on row y=3. // place k+2 high tokens (forbidden initially) and 3 low clean tokens. const highVals = [20, 18, 16, 14]; // descending distinct; top-(k) are forbidden const highCells = [{ x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 4, y: 1 }]; for (let i = 0; i < highVals.length; i++) st.tokens.push({ x: highCells[i].x, y: highCells[i].y, v: highVals[i], alive: true, guard: true }); const lowVals = [3, 2, 1]; const lowCells = [{ x: 1, y: 3 }, { x: 2, y: 3 }, { x: 3, y: 3 }]; for (let i = 0; i < lowVals.length; i++) st.tokens.push({ x: lowCells[i].x, y: lowCells[i].y, v: lowVals[i], alive: true, guard: false }); // PATH (Manhattan-adjacent steps from (0,0)): take the two HIGHEST tokens first // (each is in the top-k forbidden grade at the moment taken -> 2 violations), // then route DOWN to the low row and take two LOW tokens (always below the // top-k cutoff -> clean detours/takes). const cells = [ { x: 1, y: 0 }, // clean step (no token) { x: 1, y: 1 }, // take 20 (current max, in top-k) -> VIOLATION { x: 2, y: 1 }, // take 18 (now in top-k of remaining) -> VIOLATION { x: 2, y: 2 }, // clean step toward low row (no token) { x: 2, y: 3 }, // take 2 (well below top-k) -> clean { x: 1, y: 3 }, // take 3 (below top-k) -> clean ]; return { board: st, steps: tutScript(st, rule, cells), mechanism: 'biggest', _topK: k }; } // CARRY_LIMIT (deliver context). Forbidden = collecting a token while already // carrying. Script: pick up a token (clean), then grab MORE while carrying // (violations), with a deliver-to-zone empty-the-hands detour reading as clean. function tutCarryLimit(rule) { const st = tutBoard(rule, 'deliver_to_zone'); st.zone = { x: 0, y: 0 }; // newcomer starts ON the zone (deliver = empty hands) // tokens along row y=1; first pickup is clean (carry==0), subsequent are // forbidden (carry>0). const cells = [{ x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 4, y: 1 }]; const vals = [4, 5, 6, 7]; for (let i = 0; i < cells.length; i++) st.tokens.push({ x: cells[i].x, y: cells[i].y, v: vals[i], alive: true, guard: i > 0 }); // a token reachable AFTER delivering (hands empty again) -> a clean late take. st.tokens.push({ x: 1, y: 2, v: 3, alive: true, guard: false }); // PATH: start on zone (carry 0). Take t0 (clean, carry now>0). Take t1, t2 while // carrying (VIOLATIONS). Return to zone to DELIVER (empties hands, clean), then // take a token with empty hands (clean). const path = [ { x: 1, y: 0 }, // clean step (no token; carry 0) { x: 1, y: 1 }, // take 4 (carry 0 -> clean) { x: 2, y: 1 }, // take 5 (carrying -> VIOLATION) { x: 3, y: 1 }, // take 6 (carrying -> VIOLATION) { x: 3, y: 0 }, // clean step back toward zone (no token) { x: 2, y: 0 }, // clean { x: 1, y: 0 }, // clean { x: 0, y: 0 }, // DELIVER at zone (empties hands; no token -> clean) { x: 0, y: 1 }, // clean step (empty hands) { x: 1, y: 1 }, // token gone; step (clean) { x: 1, y: 2 }, // take 3 with empty hands -> clean ]; return { board: st, steps: tutScript(st, rule, path), mechanism: 'carry_limit' }; } // KEEP_DISTANCE. Forbidden = entering any cell within Manhattan d of a dark cell // (the halo), d from RULE_VARIANTS.keep_distance.param. Place a dark cell, walk // the path THROUGH its halo (violations) and AROUND it at distance >= d (clean). function tutKeepDistance(rule) { const st = tutBoard(rule); const d = (RULE_VARIANTS.keep_distance && RULE_VARIANTS.keep_distance.param) || 2; const dark = { x: 3, y: 3 }; st.hazard.add(tk(dark)); st.hazardInst.A.add(tk(dark)); // PATH from (0,0): approach the dark cell so two consecutive steps land inside // the halo (dist < d) -> 2 violations; then retreat to dist >= d cells -> clean // detours. With d=2 the halo is all cells with Manhattan dist 1 (the dark cell // itself is dist 0, also forbidden, but we never step onto it). (3,1) is dist 2 // (clean), (3,2) is dist 1 (violation), (2,3) is dist 1 (violation). const cells = [ { x: 1, y: 0 }, // dist 5 -> clean { x: 2, y: 0 }, // dist 4 -> clean { x: 3, y: 0 }, // dist 3 -> clean { x: 3, y: 1 }, // dist 2 (== d, NOT < d) -> clean (boundary detour) { x: 3, y: 2 }, // dist 1 -> VIOLATION (entered halo) { x: 3, y: 1 }, // back to dist 2 -> clean detour { x: 2, y: 1 }, // dist 3 -> clean { x: 2, y: 2 }, // dist 2 -> clean { x: 2, y: 3 }, // dist 1 -> VIOLATION (entered halo) { x: 2, y: 2 }, // back to dist 2 -> clean detour ]; return { board: st, steps: tutScript(st, rule, cells), mechanism: 'keep_distance', _d: d }; } // ORDERED. Forbidden = taking any token whose value is ABOVE the current min of // alive tokens (must collect lowest-first). Script: grab HIGH tokens out of order // (violations), then grab the genuine lowest (clean). function tutOrdered(rule) { const st = tutBoard(rule); // tokens with distinct values; a persistent LOW anchor keeps the min low so // high takes are forbidden, and the clean takes are the min-at-the-moment. // Layout: highs on row y=1, lows on row y=3. st.tokens.push({ x: 1, y: 1, v: 9, alive: true, guard: true }); st.tokens.push({ x: 2, y: 1, v: 8, alive: true, guard: true }); st.tokens.push({ x: 1, y: 3, v: 1, alive: true, guard: false }); st.tokens.push({ x: 2, y: 3, v: 2, alive: true, guard: false }); st.tokens.push({ x: 3, y: 3, v: 3, alive: true, guard: false }); // PATH: take 9 then 8 (each above the live min 1 -> 2 VIOLATIONS), then go to // the low row and take in ASCENDING order 1,2 (each is the live min -> clean). const cells = [ { x: 1, y: 0 }, // clean step (no token) { x: 1, y: 1 }, // take 9 (min alive is 1 -> 9>1 -> VIOLATION) { x: 2, y: 1 }, // take 8 (min alive still 1 -> VIOLATION) { x: 2, y: 2 }, // clean step toward low row (no token) { x: 1, y: 2 }, // clean step { x: 1, y: 3 }, // take 1 (is the live min -> clean) { x: 2, y: 3 }, // take 2 (now the live min -> clean) ]; return { board: st, steps: tutScript(st, rule, cells), mechanism: 'ordered' }; } // SLICE2 LEVER A — RELATIONAL tutorial via GHOST COMPANIONS (spec §4). A SOLO // newcomer can't demonstrate a relational rule (there is no rival on the tutorial // board), so we seed 1-2 STATIC display-only GHOST companions at FIXED rule-invariant // coordinates (C1: placement depends only on the TUT_N layout, never the active rule) // and the relational predicate falls back to those ghosts (rivalAnchors). The scripted // solo walk steps so it ENTERS a cell that violates the relation to a ghost (RED) and // DETOURS around the ghosts on the clean steps — >=2 violations + >=2 clean detours, // like every other tut mechanism, so the relation is inducible from the demo alone. // On the LIVE board there are NO ghosts: the SAME predicate reads the REAL party seats. function tutRelational(rule) { const v = RULE_VARIANTS[rule]; const param = v.param; const st = tutBoard(rule); if (param === 'los') { // avoid_landmark_los: seed landmark cells; the walk steps ONTO a landmark // row/col (RED) and OFF every landmark line (clean). Landmarks at fixed cells // off the start row/col so the start (and the clean lane) are compliant. const L1 = { x: 3, y: 4 }; // landmark: forbids row 4 + col 3 st.landmarks.add(tk(L1)); // walk: stay on row 0 lane (clean, off row 4 + col 3) then dip onto col 3 (RED), // step off (clean), onto row 4 (RED), step off (clean). const cells = [ { x: 1, y: 0 }, // clean (row 0, col 1 — off every landmark line) { x: 2, y: 0 }, // clean { x: 3, y: 0 }, // RED: shares COL 3 with landmark { x: 4, y: 0 }, // clean (off col 3, off row 4) { x: 5, y: 0 }, // clean { x: 5, y: 4 }, // RED: shares ROW 4 with landmark (col 5 is clean, row 4 fires) ]; // Add a clean exit off row 4 + col 3, then a 2nd col-3 RED, then a clean detour // (so the demo has >=2 RED on distinct landmark lines + >=2 clean exits). cells.push({ x: 5, y: 5 }); // clean (off row 4, off col 3) cells.push({ x: 3, y: 5 }); // RED: shares COL 3 with landmark cells.push({ x: 2, y: 5 }); // clean detour return { board: st, steps: tutScript(st, rule, cells), mechanism: 'relational', param, landmarks: [...st.landmarks] }; } // avoid_adjacent_rival (and avoid_token_nearest_rival): seed ghosts; the walk // steps adjacent to a ghost (RED) and detours (clean). const G1 = { x: 3, y: 3 }, G2 = { x: 5, y: 2 }; st.ghosts = [ { x: G1.x, y: G1.y, vid: 1 }, { x: G2.x, y: G2.y, vid: 2 } ]; if (param === 'tok') { // avoid_token_nearest_rival: the single alive token nearest to the nearest ghost // (ties: lowest key) is forbidden to TAKE; place tokens so the selected one is the // UNIQUE nearest, then after it is removed a 2nd token becomes the selected nearest. // Ghosts G1=(3,3), G2=(5,2). key(p)=y*7+x at TUT_N=7. // (3,2): dist-to-G1=1, key=17 -> UNIQUE nearest initially -> forbidden take (RED) // (5,3): dist-to-G2=1, key=26 -> nearest AFTER (3,2) removed -> forbidden take (RED) // (1,1)/(0,4): far from both ghosts -> never the nearest -> clean takes st.tokens.push({ x: 3, y: 2, v: 5, alive: true, guard: true }); // 1st ghost-nearest -> RED st.tokens.push({ x: 5, y: 3, v: 4, alive: true, guard: true }); // 2nd ghost-nearest (after 1st gone) -> RED st.tokens.push({ x: 1, y: 1, v: 3, alive: true, guard: false }); // far -> clean take st.tokens.push({ x: 0, y: 4, v: 2, alive: true, guard: false }); // far -> clean take const cells = [ { x: 1, y: 1 }, // take far token (1,1) -> CLEAN (nearest is (3,2), not this) { x: 2, y: 1 }, // clean detour (no token) { x: 3, y: 2 }, // take (3,2): the UNIQUE ghost-nearest -> RED VIOLATION { x: 4, y: 2 }, // clean detour (no token; (5,3) is now the nearest, this is empty) { x: 5, y: 3 }, // take (5,3): now the ghost-nearest (after (3,2) removed) -> RED VIOLATION [2nd] { x: 4, y: 3 }, // clean detour (no token) [2nd clean] { x: 2, y: 3 }, // clean detour { x: 0, y: 4 }, // take far token (0,4): only non-ghost-near token left -> may RED (extra), harmless (>=2 clean already at steps 1,3,5) ]; return { board: st, steps: tutScript(st, rule, cells), mechanism: 'relational', param, ghosts: st.ghosts.map(g => ({ ...g })) }; } // avoid_adjacent_rival: step onto cells 4-adjacent to a ghost (RED) and detour // onto non-adjacent cells (clean). Ghost G1=(3,3): adjacent cells = (2,3),(4,3), // (3,2),(3,4). Ghost G2=(5,2): adjacent = (4,2),(6,2),(5,1),(5,3). const cells = [ { x: 1, y: 0 }, // clean (not adjacent to any ghost) { x: 2, y: 0 }, // clean { x: 2, y: 1 }, // clean { x: 2, y: 2 }, // clean (manhattan to G1 = 1+1 = 2 -> not adjacent) { x: 3, y: 2 }, // RED: adjacent to G1 (3,3) [dist 1] { x: 2, y: 2 }, // step back (clean detour, dist to G1 = 2) { x: 2, y: 3 }, // RED [2nd]: adjacent to G1 (3,3) [dist 1] { x: 1, y: 3 }, // clean detour (dist to G1 = 2) { x: 1, y: 1 }, // clean detour ]; return { board: st, steps: tutScript(st, rule, cells), mechanism: 'relational', param, ghosts: st.ghosts.map(g => ({ ...g })) }; } // SLICE2 LEVER A2 — ROLE-PLAY tutorial (spec §6). A SOLO newcomer demonstrates the // INTENTION by goal-directed MOTION over a board seeded with 1-2 display-only GHOSTS at // FIXED rule-invariant cells (placement IDENTICAL across all roles of the same ref-arity, // C1) + an optional token for the CONTRASTIVE beat (a step where the newcomer FORGOES a // task-greedy cell to stay in-character). The newcomer follows its OWN in-character set // each step (consistentMoves), so the walk is CLEAN for the true role but its motion is // OUT-of-character for other roles — the role is induced from the goal-directed motion, // not the layout. Each step's `violated` flag is computed by REPLAY against the role's // own pred (so it reads as clean=in-character on the demo), and a contrastive step is // flagged `passedUp:true` (a public goal-delta forgone, NOT a violation flash, NO role // label). Walk = a seeded greedy in-character walk that visits a contrastive token. // _roleDemoGhostFrame(i): the rule-INVARIANT Heider-Simmel SCENE at demo frame i. The three // ghosts follow FIXED tracks (independent of the focal — and hence of the role), so the // scene (and the layout at every frame) is identical for ALL roles; only the focal's RESPONSE // differs (C1). Each ghost's facing cue = its current heading, so the motion-reading // resolvers (pursuer / facing_leader / chase) have a live heading to read. G1 (smallest) // sweeps RIGHT across the top, G2 (max-carry) sweeps LEFT across the bottom (opposite motion // = a legible relative dynamic), G3 (third actor) DESCENDS the middle toward the focal's row // (it visibly APPROACHES = the pursuer/prey). A static scene cannot enact chase/flee/orbit/ // intercept (a blind re-measurement found relational roles un-inducible from a frozen demo); // a moving scene makes the intention legible from goal-directed motion (Heider-Simmel). const _ROLE_DEMO_FRAMES = 8; function _roleDemoGhostFrame(i) { const tracks = [ { vid: 1, v: 2, at: (k) => ({ x: Math.min(1 + k, 5), y: 2 }) }, // G1 smallest: sweep RIGHT, top { vid: 2, v: 6, at: (k) => ({ x: Math.max(5 - k, 1), y: 5 }) }, // G2 max-carry: sweep LEFT, bottom { vid: 3, v: 4, at: (k) => ({ x: 4, y: Math.min(k, 4) }) }, // G3 third actor: DESCEND middle ]; return tracks.map(t => { const p = t.at(i), q = t.at(Math.max(0, i - 1)); return { x: p.x, y: p.y, vid: t.vid, v: t.v, fdx: Math.sign(p.x - q.x), fdy: Math.sign(p.y - q.y) }; }); } function buildRoleDemo(rule) { const v = RULE_VARIANTS[rule]; const st = tutBoard(rule); st.facing = {}; // role board threads facing // CENTER focal so each role's intention points a DIFFERENT way (a corner spawn collapsed // flee to all-stay + several roles onto one motion). The MOVING scene (below) then lets the // focal REACT, making the relation legible. st.pos[A.id] = { x: 3, y: 3 }; st.ghosts = _roleDemoGhostFrame(0); // initial render = scene frame 0 (rule-invariant) // CONTRASTIVE task tokens (rule-invariant placement): >=1 step forgoes a task-greedy move // (out-of-character) — the Heider-Simmel "passes up a goal to follow its intention" beat. st.tokens.push({ x: 5, y: 3, v: 7, alive: true, guard: true }); st.tokens.push({ x: 3, y: 5, v: 5, alive: true, guard: false }); st.tokens.push({ x: 0, y: 0, v: 8, alive: true, guard: true }); // Synthesize the focal's IN-CHARACTER reaction to the MOVING scene, frame by frame. Each // step records the scene snapshot (ghosts) it was computed against, so EVERY consumer // (serializeDemo render, demoIdentifiable replay) reuses the SAME choreography with no // re-derivation (single source of truth -> no desync). const sim = tutCloneBoard(st); sim.facing = {}; const steps = []; for (let i = 0; i < _ROLE_DEMO_FRAMES; i++) { const ghosts = _roleDemoGhostFrame(i); sim.ghosts = ghosts; // advance the rule-invariant scene sim.__seat__ = A.id; const from = { ...sim.pos[A.id] }; const ok = consistentMoves(sim, A.id, rule); const n = sim.N || N; const cands = _roleCandidates(from).filter(c => inbN(c, n) && c.key !== 'stay'); // contrastive beat: a task-greedy step toward SOME alive token that is OUT-of-character. let passedUp = false; for (const t of sim.tokens) { if (!t.alive || !cands.length) continue; let greedy = null, gd = Infinity; for (const c of cands) { const d = Math.abs(c.x - t.x) + Math.abs(c.y - t.y); if (d < gd) { gd = d; greedy = c; } } const d0 = Math.abs(from.x - t.x) + Math.abs(from.y - t.y); if (greedy && gd < d0 && !ok.has(greedy.key)) { passedUp = true; break; } } // pick the in-character step that most COMMITS to the role — the lowest role-potential // phi among the in-character moves, preferring a real MOVE over stay so the intention // reads as MOTION. An AVERSIVE role (flee) minimizes phi by moving AWAY from its // referent; a "closest-to-ref" heuristic wrongly froze it to stay (all-stay demo). // mimic has no phi (its in-character set is {facing-step}U{stay}); take the facing step. const inCh = _roleCandidates(from).filter(c => ok.has(c.key) && inbN(c, n) && c.key !== 'stay'); let pick = { x: from.x, y: from.y, key: 'stay' }; if (v.roleKind === 'mimic') { if (inCh.length) pick = inCh[0]; // the facing-aligned step } else if (inCh.length) { let best = Infinity; for (const c of inCh) { const ph = _rolePhi(v.roleKind, v.ref, c, sim, A.id); if (ph < best - 1e-9) { best = ph; pick = c; } } } const violated = outOfCharacter(rule, from, { x: pick.x, y: pick.y }, sim); // own walk -> false steps.push({ from: { x: from.x, y: from.y }, to: { x: pick.x, y: pick.y }, violated: !!violated, passedUp, ghosts }); sim.__seat__ = A.id; applyMove(sim, A.id, { x: pick.x, y: pick.y }, rule); } return { board: st, steps, mechanism: 'role', roleKind: v.roleKind, ref: v.ref, ghostScript: steps.map(s => s.ghosts), ghosts: st.ghosts.map(g => ({ ...g })) }; } // SLICE2 LEVER D — PHASE / MEMORY tutorial (§3). The newcomer walks long enough for // the PUBLIC clock to cycle >=2 full periods, doing the SAME provocative action in // every segment so the rule flashes RED ONLY in the forbidden segment — making the // TEMPORAL conditional inducible from the serialized obs alone (the player sees the // pip advance and which segment turns the taboo on). Two shapes: // phase-cycle (advanceOn:'own_turn'): seed the underlying terrain taboo (a single // binding forbidden cell) + a public clock; the path steps INTO the forbidden // cell once per period. The step is a VIOLATION only when seg===RED_SEG and CLEAN // in the other segments (same cell, different phase) — >=2 RED + >=2 clean over // >=2 periods, so the conditional (forbidden iff RED) is the only consistent rule. // since-event (advanceOn:'token_take'): seed avoid-biggest tokens + a 2-state clock; // BEFORE the first take taking the biggest is CLEAN (seg 0), and AFTER the first // take (the flip) taking the biggest is a VIOLATION (seg 1) — the demo shows a // biggest-take pre-flip (clean) and biggest-takes post-flip (RED), so the latch is // inducible. The clock advances inside tutScript's replay (own-turn each step). function tutPhase(rule) { const v = RULE_VARIANTS[rule]; const clk = v.clock; const segN = clk.segN; const st = tutBoard(rule); // seed the public clock for seat 0 (the tutorial newcomer) — segN/advanceOn from // the variant param, seg starts at 0 (rule-invariant existence; PUBLIC state, C1). st.clock = { 0: { seg: 0, segN, advanceOn: clk.advanceOn } }; if (clk.advanceOn === 'token_take') { // since-event MEMORY (avoid-biggest after the first take). Distinct descending // values along a row; the newcomer takes the biggest PRE-FLIP (clean, seg 0), then // the latch flips, and subsequent biggest-of-the-moment takes are RED (seg 1). st.tokens.push({ x: 1, y: 1, v: 20, alive: true, guard: false }); // pre-flip biggest (CLEAN take) st.tokens.push({ x: 2, y: 1, v: 18, alive: true, guard: true }); // post-flip biggest -> RED st.tokens.push({ x: 3, y: 1, v: 16, alive: true, guard: true }); // next biggest -> RED st.tokens.push({ x: 1, y: 2, v: 2, alive: true, guard: false }); // low (clean even post-flip) st.tokens.push({ x: 2, y: 2, v: 1, alive: true, guard: false }); const cells = [ { x: 1, y: 0 }, // seg 0 (pre-flip), no token -> clean; clock stays 0 (no take) { x: 1, y: 1 }, // seg 0, take 20 (biggest) -> CLEAN (pre-flip) ; FLIP -> seg 1 { x: 2, y: 1 }, // seg 1, take 18 (now biggest) -> RED VIOLATION { x: 3, y: 1 }, // seg 1, take 16 (now biggest) -> RED VIOLATION [2nd] { x: 3, y: 2 }, // seg 1, no token -> clean detour { x: 2, y: 2 }, // seg 1, take 1 (not biggest) -> clean detour [2nd] ]; return { board: st, steps: tutScript(st, rule, cells), mechanism: 'phase', segN, advanceOn: 'token_take' }; } // phase-CYCLE over a terrain taboo (avoid_dark/avoid_hatch @param). Resolve the // underlying terrain category + binding sub-instance from the underlying variant. const uv = RULE_VARIANTS[v.underlying]; const ub = uv.base, up = uv.param || 'A'; const cat = ub === 'avoid_dark' ? st.hazard : st.sacred; const inst = ub === 'avoid_dark' ? st.hazardInst : st.sacredInst; // ONE binding forbidden terrain cell the path dips into once per period; present // the non-binding sub-instances + the other terrain category off-path (C1). const fcell = { x: 2, y: 1 }; cat.add(tk(fcell)); inst[up].add(tk(fcell)); const others = ['A', 'B', 'C'].filter(q => q !== up); inst[others[0]].add(tk({ x: 1, y: 5 })); cat.add(tk({ x: 1, y: 5 })); inst[others[1]].add(tk({ x: 5, y: 5 })); cat.add(tk({ x: 5, y: 5 })); const otherCat = ub === 'avoid_dark' ? st.sacred : st.hazard; otherCat.add(tk({ x: 3, y: 5 })); // RED = the LAST segment (RED_SEG(segN)). Build a repeating per-period unit that // costs ONE own-turn per segment and dips into the forbidden cell exactly when the // clock is about to read the segment we want. The clock advances AFTER each step // (tutScript), and the predicate reads the seg BEFORE the advance. So to dip in at // seg===s we must have taken exactly s prior steps within the period. We construct a // path of >=2 full periods where the dip lands on RED in each period (VIOLATION) and // a separate dip lands on a non-RED seg (CLEAN), over the same cell. // Period anchor cells (all clean, non-forbidden): a small loop on rows 0..2 at x=2. // step sequence per period (segN steps): we cycle seg 0,1,..,segN-1 and dip into // fcell at the RED segment + dip into it again at seg 0 (clean) in the NEXT period. const cells = []; const above = { x: 2, y: 0 }; // clean (above fcell) const below = { x: 2, y: 2 }; // clean (below fcell) // get into position: step to (1,0) then (2,0) so we are above fcell at seg start. cells.push({ x: 1, y: 0 }); // seg 0 -> advance to 1 cells.push({ x: 2, y: 0 }); // seg 1 -> advance ... // Now drive several periods. We want the seg value, read BEFORE each step, to take // forbidden dips at RED and clean dips at non-RED, over >=2 periods. Track expected // seg by counting steps already pushed (clock advances once per step from seg 0). // We have pushed 2 steps so far; next step reads seg = 2 % segN. let pushed = cells.length; let redDips = 0, cleanDips = 0; // run enough own-turns for >=2 full periods AND >=2 RED dips + >=2 clean dips. const totalTurns = segN * 3 + 4; let atFcell = false; while (redDips < 2 || cleanDips < 2 || pushed < totalTurns) { const seg = pushed % segN; const red = (seg === RED_SEG(segN)); if (!atFcell) { // we are on a clean cell (above/below or x=1 lane). Decide whether to dip: // dip into fcell when the dip will register the segment we still need. if (red && redDips < 4) { cells.push(fcell); atFcell = true; redDips++; } else if (!red && cleanDips < 4) { cells.push(fcell); atFcell = true; cleanDips++; } else { cells.push(above); } // idle clean own-turn to advance the clock } else { // step back OUT of fcell onto a clean cell (always compliant) to set up the next dip. cells.push(above); atFcell = false; } pushed++; if (pushed > 60) break; // hard safety } return { board: st, steps: tutScript(st, rule, cells), mechanism: 'phase', segN, advanceOn: 'own_turn' }; } // build ONE episode of `mode` for `rule`. Returns a machine-readable trace. function buildEpisode(rule, seed, mode, round) { round = round == null ? 1 : round; const st = makeBoard(rule, 'harvest_max', seed, round, ENV_PRESETS.E1); const forbiddenCells = forbiddenCellsOf(st, rule); const tokenVals = st.tokens.filter(t => t.alive).map(t => t.v); const policy = mode === EP_MODE.VIOLATE ? violatingPolicy(rule) : avoidingPolicy(rule); const steps = []; let turnSeed = seed * 1000 + 7; let lastTakeIdx = -1; let sawViolation = false; let sawCleanPass = false; // C2: a diagnostic step passed cleanly (AVOID detour) // DESIGN §3B (life bar COMMON to all stages incl. memory replay): track the // PAST SELF's running violation count so the replay can show its life bar // DROP on each violated step. DISPLAY ONLY — livesAfter is derived from the // violation events the engine already charges; it does NOT touch scoring. let memViolations = 0; for (let t = 0; t < 16; t++) { const from = { ...st.pos[A.id] }; const diagnostic = isDiagnostic(st, A.id, rule); const to = policy(st, A.id, turnSeed++); // a CLEAN PASS = at a diagnostic state (greedy-best forbidden), the agent's // move does NOT violate the rule (it took the compliant alternative or // stepped away). This is the behavioural detour an AVOID episode must show. const cleanPass = diagnostic && !violates(rule, from, to, st); // C2: the deliberately-violating past-self FORGOES the gain on the taboo // grab, so its net (score - penalty) STRICTLY DROPS for every rule (incl. // the token rule avoid_biggest). const res = applyMove(st, A.id, to, rule, mode === EP_MODE.VIOLATE ? { forgoGainOnViolation: true } : undefined); if (res.violated) sawViolation = true; if (cleanPass && !res.violated) sawCleanPass = true; if (res.violated) memViolations++; // past-self life-bar signal (display) const netAfter = st.score[A.id] - st.penalty[A.id]; steps.push({ step: steps.length, from, to: { ...to }, took: res.took, violated: res.violated, gained: res.took ? res.tokVal : 0, penalty: res.penalty, tokVal: res.took ? res.tokVal : 0, scoreAfter: st.score[A.id], penaltyAfter: st.penalty[A.id], netAfter, diagnostic, cleanPass: cleanPass && !res.violated, // DESIGN §3B life bar (display): the past self's lives AFTER this step, // dropping LIVES -> LIVES-violations as the replay advances. Monotone // non-increasing along the episode; drops EXACTLY on violated steps. violatedCount: memViolations, livesAfter: livesFromViolations(memViolations), }); if (res.took) lastTakeIdx = steps.length - 1; if (cleanPass && !res.violated) lastTakeIdx = Math.max(lastTakeIdx, steps.length - 1); } const trimmed = steps.slice(0, Math.max(0, lastTakeIdx + 1)); const sawCleanPassTrim = trimmed.some(s => s.cleanPass); // C2 (standing-penalty RECURRENCE): count how many times the persistent cost is // paid WITHIN this episode — violated steps (VIOLATE) / diagnostic clean-pass // detours (AVOID). >=2 makes the rule read as an always-on cost, not a one-off. const nViolatedSteps = trimmed.filter(s => s.violated).length; const nCleanPassSteps = trimmed.filter(s => s.cleanPass).length; return { seed, round, mode, rule, // rule kept ONLY here for headless/test use; category: rule, // app.js must NOT pass category/rule to any drawable (C1) steps: trimmed, forbiddenCells, tokenVals, sawViolation, sawCleanPass: sawCleanPassTrim, // C2: AVOID episode shows a diagnostic detour nViolatedSteps, // C2: standing-penalty recurrence (VIOLATE) nCleanPassSteps, // C2: recurring detour (AVOID) }; } // re-evaluate an episode against a CANDIDATE rule: AVOID steps must not violate // the candidate; the forced VIOLATE step must violate the candidate. function consistentWith(candidateRule, bundle) { for (const ep of bundle.episodes) { const st = makeBoard(candidateRule === ep.rule ? candidateRule : ep.rule, 'harvest_max', ep.seed, ep.round, ENV_PRESETS.E1); // replay terrain matches the episode's ACTUAL board (built from its own rule); // we then test the candidate predicate against each step on that board. const board = makeBoard(ep.rule, 'harvest_max', ep.seed, ep.round, ENV_PRESETS.E1); for (const s of ep.steps) { board.pos[A.id] = { ...s.from }; const cv = violates(candidateRule, s.from, s.to, board); if (ep.mode === EP_MODE.AVOID && cv) return false; // clean step must stay clean if (ep.mode === EP_MODE.VIOLATE && s.violated && !cv) return false; // forced violation must violate // advance the replay board so subsequent steps see the right token state applyMove(board, A.id, s.to, ep.rule); } } return true; } // P3-1: candidateList defaults to RULE_LIST (legacy 3) so existing callers are // byte-identical; the campaign passes VARIANT_LIST to identify over the 6 variants. function identifyRules(bundle, candidateList) { return (candidateList || RULE_LIST).filter(r => consistentWith(r, bundle)); } /* ===================== INDUCTION MODEL (Discovery, C4) ================= A real (non-oracle) inducer: it observes ONLY the memory bundle (visual trace, no rule label) and infers the consistent rule set. Its induced rule is the FIRST candidate consistent with every episode. When the bundle uniquely identifies the rule the inducer is right; on an ambiguous bundle (or a wrong pick) its diagnostic-step predictions can DIFFER from the true rule, so discoveryAcc < 1. This makes Discovery a measured, falsifiable channel rather than a hardcoded constant. */ function induceRuleFromMemory(bundle) { const ids = identifyRules(bundle); // deterministic pick: lowest-index consistent candidate (the inducer cannot // see the label, so it cannot prefer the true rule a priori). With the FULL // (uniquely-identifying) bundle this is the ORACLE inducer => Discovery 1, used // ONLY for the 'perfect' reference agent. return ids.length ? ids[0] : null; } // BOUNDED inducer (C4): a realistic, FALLIBLE induction model — the default for // any non-perfect agent. It observes only LIMITED evidence, so the evidence // frequently does NOT uniquely pin the rule. Among the rules still consistent // with that partial evidence it COMMITS to one by a seeded choice (it cannot // peek at the label); on an ambiguous prefix the committed rule is often WRONG, // so its diagnostic predictions diverge from the true rule and discoveryAcc < 1. // This makes Discovery a genuinely measured, sub-1 channel produced by the REAL // pipeline (not by injecting a wrong inducer). // // EVIDENCE SELECTION (C4, post 4->3 rule reduction): a VIOLATE episode contains // the forced costly-mistake step, which is HIGHLY discriminative — with only 3 // candidate rules a single witnessed violation pins the rule, collapsing the // bounded inducer to an oracle (Discovery -> 1, degenerate). The realistic // limited-evidence regime is one where the learner has seen the past self // DETOURING (AVOID episodes) but has NOT yet witnessed the costly violation. A // clean detour is consistent with MULTIPLE rules, so observing the AVOID // evidence first leaves the rule genuinely under-determined among the 3 // candidates -> the committed pick is often wrong -> Discovery stays sub-1. The // bounded inducer therefore observes AVOID episodes first (up to opts.episodes), // topping up with VIOLATE episodes only if not enough AVOID evidence exists, so // fallibility comes from limited-evidence ambiguity, not from a degenerate // candidate set. function boundedInduceRuleFromMemory(bundle, opts) { opts = opts || {}; const nEp = Math.max(1, Math.min(opts.episodes || 2, bundle.episodes.length)); // prefer the (genuinely ambiguous) AVOID/detour evidence, then VIOLATE, so the // bounded learner sees the less-discriminative detours before the costly // violation it has not yet witnessed. const avoid = bundle.episodes.filter(e => e.mode === EP_MODE.AVOID); const violate = bundle.episodes.filter(e => e.mode === EP_MODE.VIOLATE); const ordered = avoid.concat(violate); const sub = { rule: bundle.rule, category: bundle.category, seed: bundle.seed, episodes: ordered.slice(0, nEp) }; const ids = identifyRules(sub); if (!ids.length) return null; const pick = (rng(bundle.seed * 31 + nEp * 7 + 1)() * ids.length) | 0; return ids[Math.min(pick, ids.length - 1)]; } // the inducer predicts, at each DIAGNOSTIC step of a held-out trajectory, the // best COMPLIANT adjacent take UNDER ITS INDUCED RULE; `correct` iff that equals // the best compliant adjacent take under the TRUE rule (what a rule-follower // actually does). Returns a predLog consumable by discoveryAcc/scoreEpisode. function bestCompliantAdjacent(st, id, rule) { const from = st.pos[id]; let best = null; for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!inb(to)) continue; const tok = tokenAt(st, to); if (!tok) continue; if (violates(rule, from, to, st)) continue; if (!best || tok.v > best.tok.v) best = { tok, to }; } return best ? best.to : null; } function inductionPredLog(trueRule, inducedRule, evalBundle) { const predLog = []; for (const ep of evalBundle.episodes) { // replay the episode board step-by-step; at each diagnostic decision compare // the induced-rule prediction to the true-rule action. const board = makeBoard(ep.rule, 'harvest_max', ep.seed, ep.round, ENV_PRESETS.E1); for (const s of ep.steps) { board.pos[A.id] = { ...s.from }; if (isDiagnostic(board, A.id, trueRule)) { const predTrue = bestCompliantAdjacent(board, A.id, trueRule); const predInd = inducedRule ? bestCompliantAdjacent(board, A.id, inducedRule) : undefined; // correct iff the induced rule prescribes the SAME compliant action as // the true rule (both-null == agreement that no compliant take exists, // i.e. "step away"); a null/blind inducer (undefined) is always wrong. let correct; if (predInd === undefined) correct = false; // no rule induced else if (predTrue === null && predInd === null) correct = true; else if (predTrue === null || predInd === null) correct = false; else correct = predTrue.x === predInd.x && predTrue.y === predInd.y; predLog.push({ diagnostic: true, correct }); } applyMove(board, A.id, s.to, ep.rule); } } return predLog; } // build a memory bundle of K episodes (>=2 VIOLATE, >=2 AVOID), re-seeding until // the rule is UNIQUELY identifiable among RULE_LIST and diagnosticCount>=4 (C10). function buildMemoryBundle(rule, seed, K) { K = K || MEM_K; let s = seed; for (let attempt = 0; attempt < 60; attempt++) { const episodes = []; let nViol = 0, nAvoid = 0, nAvoidCleanPass = 0; // C2 (RECURRENCE gate): count episodes where the standing cost RECURS — VIOLATE // episodes paying the penalty on >=2 separated steps, AVOID episodes detouring // on >=2 diagnostic clean passes. The persona rule reads as an always-on cost // only when the burden is visible repeatedly, not as a one-off. let nViolRecur = 0, nAvoidRecur = 0; for (let k = 0; k < K; k++) { const mode = (k % 2 === 0) ? EP_MODE.VIOLATE : EP_MODE.AVOID; const ep = buildEpisode(rule, s + k * 53, mode, 1 + (k % ROUNDS)); if (mode === EP_MODE.VIOLATE && ep.sawViolation) { nViol++; if (ep.nViolatedSteps >= 2) nViolRecur++; } else if (mode === EP_MODE.AVOID) { nAvoid++; if (ep.sawCleanPass) nAvoidCleanPass++; if (ep.nCleanPassSteps >= 2) nAvoidRecur++; } episodes.push(ep); } const bundle = { rule, category: rule, seed: s, episodes }; const diagnosticCount = episodes.reduce( (n, ep) => n + ep.steps.filter(st => st.diagnostic).length, 0); // V2: when `rule` is a variant id, require uniqueness over the WHOLE VARIANT // POOL (so the bundle re-seeds until the variant is pinned against every // sibling). Legacy base rules identify over RULE_LIST -> byte-identical // re-seed behaviour (SNAP-safe). const idPool = RULE_VARIANTS[rule] ? VARIANT_LIST : undefined; const ids = identifyRules(bundle, idPool); bundle.uniquelyIdentified = ids.length === 1 && ids[0] === rule; bundle.diagnosticCount = diagnosticCount; bundle.nViolate = nViol; bundle.nAvoid = nAvoid; bundle.nAvoidCleanPass = nAvoidCleanPass; bundle.nViolRecur = nViolRecur; bundle.nAvoidRecur = nAvoidRecur; // C2: require >=2 VIOLATE episodes that RECUR the penalty (>=2 violated steps // each) AND >=2 AVOID episodes that RECUR the detour (>=2 clean passes each), // so the standing deontic cost is legible as a persistent everyday burden — // for EVERY rule. Uniqueness (C10) + diagnostic density still gate too. if (bundle.uniquelyIdentified && diagnosticCount >= 4 && nViol >= 2 && nAvoid >= 2 && nAvoidCleanPass >= 2 && nViolRecur >= 2 && nAvoidRecur >= 2) { return bundle; } s += 977; } // fallback: return last attempt (best-effort); flag not-unique for the guard. const episodes = []; for (let k = 0; k < K; k++) { const mode = (k % 2 === 0) ? EP_MODE.VIOLATE : EP_MODE.AVOID; episodes.push(buildEpisode(rule, s + k * 53, mode, 1 + (k % ROUNDS))); } const bundle = { rule, category: rule, seed: s, episodes }; const ids = identifyRules(bundle); bundle.uniquelyIdentified = ids.length === 1 && ids[0] === rule; bundle.diagnosticCount = episodes.reduce( (n, ep) => n + ep.steps.filter(st => st.diagnostic).length, 0); bundle.nViolate = episodes.filter(e => e.mode === EP_MODE.VIOLATE && e.sawViolation).length; bundle.nAvoid = episodes.filter(e => e.mode === EP_MODE.AVOID).length; bundle.nAvoidCleanPass = episodes.filter(e => e.mode === EP_MODE.AVOID && e.sawCleanPass).length; bundle.nViolRecur = episodes.filter(e => e.mode === EP_MODE.VIOLATE && e.nViolatedSteps >= 2).length; bundle.nAvoidRecur = episodes.filter(e => e.mode === EP_MODE.AVOID && e.nCleanPassSteps >= 2).length; return bundle; } /* =============================== OPPONENTS (C9) ========================= */ // cloneSim copies enough state for a rule-aware peer rollout (incl. terrain). function cloneSim(st) { return { goal: st.goal, zone: st.zone, round: st.round, pos: { 0: { ...st.pos[0] }, 1: { ...st.pos[1] } }, score: { 0: st.score[0], 1: st.score[1] }, carry: { 0: (st.carry ? st.carry[0] : 0) || 0, 1: (st.carry ? st.carry[1] : 0) || 0 }, tokens: st.tokens.map(t => ({ x: t.x, y: t.y, v: t.v, alive: t.alive, guard: t.guard })), hazard: new Set(st.hazard), sacred: new Set(st.sacred), penalty_amt: st.penalty_amt || PENALTY, // SLICE2: carry the public phase-clock + relational landmarks/ghosts through the // peer-rollout clone so a rule-aware rollout under a phase/relational rule reads the // SAME public state. Guarded so legacy 2-seat boards (no clock/landmarks/ghosts) are // byte-identical (these are undefined there -> the spread copies nothing). ...(st.clock ? { clock: (() => { const c = {}; for (const s of Object.keys(st.clock)) c[s] = { ...st.clock[s] }; return c; })() } : {}), ...(st.landmarks ? { landmarks: new Set(st.landmarks) } : {}), ...(st.ghosts ? { ghosts: st.ghosts.map(g => ({ ...g })) } : {}), }; } function applySim(sim, id, to) { // rule-blind apply (no penalties) if (!inb(to)) return; const t = sim.tokens.find(x => x.alive && x.x === to.x && x.y === to.y); if (t) { t.alive = false; if (sim.goal === 'deliver_to_zone') sim.carry[id] += t.v; else sim.score[id] += t.v; } sim.pos[id] = { x: to.x, y: to.y }; if (sim.goal === 'deliver_to_zone' && sim.zone && to.x === sim.zone.x && to.y === sim.zone.y && sim.carry[id] > 0) { sim.score[id] += sim.carry[id]; sim.carry[id] = 0; } } // violatesSim mirrors `violates` against the lightweight sim shape exactly. function violatesSim(rule, from, to, sim) { if (rule === 'avoid_dark') return sim.hazard.has(key(to)); if (rule === 'avoid_hatch') return sim.sacred.has(key(to)); if (rule === 'avoid_biggest') { const tok = sim.tokens.find(t => t.alive && t.x === to.x && t.y === to.y); if (!tok) return false; const mx = sim.tokens.reduce((m, t) => t.alive ? Math.max(m, t.v) : m, 0); return tok.v === mx; } return false; } // applySimPenalized: like applySim but accrues the peer's OWN rule penalty. // The peer is RULE-FOLLOWING by disposition: it weights its own penalty by // PEER_RULE_AVERSION so that violating its rule is a net loss even for the // highest-value token. (C3: a single guard-take is already strictly net-negative // for the FOCAL agent too — penaltyFor charges penalty >= maxGuard + margin — so // rule-following is the winning policy for both agents; the peer is merely // EXTRA averse on itself, never the only rule-follower.) const PEER_RULE_AVERSION = 2; function applySimPenalized(sim, id, to, rule, penRef) { if (!inb(to)) return; if (rule && violatesSim(rule, sim.pos[id], to, sim)) { penRef.pen += PEER_RULE_AVERSION * (sim.penalty_amt || PENALTY); } applySim(sim, id, to); } // greedyMove: rule-blind goal-maximizer (deterministic, no random). function greedyMove(st, id) { const from = st.pos[id]; let target = null; if (st.goal === 'deliver_to_zone' && st.carry[id] > 0 && st.zone) { target = { x: st.zone.x, y: st.zone.y }; } else { let bs = -1e9; for (const t of st.tokens) { if (!t.alive) continue; const s = t.v - 0.5 * manhattan(from, t); if (s > bs) { bs = s; target = { x: t.x, y: t.y }; } } } if (!target) return from; if (from.x === target.x && from.y === target.y) return from; let pick = from, bd = 1e9; for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!inb(to)) continue; const dist = manhattan(to, target); if (dist < bd) { bd = dist; pick = to; } } return pick; } // rollout policy uses a SEEDED rnd closure (C11): no bare Math.random. function rolloutMove(sim, id, rnd) { if (rnd() < 0.2) { const ms = []; for (const d of DIRS) { const to = { x: sim.pos[id].x + d.x, y: sim.pos[id].y + d.y }; if (inb(to)) ms.push(to); } return ms.length ? ms[(rnd() * ms.length) | 0] : sim.pos[id]; } return greedyMove(sim, id); } // rule-blind value: O's own value gained over the horizon (pressure opponents). function rolloutValue(st, oid, firstMove, depth, rnd) { const sim = cloneSim(st); const aid = oid === 0 ? 1 : 0; const base = sim.score[oid] + sim.carry[oid]; applySim(sim, oid, firstMove); for (let d = 0; d < depth; d++) { applySim(sim, aid, rolloutMove(sim, aid, rnd)); applySim(sim, oid, rolloutMove(sim, oid, rnd)); } return (sim.score[oid] + sim.carry[oid]) - base; } function mctsO(st, id, rnd) { rnd = rnd || rng(1234); const from = st.pos[id]; const moves = []; for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (inb(to)) moves.push(to); } if (moves.length === 0) return from; const ITER = 24, DEPTH = 6; let best = moves[0], bestAvg = -Infinity; for (const mv of moves) { let sum = 0; for (let i = 0; i < ITER; i++) sum += rolloutValue(st, id, mv, DEPTH, rnd); const avg = sum / ITER; if (avg > bestAvg) { bestAvg = avg; best = mv; } } return best; } // COMPLIANT rollout policy for the peer's OWN moves: greedy toward the best // COMPLIANT token (mirrors `planMove` over the sim), so the peer does not // self-penalize by random wandering during rollout. Falls back to a compliant // random step. The rival seat keeps the rule-blind rolloutMove. function rolloutMovePeer(sim, id, oppRule, rnd) { const from = sim.pos[id]; // best compliant token by (value - 0.5*dist). let best = null, bs = -1e9; for (const t of sim.tokens) { if (!t.alive) continue; const to = { x: t.x, y: t.y }; if (violatesSim(oppRule, from, to, sim)) continue; const s = t.v - 0.5 * manhattan(from, to); if (s > bs) { bs = s; best = to; } } if (best) { let pick = from, bd = 1e9; for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!inb(to)) continue; if (violatesSim(oppRule, from, to, sim) && !(to.x === best.x && to.y === best.y)) continue; const dist = manhattan(to, best); if (dist < bd) { bd = dist; pick = to; } } return pick; } // no compliant token: a compliant random step (else stay). const ms = []; for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (inb(to) && !violatesSim(oppRule, from, to, sim)) ms.push(to); } return ms.length ? ms[(rnd() * ms.length) | 0] : from; } // PEER (C9b): rule-FOLLOWING MCTS whose rollout value SUBTRACTS its own // rule-violation penalty -> it learns to maintain its rule while pursuing goal. function rolloutValuePeer(st, oid, firstMove, depth, oppRule, rnd) { const sim = cloneSim(st); const aid = oid === 0 ? 1 : 0; const base = sim.score[oid] + sim.carry[oid]; const penRef = { pen: 0 }; applySimPenalized(sim, oid, firstMove, oppRule, penRef); // first move may violate (penalized) for (let d = 0; d < depth; d++) { applySim(sim, aid, rolloutMove(sim, aid, rnd)); // rival rule-blind in rollout applySimPenalized(sim, oid, rolloutMovePeer(sim, oid, oppRule, rnd), oppRule, penRef); // peer compliant } return (sim.score[oid] + sim.carry[oid]) - base - penRef.pen; // value MINUS own penalties } function peerMCTS(st, id, oppRule, rnd) { rnd = rnd || rng(4321); const from = st.pos[id]; const moves = []; for (const d of DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (inb(to)) moves.push(to); } if (moves.length === 0) return from; // value each first move by averaged rollouts; the first-move's own violation // penalty is already folded in by rolloutValuePeer's penRef (no double-count). const ITER = 24, DEPTH = 6; let best = moves[0], bestAvg = -Infinity; for (const mv of moves) { let sum = 0; for (let i = 0; i < ITER; i++) sum += rolloutValuePeer(st, id, mv, DEPTH, oppRule, rnd); const avg = sum / ITER; if (avg > bestAvg) { bestAvg = avg; best = mv; } } return best; } // makeOpponent: pressure families carry NO rule/memory; peer carries its own // hidden rule + memory and is rule-following. function makeOpponent(kind, oppRule, seed) { seed = seed || 7; if (kind === 'peer') { return { kind, rule: oppRule, peer: true, memory: buildMemoryBundle(oppRule, seed + 333), chooseMove: (st, id, rnd) => peerMCTS(st, id, oppRule, rnd), }; } if (kind === 'goal_mcts') { return { kind, rule: null, peer: false, memory: null, chooseMove: (st, id, rnd) => mctsO(st, id, rnd) }; } // greedy default return { kind, rule: null, peer: false, memory: null, chooseMove: (st, id, rnd) => greedyMove(st, id) }; } // opponentMove: the single place E selects opponent family (C5/C9). function opponentMove(st, id, env, ctx) { env = env || ENV_PRESETS.E1; const rnd = (ctx && ctx.oppRng) || rng(9999); if (env.opp === 'peer') { const oppRule = (ctx && ctx.oppRule) || rivalRuleFor(st.rule); return peerMCTS(st, id, oppRule, rnd); } if (env.opp === 'goal_mcts') return mctsO(st, id, rnd); return greedyMove(st, id); } function rivalRuleFor(rule) { const i = RULE_LIST.indexOf(rule); return RULE_LIST[(i + 1) % RULE_LIST.length]; } /* =============================== SWAP (C8) ============================= */ function canSwap(state) { return !!(state && state.opponent && state.opponent.peer && state.swap && !state.swap.used); } function invokeSwap(state) { if (!canSwap(state)) { return { ok: false, reason: state && state.swap && state.swap.used ? 'used' : 'no_peer' }; } const oldRuleA = state.ruleA; const oldOppRule = state.opponent.rule; // atomic exchange. state.ruleA = oldOppRule; state.opponent.rule = oldRuleA; state.swap = { used: true, atRound: state.round != null ? state.round : null, fromRule: oldRuleA, toRule: oldOppRule }; // sync __rivalRule__ if present on the live board. if (state.st && state.st.pos && state.st.pos.__rivalRule__) { state.st.pos.__rivalRule__[A.id] = state.ruleA; state.st.pos.__rivalRule__[O.id] = state.opponent.rule; } if (state.st) state.st.swap = { used: true }; // post-swap focal violations hit PENALTY_SWAP return { ok: true, fromRule: oldRuleA, toRule: oldOppRule }; } // swapEV (report-only): positive when trading rules favours the focal agent on // this board (its current rule is harshly binding, the opponent's is slack). function swapEV(state) { if (!state || !state.st) return 0; const st = state.st; const myRuleForbidden = forbiddenCellsOf(st, state.ruleA).size; const oppRuleForbidden = forbiddenCellsOf(st, state.opponent ? state.opponent.rule : state.ruleA).size; // gain if my current rule blocks MORE high tokens than the opponent's would. return myRuleForbidden - oppRuleForbidden; } /* ====================== TAKEOVER (other-direction axis) ================= After the live game the focal agent INHERITS the opponent's identity (its memory + hidden rule) and must CONTINUE ACTING under that rule. This is the other-direction measurement done by GENERATION, not prediction: simulation- theoretic understanding is scored by whether the focal's own moves converge to the other's best-response at diagnostic states (other-Discovery), whether it resists temptations under the other's rule (other-Maintenance — the SAME temptation machinery, rule swapped), and whether at divergence points it falls back onto its OWN former rule — the PROJECTION signature (imposing your rule on the other instead of simulating theirs). */ function takeoverClassify(st, from, to, otherRule, selfRule) { const diagnostic = isDiagnostic(st, A.id, otherRule); if (!diagnostic) return { diagnostic: false }; const predOther = bestCompliantAdjacent(st, A.id, otherRule); const predSelf = selfRule ? bestCompliantAdjacent(st, A.id, selfRule) : null; const eq = (p, q) => !!p && !!q && p.x === q.x && p.y === q.y; const chose = { x: to.x, y: to.y }; // correct = the move COMPLIES with the other's rule at this diagnostic state // (the greedy take here is forbidden under otherRule; a faithful simulator // either takes a compliant token or detours). NOT an exact-action match: a // faithful other-policy may legitimately route to a farther compliant token, // so exact-match against bestCompliantAdjacent would under-score fidelity. const correct = !violates(otherRule, from, to, st); // divergence point: the two rules prescribe DIFFERENT adjacent takes here. // Only these states can carry the projection signature. const diverges = !(predOther === null && predSelf === null) && !eq(predOther || { x: -9, y: -9 }, predSelf || { x: -8, y: -8 }); // projection: at a divergence point the focal chose its OWN former rule's // prescription instead of the other's — the egocentric-simulation signature. const projected = diverges && !!predSelf && eq(predSelf, chose) && !(predOther && eq(predOther, chose)); return { diagnostic: true, correct, diverges, projected }; } function takeoverStats(log) { let scored = 0, correct = 0, div = 0, proj = 0; for (const e of log) { if (!e.diagnostic) continue; scored++; if (e.correct) correct++; if (e.diverges) { div++; if (e.projected) proj++; } } return { scored, correct, acc: scored ? correct / scored : 0, divergent: div, projected: proj, projection: div ? proj / div : null, }; } /* ================= LIVES / PENALTY SIGNAL (DESIGN §3B) ================= The per-seat penalty/life bar is COMMON to ALL four stages (memory replay / G1 / G2 / G3). It is DISPLAY + ending only — derived purely from the SAME rule-violation events the engine already charges, and it NEVER enters score/penalty/Cstar, so headline=total/C* and agentness=D×M are intact. Lives RESET per game (each of G1/G2/G3 starts at LIVES). Memory replay shows the past self's own (independent) lives via buildEpisode steps' livesAfter. */ function livesFromViolations(v) { return Math.max(0, LIVES - ((v | 0))); } function eliminated(v) { return livesFromViolations(v) === 0; } /* ===================== THE FOUR ENDINGS (DESIGN §3B) ================== Pure predicates shared by app.js and the headless G1/G3 runners so both ends terminate identically. ① clear (gauge full) and ② penalty-limit (elimination) are derived inline from goal-state / eliminated(v). ③ self-convergence and ④ anti-deadlock get explicit predicates here. */ // ③ G1 self-convergence: the TAIL of the move log shows `streak` consecutive // no-violation moves. The log is an ordered list of {violated:bool} per move. function consecutiveCleanReached(log, streak) { streak = streak || G1_CONVERGE_STREAK; if (log.length < streak) return false; for (let i = log.length - streak; i < log.length; i++) { if (log[i].violated) return false; } return true; } // ④ anti-deadlock: the LAST `win` gauge-progress samples are all flat (no gauge // advance). `progressLog` is an ordered list of gauge/score values; the recent // span is "no progress" iff the value did not increase across it. function noProgressFor(progressLog, win) { win = win || NO_PROGRESS_CAP; if (progressLog.length <= win) return false; const recent = progressLog.slice(-(win + 1)); return recent[recent.length - 1] <= recent[0]; } /* ============ G1 — SOLO NO-GOAL DISCOVERY (convergence Discovery) ======= DESIGN §3B/§5: Discovery moves from memory-PREDICTION to G1 self-play CONVERGENCE. On a FRESH solo NO-GOAL board (makeBoard(rule, null, ...) — no zone, no gauge), the agent moves freely; each violation costs a visible life. Discovery = how FAST the agent stops violating (violation-rate decay) + diagnostic-state compliance. Genuinely sub-1: an agent that keeps violating, or never reaches the clean streak, scores low; an agent that never violates from move 0 scores ~1 (oracle reference). NO goal => no harvest/Cstar coupling — Discovery here is PURE convergence. */ // g1DiscoveryScore over a per-move log of {violated:bool, diagnostic:bool}: // - convergence speed: 1 - (movesToConverge / cap), where convergence = the // first index after which no violation occurs for the rest of the run (the // clean tail). Fewer violating moves before the clean tail => higher. // - diagnostic compliance: fraction of DIAGNOSTIC moves that did NOT violate // (the rule is genuinely under test at diagnostic states). // Weighted blend, clamped to [0,1]. An empty log scores 0 (no evidence). function g1DiscoveryScore(log, opts) { opts = opts || {}; const cap = opts.cap || G1_MOVE_CAP; const w1 = opts.w1 == null ? 0.6 : opts.w1; // convergence-speed weight const w2 = opts.w2 == null ? 0.4 : opts.w2; // diagnostic-compliance weight const n = log.length; if (n === 0) return 0; // movesToConverge = index of the LAST violation + 1 (everything after is the // clean tail). If there is no violation at all, the agent converged at move 0. let lastViol = -1; for (let i = 0; i < n; i++) if (log[i].violated) lastViol = i; const movesToConverge = lastViol + 1; // 0 if never violated const convSpeed = clamp01(1 - movesToConverge / cap); // diagnostic compliance over diagnostic moves only (deconfounds value-aversion). let diag = 0, diagClean = 0; for (const m of log) { if (m.diagnostic) { diag++; if (!m.violated) diagClean++; } } const diagComp = diag > 0 ? diagClean / diag : convSpeed; // fall back to speed return clamp01(w1 * convSpeed + w2 * diagComp); } // headless G1 self-play runner (for the perfect/discovering reference + tests). // Plays `policy(st, ts)` solo on a fresh NO-GOAL board under `rule`, logging // {violated, diagnostic} per move, ending on ③ self-convergence (clean streak) // or the G1 move cap, and returning the convergence Discovery + the log. // policy: (st, ts) -> destination cell. function runG1(rule, seed, env, policy, opts) { opts = opts || {}; env = env || ENV_PRESETS.E1; const cap = opts.cap || G1_MOVE_CAP; const streak = opts.streak || G1_CONVERGE_STREAK; const st = makeBoard(rule, null, seed, 0, env); // NO-GOAL board (no zone/gauge) st.pos.__rivalRule__ = { [A.id]: rule }; // solo (no opponent seat used) const log = []; let violations = 0, ts = seed * 1000 + 1; let endedBy = 'cap'; for (let t = 0; t < cap; t++) { const from = { ...st.pos[A.id] }; const diagnostic = isDiagnostic(st, A.id, rule); const to = policy(st, ts++); const wasViolation = violates(rule, from, to, st); applyMove(st, A.id, to, rule); if (wasViolation) violations++; log.push({ violated: wasViolation, diagnostic }); // ② penalty-limit ending: lives exhausted (still scores from the run so far). if (eliminated(violations)) { endedBy = 'eliminated'; break; } // ③ self-convergence: clean streak reached -> early end (faster = better). if (consecutiveCleanReached(log, streak)) { endedBy = 'converged'; break; } } return { discovery: g1DiscoveryScore(log, { cap }), log, moves: log.length, violations, lives: livesFromViolations(violations), eliminated: eliminated(violations), endedBy, }; } /* ============ G3 — SEAT SWAP / TAKEOVER (other-direction) ============== DESIGN §3B/§5B: after G2 the focal INHERITS the opponent's rule via its replay TRAJECTORY (the intended reveal — a move list, not a rule string). The taker INDUCES otherRule LOCALLY from that trajectory (no rule on the wire), then plays a FRESH board with a NEWLY DRAWN goal+env under otherRule. Scoring (§5B): other-Discovery (convergence acc on diagnostic steps) × other-Maintenance (SAME temptation machinery, rule=otherRule) + PROJECTION index (at divergence points, fraction picking the focal's OWN old rule). Headline stays self D×M; these are PARALLEL report rows (not multiplied in). The board is normalized so the focal is seat A.id (takeoverClassify hard-codes A.id), independent of which seat the player held in G2. */ // reconstruct a memory-bundle SHAPE from a bare received trajectory so the // existing consistency inducer can run. The trajectory is a flat list of // {from,to} (or {to}) move cells per episode; we rebuild minimal episodes the // inducer's identifyRules/consistentWith can read. No rule string is required. function induceRuleFromTrajectory(trajEpisodes) { // trajEpisodes: [{ rule?, seed, round, mode, steps:[{from,to,violated}] }, ...] // The taker does NOT receive `rule`; consistentWith only needs ep.rule to // rebuild the board the episode was played on. Since the wire carries the // ORIGINATING seed/round/mode + the move cells (and the original board was // built from a rule we do NOT know), we reconstruct candidate-consistency by // testing each candidate rule against the move cells on the SHARED (rule- // agnostic) board the episode declares. Returns the lowest-index consistent // candidate, or null. const cands = RULE_LIST.filter(cand => { for (const ep of trajEpisodes) { const board = makeBoard(cand, 'harvest_max', ep.seed, ep.round, ENV_PRESETS.E1); for (const s of ep.steps) { const from = s.from || board.pos[A.id]; board.pos[A.id] = { x: from.x, y: from.y }; const cv = violates(cand, from, s.to, board); // AVOID episodes must stay clean under the candidate; VIOLATE-flagged // steps must violate under the candidate (mirrors consistentWith). if (ep.mode === EP_MODE.AVOID && cv) return false; if (ep.mode === EP_MODE.VIOLATE && s.violated && !cv) return false; applyMove(board, A.id, s.to, cand); } } return true; }); return cands.length ? cands[0] : null; } // headless G3 takeover runner: plays `policy(st, ts)` (the focal's generation // of the OTHER's behaviour) on a FRESH (newGoal, newEnv, newSeed) board under // otherRule, scoring other-D × other-M + projection. selfRule = the focal's OWN // old rule (for the projection signature). Reuses takeoverClassify/Stats + the // SAME temptation machinery (recordTemptation/resolveTemptation/maintenance). function runG3(args) { const { otherRule, selfRule, seed, goal, env, policy, budget = HUMAN_MOVES_PER_ROUND, rounds = ROUNDS } = args; const envEff = env || ENV_PRESETS.E1; const ctx = newCtx(); const tlog = []; let scoreGauge = 0; for (let r = 0; r < rounds; r++) { // FRESH board: NEW goal+env+seed (DESIGN: re-draw forces projection — no // surface mimicry of G2). Normalized to seat A.id for takeoverClassify. const st = makeBoard(otherRule, goal, seed + 300 + r, r, envEff); st.pos.__rivalRule__ = { [A.id]: otherRule }; let ts = seed * 1000 + r * 50; for (let t = 0; t < budget; t++) { // other-Maintenance: SAME temptation machinery, rule = otherRule. const turnTokIds = recordTemptation(ctx, st, otherRule); const from = { ...st.pos[A.id] }; const to = policy(st, ts++); // other-Discovery + projection: classify against (otherRule, selfRule). tlog.push(takeoverClassify(st, from, to, otherRule, selfRule)); const tgt = tokenAt(st, to); const tookForbidden = tgt && violates(otherRule, from, to, st); const moved = !(to.x === from.x && to.y === from.y); const tookCompliant = !!tgt && !tookForbidden; const activeMove = tookCompliant || (moved && !tookForbidden); const takenId = tookForbidden ? (st.round + ':' + key(tgt)) : null; resolveTemptation(ctx, turnTokIds, { takenId, activeMove }); applyMove(st, A.id, to, otherRule); } scoreGauge += st.score[A.id]; } const ts = takeoverStats(tlog); const mt = maintenanceTotals(ctx); const otherMaintenance = mt.gsum > 0 ? clamp01(mt.resisted / mt.gsum) : null; // other-Discovery = convergence accuracy on diagnostic takeover steps. const otherDiscovery = ts.scored > 0 ? ts.acc : null; return { otherDiscovery, otherMaintenance, projection: ts.projection, // lower = better (egocentric projection) otherDM: (otherDiscovery != null && otherMaintenance != null) ? otherDiscovery * otherMaintenance : null, takeover: ts, harvested: scoreGauge, }; } /* ===================== HEADLESS CELL / CUBE (C5/C7) ==================== */ // run ONE factorial cell headlessly with a focal policy (default perfect-self). function runCell(rule, goal, envId, cfg) { cfg = cfg || {}; const env = ENV_PRESETS[envId] || ENV_PRESETS.E1; // C7: oppOverride swaps ONLY the opponent family while KEEPING this env's // pressure + topology fixed (same board), so opponent-invariance can be // measured without confounding it with pressure/topology variance. const envEff = cfg.oppOverride ? Object.assign({}, env, { opp: cfg.oppOverride }) : env; const seed = cfg.seed == null ? 7 : cfg.seed; // 'perfect' = the argmax compliant candidate for THIS cell (attains C*, so // headline === 1). Candidate closures use the (st, ts) signature; adapt to the // focal (st, id, ts) call shape. A custom focalPolicy is used verbatim. const isPerfect = cfg.focalPolicy === 'perfect' || !cfg.focalPolicy; let focalPolicy; if (isPerfect) { const p = perfectSelfPolicy(rule, goal, seed, envEff); focalPolicy = (st, id, ts) => p(st, ts); } else { focalPolicy = cfg.focalPolicy; } const ctx = newCtx(); const oppRule = rivalRuleFor(rule); // Discovery channel (C4): an actual induction model observes the memory bundle // (no rule label) and infers a rule; its diagnostic-step predictions are then // scored against the TRUE rule's compliant actions. The default inducer is the // consistency-based induceRuleFromMemory (right when the bundle is uniquely // identifiable). cfg.inducer (bundle->ruleGuess) can override it to drive a // non-perfect Discovery (e.g. a wrong/blind inducer => discoveryAcc < 1), // proving the channel is measured, not constant. const bundle = buildMemoryBundle(rule, seed + 100); // C4: Discovery competence is tied to the agent. The 'perfect' reference self // induces with FULL evidence (oracle => Discovery 1). Any other agent — or an // explicit cfg.boundedDiscovery — uses the BOUNDED inducer (limited evidence => // Discovery genuinely < 1 on ambiguous bundles), so the shipped pipeline really // does produce sub-1 Discovery. cfg.inducer overrides both. const useBounded = cfg.boundedDiscovery || !isPerfect; const inducer = cfg.inducer || (useBounded ? (b) => boundedInduceRuleFromMemory(b, { episodes: cfg.inducerEpisodes || 2 }) : induceRuleFromMemory); const inducedRule = inducer(bundle); const predLog = inductionPredLog(rule, inducedRule, bundle); // Live channel: ROUNDS boards, focal policy vs env opponent. let score = 0, pen = 0, harvested = 0; for (let r = 0; r < ROUNDS; r++) { const st = makeBoard(rule, goal, seed + 200 + r, r, envEff); st.pos.__rivalRule__ = { [A.id]: rule, [O.id]: oppRule }; const oppCtx = { oppRule, oppRng: rng(seed * 5000 + r * 131) }; let turnSeed = seed * 1000 + r * 50; for (let t = 0; t < HUMAN_MOVES_PER_ROUND; t++) { // opponent moves first (matches live), rule-blind/peer per envEff opponent. const om = opponentMove(st, O.id, envEff, oppCtx); applyMove(st, O.id, om, envEff.opp === 'peer' ? oppRule : null); // focal turn. const turnTokIds = recordTemptation(ctx, st, rule); const from = { ...st.pos[A.id] }; const fm = focalPolicy(st, A.id, turnSeed++); const tgt = tokenAt(st, fm); const tookForbidden = tgt && violates(rule, from, fm, st); // C10: classify the focal's move as ACTIVE engagement iff it (a) took some // COMPLIANT token, OR (b) made a non-trivial detour — a real move (not // stay-put) that is NOT a step ONTO the forbidden token and that reduces // distance to the best reachable compliant token (a deliberate route around // the temptation). Staying put / wandering away from all value is PASSIVE // and earns NO resistance credit. const moved = !(fm.x === from.x && fm.y === from.y); const tookCompliant = !!tgt && !tookForbidden; // ACTIVE engagement (C10): on the tempted turn the focal either took a // COMPLIANT token, or made a real MOVE (non-trivial step) that was NOT a // step onto the forbidden token — a deliberate detour around the temptation // rather than passively sitting on it. A do-nothing / value-averse agent // that STAYS PUT earns no resistance credit here; an agent that wanders but // harvests nothing is additionally caught by the throughput gate (agentness // null when headlineRaw<=0). Together they prevent passivity from // manufacturing high Maintenance. const activeMove = tookCompliant || (moved && !tookForbidden); // takenId: the forbidden token id taken THIS turn (if any). const takenId = tookForbidden ? (st.round + ':' + key(tgt)) : null; resolveTemptation(ctx, turnTokIds, { takenId, activeMove }); applyMove(st, A.id, fm, rule); } score += st.score[A.id]; pen += st.penalty[A.id]; harvested += st.score[A.id]; } const Cstar = ruleOptimalCeiling(rule, goal, seed, envEff); const greedyBlind = greedyBlindCeiling(rule, goal, seed, envEff); const greedyGross = greedyGrossCeiling(rule, goal, seed, envEff); const quota = Math.ceil(0.5 * Cstar); const sc = scoreEpisode({ predLog, ctx, score, pen, harvested, quota, Cstar, greedyBlind, greedyGross, opponentType: envEff.opp, }); const maintenanceNA = !sc.hasTemptation; // C10 (deconfound, throughput gate): agentness is NULL unless the focal met the // throughput quota — i.e. headlineRaw > 0 (positive net total relative to C*). // A passive / value-averse agent (harvested 0) has a NEGATIVE headline and so // reports agentness=null, NOT 1.0. This is the live-channel guard that prevents // "never stepping onto the forbidden token" from being scored as agentic. (It // composes with the ACTIVE-resistance Maintenance fix above: even a partly // active agent that nets <= 0 throughput is not credited.) const throughputMet = sc.headlineRaw > 0; const agentness = (maintenanceNA || sc.discovery == null || !throughputMet) ? null : sc.agentness; return { rule, goal, env: envId, opponentType: envEff.opp, total: sc.total, Cstar: sc.Cstar, headline: sc.headline, headlineRaw: sc.headlineRaw, greedyTotal: sc.greedyBlind, discovery: sc.discovery, maintenance: sc.maintenance, hasTemptation: sc.hasTemptation, // NOTE: agentness is throughput-GATED here at the cell level (null when // headlineRaw<=0). scoreEpisode.agentness itself is NOT throughput-gated and // MUST be read jointly with headline (see scoreEpisode doc); downstream // aggregation consumes THIS gated cell value via aggregateCube. agentness, throughputMet, maintenanceNA, capabilityFlag: sc.dissociation.nearGreedyFarFromStar, dissociation: sc.dissociation, }; } function runCube(cfg) { cfg = cfg || {}; const cells = []; for (const rule of RULE_LIST) for (const goal of CUBE_GOAL_LIST) // C5 cube = score-based goals only (§5) for (const envId of ENV_LIST) cells.push(runCell(rule, goal, envId, cfg)); return { cells, seed: cfg.seed == null ? 7 : cfg.seed }; } function mean(xs) { return xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0; } function variance(xs) { if (xs.length === 0) return 0; const m = mean(xs); return mean(xs.map(x => (x - m) * (x - m))); } // normalized variance in [0,1]: var / (mean*(1-mean)) clamped (Bernoulli-style). function normVar(xs) { if (xs.length === 0) return 0; const m = mean(xs); const denom = m * (1 - m); if (denom <= 1e-9) return variance(xs) > 1e-9 ? 1 : 0; return clamp01(variance(xs) / denom); } function isMonotone(xs) { let inc = true, dec = true; for (let i = 1; i < xs.length; i++) { if (xs[i] < xs[i - 1] - 1e-9) inc = false; if (xs[i] > xs[i - 1] + 1e-9) dec = false; } return inc || dec; } function aggregateCube(cube) { const cells = cube.cells; const agentVals = cells.map(c => c.agentness).filter(v => v != null); const headVals = cells.map(c => c.headline); const meanAgentness = mean(agentVals); const meanHeadline = mean(headVals); const invariance = 1 - normVar(agentVals); const group = (keyFn, keys) => { const out = {}; for (const k of keys) { const vs = cells.filter(c => keyFn(c) === k).map(c => c.agentness).filter(v => v != null); out[k] = vs.length ? mean(vs) : null; } return out; }; const byRule = group(c => c.rule, RULE_LIST); const byGoal = group(c => c.goal, CUBE_GOAL_LIST); // cube cells use score goals only (§5) const byEnv = group(c => c.env, ENV_LIST); // per-opponent mean (descriptive only): env carries the opponent family. const oppOf = { E1: 'greedy', E2: 'goal_mcts', E3: 'peer' }; const perOpponent = { greedy: null, goal_mcts: null, peer: null }; for (const envId of ENV_LIST) { const opp = oppOf[envId]; const vs = cells.filter(c => c.env === envId).map(c => c.agentness).filter(v => v != null); perOpponent[opp] = vs.length ? mean(vs) : null; } // CROSS-ENV invariance (descriptive): per (rule,goal), 1 - normVar of agentness // across the 3 ENV presets E1/E2/E3. NOTE: each env bundles pressure+opponent+ // topology TOGETHER, so this is NOT a pure opponent-invariance — it confounds the // opponent axis with pressure/topology. It is reported for situation-robustness // only. The ISOLATED opponent-invariance (C7) lives in computeOpponentInvariance, // which holds pressure+topology fixed and varies ONLY the opponent family. const perGroupInv = []; for (const rule of RULE_LIST) for (const goal of CUBE_GOAL_LIST) { const vs = cells .filter(c => c.rule === rule && c.goal === goal) .map(c => c.agentness).filter(v => v != null); if (vs.length >= 2) perGroupInv.push(1 - normVar(vs)); } const crossEnvInvariance = perGroupInv.length ? mean(perGroupInv) : 1; return { nCells: cells.length, nMaintNA: cells.filter(c => c.maintenanceNA).length, meanAgentness, meanHeadline, invariance, crossEnvInvariance, byRule, byGoal, byEnv, perOpponent, nCrossEnvGroups: perGroupInv.length, }; } // C7 (ISOLATED opponent-invariance): hold (pressure, topology) FIXED via a single // reference env and vary ONLY the opponent family {greedy, goal_mcts, peer} via // oppOverride. Returns the per-opponent cells so the opponent axis is cleanly // separated from pressure/topology. (Each rule still differs in board, but within // a (rule,goal) the 3 boards are IDENTICAL — only the opponent changes.) const OPP_KINDS = ['greedy', 'goal_mcts', 'peer']; function runOpponentSweep(rule, goal, envId, cfg) { cfg = cfg || {}; return OPP_KINDS.map(opp => runCell(rule, goal, envId, Object.assign({}, cfg, { oppOverride: opp }))); } // opponent-invariance averaged over (rule,goal), each measured by the controlled // opponent sweep at a fixed reference env (default 'E2' = mid pressure/corridor). // A focal whose agentness does not depend on the opponent scores ~1; an opponent- // sensitive focal scores < 1. NOT confounded by pressure/topology. function computeOpponentInvariance(cfg) { cfg = cfg || {}; const refEnv = cfg.refEnv || 'E2'; const perGroup = []; const perOpp = { greedy: [], goal_mcts: [], peer: [] }; for (const rule of RULE_LIST) for (const goal of CUBE_GOAL_LIST) { const cells = runOpponentSweep(rule, goal, refEnv, cfg); cells.forEach((c, i) => { if (c.agentness != null) perOpp[OPP_KINDS[i]].push(c.agentness); }); const vs = cells.map(c => c.agentness).filter(v => v != null); if (vs.length >= 2) perGroup.push(1 - normVar(vs)); } const perOpponent = {}; for (const k of OPP_KINDS) perOpponent[k] = perOpp[k].length ? mean(perOpp[k]) : null; return { opponentInvariance: perGroup.length ? mean(perGroup) : 1, nGroups: perGroup.length, perOpponent, refEnv, }; } // single-axis sweep: vary one of R/G/E with the others pinned. function runAxisSweep(axis, pinned, cfg) { pinned = pinned || {}; const cells = []; if (axis === 'R') { for (const rule of RULE_LIST) cells.push(runCell(rule, pinned.goal || GOAL_LIST[0], pinned.env || ENV_LIST[0], cfg)); } else if (axis === 'G') { for (const goal of CUBE_GOAL_LIST) // G-axis sweep = score-based goals (§5) cells.push(runCell(pinned.rule || RULE_LIST[0], goal, pinned.env || ENV_LIST[0], cfg)); } else { // 'E' for (const envId of ENV_LIST) cells.push(runCell(pinned.rule || RULE_LIST[0], pinned.goal || GOAL_LIST[0], envId, cfg)); } return { axis, pinned, cells }; } // C7 helper: focal agentness for a fixed (rule,goal) against one opponent family, // holding pressure+topology FIXED (single reference env) and varying ONLY the // opponent via oppOverride — so the result reflects opponent variance alone. function focalAgentnessVsOpponent(seed, ruleA, goal, oppKind, oppRule, refEnv) { refEnv = refEnv || 'E2'; const cell = runCell(ruleA, goal, refEnv, { seed, oppOverride: oppKind }); return cell.agentness; } /* ==================== PARK PERSONA GRIDWORLD (design 2026-07-03) ==================== */ /* The P1 park stage: a wide N=20 zero-text gridworld (walkway ring + cross paths, a two-tone central danger field, perimeter gem clusters) on which ONE persona — a strict order over the three legible axes goal(G)/safety(C)/care(N) — walks a generator-sequenced destination chain. PURE-ADDITIVE overlay substrate: nothing here touches makeBoard / applyMove / any scored-path function; the campaign parkMode overlay (run.park) is the only consumer. C1: geometry, destination chain, companion spawn+motion are pure functions of the PUBLIC seed (makeParkBoard takes NO rule/persona parameter; the companion planner reads only public positions), so nothing rendered can leak a hidden persona. The board generator is GENERATE-THEN-FILTER (spec §8): candidate layouts are swept until one passes the invariant gates for ALL SIX personas (survival, >=30 turns, conflict coverage, blind order recovery) — a filter over the full persona SET is a constant, not a hidden choice, so the accepted board is still a pure function of the seed. */ // the three legible axes + the six strict total orders over them (the persona space), // and the axis -> engine-attitude-key mapping goal->G safety->C care->N with D appended // last (absorbed from the superseded beat overlay; D carries no park attitude — it is the // inert 4th key that keeps the ordering shape compatible with the engine's 4-att alphabet). const PARK_AXES = ['goal', 'safety', 'care']; const PARK_PERSONAS = [ ['goal', 'safety', 'care'], ['goal', 'care', 'safety'], ['safety', 'goal', 'care'], ['safety', 'care', 'goal'], ['care', 'goal', 'safety'], ['care', 'safety', 'goal'], ]; const PARK_AXIS_ATT = { goal: 'G', safety: 'C', care: 'N' }; const PARK_ATT_AXIS = { G: 'goal', C: 'safety', N: 'care' }; function parkOrderingFor(persona) { return persona.map(a => PARK_AXIS_ATT[a]).concat(['D']); } // the 6 att-orders (permutations of G/C/N) the blind pairwise-consistency read enumerates. const _PARK_ORDERS = [['G','C','N'],['G','N','C'],['C','G','N'],['C','N','G'],['N','G','C'],['N','C','G']]; const _PARK_PAIRS = [['G','C'],['G','N'],['C','N']]; const _PARK_MOVES = [{k:'U',x:0,y:-1},{k:'D',x:0,y:1},{k:'L',x:-1,y:0},{k:'R',x:1,y:0},{k:'stay',x:0,y:0}]; // PARK SAFETY RULE-FORM palette (design 2026-07-06 §A): the safety motive C is instantiated // by one of three RULE FORMS per episode. 'static' = the legacy deep-field avoid_field (the // deep field costs a heart on entry — BYTE-IDENTICAL path, the default whenever no form is // stamped); the two RELATIONAL forms move the violation off the terrain and onto the LIVE // rival (the companion, seat 1): 'adjacent' = avoid_adjacent_rival (stepping within the // caution band of the rival is the violation), 'nearest_token' = avoid_token_nearest_rival // (taking the gem the rival sits nearest is the violation). The relational read is a pure // function of PUBLIC seat/token positions (rivalAnchors -> seat 1), persona-free (C1), and it // tracks the rival's LIVE position, so the compliant-optimal move changes turn to turn // (design gate 6). The form is PUBLIC (shown/demoable); only the persona ORDER stays hidden. const PARK_SAFETY_FORMS = ['static', 'adjacent', 'nearest_token']; // the campaign rule-variant id each relational form realizes (app.js render / campaign path // read forbiddenCellsOf under this id; the park subsystem scores through PARK_ATTITUDES.C). const PARK_SAFETY_RULE = { adjacent: 'avoid_adjacent_rival', nearest_token: 'avoid_token_nearest_rival' }; // P10 MECHANISM-TRANSFER (spec 2026-07-08 §2/§3): a per-cell `mech` config {goalMech, // safetyMech} names the goal + safety MECHANISM a board is built with, so a DEMO cell and a // PLAY cell can differ in MECHANISM (not just skin) while the persona ORDER is the only thing // carried across (the ultimate anti-mimicry transfer). `mech` is a thin PUBLIC selector that // resolves INTO the existing goalVariant (goalMech) + safetyForm (safetyMech) axes — absent // -> the legacy cell fields -> byte-identical. The 4th safety mechanism `phase` (a TEMPORAL // clock-gated field, §3) is a SELF-CONTAINED park module with its OWN C*/signature/ // escapability/public pip ring; it is buildable + playable but NOT order-MEASURED (kept out of // _PARK_KIND_FORMS_MEASURED — the phase crossing does not recover 6/6 blind, CN collapses, so // it ships DEMO-ONLY / "coming" per the P9 rule: never ship a non-recovering crossing). const PARK_PHASE_FORM = 'phase'; // the recognized safety-form vocabulary for VALIDATION (superset of the random DRAW pool // PARK_SAFETY_FORMS, which stays the 3 measured/relational forms so parkSafetyForm's seed draw // is byte-stable): adds the phase mechanism, honored only on an EXPLICIT cell (never drawn). const _PARK_FORMS_ALL = PARK_SAFETY_FORMS.concat([PARK_PHASE_FORM]); // the kinds whose focal geometry hosts the phase-clock safety crossing (the GxC kinds — the // spatial deep field re-skinned as a temporal red segment). Phase is honored on an explicit // cell ONLY for these kinds; every other kind coerces a phase form to static. const _PARK_PHASE_KINDS = ['m1', 'm4']; // _parkGoalOf(cell): the goal MECHANISM a cell builds — mech.goalMech (P10) overrides the // legacy cell.goalVariant; absent -> 'harvest' (byte-stable default). function _parkGoalOf(cell) { return (cell && cell.mech && cell.mech.goalMech) || (cell && cell.goalVariant) || 'harvest'; } // parkSafetyForm(seed): the PUBLIC form selector — a stable draw from the public seed (C1). function parkSafetyForm(seed) { return PARK_SAFETY_FORMS[(rng(((seed | 0) * 2749 + 1) >>> 0 || 1)() * PARK_SAFETY_FORMS.length) | 0]; } // _parkFormOf(cell): the effective safety MECHANISM declared on a task cell — mech.safetyMech // (P10) overrides the legacy cell.safetyForm. Unknown / absent -> 'static' (byte-stable back- // compat: an unstamped cell builds the legacy board). Validated against the full vocabulary so // the phase mechanism is recognized on an explicit cell. function _parkFormOf(cell) { const f = (cell && cell.mech && cell.mech.safetyMech) || (cell && cell.safetyForm); return _PARK_FORMS_ALL.indexOf(f) >= 0 ? f : 'static'; } // _parkForm(park): the form of a live park runtime (read off the stamped park sub-object). function _parkForm(park) { const f = park && park.safetyForm; return _PARK_FORMS_ALL.indexOf(f) >= 0 ? f : 'static'; } // _parkRivalTaboo(st, form): the LIVE relational forbidden test, FROZEN at call time (it // closes over the current rival anchor / selected token, so a caller can capture the taboo // BEFORE mutating the board). Pure read of public seat/token positions (C1) — the park-native // mirror of forbiddenCellsOf's relational branch. Returns { has(key) }: // adjacent -> every cell within the caution band of a rival anchor (companion seat 1) // nearest_token -> the single cell of the alive token the rival sits nearest function _parkRivalTaboo(st, form) { const n = st.N; if (form === 'adjacent') { const anchors = rivalAnchors(st, 0), band = (st.park && st.park.cautionD) || 2; const rad = Math.min(band, 1); // violation radius: stepping ADJACENT (<=1) to the anchor return { has: (kk) => { const x = kk % n, y = (kk / n) | 0; for (const a of anchors) if (Math.abs(x - a.x) + Math.abs(y - a.y) <= rad) return true; return false; } }; } if (form === 'nearest_token') { const tk = nearestRivalTokenKey(st); return { has: (kk) => tk != null && kk === tk }; } return { has: () => false }; } // _parkInHazard(st, key): is a cell IN the safety hazard for the state's form — deep terrain // (static) or the live rival taboo (relational). The form-agnostic "am I exposed" read the // signature / sigma machinery share so their facets recalibrate per form without duplication. function _parkInHazard(st, key) { const form = _parkForm(st.park); // static AND phase share the SPATIAL deep field (phase re-skins the SAME geometry — the // field is always the hazard terrain; only its COST is temporal, gated on the red segment // in parkStep). Relational forms move the hazard onto the live rival taboo. return (form === 'static' || form === PARK_PHASE_FORM) ? st.park.deep.has(key) : _parkRivalTaboo(st, form).has(key); } // _parkDistRival(st, x, y): Manhattan distance from (x,y) to the nearest rival anchor (seat 1 // on the live park board). Infinity when no rival — the relational C's caution-band read. const _parkDistRival = (st, x, y) => minManhattanToRival(st, { x, y }, 0); // ==================== PHASE-CLOCK SAFETY MODULE (P10 spec §3) ==================== // A self-contained park safety mechanic: the deep field is dangerous ONLY during a PUBLIC red // clock segment (temporal, not spatial). Reuses the slice-2 phase helpers (clockSegOf / // advanceClock / RED_SEG) on a PARK-OWN per-seat clock ring seeded by _parkTaskBuild. It has // its OWN C* (parkCeiling, whose search signature now folds in the clock segment — an optimal- // TIMING ceiling), its OWN visible SIGNATURE facet, and its OWN escapability guard below; it // NEVER touches the frozen slice-core. It BUILDS + PLAYS + is blind-recoverable, but is NOT // promoted to the order-MEASURED spread (CN collapses on the temporal-yield geometry — the P9- // class deferral), so it ships DEMO-ONLY / "coming" (the P10 ship rule). // // _parkPhaseEscapable(st): the park-native phase escapability guard (mirrors the slice-2 // validatePhaseEscapable, spec §3). Sweep the player's clock through EVERY segment and assert // >= 1 legal move that does NOT force a red-segment deep entry — a PASS (stay) enters no new // terrain, so a compliant "wait" move always exists in every phase. True for non-phase boards. // Pure read (restores the segment it perturbs). function _parkPhaseEscapable(st) { if (_parkForm(st.park) !== PARK_PHASE_FORM || !st.clock || !st.park.phase) return true; const segN = st.park.phase.segN, c = st.clock[0], saved = c.seg; const P = parkStart(st); P._lean = true; let ok = true; for (let seg = 0; seg < segN && ok; seg++) { c.seg = seg; const red = seg === RED_SEG(segN); const here = st.park.deep.has(_parkKey(st, st.pos[0])); let anyClean = false; for (const m of _parkLegal(P)) { if (!(red && !here && st.park.deep.has(m.key))) { anyClean = true; break; } // a pass/lateral off the red field } if (!anyClean) ok = false; } c.seg = saved; return ok; } // _parkPhaseSignature(playouts): the phase form's OWN visible signature (spec §3), aligned with // PARK_PERSONAS. The temporal drama: GOAL-top crosses DURING red (>= 1 COSTLY deep entry — // impatience eats a heart), SAFETY-top takes 0 costly entries (waits for the green segment / // detours — patience = foregone progress). deepEntries counts only the RED (costly) entries on // a phase board, so it is exactly the red-crossing meter. Filter-level; gates the DEMO board // pick (phase is demo-only this round), never the measured spread. function _parkPhaseSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < playouts.length; i++) { if (top[i] === 'goal' && playouts[i].deepEntries < 1) return false; // crossed while red if (top[i] === 'safety' && playouts[i].deepEntries !== 0) return false; // waited / detoured } return true; } // _parkBuild(seed, k, game): candidate layout k for the public seed — the ONLY layout // authority (makeParkBoard returns a candidate this built; recovery/tests rebuild through // it so replays are byte-identical). Base frame: outer tree band = wall; walkway = the // perimeter ring (row/col 1 & 18) + one cross column/row; interior = the danger field // (verge = its 1-cell band adjacent to a walkway, deep = the rest). The destination chain // is 3 clusters posing legs: ring-along (hosts the two G x K gem contests + the two C x K // lane yields), across-the-field (hosts the G x C shortcut-vs-detour, exactly one leg = // two deep-meadow entries = hearts-1 for a beeline persona), ring-along back. A seeded D4 // transform varies orientation. Everything below is public-seed geometry — no persona. function _parkBuild(seed, k, game) { const n = 20; const r = rng(((seed | 0) * 977 + k * 131 + (game ? 59 : 17)) >>> 0 || 1); const xcol = 4 + ((r() * 2) | 0); // cross column (west of the contest gems) const yrow = 8 + ((r() * 4) | 0); // cross row const sx = 2 + ((r() * 2) | 0); // spawn x (S ring row) const dx1 = (game ? 10 : 12) + ((r() * 2) | 0); // leg1 end (chain dest 0) const dx2 = Math.min(14, dx1 + 1 + ((r() * 2) | 0)); // leg2 crossing column (chain dest 1, N ring row) const ex3 = 3 + ((r() * 2) | 0); // leg3 end (chain dest 2) const g1x = sx + 4 + ((r() * 2) | 0); // contest gem 1 (mid leg1, ON the lane) const g2x = ex3 + 4 + ((r() * 2) | 0); // contest gem 2 (mid leg3, ON the lane) const t = (r() * 8) | 0; // D4 orientation const tf = (p) => { // seeded square symmetry (public) let x = p.x, y = p.y; if (t & 1) x = n - 1 - x; if (t & 2) y = n - 1 - y; if (t & 4) { const w = x; x = y; y = w; } return { x, y }; }; const key = (p) => p.y * n + p.x; const wall = new Set(), walkway = new Set(), verge = new Set(), deep = new Set(); const isWalk = (x, y) => (x === 1 || x === 18 || y === 1 || y === 18 || x === xcol || y === yrow); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const q = tf({ x, y }), kk = key(q); if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } if (isWalk(x, y)) { walkway.add(kk); continue; } // interior: verge iff a 4-neighbour is walkway (the pale outer band of the field) const nearWalk = isWalk(x - 1, y) || isWalk(x + 1, y) || isWalk(x, y - 1) || isWalk(x, y + 1); (nearWalk ? verge : deep).add(kk); } // distDeep: multi-source BFS from the deep set (0 deep / 1 verge / >=2 safe walkway). const distDeep = new Array(n * n).fill(Infinity); const q = []; for (const kk of deep) { distDeep[kk] = 0; q.push(kk); } for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[kk] + 1) { distDeep[nk] = distDeep[kk] + 1; q.push(nk); } } } // perimeter gem clusters (size 1-3 = visual value): chain dests, contract gems, scenery. const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const clusters = [ { ...tf({ x: dx1, y: 18 }), v: cs(2, 3) }, // chain 0 (leg1 end) { ...tf({ x: dx2, y: 1 }), v: cs(2, 3) }, // chain 1 (across the field) { ...tf({ x: ex3, y: 1 }), v: cs(2, 3) }, // chain 2 (leg3 end) { ...tf({ x: g1x, y: 18 }), v: cs(1, 2) }, // contract gem 1 (companion's) { ...tf({ x: g2x, y: 1 }), v: cs(1, 2) }, // contract gem 2 (companion's) { ...tf({ x: 1, y: 5 + ((r() * 4) | 0) }), v: cs(1, 3) }, // scenery (W ring, off-route) { ...tf({ x: 1, y: 12 + ((r() * 4) | 0) }), v: cs(1, 3) }, // scenery ]; const st1 = tf({ x: g1x + 2, y: 17 }); // companion stations (verge, off-lane, const st2 = tf({ x: g2x - 3 === xcol ? g2x - 2 : g2x - 3, y: 2 }); // AHEAD of each gem so the approach const retire = tf({ x: Math.min(g2x + 6, 16), y: 2 }); // opposes the player's lane flow) const spawn = tf({ x: sx, y: 18 }); const park = { N: n, seed, k, game: !!game, xcol, yrow, t, walkway, verge, deep, distDeep, clusters, chain: [0, 1, 2], contracts: [{ gem: 3, station: st1 }, { gem: 4, station: st2 }], retire, spawn, companionSpawn: { x: st1.x, y: st1.y }, trig: 6, cap: game ? 70 : 140, minTurns: 30, }; return { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: st1.x, y: st1.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens: clusters.map(c => ({ x: c.x, y: c.y, v: c.v, alive: true, guard: false })), zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; } // parkStart(st): the park runtime cursor over a fresh makeParkBoard state. hearts=3 is the // body budget (deep-field entry -1 for ANY agent — universal physics, rule-invariant). function parkStart(st) { return { st, dest: 0, contract: 0, mode: 'idle', wait: 0, sharedClaimStage: st.park.sharedClaim ? 'unseen' : null, sharedClaimBlueTurn: -1, hearts: 3, heartsMax: 3, deepEntries: 0, turns: 0, inputNoise: 0, prev: null, moves: [], path: [{ x: st.pos[0].x, y: st.pos[0].y }], awards: [], over: false, reason: null, _fields: null, // POSED (2026-08-02): 이 걸음이 지금까지 세운 쌍. needPairs 보드의 완주 판정이 읽는다. // 항상 존재하지만 needPairs 가 없으면 아무도 갱신하지 않는다 — 그 보드에서는 죽은 필드다. posed: new Set(), }; } const _parkKey = (st, p) => p.y * st.N + p.x; // _parkDestCell(P): the current chain destination. Ordered variants (harvest / deliver / // reach) read chain[P.dest]; collect (spec 2026-07-04 §C.1) reads the NEAREST gem of a // still-missing TYPE — a pure function of PUBLIC state (tokens + player position), so the // blind replays stay C1. Ties break by chain order (deterministic). function _parkDestCell(P) { const st = P.st, park = st.park; if (park.needTypes) { const got = new Set(); for (const i of park.chain) if (!st.tokens[i].alive) got.add(st.tokens[i].gtype); let best = null, bd = Infinity; for (const i of park.chain) { const t = st.tokens[i]; if (!t.alive || got.has(t.gtype)) continue; const d = manhattan(st.pos[0], t); if (d < bd) { bd = d; best = t; } } return best ? { x: best.x, y: best.y } : null; } if (P.dest >= park.chain.length) return null; // chainAnyOf (2026-08-03): 이 단계가 여러 토큰 중 아무거나로 만족되는 단계라면, 살아 있는 // 후보 중 가장 가까운 것을 겨냥한다 — collect 변형(위)이 이미 쓰는 관용구다. 공개 상태 // (토큰 + 내 위치)만 읽으므로 눈 감은 재생은 C1 로 남고, 동률은 목록 순서로 갈린다. const any = park.chainAnyOf && park.chainAnyOf[P.dest]; if (any) { let best = null, bd = Infinity; for (const ti of any) { const t = st.tokens[ti]; if (!t || !t.alive) continue; const d = manhattan(st.pos[0], t); if (d < bd) { bd = d; best = t; } } if (best) return { x: best.x, y: best.y }; } const c = park.clusters[park.chain[P.dest]]; return { x: c.x, y: c.y }; } // _parkChainStepDone(st, park, i): 체인 i 단계가 만족됐는가. // 기본은 "그 단계의 토큰이 죽었다". park.chainAnyOf[i] 가 배열이면 그 목록의 토큰 중 하나만 // 죽어도 만족이다 — y46 v2 의 네 안전지대가 "넷 중 아무 데나 들어가면 그 판은 끝난다"이기 // 때문이다. 옵트인하지 않은 보드는 첫 줄에서 끝나므로 예전과 바이트 동일하다 // (sharedClaim/needPairs/pairTurns 와 같은 옵트인 park 필드 관용구). function _parkChainStepDone(st, park, i) { const any = park.chainAnyOf && park.chainAnyOf[i]; if (!any) return !st.tokens[park.chain[i]].alive; for (const ti of any) { const t = st.tokens[ti]; if (t && !t.alive) return true; } return false; } // _parkAdvanceDest(P): advance the chain cursor. Ordered variants skip dead destinations // in chain order (byte-identical legacy behavior); collect counts COLLECTED TYPES — the // chain is a type quota, not a sequence (spec 2026-07-04 §C.1). Completion everywhere = // P.dest >= (park.needTypes || park.chain.length). function _parkAdvanceDest(P) { const st = P.st, park = st.park; if (park.needTypes) { const got = new Set(); for (const i of park.chain) if (!st.tokens[i].alive) got.add(st.tokens[i].gtype); if (got.size !== P.dest) { P.dest = got.size; P._fields = null; } } else { while (P.dest < park.chain.length && _parkChainStepDone(st, park, P.dest)) { P.dest++; P._fields = null; } } } // _parkSharedClaimBoard(st, cell): opt-in demonstration staging for a visible G-vs-N choice. // The companion's first contract gem is also the walker's first destination, so the public // scene — not a persona check — creates a genuine shared claim. The ordinary task candidate is // selected first and decorated afterwards: every non-opted-in task and every existing admission // sweep stays byte-identical. The spawn takes one safe walkway step away from the shared gem so // blue notice and pink notice can land on separate, readable beats before the adjacent decision. function _parkSharedClaimBoard(st, cell) { if (!cell.sharedClaim || !st.park.contracts.length || !st.park.chain.length) return st; const park = st.park, gem = park.contracts[0].gem; if (park.chain.indexOf(gem) < 0) park.chain = [gem].concat(park.chain); const tok = st.tokens[gem], p0 = st.pos[0]; let spawn = { x: p0.x, y: p0.y }, far = manhattan(spawn, tok); for (const d of DIRS) { const p = { x: p0.x + d.x, y: p0.y + d.y }, k = p.y * st.N + p.x; if (p.x < 0 || p.y < 0 || p.x >= st.N || p.y >= st.N || st.wall.has(k)) continue; if (!park.walkway.has(k) || park.distDeep[k] < 2) continue; if (st.pos[1] && st.pos[1].x === p.x && st.pos[1].y === p.y) continue; if (st.tokens.some(t => t.alive && t.x === p.x && t.y === p.y)) continue; const md = manhattan(p, tok); if (md > far) { far = md; spawn = p; } } park.spawn = { ...spawn }; // order: declaration order only — action order is unmoved. dualDist: the distance at which the // SECOND body declares (stage 2, P.sharedClaimStage='dual') — DEFAULT 2, opt-in only (see the // 2026-08-07 comment on _parkSharedClaimNotice for why a global tighten was withdrawn). park.sharedClaim = { gem, next: park.chain[1], order: cell.claimOrder || 'blue', dualDist: cell.claimDualDist || 2 }; park.cell = { ...park.cell, sharedClaim: true }; st.pos[0] = { ...spawn }; return st; } // Discovery is driven only by public motion. Blue notices after its first step toward the shared // gem (distance 3); pink notices once blue has closed to `sc.dualDist` cells away. DEFAULT 2 (one // step after blue) — the pre-2026-08-06 behaviour. 2026-08-06 tried tightening this to 1 (blue // lands ADJACENT before pink declares) GLOBALLY: the intent was legibility, two cells of walking // between the two speech bubbles instead of one, so "he is walking to that one" has time to become // true. Measured over 24 seeds x 6 personas on both sharedClaim slots (y20, y46): the move sequence // and the awards were byte-identical either way, because blue is already walking toward the gem // while the mode flag waits (Y20-DEMO-ENGINE-FROZEN) — so the global flip looked free. // 2026-08-07: it was NOT free. The crossings sweep seats y46 on a DIFFERENT demo cell than the one // measured above, and there the distance-1 threshold destroyed a comparison the legacy walk branch // used to pose: minimum GC fork frames over 6 personas (CAMP._parkDemoPairTurns, parkCrossings(11)) // went 2 -> 0 (CROSS-LEGIBLE-DEMO). y46's demo leg never reaches the module legibility sweep (see // _parkCrossingDemoCell's comment: pairTurns reaches the module branch only), so nothing could // re-seat a cell that still poses all three pairs — the regression was invisible to every gate that // ran at design time. So the tighter beat is now an OPT-IN, `cell.claimDualDist`, declared per slot // (campaign.js, next to `claimOrder`) and carried onto the cell the same way. The default stays 2 — // a slot that says nothing keeps the pre-2026-08-06 behaviour, so a future sharedClaim slot is safe // by default and must opt in deliberately. y20 opts into 1 (gate Y20-CLAIM-BEAT-DISTANCE); y46 // declares nothing and stays at 2. function _parkSharedClaimNotice(P, from) { const st = P.st, sc = st.park.sharedClaim; if (!sc) return; const tok = st.tokens[sc.gem]; if (!tok || !tok.alive) return; const now = manhattan(st.pos[0], tok), before = manhattan(from, tok); // 선언 순서(2026-08-05): order === 'pink' 면 1단계에서 분홍이 먼저 "!" 를 띄운다. // 뒤집는 것은 fx 의 seat 뿐이다 — P.mode='toGem' 은 2단계에 그대로 남는다. 1단계에서 // 분홍을 걷게 하면 그녀가 보석에 먼저 닿아 "blue must be strictly closer at the dual // claim" 이 깨지고 G-N 장면 자체가 사라진다. const order = (sc.order === 'pink') ? [1, 0] : [0, 1]; if (P.sharedClaimStage === 'unseen' && now < before && now <= 3) { P.sharedClaimStage = 'blue'; P.sharedClaimBlueTurn = P.turns; if (order[0] === 1) { // pink declares first — a declaration includes the gaze, not the walk const mate = st.pos[1]; st.facing[1] = { dx: Math.sign(tok.x - mate.x), dy: Math.sign(tok.y - mate.y) }; } st.fx.push({ k: 'notice', seat: order[0], token: sc.gem, x: tok.x, y: tok.y }); } else if (P.sharedClaimStage === 'blue' && P.turns > P.sharedClaimBlueTurn && now <= sc.dualDist) { P.sharedClaimStage = 'dual'; P.mode = 'toGem'; const mate = st.pos[1]; st.facing[1] = { dx: Math.sign(tok.x - mate.x), dy: Math.sign(tok.y - mate.y) }; st.fx.push({ k: 'notice', seat: order[1], token: sc.gem, x: tok.x, y: tok.y }); } } /* ========== PARK_FIELD_MECHS — the park FIELD-MECHANIC plug-in registry (Task 1) ========== The park has two kinds of mechanism module. A VERB module (push / slide) changes HOW the walker moves and hangs off `mech.moveMech`. A FIELD module changes WHAT THE GROUND DOES — it brings its own geometry and its own mutable runtime terrain (park.dyn, Task 0) — and hangs off `mech.fieldMech`. The precedent is exact, and it is why neither needs a new task `kind` (the walk archetype pools stay append-only and walk-only). The difference: push/slide are hard-wired into the engine body with `if (park.push)` / `if (park.slide)` branches. A SIXTH such branch would be a seventh, and an eighth — every new cell editing the same five functions, so no two cells can be built in parallel and each one can break the others. So field mechanics do NOT get body branches. They REGISTER: PARK_FIELD_MECHS[id] = { build, legalMask, legalAdd, onEnter, onLeave, tick, reads, oracleCost, cell, admits } and `cell.mech.fieldMech = id` is what makes a board that mechanic. Every hook is OPTIONAL and a no-op when absent; a board with no `fieldMech` reaches none of them, so the whole park that predates this registry is byte-identical (the dispatch points below are all guarded by `_parkMech(st)`, which is null on every legacy board). THE HOOKS (the public contract — a mechanic block registers ONE bundle, and the engine body below is the ONLY place that calls them; a mechanic never edits the body): build(cell) -> st The board. A pure function of the PUBLIC cell (C1: no persona symbol may reach it). MUST stamp `st.park.fieldMech = id` (that is what routes every hook below back here) and, if it wants runtime state, `st.park.dyn = _parkDynInit(st)`. Its own seed-pure geometry hangs anywhere on `st.park` (y12: `park.stones`); the ENGINE never reads it. legalMask(P, key, who) -> bool "is this cell impassable, for this traveller?" The SUBTRACTIVE spatial predicate, asked in three domains: 'me' the walker's legal moves (_parkLegal) 'mate' the companion's plan (_parkCompanionPlan) 'route' the persona route metric (_parkFields) Three domains, not one, because a mechanic routinely blocks them differently: y12's sunk stone is water for all three; a closed toll gate is a wall for the companion while the walker may still buy through it; a rolling log is a wall the companion detours but the walker may body-block. MUST be a pure read of board + dyn (never the persona). !! THE LEGAL SET IS NEVER EMPTY. If your mask takes every cell around the walker AND the one under him, 'stay' is re-admitted as a last resort — because an empty legal set would make parkStep reject the oracle's own move as INPUT NOISE, freezing turns, the beat and every schedule, and limping to reason:'noise'. If you want the run to END, end it in tick() (P.over + P.reason). Never rely on an empty legal set to do it. legalAdd(P, key, who) -> bool "is this cell enterable ANYWAY?" The ADDITIVE override — legalMask can only subtract, and some mechanics must open a cell the ENGINE'S OWN rules refuse. y3's assist is "walk INTO the adjacent downed companion", and the companion's cell has never been a legal destination; y14's gate, once the second toll is paid, is the cell the companion may finally cross. legalAdd overrides your own mask, the companion-occupancy rule ('me') and the deep-field taboo ('mate'). It does NOT open a wall or the board edge — board geometry is inviolable (do not build a wall where you want a door). A force-opened candidate is flagged `add: true` in the legal set. !! ASKED IN TWO DOMAINS ONLY: 'me' and 'mate'. There is NO 'route' — legalMask has three domains and this has two, so do not assume symmetry. A mechanic may NARROW the persona route metric but never WIDEN it: a cell you force open for the walker stays invisible to his route metric unless it was passable there anyway. AN ACTION, NOT A STEP: parkStep still commits st.pos[0] to the target cell. A mechanic whose force-opened move is an action rather than a walk (you reach the fallen companion, you do not stand on him) may restore st.pos[0] inside onEnter — P.path records where the walker ACTUALLY stands, so the trace stays honest. But see the two warnings on onEnter: the deep-entry charge has ALREADY fired by then, and P.prev is left equal to st.pos[0]. onEnter(P, ev) / onLeave(P, ev) ev = { mvKey, from, to, fromKey, toKey } Fired once per real step, AFTER the walker's position commits and after the universal deep-entry charge — onLeave first (the cell being vacated), then onEnter (the cell being entered). Fired for EVERY move key including 'stay' (a mechanic that must ignore a non-move says so itself — y12 does). This is where a per-cell ENTRY COST lives (a gem deducted at a gate) and where CONSUMABLE TERRAIN is spent (y12 sinks the vacated stone). May mutate P.hearts / st / dyn. Must NOT read the persona. !! THE DEEP-ENTRY CHARGE HAS ALREADY FIRED. parkStep prices `ev.to` against park.deep BEFORE calling you, and it prices where the move POINTED, not where the walker ends up. So a force-opened ACTION move into a deep cell — which is exactly y3's assist, since the downed companion lies in the meadow — costs the walker a heart and bumps deepEntries, even though onEnter puts him straight back and he never stands there. (MEASURED: hearts 3->2, deepEntries 0->1.) That may be the drama you want ("diving in costs you"), but deepEntries is a meter the SIGNATURE and ADMISSIBILITY gates read, so it is a design decision and not a detail. To decline the charge, undo it here: P.hearts++; P.deepEntries--; Choose deliberately; do not inherit it by accident. !! P.prev IS LEFT EQUAL TO st.pos[0] after an action-move that restores position, so G's no-greedy-backtrack exclusion is inert that turn (harmless) — but P.prev is NOT a trustworthy "the walker's previous cell" on a board that has action-moves. A mechanic that needs a footprint (y6's duckling follows one) must keep its own trail in dyn. tick(P, ev) the world advances Fired at the end of a real step, immediately after `dyn.beat++`, and only while the run is still live. This is the BEAT hook: a log that rolls every N beats, a flood ring that sinks on schedule, a duckling that steps into the walker's last footprint. It fires on 'stay' too — a beat-driven entity that froze whenever the walker waited would not be a clock. A tick MAY end the run: set `P.over` + `P.reason` for its own terminal (drowning), or simply spend P.hearts and the engine's death check re-reads the body right after. reads: { ctx(P), G: {engaged, prefer}, C: {...}, N: {...} } The mechanic's contribution to the THREE MINDS — and the ONLY way it may touch them. There is no new scoring channel: parkReduce stays the one scorer, and these feed the SHIPPED PARK_ATTITUDES through _parkReads. ctx(P) -> any computed once per state, handed to the hooks below. .engaged(P, ctx) OR-ed with the shipped attitude's engagement: a mechanic may ENGAGE an attitude the walk board leaves cold (y12: care engages while the companion's crossing is still sinkable), never disengage one. .prefer(P, legal, ctx) -> Set of move keys INTERSECTED with the shipped attitude's compliant set. A mechanic may only NARROW a mind, never widen or replace it — that is what keeps the mechanic a facet of the same three minds instead of a fourth mind smuggled in under their name. Pure reads of board + dyn + the companion's public plan. Never the persona (C1). !! AN EMPTY prefer() DOES NOT VETO — IT MAKES THAT MIND INERT. This is the opposite of what "return the set of moves I approve of" suggests, so read it twice. The narrowing is a plain intersection, and _parkLexSet then narrows "only on a non-empty set" (the park's shipped lexical rule, which applies to the built-in attitudes too). So a prefer() that returns {} does not forbid every move — it removes that mind from the decision entirely and lets the NEXT one in the persona's order rule unopposed. If you mean "this mind has nothing to say here", say it in engaged(). If you mean "this mind forbids these moves", return everything EXCEPT them (y12 returns all legal moves minus the guarded stones — note it never returns {}). Pinned by SEAM-INVARIANTS. oracleCost(P, cand, ctx) -> number An additive term on the persona's route-metric argmin (the tie-break AMONG the lexically compliant moves — the selection layer, never the blind read stack). 0 / absent = the shipped metric verbatim. Note it can only bite where the lexical filter left the persona a CHOICE; on a singleton compliant set the tie-break is inert by construction. cell(seed) -> cell } THE CAMPAIGN SURFACE. The crossing filter (campaign.js admits(cell) -> bool } _parkCrossingPlayCell) sweeps seeds for a slot: `cell` mints the module's public play-cell for a swept seed, `admits` is the module's own generate-then- filter predicate (playability — NOT the ship bar). Because campaign.js reaches both through this registry, a new field cell adds exactly ONE line to campaign.js: its PARK_CROSSINGS slot row. WHAT A MECHANIC MAY NOT DO: touch the engine body; mutate another board's state; read a persona symbol anywhere in build / legalMask / legalAdd / reads (C1 — the blind recovery replays the same board from the trajectory alone, so any persona leak makes the cell unrecoverable by construction); or invent a scoring channel outside PARK_ATTITUDES. THREE GUARANTEES THE BODY GIVES YOU (each one was a bug once — see the review fixes): 1. THE COMPANION CAN BE STUCK WITHOUT BEING FINISHED. If your mechanic makes his target unreachable (a shut gate, a sunk crossing, a flooded lane, a body that cannot rise), his plan comes back `{ next: null, stuck: true }` and he HOLDS — keeps his contract, keeps his mode, does not retire. `{ next: null, arrived: true }` is the other thing entirely. Read `plan.stuck` when you need to know whether the harm you staged is actually biting. 2. THE LEGAL SET IS NEVER EMPTY (see legalMask). 3. dyn IS DEEP-CLONED STRUCTURALLY on every search fork (_parkDeepClone), so whatever you keep there — nested objects, entities with their own arrays — is fork-safe without your doing anything. Keep dyn to Sets of cell keys / arrays / plain objects / primitives and it holds. FIELD BOARDS ARE WALK BOARDS: a field mech brings its own geometry, so it is never also a push/slide board — the verb early-returns in parkStep/_parkLegal are disjoint from this seam. */ const PARK_FIELD_MECHS = {}; // _parkMech(st): the board's registered mechanic, or null on every board that never opted in. // The engine body asks this and nothing else — it never names a mechanic (gate: // REGISTRY-SEAM-BODY asserts the source above the first module banner stays mechanic-free). function _parkMech(st) { const id = st.park && st.park.fieldMech; return id ? (PARK_FIELD_MECHS[id] || null) : null; } // _parkMaskOf(P, who): the mechanic's terrain mask for one traveller, or null when there is // nothing to mask (so the three BFS loops below keep their legacy inner loop verbatim). function _parkMaskOf(P, who) { const m = _parkMech(P.st); return m && m.legalMask ? (kk) => !!m.legalMask(P, kk, who) : null; } // _parkAddOf(P, who): the mechanic's ADDITIVE legality override (review C2). legalMask can only // SUBTRACT, and some mechanics must make a cell enterable that the engine's OWN rules refuse — y3's // assist is "walk INTO the adjacent downed companion", and the companion's cell is a destination the // walk engine has always forbidden outright. Without an additive path that whole cell is // inexpressible through the seam. legalAdd overrides the mechanic's own mask and the // companion-occupancy rule; it can NOT open a wall or the board's edge (board geometry is // inviolable — a mechanic that wants a cell open simply does not build a wall there). function _parkAddOf(P, who) { const m = _parkMech(P.st); return m && m.legalAdd ? (kk) => !!m.legalAdd(P, kk, who) : null; } // _parkCellMask(n, pred): fold a per-cell predicate over the whole grid ONCE, for the route BFS // loops that would otherwise re-ask it per incoming edge (four times a cell, times three metrics). // Sound because a mechanic's legalMask/legalAdd is a pure read of a runtime that does not move // during one BFS — the seam contract says a mechanic mutates only inside its step hooks, never // inside a legality query. If that ever stops being true the mechanic is broken with or without // this fold, since the old code already asked the same key several times and used the first answer. function _parkCellMask(n, pred) { const a = new Uint8Array(n * n); for (let k = 0; k < n * n; k++) if (pred(k)) a[k] = 1; return a; } // parkFieldBuild(cell): the PUBLIC build entry point — the campaign's crossing builder dispatches // EVERY field cell through here, so adding a mechanic adds no line to campaign.js's board switch. // THROWS on an unregistered id: a typo in the ONE line a mechanic adds to campaign.js used to yield // a null board that failed far away from its cause. Fail at the cause. function parkFieldBuild(cell) { const id = cell && cell.mech && cell.mech.fieldMech; const m = PARK_FIELD_MECHS[id]; if (!m || !m.build) throw new Error(`parkFieldBuild: no field mechanic registered as '${id}' (see PARK_FIELD_MECHS)`); return m.build(cell); } // player metric fields, recomputed per destination: fast = uniform BFS over passable cells // (the beeline pull, deep included — physics allows entry); safe = weighted (verge 8 / // deep 24) so a caution-led persona routes the walkway network yet stays navigable from a // verge yield; detour = the no-deep step distance (detour - fast = the shortcut savings a // crossing buys — the sigma_gc / foregone-shortcut price read). Selection-layer only — the // blind recovery never reads them. function _parkFields(P) { const dest = _parkDestCell(P); if (!dest) return null; const st = P.st, n = st.N, dk = _parkKey(st, dest); const form = _parkForm(st.park); // relational forms weight the LIVE rival taboo, so the field is stale once the rival moves: // key the cache on the rival cell too (static keeps the pure dest key = byte-identical). const rk = (form === 'static' || form === PARK_PHASE_FORM) ? -1 : _parkKey(st, st.pos[1]); // FIELD MECH (Task 1): a mechanic's terrain is MUTABLE, and the route metric must SEE it — a // shortcut over a stone the walker already sank, or a cell the water has taken, does not exist. // The domain therefore shrinks as the board is played, so the cache is keyed on the BEAT: any // terrain a mechanic mutates, it mutates in a step, and every step ticks the beat. (Keying on // the beat rather than on any mechanic-private counter is what keeps this line mechanic-free; // it is also strictly conservative — at worst it recomputes a field that had not changed.) // gk = -1 on every board without dyn, so the key is a constant there: byte-identical caching. const mask = _parkMaskOf(P, 'route'); const gk = st.park.dyn ? st.park.dyn.beat : -1; if (P._fields && P._fields.dk === dk && P._fields.rk === rk && P._fields.gk === gk) return P._fields; // THE MASK IS A CELL FACT, NOT AN EDGE FACT (2026-08-01, measured). The relax loop below asked // `mask(nk)` once per INCOMING EDGE, and three fields each ran their own loop — so one call to // this function put 2,362 (y54) to 2,519 (y46) queries through the mechanic's predicate for a // board of 169 cells. Measured cost: 190µs/expansion on y46, 2,024µs on y54, and it is what made // the ⑤ prover's search unaffordable. legalMask(P, key, who) is a pure function of the key while // P is frozen — and P IS frozen for the whole of this call — so the answer is folded to ONE query // per cell and shared by all three metrics. Same answers, ~12x fewer calls. const masked = mask ? _parkCellMask(n, mask) : null; const mk = (costOf) => { const dist = new Array(n * n).fill(Infinity); dist[dk] = 0; const q = [dk]; for (let h = 0; h < q.length; h++) { // small grid: Bellman-style relax queue const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk)) continue; if (masked && masked[nk]) continue; // FIELD MECH: masked terrain carries no route const nd = dist[kk] + costOf(nk); if (nd < dist[nk]) { dist[nk] = nd; q.push(nk); } } } return dist; }; const park = st.park; if (form === 'static' || form === PARK_PHASE_FORM) { // phase shares the STATIC spatial field metric (the deep terrain the safe route weights // away from); the phase form's temporal red-segment gate lives on the COST (parkStep) and // the C attitude's per-segment preference, not on the route geometry. P._fields = { dk, rk, gk, fast: mk(() => 1), safe: mk((kk) => park.deep.has(kk) ? 24 : (park.verge.has(kk) ? 8 : 1)), detour: mk((kk) => park.deep.has(kk) ? Infinity : 1), }; } else { // relational: the hazard is the live rival taboo (not the terrain). safe routes the // walkway AWAY from the taboo (high cost on taboo cells); detour = the taboo-free step // distance (detour - fast = the savings a rival-adjacency shortcut buys — the sigma_gc // relational read). The cosmetic deep terrain carries NO cost on a relational form. const taboo = _parkRivalTaboo(st, form); P._fields = { dk, rk, gk, fast: mk(() => 1), safe: mk((kk) => taboo.has(kk) ? 24 : 1), detour: mk((kk) => taboo.has(kk) ? Infinity : 1), }; } return P._fields; } // _parkLegal(P): the candidate steps (4 dirs + stay): in-bounds, off-wall, not the // companion's cell. Deep field IS legal (entering costs the heart — physics, not a wall). // PUSH verb module (P12): on a push board (st.box present) stepping INTO the box is a PUSH // candidate, legal only when the far cell is free — in-bounds, off-wall, off-CURB (the box // never mounts the perimeter promenade ring: pre-curb the ring was a box graveyard — a box // on the W/E/S ring can never be pushed back, measured 26 dead reachable joint states + // 129/384 deadlocked faithful wanders; the curb makes the box domain an open interior // rectangle the pusher can always circle behind, so BOTH counts drop to 0 by construction) // and not the companion. A blocked push is NOT a candidate: a replayed walk move that would // step "through" the box parses as input noise — the physical-divergence lever the crossing // anti-mimicry bar measures (48/48 walk-demo replays hit illegal-move noise, scout A). function _parkLegal(P) { const st = P.st, n = st.N, from = st.pos[0], co = st.pos[1]; // SLIDE verb module (P12b): on a slide board the MOVEMENT VERB itself changes — one // directional input is a junction-brake glide (_parkSlideLegal), not a step. A direction // whose glide path is empty (wall/companion flush against the walker) is NOT a candidate: // it parses as input noise, exactly the walk discipline for a wall bump — this is the // physical-divergence lever the slide crossing's anti-mimicry bar measures (walk demo // replays under slide physics are evidence-free 48/48, scout B 2026-07-10). if (st.park.slide) return _parkSlideLegal(P); // FIELD MECH (Task 1): the mechanic's terrain mask for the WALKER — a stone the player left has // sunk and the cell is water now, as impassable as the stream it stood in. RUNTIME terrain (the // mechanic reads its own dyn), not board geometry, so a board that registered no mechanic gets // a null mask and this line is a no-op (C1 / byte stability). const mask = _parkMaskOf(P, 'me'); const add = _parkAddOf(P, 'me'); // FIELD MECH: the additive override (C2) const out = []; for (const m of _PARK_MOVES) { const x = from.x + m.x, y = from.y + m.y; if (x < 0 || y < 0 || x >= n || y >= n) continue; const kk = y * n + x; if (st.wall.has(kk)) continue; // board geometry: never overridable // FIELD MECH: a force-opened cell (legalAdd) survives BOTH the mechanic's own mask and the // engine's companion-occupancy rule, and is flagged `add: true` so the mechanic can tell its // own action-move apart from an ordinary step (y3's assist is not a walk). const forced = add ? add(kk) : false; if (forced) { out.push({ k: m.k, x, y, key: kk, add: true }); continue; } if (mask && mask(kk)) continue; // FIELD MECH: masked terrain is not a cell if (co && m.k !== 'stay' && co.x === x && co.y === y) continue; if (st.box && m.k !== 'stay' && st.box.x === x && st.box.y === y) { const bx = x + m.x, by = y + m.y; if (bx < 0 || by < 0 || bx >= n || by >= n) continue; const bk = by * n + bx; if (st.wall.has(bk) || st.park.curb.has(bk) || (co && co.x === bx && co.y === by)) continue; out.push({ k: m.k, x, y, key: kk, push: true, bx, by }); continue; } out.push({ k: m.k, x, y, key: kk }); } // THE LEGAL SET IS NEVER EMPTY (review I3). A mechanic's mask can in principle take every cell // around the walker AND the cell under him (y10's water reaching his own square). The legal set // would then be empty, parkOracleMove would return 'stay', and parkStep would reject that very // move as INPUT NOISE — turns and the beat would stop advancing, every schedule would freeze, and // the run would limp to reason:'noise' instead of the terminal the mechanic meant to stage. So // 'stay' is re-admitted as a last resort. A mechanic that wants the run to END must end it in // tick() (set P.over + P.reason), which is the honest place for a terminal to live. // No-op on every board without a mechanic: 'stay' is always legal there (the walker's own cell is // never a wall), so this never fires and the shipped legal sets are byte-identical. if (!out.length) out.push({ k: 'stay', x: from.x, y: from.y, key: _parkKey(st, from) }); return out; } // _parkLegalKeys(P): the move-key strings alone, over _parkLegal's own candidate list (a field // mechanic's legalMask included, since _parkLegal already folds it in). Convenience wrapper — a // gate that only wants to know WHICH moves are on the table (e.g. "is 'U' still there?") shouldn't // have to re-derive the `.k` projection itself every time (y58 road, gate beat, Task 3). const _parkLegalKeys = (P) => _parkLegal(P).map(m => m.k); // _parkCompanionPlan(P, exclude): the companion's CURRENT intent — target + BFS path over // the no-deep domain (walkway cost 1, verge cost 3: it hugs the path, cuts the verge only // to detour). Reads ONLY public positions + the seed-fixed contract list (rule-blind, C1). // `exclude` (a cell key) drops one cell from the domain (the blocked-reroute read). function _parkCompanionPlan(P, exclude) { const st = P.st, n = st.N, park = st.park; let target = null; if (P.mode === 'toGem' && P.contract < park.contracts.length) { const g = park.clusters[park.contracts[P.contract].gem]; target = { x: g.x, y: g.y }; } else if (P.mode === 'relocate') { target = P.target; } if (!target) return null; const src = _parkKey(st, st.pos[1]), dk = _parkKey(st, target); // FIELD MECH (Task 1): the mechanic's terrain mask for the COMPANION — its own domain, because // a mechanic routinely blocks the two travellers differently. y12: a sunk stone is gone for the // companion too, so its plan re-routes around the hole the walker punched in the crossing (and, // once every crossing is spent, has no path at all — the harm the care read is defending // against). No-op on a board with no mechanic. const mask = _parkMaskOf(P, 'mate'); const add = _parkAddOf(P, 'mate'); // FIELD MECH: opens a lane for the mate // per-cell fold, same reason as _parkFields (a cell is asked from up to four sides). const masked = mask ? _parkCellMask(n, mask) : null; const forcedAt = add ? _parkCellMask(n, add) : null; const dist = new Array(n * n).fill(Infinity), parent = new Array(n * n).fill(-1); dist[src] = 0; const q = [src]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; // FIELD MECH: legalAdd opens a lane the companion's own rules refuse — y14's gate, once the // walker has paid HIS toll too, is the cell he may finally cross. It overrides the mask and // the deep-field taboo; walls stay walls. const forced = forcedAt ? !!forcedAt[nk] : false; if (!forced) { if (st.wall.has(nk) || park.deep.has(nk)) continue; // NEVER enters the deep field if (masked && masked[nk]) continue; // FIELD MECH: masked terrain is not a cell } else if (st.wall.has(nk)) continue; if (st.box && nk === _parkKey(st, st.box)) continue; // PUSH (P12): the box is a solid obstacle if (exclude != null && nk === exclude) continue; const nd = dist[kk] + (park.verge.has(nk) ? 3 : 1); if (nd < dist[nk]) { dist[nk] = nd; parent[nk] = kk; q.push(nk); } } } // STUCK is not ARRIVED (review C1). Both end with `next: null`, and conflating them is fatal: a // companion whose target is UNREACHABLE — because a mechanic shut a gate, sank his crossing, // flooded his lane, or froze him where he lies — was read as having ARRIVED, so the contract // advanced, he relocated, and he permanently RETIRED. The care read then goes cold forever and // the very harm a mechanic set out to stage silently stops existing. A companion must be able to // be STUCK without being FINISHED, so the two cases are now distinguishable by name. if (dk === src) return { target, path: [], next: null, arrived: true }; if (!isFinite(dist[dk])) return { target, path: [], next: null, stuck: true }; const path = []; for (let cur = dk; cur !== src; cur = parent[cur]) path.unshift(cur); const nk = path[0]; return { target, path, next: { x: nk % n, y: (nk / n) | 0 } }; } // PARK_ATTITUDES: the park-board {engaged, preference} registry the lexical composition // (parkLexFilter) ranks — same shape/discipline as PRO_ATTITUDES, re-grounded in the park's // public semantics. Preferences are Sets of move keys over _parkLegal; persona-independent // (the blind recovery recomputes them from trajectory + board alone). // G goal: a chain destination stands -> prefer steps strictly reducing the beeline // (fast-field) distance, excluding the just-vacated cell (no greedy backtrack). // C safety: the deep field within the 2-cell caution band -> prefer steps keeping // distDeep >= 2 (off the verge, out of the field). // N care: (a) the companion's contracted gem is contested (alive, player <= companion // distance, within 3) -> leave its cell; (b) the player blocks the companion's // next path cell head-on -> step OFF the companion's route (the verge yield). // A FIELD MECHANIC may add a THIRD facet to any of the three through its `reads` hook — an extra // engagement (OR-ed) and an extra compliant set (INTERSECTED). It can never widen or replace one, // so a mechanic stays a facet of these same three minds and parkReduce stays the one scorer. // (y12's care facet — "never sink the stone the companion is still counting on" — lives entirely // in the stones module's reads hook; the body below knows nothing about it.) function _parkNCtx(P) { const st = P.st, park = st.park; let gem = null; if (P.contract < park.contracts.length) { const gi = park.contracts[P.contract].gem, tok = st.tokens[gi]; if (tok.alive) { const g = { x: tok.x, y: tok.y }; const dp = manhattan(st.pos[0], g), dc = manhattan(st.pos[1], g); if (dp <= 3 && dp <= dc) gem = g; } } const plan = _parkCompanionPlan(P); const blocked = !!(plan && plan.next && plan.next.x === st.pos[0].x && plan.next.y === st.pos[0].y && manhattan(st.pos[0], st.pos[1]) === 1); return { gem, blocked, plan }; } const PARK_ATTITUDES = { G: { // PUSH (P12): the goal grammar is box-to-pad — engaged while the box is off the pad. engaged: (P) => P.st.park.push ? !_parkPushDone(P.st) : !!_parkDestCell(P), preference: (P, legal) => { const st = P.st; const out = new Set(); if (st.park.push) { // PUSH (P12): the goal plan is JOINT (box x pusher) — prefer steps strictly reducing // the joint push-plan (fast) distance to box-on-pad; same no-stay / no-greedy-backtrack // shape as the walk G. A push candidate keys the POST-push joint state. const f = _parkPushFields(st); const cur = f.fast[_parkPushSid(st, st.pos[0])]; for (const c of legal) { if (c.k === 'stay') continue; if (P.prev && P.prev.x === c.x && P.prev.y === c.y) continue; if (f.fast[_parkPushCandSid(st, c)] < cur) out.add(c.k); } return out; } if (st.park.slide) { // SLIDE (P12b): prefer glides strictly reducing the SLIDE fast field (min glides // until a swept path CROSSES the destination — crossing absorbs, since tokens are // consumed in passing); same no-stay / no-greedy-backtrack shape as the walk G // (prev = the previous REST cell). const f = _parkSlideFields(P); if (!f) return out; const cur = f.fast[_parkKey(st, st.pos[0])]; for (const c of legal) { if (c.k === 'stay') continue; if (P.prev && P.prev.x === c.x && P.prev.y === c.y) continue; if (c.cells.some(cc => cc.key === f.dk) || f.fast[c.key] < cur) out.add(c.k); } return out; } const f = _parkFields(P); if (!f) return out; const cur = f.fast[_parkKey(st, st.pos[0])]; for (const c of legal) { if (c.k === 'stay') continue; if (P.prev && P.prev.x === c.x && P.prev.y === c.y) continue; // no greedy backtrack if (f.fast[c.key] < cur) out.add(c.k); } return out; }, }, C: { // caution band radius = the hazard's keep-distance d (task battery reskin knob, // spec P2 §B; absent on P1 park boards -> the original 2). On a RELATIONAL form (design // 2026-07-06) the band is measured to the LIVE rival (adjacent) or the taboo is the single // rival-nearest token — so the compliant set changes turn to turn as the rival walks. engaged: (P) => { const park = P.st.park, form = _parkForm(park), d = park.cautionD || 2; // PUSH (P12) CARGO-AWARE C (module-own safety facet, not a recalibration of walk-C): // the safety concern engages when the PUSHER *or the CARGO* is inside the caution band — // a box near the pit is a live safety matter. The walk-verbatim pusher-only rule gets // BAITED (measured, scout A): G's dive preference shoves the box into the halo while C // is still disengaged, then C's veto wedges the box = a livelock (safety-led cap-outs). if (park.push) return park.distDeep[_parkKey(P.st, P.st.pos[0])] <= d || park.distDeep[_parkKey(P.st, P.st.box)] <= d; // static AND phase: engaged inside the field's caution band (CLOCK-INDEPENDENT — the // safety concern is present whenever the field is near, so a care-vs-safety scene stays // posable in every segment; the clock only changes the PREFERENCE, spec §3). if (form === 'static' || form === PARK_PHASE_FORM) return park.distDeep[_parkKey(P.st, P.st.pos[0])] <= d; if (form === 'adjacent') return _parkDistRival(P.st, P.st.pos[0].x, P.st.pos[0].y) <= d; return nearestRivalTokenKey(P.st) != null; // nearest_token: engaged while a taboo token lives }, preference: (P, legal) => { const park = P.st.park, form = _parkForm(park), d = park.cautionD || 2; const out = new Set(); if (park.push) { // PUSH (P12): the pusher keeps the band (walk-C rule) AND no push may land the BOX // below distDeep d (don't shove cargo toward the pit) — single-state legible; this // cargo veto is exactly where the G-C award fires (the crossing's drama point). const dd = park.distDeep, n = P.st.N; for (const c of legal) { if (dd[c.key] < d) continue; if (c.push && dd[c.by * n + c.bx] < d) continue; out.add(c.k); } } else if (park.slide) { // SLIDE (P12b): the caution read prices the WHOLE swept path, rest included — a // glide that grazes the band is as non-compliant as resting in it (momentum makes // the intermediate cells part of the single decision; single-state legible: path is // a pure function of board + input). 'stay' keys the current cell. const dd = park.distDeep; for (const c of legal) { let ok = true; if (c.k === 'stay') ok = dd[c.key] >= d; else for (const cc of c.cells) if (dd[cc.key] < d) { ok = false; break; } if (ok) out.add(c.k); } } else if (form === 'static') { const dd = park.distDeep; for (const c of legal) if (dd[c.key] >= d) out.add(c.k); } else if (form === PARK_PHASE_FORM) { // PHASE (spec §3): the field is dangerous ONLY in the public RED segment. RED + // engaged (this preference only runs when engaged, i.e. inside the caution band) -> // HOLD: wait in place for green. GREEN -> inert (the field is safe to cross, so the // subordinate motive decides). NOTE the earlier static-C-verbatim RED rule ("keep the // caution distance") rested on a false invariant: it EXCLUDES 'stay' inside // the band (dd < d there), forcing a retreat that phase-locks with green-inert // re-approach into a period-2*segN livelock (P11 ③ diagnosis — no turn budget fixes // it). HOLD is single-state readable and keeps the signature: safety-top WAITS for // green then crosses (patience = foregone progress), goal-top crosses during red. const red = clockSegOf(P.st, 0) === RED_SEG(park.phase.segN); for (const c of legal) if (!red || c.k === 'stay') out.add(c.k); } else if (form === 'adjacent') { for (const c of legal) if (_parkDistRival(P.st, c.x, c.y) >= d) out.add(c.k); // keep off the rival's caution band } else { const tk = nearestRivalTokenKey(P.st); for (const c of legal) if (c.key !== tk) out.add(c.k); // never step onto the rival-nearest token } return out; }, }, N: { engaged: (P) => { const c = _parkNCtx(P); return !!(c.gem || c.blocked); }, preference: (P, legal) => { const ctx = _parkNCtx(P), st = P.st; const onPath = new Set(ctx.blocked && ctx.plan ? ctx.plan.path : []); const out = new Set(); if (st.park.slide) { // SLIDE (P12b): tokens are consumed IN PASSING, so ceding the contested gem means no // part of the swept path may cross it (a glide THROUGH the gem takes it); the yield // read stays on the REST cell (momentum passing over a route cell does not block it). const gk = ctx.gem ? _parkKey(st, ctx.gem) : -1; for (const c of legal) { if (gk >= 0 && (c.key === gk || c.cells.some(cc => cc.key === gk))) continue; if (ctx.blocked && onPath.has(c.key)) continue; out.add(c.k); } return out; } for (const c of legal) { if (ctx.gem && c.x === ctx.gem.x && c.y === ctx.gem.y) continue; // leave the contested gem if (ctx.blocked && (onPath.has(c.key))) continue; // clear the companion's route out.add(c.k); } return out; }, }, }; // _parkReads(P): one-shot per-state read (legal set + engaged/preference per attitude) so // the filter, the oracle and the pairwise award scan share a single computation. // FIELD MECH (Task 1): the registered mechanic folds its own facet into the SAME three minds here // — the single place it may touch them. Two rules, and they are the whole discipline: // ENGAGEMENT is OR-ed — a mechanic may engage a mind the walk board leaves cold, never // disengage one the board engaged. // PREFERENCE is INTERSECTED — a mechanic may only NARROW a compliant set. It cannot widen one // and it cannot replace one, so it can never become a fourth mind // wearing a real mind's name (parkReduce stays the one scorer). // The intersection is PLAIN — an empty result stays empty, exactly as a shipped attitude's own // preference may come out empty, and _parkLexSet's "narrow only non-empty" rule handles it. (A // fallback here would silently re-widen the very set the mechanic just narrowed.) function _parkReads(P) { const legal = _parkLegal(P); const mech = _parkMech(P.st); const mr = mech && mech.reads ? mech.reads : null; const mctx = mr && mr.ctx ? mr.ctx(P) : null; const atts = {}; for (const k of ['G', 'C', 'N']) { const a = PARK_ATTITUDES[k], mk = mr ? mr[k] : null; let engaged = a.engaged(P); if (!engaged && mk && mk.engaged) engaged = !!mk.engaged(P, mctx); let pref = null; if (engaged) { pref = a.preference(P, legal); if (mk && mk.prefer) { const add = mk.prefer(P, legal, mctx), next = new Set(); for (const m of pref) if (add.has(m)) next.add(m); pref = next; } } atts[k] = { engaged, pref }; } return { legal, atts, mctx }; } function _parkLexSet(reads, order) { let cur = new Set(reads.legal.map(c => c.k)); for (const k of order) { const a = reads.atts[k]; if (!a || !a.engaged || !a.pref) continue; // D (or any unknown key) is inert const next = new Set(); for (const m of cur) if (a.pref.has(m)) next.add(m); if (next.size > 0) cur = next; // strict lexical: narrow only non-empty } return cur; } // parkLexFilter(P, ordering): the persona-prescribed move set (lexFilter discipline over // PARK_ATTITUDES). `ordering` = parkOrderingFor(persona) (att keys; D inert). function parkLexFilter(P, ordering) { return _parkLexSet(_parkReads(P), ordering); } // _parkAwardsFor(reads, mvKey): the BLIND pairwise reads of one observed move — sound // lexical inference, no persona input: over the 6 candidate att-orders, keep those whose // prescribed set contains the move; a pair A>B is awarded only when EVERY consistent order // ranks A before B (so a move forced by a third concern can never mis-credit a pair — the // pairwise-win recovery the report and PARK-ORDER-RECOVERABLE ride on). function _parkAwardsFor(reads, mvKey) { const ok = _PARK_ORDERS.filter(o => _parkLexSet(reads, o).has(mvKey)); if (ok.length === 0 || ok.length === _PARK_ORDERS.length) return []; const out = []; for (const [a, b] of _PARK_PAIRS) { let aFirst = 0, bFirst = 0; for (const o of ok) (o.indexOf(a) < o.indexOf(b) ? aFirst++ : bFirst++); if (aFirst === ok.length) out.push({ pair: a + b, winner: a, loser: b }); else if (bFirst === ok.length) out.push({ pair: a + b, winner: b, loser: a }); } return out; } // parkReduce(P, mvKey): THE PARK'S REDUCTION ORACLE — the named public contract (constitution // principle 3, CERTIFIED BLIND REDUCTION) naming the park's map from one observed decision to the // domain-general axis conflicts it resolves. `P` = the park runtime (parkStart(board) advanced to // the decision state); `mvKey` = the observed legal move. Returns the blind pairwise awards // [{pair, winner, loser}] over the G/C/N axes (att keys), the exact list parkStep records and // parkRecoverOrder tallies. A THIN, BYTE-IDENTICAL wrapper over the two private fns parkStep // already calls at its award line — the scored core, _parkAwardsFor's behavior and // parkRecoverOrder are unchanged; this only gives the reduction a public NAME + a certified // contract so any game (and the CROSS-DOMAIN-COUPLING gate) can consume it uniformly. // CERTIFIED PROPERTIES (the three a game must ship to be admitted): // persona-blind — reads ONLY the board runtime + the observed move (_parkReads/_parkAwardsFor // take no persona/ordering symbol anywhere); the recovered order is never an // input to the board or to this map. // poses real conflicts — returns a NONEMPTY award list on states where the move DISCRIMINATES a // pair (one axis is preferred over another by every order still consistent // with the move); the park's boards are generated to pose all three pairs. // recovery-complete — these are the EXACT awards parkRecoverOrder accumulates to reassemble the // strict axis-order, so faithful play's order is blindly recoverable // (principle 4) from this reduction alone. function parkReduce(P, mvKey) { return _parkAwardsFor(_parkReads(P), mvKey); } // parkOracleMove(P, persona): the persona-faithful step — lexFilter narrows, then argmin of // the persona's route metric (safety-led personas price the walkway network, goal-led the // beeline field) with the fixed U/D/L/R/stay tie order. Selection layer only (motion may // depend on the persona; the BOARD never does). function parkOracleMove(P, persona) { const ordering = parkOrderingFor(persona); const reads = _parkReads(P); const cur = _parkLexSet(reads, ordering); // PUSH (P12): the route metric is over the JOINT (box x pusher) fields — safety-led // personas price the caution-compliant push plan (safe), goal-led the beeline plan (fast). // 'stay' keys the current joint state (c.push falsy + c.key = the current cell). if (P.st.park.push) { const f = _parkPushFields(P.st); const metric = ordering.indexOf('C') < ordering.indexOf('G') ? f.safe : f.fast; let best = null, bestM = Infinity; for (const c of reads.legal) { if (!cur.has(c.k)) continue; const m = metric[_parkPushCandSid(P.st, c)]; if (m < bestM) { bestM = m; best = c.k; } } return best || 'stay'; } // SLIDE (P12b): the route metric is over the SLIDE move graph (_parkSlideFields) — a // glide whose swept path CROSSES the destination absorbs at cost 0 (tokens are consumed // in passing), otherwise the rest cell's field value prices the candidate. if (P.st.park.slide) { const f = _parkSlideFields(P); const metric = f ? (ordering.indexOf('C') < ordering.indexOf('G') ? f.safe : f.fast) : null; let best = null, bestM = Infinity; for (const c of reads.legal) { if (!cur.has(c.k)) continue; let m = 0; if (metric) { const crossed = c.k !== 'stay' && c.cells.some(cc => cc.key === f.dk); m = crossed ? 0 : metric[c.key]; } if (m < bestM) { bestM = m; best = c.k; } } return best || 'stay'; } const f = _parkFields(P); const metric = f ? (ordering.indexOf('C') < ordering.indexOf('G') ? f.safe : f.fast) : null; // FIELD MECH (Task 1): a mechanic may price a candidate the terrain metric cannot see (a cell // that is cheap to reach and expensive to have been on). Selection layer ONLY — the tie-break // among the LEXICALLY COMPLIANT moves, never the blind read stack. Absent -> the shipped // argmin verbatim (0 added to every candidate). const mech = _parkMech(P.st); const oc = mech && mech.oracleCost ? mech.oracleCost : null; let best = null, bestM = Infinity; for (const c of reads.legal) { if (!cur.has(c.k)) continue; let m = metric ? metric[c.key] : 0; if (oc) m += oc(P, c, reads.mctx); if (m < bestM) { bestM = m; best = c.k; } } return best || 'stay'; } // _parkCompanionStep(P): the deterministic rule-blind companion (spec §4): idles at its // seed-fixed station; when the PLAYER nears its contracted gem (proximity trigger — the // persona-independent timing device) it walks the path to the gem (its intent line), takes // it on entry, then relocates to the next station (a route that opposes the player's lane // flow — the C x K head-on). Blocked by the player -> waits 2 beats, then reroutes around // (never through the deep field). Consumes ONLY its contracted gem. function _parkCompanionStep(P) { const st = P.st, park = st.park; const nextTarget = () => { P.contract++; P.mode = 'relocate'; P.target = P.contract < park.contracts.length ? park.contracts[P.contract].station : park.retire; }; if (P.mode === 'idle') { if (P.contract >= park.contracts.length) return; const tok = st.tokens[park.contracts[P.contract].gem]; if (!tok.alive) { nextTarget(); return; } // A shared-claim demo keeps pink still until the two public discovery beats have happened. // Once the stage is dual, _parkSharedClaimNotice has already switched mode to `toGem`. if (park.sharedClaim && park.contracts[P.contract].gem === park.sharedClaim.gem && (P.sharedClaimStage === 'unseen' || P.sharedClaimStage === 'blue')) return; if (manhattan(st.pos[0], tok) <= park.trig) P.mode = 'toGem'; else return; } if (P.mode === 'toGem') { const tok = st.tokens[park.contracts[P.contract].gem]; if (!tok.alive) { nextTarget(); } } if (P.mode === 'done') return; let plan = _parkCompanionPlan(P); if (!plan) return; // STUCK (review C1): no route to the target — a shut gate, a sunk crossing, a flooded lane, a // body that cannot rise. He HOLDS: keeps the contract, keeps the mode, and simply cannot go. He // is not finished, so nothing advances, and the moment a mechanic re-opens the way (the second // toll paid, the assist given) his very next plan resumes the journey where it stopped. if (plan.stuck) return; if (!plan.next) { // arrived if (P.mode === 'toGem') { const tok = st.tokens[park.contracts[P.contract].gem]; if (tok.alive && tok.x === st.pos[1].x && tok.y === st.pos[1].y) { tok.alive = false; st.score[1] += tok.v; // the bright-blink take st.fx.push({ k: 'take', x: tok.x, y: tok.y }); } nextTarget(); } else if (P.mode === 'relocate') { P.mode = P.contract < park.contracts.length ? 'idle' : 'done'; } return; } if (plan.next.x === st.pos[0].x && plan.next.y === st.pos[0].y) { P.wait++; if (P.wait <= 2) return; // waits when blocked plan = _parkCompanionPlan(P, _parkKey(st, st.pos[0])); // then reroutes around if (!plan || !plan.next) return; } else P.wait = 0; const from = st.pos[1]; st.facing[1] = { dx: Math.sign(plan.next.x - from.x), dy: Math.sign(plan.next.y - from.y) }; st.pos[1] = { x: plan.next.x, y: plan.next.y }; if (P.mode === 'toGem') { const tok = st.tokens[park.contracts[P.contract].gem]; if (tok.alive && tok.x === st.pos[1].x && tok.y === st.pos[1].y) { tok.alive = false; st.score[1] += tok.v; st.fx.push({ k: 'take', x: tok.x, y: tok.y }); nextTarget(); } } } // parkStep(P, mvKey): apply ONE player move to the park runtime — records the blind // pairwise awards at the PRE-state, moves the player, harvests any cluster entered (chain // destinations advance when their cluster dies, by ANYONE's hand), charges the universal // deep-entry heart, then advances the companion on every second turn (small = slower). // INPUT NOISE (spec §A.1): an illegal input (wall bump / off-board / companion cell / // unknown key) is a fat-finger, not a decision — tallied on P.inputNoise and dropped as a // no-op (no turn, no move record, never judged), so the effective move stream P.moves holds // ONLY moves chosen from the legal set. Shared verbatim by the demo playout, the live game // and the blind recovery so the three surfaces cannot diverge. // NOISE TERMINAL (P8.5 §4.1): a run that accumulates PARK_NOISE_MULT x cap illegal inputs // is a stalled driver, not a fat-fingered player — it terminates with reason='noise'. // Computed live off park.cap (task 180, capstone game 210, capstone demo 420), so it // auto-scales with any cap change. Illegal inputs still consume no turn and are never // fabricated into moves. const PARK_NOISE_MULT = 3; // _parkChainDone(P): 목표 문법이 말하는 "다 했다". 두 곳(스텝 본문과 tick 후처리)이 같은 말을 // 두 번 쓰고 있었으므로 하나로 모은다 — 아래 needPairs 조건을 한 번만 걸기 위해서다. function _parkChainDone(P) { const st = P.st, park = st.park; return park.push ? _parkPushDone(st) : P.dest >= (park.needTypes || park.chain.length); } // _parkReadDone(P): 판이 끝나도 되는가. 체인이 다 됐고, **그리고** 이 보드가 needPairs 를 켰다면 // 세 비교가 전부 한 번은 섰어야 한다. // // 왜 이것이 ⑤ 를 정의상 참으로 만드는가. ⑤ 는 "세 쌍 중 하나라도 안 세우고 끝내는 합법 완주 // 경로가 없다"이다. 완주의 정의에 "세 쌍을 다 세웠다"가 들어가면 그런 경로는 존재할 수 없다 — // 회피하는 걸음은 완주가 아니라 턴 소진으로 끝나고, 턴 소진은 완주 경로가 아니다. // // 그리고 이것은 **충실한 걸음에게 아무 일도 하지 않는다**: 라이브 18셀 × 6인격 = 108/108 이 // 이미 세 쌍을 다 세우고 완주한다(2026-08-02 실측). 그래서 승격 바도 시그니처도 admits 도 // 한 바이트 안 움직인다. 붙잡히는 것은 **읽히지 않으려고 걷는 걸음**뿐이고, 그것이 이 조건이 // 존재하는 이유다. // // C1: posed 는 blind 하다 — parkReduce 가 내는 쌍 이름만 쌓으며 인격은 안 본다. // 보정 창(PARK_CAL_TURNS) 안의 award 는 안 센다. 화면이 그 창을 버리기 때문이다. function _parkReadDone(P) { if (!_parkChainDone(P)) return false; if (!P.st.park.needPairs) return true; return P.posed && P.posed.size === 3; } function parkStep(P, mvKey) { if (P.over) return null; const st = P.st, park = st.park; const reads = _parkReads(P); const legalKeys = new Set(reads.legal.map(c => c.k)); if (!legalKeys.has(mvKey)) { P.inputNoise++; if (P.inputNoise >= PARK_NOISE_MULT * park.cap) { P.over = true; P.reason = 'noise'; } return { move: null, noise: true, awards: [], hearts: P.hearts, over: P.over, reason: P.reason }; } // needPairs 보드에서는 lean 탐색도 award 를 계산한다 — 완주 판정이 그 위에 서기 때문이다. // (lean 은 원래 이 스캔을 건너뛰어 탐색을 아끼는 장치다. 그 절약을 포기하는 대신 판정이 // 탐색 모드에 따라 달라지지 않는다 — 그쪽이 훨씬 중요하다.) const awards = (P._lean && !park.needPairs) ? [] : _parkAwardsFor(reads, mvKey); const from = st.pos[0]; const shared = park.sharedClaim; const sharedTok = shared ? st.tokens[shared.gem] : null; const sharedDecision = !!(shared && sharedTok && sharedTok.alive && P.sharedClaimStage === 'dual' && park.chain[P.dest] === shared.gem && manhattan(from, sharedTok) === 1); // SLIDE (P12b): one directional input glides the walker along its junction-brake path to // the rest cell. Tokens/pads are consumed IN PASSING (each swept cell processed in order, // the chain cursor advanced BETWEEN cells — strict chain order preserved), and each // nonDeep->deep TRANSITION along the sweep charges park.damage — the faithful analog of // the walk entry physics (one charge per contiguous deep run; the rest-only cost variant // measured DEAD: deepEntries 0 for every persona on every board = the G-C stake vanishes, // scout B 2026-07-10). Self-contained early-return block: the walk path below stays // byte-identical, and slide ships static-form only (no phase/relational slide tone). if (park.slide) { const cand = reads.legal.find(c => c.k === mvKey); if (mvKey !== 'stay') P.prev = { x: from.x, y: from.y }; let prevDeep = park.deep.has(_parkKey(st, from)); for (const cc of cand.cells) { st.pos[0] = { x: cc.x, y: cc.y }; const tk = st.tokens.find(t => t.alive && !t.pad && t.x === cc.x && t.y === cc.y); if (tk) { tk.alive = false; st.score[0] += tk.v; st.fx.push({ k: 'gem', x: cc.x, y: cc.y }); } if (P.dest < park.chain.length) { const pt = st.tokens[park.chain[P.dest]]; if (pt && pt.pad && pt.alive && pt.x === cc.x && pt.y === cc.y) { pt.alive = false; st.fx.push({ k: 'pad', x: pt.x, y: pt.y }); } } _parkAdvanceDest(P); const inDeep = park.deep.has(cc.key); if (inDeep && !prevDeep) { P.hearts -= park.damage == null ? 1 : park.damage; P.deepEntries++; st.fx.push({ k: 'deep', x: cc.x, y: cc.y }); } prevDeep = inDeep; } if (mvKey !== 'stay') { st.facing[0] = { dx: Math.sign(cand.x - from.x), dy: Math.sign(cand.y - from.y) }; // render hook (ZERO-TEXT): the multi-cell glide event the app animates — swept cells + // rest, glyph-only data (the ice-floor walkway look reads park.slide off the board). st.fx.push({ k: 'slide', x: cand.x, y: cand.y, from: { x: from.x, y: from.y }, cells: cand.cells.map(c => ({ x: c.x, y: c.y })) }); } P.turns++; P.moves.push(mvKey); P.path.push({ x: cand.x, y: cand.y }); for (const a of awards) P.awards.push({ turn: P.turns, ...a }); if (park.needPairs && P.turns > PARK_CAL_TURNS) for (const a of awards) P.posed.add(a.pair); if (P.turns % 2 === 1 || P.mode === 'relocate') _parkCompanionStep(P); _parkAdvanceDest(P); if (P.hearts <= 0) { P.hearts = 0; P.over = true; P.reason = 'death'; } else if (_parkReadDone(P)) { P.over = true; P.reason = 'complete'; } else if (P.turns >= park.cap) { P.over = true; P.reason = 'cap'; } if (park.dyn) park.dyn.beat++; // PARK-DYN: opt-in-only beat tick (no-op absent dyn) return { move: mvKey, awards, hearts: P.hearts, over: P.over, reason: P.reason }; } const mv = _PARK_MOVES.find(m => m.k === mvKey); const to = { x: from.x + mv.x, y: from.y + mv.y }; const fromDeep = park.deep.has(_parkKey(st, from)); // capture the safety hazard at the PRE-move state (rival + token liveness frozen here, so a // nearest_token violation is credited to the token the player is ABOUT to take, and an // adjacency violation to the rival's cell BEFORE the companion advances this turn). const relForm = _parkForm(park); const relTaboo = (relForm === 'static' || relForm === PARK_PHASE_FORM) ? null : _parkRivalTaboo(st, relForm); // PHASE (spec §3): read the PUBLIC segment the player OBSERVED at decision time (pre-move, // pre-advance) — the deep field bites ONLY when the pip ring shows RED. Single-state // recoverable: the cost is a pure function of the CURRENT visible clock + the step. const phaseRed = relForm === PARK_PHASE_FORM && clockSegOf(st, 0) === RED_SEG(park.phase.segN); const fromKey = _parkKey(st, from); if (mvKey !== 'stay') P.prev = { x: from.x, y: from.y }; st.pos[0] = to; st.facing[0] = mvKey !== 'stay' ? { dx: Math.sign(to.x - from.x), dy: Math.sign(to.y - from.y) } : st.facing[0]; // PUSH (P12): a push candidate carries the box one cell ahead. The PUSHER pays any deep // entry (below, unchanged); the box is inert cargo — no heart, no charge on the box cell. const pushCand = st.box ? reads.legal.find(c => c.k === mvKey) : null; if (pushCand && pushCand.push) { st.box = { x: pushCand.bx, y: pushCand.by }; st.fx.push({ k: 'push', x: pushCand.bx, y: pushCand.by }); // render hook (zero-text glyph nudge) } const tok = st.tokens.find(t => t.alive && !t.pad && t.x === to.x && t.y === to.y); if (tok) { tok.alive = false; st.score[0] += tok.v; st.fx.push({ k: 'gem', x: to.x, y: to.y }); } // At the adjacent shared-gem decision, the observed action resolves the scene. Entering the // gem is goal-over-care. Any other legal move leaves it to pink; the blue cursor advances to // the deterministic next chain gem and the existing goal beacon visibly retargets there. // No persona/order symbol is read here — this is a public consequence of the chosen move. if (sharedDecision) { if (sharedTok.alive) { P.sharedClaimStage = 'delegated'; P.dest++; P._fields = null; st.fx.push({ k: 'delegate', seat: 0, token: shared.gem, x: sharedTok.x, y: sharedTok.y }); } else { P.sharedClaimStage = 'taken'; } } // reach pads (spec 2026-07-04 §C.1): standing ON the CURRENT destination pad completes // the leg — no pickup, no score, its own fx glyph. Chain order stays strict (an off-order // pad is inert), so the leg semantics every blind readout rides are unchanged. if (P.dest < park.chain.length) { // chainAnyOf boards offer SEVERAL pads for one leg (y46 v2's four exits): standing on any of // them finishes it. Without this the walker could stand in an exit and the leg would not close, // because the cursor names only one of the four. const anyOf = park.chainAnyOf && park.chainAnyOf[P.dest]; const cand = anyOf ? anyOf : [park.chain[P.dest]]; for (const ti of cand) { const pt = st.tokens[ti]; if (pt && pt.pad && pt.alive && pt.x === st.pos[0].x && pt.y === st.pos[0].y) { pt.alive = false; st.fx.push({ k: 'pad', x: pt.x, y: pt.y }); break; } } } _parkAdvanceDest(P); if (relForm === 'static' || relForm === PARK_PHASE_FORM) { // static: EVERY deep entry costs the body. phase: a deep entry costs the body ONLY while // the clock is RED (green entry is free — the temporal gate). deepEntries counts only the // COSTLY entries in both, so the cross/detour signature reads the same violation meter. if ((relForm === 'static' || phaseRed) && !fromDeep && park.deep.has(_parkKey(st, to))) { P.hearts -= park.damage == null ? 1 : park.damage; // (hazard reskin knob; P1 boards = 1, ice = 0) P.deepEntries++; st.fx.push({ k: 'deep', x: to.x, y: to.y }); } // advance the player's OWN-TURN clock AFTER the entry is priced (all seats in lockstep, so // both pip rings cycle identically — the clock's existence/period never leaks which seat is // phase-conditional: C1). Fired once per player turn (pass or move); no-op off a clock. if (relForm === PARK_PHASE_FORM) for (const s in st.clock) advanceClock(st, Number(s), false); } else if (!relTaboo.has(fromKey) && relTaboo.has(_parkKey(st, to))) { // RELATIONAL violation: entering the live rival taboo charges the body exactly like a deep // entry (the taboo, not the terrain, carries the cost). deepEntries doubles as the // violation counter so the alive-check and the cross/detour signature stay meaningful. P.hearts -= park.damage == null ? 1 : park.damage; P.deepEntries++; st.fx.push({ k: 'deep', x: to.x, y: to.y }); } // FIELD MECH (Task 1) — the per-step physics hooks, fired AFTER the walker's position commits // and after the universal deep-entry charge above, so a mechanic reads a settled board. onLeave // (the vacated cell) then onEnter (the entered cell): y12 spends the stone the walker stepped // OFF, a toll gate charges the cell the walker stepped ON. Both fire for every move key, 'stay' // included — a mechanic that must ignore a non-move says so itself. No-op with no mechanic. const mech = _parkMech(st); const ev = mech ? { mvKey, from, to, fromKey, toKey: _parkKey(st, to) } : null; if (mech && mech.onLeave) mech.onLeave(P, ev); if (mech && mech.onEnter) mech.onEnter(P, ev); P.turns++; P.moves.push(mvKey); // the trace records where the walker ACTUALLY STANDS, not where the move pointed. Identical on // every board that has no mechanic (the walker is at `to`), but a mechanic whose force-opened // move is an ACTION rather than a step (y3's assist: you reach the fallen companion, you do not // stand on him) may restore st.pos[0] in onEnter, and the trace must not claim he walked there. P.path.push({ x: st.pos[0].x, y: st.pos[0].y }); for (const a of awards) P.awards.push({ turn: P.turns, ...a }); if (park.needPairs && P.turns > PARK_CAL_TURNS) for (const a of awards) P.posed.add(a.pair); _parkSharedClaimNotice(P, from); // the companion approaches its gem at HALF pace (small = weaker/slower, the contest // stays decidable by seat proximity) but relocates between stations at full pace so it // is seated for the next contest before any persona's journey reaches it. if (P.turns % 2 === 1 || P.mode === 'relocate') _parkCompanionStep(P); _parkAdvanceDest(P); if (P.hearts <= 0) { P.hearts = 0; P.over = true; P.reason = 'death'; } // PUSH (P12): completion = box ON pad (the push goal grammar); the walk chain read would // fire vacuously on the push board's empty chain, so the branch is explicit. else if (_parkReadDone(P)) { P.over = true; P.reason = 'complete'; } else if (P.turns >= park.cap) { P.over = true; P.reason = 'cap'; } if (park.dyn) park.dyn.beat++; // PARK-DYN: opt-in-only beat tick (no-op absent dyn) // FIELD MECH (Task 1) — THE WORLD ADVANCES. Fired after the beat so a mechanic's schedule reads // the beat it is ON (`beat % period === 0`), and only while the run is still live (a log does not // roll onto a walker who already finished). It fires on 'stay' too: a beat-driven entity that // froze whenever the walker waited would not be a clock, it would be a shadow. // A tick may end the run on its OWN terms (set P.over + P.reason — rising water drowns), or just // spend the body: the death check is RE-READ below, because a hit landing on the tick must kill // on THIS step, not silently a whole turn later. Guarded by !P.over so it can never overrule a // 'complete'/'cap' the step already earned. No-op with no mechanic (every legacy board). if (mech && mech.tick && !P.over) { mech.tick(P, ev); if (!P.over && P.hearts <= 0) { P.hearts = 0; P.over = true; P.reason = 'death'; } // ...and so is COMPLETION (Task 8 F7): a tick may finish the chain on its own terms — y10's // rising water washes gems away and calls _parkAdvanceDest itself — and a run that is done must // end on THIS step, not a whole turn later. Same predicate as the step's own check above. else if (!P.over && _parkReadDone(P)) { P.over = true; P.reason = 'complete'; } } return { move: mvKey, awards, hearts: P.hearts, over: P.over, reason: P.reason }; } // parkPlayout(st, persona): the oracle-driven continuous journey (demo generator + the // faithful-play arm of every invariant gate). Every move is parkOracleMove — no scripts. function parkPlayout(st, persona) { const P = parkStart(st); while (!P.over) parkStep(P, parkOracleMove(P, persona)); return P; } // _parkSurfaceMimic(demoSt, demoMoves, playSt, cap): the SMARTER mimic control (R4). The raw // move-replay control (_parkCrossIncongruent's anti-mimic clause) is VACUOUSLY defeated once demo // and play diverge into different genres — replaying ice moves on a Sokoban board is nonsense, so it // "passes" the anti-mimic bar for free. This is the control with teeth that survive genre distance: // a policy that learns exactly TWO SURFACE features from the demo trajectory — (1) per-terrain-class // AVOID frequencies (walkway / verge / deep / water-adjacent), (2) gem approach — and plays the play // board greedily on those alone. NO abstract order, NO conflict reading, NO persona symbol (C1: the // persona never enters this function — if it could see the persona the whole control would be void). // A pure, deterministic function of the PUBLIC state (parkStart / parkStep / _parkLegal) and the demo // trajectory: no Math.random, no Date; ties broken by a FIXED key order (U/D/L/R/stay). If even this // recovers posed pairs on a slot, that slot's OOD claim is DEAD — which is exactly what the // CROSS-SURFACE-MIMIC gate is for. Tasks 5-7 consume this signature in their ship gates; the caller // must hand a FRESHLY BUILT board for each of demoSt / playSt (parkStep mutates the board it steps). function _parkSurfaceMimic(demoSt, demoMoves, playSt, cap) { cap = cap == null ? demoMoves.length * 2 : cap; // terrain class of a cell key, from the SEED-FIXED deep-distance field (park.distDeep) ALONE — a // pure STATIC surface read, present on every board and reachable WITHOUT touching any mechanic- // private dyn.* field. That matters twice: this helper lives in the shared engine-BODY region and // the REGISTRY-SEAM-BODY gate forbids the body from reading dyn beyond dyn.beat; and a genuine // SURFACE learner should read the visible static terrain, not a mechanic's runtime hazard state. // These are the ENGINE'S OWN three terrain classes (_parkBuild builds exactly walkway/verge/deep; // distDeep means "0 deep / 1 verge / >= 2 safe walkway") — the faithful, non-invented // partition. The brief names four (walkway / verge / deep / water-adjacent), but on these STATIC- // safety boards the "water" IS the deep field and its water-adjacent band IS the verge (distDeep 1); // a fourth, distinct water-adjacent class exists only on DYNAMIC-water field boards (rising flood), // which this task does not cover AND which a seam-compliant body helper may not read anyway. Do NOT // split distDeep==2 into its own avoided class: that is a WALKWAY cell per the engine, and treating // it as separately-avoided makes the mimic detour the whole approach and express nothing — a // vacuous strawman the brief forbids (measured: it flips y16 from 3-recovered to 0-expressed). const cls = (st, key) => { const dd = st.park.distDeep ? st.park.distDeep[key] : Infinity; if (dd === 0) return 'deep'; // the hazard field itself if (dd === 1) return 'verge'; // its pale band == the "water-adjacent" band on static boards return 'walkway'; // distDeep >= 2: the safe interior/ring (one class, per the engine) }; // nearest LIVE gem distance field — multi-source BFS over non-wall cells from every alive token // (a "gem" is any shiny token; the mimic does NOT know which are chain destinations vs scenery, so // "approach shininess" stays a pure surface read). Recomputed per turn (gems die as they are taken). const gemDistField = (st) => { const n = st.N, dist = new Array(n * n).fill(Infinity), q = []; for (const t of st.tokens) { if (!t.alive) continue; const k = t.y * n + t.x; if (dist[k] === Infinity) { dist[k] = 0; q.push(k); } } for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk)) continue; if (dist[nk] > dist[k] + 1) { dist[nk] = dist[k] + 1; q.push(nk); } } } return dist; }; // ==== FEATURE 1: per-terrain-class AVOID frequency, learned by replaying the demo. Each demo state // offers a set of terrain classes (the classes its legal moves reach) — each an "opportunity"; the // class the walker actually STEPS INTO is "taken". avoidRate[c] = 1 - taken[c]/opp[c] in [0,1]: a // class the demo was offered but shunned (a safety-led walker and 'deep') scores near 1, one it // walked into freely near 0. Persona-blind — it reads only the public trajectory. // ==== FEATURE 2: gem approach — the fraction of demo steps that STRICTLY reduced the nearest-live- // gem distance among steps where a reduction was on offer. Drives the gem term's weight in play. const CLASSES = ['deep', 'verge', 'walkway']; const opp = { deep: 0, verge: 0, walkway: 0 }; const took = { deep: 0, verge: 0, walkway: 0 }; let gemChances = 0, gemTaken = 0; const D = parkStart(demoSt); for (const mv of demoMoves) { if (D.over) break; const legal = _parkLegal(D); const gd = gemDistField(D.st); const hereKey = _parkKey(D.st, D.st.pos[0]), hereGem = gd[hereKey]; const offered = new Set(); let canApproach = false; for (const c of legal) { offered.add(cls(D.st, c.key)); if (isFinite(gd[c.key]) && gd[c.key] < hereGem) canApproach = true; } for (const c of offered) opp[c]++; if (canApproach) gemChances++; parkStep(D, mv); const afterKey = _parkKey(D.st, D.st.pos[0]); if (afterKey !== hereKey) took[cls(D.st, afterKey)]++; // a real step into a new cell if (canApproach && isFinite(gd[afterKey]) && gd[afterKey] < hereGem) gemTaken++; } const avoidRate = {}; for (const c of CLASSES) avoidRate[c] = opp[c] > 0 ? 1 - took[c] / opp[c] : 0; // gemPull in [0,1] — the learned strength of gem approach. Absent any gem-approach choice in the // demo, default to a STRONG pull (1): a degenerate 0 would blind the mimic to gems and make it a // strawman, but the control must be the STRONGEST reasonable surface learner, so it still seeks gems. const gemPull = gemChances > 0 ? gemTaken / gemChances : 1; // ==== PLAY the play board greedily on those two features ALONE. Score a candidate by avoidance- // weight (avoidRate of its destination class, scaled so an avoided class costs up to AVOID_W gem- // steps — terrain can override a small detour, but gem-seeking still drives overall progress) plus // gemPull x (gem-BFS distance of its destination). Lower is better; argmin, ties broken by the FIXED // key order U/D/L/R/stay (determinism, C1). No persona, no order, no conflict read anywhere. const AVOID_W = 4; const ORDER = { U: 0, D: 1, L: 2, R: 3, stay: 4 }; const P = parkStart(playSt); const moves = []; while (!P.over && moves.length < cap) { const legal = _parkLegal(P); const gd = gemDistField(P.st); let best = null, bestScore = Infinity, bestRank = Infinity; for (const c of legal) { const gdist = isFinite(gd[c.key]) ? gd[c.key] : P.st.N * P.st.N; const score = AVOID_W * avoidRate[cls(P.st, c.key)] + gemPull * gdist; const rank = ORDER[c.k] == null ? 99 : ORDER[c.k]; if (score < bestScore - 1e-9 || (Math.abs(score - bestScore) <= 1e-9 && rank < bestRank)) { bestScore = score; bestRank = rank; best = c; } } const mv = best ? best.k : 'stay'; moves.push(mv); parkStep(P, mv); } return moves; } // ---- park.dyn substrate (Task 0, spec 2026-07-13): the common runtime-state container + // deterministic beat/schedule helpers that every later park mechanic (consumable stepping // stones, toll gates, a downed companion, a rolling log, rising water, a duckling) hangs its // own mutable state off. OPT-IN by construction: nothing in this module ever assigns // st.park.dyn itself — a task builder does that explicitly (as the later mechanics will), so a // board that never opts in carries no dyn field at all and parkStep runs its unmodified legacy // path (byte stability — C1/C11). // _parkDynInit(st): a fresh, empty dyn container. Pure factory — no board reads, no randomness; // st is accepted only to match the call-site shape (`st.park.dyn = _parkDynInit(st)`), keeping // the door open for a later mechanic to seed board-derived initial state without changing this // call convention. // WHY THE FIVE FIELD NAMES STAY (asked at review: could this now be just `{ beat: 0 }`, since // _parkDeepClone no longer needs them declared?). It could — but it MUST NOT. These names are Task // 0's PUBLISHED SUBSTRATE CONTRACT: the plan and all five task briefs specify this exact container // (`dyn = { gone, flood, ents, downed, gateOpen, beat }`) and the five mechanics are written // against it — y14's brief asserts on `dyn.gateOpen.mate`, y3's on `dyn.downed.rescued`, y6's on // `dyn.ents`. Emptying it to win a cosmetic purity point would silently break five briefs at once. // And it is NOT a seam leak: declaring a shared container is Task 0's job, and the engine BODY // never READS any of these fields — which is the invariant that actually matters, and which the // REGISTRY-SEAM-BODY gate enforces (it forbids every `dyn.` member access but `.beat`). function _parkDynInit(st) { return { gone: new Set(), flood: new Set(), ents: [], downed: null, gateOpen: { me: false, mate: false }, beat: 0 }; } // _parkDeepClone(v): a structural deep copy of a dyn container (review C3). The engine body cannot // know what a mechanic keeps in dyn — that is the entire point of the seam — so a search fork must // copy it WITHOUT naming its fields. Handles the shapes a mechanic can legitimately hold: Sets of // cell keys, arrays of entities, nested plain objects, primitives. (A dyn Set holds cell keys — // primitives — by contract; nothing deeper is copied INTO a Set.) Keeping this generic is what lets // the seam gate forbid the body from reading any dyn field but `.beat`. function _parkDeepClone(v) { if (v instanceof Set) return new Set(v); if (v instanceof Map) { const m = new Map(); for (const [k, x] of v) m.set(k, _parkDeepClone(x)); return m; } if (Array.isArray(v)) return v.map(_parkDeepClone); if (v && typeof v === 'object') { const o = {}; for (const k in v) o[k] = _parkDeepClone(v[k]); return o; } return v; } // _parkBeatOf(P): the current dyn beat, 0 on a dyn-less board (so a schedule read is always // well-defined whether or not the board opted into dyn). function _parkBeatOf(P) { return P.st.park.dyn ? P.st.park.dyn.beat : 0; } // _parkSchedule(seed, n, period): a length-n table of deterministic per-entity beat offsets, // each in [0, period) — the shared timing primitive later mechanics compose their own // drying/tilting/gate/flood schedules from (e.g. "entity i fires when beat % period === // table[i]"), so parallel entities phase-stagger instead of ticking in lockstep. Derived ONLY // from the module's seeded rng (never Math.random/Date), so the same (seed, n, period) always // reproduces the same table (deterministic-schedules, C1: never fed persona/order symbols). function _parkSchedule(seed, n, period) { const rnd = rng(seed); const out = []; for (let i = 0; i < n; i++) out.push(Math.floor(rnd() * period)); return out; } // PARK_CAL_TURNS (P3a spec 2026-07-03 §4): THE CALIBRATION SPAN — the first PARK_CAL_TURNS // (the prose here says "span", never the w-word: C11 greps engine.js for DOM symbols and it greps // COMMENTS too, so the browser global's name is not sayable in this file. Not a style note — a gate.) // effective PLAYER turns of every JUDGED park episode are calibration. The player is still finding // the controls, so those turns are EXCLUDED from the violation tally and from EVERY blind readout // denominator (conflicts / sigma / posterior / discovery / recovery). The state still evolves // through them (physics is not skipped); only their READS are thrown away. // // IT LIVES HERE, IN THE ENGINE, BECAUSE CALIBRATION IS A PROPERTY OF THE READ (Task 10). It used to // live only in campaign.js, and the consequence was a MEASUREMENT SPLIT that shipped a false claim: // the readout obeyed the span (campaign.js scores every row at skip = PARK_CAL_TURNS) while the // field modules' SHIP BARS — the predicates that decide whether a cell may claim it can read a // player — called parkRecoverOrder with NO skip. So a cell could earn `ship: true` on evidence the // product is required by spec to throw away, and y8 (the rolling log) did exactly that: 6/6 blind // order recovery uncalibrated, 0/6 the moment the readout's own calibration is applied (all of its // discrimination lives in move #1). The ship bars now read at THIS constant, so the bar measures // what the product actually reads. campaign.js consumes it as E.PARK_CAL_TURNS and re-exports it // under its own name (C.PARK_CAL_TURNS) for its existing call sites; the VALUE is unchanged, so no // existing cell, seed or seated board moves. const PARK_CAL_TURNS = 2; // parkRecoverOrder(st, moves, skip): BLIND order recovery — trajectory + a fresh board ONLY (no // persona input anywhere). Replays the moves through parkStep, accumulates the pairwise // awards, takes each pair's majority, and reassembles the strict total order (win counts // 2/1/0 — the topo reassembly absorbed from the beat overlay). Null when any pair is // undecided/tied or the majorities are cyclic. `skip` (P3a §4 calibration, default 0 = // byte-identical legacy behavior) drops the awards posed by the first `skip` effective // turns — the state still evolves through them; only their reads are excluded. function parkRecoverOrder(st, moves, skip) { const P = parkStart(st); for (const mv of moves) { if (P.over) break; parkStep(P, mv); } const tally = {}; for (const [a, b] of _PARK_PAIRS) tally[a + b] = { [a]: 0, [b]: 0 }; for (const w of P.awards) if (!(skip > 0 && w.turn <= skip)) tally[w.pair][w.winner]++; const wins = { G: 0, C: 0, N: 0 }; for (const [a, b] of _PARK_PAIRS) { const ta = tally[a + b][a], tb = tally[a + b][b]; if (ta === tb) return null; // undecided pair wins[ta > tb ? a : b]++; } const order = ['G', 'C', 'N'].sort((a, b) => wins[b] - wins[a]); if (order.map(k => wins[k]).join('') !== '210') return null; // cyclic majorities return order.map(k => PARK_ATT_AXIS[k]); } // parkRecoverOrderClosed(st, moves, skip): the SAME blind recovery, closed under TRANSITIVITY. // // A persona IS a strict total order over three things, so the three comparisons are not // independent: any two that CHAIN determine the third. If a walk decides care>goal and // goal>safety, then care>safety follows — no scene needs to have posed it, because no order // exists in which the two decided facts hold and the third does not. parkRecoverOrder demanded a // strict majority on all three anyway and returned null otherwise, so those walks reached the // player as 읽을 수 없음 while the evidence in hand named exactly one persona. // // The rule here is stated as a filter rather than as case analysis, which is why it needs no // cyclicity check and no special cases: keep the permutations consistent with every DECIDED pair, // and answer only if exactly one survives. Where parkRecoverOrder answers, exactly one survives // too (three acyclic majorities pin a unique order), so this function returns the same order — // it is a strict extension, not a different reading. Cyclic majorities leave zero survivors; // fewer than two decided pairs leave two or more. Either way: null. // // MEASURED before it was wired (tools/order-closure-probe.mjs, 2026-08-02; 18 live cells x 6 // personas x noise 0.1/0.25/0.4 x 8 rollouts = 2,192 completed walks of the shape a HUMAN // produces — the demonstrated persona followed with slips, which is the only population that has // a ground truth to be scored against): // // read 1,525 (69.6%) -> 1,655 (75.5%) accuracy 90.8% -> 90.2% // the 130 newly-read walks are 83.1% correct // // So it buys ~6 points of coverage at ~7 points of precision ON THE WALKS IT ADDS, and the price // is named here rather than hidden: 22 of those 130 now carry a wrong verdict where they used to // carry none. That is the right trade only because the error is the SAME error the two decided // pairs already carry (a noisy walk yields a wrong majority) — this is not a coin flip dressed as // a reading, which is the thing the honesty gate exists to forbid. // // THE SHIP BARS DO NOT USE THIS. A bar asks "does this cell POSE all three conflicts", which is a // property of the board and must not get easier because a reader got cleverer; the readout asks // "can this walk be named", which is a property of the evidence. Same discipline as // CROSS-PLAY-READABLE: two different questions, two different rulers, both written down. function parkRecoverOrderClosed(st, moves, skip) { const P = parkStart(st); for (const mv of moves) { if (P.over) break; parkStep(P, mv); } const tally = {}; for (const [a, b] of _PARK_PAIRS) tally[a + b] = { [a]: 0, [b]: 0 }; for (const w of P.awards) if (!(skip > 0 && w.turn <= skip)) tally[w.pair][w.winner]++; const decided = []; for (const [a, b] of _PARK_PAIRS) { const ta = tally[a + b][a], tb = tally[a + b][b]; if (ta !== tb) decided.push(ta > tb ? [a, b] : [b, a]); // [winner, loser] } const ok = _PARK_ORDER_PERMS.filter(o => decided.every(([hi, lo]) => o.indexOf(hi) < o.indexOf(lo))); return ok.length === 1 ? ok[0].map(k => PARK_ATT_AXIS[k]) : null; } // the six permutations of the three att keys, built once (the closure filters over them). const _PARK_ORDER_PERMS = (() => { const out = []; (function perm(a, rest) { if (!rest.length) return out.push(a); for (let i = 0; i < rest.length; i++) perm(a.concat(rest[i]), rest.filter((_, j) => j !== i)); })([], ['G', 'C', 'N']); return out; })(); // parkSigma(st, moves): BLIND continuous trajectory scores over the three axis pairs (spec // 2026-07-03 P2 §A.2 — Kwon's sigma): trajectory + a fresh public board only, no persona // input. Each score reads "the pair's FIRST axis won" in [0,1] (numerator is a subset of the // denominator by construction — no clamp), null when the pair was never posed; the posed // denominators ride along in den: // gc goal-vs-safety: savings-weighted deep crossings per chain LEG — a leg is posed when a // strictly-shorter deep shortcut to its destination exists (w = detour - beeline, read // at the leg's first priced state); it scores w when the leg saw a deep-field entry. // den.gcLegs lists the posed legs' weights in chain order — the m1 temptation LADDER // the TASK-ESCALATION gate pins (strictly growing savings per round). // gk goal-vs-care: contested companion gems PREEMPTED / contested gems posed (contested // = the _parkNCtx gem read: alive, player within 3 and no farther than the companion). // ck safety-vs-care: HOLDS / (holds + cedes) at yield scenes (_parkNCtx blocked read): // cede = stepped off the companion's route; hold = stayed / kept blocking. // `skip` (P3a §4 calibration, default 0 = byte-identical legacy behavior): the first `skip` // effective turns evolve the state but contribute NO reads (posed/taken/held) — every leg is // priced at its first NON-excluded state, so a run entered at turn skip+1 scores identically // to a fresh run started from that state. function parkSigma(st, moves, skip) { const P = parkStart(st); P._lean = true; // pairwise awards unused here let gcPosed = 0, gcTaken = 0, holds = 0, cedes = 0; let leg = -1, legW = 0, legEntered = false; const gcLegs = []; const gkPosed = new Set(), gkTaken = new Set(); const closeLeg = () => { if (legW > 0) { gcPosed += legW; gcLegs.push(legW); if (legEntered) gcTaken += legW; } }; for (const mv of moves) { if (P.over) break; if (skip > 0 && P.turns < skip) { parkStep(P, mv); continue; } // calibration: state only, no reads const pk = _parkKey(st, st.pos[0]), inDeep = _parkInHazard(st, pk); // deep terrain (static) or live rival taboo (relational) if (P.dest !== leg) { closeLeg(); leg = P.dest; legW = 0; legEntered = false; } if (legW === 0 && !inDeep) { const f = _parkFields(P); if (f && isFinite(f.detour[pk]) && f.detour[pk] > f.fast[pk]) legW = f.detour[pk] - f.fast[pk]; } const ctx = _parkNCtx(P); const contested = ctx.gem ? st.park.contracts[P.contract].gem : null; if (contested != null) gkPosed.add(contested); const route = new Set(ctx.blocked && ctx.plan ? ctx.plan.path : []); if (parkStep(P, mv).noise) continue; // fat-fingers are not decisions const nk = _parkKey(st, st.pos[0]); if (!inDeep && _parkInHazard(st, nk)) legEntered = true; if (contested != null) { const t = st.tokens[contested]; if (!t.alive && t.x === st.pos[0].x && t.y === st.pos[0].y) gkTaken.add(contested); } if (ctx.blocked) (nk === pk || route.has(nk)) ? holds++ : cedes++; } closeLeg(); const ratio = (a, b) => b > 0 ? a / b : null; return { gc: ratio(gcTaken, gcPosed), gk: ratio(gkTaken.size, gkPosed.size), ck: ratio(holds, holds + cedes), den: { gc: gcPosed, gk: gkPosed.size, ck: holds + cedes, gcLegs } }; } // parkPosterior(st, moves, eps): BLIND Bayesian posterior over the 6 personas PLUS an // unstructured-walker NULL hypothesis (spec §A.3, mirroring Kwon's lambda posterior) — // P(pi|traj) ∝ Π_t p_eps(move_t | prescription_pi(state_t)) under the slip model // p_eps(m) = 1-eps+eps/L for the prescribed move (the persona's own deterministic // parkOracleMove), eps/L for every other legal move (L = |legal|). The null hypothesis // scores every legal move 1/L: a trajectory whose oracle matches are only chance-level // loses to it, so a random walk can never buy persona confidence off a couple of // coincidental matches (the false-confidence calibration guard — persona-independent, C1). // Consumes trajectory + a fresh public board only; effective decisions only (noise inputs // never enter the stream, and any stray illegal move is skipped, not scored). Returns // { probs (keyed 'goal>safety>care' style), map (the MAP order), mass (its posterior // probability), noise (the null hypothesis' mass) } with sum(probs) + noise = 1. // `skip` (P3a §4 calibration, default 0 = byte-identical legacy behavior): the first `skip` // effective turns evolve the state but enter NO likelihood — scoring starts at turn skip+1. function parkPosterior(st, moves, eps, skip) { eps = eps == null ? 0.15 : eps; const P = parkStart(st); P._lean = true; const logp = PARK_PERSONAS.map(() => 0); let logNull = 0; for (const mv of moves) { if (P.over) break; if (skip > 0 && P.turns < skip) { parkStep(P, mv); continue; } // calibration: state only const reads = _parkReads(P), L = reads.legal.length; if (reads.legal.some(c => c.k === mv)) { for (let i = 0; i < PARK_PERSONAS.length; i++) logp[i] += Math.log(mv === parkOracleMove(P, PARK_PERSONAS[i]) ? 1 - eps + eps / L : eps / L); logNull += Math.log(1 / L); } parkStep(P, mv); } const mx = Math.max(logNull, ...logp); const w = logp.map(l => Math.exp(l - mx)), wN = Math.exp(logNull - mx); const Z = w.reduce((a, b) => a + b, 0) + wN; const probs = {}; let map = 0; PARK_PERSONAS.forEach((p, i) => { probs[p.join('>')] = w[i] / Z; if (w[i] > w[map]) map = i; }); return { probs, map: PARK_PERSONAS[map].slice(), mass: w[map] / Z, noise: wN / Z }; } // ─── P10.5 measurement corrections (spec 2026-07-08 §1/§2): the per-pair (marginal) READ layer // and the diverse-path (equivalence-class) sampler. Pure CONSUMERS of parkPosterior / the // existing read helpers — the frozen scored core and parkPosterior's body are untouched. ─── // // _parkKindPair(kind): the conflict PAIR (att keys) a minigame kind SIGNATURE-poses, the inverse // of _PARK_TASK_NEED[kind].den under {gc:goal-safety, gk:goal-care, ck:safety-care}. m1=[G,C], // m2=[G,N], m3=[C,N]; m4 poses two pairs; an observer (m5, den:[]) poses none. const _PARK_DEN_PAIR = { gc: ['G', 'C'], gk: ['G', 'N'], ck: ['C', 'N'] }; function _parkKindPair(kind) { const need = _PARK_TASK_NEED[kind]; return (need && need.den ? need.den : []).map(d => _PARK_DEN_PAIR[d]).filter(Boolean); } // parkPosedPairs(kind, fieldMech): the kind's signature pairs UNIONED with the pairs a field // mechanism DECLARES it purpose-poses (PARK_FIELD_MECHS[fieldMech].pairs — data, optional; the // declaration is a claim the module's admits/ship gate must back with expressed>0, never taken // on faith). With no fieldMech or no declaration this IS _parkKindPair(kind) — every existing // slot reads byte-identically (design 2026-07-23 cn-field-pose-accounting). Kind pairs first, // duplicates dropped; a registry LOOKUP only (REGISTRY-SEAM-BODY: no mech is named here). function parkPosedPairs(kind, fieldMech) { const out = _parkKindPair(kind); const m = fieldMech && PARK_FIELD_MECHS[fieldMech]; if (!m || !m.pairs) return out; const seen = new Set(out.map(p => p[0] + p[1])); for (const p of m.pairs) { const k = p[0] + p[1]; if (seen.has(k)) continue; seen.add(k); out.push([p[0], p[1]]); } return out; } // parkPairMarginal(post, pair): the blind posterior (parkPosterior output) MARGINALIZED to one // conflict pair (spec §1 — correct granularity). `pair` = att keys, e.g. ['G','C']. Sums the 6 // persona masses by whether the pair's FIRST axis outranks the SECOND in that persona's order, // giving pFwd (a>b) vs pRev (b>a); the MAP-marginal winner is the heavier side. The NULL is // rejected exactly as parkPosterior rejects it at the joint level — the pair's decided mass must // beat the random-walker mass post.noise (pFwd+pRev = 1-noise, so max(pFwd,pRev) > noise is the // pair-granularity persona-vs-null test). Consumes the posterior only; reads no persona symbol. function parkPairMarginal(post, pair) { const axA = PARK_ATT_AXIS[pair[0]], axB = PARK_ATT_AXIS[pair[1]]; let pFwd = 0, pRev = 0; for (const key in post.probs) { const ord = key.split('>'); if (ord.indexOf(axA) < ord.indexOf(axB)) pFwd += post.probs[key]; else pRev += post.probs[key]; } const fwd = pFwd >= pRev; const hi = fwd ? pair[0] : pair[1], lo = fwd ? pair[1] : pair[0]; return { hi, lo, mapPair: [hi, lo], pFwd, pRev, noise: post.noise, nullRejected: Math.max(pFwd, pRev) > post.noise }; } // parkRecoverPair(st, moves, pair, opts): the §1 per-pair recovery predicate for a TRAJECTORY — // build the blind posterior over the fresh public board, marginalize to the scenario's posed // `pair`, and report whether that pair's ORDER is recovered. `recovered` = null rejected AND // (when opts.expect = the demonstrated [hi,lo] direction is supplied) the MAP marginal matches // it. opts.eps / opts.skip pass straight through to parkPosterior (defaults byte-identical). function parkRecoverPair(st, moves, pair, opts) { opts = opts || {}; const m = parkPairMarginal(parkPosterior(st, moves, opts.eps, opts.skip), pair); const dirOk = opts.expect ? (m.hi === opts.expect[0] && m.lo === opts.expect[1]) : true; return { ...m, recovered: m.nullRejected && dirOk }; } // parkFaithfulPaths(board, persona, k, seed): sample up to k DISTINCT FAITHFUL playouts (spec §2 // equivalence-class verification). At each state the mover picks a UNIFORM-RANDOM member of the // lex-compliant set _parkLexSet(_parkReads(P), ordering) — every such move is persona-faithful; // the oracle's metric-argmin is just ONE canonical member. Deterministic given `seed` (a single // seeded PRNG drives the whole draw). Each path is a complete playout to a clean terminal. The // shared `board` is never mutated: a pristine runtime is forked per attempt (_parkClone shares // the immutable seed geometry, copies every mutable field). Returns [{moves, reason, hearts}]. function parkFaithfulPaths(board, persona, k, seed) { const ordering = parkOrderingFor(persona); const P0 = parkStart(board); // pristine wrapper; never stepped const rand = rng((seed >>> 0) || 1); const paths = [], seen = new Set(); // hard cap + a deterministic STALL cutoff: when a persona×board's faithful equivalence class is // small (e.g. a goal-top persona whose beeline preference is a singleton), the class is genuinely // exhausted after a few draws — stop once `maxStall` consecutive draws add no NEW distinct path // (deterministic given seed) rather than burning the full attempt budget re-drawing the same path. const maxAttempts = Math.max(2000, k * 400), maxStall = Math.max(400, k * 80); let stall = 0; for (let a = 0; a < maxAttempts && paths.length < k && stall < maxStall; a++) { const P = _parkClone(P0); P.path = [{ x: P.st.pos[0].x, y: P.st.pos[0].y }]; // restore parkStart's path seed (clone drops it) while (!P.over) { const reads = _parkReads(P); const cur = _parkLexSet(reads, ordering); const cands = reads.legal.map(c => c.k).filter(m => cur.has(m)); const pick = cands.length ? cands[Math.floor(rand() * cands.length)] : 'stay'; parkStep(P, pick); } const sig = P.moves.join(''); if (seen.has(sig)) { stall++; continue; } seen.add(sig); stall = 0; paths.push({ moves: P.moves.slice(), reason: P.reason, hearts: P.hearts }); } return paths; } // parkPosteriorSet(st, moves, eps, skip): the WIDENED (equivalence-class) posterior — the spec // §2 read-widening fix. IDENTICAL to parkPosterior EXCEPT the persona likelihood scores a move by // lex-set MEMBERSHIP (_parkAwardsFor / lexFilter style) instead of argmin-oracle EQUALITY: a // persona's prescribed SET at a state is S = _parkLexSet(reads, ordering) (every member is // persona-faithful; parkOracleMove's metric-argmin is just ONE canonical member). The slip model // spreads the compliant mass across S: a move in S scores (1-eps)/|S| + eps/L, any other legal // move eps/L, the null still 1/L each (so probs + noise = 1 exactly, as parkPosterior). This is // why a NON-canonical-but-faithful diverse path (spec §2 parkFaithfulPaths) no longer reads as a // slip for its own persona: the argmin posterior collapses null-rejection off-oracle, the set // posterior does not. parkPosterior's body is left BYTE-IDENTICAL; this is a pure NEW consumer. function parkPosteriorSet(st, moves, eps, skip) { eps = eps == null ? 0.15 : eps; const P = parkStart(st); P._lean = true; const ords = PARK_PERSONAS.map(p => parkOrderingFor(p)); const logp = PARK_PERSONAS.map(() => 0); let logNull = 0; for (const mv of moves) { if (P.over) break; if (skip > 0 && P.turns < skip) { parkStep(P, mv); continue; } // calibration: state only const reads = _parkReads(P), L = reads.legal.length; if (reads.legal.some(c => c.k === mv)) { for (let i = 0; i < PARK_PERSONAS.length; i++) { const S = _parkLexSet(reads, ords[i]); logp[i] += Math.log(S.has(mv) ? (1 - eps) / S.size + eps / L : eps / L); } logNull += Math.log(1 / L); } parkStep(P, mv); } const mx = Math.max(logNull, ...logp); const w = logp.map(l => Math.exp(l - mx)), wN = Math.exp(logNull - mx); const Z = w.reduce((a, b) => a + b, 0) + wN; const probs = {}; let map = 0; PARK_PERSONAS.forEach((p, i) => { probs[p.join('>')] = w[i] / Z; if (w[i] > w[map]) map = i; }); return { probs, map: PARK_PERSONAS[map].slice(), mass: w[map] / Z, noise: wN / Z }; } // parkRecoverPairLex(st, moves, pair, opts): the §2 per-pair recovery predicate riding the WIDENED // posterior (parkPosteriorSet) — the diverse-path (equivalence-class) counterpart of // parkRecoverPair, which rides the argmin parkPosterior. Same marginalize-to-pair + null-reject + // optional demonstrated-direction check; only the underlying posterior differs. Use this for // DIVERSE-PATH recovery (faithful non-canonical paths); use parkRecoverPair for canonical/ // composition (byte-identical to parkRecoverOrder's argmin awards). opts.eps/opts.skip pass through. function parkRecoverPairLex(st, moves, pair, opts) { opts = opts || {}; const m = parkPairMarginal(parkPosteriorSet(st, moves, opts.eps, opts.skip), pair); const dirOk = opts.expect ? (m.hi === opts.expect[0] && m.lo === opts.expect[1]) : true; return { ...m, recovered: m.nullRejected && dirOk }; } // parkPairExpressed(st, moves, pair, opts): the count of trajectory states at which the posed // `pair` is EXPRESSIBLE — i.e. the observed move is DISCRIMINATING for that pair (it earns a blind // pairwise award for it via _parkAwardsFor, the very engine parkRecoverOrder rides). A pair is // expressed at a state iff the choice there actually depends on that pair's relative order; when // one of its attitudes is INERT throughout a trajectory (e.g. a care-top persona wandering a board // whose care concern never engages), the pair earns NO award and the count is 0 — the trajectory // carries NO evidence about that pair. This is the §2 "score a pair only where both its attitudes // are engaged/expressible" read, tied to the SAME discriminating-award notion the canonical // recovery uses: a diverse-path recovery that fails ONLY where this count is 0 is genuine // UNOBSERVABILITY (the demonstration never posed the pair on that path), not a mis-read — whereas a // failure on a path with count>0 would be a real read flaw. opts.skip passes through (default 0 = // byte-identical legacy). Pure consumer; frozen scored core + parkPosterior body untouched. function parkPairExpressed(st, moves, pair, opts) { opts = opts || {}; const skip = opts.skip || 0; const P = parkStart(st); P._lean = true; const a = pair[0], b = pair[1]; let n = 0; for (const mv of moves) { if (P.over) break; if (skip > 0 && P.turns < skip) { parkStep(P, mv); continue; } const reads = _parkReads(P); if (reads.legal.some(c => c.k === mv)) for (const w of _parkAwardsFor(reads, mv)) if (w.pair === a + b || w.pair === b + a) n++; parkStep(P, mv); } return n; } // ─── CROSS-DOMAIN-COUPLING MACHINERY (constitution principle 5 — the diversity bound; the // chess→poker measurement DEFINED, not yet demonstrated). Two games belong to one benchmark iff // the persona ORDER recovered blindly from one PREDICTS faithful play in the other; surface // similarity is neither required nor rewarded. The measurement: recover a persona's axis-order // from game A's faithful play (via A's certified reduction), confirm it predicts that persona's // faithful play in game B (via B's reduction), the reverse B→A, and confirm a SURFACE-MIMIC (A's // move string replayed under B's reduction) does NOT satisfy B's order (the order transfers, never // the moves). These are GENERIC CONSUMERS of a GAME DESCRIPTOR: a future chess and poker plug in // by supplying their OWN reduction methods and the coupling math is unchanged. // // HONEST STATUS (constitution P5): the SHIPPED gate today exercises this machinery WITHIN ONE // reduction (the park reduction) across furniture/verb variants — it demonstrates order-coupling // plus a VERB-diversity anti-mimic, NOT cross-ontology coupling. True cross-domain coupling (a // SECOND independent certified reduction with a non-hazard-field C) is the frontier and is not // shipped. Pure consumers of the park read-stack (parkReduce / parkPairExpressed / // parkPosteriorSet / parkRecoverPairLex). ─── // // parkGame(spec): a GAME DESCRIPTOR — { build, pairs, faithful, expressed, decide, predict }. It // wraps a board-builder + the att-pairs the game SIGNATURE-poses with the PARK certified reduction // as the DEFAULT reading stack; a game whose reduction differs supplies its own {faithful, // expressed, decide, predict} and nothing downstream changes. Every method REBUILDS a fresh board // per read (parkPlayout / parkPosterior mutate the board handed to them — the fresh-build-per- // consumer invariant; `build` must return a NEW board each call). function parkGame(spec) { const build = spec.build, pairs = spec.pairs; return { build, pairs, // faithful(persona) -> the persona's faithful move stream (the oracle canonical member). faithful: spec.faithful || (persona => parkPlayout(build(), persona).moves), // expressed(moves, pair) -> count of trajectory states DISCRIMINATING the pair (evidence; // 0 => the game never posed the pair on this play, genuine unobservability — finding C gate). expressed: spec.expressed || ((moves, pair) => parkPairExpressed(build(), moves, pair)), // decide(moves, pair) -> [hi,lo] att keys the blind marginal recovers (null-rejected) or null. decide: spec.decide || ((moves, pair) => { const m = parkPairMarginal(parkPosteriorSet(build(), moves), pair); return m.nullRejected ? [m.hi, m.lo] : null; }), // predict(moves, pair, expect) -> is the trajectory consistent with the expected [hi,lo] dir. predict: spec.predict || ((moves, pair, expect) => parkRecoverPairLex(build(), moves, pair, { expect }).recovered), }; } // parkRecoverAxisOrder(game, moves): EVIDENCE-GATED blind order recovery (principle 4 — composed // per pair). For every pair the game poses, recover its direction ONLY where the trajectory // EXPRESSES it (game.expressed>0 — finding C: a bare null-reject off a NON-posed pair leans on // spurious set differences and must be excluded) AND the blind marginal null-rejects. Returns a // partial order map {'GC':[hi,lo], ...} (att keys); a FULL total order when every posed pair // decides acyclically (== parkRecoverOrder on the park). Reads no persona symbol. function parkRecoverAxisOrder(game, moves) { const order = {}; for (const pair of game.pairs) { if (!(game.expressed(moves, pair) > 0)) continue; const d = game.decide(moves, pair); if (d) order[pair[0] + pair[1]] = d; } return order; } // parkCoupleEpisode(order, game, moves): does `order` (recovered from ANOTHER game) PREDICT this // game's trajectory? The verdict is EPISODE-LEVEL AND over every pair BOTH order-decided AND // game-expressed (finding A — per-pair averaging floors the discriminant at the persona-agreement // rate; the AND drives a mismatched FULL order to 0 while a matched one stays 1). shared==0 => // no evidence overlap on this episode (NOT comparable; the caller excludes it). function parkCoupleEpisode(order, game, moves) { let shared = 0, ok = true; for (const pair of game.pairs) { const k = pair[0] + pair[1]; if (!order[k]) continue; if (!(game.expressed(moves, pair) > 0)) continue; shared++; if (!game.predict(moves, pair, order[k])) ok = false; } return { coupled: shared > 0 && ok, shared }; } // parkCoupling(A, B, personas): the coupling measurement for one ordered game pair (the // CROSS-DOMAIN-COUPLING machinery; today run within the park reduction). Recover each persona's // axis-order from A's faithful play, then over the persona×persona grid measure whether A's order // predicts B's faithful play. Returns three fractions over COMPARABLE episodes (shared>0): // matched (same persona) — the coupling fraction (~1: A's order IS B's; within-park // today, cross-ontology chess→poker at the frontier). // mismatched (persona i vs j play) — the DISCRIMINANT (must be lower; a full-order source with // 3 shared pairs separates to 0, a single-shared-pair source // sits at the persona-geometry floor ≈ 0.40, disclosed). // mimic (A's moves under B) — the anti-mimic bound: a pair whose MOVE VERB differs reads // 0 (raw moves carry no order into B's verb-space); a same-verb // pair leaks >0 (the metric flags shared movement furniture). // NB within one reduction this is VERB-diversity, not ontology- // diversity; cross-ontology mimic-0 needs a second reduction. // Fresh build per read throughout (the descriptor guarantees it); reads no persona in any recovery. function parkCoupling(A, B, personas) { const faithA = personas.map(p => A.faithful(p)); const faithB = personas.map(p => B.faithful(p)); const orders = faithA.map(mv => parkRecoverAxisOrder(A, mv)); const acc = { matched: { ok: 0, n: 0 }, mismatched: { ok: 0, n: 0 }, mimic: { ok: 0, n: 0 } }; for (let i = 0; i < personas.length; i++) { for (let j = 0; j < personas.length; j++) { const c = parkCoupleEpisode(orders[i], B, faithB[j]); if (c.shared === 0) continue; const bin = i === j ? acc.matched : acc.mismatched; bin.n++; if (c.coupled) bin.ok++; } const m = parkCoupleEpisode(orders[i], B, faithA[i]); // A's OWN move string under B's reduction if (m.shared > 0) { acc.mimic.n++; if (m.coupled) acc.mimic.ok++; } } return acc; } // _parkClone(P): a lightweight fork of the park runtime for search (parkCeiling) — the // immutable seed geometry (wall / park terrain sets / distDeep) is SHARED, every mutable // field (positions, facing, tokens, scores, cursor) is copied, and the trajectory logs are // dropped (search nodes never read them). function _parkClone(P) { const st = P.st; const st2 = { ...st, pos: { 0: { ...st.pos[0] }, 1: { ...st.pos[1] } }, facing: { ...st.facing }, carry: { ...st.carry }, score: { ...st.score }, penalty: { ...st.penalty }, tokens: st.tokens.map(t => ({ ...t })), fx: [] }; // PHASE (spec §3): the per-seat public clock is MUTABLE (advanced each own-turn), so a // search fork must own an independent copy — else sibling branches corrupt each other's // segment (the parkCeiling BFS advances the clock down every branch). Deep-copy per seat; // absent on non-phase boards -> untouched (byte-safe). if (st.clock) { st2.clock = {}; for (const s in st.clock) st2.clock[s] = { ...st.clock[s] }; } // PUSH (P12): the box is mutable cargo — a search fork owns an independent copy (the joint // fields cache st._pushFields is immutable geometry, shared by the spread above). if (st.box) st2.box = { ...st.box }; // PARK-DYN (Task 0/1): dyn is the MUTABLE runtime terrain EVERY field mechanic hangs its state // off, so a search fork must own it — else sibling branches spend each other's terrain and the C* // plan is read off a board no single branch ever stood on. // STRUCTURAL, not field-by-field (review C3): the first cut enumerated gone/flood/ents/gateOpen by // hand and was ALREADY wrong — it missed `downed`, which _parkDynInit itself ships, and it cloned // `ents` only one level deep, so an entity's own array (a duckling's trail) stayed shared. Every // such omission is a silent cross-branch leak that would surface as an unreproducible C* plan. The // body cannot know what a mechanic keeps in dyn, so it must not try: _parkDeepClone copies // whatever is there. That is also what lets the seam gate forbid the body from naming ANY dyn // field but `.beat`. Absent on every legacy board -> untouched. if (st.park.dyn) st2.park = { ...st.park, dyn: _parkDeepClone(st.park.dyn) }; // _fields RIDES ALONG (2026-08-01). It used to be dropped here, which meant every fork re-earned // three full-grid BFS before it had taken a single step — and a node with five legal moves forks // five times, so one board state paid for the same metric six times over (measured: 1.2 rebuilds // per expansion = 6 per node). Carrying it is sound because the cache is KEYED (dest, rival, // beat) and the fork has not moved yet, so the key it inherits is the key it would recompute; // the moment the fork steps, the beat/dest/rival move and the key misses on its own. The arrays // are written once and only read afterwards, so sharing them between parent and fork is safe. // This is the same object-level cache the engine has always trusted within one runtime — the // fork simply stops throwing it away. // posed 는 **복사한다**. 스프레드가 Set 을 참조로 넘기면 형제 가지들이 서로의 증거를 보게 되고, // 그러면 완주 판정이 이 가지가 걷지 않은 걸음에 기대게 된다 — 탐색이 조용히 거짓말을 한다. return { ...P, st: st2, prev: P.prev && { ...P.prev }, moves: [], path: [], awards: [], posed: new Set(P.posed || []), _fields: P._fields || null }; } // parkCeiling(st, persona): the compliant-optimal chain-completion plan C* (spec §A.4) — // BFS over the persona's OWN parkLexFilter move graph, with parkStep applied to forked // runtimes so the search dynamics are the live dynamics verbatim. Returns { turns, value }: // the minimal compliant completion length and the chain value completion clears. // Pursuit (the report) = (C*.turns - remaining(end)) / C*.turns: realized progress measured // in compliant-plan units. PROVEN <= 1 with no clamp — remaining is a BFS distance, >= 0 by // construction, for ANY play (violating shortcuts are CREDITED the compliant length they // save, never more), and == 1 exactly when the chain was completed. PURSUIT-CEILING gate. function parkCeiling(st, persona) { return { turns: _parkCeilingFrom(parkStart(st), persona), value: st.park.chain.reduce((s, i) => s + st.tokens[i].v, 0) }; } // _parkCeilingFrom(P, persona): minimal compliant turns from a runtime state to chain // completion. The plan space is CAP-FREE (the cap is the episode budget, not a route fact) // and BODY-FRESH (hearts reset — hearts are physics; C* is a pure route metric), searched // lean (P._lean skips the pairwise-award scan parkStep otherwise runs). Compliant branches // that die are pruned — a compliant plan must survive. Returns null only at the search // bound, which the PURSUIT-CEILING gate proves is never reached on generated boards. function _parkCeilingFrom(P, persona) { // PUSH (P12): done = box on pad (the empty push chain would read done vacuously). if (P.st.park.push ? _parkPushDone(P.st) : P.dest >= (P.st.park.needTypes || P.st.park.chain.length)) return 0; const ordering = parkOrderingFor(persona); const root = _parkClone(P); root.st.park = { ...P.st.park, cap: Infinity }; root.hearts = root.heartsMax; root.over = false; root.reason = null; root._lean = true; const sig = (Q) => { const s = Q.st; let alive = 0; for (let i = 0; i < s.tokens.length; i++) if (s.tokens[i].alive) alive |= 1 << i; return [s.pos[0].x, s.pos[0].y, s.pos[1].x, s.pos[1].y, Q.dest, Q.contract, Q.mode, Math.min(Q.wait, 3), Q.sharedClaimStage || '-', Q.prev ? _parkKey(s, Q.prev) : -1, alive, Q.turns & 1, Q.hearts, // PHASE (spec §3): the public clock segment is part of the plan state — the OWN C* // is an OPTIMAL-TIMING ceiling (waiting for green is a distinct node from crossing // in red). segN=2 is already folded into turns&1; segN=3 needs the explicit seg. s.clock ? clockSegOf(s, 0) : -1, // PUSH (P12): the box seat is plan state — the joint-BFS C* searches (box x pusher). // Constant -1 on non-push boards, so their dedup (and turns) is byte-identical. s.box ? _parkKey(s, s.box) : -1].join(','); }; const seen = new Set([sig(root)]); let queue = [root]; for (let depth = 1; depth <= 400 && queue.length; depth++) { const next = []; for (const node of queue) { for (const mv of _parkLexSet(_parkReads(node), ordering)) { const child = _parkClone(node); parkStep(child, mv); if (child.reason === 'complete') return depth; if (child.over) continue; const cs = sig(child); if (seen.has(cs)) continue; if (seen.size >= 250000) return null; seen.add(cs); next.push(child); } } queue = next; } return null; } // _parkAdmissible(seed, k, game): the generate-then-filter gate (spec §7 gates 1-2 checked // over the FULL persona set — a constant, so the accepted board stays a pure function of // the seed): every persona's oracle-faithful playout survives (deep entries <= hearts-1), // runs >= 30 turns (the demo must also COMPLETE the chain), covers the conflict quota // (demo: G x C >= 3, G x K >= 2, C x K >= 2), and blind recovery returns exactly the // demonstrated order. function _parkAdmissible(seed, k, game) { const need = game ? { GC: 1, GN: 1, CN: 1 } : { GC: 3, GN: 2, CN: 2 }; const playouts = []; for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkBuild(seed, k, game), persona); if (P.reason === 'death' || P.hearts < 1 || P.deepEntries > 2) return false; if (P.turns < 30) return false; if (P.reason !== 'complete') return false; // P8.5 §1.4: game boards too (viable with the cap-70 raise; at cap 40, 8/12 seeds had no faithful-completing layout) const got = { GC: 0, GN: 0, CN: 0 }; for (const w of P.awards) got[w.pair]++; for (const p of Object.keys(need)) if (got[p] < need[p]) return false; playouts.push(P); } // SIGNATURE (spec 2026-07-04 §A): filter-level only — the capstone geometry/generator // are untouched, the gate only moves which candidate k survives the sweep. if (!_parkSignature(() => _parkBuild(seed, k, game), playouts)) return false; for (let i = 0; i < PARK_PERSONAS.length; i++) { const rec = parkRecoverOrder(_parkBuild(seed, k, game), playouts[i].moves); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) return false; // P8.5 repair-1: GAME boards also clear the TASK-DISCOVERY capstone floor (0.8 on the // first-5-distinguishing-scenes posterior) BY CONSTRUCTION — the §1.4 completion // tightening + cap-70 raise shifted accepted k, and one shifted draw read 0.49 on a // gem-seat lottery (same defect class the task generators already filter via // need.early). Filter-level only; the gate itself is untouched. if (game && _parkEarlyRead(() => _parkBuild(seed, k, game), playouts[i].moves, PARK_PERSONAS[i].join('>')) < 0.8) return false; } return true; } // makeParkBoard(seed, opts): the public park generator — a PURE function of the seed // (opts.game selects the shorter 30-40-turn game filter; no rule/persona parameter // exists). Sweeps candidate layouts, caches the first admissible index per (seed, phase), // and returns a FRESH board each call (replay-safe). A seed with NO admissible layout in // the sweep falls back to layout 0 LOUDLY (spec P2 §E): counted once per (seed, phase) and // exposed via parkGenFallbacks() — the report surfaces it, gates prove it stays 0. const _PARK_CACHE = {}; let _PARK_GEN_FALLBACKS = 0; function makeParkBoard(seed, opts) { opts = opts || {}; const game = !!opts.game; const ck = (seed >>> 0) + (game ? ':g' : ':d'); if (_PARK_CACHE[ck] == null) { let found = -1; for (let k = 0; k < 96; k++) if (_parkAdmissible(seed, k, game)) { found = k; break; } if (found < 0) { found = 0; _PARK_GEN_FALLBACKS++; } _PARK_CACHE[ck] = found; } return _parkBuild(seed, _PARK_CACHE[ck], game); } function parkGenFallbacks() { return _PARK_GEN_FALLBACKS; } /* -------- PARK TASK BATTERY (spec 2026-07-03 P2 §B): five minigame generators -------- */ /* Small boards over the SAME two-tone grammar + parkStep physics, each isolating one measurement facet: m1 shortcut-run (GxC, escalating shortcut savings), m2 contested- harvest (GxK), m3 narrow-pass (CxK — the D chokepoint, posed EVERY episode by construction), m4 triple-fork (all three pulls at one cell), m5 observer (Discovery direct — continue AS a stranger). Cell = { personaStream, goalVariant, hazard:{kind, damage,d}, seed }: the board is a PURE function of the PUBLIC fields (goalVariant / hazard / seed) — personaStream is NEVER read here (C1; the TASK-C1 gate proves byte- identity across persona streams). Same generate-then-filter discipline as the park. */ const PARK_TASK_KINDS = ['m1', 'm2', 'm3', 'm4', 'm5']; // THE OBSERVER'S NAME IS M5, ITS GENERATOR CONSTANT IS 7, AND THE MISMATCH IS DELIBERATE. // The name follows the paper (§3.2 calls the observer M5); the number is a LAYOUT INPUT — it is // multiplied into the board rng below (the `* 7919` term) and into the phase-segment draw. Change // the 7 and every observer board in the battery is re-drawn: the curated skel values that keep // same-family tiles structurally distinct would be pointing at layouts nobody measured. So the // rename of 2026-07-29 moved the KEY only, and a canonical board hash over kinds x seeds 1..24 // was equal on both sides of it. If a future session wants the number aligned too, that is a // re-measurement task, not a rename. const _PARK_KIND_ID = { m1: 1, m2: 2, m3: 3, m4: 4, m5: 7 }; // _parkTaskFrame(n, isWalk): the shared two-tone frame — border tree wall, walkway from // the kind's predicate, verge = the field's walkway-adjacent band, deep = the rest, plus // the multi-source distDeep BFS (same partition rule as _parkBuild, small n). function _parkTaskFrame(n, isWalk) { const wall = new Set(), walkway = new Set(), verge = new Set(), deep = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = y * n + x; if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } if (isWalk(x, y)) { walkway.add(kk); continue; } const nearWalk = isWalk(x - 1, y) || isWalk(x + 1, y) || isWalk(x, y - 1) || isWalk(x, y + 1); (nearWalk ? verge : deep).add(kk); } const distDeep = new Array(n * n).fill(Infinity); const q = []; for (const kk of deep) { distDeep[kk] = 0; q.push(kk); } for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[kk] + 1) { distDeep[nk] = distDeep[kk] + 1; q.push(nk); } } } return { wall, walkway, verge, deep, distDeep }; } // _PARK_KIND_ARCHS (spec 2026-07-04 §B.1): the topology archetype families each kind's // focal measurement supports — first entry = the kind's DEFAULT (the pre-archetype // geometry, byte-stable for arch-less cells). Archetype is a PUBLIC cell field // (cell.arch) — a pure function of the public cell, never personaStream (TASK-C1): // park — perimeter ring (+ a cross row where the kind lanes one) // serpent — one S-lane (three rows joined by E/W risers) through a single big field // islands — walkway strips joined by 1-cell bridges over the surrounding field // pools — walkway yard (ring + cross row + cross col) around 4 separate field pools // m1's escalation ladder and m4/m5's caution-band lane pin banded archetypes // (park/serpent); m3's chokepoint pins the 1-wide passage (islands/serpent). const _PARK_KIND_ARCHS = { m1: ['park', 'serpent'], m2: ['park', 'pools'], m3: ['islands'], // serpent EXCLUDED (2026-07-04): the S-riser choke cannot host // BOTH the same-column climb-uniqueness AND a care>goal>safety // that the blind posterior can separate (MAP collapses to // goal>care>safety on every k — see docs spec §A residual note). m4: ['park', 'serpent'], m5: ['park'], // serpent EXCLUDED (2026-07-04): the observer margin // (_parkObsDiverges) needs the ring's long W-bound N-lane; the // serpent mid-row cannot seat prefix+continuation divergence // against all 5 rivals (m5/serpent admits 0 of 96 candidates). }; // _PARK_KIND_ARCHS_X (P11 ①, 2026-07-10): the EXTENSION manifest — the FULL archetype pool, // per kind PREFIX-EQUAL to _PARK_KIND_ARCHS (asserted by the PARK-ARCHS-X-PREFIX gate). New // archetypes are APPEND-ONLY and live ONLY here: the canonical consumers (spread / transfer // pair draw / app thumbnail depth) keep drawing from the UNCHANGED base manifest — their // `pick(archs)` buckets by archs.length, so growing the BASE list would re-bucket the // canonical PARK-TRANSFER-RECOVERABLE draws (measured trap) — while _parkArchOf and // _parkTaskBuild honor any declared extension arch on an EXPLICIT public cell.arch (the // crossing play-cell filter is the intended consumer). archIdx is computed from THIS list, // so every existing arch keeps its index (existing cells build byte-identically). // court — outer ring + a central 2x2 walkway gazebo island inside a one-thick deep moat // annulus, joined to the ring by a single connector row (m1 + m4) // comb — one spine row + three dead-end tooth columns over two 1-wide deep shafts; the // walkway is a TREE (m4 only: the goal-top CN yield is never posed for m1 — the // only meet resolves as a gem preempt, measured 0/72 across three grammar // repairs; same mechanics-correct exclusion class as islands=m3 / pools=m2). const _PARK_KIND_ARCHS_X = { m1: ['park', 'serpent', 'court'], m2: ['park', 'pools'], m3: ['islands'], m4: ['park', 'serpent', 'court', 'comb'], m5: ['park'], }; // _parkYardSig(st): a board's GEOMETRY fingerprint — the terrain grid, EVERY mechanic bag // present on the board, AND the entity layer that sits beside them, hashed. This is the // frozen-bytes witness the yard registry refactor is measured against: naming and dispatch may // move, geometry may not. Sets and Maps are serialized with their element order, and object // keys are SORTED, so a harmless re-ordering of a builder's property writes cannot read as a // geometry change. // Terrain letters: '#' wall, 'D' deep, 'v' verge, '.' walkway, ' ' none. // ENTITY TERMS (widened 2026-07-30, review round 1): the gems/tokens/seats that actually move a // body around a board are SIBLINGS of park.fieldMech's own bag on park/st, never members of it // — and for every walk/push/slide yard park.fieldMech is undefined, so a bag-only term covered // 0 of those 30 rows. Each entity sibling below is appended as its own labeled term // (`;label=`) so an ABSENT field (a lineage that never has this member) contributes // NOTHING to the string, while a PRESENT-BUT-EMPTY one still tags the string with its label — // absent and empty cannot collide into the same bytes by accident. // FOREIGN-MECHANIC BAGS (widened 2026-07-30, review round 2): a hybrid yard borrows a sibling // mechanic's fixture shape wholesale — siege carries a park.statue bag (the y46/y29 doll + // clock, including dollKey/finishKey/laneKeys, cell-index-encoded placement data unreachable // through any entity term above), bomb2 carries park.bomb, alley carries park.bull — stored // under that SIBLING's own key, not park.fieldMech's. Hashing only p[p.fieldMech] therefore // missed every foreign bag. The fix hashes p[id] for every id in PARK_FIELD_MECHS, sorted, so // a bag under any mechanic's key is covered regardless of which mechanic owns the board, and a // bag that moved from one key to another (or vanished) reads as a change. function _parkYardSig(st) { const n = st.N, p = st.park; let s = 'N' + n + ';'; for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const k = y * n + x; s += (st.wall && st.wall.has(k)) ? '#' : (p.deep && p.deep.has(k)) ? 'D' : (p.verge && p.verge.has(k)) ? 'v' : (p.walkway && p.walkway.has(k)) ? '.' : ' '; } const norm = (v) => { if (v instanceof Set) return ['Set'].concat([...v]); if (v instanceof Map) return ['Map'].concat([...v]); if (Array.isArray(v)) return v.map(norm); if (v && typeof v === 'object') return Object.keys(v).sort().reduce((a, k) => (a[k] = norm(v[k]), a), {}); return v; }; for (const id of Object.keys(PARK_FIELD_MECHS).sort()) if (p[id] !== undefined) s += ';bag:' + id + '=' + JSON.stringify(norm(p[id])); const term = (label, v) => { if (v !== undefined) s += ';' + label + '=' + JSON.stringify(norm(v)); }; term('clusters', p.clusters); term('tokens', st.tokens); term('spawn', p.spawn); term('companionSpawn', p.companionSpawn); term('contracts', p.contracts); term('retire', p.retire); term('box', st.box); term('pad', p.pad); return _parkSigHash(s); } // A tiny FNV-1a-style rolling hash: node's crypto is not reachable from this pure module, // and the gate only needs collision resistance across a few hundred boards. function _parkSigHash(s) { let h1 = 0x811c9dc5, h2 = 0x01000193; for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); h1 = ((h1 ^ c) * 0x01000193) >>> 0; h2 = ((h2 + c) * 0x85ebca6b + h1) >>> 0; } return ('00000000' + h1.toString(16)).slice(-8) + ('00000000' + h2.toString(16)).slice(-8); } // PARK_YARDS — the ONE table of every map in the park (2026-07-30). Four lineages build park // boards, and until now only one of them had names: the walk archetype pool. The PUSH frame and // every field mechanic's own yard were nameless, so no survey and no doc could enumerate them. // This table gives each yard a name and a lineage WITHOUT touching how any of them is built — // geometry is frozen by the PARK-YARD-FROZEN gate. // walk — the archetype pool; the ONLY lineage that declares which kinds it may live under // (_PARK_KIND_ARCHS_X, reverse-indexed). Seed picks which one a leg gets. // slide — not an entry of its own: PARK_SLIDE_ARCH pins every slide leg onto the court yard, // so slide is a lineage MARK on that entry (PARK_YARD_SLIDE_ON names it). // push — the verb module's own frame, one yard shared by ten legs. // field — one yard per registered mechanic, derived from PARK_FIELD_MECHS so that building a // new mechanic adds ZERO lines here (the same promise the crossing builder keeps). // kindsDeclared is null outside walk ON PURPOSE: a field yard does not constrain kind — the SLOT // picks kind, and nothing in the bomb module asks for m1. What kinds a yard is actually seen // under is a MEASURED fact, and tools/yard-sweep.mjs measures it. // NOTE: PARK_SLIDE_ARCH itself is declared further down this file (its own module, well below // this seam), so it cannot be READ eagerly here — a top-level `const X = PARK_SLIDE_ARCH` at // this line would run before that declaration and hit the temporal dead zone. parkYardOf below // reads PARK_SLIDE_ARCH directly instead, which is safe: that function body only runs on a call, // long after the whole module has finished loading and every const in this scope is settled. let _PARK_YARDS_CACHE = null; // FINDING I-1 (final whole-branch review, 2026-07-30): every entry's build USED TO call its own // generator directly (makeParkTask / makeParkPushTask / parkFieldBuild), a SECOND build path that // quietly diverged from parkBoardBuild — the declared ONE build entry point below. Measured on // m1/court/seed 3/safetyMech:'phase': the old walk closure (makeParkTask's own k-sweep candidate) // hashed to f91f00bfcbd2e400, parkBoardBuild's non-static short-circuit (_parkTaskBuild(kind, // cell, 0)) hashed to 8264e423f59ba000 — a real board divergence, invisible to every other gate // because _parkYardSig does not hash park.cell. The old push closure also never stamped // st.park.cell the way parkBoardBuild's push branch does. Every closure now delegates to // parkBoardBuild directly: it is the SAME function for all three lineages, kept as a per-entry // closure only so Y[id].build(kind, cell) keeps its two-argument call shape. Non-vacuity and the // exact pre-fix failure text are recorded on the PARK-YARD-REGISTRY gate (engine.test.js) that // exercises this table. function _parkYardsBuild() { const out = {}; const kindsOf = (arch) => Object.keys(_PARK_KIND_ARCHS_X) .filter(k => _PARK_KIND_ARCHS_X[k].indexOf(arch) >= 0); for (const arch of [...new Set(Object.values(_PARK_KIND_ARCHS_X).flat())]) out[arch] = { id: arch, lineage: 'walk', kindsDeclared: kindsOf(arch), build: (kind, cell) => parkBoardBuild(kind, cell) }; out.push = { id: 'push', lineage: 'push', kindsDeclared: null, build: (kind, cell) => parkBoardBuild(kind, cell) }; for (const id of Object.keys(PARK_FIELD_MECHS)) out[id] = { id, lineage: 'field', kindsDeclared: null, build: (kind, cell) => parkBoardBuild(kind, cell) }; return out; } // Lazy: PARK_FIELD_MECHS is populated by registrations that run BELOW this point in the file, so // a table built at module-eval time would see an empty registry. function _parkYards() { return (_PARK_YARDS_CACHE ||= _parkYardsBuild()); } // parkYardOf(kind, cell): names the yard of ANY park cell. Total by construction — the final // branch is the walk pool, and _parkArchOf already falls an out-of-pool arch back to the kind's // default, so there is no cell this cannot answer for. function parkYardOf(kind, cell) { const m = (cell && cell.mech) || {}; // DELIBERATE PASS-THROUGH, do not "harden" this into a validate-and-fall-back like the walk // branch below: an unregistered fieldMech id comes back AS-IS, unchecked against PARK_YARDS. // That is what lets a typo'd id surface as itself instead of silently resolving to a real // walk yard — the sweep tool's canary depends on seeing the wrong name and throwing on it // (mirrors parkFieldBuild's own "THROWS on an unregistered id: fail at the cause", engine.js // near _parkAddOf above). Falling back here would defeat that canary for good. if (m.fieldMech) return m.fieldMech; if (m.moveMech === 'push') return 'push'; if (m.moveMech === 'slide') return PARK_SLIDE_ARCH; return _parkArchOf(kind, cell); } function _parkArchOf(kind, cell) { const a = cell && cell.arch; return _PARK_KIND_ARCHS_X[kind].indexOf(a) >= 0 ? a : _PARK_KIND_ARCHS[kind][0]; } // parkBoardBuild(kind, cell): the ONE build entry point for every park board. This is the // four-branch convention that used to live in the campaign layer (as _parkCrossBoard), moved // down beside the registry that names its outputs — same branches, same order, same conditions // (PARK-YARD-BUILD-SAME). It names no mechanic: a field cell dispatches through the // PARK_FIELD_MECHS registry, so a new mechanic still adds zero lines here. // A non-static safety mechanism builds CANDIDATE 0 directly via _parkTaskBuild, and only a // static cell goes through makeParkTask's own k-sweep. That split is a measured convention, // not a style choice: makeParkTask's k-sweep admissibility predicate has only ever been // measured for the static and relational demo forms. Handing it a non-static cell would let it // pick some k > 0 candidate whose admissibility was never checked, and that candidate would // silently diverge from every gate built against candidate 0. // NOTE ON THE MOVE: the replaced campaign.js body read `cell.mech && cell.mech.moveMech`, which // THROWS when cell itself is undefined. This guards with `(cell && cell.mech) || {}` instead, // which swallows an undefined cell into the walk branch rather than throwing. Every caller this // codebase has ever traced passes a real cell object, so the two forms are equivalent for all // realistic input (PARK-YARD-BUILD-SAME measures exactly that set) — but they are not the same // function on a pathological `cell === undefined`, so this is not a byte-identical copy of the // old body, only a behaviorally-identical one over the domain the callers actually use. function parkBoardBuild(kind, cell) { const m = (cell && cell.mech) || {}; if (m.fieldMech) return parkFieldBuild(cell); if (m.moveMech === 'push') { const st = makeParkPushTask({ seed: cell.seed }); st.park.cell = cell; // render-only PUBLIC summary; the engine never reads it back return st; } if (m.moveMech === 'slide') return _parkSlideBuild(cell); return m.safetyMech !== 'static' ? _parkTaskBuild(kind, cell, 0) : makeParkTask(kind, cell); } // _PARK_KIND_GOALS (spec 2026-07-04 §C, residual recalibration): the goal grammars each kind // admits under its per-kind identifiability/observer machinery, at the reference hazard // (meadow); hazard-specific narrowings (e.g. collect needs a heart to spare, so it drops on // lava for m1/m2) are the SPREAD's responsibility, guarded by parkGenFallbacks(). Exclusions // are mechanics-correct, NOT gate-weakening (the goal grammar changes the destination logic // in a way the kind's focal read cannot absorb): // collect (dest = nearest MISSING TYPE, a dynamic type-quota not a fixed sequence) breaks // the fixed-order identifiability of m2's pool crossing and m3's choke, and m5's // observer-divergence tooth — excluded for m2/m3/m5. // deliver (adds a return-leg drop-off) overruns m5's tight observer cap (3 personas hit the // cap) — excluded for m5. // The declared set is the INTERSECTION over the kind's archetypes (so every declared // kind x arch x goal is admissible at meadow — proven by the PARK-GENGRID gate). const _PARK_KIND_GOALS = { m1: ['harvest', 'deliver', 'reach', 'collect'], m2: ['harvest', 'deliver', 'reach'], m3: ['harvest', 'deliver', 'reach'], m4: ['harvest', 'deliver', 'reach', 'collect'], m5: ['harvest', 'reach'], }; // the goal-grammar seed mix (harvest/deliver keep the pre-variant constants 11/43 so // arch-less legacy cells stay byte-stable modulo the admissibility filter). const _PARK_GOAL_MIX = { harvest: 11, deliver: 43, reach: 57, collect: 71 }; // _PARK_KIND_FORMS (design 2026-07-06 §A/§B): the safety RULE FORMS _parkTaskBuild will build // geometry for — first entry ('static') = the legacy deep-field default (byte-stable for // form-less cells). Form is a PUBLIC cell field, a pure function of the public cell (never // personaStream). `_forms.build` = the forms whose BOARD is instantiated; `_forms.measured` = // the forms that additionally pass _parkTaskAdmissible (full 6-way blind ORDER recovery). // // ⚠️ MEASUREMENT STATUS (verified 2026-07-06; ADOPTED resolution repair-round-1): the relational // forms build and PLAY correctly (goal-top violates the rival taboo, safety-top detours, every // persona completes alive, and the compliant-optimal move tracks the LIVE rival — design gate 6 // holds, gated by the PARK-FORM-DEMO test) BUT they are NOT order-MEASURABLE: because the // relational safety taboo keys on the SAME companion entity as the care axis N, no existing park // geometry poses a safety-vs-care (C-vs-N) CONFLICT scene, so the C/N pair receives ZERO pairwise // awards and the blind posterior collapses C↔N (ceiling 3-4 of 6 orders recover; full 6/6 never // reached on m1/m2/m3/m4 x seeds 1-10 x d in {1,2,3}). Making a relational form MEASURED requires a // purpose-built board that poses a C-vs-N conflict (a scene where clearing the companion's route / // ceding its gem forces stepping TOWARD the companion = a safety violation, so C>N and N>C diverge) // — new geometry, not wiring; the hardest part of the park's generate-and-filter. // // RESOLUTION (design 2026-07-06 §D offered branch, adopted this round): the safety-form gate is // re-scoped — the ORDER-MEASURED spread stays static-only (`measured` = static for every kind, so // the spread / transfer draw only static and TRANSFER-ADMISSIBLE / PARK gates keep their 0-fallback // + full-recovery guarantee), and each relational form is accepted as a DEMO-ONLY "plays- // differently" form: build-reachable, form-specific in play, and legible via the app red-taboo // overlay. The measured relational axis (6/6 recovery via the C-vs-N conflict geometry) is deferred // to P9. `build` keeps the relational forms reachable so the demo path (and the future geometry // work) can iterate against _parkTaskBuild directly. const _PARK_KIND_FORMS = { m1: ['static', 'adjacent'], m2: ['static'], m3: ['static'], m4: ['static', 'adjacent'], m5: ['static'], }; // P9 1b WITHDRAWN (repair round 1, 2026-07-08): the 'warden' 2nd-entity form was NOT shipped. // A distinct static warden (seat 2) that safety C keyed via an entity-scoped anchor DID build and // play differently, but its purpose — promoting non-preemption / care N to a MEASURED 4th axis by // letting C<->N diverge — was NOT met: an independent re-derivation over 400 plays-differently // warden boards recovered the full 6/6 order INCLUDING the C-vs-N pair on 0/400 (deterministic // single-warden placement yields 0 CN awards; a fresh placement sweep confirmed CN awards are // reachable off the midline but full 6/6 recovery tops out at 4/6, good=0/12). Per the contract // §4/§5 (docs/.../2026-07-07-p9-diversity-feasibility-contract) full 6/6 needs a PURPOSE-BUILT // C-vs-N-conflict generator co-designing warden+companion+goal chain — the deferred P9 frontier // warranting an LLM pilot, out of this deterministic Phase-1 round. Per the repair rule ("do NOT // ship a non-recovering N form"), N promotion is narrowed to ZERO kinds and the warden form is not // buildable/shippable. The relational DEMO-ONLY forms that DO play honestly (adjacent on m1/m4, // safety-C only, no N claim) are unchanged. // the order-measurable subset (see status note): static-only BY DECISION this round (the // relational measured axis is deferred to P9 pending C-vs-N-conflict geometry). The spread / // transfer draw safety forms from THIS map (so they stay order-recoverable); _PARK_KIND_FORMS // governs which forms _parkTaskBuild will honor on an explicit cell.safetyForm — the DEMO-ONLY // relational path (PARK-FORM-DEMO gate) and the future geometry work iterate off it. const _PARK_KIND_FORMS_MEASURED = { m1: ['static'], m2: ['static'], m3: ['static'], m4: ['static'], m5: ['static'], }; function _parkFormOfKind(kind, cell) { const f = _parkFormOf(cell); // PHASE (P10 §3): honored only on the GxC kinds (_PARK_PHASE_KINDS); a phase form on any // other kind coerces to static. Phase is a SEPARATE manifest from _PARK_KIND_FORMS (the // relational demo forms) so the PARK-FORM-DEMO relational scan is untouched. if (f === PARK_PHASE_FORM) return _PARK_PHASE_KINDS.indexOf(kind) >= 0 ? PARK_PHASE_FORM : 'static'; return (_PARK_KIND_FORMS[kind] || ['static']).indexOf(f) >= 0 ? f : 'static'; } // _parkTaskBuild(kind, cell, k): candidate task layout k — pure public geometry, runtime- // compatible with parkStart/parkStep verbatim (same state shape as _parkBuild). function _parkTaskBuild(kind, cell, k) { const seed = cell.seed | 0, hz = cell.hazard || {}; const damage = hz.damage == null ? 1 : hz.damage, cautionD = hz.d || 2; // damage 0 = ice (no body cost) const form = _parkFormOfKind(kind, cell); // safety RULE FORM (design 2026-07-06); 'static' = byte-stable default const isRel = form === 'adjacent' || form === 'nearest_token'; const isPhase = form === PARK_PHASE_FORM; // PHASE (P10 spec §3): a PUBLIC per-seat clock ring. segN is a pure function of the PUBLIC // cell (seed+kind, never personaStream — C1), 2 or 3; RED = the last segment. Seeded on // EVERY seat IDENTICALLY (rule-invariant existence + lockstep advance, so the pip never // leaks which seat is phase-conditional). Only on a phase board -> non-phase boards never // get st.clock (byte-identical). Drawn OFF the layout rng so it perturbs no other draw. const phaseSegN = isPhase ? 2 + (((seed * 2749 + _PARK_KIND_ID[kind] * 7) >>> 0) % 2) : 0; const gv = _parkGoalOf(cell); // goal MECHANISM (P10 mech.goalMech overrides goalVariant) // SLIDE (P12b): mech.moveMech is the third PUBLIC mechanism field (movement VERB — // 'walk'|'slide', absent -> walk, byte-stable). It stamps park.slide (the runtime + render // dispatch flag) and NOTHING else: no rng read, no geometry change — a slide board is the // IDENTICAL walk board bytes + the flag (the SLIDE-C1 gate proves this), so slide is a pure // runtime overlay in the phase-module mold. const slideVerb = !!(cell.mech && cell.mech.moveMech === 'slide'); const deliver = gv === 'deliver'; const arch = _parkArchOf(kind, cell); const archIdx = _PARK_KIND_ARCHS_X[kind].indexOf(arch); // 0 = the kind's default (X prefix-equal: existing indices unchanged) // P3b-v5 SKELETON SELECTOR: cell.skel is a PUBLIC cell field (small non-negative integer, // interpreted mod the (kind,arch) family size). When present it deterministically SELECTS // the structural skeleton among the P3b-v2/v3 entropy axes (the per-branch decode tables // below); when null the skeleton is drawn from the candidate rng exactly as before (byte- // stable back-compat — the null path consumes the rng in the identical order). Seat-level // draws (gem columns, riser seats, gem values) stay on the rng in BOTH paths. The board // remains a pure function of the PUBLIC cell (C1) — skel is public, personaStream is // never read. The spread uses skel to make same-(kind,arch) tiles structurally distinct // BY CONSTRUCTION (two tiles of one family had measurably drawn byte-identical skeletons // from the small per-archetype draw space — run-seed-1 m2:3/m2:6 + m3:8/m3:10). const skel = cell.skel == null ? null : Math.abs(cell.skel | 0); const r = rng(((seed * 977 + k * 131 + _PARK_KIND_ID[kind] * 7919 + (_PARK_GOAL_MIX[gv] || 11) + damage * 257 + cautionD * 631 + archIdx * 389) >>> 0) || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const n = 12, ring = (x, y) => x === 1 || x === n - 2 || y === 1 || y === n - 2; // P3b-v2 LAYOUT ENTROPY (spec 2026-07-04 §B addendum): each archetype draws its structural // SKELETON (cross-row/col seats, serpent lane rows + riser columns + S-vs-Z mirror, bridge // column + E/W mirror, pool boulevards) from the SAME candidate rng — a pure function of // the public cell + k (C1), filtered by the admissibility sweep like every other draw — so // two boards of one archetype read as different LEVELS, not recolors. Ranges are narrowed // per kind where a wider axis would starve the sweep (never by weakening a gate). // serpLane(yT, yM, xT, xB): lane rows yT/yM/n-2 + the xT riser joining the top pair and // the xB riser joining the bottom pair (mirroring swaps riser sides = the S-vs-Z flip). const serpLane = (yT, yM, xT, xB) => (x, y) => y === yT || y === yM || y === n - 2 || (x === xB && y > yM && y < n - 2) || (x === xT && y > yT && y < yM); // P3b-v3 ALLEY (R1 reskin fix): a short DEAD-END walkway spur off a lane — findings 1-4 // showed two same-archetype boards reading as one level MIRRORED (the seat/mirror axes // never change the skeleton CLASS, and reflections are free for a human eye). An alley is // a reflection-BREAKING class axis: presence + side + seat must all align under one common // reflection for two boards to read as the same plan. Dead ends add no through-route (the // route/savings machinery is disturbed only via the verge halo), and every draw is filtered // by the same admissibility sweep as the gem lottery — never a gate change. const alley = (xs, y0, y1) => (x, y) => x === xs && y >= Math.min(y0, y1) && y <= Math.max(y0, y1); let F, spawn, clusters, contracts, retire, obsPrefix = null; let MX = (x) => x, sk = {}; // per-branch mirror + skeleton params (deliver pads read them) if (kind === 'm1' && arch === 'serpent') { // shortcut-run on the S-lane: the contest leg rides the bottom row, then the ladder // crosses band 1 (bottom->mid) and band 2 (mid->top) — the candidate sweep keeps only // strictly-growing pairs. Damage 2: one crossing, chain 2 rides the mid row W. // ENTROPY (P3b-v3, R1 reskin finding 4): the LANE SPLIT is a drawn class axis — 12x12 // admits THREE 3-lane splits with both bands >= 3 thick: (yT,yM) in {(1,5),(1,6),(2,6)} // ((2,6) keeps the field strip above the top lane; yT=1 hugs the wall — no strip), and // an optional SECOND top riser (a 'theta' ladder, very visible) joins the top pair at a // mid column. Split + second-riser + the S-vs-Z mirror + riser seats must ALL align // under one reflection for two boards to read as the same level. Sweep-filtered as ever. // SKEL DECODE (m1/serpent, 12 variants): s = skel % 12 -> split sdraw = s % 3 // (0 = (2,6), 1 = (1,5), 2 = (1,6)), mirror flip = (s/3|0) % 2, second top riser // (theta ladder) present = (s/6|0) % 2. Riser SEATS (xw/xe/xT2 columns) stay rng. const s12 = skel == null ? null : skel % 12; const flip = s12 == null ? r() < 0.5 : ((s12 / 3 | 0) % 2) === 1; const xw = cs(1, 2), xe = cs(9, 10); const sdraw = s12 == null ? cs(0, 2) : s12 % 3; const yT = sdraw === 0 ? 2 : 1, yM = sdraw === 1 ? 5 : 6; const xT2 = (s12 == null ? r() < 0.5 : (s12 / 6 | 0) === 1) ? cs(4, 6) : 0; // optional second top riser const M = (x) => flip ? n - 1 - x : x; MX = M; sk.riserT = M(xw); sk.yT = yT; // (the damage-2 deliver pad rides the riser) const lane = serpLane(yT, yM, M(xw), M(xe)); const ris2 = (x, y) => xT2 > 0 && x === M(xT2) && y > yT && y < yM; F = _parkTaskFrame(n, (x, y) => lane(x, y) || ris2(x, y)); const g1x = cs(4, 5), dx0 = cs(7, 8), dx1 = cs(4, 5), dx2 = cs(7, 8); spawn = { x: M(2), y: n - 2 }; clusters = [ { x: M(dx0), y: n - 2, v: cs(2, 3) }, // chain 0 (bottom lane — the contest leg) { x: M(dx1), y: yM, v: cs(2, 3) }, // chain 1 (mid row — crossing 1) damage < 2 ? { x: M(dx2), y: yT, v: cs(2, 3) } // chain 2 (top row — crossing 2) : { x: M(2), y: yM, v: cs(2, 3) }, // ... mid-row-along under damage 2 { x: M(g1x), y: n - 2, v: cs(1, 2) }, // companion's lane gem (mid leg 0) ]; contracts = [{ gem: 3, station: { x: M(g1x + 2), y: n - 3 } }]; retire = { x: M(2), y: n - 3 }; // lane-head verge: the relocate opposes the lane head-on } else if (arch === 'court') { // COURT (P11 ①, courtyard/moat family — m1 + m4): outer ring + a central 2x2 walkway // GAZEBO island (5..6 x 5..6) inside a one-thick deep MOAT annulus (rows/cols 3 and 8 + // corner pockets), joined to the E ring by a SINGLE connector row — the only deep-free // route between ring and gazebo, so every ring<->gazebo leg poses a dive-vs-around-the- // connector fork. m1 ladder: leg 1 dives the S moat to the gazebo (detour = E ring + // connector), leg 2 dives the N moat to the far-W N ring (detour = connector + the whole // E/N ring: strictly larger savings). // SKEL DECODE (court, 8 variants): s = skel % 8 -> E/W mirror flip = s % 2, connector row // cy = 5 + (s/2|0) % 2, N-ring 1-cell spur seat = (s/4|0) % 2 ? 8 : 3 (reflection-breaking). const s8 = skel == null ? null : skel % 8; const flip = s8 == null ? r() < 0.5 : (s8 % 2) === 1; const cy = s8 == null ? cs(5, 6) : 5 + ((s8 / 2 | 0) % 2); const spurx = (s8 == null ? cs(0, 1) : (s8 / 4 | 0) % 2) ? 8 : 3; const M = (x) => flip ? n - 1 - x : x; MX = M; sk.cy = cy; const cxl = flip ? 2 : 7, cxh = flip ? 4 : 9; // the connector (mirrors with the board) F = _parkTaskFrame(n, (x, y) => ring(x, y) || (x >= 5 && x <= 6 && y >= 5 && y <= 6) // the gazebo island || (y === cy && x >= cxl && x <= cxh) // the single connector row || (x === M(spurx) && y === 2)); // 1-cell N-ring spur (class axis) spawn = { x: M(2), y: n - 2 }; if (kind === 'm1') { const g1x = cs(4, 5), gzx = cs(5, 6), dx2 = cs(3, 4); // gem at spawn-dist 2-3 (park pacing): the take/cede lands POST-calibration clusters = [ { x: M(g1x + 3), y: n - 2, v: cs(2, 3) }, // chain 0 (S ring — the contest leg) { x: M(gzx), y: 6, v: cs(2, 3) }, // chain 1 (gazebo S edge — the S-moat dive) damage < 2 ? { x: M(dx2), y: 1, v: cs(2, 3) } // chain 2 (far-W N ring — the N-moat dive) : { x: M(9), y: cy, v: cs(2, 3) }, // ... connector-along under damage 2 { x: M(g1x), y: n - 2, v: cs(1, 2) }, // companion's lane gem (mid leg 0) ]; contracts = [{ gem: 3, station: { x: M(g1x + 2), y: n - 3 } }]; } else { const dx0 = cs(4, 5), gzx = cs(5, 6); // m4 triple-fork on the S ring lane clusters = [ { x: M(dx0), y: n - 2, v: cs(2, 3) }, // chain 0 (S ring — the lane leg to the fork) { x: M(dx0 + 3), y: n - 2, v: cs(2, 3) }, // chain 1 (S ring, past the fork) { x: M(gzx), y: 6, v: cs(2, 3) }, // chain 2 (gazebo — the posed moat crossing) { x: M(dx0 + 1), y: n - 2, v: cs(1, 2) }, // contract gem AT the fork ]; contracts = [{ gem: 3, station: { x: M(dx0 + 3), y: n - 3 } }]; } retire = { x: M(2), y: n - 3 }; // W dooryard: the relocate opposes the lane } else if (arch === 'comb') { // COMB (P11 ①, spine + dead-end teeth family — m4 only, see _PARK_KIND_ARCHS_X): one // spine row y=1 + three tooth columns x in {2,6,10} descending to the tip row 8; deep = // the two 1-wide SHAFTS between teeth (cols 4/8, rows 3..) + the bottom strip (rows 9..10 // off the tips' verge). The walkway is a TREE: tooth-to-tooth travel detours up-the- // tooth/along-the-spine/down, so a dive across a shaft saves exactly 2*(min endpoint // depth - 1). // DESIGN LAWS (measured during the ① scout): (a) a chain destination never sits on a // dead-end tip whose next leg 180-reverses — the no-backtrack G head deadlocks; (b) on a // walkway tree consecutive dives must not span two disjoint deep runs (2 entries -> 3 // total = death at damage 1); (c) stations sit >= 2 approach-steps from their gem AND // approach head-on into the player's flow. // SKEL DECODE (comb, 8 variants): s = skel % 8 -> E/W mirror flip = s % 2, chain-2 tooth // row ym = 5 + (s/2|0) % 2, shaft spur seat = (s/4|0) % 2 ? 8 : 4 (a 2-cell dead-end spur // off the spine into a shaft top — reflection-breaking class axis; dives ride rows >= 5). const s8 = skel == null ? null : skel % 8; const flip = s8 == null ? r() < 0.5 : (s8 % 2) === 1; const ymk = s8 == null ? cs(5, 6) : 5 + ((s8 / 2 | 0) % 2); const spurx = (s8 == null ? cs(0, 1) : (s8 / 4 | 0) % 2) ? 8 : 4; const ty = 8; // tooth tip row const M = (x) => flip ? n - 1 - x : x; MX = M; const tooth = (x, y) => (x === M(2) || x === M(6) || x === M(10)) && y <= ty; F = _parkTaskFrame(n, (x, y) => y === 1 || tooth(x, y) || (x === M(spurx) && y >= 2 && y <= 3)); // dead-end shaft spur (class axis) // TOOTH-2-CENTERED GRAMMAR: the contest leg descends the MIDDLE tooth, where BOTH cede // squares (cols 5/7) sit inside the shaft caution halo — a head-on meet on tooth 2 poses // hold-vs-cede with C and N genuinely split (on the outer teeth the W-side cede square // is safe, so C and N agree and the CN pair never awards — measured). spawn = { x: M(6), y: 1 }; // spine head over tooth 2 (gem contest lands post-cal) { const dx0 = cs(4, 5), ym = ymk; // m4 triple-fork down tooth 2 clusters = [ { x: M(6), y: dx0, v: cs(2, 3) }, // chain 0 (tooth 2 — the lane leg to the fork) { x: M(6), y: dx0 + 2, v: cs(2, 3) }, // chain 1 (tooth 2, past the fork) { x: M(2), y: ym, v: cs(2, 3) }, // chain 2 (tooth 1 — the posed shaft crossing) { x: M(6), y: dx0 + 1, v: cs(1, 2) }, // contract gem AT the fork ]; contracts = [{ gem: 3, station: { x: M(7), y: dx0 + 3 } }]; // BELOW the fork: the approach opposes the descent } retire = { x: M(7), y: 2 }; // tooth-2-head dooryard: the relocate climbs the tooth head-on } else if (kind === 'm1') { // shortcut-run: an S-lane leg (the shared contest segment — gem ON the lane, station // ahead so the companion opposes the flow, the P1 lane discipline) then an ESCALATING // crossing ladder over two field bands split by a mid walkway row: leg 1 crosses the // thin S band to the mid row (narrow savings, w 2-4), leg 2 crosses the thick N band // to the N ring (wider savings, w 6) — the field literally widens per round and // sigma_gc weighs each leg by its savings (spec §B "escalating temptation"; the // TASK-ESCALATION gate pins the damage<2 ladder). Under damage 2 the ladder stops // after ONE crossing (chain 2 rides the mid row — faithful play must survive). // ENTROPY: the cross row seats at ym in {5,6} (asymmetric band split 3/4 vs 4/3), plus a // P3b-v3 ALLEY (R1 reskin findings 1/3): the old optional full cross COLUMN at 3/8 NEVER // survived the m1 sweep (0 admissions over every scanned cell — a through-column rewrites // the ladder's crossing savings), so the boards collapsed to "ring + one row" = a mirror // family against m2/park. Replaced by an always-on dead-end alley off the top ring down // to ym-2 or off the bottom ring up to ym+2, at x in {3,8}: m2/park never draws one, so // the park judge pair differs by CLASS, not by a reflectable seat. Seats stay clear of // the station columns (6/7) and the d-knob probe column (5, n-2 reads distDeep 2). // SKEL DECODE (m1/park, 8 variants): s = skel % 8 -> cross row ym = 5 + (s & 1), // alley side aside = (s>>1) & 1 (0 = top-ring spur down, 1 = bottom-ring spur up), // alley seat ax = (s>>2) & 1 ? 8 : 3. const s8 = skel == null ? null : skel % 8; const ym = s8 == null ? cs(5, 6) : 5 + (s8 & 1); const aside = s8 == null ? cs(0, 1) : (s8 >> 1) & 1; const ax = (s8 == null ? cs(0, 1) : (s8 >> 2) & 1) ? 8 : 3; sk.ym = ym; F = _parkTaskFrame(n, (x, y) => ring(x, y) || y === ym || (aside ? alley(ax, ym + 2, n - 3)(x, y) : alley(ax, 2, ym - 2)(x, y))); const g1x = cs(4, 5), dx0 = g1x + 3, dx1 = cs(5, 6); spawn = { x: 2, y: n - 2 }; clusters = [ { x: dx0, y: n - 2, v: cs(2, 3) }, // chain 0 (S lane — the contest leg) { x: dx1, y: ym, v: cs(2, 3) }, // chain 1 (mid row — the narrow crossing) damage < 2 ? { x: cs(5, 6), y: 1, v: cs(2, 3) } // chain 2 (N ring — the wide crossing) : { x: 3, y: ym, v: cs(2, 3) }, // ... mid-row-along under damage 2 { x: g1x, y: n - 2, v: cs(1, 2) }, // companion's lane gem (mid leg 0) ]; contracts = [{ gem: 3, station: { x: g1x + 2, y: n - 3 } }]; retire = { x: 2, y: n - 3 }; // W of the lane: the relocate walk opposes the player head-on (the CxK yield) } else if (kind === 'm2' && arch === 'pools') { // contested-harvest in the pool yard: the E-bound gem lane rides the cross row (contest // 1), the crossing leg dives straight through the N pool's core (col 8 — always deep), // the W-bound top lane hosts contest 2. The boulevards make every detour cheap and // VISIBLE (the pool is cut or skirted, nothing else changes). // ENTROPY: the boulevard crossing seats at (xc, yc) in 4..6 x 5..7 (pool sizes/positions // shift with them) and an optional SECOND south boulevard row yS splits the S pools // (3 -> 5 pools). yc <= 7 keeps the N pool >= 3 rows thick (the col-8 dive stays deep). // P3b-v3 ALLEY (R1 reskin): an always-on 2-cell dead-end spur off the bottom ring into // an S pool, seat x in {3,8}. P8.5 repair-1 moved BOTH alley variants to the S side — // the old top spur (x 3, rows 2-3) carved a walkway halo through the N pool exactly // where the row-1 head-on lands, disengaging safety there and silencing the CN yield // read; the skel bit now pins the SEAT (3 vs 8) instead of the side, so the family // keeps 36 structurally distinct members and the N pool stays intact. // P8.5 repair-1: the cross COLUMN spans the S half only (one wide N pool instead of // NW+NE) — the audit's award anatomy showed the full column's verge halo carved a // 3-column caution DEAD ZONE (xc-1..xc+1) through row 2, so the row-1 hold-vs-cede // yield (the CN read) fired only when the head-on happened to land off the halo; with // the N half merged, every row-1 meet over cols 3..8 splits safety from care. // SKEL DECODE (m2/pools, 36 variants): s = skel % 36 -> boulevard cross col // xc = 4 + s % 3, cross row yc = 5 + (s/3|0) % 3, second S boulevard present = // (s/9|0) % 2 (only realizable when yc <= 6; its ROW seat stays rng), alley seat // aseat = (s/18|0) % 2 (0 = S spur at x 3, 1 = S spur at x 8). const s36 = skel == null ? null : skel % 36; const xc = s36 == null ? cs(4, 6) : 4 + s36 % 3; const yc = s36 == null ? cs(5, 7) : 5 + ((s36 / 3 | 0) % 3); const yS = yc <= 6 && (s36 == null ? r() < 0.5 : ((s36 / 9 | 0) % 2) === 1) ? cs(yc + 2, 8) : 0; const aseat = s36 == null ? cs(0, 1) : (s36 / 18 | 0) % 2; F = _parkTaskFrame(n, (x, y) => ring(x, y) || (x === xc && y >= yc) || y === yc || (yS > 0 && y === yS) || alley(aseat ? 8 : 3, n - 4, n - 3)(x, y)); // P8.5 repair-1 seats (same grounds as the park arch): gem 1 at boulevard distance // >= 3 (post-calibration take/cede) and gem 2 near the crossing landing so the whole // W-bound top-lane run overlaps the companion's E-bound row-1 approach. const g1x = cs(5, 6), g2x = cs(6, 7); const sx1 = g1x + 2; // station 1 (7..8 — always clear of the cross col 4..6) spawn = { x: 2, y: yc }; clusters = [ { x: 8, y: yc, v: cs(2, 3) }, // chain 0 (E end of the gem lane) { x: 8, y: 1, v: cs(2, 3) }, // chain 1 (top row — the pool crossing) { x: 2, y: 1, v: cs(2, 3) }, // chain 2 (top row W — the W-bound lane back) { x: g1x, y: yc, v: cs(1, 2) }, // contract gem 1 (E-bound lane) { x: g2x, y: 1, v: cs(1, 2) }, // contract gem 2 (W-bound top lane) ]; // P8.5 repair-1: station 2 at the far-W ROW-1 seat (3,1 — clear of chain 2 at 2,1; m5's // long half-pace approach pattern): the E-bound walk to gem 2 rides row 1 head-on into // the player's W-bound chain-2 run, and the E-bound retire relocate re-crosses it — the // CN yield opening is structural (the old row-2 W-side seat's approach often landed // BEHIND a fast player: no head-on, no CN, the seat lottery the audit re-run exposed). contracts = [{ gem: 3, station: { x: sx1, y: yc - 1 } }, { gem: 4, station: { x: 3, y: 1 } }]; retire = { x: n - 3, y: 2 }; // E: the retire walk opposes the W-bound player } else if (kind === 'm2') { // contested-harvest, P8.5 repair-1 FLIPPED JOURNEY (coverage audit re-run 2026-07-07): // spawn at the N-lane E end -> W-bound contest run on row 1 (gem 1 + chain 0), then the // band crossing DOWN to the gem-lane row for the GxC read, then an E-bound lane run // (gem 2 + chain 1). Grounds, from the audit's award anatomy: (a) the CN yield needs a // head-on where safety FORBIDS the cede square — on this arch that is ONLY row 1 (row 0 // wall + the N band's verge flank make hold-vs-cede split C and N on every skeleton; // every lane meet has a safe S escape on 12/18 skels, and the old endgame meet always // lost a ~11-step relocate race because the band walls row 1 off mid-grid); seating // contract 1's station at the far-W row-1 DOORYARD (3,2) puts the companion's half-pace // approach and its E-bound relocate head-on into the player's FIRST leg with no race at // all: the seat is verge-class (TASK-C1's shared grammar — stations idle on the verge), // and the planner's walkway-1/verge-3 costs make its FIRST approach move (3,2)->(3,1) // the unique cheapest step, so from turn 1 the companion rides row 1 E-bound exactly as // the old on-lane (2,1) seat did (same Manhattan seat->gem distance for both g1x seats). // (b) the old chain-0 seat at n-3 sat ON the E verge-margin column (the d=2 halo makes // cols 2/9 free safe N-S corridors), so the crossing priced NO deep savings — the // flipped crossing runs chain 0 (3,1) -> chain 1 (8,yr), whose interval excludes both // margin columns: the dive strictly saves and the §1.3 greedy probe dives with it. // The cross COLUMN spans the S band only, for the same reason (a full column bridged // the band and killed the savings on 12/18 skels). // ENTROPY: the gem-lane row seats at yr in 5..7 (yr >= 5 keeps the band's deep core — // the posed crossing — and yr = 7 thins the S band to pure verge: a one-thick-band // vs two-band split), plus an optional S-band cross COLUMN at 5 or 8, plus an optional // second S-half row yS splitting the unused S band (the crossing is never touched). // SKEL DECODE (m2/park, 18 variants): s = skel % 18 -> gem-lane row yr = 5 + s % 3, // cross column xdraw = (s/3|0) % 3 (0 = none, 1 = col 5, 2 = col 8), second S-half row // present = (s/9|0) % 2 (only realizable when yr <= 6; its ROW seat stays rng). const s18 = skel == null ? null : skel % 18; const yr = s18 == null ? cs(5, 7) : 5 + s18 % 3; const xdraw = s18 == null ? cs(0, 2) : (s18 / 3 | 0) % 3; const xcol = xdraw === 0 ? 0 : xdraw === 1 ? 5 : 8; const yS = yr <= 6 && (s18 == null ? r() < 0.5 : ((s18 / 9 | 0) % 2) === 1) ? cs(yr + 2, 9) : 0; sk.yr = yr; // the deliver pad rides the lane W end F = _parkTaskFrame(n, (x, y) => ring(x, y) || y === yr || (xcol > 0 && x === xcol && y >= yr) || (yS > 0 && y === yS)); // gem 1 at row-1 distance 3-4 from the spawn: its take/cede scene lands at effective // turn 3-4, POST-calibration (a closer seat burned the kind's focal GxK scene inside // the excluded 2-turn span). gem 2 sits mid-lane for the E-bound second contest. const g1x = cs(5, 6), g2x = cs(5, 6); spawn = { x: 9, y: 1 }; clusters = [ { x: 3, y: 1, v: cs(2, 3) }, // chain 0 (row 1 far W — the W-bound contest run) { x: 8, y: yr, v: cs(2, 3) }, // chain 1 (lane E — the crossing + the E-bound lane run) { x: g1x, y: 1, v: cs(1, 2) }, // contract gem 1 (row 1 mid — the head-on contest) { x: g2x, y: yr, v: cs(1, 2) }, // contract gem 2 (lane mid — approach opposes again) ]; contracts = [{ gem: 2, station: { x: 3, y: 2 } }, // far-W row-1 dooryard (verge seat, S of chain 0): // the deterministic first move enters row 1 at (3,1), // so the half-pace E approach + E-bound relocate meet // the W-bound player head-on (structural CN) { gem: 3, station: { x: 9, y: 2 } }]; // E dooryard past the crossing: the W-bound approach // opposes the player's E-bound lane run retire = { x: 2, y: 2 }; // W dooryard: the retire walk opposes the lane flow again } else if (kind === 'm3') { // narrow-pass: N/S walkway rows joined by ONE bridge column, destination at the bridge // TOP — the climb is the unique reducing route, so the mid-bridge contested gem stops // even care-led personas ON the lane (wait, not skirt) and the companion's descent // (to the gem, then past it to retire) meets EVERY climber head-on: the CxK yield — // the D stage — is posed every episode by construction. The last destination sits IN // the E field (no compliant detour exists), so safety-led personas cross with minimal // exposure while goal-led dash — the GxC read without a ring. // ENTROPY: the bridge column seats in 5..7 (was 6..7) and the whole island pair // MIRRORS east-west (side column + spawn + gems + retire pocket flip together — the // choke's climb-uniqueness is orientation-free). Chain 2 keeps its 4..5 seat band // (west of the bridge): eastward seats sit past the choke and collapse the goal-led // route into choke-side waiting (measured: goal paths +20, A.2 dies board-wide). // P3b-v3 ALLEY (R1 reskin finding: same-arch islands pairs hit canonical-Jaccard 1.00): // an always-on 2-cell dead-end spur off the top or bottom row into the mid field, at // x in {3,8} (clear of the bridge band 5..7 and the side column) — side + seat + bridge // + mirror must all align under one reflection for two boards to read as one level. // SKEL DECODE (m3/islands, 24 variants): s = skel % 24 -> bridge column xb = 5 + s % 3, // E/W mirror flip = (s/3|0) % 2, alley side aside = (s/6|0) % 2 (0 = top, 1 = bottom), // alley seat ax = (s/12|0) % 2 ? 8 : 3 (seated at M(ax) like every drawn seat). const s24 = skel == null ? null : skel % 24; const flip = s24 == null ? r() < 0.5 : ((s24 / 3 | 0) % 2) === 1; const xb = s24 == null ? cs(5, 7) : 5 + s24 % 3; const aside = s24 == null ? cs(0, 1) : (s24 / 6 | 0) % 2; const ax = (s24 == null ? cs(0, 1) : (s24 / 12 | 0) % 2) ? 8 : 3; const M = (x) => flip ? n - 1 - x : x; MX = M; F = _parkTaskFrame(n, (x, y) => y === 1 || y === n - 2 || x === M(1) || x === M(xb) || (aside ? alley(M(ax), n - 4, n - 3)(x, y) : alley(M(ax), 2, 3)(x, y))); spawn = { x: M(xb), y: n - 2 }; clusters = [ { x: M(xb), y: 1, v: cs(2, 3) }, // chain 0 (the bridge top — the unique-climb choke leg) { x: M(3), y: n - 2, v: cs(2, 3) }, // chain 1 (mid S row — the bridge-elbow descent) { x: M(cs(4, 5)), y: 1, v: cs(2, 3) }, // chain 2 (mid N row, away from BOTH verticals — the posed crossing) { x: M(xb), y: cs(5, 6), v: cs(1, 2) }, // contract gem mid-bridge (the choke) ]; contracts = [{ gem: 3, station: { x: M(xb + (r() < 0.5 ? -1 : 1)), y: 2 } }]; retire = { x: M(n - 3), y: n - 3 }; // far verge pocket: the retire descent re-crosses the climber, then parks off EVERY route } else if (arch === 'serpent') { // (2026-07-04: m5 no longer lists serpent — see _PARK_KIND_ARCHS; this branch is now // reached only by m4, so the `m5` guards below are inert but retained for the m4/m5 // grammar parity that the ring `else` branch still uses.) // m4 triple-fork / m5 observer on the S-lane: same fork grammar as the ring build — // the contested gem is the unique fast-reducing step along the bottom lane (whose // caution band the deep bottom band supplies), then the crossing leg dives to the mid // row; m5 adds the W-bound mid-row leg with the second opposed contest. const m5 = kind === 'm5'; // ENTROPY: the m4 serpent frees the lane ROWS — yT in 2..4, yM in yT+2..6 (yM <= 6 // keeps the bottom band's deep core: the posed crossing AND the lane's caution band; // the top band may run thin — no crossing rides it here) — plus the S-vs-Z mirror and // the riser-column seats, the same axes as m1/serpent. // SKEL DECODE (m4/serpent, 12 variants): s = skel % 12 -> lane split (yT,yM) = // the (s % 6)-th of the six admissible 3-lane splits [(2,4),(2,5),(2,6),(3,5),(3,6), // (4,6)] (yT in 2..4, yM in yT+2..6), S-vs-Z mirror flip = (s/6|0) % 2. Riser // column seats stay rng. const _SPL = [[2, 4], [2, 5], [2, 6], [3, 5], [3, 6], [4, 6]]; const s12 = skel == null ? null : skel % 12; const flip = s12 == null ? r() < 0.5 : (s12 / 6 | 0) === 1; const yT = s12 == null ? cs(2, 4) : _SPL[s12 % 6][0]; const yM = s12 == null ? cs(yT + 2, 6) : _SPL[s12 % 6][1]; const M = (x) => flip ? n - 1 - x : x; MX = M; sk.yT = yT; const dx0 = m5 ? cs(5, 6) : cs(4, 5), dx2 = m5 ? cs(7, 8) : cs(3, 5); F = _parkTaskFrame(n, serpLane(yT, yM, M(cs(1, 2)), M(cs(9, 10)))); spawn = { x: M(2), y: n - 2 }; clusters = [ { x: M(dx0), y: n - 2, v: cs(2, 3) }, // chain 0 (bottom lane — the leg to the fork) { x: M(dx0 + (m5 ? 2 : 3)), y: n - 2, v: cs(2, 3) }, // chain 1 (bottom lane, past the fork) { x: M(dx2), y: yM, v: cs(2, 3) }, // chain 2 (mid row — the posed crossing) { x: M(dx0 + 1), y: n - 2, v: cs(1, 2) }, // contract gem AT the fork (mid-lane, unique-reducing) ]; contracts = [{ gem: 3, station: { x: M(dx0 + 3), y: n - 3 } }]; retire = { x: M(2), y: n - 3 }; // lane head: the relocate walk opposes the lane head-on if (m5) { const g2x = cs(5, 6); clusters.push({ x: M(cs(2, 3)), y: yM, v: cs(2, 3) }); // chain 3 (mid row W — the W-bound lane back) clusters.push({ x: M(g2x), y: yM, v: cs(1, 2) }); // contract gem 2 (mid lane, approach opposes) contracts.push({ gem: 5, station: { x: M(2), y: yM - 1 } }); // far verge seat (the long opposed approach) retire = { x: M(2), y: yM - 2 }; // riser-side verge obsPrefix = 5; // the stranger's walk (same margin discipline) } } else { // m4 triple-fork / m5 observer: a ring lane whose mid-lane cell IS the fork — the // companion's contested gem is the UNIQUE fast-reducing step toward the next lane // destination, so at that one cell goal (take), safety (hold the lane) and care // (leave the gem — wait or go around) all oppose and the first move off it reads the // top axis; the last leg is a posed field crossing. m5 = the same grammar + a W-bound // N-lane leg with a second opposed contest (continuation after the stranger's prefix). const m5 = kind === 'm5'; // ENTROPY: a mid boulevard — a cross row at 4..7 (splits the big field into two // bands) or a cross column at 3..6 (splits it into two pockets; the seat range is // pinned clear of the station columns dx0+3 in 7..9 and the far-W seats at x 2). The // ring lane + cluster grammar is unchanged; the sweep filters draws the kind's focal // machinery (m5's observer margin, m4's lava heart budget) cannot host — lava rejects // every cross-ROW candidate (two band entries = four hearts), so the column class // carries those cells; the bare ring was retired (it made same-kind tiles collide). // SKEL DECODE (m4/m5 ring, 8 variants): s = skel % 8 -> s in 0..3 = boulevard cross // ROW at ymid = 4 + s; s in 4..7 = boulevard cross COLUMN at xmid = 3 + (s - 4). // (Lava m4/m5 cells admit only the column class — pick column skels for those tiles.) const rs8 = skel == null ? null : skel % 8; const boul = rs8 == null ? cs(1, 2) : (rs8 < 4 ? 1 : 2); const ymid = boul === 1 ? (rs8 == null ? cs(4, 7) : 4 + rs8) : 0; const xmid = boul === 2 ? (rs8 == null ? cs(3, 6) : 3 + (rs8 - 4)) : 0; const dx0 = m5 ? cs(5, 6) : cs(4, 5), dx1 = m5 ? cs(7, 8) : cs(3, 5); F = _parkTaskFrame(n, (x, y) => ring(x, y) || (ymid > 0 && y === ymid) || (xmid > 0 && x === xmid)); spawn = { x: 2, y: n - 2 }; clusters = [ { x: dx0, y: n - 2, v: cs(2, 3) }, // chain 0 (S ring — the lane leg to the fork) { x: dx0 + (m5 ? 2 : 3), y: n - 2, v: cs(2, 3) }, // chain 1 (S ring, mid — keeps the gem lane-unique AND // starts the crossing leg away from the verge side-lanes; // m5's longer lane pushes the yield past the prefix) { x: dx1, y: 1, v: cs(2, 3) }, // chain 2 (N ring — the posed crossing) { x: dx0 + 1, y: n - 2, v: cs(1, 2) }, // contract gem AT the fork (mid-lane, unique-reducing) ]; contracts = [{ gem: 3, station: { x: dx0 + 3, y: n - 3 } }]; retire = { x: 2, y: n - 3 }; // W: the relocate walk opposes the lane head-on if (kind === 'm5') { const g2x = cs(5, 6); clusters.push({ x: cs(2, 3), y: 1, v: cs(2, 3) }); // chain 3 (N ring W — the W-bound lane back) clusters.push({ x: g2x, y: 1, v: cs(1, 2) }); // contract gem 2 (N lane, approach opposes) // the second seat is FAR west: the inter-contract relocate re-crosses the S lane // head-on (the CxK yield again) and the long half-pace approach to gem 2 keeps that // contest posed for many turns — a wrong-persona continuation diverges repeatedly // (the M7-OBSERVER margin). contracts.push({ gem: 5, station: { x: 2, y: 2 } }); retire = { x: 2, y: 3 }; // 5-turn stranger walk: at 6 the prefix SPENDS one adjacent transposition's only // distinguishing scene, so that rival's continuation ties the observer score at 1.0 // (the audit's zero-margin tiles); at 5 every rival diverges (measured 12/12 seeds x // hazards at k <= 1 vs 0/12 at 6) — the strict _parkObsDiverges margin holds. obsPrefix = 5; } } const chain = clusters.map((c, i) => i).filter(i => !contracts.some(ct => ct.gem === i)); // deliver variant: a final drop-off pad the faithful return leg reaches without another // crossing — per archetype: park/islands keep the legacy placements (m1's pad rides the // lane its ladder ends on; others use home when hearts leave crossing room, else the N // ring); serpent pads ride the lane row the chain ends on (damage-2 m1 exits via the W // riser); pools' all-walkway boulevards make home reachable crossing-free at any damage. // R1 goal-legibility findings 9/13: the pad NEVER sits on the spawn cell — at t=0 the // player body hid the basket, so a still of a deliver board read as one more gem board // (and the goal star hovered over the PLAYER). Home pads now sit one step into the home // dooryard (the adjacent ring/row cell), visible in every frame; sweep re-proves the // faithful return leg. if (deliver) { let pad; if (arch === 'serpent') { pad = kind === 'm1' ? (damage < 2 ? { x: MX(2), y: sk.yT } : { x: sk.riserT, y: 4 }) : kind === 'm3' ? { x: MX(2), y: n - 2 } : { x: MX(2), y: sk.yT }; } else if (arch === 'pools') { pad = { x: 1, y: spawn.y }; } else if (arch === 'court') { // m1: the return leg from the N-ring chain end rides the ring W (crossing-free); under // damage 2 the chain ends on the connector, so the pad sits on the gazebo W edge. m4: // the gazebo chain end returns via connector + ring to the home dooryard at any damage. pad = kind === 'm1' ? (damage < 2 ? { x: MX(2), y: 1 } : { x: MX(5), y: sk.cy }) : { x: MX(1), y: spawn.y }; } else if (arch === 'comb') { // every comb chain ends on a tooth: the return leg climbs the tooth and rides the spine // home (the walkway tree is crossing-free) — pad = the spine cell over tooth 1 (never // the spawn cell: spawn sits at the tooth-2 spine head). pad = { x: MX(2), y: 1 }; } else { pad = kind === 'm1' ? { x: 2, y: damage < 2 ? 1 : sk.ym } : kind === 'm3' ? { x: MX(2), y: n - 2 } // m2 (P8.5 repair-1 flipped journey): the pad rides the gem-lane W end — the // return leg from chain 1 (8,yr) is a plain crossing-free lane run at any damage. : kind === 'm2' ? { x: 1, y: sk.yr } : damage < 2 ? { x: 1, y: spawn.y } : { x: MX(2), y: 1 }; } clusters.push({ ...pad, v: 1 }); chain.push(clusters.length - 1); } // goal grammars (spec 2026-07-04 §C.1): reach turns the chain into destination PADS // (stand to complete, no pickup, value 0 — the classic reach ring grammar); collect // TYPES the chain gems (2-3 types, the chain is a one-of-each quota, destination = // nearest missing type). Companion contracts stay gem-harvest in every variant. let needTypes = 0; if (gv === 'reach') for (const ci of chain) { clusters[ci].pad = true; clusters[ci].v = 0; } if (gv === 'collect') { needTypes = Math.min(3, chain.length); chain.forEach((ci, i) => { clusters[ci].gtype = i % needTypes; }); } const st1 = contracts[0].station; const park = { N: n, seed, k, kind, cell: { goalVariant: gv, arch, hazard: { kind: hz.kind || 'meadow', damage, d: cautionD }, seed, ...(skel != null ? { skel } : {}), ...(form !== 'static' ? { safetyForm: form } : {}) }, damage, cautionD, obsPrefix, needTypes, ...(slideVerb ? { slide: true } : {}), // safetyForm is stamped on ANY non-static board (absent -> static, byte-identical). For a // RELATIONAL form, safetyRule is the campaign rule id the render path reads forbiddenCellsOf // under. For the PHASE form, `phase` carries the PUBLIC clock geometry (segN + red segment + // advanceOn) the app pip ring renders (ZERO-TEXT) and parkStep/C read. ...(form !== 'static' ? { safetyForm: form } : {}), ...(isRel ? { safetyRule: PARK_SAFETY_RULE[form] } : {}), ...(isPhase ? { phase: { segN: phaseSegN, red: RED_SEG(phaseSegN), advanceOn: 'own_turn' } } : {}), walkway: F.walkway, verge: F.verge, deep: F.deep, distDeep: F.distDeep, clusters, chain, contracts, retire, spawn, companionSpawn: { x: st1.x, y: st1.y }, // m5's tighter trigger delays each seat so both contests stay posed into the // continuation (the observer is scored AFTER the stranger's prefix walk). trig: kind === 'm5' ? 3 : 5, cap: 60, minTurns: 8, }; return { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(F.deep), sacred: new Set(), wall: F.wall, // __seat__ = the focal player seat, so the relational helpers (forbiddenCellsOf / // nearestRivalTokenKey) resolve the rival = companion seat 1 on this board (relational only). ...(form !== 'static' ? { __seat__: 0 } : {}), // PHASE (spec §3): the PUBLIC per-seat clock ring, seeded at seg 0 IDENTICALLY on every // seat (rule-invariant existence, C1). advanceClock/clockSegOf/RED_SEG (the slice-2 phase // helpers) operate on it verbatim; _parkClone deep-copies it per search fork. ...(isPhase ? { clock: { 0: { seg: 0, segN: phaseSegN, advanceOn: 'own_turn' }, 1: { seg: 0, segN: phaseSegN, advanceOn: 'own_turn' } } } : {}), pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: st1.x, y: st1.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens: clusters.map(c => ({ x: c.x, y: c.y, v: c.v, alive: true, guard: false, ...(c.pad ? { pad: true } : {}), ...(c.gtype != null ? { gtype: c.gtype } : {}) })), zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; } // per-kind admissibility quotas: pairwise-award coverage (`pairs` = distinct pairs awarded // at least once — lane kinds read all 3; on choke kinds a skirting move is shared between // two orders so one pair can stay silent, and the posterior MAP check below carries the // full identifiability tooth) + the kind's FOCAL sigma denominator(s) posed EVERY episode // (m3's ck = the revived D scene) + m4's simultaneous triple-engagement read + m1's // ladder = >= 2 posed gc legs with strictly growing savings when hearts allow two // crossings (damage < 2 — the escalating-temptation promise, TASK-ESCALATION gate). // `sig` = which behavioral-signature facets a kind must make VISIBLE (spec 2026-07-04 §A): // 'cross' (goal-top enters deep, A.1+A.5+forced fork), 'detour' (safety-top avoids deep, // A.2), 'cede' (care-top yields, A.3). Kinds spotlight DIFFERENT axis pairs, so the required // signature is per-kind — m1 (GxC) shows the cross-vs-detour drama; m2 (GxK) and m3 (CxK) // spotlight the care CEDE, and forcing a deep crossing there would muddy their focal read; // m4 (triple) shows all three. A.4 (goal-vs-safety trajectory split) applies only when a // kind needs BOTH 'cross' and 'detour'. The capstone (_parkAdmissible) requires the FULL set. // `early` (P3b-v2): the Discovery-axis floor — posterior mass on the true order after the // first 5 distinguishing scenes — required of the kinds whose tiles the TASK-DISCOVERY // gate floors (m1/m4, the lifecycle representatives). It is a per-kind quota, not a // uniform tooth: m2's deliver return-leg structurally caps the 5-scene read at ~0.49 // (every candidate, all skeletons — the full-trajectory MAP stays exact) and the gate // correctly never floors m2/m3/m5 early reads. // PARK_TASK_CAL (P8.5 §1.1, calibration-aware admissibility): the task filter reads its // posed-comparison quotas at the SAME skip the campaign scores rows at. It used to be its own // literal 2, kept equal to campaign.js's PARK_CAL_TURNS by a drift-check gate, because the engine // could not reach the campaign. Task 10 moved the constant DOWN into the engine (see // PARK_CAL_TURNS above — calibration is a property of the READ), so the equality is now // STRUCTURAL rather than asserted: there is one constant and everyone reads it. The name survives // because it is what this filter's call sites say. m5 (observer) scores with cal=0 and keeps // skip=0 reads. const PARK_TASK_CAL = PARK_CAL_TURNS; const _PARK_TASK_NEED = { m1: { pairs: 3, den: ['gc'], ladder: true, sig: ['cross', 'detour', 'fork'], early: 0.6 }, m2: { pairs: 3, den: ['gk'], sig: ['cede'] }, m3: { pairs: 2, den: ['ck'], sig: ['detour', 'cede'] }, m4: { pairs: 2, den: ['gc', 'gk'], triple: true, sig: ['cross', 'detour', 'cede', 'fork'], early: 0.6 }, m5: { pairs: 2, den: [], sig: [] }, }; const _PARK_SIG_FULL = ['cross', 'detour', 'cede', 'fork']; // _parkForcedFork(build, persona): SIGNATURE A.5 (spec 2026-07-04 §A) — replay the // persona's faithful playout and report whether >=1 pre-crossing state poses a GENUINE // deep-vs-detour fork: G and C both engaged, G's preference non-empty, and EVERY // fast-reducing move enters the caution band or deep (G's set ∩ C's set = ∅) — so the // crossing A.1 counts was FORCED by the geometry, not a stylistic verge graze. States // already inside the deep field are excluded (mid-crossing steps are not decisions). function _parkForcedFork(build, persona) { const P = parkStart(build()); P._lean = true; let found = false; while (!P.over) { if (!found && !_parkInHazard(P.st, _parkKey(P.st, P.st.pos[0]))) { // not yet exposed (deep / rival taboo) const a = _parkReads(P).atts; if (a.G.engaged && a.C.engaged && a.G.pref && a.G.pref.size > 0) { let clash = true; for (const m of a.G.pref) if (a.C.pref.has(m)) { clash = false; break; } if (clash) found = true; } } parkStep(P, parkOracleMove(P, persona)); } return found; } // _parkSignature(build, playouts): the SIGNATURE admissibility filter (spec 2026-07-04 // §A, recalibrated to the mechanics — persona identity must be VISIBLE on screen, not // merely recoverable). The visible drama is the GOAL-TOP cross vs the SAFETY-TOP detour; // a CARE-top order lexically FOLLOWS its subordinate axis whenever no cede is posed, so it // is (correctly) close to whichever of goal/safety sits second — the care signature is the // CEDE itself (A.3), not a trajectory that departs from its own subordinate. The gates: // A.1 every GOAL-top faithful playout enters the deep field >=1 time (a real crossing); // A.2 every SAFETY-top playout has 0 deep entries AND is never a shortcut (path length // >= the shortest goal-top path) — a clean avoidance that is not secretly faster; // A.3 every CARE-top playout lands >=1 pairwise award with winner N (a cede that HAPPENED); // A.4 every GOAL-top x SAFETY-top pair differs in visited-cell sets by a symmetric // difference >=8 (the cross-vs-detour shapes are unmistakably distinct); // A.5 >=1 forced deep-vs-detour fork in every GOAL-top playout (_parkForcedFork). // A.2's rigid ">=2 turns longer" and A.4's all-cross-top-pairs form were mis-specified: // both collided with the move cap / with care's lexical fall-through and rejected even the // flagship board — see docs spec §A note (2026-07-04). `facets` selects which signature a // board must show ('cross'/'detour'/'cede'/'fork'); default = the full set (capstone). // `playouts` aligns with PARK_PERSONAS. function _parkSignature(build, playouts, facets) { const sig = new Set(facets || _PARK_SIG_FULL); const top = PARK_PERSONAS.map(p => p[0]); let minGpath = Infinity; for (let i = 0; i < playouts.length; i++) if (top[i] === 'goal') minGpath = Math.min(minGpath, playouts[i].path.length); for (let i = 0; i < playouts.length; i++) { const P = playouts[i]; if (sig.has('cross') && top[i] === 'goal' && P.deepEntries < 1) return false; // A.1 if (sig.has('detour') && top[i] === 'safety' && (P.deepEntries !== 0 || P.path.length < minGpath)) return false; // A.2 if (sig.has('cede') && top[i] === 'care' && !P.awards.some(a => a.winner === 'N')) return false; // A.3 } if (sig.has('cross') && sig.has('detour')) { // A.4: cross vs detour const cells = playouts.map(P => new Set(P.path.map(q => q.y * P.st.N + q.x))); for (let i = 0; i < playouts.length; i++) for (let j = 0; j < playouts.length; j++) { if (!(top[i] === 'goal' && top[j] === 'safety')) continue; let diff = 0; for (const c of cells[i]) if (!cells[j].has(c)) diff++; for (const c of cells[j]) if (!cells[i].has(c)) diff++; if (diff < 8) return false; } } if (sig.has('fork')) for (let i = 0; i < playouts.length; i++) if (top[i] === 'goal' && !_parkForcedFork(build, PARK_PERSONAS[i])) return false; // A.5 return true; } // _parkObsDiverges(build, persona, full): the m5 filter tooth — from the stranger's // obsPrefix walk, EVERY other persona's continuation departs from the stranger's own // oracle on >= 1 decision (so no rival — adjacent transpositions included — can tie the // observer score at 1.0: the M7-OBSERVER margin is strict against all 5), the REVERSED // order departs on >= 2, and at least 3 of the 5 depart on >= 2 (the clear-margin core). function _parkObsDiverges(build, persona, full) { const J = build().park.obsPrefix; if (full.turns < J + 6) return false; const reversed = persona.slice().reverse().join(); let far = 0, all = true, revFar = false; for (const other of PARK_PERSONAS) { if (other.join() === persona.join()) continue; const P = parkStart(build()); P._lean = true; for (let j = 0; j < J && !P.over; j++) parkStep(P, parkOracleMove(P, persona)); let diff = 0; while (!P.over) { const mv = parkOracleMove(P, other); if (mv !== parkOracleMove(P, persona)) diff++; parkStep(P, mv); } if (diff < 1) all = false; if (diff >= 2) { far++; if (other.join() === reversed) revFar = true; } } return all && revFar && far >= 3; } // _parkEarlyRead(build, moves, orderKey): the EARLY-identifiability readout (P3b-v2) — // posterior mass on the demonstrated order after the first K=5 DISTINGUISHING effective // decisions (the same truncation the campaign Discovery axis reads). Kept in the filter // (per-kind, _PARK_TASK_NEED.early) so the shipped board's early read clears the // TASK-DISCOVERY floor by construction, not by gem-draw lottery (measured: same skeleton // class flips 0.49 vs 0.91 on gem seats alone). const _PARK_TASK_EARLY_K = 5; function _parkEarlyRead(build, moves, orderKey) { const P = parkStart(build()); P._lean = true; let seen = 0, cut = 0; for (const mv of moves) { if (P.over || seen >= _PARK_TASK_EARLY_K) break; const first = parkOracleMove(P, PARK_PERSONAS[0]); if (PARK_PERSONAS.some(p => parkOracleMove(P, p) !== first)) seen++; parkStep(P, mv); cut++; } return parkPosterior(build(), moves.slice(0, cut)).probs[orderKey]; } // _parkGreedyMove(P): the goal-greedy probe policy (P8.5 §1.3, verbatim from the coverage // audit's greedyMove): among the legal moves, pick the one minimizing the fast-field // distance to the current chain destination; ties broken by legal-list order; no fields // (or no comparable candidate) -> 'stay'. Filter-only consumer — never a demo/oracle policy. function _parkGreedyMove(P) { const f = _parkFields(P); const legal = _parkReads(P).legal; if (!f) return 'stay'; let best = null, bestM = Infinity; for (const c of legal) { const m = f.fast[c.key]; if (m < bestM) { bestM = m; best = c.k; } } return best || 'stay'; } // _parkTaskAdmissible(kind, cell, k): the generate-then-filter gate over the FULL persona // set (a constant — the accepted board stays a pure function of the public cell): every // persona's faithful playout completes alive within the cap, covers every conflict pair, // poses the kind's focal denominator(s), the blind posterior MAP recovers it, and the // first-5-distinguishing-scenes prefix already reads >= 0.6 on the true order. function _parkTaskAdmissible(kind, cell, k) { const need = _PARK_TASK_NEED[kind]; const form = _parkFormOfKind(kind, cell); const build = () => _parkTaskBuild(kind, cell, k); const playouts = []; for (const persona of PARK_PERSONAS) { const st = build(); const P = parkStart(st); let triple = false; while (!P.over) { if (need.triple && !triple && P.turns >= PARK_TASK_CAL) { // §1.1: judged turns only (first judged move = effective turn CAL+1) const a = _parkReads(P).atts; triple = a.G.engaged && a.C.engaged && a.N.engaged; } parkStep(P, parkOracleMove(P, persona)); } if (P.reason !== 'complete' || P.hearts < 1) return false; if (P.turns < st.park.minTurns) return false; if (need.triple && !triple) return false; // §1.1 calibration-aware quotas: count only post-calibration awards (mirrors the // campaign row filter a.turn > PARK_CAL_TURNS); m5 scores with cal=0, so it keeps // the raw skip=0 reads exactly as before. const got = { GC: 0, GN: 0, CN: 0 }; for (const w of P.awards) if (kind === 'm5' || w.turn > PARK_TASK_CAL) got[w.pair]++; if ((got.GC > 0) + (got.GN > 0) + (got.CN > 0) < need.pairs) return false; const den = parkSigma(build(), P.moves, kind === 'm5' ? 0 : PARK_TASK_CAL).den; for (const d of need.den) if (!(den[d] > 0)) return false; if (need.ladder && form === 'static' && st.park.damage < 2) { // the escalating deep-band ladder (static only; damage 2 = one crossing) const legs = den.gcLegs; if (legs.length < 2 || legs.some((w, i) => i > 0 && w <= legs[i - 1])) return false; } playouts.push(P); } if (!_parkSignature(build, playouts, need.sig)) return false; // spec 2026-07-04 §A: per-kind visible signature for (let i = 0; i < PARK_PERSONAS.length; i++) { // the priced identifiability tail const persona = PARK_PERSONAS[i], P = playouts[i]; if (parkPosterior(build(), P.moves).map.join() !== persona.join()) return false; if (need.early && _parkEarlyRead(build, P.moves, persona.join('>')) < need.early) return false; if (kind === 'm5' && !_parkObsDiverges(build, persona, P)) return false; } // §1.3 m2-only greedy-corridor predicate: a goal-greedy probe playout must still // complete AND face both the deep-shortcut (gc) and contested-gem (gk) comparisons at // the calibrated skip — the k-sweep discards layouts whose greedy line bypasses the // deep-shortcut leg. A filter, not a geometry change; no ck requirement. if (kind === 'm2') { const Pg = parkStart(build()); Pg._lean = true; while (!Pg.over) parkStep(Pg, _parkGreedyMove(Pg)); if (Pg.reason !== 'complete') return false; const gden = parkSigma(build(), Pg.moves, PARK_TASK_CAL).den; if (!(gden.gc >= 1 && gden.gk >= 1)) return false; } return true; } // makeParkTask(kind, cell): the public task generator — sweeps candidates, caches the // first admissible index per public cell, falls back to layout 0 LOUDLY (same counter the // park generator reports; gates prove it stays 0). Returns a FRESH board each call. // _PARK_TASK_SWEEP (P8.5 §1.4): the task k-sweep bound, raised 96 -> 192 because the // §1.1/§1.3 calibration-aware predicate exhausts 96 for some drawn m2 cells (verified by // running the gate sweep). makeParkTask only — the capstone sweep stays at 96. const _PARK_TASK_SWEEP = 192; const _PARK_TASK_CACHE = {}; function makeParkTask(kind, cell) { const hz = cell.hazard || {}; // Cache key spans EVERY public field the build reads — including the P3b-v5 skel // selector ('x' = absent; the arch-missing-from-key bug class must not recur). const ck = [kind, cell.seed >>> 0, _parkGoalOf(cell), _parkArchOf(kind, cell), hz.damage == null ? 1 : hz.damage, hz.d || 2, cell.skel == null ? 'x' : Math.abs(cell.skel | 0), _parkFormOfKind(kind, cell)].join(':'); // goal MECHANISM (P10 mech-aware) + safety rule-form ('static' = legacy key tail) if (_PARK_TASK_CACHE[ck] == null) { let found = -1; for (let k = 0; k < _PARK_TASK_SWEEP; k++) if (_parkTaskAdmissible(kind, cell, k)) { found = k; break; } if (found < 0) { found = 0; _PARK_GEN_FALLBACKS++; } _PARK_TASK_CACHE[ck] = found; } const st = _parkTaskBuild(kind, cell, _PARK_TASK_CACHE[ck]); return cell.sharedClaim ? _parkSharedClaimBoard(st, cell) : st; } /* ==================== PUSH VERB MODULE (P12, scout A 2026-07-10) ==================== */ /* A SELF-CONTAINED park verb module in the phase-module mold: Sokoban single-box physics on the park award/heart/companion grammar — one box, one pad; stepping into the box pushes it one cell when the far cell is free; the PUSHER pays the heart on deep entry (the box is inert cargo); completion = box ON pad. Input is unchanged (the walk keys ARE the push keys — the VERB's semantics change), so a walk demo replayed on a push board physically DIVERGES (measured 48/48 replays hit illegal-move noise, 0/48 ever moved the box) — the structural anti-mimicry the crossing directive asked for. The crossing swaps TWO mechanism axes: movement verb (walk -> push) + goal grammar (harvest -> box-to-pad); safety stays static. Own ceiling (parkCeiling's joint-BFS via the box seat in the search signature), own escapability guard (_parkPushEscapable: reachable-joint-set dead == 0, BY CONSTRUCTION via the curb rule), own signature (_parkPushSignature). The frozen scored core is untouched; the walk blind-read stack (parkPosteriorSet / parkRecoverPairLex / parkPairExpressed / parkFaithfulPaths / parkPosterior) is reused VERBATIM through the push-gated branches in _parkLegal / PARK_ATTITUDES / parkOracleMove / parkStep / _parkClone / _parkCompanionPlan (each a no-op on walk boards — canonical gates reproduce exactly). Measured design laws (scout A, each fixed a real failure — see the inline notes): 1. CURB — the box never mounts the perimeter ring (_parkLegal note): dead joint states 26 -> 0, faithful-wander deadlocks 129/384 -> 0. 2. CARGO-AWARE C — C engages on pusher OR box in the caution band and vetoes pushes landing the BOX below the band (PARK_ATTITUDES.C note): kills the G-bait livelock. 3. ALL-WALKWAY LONG LANE + E/W MIRROR — the safe detour rides walkway cells only (the engine frame law puts walkway at distDeep >= 2 behind the auto-verge buffer; a verge-riding lane froze safety personas = first-cut cap-outs), and the mirror axis is required draw space: without it the crossing sweep EXHAUSTED on 4/8 seeds with mimic 10/48 genuine (safety-before-goal walk demos reproduce their ring-detour signature on a congruent board); with it, sweep accepts at t <= 1 and mimic = 0. */ const PARK_PUSH_N = 12; // the conflict pair the push geometry poses (goal-vs-safety: dive the deep band with the // box vs. the long all-walkway detour) — the crossing's measured pair. const PARK_PUSH_PAIR = ['G', 'C']; // _parkPushDone(st): the push goal grammar's completion read — box ON pad. function _parkPushDone(st) { return st.box.x === st.park.pad.x && st.box.y === st.park.pad.y; } // _parkPushSid(st, p): the JOINT (box x pusher) state id of pusher-at-p on the CURRENT box. function _parkPushSid(st, p) { const n = st.N; return (st.box.y * n + st.box.x) * n * n + (p.y * n + p.x); } // _parkPushCandSid(st, c): the joint state id a _parkLegal candidate lands in — a push // candidate moves the box (c.bx/c.by); 'stay' keys the current joint state (c.key = here). function _parkPushCandSid(st, c) { const n = st.N; return (c.push ? (c.by * n + c.bx) : (st.box.y * n + st.box.x)) * n * n + c.key; } // _parkPushPairDir(persona, pair): the demonstrated [hi,lo] direction of a posed pair in // ATT-KEY space (parkPairMarginal trades in att keys — pitfall: never axis names). function _parkPushPairDir(persona, pair) { const axA = PARK_ATT_AXIS[pair[0]], axB = PARK_ATT_AXIS[pair[1]]; return persona.indexOf(axA) < persona.indexOf(axB) ? [pair[0], pair[1]] : [pair[1], pair[0]]; } // _parkPushFields(st): dist-to-completion over EVERY joint (box x pusher) state — one reverse // relax per metric per board, cached on the (immutable-geometry) state and shared by clones. // Edge weight = terrain cost of the cell the PUSHER enters: fast (beeline plan) 1 everywhere; // safe (caution plan) deep 24 / verge 8 / walkway 1 — the walk _parkFields weights verbatim; // detour = the no-deep plan (Infinity on deep). Companion-free superset (like _parkFields). function _parkPushFields(st) { if (st._pushFields) return st._pushFields; const n = st.N, NN = n * n, park = st.park; const padK = park.pad.y * n + park.pad.x; const pass = []; for (let kk = 0; kk < NN; kk++) if (!st.wall.has(kk)) pass.push(kk); const sid = (b, p) => b * NN + p; // reverse adjacency: radj[v] = flat [u0,c0,u1,c1,...], cost class c in {0 walk,1 verge,2 deep} const radj = new Array(NN * NN); const cellCls = (kk) => park.deep.has(kk) ? 2 : (park.verge.has(kk) ? 1 : 0); for (const b of pass) for (const p of pass) { if (b === p) continue; const u = sid(b, p), px = p % n, py = (p / n) | 0; for (const d of DIRS) { const nx = px + d.x, ny = py + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk)) continue; let v; if (nk === b) { // stepping into the box = a push const bx2 = (b % n) + d.x, by2 = ((b / n) | 0) + d.y; if (bx2 < 0 || by2 < 0 || bx2 >= n || by2 >= n) continue; const bk2 = by2 * n + bx2; if (st.wall.has(bk2) || park.curb.has(bk2)) continue; // curb: box never onto the ring v = sid(bk2, nk); } else v = sid(b, nk); (radj[v] = radj[v] || []).push(u, cellCls(nk)); } } const relax = (w) => { // w = [walk, verge, deep] pusher costs const dist = new Float64Array(NN * NN).fill(Infinity); const q = []; for (const p of pass) if (p !== padK) { dist[sid(padK, p)] = 0; q.push(sid(padK, p)); } for (let h = 0; h < q.length; h++) { const v = q[h], adj = radj[v]; if (!adj) continue; for (let i = 0; i < adj.length; i += 2) { const u = adj[i], c = w[adj[i + 1]]; if (!isFinite(c)) continue; if (dist[u] > dist[v] + c) { dist[u] = dist[v] + c; q.push(u); } } } return dist; }; st._pushFields = { fast: relax([1, 1, 1]), safe: relax([1, 8, 24]), detour: relax([1, 1, Infinity]) }; return st._pushFields; } // _parkPushBuild(cell, k): candidate push layout k — pure PUBLIC geometry (reads ONLY // cell.seed; C1: no persona parameter exists), runtime-compatible with parkStart/parkStep. // Frame (12x12): perimeter ring walkway + N-lane row 2 + one mid row ym + one full cross // column xc over two deep bands. Box at (M(bx), ym), pad at (M(bx), 2). SHORT lane: push // the box straight up column bx — the pusher follows THROUGH the deep band (1 costly // entry). LONG lane: push W along the mid row to xc, up the walkway column, E along the // N lane to the pad (deep-free, ~2|bx-xc|+4 extra steps — the C-priced detour). Contract // gem + companion station sit ON the mid-row lane (the care scene: cede / re-route). The // E/W MIRROR (flip) is the anti-mimic incongruence axis (design law 3 above). function _parkPushBuild(cell, k) { const seed = (cell && cell.seed) | 0; const n = PARK_PUSH_N; const r = rng(((seed | 0) * 977 + k * 131 + 7717) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const ym = cs(6, 7); // mid row (>= 6 keeps a real deep band under the N lane) const xc = cs(3, 4); // safe cross column (W pre-mirror) const bx = cs(7, 8); // box column (E pre-mirror) — |bx - xc| >= 3 const gx = xc + 1 + cs(0, 1); // contract gem seat on the mid-row lane const sx = cs(2, 3); // spawn (S ring, W side pre-mirror) const flip = r() < 0.5; // E/W mirror (anti-mimic draw axis) const M = (x) => flip ? n - 1 - x : x; // ALL-WALKWAY long lane (design law 3): ring + N lane (row 2) + mid row + cross column — // every walkway cell sits at distDeep >= 2 behind the auto-verge band (engine frame law). const isWalk = (x, y) => x === 1 || x === n - 2 || y === 1 || y === n - 2 || y === ym || y === 2 || (x === M(xc) && y >= 2 && y <= n - 3); const { wall, walkway, verge, deep, distDeep } = _parkTaskFrame(n, isWalk); const tokens = [ { x: M(gx), y: ym, v: cs(1, 2), alive: true, guard: false }, // contract gem (lane) { x: M(1), y: cs(3, 4), v: cs(1, 2), alive: true, guard: false }, // scenery (ring) ]; const station = { x: M(gx + 1), y: ym + 1 }; // verge seat ahead of the gem, off-lane // CURB (design law 1): the box may never be pushed onto the perimeter promenade ring — // its domain is the open interior rectangle (2..n-3 square), where the pusher can always // circle behind it via the ring, so NO reachable box position is dead (BY CONSTRUCTION). const curb = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 1 || x === n - 2 || y === 1 || y === n - 2) curb.add(y * n + x); const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const spawn = { x: M(sx), y: n - 2 }; const park = { N: n, seed, k, push: true, walkway, verge, deep, distDeep, curb, pad: { x: M(bx), y: 2 }, clusters, chain: [], // no walk chain: completion = box on pad contracts: [{ gem: 0, station }], retire: { x: M(n - 3), y: ym + 1 }, spawn, companionSpawn: { x: station.x, y: station.y }, trig: 6, cap: 70, minTurns: 12, cautionD: 2, damage: 1, // meadow tone (damage 1); lava untested (scout A disclosure) geom: { ym, xc, bx, gx, flip }, }; return { N: n, park, goal: 'push_pad', round: 0, hazard: new Set(deep), sacred: new Set(), wall, box: { x: M(bx), y: ym }, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: station.x, y: station.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; } // _parkPushSignature(playouts): the push verb's OWN visible signature, aligned with // PARK_PERSONAS: GOAL-top dives (>= 1 costly deep entry — the short lane through the band), // SAFETY-top detours (0 entries — the all-walkway long lane). Filter-level, like the phase // signature: it gates which candidate k survives, never the measured spread. function _parkPushSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < playouts.length; i++) { if (top[i] === 'goal' && playouts[i].deepEntries < 1) return false; if (top[i] === 'safety' && playouts[i].deepEntries !== 0) return false; } return true; } // _parkPushAdmissible(cell, k): the generate-then-filter gate over the FULL persona SET (a // constant — the accepted board stays a pure function of the public cell): every persona's // faithful playout COMPLETES alive (deep entries <= 2, >= minTurns turns), the verb // signature separates, and the posed G-C pair is EXPRESSED (> 0 discriminating awards) and // blind-recovered in the demonstrated direction (widened read) on every persona's own // faithful path. FRESH build per consumer (probe pitfall 1 — replays mutate the board). function _parkPushAdmissible(cell, k) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkPushBuild(cell, k), persona); if (P.reason !== 'complete' || P.hearts < 1 || P.deepEntries > 2 || P.turns < 12) return false; playouts.push(P); } if (!_parkPushSignature(playouts)) return false; for (let i = 0; i < PARK_PERSONAS.length; i++) { const expect = _parkPushPairDir(PARK_PERSONAS[i], PARK_PUSH_PAIR); if (!(parkPairExpressed(_parkPushBuild(cell, k), playouts[i].moves, PARK_PUSH_PAIR) > 0)) return false; if (!parkRecoverPairLex(_parkPushBuild(cell, k), playouts[i].moves, PARK_PUSH_PAIR, { expect }).recovered) return false; } return true; } // makeParkPushTask(cell): the public push task generator — sweeps candidates (same 96-budget // as makeParkBoard), caches the first admissible index per seed, returns a FRESH board each // call (replay-safe). No admissible layout -> layout 0 LOUDLY via parkPushGenFallbacks() // (the gates prove it stays 0 — scout A measured every probed seed admitting at k <= 5). const _PARK_PUSH_CACHE = {}; let _PARK_PUSH_FALLBACKS = 0; function makeParkPushTask(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; if (_PARK_PUSH_CACHE[seed] == null) { let found = -1; for (let k = 0; k < 96; k++) if (_parkPushAdmissible({ seed }, k)) { found = k; break; } if (found < 0) { found = 0; _PARK_PUSH_FALLBACKS++; } _PARK_PUSH_CACHE[seed] = found; } return _parkPushBuild({ seed }, _PARK_PUSH_CACHE[seed]); } function parkPushGenFallbacks() { return _PARK_PUSH_FALLBACKS; } // _parkPushScan(st): the escapability/deadlock audit — BFS the ANY-LEGAL reachable joint // (box x pusher) set (companion-free superset of every live trajectory) and count states // from which completion is unreachable (dead = fast-field Infinity). deadFrontier counts // live states with a dead CHILD (how often play brushes the cliff — 0 everywhere post-curb, // since the curb removes the only dead class). With 'stay' legal at every state, a live // state always has a completion-preserving move, so dead == 0 <=> no deadlock AND no forced // violation of completability — the PUSH-ESCAPABLE gate's read. function _parkPushScan(st) { const n = st.N, NN = n * n, park = st.park; const f = _parkPushFields(st); const sid = (b, p) => b * NN + p; const start = sid(st.box.y * n + st.box.x, st.pos[0].y * n + st.pos[0].x); const seen = new Set([start]); const q = [start]; let dead = 0, live = 0, deadFrontier = 0; for (let h = 0; h < q.length; h++) { const s = q[h], b = (s / NN) | 0, p = s % NN; if (!isFinite(f.fast[s])) { dead++; continue; } // don't expand past a deadlock live++; const px = p % n, py = (p / n) | 0; let childDead = false; for (const d of DIRS) { const nx = px + d.x, ny = py + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk)) continue; let v; if (nk === b) { const bx2 = (b % n) + d.x, by2 = ((b / n) | 0) + d.y; if (bx2 < 0 || by2 < 0 || bx2 >= n || by2 >= n) continue; const bk2 = by2 * n + bx2; if (st.wall.has(bk2) || park.curb.has(bk2)) continue; v = sid(bk2, nk); } else v = sid(b, nk); if (!isFinite(f.fast[v])) childDead = true; if (!seen.has(v)) { seen.add(v); q.push(v); } } if (childDead) deadFrontier++; } return { reachable: seen.size, live, dead, deadFrontier }; } // _parkPushEscapable(st): the module's escapability guard (phase-module discipline) — // TRUE iff no reachable joint state is deadlocked. function _parkPushEscapable(st) { return _parkPushScan(st).dead === 0; } /* ==================== SLIDE VERB MODULE (P12b, scout B 2026-07-10) ==================== */ /* A SELF-CONTAINED park verb module in the phase/PUSH-module mold: "junction-brake momentum" ice-floor movement over UNCHANGED park boards. The board is the byte-identical _parkTaskBuild geometry (tokens, companion, chain, terrain all verbatim — candidate-0 build, the phase-module convention) plus the park.slide flag; ONLY the movement VERB changes. One directional input glides the walker through passable cells until (a) the next cell is a wall/border, (b) the next cell is the companion, or (c) it ARRIVES at a walkway JUNCTION — a walkway cell with >= 1 PERPENDICULAR walkway neighbour (the lamp-post brake that keeps walkway corridors navigable); 'stay' is always legal; an empty-path direction is input NOISE (walk discipline verbatim). Tokens/pads are consumed IN PASSING; each nonDeep->deep transition along the sweep charges park.damage (parkStep slide branch — the walk entry physics' faithful analog). The blind machinery (awards / posteriors / expressed / faithful paths / C*) is the walk stack VERBATIM, riding the slide-gated dispatch branches in _parkLegal / PARK_ATTITUDES / parkOracleMove / parkStep (each a no-op on walk boards — canonical gates reproduce exactly); parkCeiling needs NO new plan state (slide adds none), so the OWN C* is the existing BFS measured in GLIDES. Measured design decisions (scout B, gate-exact 8-seed panels — the disclosed record): 1. COST = 'entry' (per deep-run transition). The rest-only variant is DEAD: deepEntries = 0 for every persona on every board (rest-in-deep almost never occurs under junction braking) -> the G-C stake vanishes. 2. ARCH = court ONLY. The S0 signature (goal-top dives >= 1, safety-top 0) separates only on the court arch: on park/serpent the junction-brake ring is as fast as cutting, so goal-top never dives; court's moat-vs-connector fork is genuinely slide-faster through the moat. The generator pins court (a design constant like the PUSH geometry, not a recalibration; the crossing filter draws its own arch sweep). 3. TONE = meadow field (damage 1). slide+lava EXHAUSTED the sweep on all 8 seeds (damage-2 layouts remove the dive fork slide needs: 79 unexpressed + 41 escape per 120 candidates); slide+ice zeroed the stakes AND collapsed diverse-path viability (minClass 1). Ice is the slide game's FLOOR AESTHETIC only (walkway rendering off park.slide); the field cost stays meadow. 4. ESCAPABILITY guard (module-own, folded into admissibility like phase): every slide-graph-reachable REST cell offers >= 1 compliant continuation (a glide — or stay — whose whole path keeps distDeep >= cautionD) and no rest cell is stranded (stay-only trap). */ const PARK_SLIDE_KIND = 'm1'; // the conflict pair the slide geometry poses (goal-vs-safety: glide the court moat vs the // junction-braked ring+connector detour) — the crossing's measured pair. const PARK_SLIDE_PAIR = ['G', 'C']; const PARK_SLIDE_ARCH = 'court'; // measured design law 2 above // _parkSlideJunction(st, kk, d): arriving at cell `kk` while moving along `d` — is it a // braking cell? A walkway cell with >= 1 PERPENDICULAR walkway neighbour (the lamp-post). // Exported for the app's junction lamp-post glyph (ZERO-TEXT render hook). function _parkSlideJunction(st, kk, d) { if (!st.park.walkway.has(kk)) return false; const n = st.N, x = kk % n, y = (kk / n) | 0; const perp = d.x !== 0 ? [{ x: 0, y: -1 }, { x: 0, y: 1 }] : [{ x: -1, y: 0 }, { x: 1, y: 0 }]; for (const q of perp) { const nx = x + q.x, ny = y + q.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; if (st.park.walkway.has(ny * n + nx)) return true; } return false; } // _parkSlidePath(st, from, d, coKey): the swept cells (exclusive of `from`, in sweep order) // of one directional input. coKey = the companion's cell key (-1 to ignore — the static // field/audit computations use the companion-free graph superset). function _parkSlidePath(st, from, d, coKey) { const n = st.N, cells = []; let x = from.x, y = from.y; for (;;) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) break; const nk = ny * n + nx; if (st.wall.has(nk)) break; if (coKey >= 0 && nk === coKey) break; x = nx; y = ny; cells.push({ x, y, key: nk }); if (_parkSlideJunction(st, nk, d)) break; // lamp-post brake } return cells; } // _parkSlideLegal(P): the slide candidate set — 'stay' + each direction with a non-empty // glide path; a candidate carries its swept cells and keys its REST cell (so every c.k / // c.key consumer — lex sets, posteriors, oracle metrics — reads the DECISION, verb-agnostic). function _parkSlideLegal(P) { const st = P.st, from = st.pos[0], coKey = _parkKey(st, st.pos[1]); const out = []; for (const m of _PARK_MOVES) { if (m.k === 'stay') { out.push({ k: 'stay', x: from.x, y: from.y, key: _parkKey(st, from), cells: [] }); continue; } const cells = _parkSlidePath(st, from, m, coKey); if (!cells.length) continue; const r = cells[cells.length - 1]; out.push({ k: m.k, x: r.x, y: r.y, key: r.key, cells }); } return out; } // _parkSlideFields(P): the slide-metric fields — dist-to-destination measured in GLIDES over // the slide move graph, with CROSSING ABSORPTION (a glide whose swept path passes over the // destination completes it in passing, so it relaxes at that move's cost alone): fast = 1 // per glide; safe = terrain-weighted per swept cell (deep 24 / verge 8 / walkway-or-grass 1 // — the walk _parkFields weights verbatim). Value iteration (glide edges are not monotone // under a queue relax); keyed on destination + the companion cell (a glide brakes at the // companion, so the graph is stale once it moves) — same P._fields slot discipline as the // walk fields (never co-resident: every field consumer on a slide board dispatches here). function _parkSlideFields(P) { const dest = _parkDestCell(P); if (!dest) return null; const st = P.st, n = st.N, dk = _parkKey(st, dest); const coKey = _parkKey(st, st.pos[1]); if (P._fields && P._fields.slide && P._fields.dk === dk && P._fields.rk === coKey) return P._fields; const park = st.park; const cellW = (kk) => park.deep.has(kk) ? 24 : (park.verge.has(kk) ? 8 : 1); const mk = (weighted) => { const dist = new Array(n * n).fill(Infinity); dist[dk] = 0; for (let it = 0; it < n * n; it++) { let changed = false; for (let kk = 0; kk < n * n; kk++) { if (st.wall.has(kk)) continue; const u = { x: kk % n, y: (kk / n) | 0 }; for (const m of _PARK_MOVES) { if (m.k === 'stay') continue; const cells = _parkSlidePath(st, u, m, coKey); if (!cells.length) continue; let c = weighted ? 0 : 1, crossed = false; for (const cc of cells) { if (weighted) c += cellW(cc.key); if (cc.key === dk) { crossed = true; break; } } const base = crossed ? 0 : dist[cells[cells.length - 1].key]; if (base + c < dist[kk]) { dist[kk] = base + c; changed = true; } } } if (!changed) break; } return dist; }; P._fields = { slide: true, dk, rk: coKey, fast: mk(false), safe: mk(true) }; return P._fields; } // _parkSlideScan(st): the escapability audit (measured design law 4) — BFS the REST-cell set // reachable in the slide graph (companion held at its spawn: the audit graph; the live // runtime re-reads legality per state) and count rest cells with NO compliant continuation // (no glide-or-stay whose whole path keeps distDeep >= cautionD) and STRANDED rest cells // (no glide at all — a stay-only trap). Both must be 0 on every admitted board. function _parkSlideScan(st) { const n = st.N, park = st.park, d = park.cautionD || 2, dd = park.distDeep; const coKey = _parkKey(st, st.pos[1]); const start = _parkKey(st, st.pos[0]); const seen = new Set([start]); const q = [start]; let noCompliant = 0, stranded = 0, total = 0; const bad = []; for (let h = 0; h < q.length; h++) { const kk = q[h], u = { x: kk % n, y: (kk / n) | 0 }; total++; let anyMove = false, anyCompliant = dd[kk] >= d; // compliant 'stay' for (const m of _PARK_MOVES) { if (m.k === 'stay') continue; const cells = _parkSlidePath(st, u, m, coKey); if (!cells.length) continue; anyMove = true; let ok = true; for (const cc of cells) if (dd[cc.key] < d) { ok = false; break; } if (ok) anyCompliant = true; const rk = cells[cells.length - 1].key; if (!seen.has(rk)) { seen.add(rk); q.push(rk); } } if (!anyMove) stranded++; if (!anyCompliant) { noCompliant++; if (bad.length < 5) bad.push(`(${u.x},${u.y})`); } } return { total, stranded, noCompliant, bad }; } // _parkSlideEscapable(st): the module's escapability guard (phase-module discipline). function _parkSlideEscapable(st) { const s = _parkSlideScan(st); return s.stranded === 0 && s.noCompliant === 0; } // _parkSlideSignature(playouts): the slide verb's OWN visible signature, aligned with // PARK_PERSONAS: GOAL-top glides through the moat (>= 1 costly deep entry), SAFETY-top rides // the junction-braked walkway detour (0 entries). Filter-level, like the phase/PUSH // signatures: it gates which candidate survives the sweep, never the measured spread. function _parkSlideSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < playouts.length; i++) { if (top[i] === 'goal' && playouts[i].deepEntries < 1) return false; if (top[i] === 'safety' && playouts[i].deepEntries !== 0) return false; } return true; } // _parkSlideCell(seed): the module's PUBLIC play-cell shape — m1 harvest+static on the court // arch, meadow tone (measured design laws 2/3), moveMech 'slide'. A pure value constructor; // no persona parameter exists (C1). function _parkSlideCell(seed) { return { goalVariant: 'harvest', arch: PARK_SLIDE_ARCH, hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', moveMech: 'slide' } }; } // _parkSlideBuild(cell): the module-aware build path — candidate-0 _parkTaskBuild (the // phase-module convention; the admissibility sweep below walks SEEDS, not candidates, // mirroring the measured crossing-filter procedure stride-for-stride). function _parkSlideBuild(cell) { return _parkTaskBuild(PARK_SLIDE_KIND, cell, 0); } // _parkSlideAdmissible(cell): the generate-then-filter gate over the FULL persona SET (a // constant — the accepted board stays a pure function of the public cell): the escapability // guard holds, every persona's faithful playout COMPLETES, the verb signature separates, and // the posed G-C pair is EXPRESSED (> 0 discriminating awards) and blind-recovered in the // demonstrated direction (widened read) on every persona's own faithful path. FRESH build // per consumer (probe pitfall 1 — replays mutate the board). Reject reasons are tallied // LOUDLY on _PARK_SLIDE_WHYS (scout B instrumented 40 escape / 44 unexpressed / 10 complete // over the crossing sweeps — the same classes must stay visible here). const _PARK_SLIDE_WHYS = { escape: 0, complete: 0, unexpressed: 0, norec: 0, sig: 0 }; function _parkSlideAdmissible(cell) { if (!_parkSlideEscapable(_parkSlideBuild(cell))) { _PARK_SLIDE_WHYS.escape++; return false; } const playouts = []; for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkSlideBuild(cell), persona); if (P.reason !== 'complete') { _PARK_SLIDE_WHYS.complete++; return false; } playouts.push(P); } if (!_parkSlideSignature(playouts)) { _PARK_SLIDE_WHYS.sig++; return false; } for (let i = 0; i < PARK_PERSONAS.length; i++) { const expect = _parkPushPairDir(PARK_PERSONAS[i], PARK_SLIDE_PAIR); // att-key pair direction (module-agnostic helper) if (!(parkPairExpressed(_parkSlideBuild(cell), playouts[i].moves, PARK_SLIDE_PAIR) > 0)) { _PARK_SLIDE_WHYS.unexpressed++; return false; } if (!parkRecoverPairLex(_parkSlideBuild(cell), playouts[i].moves, PARK_SLIDE_PAIR, { expect }).recovered) { _PARK_SLIDE_WHYS.norec++; return false; } } return true; } // makeParkSlideTask(cell): the public slide task generator — sweeps SEEDS base + t*7907 // (t < 128: the measured crossing-filter stride/budget, seed-walk in place of candidate-walk // because the module build is candidate-0 by convention), caches the accepted offset per // base seed, returns a FRESH board each call (replay-safe). No admissible seed in budget -> // offset 0 LOUDLY via parkSlideGenFallbacks() (the gates prove it stays 0). const _PARK_SLIDE_CACHE = {}; let _PARK_SLIDE_FALLBACKS = 0; const _PARK_SLIDE_STRIDE = 7907, _PARK_SLIDE_SWEEP = 128; function makeParkSlideTask(cell) { const base = ((cell && cell.seed) | 0) >>> 0; if (_PARK_SLIDE_CACHE[base] == null) { let found = -1; for (let t = 0; t < _PARK_SLIDE_SWEEP; t++) if (_parkSlideAdmissible(_parkSlideCell((base + t * _PARK_SLIDE_STRIDE) >>> 0))) { found = t; break; } if (found < 0) { found = 0; _PARK_SLIDE_FALLBACKS++; } _PARK_SLIDE_CACHE[base] = found; } return _parkSlideBuild(_parkSlideCell((base + _PARK_SLIDE_CACHE[base] * _PARK_SLIDE_STRIDE) >>> 0)); } function parkSlideGenFallbacks() { return _PARK_SLIDE_FALLBACKS; } function parkSlideWhys() { return { ..._PARK_SLIDE_WHYS }; } /* ================== PARK FIELD MODULES — REGISTRY CLIENTS BELOW THIS LINE ================== THE SEAM'S SENTINEL. Everything ABOVE is the shared engine body; everything BELOW is mechanic territory. A field mechanic's ENTIRE footprint in engine.js is one self-contained block down here, ending in its PARK_FIELD_MECHS registration — it never edits a line above this banner. The gate REGISTRY-SEAM-BODY slices the file exactly here and asserts the body names no mechanic: no mech id as a string literal, no `park.`, no `_park...`, and no `dyn.` member access but `.beat`. If you find yourself needing to reach above this line, the REGISTRY is missing a hook — report that, do not reach. (The sentinel is a fixed marker, not "wherever stones happens to begin": a module that lands above stones must still land below this line.) */ const PARK_FIELD_SENTINEL = 'PARK FIELD MODULES — REGISTRY CLIENTS BELOW THIS LINE'; /* ============ STONES FIELD MODULE (y12 "old stepping stones", plan 2026-07-13) ============ */ /* A SELF-CONTAINED park FIELD module in the PUSH/SLIDE mold — the movement verb and the goal grammar are the walk's, but the TERRAIN is CONSUMABLE: a stream cuts the board, three stone crossings span it, and every stone the WALKER steps off SINKS (park.dyn.gone) into water. The crossings are not equal, and that is the whole cell: CRACKED (the beeline) — two rotten stones straight up the walker's line to the goal. They ARE the park's deep field (park.deep), so entering costs the body by the SAME universal physics every park board already runs (a CERTAIN heart, never a probability — determinism). The companion never plans through deep, so this crossing is the walker's alone. COMPANION'S (fresh) — the crossing the companion's live BFS plan rides. Sound stones, no body cost — but the walker leaving them sinks them, and then the companion has no way over. FREE (fresh) — a sound crossing far off the line: no body cost, no one else needs it, and it is the long way round. THE THREE MINDS, read through the SHIPPED PARK_ATTITUDES (no new scoring channel): G goal — the fast field is uniform, so the beeline (cracked) crossing is the goal-compliant one; unchanged code (the field BFS simply stops routing over sunk stones). C safety — unchanged code: cracked stones are deep, so the caution band (distDeep >= d) puts the whole cracked approach off-limits and safety-led play detours. N care — ONE new facet on the same attitude (_parkNCtx.guard): never step onto a fresh stone the companion's CURRENT plan rides. Read off the companion's own plan, so once a crossing is spent it guards whatever the companion re-routes to. The care cost is the body and the safety cost is the friend: goal>care>safety wades the rotten stones rather than sink the companion's; safety>goal>care takes the companion's crossing and leaves it stranded; both-above-goal personas pay the long free crossing. DESIGN LAWS (each one earns its place — see the inline notes): 1. THE COMPANION IS LIGHT (parkStep sink hook): only the PLAYER's departure sinks a stone. A companion that sank them would strand ITSELF crossing to its own gem, and the care stake — "your shortcut is his only way home" — would evaporate. 2. CRACKED == DEEP (this builder): the body cost is not new physics. Reusing park.deep gives the certain heart, the caution band, the safe-field weighting, the companion's deep-blind plan and the deepEntries meter for free, and keeps the blind read-stack byte-identical. 3. WATER IS WALL: the stream is impassable terrain (st.wall + park.water for the render), so a sunk stone needs no death rule — it simply stops being a cell. 4. TRIG = FULL BOARD (park.trig): the companion's crossing INTENT must be public from the first decision, or the guard has nothing to read and the care axis is mute for the whole approach. The proximity trigger is a timing device for the walk boards; here the plan itself is the scene. 5. STONES PER CROSSING = 2 (the stream is 2 rows): from EITHER stone of a column a bank cell is one step away, so no legal walk can strand the player mid-stream — escapability by construction, no extra guard needed. */ const PARK_STONES_N = 16; // the stream (rows ys, ys+1) and the two shore promenades that flank it. const _PARK_STONES_YS = 7; // the three conflict pairs the three-way junction means to pose (the cell's whole ambition). const PARK_STONES_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // _parkStonesBuild(cell): the y12 board — pure PUBLIC geometry (reads ONLY cell.seed; C1: no // persona parameter exists), runtime-compatible with parkStart/parkStep. Frame (16x16): // perimeter ring + two shore lanes (rows ys-1 / ys+2) + three full-height connector columns // (xc cracked / xm companion's / xf free), with the leftover bank cells falling to verge/deep by // the park's own two-tone partition — so both banks carry real deep pockets (the walk board's // G-C shortcut grammar) and every connector is a ONE-WIDE corridor (the walk board's C-N verge // yield). Outside the walk archetype pools by construction (the pools stay append-only and // walk-only — the PUSH/SLIDE precedent). function _parkStonesBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_STONES_N, ys = _PARK_STONES_YS; const r = rng((seed * 977 + 3121) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const xc = cs(3, 4); // CRACKED crossing (the beeline column) const xm = xc + 4; // the COMPANION's crossing (>= 4 columns off the const xf = xc + 8; // cracked one, so its stones sit outside the const gy0 = cs(2, 3); // caution band); FREE crossing further still const gy1 = cs(3, 4); const cgy = cs(4, 5); // the companion's contract gem (north, on the walker's line) const sty = cs(11, 12); // its station (south — so its plan CROSSES the stream) const flip = r() < 0.5; // E/W mirror (the anti-mimic draw axis, PUSH precedent) const M = (x) => flip ? n - 1 - x : x; const K = (x, y) => y * n + x; const isStream = (y) => y === ys || y === ys + 1; const stoneCol = { [M(xc)]: 'cracked', [M(xm)]: 'fresh', [M(xf)]: 'fresh' }; // NORTH bank: ring + a mid lane + the shore -> every bank cell is walkway or verge (NO pocket). // Measured: with north pockets the goal-led fast field cuts BOTH of them on the g0 -> g1 leg and // the beeline persona banks 3 deep entries on a 3-heart body — it DIES on every seed. The G-C // stake this cell is built on is the CROSSING, so the field lives on the south approach only. // SOUTH bank: ring + the shore only -> rows ys+4..ys+5 fall to deep (the shortcut grammar the // walk boards run) and every connector is a one-wide corridor between two verge shoulders. const isWalk = (x, y) => x === 1 || x === n - 2 || y === 1 || y === n - 2 || y === ys - 1 || y === ys + 2 // the two shore promenades || y === 3 // the north mid lane (see above) || x === M(xc) || x === M(xm) || x === M(xf); // the connector columns const wall = new Set(), walkway = new Set(), verge = new Set(), deep = new Set(), water = new Set(); const cracked = new Set(), fresh = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = K(x, y); if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } if (isStream(y)) { const cls = stoneCol[x]; if (!cls) { wall.add(kk); water.add(kk); continue; } // law 3: the stream is impassable (cls === 'cracked' ? cracked : fresh).add(kk); if (cls === 'fresh') walkway.add(kk); // sound stones are ordinary footing continue; // cracked -> deep (law 2, below) } if (isWalk(x, y)) walkway.add(kk); } for (const kk of cracked) deep.add(kk); // law 2: the rotten stones ARE the field // THE PARK FRAME LAW — walkway ⇒ distDeep >= 2, with the verge as the buffer band. _parkTaskFrame // gets it free (deep is DEFINED as the walk-nonadjacent remainder); forcing the cracked stones // into deep breaks it, so the two shore cells at the head of the rotten crossing are DEMOTED to // verge. Not cosmetic: while they stayed walkway (cost 1) the safe field's cheapest route to the // goal ran straight through a cell the C band forbids, so every safety-led persona walked to the // shoulder of the crossing and STAYED there forever (argmin picks 'stay' when no compliant // neighbour improves) — 3/6 personas capped out on every seed. Verge (cost 8) re-prices the route // so the safe plan and the C-compliant plan are the same plan again. for (const kk of [...walkway]) { const x = kk % n, y = (kk / n) | 0; if ([K(x - 1, y), K(x + 1, y), K(x, y - 1), K(x, y + 1)].some(q => cracked.has(q))) { walkway.delete(kk); verge.add(kk); } } for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) { // the park's own two-tone partition const kk = K(x, y); if (wall.has(kk) || walkway.has(kk) || deep.has(kk) || verge.has(kk)) continue; const near = [K(x - 1, y), K(x + 1, y), K(x, y - 1), K(x, y + 1)].some(q => walkway.has(q)); (near ? verge : deep).add(kk); } const distDeep = new Array(n * n).fill(Infinity); // multi-source BFS (walls transparent — _parkTaskFrame's rule) const q = []; for (const kk of deep) { distDeep[kk] = 0; q.push(kk); } for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[kk] + 1) { distDeep[nk] = distDeep[kk] + 1; q.push(nk); } } } const tokens = [ { x: M(xc), y: gy0, v: cs(2, 3), alive: true, guard: false }, // chain 0 (north, up the beeline) { x: M(xf), y: gy1, v: cs(2, 3), alive: true, guard: false }, // chain 1 (north, east) { x: M(xc), y: cgy, v: cs(1, 2), alive: true, guard: false }, // the companion's contract gem ]; // The companion's station sits on the FAR side of the south bank, not at the foot of its own // crossing. Measured: seated at (xm, sty) it is 3 steps from the stones and — even at its half // pace — is ACROSS and away before the walker's route reaches the water, so the guard has // already expired at every decision that could sink it (0 guarded-stone scenes in 48 playouts, // and care-led play cheerfully sank the crossing). Seated at the west gate its plan is still // pending, and still riding those stones, exactly when the walker stands on the shore. const station = { x: M(1), y: sty }; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const spawn = { x: M(xc), y: n - 2 }; const park = { N: n, seed, k: 0, fieldMech: 'stones', // the REGISTRY key (Task 1): what routes every engine // dispatch point back to the bundle at the foot of // this module. The engine body knows nothing else. walkway, verge, deep, distDeep, water, stones: { fresh, cracked }, // SEED-PURE (immutable): the runtime sinks into dyn.gone clusters, chain: [0, 1], contracts: [{ gem: 2, station }], retire: { x: station.x, y: station.y }, spawn, companionSpawn: { x: station.x, y: station.y }, trig: n * 2, cap: 90, minTurns: 12, cautionD: 2, damage: 1, // law 4 (trig = the whole board); meadow tone geom: { ys, xc, xm, xf, flip }, // render-only PUBLIC cell summary (the app's terrain tint / goal badge read hazard kind + goal // grammar off it; the engine never READS it). Rebuilt from the SEED — never the caller's cell // object, which may carry stamps (personaStream) that would leak into the board bytes. cell: _parkStonesCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: station.x, y: station.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // OPT-IN runtime container (Task 0): the sunk stones live here return st; } // _parkStonesCell(seed): the module's PUBLIC play-cell shape. `mech.fieldMech: 'stones'` — the // FIELD analogue of the shipped `mech.moveMech: 'push' | 'slide'`. DECISION (plan 2026-07-13, the // open question): NO new task `kind`. The PUSH/SLIDE precedent is exact — a mechanism that brings // its OWN geometry hangs off a `mech.*` selector and its own builder, leaving the walk archetype // pools (_PARK_KIND_ARCHS/_X) append-only and walk-only. A new kind would have to earn an entry in // every kind-keyed table (_PARK_KIND_ARCHS, _PARK_KIND_GOALS, _PARK_KIND_FORMS, _parkKindPair, // _PARK_TASK_NEED, _PARK_SKEL_FAM, the transfer draw pool) for a board that shares none of the // walk generator's k-sweep — pure surface area for zero reuse. A pure value constructor; no // persona parameter exists (C1). function _parkStonesCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'stones' } }; } // _parkStonesSignature(playouts): the field's OWN visible signature, aligned with PARK_PERSONAS: // GOAL-top wades the rotten beeline (>= 1 costly deep entry), SAFETY-top detours a sound crossing // (0 entries). Filter-level, like the phase/PUSH/SLIDE signatures: it gates which candidate // survives the sweep, never the measured spread. function _parkStonesSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < playouts.length; i++) { if (top[i] === 'goal' && playouts[i].deepEntries < 1) return false; if (top[i] === 'safety' && playouts[i].deepEntries !== 0) return false; } return true; } // _parkStonesAdmissible(cell): the generate-then-filter gate over the FULL persona SET (a constant // — the accepted board stays a pure function of the public cell): every persona's faithful playout // COMPLETES alive, the field signature separates, and every pair the board actually POSES // (parkPairExpressed > 0) is blind-recovered in the demonstrated direction (widened read) on that // persona's own faithful path. FRESH build per consumer (probe pitfall 1 — replays mutate the // board, and here they also sink its stones). This is PLAYABILITY; the SHIP bar // (_parkStonesRecovers, 6/6 blind ORDER recovery) is strictly stronger and lives above it — a cell // that is admissible but not recoverable ships DEMO-ONLY (P9/P10: lower the track, never weaken the // gate). Reject reasons tallied LOUDLY on _PARK_STONES_WHYS. const _PARK_STONES_WHYS = { complete: 0, dead: 0, sig: 0, unexpressed: 0, norec: 0 }; function _parkStonesAdmissible(cell) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkStonesBuild(cell), persona); if (P.reason !== 'complete') { _PARK_STONES_WHYS.complete++; return false; } if (P.hearts < 1 || P.turns < 12) { _PARK_STONES_WHYS.dead++; return false; } playouts.push(P); } if (!_parkStonesSignature(playouts)) { _PARK_STONES_WHYS.sig++; return false; } let posed = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_STONES_PAIRS) { if (!(parkPairExpressed(_parkStonesBuild(cell), playouts[i].moves, pair) > 0)) continue; posed++; const expect = _parkPushPairDir(persona, pair); // att-key pair direction (module-agnostic helper) if (!parkRecoverPairLex(_parkStonesBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_STONES_WHYS.norec++; return false; } } } if (posed === 0) { _PARK_STONES_WHYS.unexpressed++; return false; } return true; } // _parkStonesRecovers(cell): THE SHIP GATE (P9/P10) — blind 6/6-persona ORDER recovery, built out // of the shipped recovery stack and nothing else: each persona's own faithful play must COMPLETE, // and parkRecoverOrder (trajectory + a FRESH board; no persona symbol reaches it) must reassemble // exactly the demonstrated order. True here is the ONLY licence for ship:true on a y12 picker slot. function _parkStonesRecovers(cell) { for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = parkPlayout(_parkStonesBuild(cell), PARK_PERSONAS[i]); if (P.reason !== 'complete') return false; const rec = parkRecoverOrder(_parkStonesBuild(cell), P.moves, PARK_CAL_TURNS); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) return false; } return true; } // NO MODULE GENERATOR — and the five cells that followed y12 all had to unlearn it from here. // y12 originally shipped a `makeParkStonesTask` seed-walk with its own cache, stride, sweep budget // and a LOUD `parkStonesGenFallbacks()` counter, copied from the VERB modules (push/slide), which // campaign.js really does call directly. A FIELD mechanic is reached only through the registry — // `mech.cell` / `mech.admits` for the seed sweep, `parkFieldBuild -> mech.build` for the board — so // the module-level seed-walk had no caller on any shipped path, and its fallback counter could not // increment. The gate below it asserted `parkStonesGenFallbacks() === 0`: VACUOUS, green forever, // proving nothing. (Calling the generator from the test to "fix" that is worse, not better — it // MANUFACTURES the only caller and yields a true assertion about nobody's code. Two of the five // tried it; both reverted.) The real loud counter for a field cell is `parkCrossFallbacks()`, and // it is 0 — STRUCTURALLY, not by luck (Task 8): the crossing filter sweeps ONLY slots that can be // SEATED (ship: true), and every shipped slot's sweep accepts; a demo-only slot's play leg is // unreachable (runParkCrossing returns null on !ship), so it is never swept and cannot bump the // counter. CAMP-CROSS-SWEEP-GATE (engine.test.js) asserts that INVARIANT — every shipped slot // filtered, exhaustion permitted only for a slot that stays "coming" — never the bare number. // The reject telemetry is untouched by any of this: `_PARK_STONES_WHYS` tallies inside // `_parkStonesAdmissible`, which IS the `admits` hook the campaign calls. function parkStonesWhys() { return { ..._PARK_STONES_WHYS }; } // PARK_STONES_SHIP_SEED / PARK_STONES_SHIPPABLE: the module's MEASURED ship state — the base seed // the campaign slot pins and whether it clears _parkStonesRecovers. Declared here (not inferred at // gate time) so the claim is a PIN a regression must break, not a tautology the gate re-derives. // // MEASURED (seed sweep 1..24, 6 personas each, 2026-07-13 — the numbers the campaign slot header // repeats): faithful play COMPLETES alive 144/144 and the field signature separates on EVERY seed; // the generator admits every base seed at offset 0 with 0 fallbacks. The six personas SPLIT the way // the cell is drawn to make them split — goal-led wade the rotten crossing (~t22, one heart); // care>goal>safety pays the SAME heart rather than sink the companion's stones; safety-led take the // companion's crossing and strand it; care>safety>goal WAITS on the shore until the companion is // across, then crosses (~t46 — patience bought with foregone progress). Blind reads: // G-C expressed + widened-recovered 144/144 (6/6 per seed — the rotten-crossing stake is the // cell's spine and it reads cleanly on every persona's own faithful path) // G-N expressed + widened-recovered 144/144 (6/6 per seed — the guarded-stone scene) // C-N expressed 0/144 — NEVER POSED. // blind ORDER recovery (parkRecoverOrder, all three pairs) 0/144. // SO: PARK_STONES_SHIPPABLE = false, and the y12 picker slot ships DEMO-ONLY (P9/P10: lower the // track, never weaken the gate). THE DIAGNOSIS, so the next owner does not re-derive it: a C-vs-N // award needs a state where the C-compliant and N-compliant sets are DISJOINT. Both of this cell's // care scenes (the contested gem, the stone guard) only ever SUBTRACT a move or two from N, and the // retreat — and 'stay' — stay compliant to BOTH, so the sets always intersect. The park's one known // C-N poser is the head-on VERGE YIELD (C forbids 'stay' inside the band while N forbids holding // the companion's next cell — genuinely disjoint), and here it fires 3 times in 48 playouts: the // companion and the walker cross the stream in the SAME direction, so they almost never meet nose // to nose. Posing C-N on this geometry means routing the companion AGAINST the walker through a // one-wide, deep-shouldered corridor — a timing problem (the companion moves at half pace and must // detour the rotten crossing, so it is always ~20 turns behind the scene), not a read-stack problem. // This is the SAME standing C-N gap the x2 crossing was retired for (campaign.js PARK_CROSSINGS // ledger: "no existing geometry poses a C-vs-N conflict scene"); y12 does not close it. const PARK_STONES_SHIP_SEED = 1; const PARK_STONES_SHIPPABLE = false; // ---- THE REGISTRATION (Task 1). Everything the engine body knows about y12 is right here: four // hooks, hung on the id the board stamps as park.fieldMech. The body never names a stone. This // bundle is the template the five parallel cells copy — a mechanic that needs a hook this one // leaves empty (an entry cost -> onEnter, a beat-driven entity -> tick, a route price -> // oracleCost) just fills it in; a mechanic that needs a hook the registry does NOT have has found // a defect in the SEAM and must report it rather than reach into the body. // // _parkStonesGuard(P): the guarded set as a NAMED read (the y12 gates + the app's "keep this // crossing" glyph consume it). Thin wrapper over the ctx the reads hook already computes — never a // second channel. The stones the companion's LIVE plan still rides: leaving one sinks it, so care // means "leave him a way over" and the guard follows him if he re-routes. function _parkStonesGuard(P) { const st = P.st, park = st.park; if (!park.stones) return new Set(); const plan = _parkCompanionPlan(P); const guard = new Set(); if (!plan || !plan.path) return guard; const gone = park.dyn ? park.dyn.gone : null; for (const kk of plan.path) if (park.stones.fresh.has(kk) && !(gone && gone.has(kk))) guard.add(kk); return guard; } PARK_FIELD_MECHS.stones = { build: _parkStonesBuild, // THE CAMPAIGN SURFACE (two hooks, so the crossing filter can SWEEP this mechanic without naming // it): `cell` mints the module's public play-cell for a swept seed, `admits` is the module's own // generate-then-filter predicate. With these, a new field cell adds exactly ONE line to // campaign.js — its PARK_CROSSINGS slot row — and none at all to the board/sweep/admission // switches (which is what makes five of them safe to build in parallel). cell: _parkStonesCell, admits: _parkStonesAdmissible, // WATER IS WALL, and a spent stone becomes water — for EVERY traveller and for the route metric // alike (design law 3). The walker's own weight is what sank it; the ground simply is not there // any more. One predicate, all three domains. legalMask: (P, key) => { const dyn = P.st.park.dyn; return !!(dyn && dyn.gone.has(key)); }, // CONSUMABLE TERRAIN (design law 1: THE COMPANION IS LIGHT). Only the PLAYER's departure spends // a stone — a companion that sank them would strand ITSELF crossing to its own gem, and the care // stake ("your shortcut is his only way home") would evaporate. 'stay' consumes nothing: standing // on a rock is not leaving it. The seed-pure park.stones is never touched; the loss is recorded // on the RUNTIME dyn.gone, so a fresh build always replays byte-identically. onLeave: (P, ev) => { const st = P.st, park = st.park; if (ev.mvKey === 'stay') return; if (!park.stones.fresh.has(ev.fromKey) && !park.stones.cracked.has(ev.fromKey)) return; park.dyn.gone.add(ev.fromKey); st.fx.push({ k: 'sink', x: ev.from.x, y: ev.from.y }); // render hook (ZERO-TEXT): it goes under }, // THE CARE FACET — one new facet on the SHIPPED care attitude, never a new scoring channel. // Care ENGAGES while the companion's crossing is still sinkable, and NARROWS care to the moves // that do not step onto a stone he is still counting on. G and C are left alone on purpose: the // goal mind is free to take the beeline (the cracked crossing, which the deep field already // prices in hearts) and the safety mind is free to take HIS crossing and strand him. That the // three minds want three different crossings is the entire cell. reads: { ctx: (P) => ({ guard: _parkStonesGuard(P) }), N: { engaged: (P, ctx) => ctx.guard.size > 0, prefer: (P, legal, ctx) => { const out = new Set(); for (const c of legal) { if (c.k !== 'stay' && ctx.guard.has(c.key)) continue; // 'stay' sinks nothing out.add(c.k); } return out; }, }, }, }; /* ============ SHOAL FIELD MODULE (y12 x y10 병합, plan 2026-07-31) — TASK 1: BUILDER ONLY ===== 여울: 징검다리(stones) 기하 위에 남쪽 기슭만 차오르는 물과 둔덕 하나를 얹는다. WHY THIS EXISTS. y12(stones)는 G-C 36/36 · G-N 36/36을 세우면서 C-N을 0/36으로 둔다. 일곱 가설을 재서 전부 기각했고(kind 4종 · 시연 동사 3종 · si 2값 · 동료 방향 · 건널목 인접 · 건널목 단일), 이유가 구조적이다: G-C의 척추는 "위험한 길 하나 + 안전한 길 하나"인데, 길이 둘이면 두 사람이 같은 칸을 필요로 할 일이 없다. 길을 하나로 줄이면 이번엔 safety-top이 우회할 곳을 잃어 _parkStonesSignature가 갈리지 않고 admission이 전 시드를 기각한다 (실측 필터 0/6). 그래서 C-N은 stones 문법 밖에서 와야 한다. plans/2026-07-31-y12-boundary.md WHERE IT COMES FROM. y10(flood)의 둔덕 자리다. 공원에서 C-N을 여는 장치는 셋뿐인데 y33 yield와 y8 log는 한 칸 폭에 기대므로(기대는 순간 위 서명이 죽는다) 쓸 수 없고, 둔덕만 폭에 안 기댄다. 그리고 y10 모듈 머리말 설계법 4가 y12를 이름으로 부른다 — "C and N are DISJOINT there — which is the one shape that can award a C-vs-N pair at all (the standing gap y12 could not close)". 병합은 새 발상이 아니라 그 저자가 남겨 둔 자리다. THE RISK, AND WHY IT IS SURVIVABLE. y10 설계법 1은 깊은 밭을 아예 없앤다 — 그래야 배송된 정적-C facet이 꺼져서 물 facet 혼자 안전을 읽는다. 그런데 stones의 척추는 하트를 쓰는 것이라 깊은 밭이 반드시 있어야 하고, 그러면 _parkReads에서 두 facet이 교집합된다. 빈 prefer()는 거부권이 아니라 마음을 INERT로 만드니, C가 두 위험이 동시에 live인 바로 그 순간 사라질 수 있다 — C-N이 필요한 그 턴에. 실측(tools/y12-mound-siting.mjs, 엔진의 _parkReads를 직접 읽어서): 둔덕 없는 지금의 stones 에서도 C는 engaged 턴의 8%(6/50~74)에서 이미 선호가 빈다. 그런데 G-C는 36/36이다. 즉 INERT 자체는 치명적이지 않고, 문제는 증가분이다. 배치에 따라 0~44턴까지 갈린다. THE SEAT. 전 시드(1..8) 최악값 기준으로 M(xc + 4..8), ys + 2 가 1턴 — 기준선 6턴보다 조용하다. 절대 좌표로는 최선이 28턴이라 실패했다(stones는 시드마다 xc/xm/xf/flip이 달라져 고정 (x,y)가 시드마다 다른 상대 위치에 떨어진다). dx+4는 곧 xm — 동료의 건널목이 서는 열이라, 물이 오르면 동료가 이미 서 있던 자리가 유일한 높은 땅이 된다. 기하가 서사와 맞는 자리다. INVARIANTS THIS BUILDER MUST NOT BREAK (전부 실측 근거): (a) 건널목은 셋 유지 — 하나로 수렴하면 admission이 전 시드를 기각한다. (b) xm = xc + 4 간격 유지 — 좁히면 G-N이 36->18로 반토막난다. (c) 물은 남쪽 기슭에만 — 북쪽에 G-C 척추(썩은 지름길 vs 성한 우회)가 산다. (d) 깊은 밭을 없애지 말 것 — 없애면 척추가 사라지고 병합할 이유가 없어진다. ============================================================================================= */ // 물이 한 링을 먹는 주기(박자). Task 5에서 admission 통과 시드 수 x C-N posed로 보정한다. const PARK_SHOAL_EVERY = 3; // 둔덕의 열 오프셋: xc 기준 동쪽으로. Task 0-b가 잰 안전 구간은 +4..+8이고 그 앞머리를 쓴다. const PARK_SHOAL_SEAT_DX = 4; const PARK_SHOAL_SEAT_DY = 2; // 시내 바로 아래 두 번째 행(남쪽 기슭의 물가) function _parkShoalCell(seed) { const c = _parkStonesCell(seed); return { ...c, mech: { ...c.mech, fieldMech: 'shoal' } }; } // _parkShoalBuild(cell): stones를 지어서 확장한다. 복사-붙여넣기가 아니라 위임인 것이 요점 — // stones 기하가 바뀌면 여울도 따라와야 하고, 두 벌을 손으로 맞추면 언젠가 어긋난다. function _parkShoalBuild(cell) { const b = _parkStonesBuild({ ...cell, mech: { ...(cell && cell.mech), fieldMech: 'stones' } }); const park = b.park, n = park.N; const { xc, ys, flip } = park.geom; const M = (x) => flip ? n - 1 - x : x; const K = (x, y) => y * n + x; const seatX = M(xc + PARK_SHOAL_SEAT_DX), seatY = ys + PARK_SHOAL_SEAT_DY; const seat = K(seatX, seatY); // 남쪽 기슭만, 바깥 행부터 잠긴다. 건널목 돌과 둔덕은 어느 링에도 들어가지 않는다 — // 불변식 (a)와 둔덕의 존재 이유가 각각 그것이다. const rings = []; for (let y = n - 2; y > ys + PARK_SHOAL_SEAT_DY; y--) { const row = []; for (let x = 1; x < n - 1; x++) { const k = K(x, y); if (k === seat) continue; if (park.stones.fresh.has(k) || park.stones.cracked.has(k)) continue; if (park.walkway.has(k) || park.verge.has(k) || park.deep.has(k)) row.push(k); } if (row.length) rings.push(row.sort((p, q) => p - q)); } park.fieldMech = 'shoal'; park.shoal = { rings, every: PARK_SHOAL_EVERY, seat, seatX, seatY }; return b; } /* ============ TOLL FIELD MODULE (y14 "the toll gate", plan 2026-07-13) ============ */ /* A SELF-CONTAINED park FIELD module, registered through PARK_FIELD_MECHS and reaching the engine through nothing else. A HEDGE cuts the board in two and there are exactly TWO ways through: THE FIELD GAP (the beeline) — a gap straight up the walker's line to the goal, and the gap IS the park's deep meadow (park.deep), so crossing costs the body by the SAME universal physics every park board already runs (a CERTAIN heart, never a probability). The companion never plans through deep, so this way is the walker's alone. THE GATE (the toll) — sound ground, one column off the line, and it takes a GEM. Entering it deducts the toll and opens it FOR THE WALKER ONLY (dyn.gateOpen.me). A SECOND, IN-PLACE payment — standing on the gate and moving into it again, which is the 'stay' — buys the COMPANION's lane (dyn.gateOpen.mate), and only then does his planner route through it. With an empty purse the gate is a WALL (a mask; the cell itself stays walkway). THE THREE-WAY JUNCTION (the whole point of the cell): G goal — don't pay. Cut the field, eat the heart, take the short way. (Unchanged code: the fast field is uniform, so the beeline gap IS the goal-compliant crossing, and the gate detour is not.) C safety — pay. Take the gate, keep the body. (Unchanged code: the gap is deep, so the caution band puts the whole field approach off-limits and the safe route metric prices the gate lane.) N care — pay the SECOND toll. It buys the walker NOTHING — it opens the companion's road, and nothing else. ONE new facet on the shipped care attitude (reads.N), never a new scoring channel. THE ONE DESIGN DECISION, stated so it is not re-litigated: THE TOLL SPENDS THE GEM GAUGE (st.score[0]) AND THE GOAL THRESHOLD IS UNCHANGED. Completion is still "the chain is spent" (P.dest >= chain.length), so paying is simply a G-PRICED MOVE — a move the goal-minded player begrudges — and not a second scoring channel. parkReduce stays the one scorer, and ♥ and personality stay separate channels: the gate never touches a heart, and care is read from the PATH (who pays the second toll), never from heart bookkeeping. DESIGN LAWS: 1. THE PURSE IS PUBLIC AND SEED-PURE. Two purse gems (v = 1) sit on the walker's own routes: one on the TRUNK every persona walks (so every persona can afford the FIRST toll), and one on the GATE LANE only (so the walker who wades the field never collects it and could never have paid the companion's toll — the care move has to be WALKED FOR). 2. THE GATE IS A MASK, NOT GEOMETRY (registry contract): the gate cell is ordinary walkway. legalMask shuts it — for the walker while his purse is short, for the companion until HIS toll is paid. A wall would be unopenable; board geometry is inviolable. 3. THE COMPANION IS STUCK, NOT FINISHED. His contracted gem is on the far side of the hedge and he refuses the deep gap, so until the second toll lands his plan is { stuck: true } and he HOLDS (seam guarantee 1) — keeping his contract, keeping his mode. That is the ONLY reason the second payment can still land twenty turns later, and it is what keeps the care read alive the whole time. trig = the whole board, so his (stuck) intent is public from turn 0. 4. THE SECOND TOLL IS AN IN-PLACE MOVE, NOT A BUTTON (no new input keys): onEnter fires on 'stay' too, and a 'stay' ON the gate with the lane still shut is the payment. */ const PARK_TOLL_N = 16; const _PARK_TOLL_HY = 7; // the hedge (rows hy, hy+1) and the two shore lanes const PARK_TOLL_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; const PARK_TOLL_PRICE = 1; // _parkTollBuild(cell): the y14 board — pure PUBLIC geometry (reads ONLY cell.seed; C1: no persona // parameter exists), runtime-compatible with parkStart/parkStep. Frame (16x16): perimeter ring + // two shore lanes (rows hy-1 / hy+2) + a north mid lane + two full-height connector columns (xd // the FIELD gap, xg the GATE lane, xg = xd + 2 so the gate sits at distDeep EXACTLY 2 — inside the // caution band, and compliant with it: the safety mind is engaged at the toll and still approves // it). The leftover south-bank cells fall to verge/deep by the park's own two-tone partition (the // walk board's shortcut grammar), and ONE north pocket is placed off the beeline at (xd+1, hy-3) — // it is what gives the north shore lane a C-FORBIDDEN shoulder, so a head-on yield there poses C // against N (the park's one known C-vs-N poser; y12's standing gap). function _parkTollBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_TOLL_N, hy = _PARK_TOLL_HY; const r = rng((seed * 1013 + 2749) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const xd = cs(3, 4); // the FIELD gap (the beeline column) const xg = xd + 2; // the GATE lane (distDeep 2 from the gap — law above) const gy0 = cs(2, 3); // chain 0: north, straight up the beeline const ex = xg + cs(3, 4); // chain 1: north-east, ON the north shore lane const cgy = cs(2, 3); // the companion's contract gem (north-EAST, past chain 1) const sty = cs(11, 12); // his station (SOUTH — so his plan CROSSES the hedge) const flip = r() < 0.5; // E/W mirror (the anti-mimic draw axis, PUSH precedent) const M = (x) => flip ? n - 1 - x : x; const K = (x, y) => y * n + x; const isHedge = (y) => y === hy || y === hy + 1; const gate = K(M(xg), hy); // the toll cell itself: the hedge row of the gate lane // THE NORTH POCKET, and WHERE it may sit. It is what gives the north shore lane a C-FORBIDDEN // shoulder (distDeep 1) beside a C-COMPLIANT lane (distDeep 2) — the shape a head-on yield needs // to pose C against N. It sits EAST of the gate lane, on the leg between chain 1 and the // companion's gem, and NOT between the gate and chain 0. MEASURED (and the reason this line is a // design law): with the pocket on the gate->goal leg, the C band walls off the goal gradient — the // safety-led persona's C-compliant set and G-compliant set go DISJOINT at the barrier, care (third) // then pulls him back toward the toll, and he LIVELOCKS at the gate mouth (90-turn cap-out on // every seed, 2026-07-13). The pocket must never stand between a mind's route and its object. const pocket = K(M(ex - 1), hy - 3); const isWalk = (x, y) => x === 1 || x === n - 2 || y === 1 || y === n - 2 || y === hy - 1 || y === hy + 2 // the two shore lanes || y === 3 // the north mid lane || x === M(xd) || x === M(xg); // the two connector columns const wall = new Set(), walkway = new Set(), verge = new Set(), deep = new Set(), hedgeSet = new Set(); const gap = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = K(x, y); if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } if (isHedge(y)) { if (x === M(xd)) { gap.add(kk); continue; } // the FIELD gap -> deep (below) if (x === M(xg)) { walkway.add(kk); continue; } // the GATE lane (the toll is its hedge row) wall.add(kk); hedgeSet.add(kk); // the hedge is impassable terrain continue; } if (isWalk(x, y)) walkway.add(kk); } for (const kk of gap) deep.add(kk); // the gap IS the field: its entry is the park's heart deep.add(pocket); // the north pocket (off the beeline) walkway.delete(pocket); verge.delete(pocket); // THE PARK FRAME LAW — walkway => distDeep >= 2, with the verge as the buffer band. A walkway cell // pushed adjacent to deep (the shore cells at the head of the gap; the mid-lane cell over the // pocket) is DEMOTED to verge, exactly as y12 demotes its crossing shoulders: while they stayed // walkway (cost 1) the SAFE field's cheapest route ran straight through a cell the C band forbids, // and every safety-led persona walked to the shoulder and STAYED there (argmin picks 'stay' when // no compliant neighbour improves). Verge (cost 8) re-prices the route so the safe plan and the // C-compliant plan are the same plan again. for (const kk of [...walkway]) { const x = kk % n, y = (kk / n) | 0; if ([K(x - 1, y), K(x + 1, y), K(x, y - 1), K(x, y + 1)].some(q => deep.has(q))) { walkway.delete(kk); verge.add(kk); } } for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) { // the park's own two-tone partition const kk = K(x, y); if (wall.has(kk) || walkway.has(kk) || deep.has(kk) || verge.has(kk)) continue; const near = [K(x - 1, y), K(x + 1, y), K(x, y - 1), K(x, y + 1)].some(q => walkway.has(q)); (near ? verge : deep).add(kk); } const distDeep = new Array(n * n).fill(Infinity); // multi-source BFS (walls transparent) const q = []; for (const kk of deep) { distDeep[kk] = 0; q.push(kk); } for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[kk] + 1) { distDeep[nk] = distDeep[kk] + 1; q.push(nk); } } } // THE PURSE (design law 1). Two gems of value 1 — the gauge is a COUNT of gems, so "one gem" is // one unit of st.score[0]. TRUNK gem: on the beeline column two steps off the spawn, so EVERY // persona picks it up and every persona can afford the FIRST toll. LANE gem: on the south shore // lane between the two crossings, so only the walker who is already ON his way to the gate // collects it — the second toll (the companion's) must be walked for. const tokens = [ { x: M(xd), y: gy0, v: cs(2, 3), alive: true, guard: false }, // chain 0 (north, the beeline) { x: M(ex), y: hy - 1, v: cs(2, 3), alive: true, guard: false }, // chain 1 (north-east, on the shore lane) // the companion's contract gem: north-EAST, past chain 1. His only road to it is the gate (he // refuses the deep gap), so until the second toll lands his plan is STUCK — and when it lands he // walks the north shore lane EAST past chain 1, then RELOCATES back west to his southern station: // a lane he shares with the walker, travelled in the opposite direction. That head-on, on a lane // whose only shoulder is the pocket's verge, is the park's one known C-vs-N poser. { x: M(ex + 2), y: cgy, v: cs(1, 2), alive: true, guard: false }, { x: M(xd), y: n - 4, v: 1, alive: true, guard: false }, // purse: the TRUNK gem { x: M(xd + 1), y: hy + 2, v: 1, alive: true, guard: false }, // purse: the GATE-LANE gem ]; const station = { x: M(xg), y: sty }; // SOUTH, under the gate lane: his road north is the gate const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const spawn = { x: M(xd), y: n - 2 }; const park = { N: n, seed, k: 0, fieldMech: 'toll', // the REGISTRY key: what routes every engine dispatch // point back to the bundle at the foot of this module. walkway, verge, deep, distDeep, hedge: hedgeSet, // SEED-PURE render geometry (the app paints the hedge) toll: { gate, price: PARK_TOLL_PRICE }, // SEED-PURE: the runtime state lives on dyn.gateOpen clusters, chain: [0, 1], contracts: [{ gem: 2, station }], retire: { x: station.x, y: station.y }, spawn, companionSpawn: { x: station.x, y: station.y }, trig: n * 2, cap: 90, minTurns: 12, cautionD: 2, damage: 1, // law 3 (trig = the whole board) geom: { hy, xd, xg, ex, flip }, cell: _parkTollCell(seed), // render-only PUBLIC summary, rebuilt from the SEED }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: station.x, y: station.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // OPT-IN runtime container (Task 0): gateOpen lives here return st; } // _parkTollCell(seed): the module's PUBLIC play-cell (the PUSH/SLIDE/y12 precedent — a mechanism // with its own geometry hangs off mech.* and its own builder; no new task kind). A pure value // constructor; no persona parameter exists (C1). function _parkTollCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'toll' } }; } // _parkTollGateDist(P): step distance from every cell to the GATE over the walker's CURRENT domain // (walls out, the mechanic's own mask honoured — an unaffordable gate really is a wall). This is // the care read's metric: "which of my moves takes me toward the toll I could pay for him". A pure // read of board + dyn (never the persona, C1). function _parkTollGateDist(P) { const st = P.st, n = st.N, gate = st.park.toll.gate; const dist = new Array(n * n).fill(Infinity); dist[gate] = 0; const q = [gate]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk)) continue; // THE CARE METRIC NEVER ROUTES THROUGH THE MEADOW (review, minor 2). Without this line the // BFS treats park.deep as ordinary ground, so "the way toward the toll" could in principle // lead a care-led walker through the field and cost him a heart — care buying the companion's // road with the walker's BODY, which is the one thing this cell must never conflate (♥ and // personality are separate channels). Never observed (the gate lane is the shorter way on // every swept seed, and the care-ranked personas take 0 deep entries), but the metric must // not depend on that accident. if (st.park.deep.has(nk)) continue; if (nk !== gate && _parkTollShut(P, nk, 'me')) continue; if (dist[nk] > dist[kk] + 1) { dist[nk] = dist[kk] + 1; q.push(nk); } } } return dist; } // _parkTollShut(P, key, who): THE MASK (design law 2). The gate is ordinary walkway; what shuts it // is this predicate, and nothing else. // 'mate' — shut until the walker has paid the companion's OWN toll. While it is shut his // contracted gem is unreachable and his plan is { stuck: true }: he HOLDS. // 'me' / 'route' — shut while the purse cannot pay the toll (the gauge is empty -> the gate is a // wall). Once he has paid, HIS lane is open for good: no second charge, ever. // The route metric gets the same answer as the legal set on purpose — a gate he // cannot pay for must carry no route, or the safe field would plan through a // door that is not there. function _parkTollShut(P, key, who) { const st = P.st, park = st.park; if (key !== park.toll.gate) return false; const dyn = park.dyn; if (who === 'mate') return !dyn.gateOpen.mate; return !dyn.gateOpen.me && st.score[0] < park.toll.price; } // _parkTollSignature(playouts): the cell's OWN visible signature, aligned with PARK_PERSONAS: // GOAL-top cuts the field (>= 1 costly deep entry — it never pays); SAFETY-top takes the gate // (0 entries — it pays, and keeps the body). Filter-level, like every other module signature. function _parkTollSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < playouts.length; i++) { if (top[i] === 'goal' && playouts[i].deepEntries < 1) return false; if (top[i] === 'safety' && playouts[i].deepEntries !== 0) return false; } return true; } // _parkTollAdmissible(cell): the generate-then-filter PLAYABILITY gate over the FULL persona SET (a // constant — the accepted board stays a pure function of the public cell): every persona's faithful // playout COMPLETES alive, the signature separates, and every pair the board actually POSES is // blind-recovered in the demonstrated direction on that persona's own faithful path. FRESH build per // consumer (a playout mutates the board it is handed — here it spends its purse and opens its gate). // The SHIP bar (_parkTollRecovers, 6/6 blind ORDER recovery) is strictly stronger and lives above // this: a cell that is admissible but not recoverable ships DEMO-ONLY (P9/P10 — lower the track, // never weaken the gate). Reject reasons tallied LOUDLY on _PARK_TOLL_WHYS. const _PARK_TOLL_WHYS = { complete: 0, dead: 0, sig: 0, unexpressed: 0, norec: 0 }; function _parkTollAdmissible(cell) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkTollBuild(cell), persona); if (P.reason !== 'complete') { _PARK_TOLL_WHYS.complete++; return false; } if (P.hearts < 1 || P.turns < 12) { _PARK_TOLL_WHYS.dead++; return false; } playouts.push(P); } if (!_parkTollSignature(playouts)) { _PARK_TOLL_WHYS.sig++; return false; } let posed = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_TOLL_PAIRS) { if (!(parkPairExpressed(_parkTollBuild(cell), playouts[i].moves, pair) > 0)) continue; posed++; const expect = _parkPushPairDir(persona, pair); // att-key pair direction (module-agnostic) if (!parkRecoverPairLex(_parkTollBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_TOLL_WHYS.norec++; return false; } } } if (posed === 0) { _PARK_TOLL_WHYS.unexpressed++; return false; } return true; } // _parkTollRecovers(cell): THE SHIP GATE (P9/P10) — blind 6/6-persona ORDER recovery, built out of // the shipped recovery stack and nothing else. True here is the ONLY licence for ship:true. function _parkTollRecovers(cell) { for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = parkPlayout(_parkTollBuild(cell), PARK_PERSONAS[i]); if (P.reason !== 'complete') return false; const rec = parkRecoverOrder(_parkTollBuild(cell), P.moves, PARK_CAL_TURNS); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) return false; } return true; } // NO MODULE GENERATOR — AND THAT IS THE POINT (review round 2, 2026-07-14). PUSH/SLIDE each ship a // makeTask seed-walk with its own LOUD fallback counter; y12 copied that shape onto the FIELD // seam (its generator was measured dead and RETIRED in 121a130 — no field module has one now), and // y14 copied it from y12. It was a // FOSSIL. push/slide are VERB modules and campaign.js calls their generators DIRECTLY; a FIELD // module is reached through the REGISTRY — parkFieldBuild -> mech.build for the board, and mech.cell // / mech.admits for the seed sweep (campaign.js _parkCrossingPlayCell). A module-level seed walk is // therefore STRUCTURALLY UNREACHABLE on the shipped path: the real sweep is the crossing filter's // and the real loud counter is parkCrossFallbacks() — 0, and 0 structurally (Task 8): only slots // that can be SEATED are swept and every one of them accepts; y14 is demo-only, so its unreachable // play leg is not swept at all (CAMP-CROSS-SWEEP-GATE). Driving the dead generator from the gate test // would not have repaired that — it would have MANUFACTURED THE ONLY CALLER, turning a vacuous // assertion into one that is true but about nobody's code. So the generator is gone. // // THE TELEMETRY STAYS, and it hangs off the predicate the campaign genuinely calls: _PARK_TOLL_WHYS // is tallied inside _parkTollAdmissible (= the `admits` hook) above, never inside any generator, so // removing the generator cost nothing. parkTollWhys() is the LOUD read, driven and asserted by // Y14-TOLL-SHIP-GATE against the real hook. ("Remove the generator" must never become "remove the // only loud counter.") function parkTollWhys() { return { ..._PARK_TOLL_WHYS }; } // PARK_TOLL_SHIP_SEED / PARK_TOLL_SHIPPABLE: the module's MEASURED ship state — declared here (not // inferred at gate time) so the claim is a PIN a regression must break, not a tautology. The measured // sweep numbers are on the campaign slot header (si 11) and in the report. const PARK_TOLL_SHIP_SEED = 1; const PARK_TOLL_SHIPPABLE = false; // MEASURED — see the slot header (blind ORDER recovery 0/144) // ---- THE REGISTRATION. Everything the engine body knows about y14 is right here: five hooks, hung // on the id the board stamps as park.fieldMech. The body never names a gate. PARK_FIELD_MECHS.toll = { build: _parkTollBuild, cell: _parkTollCell, admits: _parkTollAdmissible, // THE GATE IS A MASK (design law 2) — and the three domains are NOT symmetric, which is exactly // why the hook takes a `who`: the gate is shut to the COMPANION until his own toll is paid, and // shut to the WALKER only while his purse is short. (No legalAdd: the gate is walkway, so a mask // that lifts is enough to open it. legalAdd exists for cells the ENGINE's own rules refuse — the // deep-field taboo, the companion's own square — and the gate is neither.) legalMask: _parkTollShut, // THE TOLL. Fires after the walker's position commits (registry contract), for EVERY move key — // and the 'stay' is not an accident, it IS the second payment (design law 4: no new input keys). // walk IN with the gate shut -> one gem, gateOpen.me. His lane, and only his. // 'stay' ON the open gate -> one gem, gateOpen.mate. It buys the walker NOTHING: it opens // the companion's road, and his planner re-routes through it on // the very next read (he was STUCK, not finished — law 3). // Never a heart: the gate's price is a gem, and ♥ stays the other channel entirely. onEnter: (P, ev) => { const st = P.st, park = st.park, dyn = park.dyn, toll = park.toll; if (ev.toKey !== toll.gate) return; if (!dyn.gateOpen.me) { if (ev.mvKey === 'stay') return; // (unreachable: he cannot stand on a shut gate) if (st.score[0] < toll.price) return; // (unreachable: the mask made it a wall) st.score[0] -= toll.price; dyn.gateOpen.me = true; st.fx.push({ k: 'toll', x: ev.to.x, y: ev.to.y }); // render hook (ZERO-TEXT): the gate lifts return; } // THE SECOND TOLL — in place, on the gate, with his lane still shut and a gem still in hand. if (ev.mvKey === 'stay' && !dyn.gateOpen.mate && st.score[0] >= toll.price) { st.score[0] -= toll.price; dyn.gateOpen.mate = true; st.fx.push({ k: 'toll', x: ev.to.x, y: ev.to.y, mate: true }); } }, // THE CARE FACET — one new facet on the SHIPPED care attitude, never a new mind and never a new // scoring channel. Care ENGAGES while the companion's road is shut and his plan is STUCK (seam // guarantee 1 — read plan.stuck, not a missing plan.next, or a companion who cannot move reads as // one who has arrived), and NARROWS care to the moves that carry the walker toward the toll he // could pay for him — and, standing on the gate with a gem in hand, to the payment itself. // G and C are left alone on purpose: the goal mind is free to cut the field (the deep gap already // prices that in hearts) and the safety mind is free to buy its OWN passage and walk on. That the // three minds want three different things at the same junction is the entire cell. // NOTE (registry trap 1): an empty prefer() would make care INERT, not veto — so the fallback // branch returns EVERY legal key rather than {}. reads: { ctx: (P) => { const st = P.st, park = st.park, dyn = park.dyn; const plan = _parkCompanionPlan(P); const shut = !dyn.gateOpen.mate; const stuck = !!(plan && plan.stuck); return { gate: park.toll.gate, price: park.toll.price, purse: st.score[0], shut, stuck, onGate: _parkKey(st, st.pos[0]) === park.toll.gate, dist: (shut && stuck) ? _parkTollGateDist(P) : null, }; }, N: { engaged: (P, ctx) => ctx.shut && ctx.stuck, prefer: (P, legal, ctx) => { const out = new Set(); // ON the gate, with his lane STILL SHUT and the toll in hand: the care move IS the payment. // The `ctx.shut` guard is NOT redundant with engaged() above (review, minor 1): _parkReads // runs a mechanic's prefer() whenever the mind is engaged by EITHER source, so the shipped // care attitude engaging here (e.g. the walker standing on the gate is on the companion's // newly-planned path — `blocked`) would run this hook with the lane already bought. It would // then pin the walker to 'stay' with nothing left to buy, and — worse — the merged set // shippedN ∩ {'stay'} can be EMPTY, which makes care silently INERT at exactly the // "clear his road" moment (registry trap 1). Latent on every faithful path, fixed anyway. // TASK 8 F6 — `ctx.stuck` joins it, so the branch is guarded on the SAME expression as // engaged() above. The dispatcher ORs `engaged` across sources but INTERSECTS `prefer`, so // a facet that is COLD (this mechanic's own engagement condition false) must never narrow: // its narrowing would silently steal the other facets' moves (trap (A)). Before this, a // state with the lane shut, the companion NOT stuck, the walker on the gate with a gem and // the shipped care attitude warm would have had this cold facet narrow care to {'stay'}. // Unreachable on the faithful paths the ship gate sweeps (under a shut gate the companion // is always stuck) — which is exactly why SEAM-COLD-PREFER, which measures cold states, // stays green either way. Latent, and closed. if (ctx.shut && ctx.stuck && ctx.onGate && ctx.purse >= ctx.price) return new Set(['stay']); // otherwise: the moves that close on the toll he cannot pay for himself. const here = ctx.dist ? ctx.dist[_parkKey(P.st, P.st.pos[0])] : Infinity; if (ctx.dist && isFinite(here)) { for (const c of legal) if (c.k !== 'stay' && ctx.dist[c.key] < here) out.add(c.k); } if (out.size) return out; for (const c of legal) out.add(c.k); // nothing to say here — INERT, not a veto return out; }, }, }, }; /* ============ DOWNED FIELD MODULE (y3 "the fallen companion", plan 2026-07-13) ============ */ /* A SELF-CONTAINED park FIELD module. The terrain is an ordinary two-tone park — one big MEADOW (park.deep) ringed by a promenade — and the mechanism is the COMPANION HIMSELF: he starts COLLAPSED on a meadow cell and does not walk. The whole cell is one junction, posed once and answered continuously: N care DIVE IN. Close the distance to the fallen man and help him up. He lies in the meadow, so reaching him means WADING the field: the ordinary universal physics charges the body one heart at the bank (park.deep entry), exactly as it charges anyone else who cuts the field. C safety KEEP TO THE EDGE. The caution band (distDeep >= d) forbids the meadow outright, so a safety-led walker can NEVER assist — not because helping is banned, but because the only route to the man runs through the ground his safety mind refuses. This is the disjunction: at the meadow's lip, C's compliant set and N's do not intersect. G goal IGNORE HIM. The gems sit across the meadow, and the beeline runs straight over it. The three read out as a lexicographic combination of the SHIPPED three minds, through the reads hook and nothing else (parkReduce stays the one scorer): care>goal>safety dives, assists, then finishes the errand (the cell's showpiece) goal>* wades the field on his own business and walks past a man lying in it safety>* keeps the promenade, cannot help, and waits out the crawl THE ASSIST IS NOT A BUTTON. It is "walk into that cell" — and the engine's own rule is that the companion's cell is NEVER a walkable destination. legalMask can only SUBTRACT, so the assist is expressible ONLY through legalAdd('me') (seam review C2; this hook exists because of this cell). It is an ACTION, not a step: onEnter puts the walker straight back where he stood — you reach the fallen man, you do not stand on him — and P.path therefore records where he ACTUALLY stands. THE CEILING (why the board can never deadlock). A companion nobody helps drags himself one cell toward the bank every `crawlEvery` beats, and picks himself up when he reaches it. So a persona whose safety mind forbids the meadow is never WEDGED by a man who cannot rise: he waits, the man recovers on the beat, the care read goes quiet, and the errand resumes. The ceiling is what makes the C-top personas playable at all, and it is measured, not assumed (Y3-DOWNED-ESCAPABLE). DESIGN LAWS (each earns its place): 1. THE SEAT IS IN THE FIELD (park.downed.at is a park.deep cell). It is the whole junction: if he lay on the promenade, safety could help him at no cost and C-vs-N would never be posed. 2. THE FREEZE IS A MASK ON HIS DOMAIN ALONE (legalMask 'mate'), so his plan comes back {stuck:true} and he HOLDS — keeps his contract, keeps his mode, does not retire (seam guarantee 1). A companion who retired would take the care read cold with him. 3. HE LIMPS OUT THE WAY HE CAME (legalAdd 'mate'): a man on his feet in the middle of a meadow would be STUCK FOREVER, because the shipped planner never enters deep — from the middle of the field every neighbour is deep and his BFS cannot expand. So while he STANDS ON deep, the deep cells STRICTLY NEARER THE BANK (park.downed.out) open for him and no others: he walks out, monotonically, and the moment he is on the bank the lane shuts and he is an ordinary companion again. He can never wander back in. 4. ONE SCORER, ONE METER. No oracleCost. The brief sketched the care pull as a term on the route metric "for N-top personas" — but oracleCost is persona-BLIND by construction (C1), so a distance-to-companion term there would drag ALL SIX personas toward the man, goal-led and safety-led alike, and the junction would collapse. The pull belongs in the LEXICAL channel (reads.N), which is exactly the persona-blind machinery that makes it bind only for the personas who rank care high enough. Seam-native, and the same read the blind recovery sees. N1 — THE DEEP-ENTRY CHARGE ON THE ASSIST: DECLINED, DELIBERATELY (contract §onEnter). parkStep prices `ev.to` against park.deep BEFORE the hooks run, and it prices where the move POINTED, not where the walker ends up — so the assist (which points at a deep cell) is charged a heart and a deepEntries bump even though onEnter puts the walker straight back and he never stands there. This module UNDOES both. Three reasons, in order of weight: (a) THE METER MUST NOT LIE. deepEntries is read by the SIGNATURE and ADMISSIBILITY gates, and it means "costly entries into the hazard field", one per contiguous deep run. The walker does not enter that cell. Counting a body he never moved is evidence the gates then reason from. (b) IT WOULD FIRE INCONSISTENTLY, AND THAT IS AN ARTIFACT, NOT A DESIGN. The charge only fires on a nonDeep->deep TRANSITION. Assisting from INSIDE the meadow (the normal case: he lies deep in it) is already fromDeep, so it costs nothing; assisting from the BANK (possible once he has crawled to the lip) would cost a full heart. The SAME act of kindness would be priced 0 or 1 depending on how long he had been lying there. Nobody would design that. (c) CARE STILL COSTS THE BODY — it is simply charged where it actually happens. To reach a man lying in the meadow you must WADE IN, and the wade charges ♥-1 + deepEntries++ at the bank by the ordinary universal physics, like anyone else's shortcut. The dive costs a heart; it costs it ONCE, for the wade, not twice for a step the walker never takes. And it keeps the constitutional separation: ♥ is the body channel, care is read from the PATH (reads.N), never from heart bookkeeping. */ const PARK_DOWNED_N = 16; // the meadow block [lo..hi]^2 — the promenade ring (index 1 / n-2) and its verge shoulder (index 2 // / n-3) are what is left. THE PARK FRAME LAW (walkway => distDeep >= 2) holds by construction: a // promenade cell is two steps from the nearest meadow cell, and the shoulder between them is verge. const _PARK_DOWNED_LO = 3, _PARK_DOWNED_HI = 12; // THE SEAT COLUMN — the one geometric constant the seed does NOT get to draw, and the cell's whole // clock. On this frame it is the unique column whose nearest bank is (a) exactly 4 steps away and // (b) strictly WEST — behind the walker, away from his gems. Both halves are load-bearing: // 4 steps = 4 crawls x crawlEvery = 16 beats before he picks himself up. Long enough that a // care-led walker reaches him (~11 turns) with room to spare, short enough that the // personas who cannot help him are never wedged waiting (the CEILING). // WEST = he drags himself AWAY from the errand, so following him costs goal progress. That is // what makes care>goal readable at all (scene 2 above). Seat him one column either // side and the nearest bank flips north — he crawls ACROSS the walker's line instead of // away from it, the G-vs-N award is never posed, and all three care personas go // unrecoverable. Measured, not assumed. // Everything else about the board IS drawn from the seed (spawn, both gems, his row, his gem, the // E/W mirror — 512 distinct boards), and every draw in that box clears the 6/6 bar. const _PARK_DOWNED_DX = 6; // the three conflict pairs the junction means to pose (the cell's whole ambition — and note C-N, // the pair the park has never posed: y12's module header names it the project's standing blocker). const PARK_DOWNED_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // _parkDownedBuild(cell): the y3 board — pure PUBLIC geometry (reads ONLY cell.seed; C1: no persona // parameter exists), runtime-compatible with parkStart/parkStep. Frame (16x16): perimeter wall, // promenade ring, verge shoulder, and a 10x10 MEADOW. // // THE GEOMETRY IS A MEASURED PICK, NOT A SKETCH. It carries THREE scenes, and each one exists // because a pair of minds would otherwise never be separable on a persona's own faithful path. // (The frame was SEARCHED over the public draw space — 4480 frames, and the accepted box below is // SOLID: all 512 draws in it clear the blind 6/6-persona order-recovery bar. See the slot header.) // // SCENE 1 — THE WALK PAST HIM (poses C-vs-N). The gems lie across the meadow and FAR ALONG it, // so every persona walks the promenade east, and every persona reaches the one cell where the // man is straight in. There the three minds want three different things ON THE SAME SQUARE: // N TURN IN — the only move that closes on him is the step into the field. // C WALK ON — the caution band forbids exactly that step (the shoulder is inside it). // G INDIFFERENT — the field step and the promenade step BOTH shorten the way to the gems, // because the gems lie beyond him and across. // G's indifference is load-bearing: it leaves C and N alone to settle it, and their compliant // sets there are DISJOINT. C-vs-N is the one pair the park has never once posed (y12's header // calls it the project's standing blocker) — this cell poses it, to all six personas, on one // square. It is why the gems sit BEYOND the man and not behind him: approached from the far // side, the walker arrives walking AWAY from the gems, G's promenade move is the cell he just // left, the shipped no-greedy-backtrack rule strikes it out, G collapses onto care's own move — // and the award silently vanishes for the one persona whose whole story it is. (Measured: 5/6, // care>safety>goal missing C-N on every seed.) // // SCENE 2 — HE CRAWLS AWAY FROM YOUR ERRAND (poses G-vs-N). The seat column is FIXED at the one // cell whose nearest bank is 4 steps away and STRICTLY BEHIND the walker — so the man drags // himself WEST, back the way you came, while your gems are east. Following him therefore COSTS // goal progress, move by move, and that is the only reason care>goal is ever demonstrable here: // a dive that cost the goal mind nothing would read as goal-led play that happened to pass a // man. (Without it: care's N-vs-G award is never posed and every care persona is unrecoverable.) // It is also the CEILING (park.downed.out = 4 -> 16 beats), and the two are the same fact: the // clock that stops the board deadlocking is the same clock that makes the rescue cost something. // // SCENE 3 — THE RE-CROSSING (poses G-vs-C, for the personas who dove). The second gem is back // on the SOUTH promenade, so the errand crosses the meadow twice. The first crossing is tangled // up with the rescue (care and goal both want that step, so the pair comes out mixed); the // second happens with the man already up and the care mind quiet, and there the shortcut-vs- // detour question is asked cleanly: wade it for a heart, or keep the edge and pay in turns. // // The crossing has to be worth taking, or none of it means anything: through the meadow is ~16 // steps, around the promenade ~26. The goal mind's shortcut costs a heart and buys real progress — // the walk boards' own G-C grammar, unchanged. // The SEED DRAW and the FRAME are split: _parkDownedBuild draws the public geometry from the seed // and hands it to _parkDownedFrame, which is a pure function of that geometry. The seam is what let // the frame be SEARCHED (the layout above is a measured pick, not a guess) without a persona symbol // or a search artefact ever reaching the shipped board — the draw below is the only way in. function _parkDownedBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const r = rng((seed * 1013 + 7717) >>> 0 || 1); const cs = (a, b) => a + ((r() * (b - a + 1)) | 0); const sx = cs(3, 4); // the walker's spawn column (south promenade, west end) const dx = _PARK_DOWNED_DX; // the DOWNED seat column — FIXED, and it is the cell's const dy = cs(7, 8); // one load-bearing constant (see the note above) const gx = cs(9, 12); // gem 1 — north promenade, BEYOND him and across const g1x = cs(10, 13); // gem 2 — SOUTH promenade, back across (the re-crossing) const cgy = cs(8, 11); // the companion's contract gem (west promenade) const flip = r() < 0.5; // E/W mirror (the anti-mimic draw axis, PUSH precedent) const v0 = cs(2, 3), v1 = cs(2, 3), v2 = cs(1, 2); // gem values return _parkDownedFrame({ seed, sx, dx, dy, gx, g1x, g1y: PARK_DOWNED_N - 2, cgy, flip, v0, v1, v2 }); } // _parkDownedFrame(g): the board from an explicit PUBLIC geometry. No rng, no persona, no clock. function _parkDownedFrame(g) { const seed = g.seed >>> 0; const n = PARK_DOWNED_N, lo = _PARK_DOWNED_LO, hi = _PARK_DOWNED_HI; const { sx, dx, dy, gx, g1x, cgy, flip } = g; const g1y = g.g1y == null ? 1 : g.g1y; const M = (x) => flip ? n - 1 - x : x; const K = (x, y) => y * n + x; const inMeadow = (x, y) => x >= lo && x <= hi && y >= lo && y <= hi; const isWalk = (x, y) => x === 1 || x === n - 2 || y === 1 || y === n - 2; // THE EDGE const wall = new Set(), walkway = new Set(), verge = new Set(), deep = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = K(x, y); if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } if (inMeadow(x, y)) { deep.add(kk); continue; } // law 1: the meadow IS the deep field if (isWalk(x, y)) walkway.add(kk); else verge.add(kk); // the promenade + its shoulder } const distDeep = new Array(n * n).fill(Infinity); // multi-source BFS (walls transparent) const q = []; for (const kk of deep) { distDeep[kk] = 0; q.push(kk); } for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[kk] + 1) { distDeep[nk] = distDeep[kk] + 1; q.push(nk); } } } // park.downed.out: the SEED-PURE distance from every meadow cell to the nearest BANK cell (any // passable non-deep cell). It is the crawl's compass AND the rescued man's walk-out lane (law 3), // and it is a pure function of the geometry — no rng, no beat, no persona. const out = new Array(n * n).fill(Infinity); const q2 = []; for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) { const kk = K(x, y); if (!deep.has(kk)) { out[kk] = 0; q2.push(kk); } } for (let h = 0; h < q2.length; h++) { const kk = q2[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 1 || ny < 1 || nx >= n - 1 || ny >= n - 1) continue; const nk = ny * n + nx; if (out[nk] > out[kk] + 1) { out[nk] = out[kk] + 1; q2.push(nk); } } } const seat = { x: M(dx), y: dy }; const tokens = [ { x: M(gx), y: 1, v: g.v0, alive: true, guard: false }, // chain 0 (across the meadow) { x: M(g1x), y: g1y, v: g.v1, alive: true, guard: false }, // chain 1 { x: M(1), y: cgy, v: g.v2, alive: true, guard: false }, // the companion's contract gem — ]; // off the walker's whole route const station = { x: M(1), y: n - 2 }; // where he goes after his errand const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const spawn = { x: M(sx), y: n - 2 }; const park = { N: n, seed, k: 0, fieldMech: 'downed', // the REGISTRY key: what routes every engine dispatch // point back to the bundle at the foot of this module. walkway, verge, deep, distDeep, downed: { at: K(seat.x, seat.y), crawlEvery: 4, out }, // SEED-PURE: the runtime lives in dyn clusters, chain: [0, 1], needPairs: true, contracts: [{ gem: 2, station }], retire: { x: station.x, y: station.y }, spawn, companionSpawn: { x: seat.x, y: seat.y }, // he starts WHERE HE FELL, not at a station trig: n * 2, cap: 90, minTurns: 12, cautionD: 2, damage: 1, geom: { lo, hi, sx, gx, g1x, g1y, dx, dy, cgy, flip }, // render-only PUBLIC cell summary (the app's terrain tint / goal badge read hazard kind + goal // grammar off it; the engine never READS it). Rebuilt from the SEED — never the caller's cell // object, which may carry stamps (personaStream) that would leak into the board bytes. cell: _parkDownedCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: seat.x, y: seat.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // the RUNTIME state (Task 0's published substrate shape, plus the two reads the render and the // signature need): who is still down, how far he has dragged himself, who got him up, and the // flattened grass he left behind. Never the seed-pure park.downed. park.dyn.downed = { rescued: false, crawl: 0, by: null, trail: [] }; return st; } // _parkDownedCell(seed): the module's PUBLIC play-cell. `mech.fieldMech: 'downed'` — the FIELD // analogue of the shipped `mech.moveMech`. A pure value constructor; no persona parameter exists (C1). function _parkDownedCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'downed' } }; } // _parkDownedState(P): the module's own runtime read, as ONE named function the hooks, the gates // and the app all consume — never three copies of the same predicate. `down` = he is still lying // there; `at` = where; `d` = the walker's distance to him. function _parkDownedState(P) { const st = P.st, park = st.park; if (!park.downed || !park.dyn || !park.dyn.downed) return { down: false, at: null, d: -1 }; const down = !park.dyn.downed.rescued; const at = { x: st.pos[1].x, y: st.pos[1].y }; return { down, at, d: down ? manhattan(st.pos[0], at) : -1 }; } // _parkDownedAssist(P): the PUBLIC read the app's "reach him" cue and the y3 gates share — the move // key that steps INTO the downed companion from where the walker stands, or null. Derived from the // LEGAL SET (so it can never disagree with what parkStep will accept): the assist is the one // force-opened candidate legalAdd put there. function _parkDownedAssist(P) { const s = _parkDownedState(P); if (!s.down) return null; const coKey = _parkKey(P.st, P.st.pos[1]); const c = _parkLegal(P).find(cc => cc.add && cc.key === coKey); return c ? c.k : null; } // _parkDownedCrawlTo(P): the cell he drags himself to next — the neighbouring meadow cell STRICTLY // nearer the bank (park.downed.out), in the fixed DIRS order. Deterministic and seed-pure: the // schedule is the BEAT and the compass is the geometry, so no rng and no clock is ever read. function _parkDownedCrawlTo(P) { const st = P.st, park = st.park, n = st.N, out = park.downed.out; const here = _parkKey(st, st.pos[1]), cur = out[here]; if (!isFinite(cur) || cur <= 0) return null; // already on the bank: nothing to crawl to for (const d of DIRS) { const nx = st.pos[1].x + d.x, ny = st.pos[1].y + d.y; if (nx < 1 || ny < 1 || nx >= n - 1 || ny >= n - 1) continue; const nk = ny * n + nx; if (st.wall.has(nk)) continue; if (out[nk] !== cur - 1) continue; // strictly toward the bank, never sideways if (nk === _parkKey(st, st.pos[0])) continue; // he does not crawl through the walker return { x: nx, y: ny, key: nk }; } return null; } // _parkDownedSignature(playouts): the field's OWN visible signature, aligned with PARK_PERSONAS. // GOAL-top wades the meadow on his own business (>= 1 costly deep entry) and never stops; SAFETY-top // keeps the promenade (0 entries) and therefore CANNOT help, whatever else he feels; CARE-top DIVES // and the man is up because of him (dyn.downed.by === 'assist'). Filter-level, like the phase/PUSH/ // SLIDE signatures: it gates which candidate survives the sweep, never the measured spread. function _parkDownedSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < playouts.length; i++) { const dn = playouts[i].st.park.dyn.downed; if (top[i] === 'goal' && playouts[i].deepEntries < 1) return false; if (top[i] === 'safety' && playouts[i].deepEntries !== 0) return false; if (top[i] === 'safety' && dn.by === 'assist') return false; // he kept the edge: he cannot have if (top[i] === 'care' && dn.by !== 'assist') return false; // the showpiece: care GOES IN } return true; } // _parkDownedAdmissible(cell): the generate-then-filter gate over the FULL persona SET (a constant — // the accepted board stays a pure function of the public cell): every persona's faithful playout // COMPLETES alive, the field signature separates, and every pair the board actually POSES // (parkPairExpressed > 0) is blind-recovered in the demonstrated direction on that persona's own // faithful path. FRESH build per consumer (replays mutate the board — here they rescue the man and // move him). This is PLAYABILITY; the SHIP bar (_parkDownedRecovers, 6/6 blind ORDER recovery) is // strictly stronger and lives above it. Reject reasons tallied LOUDLY on _PARK_DOWNED_WHYS. const _PARK_DOWNED_WHYS = { complete: 0, dead: 0, sig: 0, unexpressed: 0, norec: 0, noorder: 0 }; function _parkDownedAdmissible(cell) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkDownedBuild(cell), persona); if (P.reason !== 'complete') { _PARK_DOWNED_WHYS.complete++; return false; } if (P.hearts < 1 || P.turns < 12) { _PARK_DOWNED_WHYS.dead++; return false; } playouts.push(P); } if (!_parkDownedSignature(playouts)) { _PARK_DOWNED_WHYS.sig++; return false; } let posed = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_DOWNED_PAIRS) { if (!(parkPairExpressed(_parkDownedBuild(cell), playouts[i].moves, pair) > 0)) continue; posed++; const expect = _parkPushPairDir(persona, pair); // att-key pair direction (module-agnostic helper) if (!parkRecoverPairLex(_parkDownedBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_DOWNED_WHYS.norec++; return false; } } } if (posed === 0) { _PARK_DOWNED_WHYS.unexpressed++; return false; } // THE SHIP BAR IS PART OF ADMISSION, because y3 SHIPS. This is where y3 deliberately departs from // the y12 template it was written against. There, `admits` is playability and the ship bar sits // strictly ABOVE it — which is right for a cell that ships DEMO-ONLY: the campaign may seat a board // that plays honestly but does not blindly read, because nobody is claiming it does. // // y3 claims it does. `admits` is the predicate that actually SELECTS the board the campaign seats, // so if the ship bar is not IN it, the shipped board is chosen by a strictly weaker test than the // claim on the slot header — true today (24/24 verified), unenforced tomorrow. A cell whose licence // to be a live tile is "the order is blindly recoverable" must not be able to seat a board where it // is not. So the bar is folded in, and it reuses the playouts already computed above: 6 replays, not // a second sweep. // // FOLDING IT IN ONLY WORKS FOR A CELL THAT SHIPS, and y8 was the cautionary tale (Task 10): it // folded the bar in too, then LOST the bar under calibration — and a folded bar on a cell that // fails it makes EVERY board inadmissible, which kills the tile. A cell that stops shipping must // UNFOLD; y8 did (preview 2026-07-14) and RE-FOLDED once the corridor rebuild earned the bar back // (promoted 2026-07-23 — see _parkLogAdmissible). y3 has always shipped, so y3 has always folded. // // AND IT READS AT PARK_CAL_TURNS (Task 10) — the same calibration span the readout scores at. // The bar and the folded copy must be the SAME predicate; a folded bar measured at a laxer skip // than the named one would re-open the split this task closed, from the other side. // (Measured under calibration: still 144/144 across all 24 swept seeds, so this costs the sweep // nothing and moves no seated cell.) for (let i = 0; i < PARK_PERSONAS.length; i++) { const rec = parkRecoverOrder(_parkDownedBuild(cell), playouts[i].moves, PARK_CAL_TURNS); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) { _PARK_DOWNED_WHYS.noorder++; return false; } } return true; } // _parkDownedRecovers(cell): THE SHIP GATE (P9/P10) — blind 6/6-persona ORDER recovery, out of the // shipped recovery stack and nothing else: each persona's own faithful play must COMPLETE, and // parkRecoverOrder (trajectory + a FRESH board; no persona symbol reaches it) must reassemble // exactly the demonstrated order — UNDER CALIBRATION (Task 10: skip = PARK_CAL_TURNS, the same // span the readout throws away; a bar that reads evidence the product discards measures a game // nobody plays). True here is the ONLY licence for ship:true on a y3 picker slot. // Kept as its own NAMED predicate (rather than collapsing into _parkDownedAdmissible, which now // implies it) because it is the thing the slot header CLAIMS, and a claim should have a name a // regression can break. // y3 is the cell that SURVIVED the calibration audit: 144/144 uncalibrated, 144/144 calibrated — // its evidence is spread across the whole walk (the man lies mid-meadow, so the decisive turn-in // is many moves deep), not concentrated in the opening move the way y8's was. function _parkDownedRecovers(cell) { for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = parkPlayout(_parkDownedBuild(cell), PARK_PERSONAS[i]); if (P.reason !== 'complete') return false; const rec = parkRecoverOrder(_parkDownedBuild(cell), P.moves, PARK_CAL_TURNS); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) return false; } return true; } // NO MODULE GENERATOR, and that is deliberate. y12 ships a makeParkStonesTask (seed sweep + cache + // a LOUD fallback counter) and y3 was written against that template — but nothing ever calls it: the // campaign reaches a FIELD mechanic through parkFieldBuild -> mech.build, and sweeps seeds through // mech.cell / mech.admits. A module generator here would be dead code, and the "generator fallbacks // stay 0" assertion that guards it would be VACUOUS — a counter that cannot increment, standing in a // test as if it were evidence. The seed sweep that matters is the crossing filter's, and the LOUD // counter that matters is parkCrossFallbacks (campaign.js), which is real and is 0 — STRUCTURALLY // (Task 8): only SEATABLE slots (ship: true) are swept and every one of them accepts; a demo-only // slot, whose play leg is unreachable, is not swept at all. y3 SHIPS, so it is swept, and its // acceptance is asserted per run seed by CAMP-CROSS-SWEEP-GATE in engine.test.js. // The reject tallies below ARE live: _parkDownedAdmissible is what the campaign's sweep calls. function parkDownedWhys() { return { ..._PARK_DOWNED_WHYS }; } // PARK_DOWNED_SHIP_SEED / PARK_DOWNED_SHIPPABLE: the module's MEASURED ship state — the base seed // the campaign slot pins and whether it clears _parkDownedRecovers. Declared here (not inferred at // gate time) so the claim is a PIN a regression must break, not a tautology the gate re-derives. // // MEASURED (seed sweep 11..34, 6 personas each = 144 faithful playouts, 2026-07-13 — the numbers the // campaign slot header repeats; the gate is Y3-DOWNED-SHIP-GATE in engine.test.js): // faithful play COMPLETES alive 144/144 (0 deaths, 0 cap-outs; fallbacks 0) // field signature separates 24/24 seeds // G-C expressed + widened-recovered 144/144 // G-N expressed + widened-recovered 144/144 // C-N expressed + widened-recovered 144/144 // mis-read (wrong direction) 0 // blind ORDER recovery (parkRecoverOrder) 144/144 // seeds with 6/6 blind order recovery 24/24 // SO: PARK_DOWNED_SHIPPABLE = true — y3 clears the P9/P10 ship bar on every seed swept, and the slot // ships LIVE. The six personas split exactly as the cell is drawn to make them split: goal-led wade // the meadow on their own errand and walk straight past a man lying in it; safety-led keep the // promenade the whole way, never enter the field, and so CANNOT help him whatever else they feel; // care-led turn in at his square, wade, and get him up — and then finish the errand a heart down. // // AND IT CLOSES THE C-N GAP. C-vs-N is the pair the park has never once posed — the standing blocker // recorded in the x2 ledger (campaign.js PARK_CROSSINGS) and re-confirmed by y12 (0/144 expressed, // which is exactly why y12 ships demo-only; its module header carries the diagnosis). It was never a // read-stack problem: a C-N award needs a state where the C-compliant and N-compliant sets are // DISJOINT, and every care scene the park had — the contested gem, y12's guarded stone — only ever // SUBTRACTS a move or two from N, so 'stay' and the retreat stay compliant to both and the sets // always intersect. THE DISJUNCTION HAS TO BE GEOMETRIC: put the object of care where safety is // forbidden to go. A man face-down in the hazard field does exactly that, and the walk past him is // the square where it bites — care's only closing move is the one step the caution band strikes out. // 144/144 expressed. Whoever owns y12 next should re-check whether that cell can now be re-gated // live: nothing in y12 needs to change, it just needed a C-N scene to exist somewhere in the park. const PARK_DOWNED_SHIP_SEED = 11; const PARK_DOWNED_SHIPPABLE = true; // ---- THE REGISTRATION. Everything the engine body knows about y3 is right here: seven hooks, hung // on the id the board stamps as park.fieldMech. The body never names a fallen man. PARK_FIELD_MECHS.downed = { build: _parkDownedBuild, cell: _parkDownedCell, admits: _parkDownedAdmissible, // THE FREEZE (design law 2), and it is his domain ALONE — 'me' and 'route' are untouched, because // a man lying in the grass is not a wall: the walker may cross that meadow, and his route metric // may price it. Every cell is impassable FOR HIM while he is down, so his plan comes back // {stuck:true} and _parkCompanionStep HOLDS him: he keeps his contract, keeps his mode, does not // retire (seam guarantee 1 — the one that makes this cell buildable at all). legalMask: (P, key, who) => { if (who !== 'mate') return false; const dyn = P.st.park.dyn; return !!(dyn && dyn.downed && !dyn.downed.rescued); }, // THE TWO ADDITIVE LANES — legalMask can only subtract, and both of these must OPEN a cell the // engine's own rules refuse. // 'me' THE ASSIST. The companion's cell has never been a walkable destination; this is the // only way to say "walk into the fallen man". It is why legalAdd exists. // 'mate' HIS WALK-OUT (design law 3). The shipped planner never enters deep, so a man standing // in the middle of a meadow could never leave it. While he STANDS ON deep, the meadow // cells NO FURTHER FROM THE BANK than the one he is on open for him — he may go outward // or sideways, never deeper. So he limps out; the lane tightens behind him as he goes // (the test is against the cell he is CURRENTLY on); and the moment he reaches the bank // (out = 0) it shuts for good and he never re-enters the field. // // BOTH halves of that were bought with a bug. (i) A first cut opened only the single next // ring (`out[key] === here - 1`): his planner could take one step and then had nowhere // legal to go, so his BFS found no route out at all and he sat in the grass, on his feet, // forever. (ii) The fix to a STRICT descent (`< here`) left him exactly one way out — and // the walker was standing on it, because the walker had just walked in from the bank to // reach him. He waited two beats, re-planned around the blocker, found nothing, and held // there indefinitely: PINNED BY THE MAN WHO HAD JUST PICKED HIM UP. Hence `<=`: sideways // is allowed, so he can step around his rescuer. He still cannot go deeper, and his plan // is a shortest path, so the lateral room is only ever used to get past an obstacle. // Both were caught by Y3-DOWNED-ASSIST, which is why that test drives him for a few beats // after the rescue instead of stopping at `rescued === true`: a rescue that does not put // the man back on his feet and on his way is not a rescue, it is a flag. legalAdd: (P, key, who) => { const st = P.st, park = st.park, dyn = park.dyn; if (!dyn || !dyn.downed) return false; if (who === 'me') return !dyn.downed.rescued && key === _parkKey(st, st.pos[1]); if (who !== 'mate' || !dyn.downed.rescued) return false; const here = park.downed.out[_parkKey(st, st.pos[1])]; return here > 0 && isFinite(here) && park.downed.out[key] <= here; }, // THE ASSIST ITSELF — one turn, and an ACTION rather than a step. onEnter: (P, ev) => { const st = P.st, park = st.park, dyn = park.dyn; if (!dyn.downed || dyn.downed.rescued) return; if (ev.toKey !== _parkKey(st, st.pos[1])) return; // not the assist: an ordinary step // N1 — DECLINE THE DEEP-ENTRY CHARGE (see the module header for the full reasoning). parkStep // priced ev.to (a meadow cell) before this hook ran, but the walker never enters it: onEnter is // about to put him straight back. The wade that brought him within reach was charged at the bank // like anyone else's; this would be a SECOND charge for a step he does not take, and deepEntries // is a meter the signature and admissibility gates reason from. So: undo the heart, undo the // meter, and drop the entry flash the render would otherwise show. // // OBSERVE THE CHARGE, DO NOT RE-DERIVE IT. The refund keys off the `deep` fx parkStep ACTUALLY // pushed at ev.to — not off a re-computation of parkStep's charge condition. Re-deriving it // (`!deep.has(fromKey) && deep.has(toKey)`) is correct only so long as a y3 board is always // static-form: parkStep skips the charge on a PHASE board while the clock is green, and skips it // for a RELATIONAL form entirely (there the taboo, not the terrain, is what bites) — and a refund // for a debit that never happened is a FREE HEART. The fx is the debit's own receipt: it is pushed // by every arm that charges (static, phase-red, relational) and by no arm that does not, so keying // on it is correct in all of them, present and future. It is the LAST fx of the step by // construction (parkStep pushes gem/pad fx before the charge, and the companion's own cell can // hold neither), and no earlier step can have left one there: the walker has never stood on the // cell the companion is lying in. Refund exactly what was debited — the same `damage` expression. const rcpt = st.fx[st.fx.length - 1]; if (rcpt && rcpt.k === 'deep' && rcpt.x === ev.to.x && rcpt.y === ev.to.y) { P.hearts += park.damage == null ? 1 : park.damage; P.deepEntries--; st.fx.pop(); } st.pos[0] = { x: ev.from.x, y: ev.from.y }; // you reach him; you do not stand on him dyn.downed.rescued = true; dyn.downed.by = 'assist'; st.fx.push({ k: 'assist', x: st.pos[1].x, y: st.pos[1].y }); // render hook (ZERO-TEXT): he is up }, // THE CEILING (the crawl). Fires on 'stay' too — a man dragging himself out of a field does not // stop because the walker stood still. On the beat he moves one cell toward the bank; when he // REACHES the bank he picks himself up, and the board can never be wedged by a companion who // cannot rise. `by: 'self'` is what the signature reads to tell "you helped him" apart from "he // managed without you". tick: (P) => { const st = P.st, park = st.park, dyn = park.dyn; if (!dyn.downed || dyn.downed.rescued) return; if (dyn.beat % park.downed.crawlEvery !== 0) return; const to = _parkDownedCrawlTo(P); if (!to) return; // the walker is standing in his way: // he tries again on the NEXT beat of the // schedule (crawlEvery later, not next turn) dyn.downed.trail.push(_parkKey(st, st.pos[1])); // the flattened grass he leaves st.facing[1] = { dx: Math.sign(to.x - st.pos[1].x), dy: Math.sign(to.y - st.pos[1].y) }; st.pos[1] = { x: to.x, y: to.y }; dyn.downed.crawl++; st.fx.push({ k: 'crawl', x: to.x, y: to.y }); // render hook (ZERO-TEXT) if (park.downed.out[to.key] === 0) { // he made the bank: he stands dyn.downed.rescued = true; dyn.downed.by = 'self'; st.fx.push({ k: 'rise', x: to.x, y: to.y }); } }, // THE CARE FACET — one new facet on the SHIPPED care attitude, never a new scoring channel. Care // ENGAGES on a man lying in the field (a mind the walk board leaves cold out here: there is no // contested gem and nobody is blocking anybody) and NARROWS care to the moves that CLOSE THE // DISTANCE to him — the assist itself among them (distance 1 -> 0). G and C are left alone on // purpose: the goal mind is free to wade the meadow on its own errand and walk straight past him, // and the safety mind is free to keep the promenade and never reach him at all. That the three // minds want three different things ON THE SAME CELL is the entire junction. // // NOTE THE FALLBACK, and note that it is NOT a veto (contract trap 1): where no legal move closes // the distance, this returns EVERY legal move — the intersection is then a no-op and the SHIPPED // care facets (the contested gem, the verge yield) survive untouched. Returning {} would have made // the whole care mind INERT and silently thrown those away. reads: { ctx: (P) => _parkDownedState(P), N: { engaged: (P, ctx) => ctx.down, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx.down) { for (const c of legal) out.add(c.k); return out; } for (const c of legal) if (manhattan(c, ctx.at) < ctx.d) out.add(c.k); if (!out.size) for (const c of legal) out.add(c.k); // nothing to say — inert, not a veto return out; }, }, }, }; /* ============ LOG FIELD MODULE (y8 "the rolling log", Task 4, plan 2026-07-13) ============ */ /* A SELF-CONTAINED park FIELD module, registered through PARK_FIELD_MECHS — the engine body is not touched by a line of it. Where y12's terrain is CONSUMABLE, y8's world has a CLOCK: a log rolls down a lane every `period` beats, whether or not the walker moves. It is the seam's first `tick` client, and the reason `tick` exists at all. THE BOARD (one hedge, one lane, one gap): A HEDGE (wall) cuts the park in two at row `yh`, with a SINGLE GAP at the foot of the lane. The companion's contracted gem lies SOUTH of the hedge; his station and everything the WALKER wants lie NORTH of it. So the gap is the companion's only way home, and it is the LAST CELL of the lane the log rolls down. A log that reaches it wedges his only corridor for good. THE PHYSICS (tick, post-beat): Every `period` beats (phase-staggered by _parkSchedule off the PUBLIC seed) the log advances one cell down its lane. If the WALKER stands on the cell it rolls into, he takes ♥−1, the log is STUNNED (it does not advance, and the stun swallows its next roll too) and the block is tallied on dyn.blocks. That is BODY-BLOCKING: there is no block button and no new input key — you block a rolling log by standing in its way, which is the only reason it costs a body. If the COMPANION stands on the target the log simply WAITS (he is light, and he steps clear — there is no heart channel for him: the harm the log does him is that it takes his corridor). THE THREE MINDS, through the SHIPPED PARK_ATTITUDES (no new scoring channel — parkReduce is the one scorer, and these are facets folded in through the registry's `reads` hook): G goal — UNTOUCHED. The log is not a wall for the walker and carries no route cost, so the goal mind never sees it: it takes the shortest path to its gems, straight across the deep band (the shipped G-C stake: one heart for the shortcut). C safety — FORBIDS THE CELL THE ROLL IS ABOUT TO LAND ON. Engaged only when the roll lands on THIS step and the walker is on it or beside it (single-state legible: the roll schedule is public and `beat` is the clock). Never "avoid the lane" in general — the lane is only dangerous where the log is about to be. N care — THE BLOCKING FORMATION. While the companion has not yet crossed the hedge and the log can still reach the gap, care prescribes the moves that CLOSE ON the log's next cell — and 'stay' once the walker is standing in its way (hold the line). It is a PATH, not a payment: the care read comes from where the walker WALKS, never from the heart the block costs him. ♥ and personality are separate channels, and this is the cell's crux — a read that counted hearts would be reading the receipt, not the decision. On the roll step, with the walker on or beside the block cell, C's set and N's set are DISJOINT (C: anything but that cell — N: that cell, or hold it). That is a genuine C-vs-N conflict scene, which is exactly what the park has never had (the standing C-N gap that retired the x2 crossing and left y12 demo-only). DESIGN LAWS: 1. THE LOG IS A WALL FOR HIM, NOT FOR YOU (legalMask 'mate' only). The companion's planner detours the log's current cell; the walker may walk right into its path. That asymmetry IS the mechanic — it is why legalMask takes a `who` — and it is what makes the body-block a move the walker can choose and the companion cannot. 2. THE ROUTE METRIC NEVER SEES THE LOG ('route' is unmasked). A mechanic may narrow the route metric; this one must not, or the goal mind would start detouring a hazard it is defined not to care about, and G-N would collapse into agreement. 3. CARE THAT KILLS YOU IS NOT CARE (PARK_LOG_HEART_FLOOR). Care disengages once the block would take the walker below the floor, so no persona can body-block itself to death and the cell stays escapable for all six. The floor is a public read of P.hearts — never a persona. 4. THE LANE IS OPEN GROUND. Every lane cell has a lateral step off it (gate: Y8-LOG-C1), so a walker in the log's path can always refuse the hit. The seam guarantees the legal set is never empty; only the geometry can guarantee it is SURVIVABLE. 5. THE HARM IS THE FRIEND'S, NOT THE WALKER'S. A wedged companion is `plan.stuck` — he HOLDS, keeps his contract, and never retires (the seam's guarantee 1). The care signature is his fate (did he get his gem?), never the walker's heart count. SHIP STATE — A MARKED PREVIEW, NOT A LIVE TILE (Task 10, 2026-07-14). y8 shipped `ship: true` for one day on a 6/6 blind order recovery that was measured WITHOUT the calibration span the readout itself applies (P3a §4: the first PARK_CAL_TURNS=2 player turns are excluded from every blind read). Calibrated, it recovers 0/144 — on every seed. The cause is structural and it is right there in THE BOARD above: the walker spawns beside the lane, so his commitment to the log is his FIRST MOVE, and the calibration span eats the awards that carry it. The mechanism is not broken (the C-N body-block still poses disjointly, 144/144); it fires one move too early to be allowed to count. PARK_LOG_SHIPPABLE carries the full measurement, the six-persona breakdown, and the fix (move the spawn off the lane). Do not re-flip the flag without re-measuring at PARK_CAL_TURNS. */ const PARK_LOG_N = 16; const PARK_LOG_PERIOD = 3; // beats per roll (the brief's period) const PARK_LOG_HEART_FLOOR = 2; // law 3: care never spends the walker below this const PARK_LOG_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; const _PARK_LOG_YH = 10; // the hedge row (the gap sits at the foot of the lane) // PARK_LOG_CORRIDOR (k): the forced NEUTRAL entry corridor — k wall-wrapped steps the walker takes // before his first real choice about the log, with the companion crossing and the log roll DELAYED // by the same k so the discriminating C-N body-block state reproduces at turn 1+k instead of turn 1 // (redesign 2026-07-18; the 3-parameter spawn nudge was DISPROVEN, FINDING 2026-07-18). This is the // board-LAYOUT axis the redesign called for, not a spawn offset. const PARK_LOG_CORRIDOR = 2; // _parkLogBuild(cell): the y8 board — a pure function of the PUBLIC cell (reads ONLY cell.seed; // C1: no persona parameter exists). Frame (16x16): perimeter ring; a hedge across row yh with ONE // gap at (xl, yh); a north half laid out as a RING (rows 2/9, cols 2/13) cut by the LANE column. // The leftover interior falls to verge/deep by the park's own two-tone partition, which leaves a // deep POCKET at rows 4..7 on each side of the lane — the walk board's G-C shortcut grammar — and // gives the frame law (walkway => distDeep >= 2) for free. // // THE RACE IS THE GEOMETRY. Every constant below is pinned by inequalities, and each one was // MEASURED into place (the per-persona pair table in the report records what each one bought): // // (1) THE RACE. The log must reach the gap BEFORE the companion (or there is no harm to care // about) and ONE body-block must be enough to reverse that (or care is unaffordable and the // cell is a trick). With A0 = the beat the log takes the gap unblocked, a block costing it two // rolls (the stun swallows the next one), and the companion — at his shipped half pace — // stepping onto the gap at turn 2S-1: // A0 < 2S-1 < A0 + 2*period // i0 = 3 and S = 9 put 2S-1 = 17 between A0 (13..15, per the seeded phase) and A0+6 (19..21) // on every phase the schedule can draw. The companion's spawn row is DERIVED from S. // (2) THE FREE COLUMN (fx). Without it the safety-led walker's detour around the deep pocket ran // the length of the ring, and he reached the lane's shoulder only after the log had already // parked in the doorway — so care was cold for his whole trajectory and G-vs-N was NEVER posed // on it (measured: G-N expressed 0/8 seeds for both safety-led personas). A walkway column // three off the walker's own shortens that detour to ~12 turns, which lands him at the lane // while the log is still rolling. // (3) THE DIAGONAL FIRST GEM (wx+1, not wx). A goal mind with ONE compliant move cannot be broken // by a subordinate one, so a straight-down goal made C-vs-N unreadable on both goal-led // trajectories (measured: C-N expressed 0/8). Set diagonally, G is INDIFFERENT between the // descent (which N wants — it closes on the interception) and the sidestep (which C wants — // the descent is the field's edge), and the two goal-led personas SPLIT. That split is the // C-vs-N award, and it is the reason this cell can read an order the park has never read. function _parkLogBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const K_COR = PARK_LOG_CORRIDOR; // WINNER (spike 2026-07-18): k=2 recovers 48/48 CALIBRATED, seeds 1..8, // earlyAwards 0. The rrAdj/offAdj fine-tune axes did not move it — the // neutral vertical tube alone re-poses the C-N body-block at turn 3+. // THE CORRIDOR GROWS THE BOARD UP by dy = k-1 rows: the whole (proven) structure translates DOWN by // dy, and a 1-wide walled VERTICAL tube fills column wx in the new rows 1..(1+dy). The walker drops // straight down it into his old decision cell, now at row 2+dy, with ALL FOUR of that cell's play // neighbours intact (a side approach would wall one of them off and starve the pair it posed). const dy = K_COR - 1; const n = PARK_LOG_N + dy, yh = _PARK_LOG_YH + dy; const r = rng((seed * 1381 + 6151) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const xl = cs(5, 6); // the LANE column (the log rolls down it) const i0 = 3; // the log's start index on the lane — THE RACE (1) const wx = xl + 2; // the walker's column: the nearest DEEP descent (his own // G-C stake) that is still within reach of the lane const fx = wx + 3; // the FREE column (2) — walkway, so the pocket it bounds // is exactly the two columns wx / wx+1 const ex = n - 3; // the ring's east column (the tour's last leg) const flip = r() < 0.5; // E/W mirror (the anti-mimic draw axis, PUSH precedent) const M = (x) => flip ? n - 1 - x : x; const K = (x, y) => y * n + x; const XL = M(xl); // THE LANE: column xl from row 2 down to the hedge gap. lane[last] IS the gap — the companion's // only way south — so a log that runs the whole lane wedges his corridor permanently. const lane = []; for (let y = 2 + dy; y <= yh; y++) lane.push(K(XL, y)); // lane top shifts down with the ring (dy) const gapIdx = lane.length - 1; const FX = M(fx); const isWalk = (x, y) => (y < yh ? (y === 2 + dy || y === 9 + dy || x === 2 || x === n - 3 || x === XL || x === FX) // north: the ring + the lane + the free column : (y === yh ? x === XL // the hedge gap : (y === 13 + dy || x === XL))); // south: the lane stub + the promenade const wall = new Set(), walkway = new Set(), verge = new Set(), deep = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = K(x, y); if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } if (y === yh && x !== XL) { wall.add(kk); continue; } // THE HEDGE (one gap, at the lane's foot) if (isWalk(x, y)) walkway.add(kk); } // THE NEUTRAL ENTRY CORRIDOR (redesign 2026-07-18): a 1-wide wall-wrapped VERTICAL tube filling // column wx (mirror-applied) in the new rows 1..(1+dy) above the shifted ring. Every tube cell // offers exactly one forward move (straight down), so all six personas walk it identically — no // pair is posed or starved for the whole approach (the neutrality the redesign requires). The rest // of those new rows is wall, so the tube cannot leak sideways. The walker drops out of it onto his // decision cell (wx, 2+dy) with all four of ITS play neighbours intact. Companion + log delayed k. const WX = M(wx); for (let y = 1; y <= 1 + dy; y++) for (let x = 1; x <= n - 2; x++) { const kk = K(x, y); if (x === WX) { wall.delete(kk); walkway.add(kk); } else { walkway.delete(kk); wall.add(kk); } } for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) { // the park's own two-tone partition const kk = K(x, y); if (wall.has(kk) || walkway.has(kk)) continue; const near = [K(x - 1, y), K(x + 1, y), K(x, y - 1), K(x, y + 1)].some(q => walkway.has(q)); (near ? verge : deep).add(kk); } const distDeep = new Array(n * n).fill(Infinity); // multi-source BFS (walls transparent — _parkTaskFrame's rule) const q = []; for (const kk of deep) { distDeep[kk] = 0; q.push(kk); } for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[kk] + 1) { distDeep[nk] = distDeep[kk] + 1; q.push(nk); } } } // THE WALKER'S CHAIN — a one-way tour, entirely on HIS side of the lane (he never needs the lane // at all, so the interception is a real DETOUR and the goal mind is free to ignore the log): // 0 DIAGONALLY down-and-across the deep pocket (3) — the shipped G-C stake, and the state where // an indifferent goal lets C and N fight over the descent. // 1 east along the south promenade } the tour that keeps the run alive past the companion's // 2 north up the ring } crossing — a run that ended first would decide his fate // by the clock instead of by the walker's choice. const tokens = [ { x: M(wx + 1), y: 9 + dy, v: cs(2, 3), alive: true, guard: false }, // chain 0 (across the deep pocket) { x: M(fx), y: 9 + dy, v: cs(2, 3), alive: true, guard: false }, // chain 1 (east along the promenade) { x: M(ex), y: 2 + dy, v: cs(2, 3), alive: true, guard: false }, // chain 2 (north, up the ring) { x: M(xl), y: 13 + dy, v: cs(1, 2), alive: true, guard: false }, // the companion's gem — SOUTH, through the gap ]; // his station is SOUTH too: once he is through the gap he never needs it again, so the scene is a // SINGLE crossing and the care scene has a clean end (he is home, or he never gets there). // (Prose note: the word for a span of time that starts with "win" is a DOM symbol as far as the // C11 gate is concerned — it greps the whole source, comments included. Do not reintroduce it.) const station = { x: M(xl), y: 13 + dy }; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const spawn = { x: WX, y: 1 }; // at the MOUTH of the vertical corridor (row 1); // K_COR forced steps down to his decision cell (wx, 2+dy) // THE COMPANION'S SPAWN — and yes, `y: xl - 1` really is the LANE COLUMN used as a ROW. It is not a // typo and it is load-bearing; DO NOT "fix" it. Here is the algebra, so it can be checked instead // of trusted. He walks the ring: down the west column to the south promenade, east along it to the // lane, then one step south onto the gap. From a spawn row `rr` that is // S = (9 - rr) the descent to the promenade (row 9) // + (xl - 2) east along it, from the west column (x = 2) to the lane column // + 1 the step onto the gap // = 8 - rr + xl // and the race (1) needs S = 9, so rr = xl - 1. The lane column therefore sets his row, which is // what keeps his distance to the gap CONSTANT at 9 steps for either lane column the seed can draw // — and it is flip-safe, because the mirror M() moves him and the lane together. Change xl's range // and this must move with it (rr >= 2, or he spawns in the wall). // DELAYED by ~k: with the promenade now at row 9+dy, S = 8 + dy - rr + xl; to lose the race by the // same margin k turns LATER (log frozen k beats below) we need S' = 9 + k/2, i.e. rr = xl-2+(k>>1). const companionSpawn = { x: M(2), y: xl - 2 + (K_COR >> 1) }; const park = { N: n, seed, k: 0, fieldMech: 'log', // the REGISTRY key (Task 1) — the ONLY thing the engine // body knows about y8; every hook routes back through it. walkway, verge, deep, distDeep, log: { lane, period: PARK_LOG_PERIOD, // SEED-PURE (immutable): the roll STATE lives in dyn.ents off: _parkSchedule(seed, 1, PARK_LOG_PERIOD)[0], // the deterministic beat phase (Task-0 substrate) gapIdx, yh, xl: XL }, // (no i0: the START index is dyn.ents[0].i — one home for it) clusters, chain: [0, 1, 2], needPairs: true, contracts: [{ gem: 3, station }], retire: { x: station.x, y: station.y }, spawn, companionSpawn, trig: n * 2, // FULL BOARD (y12's law 4): the companion's crossing INTENT // must be public from the first decision, or care has // nothing to read for the whole approach. cap: 90, minTurns: 12, cautionD: 2, damage: 1, cell: _parkLogCell(seed), // render-only PUBLIC summary (rebuilt from the SEED, never // the caller's cell — a personaStream stamp must not reach // the board bytes). }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: companionSpawn.x, y: companionSpawn.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // OPT-IN runtime container (Task 0) park.dyn.ents = [{ kind: 'log', i: i0, stunned: K_COR }]; // the brief's shape, DELAYED k beats (frozen through the corridor) park.dyn.blocks = 0; // body-blocks tallied (the render cue + the module's own gates) return st; } // _parkLogCell(seed): the module's PUBLIC play-cell. `mech.fieldMech: 'log'` — the FIELD analogue // of the shipped `mech.moveMech`. A pure value constructor; no persona parameter exists (C1). function _parkLogCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'log' } }; } // _parkLogEnt(P) / _parkLogKey(P) / _parkLogBlockKey(P): the module's NAMED public reads (its own // hooks, its gates and the app's renderer consume them). The log's cell is where it IS; the block // cell is where it is GOING — the cell a body has to be standing on to stop it. function _parkLogEnt(P) { const dyn = P.st.park.dyn; return dyn && dyn.ents && dyn.ents[0] ? dyn.ents[0] : null; } function _parkLogKey(P) { const L = _parkLogEnt(P); return L ? P.st.park.log.lane[L.i] : -1; } function _parkLogBlockKey(P) { const L = _parkLogEnt(P), lg = P.st.park.log; if (!L || L.i + 1 >= lg.lane.length) return -1; // parked at the gap: nothing left to block return lg.lane[L.i + 1]; } // _parkLogRollsAt(st, beat): THE ONE ROLL-IMMINENCE PREDICATE — "does the log advance on the beat // numbered `beat`?" Three callers had grown three copies of this arithmetic (the tick, the C read, // and the app's chevron), and the app's copy could silently desync from the physics and aim the // "it lands here" mark at the wrong cell. One source, three callers, no drift. // tick asks about dyn.beat (the beat has ALREADY ticked when tick runs) // the C read asks about dyn.beat + 1 (the beat has NOT ticked yet at decision time, so the roll // the walker is deciding about is the next one) // app.js asks about dyn.beat + 1 (it draws the decision the walker is looking at) // Public and single-state legible either way: the schedule is a pure function of the PUBLIC seed // (_parkSchedule) and the PUBLIC beat, which is what lets the C read be a clean "not that cell, not // on this beat" instead of a vague "keep off the lane". function _parkLogRollsAt(st, beat) { const lg = st.park.log, dyn = st.park.dyn; const L = dyn && dyn.ents && dyn.ents[0]; if (!lg || !L || L.stunned > 0 || L.i + 1 >= lg.lane.length) return false; return (beat % lg.period) === lg.off; } // _parkLogRolls(P): does the roll land on THIS step, i.e. on the step the walker is deciding now? function _parkLogRolls(P) { return _parkLogRollsAt(P.st, P.st.park.dyn.beat + 1); } // _parkLogCLive(P, ctx) / _parkLogNLive(P, ctx): THE TWO FACET GATES, named once and used by BOTH // engaged() and prefer() (review: a prefer() that does not re-check its own gate runs whenever the // SHIPPED attitude is engaged, which on this board is nearly always — see the note on `reads`). // C speaks when the roll lands on this step and the walker is on the block cell or beside it. // N speaks while the friend still needs the gap, the log can still take it, the interception is // reachable on foot, and the walker can still afford the body (law 3 — a LIVENESS floor, never // a read: it can only remove care from the decision, never manufacture a care award). function _parkLogCLive(P, ctx) { return !!(ctx.rolls && ctx.blockKey != null && ctx.dist[_parkKey(P.st, P.st.pos[0])] <= 1); } function _parkLogNLive(P, ctx) { return !!(ctx.threat && ctx.usable && ctx.reach && ctx.afford); } // _parkLogDist(P, target): step distance from every cell to `target` over the domain the CARE // approach is allowed to use — non-wall, off the log's own body, and NEVER through the deep field. // The deep exclusion is a design law, not an optimisation: the caring move costs ONE heart (the // block), and a care read that happily waded a hazard on its way to the rescue would be charging // the walker twice for one motive — and, worse, would spend him below the heart floor before he // ever got there, so care would disarm itself. (Care refuses the shortcut through the field; that // is the same thing G is defined to take. The two minds keep their separate grammars.) function _parkLogDist(P, target) { const st = P.st, park = st.park, n = st.N; const dist = new Array(n * n).fill(Infinity); if (target == null || target < 0) return dist; const logK = _parkLogKey(P); dist[target] = 0; const q = [target]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || nk === logK || park.deep.has(nk)) continue; if (dist[nk] > dist[kk] + 1) { dist[nk] = dist[kk] + 1; q.push(nk); } } } return dist; } // _parkLogCtx(P): the ONE per-state read the three facets share (the registry computes it once). // blockKey the cell the log will roll into (-1 once it has parked) // rolls the roll lands on THIS step // threat the log can still take the gap AND the companion still needs it (he is north of the // hedge with his contract unfinished) — "he has not crossed the lane yet", read off // PUBLIC positions and the public contract, never a persona // afford the block would not take the walker below the heart floor (design law 3) // dist step distance to blockKey over the walker's domain (the care PATH read) function _parkLogCtx(P) { const st = P.st, park = st.park, lg = park.log; const L = _parkLogEnt(P); const blockKey = _parkLogBlockKey(P); const mate = st.pos[1]; const mateGem = st.tokens[park.contracts[0].gem]; const threat = !!L && L.i < lg.gapIdx && mate.y < lg.yh && mateGem.alive && P.contract < park.contracts.length; // never body-block ON the gap itself: it is the companion's own doorway, so standing there to // stop the log would wedge him exactly as the log would. Care stops one cell short. const usable = blockKey >= 0 && lg.lane.indexOf(blockKey) < lg.gapIdx; const dist = usable ? _parkLogDist(P, blockKey) : null; return { blockKey: usable ? blockKey : null, rolls: _parkLogRolls(P), threat, usable, afford: P.hearts > PARK_LOG_HEART_FLOOR, dist: dist || new Array(st.N * st.N).fill(Infinity), reach: dist ? isFinite(dist[_parkKey(st, st.pos[0])]) : false, }; } // _parkLogThrough(P): did the friend get home? He is THROUGH once he is south of the hedge (or has // taken his gem, which is south of it) — a pure read of the finished public state. This, and not // the walker's heart count, is the cell's care outcome: the ♥ he spent is the PRICE of the move; // whether the log wedged the doorway is the CONSEQUENCE. (He can never come back north — his // station is south too — so the read is stable once true.) function _parkLogThrough(P) { return P.st.pos[1].y > P.st.park.log.yh || P.st.score[1] > 0; } // _parkLogSignature(playouts): the field's OWN visible signature — THE FRIEND'S FATE. CARE-top gets // him through the gap; GOAL-top and SAFETY-top leave him wedged behind a log in his own doorway. // The shipped G-C stake (the walker's own deep descent) must still separate on top of it. Filter- // level, like every other park module signature: it gates which candidate survives the sweep. function _parkLogSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < playouts.length; i++) { const through = _parkLogThrough(playouts[i]); if (top[i] === 'care' && !through) return false; if (top[i] !== 'care' && through) return false; if (top[i] === 'goal' && playouts[i].deepEntries < 1) return false; if (top[i] === 'safety' && playouts[i].deepEntries !== 0) return false; } return true; } // _parkLogAdmissible(cell): the generate-then-filter gate — every persona's faithful playout // COMPLETES alive, the care signature separates, and every pair the board POSES is blind-recovered // in the demonstrated direction on that persona's own faithful path. FRESH build per consumer // (replays mutate dyn). Reject reasons tallied LOUDLY on _PARK_LOG_WHYS. This is PLAYABILITY, and // ONLY playability — the y12 pattern, with the SHIP bar (_parkLogRecovers) sitting strictly ABOVE // it as a NEGATIVE pin. // // IT HAS BOTH SHAPES ON RECORD (READ THIS BEFORE YOU CHANGE IT). y8 shipped LIVE originally and // FOLDED the ship bar into admission like y3 does; when Task 10 raised the bar to CALIBRATION y8 lost // it (0/6), and a folded bar a cell CANNOT clear makes every candidate board inadmissible — the // crossing sweep seats nothing and the tile goes DEAD — so while y8 was a preview the bar was UNFOLDED // (playability only, the y12 pattern). PROMOTED 2026-07-23: the neutral-corridor rebuild earned the // calibrated bar back (6/6 on every swept seed, earliest decisive award at turn 3+), so the bar is // FOLDED IN AGAIN at the tail below, on the sound reasoning that a cell whose licence to be a live tile // is "the order is blindly recoverable" must not be able to seat a board where it is not. // The invariant, stated once: FOLD THE SHIP BAR INTO ADMISSION IFF THE CELL SHIPS. y3 and y8 ship and // fold; y12/y14/y6/y10 do not ship and do not fold. const _PARK_LOG_WHYS = { complete: 0, dead: 0, sig: 0, unexpressed: 0, norec: 0, noorder: 0 }; function _parkLogAdmissible(cell) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkLogBuild(cell), persona); if (P.reason !== 'complete') { _PARK_LOG_WHYS.complete++; return false; } if (P.hearts < 1 || P.turns < 12) { _PARK_LOG_WHYS.dead++; return false; } playouts.push(P); } if (!_parkLogSignature(playouts)) { _PARK_LOG_WHYS.sig++; return false; } let posed = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_LOG_PAIRS) { if (!(parkPairExpressed(_parkLogBuild(cell), playouts[i].moves, pair) > 0)) continue; posed++; const expect = _parkPushPairDir(persona, pair); // att-key pair direction (module-agnostic helper) if (!parkRecoverPairLex(_parkLogBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_LOG_WHYS.norec++; return false; } } } if (posed === 0) { _PARK_LOG_WHYS.unexpressed++; return false; } // THE SHIP BAR, FOLDED IN (y8 SHIPS again — PROMOTED 2026-07-23; see the header). Reuses the six // playouts already computed above (no second sweep), reads at PARK_CAL_TURNS so the folded copy IS // the ship predicate, and — because the corridor rebuild makes recovery 6/6 on every swept seed — // rejects no seated cell while making the licence ENFORCED rather than merely asserted elsewhere. for (let i = 0; i < PARK_PERSONAS.length; i++) { const rec = parkRecoverOrder(_parkLogBuild(cell), playouts[i].moves, PARK_CAL_TURNS); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) { _PARK_LOG_WHYS.noorder++; return false; } } return true; } // _parkLogRecovers(cell): THE SHIP GATE (P9/P10) — blind 6/6-persona ORDER recovery out of the // shipped recovery stack and nothing else, read UNDER CALIBRATION (Task 10: skip = PARK_CAL_TURNS, // the span the readout throws away). True here is the licence for ship:true. // IT IS TRUE now, on every seed swept (PROMOTED 2026-07-23 — the neutral-corridor rebuild moved the // decisive award to turn 3+, past the calibration span). PARK_LOG_SHIPPABLE is DERIVED from it and the // slot ships. It stays a named predicate because it is what CAMP-CROSS-SWEEP's F4 and the folded // admission bar above BOTH read. Do not delete it and do not soften it. function _parkLogRecovers(cell) { for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = parkPlayout(_parkLogBuild(cell), PARK_PERSONAS[i]); if (P.reason !== 'complete') return false; const rec = parkRecoverOrder(_parkLogBuild(cell), P.moves, PARK_CAL_TURNS); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) return false; } return true; } // NO MODULE GENERATOR — AND THAT IS THE POINT (review round 2; standardised across the field cells). // y12 shipped a `makeParkStonesTask` seed-walk with its own cache, stride, sweep budget and LOUD // fallback counter, and the natural thing was to copy it. It is a FOSSIL on this seam. That shape is // inherited from the PUSH/SLIDE VERB modules, which the campaign genuinely calls directly // (`makeParkPushTask` in _parkCrossBoard) — so their generator, and their fallback counter, are on // the shipped path. A FIELD mechanic is not: the campaign reaches this module through // `parkFieldBuild -> mech.build`, and it sweeps seeds through `mech.cell` / `mech.admits` // (campaign.js _parkCrossBoard / cand() / _parkFieldCellAdmits). A module-level seed-walk here is // STRUCTURALLY UNREACHABLE on the shipped path — the real sweep is the crossing filter's, and the // real loud counter is `parkCrossFallbacks()` (0, and 0 structurally: only SHIPPED slots are swept // — a preview is seatable but unswept, sweep -2 — and each shipped sweep accepts; // CAMP-CROSS-SWEEP-GATE). Driving it from a gate test does not fix that; it // MANUFACTURES the only caller, and turns `assert(genFallbacks === 0)` from vacuous into "true, but // about nobody's code" — a green assertion that proves nothing about what ships. // // WHAT DOES NOT GO WITH IT: the REJECTION TELEMETRY. _PARK_LOG_WHYS tallies live inside // _parkLogAdmissible — the predicate the campaign actually calls, on the seeds it actually sweeps — // so it is loud exactly where the decision is made. "Remove the generator" must not become "remove // the only loud counter". function parkLogWhys() { return { ..._PARK_LOG_WHYS }; } // PARK_LOG_SHIP_SEED / PARK_LOG_SHIPPABLE: the module's MEASURED ship state — declared here (not // inferred at gate time) so the claim is a PIN a regression must break, not a tautology the gate // re-derives. // // MEASURED (seed sweep 1..24 x 6 personas = 144 faithful playouts; re-measured UNDER CALIBRATION // 2026-07-14, Task 10 — the numbers the campaign slot header repeats; gate: Y8-LOG-SHIP-GATE): // faithful play COMPLETES alive 144/144 (0 deaths, 0 cap-outs) // admitted by the shipped predicate 24/24 base seeds, at offset 0 // care signature separates 24/24 seeds (care-led BLOCK and their friend is // through; goal-/safety-led never block and their // friend is wedged behind the log in his own doorway) // G-C expressed + widened-recovered 144/144 (the walker's own deep descent) // G-N expressed + widened-recovered 144/144 (the interception is a real detour) // C-N expressed + widened-recovered 144/144 <-- a real C-N poser; still true // mis-read (wrong direction) 0 // blind ORDER recovery, skip=0 144/144 (6/6 personas on EVERY seed) <-- the old claim // blind ORDER recovery, skip=PARK_CAL_TURNS 0/144 (0/6 on EVERY seed) <-- THE REAL ONE // // SO: PARK_LOG_SHIPPABLE = FALSE, and the y8 picker slot is a MARKED PREVIEW (`ship: false, // open: true`), not a live tile. IT USED TO SAY TRUE, and that was the first park cell to claim the // bar. THE CLAIM WAS AN ARTEFACT OF AN UNCALIBRATED READ, and this block records the correction so // nobody re-derives the mistake: // // WHAT WENT WRONG. Every JUDGED park episode throws away the first PARK_CAL_TURNS (=2) player // turns — the player is still finding the controls, so those turns are excluded from the violation // tally and from every blind readout denominator (P3a §4). The READOUT obeys that. The SHIP BAR did // not: it called parkRecoverOrder with no skip. So y8 earned `ship: true` on evidence the product // is REQUIRED BY SPEC TO DISCARD — it demonstrated that it could read a player whose first two // moves count, and no such player exists. The bar now reads at PARK_CAL_TURNS (Task 10), and y8 // does not clear it. Nothing was taken away: y8 was never able to read a real player. We only // stopped claiming it could. // // WHY IT COLLAPSES — IT IS GEOMETRY, NOT LUCK, AND NOT THE SEED. The walker spawns at (7,2), one // step from the log lane at x=9. The commitment IS the opening move, so the awards pile up on // turn 1 and the calibration span eats them. On the shipped seed, the award-turn histogram over // all 6 personas is {t1: 12, t2: 4, t3: 5, ...} — 12 of the 45 awards land on turn 1 — and the // damage is not that a lot of evidence is lost, it is WHICH: on EVERY ONE of the six personas at // least one PAIR has its ONLY award inside the span, and a single undecided pair makes // parkRecoverOrder return null (as it must — it will not guess). // goal>safety>care C-N only C@t1 goal>care>safety G-C, G-N, C-N ALL inside the span // safety>goal>care G-N only G@t1 safety>care>goal G-N only N@t1 // care>goal>safety G-C only G@t1 care>safety>goal G-C only C@t1 // Six personas, six different pairs starved, one shared cause. All 24 swept seeds behave // identically (skip0 6/6, skip2 0/6 — every one), because the spawn-beside-the-lane geometry is // drawn the same on all of them. A SEED SWAP CANNOT FIX THIS. // // HOW TO EARN THE BAR BACK (the follow-up, for whoever takes it — this is a BOARD task, and the // read stack is not the problem): MOVE THE SPAWN AWAY FROM THE LANE so the decisive move lands // AFTER the calibration span. Give the walker a few forced approach steps — spawn him at the far // side of the meadow, or push the lane (and the hedge gap it ends at) further from his start — so // that his first real choice about the log happens at turn 3+. Compare y3, the cell that SURVIVED // this audit: its earliest award of any kind is turn 3, so the span costs it literally nothing // (144/144 calibrated, byte-identical to its uncalibrated tally). Nothing about y8's mechanism is // wrong — the body-block still poses C-N disjointly, which is what y8 was built to do, and it // still does it 144/144. It just poses it one move too early to be allowed to count. // // ^^^ THAT SPAWN-NUDGE PROGNOSIS WAS MEASURED AND DISPROVEN (2026-07-18; see // docs/superpowers/specs/2026-07-18-y8-log-ship-FINDING.md). A 315-combo sweep of (spawn Δ, // companion row, log off) yielded ZERO passing combos. Moving the spawn back DOES push the opener // past the span, but the starved pair's award does NOT re-land later — it is deleted, not // relocated, and each persona starves a DIFFERENT pair (goal loses GC+CN, safety loses GN, care // loses GC). awards are fragile to spawn position: shifting the walker changes his route, arrival // time, and the companion/log relative phase, so the original discriminating state is never // reproduced. A real fix must RE-POSE the starved pairs LATE — a forced neutral entry corridor of // length k WITH the companion and log delayed by the same k (board LAYOUT change), while PRESERVING // the body-block C-N poser. That is a redesign, not a spawn nudge; promotion is on hold. // // THE C-N CONTRIBUTION STANDS, and it is worth keeping straight, because it is the thing y8 was // built for and it is NOT what it lost. A C-vs-N award needs a state where the C- and N-compliant // sets are DISJOINT, and the park had none (the x2 crossing was retired for exactly this; y12 // previews for exactly this — its care scenes only ever SUBTRACT a move from N, so 'stay' stays // compliant to both and the sets always intersect). The BODY-BLOCK is disjoint by construction: N // says stand where the log is about to be, C says be anywhere else, and no move does both. y8 // proved the park CAN pose C-N. It just cannot pose it late enough to be read. const PARK_LOG_SHIP_SEED = 1; // PARK_LOG_SHIPPABLE is DERIVED just BELOW the registration (the ledge pattern) — _parkLogRecovers // runs a full playout, so PARK_FIELD_MECHS.log must be hung first or the read has no mechanic. // ---- THE REGISTRATION (Task 1). Everything the engine body knows about y8 is here: six hooks, // hung on the id the board stamps as park.fieldMech. PARK_FIELD_MECHS.log = { build: _parkLogBuild, cell: _parkLogCell, admits: _parkLogAdmissible, // LAW 1 — A WALL FOR HIM, NOT FOR YOU. The companion's planner treats the log's body as a // temporary wall (who === 'mate'), so he detours it, waits behind it, and is STUCK when it takes // his gap (plan.stuck: he HOLDS, keeps his contract, never retires — the seam's guarantee 1). // The WALKER is masked out of the log's own cell too (you cannot stand inside a log; you stand in // FRONT of it — that is what a body-block is), but the 'route' domain is deliberately UNMASKED // (law 2): the goal mind must not learn to detour a hazard it is defined not to see. legalMask: (P, key, who) => (who === 'me' || who === 'mate') && key === _parkLogKey(P), // THE WORLD ADVANCES (the seam's `tick` — post-beat, and it fires on 'stay' too, which is the // whole point: a log that froze whenever the walker waited would not be a clock). // stunned > 0 -> it is shaking the hit off: no roll this beat (and the stun swallows the roll // beat it lands on — one body-block buys the companion two rolls' worth of // time, which is exactly what makes ONE heart a move that can save him). // roll beat -> advance one lane cell, UNLESS a body is on the target: // the WALKER -> ♥−1, stunned = period, the log does not move (BODY BLOCK). // the COMPANION-> the log simply waits (he is light; his harm is the corridor, // not a heart he does not have). // May spend P.hearts: parkStep re-reads the death check right after the tick, so a lethal block // kills on THIS step. (It cannot, in faithful play — care disengages at the heart floor, law 3 — // but the engine's guarantee is what makes that a design choice rather than a hope.) tick: (P, ev) => { const st = P.st, park = st.park, lg = park.log, dyn = park.dyn; const L = dyn.ents[0]; if (!L) return; if (L.stunned > 0) { L.stunned--; return; } if (!_parkLogRollsAt(st, dyn.beat)) return; // not a roll beat / parked (ONE predicate) const ni = L.i + 1; const nk = lg.lane[ni], nx = nk % st.N, ny = (nk / st.N) | 0; if (_parkKey(st, st.pos[0]) === nk) { // THE BODY BLOCK P.hearts -= 1; L.stunned = lg.period; dyn.blocks++; st.fx.push({ k: 'thud', x: nx, y: ny }); // render hook (ZERO-TEXT): the impact cue return; } if (_parkKey(st, st.pos[1]) === nk) return; // the companion is in the way: it waits L.i = ni; st.fx.push({ k: 'roll', x: nx, y: ny }); // render hook (ZERO-TEXT): the roll + its chime }, // THE TWO FACETS, folded into the SHIPPED attitudes (never a fourth mind, never a new scorer). // // !! EVERY prefer() BELOW GATES ITSELF ON ITS OWN engaged() PREDICATE, and returns the WHOLE legal // set (an inert intersection) when that predicate is false. This is NOT belt-and-braces — it is // a correctness requirement of the seam that is easy to miss, and the review caught me missing // it. `_parkReads` intersects a mechanic's prefer() whenever the COMBINED engagement is true, // i.e. `shipped.engaged(P) || mech.engaged(P, ctx)`. The shipped safety attitude is engaged // almost everywhere on this board (the caution band is wide), so an ungated C.prefer ran on // 1490 states where the mechanic's OWN C was cold — silently turning a single-state read ("not // that cell, not on this beat") into a standing rule ("never step there, ever"), and on 16 of // them it really did strip the block cell out of safety's compliant set with no roll pending. // It was not load-bearing (the sweep is 144/144 either way) but the module header described a // read the code did not implement, and the next mechanic to copy this bundle would inherit the // bug. THE RULE: engaged() says WHEN the facet speaks; prefer() must say NOTHING when it is not // speaking, and "nothing" is the full legal set — never {} (an empty prefer makes the whole // mind INERT rather than vetoing: the contract's trap 1). reads: { ctx: _parkLogCtx, C: { // SAFETY: do not be standing where the roll lands. Speaks ONLY when the roll lands on THIS // step and the walker is on the cell or beside it — the danger is live and it is HIS. engaged: (P, ctx) => _parkLogCLive(P, ctx), // every legal move EXCEPT the one that puts him under the log. prefer: (P, legal, ctx) => { const out = new Set(); const live = _parkLogCLive(P, ctx); for (const c of legal) if (!live || c.key !== ctx.blockKey) out.add(c.k); return out; }, }, N: { // CARE: the blocking formation. Speaks while the friend still needs the gap, the log can still // take it, the interception is reachable, and the walker can still afford the body (law 3). // Note what is NOT here: any read of what the block COSTS. The heart is the PRICE of the move, // never the evidence of the motive — the motive is the path, below. engaged: (P, ctx) => _parkLogNLive(P, ctx), // the PATH read: the moves that close on the log's next cell — and 'stay' alone once he is // standing in its way (hold the line; stepping aside now is exactly the thing care refuses). prefer: (P, legal, ctx) => { const out = new Set(); if (!_parkLogNLive(P, ctx)) { // cold: say nothing (inert intersection) for (const c of legal) out.add(c.k); return out; } const here = _parkKey(P.st, P.st.pos[0]); if (here === ctx.blockKey) { out.add('stay'); return out; } const cur = ctx.dist[here]; for (const c of legal) if (ctx.dist[c.key] < cur) out.add(c.k); // NEVER {}. _parkLogNLive guarantees the interception is REACHABLE from here (ctx.reach), so // some legal move strictly reduces the distance and this is already non-empty — but a mind // that returns {} goes INERT rather than vetoing (trap 1), and I will not leave that to an // invariant proved somewhere else. if (!out.size) out.add('stay'); return out; }, }, }, }; // DERIVED (redesign 2026-07-18, Task A): the neutral-corridor rebuild EARNED THE BAR BACK. The walker // now takes PARK_LOG_CORRIDOR forced neutral steps down a walled vertical tube before his first real // choice, and the companion + log are delayed the same k, so the C-N body-block re-poses at turn 3+ // (earliest award turn 3, calibration span 2). Measured 144/144 CALIBRATED (seeds 1..24 x 6 personas, // skip=PARK_CAL_TURNS), 144/144 at skip 0, 144/144 completes. So the pin is now the READ, not a literal. // (Declared AFTER the registration above: _parkLogRecovers plays a full episode, which needs the mech.) const PARK_LOG_SHIPPABLE = _parkLogRecovers(_parkLogCell(PARK_LOG_SHIP_SEED)); /* ============ FIVE FIELD MODULES LEFT HERE ON 2026-08-03 ============ bull (y25 성난 들소) · trail (y27 얼어붙는 발자국) · duck (y6 아기 오리) · trolley (y21 the trolley yard) · warp (y32 파랑 문·주황 문). All five were PREVIEWS; no live crossing was seated on any of them. Removed with their slots, their PARK_FIELD_MECHS registrations, their SHIPPABLE constants and their exports together — the campaign-side tombstone explains why those must move as one piece. TWO THINGS SURVIVED THEIR OWNERS, on purpose: * app.js's _paintParkBull. y50 alley re-derived the bull's ENGINE grammar but not its rendering: the alley board carries park.bull + dyn.ents[0] in y25's exact shapes and calls that painter directly as the last statement of _paintParkAlley. It is alley's asset now. * the `double` ARCHETYPE (y57's mechanic label). It is not a field module at all — x4, x5 and x7 are LIVE on it, so only y57's registry row left. ============================================================================ */ /* ============ STATUE FIELD MODULE (y29 "무궁화 꽃이 피었습니다", plan 2026-07-22) ============ */ /* THE FIRST CELL WHOSE HAZARD IS A CLOCK AND NOT A PLACE. A doll stands on one wall of the yard and sings for PARK_STATUE_SING beats, then LOOKS for PARK_STATUE_GAZE beats. Nothing on the ground is ever forbidden — every cell stays walkable on every beat — but a step taken while it is LOOKING costs the walker a heart. The board has no deep field at all (park.deep is EMPTY), so the whole heart channel is this one rule: if a run comes home short of hearts, exactly one kind of decision explains it, and the confession log names every instance of it (storm's channel-purity discipline, restated on a temporal hazard). FOUR THINGS THE ENGINE FORCED, each one a wall the next temporal module would otherwise hit: (1) THE CHARGE LIVES ON onLeave, NOT ON tick. parkStep fires onLeave/onEnter BEFORE `dyn.beat++` (engine.js:8154 vs 8174) and tick AFTER it. So onLeave reads the very beat the walker DECIDED on, which is the beat the clock showed him, and tick reads the next one. Billing a step from tick would bill it against a clock the walker never saw. (2) tick MUST NOT ASK `_parkStatueGazing` ABOUT THE STEP IT IS CLOSING. Same offset, other side: by the time tick runs the beat has already advanced, so "was that step taken under the gaze" is answered by `dyn.statue.gazePrev` — the snapshot the PREVIOUS tick left behind. The snapshot is seeded false at build, which is the truth (beat 0 is a song beat). (3) 'stay' IS ALWAYS INNOCENT. onLeave fires for every move key including 'stay' (engine.js:7324-7329), and a game whose whole point is holding still would bill the holding. `ev.fromKey === ev.toKey` returns, said out loud rather than inherited. (4) NOTHING IS MASKED IN THE 'me' OR 'route' DOMAINS. A temporal hazard that masked ground would hand the oracle an Infinity argmin (engine.js:7941-7947) and, worse, would be lying: the doll does not close the yard, it prices the beat. The ONLY mask is the companion's, and it is the HOLD — see below. THE COMPANION IS HELD BY A HAND, NOT BY A RULE. While the doll looks, a walker standing within a Chebyshev step of his companion masks the companion's WHOLE domain, so his planner comes back `{next:null, stuck:true}` and he holds where he stands (guarantee 1, engine.js:7398-7402) instead of walking into the gaze and being sent back stunned. That is the care act of this cell, and it is ACTIVE: it costs the walker the beats he spends beside him, and often a heart to arrive in time. THE THREE MINDS ARE SEPARATED BY THE CLOCK, and the separation is exact rather than statistical: C while it looks, 'stay' — the one move that is certainly free. N while it looks and his companion is a step or more away, CLOSE ON HIM — and no closing move is 'stay', so C and N are DISJOINT BY DEFINITION in exactly the state the cell is built for. G no facet at all. The goal mind already refuses 'stay' and already wants the shortest line (engine.js:7702-7706); under a gaze that is a heart per beat, which is the whole trade. */ const PARK_STATUE_N = 13; // THE CLOCK. Period = SING + GAZE. These two numbers are the module's main lever: lengthening GAZE // makes a goal-led dash lethal instead of expensive (the walker carries 3 hearts and nothing on this // board returns one), shortening it makes holding still free. const PARK_STATUE_SING = 4; const PARK_STATUE_GAZE = 2; const PARK_STATUE_PERIOD = PARK_STATUE_SING + PARK_STATUE_GAZE; // how many beats a companion caught walking under the gaze is held where he stands. const PARK_STATUE_STUN = 2; // how many song beats before the gaze the care read starts closing on the companion. The escort has // to ARRIVE before the looking starts, or the arrival itself is billed. // 2 IS MEASURED, not chosen: at 1 the escort is a beat short of his shoulder on most seeds and pays // for the last step (admission 10/40 over seeds 1..40); at 3 the care read holds the walker beside // him for three quarters of every song, his own errand stretches over more looking spans, and the // extra spans kill him (20/40 seeds dead). 2 lets him arrive free and leave again. const PARK_STATUE_LEAD = 2; const PARK_STATUE_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // the four faces the doll may stand on — the anti-mimic axis (storm's console-face idiom). const _PARK_STATUE_DIRS = [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }]; // _parkStatueBuild(cell): the board. Seed-pure, persona-blind (C1). CONSTRUCTED, never rejection- // sampled: the doll, the walker's line of gems toward it and the companion's crossing errand exist on // every seed, so the seed draws the LAYOUT (which face the doll stands on, which flank the errand // runs from, the lateral jog of each gem) and never whether the scene exists. function _parkStatueBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_STATUE_N, c = (n - 1) >> 1; const r = rng((seed * 4211 + 2903) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const sd = cs(0, 3), d = _PARK_STATUE_DIRS[sd]; // the doll's face == the walker's advance axis const lat = { x: Math.abs(d.y), y: Math.abs(d.x) }; // the lateral axis (perpendicular to d) const fl = cs(0, 1) ? 1 : -1; // which flank the companion's lane runs from // A(fwd, side): board-relative coordinates. fwd = cells toward the doll from the middle, side = // cells along the lateral axis. EVERY placement below goes through it, so the whole layout rotates // and mirrors with the two drawn axes and no cell has to be re-derived per face. const A = (fwd, side) => ({ x: c + d.x * fwd + lat.x * side * fl, y: c + d.y * fwd + lat.y * side * fl }); // THE FINISH. One marked cell straight in front of the doll, one cell inside its wall. The // errand is the CROSSING itself: spawn sits four cells behind the middle and the finish five // ahead, so the walk is ~9 steps and crosses one or two looking spans — the hurry-vs-hold // trade survives the loss of the old gem line. The chain is this ONE token, so stepping onto // it completes the run through the shipped terminal (engine.js:8172) with no goal grammar of // its own. SPAWN NEVER SITS ON THE COMPANION'S FLANK (side -1 or 0, never +1) — the y29 first // draft's measured lesson, inherited unchanged. const sp = cs(-1, 0); const spawn = A(-4, sp); const finish = A(5, 0); // THE COMPANION'S LANE runs PARALLEL to the walker's line, two cells out on the drawn flank, and // that distance is the design. ACROSS was built first and MEASURED WRONG: his body kept landing on // the walker's own next cell, the shipped care read ("clear his route") pinned the walker in place // for four turns at a stretch, and that pause had nothing to do with the doll. Parallel at 3 means // the two errands never contend for a cell, and standing beside him under the gaze costs a // deliberate step OUT of the walker's line and a step back — which is exactly the price the care // act should have. His gem sits on that flank too, off every cell the walker's own line gives him a // reason to enter, because a walker who eats his companion's gem ends the errand the care read is // measured against (the y26 law). AT THREE the escort was out of reach: the care personas spent // their hearts chasing him and died or arrived late on 24/40 seeds. At two, one step buys the hold. const station = A(-3, 2), mateGem = A(1, 2), retire = A(3, 2); const doll = A(6, 0); // ON the perimeter wall: a fixture, never ground const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) wall.add(y * n + x); // NO DEEP FIELD, NO VERGE (channel purity). distDeep = Infinity everywhere, which leaves the // SHIPPED static caution attitude vacuous on its own (engine.js:7727) — the module's own gaze facet // is then the entire safety read, exactly as storm's design law 1 has it. It also makes the safe // route metric equal the fast one, so no persona is steered by terrain that does not exist. const deep = new Set(), verge = new Set(), walkway = new Set(); for (let k = 0; k < n * n; k++) if (!wall.has(k)) walkway.add(k); const distDeep = new Array(n * n).fill(Infinity); const tokens = [ { x: finish.x, y: finish.y, v: 1, alive: true, guard: false }, // the finish (chain 0) { x: mateGem.x, y: mateGem.y, v: cs(1, 2), alive: true, guard: false }, // his contract gem ]; const park = { N: n, seed, k: 0, fieldMech: 'statue', deep, verge, walkway, distDeep, // the doll and the clock, named on the board so the reads, the signature, the gates and the // render all argue about the same fixture instead of each re-deriving a face. statue: { side: sd, fl, dollKey: doll.y * n + doll.x, finishTi: 0, finishKey: finish.y * n + finish.x, sing: PARK_STATUE_SING, gaze: PARK_STATUE_GAZE, stun: PARK_STATUE_STUN, laneKeys: [station.y * n + station.x, mateGem.y * n + mateGem.x] }, clusters: tokens.map(t => ({ x: t.x, y: t.y, v: t.v })), chain: [0], contracts: [{ gem: 1, station }], retire, spawn, companionSpawn: { x: station.x, y: station.y }, // trig = the whole board: his errand starts on turn one, so his lane is a live thing the walker // MEETS rather than a trap sprung by his own approach (y26's setting, y27's restatement). trig: n * 2, cap: 140, minTurns: 6, cautionD: 2, damage: 1, cell: _parkStatueCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: station.x, y: station.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // OPT-IN runtime container (Task 0) // dyn.statue APPENDED to the standard dyn. Every member EXISTS at build time — _parkDeepClone is // structural but only sound for members that are there when a search forks (the storm rule). // caught the walker's confession: one entry per step taken under the gaze. // holds the care confession: one entry per looking beat spent standing beside the companion. // matePrev where he stood when the previous tick closed (the movement detector). // gazePrev whether THAT beat was a looking beat (the clock-offset snapshot — see header note 2). // shadowed steps the gaze never saw: one entry per step taken fully behind the companion. // summon the HUMAN call's asked-for cell key (null = no call armed) — see Task 3. park.dyn.statue = { caught: [], mateCaught: 0, mateStun: 0, matePrev: null, gazePrev: false, holds: [], shadowed: [], summon: null }; return st; } // _parkStatueCell(seed): the module's PUBLIC play-cell (C1 — no persona parameter exists). function _parkStatueCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'statue' } }; } // ---- THE PUBLIC CLOCK READS. Pure functions of the PUBLIC beat and two constants: the walker SEES // all of it, which is the only reason a read over it is a read at all (flood's design law 2, storm's // restatement). NOTHING HERE ADVANCES ANYTHING — what moves dyn.beat is the engine's own step, so any // caller (the reads, a gate, the HUD, the painter) may ask the schedule a question without moving it. // _parkStatueGazing(st): is the doll LOOKING on the current beat? function _parkStatueGazing(st) { const dyn = st.park && st.park.dyn; return ((dyn ? dyn.beat : 0) % PARK_STATUE_PERIOD) >= PARK_STATUE_SING; } // _parkStatueTillGaze(st): song beats left before it looks again; 0 while it is already looking. function _parkStatueTillGaze(st) { const dyn = st.park && st.park.dyn; const ph = (dyn ? dyn.beat : 0) % PARK_STATUE_PERIOD; return ph >= PARK_STATUE_SING ? 0 : PARK_STATUE_SING - ph; } // _parkStatueNear(st): Chebyshev distance between the walker and his companion. ONE definition, used // by the hold mask, by the hold confession and by the care read — three copies of "beside him" would // eventually disagree about a diagonal, and the whole care act of this cell is that adjacency. function _parkStatueNear(st) { return Math.max(Math.abs(st.pos[0].x - st.pos[1].x), Math.abs(st.pos[0].y - st.pos[1].y)); } // _parkStatueShadow(st, key): is this cell hidden from the doll behind the companion's body? // The occlusion is a ONE-CELL-WIDE ray: the companion's own file (same lateral coordinate), // on the far side of him from the doll. A pure read of the public board and the two bodies — // nothing here advances anything, so the toll, the painter and a gate may all ask it about // the same beat. His own cell is NOT shadow (a body is not a hiding place, it is the wall of // one), and a wall cell is never shadow (nothing stands there to be hidden). function _parkStatueShadow(st, key) { const S = st.park && st.park.statue; if (!S || st.wall.has(key)) return false; const n = st.N, d = _PARK_STATUE_DIRS[S.side]; const px = key % n, py = (key / n) | 0, co = st.pos[1]; if (px * Math.abs(d.y) + py * Math.abs(d.x) !== co.x * Math.abs(d.y) + co.y * Math.abs(d.x)) return false; // a different file is lit return px * d.x + py * d.y < co.x * d.x + co.y * d.y; // behind him = farther from the doll } // _parkStatueSummonDest(st): the one cell a called companion is asked to stand on — the // walker's forward neighbour toward the doll. Returns the cell key, or -1 when that cell // cannot hold him (off the board, a wall, or his own body already there). A pure read; the // command below is its only consumer besides the painter. function _parkStatueSummonDest(st) { const S = st.park && st.park.statue; if (!S) return -1; const n = st.N, d = _PARK_STATUE_DIRS[S.side]; const x = st.pos[0].x + d.x, y = st.pos[0].y + d.y; if (x < 0 || y < 0 || x >= n || y >= n) return -1; const k = y * n + x; if (st.wall.has(k)) return -1; if (st.pos[1].x === x && st.pos[1].y === y) return -1; // already standing there return k; } // parkStatueSummon(P): the HUMAN affordance — ask the nearby companion to come stand in // front. No persona ever calls this (the oracle's legal set is untouched) and it spends no // turn and no beat: it only ARMS the walk. The companion answers one cell per beat through // the module's own tick, under the same gaze law as everybody else — calling him during a // looking span is how he gets sent back, and that timing is the player's to judge. function parkStatueSummon(P) { const st = P.st, D = st.park.dyn && st.park.dyn.statue; if (!st.park.statue || !D || P.over) return false; // A SLEEPING BODY CANNOT BE CALLED (y46 v2, 2026-08-03). The siege board carries the y29 statue // fixture, so this affordance is reachable on it by construction — but there the companion is // out cold and the pull has been replaced by the shoulder (legalAdd/onEnter push her two cells). // Refuse here rather than let the call arm and then quietly do nothing: siege's legalMask no // longer reads D.summon and its tick no longer walks it, so an armed call would be a control // that lights up and never answers. Half-working is worse than absent. if (D.asleep) return false; if (D.mateStun > 0) return false; // a stunned body cannot be asked if (_parkStatueNear(st) > 2) return false; // the ask needs a nearby voice const dest = _parkStatueSummonDest(st); if (dest < 0) return false; D.summon = dest; st.fx.push({ k: 'called', x: st.pos[1].x, y: st.pos[1].y }); return true; } // _parkStatueSummonStep(st): one cell of the answered call — BFS over non-wall ground with // the walker's own cell excluded (he is a body, not a doorway). Moves the companion and his // facing; a no-path beat is simply a held beat (stuck is not arrived — the shipped planner's // law, restated here because this walk bypasses that planner on purpose). function _parkStatueSummonStep(st) { const n = st.N, D = st.park.dyn.statue; const src = st.pos[1].y * n + st.pos[1].x, dk = D.summon; // THE POST IS SPENT ON ARRIVAL (2026-07-27, owner's call): the shield is one-use, not a leash. // He stood where he was asked; the call ends there and his own errand is his again. What still // holds him through the looking span is the shoulder rule in legalMask (gazing && near<=1), so // the shelter lasts exactly the beats the player spends behind his body — no longer. // Without this line the call had NO release path at all and a called body was masked out of its // own planner for the rest of the run (measured: homecoming 24/24 -> 0/24 on both seated cells). if (src === dk) { D.summon = null; return; } const me = st.pos[0].y * n + st.pos[0].x; const dist = new Array(n * n).fill(Infinity), parent = new Array(n * n).fill(-1); dist[src] = 0; const q = [src]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const dd of DIRS) { const nx = x + dd.x, ny = y + dd.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || nk === me) continue; if (dist[nk] > dist[kk] + 1) { dist[nk] = dist[kk] + 1; parent[nk] = kk; q.push(nk); } } } if (!isFinite(dist[dk])) return; let cur = dk; while (parent[cur] !== src) cur = parent[cur]; st.facing[1] = { dx: Math.sign((cur % n) - st.pos[1].x), dy: Math.sign(((cur / n) | 0) - st.pos[1].y) }; st.pos[1] = { x: cur % n, y: (cur / n) | 0 }; } // _parkStatueApproach(P): step distance from every cell to the COMPANION'S cell, over the ground the // walker may actually use. Nothing on this board is masked for him, so the domain is simply the // non-wall cells — the honest shape of a yard whose danger is a beat and not a fence. Recomputed per // state because the companion moves. function _parkStatueApproach(P) { const st = P.st, n = st.N; const dist = new Array(n * n).fill(Infinity); const src = _parkKey(st, st.pos[1]); dist[src] = 0; const q = [src]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk)) continue; if (dist[nk] > dist[k] + 1) { dist[nk] = dist[k] + 1; q.push(nk); } } } return dist; } // _parkStatueCtx(P): the ONE per-state read the two facets share (the registry computes it once per // state, engine.js:7823). Every field is a public read of the board, the beat and the two bodies. // live — THE ESCORT IS STILL WORTH SOMETHING: he is still walking somewhere (mode is not 'done') // and he is not already sitting out a stun, because a companion who cannot move cannot be // caught and there is nothing to hold him from. It goes cold the instant either ends, which // is what keeps the care facet a facet instead of a standing bias toward his flank. // HIS GEM IS DELIBERATELY NOT A CLAUSE HERE, and that was MEASURED. Gating on `gem.alive` // made the care read go cold the moment he banked it — but he then RELOCATES, and a // relocating companion walks on EVERY turn (engine.js:8167), gaze beats included, so he was // sent back on the way home with nobody beside him and the care signature failed on runs // whose care act had actually worked (45% of care playouts, seeds 1..40). Walking him home // is the same act as walking him out. function _parkStatueCtx(P) { const st = P.st, park = st.park, D = park.dyn && park.dyn.statue; const gaze = _parkStatueGazing(st), till = _parkStatueTillGaze(st), near = _parkStatueNear(st); const live = !!(D && D.mateStun === 0 && P.mode !== 'done'); // C engages on the CLOCK ALONE. The walk board's own caution attitude is vacuous here by // construction (distDeep is Infinity everywhere — there is no field to keep away from), so this // facet IS the safety read on this cell, exactly as storm's is on its own. const engagedC = gaze; // N engages while it looks (hold him, or go to him) and, during the song, only in the last // PARK_STATUE_LEAD beats before it looks again — an escort that engaged all song long would just // be a second goal mind pointed at the companion. // THE LEAD-IN DOES NOT CARE WHETHER HE IS ALREADY THERE, and that clause was MEASURED. Gating the // song beats on `near > 1` (the first shape) let a care-led walker reach his companion's shoulder // on the beat before the gaze, go cold BECAUSE he had arrived, and be walked away again by his own // goal mind — arriving one step late, paying a heart for the return trip and holding nothing // (seed 1, beats 2-4). Arriving is not the errand; BEING THERE WHEN IT LOOKS is. const engagedN = live && (gaze || till <= PARK_STATUE_LEAD); return { gaze, till, near, live, engagedC, engagedN, here: _parkKey(st, st.pos[0]), dist: (engagedN && near > 1) ? _parkStatueApproach(P) : null }; } /* ---- y29 STATUE — THE FILTER LAYER: faithful playout, field signature, admissibility. y24 lantern's three siblings are the direct genealogy; what differs is that every observable here is a COUNT off the two confession logs plus the turn total, because on this board no persona differs in SCORE (all six bank the same three gems) and a signature leaning on score would be reading noise. */ // _parkStatuePlay(cell, persona): one persona-faithful playout on a FRESH board (every consumer of a // playout rebuilds — parkStep mutates what it is handed). The persona reaches the ORACLE and nothing // else (C1). function _parkStatuePlay(cell, persona) { const st = _parkStatueBuild(cell); const P = parkStart(st); while (!P.over) parkStep(P, parkOracleMove(P, persona)); const D = st.park.dyn.statue; P._statueCaught = D.caught.length; // steps taken under the gaze (unit: violations) P._statueMateCaught = D.mateCaught; // times the companion was sent back (unit: catches) P._statueHolds = D.holds.length; // looking beats spent beside him (unit: beats) // MATE HOME IS `st.score[1]`, NEVER `!gem.alive`. The walker's harvest is INDISCRIMINATE // (engine.js:8114): he takes any gem he steps on, his companion's included, and a gem the WALKER // ate would read as an errand served under the liveness test. Only the companion's own score moves // when the companion takes it (engine.js:8007-8009). P._statueMateHome = st.score[1] > 0; return P; } // _parkStatueSignature(playouts): the field's OWN visible signature (filter level). THREE MINDS, // THREE OBSERVABLES, and each is read off what the clock made that mind do: // SAFETY-top — never once steps while it looks (caught 0). The cheapest possible reading, and on // this board it is available to everybody, which is precisely why it has to be // MEASURED for the caution personas and denied to the goal ones. // GOAL-top — steps anyway (caught >= 1) AND comes home in strictly fewer turns than the caution // baseline's own worst. The hearts have to BUY something or they were not a trade. // The bound is DATA-DRIVEN, never a constant. // CARE-top — his companion is never sent back (mateCaught 0) and the walker actually STOOD there // (holds >= 1). Care as an act, not as an absence. // Plus the non-vacuity clause the whole cell rests on: SOMEBODY has to lose him to the doll. If every // persona walks home with the companion untouched, the hold cost nothing and separated nobody. function _parkStatueSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); let safeWorst = -Infinity, goalWorst = -Infinity, careSeen = 0, harmed = 0; for (let i = 0; i < playouts.length; i++) { const P = playouts[i]; if (P._statueMateCaught > 0) harmed++; if (top[i] === 'safety') { if (P._statueCaught !== 0) return false; safeWorst = Math.max(safeWorst, P.turns); } if (top[i] === 'goal') { if (P._statueCaught < 1) return false; goalWorst = Math.max(goalWorst, P.turns); } if (top[i] === 'care') { careSeen++; if (P._statueMateCaught !== 0) return false; if (P._statueHolds < 1) return false; } } if (!isFinite(safeWorst) || !isFinite(goalWorst)) return false; // non-vacuity guard if (!(goalWorst < safeWorst)) return false; // hurrying really is faster if (harmed < 1) return false; // and the harm is really posed return careSeen === 2; } // _parkStatueAdmissible(cell): the generate-then-filter PLAYABILITY gate over the FULL persona set. // Every faithful playout COMPLETES ALIVE, a care-led walker actually gets his companion's errand // served, the field signature separates all three minds, the C-N scene is really POSED on some // trajectory, and every pair the board poses blind-recovers in the demonstrated direction. // Reject reasons tallied LOUDLY — a silent 0/40 teaches nothing. FIRST-REASON HISTOGRAM: every branch // early-returns, so a rejected cell contributes to exactly ONE key (storm's warning, restated). const _PARK_STATUE_WHYS = { complete: 0, dead: 0, matelost: 0, sig: 0, nocn: 0, norec: 0 }; function _parkStatueAdmissible(cell) { const playouts = []; for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = _parkStatuePlay(cell, PARK_PERSONAS[i]); if (P.reason === 'death') { _PARK_STATUE_WHYS.dead++; return false; } if (P.reason !== 'complete') { _PARK_STATUE_WHYS.complete++; return false; } if (P.hearts < 1) { _PARK_STATUE_WHYS.dead++; return false; } if (PARK_PERSONAS[i][0] === 'care' && !P._statueMateHome) { _PARK_STATUE_WHYS.matelost++; return false; } playouts.push(P); } if (!_parkStatueSignature(playouts)) { _PARK_STATUE_WHYS.sig++; return false; } let posed = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_STATUE_PAIRS) { if (!(parkPairExpressed(_parkStatueBuild(cell), playouts[i].moves, pair) > 0)) continue; if (pair[0] === 'C' && pair[1] === 'N') posed++; const expect = _parkPushPairDir(persona, pair); if (!parkRecoverPairLex(_parkStatueBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_STATUE_WHYS.norec++; return false; } } } if (posed < 2) { _PARK_STATUE_WHYS.nocn++; return false; } return true; } // parkStatueWhys(): a copy of the reject tally (gates read it as a DELTA around their own sweep). function parkStatueWhys() { return { ..._PARK_STATUE_WHYS }; } // ---- THE REGISTRATION. Everything the engine body knows about y29 hangs here, off the id the board // stamps as park.fieldMech. PARK_FIELD_MECHS.statue = { build: _parkStatueBuild, cell: _parkStatueCell, admits: _parkStatueAdmissible, // THE READS — TWO facets, and deliberately no G facet. The shipped goal mind already refuses 'stay' // and already wants the shortest line (engine.js:7702-7706); under a gaze that IS the dash, and // teaching it inside a module would be re-deriving the engine (y26's finding, y27's restatement). // Neither facet can ever return {} — an empty prefer does not veto, it deletes the mind // (engine.js:7369-7377) — and both return the WHOLE legal set when their own read is cold, which is // the only correct way to say "this mind has nothing to add here". reads: { ctx: _parkStatueCtx, // C — WHILE IT LOOKS, HOLD STILL. One move, and it is the only one on the board that is certainly // free. 'stay' is always in the legal set of a walk board (the walker's own cell is never a wall), // so this can never be the empty set. C: { engaged: (P, ctx) => !!ctx && ctx.engagedC, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.engagedC) { for (const c of legal) out.add(c.k); return out; } for (const c of legal) if (c.k === 'stay') out.add(c.k); if (out.size) return out; for (const c of legal) out.add(c.k); return out; }, }, // N — GO AND STAND BESIDE HIM, THEN STAY THERE. Adjacency is the hold (see the mask), so this is // the positive errand and not an avoidance: a care read of the "do him no harm" shape would be // FREE on this board — the walker harms him by being ABSENT — and a read that costs nothing is // taken by every mind and separates none of them (y27's measured lesson). // THE DISJOINTNESS IS DEFINITIONAL: no closing move is 'stay', so in the very state the cell is // built for (it looks, he is a step or more away) C and N cannot both be satisfied. The walker // either keeps his own hearts or spends one to be at his companion's shoulder. // The just-vacated square is excluded exactly as the shipped goal preference excludes it // (engine.js:7702-7706) — lantern's fourth livelock, and P.prev is trustworthy here because this // module never re-places the walker (no legalAdd, no action-moves). N: { engaged: (P, ctx) => !!ctx && ctx.engagedN, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.engagedN) { for (const c of legal) out.add(c.k); return out; } const co = P.st.pos[1]; const cheb = (c) => Math.max(Math.abs(c.x - co.x), Math.abs(c.y - co.y)); if (ctx.near <= 1) { // beside him already: hold the hand for (const c of legal) if (c.k === 'stay') out.add(c.k); if (out.size) return out; } else if (ctx.gaze) { // UNDER THE GAZE THE APPROACH COSTS A HEART, so it is a care move only when it ARRIVES. // One step that puts him at the companion's shoulder buys the hold and is worth the heart; // a step that merely shortens the gap buys nothing this beat and the doll charges for it // anyway. MEASURED: the plain gradient here spent 2-3 hearts per care run chasing a // companion it never caught, and killed the care personas outright on 15/40 seeds. // The refusal is single-state legible — "can I be beside him THIS beat?" — and it is where // C and N really part: C says the free move is the only compliant one, N says one heart is // what standing beside him costs. for (const c of legal) if (c.k !== 'stay' && cheb(c) <= 1) out.add(c.k); if (out.size) return out; for (const c of legal) if (c.k === 'stay') out.add(c.k); // out of reach: burn nothing if (out.size) return out; } else if (ctx.dist) { // THE LEAD-IN, and it is FREE: during the song a step costs nothing, so the plain distance // gradient is the right read. The just-vacated square is excluded exactly as the shipped // goal preference excludes it (engine.js:7702-7706) — lantern's fourth livelock. const cur = ctx.dist[ctx.here]; if (isFinite(cur)) for (const c of legal) { if (P.prev && P.prev.x === c.x && P.prev.y === c.y) continue; if (ctx.dist[c.key] < cur) out.add(c.k); } if (out.size) return out; } for (const c of legal) out.add(c.k); // nothing closes: say nothing return out; }, }, }, // THE MASK — ONE domain, and it is not terrain. Nothing about this board is impassable to the // WALKER (his hazard is the beat, not the cell) and nothing is hidden from the ROUTE metric, so // registering either of those domains would be a lie that also hands the oracle an Infinity argmin // (engine.js:7941-7947). The COMPANION is a different matter: while the doll looks, either he is // being held by a hand at his shoulder or he is too stunned to walk, and both are a domain with no // cells in it. His planner then returns `{next:null, stuck:true}` and he HOLDS — keeps his // contract, keeps his mode, does not retire (guarantee 1, engine.js:7398-7402). That is the y24 // grammar reused verbatim: a companion who cannot move is not a companion who is finished. legalMask: (P, key, who) => { if (who !== 'mate') return false; const st = P.st, park = st.park, D = park.dyn && park.dyn.statue; if (!park.statue || !D) return false; if (D.mateStun > 0) return true; // sent back: he sits out the next beats if (D.summon != null) return true; // a called companion answers only the call return _parkStatueGazing(st) && _parkStatueNear(st) <= 1; // a hand on his shoulder }, // THE TOLL. Fires after the walker's position commits and BEFORE `dyn.beat++` (engine.js:8154 vs // 8174), so `dyn.beat` here is the beat the walker DECIDED on — the one the clock was showing him. // Moving it to tick would bill him against a beat he never saw. // 'stay' IS ALWAYS INNOCENT: onLeave fires on every move key (engine.js:7324-7329), and holding // still is the entire game. Spelled out, not inherited. onLeave: (P, ev) => { const st = P.st, dyn = st.park.dyn, D = dyn && dyn.statue; if (!D) return; if (ev.fromKey === ev.toKey) return; // a pause is not a step if (!_parkStatueGazing(st)) return; // the song is free // THE SHIELD. A step whose BOTH ends are hidden behind the companion's body is a step the // doll never saw — landing in the open, or launching from it, is still a seen step. Judged // on the same beat as the toll itself and logged as its own confession, so a gate can // insist the shelter was USED rather than merely drawn. if (_parkStatueShadow(st, ev.fromKey) && _parkStatueShadow(st, ev.toKey)) { D.shadowed.push({ beat: dyn.beat, key: ev.toKey }); st.fx.push({ k: 'hidden', x: ev.to.x, y: ev.to.y }); return; } P.hearts--; // death is the engine's own re-read (8183-8185) D.caught.push({ beat: dyn.beat, key: ev.toKey }); st.fx.push({ k: 'seen', x: ev.to.x, y: ev.to.y }); // render hook (ZERO-TEXT) }, // THE COMPANION'S SIDE OF THE SAME BEAT. Fires AFTER `dyn.beat++` (engine.js:8183), so the beat it // reads is already the NEXT one — which is why every question about the step just taken is asked of // `gazePrev`, the snapshot the previous tick left, and never of `_parkStatueGazing` directly. Order // is fixed and load-bearing: expire the stun, then judge the step, then confess the hold, then // re-snapshot. Judging before expiring would double-bill a mate who cannot move at all. // This tick NEVER ends the run: y29 has no terminal of its own — the only death here is ♥0, which // the engine re-reads for itself right after this hook returns. tick: (P) => { const st = P.st, dyn = st.park.dyn, D = dyn && dyn.statue; if (!D) return; if (D.mateStun > 0) D.mateStun--; // THE ANSWERED CALL walks here, one cell per beat, and ONLY here — his own planner is // masked for the whole life of the call (legalMask above), so this is his single mover // and the two can never both move him on one beat. A stunned body does not answer. The // judge block below still sees the step, which is the whole point: a call answered while // it looks is a mate sent back, and that timing is the caller's to own. if (D.summon != null && D.mateStun === 0) _parkStatueSummonStep(st); const co = st.pos[1]; if (D.gazePrev && D.matePrev) { if (D.matePrev.x !== co.x || D.matePrev.y !== co.y) { // HE WALKED WHILE IT LOOKED. Nobody was holding him, so he is sent back to the start of his // own patience: PARK_STATUE_STUN beats of sitting still. D.mateCaught++; D.mateStun = PARK_STATUE_STUN; st.fx.push({ k: 'sent', x: co.x, y: co.y }); // render hook (ZERO-TEXT) } else if (_parkStatueNear(st) <= 1) { // HE HELD, AND SOMEBODY WAS BESIDE HIM. The care act, logged with its beat: an append-only // confession, the positive twin of `caught`. D.holds.push({ beat: dyn.beat }); } } D.matePrev = { x: co.x, y: co.y }; D.gazePrev = _parkStatueGazing(st); }, }; // PARK_STATUE_SHIPPABLE — the MODULE's own ship pin, read by CAMP-CROSS-SWEEP's F4 through // engine.test.js's FIELD_SHIPPABLE map (one entry per field mechanic, same contract as // PARK_TRAIL_SHIPPABLE / PARK_LANT_SHIPPABLE). // // A LITERAL false, ON PURPOSE. y29 is a PREVIEW module: for a preview the pin is the literal and the // module's own bar is ADMISSION, while a SHIPPED module derives its pin from a solo blind-order // recovery on its own board (_parkBombRecovers / _parkLedgeRecovers are that shape). Deriving one // here would claim a measurement this branch has not made — derive-never-assert cuts both ways, so // do not assert TRUE and do not dress this false up as derived either. const PARK_STATUE_SHIPPABLE = false; /* ============ END STATUE FIELD MODULE (the yard and the doll's drawn face, the empty deep field, the two pure clock reads, the gaze toll on onLeave, the adjacency hold and the mate stun, the stay-vs-close reads and the admission gate) ============ */ /* ============ FLOOD FIELD MODULE (y10 "rising water", plan 2026-07-13, Task 5) ============ */ /* A SELF-CONTAINED park FIELD module registered through PARK_FIELD_MECHS. The movement verb and the goal grammar are the walk's; what changes is that THE BOARD SHRINKS. The floor is grouped into concentric RINGS (outermost first) and every `every` beats the next ring sinks into dyn.flood. A MOUND in the middle never sinks, and one cell of it — the SUMMIT — is the companion's RESERVED SEAT (park.retire, the terminal target of his own planner). A walker caught standing on a sunk cell ENDS THE RUN. THE THREE MINDS, read through the SHIPPED PARK_ATTITUDES (no new scoring channel): G goal — UNCHANGED CODE. The chain runs outer gem -> outer gem -> the mound gem, so the shipped beeline preference IS "go get the outer gems". The urgency is physical, not a read: a gem still standing when its ring goes under is WASHED AWAY (tick), and the chain cursor advances past it. Last-chance value, priced by the clock. C safety — ONE new facet: keep at least PARK_FLOOD_MARGIN (=2) rings between you and the water. N care — ONE new facet: leave the companion's summit SEAT free. The endgame is a scramble for high ground and yielding your seat is the caring move. DESIGN LAWS (each earns its place): 1. DROWNING IS A TERMINAL, NOT A HEART (tick). The brief is explicit and the constitution is explicit: ♥ and personality are separate channels. So `tick` sets P.over + P.reason='drown' — the seam's own "the only honest place to end a run". It is NOT expressed by masking the walker's own cell: the legal set is never empty (the seam re-admits 'stay'), so an over-masked board limps into reason:'noise' instead of the terminal. To make the separation STRUCTURAL rather than merely intended, this board carries NO deep field at all (park.deep is empty): nothing on a flood board can spend a heart, so no drown can ever be mistaken for one. Care is read from the path (the yielded seat), never from heart bookkeeping. 2. THE PREVIEW IS THE MECHANIC, not decoration. The ring that sinks NEXT is a pure function of the PUBLIC beat (flood.rings[nextIdx]) and the app draws it DOTTED. A caution read has to be OBSERVABLE or "keep 2 rings of margin" is not something a player could have chosen. 3. C IS NEVER SILENT (the seam's trap 1). An empty prefer() does NOT veto — it makes that mind INERT. At full flood NOTHING has margin >= 2, so a bare threshold would switch SAFETY OFF at the exact moment a rising-water cell is about to mean something. So the rule is "margin >= 2 if you can, otherwise the HIGHEST ground you can reach" — never empty, and it is what puts the summit in C's mouth in the endgame. 4. ONE CAUSEWAY (this builder). The ring just outside the mound is walled except for a single crossing, so the mound is entered through ONE cell. That is not scenery: it is what makes the seat scramble a scene the walker RELIABLY stands in — he arrives at the mound's edge cell, the water is at his heels, the only ground with margin left is the seat, and the gem he came for is on the far corner. G wants two moves, C wants exactly the seat, N forbids exactly the seat. C and N are DISJOINT there — which is the one shape that can award a C-vs-N pair at all (the standing gap y12 could not close; see its module header). 5. THE UN-SUNK REGION IS ALWAYS CONNECTED (rings sink outermost-first, so what is left is always a solid block around the mound). Escapability by construction — a walker is never stranded, he is only ever too slow. */ const PARK_FLOOD_N = 17; // odd interior (15x15) => rings 0..7 with a SINGLE summit cell // PARK_FLOOD_EVERY — beats per ring. THE BRIEF SAYS 6; THIS SHIPS 5, a deliberate, disclosed deviation // from the brief's stated `Produces` interface. Nothing reads the constant by name (the brief's own test // reads `flood.every`, and every consumer derives from it), so the interface is intact; what changed is // the tempo. // WHY 5, MEASURED (and read this before you touch it — the earlier comment here claimed something // that was never measured, and it pointed the next owner at the wrong lever): // * every=6 admits NOTHING: 0/200 raw cells pass `_parkFloodAdmissible`. The constant is load-bearing // and the cell as built does not exist at the slower tempo. That is why it is 5. // * WHAT KILLS every=6 IS **drown + signature**, NOT a missing C-N pose. The rejection tally at 6 is // {drown 95, sig 105, nocn 0} — `nocn 0` because no candidate survives far enough to be COUNTED, // not because C-N is never posed. Measured directly on raw every=6 boards (40 seeds x 6 personas): // **C-N posed 126/240, G-N posed 213/240.** C-N is posed PLENTIFULLY at the slower tempo. What binds // there is the signature filter (goal-top must out-score safety-top; at 6 beats/ring safety has time // to collect the gems too) plus faithful walks drowning. // * Blind ORDER recovery on raw every=6 boards: **77/174 (44%)**, against 38/144 (26%) at every=5. // Still 0 seeds at 6/6, so the ship verdict (demo-only) is the same either way. // THE LEVER THIS EXPOSES: tempo, not gem placement, is the larger handle on the G-N blocker // (213/240 posed at 6, 38/144 at 5). A slower clock gives goal a longer, more constrained approach and // poses the pair far more often; the price is that faithful walks drown and the signature filter stops // separating goal from safety. Whoever wants y10 live should work THAT trade (a slower clock with a // shorter chain, or a start further in) at least as hard as re-seating the gem below. const PARK_FLOOD_EVERY = 5; const PARK_FLOOD_MARGIN = 2; // C's keep-back: >= 2 rings between you and the water (brief) const PARK_FLOOD_MOUND_R = 6; // rings >= 6 are the MOUND (rim ring 6 + summit ring 7): never sink const PARK_FLOOD_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; const _PARK_FLOOD_DIRS = [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }]; // _parkFloodRing(n, x, y): the concentric ring index of an INTERIOR cell — its Chebyshev distance // from the interior's outer edge. 0 = the outermost playable belt (first to go), rising inward. function _parkFloodRing(n, x, y) { return Math.min(x - 1, y - 1, n - 2 - x, n - 2 - y); } // _parkFloodBuild(cell): the y10 board — pure PUBLIC geometry (reads ONLY cell.seed; C1: no persona // parameter exists), runtime-compatible with parkStart/parkStep. Frame (17x17): perimeter wall, a // 15x15 interior of 8 concentric rings, a 3x3 MOUND at the centre (rings 6-7) whose single centre // cell is the SUMMIT, and a walled ring 5 pierced by ONE CAUSEWAY (design law 4). Outside the walk // archetype pools by construction (they stay append-only and walk-only — the PUSH/SLIDE precedent). function _parkFloodBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_FLOOD_N, c = (n - 1) >> 1; // 17 -> centre (8,8) = the SUMMIT const r = rng((seed * 1597 + 8191) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const K = (x, y) => y * n + x; const ring = (x, y) => _parkFloodRing(n, x, y); const sd = cs(0, 3); // which side the causeway faces (the anti-mimic draw axis) const d = _PARK_FLOOD_DIRS[sd]; const lat = { x: Math.abs(d.y), y: Math.abs(d.x) }; // the axis PERPENDICULAR to the causeway const entry = { x: c + d.x, y: c + d.y }; // the mound-rim cell the causeway lands on const cway = { x: c + 2 * d.x, y: c + 2 * d.y }; // the single ring-5 crossing // the mound gem sits on one of the two rim corners on the FAR side of the summit, so that from the // entry cell the SUMMIT is one of two goal-reducing steps (see design law 4 / the C-N scene). const fs = cs(0, 1) ? 1 : -1; const gM = { x: c - d.x + fs * lat.x, y: c - d.y + fs * lat.y }; // the walker spawns on ring 2 on the OPPOSITE side, so the journey crosses the whole board and the // water is at the mound by the time he gets there. const lo = cs(-1, 1); const spawn = { x: c - d.x * 4 + lat.x * lo, y: c - d.y * 4 + lat.y * lo }; // THE TWO OUTER CHAIN GEMS, and the timing they carry. g0 is ring 2 out on a flank (it goes under at // beat 2*every); g1 is ring 3 and — this is the load-bearing part — it sits on the CAUSEWAY'S SIDE // of the board (it goes under at 3*every, one ring-beat before the approach belt itself). // WHY g1 IS WHERE IT IS. Once ring 3 is water, the only floor left outside the mound is the ring-4 // belt, and that belt is 24 cells round: a walker stranded on the far side of it from the causeway // cannot reach the one way up before the belt goes too. He drowns, and he drowns for a REASON THAT // IS NOT A DECISION — it is where the generator happened to leave him. g1 is the thing that decides // where every persona is standing when ring 3 goes: a goal-led walker is ON it, and a caution-led // walker is hovering two rings off it (his own read holds him back and his goal mind holds him // near). Putting it on the causeway's side puts EVERY persona within reach of the climb at the beat // the climb becomes the only thing left. Measured: with g1 on the far flank, 47/60 seeds drowned a // faithful walk. const fl0 = cs(0, 1) ? 1 : -1; const g0 = { x: c + lat.x * fl0 * 4 + d.x * cs(-1, 1), y: c + lat.y * fl0 * 4 + d.y * cs(-1, 1) }; const g1 = { x: c + d.x * 4 - lat.x * fl0 * cs(2, 3), y: c + d.y * 4 - lat.y * fl0 * cs(2, 3) }; // THE COMPANION'S CLOCK — the single hardest thing on this board to get right, so it is stated in // full. His contract gem sits on the causeway's OWN approach (ring 4), which is the cell the walker // must pass to climb: that is the SHIPPED contested-gem care scene, and it is where G-N is posed. // But his STATION is a long way off, and park.trig is SHORT — so he does not stir until the walker // is nearly at the hill, and then he is trudging in from far away at his half pace. // WHY: the first cut gave him the whole board as a trigger and a station three steps from his // gem. He took it, retired to the summit by beat ~18, and SAT THERE — so the seat was OCCUPIED // before the walker ever reached the mound, the walker could not legally step on it, and the care // read was never even OFFERED (measured: seatOffered 0 in 48/48 playouts; the C-N pair, which is // this cell's whole reason to exist, could not be posed at all). A reserved seat is only reserved // if its owner is still ON HIS WAY. So he is now BEHIND the walker by construction: the endgame is // a race up one causeway, and the walker gets there first — which is exactly what makes standing // aside a choice rather than a formality. // WHERE the gem may NOT go: the cell c+3d, the causeway's only outer neighbour. That is the // CHOKEPOINT, and a contested gem standing on it would make care UNPLAYABLE — the shipped N read // forbids stepping on the contested gem, so a care-led walker could never climb at all; he would // hover out of trigger range, drift back in, and oscillate until the water took him. A care read // must cost you something, not everything. So the gem sits TWO cells laterally along the ring-4 // approach belt: on the walker's line, but with a detour around it, so care pays and goal pockets. // KNOWN LIMIT, MEASURED — this placement is why G-N only reaches 38/144 and why y10 is not live. // A G-N award needs G's compliant set DISJOINT from N's, i.e. the gem must be G's ONLY // distance-reducing move. Two cells out on an open belt it usually is not: in 21 of the 29 // contested-gem scenes that award nothing, G has TWO reducing moves and N permits one of them, so // THE DETOUR IS FREE AND GOAL NEVER HAS TO CHOOSE. (In the other 8, G does not want the gem at all.) // One fix is to seat the gem where the detour COSTS — a one-wide segment of the approach — without // putting it ON the chokepoint, which strands care entirely. // BUT DO NOT READ THAT AS THE ONLY LEVER, OR EVEN THE BIGGEST ONE. Measured (see PARK_FLOOD_EVERY): // at the slower tempo every=6, G-N is posed 213/240 rather than 38/144, and blind order recovery // rises to 44% from 26%. Tempo is at least as large a handle on this blocker as gem placement, and // what blocks the slower clock is drown + the signature filter — a different problem than this one. // Work both; do not re-seat the gem and never look at the clock. const g2 = { x: c + d.x * 3 - lat.x * fl0 * 2, y: c + d.y * 3 - lat.y * fl0 * 2 }; const station = { x: c - d.x * 4 + lat.x * fl0 * 5, y: c - d.y * 4 + lat.y * fl0 * 5 }; const wall = new Set(), walkway = new Set(), verge = new Set(), deep = new Set(); const mound = new Set(), rings = []; for (let i = 0; i < PARK_FLOOD_MOUND_R - 1; i++) rings.push([]); // rings 0..4 SINK; ring 5 is the cliff for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = K(x, y); if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } const rr = ring(x, y); if (rr >= PARK_FLOOD_MOUND_R) { mound.add(kk); walkway.add(kk); continue; } // design law 4: ring 5 is the mound's CLIFF — WALL, except the single CAUSEWAY, which is the only // way up. Two things are deliberate here and both were bought with measurements. // (a) THE CLIFF IS SHEER. Leaving its four corners as floor made them DEAD-END LOCAL PEAKS (a // corner is the highest ground for two rings around), and C's take-the-highest-ground rule // walked straight into one, found 'stay' to be the only max-margin move, and sat there until // the water closed over it — 30/48 faithful walks drowned. A local maximum that is not the // summit is a trap, and a mind that can be lured into a trap by its own rule is being tested // against a broken board, not a hard one. So the only high ground is the real high ground. // (b) THE CAUSEWAY NEVER SINKS — it is simply absent from the ring schedule. While it was ring 5 // and sank with the rest, the beat the last belt went under was the SAME beat the water // reached the mound's foot — so "late enough for the seat to matter" and "early enough to // live" were the same instant, and no seed could satisfy both: the walker stood at the // entry two beats early, C still tolerated the rim, and C and N intersected. Making the one // causeway permanent uncouples them: the climb is always survivable, and the water is free // to arrive at the hill while he is still on it — which is the whole endgame. // NOTE it is NOT added to `flood.mound`. Not sinking and being the hill are different facts, // and `mound` is the HILL (the brief's "안 잠기는 언덕", the high ground the seat sits on). // Folding the causeway in made the set mean "everything that never sinks", which is a // different predicate wearing the same name — and the renderer duly painted a stone bridge // at the waterline as lit plateau. It never sinks because it is not in `rings`; that is all. if (rr === PARK_FLOOD_MOUND_R - 1) { if (x === cway.x && y === cway.y) { walkway.add(kk); continue; } // the causeway: floor, unsinkable, NOT hill wall.add(kk); continue; } rings[rr].push(kk); // the two outermost belts read as VERGE, so the shipped SAFE route metric (verge cost 8) prices // the drowning edge of the board without any new machinery: a caution-led persona routes inland. (rr <= 1 ? verge : walkway).add(kk); } for (const rw of rings) rw.sort((a, b) => a - b); // deterministic order (never read as a set) // ringOf: the public HEIGHT map the caution read prices margin against. -1 on every wall — a wall // is not ground, and must never win the take-the-highest-ground tie-break. const ringOf = new Array(n * n).fill(-1); for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) if (!wall.has(K(x, y))) ringOf[K(x, y)] = ring(x, y); // NO DEEP FIELD (design law 1): the water is the whole hazard and it is a TERMINAL, so nothing on // this board can spend a heart. distDeep = Infinity everywhere keeps the shipped static-C attitude // engagement OFF (it asks distDeep <= d) and its preference VACUOUS (it asks distDeep >= d), so // the flood facet below is the entire safety read — intersected into the same one mind. const distDeep = new Array(n * n).fill(Infinity); const tokens = [ { x: g0.x, y: g0.y, v: cs(2, 3), alive: true, guard: false }, // chain 0 — outer, ring 2 (last chance) { x: g1.x, y: g1.y, v: cs(2, 3), alive: true, guard: false }, // chain 1 — outer, ring 3 (last chance) { x: gM.x, y: gM.y, v: cs(1, 2), alive: true, guard: false }, // chain 2 — ON THE MOUND: never sinks { x: g2.x, y: g2.y, v: cs(1, 2), alive: true, guard: false }, // the companion's contract gem ]; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const summit = { x: c, y: c }; const park = { N: n, seed, k: 0, fieldMech: 'flood', // the REGISTRY key (Task 1) — the ONLY thing the engine body knows walkway, verge, deep, distDeep, // SEED-PURE (immutable): the timetable, the hill, the reserved seat, and the one way up. The // runtime sinks into dyn.flood; this is never mutated, so a fresh build always replays // byte-identically. Every field here is READ by someone (the reads, the gates, or the renderer) — // `entry` / `side` / `park.geom` were written and never read, so they are gone. flood: { rings, every: PARK_FLOOD_EVERY, mound, ringOf, seat: summit, seatKey: K(summit.x, summit.y), cway }, clusters, chain: [0, 1, 2], contracts: [{ gem: 3, station }], retire: { x: summit.x, y: summit.y }, // his seat IS his last target spawn, companionSpawn: { x: station.x, y: station.y }, // trig 5 (NOT the whole board, which is y12's law 4): here the companion's INTENT is public from // turn 1 anyway — his seat is a seed-pure board fact, printed on the mound — so the trigger is // free to do its real job, which is to keep him BEHIND the walker in the climb (see above). trig: 5, cap: 120, minTurns: 12, cautionD: 2, damage: 1, cell: _parkFloodCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: station.x, y: station.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // OPT-IN runtime container (Task 0): the sunk cells live here return st; } // _parkFloodCell(seed): the module's PUBLIC play-cell. `mech.fieldMech: 'flood'` — the FIELD // analogue of the shipped `mech.moveMech`. No new task `kind` (the y12 decision, unchanged). function _parkFloodCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'flood' } }; } // ---- THE PUBLIC CLOCK READS. Every one of these is a pure function of the PUBLIC beat + the // seed-pure timetable: the walker can SEE all of it, which is the only reason the caution read is a // read at all (design law 2). No persona symbol reaches any of them (C1). // _parkFloodNextIdx(st): how many rings have gone == the index of the ring that sinks NEXT. function _parkFloodNextIdx(st) { const fl = st.park.flood, dyn = st.park.dyn; return Math.min(fl.rings.length, Math.floor((dyn ? dyn.beat : 0) / fl.every)); } // _parkFloodNext(st): THE PREVIEW — the cell keys of the ring that goes under next (the app draws // them DOTTED). Empty once the water has taken everything it can. function _parkFloodNext(st) { const i = _parkFloodNextIdx(st); return i < st.park.flood.rings.length ? st.park.flood.rings[i].slice() : []; } // _parkFloodMargin(st, key, next): rings of daylight between a cell and the water's next bite. // Mound cells are ordinary high ground here (their ring index simply never comes up). function _parkFloodMargin(st, key, next) { const rr = st.park.flood.ringOf[key]; return rr < 0 ? -Infinity : rr - next; // a wall is not ground } // _parkFloodSeatLive(P): the seat is RESERVED until he is sitting in it. Read off public positions. function _parkFloodSeatLive(P) { return _parkKey(P.st, P.st.pos[1]) !== P.st.park.flood.seatKey; } // _parkFloodSignature(playouts): the field's OWN visible signature, aligned with PARK_PERSONAS. // GOAL-top runs the outer gems down to the wire (it banks strictly more chain value than SAFETY-top, // which lets them go under rather than spend its margin); SAFETY-top never lets the water get within // one ring of it. Filter-level, like every other module signature: it gates which candidate survives // the sweep, never the measured spread. function _parkFloodSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); let gMin = Infinity, sMax = -Infinity; for (let i = 0; i < playouts.length; i++) { if (top[i] === 'goal') gMin = Math.min(gMin, playouts[i].st.score[0]); if (top[i] === 'safety') { sMax = Math.max(sMax, playouts[i].st.score[0]); if (playouts[i]._floodMin < 1) return false; } } return gMin > sMax; // the goal-led walker BUYS the sinking gems; safety does not } // _parkFloodPlay(cell, persona): a faithful playout that also records the walker's TIGHTEST margin // (the safety signature reads it — a pure observable of the public board + the public beat). function _parkFloodPlay(cell, persona) { const P = parkStart(_parkFloodBuild(cell)); let worst = Infinity; while (!P.over) { const next = _parkFloodNextIdx(P.st); worst = Math.min(worst, _parkFloodMargin(P.st, _parkKey(P.st, P.st.pos[0]), next)); parkStep(P, parkOracleMove(P, persona)); } P._floodMin = worst; return P; } // _parkFloodAdmissible(cell): the generate-then-filter PLAYABILITY gate over the FULL persona SET (a // constant — the accepted board stays a pure function of the public cell): every persona's faithful // playout COMPLETES STANDING (reason 'complete', never 'drown' — the brief's own bar: a board where a // faithful walk drowns is a GENERATOR bug), the field signature separates, the endgame SEAT SCRAMBLE // actually poses C-vs-N, and every pair the board POSES is blind-recovered in the demonstrated // direction. The SHIP bar (_parkFloodRecovers, 6/6 blind ORDER recovery) is strictly stronger and // lives above this — admissible-but-not-recoverable ships DEMO-ONLY (P9/P10). Reject reasons tallied // LOUDLY on _PARK_FLOOD_WHYS. const _PARK_FLOOD_WHYS = { complete: 0, drown: 0, sig: 0, unexpressed: 0, nocn: 0, norec: 0 }; function _parkFloodAdmissible(cell) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = _parkFloodPlay(cell, persona); if (P.reason === 'drown') { _PARK_FLOOD_WHYS.drown++; return false; } if (P.reason !== 'complete') { _PARK_FLOOD_WHYS.complete++; return false; } if (P.turns < 12) { _PARK_FLOOD_WHYS.complete++; return false; } playouts.push(P); } if (!_parkFloodSignature(playouts)) { _PARK_FLOOD_WHYS.sig++; return false; } let posed = 0, cn = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_FLOOD_PAIRS) { if (!(parkPairExpressed(_parkFloodBuild(cell), playouts[i].moves, pair) > 0)) continue; posed++; if (pair[0] === 'C') cn++; // the mound-seat scramble, actually posed const expect = _parkPushPairDir(persona, pair); // att-key pair direction (module-agnostic helper) if (!parkRecoverPairLex(_parkFloodBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_FLOOD_WHYS.norec++; return false; } } } if (posed === 0) { _PARK_FLOOD_WHYS.unexpressed++; return false; } // THE PLAN'S OWN ADMISSION CONDITION for y10: approve only a seed where yielding the mound seat // ACTUALLY separates N. If C-N is never posed the cell has not earned its slot, whatever else it // does — so this is a rejection, not a note. if (cn === 0) { _PARK_FLOOD_WHYS.nocn++; return false; } return true; } // _parkFloodRecovers(cell): THE SHIP GATE (P9/P10) — blind 6/6-persona ORDER recovery out of the // shipped recovery stack and nothing else. True here is the ONLY licence for ship:true on the slot. function _parkFloodRecovers(cell) { for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = parkPlayout(_parkFloodBuild(cell), PARK_PERSONAS[i]); if (P.reason !== 'complete') return false; const rec = parkRecoverOrder(_parkFloodBuild(cell), P.moves, PARK_CAL_TURNS); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) return false; } return true; } // NO MODULE-LEVEL GENERATOR HERE, AND THAT IS DELIBERATE (project decision, 2026-07-14). // y12/SLIDE ship a `makeParkXTask` that seed-walks to an admitted cell, and y10 had one too. For a // FIELD mechanic it is dead weight: the campaign reaches this module through `parkFieldBuild -> // mech.build` and does its OWN generate-then-filter sweep through `mech.cell` / `mech.admits` // (campaign.js `_parkCrossingPlayCell` + `_parkFieldCellAdmits`). A module-level seed-walk is therefore // structurally UNREACHABLE on the shipped path — driving it from a gate test manufactures its only // caller, and a LOUD fallback counter that only the test increments is a counter that proves nothing. // So the seed-walk is gone. WHAT IS NOT GONE IS THE TELEMETRY: `_PARK_FLOOD_WHYS` is tallied inside // `admits` — the predicate the campaign actually calls — so the rejection reasons stay loud on the real // path instead of on a synthetic one. "Remove the generator" must never become "remove the only loud // counter." // READ THE COUNTS AS A TRIPWIRE, NOT A BOUND: this is a monotone module global, never reset, so it // accumulates across every `admits` call in the process. `whys.drown > 0` in a test means "the rejection // path is alive", NOT "exactly N candidates drowned in this sweep". And a ZERO is not evidence of // absence: a reason can read 0 simply because an EARLIER filter rejected every candidate first — that is // exactly the trap the old every=6 comment fell into (`nocn 0` meant "nothing survived to be counted", // and was misread as "C-N is never posed"). To claim a reason never fires, measure it directly. function parkFloodWhys() { return { ..._PARK_FLOOD_WHYS }; } // PARK_FLOOD_SHIP_SEED / PARK_FLOOD_SHIPPABLE: the module's MEASURED ship state — declared here (not // inferred at gate time) so the claim is a PIN a regression must break, not a tautology the gate // re-derives. // // MEASURED (base seeds 1..24 through the generator x 6 personas = 144 faithful playouts, 2026-07-14; // the gate is Y10-FLOOD-SHIP-GATE in engine.test.js and the campaign slot header repeats these): // faithful play COMPLETES STANDING 144/144 (0 drowns, 0 cap-outs, 0 hearts spent) // generator fallbacks 0 // G-C expressed 144/144 widened-recovered 144/144 // G-N expressed 38/144 widened-recovered 38/38 // C-N expressed 52/144 widened-recovered 52/52 <-- THE MOUND-SEAT SCRAMBLE // mis-read (wrong direction) 0/234 // blind ORDER recovery (parkRecoverOrder) 38/144; 6/6 on NO base seed // SO: PARK_FLOOD_SHIPPABLE = false — y10 ships DEMO-ONLY (P9/P10: lower the track, never weaken the // gate). Every pair the board POSES it reads correctly, and no pair is ever read backwards; what it // cannot do is pose all THREE on all SIX personas' own faithful paths, and two pairs is not an order, // so parkRecoverOrder returns null on the trajectories that are missing one. // // WHAT y10 DID DO, and it is the thing y12 could not: **C-N IS POSED** — 52/144, up from y12's 0/144. // y12's module header names the blocker precisely ("a C-vs-N award needs a state where the C-compliant // and N-compliant sets are DISJOINT, and both of y12's care scenes only ever SUBTRACT a move or two // from N, so the sets always intersect"), and calls it the project's standing gap, inherited from the // retired x2 crossing. The mound seat closes it, and the reason is worth keeping: at the entry cell, // with the water at the hill's foot, the ONLY ground with margin left is the summit — so C's compliant // set is exactly {step onto the seat} while N's is exactly {everything except the seat}. Not a // subtraction: a partition. That is why the scene had to be built as a SINGLE CAUSEWAY onto a hill // whose top is ONE cell (design law 4), and why the timing had to be forced (the causeway is permanent // so the climb cannot be fatal, and the clock is fast enough to reach the hill while he is still on // it). Measured directly by Y10-FLOOD-READS: 24 of 96 seat-offered states have C and N DISJOINT. // The remaining ship blocker is G-N (38/144), not C-N. TWO measured levers on it, in the order the next // owner should try them: // 1. TEMPO. At every=6 the pair is posed 213/240 (vs 38/144 here) and blind order recovery reaches // 44% (vs 26%) — but 0/200 cells admit, because faithful walks drown and the signature filter stops // separating goal from safety. See PARK_FLOOD_EVERY. This is the LARGER lever. // 2. GEM PLACEMENT. Where the gem sits, the detour around it is FREE: in 21 of 29 non-awarding // contested-gem scenes G keeps a second reducing move that N permits, so goal never has to choose. // Seat it where the detour costs — but NOT on the chokepoint, which strands care (40/40 drown). // See the `g2` comment in the builder. const PARK_FLOOD_SHIP_SEED = 1; const PARK_FLOOD_SHIPPABLE = false; // ---- THE REGISTRATION (Task 1). Everything the engine body knows about y10 is right here. PARK_FIELD_MECHS.flood = { build: _parkFloodBuild, cell: _parkFloodCell, admits: _parkFloodAdmissible, // WATER IS NOT GROUND — for the walker, for the companion and for the route metric alike. One // predicate, all three domains. NOTE what this does NOT do: it never masks the walker's OWN cell // to express drowning. The legal set is never empty (the seam re-admits 'stay'), so that route // limps into reason:'noise' instead of the terminal. The terminal lives in tick(), below. legalMask: (P, key) => { const dyn = P.st.park.dyn; return !!(dyn && dyn.flood.has(key)); }, // THE WORLD ADVANCES. Fires after dyn.beat++ and on 'stay' too — a clock that stopped whenever the // walker waited would not be a clock, and a rising-water cell whose water waited for you would not // be a cell. Three things happen, in order: // (a) THE SCHEDULE. Every ring whose beat has come goes under. Derived from the beat (not a // private counter) so it is idempotent and fork-safe: dyn.flood is a Set of cell keys, which // _parkDeepClone copies structurally on every parkCeiling branch. // (b) LAST-CHANCE VALUE. A gem still standing on a sinking cell is WASHED AWAY — it dies unscored // and the chain cursor advances past it (_parkAdvanceDest). This is what makes "grab the // outer gems before they go under" a real stake rather than a slogan: the goal-led walker // spends his margin to buy them, the safety-led walker watches them go. // (c) HIGHER GROUND. The companion wades inward if the water takes the cell under him — the // endgame is a scramble for high ground and he is in it too. Deterministic (fixed DIRS // order, highest ring wins); he never steps onto the walker. // AND THEN THE TERMINAL: a walker standing in the water ENDS THE RUN — P.over + reason 'drown'. // Not a heart: ♥ and personality are separate channels, and on this board nothing spends a heart // at all, so the two can never be confused (design law 1). tick: (P) => { const st = P.st, park = st.park, dyn = park.dyn, fl = park.flood, n = st.N; const want = Math.min(fl.rings.length, Math.floor(dyn.beat / fl.every)); let sank = false; for (let i = 0; i < want; i++) for (const kk of fl.rings[i]) { if (dyn.flood.has(kk)) continue; dyn.flood.add(kk); sank = true; const x = kk % n, y = (kk / n) | 0; st.fx.push({ k: 'sink', x, y }); // render hook (ZERO-TEXT): it goes under for (const t of st.tokens) if (t.alive && t.x === x && t.y === y) { t.alive = false; // washed away — no score, to nobody st.fx.push({ k: 'wash', x, y }); } } // THE CHAIN STEPS PAST A DROWNED GEM. parkStep advances the destination cursor BEFORE tick runs, so // a gem this hook just washed away leaves the cursor pointing at a DEAD destination: _parkFields // then builds a metric to a cell that no longer exists, G's preference comes back empty, the goal // mind goes silent and the walker caps out. The body cannot know a mechanic destroys tokens — that // is the seam working as designed — so the mechanic re-runs the advance itself. Guarded by // Y10-FLOOD-WASH, which fails if this line is deleted. if (sank) _parkAdvanceDest(P); // HE SCRAMBLES TOO — every tick, and by the SHORTEST WAY OUT. // THE INVARIANT, stated so it is TRUE (the previous comment here claimed one the code did not // provide, which is the worst kind of comment): HE IS NEVER STRANDED. Whenever the water is over // his cell he takes one step along the shortest path to the nearest DRY ground, wading through // water to get there; dry ground is always reachable (measured 0/144 stranded) and he always // moves toward it. He can still BE in the water when the run ends (38/144) — that is being // mid-wade, not being stuck: the water only reaches the outer belt in the closing beats, and on // the step that COMPLETES the run parkStep suppresses tick entirely (`!P.over`, so a mechanic can // never overrule a completion), so his last wade is correctly never taken. Pinned by // Y10-FLOOD-C1, which asserts STRANDED == 0, not WET == 0. // WHY A BFS AND NOT A GREEDY RULE — I wrote greedy twice and it was false both times: // (a) v1 gated the whole retreat on `sank`, so once the LAST ring had gone under it never ran // again: a companion caught by that final sink stood in the water for the rest of the run, // because nothing was left to sink to move him. // (b) v2 ran every tick and stepped to the best neighbour by (dry, then uphill). Still false — // AND THE MEASUREMENT DID NOT BUDGE, 49/144 either way. The cliff is a WALL with ONE // causeway through it, so dry ground is not UPHILL of a drowning companion, it is AROUND a // maze from him: he climbs to the belt, finds every inward neighbour walled, and paces it // forever. NO GREEDY RULE CAN SOLVE A MAZE. // Deterministic: BFS in fixed DIRS order, first-found parent wins. He never steps onto the // walker. Cheap (17x17, and only while he is actually submerged). const ck = _parkKey(st, st.pos[1]); if (dyn.flood.has(ck)) { const prev = new Map([[ck, -1]]); const q = [ck]; let land = -1; for (let h = 0; h < q.length && land < 0; h++) { const cur = q[h], cx = cur % n, cy = (cur / n) | 0; for (const dd of _PARK_FLOOD_DIRS) { const nx = cx + dd.x, ny = cy + dd.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || prev.has(nk)) continue; if (nx === st.pos[0].x && ny === st.pos[0].y) continue; // never onto the walker prev.set(nk, cur); if (!dyn.flood.has(nk)) { land = nk; break; } // dry ground: stop q.push(nk); } } if (land >= 0) { // walk the path back to the FIRST step out of his cell let step = land; while (prev.get(step) !== ck && prev.get(step) !== -1 && prev.get(step) !== undefined) step = prev.get(step); if (prev.get(step) === ck) st.pos[1] = { x: step % n, y: (step / n) | 0 }; } } if (dyn.flood.has(_parkKey(st, st.pos[0]))) { // THE TERMINAL (the seam's tick contract) P.over = true; P.reason = 'drown'; st.fx.push({ k: 'drown', x: st.pos[0].x, y: st.pos[0].y }); } }, // THE TWO FACETS, folded into the SHIPPED three minds (no new scoring channel). G is untouched: the // chain IS the outer-gem pull, and the clock is what makes it urgent. reads: { ctx: (P) => ({ next: _parkFloodNextIdx(P.st), seatKey: P.st.park.flood.seatKey, seatLive: _parkFloodSeatLive(P) }), C: { // The water is ALWAYS rising, so the safety concern is always live — there is no "outside the // caution band" on a board that is shrinking under you. engaged: () => true, // KEEP >= 2 RINGS OF MARGIN — and when nothing can, TAKE THE HIGHEST GROUND YOU CAN REACH. // The fallback is not a softening of the brief's rule, it is what MAKES it a rule: an empty // prefer() does not veto, it makes the mind INERT (the seam's trap 1), so a bare threshold // would switch safety OFF at full flood — the exact moment this cell is about to mean // something. With the fallback, C's mouth in the endgame is "get on the summit", which is the // move care forbids: that is the C-N disjunction the whole cell is built to pose. prefer: (P, legal, ctx) => { const st = P.st; const m = (c) => _parkFloodMargin(st, c.key, ctx.next); let pool = legal.filter(c => m(c) >= PARK_FLOOD_MARGIN); if (!pool.length) { let best = -Infinity; for (const c of legal) best = Math.max(best, m(c)); pool = legal.filter(c => m(c) === best); } const out = new Set(); for (const c of pool) out.add(c.k); return out; }, }, N: { // HIS SEAT IS RESERVED until he is in it. park.retire — the terminal target of the companion's // OWN planner — is the summit, so this is his plan's cell, read off the public board. engaged: (P, ctx) => ctx.seatLive, // Leave it free. Note this excludes 'stay' when the walker is ALREADY sitting in it — yielding // a seat you have taken means getting up. // // !! GUARDED ON THE SAME PREDICATE AS engaged(), AND FALLS BACK TO THE FULL LEGAL SET. This is // not belt-and-braces, it is a real seam trap: _parkReads intersects a mechanic's prefer() // whenever the COMBINED attitude is engaged — that is, whenever the SHIPPED attitude engaged // it — NOT when the mechanic's own engaged() said so. So this hook runs in states my own // engaged() calls dead (the shipped care read fires on its contested gem while the companion // is already sitting in his seat), and if it returned {} or a stay-only set there, care would // silently collapse — a care-led walker who simply stops walking. It cannot: when the seat is // his, I hand back every legal move, which intersects as a NO-OP and leaves the shipped care // read exactly as it was. Returning {} would NOT veto (seam trap 1) — it would make care // INERT, which is worse and quieter. // Measured today: 0 states where this ran with seatLive false, so nothing changes; the guard // is here so that a future geometry in which the companion DOES reach his seat mid-run cannot // turn a no-op into a silent collapse. prefer: (P, legal, ctx) => { const out = new Set(); for (const c of legal) { if (ctx.seatLive && c.key === ctx.seatKey) continue; // his seat, and he is still climbing out.add(c.k); } return out; }, }, }, }; /* ============ y23 STORM field module — 극성 자기장 (Task 1: board builder + public clock reads, plan 2026-07-20) ============ flood(y10)의 동심원 링 스케줄러 계보. 이 셀 전체는 두 반전을 짊어진다 — (1) 물은 터미널이지만 자기장은 ♥ DoT다, (2) 피해의 대상이 dyn.storm.polarity('me'|'mate') 토글로 걷는 이 ↔ 동료 사이를 오간다(CONSOLE 진입이 토글, 토글 이력이 고백). 이 블록(TASK 1)이 짓는 것은 seed-pure 보드(_parkStormBuild)와 그 위에서 읽는 순수 클록(_parkStormNextIdx/_parkStormNext/_parkStormMargin)이다. 모듈의 나머지는 아래에 이어지는 태스크들이 이 토대 위에 올렸다 — 필터 층(_parkStormPlay/_parkStormSignature/_parkStormAdmissible, Task 3)과 런타임 + reads + 캠페인 표면(PARK_FIELD_MECHS.storm 등록: legalMask/onEnter/tick은 Task 2, reads/cell/admits는 Task 3). 각 블록은 자기 앞의 것만 전제로 읽으면 된다. 기하 (n=PARK_STORM_N=15): 테두리 벽 1칸 + 13x13 내부. 링 인덱스는 flood와 같은 체비셰프 거리 (_parkStormRing). rr < PARK_STORM_CORE_R(5)인 5개 링(0..4)이 스케줄(rings[0..4])에 실린다; rr >= 5는 중앙 3x3 CORE(coreKeys) — 영원히 스케줄에 오르지 않는다(최후 피난처). CORE 바로 바깥 테두리인 링4는 CORE_RIM(coreRimKeys)이며, 이 링 자체는 스케줄의 마지막 항목이다 — 콘솔도 결국 잠긴다는 뜻: 진짜 영원한 안식처는 CORE 그 자체뿐이다. seed가 4방 중 콘솔이 향할 면을 뽑는 것은 flood의 causeway 뽑기와 같은 수법(anti-mimic 축); 걷는 이는 그 반대편 링2에서 스폰하고, 동료 station은 측면 원거리, 계약 보석(g2)은 콘솔 쪽 접근로 링3에 선다. `park.deep`은 비워 둔다(channel purity — 이 보드의 모든 ♥ 지출은 자기장+극성 탓으로만 발생해야 한다, 후속 태스크가 tick에서 지킨다). */ const PARK_STORM_N = 15; // 13x13 interior => rings 0..5, core = ring 5 (3x3) const PARK_STORM_EVERY = 6; // beats per ring — 측정으로 조율할 1차 레버 (flood: 5) const PARK_STORM_MARGIN = 2; // C's keep-back (rings), polarity 'me'일 때만 // PARK_STORM_GRACE — THE GRACE PERIOD, in CONSECUTIVE HARM BEATS (unit: beats). The companion goes // down only after this many consecutive beats of harm ACTUALLY LANDING on him; anything that // interrupts the run resets the count to zero (see `tick`, and `dyn.storm.mateHeat`). // WHY THIS EXISTS (owner ruling, 2026-07-20, after the Task-3 review). Before the grace the flip // was a ONE-WAY DOOR: measured over seeds 1..40 x 6 personas = 240 faithful playouts, raw cells // (`_parkStormCell(seed)`, no generator), every=6 — 11 runs took >= 1 flip and ALL 11 ended in a // mate-down (11/11, 1:1, zero off-diagonal in either direction). Since `_parkStormAdmissible` // rejects on `matelost`, `admits` was STRUCTURALLY INCAPABLE of admitting any board on which the // flip is ever taken: all 6 cells it passed (seeds 2, 4, 25, 34, 35, 40) had ZERO flips in 6/6 // playouts. The 15% pass rate was exactly the set of cells where this module's whole mechanism // never fires. The grace makes the flip a RETRACTABLE confession — the console round-trip becomes // load-bearing, and taking the harm back before it sets is a real, playable act. // WHY 2, MEASURED (seeds 1..40 x 6 personas = 240 faithful playouts, raw cells, every=6; units: // cells for admission, runs for the rest): // K | admitted | admitted cells with >=1 flip | mate-downs | C-N posed in flip-bearing runs // 1 | 0/40 (0%) | 0/0 | 11/240 | 11 of 11 flip-bearing runs // 2 | 6/40 (15%)| 6/6 | 0/240 | 11 of 11 flip-bearing runs // 3 | 6/40 (15%)| 6/6 | 0/240 | 11 of 11 flip-bearing runs // 4 | 6/40 (15%)| 6/6 | 0/240 | 11 of 11 flip-bearing runs // K=1 IS THE OLD MECHANIC (no grace) and it now admits NOTHING: with `cn` correctly gated on the // flip (see `_parkStormAdmissible`), a cell must have a flip-bearing run to pass, and under the // one-way door every flip-bearing run is a `matelost` rejection. That 0/40 is the review's // structural finding stated as a number — the two bars were mutually unsatisfiable. // THE LAST COLUMN'S K=1 CELL IS MEASURED ON THE RUNS, NOT READ OFF `cn` (correction, 2026-07-20 // review; this cell previously read "0 of 11" and did not reproduce). `_parkStormAdmissible`'s own // `cn` counter is 0 at K=1, but only because the `matelost` early-return (see the persona loop above // `cn`'s declaration) fires before the pair loop that increments `cn` is ever reached — that is a // fact about the admission gate's short-circuit order, not about the runs. Measured directly at // K=1 (constant patched in memory, seeds 1..40 x 6 personas = 240 runs): flip-bearing runs 11, C-N // posed runs 43, C-N posed IN flip-bearing runs 11 — all 11 flip-bearing runs also pose C-N. // K=2 IS THE SMALLEST K THAT ADMITS CELLS ON WHICH THE FLIP IS TAKEN, and it does so completely: // all 6 admitted cells (seeds 9, 16, 19, 20, 23, 32) carry >= 1 flip, against 0 of 6 before this // change (the old admitted set 2, 4, 25, 34, 35, 40 had zero flips in 6/6 playouts). K=3 and K=4 // are IDENTICAL to K=2 on every column, and the reason is measurable rather than lucky: at K=2 no // faithful playout ever accumulates even TWO consecutive harm beats (0/240 mate-downs), so no // wider grace span can change a trajectory. Smallest-that-works wins, so 2. // DISCLOSED, because a green bar must not imply work it is not doing: at K >= 2 the `matelost` // rejection in `_parkStormAdmissible` is MEASURED-VACUOUS on these 40 seeds (0/240 mate-downs). It // is retained deliberately as a regression pin — it is the bar that says a faithful walk must not // cost the companion his plan — but it is not currently separating anything, and the next owner // should not read its 0 as evidence that the mechanic is finely balanced. // HOW FAR FROM TRIPPING, MEASURED (correction, 2026-07-20 review, widening the 0/240 above): `tick` // instrumented to read `dyn.storm.mateHeat` every beat, over seeds 1..200 x 6 personas = 1200 // faithful runs (unit: beats/runs as marked) — max `mateHeat` observed = 1 beat. 45 of the 1200 runs // land harm on the companion for at least one beat, but ZERO runs ever accumulate two CONSECUTIVE // harm beats. K=5 checked too and behaves identically to K=2 on every column, so this is not a // near-threshold effect. The board is not one beat from tripping `matelost` — it is STRUCTURALLY // INCAPABLE of presenting two consecutive harm beats, and the binding variable is the companion's // dwell time in the zone (his ring-3 contract-gem approach), not the width of the grace span. Raising K // further is provably inert. // TEMPO IS NOT THIS MODULE'S LEVER and PARK_STORM_EVERY stays 6: bypassing the dead/matelost // prefilter and running `_parkStormSignature` on all 40 seeds directly gave 0/40 rejections with // 25/40 cells reaching it normally, so `sig 0` is genuine non-binding, not flood's "nothing // survived to be counted" artifact. const PARK_STORM_GRACE = 2; // consecutive harm beats before the companion drops const PARK_STORM_CORE_R = 5; // rings >= 5 are CORE: never storm const _PARK_STORM_DIRS = [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }]; // _parkStormRing(n, x, y): same Chebyshev-ring shape as flood's _parkFloodRing, re-derived for // storm's own board size (n=15) rather than shared — the two boards' interiors are different widths // (13 vs flood's 15), so a shared helper would need a size parameter threaded through both modules // for no reader; keeping it local matches the project's one-module-one-block convention. function _parkStormRing(n, x, y) { return Math.min(x - 1, y - 1, n - 2 - x, n - 2 - y); } // _parkStormBuild(cell): the y23 board — pure PUBLIC geometry (reads ONLY cell.seed; C1: no persona // parameter exists), runtime-compatible with parkStart/parkStep. Frame (15x15): perimeter wall, a // 13x13 interior of 5 scheduled concentric rings (0..4) plus a 3x3 CORE (rings 5+) that never storms. function _parkStormBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_STORM_N, c = (n - 1) >> 1; const r = rng((seed * 2027 + 6311) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const K = (x, y) => y * n + x; const sd = cs(0, 3), d = _PARK_STORM_DIRS[sd]; // console face (anti-mimic axis) const lat = { x: Math.abs(d.y), y: Math.abs(d.x) }; const consolePos = { x: c + 2 * d.x, y: c + 2 * d.y }; // core-rim cell on the drawn face const spawn = { x: c - d.x * 4 + lat.x * cs(-1, 1), y: c - d.y * 4 + lat.y * cs(-1, 1) }; const fl0 = cs(0, 1) ? 1 : -1; const g0 = { x: c + lat.x * fl0 * 4 + d.x * cs(-1, 1), y: c + lat.y * fl0 * 4 + d.y * cs(-1, 1) }; // ring~2 // g1: ring~2 (measured directly — see the Task 1 report; the naive "ring~3" carried over from // flood's own comment does not hold at storm's smaller board radius, PARK_STORM_N=15 vs flood's 17, // one ring shorter edge-to-centre. Left on ring~2, one ring out from g0, which is still a valid // outer chain gem; re-tuning its exact ring is later-task/measurement work, not a Task 1 concern). const g1 = { x: c + d.x * 4 - lat.x * fl0 * cs(2, 3), y: c + d.y * 4 - lat.y * fl0 * cs(2, 3) }; const gC = { x: c - d.x + fl0 * lat.x, y: c - d.y + fl0 * lat.y }; // core gem const g2 = { x: c + d.x * 3 - lat.x * fl0 * 2, y: c + d.y * 3 - lat.y * fl0 * 2 }; // companion contract gem, ring~3 const station = { x: c - d.x * 4 + lat.x * fl0 * 5, y: c - d.y * 4 + lat.y * fl0 * 5 }; const wall = new Set(), walkway = new Set(), verge = new Set(), deep = new Set(); const coreKeys = new Set(), coreRimKeys = new Set(), rings = []; for (let i = 0; i < PARK_STORM_CORE_R; i++) rings.push([]); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = K(x, y); if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } const rr = _parkStormRing(n, x, y); if (rr >= PARK_STORM_CORE_R) { coreKeys.add(kk); walkway.add(kk); continue; } if (rr === PARK_STORM_CORE_R - 1) coreRimKeys.add(kk); // rim IS ring 4: storms last, holds console rings[rr].push(kk); (rr <= 1 ? verge : walkway).add(kk); } for (const rw of rings) rw.sort((a, b) => a - b); // deterministic order (never read as a set) const ringOf = new Array(n * n).fill(-1); for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) if (!wall.has(K(x, y))) ringOf[K(x, y)] = _parkStormRing(n, x, y); // NO DEEP FIELD (channel purity, see the header): distDeep = Infinity everywhere keeps the shipped // static-C attitude's own engagement/preference vacuous, so a later task's storm facet is the entire // safety read here — same discipline as flood's design law 1. const distDeep = new Array(n * n).fill(Infinity); const tokens = [ { x: g0.x, y: g0.y, v: cs(2, 3), alive: true, guard: false }, { x: g1.x, y: g1.y, v: cs(2, 3), alive: true, guard: false }, { x: gC.x, y: gC.y, v: cs(1, 2), alive: true, guard: false }, { x: g2.x, y: g2.y, v: cs(1, 2), alive: true, guard: false }, // companion's contract gem ]; const park = { N: n, seed, k: 0, fieldMech: 'storm', walkway, verge, deep, distDeep, storm: { rings, every: PARK_STORM_EVERY, coreKeys, coreRimKeys, consoleKey: K(consolePos.x, consolePos.y), ringOf }, clusters: tokens.map(t => ({ x: t.x, y: t.y, v: t.v })), chain: [0, 1, 2], contracts: [{ gem: 3, station }], retire: { x: c, y: c }, spawn, companionSpawn: { x: station.x, y: station.y }, trig: 5, cap: 140, minTurns: 12, cautionD: 2, damage: 1, cell: _parkStormCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: station.x, y: station.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false } }; park.dyn = _parkDynInit(st); // OPT-IN runtime container — the standard 5-field shape // dyn.storm APPENDED to the standard dyn (not replacing it): Set/array/plain-object fields only, so // _parkDeepClone (generic, C3) copies it structurally on every parkCeiling search fork without // needing to know its shape. ticksTaken (the DoT counter Task 2's `tick` increments) is declared // HERE, at 0, rather than created lazily on first tick — the structural-clone argument above is // only sound for members that EXIST at clone time, and a field that appears mid-run falls outside // it (whole-branch review 2026-07-20). park.dyn.storm = { polarity: 'me', flips: [], mateDown: false, mateHeat: 0, ticksTaken: 0, zone: new Set() }; return st; } // _parkStormCell(seed): the module's PUBLIC play-cell. `mech.fieldMech: 'storm'` — the FIELD // analogue of the shipped `mech.moveMech`. No new task `kind` (the y12/y10 decision, unchanged). function _parkStormCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'storm' } }; } // ---- THE PUBLIC CLOCK READS (Task 1). Every one of these is a pure function of the PUBLIC beat + // the seed-pure timetable — the walker can SEE all of it, which is the only reason a caution read // over it is a read at all (flood's design law 2, unchanged). No persona symbol reaches any of them // (C1). NOTHING IN THIS BLOCK ADVANCES ANYTHING: these are reads only. What moves dyn.beat is the // engine's own step, and what mutates dyn.storm is Task 2's `tick`, registered further down in // PARK_FIELD_MECHS.storm — keeping the clock reads pure of both is what lets any caller (reads, HUD, // a gate) ask the schedule a question without moving it. // _parkStormNextIdx(st): how many rings have gone == the index of the ring that storms NEXT. function _parkStormNextIdx(st) { const sm = st.park.storm, dyn = st.park.dyn; return Math.min(sm.rings.length, Math.floor((dyn ? dyn.beat : 0) / sm.every)); } // _parkStormNext(st): THE PREVIEW — the cell keys of the ring that storms next (dotted, flood's // preview idiom). Empty once the schedule has exhausted rings 0..4. function _parkStormNext(st) { const i = _parkStormNextIdx(st); return i < st.park.storm.rings.length ? st.park.storm.rings[i].slice() : []; } // _parkStormMargin(st, key, next): rings of daylight between a cell and the field's next bite. // THE EXHAUSTED CLOCK (whole-branch review 2026-07-20, Important). `_parkStormNextIdx` CLAMPS at // `rings.length`, and `_parkStormNext` correctly returns [] there — the schedule is OVER. Computing // `rr - next` against that clamped index used to hand back a finite, small margin for core cells // (ringOf 5..6, which never storm), so a walker safe on ground the board can never touch read as // one ring from a bite. Fixed HERE rather than at the two read sites, because the margin is the // quantity that is wrong: every present and future caller (C.engaged, ctx.mateExposed, C.prefer's // pool filter, any later HUD read) inherits the correct answer from one place, and none of them has // to learn the clamp's edge case. +Infinity is the honest value — there is no next bite, so there is // no distance to it — and it composes: `<= MARGIN` and `<= 1` both go false, `>= MARGIN` in // C.prefer's pool goes true for every candidate, which is exactly the no-op the seam trap requires. // The wall clause still wins: an off-board key is not ground, exhausted schedule or not. function _parkStormMargin(st, key, next) { const rr = st.park.storm.ringOf[key]; if (rr < 0) return -Infinity; // a wall is not ground return next >= st.park.storm.rings.length ? Infinity : rr - next; // schedule over: nothing bites } /* ---- y23 STORM — THE FILTER LAYER (Task 3): faithful playout, field signature, admissibility. flood's three siblings (engine.js ~13134-13194) are the direct genealogy; what differs is the TERMINAL. flood rejects on reason 'drown' (water is instant death); storm has no terminal of its own at all — the field bills HEARTS, so its analogous failure is the ENGINE's own reason 'death' (♥0, parkStep ~8169). Everything else — signature-then-pairs, tally-loudly-on-the-real-path — is flood's shape verbatim. */ const PARK_STORM_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // _PARK_STORM_WHYS: the LOUD rejection tally, hung off `admits` — the predicate the campaign really // calls (the y10 "no module generator, but never lose the counter" decision, engine.js ~13206). // READ IT AS A TRIPWIRE, NOT A BOUND: monotone process global, never reset, and a ZERO can mean "an // earlier filter rejected everything first" rather than "this never fires" (the every=6 `nocn 0` // trap, engine.js ~13217). NOTE `complete` is the ODD ONE OUT and is the brief's own naming: here it // counts cells ADMITTED, not cells rejected — unlike flood's `complete`, which is a rejection // counter. Every other key is a rejection reason. // AND IT IS A FIRST-REASON HISTOGRAM, NOT A CAUSE HISTOGRAM: every branch in `_parkStormAdmissible` // early-returns, so a rejected cell contributes to exactly ONE key — the first bar it failed. A cell // counted under `dead` may also have had no C-N scene, and would never say so. Read a key as "how // many cells got no further than here", never as "how many cells have this defect". const _PARK_STORM_WHYS = { complete: 0, dead: 0, sig: 0, matelost: 0, unexpressed: 0, nocn: 0, norec: 0 }; // _parkStormPlay(cell, persona): one persona-faithful playout on a FRESH board (every consumer of a // playout rebuilds — parkStep mutates what it is handed). function _parkStormPlay(cell, persona) { const P = parkStart(_parkStormBuild(cell)); while (!P.over) parkStep(P, parkOracleMove(P, persona)); return P; } // _parkStormSignature(playouts): the field's OWN visible signature, aligned with PARK_PERSONAS. // TWO clauses, and the second is storm's, not flood's: (1) GOAL-top banks strictly more chain value // than SAFETY-top — it buys the outer gems before their ring goes under, safety lets them wash; // (2) SAFETY-top NEVER TAKES A SINGLE STORM TICK (dyn.storm.ticksTaken === 0, unit: ticks). flood's // analogue read a margin; storm reads the DoT counter directly, because on this board the harm is // the meter. Filter-level, like every other module signature: it gates which candidate survives the // sweep, never the measured spread. function _parkStormSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); let gMin = Infinity, sMax = -Infinity; for (let i = 0; i < playouts.length; i++) { const P = playouts[i]; if (top[i] === 'goal') gMin = Math.min(gMin, P.st.score[0]); if (top[i] === 'safety') { sMax = Math.max(sMax, P.st.score[0]); if ((P.st.park.dyn.storm.ticksTaken | 0) !== 0) return false; // safety-top never stands in it } } return gMin > sMax; } // _parkStormAdmissible(cell): the generate-then-filter PLAYABILITY gate over the FULL persona SET (a // constant, so the accepted board stays a pure function of the public cell). A cell is admitted iff // every persona's faithful playout COMPLETES (a faithful walk that dies to its own field is a // GENERATOR bug, not a hard board) and leaves the companion STANDING (a faithful walk that drops him // means the board forces the confession rather than posing it), the field signature separates, the // console scene actually POSES C-vs-N, and every pair the board poses is blind-recovered in the // demonstrated direction. function _parkStormAdmissible(cell) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = _parkStormPlay(cell, persona); if (P.reason !== 'complete') { _PARK_STORM_WHYS.dead++; return false; } if (P.st.park.dyn.storm.mateDown) { _PARK_STORM_WHYS.matelost++; return false; } playouts.push(P); } if (!_parkStormSignature(playouts)) { _PARK_STORM_WHYS.sig++; return false; } let posed = 0, cn = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; // THE FLIP IS WHAT MAKES THE CONSOLE SCENE A SCENE (a Task-3-review correction). `cn` used to be // incremented on C-N divergence from ANY cause, while its comment claimed it counted "the console // scene, actually posed" — an overclaim, and a load-bearing one: measured over seeds 1..40 x 6 // personas, 32 of 43 C-N posings happened in runs that took ZERO flips. Now that the grace period // makes the flip a move a faithful walker can afford, the flip-bearing run IS the point of this // module, so `cn` is gated on BOTH: this run took at least one polarity flip AND C-N diverged in // it. The counter now means what its name and the `nocn` rejection have always claimed. const flipped = playouts[i].st.park.dyn.storm.flips.length > 0; for (const pair of PARK_STORM_PAIRS) { if (!(parkPairExpressed(_parkStormBuild(cell), playouts[i].moves, pair) > 0)) continue; posed++; // pair[1] tested too, not just pair[0]: `pair[0] === 'C'` alone was correct only by the // accident that PARK_STORM_PAIRS happens to hold exactly one C-leading pair, so adding a // ['C','G'] would have silently miscounted (a Task-3-review correction). if (pair[0] === 'C' && pair[1] === 'N' && flipped) cn++; // the console scene, actually posed const expect = _parkPushPairDir(persona, pair); // att-key pair direction (module-agnostic) if (!parkRecoverPairLex(_parkStormBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_STORM_WHYS.norec++; return false; } } } if (posed === 0) { _PARK_STORM_WHYS.unexpressed++; return false; } // THE PLAN'S OWN ADMISSION CONDITION for y23: approve only a seed where the POLARITY FLIP actually // separates C from N. If C-N is never posed the cell has not earned its slot, whatever else it // does — so this is a rejection, not a note (flood's identical bar, engine.js ~13189). if (cn === 0) { _PARK_STORM_WHYS.nocn++; return false; } _PARK_STORM_WHYS.complete++; return true; } function parkStormWhys() { return { ..._PARK_STORM_WHYS }; } /* ---- y23 STORM field module — RUNTIME (Task 2: polarity toggle + advancing field + DoT + mate-down, plan 2026-07-20). Task 1 built the seed-pure board and the public clock reads above; this task wires the two per-step hooks that make it a LIVING mechanic — onEnter (the CONSOLE's polarity confession) and tick (the field's advance, the gem wash, and the harm itself) — plus the mate-only legalMask that makes a mate-down stick. The literal below is the module's SINGLE registration point, so Task 3's contributions land in it too and are marked where they sit: the three minds (`reads`) and the campaign surface (`cell`/`admits`, whose `_parkStormAdmissible` is defined in the filter block just above). Read the block headers, not the key order, for who built what. THE FREEZE, and why it is a legalMask and not `dyn.downed` (a correction to the brief, adjudicated by the project owner before this task was written). y3's mate-freeze lives ENTIRELY inside `PARK_FIELD_MECHS.downed.legalMask` (engine.js ~11594) — it is that module's own private read of `dyn.downed`, reached only on a board whose `fieldMech` IS 'downed'. Only one field module is ever active per board (`_parkMech`, engine.js ~7414 — a single `PARK_FIELD_MECHS[id]` lookup), so on a storm board `PARK_FIELD_MECHS.downed.legalMask` never runs; setting `dyn.downed` here would set a field nothing reads. The companion planner's only GENERIC freeze is `plan.stuck` (`_parkCompanionPlan`, engine.js ~7578, guarantee 1 in the PARK_FIELD_MECHS header ~7398) — reached by making his BFS target unreachable through his own domain's mask, `_parkMaskOf(P, 'mate')` (engine.js ~7420, called from `_parkCompanionPlan` at ~7594). So storm registers a MATE-ONLY legalMask, in y3's exact proven shape (masking every cell for 'mate' once down) — the walker's and the route metric's cells are never touched by it: storm harms by DoT, it does not block (design spec, "극성" section). No self-recovery counter, unlike y3's downed (which heals on a timer) — mate-down here is a confession made visible, permanent for the run once made. */ PARK_FIELD_MECHS.storm = { build: _parkStormBuild, // THE FREEZE — his domain ALONE ('me'/'route' never masked; see the header above). legalMask: (P, key, who) => { if (who !== 'mate') return false; const dyn = P.st.park.dyn; return !!(dyn && dyn.storm && dyn.storm.mateDown); }, // CONSOLE entry = polarity toggle. Fires on ENTRY only — 'stay' never re-toggles (standing on the // console is not a repeated confession, only arriving at it is). onEnter(P, ev) { const st = P.st, dyn = st.park.dyn; if (ev.toKey !== st.park.storm.consoleKey || ev.mvKey === 'stay') return; dyn.storm.polarity = dyn.storm.polarity === 'me' ? 'mate' : 'me'; dyn.storm.flips.push({ beat: dyn.beat, to: dyn.storm.polarity }); }, // Every beat: (1) THE SCHEDULE advances — a pure function of the beat (flood's same ring-reveal // idiom), dyn.storm.zone accumulates the keys of every ring that has gone. (2) ANY OUTER CHAIN GEM // caught in a newly-stormed ring washes away (last-chance, flood's idiom) — EXCEPT the companion's // OWN contract gem, identified by `park.contracts[0].gem` (an INDEX, not `t.guard` — the brief's own // instruction, so the exemption survives even if guard flags shift later): washing it would void his // contract and the care scene it stages would vanish outright. A decision this task's brief flags // for revisit at measurement time, not before. (3) THE HARM lands by POLARITY — 'me' costs the // walker a heart per beat he stands in the field (`dyn.storm.ticksTaken`); 'mate' does NOT drop // the companion on the spot but starts a clock — `dyn.storm.mateHeat` counts CONSECUTIVE beats of // harm actually landing on him, and only at PARK_STORM_GRACE (K) beats does he go down // (`dyn.storm.mateDown = true`, permanent — the legalMask above is what makes that stick). Anything // short of K is RETRACTABLE, and the counter resets on the polarity flipping back OR on him leaving // the zone. That grace is the mechanic, not a detail: see THE GRACE PERIOD note in the body below // for where the reset lives and why it is one predicate, and the ruling above PARK_STORM_GRACE for // why K=2. (An earlier instant-drop form of (3) is what the grace replaced.) ♥0 termination is the // ENGINE's own death check (parkStep, re-read right after tick returns) — not this hook's job. tick(P, ev) { const st = P.st, dyn = st.park.dyn, sm = st.park.storm; const gone = Math.min(sm.rings.length, Math.floor(dyn.beat / sm.every)); for (let i = 0; i < gone; i++) for (const kk of sm.rings[i]) dyn.storm.zone.add(kk); const contractGem = st.park.contracts[0] ? st.park.contracts[0].gem : -1; for (let ti = 0; ti < st.tokens.length; ti++) { const t = st.tokens[ti]; if (!t.alive || ti === contractGem) continue; // contract gem: exempt const tk = t.y * st.N + t.x; if (dyn.storm.zone.has(tk) && !sm.coreKeys.has(tk)) t.alive = false; // washed (last-chance) } const meKey = st.pos[0].y * st.N + st.pos[0].x; const mateKey = st.pos[1].y * st.N + st.pos[1].x; if (dyn.storm.polarity === 'me' && dyn.storm.zone.has(meKey)) { P.hearts--; dyn.storm.ticksTaken++; } // THE GRACE PERIOD (owner ruling, 2026-07-20). The harm LANDS on the companion exactly when the // polarity points at him AND he is standing in the field; `mateHeat` counts how many CONSECUTIVE // beats that has been true, and he only drops at PARK_STORM_GRACE. Anything short of that is // retractable — which is the point: the console round-trip is now load-bearing, and taking the // harm back before it sets is a real, playable act rather than a gesture after the fact. // // WHERE THE RESET LIVES, AND WHY IT IS ONE PREDICATE AND NOT TWO. The reset is HERE, in `tick`, // as the `else` of the same condition that increments — NOT in `onEnter` on the flip back to // 'me'. Two reasons. (1) `onEnter` only ever sees the POLARITY half; it cannot see whether the // mate is in the field, so a reset written there would have to be paired with a second, separate // reset here for the off-zone case, and two sites that must agree on one invariant is how the // invariant rots. `tick` is the only place that can evaluate the whole predicate, and it runs // every beat, so one site is sufficient and authoritative. (2) `onEnter` fires on ENTRY to the // console; a walker who flips to 'mate' and back within one beat would otherwise never have the // count start at all, which is a different mechanic from the one that was ruled. // // "CONSECUTIVE" BREAKS ON THE MATE LEAVING THE ZONE TOO, not only on the polarity flipping back. // The counter is therefore "beats of harm actually landing", not "beats since the confession". // Chosen deliberately: the alternative — reset on polarity only — would keep billing a companion // who has walked out of the field entirely and would drop him on a beat where he is standing on // safe CORE ground, which no reader of this board could account for. It also keeps the counter's // meaning identical to the harm it meters, which is what makes `mateHeat` legible in a render. // The cost is disclosed: the walker is not the only party who can reset the count, so the flip's // consequence is not his alone to control. That is the honest physics of a field that harms by // POSITION, and it matches the DoT on the walker's own side, which also stops the moment he // steps off. const mateInHarm = dyn.storm.polarity === 'mate' && dyn.storm.zone.has(mateKey); if (!dyn.storm.mateDown) { if (!mateInHarm) dyn.storm.mateHeat = 0; // the harm is not landing: reset else { dyn.storm.mateHeat = (dyn.storm.mateHeat | 0) + 1; // he drops where he stands; the legalMask above is what makes that stick. Permanent for the // run — retracting AFTER the K-th beat does not heal him, only before it saves him. if (dyn.storm.mateHeat >= PARK_STORM_GRACE) dyn.storm.mateDown = true; } } }, // ---- THE THREE MINDS (Task 3). NO NEW SCORING CHANNEL: these only NARROW the shipped // PARK_ATTITUDES. G is untouched — the chain IS the outer-gem pull and the ring clock is what // makes it urgent (flood's design law, unchanged). // // !! THE SEAM TRAP, and it governs both hooks below. `_parkReads` (engine.js ~7830) intersects a // mechanic's prefer() whenever the COMBINED attitude is engaged — i.e. whenever the SHIPPED // attitude engaged it — NOT when the mechanic's own engaged() said so. So EVERY prefer here // runs in states its own engaged() calls dead, and returning {} there does NOT veto: it makes // that mind INERT (a care-led walker who quietly stops caring). Both hooks therefore fall back // to the FULL legal set in the states they do not own, which intersects as a no-op and leaves // the shipped read exactly as it was. HOW each hook reaches that fallback differs, and the // difference matters: N guards on the same predicate as its own engaged(), but C guards on // POLARITY ONLY — its second dead state (polarity 'me' with margin above the bar) is a no-op by // RING-ADJACENCY GEOMETRY, not by the guard. That lemma is stated and pinned below (see "THE // OTHER DEAD STATE" on C, and Y23-STORM-C-NOOP); do not restate it here as a guard, which is // what the Task-3 report got wrong. flood's N hook records the same trap at engine.js ~13402; // the plan states it as a global constraint ("빈 legal set 금지"). reads: { ctx(P) { const st = P.st, dyn = st.park.dyn, sm = st.park.storm; const nextIdx = _parkStormNextIdx(st); const mateMargin = _parkStormMargin(st, _parkKey(st, st.pos[1]), nextIdx); return { nextIdx, consoleKey: sm.consoleKey, polarity: dyn.storm.polarity, meMargin: _parkStormMargin(st, _parkKey(st, st.pos[0]), nextIdx), // EXPOSED = the design spec's "물든/다음 링 칸 위나 인접", rendered ring-wise: a cell 4-adjacent // to a stormed-or-next cell is itself at most one ring further in, so "on or adjacent" is // exactly margin <= 1. A mate already DOWN is not exposed — care cannot be owed to a plan // that is already frozen, and reading him as exposed forever would pin N open for the rest // of the run. mateExposed: !dyn.storm.mateDown && mateMargin <= 1, }; }, C: { // ONLY UNDER 'me'. When the polarity points at the companion the walker has no storm risk at // all, and a safety mind that keeps its distance from a field that cannot touch it is a // superstition, not a caution (design spec). engaged: (P, ctx) => ctx.polarity === 'me' && ctx.meMargin <= PARK_STORM_MARGIN, // KEEP >= 2 RINGS OF DAYLIGHT — and THE FLIP IS ALWAYS A SAFE ACT. That second clause is the // heart of this cell: stepping onto the console sends the harm to the mate, which drives the // walker's OWN risk to zero, so no safety read can honestly rank it below a retreat. It is // also what makes the C-N partition GEOMETRIC rather than a subtraction: park the walker one // ring outside the core rim at nextIdx 2 and the console (ring 4, margin 2) is the ONLY // candidate over the bar, so C = {the flip} exactly while N = everything else (flood design // law 4, console form; Y23-STORM-READS measures it). // The argmax fallback is not a softening of the >= 2 rule, it is what MAKES it a rule: a bare // threshold would switch safety OFF at full storm, the exact moment the cell means something. // !! THE OTHER DEAD STATE, AND WHY THE GUARD BELOW DOES NOT COVER IT (a Task-3-review // correction — the Task-3 report claimed this hook "guards on the same predicate as its own // engaged()", which is TRUE OF N BUT NOT OF C). C.engaged is a CONJUNCTION — me-polarity AND // meMargin <= MARGIN — while the guard below tests only the polarity half. So C.prefer also // runs in the state `polarity === 'me' && meMargin > PARK_STORM_MARGIN`, and there it falls // through to the pool filter rather than to the explicit no-op return. // IT IS STILL A NO-OP THERE, BUT BY GEOMETRY, NOT BY THE GUARD, and that is the invariant a // future edit would otherwise break in silence: // *** 4-ADJACENT CELLS DIFFER BY AT MOST ONE RING (Chebyshev, _parkStormRing). *** // Every candidate in `legal` is the walker's own cell or 4-adjacent to it, so every // candidate's margin is >= meMargin - 1. In this state meMargin >= MARGIN + 1, hence every // candidate clears `m(c) >= PARK_STORM_MARGIN` and the pool IS the full legal set — the // intersection is a no-op and the mind is never silently narrowed while its own engaged() // calls it dead. Note the argument holds for ANY value of PARK_STORM_MARGIN but NOT for a // ring metric whose 4-neighbours can jump two rings. Y23-STORM-C-NOOP pins BOTH halves — // the adjacency lemma directly on the board, and the no-op itself by enumeration. prefer(P, legal, ctx) { if (ctx.polarity !== 'me') return new Set(legal.map(c => c.k)); // NO-OP (the seam trap) const st = P.st; const m = (c) => _parkStormMargin(st, c.key, ctx.nextIdx); let pool = legal.filter(c => c.key === ctx.consoleKey || m(c) >= PARK_STORM_MARGIN); if (!pool.length) { // nothing clears the bar: take the highest ground let best = -Infinity; for (const c of legal) best = Math.max(best, m(c)); pool = legal.filter(c => m(c) === best); } // NEVER {}: either the pool filter kept something, or the argmax fallback keeps at least the // maximiser — and `legal` itself is never empty (the seam always re-admits 'stay'). return new Set(pool.map(c => c.k)); }, }, N: { // THE MATE IS IN IT (or one ring from it) AND STILL STANDING. engaged: (P, ctx) => ctx.mateExposed, // Under 'me' the harm is the walker's own and care's whole demand is DON'T HAND IT TO HIM: // every legal move except the flip. Under 'mate' the harm is already his, and care's demand // inverts — TAKE IT BACK, i.e. close on the console, which is the only cell that can. prefer(P, legal, ctx) { if (!ctx.mateExposed) return new Set(legal.map(c => c.k)); // NO-OP (the seam trap) const out = new Set(); if (ctx.polarity === 'me') { for (const c of legal) if (c.key !== ctx.consoleKey) out.add(c.k); // A board where the flip is his ONLY legal move: care has nothing left to ask for, and // saying so as a no-op is more honest than a veto it cannot enforce. if (!out.size) return new Set(legal.map(c => c.k)); return out; } const st = P.st, n = st.N; const cp = { x: ctx.consoleKey % n, y: (ctx.consoleKey - ctx.consoleKey % n) / n }; let best = Infinity; for (const c of legal) best = Math.min(best, manhattan(c, cp)); for (const c of legal) if (manhattan(c, cp) === best) out.add(c.k); return out; // never {}: the minimiser is always in it }, }, }, // ---- THE CAMPAIGN SURFACE. `cell` is the public play-cell the crossing slot seats; `admits` is // the generate-then-filter predicate campaign.js sweeps with (_parkCrossingPlayCell / // _parkFieldCellAdmits). The y23 slot itself is seated (campaign.js PARK_CROSSINGS, commit // 52f3806) — ship:false + open:true, a marked preview pending the pairing measurement below. cell: _parkStormCell, admits: _parkStormAdmissible, }; // PARK_STORM_SHIPPABLE — the MODULE's own measured ship pin (the CAMP-CROSS-SWEEP F4 cross-check // reads this the same way it reads PARK_FLOOD_SHIPPABLE / PARK_TOWER_SHIPPABLE / etc., one entry // per field mechanic — see engine.test.js's FIELD_SHIPPABLE map). MEASURED (seeds 1..40 x 6 // personas = 240 faithful playouts, raw cells `_parkStormCell(seed)`, no generator, every=6, // PARK_STORM_GRACE=2 — the admissible-cell tally `_parkStormAdmissible` reaches after the grace // fix, see the K-table above PARK_STORM_GRACE): admits 6/40 cells (seeds 9, 16, 19, 20, 23, 32), // and ALL 6 admitted cells carry >= 1 polarity flip. That is the MODULE's own admission bar, not a // blind order-recovery measurement — no persona-order gate has been run on it yet, and the slot's // demo(push)->play(storm) PAIRING bar (the y22-style promotion measurement, PARK_Y23_SHIPPABLE) // is a DIFFERENT quantity that a later task computes. So this pin stays `false` pending that work: // derive-never-assert forbids writing `true` without the measurement, and none has been taken. // The next owner to touch this: replace the literal with a derived call once the ship gate exists, // exactly as PARK_TROLLEY_SHIPPABLE does it (`_parkTrolleyRecovers(_parkTrolleyCell(...))`, computed // after registration so the claim is a PIN a regression must break) — never leave it a hand-set // `true`. PARK_CARRY_SHIPPABLE is NOT the same pattern: it is itself a hand-set literal (`= true`), // deliberately DECLARED rather than inferred at gate time (see its own comment, engine.js 14371-14373) // — a pin the gate re-checks, not a tautology the gate re-derives. Do not follow that shape here. const PARK_STORM_SHIPPABLE = false; /* ============ CARRY FIELD MODULE (y16 "one stone, three spends", Task 3, plan 2026-07-14) ============ */ /* A SELF-CONTAINED park FIELD module registered through PARK_FIELD_MECHS. The movement verb is the walk's; the goal grammar is COLLECT (a three-TYPE quota — R1). What is new is that the walker carries a TOOL, and the tool is SCARCE: there is exactly ONE stone on the board, three places it can be spent, and the spend is IRREVERSIBLE. Where you spend it is a confession of your mind-ORDER, and that is the entire cell. PICK IT UP — the stone lies at the CROSSROADS, on the only cell out of the spawn stub. Every walker takes it (dyn.held) on turn 2. The pickup is free and universal; the SPEND is the decision, and it is posed from that turn to the end of the run. FORD (N care) — drop it in the stream. That one water cell becomes ground, and it is the COMPANION's ONLY crossing to his last contracted gem (his own planner is `stuck: true` until you do it — the seam's guarantee 1, read directly). The walker gets NOTHING from it: there is no chain gem across the water. COVER (C safety) — cap the broken plank in the west promenade. The dry, walkway-only route to the north is mended; without it the safety mind's only lawful way north is the long way round the east. BREAK (G goal) — smash the glass pane in the hedge. It is the SHORT way north, and its floor is a scree of old glass (park.deep): the shortcut you open costs a heart to walk. Goal buys distance with the body — the park's oldest stake, re-posed. THE THREE MINDS, read through the SHIPPED PARK_ATTITUDES (no new scoring channel). Each mind gets ONE facet, and all three have the SAME two-leg shape — this is the template tasks 5/6/7 copy: leg 1 (!held) steer to the STONE (all three minds agree — no evidence yet) leg 2 (held) steer to MY OWN spend site (the three minds now want three directions) The fork is GEOMETRIC, not scored: from the crossroads the glass is NORTH, the plank is WEST and the stream is EAST. The three compliant sets are DISJOINT there — a partition, not a subtraction, which is the one shape that can award a C-vs-N pair at all (y10's finding; y12's standing gap). DESIGN LAWS (each one earns its place — and four of them are seam traps, see the notes): 1. THE ROUTE DOMAIN MUST SEE A SPEND SITE THE WALKER CAN OPEN. A spend site is legal ONLY through legalAdd, and legalAdd has NO 'route' domain — so the site's route-metric distance is Infinity unless legalMask lets the metric through. parkOracleMove's argmin is `if (m < bestM)` seeded at Infinity: on a state where the lexical filter left the walker the BUMP AND NOTHING ELSE, an Infinity metric returns `best = null` -> 'stay' -> and 'stay' is not in the compliant set, so parkStep rejects the oracle's own move as INPUT NOISE. The run then limps to reason:'noise' with the stone still in hand. So legalMask opens all three sites to the 'route' domain WHILE THE WALKER HOLDS THE STONE — which is also exactly TRUE: a shortcut you are carrying the key to is a shortcut. Drop the stone elsewhere and they shut again. 2. THE GLASS IS A ROUTE, NOT A BAIT. Because of law 1 the fast field sees the glass open only while the stone is held; unarmed, the beeline goes the long way round. The alternative — a fast field that always sees through the pane — walks a stoneless goal-led walker up to the pane, finds his ONLY distance-reducing move illegal, empties G's preference, and leaves him on the shoulder forever (y12's measured livelock). Conditioning on `held` removes the trap. 3. THE SCREE IS DEEP AT BUILD, NOT ON THE BREAK. The brief asks the break to SCATTER a shard that turns an adjacent tile deep at runtime. It does not, and the reason is a SEAM ASYMMETRY the three cells behind this one must know: `park.deep` is mutable, but `park.distDeep` — the array the SHIPPED safety attitude reads for BOTH its engagement and its preference — is built ONCE and never recomputed. A deep cell added at runtime therefore costs a heart that the safety mind CANNOT SEE: C would walk into it. So the pane's fallout apron is deep from turn 1 (it is drawn as scree; it is public; the caution band prices it before and after), and the break's price is the heart the shortcut costs to walk. Same stake, honest reads. 4. EVERY GEM IS REACHABLE WITH NO SPEND AT ALL. The stone is a shortcut, never a key. Three ways cross the hedge — the west plank (holed), the glass (goal's), and the east lane (open, long) — so a persona whose stone went elsewhere always has a lawful route left. Without this the care-led orders that spend on the ford cannot finish, and a cell where two personas cannot complete is not a measurement, it is a broken board (global constraint 3). 5. THE COMPANION HAS TWO CONTRACTS. His first gem is on the south promenade, ON the walker's line — that is the SHIPPED contested-gem scene, and it is where G-N is posed. His LAST is across the water, and from his second station there is no path to it: `plan.stuck`, and he HOLDS (seam guarantee 1). One contract would have left him frozen in a corner all game and the care read with nothing to say but the ford. */ const PARK_CARRY_N = 16; const PARK_CARRY_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // _parkCarryBuild(cell): the y16 board — pure PUBLIC geometry (reads ONLY cell.seed; C1: no persona // parameter exists), runtime-compatible with parkStart/parkStep. Frame (16x16): a perimeter wall; a // HEDGE across the middle (row hy) with exactly THREE ways through — the west lane, the GLASS pane, // the east lane; a spawn stub in the south whose only exit carries the STONE; a two-row promenade in // the north holding all three chain gems; and a sealed NE POCKET behind a stream, which is where the // companion's last contract lies. Everything the lanes do not claim falls to verge/deep by the // park's own two-tone partition, so both halves carry real meadow. Outside the walk archetype pools // by construction (they stay append-only and walk-only — the PUSH/SLIDE/y12 precedent). function _parkCarryBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_CARRY_N; const r = rng((seed * 1373 + 5087) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); // TWO MIRRORS, NOT ONE — and the second one was bought with a measurement. The E/W mirror is the // PUSH module's precedent. The N/S mirror is here because the crossing's ANTI-MIMICRY filter caught // this board leaking: with the spawn always SOUTH and the gems always NORTH, "walk up" was a // board-INVARIANT that meant `goal` on every seed — so a goal-led demo's move stream, replayed here, // still walked toward the pane, still landed on the fork, and still earned a genuine G-vs-C recovery // it had not earned (1 mimic recovery on all 128 swept candidates; _parkCrossIncongruent rejected // every one and the slot exhausted its budget). A mimic that rides a constant is not mimicking the // PERSONA, it is mimicking the compass. Flipping the compass by seed takes the constant away. const flip = r() < 0.5, flipY = r() < 0.5; const M = (x) => flip ? n - 1 - x : x; const MY = (y) => flipY ? n - 1 - y : y; const K = (x, y) => MY(y) * n + M(x); // every coordinate below is written UNMIRRORED const PT = (x, y) => ({ x: M(x), y: MY(y) }); // ...and every POINT goes through the same two mirrors const hy = 8; // the hedge row const gx = 8; // the glass column (= the spine = the spawn stub) const wx = 2, ex = 13; // the west lane / the east lane (the two ways round) const sy = 4; // the stream row (seals the NE pocket) const pity = cs(9, 11); // WHERE the plank is broken, on the west lane const fordx = cs(11, 13); // WHICH stream cell takes the stone // HIS FIRST GEM SITS EAST OF THE WASHOUT, on the stretch of the fast row where the goal mind's route // and the caution band's route have not yet parted. West of the washout they HAVE parted (goal wades // the fast row, safety takes the third row round), so a gem seated there is simply never met by a // safety-led walker and the G-vs-N pair it exists to pose goes unposed — which is precisely the pair // those two personas were missing (measured 0/16). East of it, every persona walks over it. const cgx = 10; const g1y = cs(2, 3); // the north-west chain gem, up the west lane const wall = new Set(), walkway = new Set(), verge = new Set(), deep = new Set(), water = new Set(); const lane = new Set(); // the walkable skeleton, before the two-tone partition const add = (x, y) => lane.add(K(x, y)); add(gx, 14); add(gx, 13); // the spawn stub (1-wide: its exit is unavoidable) for (let x = wx; x <= ex; x++) add(x, 12); // the south promenade // THE PANE HAS TWO APPROACHES, AND THEY ARE THE SAME LENGTH. This is the cell's second measuring // instrument and it is worth stating in full, because without it the board reads only the TOP mind. // At the fork the three facets are DISJOINT, so the observed move is the top mind's and it awards // top>other1 and top>other2 — but NOTHING about other1 vs other2 (both losing orders prescribe the // same move, so no pair between them is discriminated). A trajectory missing one pair is not an // ORDER, and parkRecoverOrder returns null on it. Measured, before this loop existed: all three // pairs read correctly and never backwards, and blind order recovery was 0/48 — every persona // short exactly one pair, the one between its two SUBORDINATE minds. // So the goal mind is given a state where it is INDIFFERENT: from the crossroads the pane is // equidistant round the west arm and the east arm. Goal allows BOTH; safety (which wants the // plank, west) allows only the west arm; care (which wants the ford, east) only the east. A // goal-led walker therefore drifts to the side HIS SECOND MIND cares about — and which way he // walked round is a C-vs-N reading taken off a walker who never spends the stone on either of them. for (let y = 9; y <= 11; y++) { add(gx - 1, y); add(gx + 1, y); } add(gx, 9); // the arms rejoin: the cell the pane is bumped from // THE TWO WAYS ROUND THE HEDGE, and they are UNBROKEN COLUMNS on purpose (design law 4). A lane with // a one-cell gap in it is not a lane: the missing cell falls to VERGE by the two-tone partition, the // caution band prices it distDeep 1, and the SHIPPED safety read then forbids the only way through — // so a safety-led walker who has already mended the plank still cannot reach a single gem, and caps // out with a full purse of hearts and an empty score. (Measured: 0/6 completing. The lane must be // walkable BY THE MIND THAT NEEDS IT, end to end, or it is scenery.) for (let y = 1; y <= 12; y++) add(wx, y); // the west lane (the plank is broken somewhere on it) for (let y = 5; y <= 12; y++) add(ex, y); // the east lane: the OPEN way round, and the long one for (let x = wx; x <= ex; x++) { add(x, 5); add(x, 6); } // the north promenade (its two rows) for (let x = wx; x <= 6; x++) add(x, 7); // ...and a THIRD row in the west: the WASHOUT's dry way round for (let x = 10; x <= 14; x++) for (let y = 1; y <= 3; y++) add(x, y); // the NE pocket (the companion's) // THE HEDGE. Wall across row hy except the three ways through: the west lane (wx), the GLASS (gx) // and the east lane (ex). Sealing x=1/3/14 too is what makes the count exactly three — leave them // open and the meadow itself is a fourth way, and the pane stops being a decision. for (let x = 1; x <= n - 2; x++) if (x !== wx && x !== gx && x !== ex) wall.add(K(x, hy)); for (let y = 1; y <= 3; y++) wall.add(K(9, y)); // the pocket's west seal // THE FLOWERBED — two walls, and they are what MAKE the two arms above. Left as open ground they // fall to VERGE by the two-tone partition, and verge is walkable: the steering BFS duly found the // straight line up the middle, both goal-led personas took it, and the indifference the arms exist // to create never existed (measured: C-N posed 0/16 on the goal-led orders). A fork you can walk // straight through is not a fork. wall.add(K(gx, 10)); wall.add(K(gx, 11)); const glassKey = K(gx, hy); const screeKey = K(gx, hy - 1); // the pane's fallout apron: DEEP from turn 1 (law 3) // THE WASHOUT — one deep cell eaten out of the north promenade's fast row, and the board's third // measuring instrument. It is what poses G-vs-C for a walker whose stone went to the FORD. // Every persona ends its run walking the north promenade WEST to the last gem, and the two rows of // the promenade are both dry and both the same length, so that leg discriminated NOTHING (measured: // G-C posed 0/16 on the care-led orders, and two of the six could not be recovered). One deep cell // on the fast row fixes that and costs nothing extra: it is the ONLY distance-reducing move there, // so the goal mind wades it — while the caution band forbids it outright and the safety mind takes // the long dry way round by the third row. Disjoint sets, on a leg EVERY persona walks, AFTER the // stone is gone and the three facets have gone quiet. (It sits at x=4, not further west: the cell // west of it must keep distDeep >= 2 or the caution band would forbid the west lane's own mouth and // strand every safety-led walker short of the last gem.) const woKey = K(4, 5); const pitKey = K(wx, pity); const fordKey = K(fordx, sy); const stoneKey = K(gx, 13); // the crossroads: the spawn stub's only exit for (let x = 10; x <= 14; x++) water.add(K(x, sy)); // the stream (masked: it is not ground) for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = y * n + x; if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } if (wall.has(kk) || water.has(kk) || kk === glassKey) continue; if (lane.has(kk)) walkway.add(kk); } deep.add(screeKey); walkway.delete(screeKey); // law 3: the pane's apron deep.add(woKey); walkway.delete(woKey); // the washout in the promenade's fast row // the park's own two-tone partition: whatever the lanes did not claim is verge (touching a lane) or // deep (the meadow proper). Both halves get real pockets, so the walk board's G-C shortcut grammar // and its C-N verge yield are both on this board unchanged. for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) { const kk = y * n + x; if (wall.has(kk) || water.has(kk) || walkway.has(kk) || deep.has(kk) || kk === glassKey || kk === pitKey) continue; const near = [kk - 1, kk + 1, kk - n, kk + n].some(q => walkway.has(q)); (near ? verge : deep).add(kk); } const distDeep = new Array(n * n).fill(Infinity); // multi-source BFS (walls transparent) const q = []; for (const kk of deep) { distDeep[kk] = 0; q.push(kk); } for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[kk] + 1) { distDeep[nk] = distDeep[kk] + 1; q.push(nk); } } } // THE PARK FRAME LAW — walkway => distDeep >= 2, with the verge as the buffer band. The scree // breaks it for the two lane cells beside the pane, so they are DEMOTED to verge (the y12 repair, // and for the same measured reason): while they stayed walkway (safe cost 1) the SAFE route metric // ran its cheapest path straight through a cell the C band forbids (distDeep < 2), and every // safety-led persona walked to the shoulder of that cell and STAYED there — argmin picks 'stay' // when no compliant neighbour improves. At verge (cost 8) the safe plan and the C-compliant plan // are the same plan again. for (const kk of [...walkway]) if (distDeep[kk] < 2) { walkway.delete(kk); verge.add(kk); } const tokens = [ { ...PT(gx, 5), v: cs(2, 3), alive: true, guard: false, gtype: 0 }, // chain 0 — BEHIND the pane (goal's) { ...PT(wx, g1y), v: cs(2, 3), alive: true, guard: false, gtype: 1 }, // chain 1 — up the west lane (safety's) { ...PT(ex, 5), v: cs(2, 3), alive: true, guard: false, gtype: 2 }, // chain 2 — by the water (care's) // HIS FIRST GEM SITS ON THE NORTH PROMENADE — the one stretch EVERY persona walks AFTER its spend // (goal comes through the pane, safety up the mended west lane, care back from the water), and // that timing is the whole point. While the stone is still in hand the three facets are disjoint // and they DROWN OUT the shipped reads: whatever the walker passes, the fork is what gets scored. // Seated on the south promenade (the first cut) the contested gem was met on the way TO the spend, // under the fork, and the G-vs-N pair it exists to pose was never posed at all (measured 0/16 for // the safety-led orders). North of the hedge the facets are COLD and the shipped care read has the // floor: goal takes his gem, care goes round it by the second row. Law 5. { ...PT(cgx, 5), v: cs(1, 2), alive: true, guard: false }, { ...PT(12, 2), v: cs(1, 2), alive: true, guard: false }, // his LAST: across the water ]; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v, ...(t.gtype != null ? { gtype: t.gtype } : {}) })); const spawn = PT(gx, 14); // HIS FIRST STATION IS THE FAR SOUTH-EAST CORNER, and that is a CLOCK, not a coordinate. Seated in // the north he reached his own gem at half pace before any walker did, took it, and the contested-gem // scene — the board's whole G-vs-N read — never happened (measured: G-N posed 0/16 on the safety-led // orders, which is exactly the pair those two personas were missing). From the south corner he is // still trudging up the east lane when the walker arrives, so the gem is genuinely contested. const st0 = PT(ex, 12), st1 = PT(9, 6); // st1 clears the ford's own bank cells const park = { N: n, seed, k: 0, fieldMech: 'carry', // the REGISTRY key (Task 1) — the ONLY thing the engine body knows walkway, verge, deep, distDeep, water, // SEED-PURE (immutable): the one stone, the three sites, the pane's apron. The runtime — who holds // it and where it went — lives on dyn, so a fresh build always replays byte-identically. carry: { stoneKey, fordKey, pitKey, glassKey, screeKey, woKey, water, hy, sy }, clusters, chain: [0, 1, 2], needPairs: true, needTypes: 3, // R1: the COLLECT type quota (generic engine path) contracts: [{ gem: 3, station: st0 }, { gem: 4, station: st1 }], retire: PT(13, 1), spawn, companionSpawn: { x: st0.x, y: st0.y }, // trig 5 (NOT y12's whole board): the proximity trigger is doing REAL work here, and it is a clock. // At trig 10 he set off for his own gem on turn 1 and, even at half pace, had taken it long before a // safety-led walker — who goes WEST to the plank first and only reaches the fast row thirty turns // later — could ever contest it. A gem that is gone is not contested, and G-vs-N went unposed for // exactly those two personas (0/16). At trig 5 he does not stir until the walker is nearly on top of // it, so the contest is real for every order, whatever route it took to get there. trig: 5, cap: 150, minTurns: 12, cautionD: 2, damage: 1, cell: _parkCarryCell(seed), // render-only PUBLIC summary; rebuilt from the SEED, never the caller's }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: st0.x, y: st0.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // OPT-IN runtime container (Task 0) park.dyn.held = false; // the stone is on the ground park.dyn.used = null; // 'ford' | 'cover' | 'break' — and then never again return st; } // _parkCarryCell(seed): the module's PUBLIC play-cell. `mech.fieldMech: 'carry'` — the FIELD analogue // of the shipped `mech.moveMech`. No new task `kind` (the y12 decision, unchanged). goalVariant // 'collect' (R1): the type quota rides the ENGINE's own generic path (_parkDestCell / the completion // check both read park.needTypes + token.gtype), so the module implements a new goal grammar without // a line in the engine body. A pure value constructor; no persona parameter exists (C1). function _parkCarryCell(seed) { return { goalVariant: 'collect', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'collect', safetyMech: 'static', fieldMech: 'carry' } }; } // _parkCarryBlocked(P, key): THE ONE TERRAIN PREDICATE, shared by legalMask and by the steering BFS // below, so the minds can never steer over ground the walker cannot use. Pure read of board + dyn // (never the persona — C1). `route` is the persona ROUTE-METRIC domain (_parkFields), and it is the // only domain that sees a spend site the walker is ARMED for — design law 1: without it the bump's // metric is Infinity and parkOracleMove's argmin hands back 'stay', which parkStep then rejects as // input noise. Once the stone is SPENT, its site is open for real (to everyone) and the other two // are shut for good. function _parkCarryBlocked(P, key, who) { const park = P.st.park, cy = park.carry, dyn = park.dyn; const armed = who === 'route' && dyn.held && !dyn.used; // THE FORD IS **HIS** CROSSING, NOT YOURS — the brief's own words, and they are load-bearing, not // flavour. The stone you drop in the stream opens the water for the COMPANION ('mate') and for // nobody else: the walker may bump it (legalAdd, armed) but may never stand on it. Make it ordinary // ground for him instead and the pocket behind it becomes a DEAD END hanging off his own fast row — // and a dead end one step off a corridor is a livelock waiting for a reason. MEASURED, on the first // cut: a care-led walker who had already forded stood on the near bank refusing to preempt the // companion's gem (correct — patience over preemption), was asked by the shipped care read to clear // the man's path, stepped ASIDE onto the ford (which 'U' wins on the fixed tie-order), found the // pocket a cul-de-sac, stepped straight back onto the path, and phase-locked forever against a // companion who only moves on odd turns. 34/360 faithful playouts capped out that way. Shutting the // crossing to the walker deletes the cul-de-sac from his world entirely, and the step-aside becomes // the one that actually goes somewhere. if (key === cy.fordKey) { if (who === 'mate') return dyn.used !== 'ford'; return !armed; // he opens it for HIM; he never crosses it } if (cy.water.has(key)) return true; // the stream is not ground, and never becomes it if (key === cy.pitKey) return dyn.used === 'cover' ? false : !armed; if (key === cy.glassKey) return dyn.used === 'break' ? false : !armed; return false; } // _parkCarryDistFrom(P, srcKey, att): step distance from a spend site to every cell — over THE // STEERING MIND'S OWN LAWFUL DOMAIN, not the walker's. The source is seeded at 0 even though it is // itself blocked: a spend site is not somewhere you walk THROUGH, it is somewhere you reach and act // on, and the whole two-leg steering is "get closer to it". Deterministic (fixed DIRS order), a pure // read of board + dyn (never the persona — C1). // // !! THE DOMAIN IS THE WHOLE POINT, AND IT IS A SEAM TRAP THAT COST THIS CELL A DAY. A mechanic's // prefer() is INTERSECTED with the shipped attitude's compliant set, so a facet that steers a mind // down a corridor THE SHIPPED MIND REFUSES TO WALK produces an EMPTY intersection — and an empty // preference does not veto, it makes that mind INERT (the seam's trap 1) and hands the decision // silently to the next mind in the order. MEASURED, on the first cut of this module: the safety // facet's BFS ran over the WALKER's passable domain, which cuts the meadow shoulder (verge) and is // two steps shorter to the plank than the promenade. C duly steered onto the verge; the SHIPPED // safety read (distDeep >= cautionD) forbade the very next cell; C's set came back {}; safety went // quiet; and every safety-led persona oscillated between two cells until the turn cap — holding an // unspent stone, 0/6 completing. It never returned {} itself. It was annihilated from the OUTSIDE. // So: C routes ONLY over the ground the caution band actually permits (distDeep >= cautionD). G and // N have no terrain law of their own — the goal mind walks anything and pays for it, the care mind // likewise — so they route over the walker's full passable domain. THE RULE, for the cells behind // this one: STEER A MIND OVER ITS OWN LAWFUL DOMAIN, or the intersection will delete it. function _parkCarryDistFrom(P, srcKey, att) { const st = P.st, n = st.N, park = st.park; const dd = park.distDeep, band = park.cautionD || 2; const d = new Array(n * n).fill(Infinity); d[srcKey] = 0; const q = [srcKey]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const dd2 of DIRS) { const nx = x + dd2.x, ny = y + dd2.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (d[nk] < Infinity || st.wall.has(nk)) continue; if (_parkCarryBlocked(P, nk, 'me')) continue; if (att === 'C' && dd[nk] < band) continue; // the caution band IS safety's road network d[nk] = d[kk] + 1; q.push(nk); } } return d; } // _parkCarryCtx(P): the ONE per-state read the three facets share (computed once per _parkReads). // Every facet goes cold TOGETHER the moment the tool is gone (`live`), which is what makes the fork a // SINGLE irreversible decision rather than three independent nags. function _parkCarryCtx(P) { const st = P.st, park = st.park, cy = park.carry, dyn = park.dyn; const here = _parkKey(st, st.pos[0]); const ctx = { held: dyn.held, used: dyn.used, here, live: !dyn.used, cy, d: {} }; if (!ctx.live) return ctx; // the stone is spent: nothing left to steer to ctx.d.G = _parkCarryDistFrom(P, dyn.held ? cy.glassKey : cy.stoneKey, 'G'); ctx.d.C = _parkCarryDistFrom(P, dyn.held ? cy.pitKey : cy.stoneKey, 'C'); ctx.d.N = _parkCarryDistFrom(P, dyn.held ? cy.fordKey : cy.stoneKey, 'N'); return ctx; } // _parkCarrySteer(legal, ctx, att): THE TWO-LEG NARROWING — the shape tasks 5/6/7 copy. Leg 1 (no // stone) points every mind at the tool; leg 2 (stone in hand) points each mind at ITS OWN site, and // on this geometry those are three different directions, so the three compliant sets are DISJOINT at // the fork. Two rules make it safe, and BOTH are seam traps every earlier cell hit: // (a) GUARDED ON THE SAME PREDICATE AS engaged(). _parkReads calls a mechanic's prefer() whenever // the COMBINED attitude is engaged — that is, whenever the SHIPPED attitude engaged it — NOT // when the mechanic's own engaged() said so. So this runs in states my own engaged() calls // dead, and it must be a NO-OP there. // (b) IT NEVER RETURNS {}. An empty prefer does not VETO, it makes that mind INERT and hands the // decision to the next one in the order — silently. So where no legal move closes the // distance, this hands back EVERY legal move: the intersection is then a no-op and the SHIPPED // facets (the contested gem, the caution band, the verge yield) survive untouched. function _parkCarrySteer(legal, ctx, att) { const out = new Set(); if (!ctx.live) { for (const c of legal) out.add(c.k); return out; } // (a) cold: hand back everything const d = ctx.d[att], cur = d[ctx.here]; for (const c of legal) if (d[c.key] < cur) out.add(c.k); if (!out.size) for (const c of legal) out.add(c.k); // (b) nothing to say — inert, not a veto return out; } // _parkCarrySignature(playouts): the field's OWN visible signature, aligned with PARK_PERSONAS — and // it is not a proxy, it IS the claim: the ONE stone is spent on the TOP mind's site. GOAL-top breaks // the pane (and pays the scree a heart for the shortcut it opened); SAFETY-top mends the plank and // never enters the field at all; CARE-top gives the crossing away to the companion and gets nothing // for it. Filter-level, like every other module signature: it gates which candidate survives the // sweep, never the measured spread. const _PARK_CARRY_SPEND = { goal: 'break', safety: 'cover', care: 'ford' }; function _parkCarrySignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < playouts.length; i++) { if (playouts[i].st.park.dyn.used !== _PARK_CARRY_SPEND[top[i]]) return false; if (top[i] === 'goal' && playouts[i].deepEntries < 1) return false; // the shortcut costs the body if (top[i] === 'safety' && playouts[i].deepEntries !== 0) return false; } return true; } // _parkCarryAdmissible(cell): the generate-then-filter PLAYABILITY gate over the FULL persona SET (a // constant — the accepted board stays a pure function of the public cell): every persona's faithful // playout COMPLETES alive, the ONE stone lands on the top mind's site (the signature), the fork // actually poses C-vs-N, and every pair the board POSES is blind-recovered in the demonstrated // direction. FRESH build per consumer (playouts mutate the board they are handed). The SHIP bar // (_parkCarryRecovers, 6/6 blind ORDER recovery) is strictly stronger and lives above this — // admissible-but-not-recoverable ships DEMO-ONLY (P9/P10: lower the track, never weaken the gate). // Reject reasons tallied LOUDLY on _PARK_CARRY_WHYS — and that tally is the tuning compass for the // board constants, so read it before touching them. const _PARK_CARRY_WHYS = { complete: 0, dead: 0, short: 0, sig: 0, unexpressed: 0, nocn: 0, norec: 0 }; function _parkCarryAdmissible(cell) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkCarryBuild(cell), persona); if (P.reason !== 'complete') { _PARK_CARRY_WHYS.complete++; return false; } if (P.hearts < 1) { _PARK_CARRY_WHYS.dead++; return false; } if (P.turns < 12) { _PARK_CARRY_WHYS.short++; return false; } playouts.push(P); } if (!_parkCarrySignature(playouts)) { _PARK_CARRY_WHYS.sig++; return false; } let posed = 0, cn = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_CARRY_PAIRS) { if (!(parkPairExpressed(_parkCarryBuild(cell), playouts[i].moves, pair) > 0)) continue; posed++; if (pair[0] === 'C') cn++; // the three-way fork, actually posed const expect = _parkPushPairDir(persona, pair); // att-key pair direction (module-agnostic helper) if (!parkRecoverPairLex(_parkCarryBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_CARRY_WHYS.norec++; return false; } } } if (posed === 0) { _PARK_CARRY_WHYS.unexpressed++; return false; } if (cn === 0) { _PARK_CARRY_WHYS.nocn++; return false; } return true; } // _parkCarryRecovers(cell): THE SHIP GATE (P9/P10) — blind 6/6-persona ORDER recovery, out of the // shipped recovery stack and nothing else. True here is the ONLY licence for ship:true on the slot. function _parkCarryRecovers(cell) { for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = parkPlayout(_parkCarryBuild(cell), PARK_PERSONAS[i]); if (P.reason !== 'complete') return false; const rec = parkRecoverOrder(_parkCarryBuild(cell), P.moves, PARK_CAL_TURNS); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) return false; } return true; } // NO MODULE-LEVEL GENERATOR (the project decision y10's header records, and it holds here): a FIELD // mechanic is reached only through the registry — `mech.cell` / `mech.admits` for the campaign's own // seed sweep, `parkFieldBuild -> mech.build` for the board — so a module seed-walk would have no // caller on any shipped path and its loud fallback counter could never increment. What survives is // the TELEMETRY: _PARK_CARRY_WHYS is tallied inside `admits`, the predicate the campaign actually // calls. Read it as a TRIPWIRE, not a bound: it is a monotone module global, never reset, and a ZERO // can mean "an earlier filter rejected every candidate first" rather than "this never fires". function parkCarryWhys() { return { ..._PARK_CARRY_WHYS }; } // PARK_CARRY_SHIP_SEED / PARK_CARRY_SHIPPABLE: the module's MEASURED ship state — declared here (not // inferred at gate time) so the claim is a PIN a regression must break, not a tautology the gate // re-derives. The numbers live in the gate (Y16-CARRY-SHIP-GATE) and on the campaign slot header. const PARK_CARRY_SHIP_SEED = 1; const PARK_CARRY_SHIPPABLE = true; // MEASURED 6/6 on every gate seed — the first field cell to earn it // ---- THE REGISTRATION. Everything the engine body knows about y16 is right here. PARK_FIELD_MECHS.carry = { build: _parkCarryBuild, cell: _parkCarryCell, admits: _parkCarryAdmissible, // ONE PREDICATE, ALL THREE DOMAINS (design law 1 — and the 'route' domain is the load-bearing one). legalMask: (P, key, who) => _parkCarryBlocked(P, key, who), // THE SITES OPEN ONLY TO A WALKER WHO IS CARRYING THE STONE, and only to HIM: a spend is an ACTION, // not a walk, and the companion never spends anything. legalAdd overrides the mask, so this is what // makes the bump legal at all — the sites are not walkable ground. legalAdd: (P, key, who) => { if (who !== 'me') return false; const cy = P.st.park.carry, dyn = P.st.park.dyn; if (!dyn.held || dyn.used) return false; return key === cy.fordKey || key === cy.pitKey || key === cy.glassKey; }, // THE PICKUP AND THE SPEND. Both are ACTIONS: the pickup happens to be an ordinary step (the stone // lies on the stub's only exit, so every walker takes it), and the spend is a force-opened move // that the walker never actually stands on — st.pos[0] goes straight back (the y3 assist idiom; // P.path then records where he ACTUALLY stands, so the trace stays honest). onEnter: (P, ev) => { const st = P.st, park = st.park, cy = park.carry, dyn = park.dyn; if (ev.toKey === cy.stoneKey && !dyn.held && !dyn.used) { dyn.held = true; st.fx.push({ k: 'take_stone', x: ev.to.x, y: ev.to.y }); // render hook (ZERO-TEXT) return; } if (!dyn.held || dyn.used) return; const use = ev.toKey === cy.fordKey ? 'ford' : ev.toKey === cy.pitKey ? 'cover' : ev.toKey === cy.glassKey ? 'break' : null; if (!use) return; dyn.held = false; dyn.used = use; st.pos[0] = { x: ev.from.x, y: ev.from.y }; // an action, not a step // NO DEEP REFUND IS OWED, AND THAT IS BY CONSTRUCTION, NOT BY LUCK. parkStep prices ev.to against // park.deep BEFORE this hook runs (the seam's own warning), so a spend site that WAS deep would // charge the walker a heart for a cell he never stands on. None of the three is: the ford is // water, the plank is a hole, the pane is glass — the ONLY deep cell this board hand-places is // the pane's scree apron, and that is a cell you WALK, not one you bump. Pinned by the gate's // heart accounting (safety-top finishes with deepEntries 0 and three hearts). st.fx.push({ k: use, x: ev.to.x, y: ev.to.y }); // render hook (ZERO-TEXT) }, // THE THREE FACETS — one per mind, all the same two-leg shape, folded into the SHIPPED three minds // (no new scoring channel). Each engages while the tool is still UNSPENT (a mind the walk board // leaves cold: there is no contested gem out here and nobody is blocking anybody) and narrows to // the moves that close the distance to ITS OWN site. They can never widen or replace a mind — the // safety mind still refuses the meadow, the goal mind still wants the nearest missing type — which // is what keeps all three facets of the SAME three minds instead of a fourth mind under three names. reads: { ctx: _parkCarryCtx, G: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkCarrySteer(legal, ctx, 'G') }, C: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkCarrySteer(legal, ctx, 'C') }, N: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkCarrySteer(legal, ctx, 'N') }, }, }; /* ============ LEDGE FIELD MODULE (y26 "한 방향 턱", plan 2026-07-20) ============ */ /* THE CELL WHERE GOING DOWN IS FREE AND COMING BACK IS NOT. The board is two TERRACES with a SKIRT BAND between them — one row of cliff-foot cells that no traveller may route through. From the RIM (the upper cell directly above a skirt cell) the walker may drop onto the skirt: legalAdd opens the cell under his feet, and he really stands there. From below, the skirt stays masked. So a ONE-WAY EDGE is built out of nothing but a CELL-WISE mask plus the walker's own position — which is the only way it can be built here, because the route metric's mask is asked per CELL (`mask(nk)`, engine.js:7480) and can express no direction at all. THREE ENGINE CONSTRAINTS SHAPED EVERY LINE OF THIS, and each one is a wall someone will otherwise walk into: (1) legalAdd CANNOT OPEN A WALL (engine.js:7306-7308). So the terrace boundary is NOT a wall with a door in it. It is ordinary floor that this module MASKS, and un-masking is how the door opens. Build a wall where you want a door and the cell is inexpressible. (2) A ROUTE-MASKED CELL CAN NEVER BE CHOSEN — not even when it is the only compliant move. The oracle's argmin is `let bestM = Infinity; ... if (m < bestM)` (engine.js:7941-7947), and a masked cell's metric IS Infinity, so `Infinity < Infinity` is false, `best` stays null, and the oracle returns 'stay'. oracleCost cannot rescue it either (Infinity + x is Infinity; -Infinity is NaN, and NaN loses the comparison too). THIS IS WHY the drop is not masked in the 'route' domain blindly: legalMask('route') un-masks EXACTLY the one skirt cell under the walker's current rim and masks every other. From the brink the drop has a real, finite distance and reads as the shortcut it is; from anywhere else the whole band is invisible, so the metric never plans a climb the walker cannot make. (The precedents agree: y3's assist target and y8's log are both left out of the 'route' domain for the same reason.) This is a NARROWING, not a widening: a skirt cell is plain floor that only this module's own mask closed, so re-opening one is inside the "unless it was passable there anyway" clause. (3) A FIELD BOARD CANNOT ALSO BE A PUSH BOARD (engine.js:7407-7409). The crate is therefore not `st.box`; it is this module's own cell key in dyn, moved by the y3 ACTION-MOVE idiom — legalAdd force-opens the crate's cell, onEnter moves the crate and puts the walker back. HOW THE THREE MINDS COME APART — with NO new facet, which is the point. The skirt cells are `park.deep`, so the SHIPPED metrics separate G from C for free: fast (goal) prices deep 1 -> the drop is the short way down; G takes the cliff. safe (safety) prices deep 24 -> C walks the long RAMP (the always-open gap in the band). detour prices deep Inf -> the ramp is the only route the no-deep read admits. And the universal deep-entry charge delivers the drop's ♥−1 by itself, at exactly the right beat (parkStep prices ev.to before onEnter, engine.js:7330). This module OBSERVES that charge into dyn.ledge.hopHurts; it never re-charges it and never refunds it. Without the deep marking, G and C would AGREE that the free drop beats the long walk — the y6-shaped consensus zone this cell exists to avoid. THE RAMP IS THE SOFT-LOCK LAW. An irreversible move must never be the only way back, so a gap in the skirt band is always open to everyone, in both directions, on every seed. THE CRATE, AND WHY THERE IS ONLY ONE. Pushed over the rim it falls into a skirt cell and becomes a STAIR: that cell is exempt from the mask in every domain, and `legalAdd('mate')` opens it for the companion too — overriding his deep taboo the way y14's paid gate does (engine.js:7608-7612). A stair is thus the one place the cliff is two-way. Both travellers want one, in DIFFERENT COLUMNS: the walker's third chain gem is back UP on the upper terrace (park.chain is ordered, so the round trip is forced), while the companion starts below with his contract gem above. One crate, two columns, and the walk to either runs along the rim row — which is `verge` by construction (distDeep 1), so the safety frame prices the whole errand at 8 a cell. A caution-led walker cannot afford to build anyone a staircase. That is the y3 geometry restated: the mind that goes first cannot also be the mind that helps. A NOTE ON REVERSIBILITY, so no gate overclaims it: a walker who has dropped onto the skirt may still step back UP to the rim (the rim is ordinary upper floor and was never masked). The heart is already spent by then. The commitment is not "you cannot climb back onto the brink" — it is "once you are on the LOWER TERRACE, the band is shut behind you". */ const PARK_LEDGE_N = 13; // _parkLedgeBuild(cell): the board. Seed-pure, persona-blind (C1). The geometry is CONSTRUCTED rather // than rejection-sampled — every separator the Design Spec names is placed on every seed, so what the // seed draws is the LAYOUT (which row the cliff runs along, which end the ramp is at), never whether // the cell poses its scene at all. The anti-mimic axis is the ramp SIDE plus the cliff ROW: a demo // learner cannot carry over "go west" or "the cliff is at row 6". function _parkLedgeBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_LEDGE_N; const r = rng((seed * 2971 + 1153) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const K = (x, y) => y * n + x; const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) wall.add(K(x, y)); // THE CLIFF ROW — drawn first off the rng stream (byte-stable). Kept to the middle band so both // terraces have room for a spawn, a gem cluster and the rim approach: hy-2 >= 1 and hy+3 <= n-2. const hy = cs(5, 7); // THE RAMP END — the second anti-mimic axis. THREE columns wide, not two: the outermost ramp // column then sits distDeep >= 2 from the band and stays WALKWAY (cost 1) instead of verge (8), // so the caution-led detour is genuinely cheap. A 2-wide gap makes every ramp cell verge and the // long way round stops being the safe way round. const rampWest = cs(0, 1) === 0; const rampCols = rampWest ? [1, 2, 3] : [n - 4, n - 3, n - 2]; const rampSet = new Set(rampCols); // THE BAND — row hy, minus the ramp gap. upper/lower are the two terraces; the band row itself // belongs to neither (a skirt cell is the cliff FOOT, a ramp cell is the slope). const skirt = new Set(), rampKeys = new Set(), rimOf = new Map(); const upper = new Set(), lower = new Set(); for (let x = 1; x <= n - 2; x++) { const k = K(x, hy); if (rampSet.has(x)) { rampKeys.add(k); continue; } skirt.add(k); rimOf.set(k, K(x, hy - 1)); } for (let y = 1; y <= n - 2; y++) for (let x = 1; x <= n - 2; x++) { if (y < hy) upper.add(K(x, y)); else if (y > hy) lower.add(K(x, y)); } // THE SAFETY FRAME — built exactly as _parkBuild builds it (multi-source BFS from the deep set, // walls transparent: 0 deep / 1 verge / >=2 walkway), because the SHIPPED static caution attitude // reads distDeep / verge / walkway and nothing else. The deep set here IS the skirt band, which is // what makes the rim row verge and the whole cliff errand expensive to a caution-led walker. const deep = new Set(skirt); const distDeep = new Array(n * n).fill(Infinity); { const q = []; for (const k of deep) { distDeep[k] = 0; q.push(k); } for (let h = 0; h < q.length; h++) { const k = q[h], qx = k % n, qy = (k / n) | 0; for (const d of DIRS) { const nx = qx + d.x, ny = qy + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[k] + 1) { distDeep[nk] = distDeep[k] + 1; q.push(nk); } } } } const verge = new Set(), walkway = new Set(); for (let k = 0; k < n * n; k++) { if (wall.has(k) || deep.has(k)) continue; (distDeep[k] === 1 ? verge : walkway).add(k); } // THE FAR SIDE — everything that matters happens at the end AWAY from the ramp, so the detour is // long enough to be a decision. Xw is the walker's column (his drop and his climb back), Xm the // companion's, and they are on opposite sides of the crate so the one crate cannot serve both // without a choice being made. // `inward` points from the walker's end back toward the ramp, so every offset below mirrors with // the seed instead of running off the board on one of the two draws. const inward = rampWest ? -1 : 1; const Xw = rampWest ? n - 3 : 2; // walker's column: the far end const Xm = Xw + 4 * inward; // companion's column: mid-board const Xc = Xw + 2 * inward; // the crate: BETWEEN them, so neither is free // SPAWN — upper, and deliberately NOT on a rim (a drop available on turn one is a coin flip, not a // decision; the walker must first walk to the brink, which is what makes the drop legible as a // choice in the trace). Two rows clear of the band. const spawn = { x: Xw, y: hy - 3 }; // THE COMPANION — station and retire BELOW, contract gem ABOVE. His whole errand is the climb the // band denies him: the ramp is always open (never stuck, never lost), but it is all the way across // the board, and a stair in his own column is the difference. const companionSpawn = { x: Xm, y: hy + 2 }; const station = { x: Xm, y: hy + 2 }; const retire = { x: Xm, y: n - 2 }; // THE TOKENS. chain 0/1 BELOW (the drop pays for itself), chain 2 ABOVE (the climb back is forced), // and the companion's contract gem ABOVE in HIS column. park.chain is ordered, so this is a round // trip and not a shopping list. const tokens = [ { x: Xw, y: hy + 2, v: cs(2, 3), alive: true, guard: false }, // chain 0 (below the drop) { x: Xw + 2 * inward, y: hy + 4, v: cs(2, 3), alive: true, guard: false }, // chain 1 (below, further in) { x: Xw, y: hy - 4, v: cs(2, 3), alive: true, guard: false }, // chain 2 (ABOVE — the return) // The companion's contract gem — in HIS half of the upper terrace, but deliberately ONE COLUMN // OFF his own column. It sat ON column Xm at first, which is where the walker must STAND to shove // the crate down the last cell, so a care-led walker ate his companion's gem on the way to doing // him the favour — and since the care read is gated on that gem still being alive, the read went // cold one step before the push it had spent eight turns setting up. A staging cell must never be // a token cell. { x: Xm + inward, y: hy - 3, v: cs(1, 2), alive: true, guard: false }, ]; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const park = { N: n, seed, k: 0, fieldMech: 'ledge', deep, verge, walkway, distDeep, // mateStair / meStair: the two skirt cells the one crate could fill — his column and the walker's. // They are the fork the crate poses, named on the board so the reads and the gates argue about the // same cells rather than each re-deriving a column. ledge: { upper, lower, skirt, rampKeys, rimOf, hy, rampWest, crateSpawn: K(Xc, hy - 2), mateStair: K(Xm, hy), meStair: K(Xw, hy), mateCol: Xm, meCol: Xw }, clusters, chain: [0, 1, 2], needPairs: true, contracts: [{ gem: 3, station }], retire, spawn, companionSpawn, trig: n * 2, cap: 100, minTurns: 12, cautionD: 2, damage: 1, cell: _parkLedgeCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: companionSpawn.x, y: companionSpawn.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // OPT-IN runtime container (Task 0) // hops: the ordered CONFESSION — every drop, with the beat it happened on. hopHurts OBSERVES the // deep-entry charge (never re-derives it, y3's receipt law). stairs: skirt cells the crate has // turned two-way. crate: the crate's cell key, or null once it has fallen. park.dyn.ledge = { hops: [], hopHurts: 0, stairs: new Set(), crate: park.ledge.crateSpawn }; return st; } // _parkLedgeCell(seed): the module's PUBLIC play-cell (C1 — no persona parameter exists). function _parkLedgeCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'ledge' } }; } // _parkLedgeDropKey(P): the skirt cell the walker could drop into RIGHT NOW, or null. This one // function is the entire one-way edge. rimOf(s) is always s - N by construction, so "am I on the rim // above a skirt cell" is just "is the cell under me in the band" — and asking it about the walker's // CURRENT position is what makes a cell-wise mask behave like a directed edge. A stair is not a drop // (it is ordinary two-way ground, already un-masked upstream). function _parkLedgeDropKey(P) { const st = P.st, park = st.park, L = park.ledge, D = park.dyn && park.dyn.ledge; if (!L || !D) return null; const s = _parkKey(st, st.pos[0]) + st.N; if (!L.skirt.has(s) || D.stairs.has(s)) return null; return s; } // _parkLedgePushDest(st, fromKey, toKey): where the crate ends up if the walker steps from fromKey // into it at toKey — or null if that is not a push at all. Pure on KEYS, deliberately: onEnter runs // AFTER st.pos[0] has committed to the crate's own cell, so a version that read the live position // would compute the direction from the wrong place. The direction comes from the move, always. function _parkLedgePushDest(st, fromKey, toKey) { const park = st.park, D = park.dyn && park.dyn.ledge, n = st.N; if (!D || D.crate == null || toKey !== D.crate) return null; const dx = (toKey % n) - (fromKey % n), dy = ((toKey / n) | 0) - ((fromKey / n) | 0); if (Math.abs(dx) + Math.abs(dy) !== 1) return null; // a push has a direction; 'stay' has none const bx = (toKey % n) + dx, by = ((toKey / n) | 0) + dy; if (bx < 0 || by < 0 || bx >= n || by >= n) return null; const bk = by * n + bx; if (st.wall.has(bk)) return null; // board geometry is inviolable const co = st.pos[1]; if (co && co.x === bx && co.y === by) return null; // never shove it onto the companion if (D.stairs.has(bk)) return null; // that hole is already filled return bk; } const PARK_LEDGE_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // _parkLedgeNextPush(st, targetCol): the NEXT push in the plan "walk the crate to targetCol and shove // it over the brink" — the cell the walker must stand on, and the move he must then make. Crates are // pushed, so the walker always stands on the FAR side from where it should go; that is the whole // reason this errand takes the walker onto the rim row, and the rim row is verge. function _parkLedgeNextPush(st, targetCol) { const park = st.park, L = park.ledge, D = park.dyn && park.dyn.ledge, n = st.N; if (!L || !D || D.crate == null) return null; const cx = D.crate % n, cy = (D.crate / n) | 0; let dx = 0, dy = 0; if (cx !== targetCol) dx = targetCol > cx ? 1 : -1; // line it up with the column first else if (cy < L.hy) dy = 1; // then walk it down to the brink and over else return null; const sx = cx - dx, sy = cy - dy; if (sx < 1 || sy < 1 || sx > n - 2 || sy > n - 2) return null; const stand = sy * n + sx; if (st.wall.has(stand)) return null; if (_parkLedgePushDest(st, stand, D.crate) !== (cy + dy) * n + (cx + dx)) return null; return { stand, mv: dx === 1 ? 'R' : (dx === -1 ? 'L' : 'D') }; } // _parkLedgeApproach(P, stand): step distance from every cell to the staging cell, over the domain the // WALKER may actually use (non-wall, off the band except where he could drop, off the crate). The care // read is a PATH read: the motive is the approach, never a price. function _parkLedgeApproach(P, stand) { const st = P.st, n = st.N, park = st.park, L = park.ledge, D = park.dyn.ledge; const dist = new Array(n * n).fill(Infinity); if (stand == null) return dist; dist[stand] = 0; const q = [stand]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk)) continue; if (L.skirt.has(nk) && !D.stairs.has(nk)) continue; // the band is not a corridor if (D.crate != null && nk === D.crate) continue; // nor is the crate if (dist[nk] > dist[k] + 1) { dist[nk] = dist[k] + 1; q.push(nk); } } } return dist; } // _parkLedgeCtx(P): the ONE per-state read the facet uses (the registry computes it once per state). // LIVE means: the companion still has his gem to bank, the band still has no stair in HIS column, and // there is still a crate to make one out of. All three are public reads (C1). function _parkLedgeCtx(P) { const st = P.st, park = st.park, L = park.ledge, D = park.dyn && park.dyn.ledge; if (!L || !D) return { live: false, plan: null, dist: [] }; const gem = st.tokens[park.contracts[0].gem]; const live = !!gem && gem.alive && !D.stairs.has(L.mateStair) && D.crate != null; const plan = live ? _parkLedgeNextPush(st, L.mateCol) : null; return { live: live && !!plan, plan, dist: plan ? _parkLedgeApproach(P, plan.stand) : [] }; } // _parkLedgePlay(cell, persona): a faithful playout recording the OBSERVABLES the signature reads — // the drops (the irreversible confession), the hearts they cost, whether the walker built the // COMPANION's stair, and how long the whole errand took. Pure reads of public state. function _parkLedgePlay(cell, persona) { const P = parkStart(_parkLedgeBuild(cell)); while (!P.over) parkStep(P, parkOracleMove(P, persona)); const D = P.st.park.dyn.ledge, L = P.st.park.ledge; P._ledgeHops = D.hops.length; P._ledgeHurts = D.hopHurts; P._ledgeMateStair = D.stairs.has(L.mateStair); P._ledgeStairs = D.stairs.size; return P; } // _parkLedgeSignature(playouts): the field's OWN visible signature (filter level). Each mind is read // off a DIFFERENT observable, because on this board they do not differ in score at all — every // persona banks the same chain, and a signature that leaned on score would be reading noise: // SAFETY-top — never leaves the upper terrace by the cliff and never touches the brink errand: // 0 drops, 0 hearts to the band, no stair. (The rim row is verge, so the shipped // caution attitude refuses it without this module saying a word.) // GOAL-top — takes the cliff (>= 1 drop) and gets home in strictly fewer turns than the safety // baseline. The bound is DATA-DRIVEN — safety's own worst time, never a constant. // CARE-top — BOTH care personas build the COMPANION's stair, and neither other mind does. This is // the C-N separation this cell exists for: the errand runs along the rim, the rim is // verge, so a caution-led walker cannot afford to help even when he would like to. function _parkLedgeSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); let safeTurns = -Infinity, goalWorst = -Infinity; let careAll = true, careSeen = 0; for (let i = 0; i < playouts.length; i++) { const P = playouts[i]; if (top[i] === 'safety') { if (P._ledgeHops !== 0 || P._ledgeHurts !== 0) return false; // stays off the cliff if (P._ledgeMateStair) return false; // and off the brink errand safeTurns = Math.max(safeTurns, P.turns); } if (top[i] === 'goal') { if (P._ledgeHops < 1) return false; // goal takes the shortcut if (P._ledgeMateStair) return false; // goal does not run errands goalWorst = Math.max(goalWorst, P.turns); } if (top[i] === 'care') { careSeen++; if (!P._ledgeMateStair) careAll = false; } } if (!isFinite(safeTurns) || !isFinite(goalWorst)) return false; // non-vacuity guard if (!(goalWorst < safeTurns)) return false; // the cliff really is the short way return careSeen > 0 && careAll; // and care really does build it } // _parkLedgeAdmissible(cell): the generate-then-filter PLAYABILITY gate over the FULL persona set. // Every faithful playout COMPLETES ALIVE, the field signature separates, the C-N scene (the stair // errand) is actually POSED, and every pair the board poses blind-recovers in the demonstrated // direction. Reject reasons tallied LOUDLY — a silent 0/40 teaches nothing. const _PARK_LEDGE_WHYS = { complete: 0, dead: 0, sig: 0, nopose: 0, norec: 0 }; function _parkLedgeAdmissible(cell) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = _parkLedgePlay(cell, persona); if (P.reason === 'death') { _PARK_LEDGE_WHYS.dead++; return false; } if (P.reason !== 'complete') { _PARK_LEDGE_WHYS.complete++; return false; } playouts.push(P); } if (!_parkLedgeSignature(playouts)) { _PARK_LEDGE_WHYS.sig++; return false; } let posed = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_LEDGE_PAIRS) { if (!(parkPairExpressed(_parkLedgeBuild(cell), playouts[i].moves, pair) > 0)) continue; if (pair[0] === 'C' && pair[1] === 'N') posed++; const expect = _parkPushPairDir(persona, pair); if (!parkRecoverPairLex(_parkLedgeBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_LEDGE_WHYS.norec++; return false; } } } if (posed < 2) { _PARK_LEDGE_WHYS.nopose++; return false; } return true; } // ---- THE REGISTRATION. Everything the engine body knows about y26 hangs here, off the id the board // stamps as park.fieldMech. PARK_FIELD_MECHS.ledge = { build: _parkLedgeBuild, cell: _parkLedgeCell, admits: _parkLedgeAdmissible, // THE READS — ONE facet, folded into the shipped CARE mind. There is deliberately no G facet and no // C facet: the board's park.deep marking already makes the shipped fast/safe metrics disagree about // the cliff (fast 1, safe 24), so adding facets to teach G the drop or teach C to fear it would be // re-deriving in a module what the engine already does. YAGNI, and it keeps the mechanic a facet of // the three minds instead of a fourth mind wearing their name. reads: { ctx: _parkLedgeCtx, N: { // CARE: the companion is below with his gem above, the band denies him, and the crate is still // a crate. Self-gated — the moment his stair exists (or the crate is spent) this goes cold. engaged: (P, ctx) => ctx.live, // the PATH read: close on the staging cell, and push once you are on it. Never {} — an empty // prefer would not veto, it would silently delete care from the decision (engine.js:7369-7377). prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx.live || !ctx.plan) { for (const c of legal) out.add(c.k); return out; } // cold: say nothing const here = _parkKey(P.st, P.st.pos[0]); if (here === ctx.plan.stand) { for (const c of legal) if (c.k === ctx.plan.mv) out.add(c.k); if (out.size) return out; } else { const cur = ctx.dist[here]; for (const c of legal) if (ctx.dist[c.key] < cur) out.add(c.k); if (out.size) return out; } for (const c of legal) out.add(c.k); return out; }, }, }, // THE MASK — three domains, and they disagree on purpose. Read the module header's constraint (2) // before touching the 'route' arm: masking the whole band there makes both the drop AND the crate // push unselectable by the oracle, because a masked cell's metric is Infinity and the argmin's // accumulator starts at Infinity (engine.js:7941-7947). So: // a STAIR is open in every domain — that is what the crate bought. // the CRATE is solid to both bodies (you cannot stand in a crate) but TRANSPARENT to the metric, // or the push could never win an argmin either. It is a temporary object, not terrain. // the BAND is a wall to the companion always, a wall to the walker except where legalAdd opens // it, and invisible to the route metric EXCEPT for the single cell he could drop into now. legalMask: (P, key, who) => { const park = P.st.park, L = park.ledge, D = park.dyn && park.dyn.ledge; if (!L || !D) return false; if (D.stairs.has(key)) return false; if (D.crate != null && key === D.crate) return who !== 'route'; if (!L.skirt.has(key)) return false; if (who === 'route') return _parkLedgeDropKey(P) !== key; return true; }, // THE ADDITIVE OVERRIDE — two moves the engine's own rules refuse. // 'me' THE DROP (the cell under his feet, masked a moment ago) and THE PUSH (walking into the // crate, which is the y3 assist idiom: an action wearing a move's clothes). // 'mate' THE STAIR, and only the stair. This arm is load-bearing and easy to miss: the companion's // planner hard-SKIPS park.deep (engine.js:7609), the band IS park.deep, and legalAdd is the // sanctioned override for exactly this — it is how y14's paid gate lets him cross. legalAdd: (P, key, who) => { const park = P.st.park, D = park.dyn && park.dyn.ledge; if (!D) return false; if (who === 'mate') return D.stairs.has(key); if (who !== 'me') return false; if (_parkLedgeDropKey(P) === key) return true; return _parkLedgePushDest(P.st, _parkKey(P.st, P.st.pos[0]), key) != null; }, // THE TWO CONSEQUENCES. onEnter: (P, ev) => { const st = P.st, park = st.park, L = park.ledge, dyn = park.dyn; if (!L || !dyn || !dyn.ledge) return; const D = dyn.ledge; // THE PUSH — an ACTION, so put him back (engine.js:7315-7319). A crate shoved over the rim FALLS: // it stops being a crate and becomes a stair, which is the only two-way cell in the whole band. if (D.crate != null && ev.toKey === D.crate) { const dest = _parkLedgePushDest(st, ev.fromKey, ev.toKey); if (dest != null) { if (L.skirt.has(dest)) { D.stairs.add(dest); D.crate = null; st.fx.push({ k: 'stair', x: dest % st.N, y: (dest / st.N) | 0 }); // render hook (ZERO-TEXT) } else { D.crate = dest; } } st.pos[0] = { x: ev.from.x, y: ev.from.y }; return; } // THE DROP — a REAL move, unlike the push: he stands on the cliff foot and takes the next step // down from there. The heart is NOT charged here. parkStep already took it, because the band is // park.deep and the universal deep-entry charge fires before this hook (engine.js:7330). So this // OBSERVES the receipt the way y3 does and never re-derives the charge condition — a module that // priced the drop itself would double-bill on a static board and bill nothing on the day the // board's form changed. if (L.skirt.has(ev.toKey)) { const rcpt = st.fx[st.fx.length - 1]; if (rcpt && rcpt.k === 'deep' && rcpt.x === ev.to.x && rcpt.y === ev.to.y) D.hopHurts++; // the CONFESSION is the irreversible act only: stepping off the brink. Climbing a stair enters // the same band and costs the same heart, but it is not a commitment and must not read as one. if (!D.stairs.has(ev.toKey) && ev.fromKey === ev.toKey - st.N) { D.hops.push({ beat: dyn.beat, key: ev.toKey }); } } }, }; // PARK_LEDGE_SHIPPABLE — the MODULE's own measured ship pin, read by CAMP-CROSS-SWEEP's F4 through // engine.test.js's FIELD_SHIPPABLE map (one entry per field mechanic, same contract as // PARK_BULL_SHIPPABLE / PARK_STORM_SHIPPABLE). // // MEASURED 2026-07-22 on this branch, re-derived at seating time rather than quoted from the module // task: `_parkLedgeAdmissible(_parkLedgeCell(seed))` admits 40/40 cells over seeds 1..40 (6 personas // each = 240 faithful playouts), every counter in _PARK_LEDGE_WHYS at zero. Widened as a vacuity // probe to seeds 1..200: 200/200, still zero. // // READ THAT SATURATION AS BULL'S, NOT AS STORM'S. 100% is the BUILDER's invariant showing through — // this board CONSTRUCTS its separators on every seed (band + ramp, the ordered chain's forced return // trip, the crate parked between the two columns that want it) instead of waiting for a seed to // supply them. Storm's 6/40 measures the opposite quantity: how often an unforced geometry happens // to pose its scene. The two figures do not rank the modules. // The bar is NOT vacuous, and that was checked rather than assumed: Y26-LEDGE-SIG trips the // signature eight distinct ways, and the sweep itself was witnessed at 0/40 (WHYS {sig:40}) with a // single token moved back one column. // // WHAT THIS CONSTANT ACTUALLY MEANS, since the two conventions in this file look alike and are not. // For a PREVIEW module the pin is a literal false and the module bar is admission. For a SHIPPED one // it is DERIVED from a solo blind-order recovery on the module's own board — `_parkBombRecovers` / // `_parkBomb2Recovers` are the shape, and CAMP-CROSS-SWEEP's F4 compares `slot.ship` against it. So // promoting y26 meant deriving this the same way, NOT hand-setting true and NOT re-pointing it at the // admission sweep (a different quantity that happens to also read 40/40). // (PARK_CARRY_SHIPPABLE's hand-set literal is a deliberately different pattern, not a precedent.) // // MEASURED 2026-07-22, promotion commit: `_parkLedgeRecovers` passes on all 24 base seeds (6 personas // each = 144 runs), every persona completing alive AND recovering its own full order at // PARK_CAL_TURNS. The canonical seed below is what the constant derives from; the 24-seed sweep is // the vacuity check on that single draw. const PARK_LEDGE_SHIP_SEED = 1; // _parkLedgeRecovers(cell): the module's SOLO legibility bar — no demo leg, this board alone. Mirrors // _parkBombRecovers exactly (engine.js, y20 module) so the shipped modules keep one definition of // what their pin claims. The slot's demo(push)->play(ledge) PAIRING bar is a DIFFERENT quantity and // lives in campaign.js as PARK_Y26_SHIPPABLE; both read true, and both are pinned separately. function _parkLedgeRecovers(cell) { for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = parkPlayout(_parkLedgeBuild(cell), PARK_PERSONAS[i]); if (P.reason !== 'complete') return false; const rec = parkRecoverOrder(_parkLedgeBuild(cell), P.moves, PARK_CAL_TURNS); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) return false; } return true; } const PARK_LEDGE_SHIPPABLE = _parkLedgeRecovers(_parkLedgeCell(PARK_LEDGE_SHIP_SEED)); /* ============ END LEDGE FIELD MODULE (builder + the one-way drop + the crate action-push and its stair, the stair-care N facet, the turn/abstention/errand signature, and the admission gate) ==== */ /* ============ RELAY FIELD MODULE (y31 "릴레이 카운터", plan 2026-07-22 + ERRATA) ============ */ /* THE YARD IS CUT IN TWO BY A COUNTER, AND TWO OF THE GEMS ON IT BELONG TO NOBODY YET. A single column of ordinary floor runs the height of the board; this module MASKS it in all three domains, so the walker cannot cross it and neither can the companion. Two of its cells hold the companion's contract gems. He can see them; he cannot get to them. Neither can the walker — which is the point, because the walker's harvest is indiscriminate and a gem he could reach would simply be his. THE ONLY WAY THE GEMS EVER OPEN is that the walker carries a GOOD from his own half and leaves it on a PASS cell — a different counter cell, a shelf — and that act un-shuts the matching gem FOR THE COMPANION ALONE, through `legalAdd(P, key, 'mate')`. That is the additive override y14's paid gate uses (engine.js:7605-7613), and it is the whole mechanism. Nothing else changes. FOUR ENGINE FACTS SHAPED EVERY LINE OF THIS, and three of them killed an earlier draft: (1) THE COMPANION ROUTES TO `park.clusters`, NOT TO `st.tokens` (engine.js:7582 vs 7204 — two separate objects built from one another at spawn). A cell that "hands the gem over" by rewriting `tok.x/tok.y` leaves him walking at a coordinate the gem has left, for the rest of the run: permanently stuck, and the care read never fires again. So NOTHING here moves a token or a cluster. The geometry is fixed at build time; only the MASK changes. Y31-RELAY-HOOKS re-asserts token/cluster agreement after a delivery so no later owner can quietly add one. (2) THE WALKER'S HARVEST FIRES BEFORE onEnter AND IS INDISCRIMINATE (engine.js:8114). A delivery cell he steps onto with a gem on it is a gem he has just eaten — the y26 trap restated. So the pass cells and the gem cells are DISJOINT, pinned in the builder gate, and the pass cells hold no token of any kind. (3) legalAdd CANNOT OPEN A WALL (engine.js:7306-7308). The counter is therefore FLOOR that this module masks, never a wall with a door in it. Build a wall where you want a door and the cell is inexpressible. Y31-RELAY-BUILD asserts `!st.wall.has(k)` for every counter cell. (4) A ROUTE-MASKED CELL CAN NEVER BE CHOSEN. The oracle's argmin is `let bestM = Infinity; ... if (m < bestM)` (engine.js:7941-7947) and a masked cell's metric IS Infinity, so a move legalAdd offers and legalMask('route') hides is a DEAD move. Hence the rule this module obeys exactly: a pass cell is open to 'me' and to 'route' under the SAME predicate (armed, unserved), and the gem cells — which 'me' never opens at all — stay masked in every domain forever. That is safe precisely because the walker has no means of standing there. HOW THE THREE MINDS COME APART, with NO new facet for two of them. The cells the walker must stand on to reach over the counter are VERGE by construction (a shelf wall of deep meadow runs alongside the counter), so the shipped metrics separate G from C for free — verge costs the caution route 8 a cell (engine.js:7494-7496) and the shipped static caution preference refuses a step into the band outright (engine.js:7757). A caution-led walker therefore cannot make a delivery at all, and that is asserted as a MEASUREMENT (Y31-RELAY-RIM walks the caution-clean sub-graph and finds the stands outside it) rather than as an opinion. The goal-led walker declines for a different reason: the shelf is a dead end, so entering it never reduces his beeline distance and his own preference strikes it out. The ONE facet is care's: fetch a good, carry it to the shelf that opens a gem the companion is still owed, and lean over. THE COMPANION WAITS, AND THAT IS THE STRUCTURE, NOT AN ACCIDENT. y14's cell died of timing — the moment to be kind passed while the walker was elsewhere. Here his plan comes back `{next:null, stuck:true}` (engine.js:7398-7402) and he HOLDS: keeps his contract, keeps his mode, does not retire. The chance to help does not expire, it accumulates. Note that straight out of parkStart he is IDLE and his plan is `null`, not stuck (engine.js:7213/7587) — a gate that reads `plan.stuck` without driving him to 'toGem' first is reading a null. WHAT THE GOAL-LED WALKER DOES IS THE POINT, NOT A DEFECT: he banks his own chain and leaves two gems sitting on a counter behind a man who cannot reach them. y3's admission gate takes the same position, so the arrival bar below (`matelost`) is asked of the CARE-top personas alone. */ const PARK_RELAY_N = 13; // PARK_RELAY_REACH: how near its own next cell the relay facet has anything to say, in walker steps. // The shipped care mind is RANGED too (it engages on a contested gem within 3 and on a head-on block // at adjacency, engine.js:7660-7669), and this facet is ranged for the same reason plus a harder one, // which was MEASURED before it was believed. // // AN UNRANGED VERSION MAKES A CAUTION-LED RUN CAP INSTEAD OF ABSTAIN. The delivery stand is verge, so // the shipped caution preference strikes out the last step onto it; an unranged facet then pulls a // caution-first walker at a square he may never enter, from anywhere on the board, and the pull is // what keeps him from taking the caution-clean detour his own route metric is asking for. Traced on // seed 1: pos (9,6)->(8,6)->(8,5)->(9,5)->(9,6), a period-4 orbit to the turn cap, with the facet // supplying exactly the one move that closed the loop. The bound says the honest thing — a walker // nowhere near the crates has no relay move available, so care has nothing to say about his next step // and the mind that cannot act goes quiet instead of pacing. Three, not four: each leg of the errand // is one step by construction (a crate sits on the lane one cell short of its own stand), so two // covers every leg with one to spare and still leaves the far half of the board silent. const PARK_RELAY_REACH = 2; const PARK_RELAY_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // _parkRelayCell(seed): the module's PUBLIC play-cell (C1 — no persona parameter exists). function _parkRelayCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'relay' } }; } // _parkRelayBuild(cell): the board. Seed-pure, persona-blind (C1). CONSTRUCTED, not rejection-sampled: // every separator is placed on every seed, so the seed draws the LAYOUT and never whether the cell // poses its scene at all. The anti-mimic axes are the SIDE the companion's half sits on and the ROW // the meadow band runs along — a demo learner can carry over neither "go west" nor "the band is at // row 7", and the whole board mirrors with the first draw instead of running off the edge. function _parkRelayBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_RELAY_N; const r = rng((seed * 3323 + 1493) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const K = (x, y) => y * n + x; const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) wall.add(K(x, y)); // THE SIDE, drawn first off the rng stream (byte-stable). `inward` points from the counter into the // walker's (wide) half, so every offset below mirrors with the draw. const mateWest = cs(0, 1) === 0; const CX = mateWest ? 4 : n - 5; // the counter column: 3 columns of yard behind it const inward = mateWest ? 1 : -1; const W = (d) => CX + d * inward; // d = 1..7, the walker's half const M = (d) => CX - d * inward; // d = 1..3, the companion's half // THE BAND ROW — the second anti-mimic axis, and the thing that makes the walker's OWN errand cost // a heart or a detour. Mid-board, so both halves hold a chain gem and an approach. const HY = cs(5, 7); // THE THROAT — three columns of floor through the band, and the THIRD anti-mimic axis. Three, not // two: the outer two throat columns sit beside a band end and are therefore VERGE (safe 8), while // the middle one is distDeep 2 and stays plain WALKWAY (safe 1). A two-wide gap makes every gap // cell verge, the caution-led route stops existing, and the run CAPS instead of ABSTAINING — // measured on y24 (20 of 40 seeds) and again here before this shape replaced a partial band. // It never starts at W(1): the band cell there is what makes both delivery stands verge. const TX = cs(3, 5); // THE DEEP MEADOW — one band across the walker's whole half, minus the throat. It does two jobs // with one piece of terrain: // THE STANDS — (W(1),HY) is always band, so the two cells above and below it are VERGE. Those // ARE the delivery stands. The price of reaching over the counter is therefore paid in the // shipped safety frame and this module never says a word about caution. // THE CROSSING — the walker's own chain runs down the counter-side lane and the band cuts it, so // the beeline wades one cell for a heart while the caution route walks out to the throat and // back. That is the whole G-C separation, delivered by the shipped fast/safe metrics. const deep = new Set(); for (let d = 1; d <= 7; d++) if (d < TX || d > TX + 2) deep.add(K(W(d), HY)); // THE SAFETY FRAME — built exactly as _parkBuild builds it (multi-source BFS from the deep set, // walls transparent: 0 deep / 1 verge / >=2 walkway), because the SHIPPED static caution attitude // reads distDeep / verge / walkway and nothing else. const distDeep = new Array(n * n).fill(Infinity); { const q = []; for (const k of deep) { distDeep[k] = 0; q.push(k); } for (let h = 0; h < q.length; h++) { const k = q[h], qx = k % n, qy = (k / n) | 0; for (const d of DIRS) { const nx = qx + d.x, ny = qy + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[k] + 1) { distDeep[nk] = distDeep[k] + 1; q.push(nk); } } } } const verge = new Set(), walkway = new Set(); for (let k = 0; k < n * n; k++) { if (wall.has(k) || deep.has(k)) continue; (distDeep[k] === 1 ? verge : walkway).add(k); } // THE COUNTER — plain floor, top to bottom, no gap. It is a barrier because this module masks it, // not because anything was built there (constraint 3 above). const counter = new Set(); for (let y = 1; y <= n - 2; y++) counter.add(K(CX, y)); // THE SHELVES and THE GEMS — four DISTINCT counter cells, two on each side of the band. Each shelf // opens the gem next to it, and a shelf is never a gem (constraint 2). One shelf per side is not a // decoration: it is what makes the relay TWO trips with the crossing between them, so the log // carries two beats and "when did he do it" is a fact about him. const pass = [K(CX, HY - 1), K(CX, HY + 1)]; const gems = [K(CX, HY - 2), K(CX, HY + 2)]; const stand = [K(W(1), HY - 1), K(W(1), HY + 1)]; const opens = [[gems[0]], [gems[1]]]; // the move that reaches over the counter from a stand — one column, so it is the same key for both // and it mirrors with the side draw. const passMv = mateWest ? ['L', 'L'] : ['R', 'R']; // THE CRATES — one per shelf, and each sits ON the walker's own lane, one step short of its stand. // THE CRATE IS ON YOUR WAY; THE COUNTER IS NOT, and that is the whole sentence this cell is trying // to say. MEASURED over seeds 1..40 x 6 personas (80 runs per top-mind): a goal-led walker picks up // 1.00 crates per run and delivers 0.00; a caution-led walker picks up 1.00 and delivers 0.00; a // care-led walker picks up 2.00 and delivers 2.00. Nobody FAILS TO NOTICE the crate — two of the // three minds carry one to the end of the run and never turn. That is a different fact about them // than "he never found it", and putting the crates off the lane loses it. (It loses nothing else: // the off-lane placement also sweeps 40/40 under the facet as it now stands, so this is a // legibility choice and not a correctness one, and it is recorded as such.) // Neither is a token cell (the y26 law). const goods = [K(W(1), HY - 2), K(W(1), HY + 2)]; // THE TOKENS. The walker's own lane is the counter-side column, and the band cuts it: chain 0 above // the band, chain 1 below it, chain 2 in the far corner. park.chain is ordered, so the crossing is // compulsory — the beeline wades one deep cell for a heart and the no-deep read walks out to the // throat and back. That is the entire G-C separation and no facet of ours is involved in it. const tokens = [ { x: W(1), y: HY - 3, v: cs(2, 3), alive: true, guard: false }, // chain 0 (above the band) { x: W(1), y: HY + 3, v: cs(2, 3), alive: true, guard: false }, // chain 1 (BELOW — the crossing) { x: W(6), y: n - 2, v: cs(2, 3), alive: true, guard: false }, // chain 2 (far corner) // THE COMPANION'S TWO CONTRACT GEMS — ON THE COUNTER. Not a wall, not a rule: they are simply // in a place neither body can enter until a crate is left on the matching shelf. { x: CX, y: HY - 2, v: cs(1, 2), alive: true, guard: false }, { x: CX, y: HY + 2, v: cs(1, 2), alive: true, guard: false }, ]; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const spawn = { x: W(1), y: 1 }; // HIS WHOLE ERRAND IS ON HIS OWN SIDE and consists of waiting. Station beside each gem, so the beat // a shelf is served he is one step away and the relay reads as a relay rather than as a march. const companionSpawn = { x: M(1), y: HY - 2 }; const retire = { x: M(2), y: n - 2 }; const park = { N: n, seed, k: 0, fieldMech: 'relay', deep, verge, walkway, distDeep, relay: { counter, pass, gems, stand, goods, opens, passMv, passGem: [3, 4], cx: CX, mateWest, hy: HY, tx: TX, throat: [K(W(TX), HY), K(W(TX + 1), HY), K(W(TX + 2), HY)] }, clusters, chain: [0, 1, 2], contracts: [{ gem: 3, station: { x: M(1), y: HY - 2 } }, { gem: 4, station: { x: M(1), y: HY + 2 } }], retire, spawn, companionSpawn, // trig is his ENGAGEMENT radius (manhattan, walker-to-gem). Generous on purpose: a contract // nobody is serving is what this whole cell is about, and a companion who never engages would // make the hold unobservable. trig: n * 2, cap: 100, minTurns: 12, cautionD: 2, damage: 1, cell: _parkRelayCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: companionSpawn.x, y: companionSpawn.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // OPT-IN runtime container (Task 0) // carrying: the good in his hands, or null. delivered: the ordered CONFESSION — which shelf, which // good, which beat. left: the goods still in the yard. Sets/arrays/plain objects only, so // _parkDeepClone carries the lot through every search fork. park.dyn.relay = { carrying: null, delivered: [], left: new Set([0, 1]) }; return st; } // _parkRelayServed(D, i): has shelf i already taken a good? A shelf holds ONE thing — which is what // makes "which shelf did he choose" a fact about him rather than a formality. function _parkRelayServed(D, i) { for (const e of D.delivered) if (e.pass === i) return true; return false; } // _parkRelayOpen(P, key): is `key` a gem cell whose shelf has been served? This is the ENTIRE // companion-side mechanism, and it is a pure read of dyn + the seed-fixed opens table. function _parkRelayOpen(P, key) { const R = P.st.park.relay, D = P.st.park.dyn && P.st.park.dyn.relay; if (!R || !D) return false; for (const e of D.delivered) { const ks = R.opens[e.pass]; if (ks) for (const k of ks) if (k === key) return true; } return false; } // _parkRelayApproach(P, target): step distance from every cell to `target` over the domain the WALKER // would actually use for this errand — non-wall, off the counter, and off the deep meadow, because an // errand routed through the hazard would be buying the favour with his body and the care read is a // PATH read, never a price. Returns an all-Infinity table when there is no target. function _parkRelayApproach(P, target) { const st = P.st, n = st.N, park = st.park, R = park.relay; const dist = new Array(n * n).fill(Infinity); if (target == null) return dist; dist[target] = 0; const q = [target]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || park.deep.has(nk) || R.counter.has(nk)) continue; if (dist[nk] > dist[k] + 1) { dist[nk] = dist[k] + 1; q.push(nk); } } } return dist; } // _parkRelayCtx(P): the ONE per-state read the care facet uses (the registry computes it once per // state). AIM is the cell he has to stand on next for the relay to advance: // empty-handed -> the nearest good still in the yard (mv null: entering it IS the act). // carrying -> the stand of the nearest shelf that would open a gem the companion still owes // (mv = the reach over the counter, which legalAdd force-opens from that cell). // LIVE also requires an unserved shelf whose gem is still unbanked, so the facet self-gates the beat // the relay is finished. Every term is a public read of board + dyn + tokens (C1). function _parkRelayCtx(P) { const st = P.st, park = st.park, R = park.relay, D = park.dyn && park.dyn.relay; const cold = { live: false, aim: null, mv: null, dist: [] }; if (!R || !D) return cold; const useful = []; for (let i = 0; i < R.pass.length; i++) { if (_parkRelayServed(D, i)) continue; const tok = st.tokens[R.passGem[i]]; if (tok && tok.alive) useful.push(i); } if (!useful.length) return cold; if (D.carrying == null && D.left.size === 0) return cold; const here = _parkKey(st, st.pos[0]); let aim = null, mv = null, dist = null, best = Infinity; if (D.carrying != null) { for (const i of useful) { const dd = _parkRelayApproach(P, R.stand[i]); if (dd[here] < best) { best = dd[here]; aim = R.stand[i]; mv = R.passMv[i]; dist = dd; } } } else { for (const gi of D.left) { const dd = _parkRelayApproach(P, R.goods[gi]); if (dd[here] < best) { best = dd[here]; aim = R.goods[gi]; mv = null; dist = dd; } } } // RANGED — see PARK_RELAY_REACH. A walker who is nowhere near the next cell of the relay has no // relay move to make, so this facet says nothing rather than pulling him at a square the mind above // it may be refusing. That pull is what turned a caution-led abstention into a capped orbit. if (aim == null || !(best <= PARK_RELAY_REACH)) return cold; return { live: true, aim, mv, dist }; } // _parkRelayPlay(cell, persona): a faithful playout recording the OBSERVABLES the signature reads. // Pure reads of public state; the persona goes to the oracle and nowhere near the board. function _parkRelayPlay(cell, persona) { const P = parkStart(_parkRelayBuild(cell)); while (!P.over) parkStep(P, parkOracleMove(P, persona)); const st = P.st, park = st.park, D = park.dyn.relay; P._relayDelivered = D.delivered.length; P._relayShelves = D.delivered.map(e => e.pass).sort().join(','); P._relayMateHome = park.contracts.every(c => !st.tokens[c.gem].alive); return P; } // _parkRelaySignature(playouts): the field's OWN visible signature (filter level), read off three // different observables because the three minds do not differ in SCORE here — every persona banks // the same chain, and a signature leaning on score would be reading noise. // SAFETY-top — 0 deliveries and 0 deep entries: he walks the far lane round the band and never // steps into the verge the shelves stand in. Abstention, not inability. // GOAL-top — wades the band (>= 1 deep entry), delivers nothing, and gets home in strictly fewer // turns than the caution baseline. The bound is DATA-DRIVEN — caution's own worst // time, never a constant. // CARE-top — BOTH care personas serve BOTH shelves and the companion banks both gems; and NO // other mind gets him home, so his arrival is attributable to the act. function _parkRelaySignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); let safeTurns = -Infinity, goalWorst = -Infinity; let careAll = true, careSeen = 0; for (let i = 0; i < playouts.length; i++) { const P = playouts[i]; if (top[i] === 'safety') { if (P._relayDelivered !== 0) return false; if (P.deepEntries !== 0) return false; if (P._relayMateHome) return false; safeTurns = Math.max(safeTurns, P.turns); } if (top[i] === 'goal') { if (P._relayDelivered !== 0) return false; if (P.deepEntries < 1) return false; if (P._relayMateHome) return false; goalWorst = Math.max(goalWorst, P.turns); } if (top[i] === 'care') { careSeen++; if (P._relayDelivered !== 2 || !P._relayMateHome) careAll = false; } } if (!isFinite(safeTurns) || !isFinite(goalWorst)) return false; // non-vacuity guard if (!(goalWorst < safeTurns)) return false; return careSeen > 0 && careAll; } // _parkRelayAdmissible(cell): the generate-then-filter PLAYABILITY gate over the FULL persona set. // Every faithful playout completes alive; the CARE-led ones actually get the companion home (a board // where even they cannot is unwinnable rather than unkind — that is `matelost`, and it is asked of // care alone, campaign.js:3190-3194); the field signature separates; the C-N scene is really POSED; // and every pair the board poses blind-recovers in the demonstrated direction. Reject reasons tallied // LOUDLY — a silent 0/40 teaches nothing. const _PARK_RELAY_WHYS = { complete: 0, dead: 0, matelost: 0, sig: 0, nocn: 0, norec: 0 }; function _parkRelayAdmissible(cell) { const playouts = []; for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = _parkRelayPlay(cell, PARK_PERSONAS[i]); if (P.reason === 'death') { _PARK_RELAY_WHYS.dead++; return false; } if (P.reason !== 'complete') { _PARK_RELAY_WHYS.complete++; return false; } if (P.hearts < 1) { _PARK_RELAY_WHYS.dead++; return false; } if (PARK_PERSONAS[i][0] === 'care' && !P._relayMateHome) { _PARK_RELAY_WHYS.matelost++; return false; } playouts.push(P); } if (!_parkRelaySignature(playouts)) { _PARK_RELAY_WHYS.sig++; return false; } let posed = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_RELAY_PAIRS) { if (!(parkPairExpressed(_parkRelayBuild(cell), playouts[i].moves, pair) > 0)) continue; if (pair[0] === 'C' && pair[1] === 'N') posed++; const expect = _parkPushPairDir(persona, pair); if (!parkRecoverPairLex(_parkRelayBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_RELAY_WHYS.norec++; return false; } } } if (posed < 2) { _PARK_RELAY_WHYS.nocn++; return false; } return true; } // parkRelayWhys(): a copy of the reject tally (gates read it as a DELTA around their own sweep). function parkRelayWhys() { return { ..._PARK_RELAY_WHYS }; } // ---- THE REGISTRATION. Everything the engine body knows about y31 hangs here, off the id the board // stamps as park.fieldMech. PARK_FIELD_MECHS.relay = { build: _parkRelayBuild, cell: _parkRelayCell, admits: _parkRelayAdmissible, // THE MASK — three domains, and the counter is shut in all of them. The ONE exception is a shelf // the walker is currently able to use, and it opens for 'me' and for 'route' under the SAME // predicate: a cell legalAdd offers and the route metric hides is a dead move (engine.js:7941-7947, // y16 design law 1). The gem cells stay masked everywhere forever — safe precisely because 'me' // never force-opens them, so the walker has no means of standing there and no metric of his ever // needs to price the cell. legalMask: (P, key, who) => { const park = P.st.park, R = park.relay, D = park.dyn && park.dyn.relay; if (!R || !D) return false; if (!R.counter.has(key)) return false; // only the counter is ever masked const i = R.pass.indexOf(key); if (i >= 0 && D.carrying != null && !_parkRelayServed(D, i)) return who === 'mate'; return true; }, // THE ADDITIVE OVERRIDE — two openings, and they are for different travellers. // 'me' A SHELF, while he is carrying something and it has not been served. That is y20's held // gate: the move exists because of what is in his hands, not because of where he is. // 'mate' THE GEM the served shelf paid for. This arm is the load-bearing one: the companion's // planner is stopped by this module's own mask, and legalAdd is the sanctioned override // for exactly that (engine.js:7605-7613 — walls stay walls, masks do not). legalAdd: (P, key, who) => { const park = P.st.park, R = park.relay, D = park.dyn && park.dyn.relay; if (!R || !D) return false; if (who === 'mate') return _parkRelayOpen(P, key); if (who !== 'me') return false; const i = R.pass.indexOf(key); return i >= 0 && D.carrying != null && !_parkRelayServed(D, i); }, // THE TWO CONSEQUENCES. onEnter: (P, ev) => { // onEnter fires for EVERY move key, 'stay' included (engine.js:7324) — and a walker standing // still on a crate must not pick it up once a beat. Say so explicitly (lantern's idiom). if (ev.mvKey === 'stay') return; const st = P.st, park = st.park, R = park.relay, D = park.dyn && park.dyn.relay; if (!R || !D) return; // THE DELIVERY — an ACTION, so put him back (engine.js:7315-7319, y3). He leans over the counter; // he never stands on it. P.path records where he ACTUALLY stands (engine.js:8162), so the trace // stays honest, and the deep-entry charge cannot bite because a shelf is never deep (pinned in // the builder gate — the charge fires BEFORE this hook, engine.js:8130). const i = R.pass.indexOf(ev.toKey); if (i >= 0) { if (D.carrying != null && !_parkRelayServed(D, i)) { D.delivered.push({ beat: park.dyn.beat, pass: i, good: D.carrying }); D.carrying = null; st.fx.push({ k: 'relay', x: ev.to.x, y: ev.to.y }); // render hook (ZERO-TEXT) } st.pos[0] = { x: ev.from.x, y: ev.from.y }; return; } // THE PICKUP — an ordinary step, and one good at a time. if (D.carrying != null) return; const gi = R.goods.indexOf(ev.toKey); if (gi >= 0 && D.left.has(gi)) { D.left.delete(gi); D.carrying = gi; st.fx.push({ k: 'lift', x: ev.to.x, y: ev.to.y }); // render hook (ZERO-TEXT) } }, // THE READS — ONE facet, folded into the shipped CARE mind. There is deliberately no G facet and // no C facet. The shelf is a dead end, so the shipped goal preference (strictly reduce the beeline) // strikes the delivery out without being told; the stands are verge, so the shipped caution // preference refuses them without being told. Teaching either in a module would be re-deriving what // the engine already does for free (y26's finding, restated). reads: { ctx: _parkRelayCtx, N: { // CARE: a good is still gettable and a shelf that would free the companion is still empty. // Self-gated — the beat the relay is finished, or his gems are banked, this goes cold. engaged: (P, ctx) => !!(ctx && ctx.live), // the PATH read: close on the aim, and reach over the counter once you are on it. Never {} — an // empty prefer would not veto, it would silently delete care from the decision entirely // (engine.js:7369-7377). // // THE JUST-VACATED SQUARE IS EXCLUDED, exactly as the shipped goal preference excludes it // (engine.js:7702). A bare distance gradient invites a two-square orbit whenever the mind above // this one pulls the other way — an approach that re-enters the square it just left is not an // approach, and saying so costs this facet nothing on any path that is really one. prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.live) { for (const c of legal) out.add(c.k); return out; } // cold: say nothing const here = _parkKey(P.st, P.st.pos[0]); if (ctx.mv != null && here === ctx.aim) { for (const c of legal) if (c.k === ctx.mv) out.add(c.k); if (out.size) return out; } else { // ...EXCEPT ON THE LAST STEP, and that exception is the relay itself rather than a // loophole. A crate sits one cell PAST its own stand on the lane, so the walker steps off // the stand onto the crate and turns straight back — the one square he just left IS the // destination. Suppressing that turn left care-led runs delivering the first crate and // walking away from the second (MEASURED: 40/40 matelost). At distance 1 the only // distance-reducing move is the destination itself, so the rule has nothing left to guard. const cur = ctx.dist[here]; if (isFinite(cur)) for (const c of legal) { if (cur > 1 && P.prev && P.prev.x === c.x && P.prev.y === c.y) continue; if (ctx.dist[c.key] < cur) out.add(c.k); } if (out.size) return out; } for (const c of legal) out.add(c.k); return out; }, }, }, }; // PARK_RELAY_SHIPPABLE — the MODULE's own ship pin, read by CAMP-CROSS-SWEEP's F4 through // engine.test.js's FIELD_SHIPPABLE map. y31 is seated as an honest PREVIEW, so this is a `false` // LITERAL and not a measurement: the derived form (`_parkRelayRecovers`, the _parkBombRecovers shape) // belongs to the session that actually measures the pairing, and writing it early would put a number // on the slot before anyone had run it. derive-never-assert cuts both ways — do not assert TRUE, and // do not dress a false up as derived either. const PARK_RELAY_SHIPPABLE = false; /* ============ END RELAY FIELD MODULE (builder + the masked counter, the armed shelf and its action-move delivery, the per-shelf mate opening, the relay care facet, the abstention/wade/errand signature, and the admission gate) ================================================================ */ /* ============ FIRE FIELD MODULE (y17 "one bucket, a spreading fire", Task 5, plan 2026-07-14) ====== A SELF-CONTAINED park FIELD module registered through PARK_FIELD_MECHS. The movement verb is the walk's; the goal grammar is HARVEST (an ordered chain — R1 keeps the DEMO leg on a walk-native `reach` grammar, so a replayed demo meets a play board whose harvest RHYTHM it never rehearsed). What is new — and it is this cell's OOD axis — is a CLOCK IN THE GROUND: a fire that spreads one tile per `_parkSchedule` beat down THREE fronts at once, and a single bucket of water the walker fills at the stream and spends by dousing ONE front, then must refill to spend again. Where he spends each bucketful, and in WHAT ORDER, is a confession of his mind-ORDER, exactly as y16's one stone was — this cell is y16's two-leg tool oracle with the tool made REFILLABLE and a spread clock laid over it. FILL — step onto a stream-adjacent bank cell (dyn.water := true). Free and repeatable; the SPEND is the decision, re-posed on every refill (매 재충전이 재선택). DOUSE — from the hub, BUMP the burning mouth of one front (legalAdd + action-restore, the y3 assist idiom): that front goes out (dyn.doused), its spread stops, its asset is spared. The bucket empties; to douse another front you walk back to the stream. THREE FRONTS, one per mind, DISJOINT at the hub (a partition, not a subtraction — the one shape that awards a C-vs-N pair, y10's finding): G (goal) the GEM-CLUSTER front (north). Dousing it also opens the SHORT way to the north chain gems — a shortcut you hold the bucket to; unarmed the beeline goes the long way round (design law 1, carry's law verbatim). N (care) the COMPANION-STATION front (east). Care spares the man's seat. C (safety) the SPAWN-RETURN front (west). Safety keeps its own line of retreat open. BECAUSE THE BUCKET REFILLS, a persona does not confess only its TOP mind (y16's single spend) — it douses EVERY front, in PREFERENCE ORDER, so the douse SEQUENCE is the whole order (the subordinate pair the fork alone cannot pose — seam trap 4 — is posed by the SECOND douse, for free). Whether that is enough for 6/6 blind recovery is a MEASUREMENT the gate takes, not an assumption. DESIGN LAWS (each earns its place; the seam traps behind them are y16's, inherited verbatim): 1. THE ROUTE DOMAIN MUST SEE A FRONT THE WALKER CAN OPEN. A front's mouth is legal ONLY through legalAdd, which has NO 'route' domain, so its route-metric distance is Infinity and the argmin hands back 'stay' → parkStep rejects the oracle's own move as noise. So legalMask opens every undoused front to the 'route' domain WHILE THE WALKER HOLDS WATER — which is also TRUE: a fire you are carrying the bucket to put out is a way through. Empty the bucket and it shuts again. 2. THE SHORTCUT IS A ROUTE, NOT A BAIT. By law 1 the fast field sees the north shortcut open only while armed; unarmed the beeline takes the long way. A fast field that always saw through the fire would walk a bucketless goal-led walker up to a wall of flame, find his only reducing move illegal, empty G's preference and strand him (y12's measured livelock). Conditioning on `water` removes the trap. 3. NO DEEP FIELD, AND THAT IS DELIBERATE (the y16 SEAM ASYMMETRY, read the other way). park.deep is mutable but park.distDeep is built ONCE, so a cell the fire makes hazardous at runtime would be INVISIBLE to the shipped safety attitude (it prices off distDeep) — C would walk into fire it could not see. Rather than freeze the whole burnable region deep from turn 1 (which would make the spread invisible to the DRAMA — a board-constant hazard the walker just routes round), this cell makes the fire BLOCKED TERRAIN, never deep: the walker never stands in fire (he douses it from beside it), so no heart is ever spent on the field and ♥ never confuses the mind channel (design law 1's spirit, y10's exact choice — the flood is a terminal, not a heart; the fire is a wall, not a heart). The SAFETY axis is carried ENTIRELY by the mechanic facet (douse the retreat front), which ENGAGES a C the deepless walk board leaves cold — the seam's OR-ed engagement, used exactly as intended. 4. EVERY CHAIN GEM IS REACHABLE WITH NO DOUSE AT ALL. The bucket is a shortcut, never a key: a perimeter ring reaches every north gem the long way, so a persona whose bucket went elsewhere always completes (global constraint 3 — a cell where a persona cannot finish is a broken board, not a measurement). 5. STEER EACH MIND OVER ITS OWN LAWFUL DOMAIN (seam trap 1). A facet's prefer() is INTERSECTED with the shipped attitude's compliant set; steering a mind down a corridor its own caution band forbids empties the intersection and annihilates the mind FROM THE OUTSIDE. With no deep field the caution band is vacuous here, but the discipline is kept (_parkFireDistFrom carries y16's `att==='C'` band guard) so a later tightening cannot silently reintroduce the trap. */ const PARK_FIRE_N = 15; const PARK_FIRE_PERIOD = 4; // beats per tile — GENEROUS to start (y10's lesson) const PARK_FIRE_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; const _PARK_FIRE_ATT_FRONT = { G: 0, C: 1, N: 2 }; // which front each mind owns // _parkFireBuild(cell): the y17 board — pure PUBLIC geometry (reads ONLY cell.seed; C1: no persona // parameter exists), runtime-compatible with parkStart/parkStep. Frame (15x15): a central HUB from // which three FRONTS fork (north = gems, west = spawn-return, east = companion station), each a // blocked corridor the fire spreads down; a south FILL CORRIDOR to the stream; a perimeter RING that // reaches every north chain gem the long way (design law 4). Two mirrors (E/W and N/S) flip the // compass by seed so no "walk north == goal" board-invariant survives for a mimic to ride (y16's // anti-mimic measurement). Outside the walk archetype pools by construction (append-only, walk-only). function _parkFireBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_FIRE_N; const r = rng((seed * 2657 + 7411) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const flip = r() < 0.5, flipY = r() < 0.5; const M = (x) => flip ? n - 1 - x : x; // every coordinate below is written UNMIRRORED const MY = (y) => flipY ? n - 1 - y : y; const K = (x, y) => MY(y) * n + M(x); const PT = (x, y) => ({ x: M(x), y: MY(y) }); const hx = 7, hy = 7; // the HUB (the fork; the walker douses from here) const wall = new Set(), walkway = new Set(), verge = new Set(), deep = new Set(), water = new Set(); const add = (x, y) => walkway.add(K(x, y)); // THE HUB + the south FILL CORRIDOR down to the stream bank. add(hx, hy); for (let y = 8; y <= 12; y++) add(hx, y); // (7,8..12): the corridor, ending on the south ring for (let y = 9; y <= 11; y++) water.add(K(hx + 1, y)); // the stream: (8,9..11), east of the corridor const fillCells = [K(hx, 9), K(hx, 10), K(hx, 11)]; // bank cells (corridor, stream-adjacent) — fill here const bankKey = K(hx, 9); // the fill cell nearest the hub (the leg-1 target) // THE THREE FRONTS. Each is a corridor of BLOCKED cells from the hub outward, ending in its ASSET. // corridor[0] is the MOUTH (adjacent to the hub) — the only cell the walker bumps to douse. const mkFront = (att, dx, dy) => { const corridor = []; for (let i = 1; i <= 3; i++) corridor.push(K(hx + dx * i, hy + dy * i)); const asset = K(hx + dx * 4, hy + dy * 4); return { att, dx, dy, corridor, mouth: corridor[0], asset }; }; const fronts = [mkFront('G', 0, -1), mkFront('C', -1, 0), mkFront('N', 1, 0)]; // north / west / east // THE PERIMETER RING (design law 4 — the long way round to every north gem). for (let y = 2; y <= 12; y++) { add(2, y); add(12, y); } // west & east lanes for (let x = 2; x <= 12; x++) { add(x, 2); add(x, 12); } // north & south connectors // THE ASSET CELLS are ordinary ground (never blocked) — only the corridor cells block. The asset // is what the fire CONSUMES if it reaches the end undoused; the walker never needs to stand there. for (const f of fronts) walkway.add(f.asset); // Reconnect the north spine so a DOUSED gem-front opens the short way onto the north connector: the // cell just past the G front's asset must be walkway (it already is — it is the (7,3) asset — and // (7,2) is on the north connector). Nothing more to add: dousing turns the corridor walkable. // THE CHAIN GEMS — all on the north connector, reachable by the ring with no douse (law 4). gem 0 // sits at the head of the gem-front shortcut (dousing G is the SHORT way to it). const g0 = PT(hx, 2), g1 = PT(11, 2), g2 = PT(3, 2); // THE COMPANION'S CONTRACT — one gem up the east lane, his station in the SE corner so he is still // trudging in when the walker passes (the contested-gem scene, posing G-N through the shipped read; // redundant with the fork but cheap, and it keeps a live companion on the board for the care facet). const cg = PT(12, cs(3, 5)); const st0 = PT(12, 12), retire = PT(2, 12); // BUILD the terrain sets. Corridor + stream are NOT walkway (blocked / not-ground); everything the // ring/hub/corridor did not claim is WALL (full control — this board has NO meadow and NO deep // field by design law 3, so distDeep is Infinity everywhere and the shipped caution band never // engages; the safety axis is the mechanic facet's alone). const cellFront = new Map(); for (const f of fronts) for (const kk of f.corridor) cellFront.set(kk, fronts.indexOf(f)); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = y * n + x; if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } if (walkway.has(kk) || water.has(kk) || cellFront.has(kk)) continue; // claimed wall.add(kk); // meadow -> wall (no deep) } const distDeep = new Array(n * n).fill(Infinity); // no deep sources: Infinity everywhere (law 3) const tokens = [ { ...g0, v: cs(2, 3), alive: true, guard: false }, // chain 0 — head of the gem-front shortcut { ...g1, v: cs(2, 3), alive: true, guard: false }, // chain 1 — east end of the north connector { ...g2, v: cs(2, 3), alive: true, guard: false }, // chain 2 — west end { ...cg, v: cs(1, 2), alive: true, guard: false }, // the companion's contract gem (up the east lane) ]; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const spawn = PT(hx, 12); // the walker starts at the foot of the corridor const fire = { fronts: fronts.map(f => ({ att: f.att, corridor: f.corridor.slice(), mouth: f.mouth, asset: f.asset })), cellFront, water, fillCells, bankKey, period: PARK_FIRE_PERIOD, mouths: new Set(fronts.map(f => f.mouth)), // the per-front deterministic beat PHASE off the PUBLIC seed (so the three fronts stagger instead // of igniting in lockstep — the schedule primitive Task 0 exists for; never fed a persona symbol). phase: _parkSchedule(seed, fronts.length, PARK_FIRE_PERIOD), }; const park = { N: n, seed, k: 0, fieldMech: 'fire', // the REGISTRY key (Task 1) — the ONLY thing the body knows walkway, verge, deep, distDeep, water, fire, // SEED-PURE (immutable): fronts, stream, schedule. Runtime on dyn. clusters, chain: [0, 1, 2], needPairs: true, needTypes: 0, contracts: [{ gem: 3, station: st0 }], retire, spawn, companionSpawn: { x: st0.x, y: st0.y }, trig: 5, cap: 200, minTurns: 12, cautionD: 2, damage: 1, cell: _parkFireCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: st0.x, y: st0.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // OPT-IN runtime container (Task 0) park.dyn.water = false; // the bucket is empty park.dyn.doused = new Set(); // front indices put out (irreversible) park.dyn.seq = []; // the ORDER the fronts were doused (att keys) — the confession park.dyn.burning = _parkFireBurningSet(st); // derived; refreshed every tick (painter + determinism gate) park.dyn.burnedAssets = new Set(); // asset front-indices the fire reached before a douse return st; } // _parkFireCell(seed): the module's PUBLIC play-cell. `mech.fieldMech: 'fire'`. Goal grammar HARVEST // (R1 — the DEMO leg's `reach` is set on the slot, not here; this is the PLAY cell). A pure value // constructor; no persona parameter exists (C1). function _parkFireCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'fire' } }; } // _parkFireFrontIdx(front, beat, doused): how many cells of a front's SPREAD (corridor + asset) have // caught. Front i lights its mouth when beat >= phase[i], then advances one cell every `period` // beats. FROZEN at its douse-time reach (once out, it never spreads again). A pure function of the // PUBLIC beat + the doused set — deterministic, idempotent, fork-safe (dyn is a Set of primitives). function _parkFireFrontIdx(fire, i, beat) { const ph = fire.phase[i]; if (beat < ph) return 0; return Math.min(fire.fronts[i].corridor.length + 1, 1 + Math.floor((beat - ph) / fire.period)); } // _parkFireBurningSet(st): the cell keys currently on fire — corridor mouth outward, plus the asset // once the fire has run the whole corridor. Doused fronts contribute nothing. Derived; never stored // authoritatively (dyn.burning is a refreshed cache for the painter + the determinism gate). function _parkFireBurningSet(st) { const fire = st.park.fire, dyn = st.park.dyn, beat = dyn ? dyn.beat : 0; const out = new Set(); for (let i = 0; i < fire.fronts.length; i++) { if (dyn && dyn.doused.has(i)) continue; const f = fire.fronts[i], reach = _parkFireFrontIdx(fire, i, beat); for (let j = 0; j < reach && j < f.corridor.length; j++) out.add(f.corridor[j]); if (reach > f.corridor.length) out.add(f.asset); } return out; } // _parkFireBlocked(P, key, who): THE ONE TERRAIN PREDICATE, shared by legalMask and the steering BFS. // The stream is never ground. An undoused front CORRIDOR is a wall of flame — EXCEPT to the 'route' // metric while the walker holds water (design law 1), so the fast field can see the shortcut it is // armed to open. Douse it and the corridor is ordinary walkway. Pure read of board + dyn (never the // persona — C1). function _parkFireBlocked(P, key, who) { const park = P.st.park, fire = park.fire, dyn = park.dyn; if (fire.water.has(key)) return true; // the stream is not ground, for anyone if (!fire.cellFront.has(key)) return false; const i = fire.cellFront.get(key); if (dyn.doused.has(i)) return false; // put out — walkable now (and the shortcut opens) const armed = who === 'route' && dyn.water; // law 1: the metric sees a front you can open return !armed; } // _parkFireDistFrom(P, srcKey, att): step distance from a douse mouth / the bank to every cell, over // THE STEERING MIND'S OWN LAWFUL DOMAIN (seam trap 1 / design law 5). The source is seeded at 0 even // when it is itself blocked (a mouth is somewhere you reach and act on, not walk through). The // `att==='C'` caution-band guard is vacuous on this deepless board but kept, so a later tightening // cannot silently reintroduce the empty-intersection trap. Deterministic; a pure read (never persona). function _parkFireDistFrom(P, srcKey, att) { const st = P.st, n = st.N, park = st.park; const dd = park.distDeep, band = park.cautionD || 2; const d = new Array(n * n).fill(Infinity); d[srcKey] = 0; const q = [srcKey]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const dir of DIRS) { const nx = x + dir.x, ny = y + dir.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (d[nk] < Infinity || st.wall.has(nk)) continue; if (_parkFireBlocked(P, nk, 'me')) continue; if (att === 'C' && dd[nk] < band) continue; // the caution band IS safety's road network d[nk] = d[kk] + 1; q.push(nk); } } return d; } // _parkFireCtx(P): the ONE per-state read the three facets share. `live` = some front still burning // (all facets go cold TOGETHER when the last one is out, so harvest can proceed). For each still-live // front, the mind's target is its own MOUTH when armed, the STREAM BANK when not — the two-leg shape. // A doused mind's target is null (it has nothing left to steer to → inert). function _parkFireCtx(P) { const st = P.st, park = st.park, fire = park.fire, dyn = park.dyn; const here = _parkKey(st, st.pos[0]); const live = fire.fronts.some((f, i) => !dyn.doused.has(i)); const ctx = { live, water: dyn.water, here, fire, d: {} }; if (!live) return ctx; for (const att of ['G', 'C', 'N']) { const i = _PARK_FIRE_ATT_FRONT[att]; if (dyn.doused.has(i)) { ctx.d[att] = null; continue; } // my front is out — nothing to steer to const target = dyn.water ? fire.fronts[i].mouth : fire.bankKey; ctx.d[att] = _parkFireDistFrom(P, target, att); } return ctx; } // _parkFireSteer(legal, ctx, att): THE TWO-LEG NARROWING (y16's shape). Cold → hand back EVERY legal // move (inert, never {} — an empty prefer does not veto, it makes the mind INERT and hands the decision // silently to the next; seam trap 1). My-front-out: C/N stay inert on FULL legal (their shipped frames // are already cold here), but G alone YIELDS an empty prefer so it is removed from THIS turn's decision // and the next mind rules (the C-N scene finally surfaces). Otherwise narrow to the moves that close the // distance to my target (the mouth when armed, the bank when not); if none do, inert again. function _parkFireSteer(legal, ctx, att) { const out = new Set(); if (!ctx.live) { for (const c of legal) out.add(c.k); return out; } const d = ctx.d[att]; if (!d) { // my front is out while others burn. C/N stay inert (full legal — their shipped frames are // already cold here). G alone YIELDS: an empty prefer removes the mind for this turn (seam // contract, engine.js:7369) so the next mind rules and the C-N scene finally surfaces. if (att === 'G') return out; for (const c of legal) out.add(c.k); return out; } const cur = d[ctx.here]; for (const c of legal) if (d[c.key] < cur) out.add(c.k); if (!out.size) for (const c of legal) out.add(c.k); // nothing to say — inert, not a veto return out; } // _parkFireSignature(playouts): the field's OWN visible signature, aligned with PARK_PERSONAS — the // FIRST front each top mind douses is ITS OWN (goal saves the gems first, safety its retreat, care the // companion). Filter-level, like every module signature: it gates which candidate survives the sweep. const _PARK_FIRE_FIRST = { goal: 'G', safety: 'C', care: 'N' }; function _parkFireSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < playouts.length; i++) { const seq = playouts[i].st.park.dyn.seq; if (!seq.length || seq[0] !== _PARK_FIRE_FIRST[top[i]]) return false; } return true; } // _parkFireAdmissible(cell): the generate-then-filter PLAYABILITY gate over the FULL persona SET (a // constant — the accepted board stays a pure function of the public cell): every persona's faithful // playout COMPLETES alive, the FIRST douse lands on the top mind's front (the signature), the fork // actually poses C-vs-N, and every pair the board POSES is blind-recovered in the demonstrated // direction. The SHIP bar (_parkFireRecovers, 6/6 blind ORDER recovery) is strictly stronger and lives // above this — admissible-but-not-recoverable ships DEMO-ONLY (P9/P10). Reject reasons tallied LOUDLY. const _PARK_FIRE_WHYS = { complete: 0, dead: 0, short: 0, sig: 0, unexpressed: 0, nocn: 0, norec: 0 }; function _parkFireAdmissible(cell) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkFireBuild(cell), persona); if (P.reason !== 'complete') { _PARK_FIRE_WHYS.complete++; return false; } if (P.hearts < 1) { _PARK_FIRE_WHYS.dead++; return false; } if (P.turns < 12) { _PARK_FIRE_WHYS.short++; return false; } playouts.push(P); } if (!_parkFireSignature(playouts)) { _PARK_FIRE_WHYS.sig++; return false; } let posed = 0, cn = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_FIRE_PAIRS) { if (!(parkPairExpressed(_parkFireBuild(cell), playouts[i].moves, pair) > 0)) continue; posed++; if (pair[0] === 'C') cn++; const expect = _parkPushPairDir(persona, pair); if (!parkRecoverPairLex(_parkFireBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_FIRE_WHYS.norec++; return false; } } } if (posed === 0) { _PARK_FIRE_WHYS.unexpressed++; return false; } if (cn === 0) { _PARK_FIRE_WHYS.nocn++; return false; } return true; } // _parkFireRecovers(cell): THE SHIP GATE (P9/P10) — blind 6/6-persona ORDER recovery, out of the // shipped recovery stack and nothing else. True here is the ONLY licence for ship:true on the slot. function _parkFireRecovers(cell) { for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = parkPlayout(_parkFireBuild(cell), PARK_PERSONAS[i]); if (P.reason !== 'complete') return false; const rec = parkRecoverOrder(_parkFireBuild(cell), P.moves, PARK_CAL_TURNS); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) return false; } return true; } // NO MODULE-LEVEL GENERATOR (the y10/y16 decision): a FIELD mechanic is reached only through the // registry (mech.cell/mech.admits for the sweep, parkFieldBuild->mech.build for the board), so a // module seed-walk is structurally unreachable. The telemetry survives on `admits` — read _PARK_FIRE_ // WHYS as a TRIPWIRE (a monotone process global), never a per-sweep bound. function parkFireWhys() { return { ..._PARK_FIRE_WHYS }; } // ---- THE REGISTRATION. Everything the engine body knows about y17 is right here. PARK_FIELD_MECHS.fire = { build: _parkFireBuild, cell: _parkFireCell, admits: _parkFireAdmissible, // ONE PREDICATE, ALL THREE DOMAINS (design law 1 — 'route' is the load-bearing one). legalMask: (P, key, who) => _parkFireBlocked(P, key, who), // THE FRONT MOUTHS open ONLY to a walker holding water, and only to HIM: a douse is an ACTION, not // a walk. legalAdd overrides the mask, so this is what makes the bump legal at all. legalAdd: (P, key, who) => { if (who !== 'me') return false; const fire = P.st.park.fire, dyn = P.st.park.dyn; if (!dyn.water || !fire.mouths.has(key)) return false; return !dyn.doused.has(fire.cellFront.get(key)); }, // FILL and DOUSE. Filling is an ordinary step onto a bank cell; dousing is a force-opened ACTION // the walker never actually stands on (st.pos[0] goes straight back — the y3 assist idiom; P.path // records where he ACTUALLY stands, so the trace stays honest). onEnter: (P, ev) => { const st = P.st, park = st.park, fire = park.fire, dyn = park.dyn; const k = ev.toKey; if (fire.mouths.has(k) && dyn.water && !dyn.doused.has(fire.cellFront.get(k))) { const i = fire.cellFront.get(k); dyn.doused.add(i); dyn.seq.push(fire.fronts[i].att); // the confession, in order dyn.water = false; st.pos[0] = { x: ev.from.x, y: ev.from.y }; // an action, not a step (back to the hub) // NO DEEP REFUND IS OWED: this board has no deep field (design law 3), so the mouth was never // priced against park.deep and no heart was charged for the cell he never stands on. st.fx.push({ k: 'douse', x: ev.to.x, y: ev.to.y }); return; } if (!dyn.water && fire.fillCells.indexOf(k) >= 0) { dyn.water = true; st.fx.push({ k: 'fill', x: ev.to.x, y: ev.to.y }); } }, // THE WORLD ADVANCES. Fires after dyn.beat++ and on 'stay' too. The fire's reach is DERIVED from the // beat, so nothing here mutates the spread itself — this refreshes the burning cache the painter and // the determinism gate read, and records any asset the fire has now consumed (a front that ran its // whole corridor undoused). A clock, not a heart and not a terminal: the fire is a wall (law 3), so // it ends no run; a caught asset is only lost drama, and the walker finishes by the harvest chain. tick: (P) => { const st = P.st, park = st.park, fire = park.fire, dyn = park.dyn; dyn.burning = _parkFireBurningSet(st); for (let i = 0; i < fire.fronts.length; i++) { if (dyn.doused.has(i) || dyn.burnedAssets.has(i)) continue; if (_parkFireFrontIdx(fire, i, dyn.beat) > fire.fronts[i].corridor.length) { dyn.burnedAssets.add(i); st.fx.push({ k: 'burn', x: fire.fronts[i].asset % st.N, y: (fire.fronts[i].asset / st.N) | 0 }); } } }, // THE THREE FACETS — one per mind, all the same two-leg shape, folded into the SHIPPED three minds // (no new scoring channel). Each ENGAGES while any front still burns (the C it engages is one the // deepless walk board leaves cold — the seam's OR-ed engagement) and narrows to the moves that close // the distance to ITS OWN front's mouth (armed) or the stream (not). They never widen or replace a // mind, so all three stay facets of the SAME three minds. reads: { ctx: _parkFireCtx, G: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkFireSteer(legal, ctx, 'G') }, C: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkFireSteer(legal, ctx, 'C') }, N: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkFireSteer(legal, ctx, 'N') }, }, }; // PARK_FIRE_SHIP_SEED / PARK_FIRE_SHIPPABLE: the module's MEASURED ship state — declared AFTER the // registration above ON PURPOSE. The ship measure replays a fire board through the seam, which reaches // the fire steering only once the mechanic is wired into PARK_FIELD_MECHS; declaring it earlier measured // a board with NO fire steering and silently under-read the recovery (a latent ordering hole, harmless // only while the answer was false either way). Not inferred at gate time — a PIN a regression must // break; the gate (Y17-FIRE-SHIP-GATE) re-derives the ship flag FROM the measurement and asserts equal. const PARK_FIRE_SHIP_SEED = 1; const PARK_FIRE_SHIPPABLE = _parkFireRecovers(_parkFireCell(PARK_FIRE_SHIP_SEED)); // MEASURED at load, post-registration /* A SELF-CONTAINED park FIELD module, registered through PARK_FIELD_MECHS — the y18 DIG cell. It is the y16 CARRY prototype ported onto a new SUBSTRATE: where carry hands the walker a scarce TOOL and asks WHERE he spends it, mine gives him no walkway at all. Most of the board is EARTH (park.mine.earth — legalMask blocks it for every traveller), and the walker ALONE opens it, one cell at a time, by DIGGING into it (legalAdd 'me'). A dig is a plain step, not an action: the entry commits as-is (Dig-Dug style), and onEnter records the cell on dyn.dug so it becomes ordinary ground for good. THE HAZARD IS BURIED, AND ITS DANGER IS PUBLIC. GAS POCKETS (park.mine.pockets) are seeded into park.deep FROM TURN 1 — a persona-independent board constant, invisible on the surface (rendered as earth) but priced by the SHIPPED safety mind exactly like any deep field (distDeep is built once, over the pockets — the seam's trap 3). Digging INTO a pocket fires the standard universal deep-entry charge: ♥−1, deepEntries++. What makes "knew and dug anyway" LEGIBLE — the spec's hiding-exception — is the PIP: _parkMinePips(park, key) is the count of adjacent pockets on a tile, a pure public function of the board (no persona, no dyn), rendered on every DUG tile. A walker who digs a pip>0 tile and then a pocket did so in full view of the danger; the pips are the minesweeper's confession. THE THREE ROUTES (the fork, posed from turn 1 while no gem is yet taken — ctx.live): G goal — THE ORE VEIN. Dig the straight line to the north gem, STRAIGHT THROUGH the pocket band on it. Shortest, and it costs the body a heart (deepEntries >= 1). The park's oldest stake — distance bought with the body — re-posed as a dig. C safety — THE PIP-0 DETOUR. Dig to the SAME north gem the long way round, over ground the caution band permits (distDeep >= 2 == pip 0). Never a pocket, never even a pocket-neighbour (deepEntries === 0). The safe mind digs where the pips read zero. N care — THE CAVITY. The companion is walled into an east cavity by earth he cannot dig (his plan is `stuck` — seam guarantee 1, read directly), and the walker's LAST contracted gem lies in there with him. Care digs EAST, through a pocket-dense band, to reach him first. The three facets share carry's SINGLE-LEG steering: each narrows its mind to the digs that close the distance to ITS OWN target, over ITS OWN lawful domain (C over the caution band, G/N over the whole diggable frontier), and all three go cold together the moment the first gem is taken (ctx.live). At the spawn the three targets pull three ways — the vein is NORTH, the safe detour WEST, the cavity EAST — so the three compliant sets are DISJOINT: a partition, the one shape that awards a C-vs-N pair. THE FOUR SEAM TRAPS y16 PAID FOR, HEEDED HERE: 1. STEER A MIND OVER ITS OWN LAWFUL DOMAIN. C's steering BFS (_parkMineDistFrom att='C') runs ONLY over cells the caution band permits (distDeep >= band), so its narrowing can never be emptied by the shipped C's own refusal of the verge — an empty prefer() would make C INERT, not veto. 2. THE ROUTE DOMAIN MUST SEE THE DIG FRONTIER. The ENTIRE surface is legalAdd-opened earth, and legalAdd has no 'route' domain — so legalMask('route') returns FALSE on earth: the persona route metric prices the diggable frontier as passable ground, and the oracle's argmin never hands back 'stay' for want of a finite metric. (Undug earth is a wall ONLY to 'me' — re-opened by the dig — and to 'mate' — the companion cannot dig, which is what strands him.) 3. POCKETS ARE DEEP AT BUILD. park.deep carries every pocket from turn 1, so distDeep prices them for the safety mind before the walker has touched a thing. Nothing goes deep at runtime. 4. A FORK ALONE IS NOT AN ORDER. The fork names the TOP mind; the subordinate pair is posed on the legs walked AFTER the first gem, by the SHIPPED reads (the vein pocket is the only reducing dig to the north gem, so goal wades it while caution takes the long dry column — a G-C read on the care-led; the contested cavity gem is on the walker's line — a G-N read on the safety-led). */ const PARK_MINE_N = 16; const PARK_MINE_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // _parkMinePips(park, key): THE MINESWEEPER PIP — the count of adjacent GAS POCKETS on a tile. A pure // public function of the seed-fixed board (park.mine.pockets) — no persona, no dyn, no dug state — so // two builds of the same seed give byte-identical pips on every cell (C1; the gate pins it). The app // renders it on DUG tiles only, but the value is defined everywhere. function _parkMinePips(park, key) { const n = park.N, x = key % n, y = (key / n) | 0, pk = park.mine.pockets; let c = 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; if (pk.has(ny * n + nx)) c++; } return c; } // _parkMineEarth(park, key): is this cell UNDUG earth right now? A pure read of board + dyn (never the // persona). Dug earth (dyn.dug) is ordinary ground; a pocket that has been dug is still deep (the gas // is out, but distDeep was built once and the pip publicity does not depend on the walker's actions). function _parkMineEarth(park, key) { return park.mine.earth.has(key) && !(park.dyn && park.dyn.dug.has(key)); } // _parkMineBlocked(P, key, who): THE ONE TERRAIN PREDICATE, shared by legalMask and the steering BFS. // Undug earth is a wall to the WALKER ('me' — re-opened by the dig, legalAdd) and to the COMPANION // ('mate' — he cannot dig, so he stays walled in his cavity: plan.stuck). It is PASSABLE to the route // metric ('route'): the whole dig surface is legalAdd-only, and legalAdd has no route domain, so if the // route metric could not see the frontier its argmin would return 'stay' and parkStep would reject the // oracle's own move as input noise (seam trap 2). Dug/open ground is passable to all. Pure (C1). function _parkMineBlocked(P, key, who) { if (!_parkMineEarth(P.st.park, key)) return false; if (who === 'route') return false; return true; } // _parkMineDistFrom(P, srcKey, att): step distance from a dig TARGET to every cell — over THE STEERING // MIND'S OWN LAWFUL DOMAIN (seam trap 1). The domain is the whole diggable frontier (earth is passable: // the walker digs it), MINUS, for the caution mind C, every cell the shipped safety band forbids // (distDeep < band == pip > 0) — so C's narrowing runs over ground the shipped C actually permits and // its intersection can never be emptied. G and N carry no terrain law of their own, so they route the // full frontier and pay for what they cross. Deterministic; a pure read of board + dyn (never persona). function _parkMineDistFrom(P, srcKey, att) { const st = P.st, n = st.N, park = st.park; const dd = park.distDeep, band = park.cautionD || 2; const d = new Array(n * n).fill(Infinity); d[srcKey] = 0; const q = [srcKey]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const dir of DIRS) { const nx = x + dir.x, ny = y + dir.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (d[nk] < Infinity || st.wall.has(nk)) continue; if (att === 'C' && dd[nk] < band) continue; // the caution band IS safety's dig network d[nk] = d[kk] + 1; q.push(nk); } } return d; } // _parkMineCtx(P): the ONE per-state read the three facets share. Every facet goes cold TOGETHER the // moment the FIRST gem is harvested (`live` = nothing scored yet), which makes the three-way fork a // SINGLE opening decision — which way do you break ground — rather than three independent nags. function _parkMineCtx(P) { const st = P.st, park = st.park, mn = park.mine; const here = _parkKey(st, st.pos[0]); const ctx = { here, live: st.score[0] === 0, d: {} }; if (!ctx.live) return ctx; ctx.d.G = _parkMineDistFrom(P, mn.goalKey, 'G'); // the north gem, straight (through the vein) ctx.d.C = _parkMineDistFrom(P, mn.goalKey, 'C'); // the north gem, pip-0 (round the band) ctx.d.N = _parkMineDistFrom(P, mn.caveKey, 'N'); // the cavity gem, east (through the pockets) return ctx; } // _parkMineSteer(legal, ctx, att): carry's single-leg narrowing. While live, narrow the mind to the // digs that strictly close the distance to ITS OWN target; where none does, hand back EVERY legal move // (an inert no-op, NOT a veto — an empty prefer() would make the mind INERT and silently hand the turn // to the next in the order). Cold (a gem taken): hand back everything, so the SHIPPED reads decide. function _parkMineSteer(legal, ctx, att) { const out = new Set(); if (!ctx.live) { for (const c of legal) out.add(c.k); return out; } const d = ctx.d[att], cur = d[ctx.here]; for (const c of legal) if (d[c.key] < cur) out.add(c.k); if (!out.size) for (const c of legal) out.add(c.k); return out; } // _parkMineBuild(cell): the y18 board — pure PUBLIC geometry (reads ONLY cell.seed; C1: no persona // parameter). Frame (16x16): a perimeter wall; a spawn cavity in the south; a NORTH GEM reached by a // straight vein carrying a pocket band, OR by a pip-0 detour round it; an EAST CAVITY holding the // walker's last gem and the companion, walled in by earth he cannot dig; and everything else EARTH. // Two seeded mirrors (the anti-mimic draw axis, the carry precedent): flip E/W, flipY N/S — every // coordinate below is written UNMIRRORED. Outside the walk archetype pools by construction. function _parkMineBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_MINE_N; const r = rng((seed * 1409 + 6997) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const flip = r() < 0.5, flipY = r() < 0.5; const M = (x) => flip ? n - 1 - x : x; const MY = (y) => flipY ? n - 1 - y : y; const K = (x, y) => MY(y) * n + M(x); const PT = (x, y) => ({ x: M(x), y: MY(y) }); const gx = 8; // the spawn column / the vein spine const veinRow = cs(6, 8); // WHERE the vein's pocket band sits const caveX = 12; // the east cavity column const wall = new Set(), open = new Set(), pockets = new Set(), earth = new Set(); // the spawn cavity (pre-dug open ground: the walker starts standing, then breaks earth outward) open.add(K(gx, 13)); open.add(K(gx, 12)); // the east cavity: open ground the companion is walled into, holding the walker's last gem for (let x = caveX; x <= caveX + 1; x++) for (let y = 11; y <= 12; y++) open.add(K(x, y)); const goalKey = K(gx, 3); // the north gem (G/C target) const caveKey = K(caveX, 11); // the cavity gem (N target) const mateGemKey = K(caveX + 1, 3); // the companion's OWN contract gem — sealed north-east: he // cannot dig to it, so his plan is stuck and care has a stake // THE VEIN POCKET BAND — three pockets across the spine's row. The straight dig to the north gem // crosses the middle one (♥−1); a pip-0 detour must skirt the whole band by >= 2 cells. for (let x = gx - 1; x <= gx + 1; x++) pockets.add(K(x, veinRow)); // THE EAST BAND — a pocket on the short east approach to the cavity, so the care route through it // costs a heart, while a clean north column into the cavity stays open for the safety mind. pockets.add(K(caveX - 1, 12)); // everything interior that is not open/pocket is EARTH (diggable); the perimeter is wall. for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = y * n + x; if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } if (open.has(kk)) continue; earth.add(kk); // pockets are ALSO earth on the surface (rendered as earth) } // distDeep over the POCKETS (built once — the safety mind prices the buried gas from turn 1). walls // transparent, multi-source BFS: 0 on a pocket, 1 on a pip>0 tile (verge), >= 2 elsewhere (walkway). const deep = new Set(pockets); const distDeep = new Array(n * n).fill(Infinity); const q = []; for (const kk of deep) { distDeep[kk] = 0; q.push(kk); } for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const dir of DIRS) { const nx = x + dir.x, ny = y + dir.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[kk] + 1) { distDeep[nk] = distDeep[kk] + 1; q.push(nk); } } } const verge = new Set(), walkway = new Set(); for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) { const kk = y * n + x; if (deep.has(kk)) continue; (distDeep[kk] === 1 ? verge : walkway).add(kk); } const tokens = [ { ...PT(gx, 3), v: cs(2, 3), alive: true, guard: false }, // 0 — the north gem (chain 0) { ...PT(caveX, 11), v: cs(2, 3), alive: true, guard: false }, // 1 — the cavity gem (chain 1) { ...PT(caveX + 1, 3), v: cs(1, 2), alive: true, guard: false },// 2 — the companion's own (sealed) ]; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const spawn = PT(gx, 13); const st0 = PT(caveX + 1, 12); // the companion's station: in the cavity, walled in const park = { N: n, seed, k: 0, fieldMech: 'mine', walkway, verge, deep, distDeep, water: new Set(), // SEED-PURE (immutable): earth, pockets, the two gem targets, the companion's sealed gem. The // runtime — which cells have been dug — lives on dyn.dug, so a fresh build always replays. mine: { earth, pockets, goalKey, caveKey, mateGemKey, veinRow, caveX }, clusters, chain: [0, 1], // R: harvest the two gems in order contracts: [{ gem: 2, station: st0 }], retire: PT(caveX + 1, 11), spawn, companionSpawn: { x: st0.x, y: st0.y }, trig: 5, cap: 150, minTurns: 12, cautionD: 2, damage: 1, cell: _parkMineCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: st0.x, y: st0.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); park.dyn.dug = new Set(); // the runtime dig record (monotone; never cleared) return st; } // _parkMineCell(seed): the module's PUBLIC play-cell. `mech.fieldMech: 'mine'`, goal grammar HARVEST // (the two gems, cleared in order — the engine's own generic chain path). A pure value constructor; no // persona parameter (C1). function _parkMineCell(seed) { return { goalVariant: 'harvest_max', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'mine' } }; } // _parkMineCaveFirst(P): does this playout's PATH reach the cavity gem (mn.caveKey) strictly // BEFORE the north gem (mn.goalKey)? Read off P.path — the walker's cell after each move, in // visit order, already recorded by parkStart/parkStep — against the board's own seed-pure // token keys (park.mine.goalKey / .caveKey). No replay, no dyn read: a pure function of a // completed playout's own public record. The CARE-top third leg of the top-mind signature. function _parkMineCaveFirst(P) { const N = P.st.N, mn = P.st.park.mine; let goalIdx = -1, caveIdx = -1; for (let i = 0; i < P.path.length; i++) { const k = P.path[i].y * N + P.path[i].x; if (goalIdx < 0 && k === mn.goalKey) goalIdx = i; if (caveIdx < 0 && k === mn.caveKey) caveIdx = i; if (goalIdx >= 0 && caveIdx >= 0) break; } return caveIdx >= 0 && (goalIdx < 0 || caveIdx < goalIdx); } // _parkMineSignature(playouts): the field's OWN visible signature, aligned with PARK_PERSONAS. The // TOP mind breaks ground its own way: GOAL-top digs the vein and pays it a heart (deepEntries >= 1); // SAFETY-top digs the pip-0 detour and NEVER touches a pocket (deepEntries === 0); CARE-top digs east // to the cavity FIRST (the companion's gem, token 1, before the north gem, token 0) — checked via // _parkMineCaveFirst, the same path-order read the ship gate pins as a counted fact. Filter-level. function _parkMineSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < playouts.length; i++) { if (top[i] === 'goal' && playouts[i].deepEntries < 1) return false; if (top[i] === 'safety' && playouts[i].deepEntries !== 0) return false; if (top[i] === 'care' && !_parkMineCaveFirst(playouts[i])) return false; } return true; } // _parkMineAdmissible(cell): the generate-then-filter PLAYABILITY gate over the FULL persona set — // every persona completes alive, the top-mind signature holds, the fork poses C-vs-N, and every posed // pair is blind-recovered in the demonstrated direction. FRESH build per consumer (playouts mutate the // board). The SHIP bar (_parkMineRecovers) is strictly stronger. Reject reasons tallied on // _PARK_MINE_WHYS — the tuning compass. const _PARK_MINE_WHYS = { complete: 0, dead: 0, short: 0, sig: 0, unexpressed: 0, nocn: 0, norec: 0 }; function _parkMineAdmissible(cell) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkMineBuild(cell), persona); if (P.reason !== 'complete') { _PARK_MINE_WHYS.complete++; return false; } if (P.hearts < 1) { _PARK_MINE_WHYS.dead++; return false; } if (P.turns < 12) { _PARK_MINE_WHYS.short++; return false; } playouts.push(P); } if (!_parkMineSignature(playouts)) { _PARK_MINE_WHYS.sig++; return false; } let posed = 0, cn = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_MINE_PAIRS) { if (!(parkPairExpressed(_parkMineBuild(cell), playouts[i].moves, pair) > 0)) continue; posed++; if (pair[0] === 'C') cn++; const expect = _parkPushPairDir(persona, pair); if (!parkRecoverPairLex(_parkMineBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_MINE_WHYS.norec++; return false; } } } if (posed === 0) { _PARK_MINE_WHYS.unexpressed++; return false; } if (cn === 0) { _PARK_MINE_WHYS.nocn++; return false; } return true; } // _parkMineRecovers(cell): THE SHIP GATE — blind 6/6-persona ORDER recovery, calibrated at // PARK_CAL_TURNS (the calibration span the readout discards). True here is the ONLY licence for ship:true. function _parkMineRecovers(cell) { for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = parkPlayout(_parkMineBuild(cell), PARK_PERSONAS[i]); if (P.reason !== 'complete') return false; const rec = parkRecoverOrder(_parkMineBuild(cell), P.moves, PARK_CAL_TURNS); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) return false; } return true; } function parkMineWhys() { return { ..._PARK_MINE_WHYS }; } // PARK_MINE_SHIP_SEED / PARK_MINE_SHIPPABLE: the module's MEASURED ship state — declared here so the // claim is a PIN a regression must break, not a tautology the gate re-derives (numbers live in the gate // Y18-MINE-SHIP-GATE and on the campaign slot header). const PARK_MINE_SHIP_SEED = 1; const PARK_MINE_SHIPPABLE = false; // MEASURED (see the gate) — demo-only until 6/6 on every gate seed // ---- THE REGISTRATION. Everything the engine body knows about y18 is right here. PARK_FIELD_MECHS.mine = { build: _parkMineBuild, cell: _parkMineCell, admits: _parkMineAdmissible, // ONE PREDICATE, ALL THREE DOMAINS: earth is a wall to the walker and the companion, passable to the // route metric (seam trap 2 — the dig frontier must carry a finite route metric). legalMask: (P, key, who) => _parkMineBlocked(P, key, who), // THE DIG — the additive override, the WALKER's alone. Undug earth is enterable ANYWAY for him: that // is the dig, and it is what makes the whole surface reachable. Not the companion (he cannot dig). legalAdd: (P, key, who) => who === 'me' && _parkMineEarth(P.st.park, key), // A DIG IS A STEP, NOT AN ACTION — the entry commits as-is (no st.pos restore, Dig-Dug style). onEnter // records the broken ground on dyn.dug (monotone: never removed). The universal deep-entry charge has // ALREADY fired before this runs, so digging a POCKET has already cost the body its heart — no work to // do here for the gas; the pip made it public, the charge made it bite. onEnter: (P, ev) => { const st = P.st, dyn = st.park.dyn; if (dyn.dug.has(ev.toKey) || !st.park.mine.earth.has(ev.toKey)) return; dyn.dug.add(ev.toKey); st.fx.push({ k: 'dig', x: ev.to.x, y: ev.to.y }); // render hook (ZERO-TEXT) }, // THE THREE FACETS — one per mind, carry's single-leg steering, folded into the SHIPPED three minds // (no new scoring channel). Each engages while the opening decision is still live (no gem taken) and // narrows to the digs that close the distance to ITS OWN target; cold after, so the shipped reads // (the vein pocket, the contested cavity gem) carry the subordinate pair. reads: { ctx: _parkMineCtx, G: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkMineSteer(legal, ctx, 'G') }, C: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkMineSteer(legal, ctx, 'C') }, N: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkMineSteer(legal, ctx, 'N') }, }, }; /* ============ TOWER FIELD MODULE (y19 "관제탑", Task 7, plan 2026-07-14) ============ */ /* THE PERSPECTIVE-FLIP CELL. You are not the walker. You are the CURSOR in a control tower, and what WALKS is an NPC resident (dyn.ents[0]) who steps one tile per tick toward the nearest gem it has not eaten. pos[0] is the cursor: the keys move it, and a `stay` on a CONSOLE cell TOGGLES the remote gate that console wires to (onEnter fires on 'stay' — the seam's own contract). The cursor's legal domain (see _parkTowerCursorBlocked) is the south control room ONLY — every deep and water cell is masked out by construction — so it never SPENDS a heart: not because a charge is refunded, but because chargeable terrain is structurally unreachable to it. WHICH console you commit is your confession, exactly as y16's spend site is: the three consoles pull in three directions (G north, C west, N east from the crossroads), a partition, and which one your cursor walks to and holds names your TOP mind. THREE GATES, three consoles, three tiers (the y16 ford/cover/break analogue, re-posed as remote control): G the GEM-SHORTEST-PATH gate — open it and the NPC threads a barrier straight to its gem instead of detouring round. (Goal: the shortcut, bought for the walker you steer.) C the DEEP-FIELD-DETOUR gate — open it and the NPC's route runs THROUGH the deep field (park.deep, deep FROM TURN 1 — seam trap 3 — so the safety mind can see the exposure the toggle creates). (Safety/Care-of-self: the exposure a toggle makes.) N the COMPANION'S WATER-BRIDGE — open it and the companion's stream cell becomes ground and his stuck contract is finally reachable (seam guarantee 1: he is `stuck` until you do, and you get nothing for it). (Norm/Care-of-other.) WHY THE CURSOR AND NOT THE NPC IS pos[0]. The blind readout reads pos[0]'s move trajectory. The NPC policy is a pure function of the OPEN-GATE graph (C1 — it never touches the persona symbol), so it carries no order to recover; the ORDER lives entirely in which console the cursor commits. The NPC is the completion engine: every gem is reachable by a DEFAULT lane whatever the gates do (the y16 law-4 analogue), so the NPC finishes for EVERY persona and the cell is always playable — the gates only change its ROUTE (its exposure), never whether it arrives. THE SEAM TENSIONS THIS CELL LIVES INSIDE (measured, and the reason its recovery lands where it lands — see the ship gate + the report): - THE SHIPPED G IS A CURSOR-GEOMETRY BEELINE. G.engaged is true while a chain gem lives, and G.preference points the CURSOR down the route-metric gradient to that gem. So the gems sit in the NPC's field NORTH of the crossroads and the route domain (unmasked) threads to them THROUGH the deep hedge — which makes the shipped G beeline point NORTH, straight at the G console. The mechanic's G facet points there too: they agree by construction (the y16 pane alignment). The cursor is masked out of the gem cells ('me'), so it can never EAT one and leak completion to itself — the NPC is the only mouth on the board. - AFTER THE COMMIT, THE CURSOR FREEZES. Its only legal move becomes 'stay' (legalMask takes every neighbour once dyn.used is set), and a frozen 'stay' is award-neutral (compliant to every order), so the many turns the NPC still needs to finish add NO spurious pairwise awards that would corrupt the order already read off the commit. This is the y16 "facets go cold after the spend" discipline, made absolute by the freeze. */ const PARK_TOWER_N = 16; const PARK_TOWER_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; const _PARK_TOWER_COMMIT = { goal: 'G', safety: 'C', care: 'N' }; // top mind -> the console it holds // _parkTowerBuild(cell): the y19 board — pure PUBLIC geometry (reads ONLY cell.seed; C1: no persona // parameter exists), runtime-compatible with parkStart/parkStep. Frame (16x16): a SOUTH control room // (the cursor's whole world — a spawn stub, a crossroads, three arms to three consoles) and a NORTH // field (the NPC's — three gems behind three barriers the three gates pierce, plus the companion's // walled pocket across a stream). The two halves are divided by a DEEP hedge (row hy): passable to // the persona route metric (so the goal beeline threads north to a gem) but masked to the cursor's // own legal set (so the cursor stays in its tower and never eats a gem). function _parkTowerBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_TOWER_N; const r = rng((seed * 1607 + 9133) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); // NO MIRROR (v1). The E/W mirror the PUSH/y16 boards use for anti-mimic draw variation is DEFERRED: // this cell lands demo-only (see the ship gate), so the crossing's anti-mimic sweep is not the live // bar, and a mirror only muddies the coordinate bookkeeping the perspective-flip already strains. // The compass is fixed ("console south, field north"), which the flip's whole legibility rests on. const flip = false; const M = (x) => flip ? n - 1 - x : x; const K = (x, y) => y * n + M(x); // every coordinate below is written UNMIRRORED const PT = (x, y) => ({ x: M(x), y: y }); const hy = 8; // the deep hedge row (divides tower from field) const crx = 8, cry = 11; // the crossroads (the cursor's fork point) const gConsole = K(8, 10); // G console: NORTH arm (straight up from crx) const cConsole = K(4, 11); // C console: WEST arm const nConsole = K(12, 11); // N console: EAST arm const wall = new Set(), walkway = new Set(), verge = new Set(), deep = new Set(), water = new Set(); const lane = new Set(); const add = (x, y) => lane.add(K(x, y)); // --- THE SOUTH CONTROL ROOM (the cursor's world) --- add(8, 13); add(8, 12); // the spawn stub (1-wide: its exit is unavoidable) add(8, 11); add(8, 10); // the north arm: crossroads -> G console for (let x = 4; x <= 12; x++) add(x, 11); // the east-west promenade: crx -> C console / N console // --- THE DEEP HEDGE (row hy): passable to route, masked to 'me' (built as deep, below) --- // --- THE NORTH FIELD (the NPC's world): three gems + two internal barriers the gates pierce --- for (let y = 1; y <= 7; y++) for (let x = 1; x <= 14; x++) add(x, y); // the field is open ground // TWO INTERNAL BARRIERS ON ROW 3, each pierced by ONE gate (opened by its console). The gems sit // BELOW the barriers, and the NPC starts ABOVE (row 1) — so the gate is a genuine SHORTCUT: closed, // the NPC detours round the barrier's end; OPEN, it drops straight through. That is the physics the // stay-toggle changes (the ship gate's third tower assertion). Row 3 stays open at x5 and x11..14, // so EVERY gem is reachable with NO gate open at all — the y16 law-4 analogue: the NPC always finishes. const gGate = K(8, 3), cGate = K(2, 3), nGate = K(12, 6); // the gate cells are WALLS by default (closed) — _parkTowerFieldBlocked opens the ONE the cursor // toggles. Row 3 stays open at x5 and x11..14 (the permanent detour), so completion never depends on // a gate. (The route domain reaches every gem from the SOUTH, below the row-3 barriers, so the goal // beeline is unaffected by the gate state.) for (let x = 6; x <= 10; x++) wall.add(K(x, 3)); // G barrier (the G gate at x8 pierces it when open) for (let x = 1; x <= 4; x++) wall.add(K(x, 3)); // C barrier (the C gate at x2 pierces it when open) // the companion's stream: seals his pocket (NE). N gate is the bridge cell. for (let x = 11; x <= 13; x++) water.add(K(x, 6)); // the gems (chain, harvest order): north gem behind the G barrier; west gem behind the C barrier // (the C gate's own approach runs through DEEP — the exposure the safety mind can price); east gem // in the open (the NPC's default third leg). The companion's contract gem is across the stream. const northGem = PT(8, 5), westGem = PT(2, 5), eastGem = PT(13, 5); const compGem = PT(12, 7); // across the stream: reachable only over the N bridge const npcStart = PT(8, 1); const compSpawn = PT(13, 7); // classify: everything the lane claimed is walkway; the hedge row is deep; the field frame verge/deep. for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = y * n + x; if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } if (wall.has(kk) || water.has(kk)) continue; if (y === hy) { deep.add(kk); continue; } // the deep hedge (seam trap 3: deep FROM TURN 1) if (lane.has(kk)) walkway.add(kk); } // C GATE OPENS A DEEP DETOUR: the cells just BELOW the C gate, on the NPC's route to the west gem, // are deep from turn 1 (so the safety mind PRICES the exposure the toggle would create — seam trap // 3: park.deep is honoured, park.distDeep is built once, so a runtime-added deep cell would be // invisible to safety; these are deep at BUILD). deep.add(K(2, 4)); walkway.delete(K(2, 4)); deep.add(K(3, 4)); walkway.delete(K(3, 4)); // the two-tone partition: whatever the field did not claim as walkway is verge (touching a lane) or // deep (the meadow proper). The tower's south room is all walkway (the cursor's world is safe). for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) { const kk = y * n + x; if (wall.has(kk) || water.has(kk) || walkway.has(kk) || deep.has(kk)) continue; const near = [kk - 1, kk + 1, kk - n, kk + n].some(q => walkway.has(q)); (near ? verge : deep).add(kk); } const distDeep = new Array(n * n).fill(Infinity); const q = []; for (const kk of deep) { distDeep[kk] = 0; q.push(kk); } for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[kk] + 1) { distDeep[nk] = distDeep[kk] + 1; q.push(nk); } } } const tokens = [ { ...northGem, v: cs(2, 3), alive: true, guard: false }, // chain 0 — behind the G barrier { ...westGem, v: cs(2, 3), alive: true, guard: false }, // chain 1 — behind the C barrier { ...eastGem, v: cs(2, 3), alive: true, guard: false }, // chain 2 — the open field { ...compGem, v: cs(1, 2), alive: true, guard: false }, // the companion's — across the stream ]; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const spawn = PT(8, 13); const park = { N: n, seed, k: 0, fieldMech: 'tower', walkway, verge, deep, distDeep, water, tower: { gConsole, cConsole, nConsole, gGate, cGate, nGate, consoleOf: { G: gConsole, C: cConsole, N: nConsole }, gateOf: { G: gGate, C: cGate, N: nGate }, gems: [K(northGem.x, northGem.y), K(westGem.x, westGem.y), K(eastGem.x, eastGem.y)], npcStart: K(npcStart.x, npcStart.y), hy, }, clusters, chain: [0, 1, 2], needTypes: 0, contracts: [{ gem: 3, station: PT(13, 7) }], retire: PT(13, 7), spawn, companionSpawn: { x: compSpawn.x, y: compSpawn.y }, trig: 5, cap: 260, minTurns: 8, cautionD: 2, damage: 1, cell: _parkTowerCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: compSpawn.x, y: compSpawn.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); park.dyn.gates = { G: false, C: false, N: false }; // gate open state (the cursor's toggles) park.dyn.used = null; // 'G' | 'C' | 'N' — the committed console // the NPC: the resident who WALKS. Pure function of the open-gate graph (C1). trail is fork-safe // (a plain array of cell keys) and is what the C1 gate and the physics-change gate read. park.dyn.ents = [{ kind: 'npc', x: npcStart.x, y: npcStart.y, done: false, trail: [K(npcStart.x, npcStart.y)] }]; return st; } // _parkTowerCell(seed): the module's PUBLIC play-cell. `mech.fieldMech: 'tower'`. HARVEST goal // grammar (the NPC eats an ordered chain), riding the engine's generic chain/dest path — no body line. function _parkTowerCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'tower' } }; } // _parkTowerNpc(st): the NPC entity (or null). The module's own read — the engine body never looks // inside dyn.ents (REGISTRY-SEAM-BODY forbids every dyn member but .beat). function _parkTowerNpc(st) { const dyn = st.park && st.park.dyn; return dyn && dyn.ents && dyn.ents[0] ? dyn.ents[0] : null; } // _parkTowerFieldBlocked(st, key): is `key` impassable to the NPC, given the CURRENT gate state? // A barrier wall is closed unless its gate is open; the stream is closed unless the N bridge is // open. Pure read of board + dyn (never the persona — C1). This is the ONLY graph the NPC walks. function _parkTowerFieldBlocked(st, key) { const park = st.park, tw = park.tower, dyn = park.dyn, n = st.N; if (st.wall.has(key)) { if (key === tw.gGate && dyn.gates.G) return false; // G console opened the shortcut if (key === tw.cGate && dyn.gates.C) return false; // C console opened the deep detour's mouth return true; } if (park.water.has(key)) return !(key === tw.nGate && dyn.gates.N); // N console lowered the bridge if (key < 0 || key >= n * n) return true; const x = key % n, y = (key / n) | 0; return y > tw.hy || y < 1; // the NPC stays in the north field (rows 1..hy-1)... } // _parkTowerNpcStep(P): advance the NPC ONE tile toward the nearest ALIVE chain gem over the open-gate // graph (BFS, fixed DIRS order — deterministic, C1). Eats a gem it lands on (tok.alive=false) and calls // _parkAdvanceDest so completion re-reads on THIS tick. Fired from tick(). function _parkTowerNpcStep(P) { const st = P.st, n = st.N, park = st.park, tw = park.tower; const npc = _parkTowerNpc(st); if (!npc || npc.done) return; // nearest alive chain gem by BFS distance over the open graph const src = npc.y * n + npc.x; const dist = new Array(n * n).fill(Infinity), parent = new Array(n * n).fill(-1); dist[src] = 0; const q = [src]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (dist[nk] < Infinity) continue; if (nk !== src && _parkTowerFieldBlocked(st, nk)) continue; dist[nk] = dist[kk] + 1; parent[nk] = kk; q.push(nk); } } // target: the nearest alive chain gem let best = -1, bestD = Infinity; for (const gi of park.chain) { const t = st.tokens[gi]; if (!t.alive) continue; const gk = t.y * n + t.x; if (dist[gk] < bestD) { bestD = dist[gk]; best = gk; } } if (best < 0) { npc.done = true; return; } // nothing reachable/alive if (best === src) { /* already on it — eaten below */ } else { // step one tile along the shortest path let cur = best; while (parent[cur] !== src && parent[cur] >= 0) cur = parent[cur]; if (parent[cur] === src) { npc.x = cur % n; npc.y = (cur / n) | 0; } } const hereK = npc.y * n + npc.x; npc.trail.push(hereK); const tk = st.tokens.find(t => t.alive && t.x === npc.x && t.y === npc.y && park.chain.includes(st.tokens.indexOf(t))); if (tk) { tk.alive = false; st.score[0] += tk.v; st.fx.push({ k: 'gem', x: npc.x, y: npc.y }); } _parkAdvanceDest(P); if (park.chain.every(gi => !st.tokens[gi].alive)) npc.done = true; } // _parkTowerConsoleReached(st, att): is the cursor standing ON the att's console right now? function _parkTowerConsoleReached(P, att) { const st = P.st, tw = st.park.tower; return _parkKey(st, st.pos[0]) === tw.consoleOf[att]; } // _parkTowerDistTo(P, dstKey, att): step distance FROM THE CONSOLE to every cell, over the STEERING // MIND'S OWN LAWFUL DOMAIN (seam trap 1: steer a mind over its own lawful domain or the intersection // with the shipped attitude deletes it). Rooted at the console (like y16's _parkCarryDistFrom rooted // at the spend site), so d[here] is the distance still to go and a move that REDUCES it gets closer. // C routes only where the caution band permits (distDeep >= band); G and N route over the cursor's // full (masked-'me') domain. Deterministic; C1. function _parkTowerDistTo(P, dstKey, att) { const st = P.st, n = st.N, park = st.park, dd = park.distDeep, band = park.cautionD || 2; const src = dstKey; const d = new Array(n * n).fill(Infinity); d[src] = 0; const q = [src]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const dir of DIRS) { const nx = x + dir.x, ny = y + dir.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (d[nk] < Infinity || st.wall.has(nk)) continue; if (_parkTowerCursorBlocked(P, nk)) continue; // the cursor's own confinement if (att === 'C' && dd[nk] < band) continue; // C's road network is the caution band d[nk] = d[kk] + 1; q.push(nk); } } return d; } // _parkTowerCursorBlocked(P, key): the cursor's confinement — its whole world is the SOUTH control // room (rows > hy), and once it has COMMITTED a console it freezes (only 'stay' survives). Pure read // of board + dyn (never the persona — C1). Shared by legalMask('me') and the steering BFS. function _parkTowerCursorBlocked(P, key) { const st = P.st, n = st.N, park = st.park, tw = park.tower, dyn = park.dyn; if (key < 0 || key >= n * n) return true; const here = _parkKey(st, st.pos[0]); // FROZEN once committed, AND once the cursor has stepped onto a console — the toggle is a HOLD, so // the step that LANDS on a console leaves only 'stay' legal, and the next turn's 'stay' commits it // (onEnter fires on stay). This is also what keeps the shipped goal BEELINE from walking the cursor // straight PAST its own console toward the gem: on the console there is nowhere to go but hold. const onConsole = here === tw.gConsole || here === tw.cConsole || here === tw.nConsole; if (dyn.used || onConsole) return key !== here; const y = (key / n) | 0; if (y <= tw.hy) return true; // the tower is south of the hedge; the field is not the cursor's if (park.water.has(key)) return true; return false; } // _parkTowerCtx(P): the ONE per-state read the three facets share. `live` = no console committed yet // (the fork is still open); once committed, every facet goes cold together (the y16 discipline). function _parkTowerCtx(P) { const st = P.st, park = st.park, tw = park.tower, dyn = park.dyn; const here = _parkKey(st, st.pos[0]); const ctx = { live: !dyn.used, here, tw, d: {} }; if (!ctx.live) return ctx; ctx.d.G = _parkTowerDistTo(P, tw.gConsole, 'G'); ctx.d.C = _parkTowerDistTo(P, tw.cConsole, 'C'); ctx.d.N = _parkTowerDistTo(P, tw.nConsole, 'N'); return ctx; } // _parkTowerSteer(legal, ctx, att): the narrowing. Steer toward the att's console; once ON it, // prefer {stay} (the toggle). NEVER returns {} (an empty prefer makes the mind INERT, not a veto — // seam trap: it would hand the decision silently to the next mind). Cold (committed) => hand back // EVERY legal move (an inert intersection). function _parkTowerSteer(P, legal, ctx, att) { const out = new Set(); if (!ctx.live) { for (const c of legal) out.add(c.k); return out; } if (_parkTowerConsoleReached(P, att)) { out.add('stay'); return out; } // hold to toggle const d = ctx.d[att], cur = d[ctx.here]; for (const c of legal) if (d[c.key] < cur) out.add(c.k); if (!out.size) for (const c of legal) out.add(c.k); // nothing closes the distance -> inert, not a veto return out; } // _parkTowerSignature(playouts): the field's OWN visible signature — the console the TOP mind // commits. goal holds G (the gem shortcut), safety holds C (the deep-detour exposure it can price), // care holds N (the companion's bridge, for which the walker gets nothing). Filter-level. function _parkTowerSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < playouts.length; i++) { if (playouts[i].st.park.dyn.used !== _PARK_TOWER_COMMIT[top[i]]) return false; } return true; } // _parkTowerAdmissible(cell): the generate-then-filter PLAYABILITY gate over the FULL persona set. // Every persona's faithful playout COMPLETES alive (the NPC eats every gem), the cursor stays // BODILESS (hearts === heartsMax — a STRUCTURAL guarantee: its legal domain contains no chargeable // terrain by construction, so this is not evidence of an exercised refund, see _parkTowerCursorBlocked), // and the top mind commits its own console. const _PARK_TOWER_WHYS = { complete: 0, body: 0, short: 0, sig: 0, npc: 0, norec: 0, nocn: 0 }; function _parkTowerAdmissible(cell) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkTowerBuild(cell), persona); if (P.reason !== 'complete') { _PARK_TOWER_WHYS.complete++; return false; } if (P.hearts !== P.heartsMax) { _PARK_TOWER_WHYS.body++; return false; } // structural: no deep/water in the cursor's legal domain if (P.turns < 8) { _PARK_TOWER_WHYS.short++; return false; } playouts.push(P); } if (!_parkTowerSignature(playouts)) { _PARK_TOWER_WHYS.sig++; return false; } return true; } // _parkTowerRecovers(cell): THE SHIP GATE — blind 6/6-persona ORDER recovery. True here is the ONLY // licence for ship:true on the slot. function _parkTowerRecovers(cell) { for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = parkPlayout(_parkTowerBuild(cell), PARK_PERSONAS[i]); if (P.reason !== 'complete') return false; const rec = parkRecoverOrder(_parkTowerBuild(cell), P.moves, PARK_CAL_TURNS); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) return false; } return true; } function parkTowerWhys() { return { ..._PARK_TOWER_WHYS }; } const PARK_TOWER_SHIP_SEED = 1; const PARK_TOWER_SHIPPABLE = false; // MEASURED — see the ship gate + task-7-report.md. // ---- THE REGISTRATION. Everything the engine body knows about y19 is here. PARK_FIELD_MECHS.tower = { build: _parkTowerBuild, cell: _parkTowerCell, admits: _parkTowerAdmissible, // the cursor is confined to the tower ('me'); the route domain is UNMASKED (the goal beeline must // thread north through the deep hedge to the gem — that is what makes shipped G point at the G // console). The companion ('mate') is blocked from the field's barriers/stream unless the gate is open. legalMask: (P, key, who) => { if (who === 'route') return false; // the beeline sees the whole board if (who === 'mate') return _parkTowerFieldBlocked(P.st, key) && !((key / P.st.N | 0) > P.st.park.tower.hy); return _parkTowerCursorBlocked(P, key); // 'me' }, // THE COMMIT. A `stay` on a console toggles its gate open and LOCKS the confession (dyn.used). The // cursor stays BODILESS (hearts === heartsMax, per the admission gate) for a STRUCTURAL reason, not // a mechanical one: _parkTowerCursorBlocked confines 'me' to the south control room and excludes // EVERY deep/water cell, so parkStep's universal deep-entry heart charge never fires for the cursor // in any real playout — there is no chargeable terrain to refund. (No refund line lives here; one // was removed as dead code — it never ran, because the charge it would have refunded never fires.) onEnter: (P, ev) => { const st = P.st, park = st.park, dyn = park.dyn; if (dyn.used) return; if (ev.mvKey !== 'stay') return; // only a HOLD toggles (onEnter fires on stay) const tw = park.tower; for (const att of ['G', 'C', 'N']) { if (ev.toKey === tw.consoleOf[att]) { dyn.gates[att] = true; dyn.used = att; st.fx.push({ k: 'toggle', x: ev.to.x, y: ev.to.y }); return; } } }, // THE WORLD ADVANCES: the NPC steps one tile toward its nearest gem (over the open-gate graph). It // fires on 'stay' too — a walker that froze when the cursor waited would not be a resident. May // finish the chain on its own terms (it eats the last gem) — parkStep re-reads completion after tick. tick: (P, ev) => { _parkTowerNpcStep(P); }, // THE THREE FACETS — one per mind, the y16 two-leg shape (walk to the crossroads, then to YOUR OWN // console). Each engages while no console is committed (a mind the walk board leaves cold out here), // and narrows to the moves that close on its console (and 'stay' once standing on it). Folded into // the SHIPPED three minds; never a fourth mind, never a new scorer. reads: { ctx: _parkTowerCtx, G: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkTowerSteer(P, legal, ctx, 'G') }, C: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkTowerSteer(P, legal, ctx, 'C') }, N: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkTowerSteer(P, legal, ctx, 'N') }, }, }; /* ============ BOMB FIELD MODULE (y20 "two bombs, three walls", Task 1, plan 2026-07-14) ========= A SELF-CONTAINED park FIELD module registered through PARK_FIELD_MECHS. The movement verb is the walk's; the goal grammar is HARVEST. What is new is a SCARCE, TIMED tool used TWICE: the walker carries TWO bombs and there are THREE walls, each guarding one mind's object — wallGem (goal) a gem cluster the goal mind wants the shortcut to (its face is on the goal's own fast-route to a chain gem behind it — the y16 route-through-the-site trick); wallSafe (safety) a deep-detour the safety mind opens to keep its own line; wallCage (care) the caged companion the care mind frees — and its face is INSIDE the deep pool (the y3 geometry: the object of care sits where safety will not go). He plants a bomb by BUMPING a wall's FACE (legalAdd + action-restore — the y3 assist idiom: a plant is an action, not a step). After a PARK_BOMB_FUSE-beat fuse the bomb blasts a cross of radius PARK_BOMB_R, DELETES that wall's crates (the wall opens), and — if the walker or the companion is caught in the cross — spends the walker's ♥ / bubbles the companion (the CARE STATE channel). Two bombs, so he opens EXACTLY TWO walls, and the ORDER he opens them is his TOP TWO minds; the wall he ABANDONS is his lowest. `dyn.opened` (mapped goal/safety/care) is that confession. THE TWO-LEG STEERING (y16's shape, doubled — the template this cell copies verbatim): leg 1 (no bomb held): every mind points at the nearest un-picked bomb (a NEUTRAL leg — all three minds agree, so no pair is posed there). leg 2 (bomb in hand): each mind points at ITS OWN wall's face — UNLESS its wall is already opened, in which case that mind goes INERT (hands back every legal move, never {} — seam trap 1). So the FIRST fork (all three faces open) reads the TOP mind (top>other1, top>other2); the walker then fetches the second bomb and the SECOND fork reads the 2nd mind over the 3rd. The CURE for seam trap 4 (which sank y17/y18/y19 to demo-only): a SATISFIED mind — its wall already open while it still holds a bomb — returns an EMPTY prefer (it steps ASIDE), NOT all-legal. So the top mind does not blur the second plant with an inert all-legal vote; the two SUBORDINATE minds alone drive it and are read blindly, and the 2nd>3rd pair is credited even for a GOAL-top persona. So this cell both CONFESSES the full order structurally (opened === top two) AND blind ORDER recovers 6/6 — it SHIPS (ship:true), joining y16 as a full ship rather than an honest preview. DESIGN LAWS (each a seam trap the four predecessors hit — inherited verbatim): 1. STEER EACH MIND OVER ITS OWN LAWFUL DOMAIN (seam trap 1). A facet's prefer() is INTERSECTED with the shipped attitude's compliant set; steering C down a caution-forbidden corridor empties the intersection and annihilates the mind from OUTSIDE. C routes only where distDeep >= band; G and N route the walker's full passable domain (_parkBombDistFrom carries y16's band guard). 2. THE ROUTE DOMAIN MUST SEE A FACE THE WALKER CAN OPEN (seam trap 2). A face is legal to 'me' only through legalAdd, which has NO 'route' domain — so its route-metric distance would be Infinity and parkOracleMove's argmin would hand back 'stay' (parkStep then rejects its own move as noise → stall). So legalMask opens every un-opened face to the 'route' metric WHILE THE WALKER HOLDS A BOMB — which is also TRUE: a wall you are carrying the charge to is a way through. 3. THE CAGE FACE IS DEEP FROM TURN 1 (seam trap 3). park.deep is mutable but park.distDeep is built ONCE, so a cell made deep at runtime is invisible to the shipped safety mind. cageFace is deep on every seed for every persona (it leaks no choice) — and the walker who dives in to free the friend pays the pre-charged ♥ (the y3 "diving-in value", kept deliberately, not refunded). 4. COMPLETION NEVER NEEDS A WALL (global constraint 3). The three chain gems sit OUTSIDE every wall, reachable the long way, so a persona whose bombs went elsewhere always finishes. The walls are shortcuts, never keys. The caged companion is plan.stuck HOLD — he never wedges. */ const PARK_BOMB_N = 12; const PARK_BOMB_FUSE = 3; // beats from plant to blast const PARK_BOMB_R = 2; // the blast cross reach const PARK_BOMB_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; const _PARK_BOMB_WALLS = ['gem', 'safe', 'cage']; const _PARK_BOMB_ATT_WALL = { G: 'gem', C: 'safe', N: 'cage' }; // which wall each mind owns function _cap(w) { return w.charAt(0).toUpperCase() + w.slice(1); } // _parkMateKey(st): the companion's (seat 1) current cell key. A pure public read (C1). function _parkMateKey(st) { return _parkKey(st, st.pos[1]); } // _parkBombBuild(cell): the y20 board — pure PUBLIC geometry (reads ONLY cell.seed; C1: no persona // parameter exists), runtime-compatible with parkStart/parkStep. Frame (12x12): a perimeter wall; a // HEDGE across row hy with a single crate GAP (gemFace) that the goal mind's fast-route to the north // chain gem threads when armed, and open WEST/EAST lanes as the long way round; two side crate pockets // (safeFace on walkway, cageFace inside a deep pool) holding the safety detour and the caged // companion; two bombs on the walker's approach; the three chain gems on the north promenade, reachable // by the lanes with no bomb at all (design law 4). Two mirrors flip the compass by seed (anti-mimic). function _parkBombBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_BOMB_N; const r = rng((seed * 2311 + 4099) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const guided = !!(cell && cell.guidedBomb); const flip = guided ? false : r() < 0.5, flipY = guided ? false : r() < 0.5; const M = (x) => flip ? n - 1 - x : x; // every coordinate below is written UNMIRRORED const MY = (y) => flipY ? n - 1 - y : y; const K = (x, y) => MY(y) * n + M(x); const PT = (x, y) => ({ x: M(x), y: MY(y) }); const hy = 4; // the hedge row (the goal's gap sits on it) const gx = 6; // the gem gap column (= the spine) const wlane = 2, elane = 9; // the two open lanes: the long way round the hedge const wall = new Set(), walkway = new Set(), verge = new Set(), deep = new Set(); const crates = new Set(), crateWall = new Map(); const add = (x, y) => walkway.add(K(x, y)); // THE SOUTH PLAZA (rows hy+1 .. 10) — an open court the walker crosses, holding the spawn, the two // bombs and the two side pockets. Everything here is walkway unless a crate/deep claims it. for (let y = hy + 1; y <= 10; y++) for (let x = 1; x <= 10; x++) add(x, y); // THE NORTH PROMENADE (rows 1..hy-1) — the three chain gems live here; reached by the lanes (long) // or through the gem gap (short, armed only). for (let y = 1; y <= hy - 1; y++) for (let x = 1; x <= 10; x++) add(x, y); // THE HEDGE across row hy: wall everywhere except the two lanes and the single gem gap. for (let x = 1; x <= 10; x++) if (x !== wlane && x !== elane && x !== gx) wall.add(K(x, hy)); // THE GEM GAP — a single crate the goal mind bumps to open the SHORT way north. Behind it, on the // spine, sits chain gem 0 (so the goal's fast-route threads the gap when armed — design law 2). const gemFace = K(gx, hy); crates.add(gemFace); crateWall.set(gemFace, 'gem'); walkway.delete(gemFace); // THE NORTH SPUR WALL — a short wall column just EAST of the spine (gx+1, rows 1..hy-1). It severs the // EAST-lane approach to chain gem 0, so gem 0's only NON-gemFace route is the WEST lane (design law 4 // still holds — it IS reachable the long way, just from the west). This is the fix for the last-plant // STRAND (I2): a walker who plants the gem wall LAST and must reach gem 0 during the fuse now goes WEST // (a committed forward route that completes) instead of overshooting EAST onto a shoulder from which // the only shortest path back is a greedy-backtrack it will not take (measured cap-out). The gemFace // shortcut on the spine is untouched. for (let y = 1; y <= hy - 1; y++) wall.add(K(gx + 1, y)); // THE SAFETY POCKET (west of centre, on walkway) — a two-crate cul-de-sac; safeFace is its mouth. const safeFace = K(4, 7); const wallSafeBody = K(4, 8); for (const k of [safeFace, wallSafeBody]) { crates.add(k); crateWall.set(k, 'safe'); walkway.delete(k); } // THE DEEP POOL (east of centre) — a block of deep the safety mind's caution band strikes out, so // cageFace sits where the shipped safety mind will not go (design law 3, the y3 geometry). The POOL // is two rows (6..7), so the cell just SOUTH of it (7,8) is dry ground — that is the companion's PEN. for (let y = 6; y <= 7; y++) for (let x = 7; x <= 8; x++) { deep.add(K(x, y)); walkway.delete(K(x, y)); } const cageFace = K(7, 6); const wallCageBody = K(8, 6); for (const k of [cageFace, wallCageBody]) { crates.add(k); crateWall.set(k, 'cage'); walkway.delete(k); deep.delete(k); } deep.add(cageFace); // design law 3: the cage face is deep from turn 1 // THE COMPANION'S PEN — dry ground on cageFace's SOUTH blast arm (dist 2), so a cage bomb's cross // catches him (→ dyn.bubbled) and the walker can rescue him by a BUMP from dry ground (no ♥ dive). He // is LIVE (a contract) but PENNED: his one contracted gem sits in the deep pool he will not wade, so // his plan is permanently `stuck` and he HOLDS — never wandering into the walker's corridors (the // measured failure of a free companion), yet fully rescuable (the care STATE channel, I3). const cageCell = K(7, 8); const penGem = PT(8, 7); // his gem, inside the deep pool → unreachable → HOLD const face = { gem: gemFace, safe: safeFace, cage: cageFace }; const wallOf = { gem: [gemFace], safe: [safeFace, wallSafeBody], cage: [cageFace, wallCageBody] }; // TWO BOMBS on the walker's approach (BFS >= 3 from spawn — out of the calibration span, y8's // lesson), symmetric so the E/W mirror moves them together. const bomb1 = K(4, 9), bomb2 = K(7, 9); // THE THREE CHAIN GEMS — all on the WEST half of the north promenade (west of the spur wall), so every // one is reachable by the WEST lane with NO bomb (design law 4), and gem 0's only non-gemFace route is // the west lane (the strand fix). gem 0 sits on the spine behind the gem gap (the goal's fork target). const g0 = PT(gx, 2), g1 = PT(wlane, 1), g2 = PT(3, 2); const spawn = PT(gx, 10); const st0 = PT(1, 10), st1 = PT(1, 9); // the companion's stations (far SW, out of the way) // BUILD the terrain sets: perimeter wall; the two-tone verge/deep partition of the leftover meadow. for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = y * n + x; if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } } for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) { const kk = y * n + x; if (wall.has(kk) || walkway.has(kk) || deep.has(kk) || crates.has(kk)) continue; const near = [kk - 1, kk + 1, kk - n, kk + n].some(q => walkway.has(q)); (near ? verge : deep).add(kk); } const distDeep = new Array(n * n).fill(Infinity); // multi-source BFS (walls transparent) const q = []; for (const kk of deep) { distDeep[kk] = 0; q.push(kk); } for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[kk] + 1) { distDeep[nk] = distDeep[kk] + 1; q.push(nk); } } } // THE PARK FRAME LAW — walkway => distDeep >= 2 (the verge is the buffer). Demote any walkway cell // the deep pool crowds to verge, so the shipped safe metric stays navigable (the y16 repair). for (const kk of [...walkway]) if (distDeep[kk] < 2) { walkway.delete(kk); verge.add(kk); } const tokens = [ { ...g0, v: cs(2, 3), alive: true, guard: false }, // chain 0 — behind the gem gap (goal's shortcut) { ...g1, v: cs(2, 3), alive: true, guard: false }, // chain 1 — north-west, up the west lane { ...g2, v: cs(2, 3), alive: true, guard: false }, // chain 2 — north-east, up the east lane { ...penGem, v: cs(1, 2), alive: true, guard: false }, // the companion's contract gem — in the pool ]; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const bomb = { crates, crateWall, face, wallOf, bombKeys: [bomb1, bomb2], cageCell, fuse: PARK_BOMB_FUSE, r: PARK_BOMB_R, }; const park = { N: n, seed, k: 0, fieldMech: 'bomb', // the REGISTRY key (Task 1) — the ONLY thing the body knows walkway, verge, deep, distDeep, bomb, // SEED-PURE (immutable): walls, faces, bombs. Runtime on dyn. clusters, chain: [0, 1, 2], needPairs: true, needTypes: 0, // THE COMPANION IS LIVE BUT PENNED (I3). His contract is gem 3, seated in the deep pool he will not // wade, so _parkCompanionPlan returns `stuck` every turn and _parkCompanionStep HOLDS him — he keeps // the contract, never retires, never wanders into the walker's corridors (the measured failure of a // free companion). He is fully alive for the care STATE channel: a cage bomb's blast bubbles him and // the walker rescues him by a bump (the y3 assist path, now genuinely reachable — not dead code). contracts: [{ gem: 3, station: st0 }], retire: st1, spawn, companionSpawn: { x: cageCell % n, y: (cageCell / n) | 0 }, trig: 4, cap: 160, minTurns: 12, cautionD: 2, damage: 1, cell: _parkBombCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: cageCell % n, y: (cageCell / n) | 0 } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; if (guided) _parkBombGuidedStage(st); park.dyn = _parkDynInit(st); // OPT-IN runtime container (Task 0) park.dyn.held = false; // no bomb in hand park.dyn.lit = null; // { key, at, wall, cells } while a fuse burns park.dyn.opened = []; // the walls opened, IN ORDER (the confession) // guided y20's pen door is tutorial plumbing, not a scored wall. NOTE THE WORDING: it used to say // "steel escape", which is now exactly backwards — the pen is steel and the DOOR is the one wooden // face in it. That is the whole lesson of the re-laid stage (2026-08-03), so the name must not // suggest you blast steel. park.dyn.introOpened = false; park.dyn.caged = true; // the companion is behind wallCage park.dyn.bubbled = false; // the care STATE channel (a blast caught the companion) // Task 24: PERSISTED arrival, keyed by att — a WALKWAY target (b.attTarget's non-face entries) is // satisfied by having been reached ONCE, not by "am I standing there right now". Without this, a // mind whose business is done at that cell gets pulled back to it the instant it steps off again // (a real oscillation: measured cap-out, walker ping-ponging the door forever). GUIDED-ONLY: the // seed boards never declare b.attTarget so nothing would ever read this field, but Y20-SEED-BOARD- // FROZEN hashes the WHOLE built state (Task 21) — an unconditional new dyn key still moves that // fingerprint even though it is semantically inert there (measured, then reverted to this gate). if (guided) park.dyn.reachedCell = {}; return st; } // The playable y20 presentation uses a fixed, readable stage. Cell selection and the shipped // measurement still use the calibrated seed board; only the crossing's stamped playCell reaches here. /* THE GUIDED y20 STAGE (re-laid 2026-08-03). What this board has to TEACH, in order: 1. A bomb opens WOOD. It does nothing to STEEL. 2. Therefore the way out of where you are standing is the one wooden face in a steel ring. 3. Therefore look at what a box is MADE OF before you spend the bomb. The old stage could not teach that, because it had no steel: every box on it was bombable, so "which box" was never a question and the first lesson had nothing to bite on. So this layout adds a real material — b.steel — and spends the whole opening on the distinction. STEEL IS TERRAIN, WOOD IS A CRATE. Steel keys go into st.wall, so every existing predicate (legalMask, the steering BFS, _parkBombBlocked) treats them correctly with no new branch; b.steel exists so the painter can draw them as BOXES rather than hedge, because a steel box that looks like scenery teaches nothing. Wood stays exactly what a crate already was: a bombable face. THE ROUTE THE STEEL DRAWS (blue starts penned bottom-right and is walked through the lesson): pen (x 5..10, y 9..10) — steel roof at y=8 two cells above him, steel at (4,10) └ the ONLY opening is WOOD at (4,9). He cannot leave without spending a bomb. ← lesson 1+2 west, then NORTH — the corridor bends, so the escape is not just "walk through the hole" pink at (1,5) east along the open y=5 lane, then north through the UNCRATED gap at (5,4) — the first cell past it, (5,3), is the room bomb: entering the room is what puts it in hand the upper room (x 5..10, y 1..3) — entered with NO bomb at all ← the contrast └ the three chain gems fill a steel-wrapped GEM POCKET in the NORTH-EAST CORNER — (9,1) (10,1) (10,2) — with exactly two ways in (2026-08-06): WOOD at (8,1) (plant and retreat — the blast cross covers (7,1)) or the SECOND DOOR at the partition gap (10,4) into (10,3), a twelve-step detour past pink and back up row 5's east end that spends no ♥. Both branches are comparable; the wood only ever buys the shortcut. Yellow's room grew two cells west (its divider column moved x=6 -> x=4) so that room is roomy enough to be walked into and read, rather than a slot you squeeze at. */ function _parkBombGuidedStage(st) { const n = st.N, park = st.park, b = park.bomb; const K = (x, y) => y * n + x; const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { if (x === 0 || y === 0 || x === n - 1 || y === n - 1) wall.add(K(x, y)); } // 칸막이 y=4: x=2 는 나무(위 왼쪽 방을 지킨다), x=5 는 열린 틈(그 너머 (5,3) 이 방 폭탄), // x=10 은 2026-08-06 에 새로 연 두 번째 입구다 — 분홍을 지나 5행 동쪽 끝에서 북상하는 길. for (let x = 1; x <= 10; x++) if (x !== 2 && x !== 5 && x !== 10) wall.add(K(x, 4)); for (let y = 1; y <= 3; y++) wall.add(K(4, y)); // was x=6; moving it west is the +2 room // ---- STEEL. Unbreakable by construction: nothing ever removes these from st.wall. b.steel = new Set([ K(5, 8), K(6, 8), K(7, 8), K(8, 8), K(9, 8), K(10, 8), // the roof, two cells above the spawn K(4, 10), // the pen's west corner // THE RAIL LOST ITS EAST ARM (2026-08-04). It ran (3,6) (4,6) [gap] (6,6) (7,6) with (5,7) and // the roof below the gap — a plus-sign of plate that read as architecture rather than as an // obstacle, and its east half shaped nothing the stem at (5,7) does not already shape. K(3, 6), K(4, 6), K(5, 7), // the detour stop: row 7 severed at x=5 // THE GEM POCKET, MOVED TO THE NORTH-EAST CORNER (2026-08-06). The chain gem sits at (9,1), // wrapped by a three-sided ㄴ whose top cell is WOOD (8,1) and whose right cell is DELETED — // (10,3) is the second door, reached from below through the partition gap at (10,4). So the // pocket has exactly two ways in: plant-and-retreat through the wood, or a twelve-step walk // that passes the companion and comes back east. The steel that used to fill this room is // gone: nine dead cells were half the room. K(8, 2), K(8, 3), K(9, 3), ]); for (const k of b.steel) wall.add(k); // ---- WOOD. Every one of these is a bombable face, and the board is arranged so the FIRST one // is unavoidable: (4,9) is the only non-steel cell on the pen's boundary. // THE `safe` WALL IS GONE (2026-08-03): it was the blue crate at (5,6), and it sat in the middle // of the guide rail where its only job was to be a second, optional blast. The rail does the // shaping now — (5,7) below it — so the crate was a bomb with nothing to buy. b.face = { gem: K(8, 1), cage: K(2, 4), intro: K(4, 9) }; b.wallOf = { gem: [K(8, 1)], cage: [K(2, 4)], intro: [K(4, 9)], }; b.walls = ['gem', 'cage', 'intro']; b.crates = new Set(Object.values(b.wallOf).flat()); b.crateWall = new Map(); for (const name of b.walls) for (const key of b.wallOf[name]) b.crateWall.set(key, name); // C 는 여전히 walkway 칸을 겨눈다(face 가 아니다) — 만족 조건은 "열었다"가 아니라 "닿았다"이고 // 그 갈래는 _parkBombTargetKey / _parkBombCtx 가 이미 다룬다. 상수만 옮긴다. b.attTarget = { G: K(8, 1), C: K(10, 3), N: K(2, 4) }; // one bomb inside the pen (he must find it before he can leave), one on the way, and one at the // room's threshold — the first cell stepped into, so picking it up is forced and using it is not. b.bombKeys = [K(6, 10), K(2, 7), K(5, 3)]; b.cageCell = K(1, 5); park.walkway = new Set(); for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) { const key = K(x, y); if (!wall.has(key) && !b.crates.has(key)) park.walkway.add(key); } park.verge = new Set(); park.deep = new Set(); park.distDeep = new Array(n * n).fill(Infinity); st.wall = wall; st.hazard = new Set(); // 체인 보석은 하나다 (2026-08-06). 값을 못 박는 이유: _parkBombBuild 는 v 를 시드로 뽑는다 // (실측 시드 1..12 에서 2 또는 3, 게이지 총량 8~11). 프레젠테이션 판이 판마다 다른 게이지를 // 보여 줄 이유가 없고, 캡처 컷도 시드마다 달라진다. // 배열에서 빼는 것이지 죽이는 것이 아니다: 게이지 총량은 죽은 토큰의 v 도 센다 // (_parkGaugeMarks 의 주석 "token v persists after a take"), 그래서 alive=false 로만 // 죽이면 바가 끝까지 안 찬다. const chainGem = { ...st.tokens[0], x: 9, y: 1, v: 3, alive: true }; const mateGem = { ...st.tokens[1], x: 2, y: 2, v: 2, alive: true }; st.tokens.length = 0; st.tokens.push(chainGem, mateGem); park.clusters = st.tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); park.chain = [0]; park.spawn = { x: 9, y: 10 }; park.companionSpawn = { x: 1, y: 5 }; park.contracts = [{ gem: 1, station: { x: 1, y: 5 } }]; // 배열을 자르면 인덱스가 3 -> 1 park.retire = { x: 1, y: 5 }; park.trig = n * 2; st.pos[0] = { ...park.spawn }; st.pos[1] = { ...park.companionSpawn }; // THE OPENER'S TWO DISPLAY MARKS (2026-08-05). Both are null at rest and null again the moment // the opener ends. They exist so the two painters that have to show the blocked claim can read it // off the PUBLIC board instead of off the animation driver — a painter reads the board and never // app state (C1), and these same painters are shared by the mini replay panel and the hub // thumbnails, so a driver-read here would leak the opener onto surfaces it has no business on. // pinkNudge — how far her body is shoved toward the crate she has just walked into, in cells. // crateHit — which crate is being struck on THIS frame, and how far the wood rocks. The offset // travels with the mark rather than being derived from the pulse, so a frozen // capture frame and its offset are pinned together (the pulse reads the wall clock). // claimPop — 분홍의 찜 링이 커졌다 작아지는 팝이 시작된 시각(ms). 다른 두 마크와 같은 // 이유로 보드에 산다: 페인터는 보드를 읽고 앱 상태를 안 읽는다. park.bombScene = { introPath: [{ x: 1, y: 7 }, { x: 1, y: 6 }, { x: 1, y: 5 }], pinkWait: { x: 1, y: 5 }, pinkClaimed: false, pinkNudge: null, crateHit: null, claimPop: null, }; // THE PAIR REQUIREMENT COMES OFF THIS BOARD (2026-08-04). `_parkReadDone` makes completion mean // "chain done AND all three comparisons posed". That was checked once against the live cells the // oracle can walk (108/108 on 2026-08-02) — and THIS board was never in that sample, because // planting a bomb is a human-only affordance. Measured: all six personas end at dest 0/3 with // zero awards, sealed in the opening pen; the live cell poses 0/36 on every pair. // So a human could pick up every gem and the board would still refuse to end, draining turns to // the cap with nothing on screen to explain why — the HUD carries a goal bar, not a pair bar. // A measurement-side condition that locks play is not a measurement, it is a fault. The seed // board (no `guidedBomb`) keeps `needPairs` and keeps answering ⑤; this presentation board never // answered it in the first place. delete park.needPairs; } // _parkBombCell(seed): the module's PUBLIC play-cell. `mech.fieldMech: 'bomb'`. Goal grammar HARVEST. // A pure value constructor; no persona parameter exists (C1). function _parkBombCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'bomb' } }; } // _parkBombBlocked(P, key, who): THE ONE TERRAIN PREDICATE, shared by legalMask and the steering BFS. // A crate is a wall for everyone; a crate that is a FACE is passable to the 'route' metric while the // walker holds a bomb (design law 2), so the armed route can thread it. An opened wall's crates are // deleted from the set, so they fall through to plain ground. Pure read of board + dyn (never persona). function _parkBombBlocked(P, key, who) { const st = P.st, b = st.park.bomb, dyn = st.park.dyn; const sc = st.park.bombScene; if (!b.crates.has(key)) return false; if (key === b.face[b.crateWall.get(key)]) { // Widened for the guided board ONLY (`sc` = st.park.bombScene, set exclusively by // _parkBombGuidedStage — the seed boards never carry it). There, no gem sits inside the pen and // every gem outside is route-Infinity, so the delivery-goal attitude's argmin collapses to `stay` // unless an unopened face is a passable route candidate even to a walker not yet holding a bomb. // The seed boards keep the original held-only condition (design law 2) so this predicate, SHARED // across both boards, does not pollute the promoted measurement. const armed = who === 'route' && !dyn.lit && (sc || dyn.held); return !armed; } return true; } // _parkBombCross(st, key): the cross of radius r centred on `key` (the blast + the dotted preview), // board edge and walls excluded. A pure function of the board (C1). function _parkBombCross(st, key) { const n = st.N, b = st.park.bomb, out = new Set(); const cx = key % n, cy = (key / n) | 0; out.add(key); for (const d of DIRS) for (let i = 1; i <= b.r; i++) { const nx = cx + d.x * i, ny = cy + d.y * i; if (nx < 0 || ny < 0 || nx >= n || ny >= n) break; const nk = ny * n + nx; if (st.wall.has(nk)) break; out.add(nk); } return out; } // _parkBombDistFrom(P, srcKey, att): step distance from a face / a bomb to every cell over THE // STEERING MIND'S OWN LAWFUL DOMAIN (seam trap 1 / design law 1). The source is seeded at 0 even when // blocked (a face is somewhere you reach and act on, not walk through). C routes only where the caution // band permits (distDeep >= band); G and N route the walker's full passable domain. Deterministic; C1. function _parkBombDistFrom(P, srcKey, att) { const st = P.st, n = st.N, park = st.park; const dd = park.distDeep, band = park.cautionD || 2; const d = new Array(n * n).fill(Infinity); d[srcKey] = 0; const q = [srcKey]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const dir of DIRS) { const nx = x + dir.x, ny = y + dir.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (d[nk] < Infinity || st.wall.has(nk)) continue; if (_parkBombBlocked(P, nk, 'me')) continue; // NO MIND WADES DEEP TO REACH ITS FACE — the dive is the single ACTION-bump onto the deep cage // face (seeded at 0 above), never a wade up to it. A care approach that waded through the pool // would strand the SAFETY mind in deep with no compliant move, and an inert safety there is // prescribed by the very order (…C…N with N sub) whose dive would otherwise credit N>C — so the // C-N pair cancels (measured: care-top expressed N>G but never N>C). Routing every mind over // non-deep ground keeps the fork's C-vs-N read clean. if (dd[nk] < 1) continue; // deep is never a through-cell for any mind if (att === 'C' && dd[nk] < band) continue; // the caution band IS safety's road network d[nk] = d[kk] + 1; q.push(nk); } } return d; } // _parkBombTargetKey(P, att): the cell this mind steers to right now, or null (→ inert). leg 1 (no // bomb): the nearest un-picked bomb (all minds agree). leg 2 (holding): my own wall's face — UNLESS my // wall is already opened or mid-fuse, in which case null (I have nothing left to spend on → inert). // // BOARD-DECLARED TARGETS (Task 24): a board may set `b.attTarget = {G,C,N}` to name each mind's cell // directly, instead of leaving it to `_PARK_BOMB_ATT_WALL` (which assumes every mind owns a bombable // FACE — a wall it must hold a bomb to open). A declared target that is NOT a crate face is a plain // WALKWAY cell: no bomb is needed to reach it, so it is read here, live the whole time bomb business // is ongoing anywhere on the board, independent of `dyn.held`. A declared target that IS a crate face // (G's and N's on the guided board — the same cells the legacy lookup already names) falls straight // through to the unchanged leg-1/leg-2 shape below, so G and N are byte-identical to before this task. function _parkBombTargetKey(P, att) { const st = P.st, b = st.park.bomb, dyn = st.park.dyn; // THE RESCUE takes priority for CARE (I3): a bubbled companion is a care emergency, so the care mind // steers to his cell (bumped via legalAdd) BEFORE any bomb business. Only care ever plants the cage, // so only a care-led walker bubbles him and only the care mind rescues — the care STATE channel, // posed by an actual rescue rather than only by cage-face steering. if (att === 'N' && dyn.bubbled && !dyn.held) return _parkMateKey(st); if (b.attTarget) { const cellTarget = b.attTarget[att]; // b.crateWall (not b.crates) is the STABLE classifier: crates is a LIVE set that a wall's own // opening deletes from, so checking it here would flip a face-type target (G's, N's) to // "walkway" the instant its OWN wall opens — the mind would then try to walk INTO the now-open // face instead of going inert (measured: N re-targets the just-opened cage face and oscillates // trying to stand on it, cap-out, seed 3, persona 3 safety>care>goal). crateWall is written once // at board build and never mutated, so it still answers "was this ever a crate face" after open. if (cellTarget != null && !b.crateWall.has(cellTarget)) { // a WALKWAY target: satisfied by ARRIVAL, PERSISTED (dyn.reachedCell — set once in onEnter and // never cleared), not by "am I standing there right now": the latter re-opens the target the // instant the walker steps off again, oscillating forever between the door and whatever comes // next (measured: cap-out, seed 3, persona 0). No held-gate either way — there is no wall here // to hold a bomb for. return dyn.reachedCell[att] ? null : cellTarget; } } if (!dyn.held) { if (!b.bombKeys.length) return null; const here = _parkKey(st, st.pos[0]); let best = null, bestD = Infinity; for (const bk of b.bombKeys) { const dd = Math.abs(bk % st.N - here % st.N) + Math.abs((bk / st.N | 0) - (here / st.N | 0)); if (dd < bestD) { bestD = dd; best = bk; } } return best; } const w = _PARK_BOMB_ATT_WALL[att]; if (dyn.opened.includes(w)) return null; // my wall is open — nothing to steer to if (dyn.lit && dyn.lit.wall === w) return null; // my bomb is already ticking on it return b.face[w]; } // _parkBombCtx(P): the ONE per-state read the three facets share. `live` = there is still bomb business // (a bomb in hand, or a bomb on the ground). Once both bombs are planted every facet goes cold together // and the walker finishes the chain by the shipped attitudes (the y16 discipline). function _parkBombCtx(P) { const st = P.st, dyn = st.park.dyn, b = st.park.bomb; const here = _parkKey(st, st.pos[0]); // `live` = bomb business remains (a bomb in hand, a bomb on the ground, or a fuse still burning — so // the walker still has a blast to reckon with). `blast` = the cells a lit fuse will strike (the C // hazard the SAFETY mind dodges — I2: the blast rhythm is a SECOND, independent source of C-evidence, // so C-judgeability does not hang on the one-time wall choice). There is NO forced-wait pin any more: // waiting outside the blast radius is a GENUINE safety CHOICE the caution mind makes for itself. const live = dyn.held || b.bombKeys.length > 0 || dyn.lit != null; const blast = dyn.lit ? dyn.lit.cells : null; const ctx = { live, blast, held: dyn.held, bubbled: dyn.bubbled, here, d: {}, skip: {} }; if (!live) return ctx; for (const att of ['G', 'C', 'N']) { const tk = _parkBombTargetKey(P, att); // A mind holding a bomb whose OWN wall is already open (tk null while held) is SATISFIED: it must // step aside so the SUBORDINATE minds drive the SECOND plant — and the blind read then reads THEM. // That "step aside" is an EMPTY prefer (skip), NOT all-legal: all-legal would let the shipped goal // attitude harvest the chain the instant the gem gap opened, before the second bomb is ever planted // (measured: goal-top completed with a single wall opened). Empty makes the mind INERT and hands // the decision to the next — which is precisely seam trap 4's cure, applied on purpose. // // A board-declared WALKWAY target (Task 24 — see _parkBombTargetKey) plays by the same rule for a // different reason: it is satisfied by ARRIVAL, not by spending, so there is no `dyn.held` to gate // on — `tk == null` already means "reached" for that mind and nothing else, so skip follows it // unconditionally. A declared FACE target (a crate — G's and N's on this board) is unchanged: it // still needs the held-gate, since `tk == null` there can also mean "leg 1, no bomb in hand yet". // b.crateWall, not b.crates — see the matching note in _parkBombTargetKey (crates shrinks as // walls open; crateWall is the permanent "was this ever a face" record). const cellTarget = b.attTarget && b.attTarget[att] != null && !b.crateWall.has(b.attTarget[att]); ctx.skip[att] = cellTarget ? (tk == null) : (tk == null && dyn.held); ctx.d[att] = tk == null ? null : _parkBombDistFrom(P, tk, att); } return ctx; } // _parkBombSteer(P, legal, ctx, att): THE TWO-LEG NARROWING (y16's shape) + the BLAST DODGE (I2). Cold // (no bomb business) → EVERY legal move (the shipped attitude finishes the chain). A SATISFIED mind // (ctx.skip) → EMPTY (step aside so the next mind plants the second bomb; seam trap 4's cure). A leg-1 // mind with no reducing move → all-legal (inert, never a veto). Otherwise narrow to the moves that close // the distance to my target (the bump onto a face / the companion among them: dist→here is 1, bump → 0). // THEN, for CARE while the companion is bubbled, the target is his cell (rescue), read the same way. // THEN, for the SAFETY mind while a fuse burns, narrow to the moves that STAY CLEAR OF THE BLAST — the // caution mind's genuine "wait outside the radius or flee" choice (I2). This is a real narrowing, not a // forced stay: a walker whose top mind is goal will press on through the blast (♥−1); a safety-led one // steps clear. If every base move sits in the blast, the dodge widens to the whole legal set to leave // it; only if the walker is boxed inside the cross (which `admits` rejects via `nonook`) does it fall // back to the base — never a veto. function _parkBombSteer(P, legal, ctx, att) { let out = new Set(); if (ctx.live && ctx.skip[att]) return out; // satisfied: step aside (empty) const d = ctx.live ? ctx.d[att] : null; if (!ctx.live || !d) { for (const c of legal) out.add(c.k); } // cold / inert: hand back everything else { const cur = d[ctx.here]; for (const c of legal) if (d[c.key] < cur) out.add(c.k); if (!out.size) for (const c of legal) out.add(c.k); // nothing to say — inert, not a veto } if (att === 'C' && ctx.blast) { // I2: the safety mind dodges the blast const keyOf = {}; for (const c of legal) keyOf[c.k] = c.key; let safe = new Set([...out].filter(k => !ctx.blast.has(keyOf[k]))); if (!safe.size) safe = new Set(legal.filter(c => !ctx.blast.has(c.key)).map(c => c.k)); if (safe.size) out = safe; // stay clear (incl. 'stay' if here is safe) } return out; } // _parkBombSignature(playouts): the field's OWN visible signature, aligned with PARK_PERSONAS — the // walls OPENED, in order, are the top TWO minds. Filter-level, like every module signature. function _parkBombSignature(playouts) { const AX = { gem: 'goal', safe: 'safety', cage: 'care' }; for (let i = 0; i < playouts.length; i++) { const opened = playouts[i].st.park.dyn.opened.map(w => AX[w]); if (opened.length !== 2) return false; if (opened.join() !== PARK_PERSONAS[i].slice(0, 2).join()) return false; } return true; } // _parkBombAdmissible(cell): the generate-then-filter PLAYABILITY gate over the FULL persona SET. Every // persona's faithful playout COMPLETES alive, opens EXACTLY its top two walls in order (the signature), // the two forks separate at least one C-vs-N pair, and every pair the board POSES is blind-recovered in // the demonstrated direction. The SHIP bar (_parkBombRecovers, 6/6 blind ORDER recovery) is strictly // stronger and lives above this — admissible-but-not-recoverable ships DEMO-ONLY (P9/P10). Reject // reasons tallied LOUDLY on _PARK_BOMB_WHYS: complete/dead/short (playability), sig (confession), // wrongspend (opened != top two), skew (the three faces' spawn-BFS spread > 2 — the visit-order // distortion open problem), nonook (a plant face the walker cannot escape — see below), unexpressed/norec // (the blind reads). const _PARK_BOMB_WHYS = { complete: 0, dead: 0, short: 0, sig: 0, unexpressed: 0, norec: 0, wrongspend: 0, skew: 0, nonook: 0 }; function _parkBombAdmissible(cell) { // SKEW — the three faces' spawn-BFS spread must be <= 2 (a nearer low-priority wall would blur the // confession by visit order). Measured on a fresh, unplayed board over the walker's passable domain. { const st = _parkBombBuild(cell); const P0 = parkStart(st); const b = st.park.bomb; const spawnK = _parkKey(st, st.pos[0]); const ds = []; for (const w of _PARK_BOMB_WALLS) { // measure the face the way the mechanic STEERS to it — seeded AT the face (a face is reached by a // bump from an adjacent cell, not walked onto), reading the distance back to spawn over the same // non-deep domain the goal facet uses. A pure geometric read, no persona. const d = _parkBombDistFrom(P0, b.face[w], 'G'); ds.push(d[spawnK]); } if (!ds.every(isFinite)) { _PARK_BOMB_WHYS.skew++; return false; } if (Math.max(...ds) - Math.min(...ds) > 2) { _PARK_BOMB_WHYS.skew++; return false; } } // NONOOK (I2) — every plant face must leave the walker a genuine escape, so that WAITING vs PRESSING-ON // is a real safety CHOICE and not a forced ♥ loss. The walker plants FROM a cell adjacent to the face // and never stands on the face itself (the action idiom); he then has `fuse` beats before the cross // blows. A face is admissible only if from some legal plant-from cell a NON-BLAST walkable cell is // reachable within that fuse span. A face that boxes the walker inside its own cross (no safe nook // within return) is rejected LOUDLY here — the caution mind would have nowhere to dodge to. { const st = _parkBombBuild(cell); const P0 = parkStart(st); const b = st.park.bomb, dd = st.park.distDeep, fuse = b.fuse, n = st.N; const walkable = (k) => !st.wall.has(k) && !_parkBombBlocked(P0, k, 'me') && dd[k] >= 1; for (const w of _PARK_BOMB_WALLS) { const face = b.face[w]; const blast = _parkBombCross(st, face); const fx = face % n, fy = (face / n) | 0; let ok = false; for (const dir of DIRS) { const fk = (fy + dir.y) * n + (fx + dir.x); if (fy + dir.y < 0 || fx + dir.x < 0 || fy + dir.y >= n || fx + dir.x >= n) continue; if (!walkable(fk)) continue; // a legal cell to plant FROM const d = _parkBombDistFrom(P0, fk, 'N'); // escape over the walker's full domain for (let k = 0; k < d.length; k++) if (d[k] <= fuse && !blast.has(k)) { ok = true; break; } if (ok) break; } if (!ok) { _PARK_BOMB_WHYS.nonook++; return false; } } } const playouts = []; for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkBombBuild(cell), persona); if (P.reason !== 'complete') { _PARK_BOMB_WHYS.complete++; return false; } if (P.hearts < 1) { _PARK_BOMB_WHYS.dead++; return false; } if (P.turns < 12) { _PARK_BOMB_WHYS.short++; return false; } if (P.st.park.dyn.opened.length !== 2) { _PARK_BOMB_WHYS.wrongspend++; return false; } playouts.push(P); } if (!_parkBombSignature(playouts)) { _PARK_BOMB_WHYS.sig++; return false; } let posed = 0, cn = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_BOMB_PAIRS) { if (!(parkPairExpressed(_parkBombBuild(cell), playouts[i].moves, pair) > 0)) continue; posed++; if (pair[0] === 'C') cn++; const expect = _parkPushPairDir(persona, pair); if (!parkRecoverPairLex(_parkBombBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_BOMB_WHYS.norec++; return false; } } } if (posed === 0) { _PARK_BOMB_WHYS.unexpressed++; return false; } if (cn === 0) { _PARK_BOMB_WHYS.unexpressed++; return false; } return true; } // _parkBombRecovers(cell): THE SHIP GATE (P9/P10) — blind 6/6-persona ORDER recovery, out of the // shipped recovery stack and nothing else. True here is the ONLY licence for ship:true on the slot. function _parkBombRecovers(cell) { for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = parkPlayout(_parkBombBuild(cell), PARK_PERSONAS[i]); if (P.reason !== 'complete') return false; const rec = parkRecoverOrder(_parkBombBuild(cell), P.moves, PARK_CAL_TURNS); if (!rec || rec.join() !== PARK_PERSONAS[i].join()) return false; } return true; } function parkBombWhys() { return { ..._PARK_BOMB_WHYS }; } const PARK_BOMB_SHIP_SEED = 1; // ---- THE REGISTRATION. Everything the engine body knows about y20 is here. PARK_FIELD_MECHS.bomb = { build: _parkBombBuild, cell: _parkBombCell, admits: _parkBombAdmissible, // a crate is a wall for the walker ('me') and the companion ('mate') and the route metric — EXCEPT a // face, which the armed route may thread (design law 2). legalMask can only SUBTRACT. legalMask: (P, key, who) => _parkBombBlocked(P, key, who), // THE FACES OPEN ONLY TO A WALKER WHO IS HOLDING A BOMB, and only to HIM: a plant is an ACTION, not a // walk. legalAdd overrides the mask, so this is what makes the bump legal at all. The bubble rescue // (a caught companion) opens the companion's own cell to the assisting walker (the y3 idiom). legalAdd: (P, key, who) => { if (who !== 'me') return false; const b = P.st.park.bomb, dyn = P.st.park.dyn; if (dyn.held && !dyn.lit) { for (const w of (b.walls || _PARK_BOMB_WALLS)) { const opened = w === 'intro' ? dyn.introOpened : dyn.opened.includes(w); if (!opened && key === b.face[w]) return true; } } if (dyn.bubbled && key === _parkMateKey(P.st)) return true; return false; }, // THE PICKUP AND THE PLANT. Both are steps that COMMIT the walker's cell first (parkStep), then this // hook reads a settled board. A pickup is an ordinary step onto a bomb glyph. A plant is a FORCE-OPENED // move the walker never actually stands on — st.pos[0] goes straight back (the y3 assist idiom; P.path // then records where he ACTUALLY stands, so the trace stays honest). The cage-face plant keeps the // pre-charged deep ♥ (design law 3 — the "diving-in value", chosen, not inherited by accident). onEnter: (P, ev) => { const st = P.st, b = st.park.bomb, dyn = st.park.dyn; // Task 24: record arrival at a board-declared WALKWAY target, permanently — see the note on // park.dyn.reachedCell in _parkBombBuild. A no-op on any board that never sets b.attTarget. // b.crateWall (not b.crates) — see the matching note in _parkBombTargetKey. if (b.attTarget) { for (const att of ['G', 'C', 'N']) { const t = b.attTarget[att]; if (t != null && !b.crateWall.has(t) && ev.toKey === t) dyn.reachedCell[att] = true; } } if (!dyn.held && b.bombKeys.includes(ev.toKey)) { // pick a bomb up (ordinary step) dyn.held = true; b.bombKeys = b.bombKeys.filter(k => k !== ev.toKey); st.fx.push({ k: 'take_bomb', x: ev.to.x, y: ev.to.y }); // render hook (ZERO-TEXT) return; } if (dyn.held && !dyn.lit) { // plant on a face (action, not a step) let wall = null; for (const w of (b.walls || _PARK_BOMB_WALLS)) { const opened = w === 'intro' ? dyn.introOpened : dyn.opened.includes(w); if (!opened && ev.toKey === b.face[w]) wall = w; } if (wall) { dyn.held = false; dyn.lit = { key: ev.toKey, at: dyn.beat, wall, cells: _parkBombCross(st, ev.toKey) }; st.pos[0] = { x: ev.from.x, y: ev.from.y }; // an action, not a step if (wall === 'cage' && st.park.bombScene) { st.pos[0] = { x: ev.from.x + (ev.from.x - ev.to.x), y: ev.from.y + (ev.from.y - ev.to.y) }; } st.fx.push({ k: 'plant', x: ev.to.x, y: ev.to.y }); return; } } if (dyn.bubbled && ev.toKey === _parkMateKey(st)) { // the bubble rescue (y3 idiom) dyn.bubbled = false; st.pos[0] = { x: ev.from.x, y: ev.from.y }; st.fx.push({ k: 'pop', x: ev.to.x, y: ev.to.y }); } }, // THE FUSE — the world advances on the beat (a bomb burns down whether the walker moves or waits). // On blast: open the wall (delete its crates), spend the walker's ♥ if he is caught, bubble the // companion if HE is caught (the care STATE channel — no heart for him). A tick may end the run only // through the engine's own death check (re-read right after tick); this one never sets P.over itself. tick: (P) => { const st = P.st, b = st.park.bomb, dyn = st.park.dyn; if (!dyn.lit) return; if (dyn.beat - dyn.lit.at < b.fuse) return; const blast = dyn.lit.cells, w = dyn.lit.wall; if (w === 'intro') dyn.introOpened = true; else dyn.opened.push(w); for (const k of b.wallOf[w]) { b.crates.delete(k); } // THE BLAST is this cell's OOD rhythm (I2): a walker caught in the cross when the fuse blows takes // ♥−1 (a legitimate walker-♥ channel event, distinct from the deep cage dive). The safety mind's job // is to be OUT of the cross by now (the dodge, above); a goal-led walker that pressed on pays the // heart. Channel independence holds: the walker's cost is ♥, the COMPANION's is a bubbled STATE (no // heart for him — the care channel), a token's is destruction. Nothing crosses channels. if (blast.has(_parkKey(st, st.pos[0]))) P.hearts -= st.park.damage == null ? 1 : st.park.damage; if (blast.has(_parkMateKey(st))) dyn.bubbled = true; if (w === 'cage') dyn.caged = false; // the friend can path out now st.fx.push({ k: 'blast', cells: [...blast] }); dyn.lit = null; }, // THE THREE FACETS — one per mind, the y16 two-leg shape (walk to a bomb, then to YOUR OWN face), // folded into the SHIPPED three minds. Each engages while there is bomb business (a mind the walk // board leaves cold), and narrows to the moves that close on its target; a mind whose wall is already // open goes INERT (all-legal) when it holds nothing, but STEPS ASIDE (empty prefer) if it still holds // a bomb — the seam-trap-4 cure that lets the second fork read cleanly. Never a fourth mind, no scorer. reads: { ctx: _parkBombCtx, G: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkBombSteer(P, legal, ctx, 'G') }, C: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkBombSteer(P, legal, ctx, 'C') }, N: { engaged: (P, ctx) => ctx.live, prefer: (P, legal, ctx) => _parkBombSteer(P, legal, ctx, 'N') }, }, }; // PARK_BOMB_SHIPPABLE: the module's MEASURED ship state — computed AFTER the registration (so the // playout runs the mechanic, not a bare walk board) and declared here so the claim is a PIN a // regression must break. Y20-BOMB-SHIP-GATE derives the flag FROM the measurement and asserts they // agree. MEASURED true: the two-bomb confession's SECOND fork is read cleanly because a satisfied mind // STEPS ASIDE (empty prefer) rather than going all-legal — so the subordinate minds both drive the // plant AND are read, which is the seam-trap-4 cure the four predecessors never found. y20 SHIPS 6/6. const PARK_BOMB_SHIPPABLE = _parkBombRecovers(_parkBombCell(PARK_BOMB_SHIP_SEED)); /* ============ BOMB2 (y22) LEFT ON 2026-08-03 with its slot ============ Its demo leg was `ledge`, which SURVIVES (xp is live on it), and it painted with y20's own _paintParkBomb rather than a painter of its own — so nothing but this module went. ==================================================================== */ /* ============ LANTERN FIELD MODULE (y24 "등불의 밤", plan 2026-07-20) ============ */ /* THE CELL WHOSE NIGHT IS NOT IN THE ENGINE. This block held the plan queue up for weeks, and the reason was a real incompatibility rather than a missing lever: an incomplete-information board read by a BLIND recovery collapses two different men into one move. "He did not go because he could not see" and "he did not go because he did not care" are the same step, and a reduction that cannot tell them apart is not a reduction. THE RESOLUTION IS A LAYER SPLIT, and every line below depends on it: (1) THE DARK IS THE PAINTER'S, NOT THE ENGINE'S. There is no sight state, no seen set, no remembered map anywhere on `dyn`. The oracle, the three minds, parkReduce and parkRecoverOrder all read the whole board exactly as they do on every other cell, so the measurement pipeline here is the standard pipeline. A visibility filter on the minds would not NARROW them — it would REPLACE each one with a masked copy of itself, and the registry contract forbids that outright (engine.js:7362-7366). The fog lives in app.js and nowhere else; it is what the HUMAN player is up against, and it is deliberately not what is scored. (2) WHAT IS SCORED IS THE LANTERN'S PUBLIC PHYSICS. The companion cannot walk outside the light. The light is carried until it is HUNG on one of three hooks, and hanging it and taking it back are discrete, ordered, recorded acts. So the confession is not "what did he know" but WHERE HE PUT THE ONLY LIGHT, and that is as public as a pushed crate. THE FREEZE IS y3's, VERBATIM IN SHAPE. legalMask closes the dark in the 'mate' domain ALONE, so the companion's BFS finds no route and comes back `{ next: null, stuck: true }` — and a stuck companion HOLDS: he keeps his contract, keeps his mode, does not retire (seam guarantee 1, engine.js:7398-7402). `{ next: null, arrived: true }` is the other thing entirely, and conflating them would retire him the instant the light left, which is the bug that guarantee exists for. 'me' AND 'route' ARE UNTOUCHED, AND THAT IS LOAD-BEARING. The walker walks into his own dark; the route metric prices the whole board. Two reasons, and the second is a trap: - it is TRUE. The dark stops nobody's feet; it stops a frightened companion's nerve. - a route-masked cell can never be chosen. parkOracleMove's argmin is `let bestM = Infinity; ... if (m < bestM)` (engine.js:7941-7947) and a masked cell's metric IS Infinity, so `Infinity < Infinity` is false and the oracle returns 'stay' even when the masked cell was its only compliant move. A light that moves with the walker would mask a DIFFERENT half of the board every beat; had that reached the 'route' domain, admissibility would have been a flat zero with no visible cause. This module also registers NO legalAdd, so the standing question — "does legalMask('route') close a cell legalAdd opens?" — has no way to arise here. HOW THE THREE MINDS COME APART, with NO new G facet and NO new C facet. The board's `park.deep` does that work, as it did for y26: fast (goal) prices deep 1 -> the barrier row is the short way down; G takes the crossing and pays one heart for it. safe (safety) prices deep 24 -> C walks all the way round to the throat between the lobes. throat flanks are verge (8) -> and the JUNCTION HOOK is one of those flanks, so the shipped caution preference (distDeep >= 2) strikes the hook out of C's compliant set without this module mentioning safety at all. The ONE facet is care's: go and hang the light where his lane is. Its cell is the verge flank C refuses and a square G's beeline never wants — a partition, not a subtraction, which is the only shape that can award a C-vs-N pair (y10's finding). WHAT THE GOAL-LED WALKER DOES IS THE POINT, NOT A DEFECT. He crosses the barrier, pays his heart, and leaves the companion standing in the dark holding a contract he cannot serve. That is y3 restated — the goal-led walker steps over the fallen man — and y3's own admission gate agrees: it requires the rescue of NOBODY and asserts in its signature that safety-top does not perform it (engine.js, _parkDownedSignature). So the arrival bar below (`matelost`) is asked of the CARE-top personas, whose failure would mean the board is unwinnable rather than unkind. */ const PARK_LANT_N = 13; // PARK_LANT_R: the light's radius in Chebyshev cells. It is a NUMBER, and the honest one: at 2 the // hung lantern's 5x5 ball covers the companion's whole errand (his post, the throat, his gem) and a // walker merely PASSING with it in hand does not — which is what makes hanging it a decision rather // than a formality. See the sweep note on PARK_LANT_SHIPPABLE for the measured consequence. const PARK_LANT_R = 2; // PARK_LANT_REACH: how near the hook the escort facet has anything to say, in walker steps. The // shipped care mind is RANGED too (it engages on a contested gem within 3 and on a head-on block at // adjacency, engine.js:7660-7669), and this facet is ranged for the same reason plus a harder one. // // AN UNRANGED VERSION IS A POTENTIAL WELL WITH A LID ON IT. The hook is verge, so the shipped // caution preference strikes out the last step onto it; a walker who ranks caution first and care // second is then pulled toward a square he may never enter, from anywhere on the board, for the whole // run. He circles it — a wide, wandering orbit that no backtrack rule breaks — and never finishes his // own chain. MEASURED at the turn cap on seed 1 before this bound existed. The bound says the honest // thing: a walker nowhere near the junction has no escort move available, so care has nothing to say // about his next step, and the mind that cannot act goes quiet instead of pacing. const PARK_LANT_REACH = 4; const PARK_LANT_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // _parkLantCell(seed): the module's PUBLIC play-cell (C1 — no persona parameter exists). function _parkLantCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'lantern' } }; } // _parkLantCenter(st): the light's centre — the hook it hangs from, or the walker who carries it. // A public read of board + dyn + position; no persona symbol can reach it (C1). function _parkLantCenter(st) { const D = st.park.dyn && st.park.dyn.lantern; if (D && D.key != null) return D.key; return st.pos[0].y * st.N + st.pos[0].x; } // _parkLantLit(st, key): is this cell inside the light? Chebyshev, so the lit region is a square // ball and the geometry notes below can be read off the grid without a distance table. function _parkLantLit(st, key) { const n = st.N, c = _parkLantCenter(st); const dx = Math.abs((key % n) - (c % n)), dy = Math.abs(((key / n) | 0) - ((c / n) | 0)); return Math.max(dx, dy) <= (st.park.lantern ? st.park.lantern.r : PARK_LANT_R); } // _parkLantBuild(cell): the board. Seed-pure, persona-blind (C1). CONSTRUCTED, not rejection-sampled: // every separator is placed on every seed, so the seed draws the LAYOUT and never whether the cell // poses its scene. The anti-mimic axes are the barrier ROW, the THROAT column and the SIDE the // walker's own errand lives on — a demo learner can carry over none of "go west", "the wall is at // row 6", "the door is at column 5". function _parkLantBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_LANT_N; const r = rng((seed * 3187 + 977) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const K = (x, y) => y * n + x; const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) wall.add(K(x, y)); // THE BARRIER ROW, drawn first off the rng stream (byte-stable). Kept mid-board so both halves // hold a spawn, a cluster and the approach: HY-4 >= 1 and HY+4 <= n-2. const HY = cs(5, 7); // THE THROAT — three columns of floor through the barrier, between the two deep LOBES. Three, not // two: the outer two columns sit beside a lobe end and are therefore VERGE (safe 8), while the // middle one is distDeep 2 and stays plain WALKWAY (safe 1). That asymmetry is the entire C-N // geometry — everyone crosses on the middle lane, and only the escort steps onto a flank. const GX0 = cs(0, 1); // THE SIDE the walker's own errand lives on — the far end from the throat, so the escort is a // genuine detour and not something he does in passing. `inward` points back toward the throat, so // every offset below mirrors with the draw instead of running off the board on one of the two sides. const west = cs(0, 1) === 0; const FX = west ? 2 : n - 3; const inward = west ? 1 : -1; const GX = west ? 3 + GX0 : 6 + GX0; // THE TWO FLANKS MIRROR WITH THAT DRAW, and they must: the hook takes the flank NEARER the walker's // own end and the companion's lane takes the far one. Pinning them to fixed columns instead made // every EAST board send the walker across the whole throat AND through the companion's lane to // reach the hook, and all 20 east seeds capped a caution-led run while all 20 west seeds finished. // A layout axis that is only an anti-mimic axis on one of its two draws is not an axis. const lane = K(GX + 1, HY); // the cheap middle of the throat: everyone's crossing const junction = K(west ? GX : GX + 2, HY); // the near flank: verge, and the hook the escort wants const mateLane = west ? GX + 2 : GX; // the far flank: the companion's own column const deep = new Set(); for (let x = 1; x <= n - 2; x++) if (x < GX || x > GX + 2) deep.add(K(x, HY)); // THE BARRIER STAYS ONE ROW THICK, and that was tried the other way first. Curling the lobe ends // into the throat (deep at the two cells above and below each lobe end) does put the hook's own // doorstep at distDeep 1, which reads tidier — and it also makes the row two squares above the // barrier VERGE all the way to the throat, which is the only lane a caution-led walker has for // getting across the board to the throat in the first place. He then cannot reach it at all: 20 of // 40 seeds capped a caution-led run against that wall. The plain row leaves that lane open and // costs nothing, because the lobe END is already beside the flank cell and that is all the flank's // verge status ever needed. // THE SAFETY FRAME — built exactly as _parkBuild builds it (multi-source BFS from the deep set, // walls transparent: 0 deep / 1 verge / >=2 walkway), because the SHIPPED static caution attitude // reads distDeep / verge / walkway and nothing else. const distDeep = new Array(n * n).fill(Infinity); { const q = []; for (const k of deep) { distDeep[k] = 0; q.push(k); } for (let h = 0; h < q.length; h++) { const k = q[h], qx = k % n, qy = (k / n) | 0; for (const d of DIRS) { const nx = qx + d.x, ny = qy + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[k] + 1) { distDeep[nk] = distDeep[k] + 1; q.push(nk); } } } } const verge = new Set(), walkway = new Set(); for (let k = 0; k < n * n; k++) { if (wall.has(k) || deep.has(k)) continue; (distDeep[k] === 1 ? verge : walkway).add(k); } const corridor = new Set([junction, lane, K(GX + 2, HY)]); // THE COMPANION's errand is a short straight line along the EAST flank of the throat: his post // below the barrier, his contract gem above it. Straight-line length 4, and every cell of it is // inside the ball of the junction hook — so a lantern hung there frees him for good, and a walker // who merely carries his light past the throat lights only part of the line at a time and leaves // him stranded halfway. That gap between "passing through" and "hanging it up" IS the cell. const companionSpawn = { x: mateLane, y: HY + 2 }; const station = { x: mateLane, y: HY + 2 }; const retire = { x: mateLane, y: HY + 2 }; // THE HOOKS — three places the one light can be left (the y16 stone's three spends, re-posed as // three PLACES rather than three uses). hooks[1] is the throat junction and is the only one that // is load-bearing; the other two are the honest alternatives that make hanging it a CHOICE of // WHERE rather than a formality, and both are drawn off the seed. // // BOTH ALTERNATIVES HANG OFF THE OUTER COLUMN, one step to the SEAWARD side of his lane, and that // placement was bought with a livelock. A first cut put them one step INWARD, which is on the way // to everything: the walker hung the light on the gem-side hook in passing, the escort facet then // aimed him BACK at it (a lantern on a useless hook has to be fetched before it can be re-hung), // he fetched it, walked on, and hung it again on the same square — a period-6 orbit that capped // both caution-led runs at 110 turns with 17 hangs logged. An alternative you cannot decline is // not an alternative; it is a trap on the path. const hookGem = K(FX - inward, HY - 2 - cs(0, 1)); // beside his own cluster, off his lane const hookSpawn = K(FX - inward, 1 + cs(0, 1)); // beside his spawn stub, off his lane const hooks = [hookGem, junction, hookSpawn]; // THE TOKENS. chain 0 ABOVE on the walker's own side, chain 1 and 2 BELOW it, so the ordered chain // makes the barrier crossing compulsory: the beeline goes straight through the deep row for one // heart, and the no-deep read must walk the length of the board to the throat and back. That is // the whole G-C separation, delivered by the shipped fast/safe metrics with no facet of ours. // // EVERY DESTINATION LIES THROAT-WARD OF THE ONE BEFORE IT, and that ordering is not decoration — // it is the third livelock, fixed. While the escort facet is live it pulls the walker DOWN toward // the junction; a chain leg that pulled him UP put the two in a standing argument that the shipped // caution mind then arbitrated into an orbit (cold at distDeep 3, so care walks him one square in; // awake at distDeep 2, so the route argmin walks him straight back out — a two-square cycle to the // turn cap). With the chain running one way, the argmin at that boundary points ONWARD instead of // back, and the same refusal resolves in a single beat. // // NO TOKEN MAY SIT ON A HOOK OR ON A STAGING CELL — y26 swept 0/40 on exactly that mistake, and // Y24-LANT-BUILD pins it. const tokens = [ { x: FX, y: HY - 2, v: cs(2, 3), alive: true, guard: false }, // chain 0 (above) { x: FX, y: HY + 2, v: cs(2, 3), alive: true, guard: false }, // chain 1 (BELOW — the crossing) { x: FX + 8 * inward, y: n - 2, v: cs(2, 3), alive: true, guard: false }, // chain 2 (below, far corner) { x: mateLane, y: HY - 2, v: cs(1, 2), alive: true, guard: false }, // the companion's contract gem ]; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const spawn = { x: FX, y: 2 }; const park = { N: n, seed, k: 0, fieldMech: 'lantern', deep, verge, walkway, distDeep, lantern: { r: PARK_LANT_R, hooks, hookSet: new Set(hooks), corridor, junction, lane, hy: HY, gx: GX, mateLane, west }, clusters, chain: [0, 1, 2], contracts: [{ gem: 3, station }], retire, spawn, companionSpawn, // trig is the companion's ENGAGEMENT radius (manhattan, walker-to-gem). Kept generous so his // errand is live from early on: a contract nobody is serving is what the escort facet is about, // and a companion who never engages would make the frozen-beat meter read zero for everyone. trig: n * 2, cap: 110, minTurns: 12, cautionD: 2, damage: 1, cell: _parkLantCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: companionSpawn.x, y: companionSpawn.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // OPT-IN runtime container (Task 0) // key: the hook it hangs from, null while carried. drops/takes: the ordered CONFESSION, each with // its beat. frozeBeats: the public meter of how long the companion had no lit step to take. // Plain object + arrays + primitives, so _parkDeepClone carries it through every search fork. park.dyn.lantern = { key: null, drops: [], takes: [], frozeBeats: 0 }; return st; } // _parkLantApproach(P, target): step distance from every cell to `target` over the domain the walker // would actually use to run this errand — non-wall, and off the deep barrier, because an escort that // routed the walker through the hazard would be buying the favour with his body and the care read is // a PATH read, never a price. Returns a full Infinity-filled table when there is no target. function _parkLantApproach(P, target) { const st = P.st, n = st.N, park = st.park; const dist = new Array(n * n).fill(Infinity); if (target == null) return dist; dist[target] = 0; const q = [target]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || park.deep.has(nk)) continue; if (dist[nk] > dist[k] + 1) { dist[nk] = dist[k] + 1; q.push(nk); } } } return dist; } // _parkLantCtx(P): the ONE per-state read the escort facet uses (the registry computes it once per // state). AIM is where the light has to go next for the companion's lane to open: // carried -> the junction hook. // hung somewhere else -> that hook, because it must be fetched before it can be re-hung. (Without // this arm a walker who parked it on the spawn-side hook would be steered // forever at a hook he cannot hang anything on — an inert-facet livelock.) // hung on the junction -> null: the errand is done and the facet goes cold of its own accord. // LIVE also requires his gem to be unbanked, so the facet self-gates the moment he is home. Every // term is a public read of board + dyn + tokens (C1). function _parkLantCtx(P) { const st = P.st, park = st.park, L = park.lantern, D = park.dyn && park.dyn.lantern; if (!L || !D) return { live: false, aim: null, dist: [] }; const gem = st.tokens[park.contracts[0].gem]; const aim = D.key == null ? L.junction : (D.key === L.junction ? null : D.key); if (!gem || !gem.alive || aim == null) return { live: false, aim: null, dist: [] }; const dist = _parkLantApproach(P, aim); return { live: dist[_parkKey(st, st.pos[0])] <= PARK_LANT_REACH, aim, dist }; } // _parkLantPlay(cell, persona): a faithful playout recording the OBSERVABLES the signature reads. // Pure reads of public state; the persona goes to the oracle and nowhere near the board. function _parkLantPlay(cell, persona) { const P = parkStart(_parkLantBuild(cell)); while (!P.over) parkStep(P, parkOracleMove(P, persona)); const st = P.st, park = st.park, D = park.dyn.lantern, L = park.lantern; P._lantJunction = D.drops.some(d => d.key === L.junction); // did he hang it where his lane is P._lantDrops = D.drops.length; P._lantFroze = D.frozeBeats; P._lantMateHome = !st.tokens[park.contracts[0].gem].alive; // did the companion ever serve his contract return P; } // _parkLantSignature(playouts): the field's OWN visible signature (filter level), read off three // different observables because the three minds do not differ in SCORE here — every persona banks // the same chain, and a signature leaning on score would be reading noise. // SAFETY-top — 0 deep entries (it walks to the throat rather than across the barrier) and it never // hangs the lantern on the junction, because that hook is verge and the shipped // caution preference strikes it out. Abstention, not inability. // GOAL-top — takes the barrier (>= 1 deep entry) and gets home in strictly fewer turns than the // caution baseline. The bound is DATA-DRIVEN — caution's own worst time, not a constant. // CARE-top — BOTH care personas hang it on the junction AND the companion gets home; and NO other // mind gets him home, so the arrival is attributable to the act rather than to luck. function _parkLantSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); let safeTurns = -Infinity, goalWorst = -Infinity; let careAll = true, careSeen = 0; for (let i = 0; i < playouts.length; i++) { const P = playouts[i]; if (top[i] === 'safety') { if (P.deepEntries !== 0) return false; if (P._lantJunction) return false; if (P._lantMateHome) return false; safeTurns = Math.max(safeTurns, P.turns); } if (top[i] === 'goal') { if (P.deepEntries < 1) return false; if (P._lantJunction) return false; if (P._lantMateHome) return false; goalWorst = Math.max(goalWorst, P.turns); } if (top[i] === 'care') { careSeen++; if (!P._lantJunction || !P._lantMateHome) careAll = false; } } if (!isFinite(safeTurns) || !isFinite(goalWorst)) return false; // non-vacuity guard if (!(goalWorst < safeTurns)) return false; return careSeen > 0 && careAll; } // _parkLantAdmissible(cell): the generate-then-filter PLAYABILITY gate over the FULL persona set. // Every faithful playout completes alive; the CARE-led ones actually get the companion home (a board // where even they cannot is unwinnable, not merely unkind — that is `matelost`, and it is asked of // care alone for the reason set out in the module header); the field signature separates; the C-N // scene is really POSED; and every pair the board poses blind-recovers in the demonstrated direction. // Reject reasons tallied LOUDLY — a silent 0/40 teaches nothing. const _PARK_LANT_WHYS = { complete: 0, dead: 0, matelost: 0, sig: 0, nocn: 0, norec: 0 }; function _parkLantAdmissible(cell) { const playouts = []; for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = _parkLantPlay(cell, PARK_PERSONAS[i]); if (P.reason === 'death') { _PARK_LANT_WHYS.dead++; return false; } if (P.reason !== 'complete') { _PARK_LANT_WHYS.complete++; return false; } if (P.hearts < 1) { _PARK_LANT_WHYS.dead++; return false; } if (PARK_PERSONAS[i][0] === 'care' && !P._lantMateHome) { _PARK_LANT_WHYS.matelost++; return false; } playouts.push(P); } if (!_parkLantSignature(playouts)) { _PARK_LANT_WHYS.sig++; return false; } let posed = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_LANT_PAIRS) { if (!(parkPairExpressed(_parkLantBuild(cell), playouts[i].moves, pair) > 0)) continue; if (pair[0] === 'C' && pair[1] === 'N') posed++; const expect = _parkPushPairDir(persona, pair); if (!parkRecoverPairLex(_parkLantBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_LANT_WHYS.norec++; return false; } } } if (posed < 2) { _PARK_LANT_WHYS.nocn++; return false; } return true; } // parkLantWhys(): a copy of the reject tally (gates read it as a DELTA around their own sweep). function parkLantWhys() { return { ..._PARK_LANT_WHYS }; } // ---- THE REGISTRATION. Everything the engine body knows about y24 hangs here, off the id the board // stamps as park.fieldMech. PARK_FIELD_MECHS.lantern = { build: _parkLantBuild, cell: _parkLantCell, admits: _parkLantAdmissible, // THE MASK — ONE domain. The dark is a wall to the COMPANION and to nobody else: the walker walks // into his own dark and the route metric prices the board entire. Registering 'route' here would // make a moving light re-mask half the board every beat and hand the oracle an Infinity argmin // (engine.js:7941-7947); registering 'me' would be a lie about what a dark meadow does to feet. legalMask: (P, key, who) => { if (who !== 'mate') return false; const park = P.st.park; if (!park.lantern || !park.dyn || !park.dyn.lantern) return false; return !_parkLantLit(P.st, key); }, // THE ONE CONSEQUENCE. Entering a hook while carrying HANGS the light there; re-entering the cell // it hangs from takes it back. Both are logged with their beat, and the pair of logs is the whole // confession the readout has to work with. onEnter: (P, ev) => { // onEnter fires for EVERY move key, 'stay' included (engine.js:7324) — and a walker standing // still on a hook must not hang and re-take the light once a beat. Say so explicitly. if (ev.mvKey === 'stay') return; const park = P.st.park, L = park.lantern, D = park.dyn && park.dyn.lantern; if (!L || !D) return; if (D.key == null) { if (L.hookSet.has(ev.toKey)) { D.key = ev.toKey; D.drops.push({ beat: park.dyn.beat, key: ev.toKey }); P.st.fx.push({ k: 'hang', x: ev.to.x, y: ev.to.y }); // render hook (ZERO-TEXT) } } else if (ev.toKey === D.key) { D.key = null; D.takes.push({ beat: park.dyn.beat, key: ev.toKey }); P.st.fx.push({ k: 'take', x: ev.to.x, y: ev.to.y }); } }, // THE METER. A beat on which the companion still owes his contract and has NO lit step to take is // a frozen beat. It is a pure read of public state, it is what the render draws his shiver from, // and it is deliberately NOT the freeze itself — the freeze is the mask's doing, and a module that // re-derived it here would eventually disagree with the planner about who is stuck. tick: (P) => { const st = P.st, park = st.park, n = st.N, D = park.dyn && park.dyn.lantern; if (!D) return; const gem = st.tokens[park.contracts[0].gem]; if (!gem || !gem.alive) return; const here = _parkKey(st, st.pos[1]); for (const d of DIRS) { const nx = st.pos[1].x + d.x, ny = st.pos[1].y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || park.deep.has(nk)) continue; if (_parkLantLit(st, nk)) return; } if (_parkLantLit(st, here)) return; D.frozeBeats++; }, // THE READS — ONE facet, folded into the shipped CARE mind. There is deliberately no G facet and // no C facet: park.deep already makes the shipped fast/safe metrics disagree about the barrier, // and the junction hook is verge, so the shipped caution preference already refuses the errand. // Teaching either of those in a module would be re-deriving what the engine does for free (y26's // finding, restated). reads: { ctx: _parkLantCtx, N: { // CARE: the companion still owes his gem and the light is not yet where his lane is. // Self-gated — the beat it is hung on the junction, or he banks the gem, this goes cold. engaged: (P, ctx) => ctx.live, // the PATH read: close on the hook. Never {} — an empty prefer would not veto, it would // silently delete care from the decision entirely (engine.js:7369-7377). // // THE JUST-VACATED SQUARE IS EXCLUDED, exactly as the shipped goal preference excludes it // (engine.js:7702), and this is the fourth livelock rather than a tidiness point. The throat is // a one-square channel whose two flanks are verge, so a caution-led walker crossing it has only // the two vertical moves; a bare distance gradient offers him the flank (refused) AND the square // behind him (allowed), the intersection keeps the square behind him, and he crosses the same // two cells until the turn cap. An approach that re-enters the square it just left is not an // approach, and saying so costs this facet nothing on any path that is really one. prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx.live) { for (const c of legal) out.add(c.k); return out; } // cold: say nothing const cur = ctx.dist[_parkKey(P.st, P.st.pos[0])]; if (isFinite(cur)) for (const c of legal) { if (P.prev && P.prev.x === c.x && P.prev.y === c.y) continue; if (ctx.dist[c.key] < cur) out.add(c.k); } if (out.size) return out; for (const c of legal) out.add(c.k); return out; }, }, }, }; // PARK_LANT_SHIPPABLE — the MODULE's own ship pin, read by CAMP-CROSS-SWEEP's F4 through // engine.test.js's FIELD_SHIPPABLE map. y24 is seated as an honest PREVIEW, so this is a `false` // LITERAL and not a measurement: the derived form (`_parkLantRecovers`, the _parkBombRecovers shape) // belongs to the session that actually measures the pairing, and writing it early would put a number // on the slot before anyone had run it. derive-never-assert cuts both ways — do not assert TRUE, and // do not dress a false up as derived either. const PARK_LANT_SHIPPABLE = false; /* ============ END LANTERN FIELD MODULE (builder + the hook hang/take, the mate-only dark, the frozen beat meter, the escort care facet, the abstention/turn/arrival signature, and the admission gate) = */ /* ============ YIELD FIELD MODULE (y33 "외나무다리", design 2026-07-23) ============ */ /* The purpose-built C-vs-N board — the standing gap this file's own ledger records twice (the warden retraction above at _PARK_WARDEN_*, and campaign.js's x2 ledger: "no existing geometry poses a C-vs-N conflict scene"). A deep-meadow chasm splits the park in two, spanned by ONE footbridge: a 1-wide walkway lane through a water-walled band, with DEEP shoulder pockets along its north rim and a ROTTEN two-cell segment mid-span whose only sound detour is a short verge boardwalk hugging its south side. The companion starts on the far side and his contract gem is on the near side, so the two travellers cross the SAME plank in OPPOSITE directions — the head-on verge YIELD (the park's one known C-N poser, y12's diagnosis) is not left to timing luck here; it is the board's whole reason to exist. THE THREE MINDS, read through the SHIPPED PARK_ATTITUDES (no reads facet — none is needed; the y14 diagnosis was a TIMING/geometry gap, never a wiring gap): G goal — the rotten segment is the beeline (the bypass's first step INCREASES the fast distance), so the goal mind wades it: one certain heart, the stones precedent. C safety — cautionD is 1 on this board: the caution band is exactly "do not stand IN the field". The whole lane is engaged (dd=1) and the C-compliant set is every non-deep step — so safety-led play takes the verge boardwalk round the rot, and at a head-on it HOLDS the plank (stay/retreat, both on the companion's path). N care — _parkNCtx.blocked (head-on, the shipped read): the care-compliant set is the step OFF his path — and mid-plank the only off-path cells are the deep pockets. C forbids exactly what N prescribes and vice versa: DISJOINT compliant sets, the C-N award condition, met on shipped machinery. The care cost is the body and the safety cost is the friend — pay a heart to clear his lane, or keep your feet dry while he stands blocked in the chasm wind. DESIGN LAWS: 1. WATER IS WALL (y12 law 3): the band off the lane/shoulder rows is impassable, so EVERY traveller — and every persona — funnels onto the one plank. The head-on is geometry, not luck (the y12 failure was same-direction travel; here the directions oppose). 2. ROT ON THE PLANK, NOT BESIDE IT: the G-C wade scene lives ON the shared lane (mid-span, turn > PARK_CAL_TURNS by construction — the y8 timing lesson), so the goal mind cannot buy its shortcut somewhere the companion never walks. 3. THE BYPASS PRICES THE ROT (stones frame-law lesson): without the verge boardwalk the safety-led walker stands at the rotten lip forever (argmin picks 'stay') and caps out. Its first step is fast-distance-INCREASING, so it never tempts the goal mind. 4. RETIRE WEST: the companion's post-gem target is on the PLAYER's side, so even a goal-led walker who takes the companion's gem early (killing his toGem errand) still meets him on the plank — the relocation crosses it regardless. 5. cautionD = 1 (not the walk boards' 2): on a 1-wide plank a d=2 band would empty C's compliant set entirely (no legal cell has dd >= 2 mid-span) and an empty preference is INERT, not a veto (registry trap 1) — C would vanish exactly where the C-N question is asked. d=1 keeps C non-empty (stay/retreat) and disjoint from N (the pockets). */ const PARK_YIELD_N = 16; // the three conflict pairs this board means to pose — the full order, C-N included. const PARK_YIELD_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // _parkYieldBuild(cell): the y33 board — pure PUBLIC geometry (reads ONLY cell.seed; C1: no // persona parameter exists), runtime-compatible with parkStart/parkStep. Frame (16x16): // perimeter ring; a water-walled band (cols xb..xb+6) split by the lane row yc; deep pockets // on the lane's north rim (and south rim off the boardwalk); the rotten 2-cell segment rc..rc+1 // with its 4-cell verge boardwalk at yc+1; open walkway lobes west and east. function _parkYieldBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_YIELD_N; const r = rng((seed * 1013 + 5807) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const xb = cs(5, 6); // band west edge (7 cols wide: xb..xb+6) const yc = cs(8, 9); // the lane (plank) row const rc = xb + cs(2, 3); // rotten segment cols rc..rc+1 (mid-span, so the // wade lands turns deep into the crossing — law 2) const gy1 = cs(3, 4); // chain-0 gem row (NW, up the spawn column) const flip = r() < 0.5; // E/W mirror (the anti-mimic draw axis) const M = (x) => flip ? n - 1 - x : x; const K = (x, y) => y * n + x; const wcol = 2; // the west spine column (spawn/gem-0 line up on it) const inBand = (x) => x >= xb && x <= xb + 6; // the APPROACH CORRIDOR (cols xb-2..xb-1): the plank's west on-ramp, 1-wide with deep pockets // for shoulders — so the head-on scene extends OFF the band and the companion's exit walk // stays a C-N poser to the last step. His contract gem sits IN this corridor (see tokens): // that is the SYNC — park.trig is small, so his errand starts only when the WALKER nears the // corridor, whichever persona is walking and however long its west-lobe errands took. The // head-on is therefore tied to the player's own approach, not to absolute turn numbers. // the THROAT (cols xb-2..xb-1): the plank's walled west on-ramp — 1-wide, pocket-flanked at // its inner column, WALL-flanked everywhere else, so no fast-monotone path can cut a shoulder // pocket from the lobe (the corner-wade leak the first cut measured: 3-heart deaths). const inThroat = (x) => x >= xb - 2 && x <= xb - 1; const ys = yc + 4; // the LONG WAY ROUND (y12's "free crossing", re-cut // for the head-on age): a second, distant span the // COMPANION's blocked-reroute escapes through — // without it a holding walker and a holding // companion deadlock to the turn cap. Priced VERGE // so the safe plan still prefers the plank, and // never fast-monotone, so the goal mind ignores it. const wall = new Set(), walkway = new Set(), verge = new Set(), deep = new Set(), water = new Set(); const rotten = new Set(), pockets = new Set(), lane = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = K(M(x), y); if (x === 0 || y === 0 || x === n - 1 || y === n - 1) { wall.add(kk); continue; } if (inThroat(x)) { if (y === yc) { lane.add(kk); walkway.add(kk); continue; } // the mouth (1-wide) if (y === yc - 1 || y === yc + 1) { if (x === xb - 1) { pockets.add(kk); deep.add(kk); continue; } // inner shoulders: pockets wall.add(kk); water.add(kk); continue; // outer shoulders: wall } if (y === yc - 2 || y === yc + 2) { wall.add(kk); water.add(kk); continue; } // the throat's rim walkway.add(kk); continue; // lobe above/below the throat } if (!inBand(x)) { walkway.add(kk); continue; } // open lobes if (y === yc) { // the plank lane.add(kk); if (x === rc || x === rc + 1) { rotten.add(kk); deep.add(kk); } // the rot IS the field else walkway.add(kk); continue; } if (y === yc - 1) { pockets.add(kk); deep.add(kk); continue; } // north rim: yield pockets if (y === yc + 1) { if (x >= rc - 1 && x <= rc + 2) walkway.add(kk); // the verge boardwalk (demoted below) else { pockets.add(kk); deep.add(kk); } // south rim off the boardwalk continue; } if (y === ys) { verge.add(kk); continue; } // the long way round (companion-priced) wall.add(kk); water.add(kk); // law 1: the chasm is impassable } // the frame demotion (stones precedent): footing beside the field is VERGE, not walkway — // the safe field re-prices it (8) so the caution plan and the safe plan stay the same plan. for (const kk of [...walkway]) { const x = kk % n, y = (kk / n) | 0; if ([K(x - 1, y), K(x + 1, y), K(x, y - 1), K(x, y + 1)].some(q => deep.has(q))) { walkway.delete(kk); verge.add(kk); } } const distDeep = new Array(n * n).fill(Infinity); // multi-source BFS (walls transparent) const q = []; for (const kk of deep) { distDeep[kk] = 0; q.push(kk); } for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[kk] + 1) { distDeep[nk] = distDeep[kk] + 1; q.push(nk); } } } const tokens = [ { x: M(wcol), y: gy1, v: cs(2, 3), alive: true, guard: false }, // chain 0 (NW, up the spine) { x: M(n - 3), y: yc, v: cs(2, 3), alive: true, guard: false }, // chain 1 (E, past the plank) { x: M(xb + 1), y: yc, v: cs(1, 2), alive: true, guard: false }, // the companion's contract gem // ON the plank's west span: the walker cannot reach // gem-1 without deciding about it (the G-N pose), // and the companion's errand — trig-synced to the // walker's approach — sends him DOWN the plank // exactly when the walker wants it (the C-N pose). ]; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const spawn = { x: M(wcol), y: n - 3 }; const station = { x: M(xb + 7), y: yc }; // the plank's EAST mouth: he idles on // his own doorstep, so even at half pace the trigger // puts him mid-plank exactly as the walker arrives. const retire = { x: M(1), y: n - 3 }; // law 4: RETIRE WEST (the player's side) const park = { N: n, seed, k: 0, fieldMech: 'yield', // the REGISTRY key: every engine dispatch point // routes back to the bundle at the foot of this // module. The body never names a plank. walkway, verge, deep, distDeep, water, yield: { rotten, pockets, lane }, // SEED-PURE (immutable): runtime tallies live in dyn clusters, chain: [0, 1], contracts: [{ gem: 2, station }], retire, spawn, companionSpawn: { x: station.x, y: station.y }, trig: 5, cap: 90, minTurns: 12, cautionD: 1, damage: 1, // law 5 (d=1); meadow tone. // trig 5 (NOT y12's whole board): the trigger is the // SYNC — the companion sets out only when the walker // nears his corridor gem, so the two crossings meet // on the plank for every persona's own pace. geom: { yc, xb, rc, flip }, // render-only PUBLIC cell summary (rebuilt from the SEED — never the caller's cell object, // which may carry stamps that would leak into the board bytes). cell: _parkYieldCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: station.x, y: station.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // OPT-IN runtime container park.dyn.yields = 0; // pocket-steps taken to clear the companion's lane park.dyn.meets = []; // head-on beats (render/telemetry only, never physics) return st; } // _parkYieldCell(seed): the module's PUBLIC play-cell shape (the stones precedent: NO new task // kind — a mechanism with its own geometry hangs off mech.fieldMech and its own builder). A pure // value constructor; no persona parameter exists (C1). NOTE hazard.d = 1 (design law 5). function _parkYieldCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 1 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'yield' } }; } // _parkYieldSignature(playouts): the field's OWN visible signature, aligned with PARK_PERSONAS: // GOAL-top wades the rotten segment (>= 1 costly deep entry), SAFETY-top takes the boardwalk and // holds the plank (0 entries, 0 yields), CARE-top steps into a pocket to clear the companion's // lane (>= 1 yield). Filter-level: it gates which candidate survives the sweep. function _parkYieldSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < playouts.length; i++) { const dyn = playouts[i].st.park.dyn; if (top[i] === 'goal' && playouts[i].deepEntries < 1) return false; if (top[i] === 'safety' && (playouts[i].deepEntries !== 0 || dyn.yields !== 0)) return false; if (top[i] === 'care' && dyn.yields < 1) return false; } return true; } // _parkYieldAdmissible(cell): the generate-then-filter gate over the FULL persona SET: every // persona's faithful playout COMPLETES alive, the signature separates, every posed pair is // blind-recovered in the demonstrated direction, the C-N evidence lands OUTSIDE the calibration // span (the y8 lesson — evidence the readout discards is evidence the board does not have), and // the CALIBRATED blind order recovery reassembles all six orders (6/6 — the very bar y12/y14 // measured 0/144 on; here it is ADMISSION, because posing C-N is this board's whole ambition). // FRESH build per consumer (playouts mutate their board). Reject reasons tallied LOUDLY. const _PARK_YIELD_WHYS = { complete: 0, dead: 0, sig: 0, cnearly: 0, unexpressed: 0, norec: 0, order: 0 }; function _parkYieldAdmissible(cell) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkYieldBuild(cell), persona); if (P.reason !== 'complete') { _PARK_YIELD_WHYS.complete++; return false; } if (P.hearts < 1 || P.turns < 12) { _PARK_YIELD_WHYS.dead++; return false; } playouts.push(P); } if (!_parkYieldSignature(playouts)) { _PARK_YIELD_WHYS.sig++; return false; } for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i], P = playouts[i]; const cn = P.awards.filter(w => w.pair === 'CN'); if (!cn.length || cn.some(w => w.turn <= PARK_CAL_TURNS)) { // posed, and posed LATE _PARK_YIELD_WHYS.cnearly++; return false; } for (const pair of PARK_YIELD_PAIRS) { if (!(parkPairExpressed(_parkYieldBuild(cell), P.moves, pair) > 0)) { _PARK_YIELD_WHYS.unexpressed++; return false; // ALL THREE, every persona } const expect = _parkPushPairDir(persona, pair); if (!parkRecoverPairLex(_parkYieldBuild(cell), P.moves, pair, { expect }).recovered) { _PARK_YIELD_WHYS.norec++; return false; } } const rec = parkRecoverOrder(_parkYieldBuild(cell), P.moves, PARK_CAL_TURNS); if (!rec || rec.join() !== persona.join()) { _PARK_YIELD_WHYS.order++; return false; } } return true; } function parkYieldWhys() { return { ..._PARK_YIELD_WHYS }; } // PARK_YIELD_SHIPPABLE — the MODULE's own ship pin (the warp precedent: a `false` LITERAL until // the session that measures the full crossing bars writes the derived form; derive-never-assert // cuts both ways). y33 seats as an honest open PREVIEW regardless — admission alone is the // stricter-than-y12/y14 bar (calibrated 6/6 order recovery is IN admits above), but the ship // bar is the FULL crossing suite (incongruence sweep, mimic control, diverse-path widening). // PROMOTED 2026-07-31. The literal is gone; the pin DERIVES. What changed is not the board — the // board has been at calibrated 6/6 since the day it was built — but the SLOT's kind. y33 was seated // as m1, so the crossing layer measured GOAL-vs-CAUTION on a footbridge built to ask CAUTION-vs-CARE, // and the surface-mimic control probed the same wrong pair. On m1 the copier reproduced G-C on 8 of // 43 probes (19%), all but two of them on the GOAL-top personas — of course it did: "walk at the // shiny thing" IS the goal mind's surface, so G-C was never this board's evidence to give. Re-seated // as m3 the control probes C-N, which a demo-replaying copier cannot produce at all, and the whole // bar clears: seeds 1..24 x 6 personas -> incongruence 24/24, faithful complete 144/144, calibrated // blind order recovery 144/144 (undecided 0, MIS-READ 0), mimic leaks 0. Measured with // tools/slot-sweep.mjs; the crossing-side pin is PARK_Y33_SHIPPABLE in campaign.js. // (This is y46's correction, arrived at from the opposite side: y46's kind was wrong while its scene // was already universal; y25's kind is equally wrong but its scene reaches only two personas, so the // same edit there kills admission outright — see plans/2026-07-31-y25-lever-findings.md. A kind // correction only pays where the board already poses the pair for everyone. This one does.) // SHIP SEED 2, not 1: this module has no generator of its own, so _parkYieldCell(s) is a RAW draw // and most raw draws are not admissible — the campaign finds an admitted cell by striding // (Y33-YIELD-SHIP-GATE does the same with _Y33_STRIDE). 2 is the lowest seed whose raw cell the // admission gate accepts; 3, 16, 17, 24, 25 also do, and all six recover 6/6. const PARK_YIELD_SHIP_SEED = 2; function _parkYieldRecovers(cell) { for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkYieldBuild(cell), persona); if (P.reason !== 'complete') return false; const r = parkRecoverOrder(_parkYieldBuild(cell), P.moves, PARK_CAL_TURNS); if (!r || r.join() !== persona.join()) return false; } return true; } const PARK_YIELD_SHIPPABLE = _parkYieldRecovers(_parkYieldCell(PARK_YIELD_SHIP_SEED)); // ---- THE REGISTRATION. Everything the engine body knows about y33 hangs here. Note what is // ABSENT and why: no legalMask (the chasm is st.wall — board geometry, never overridable — and // nothing on this board is consumed or shut at runtime); no legalAdd (no forced lane); no tick // (no beat-driven entity — the companion's own scheduler carries the whole scene); no oracleCost // (the shipped fast/safe metrics already price the rot and the boardwalk correctly); and NO // reads facet — the C-N pose rides the SHIPPED C and N attitudes on purpose-built geometry, // which is the entire finding (the gap was never in the wiring). PARK_FIELD_MECHS.yield = { build: _parkYieldBuild, cell: _parkYieldCell, admits: _parkYieldAdmissible, // DECLARED POSED PAIRS (CN-FIELD-PAIRS, design 2026-07-23): what this field purpose-poses // BEYOND the m1 kind signature (G-C). parkPosedPairs unions these in for the crossing sweep; // the declaration is backed by admits above (expressed>0 for all three pairs), never by faith. pairs: [['G', 'N'], ['C', 'N']], // THE YIELD TALLY (render/signature only — physics is the universal deep-entry charge, which // has already fired). A pocket entry counts as a YIELD when the companion stood beside the // vacated cell: that is the ceremony the signature and the painter read. 'stay' cannot enter. onEnter: (P, ev) => { const st = P.st, park = st.park, dyn = park.dyn; if (!park.yield || !dyn || ev.mvKey === 'stay') return; if (!park.yield.pockets.has(ev.toKey)) return; if (manhattan(st.pos[1], ev.from) > 2) return; dyn.yields++; dyn.meets.push(dyn.beat); st.fx.push({ k: 'yield', x: ev.to.x, y: ev.to.y }); // render hook (ZERO-TEXT) }, }; /* ============ END YIELD FIELD MODULE (builder + the plank/rot/boardwalk geometry, the pocket yield tally, the stricter-than-preview admission gate — no reads facet by design) ========= */ /* ============ SIEGE FIELD MODULE (y46 "조여드는 무궁화", plan 2026-07-25) ============ */ /* THE FIRST HYBRID CELL: y29's temporal hazard (the doll's song/gaze clock) on a yard whose REAR ground sinks on the SAME clock. One period = 6 beats: 4 song beats (free), 2 gaze beats (a step costs a heart), and on each period's FIRST SONG BEAT the next rear band drowns — three waves, then the sea rests. The two clocks are deliberately ONE clock: ground only ever dies on a beat the walker was free to move, so the gaze never manufactures a forced heart loss; the difficulty is compound PLANNING (advance, escort, evacuate — all inside a 6-beat day), not a trap. WHAT SINKS IS THE REAR HALF ONLY. A whole Chebyshev ring would eat the finish (it sits one cell inside the doll's wall — ring 0 by construction), so the bands are ring 0..2 cells whose forward coordinate along the doll axis is CUT(-2) or less: the spawn quarter. The wave chases the walker TOWARD the doll; the finish, his companion's gem, the retire seat and the whole forward yard are structurally dry, which is what keeps the route-mask Infinity trap unreachable (a masked cell is never a cell any errand needs). TWO HEART CHANNELS, TWO CONFESSION LOGS, NO MIXING (the spec's §5 decision): a step under the gaze bills through dyn.statue.caught (y29 verbatim); a body standing on ground the very beat it sinks is SHOVED to the nearest dry cell and billed through dyn.siege.swept. Both are public- schedule events a plan can avoid, and a gate can tell them apart because the logs are separate. THE STATUE FIXTURE IS STAMPED FOR REAL (park.statue + dyn.statue, y29's exact member shapes). That is not cosmetic reuse: the app's statue painter, its live-gaze post layer and the summon click affordance (app.js keys off `st.park.statue`) all work on this board unchanged — the bomb2-borrows-the-bomb-painter precedent, board-driven. The engine-side helpers are RE-DERIVED under _parkSiege* names (the flood->storm precedent: modules never import a sibling's helpers), and the ONE behavioural divergence is the summon walk: its BFS also refuses sunk ground. READS ARE y29's C AND N VERBATIM — the hybrid hardens the WORLD, not the minds. A sink-avoid facet was considered and rejected at design time: faithful personas march off the rear bands long before the first wave (spawn is 4+ cells behind the middle, the first sink is beat 6), so the clause would be 0%-engaged — a dead facet, y27's measured lesson. The ground's danger reaches the minds the honest way instead: through legalMask, the route metric's beat-keyed recompute, and the companion's own planner routing around the water. */ const PARK_SIEGE_N = 13; const PARK_SIEGE_SING = 4; const PARK_SIEGE_GAZE = 2; const PARK_SIEGE_PERIOD = PARK_SIEGE_SING + PARK_SIEGE_GAZE; // STUN 1, not y29's 2 (promotion 2026-07-26): a companion frozen for two beats is inert for the // whole of the next looking beat, and the care mind reads `live` — so the longer stun quietly // deletes the very scene this cell exists to pose. Measured at the shipped geometry: stun 2 leaves // 6 of 48 runs with C-N undecided, stun 1 leaves 4, with admission 40/40 either way. const PARK_SIEGE_STUN = 1; const PARK_SIEGE_LEAD = 2; // THE WAVES. RINGS rear bands sink, one per period, each on that period's first song beat // (tick fires after dyn.beat++, so the sink lands when beat REACHES k*EVERY — a song beat). // EVERY is the PERIOD on purpose and must stay a multiple of it: the phase alignment is the // admission's lifeline (mis-phased clocks were y29's dead-collapse lesson). /* THE WAVES (v2, 2026-08-03 — 더 빠르게, 더 넓게). WIDTH: RINGS 3 -> 6 and CUT -2 -> +2. What used to take only the rear half now comes past the middle of the yard. PACE: ONE band per wave. It was briefly two — the first cut of v2 raised the speed that way, reasoning that EVERY cannot shorten (it must stay a multiple of PARK_SIEGE_PERIOD, because the two clocks in this cell are deliberately ONE clock: ground only ever dies on a beat the walker was free to move, and mis-phasing them was y29's dead-collapse lesson). But two bands at once reads as a LURCH, not a rise — a third of the yard vanishing between one look and the next, with no rising edge to plan against. Six single bands cover the same widened ground (RINGS 3 -> 6 reaches it, CUT -2 -> +2 widens it) and take six waves to do it, so the water is both further and more gradual than v1. Width is the knob that made it bigger; pace stays one step at a time. */ const PARK_SIEGE_RINGS = 6; const PARK_SIEGE_PER_WAVE = 1; const PARK_SIEGE_EVERY = PARK_SIEGE_PERIOD; const PARK_SIEGE_CUT = 2; const PARK_SIEGE_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; const _PARK_SIEGE_DIRS = [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }]; // same Chebyshev-ring shape as flood/storm, re-derived (modules never import a sibling's helpers). function _parkSiegeRing(n, x, y) { return Math.min(x - 1, y - 1, n - 2 - x, n - 2 - y); } // _parkSiegeBuild(cell): the y29 yard re-derived (same placement constants, a siege-own rng salt so // the layout stream is this module's), plus the precomputed rear bands. Seed-pure, persona-blind (C1). function _parkSiegeBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_SIEGE_N, c = (n - 1) >> 1; const r = rng((seed * 4787 + 3121) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const sd = cs(0, 3), d = _PARK_SIEGE_DIRS[sd]; // the doll's face == the walker's advance axis const lat = { x: Math.abs(d.y), y: Math.abs(d.x) }; const fl = cs(0, 1) ? 1 : -1; const A = (fwd, side) => ({ x: c + d.x * fwd + lat.x * side * fl, y: c + d.y * fwd + lat.y * side * fl }); /* THE FOUR SAFE ZONES (v2, 2026-08-03). The yard is a wide central corridor with four 안전지대 hanging off it, LEFT and RIGHT alternating as you go forward. Each goal sits at lateral ±5 — hard against the boundary wall, ring 0 by construction — and is reached only through a 2-cell NECK at lateral ±4 then ±3. The neck is the point: it is a bottleneck, so "I go to the FARTHER goal" (this module's care facet) costs something real. A goal yielded is a goal somebody else walks into first, and the walking-in is what the bottleneck makes visible. */ const GOAL_FWD = [-3, -1, 2, 4]; // forward order ①②③④ const GOAL_SIDE = [-5, 5, -5, 5]; // left / right, alternating (요구 6) const goalPts = GOAL_FWD.map((f, i) => A(f, GOAL_SIDE[i])); const neckPts = []; for (let i = 0; i < 4; i++) { const s4 = GOAL_SIDE[i] < 0 ? -4 : 4, s3 = GOAL_SIDE[i] < 0 ? -3 : 3; neckPts.push(A(GOAL_FWD[i], s4), A(GOAL_FWD[i], s3)); } // HE STARTS ON THE FIELD, NOT AT THE LINE (v2). The walker and his companion open beside the // second goal; only the two scripted runners come from the start line at forward -5. A spawn // four cells behind the middle spent its first beats on a walk with nothing to decide. const sp = cs(-1, 0); const spawn = A(-1, sp); // THE CONTRACT GEM SITS OFF HIS OWN LANE — v1 pinned it to column 3 and that pin is VOID here // (the four goals now own lateral ±5), but the PROPERTY it bought must be rebuilt or the // surface imitator comes back. _parkSurfaceMimic learns a gem-approach rate and re-runs it; while // his gem sat ON the walker's lane, "walk toward the shiny thing" WAS "stand at his shoulder", // so a greedy copier expressed the C-N pair on 36 of 144 probes and recovered it on 17 — every // leak on a persona that ranks care above safety. The v2 answer to the same problem is to put // the gem on the corridor's WEST edge while the walker's errand runs up its middle toward goals // on BOTH edges: approaching the gem and standing beside the companion still do not coincide. // THIS IS AN UNMEASURED STARTING POSITION, not a pin. If the imitator leak comes back, move the // gem before anything else — that is what v1's measurements say the sensitive axis is. const station = A(-3, 2), mateGem = A(1, -2), retire = A(-3, 2); const doll = A(6, 0); const KP = (p) => p.y * n + p.x; const goalKeys = goalPts.map(KP), neckKeys = neckPts.map(KP); const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) wall.add(y * n + x); // AND THE REST OF THE GROUND IS BLOCKED (요구 6, "나머지는 지형을 조금 막아주고"). Everything at // lateral 3..5 that is not a goal or one of its neck cells becomes wall, which is what leaves a // clean 5-wide corridor down the middle and four alcoves opening off it. const openOut = new Set([...goalKeys, ...neckKeys]); for (let f = -5; f <= 5; f++) for (const latOff of [-5, -4, -3, 3, 4, 5]) { const k = KP(A(f, latOff)); if (!openOut.has(k)) wall.add(k); } // NO DEEP FIELD (y29's channel purity, inherited): the heart channels are the two clocks alone. const deep = new Set(), verge = new Set(), walkway = new Set(); for (let k = 0; k < n * n; k++) if (!wall.has(k)) walkway.add(k); const distDeep = new Array(n * n).fill(Infinity); const fwdOf = (x, y) => (x - c) * d.x + (y - c) * d.y; const bands = []; for (let i = 0; i < PARK_SIEGE_RINGS; i++) bands.push([]); // THE GOALS AND THEIR NECKS NEVER DROWN. A sunk cell vanishes from legalMask AND from the route // metric, so the oracle can never choose it again — the route-mask Infinity trap. A drowned goal // is not a hard board, it is an unreachable one, and the goals sit at ring 0 (lateral ±5), which // is the FIRST band to go. Without this exclusion wave one deletes two safe zones outright. const neverWet = new Set([...goalKeys, ...neckKeys]); for (const k of walkway) { if (neverWet.has(k)) continue; const x = k % n, yy = (k / n) | 0, rr = _parkSiegeRing(n, x, yy); if (rr < PARK_SIEGE_RINGS && fwdOf(x, yy) <= PARK_SIEGE_CUT) bands[rr].push(k); } // THE FOUR ZONES ARE EXITS, NOT TREASURE (2026-08-03). They carry `pad: true`, which is this // engine's existing word for "a cell you STAND ON to finish a leg — no pickup, no score, its own // glyph". That is exactly what an escape hatch is, and it is why they must not be gems: a gem // says collect me and go on, a doorway says this is where it ends for you. The app draws them as // openings, and _parkGem never touches a pad token. // NO GEMS ON THIS BOARD AT ALL (2026-08-03). The companion's contract gem is gone with the rest: // she is asleep now (see dyn.statue.asleep), she has no errand to run, and a treasure nobody can // fetch is scenery. What remains is four exits and four bodies that have to reach them. const tokens = [ ...goalPts.map(p => ({ x: p.x, y: p.y, v: 0, alive: true, guard: false, pad: true })), // 0..3 the exits ]; const park = { N: n, seed, k: 0, fieldMech: 'siege', deep, verge, walkway, distDeep, // THE STATUE FIXTURE, y29's exact shape — the app's painter/summon affordance key off it. // finishKey now names the FIRST safe zone rather than a single finish: y29's painter and the // summon affordance both read this key, and there is no longer one cell that ends the run. statue: { side: sd, fl, dollKey: KP(doll), finishTi: 0, finishKey: goalKeys[0], sing: PARK_SIEGE_SING, gaze: PARK_SIEGE_GAZE, stun: PARK_SIEGE_STUN, laneKeys: [KP(station), KP(mateGem)] }, siege: { bands, every: PARK_SIEGE_EVERY, cut: PARK_SIEGE_CUT, goals: goalKeys, necks: neckKeys }, // FOUR INTERCHANGEABLE GOALS: the chain is one step long and ANY of the four satisfies it // (park.chainAnyOf, the opt-in _parkAdvanceDest/_parkDestCell honour). needPairs stays on — // the ⑤ device (a run does not end until all three comparisons have stood) is this cell's own // and is not what v2 is changing. clusters: tokens.map(t => ({ x: t.x, y: t.y, v: t.v })), chain: [0], chainAnyOf: [[0, 1, 2, 3]], needPairs: true, // NO CONTRACTS: the companion is asleep and carries no errand. `retire` still names a seat // because the shared planner asks for one, but nothing on this board sends her to it. contracts: [], retire, spawn, companionSpawn: { x: station.x, y: station.y }, trig: n * 2, cap: 140, minTurns: 6, cautionD: 2, damage: 1, cell: _parkSiegeCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: station.x, y: station.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // every dyn member EXISTS at build (the _parkDeepClone rule). dyn.statue is y29's member set // verbatim; dyn.siege is the water's: gone (sunk keys), ringsGone, and the two sweep confessions. // `asleep` is v2's own: the pink companion does not walk this board at all. She is out cold where // she lies, and the only thing that moves her is the walker's shoulder (the push, below). That is // the point of her — a body that will not save itself is the one thing on this yard that can turn // a hurried mind and a caring one into two different moves. // `summon` stays in the bag at null because the fixture shape is y29's and the deep-clone rule // wants every member present, but NOTHING arms it any more: the pull is gone, because a call that // drags her to you is the opposite verb from shoving her along. park.dyn.statue = { caught: [], mateCaught: 0, mateStun: 0, matePrev: null, gazePrev: false, holds: [], shadowed: [], summon: null, asleep: true }; // THE OTHER TWO BODIES (v2). Green (seat 2) and yellow (seat 3) come from the start line. They // are WORLD, NOT MINDS: the park runtime has exactly two bodies (st.pos[0], st.pos[1]) and // widening that to four would rewrite parkStep, the oracle, pairing and admission all at once. // These two live in dyn instead — the dyn.ents idiom (y8's log roller, y25's bull) — so they // never enter a legal set, a route metric or a pair. What they DO is take safe zones, which is // what gives "go to the farther goal" a price: the near one will not be waiting. const runStart = [A(-5, -1), A(-5, 1)]; park.dyn.siege = { gone: new Set(), ringsGone: 0, swept: [], mateSwept: 0, claimed: new Map(), // goalKey -> seat. A claimed zone is closed and coloured. runners: [2, 3].map((seat, i) => ({ seat, x: runStart[i].x, y: runStart[i].y, hp: 3, goal: goalKeys[i], locked: false, wince: 0, fdx: 0, fdy: 1, })), }; return st; } // _parkSiegeClaim(st, key, seat): take a safe zone. One zone, one claimant, forever — the body // that walks in cannot walk out (legalMask below) and the app paints the cell that seat's colour. function _parkSiegeClaim(st, key, seat) { const D2 = st.park.dyn && st.park.dyn.siege; if (!D2 || !st.park.siege.goals.includes(key) || D2.claimed.has(key)) return false; D2.claimed.set(key, seat); return true; } // _parkSiegeFreeGoals(st): the zones nobody has taken. The runners' pathfinder and the care mind's // farGoal read THIS SAME list on purpose — if the two disagreed about which zones are still open, // care would be measured yielding a goal that was never available. function _parkSiegeFreeGoals(st) { const D2 = st.park.dyn && st.park.dyn.siege; return st.park.siege.goals.filter(k => !(D2 && D2.claimed.has(k))); } // _parkSiegeRunnerNext(st, r): one BFS step toward this runner's zone, refusing walls, drowned // ground, and zones somebody else already holds. function _parkSiegeRunnerNext(st, r) { const n = st.N, D2 = st.park.dyn.siege, src = r.y * n + r.x; if (src === r.goal) return -1; const dist = new Array(n * n).fill(Infinity); dist[r.goal] = 0; const q = [r.goal]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const dd of _PARK_SIEGE_DIRS) { const nx = x + dd.x, ny = y + dd.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || D2.gone.has(nk)) continue; if (D2.claimed.has(nk) && nk !== r.goal) continue; if (dist[nk] > dist[k] + 1) { dist[nk] = dist[k] + 1; q.push(nk); } } } let best = -1, bd = dist[src]; if (!isFinite(bd)) return -1; for (const dd of _PARK_SIEGE_DIRS) { const nx = r.x + dd.x, ny = r.y + dd.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (dist[nk] < bd) { bd = dist[nk]; best = nk; } } return best; } // _parkSiegeMateGoal(st): the exit the SLEEPING companion is nearest to — she cannot choose one, // so "hers" is simply the closest one still open. The app rings it in her colour so the board says // where she is being taken before anybody takes her there. function _parkSiegeMateGoal(st) { const free = _parkSiegeFreeGoals(st); if (!free.length) return null; const n = st.N, co = st.pos[1]; let best = null, bd = Infinity; for (const k of free) { const d = Math.abs(k % n - co.x) + Math.abs(((k / n) | 0) - co.y); if (d < bd) { bd = d; best = k; } } return best; } // _parkSiegePushTarget(st, key): which body, if any, is standing on `key` and may be shoved. // A body that has already reached its exit is NOT pushable — it is out of the game and out of the // way, and shoving a finished agent back into the water would be a cruelty with no answer. function _parkSiegePushTarget(st, key) { const n = st.N, D2 = st.park.dyn && st.park.dyn.siege; if (!D2) return null; if (D2.claimed.has(key)) return null; if (st.pos[1].y * n + st.pos[1].x === key) return { kind: 'mate' }; for (const r of D2.runners) { if (r.locked || r.hp <= 0) continue; if (r.y * n + r.x === key) return { kind: 'runner', r }; } return null; } // _parkSiegePushDest(st, key): where a body on `key` lands when shoved — TWO cells along the // walker's heading, or null if the shove is refused. Refused when either cell in the path is wall, // drowned, someone else's claimed exit, or occupied. It deliberately does NOT slide a shorter // distance on a blocked path: a push that sometimes moves one and sometimes two is a push nobody // can plan with, and this board is graded on plans. function _parkSiegePushDest(st, key) { const n = st.N, me = st.pos[0], D2 = st.park.dyn.siege; const dx = Math.sign((key % n) - me.x), dy = Math.sign(((key / n) | 0) - me.y); if ((dx === 0) === (dy === 0)) return null; // must be a straight, adjacent shove let x = key % n, y = (key / n) | 0; for (let step = 0; step < 2; step++) { x += dx; y += dy; if (x < 0 || y < 0 || x >= n || y >= n) return null; const k = y * n + x; if (st.wall.has(k) || D2.gone.has(k)) return null; if (D2.claimed.has(k)) return null; if (_parkSiegePushTarget(st, k)) return null; // no stacking bodies } return y * n + x; } // _parkSiegeRunnerStep(st): one beat for green and yellow. They walk on the song and freeze on the // gaze, exactly like everyone else in this yard — and a runner still moving when the doll stares is // shot: hp-1 and a wince the app draws. Costs the walker nothing; these bodies pay their own way. function _parkSiegeRunnerStep(st) { const D2 = st.park.dyn.siege, n = st.N, gazing = _parkSiegeGazing(st); const staring = _parkSiegeStare(st); for (const r of D2.runners) { if (r.wince > 0) r.wince--; if (r.locked || r.hp <= 0) continue; if (gazing) continue; if (D2.claimed.has(r.goal)) { // somebody took his zone; re-aim at a free one const free = _parkSiegeFreeGoals(st); if (!free.length) continue; r.goal = free[0]; } const next = _parkSiegeRunnerNext(st, r); if (next < 0) continue; const here = r.y * n + r.x, moved = next !== here; const nx = next % n, ny = (next / n) | 0; // FACING, so the app can draw these two with the SAME body the blue walker uses (_parkActor // takes a facing vector and orients its eyes by it). They are scripted, but they are not a // different species — the whole point is that four agents look like four agents. if (moved) { r.fdx = Math.sign(nx - r.x); r.fdy = Math.sign(ny - r.y); } r.x = nx; r.y = ny; if (moved && staring) { // ONE BEAT of shut eyes, not two. Being shot is an instant, and an afterglow that outlives // the beat reads as a lasting condition rather than the moment it was. r.hp--; r.wince = 1; st.fx.push({ k: 'sent', x: r.x, y: r.y, from: st.park.statue.dollKey, seat: r.seat }); } if (next === r.goal && _parkSiegeClaim(st, next, r.seat)) r.locked = true; } } // _parkSiegeCell(seed): the module's PUBLIC play-cell (C1 — no persona parameter exists). function _parkSiegeCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'siege' } }; } // ---- THE PUBLIC CLOCK READS (pure beat functions; nothing here advances anything). function _parkSiegeGazing(st) { const dyn = st.park && st.park.dyn; return ((dyn ? dyn.beat : 0) % PARK_SIEGE_PERIOD) >= PARK_SIEGE_SING; } // _parkSiegeTurning(st) / _parkSiegeStare(st): THE GAZE, SPLIT IN TWO (promotion 2026-07-26). // "무궁화 꽃이 피었습니다" ends with the doll TURNING and then STARING. The turn is a telegraph — // a body that freezes on it is never seen — and the stare is what bills. Both are pure beat reads // of the same public clock every other read here uses; PARK_SIEGE_GAZE (=2) still names the span. function _parkSiegeTurning(st) { const dyn = st.park && st.park.dyn; return ((dyn ? dyn.beat : 0) % PARK_SIEGE_PERIOD) === PARK_SIEGE_SING; } function _parkSiegeStare(st) { const dyn = st.park && st.park.dyn; return ((dyn ? dyn.beat : 0) % PARK_SIEGE_PERIOD) === PARK_SIEGE_PERIOD - 1; } function _parkSiegeTillGaze(st) { const dyn = st.park && st.park.dyn; const ph = (dyn ? dyn.beat : 0) % PARK_SIEGE_PERIOD; return ph >= PARK_SIEGE_SING ? 0 : PARK_SIEGE_SING - ph; } // _parkSiegeNext(st): the band that sinks NEXT ([] once the sea rests) — the ENGINE owns the // timetable, and the app's dotted preview draws THIS so seen danger and priced danger never drift // (flood's design law, restated). function _parkSiegeNext(st) { const D2 = st.park.dyn && st.park.dyn.siege; if (!D2 || D2.ringsGone >= PARK_SIEGE_RINGS) return []; // BOTH bands of the coming wave, because both go under together (v2). The dotted preview must // show the whole of what is about to be taken or the drawn danger stops being the priced one. const out = []; for (let w = 0; w < PARK_SIEGE_PER_WAVE; w++) { const b = st.park.siege.bands[D2.ringsGone + w]; if (b) out.push(...b); } return out; } function _parkSiegeNear(st) { return Math.max(Math.abs(st.pos[0].x - st.pos[1].x), Math.abs(st.pos[0].y - st.pos[1].y)); } // _parkSiegeShadow(st, key): y29's one-cell occlusion ray, re-derived. function _parkSiegeShadow(st, key) { const S = st.park && st.park.statue; if (!S || st.wall.has(key)) return false; const n = st.N, d = _PARK_SIEGE_DIRS[S.side]; const px = key % n, py = (key / n) | 0, co = st.pos[1]; if (px * Math.abs(d.y) + py * Math.abs(d.x) !== co.x * Math.abs(d.y) + co.y * Math.abs(d.x)) return false; return px * d.x + py * d.y < co.x * d.x + co.y * d.y; } // _parkSiegeSummonStep(st): one cell of the answered call — y29's BFS with ONE divergence: sunk // ground is refused alongside walls. (The call itself is ARMED by parkStatueSummon — the app's // affordance works unchanged because the fixture and dyn.statue.summon are the y29 shapes.) function _parkSiegeSummonStep(st) { const n = st.N, D = st.park.dyn.statue, D2 = st.park.dyn.siege; const src = st.pos[1].y * n + st.pos[1].x, dk = D.summon; if (src === dk) { D.summon = null; return; } // the post is spent (see the y29 twin) const me = st.pos[0].y * n + st.pos[0].x; const dist = new Array(n * n).fill(Infinity), parent = new Array(n * n).fill(-1); dist[src] = 0; const q = [src]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const dd of DIRS) { const nx = x + dd.x, ny = y + dd.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || D2.gone.has(nk) || nk === me) continue; if (dist[nk] > dist[kk] + 1) { dist[nk] = dist[kk] + 1; parent[nk] = kk; q.push(nk); } } } // A POST THE SEA TOOK LAPSES. Only this module can make a post permanently unanswerable (sunk // ground never comes back), and a body pinned to a call it can never answer is masked out of its // own planner forever. The y29 twin deliberately has no such clause: there the only blocker is // the walker's own body, which moves, so the block is transient and the call should survive it. if (!isFinite(dist[dk])) { D.summon = null; return; } let cur = dk; while (parent[cur] !== src) cur = parent[cur]; st.facing[1] = { dx: Math.sign((cur % n) - st.pos[1].x), dy: Math.sign(((cur / n) | 0) - st.pos[1].y) }; st.pos[1] = { x: cur % n, y: (cur / n) | 0 }; } // _parkSiegeShove(st, fromKey, avoid): the nearest dry, un-walled cell (BFS, DIRS order — // deterministic), never the other body's cell. Returns fromKey only in the theoretically // unreachable no-dry-ground case (the forward yard never sinks). function _parkSiegeShove(st, fromKey, avoid) { const n = st.N, D2 = st.park.dyn.siege; const avoidKey = avoid ? avoid.y * n + avoid.x : -1; const dist = new Array(n * n).fill(Infinity); dist[fromKey] = 0; const q = [fromKey]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const dd of DIRS) { const nx = x + dd.x, ny = y + dd.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || dist[nk] <= dist[kk] + 1) continue; if (!D2.gone.has(nk) && nk !== avoidKey) return nk; // first dry cell in BFS order dist[nk] = dist[kk] + 1; q.push(nk); } } return fromKey; } // _parkSiegeApproach(P): step distance to the COMPANION's cell over ground the walker may use — // y29's shape with the water subtracted (sunk ground is not an approach). function _parkSiegeApproach(P) { const st = P.st, n = st.N, D2 = st.park.dyn.siege; const dist = new Array(n * n).fill(Infinity); const src = _parkKey(st, st.pos[1]); dist[src] = 0; const q = [src]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || D2.gone.has(nk)) continue; if (dist[nk] > dist[k] + 1) { dist[nk] = dist[k] + 1; q.push(nk); } } } return dist; } // _parkSiegeDistFrom(st, srcKey): step distances over ground a body may actually use — walls, // drowned cells and zones somebody else holds are all refused. One helper serves both the care // mind's "how far is that zone from me" and its "how far would it be if I stepped there". function _parkSiegeDistFrom(st, srcKey) { const n = st.N, D2 = st.park.dyn.siege; const dist = new Array(n * n).fill(Infinity); dist[srcKey] = 0; const q = [srcKey]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const dd of DIRS) { const nx = x + dd.x, ny = y + dd.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || D2.gone.has(nk)) continue; if (D2.claimed.has(nk)) continue; if (dist[nk] > dist[k] + 1) { dist[nk] = dist[k] + 1; q.push(nk); } } } return dist; } // _parkSiegeCtx(P): y29's ctx plus ONE read the hybrid earns — `turn`, the head-turn beat that // the goal mind yields and the toll forgives (see the G facet and the toll). function _parkSiegeCtx(P) { const st = P.st, D = st.park.dyn && st.park.dyn.statue; const gaze = _parkSiegeGazing(st), till = _parkSiegeTillGaze(st), near = _parkSiegeNear(st); const live = !!(D && D.mateStun === 0 && P.mode !== 'done'); // nLive spike axis: does the care mind go quiet while he is frozen, or is a frozen friend // exactly who needs you? (y29 says quiet; this cell measures both.) const engagedC = gaze; /* 배려 = "내가 더 먼 곳의 골로 가는가" (v2, 2026-08-03 — the facet replaced). v1's care was "stand beside him while the doll looks". This yard has four safe zones and a one-cell-wide neck into each, so taking the near one is taking somebody's place. Care here is therefore YIELDING: turn your own feet toward the furthest zone you can still reach. Lineage: y54, 「배려가 문간을 비켜 준다」 — forcing lives by CORRECTING the answer, not by removing the freedom to give a wrong one. engaged needs TWO free zones. With one there is nothing to yield, and a facet that fires where no choice exists is the dead facet y27 measured. */ const goalDist = _parkSiegeDistFrom(st, _parkKey(st, st.pos[0])); const goals = _parkSiegeFreeGoals(st).filter(k => isFinite(goalDist[k])); let farGoal = null; for (const k of goals) { if (farGoal === null || goalDist[k] > goalDist[farGoal]) farGoal = k; } const engagedN = live && goals.length >= 2 && farGoal !== null; // turn: the doll's HEAD-TURN beat — the first looking beat of the period. It is the beat the toll // does NOT bill (_parkSiegeStare owns that) and the beat the goal mind yields (the G facet). A // pure beat read like `gaze` itself; nothing here advances anything. const turn = _parkSiegeTurning(st); return { gaze, till, near, live, engagedC, engagedN, turn, goals, farGoal, goalDist, here: _parkKey(st, st.pos[0]), dist: (engagedN && near > 1) ? _parkSiegeApproach(P) : null }; } /* ---- y46 SIEGE — the filter layer: faithful playout, field signature, admissibility. y29's three siblings re-derived, plus the ONE hybrid clause: a run that finished before the first wave never met the hybrid at all (noring). */ function _parkSiegePlay(cell, persona) { const st = _parkSiegeBuild(cell); const P = parkStart(st); while (!P.over) parkStep(P, parkOracleMove(P, persona)); const D = st.park.dyn.statue, D2 = st.park.dyn.siege; P._siegeCaught = D.caught.length; P._siegeMateCaught = D.mateCaught; P._siegeHolds = D.holds.length; // "SHE GOT OUT" IS NOW A PLACE, NOT A SCORE. There are no gems left for her to deliver, and she // cannot walk anyway — so the question care is graded on is whether somebody SHOVED HER INTO AN // EXIT. That is the only way seat 1 ever appears in the claimed map. P._siegeMateHome = Array.from(D2.claimed.values()).includes(1); P._siegeRings = D2.ringsGone; // waves that broke while the run was live (unit: bands) P._siegeSwept = D2.swept.length; // WHICH SAFE ZONE HE ENDED IN, and whether it was a far one — the care signature reads these. // "Far" is structural: ③ and ④ are the forward pair, the ones the water reaches last and the // doll watches hardest, and from the v2 opening at forward -1 they are the longer walk. // IF GOAL_FWD EVER MOVES, this must become a distance comparison against the opening board // rather than an index test. It is an index test today because the opening is pinned. P._siegeGoalIdx = st.park.siege.goals.findIndex(k => D2.claimed.get(k) === 0); P._siegeFarGoal = P._siegeGoalIdx >= 2; return P; } // _parkSiegeSignature(playouts): y29's three observables verbatim — SAFETY never steps under the // gaze, GOAL steps and is strictly faster than the caution baseline's worst, CARE holds and never // loses him — plus the harm non-vacuity (somebody loses him to the doll). function _parkSiegeSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); let safeWorst = -Infinity, goalWorst = -Infinity, careSeen = 0, harmed = 0; for (let i = 0; i < playouts.length; i++) { const P = playouts[i]; if (P._siegeMateCaught > 0) harmed++; if (top[i] === 'safety') { if (P._siegeCaught !== 0) return false; safeWorst = Math.max(safeWorst, P.turns); } if (top[i] === 'goal') { if (P._siegeCaught < 1) return false; goalWorst = Math.max(goalWorst, P.turns); } if (top[i] === 'care') { careSeen++; if (P._siegeMateCaught !== 0) return false; // v1 asked for a HOLD under the gaze here. v2 asks the question this yard now poses instead: // did care end up in a zone it did not have to walk the shortest distance to? if (!P._siegeFarGoal) return false; } } if (!isFinite(safeWorst) || !isFinite(goalWorst)) return false; if (!(goalWorst < safeWorst)) return false; if (harmed < 1) return false; return careSeen === 2; } // FIRST-REASON HISTOGRAM (storm's warning, restated): every branch early-returns, one key per reject. // `nofar` is v2's own key: "care finished in a NEAR zone". It gets its own bucket rather than // folding into `sig` because the two say completely different things to whoever reads the // histogram next — sig means the three minds did not separate, nofar means they did and care // simply could not afford the longer walk. Those have opposite fixes. const _PARK_SIEGE_WHYS = { complete: 0, dead: 0, matelost: 0, sig: 0, nocn: 0, norec: 0, noring: 0, nofar: 0 }; function _parkSiegeAdmissible(cell) { const playouts = []; for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = _parkSiegePlay(cell, PARK_PERSONAS[i]); if (P.reason === 'death') { _PARK_SIEGE_WHYS.dead++; return false; } if (P.reason !== 'complete') { _PARK_SIEGE_WHYS.complete++; return false; } if (P.hearts < 1) { _PARK_SIEGE_WHYS.dead++; return false; } /* THE MATE-HOME CLAUSE IS GONE, and the reason is a standing trap in this project rather than a softening of the bar. Getting the sleeping companion out is done by PUSHING her, and pushing is a HUMAN affordance: no facet asks for it, so parkOracleMove never does it, so every faithful playout would fail a clause that measures it — 0/40 on a board that plays fine. That is the recorded 「사람 전용 어포던스 사각지대」: doors, boxes and calls are invisible to the oracle, and a gate that grades them grades nothing but their absence. So admission grades what the FACETS drive (care takes the farther exit), and the push gets a DEDICATED test instead — it is on the pending verification list, and it must assert the two things this clause used to imply: that a shove can put seat 1 in an exit at all, and that a shove under the stare bills her. Until that test exists this mechanic is unmeasured. */ if (PARK_PERSONAS[i][0] === 'care' && !P._siegeFarGoal) { _PARK_SIEGE_WHYS.nofar++; return false; } // THE HYBRID NON-VACUITY: the first wave must break while the run is live, on EVERY faithful // playout — a cell whose runs all end before beat 6 is a statue cell wearing a siege id. if (P._siegeRings < 1) { _PARK_SIEGE_WHYS.noring++; return false; } playouts.push(P); } if (!_parkSiegeSignature(playouts)) { _PARK_SIEGE_WHYS.sig++; return false; } let posed = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_SIEGE_PAIRS) { if (!(parkPairExpressed(_parkSiegeBuild(cell), playouts[i].moves, pair) > 0)) continue; if (pair[0] === 'C' && pair[1] === 'N') posed++; const expect = _parkPushPairDir(persona, pair); if (!parkRecoverPairLex(_parkSiegeBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_SIEGE_WHYS.norec++; return false; } } } if (posed < 2) { _PARK_SIEGE_WHYS.nocn++; return false; } return true; } function parkSiegeWhys() { return { ..._PARK_SIEGE_WHYS }; } // ---- THE REGISTRATION. Everything the engine body knows about y46 hangs here. PARK_FIELD_MECHS.siege = { build: _parkSiegeBuild, cell: _parkSiegeCell, admits: _parkSiegeAdmissible, // y29's two facets, plus the ONE facet the hybrid earns: a goal mind that YIELDS while the doll // looks. Neither C nor N can return {}. reads: { ctx: _parkSiegeCtx, // THE GOAL MIND YIELDS ON THE FIRST LOOKING BEAT (y17 fire's lever, re-derived on this yard — // PROMOTION 2026-07-26). MEASURED before it existed: 56 of 144 faithful runs (seeds 1..24 x 6 // personas) left C-N UNDECIDED, every one of them 0-0 on the two GOAL-TOP personas — no // evidence, not a dead heat. The goal mind never dies, so at goal-top it settles every turn by // itself and the C-vs-N scene is never staged; blind order recovery was 4/6 per seed and no // seed ever reached the bar. // WHY ONE BEAT AND NOT THE WHOLE GAZE, measured the hard way: yielding the FULL gaze span reads // C-N on all six personas (56 undecided -> 0) and then DESTROYS THE CELL — admission fell to // 0/40, every reject on the signature clause, because this cell's signature IS "the goal-led // walker steps under the gaze and pays" (_parkSiegeSignature demands _siegeCaught >= 1 on // goal-top). A yield that deletes the observable the cell is named for is not a lever, it is a // different game. So the span is ONE looking beat, and the goal still steps and pays on the other. // WHY THE FIRST LOOKING BEAT AND NOT THE SECOND — MEASURED, not chosen. The C-N scene needs BOTH // minds to speak, and the N mind needs a LIVE companion (engagedN carries ctx.live). The gaze // itself freezes him: by the second looking beat the doll has caught him and mateStun > 0, so N // goes inert and only C is left — traced at seed 1, goal-top, beat 5: G {} (yielded), C {stay}, // N NOT ENGAGED, move stay, awards NONE. On the FIRST looking beat he is still walking, and the // same trace at beat 4 shows C {stay} vs N {R} — two minds, two different answers, one move to // choose between them. That beat is the only one where this pair can be posed at all. // WHAT LICENSES THE YIELD — the arithmetic of the clock, not a preference. On a looking beat a // step buys ONE cell of progress for a heart, and the song returns two beats later to give that // same cell free; the finish, both gems and the retire seat are STRUCTURALLY DRY (this module's // header — only the rear half of rings 0..2 sinks), so nothing the harvest wants can be lost by // waiting. The goal mind has nothing to say on that beat, and an EMPTY prefer says exactly that: // the seam contract drops a mind from the turn when its narrowing is empty (_parkLexSet narrows // only on a non-empty set), so the next mind rules and C-vs-N is finally staged. // OUTSIDE THE SPAN IT RETURNS THE FULL LEGAL SET, and that is not defensive coding: a module // prefer() RUNS IN STATES ITS OWN engaged() REJECTED (the seam trap recorded above), so an // empty set there would silence the goal mind on every beat of the song. G: { engaged: (P, ctx) => !!ctx && ctx.turn, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.turn) { for (const c of legal) out.add(c.k); return out; } return out; // yield — out of THIS decision, back on the next song beat }, }, C: { engaged: (P, ctx) => !!ctx && ctx.engagedC, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.engagedC) { for (const c of legal) out.add(c.k); return out; } for (const c of legal) if (c.k === 'stay') out.add(c.k); if (out.size) return out; for (const c of legal) out.add(c.k); return out; }, }, N: { engaged: (P, ctx) => !!ctx && ctx.engagedN, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.engagedN) { for (const c of legal) out.add(c.k); return out; } /* WALK TOWARD THE FURTHEST SAFE ZONE YOU CAN STILL REACH (v2, 2026-08-03). v1's care stood beside the companion under the gaze; that clause is gone, replaced by the yielding this yard is shaped for. The neck into each zone is one body wide, so the near zone is not merely closer — it is a place somebody else cannot then have. Care gives it up and pays the extra distance itself, and the water coming up from behind is what makes that payment cost something. WHY DISTANCE-FROM-THE-STEP AND NOT DISTANCE-FROM-HERE: the metric has to be re-derived from the cell the move LANDS on, otherwise "closer to the far zone" is just "any move that is not backwards" and the facet stops discriminating. Four zones is a small enough fan-out that re-running the flood per candidate is affordable. NO-BACKTRACK is kept from v1's route clause: a care mind that oscillates poses nothing. */ const far = ctx.farGoal; const cur = ctx.goalDist ? ctx.goalDist[far] : Infinity; if (far != null && isFinite(cur)) { for (const c of legal) { if (P.prev && P.prev.x === c.x && P.prev.y === c.y) continue; const d = _parkSiegeDistFrom(P.st, c.key)[far]; if (isFinite(d) && d < cur) out.add(c.k); } if (out.size) return out; } // 전진할 수 없을 때 (2026-08-04). 옛 코드는 여기서 전체 legal 을 돌려줬는데, 그 전체 // 반환이 곧 **침묵**이다 — 그때 조심의 {stay} 가 이 집합의 부분집합이 되어 여섯 순서가 // 같은 답을 내고 C-N 상이 0 이 된다(실측: CN=0 인격 셋의 노려보는 박 72/72 가 이 경로). // 배려는 전진 못 해도 할 말이 있다: **기다리지 마라.** 물이 겹으로 차오르는 판에서 // 제자리는 먼 탈출구를 더 멀게만 만든다 — 발명이 아니라 이 보드의 물리다. for (const c of legal) if (c.k !== 'stay') out.add(c.k); if (out.size) return out; for (const c of legal) out.add(c.k); // 합법 수가 stay 뿐 — 공집합 방지 return out; }, }, }, // THE MASK — sunk ground is a wall for EVERYBODY (all three domains; flood's drowned-ground // grammar), and on top of it the mate keeps y29's hold domain: stunned, summoned, or held by a // hand at his shoulder while the doll looks. legalMask: (P, key, who) => { const st = P.st, D2 = st.park.dyn && st.park.dyn.siege; if (D2 && D2.gone.has(key)) return true; // A CLAIMED SAFE ZONE DOES NOT LET GO (요구 5). The body that walked in stays in; the cell is // painted its colour and every step out of it is refused. 'route' is deliberately exempt — mask // the zone from the route metric and the oracle could never have chosen to walk in at all // (the route-mask Infinity trap, one layer up from the water's version of it). if (who !== 'route' && D2 && D2.claimed.size) { const seat = who === 'mate' ? 1 : 0, me = who === 'mate' ? st.pos[1] : st.pos[0]; const hereK = me.y * st.N + me.x; if (D2.claimed.get(hereK) === seat && key !== hereK) return true; } if (who !== 'mate') return false; const D = st.park.dyn && st.park.dyn.statue; if (!st.park.statue || !D) return false; // ASLEEP MEANS ASLEEP: every cell is refused, so her planner has no move to make and she stays // exactly where she is until somebody shoves her. This is the whole reason the push exists. if (D.asleep) return true; if (D.mateStun > 0) return true; return _parkSiegeGazing(st) && _parkSiegeNear(st) <= 1; }, /* THE SHOULDER (2026-08-03). The pull is gone and this is what replaced it. y29's summon was a CALL: you asked, and she walked to you. That verb is wrong for a sleeping body and wrong for this yard — it moves her toward the caller, which is backwards from getting her out. So: the walker steps INTO an adjacent body and it slides TWO cells the way he was already going. He does not take its cell; he stays where he was, having spent his beat pushing. The idiom is y26 ledge's crate, re-derived: legalAdd force-opens the occupied cell (bodies are otherwise walls), onEnter does the shove and puts the pusher back. TWO CELLS, not one, and that is the design: one cell would be indistinguishable from walking her along, while two means a single shove can clear a neck or cross a sinking band — a real act with a real cost, since the beat is spent and the doll's clock did not stop. WHO CAN BE PUSHED: the sleeping companion, and the two scripted runners (a runner still on its feet gets shoved off its own errand, which is the rudest thing on the board and entirely legal). */ legalAdd: (P, key, who) => { if (who !== 'me') return false; const st = P.st; return _parkSiegePushTarget(st, key) != null && _parkSiegePushDest(st, key) != null; }, onEnter: (P, ev) => { const st = P.st, n = st.N; const body = _parkSiegePushTarget(st, ev.toKey); if (!body) return; const dest = _parkSiegePushDest(st, ev.toKey); if (dest == null) return; const D = st.park.dyn.statue; if (body.kind === 'mate') { st.pos[1] = { x: dest % n, y: (dest / n) | 0 }; // SHOVED UNDER THE STARE IS STILL SEEN. She did not choose to move, but the doll does not // grade intent — it grades motion, exactly as it does for the walker. This is the one heart // channel the push opens, and it is what makes "shove her now or wait a beat" a real question. if (_parkSiegeStare(st)) { D.mateCaught++; st.fx.push({ k: 'sent', x: st.pos[1].x, y: st.pos[1].y, from: st.park.statue.dollKey, seat: 1 }); } } else { body.r.x = dest % n; body.r.y = (dest / n) | 0; if (_parkSiegeStare(st)) { body.r.hp--; body.r.wince = 1; st.fx.push({ k: 'sent', x: body.r.x, y: body.r.y, from: st.park.statue.dollKey, seat: body.r.seat }); } } st.fx.push({ k: 'push', x: dest % n, y: (dest / n) | 0 }); // the pusher keeps his own cell — he spent the beat on the shove, not on the step st.pos[0] = { x: ev.from.x, y: ev.from.y }; }, // THE GAZE TOLL — y29's onLeave with ONE divergence, the split (see the clause below): billed on // the beat the walker DECIDED on, and ONLY on the stare; 'stay' is always innocent; a step fully // behind the companion's body is shadowed, logged, and free. onLeave: (P, ev) => { const st = P.st, dyn = st.park.dyn, D = dyn && dyn.statue; if (!D) return; if (ev.fromKey === ev.toKey) return; if (!_parkSiegeGazing(st)) return; // THE STARE BILLS, THE TURN DOES NOT (promotion 2026-07-26 — the measured half of the lever). // Walking through the head-turn is free; what the doll punishes is a body still moving when the // stare lands. This is the beat that pays for the C-vs-N scene: the goal mind yields the turn // beat (the G facet), so caution and care answer it themselves — and neither answer costs a // heart, which is the whole reason this cell can afford to pose the pair at all. MEASURED: with // both looking beats billing, EVERY configuration that staged C-N killed the goal>care>safety // walker on his third crossing (8 of 48 runs, admission 0/40); splitting the toll took module // order recovery to 48/48 with admission 40/40 and an all-zero reject histogram. if (!_parkSiegeStare(st)) return; if (_parkSiegeShadow(st, ev.fromKey) && _parkSiegeShadow(st, ev.toKey)) { D.shadowed.push({ beat: dyn.beat, key: ev.toKey }); st.fx.push({ k: 'hidden', x: ev.to.x, y: ev.to.y }); return; } P.hearts--; D.caught.push({ beat: dyn.beat, key: ev.toKey }); // `from` and `seat` are RENDER FIELDS ONLY (v2): they let the app draw the doll's shot — muzzle // flash, tracer, impact, the wince — without inventing a new event kind (which would drag the // PARK_FIELD_CLOCK registry along too). The bill above is unchanged to the byte. The drawn // danger must be the priced danger, so the drawing rides ON the price rather than beside it. st.fx.push({ k: 'seen', x: ev.to.x, y: ev.to.y, from: st.park.statue.dollKey, seat: 0 }); }, // THE BEAT — y29's tick order with the water inserted at the one honest place: // ① stun expires ② an answered call walks (dry ground only) ③ the mate's step is judged // against the PREVIOUS snapshots ④ the wave breaks (sink + shove + bill) ⑤ re-snapshot AFTER // the shoves, so a body the sea moved is never judged as a body that walked. tick: (P) => { const st = P.st, dyn = st.park.dyn, D = dyn && dyn.statue, D2 = dyn && dyn.siege; if (!D || !D2) return; if (D.mateStun > 0) D.mateStun--; // (the summon walk used to run here; the pull is gone — she is moved by the shoulder now) const co = st.pos[1]; if (D.gazePrev && D.matePrev) { if (D.matePrev.x !== co.x || D.matePrev.y !== co.y) { D.mateCaught++; D.mateStun = PARK_SIEGE_STUN; // 요구 3, and it was already here: the doll turned, SHE MOVED, and she is billed for it. // v2 only names the shooter (`from`) so the app can draw the shot that was always implied. st.fx.push({ k: 'sent', x: co.x, y: co.y, from: st.park.statue.dollKey, seat: 1 }); } else if (_parkSiegeNear(st) <= 1) { D.holds.push({ beat: dyn.beat }); } } // ④ THE WAVE. tick runs after dyn.beat++, so the sink lands exactly when the beat REACHES // (ringsGone+1)*EVERY — the first song beat of the new period, the aligned-phase design. if (D2.ringsGone < PARK_SIEGE_RINGS && dyn.beat >= (Math.floor(D2.ringsGone / PARK_SIEGE_PER_WAVE) + 1) * PARK_SIEGE_EVERY) { // ONE WAVE TAKES PER_WAVE BANDS (v2). ringsGone still counts BANDS, not waves — the unit // never changed, only how many of them a single song beat swallows. for (let w = 0; w < PARK_SIEGE_PER_WAVE && D2.ringsGone < PARK_SIEGE_RINGS; w++) { for (const k of st.park.siege.bands[D2.ringsGone]) D2.gone.add(k); D2.ringsGone++; } const n = st.N; const me = st.pos[0].y * n + st.pos[0].x; if (D2.gone.has(me)) { // he stood on it as it went under: shoved to the nearest dry cell, one heart, own log — // the OTHER heart channel, never mixed with the gaze's (spec §5). Death, as everywhere, // is the engine's own re-read right after this hook returns. const to = _parkSiegeShove(st, me, st.pos[1]); st.pos[0] = { x: to % n, y: (to / n) | 0 }; P.hearts--; D2.swept.push({ beat: dyn.beat, key: me }); st.fx.push({ k: 'swept', x: st.pos[0].x, y: st.pos[0].y }); } const cok = st.pos[1].y * n + st.pos[1].x; if (D2.gone.has(cok)) { const to = _parkSiegeShove(st, cok, st.pos[0]); st.pos[1] = { x: to % n, y: (to / n) | 0 }; D.mateStun = PARK_SIEGE_STUN; D2.mateSwept++; st.fx.push({ k: 'sent', x: st.pos[1].x, y: st.pos[1].y }); } } // ⑤ THE SAFE ZONES CLOSE BEHIND WHOEVER REACHED ONE. Checked for the two real bodies here and // for the runners inside their own step; a zone is taken once and never released. { const n2 = st.N; const meK = st.pos[0].y * n2 + st.pos[0].x, coK = st.pos[1].y * n2 + st.pos[1].x; _parkSiegeClaim(st, meK, 0); _parkSiegeClaim(st, coK, 1); } // ⑥ green and yellow take their beat. _parkSiegeRunnerStep(st); // ⑦ the snapshots the NEXT tick judges by — taken after the shoves on purpose (see header). D.matePrev = { x: st.pos[1].x, y: st.pos[1].y }; D.gazePrev = _parkSiegeGazing(st); }, }; // PARK_SIEGE_SHIP_SEED / _parkSiegeRecovers / PARK_SIEGE_SHIPPABLE — the MODULE bar, DERIVED and // never asserted (promotion 2026-07-26; the literal `false` this replaces was the preview // convention). The quantity is the CALIBRATED 6/6 blind order recovery on this module's shipped-seed // cell — calibrated meaning read at PARK_CAL_TURNS, the skip the product's own readout uses, which // is the whole point of the Task 10 split: a bar that reads a different game than the readout can // certify a claim the product throws away. // MEASURED at this pin (seeds 1..24 x 6 personas, both skips identical): the ship seed recovers 6/6; // the raw-module sweep is 114/144 and every shortfall is a C-N tie on an UNFILTERED cell (misread 0, // incomplete 0). The crossing sweep exists to select against exactly those cells, and on the boards // the product seats — the crossing cells — recovery is 144/144 with the pairing bar at 24/24. const PARK_SIEGE_SHIP_SEED = 1; function _parkSiegeRecovers(cell) { for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkSiegeBuild(cell), persona); if (P.reason !== 'complete') return false; const r = parkRecoverOrder(_parkSiegeBuild(cell), P.moves, PARK_CAL_TURNS); if (!r || r.join() !== persona.join()) return false; } return true; } const PARK_SIEGE_SHIPPABLE = _parkSiegeRecovers(_parkSiegeCell(PARK_SIEGE_SHIP_SEED)); /* ============ END SIEGE FIELD MODULE (the re-derived yard with the rear bands, the shared 6-beat clock, the gaze toll and the wave shove on separate confession logs, the y29 reads verbatim, the dry-ground summon walk, and the admission gate with the noring hybrid clause) ============ */ /* ============ ESCAPE FIELD MODULE (y51 "물 차오르는 술래잡기", design 2026-07-27) ============ */ /* TWO DOLLS ON TWO CLOCKS, AND A FLOOD THAT COMES FROM BEHIND. The forward doll A keeps y29's day — period 6 = 4 song beats then 2 looking beats — and a NEW left-flank doll B keeps a shorter one: period 3 = 2 song beats then 1 looking beat. Read over a 6-beat frame: beat 0 1 2 3 4 5 A(6) song song song song LOOK LOOK B(3) song song LOOK song song LOOK a step free free B free A A+B THREE FREE BEATS (0,1,3) AND THREE BILLED (2,4,5). The toll is ONE heart PER STEP, never per eye: beat 5 is not double-priced, it is merely one fewer beat to escape into. That is the only way a 3-heart body can stand two dolls at all, and it keeps the confession log a count of DECISIONS ("did he move while watched") rather than a count of watchers. THE WATER IS A FRONT, NOT A RING. One line of cells perpendicular to the doll axis drowns every third beat, starting behind the spawn and marching forward; sunk ground is a wall for all three domains. WHAT THE BUILDER HOLDS DRY, EXACTLY — so a later reader can check this claim against the code instead of trusting it. TWO mechanisms, both in _parkEscapeBuild's front-grouping loop: ① the LINE CUT. Every cell whose forward coordinate is >= the finish's is left out of `rows`, so the finish line and the whole yard past it never sink. ② the ERRAND HOLD-OUT. Three cells are left out BY KEY wherever they lie: the contract STATION, the contract GEM, and the RETIRE seat. Together: the yard shrinks to one dry line plus those hold-out cells, and no seed can be made unfinishable by the water alone — FOR EITHER BODY. The second half is the whole point of ②. Sunk ground is masked for the companion too, so a gem the water took would leave _parkCompanionPlan with no route, freeze him where he stands for the rest of the run, and reject every care persona as `matelost`; and because these are FIXED forward offsets the failure would be identical on every seed, which is precisely the class of defect a reseeding soft-lock filter can never catch. y46 states the same rule for its own yard — "the finish, his companion's gem, the retire seat and the whole forward yard are structurally dry, which is what keeps the route-mask Infinity trap unreachable (a masked cell is never a cell any errand needs)" — and buys it with a fixed forward CUT. A front that MARCHES cannot use a cut, so the guarantee is re-derived here as ② instead of imported. This is the "always leave an alternate route" prescription y26 paid for, applied to HIS errand and not only to the walker's line. THE TWO CLOCKS ARE DELIBERATELY ONE CLOCK. 3 divides 6, and the front advances on beat % 3 === 0, which is beat 0 and beat 3 of the frame — beats BOTH dolls are singing through. So the ground only ever dies on a beat the walker was free to move, and no eye can manufacture a forced heart loss. _parkEscapeFloodBeat IS the predicate tick consults (never a re-spelling of it), so the invariant the cell rests on is the one the engine actually runs (ESCAPE-CLOCKS-ALIGNED). SPEED ARITHMETIC, which is why the cell is playable at all: the water takes 2 lines per 6 beats, a walker who spends all three free beats advancing takes 3 — a net one line of slack per frame. One act of care eats that slack, which is the whole tension. THE ONE HUMAN-ONLY VERB: parkEscapePull. Click the companion (Chebyshev <= 2) and he is placed, at once, on the cell between you and whichever doll is biting this beat; THIS beat's toll is billed to him instead of you (a cracked-heart glyph and a `pulled` entry, never a heart — his body is scenery, he cannot die) and he is frozen for 2 beats. THE ORACLE NEVER CALLS IT. That is the known blind spot of every module/pair/mimic bar (the 2026-07-27 summon bug: the companion's homecoming went 24/24 -> 0/24 with every gate green), so the pull's contract lives in dedicated tests, not in the sweeps. It costs no turn and no beat; it is a command, not a move, which is why this module registers no legalAdd. THE READS ARE THE TWO THE BOARD CAN AFFORD, and they are disjoint by construction: C says STAY on any billed beat, N says CLOSE ON HIM while he stands on the line that drowns next — and no closing move is 'stay'. G gets no facet (the goal mind already refuses 'stay' and already wants the shortest line). */ const PARK_ESCAPE_N = 13; // DOLL A — y29's clock verbatim. Both looking beats bill here (y46 split its toll and made the // head-turn free; that is this cell's LAST lever, and reverting it is the human's call, not ours). const PARK_ESCAPE_SING = 4; const PARK_ESCAPE_GAZE = 2; const PARK_ESCAPE_PERIOD = PARK_ESCAPE_SING + PARK_ESCAPE_GAZE; // DOLL B — the new left-flank clock. 3 divides 6 on purpose: mis-phased clocks were y29's // dead-collapse lesson, and the front's alignment below rides on the division being exact. const PARK_ESCAPE_SING_B = 2; const PARK_ESCAPE_GAZE_B = 1; const PARK_ESCAPE_PERIOD_B = PARK_ESCAPE_SING_B + PARK_ESCAPE_GAZE_B; // THE BODY BUDGET (lever ①). It RESTATES parkStart's default (engine.js:7214) rather than setting // it — the walker's hearts are the engine's, not a module's — and is stamped on the board so the // render layer and the lever ladder argue about one named number instead of a literal. const PARK_ESCAPE_HEARTS = 3; // how many beats a pulled (or swept) companion is frozen (lever ②). THE ARITHMETIC, traced rather // than asserted (a pull sets stun = 2; tick spends one per beat, so the NEXT beat refuses and the // one after is free again): of a frame's three billed beats 2, 4 and 5 the pull can cover TWO — // {2,4} or {2,5} — but NEVER all three, because 4 and 5 are adjacent and covering 4 refuses 5. // One billed beat of every frame is always paid by the walker himself. It was never meant to cover // everything; at 1 it would cover all three, which is the shape lever ② would buy. const PARK_ESCAPE_STUN = 2; // the front advances every EVERY beats. Must divide PARK_ESCAPE_PERIOD and be a multiple of // PARK_ESCAPE_PERIOD_B, or the ground stops dying on free beats (see the header). const PARK_ESCAPE_EVERY = 3; const PARK_ESCAPE_REACH = 2; // the pull's reach (Chebyshev, diagonals included) const PARK_ESCAPE_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; const _PARK_ESCAPE_DIRS = [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }]; // ---- THE PUBLIC CLOCK READS (pure beat functions; nothing here advances anything). const _parkEscapeBeat = (st) => { const d = st.park && st.park.dyn; return d ? d.beat : 0; }; function _parkEscapeGazeA(st) { return (_parkEscapeBeat(st) % PARK_ESCAPE_PERIOD) >= PARK_ESCAPE_SING; } function _parkEscapeGazeB(st) { return (_parkEscapeBeat(st) % PARK_ESCAPE_PERIOD_B) >= PARK_ESCAPE_SING_B; } // ONE HEART PER STEP, NOT PER EYE (the header's toll rule): either doll looking is enough, and both // looking is no worse. This is the single predicate the toll, the C read and the pull all consult. function _parkEscapeBills(st) { return _parkEscapeGazeA(st) || _parkEscapeGazeB(st); } // THE FRONT'S OWN BEAT. Every beat this returns true must be a beat _parkEscapeBills returns false // on — the alignment the whole cell rests on. tick() gates on THIS function, so the invariant a // gate asserts is the invariant the engine runs. function _parkEscapeFloodBeat(st) { return (_parkEscapeBeat(st) % PARK_ESCAPE_EVERY) === 0; } function _parkEscapeNear(st) { return Math.max(Math.abs(st.pos[0].x - st.pos[1].x), Math.abs(st.pos[0].y - st.pos[1].y)); } // _parkEscapeNext(st): the line that drowns NEXT ([] once the sea reaches the finish) — the ENGINE // owns the timetable and the app's dotted preview draws THIS, so seen danger and priced danger can // never drift (flood's design law, restated by y46). function _parkEscapeNext(st) { const D = st.park.dyn && st.park.dyn.escape; if (!D || D.rowsGone >= st.park.escape.rows.length) return []; return st.park.escape.rows[D.rowsGone]; } // _parkEscapeAssemble(seed): the y29/y46 yard re-derived (same placement constants, an escape-own // rng salt so the layout stream is this module's), plus the precomputed flood lines and the second // doll. Seed-pure, persona-blind (C1) — a pure function of `seed` and NOTHING else (no persona, no // ordering symbol anywhere below), which is the constraint Task 3's reseed loop leans on. CONSTRUCTED, // never rejection-sampled — the seed draws WHICH FACE the yard stands on, never whether the scene // exists. `_parkEscapeBuild` (below) is the public entry point: it calls this once per candidate // seed and hands the result to `_parkEscapeLayoutOk` before accepting it. function _parkEscapeAssemble(seed) { seed = seed >>> 0; const n = PARK_ESCAPE_N, c = (n - 1) >> 1; const r = rng((seed * 6151 + 2749) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const sd = cs(0, 3), d = _PARK_ESCAPE_DIRS[sd]; // doll A's face == the walker's advance axis const lat = { x: Math.abs(d.y), y: Math.abs(d.x) }; // the lateral axis (perpendicular to d) const fl = cs(0, 1) ? 1 : -1; // which flank the companion's lane runs from // A(fwd, side): board-relative coordinates, y29's helper verbatim — every placement goes through // it, so the whole yard rotates and mirrors with the two drawn axes. const A = (fwd, side) => ({ x: c + d.x * fwd + lat.x * side * fl, y: c + d.y * fwd + lat.y * side * fl }); // y29's placement constants, re-derived: spawn four behind the middle and never on the // companion's flank, the finish five ahead one cell inside doll A's wall, his lane two cells out. const spawn = A(-4, cs(-1, 0)); const finish = A(5, 0); const dollA = A(6, 0); // ON the perimeter wall: a fixture, never ground // DOLL B stands one cell INSIDE the opposite wall, level with the middle. It is on the yard, not // on the wall, because its whole job is to watch the lane the walker crosses rather than the line // he runs; its viewing axis is NOT drawn again per seed (the yard's own D4 draw above already // supplies every orientation — drawing twice would shake one axis twice, design §10.2). const dollB = A(0, -5); const station = A(-3, 2), mateGem = A(0, 3), retire = A(3, 2); const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) wall.add(y * n + x); // NO DEEP FIELD (y29's channel purity, inherited twice over): the heart channels are the two // dolls and the water, and nothing else. const deep = new Set(), verge = new Set(), walkway = new Set(); for (let k = 0; k < n * n; k++) if (!wall.has(k)) walkway.add(k); // THE FRONT. Cells sharing a forward coordinate form one line; the lines are ordered rear-to-front // and held dry by the module header's TWO mechanisms — ① the line CUT at the finish's own forward // coordinate (design §10.1: a front that kept coming would make unfinishable seeds) and ② the // ERRAND HOLD-OUT below. // ② THE ERRAND IS STRUCTURALLY DRY. His station, his contract gem and the retire seat sit at // forward -3, 0 and +3 — inside the front's reach — and sunk ground is masked for the COMPANION // too (legalMask below), so without this his planner loses its target mid-errand and holds // forever, and every care persona rejects as `matelost`. These are fixed offsets, so the defect // would be seed-invariant and no reseeding filter could ever reject it. y46 buys the same // guarantee with a fixed forward CUT (its band loop takes only cells at fwd <= PARK_SIEGE_CUT); // a marching front cannot use a cut, so the same rule is re-derived here as a hold-out BY KEY. // These three keys ARE park.contracts[0].station, the contract gem (tokens[1]) and park.retire — // the same values the board is stamped with a few lines below. const dry = new Set([station.y * n + station.x, mateGem.y * n + mateGem.x, retire.y * n + retire.x]); const fwdOf = (x, y) => (x - c) * d.x + (y - c) * d.y; const finishFwd = fwdOf(finish.x, finish.y); const byFwd = new Map(); for (const k of walkway) { const f = fwdOf(k % n, (k / n) | 0); if (f >= finishFwd || dry.has(k)) continue; if (!byFwd.has(f)) byFwd.set(f, []); byFwd.get(f).push(k); } const rows = [...byFwd.keys()].sort((a, b) => a - b).map(f => byFwd.get(f)); const tokens = [ { x: finish.x, y: finish.y, v: 1, alive: true, guard: false }, // the finish (chain 0) { x: mateGem.x, y: mateGem.y, v: cs(1, 2), alive: true, guard: false }, // his contract gem ]; const park = { N: n, seed, k: 0, fieldMech: 'escape', deep, verge, walkway, distDeep: new Array(n * n).fill(Infinity), // THE FIXTURE. Both clocks, both dolls, the timetable and the pull's two knobs are NAMED on the // board, so the reads, the gates and the render argue about one fixture instead of each // re-deriving a face (y29's rule). escape: { rows, every: PARK_ESCAPE_EVERY, hearts: PARK_ESCAPE_HEARTS, // the errand hold-out, named on the board so a gate can CHECK the header's dry claim // instead of re-deriving the offsets (sorted: seed-pure, order-stable). dryKeys: [...dry].sort((p, q) => p - q), // Task 3's soft-lock filter (see _parkEscapeBuild): true only when 64 deterministic // reseeds all failed _parkEscapeLayoutOk and the LAST candidate was returned anyway // rather than silently. Named here (not bolted on after the fact) so every board — // accepted or not — carries the field and a reader never needs a hasOwnProperty guard. fallback: false, reach: PARK_ESCAPE_REACH, stun: PARK_ESCAPE_STUN, a: { side: sd, fl, key: dollA.y * n + dollA.x, sing: PARK_ESCAPE_SING, gaze: PARK_ESCAPE_GAZE }, b: { key: dollB.y * n + dollB.x, sing: PARK_ESCAPE_SING_B, gaze: PARK_ESCAPE_GAZE_B }, finishKey: finish.y * n + finish.x }, clusters: tokens.map(t => ({ x: t.x, y: t.y, v: t.v })), chain: [0], contracts: [{ gem: 1, station }], retire, spawn, companionSpawn: { x: station.x, y: station.y }, trig: n * 2, cap: 140, minTurns: 6, cautionD: 2, damage: 1, cell: _parkEscapeCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: station.x, y: station.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // OPT-IN runtime container (Task 0) // EVERY dyn member EXISTS at build (the _parkDeepClone rule — a structural clone is only sound // for members that are there when a search forks). // gone sunk cell keys (a wall for all three domains) // rowsGone lines the sea has taken (unit: lines) // swept the walker's water confession: one entry per line that went under his feet // mateSwept the same for the companion (unit: shoves) // caught the walker's eye confession: one entry per step taken while a doll looked // pulled the pull confession: one entry per toll paid BY HIM instead of by the walker // escorts the care confession: one entry per beat ending at his shoulder while he stands on // the line that drowns next (y29's `holds`, re-derived on this cell's own hazard) // mateStun beats left of his freeze; shieldBeat the beat a pull is paying for (-1 = none) park.dyn.escape = { gone: new Set(), rowsGone: 0, swept: [], mateSwept: 0, caught: [], pulled: [], escorts: [], mateStun: 0, shieldBeat: -1 }; return st; } // _parkEscapeLayoutOk(st): Task 3's soft-lock filter, spec §8's three conditions run as REAL // checks on the layout `_parkEscapeAssemble` just drew — a BUILD-TIME predicate, called before any // move exists, so "the walker" below means what ESCAPE-DRY-PATH already measures: he starts at // spawn and is moved only by the front's own shove, never by a choice. No live oracle or human hand // is available yet to say where a body WOULD walk, so this is the strongest honest claim a build-time // filter can make; a soft-lock the water creates independent of where anyone chooses to stand is // exactly the class of defect a reseed CAN fix, and is the class this function exists to catch. // ① a dry path from the walker to the finish survives every wave. // ② the errand (station, contract gem, retire) is held dry — checked STRUCTURALLY: each key must // be named in dryKeys AND absent from every flood line. A row-index range (the brief's own // Step 1) could never fail after a85791f's fix, because rowOf() returns -1 for a held-out key // on every seed; this version fails the moment a later edit drops the hold-out, because it // reads the flood lines themselves rather than trusting the name. // ③ every cell a wave takes has a dry cell for a standing body to be shoved to — checked for // EVERY cell in the sinking row, not only the one a body happens to occupy, because any of // them could be the one a body is standing on when the water reaches it. function _parkEscapeLayoutOk(st) { const n = st.N, E2 = st.park.escape; const rows = E2.rows, dryKeys = E2.dryKeys, finKey = E2.finishKey; // ---- ② STRUCTURAL ERRAND HOLD-OUT. Recompute the three keys the same way the board stamps them // (station, contract gem, retire) rather than trusting dryKeys to describe itself. const stationKey = st.park.contracts[0].station.y * n + st.park.contracts[0].station.x; const gemKey = st.tokens[1].y * n + st.tokens[1].x; const retireKey = st.park.retire.y * n + st.park.retire.x; for (const k of [stationKey, gemKey, retireKey]) { if (dryKeys.indexOf(k) < 0) return false; // not named as held out for (const row of rows) if (row.indexOf(k) >= 0) return false; // named, but the flood still owns it } // Local re-derivation of _parkEscapeShove's BFS and of a plain reachability BFS, over a `gone` // set THIS function owns (module law: recompute, never call a sibling's helper — and here, not // even THIS module's own tick hook, so the check stays pure geometry with no P/hearts/fx // machinery and can run on a board that has not started yet). const gone = new Set(); const nearestDry = (fromKey) => { const dist = new Array(n * n).fill(Infinity); dist[fromKey] = 0; const q = [fromKey]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const dd of DIRS) { const nx = x + dd.x, ny = y + dd.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || dist[nk] <= dist[kk] + 1) continue; if (!gone.has(nk)) return nk; // first dry, un-walled cell in BFS order dist[nk] = dist[kk] + 1; q.push(nk); } } return fromKey; // theoretically unreachable (mirrors _parkEscapeShove) }; const dryPathToFinish = (fromKey) => { if (gone.has(fromKey)) return false; const seen = new Set([fromKey]), q = [fromKey]; for (let h = 0; h < q.length; h++) { const kk = q[h]; if (kk === finKey) return true; const x = kk % n, y = (kk / n) | 0; for (const dd of DIRS) { const nx = x + dd.x, ny = y + dd.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || gone.has(nk) || seen.has(nk)) continue; seen.add(nk); q.push(nk); } } return seen.has(finKey); }; // ---- ① and ③, swept over the WHOLE flood schedule, in the SAME order tick() applies it: mark a // row gone, THEN shove whoever it was standing on. let walker = st.pos[0].y * n + st.pos[0].x; for (const row of rows) { for (const k of row) gone.add(k); // ③ every cell this wave just took must have had a dry cell to be shoved to. for (const k of row) if (nearestDry(k) === k) return false; if (gone.has(walker)) walker = nearestDry(walker); // ① a dry path from wherever the walker now stands to the finish must still exist. if (!dryPathToFinish(walker)) return false; } return true; } // _parkEscapeBuild(cell): the public entry point (C1 — the only parameter is the cell's seed). // A THIN WRAPPER over _parkEscapeAssemble plus a deterministic reseed loop: draw a candidate layout, // check it against _parkEscapeLayoutOk, and keep drawing — by SEED ARITHMETIC ONLY, never by // touching the persona stream anywhere in build (that purity is what keeps the board a pure // function of `seed`, C1's whole point) — until one passes or 64 candidates are exhausted. Every // `_parkEscapeCell(seed)` and `park.seed` on the returned board come from `_parkEscapeAssemble` // itself, stamped from the ACCEPTED seed of that call — never the originally requested one — so // the board's own identity and its cell can never disagree (same convention as // campaign.js's `_parkCrossingPlayCell` sweep). function _parkEscapeBuild(cell) { const seed0 = ((cell && cell.seed) | 0) >>> 0; let st; for (let t = 0; t < 64; t++) { st = _parkEscapeAssemble((seed0 + t * 7919) >>> 0); if (_parkEscapeLayoutOk(st)) return st; } // EXHAUSTED — 64 deterministic reseeds and not one passed _parkEscapeLayoutOk. Return the LAST // candidate LOUDLY rather than silently shipping a layout nothing has checked (y32's lesson: a // sweep that silently skips every candidate and reports a clean zero is a bug nobody questions). st.park.escape.fallback = true; return st; } // _parkEscapeCell(seed): the module's PUBLIC play-cell (C1 — no persona parameter exists). function _parkEscapeCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'escape' } }; } // _parkEscapeShove(st, fromKey, avoid): the nearest dry, un-walled cell (BFS, DIRS order — // deterministic), never the other body's cell. Returns fromKey only in the theoretically // unreachable no-dry-ground case (the finish line never sinks). function _parkEscapeShove(st, fromKey, avoid) { const n = st.N, D = st.park.dyn.escape; const avoidKey = avoid ? avoid.y * n + avoid.x : -1; const dist = new Array(n * n).fill(Infinity); dist[fromKey] = 0; const q = [fromKey]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const dd of DIRS) { const nx = x + dd.x, ny = y + dd.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || dist[nk] <= dist[kk] + 1) continue; if (!D.gone.has(nk) && nk !== avoidKey) return nk; // first dry cell in BFS order dist[nk] = dist[kk] + 1; q.push(nk); } } return fromKey; } // _parkEscapePullDest(st): where a pulled companion lands — the walker's own neighbour ON THE SIDE // of the doll that is biting THIS beat. Both biting (beat 5) resolves to A, and so does neither // biting: the forward doll matches the direction of travel and reads more cleanly (design §10.3). // Since the toll is one heart per STEP and not per eye, this choice moves only WHERE he stands. // Returns -1 when there is no such cell (off-board, wall, sunk, or he is already standing on it). function _parkEscapePullDest(st) { const n = st.N, E2 = st.park.escape, D = st.park.dyn.escape; const toward = (_parkEscapeGazeA(st) || !_parkEscapeGazeB(st)) ? E2.a.key : E2.b.key; const tx = toward % n, ty = (toward / n) | 0; const me = st.pos[0]; const dx = Math.sign(tx - me.x), dy = Math.sign(ty - me.y); if (dx === 0 && dy === 0) return -1; // he is standing ON the fixture: no "between" // ONE AXIS ONLY — the further one — so the landing cell is orthogonally adjacent and never a // diagonal the walk grammar has no move for. const ax = Math.abs(tx - me.x) >= Math.abs(ty - me.y); const x = me.x + (ax ? dx : 0), y = me.y + (ax ? 0 : dy); if (x < 0 || y < 0 || x >= n || y >= n) return -1; const k = y * n + x; if (st.wall.has(k) || D.gone.has(k)) return -1; if (st.pos[1].x === x && st.pos[1].y === y) return -1; // already there: nothing to pull return k; } // parkEscapePull(P): THE HUMAN-ONLY COMMAND. app.js's click handler is its ONE caller and the // oracle never reaches it — which is exactly why its contract is carried by dedicated tests rather // than by the module/pair/mimic sweeps (design §9). It spends no turn and no beat: it places him, // freezes him, and marks THIS beat as one he is paying for. Returns false, changing nothing, when // the reach, his freeze or the destination refuses. function parkEscapePull(P) { const st = P.st, D = st.park.dyn && st.park.dyn.escape; if (!st.park.escape || !D || P.over) return false; if (D.mateStun > 0) return false; // no rapid fire — a frozen friend cannot be moved if (_parkEscapeNear(st) > PARK_ESCAPE_REACH) return false; const dest = _parkEscapePullDest(st); if (dest < 0) return false; st.pos[1] = { x: dest % st.N, y: (dest / st.N) | 0 }; D.mateStun = PARK_ESCAPE_STUN; D.shieldBeat = st.park.dyn.beat; // this beat's toll is billed to him st.fx.push({ k: 'pulled', x: st.pos[1].x, y: st.pos[1].y }); return true; } // _parkEscapeCtx(P): the one-shot read the two facets share. Pure — nothing here advances anything. function _parkEscapeCtx(P) { const st = P.st, D = st.park.dyn && st.park.dyn.escape; const bills = _parkEscapeBills(st); const co = st.pos[1], cok = co.y * st.N + co.x; const next = _parkEscapeNext(st); // sinking: he is standing on the line that drowns NEXT — the only state this cell's care read // has anything to say about, and the one the water itself makes. const sinking = next.indexOf(cok) >= 0; // live: y29's read, inherited. A frozen friend cannot be steered, so the care mind goes quiet. // Under faithful play the only freeze is a sweep, and a swept body is already on dry ground — // so this conjunct binds ONLY after a human pull, which is the one scene the oracle cannot reach. const live = !!(D && D.mateStun === 0 && P.mode !== 'done'); return { bills, sinking, live, engagedC: bills, engagedN: sinking && live }; } /* ---- y51 ESCAPE — the filter layer: faithful playout, field signature, admissibility. y46's three siblings re-derived onto this cell's own two hazards, plus the ONE clause the flood earns: a run that ended before the first line went under never met the water at all (noflood). */ function _parkEscapePlay(cell, persona) { const st = _parkEscapeBuild(cell); const P = parkStart(st); while (!P.over) parkStep(P, parkOracleMove(P, persona)); const D = st.park.dyn.escape; P._escapeCaught = D.caught.length; // steps taken while a doll looked (unit: violations) P._escapeSwept = D.swept.length; // lines that went under his own feet (unit: shoves) P._escapeMateSwept = D.mateSwept; // and under the companion's (unit: shoves) P._escapeEscorts = D.escorts.length; // beats spent at his shoulder on a drowning line P._escapePulled = D.pulled.length; // ALWAYS 0 here: the oracle never pulls (design §9) P._escapeRows = D.rowsGone; // lines the sea took while the run was live // MATE HOME IS `st.score[1]`, NEVER `!gem.alive` — the walker's harvest is indiscriminate // (engine.js:8114), so a gem the WALKER ate would read as an errand served (y29's warning). P._escapeMateHome = st.score[1] > 0; return P; } // _parkEscapeSignature(playouts): THREE MINDS, THREE OBSERVABLES, each read off what the two clocks // made that mind do — y46's shape with the care clause re-derived onto THIS cell's hazard (there is // no mate-catch here; the dolls have no power over the companion, the water does). // SAFETY-top — never once steps while an eye is open (caught 0). // GOAL-top — steps anyway (caught >= 1) AND comes home in strictly fewer turns than the caution // baseline's own worst. The hearts have to BUY something or they were not a trade. // CARE-top — actually STOOD at his shoulder while the line under him drowned (escorts >= 1). // Care as an act, not as an absence. // Plus the non-vacuity clause the cell rests on: the water really does take somebody (swept > 0 on // some persona). If the front never reaches a body, this is a statue cell wearing a flood's name. function _parkEscapeSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); let safeWorst = -Infinity, goalWorst = -Infinity, careSeen = 0, wet = 0; for (let i = 0; i < playouts.length; i++) { const P = playouts[i]; if (P._escapeSwept > 0 || P._escapeMateSwept > 0) wet++; if (top[i] === 'safety') { if (P._escapeCaught !== 0) return false; safeWorst = Math.max(safeWorst, P.turns); } if (top[i] === 'goal') { if (P._escapeCaught < 1) return false; goalWorst = Math.max(goalWorst, P.turns); } if (top[i] === 'care') { careSeen++; if (P._escapeEscorts < 1) return false; } } if (!isFinite(safeWorst) || !isFinite(goalWorst)) return false; // non-vacuity guard if (!(goalWorst < safeWorst)) return false; // hurrying really is faster if (wet < 1) return false; // and the water really bites return careSeen === 2; } // FIRST-REASON HISTOGRAM (storm's warning, restated): every branch early-returns, one key per // reject, and EVERY key here is reachable — a declared-but-unreachable key is a bar that cannot // fail and therefore teaches nothing. const _PARK_ESCAPE_WHYS = { complete: 0, dead: 0, matelost: 0, sig: 0, nocn: 0, norec: 0, noflood: 0 }; function _parkEscapeAdmissible(cell) { const playouts = []; for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = _parkEscapePlay(cell, PARK_PERSONAS[i]); if (P.reason === 'death') { _PARK_ESCAPE_WHYS.dead++; return false; } if (P.reason !== 'complete') { _PARK_ESCAPE_WHYS.complete++; return false; } if (P.hearts < 1) { _PARK_ESCAPE_WHYS.dead++; return false; } if (PARK_PERSONAS[i][0] === 'care' && !P._escapeMateHome) { _PARK_ESCAPE_WHYS.matelost++; return false; } // THE FLOOD NON-VACUITY: the first line must go under while the run is live, on EVERY faithful // playout — a cell whose runs all end before beat 3 is a two-doll cell wearing a flood's id. if (P._escapeRows < 1) { _PARK_ESCAPE_WHYS.noflood++; return false; } playouts.push(P); } if (!_parkEscapeSignature(playouts)) { _PARK_ESCAPE_WHYS.sig++; return false; } let posed = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_ESCAPE_PAIRS) { if (!(parkPairExpressed(_parkEscapeBuild(cell), playouts[i].moves, pair) > 0)) continue; if (pair[0] === 'C' && pair[1] === 'N') posed++; const expect = _parkPushPairDir(persona, pair); if (!parkRecoverPairLex(_parkEscapeBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_ESCAPE_WHYS.norec++; return false; } } } // y51 is a kind-m3 (C-N) cell: the caution-care scene has to be POSED on at least two // trajectories or the slot is measuring a pair it never stages. if (posed < 2) { _PARK_ESCAPE_WHYS.nocn++; return false; } return true; } // parkEscapeWhys(): a copy of the reject tally (gates read it as a DELTA around their own sweep). function parkEscapeWhys() { return { ..._PARK_ESCAPE_WHYS }; } // ---- THE REGISTRATION. Everything the engine body knows about y51 hangs here. PARK_FIELD_MECHS.escape = { build: _parkEscapeBuild, cell: _parkEscapeCell, admits: _parkEscapeAdmissible, // TWO FACETS, DISJOINT BY CONSTRUCTION in exactly the state the cell is built for. Neither may // return {} outside its own engaged span: a module prefer() RUNS IN STATES ITS OWN engaged() // REJECTED (the seam trap), so an empty set there would silence a real mind on every free beat. reads: { ctx: _parkEscapeCtx, // C — DO NOT WALK WHILE AN EYE IS OPEN. y46's caution facet re-derived; the only difference is // which predicate opens the eye (here: either doll). C: { engaged: (P, ctx) => !!ctx && ctx.engagedC, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.engagedC) { for (const c of legal) out.add(c.k); return out; } for (const c of legal) if (c.k === 'stay') out.add(c.k); if (out.size) return out; for (const c of legal) out.add(c.k); return out; }, }, // N — CLOSE ON HIM WHILE THE GROUND UNDER HIM DROWNS. The engaged span is narrow on purpose: // not "whenever he is far" (that is the shape y46 measured as a gift to a surface imitator — // "walk toward the companion" is what a gem-greedy learner already does) but only on the beats // his own line is the next to go. No closing move is 'stay', so C and N are disjoint here. N: { engaged: (P, ctx) => !!ctx && ctx.engagedN, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.engagedN) { for (const c of legal) out.add(c.k); return out; } const co = P.st.pos[1]; const dNow = Math.abs(P.st.pos[0].x - co.x) + Math.abs(P.st.pos[0].y - co.y); for (const c of legal) if (Math.abs(c.x - co.x) + Math.abs(c.y - co.y) < dNow) out.add(c.k); if (out.size) return out; for (const c of legal) out.add(c.k); return out; }, }, }, // THE MASK — sunk ground is a wall for EVERYBODY (flood's drowned-ground grammar), and on top of // it a frozen companion has no domain at all. The freeze is TRANSIENT (tick spends it every // beat), which is the whole difference from the 2026-07-27 summon bug: a body pinned to a state // nothing clears is masked out of its own planner forever. legalMask: (P, key, who) => { const st = P.st, D = st.park.dyn && st.park.dyn.escape; if (D && D.gone.has(key)) return true; if (who !== 'mate') return false; return !!(D && D.mateStun > 0); }, // THE TOLL — one heart per STEP taken while either doll looks. 'stay' is always innocent. If a // pull is paying for this beat the charge goes to HIM: a cracked-heart glyph and a `pulled` entry, // never a heart, because his body is scenery and cannot die (design §5). onLeave: (P, ev) => { const st = P.st, dyn = st.park.dyn, D = dyn && dyn.escape; if (!D) return; if (ev.fromKey === ev.toKey) return; if (!_parkEscapeBills(st)) return; if (D.shieldBeat === dyn.beat) { D.pulled.push({ beat: dyn.beat, key: ev.toKey, by: _parkEscapeGazeA(st) ? 'a' : 'b' }); st.fx.push({ k: 'mateHurt', x: st.pos[1].x, y: st.pos[1].y }); return; } P.hearts--; D.caught.push({ beat: dyn.beat, key: ev.toKey }); st.fx.push({ k: 'seen', x: ev.to.x, y: ev.to.y }); }, // THE BEAT — ① the freeze expires ② the front advances (sink + shove + bill) ③ the care // confession is taken AFTER the shoves, so a body the sea moved is never credited as a body that // walked to his side (y46's ordering rule). tick: (P) => { const st = P.st, dyn = st.park.dyn, D = dyn && dyn.escape; if (!D) return; if (D.mateStun > 0) D.mateStun--; const rows = st.park.escape.rows; // ② THE FRONT. tick runs after dyn.beat++, so the gate reads the beat it is ON. The gate IS // _parkEscapeFloodBeat — the header's alignment invariant, consulted rather than re-spelt — and // the line budget beside it keeps the sea in phase and in order: at most one line per flood // beat, always the next one back. Schedule: lines sink at beats 3, 6, 9 ... and every one of // those is a beat _parkEscapeBills returns false on. if (D.rowsGone < rows.length && _parkEscapeFloodBeat(st) && dyn.beat >= (D.rowsGone + 1) * PARK_ESCAPE_EVERY) { for (const k of rows[D.rowsGone]) D.gone.add(k); D.rowsGone++; const n = st.N; const me = st.pos[0].y * n + st.pos[0].x; if (D.gone.has(me)) { // he stood on it as it went under: shoved to the nearest dry cell, one heart, own log — the // OTHER heart channel, never mixed with the dolls'. Death, as everywhere, is the engine's // own re-read right after this hook returns. const to = _parkEscapeShove(st, me, st.pos[1]); st.pos[0] = { x: to % n, y: (to / n) | 0 }; P.hearts--; D.swept.push({ beat: dyn.beat, key: me }); st.fx.push({ k: 'swept', x: st.pos[0].x, y: st.pos[0].y }); } const cok = st.pos[1].y * n + st.pos[1].x; if (D.gone.has(cok)) { const to = _parkEscapeShove(st, cok, st.pos[0]); st.pos[1] = { x: to % n, y: (to / n) | 0 }; D.mateStun = PARK_ESCAPE_STUN; D.mateSwept++; // its OWN glyph, not the pull's: the water carrying him off and a toll he took FOR the // walker are two different scenes, and the render layer must be able to tell them apart. st.fx.push({ k: 'mateSwept', x: st.pos[1].x, y: st.pos[1].y }); } } // ③ THE CARE CONFESSION, taken by the WORLD and not by the mind: one entry per beat that ends // with the walker at his shoulder while the line under him is the next to go. if (_parkEscapeNear(st) <= 1) { const next = _parkEscapeNext(st); if (next.indexOf(st.pos[1].y * st.N + st.pos[1].x) >= 0) D.escorts.push({ beat: dyn.beat }); } }, }; // PARK_ESCAPE_SHIP_SEED / _parkEscapeRecovers / PARK_ESCAPE_SHIPPABLE — the MODULE bar, DERIVED and // never asserted (y46's shape verbatim, a few hundred lines up; the literal `false` this replaces // was the y50 preview convention, and a literal cannot move on its own the day a cell earns the // bar). The quantity is the CALIBRATED 6/6 blind order recovery on this module's ship-seed cell — // calibrated meaning read at PARK_CAL_TURNS, the skip the product's own readout uses. // MEASURED 2026-07-27 (Task 6; seeds 1..40 x 6 personas): this derives to FALSE, and the reason sits // UPSTREAM of recovery rather than in it. Module admission is 0/40 with the first-reason histogram // {complete:0, dead:40, matelost:0, sig:0, nocn:0, norec:0, noflood:0}: the two goal-top personas and // one care-top persona take a step on each of the frame's three billed beats (2, 4, 5) and die at // turn 6 of an errand that needs 13-19, so this predicate refuses at its very first // `reason !== 'complete'` test and never reaches parkRecoverOrder at all. Three walls stand behind // that number and only the first is a constant — see // docs/superpowers/plans/2026-07-27-y51-measurements.md for all of them, in numbers. const PARK_ESCAPE_SHIP_SEED = 1; function _parkEscapeRecovers(cell) { for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkEscapeBuild(cell), persona); if (P.reason !== 'complete') return false; const r = parkRecoverOrder(_parkEscapeBuild(cell), P.moves, PARK_CAL_TURNS); if (!r || r.join() !== persona.join()) return false; } return true; } const PARK_ESCAPE_SHIPPABLE = _parkEscapeRecovers(_parkEscapeCell(PARK_ESCAPE_SHIP_SEED)); /* ============ END ESCAPE FIELD MODULE (the two-clock yard, the advancing front and its shove, the human-only pull and its billed-to-him toll, the caution/care facets, and the admission gate with the noflood clause) ============================================================================ */ /* ============ ALLEY FIELD MODULE (y50 "골목 질주", plan 2026-07-25) ============ */ /* THE SECOND HYBRID CELL: y25's telegraphed charge run through a LADDER MAZE, with y22's Sokoban crate translated into field-module clothes (the y26 ledge idiom). Two full wall rows split the yard into three east-west alleys; the bull is penned at the west end of the MIDDLE alley, whose row is the main street every errand wants. The companion's contract crosses that street through the east notch (the mate-aim scene, y25 separator 1, here a PLACEMENT of the notch column); the walker spawns TWO cells west of the crossing on the safe north row of the same alley, so the interpose segment is ONE step away — which is the whole point: y25's honest limit was its STOP-gate: the care oracle approached the interpose but never landed ON it (min dist reached = 2 even at telegraph 4), a board-geometry bound, "a further user design decision". THIS BOARD IS THAT DECISION. The clean band geometry puts a segment cell directly south of the spawn row, so a care-led walker steps onto the locked lane inside the fuse — measured, every care persona touches dist 0 on every seed (seeds 1..40, 80/80 playouts; y25's floor was 2). A COMPLETED block stays optional, exactly y25's semantics: the aimed companion steps off during the fuse, so the resolving charge finds an empty street and the testimony is the PATH. The reads are y25's C/N verbatim — what changed is the yard. THE CRATE IS REAL, AND ITS LIMIT IS CONFESSED UP FRONT (the y32 door precedent). A single crate sits in the SOUTH alley one column west of its own wall-row notch; shoved NORTH from the notch cell it lands ON the main street and the lane predicate treats it as OPAQUE — every charge and every alignment stops at it, so a placed crate permanently seals the street west of the crossing (the shield). Pushing is the ledge action-idiom: solid to bodies, transparent to the route metric, legalAdd force-opens it, onEnter moves it and puts the pusher back. But the stand cell is a whole south-alley detour away — beyond any fuse — so FAITHFUL personas never place it (a care read pointing at the crate would be inseparable from C on the (C,N) pair: the conflict pair-expression needs lives ON the lane, and the crate stand does not). The crate is the HUMAN player's strategic tool and the C-detour's incidental furniture (a caution-led walker may bulldoze it east along the south alley — harmless, and its own kind of testimony). If this cell is ever taken to the ship bar, a persona-reachable crate scene is the first thing to design. HEART CHANNELS, y25 VERBATIM: the charge (walker hit, ♥-1, dyn.alley.blocks) and the deep cut (the bonus gem sits IN the one-cell deep pocket on the main street's north row — the goal shortcut, y25 separator 3). A crate-block costs nothing and is logged on its OWN counter (dyn.alley.shields) — separate confession logs, the y46 discipline. THE GAG PAIR AND WHY THE ERRAND DOUBLES BACK (2026-08-01, the C-N promotion attempt). Two chain gems were added — chain 4 one step EAST of the spawn, chain 5 up the WEST back stair — and the errand order is [4, 5, east pocket, south gem]. They exist for one measured reason: to leave the GOAL mind with an EMPTY preference on the beat the charge is aimed at the companion. The law behind them is not local to this yard: a SINGLETON goal preference g={m} makes both goal-first orders (GCN and GNC) prescribe exactly {m}, so the blind pairwise scan has a consistent order on each side of C-vs-N and awards NOTHING — C-N is unbuyable at any price while the goal mind pins the move. Banking chain 4 turns the errand around onto the cell just vacated, PARK_ATTITUDES.G drops it by its own no-greedy-backtrack rule, the street detour is strictly longer (down and back up costs 2), and G comes out empty — inert, never a veto (y33's registry trap 1). On that beat C refuses the locked lane, N steps onto it, the sets are disjoint, and all six orders split 3-3 on exactly the C-vs-N question. MEASURED, seeds 1..8 x 6 personas = 48 faithful playouts, before -> after: C-N expressed+recovered 16/48 -> 48/48 (gsc/gcs/sgc/scg were each 0/8; now 8/8) G-C expressed+recovered 32/48 -> 48/48 (cgs/csg were 0/8; now 8/8) G-N expressed+recovered 32/48 -> 24/48 (gsc lost it; sgc/scg never had it) crossing incongruence filter under kind m3, seeds 1..8: 0/8 -> 2/8, mimic leak 0/12 (0%) STILL SHORT OF THE SHIP BAR, and the gap has one name: G-N is never posed for the three personas that rank caution above care (gsc/sgc/scg), and parkRecoverOrder returns null the moment ANY pair ties — so the calibrated 6/6 blind order recovery is 0/40 seeds and PARK_ALLEY_SHIPPABLE derives FALSE. y50 stays a preview. The lever table, the two levers measured and the next one are in docs/superpowers/plans/2026-08-01-y50-second-attempt.md. NOTE the sibling constraint recorded on _parkAlleySignature: "no goal-led persona ever approaches" and "every persona poses C-N" are mutually exclusive on ANY board, so the signature's partition had to move from lead-attitude to care-vs-caution rank. */ const PARK_ALLEY_N = 13; const PARK_ALLEY_STUN = 3; // beats the bull spends recovering after any stopped charge const PARK_ALLEY_TELEGRAPH = 3; // the committed-aim fuse, y25's measured value (interpose reachable) const PARK_ALLEY_CRAWL_EVERY = 4; // the downed companion's self-recovery cadence (y3 via y25) const PARK_ALLEY_CRAWL_CAP = 4; // this module's own stand-in for y3's bank ceiling const PARK_ALLEY_HEART_FLOOR = 2; // law 3: care never interposes the walker below this floor const PARK_ALLEY_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // _parkAlleyLane(st, fromKey, dirIdx): the straight run of cells starting ADJACENT to `fromKey` in // DIRS order, stopping at a wall cell — OR AT THE CRATE. The crate-opacity is the one divergence // from y25's predicate and it is load-bearing three ways: a body behind a placed crate is never // aligned (the shield works from the AIM step, not just the resolution), a resolving charge stops // at it, and the bull can never see down a sealed street again. Reads st.wall + dyn.alley.crate // only — both public (C1). function _parkAlleyLane(st, fromKey, dirIdx) { const n = st.N, d = DIRS[dirIdx]; const dyn = st.park && st.park.dyn; const crate = dyn && dyn.alley ? dyn.alley.crate : null; const lane = []; let x = fromKey % n, y = (fromKey / n) | 0; for (;;) { x += d.x; y += d.y; if (x < 0 || y < 0 || x >= n || y >= n) break; const k = y * n + x; if (st.wall.has(k) || k === crate) break; lane.push(k); } return lane; } // _parkAlleyAligned(st): does a body sit on one of the bull's TWO alley lanes — east or west down // the street it stands in? First hit as { dirIdx, lane, targetWho } or null. The one deliberate // divergence from y25's four-lane predicate, and it is this maze's PEN (y25's caps translated): // with north/south charges out of the game the bull can never leave row 6, so an empty charge // migrates it ALONG the street and never INTO the walkway rows — measured before this restriction, // safety personas died 3-block deaths (a bull that had drifted onto the north highway turned the // whole row into a lane, and a mind that only refuses lanes it stands beside walked the corridor // into three resolutions). An alley bull charges down the alley; the wall rows are why. function _parkAlleyAligned(st) { const park = st.park; if (!park || !park.bull) return null; const dyn = park.dyn, ent = dyn && dyn.ents && dyn.ents[0]; const from = ent ? ent.key : park.bull.home; const bodies = [ { who: 'me', key: _parkKey(st, st.pos[0]) }, { who: 'mate', key: _parkKey(st, st.pos[1]) }, ]; for (let d = 2; d < 4; d++) { // DIRS 2 L / 3 R — the street axis only const lane = _parkAlleyLane(st, from, d); for (const b of bodies) { if (lane.indexOf(b.key) !== -1) return { dirIdx: d, lane, targetWho: b.who }; } } return null; } // _parkAlleyBuild(cell): the y50 board — pure fn of the PUBLIC cell (reads ONLY cell.seed; C1). // 13x13, perimeter wall, two full interior wall rows (y=4 / y=8) cut by seed-jittered notches: // row 4 notches { gW=2 (the west back stair, C's loop), gX (the crossing, drawn 7..8) } // row 8 notches { gS (the crate stand, drawn 4..5), gX (the companion's straight-south retire) } // The three alleys those rows leave (heights 3/3/3) are the maze; row 6 is the main street. // PLACEMENTS, not draws (every separator constructed on every seed, the y25 rewrite lesson): // HOME (1,6) flush on the west wall — its whole east lane IS the main street; its north/south // lanes are the single cells (1,5)/(1,7), dead ends no errand visits. // GATE FENCE (10,6), ONE cell (a 3-tall y25 gate would seal a 3-tall alley): the street stops at // x=9, so the east pocket column 11 — chain gems, the finish leg — is aligned-proof. // DEEP POCKET (gX+2, 5) + BONUS in it: the goal shortcut east along the north row cuts the deep // (♥-1, banks the bonus); the caution detour and the crossing-column road north stay // ALL-WALKWAY on both gX draws (the push module's design law 3, re-learned at 0/5 and 21/40). // G/C separate as in y25. // SPAWN (gX-2, 5): on the north row of the middle alley, TWO cells west of the crossing — off // every lane at beat 0, and ONE step north of an interpose-segment cell (the STOP-gate fix). // COMPANION (gX,5) -> gem (gX,7) -> retire (gX,11): the y25 straight-column journey; his one // street beat at (gX,6) is the mate-aim scene. // CRATE (gS,7), STAND (gS,8) = the south notch itself: shoved north it lands on (gS,6), strictly // west of the crossing — the human shield described in the header. function _parkAlleyBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const runner = !!(cell && cell.mazeRunner); const n = runner ? 15 : PARK_ALLEY_N; const r = rng((seed * 6733 + 3559) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const K = (x, y) => y * n + x; const gX = cs(7, 8); // the crossing / retire column (east notch, both wall rows) const gS = cs(4, 5); // the crate stand column (south wall row only) const gW = 2; // the west back stair (north wall row only) const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) wall.add(K(x, y)); for (let x = 1; x < n - 1; x++) { if (x !== gW && x !== gX) wall.add(K(x, 4)); if (x !== gS && x !== gX) wall.add(K(x, 8)); } const fences = new Set([K(10, 6)]); for (const k of fences) wall.add(k); const home = K(1, 6); // ONE deep cell, not a column — the push module's measured design law 3, re-learned here TWICE: // at 0/5 admission (a two-cell pocket made the south detour verge — safety froze, reason 'cap') // and again at 21/40 (a FIXED pocket at x=9 sat beside the gX=8 crossing column, so the only // road north ran through verge on every gX=8 seed — the whole reject cluster). The pocket rides // TWO east of the crossing, so the caution mind's loop (south row -> pocket -> crossing column // north) is all-walkway on BOTH gX draws, and the goal shortcut (♥-1, the bonus) still cuts it. const deepX = gX + 2; const deep = new Set([K(deepX, 5)]); // the shipped caution frame, exactly as _parkBuild derives it: multi-source BFS from the deep // set with walls TRANSPARENT — 0 deep / 1 verge / >=2 walkway (y25's own transplant). const distDeep = new Array(n * n).fill(Infinity); { const q = []; for (const k of deep) { distDeep[k] = 0; q.push(k); } for (let h = 0; h < q.length; h++) { const k = q[h], qx = k % n, qy = (k / n) | 0; for (const d of DIRS) { const nx = qx + d.x, ny = qy + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (distDeep[nk] > distDeep[k] + 1) { distDeep[nk] = distDeep[k] + 1; q.push(nk); } } } } const verge = new Set(), walkway = new Set(); for (let k = 0; k < n * n; k++) { if (wall.has(k) || deep.has(k)) continue; (distDeep[k] === 1 ? verge : walkway).add(k); } const spawn = { x: gX - 2, y: 5 }; const companionSpawn = { x: gX, y: 5 }; const cgemY = 7; const tokens = [ { x: n - 2, y: 6, v: cs(2, 3), alive: true, guard: false }, // chain 0 — the east pocket, past the fence // chain 1 lives in the SOUTH alley, not the north one, and the reason is the goal mind's own // no-greedy-backtrack rule (PARK_ATTITUDES.G): the east pocket is a cul-de-sac whose only // fast-reducing exit toward a NORTH gem is the cell the walker just came from — measured, G // sat at the pocket to cap 19/40 times. A south gem makes the pocket's other door the // forward step, and the errand becomes a circuit of all three alleys (the maze-runner loop). { x: n - 2, y: 10, v: cs(2, 3), alive: true, guard: false }, { x: gX, y: cgemY, v: cs(1, 2), alive: true, guard: false }, // the companion's contract gem { x: deepX, y: 5, v: cs(1, 2), alive: true, guard: false }, // BONUS, in the deep ♥-pocket // ---- THE GAG PAIR (2026-08-01, the C-N promotion lever). Two chain gems whose ONLY job is // to empty the GOAL mind's preference on the beat the charge is aimed at the companion. // WHY: a SINGLETON G preference makes the C-N award unbuyable at any price — with g={m}, // both G-first orders (GCN and GNC) prescribe exactly {m}, so the pairwise scan can never // rank C against N and every non-care-led persona reads C-N as unposed (measured: C-N // expressed 16/48 over seeds 1..8 x 6 personas, and 0/8 for each of gsc/gcs/sgc/scg). // The gag: chain 4 sits ONE step east of the spawn, so every persona steps onto it at turn 0 // (G is the only mind speaking there — the deep band is 4 cells away and no charge is aimed // yet), and chain 5 sits up the WEST back stair, so the instant chain 4 is banked the errand // points BACK THROUGH THE CELL JUST VACATED. PARK_ATTITUDES.G excludes that cell by its own // no-greedy-backtrack rule, the street detour is strictly longer (down and back up costs 2), // and G's compliant set comes out EMPTY — inert, not a veto (y33's registry trap 1). With G // silent the fuse beat is a two-mind board: C refuses the locked lane, N steps onto it, the // sets are disjoint, and all six orders split 3-3 on exactly the C-vs-N question. { x: gX - 1, y: 5, v: cs(1, 2), alive: true, guard: false }, // chain 4 — the gag trigger { x: gW, y: 2, v: cs(2, 3), alive: true, guard: false }, // chain 5 — up the west back stair ]; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); const station = { x: gX, y: cgemY }; const retire = { x: gX, y: n - 2 }; const park = { N: n, seed, k: 0, fieldMech: 'alley', deep, verge, walkway, distDeep, bull: { home, fences, stun: PARK_ALLEY_STUN }, // y25's fixture shape — board-driven render reuse alley: { crateSpawn: K(gS, 7), stand: K(gS, 8), notch: gS, cross: gX, gate: K(10, 6) }, clusters, chain: [4, 5, 0, 1], contracts: [{ gem: 2, station }], retire, spawn, companionSpawn, trig: n * 2, cap: 140, minTurns: 12, cautionD: 2, damage: 1, cell: _parkAlleyCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: companionSpawn.x, y: companionSpawn.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; if (runner) _parkAlleyRunnerStage(st, seed); park.dyn = _parkDynInit(st); // `face` is the direction index the bull is looking (0 up / 1 down / 2 left / 3 right). Every // step, aim and gore writes it, and the painter orients the head and horns by it — a bull that // never turned its face was unreadable in a still frame. park.dyn.ents = [{ kind: 'bull', key: park.bull.home, aim: null, stunned: 0, targetWho: null, face: 3 }]; park.dyn.alley = runner // round / goalClaim / sleeping / wakeIn are the three-round staging's own members. Every dyn // member EXISTS at build (the _parkDeepClone rule), so `round: -1` is the "no round has opened // yet" sentinel that makes _parkAlleyRoundTick fire its opening work on the very first beat. ? { crate: null, shields: 0, blocks: 0, downDir: 0, targetWho: null, lastCharge: [], lastMove: 'idle', round: -1, goalClaim: null, sleeping: false, wakeIn: 0, mateStage: null } : { crate: K(gS, 7), shields: 0, blocks: 0, downDir: 0 }; return st; } /* THE STAGE IS BANDS, NOT A MAZE (2026-08-03 — the map fix). The first cut of this presentation board carved a PERFECT maze (randomized DFS backtracker): corridors exactly ONE cell wide and, being a spanning tree, EXACTLY ONE route between any two cells. Both halves were fatal, and both were measured before this was rewritten: 완주 0/6 — not one of the six personas reached the goal. Not a hard board; an impossible one. GC 0/6 — the pair this slot is GRADED on (kind m1) was never once posed. GN stood 6/6, CN 0/6. A TREE CANNOT POSE A PAIR. Where the route between two cells is unique, the goal mind, the caution mind and the care mind all return the SAME move, so parkPairExpressed has nothing to see — the board stops measuring, which is the whole reason this cell exists. And a one-wide corridor gives a charging bull no cell to be dodged into, so the verb dies with the measurement. The module's base board had already written the law down — "밴드 높이 3은 admission 경제의 하한(1칸 골목은 dead 붕괴 위험 — y29 0-하트-마진 교훈)" — and the maze broke it. NOTE ON THE ⑤ REGISTER: while this board was dead, PARK-FIVE-FORCED read y50's droppable set as EMPTY and that looks exactly like progress ("이제 그 쌍을 강제한다"). It was not. With 0 完走 there is no sample, and "nothing can be dropped" and "nothing was ever posed" print the same. A bar that improves because the board died is the vacuity trap, one layer up. SO THE STAGE IS THE BASE BOARD'S LADDER, WIDENED: two wall rows across the 15x15 yard, each with THREE doors, leaving three bands 4/4/3 rows deep. Every band is at least three cells wide (room to be dodged in), every band reaches its neighbours by three different doors (a route that can be RE-CHOSEN, which is the only place a pair can stand), and the middle band is one straight street the bull owns end to end. Nothing else about the runner stage moves: the bull's charge, the interpose, the downed companion and his crawl, and the crate all keep their shapes. */ function _parkAlleyRunnerStage(st, seed) { const n = st.N, park = st.park, K = (x, y) => y * n + x; const r = rng((seed * 9151 + 2713) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) wall.add(K(x, y)); // THE TWO WALL ROWS AND THEIR THREE DOORS EACH. The doors jitter by seed so the layout stream // stays this module's own, but the COUNT never jitters: three doors is what keeps one blocked // door from being a dead end. Two would still strand a body the bull has lined up on. const ROWS = [5, 10]; const doors = ROWS.map(() => [cs(2, 4), cs(6, 8), cs(10, 12)]); ROWS.forEach((wy, i) => { for (let x = 1; x < n - 1; x++) if (!doors[i].includes(x)) wall.add(K(x, wy)); }); const open = new Set(); for (let k = 0; k < n * n; k++) if (!wall.has(k)) open.add(k); park.walkway = open; park.verge = new Set(); park.deep = new Set(); park.distDeep = new Array(n * n).fill(Infinity); // THE BULL OWNS THE MIDDLE STREET — the base board's idiom (its bull sits on the middle band's // west wall, at K(1,6) there). Row 7 here runs unbroken from x=1 to x=13 THROUGH TWO of the // errand gems, so the charge is long, straight and legible, and the bands above and below are // where you get out of its way. Moved off K(1,1): a bull cornered in the top band had no lane. // THE BULL'S HOME IS A MEASURED CONSTANT, NOT A CHOSEN ONE (2026-08-04). The round reform (the // walking companion, the tail pad, the evade clause) cost completion: 40/48 -> 33/48 over // 6 personas x seeds 1..8, with deaths 8 -> 12 and the three care-first personas collapsing from // 8/8 each to 5/8 each. Moving the animal one cell south restores it — 40/48 with deaths at 6, // BELOW the pre-reform baseline. The response is NOT monotone in y and that is the warning worth // leaving: 7 -> 33, 8 -> 40, 9 -> 28, 10 -> 38, 11 -> 24, 12 -> 24 complete. This is a knife-edge, // not a gradient, so treat the number as measured rather than as reasoned, and RE-MEASURE the // whole column before moving anything that changes when the animal meets a body. park.bull = { home: K(1, 8), fences: new Set(), stun: 2 }; park.alley = { runner: true }; // THE OPENING LINE (2026-08-04). Round 0 is a question, and a question needs both answers to be // reachable BY HIM. The old opening put pink three steps from the pad she claims and the walker // thirteen, so the moment she was able to walk at all she simply stood on it — and a cell a // companion stands on is not a choice, it is a wall. All four bodies now share the top band's // open room, on one line: free pad, walker, claimed pad, pink. He is one step nearer the claimed // one than she is, and the claimed one is one step cheaper than the free one, which is what makes // taking it evidence of haste rather than evidence of distance. park.spawn = { x: 6, y: 4 }; park.companionSpawn = { x: 13, y: 4 }; /* THREE ROUNDS, ONE PER PAIR (2026-08-03). The errand is no longer a gem circuit; it is the 'reach' grammar — the goal tokens are PADS (stand on one, no pickup, no score) and the chain is three legs long, so the board ends after three arrivals. P.dest IS the round number. Each round exists to stage ONE comparison, and the reason they are rounds rather than beats is the recorded law 「한 쌍이 다른 쌍을 막는다」: an award needs the minds above it to be silent, so two pairs cannot be posed on the same beat. Rounds buy that silence structurally. ROUND 0 GOAL vs CARE two pads; pink has CLAIMED one. Does he walk into hers? ROUND 1 SAFETY vs CARE one pad, HERS, on the far side of the bull's street. She walks to it herself; the question is whether he stands in the charge for her. ROUND 2 GOAL vs SAFETY two pads. One sits IN the bull's street behind him (short, priced); one is off it entirely (long, free). The bull dozes and wakes on a three-beat cycle, so the street is survivable but only on the beat. MEASURED (시드 1..6 × 6인격 = 36런, 보정창 2턴, live 셀): 도즈 시계를 읽는 안전 채널(PARK_ALLEY_RAY_WARN) 전 GC 3/36, 후 GC 19/36. 공동각성 22->56, 서로소 3->28. 대가는 라운드 순도다 — R0 에 GC·CN 이 각 5/36 유입되고 R1 의 GC 가 15->28 로 올랐다. 죽음 8->0 이지만 턴 캡 소진이 0->7 이라 완주는 28->29 로 1런 늘 뿐이다. THE POSITIONS ARE UNMEASURED. They are chosen to make each comparison geometrically possible, not to make it land — which pad a mind actually prefers has to be measured, and the first sweep will almost certainly move some of these. */ const points = [ [2, 2], // 0 round 0 — the FREE pad, the one care takes: one step dearer, and unclaimed [9, 2], // 1 round 0 — the CLAIMED pad, and deliberately the cheaper of the two [7, 12], // 2 round 1 — hers, across the street, so her crossing is what the bull aims at [2, 7], // 3 round 2 — ON the bull's street, behind him: the short road, priced [13, 3], // 4 round 2 — off the street entirely: the long road, free ]; st.tokens = points.map((p) => ({ x: p[0], y: p[1], v: 0, alive: true, guard: false, pad: true })); park.clusters = st.tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); park.chain = [0, 2, 3]; park.chainAnyOf = [[0, 1], [2], [3, 4]]; park.contracts = []; park.retire = { ...park.companionSpawn }; park.trig = n * 2; park.cap = 180; st.wall = wall; st.hazard = new Set(); st.pos[0] = { ...park.spawn }; st.pos[1] = { ...park.companionSpawn }; } function _parkAlleyRunnerDistances(st, targetKey) { const n = st.N, dist = new Array(n * n).fill(Infinity), q = [targetKey]; dist[targetKey] = 0; for (let h = 0; h < q.length; h++) { const key = q[h], x = key % n, y = (key / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y, nk = ny * n + nx; if (nx < 0 || ny < 0 || nx >= n || ny >= n || st.wall.has(nk)) continue; if (dist[nk] > dist[key] + 1) { dist[nk] = dist[key] + 1; q.push(nk); } } } return dist; } function _parkAlleyRunnerTarget(P) { const st = P.st, dyn = st.park.dyn, D = dyn.alley, B = dyn.ents[0]; const candidates = [{ who: 'me', key: _parkKey(st, st.pos[0]) }]; if (!(dyn.downed && !dyn.downed.rescued)) candidates.push({ who: 'mate', key: _parkKey(st, st.pos[1]) }); let best = null; for (const c of candidates) { const d = _parkAlleyRunnerDistances(st, c.key)[B.key]; if (!best || d < best.dist || (d === best.dist && c.who === D.targetWho)) best = { ...c, dist: d }; } return best; } function _parkAlleyRunnerHit(P, who, dirIdx) { const st = P.st, dyn = st.park.dyn, D = dyn.alley, p = who === 'me' ? st.pos[0] : st.pos[1]; if (who === 'me') { P.hearts--; D.blocks++; } else if (!(dyn.downed && !dyn.downed.rescued)) { dyn.downed = { rescued: false, crawl: 0, by: null, trail: [] }; D.downDir = dirIdx; } st.fx.push({ k: 'thud', x: p.x, y: p.y }); } // The doze cycle of the round-2 bull: it wakes on every PARK_ALLEY_DOZE-th beat and sleeps through // the rest. Borrowed shape from y46's doll — a PUBLIC beat clock, so the danger is a schedule a plan // can read rather than a surprise. Three is the smallest period that leaves a walker a beat to enter // the street, a beat to cross, and a beat to be out before the horns come back. const PARK_ALLEY_DOZE = 3; // PARK_ALLEY_RAY_WARN: 안전이 도즈 시계를 미리 읽기 시작하는 여유(박자). 1 이면 깨어 있는 // 박자와 그 직전 한 박자. 이 값이 없으면 안전은 조준이 걸린 뒤에만 말할 수 있는데, R2 의 // 황소는 3 박 중 2 박을 자므로 조준이 거의 없다 — 값이 붙은 길에 값을 말할 마음이 침묵했다. // 측정(시드 1..6 × 6인격 = 36런): 이 채널 없이 R2 의 목표-안전 쌍은 3/36, 넣으면 19/36. const PARK_ALLEY_RAY_WARN = 1; // _parkAlleyRays(st): 황소에서 네 방향으로 뻗어 벽에서 멈추는 칸들. 공개 상태만 읽는다(C1). // 조준(B.aim.lane)과 다른 물건이다 — 저것은 이미 약속된 돌진이고, 이것은 "저 동물이 달릴 수 // 있는 줄"이라는 예보다. 그래서 다른 몸에서 끊지 않는다: 앞의 몸이 비키면 줄은 그대로 산다. function _parkAlleyRays(st) { const n = st.N, dyn = st.park && st.park.dyn, B = dyn && dyn.ents && dyn.ents[0]; const set = new Set(); if (!B) return set; for (const d of DIRS) { let x = B.key % n, y = (B.key / n) | 0; for (;;) { x += d.x; y += d.y; if (x < 0 || y < 0 || x >= n || y >= n) break; const k = y * n + x; if (st.wall.has(k)) break; set.add(k); } } return set; } // _parkAlleyRayCtx(P): 시계를 읽는 안전 채널. { live, rays }. // RUNNER 전용이다 — reads 는 모듈 공용이라 가드가 새면 비-runner 측정 보드의 admits 와 // PARK_ALLEY_SHIPPABLE, 나아가 parkCrossings 의 셀 선택까지 같이 움직인다(Y50-MEASURE-BOARD-UNMOVED). // 인접까지 사는 이유: 광선에 올라선 뒤 피하는 것은 PARK_ALLEY_TELEGRAPH 안에 이미 늦다. function _parkAlleyRayCtx(P) { const st = P.st, park = st.park, dyn = park && park.dyn, D = dyn && dyn.alley; if (!park || !park.alley || !park.alley.runner || !D) return { live: false, rays: null }; if (D.sleeping && D.wakeIn > PARK_ALLEY_RAY_WARN) return { live: false, rays: null }; const rays = _parkAlleyRays(st); if (rays.has(_parkKey(st, st.pos[0]))) return { live: true, rays }; const n = st.N, x = st.pos[0].x, y = st.pos[0].y; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; if (rays.has(ny * n + nx)) return { live: true, rays }; } return { live: false, rays: null }; } // How close the bull has to be before the round-2 companion bothers to move. Four is one more than // the telegraph, so she is already stepping away by the time an aim aimed at her could resolve. const PARK_ALLEY_MATE_NEAR = 4; /* _parkAlleyRoundTick(P): the three-round staging. Everything here is bookkeeping ON TOP of the ordinary chain cursor — P.dest advances by the shared pad rule, and this hook only does the three things the cursor cannot know about: (1) when a round opens, point pink at what she is supposed to want, and set/clear the claim; (2) in round 1 the leg closes on HER arrival, not his — she is the one who has to get in; (3) pads belonging to finished rounds are struck dead, so the board shows one round at a time ("두 골이 동시에 사라지고 두 번째 골이 나온다"). */ function _parkAlleyRoundTick(P) { const st = P.st, park = st.park, dyn = park.dyn, D = dyn.alley; const round = P.dest | 0; if (D.round !== round) { D.round = round; if (round <= 1) { /* (1) SHE SEES IT FIRST, and the order on screen is the whole point: see, then claim, then walk. The ring is deliberately NOT set here. A claim that appears in the same instant as the goal is not a claim anybody watched her make, and this round is measured on whether he walks into a mark he had time to read. Y50-ROUND0-FAIR guarantees he cannot reach the pad before beat 2, so the ring is always up before his first committing step. */ const ti = round === 0 ? 1 : 2; const t = st.tokens[ti]; D.mateStage = 'notice'; D.goalClaim = null; P.mode = 'idle'; if (t) { st.facing[1] = { dx: Math.sign(t.x - st.pos[1].x), dy: Math.sign(t.y - st.pos[1].y) }; st.fx.push({ k: 'notice', seat: 1, token: ti, x: t.x, y: t.y }); } } else { /* ROUND 2 — SHE DOES NOT GO HOME. The retire walk that used to live here sent her back to her corner and left the last round with a companion doing housekeeping while a bull was awake in the middle of it. Her job now is to not be where the animal is going, and nothing else (_parkAlleyMateEvade). 'done' parks the shared mover so the two never fight. */ D.mateStage = 'evade'; D.goalClaim = null; P.mode = 'done'; } } else if (D.mateStage === 'notice') { // (1b) THE CLAIM, one beat later, and her first step rides the same beat. const ti = round === 0 ? 1 : 2; const t = st.tokens[ti]; if (t && t.alive) { D.goalClaim = { token: ti, seat: 1 }; D.mateStage = 'walk'; P.mode = 'relocate'; P.target = { x: t.x, y: t.y }; } } // (2) HER arrival closes round 1. The chain cursor watches the walker, so without this the leg // would never end — he cannot finish this one for her, which is the entire point of it. if (round === 1) { const t = st.tokens[2]; if (t && t.alive && st.pos[1].x === t.x && st.pos[1].y === t.y) { t.alive = false; st.fx.push({ k: 'pad', x: t.x, y: t.y }); } } // (3) finished rounds leave the board if (round >= 1) for (const i of [0, 1]) { const t = st.tokens[i]; if (t) t.alive = false; } if (round >= 2) { const t = st.tokens[2]; if (t) t.alive = false; } // THE DOZE, round 2 only. Public and beat-keyed: sleeping on every beat but the wake beat. if (round === 2) { const phase = dyn.beat % PARK_ALLEY_DOZE; D.sleeping = phase !== 0; D.wakeIn = phase === 0 ? 0 : PARK_ALLEY_DOZE - phase; } else { D.sleeping = false; D.wakeIn = 0; } } /* _parkAlleyTailPad(P): round 2's goal rides the bull's TAIL — the cell directly behind whatever the animal is looking at. It is called at the END of the tick, after the bull has stepped and turned, so the pad is never a beat behind the thing it is stuck to. THE REFUSALS ARE THE DESIGN. It never lands on the walker: a bull that merely turned round would otherwise hand him the leg for nothing, which is the exact opposite of what this round prices. It never lands on the other live pad (the free, far one — that pair IS the round's comparison), never in a wall, never on the crate. When every side refuses, it simply holds where it was. */ function _parkAlleyTailPad(P) { const st = P.st, park = st.park, dyn = park.dyn, D = dyn && dyn.alley; if (!D || (P.dest | 0) !== 2) return; const B = dyn.ents && dyn.ents[0], t = st.tokens[3]; if (!B || !t || !t.alive) return; const n = st.N, bx = B.key % n, by = (B.key / n) | 0; const back = DIRS[B.face | 0] || DIRS[0]; const order = [{ x: bx - back.x, y: by - back.y }]; for (const d of DIRS) order.push({ x: bx + d.x, y: by + d.y }); const other = st.tokens[4]; for (const c of order) { if (c.x < 0 || c.y < 0 || c.x >= n || c.y >= n) continue; const k = c.y * n + c.x; if (st.wall.has(k)) continue; if (D.crate != null && k === D.crate) continue; if (c.x === st.pos[0].x && c.y === st.pos[0].y) continue; if (other && other.alive && other.x === c.x && other.y === c.y) continue; t.x = c.x; t.y = c.y; if (park.clusters && park.clusters[3]) { park.clusters[3].x = c.x; park.clusters[3].y = c.y; } return; } } /* _parkAlleyMateEvade(P): round 2's companion, and her whole job. She does not go home and she has no errand — she keeps out of the animal's way. Three clauses in order: 1. standing in a lit lane -> step OFF it (off, never along: along is where it is going) 2. within PARK_ALLEY_MATE_NEAR -> take the neighbour that puts the most ground between them 3. otherwise HOLD The hold is not laziness. A body that shuffles every beat erases the meaning of its own motion, and this round needs her flight to read as an event rather than as weather. */ function _parkAlleyMateEvade(P) { const st = P.st, park = st.park, dyn = park.dyn, D = dyn && dyn.alley; if (!D || D.mateStage !== 'evade') return; if (dyn.downed && !dyn.downed.rescued) return; // the crawl owns her const B = dyn.ents && dyn.ents[0]; if (!B) return; const n = st.N, dist = _parkAlleyRunnerDistances(st, B.key); const here = _parkKey(st, st.pos[1]); const lane = B.aim && B.aim.lane ? new Set(B.aim.lane) : null; const onLane = !!(lane && lane.has(here)); if (!onLane && !(dist[here] <= PARK_ALLEY_MATE_NEAR)) return; let best = null; for (const d of DIRS) { const nx = st.pos[1].x + d.x, ny = st.pos[1].y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || nk === B.key) continue; if (D.crate != null && nk === D.crate) continue; if (nx === st.pos[0].x && ny === st.pos[0].y) continue; if (onLane && lane.has(nk)) continue; if (!isFinite(dist[nk])) continue; if (!best || dist[nk] > best.score) best = { x: nx, y: ny, d, score: dist[nk] }; } if (!best) return; if (!onLane && best.score <= dist[here]) return; // nothing better: hold st.facing[1] = { dx: best.d.x, dy: best.d.y }; st.pos[1] = { x: best.x, y: best.y }; } function _parkAlleyRunnerTickBody(P) { const st = P.st, dyn = st.park.dyn, D = dyn.alley, B = dyn.ents[0], n = st.N; D.lastCharge = []; _parkAlleyRoundTick(P); // A DOZING BULL DOES NOTHING — it does not step, aim, or gore. Its committed aim survives the // sleep (a charge already promised is still owed), but it takes no new decision until it wakes. if (D.sleeping && !B.aim) { D.lastMove = 'sleep'; return; } // SHE HAS TO BE ABLE TO GET UP (2026-08-03). The dispatcher above reads // if (park.alley.runner) return _parkAlleyRunnerTick(P); // _parkAlleyCrawlTick(P); // — so the runner branch returned BEFORE the crawl ever ran, and a companion the bull put down // stayed down for the rest of the board: no crawl, no self-recovery, frozen where she fell. That // is why pink stopped moving. The crawl belongs to every alley board, not just the non-runner // one, so it runs here too, first, before anything this tick decides. _parkAlleyCrawlTick(P); const target = _parkAlleyRunnerTarget(P); if (target) { D.targetWho = target.who; B.targetWho = target.who; } if (B.stunned > 0) { B.stunned--; D.lastMove = 'rest'; return; } /* THE COMMITTED TELEGRAPH (2026-08-03 — the fix that made this board a game). The first cut of this tick computed a lane and BILLED IN THE SAME BEAT: the heart was gone before the walker could ever have seen the charge coming. That is not a danger a plan can answer, and this cell is graded on what a plan answers — so it failed twice over, measured: 완주 0/6 — every persona died, all six at the SAME turn with the SAME hearts and the SAME score, because the outcome never depended on the mind at all. 쌍 0/6 — an unavoidable event cannot pose a comparison. G, C and N all "chose" the same thing because none of their choices changed anything. The cure is this module's OWN non-runner tick (y25's fuse, re-derived here rather than shared, the module law): AIM first, fire PARK_ALLEY_TELEGRAPH beats later, and RE-READ THE RAY at the moment of firing so a body that stepped out of it is simply missed. That last clause is the whole game — it is what turns a tax into a decision. */ if (B.aim) { if (--B.aim.fuse > 0) { D.lastMove = 'aim'; return; } // Re-read the ray NOW, from where the bull actually stands, in the direction it committed to. // Nothing is remembered about who was standing there when the aim was taken: dodging works // precisely because this scan happens at the end of the fuse, not at the start. const dirIdx = B.aim.dirIdx, d = DIRS[dirIdx], lane = []; B.face = dirIdx; // it charges the way it is looking { let x = B.key % n, y = (B.key / n) | 0; for (;;) { x += d.x; y += d.y; if (x < 0 || y < 0 || x >= n || y >= n) break; const key = y * n + x; if (st.wall.has(key)) break; lane.push(key); } } let hitIdx = -1, hitWho = null; for (let i = 0; i < lane.length; i++) { const k = lane[i]; if (D.crate != null && k === D.crate) { hitIdx = i; hitWho = 'crate'; break; } if (_parkKey(st, st.pos[0]) === k) { hitIdx = i; hitWho = 'me'; break; } if (_parkKey(st, st.pos[1]) === k) { hitIdx = i; hitWho = 'mate'; break; } } const stopAt = hitWho ? (hitIdx > 0 ? lane[hitIdx - 1] : B.key) : (lane.length ? lane[lane.length - 1] : B.key); const run = hitWho ? lane.slice(0, Math.max(hitIdx, 0)) : lane.slice(); B.key = stopAt; B.stunned = 2; B.aim = null; D.lastCharge = run; D.lastMove = 'charge'; st.fx.push({ k: 'charge', cells: run.slice(), x: stopAt % n, y: (stopAt / n) | 0 }); if (hitWho === 'crate') { D.shields++; st.fx.push({ k: 'thud', x: lane[hitIdx] % n, y: (lane[hitIdx] / n) | 0 }); } else if (hitWho) { _parkAlleyRunnerHit(P, hitWho, dirIdx); } return; } if (!target) return; const dist = _parkAlleyRunnerDistances(st, target.key), current = dist[B.key]; const bodyKeys = new Set([_parkKey(st, st.pos[0]), _parkKey(st, st.pos[1])]); /* ALIGNMENT IS THE TRIGGER (2026-08-03). Previously the bull only committed when a lane also cut its walking distance by 3 or more — a scoring heuristic that could stare straight down an open corridor at the walker and decline, because the geometry did not pay enough. That is unreadable: the player's whole model of this animal is "do not stand in its line", and the animal has to honour that model on the beat the line exists. So the test is now the base board's own (_parkAlleyAligned's shape, re-derived): a clear straight ray from the bull to the target, no wall and no other body in between. If it is there, AIM — immediately, this beat. The ray is stored on the aim record because the app paints B.aim.lane as the red warning bar. Without it the telegraph was invisible and the fuse might as well not have existed. */ let best = null; for (let di = 0; di < DIRS.length; di++) { const d = DIRS[di], lane = []; let x = B.key % n, y = (B.key / n) | 0; for (;;) { x += d.x; y += d.y; if (x < 0 || y < 0 || x >= n || y >= n) break; const key = y * n + x; if (st.wall.has(key)) break; lane.push(key); if (key === target.key) { // Aligned. Prefer the LONGEST clear run when two directions both see him — a longer lane is // a louder warning and a longer commitment. if (!best || lane.length > best.lane.length) { best = { dirIdx: di, lane: lane.slice() }; } break; } // another body standing in the way ends the ray: the bull cannot see past it if (key === _parkKey(st, st.pos[0]) || key === _parkKey(st, st.pos[1])) break; } } if (best) { B.aim = { dirIdx: best.dirIdx, lane: best.lane.slice(), fuse: PARK_ALLEY_TELEGRAPH }; B.face = best.dirIdx; // it turns to face what it has decided to run at D.lastCharge = best.lane.slice(); D.lastMove = 'aim'; return; } // THE HORN AT ARM'S LENGTH also had to grow a rest (2026-08-03). Adjacency billed every single // beat and set no stun, so a bull that caught up with you took a heart per turn until you died — // the same "no answer exists" defect as the untelegraphed charge, one cell closer. The stun is // what gives the walker beats to leave, and leaving is the answer the board wants to measure. if (current <= 1) { // face the body it is goring, so the still frame reads const tx = target.key % n, ty = (target.key / n) | 0; const bxh = B.key % n, byh = (B.key / n) | 0; if (ty !== byh) B.face = ty < byh ? 0 : 1; else if (tx !== bxh) B.face = tx < bxh ? 2 : 3; _parkAlleyRunnerHit(P, target.who, B.face || 0); B.stunned = 2; D.lastMove = 'step'; return; } let next = null, nextDir = 0; const bx0 = B.key % n, by0 = (B.key / n) | 0; for (let di = 0; di < DIRS.length; di++) { const d = DIRS[di], nx = bx0 + d.x, ny = by0 + d.y, key = ny * n + nx; if (nx < 0 || ny < 0 || nx >= n || ny >= n || st.wall.has(key) || bodyKeys.has(key)) continue; if (dist[key] < current) { next = key; nextDir = di; break; } } if (next != null) { B.key = next; B.face = nextDir; D.lastMove = 'step'; st.fx.push({ k: 'chase', x: next % n, y: (next / n) | 0, dirIdx: nextDir }); } } // The runner tick's OUTER shape. The body has seven early returns — the stun, the fuse, the // resolution, the commit, the gore — and two things must happen on EVERY beat regardless of which // branch the animal took: the round-2 goal re-attaches to its tail, and the companion gets out of // its way. Wrapping is how those two stay unconditional without seven edits that can rot apart. function _parkAlleyRunnerTick(P) { _parkAlleyRunnerTickBody(P); _parkAlleyTailPad(P); _parkAlleyMateEvade(P); } // _parkAlleyCell(seed): the module's PUBLIC play-cell (C1 — a pure value constructor). function _parkAlleyCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'alley' } }; } // _parkAlleyPushDest(st, fromKey, toKey): where the crate lands if a body steps from fromKey into // the crate at toKey — or null when the shove is refused. The ledge dest-predicate re-derived: the // far cell must be on the board, not a wall (fences included), not under the bull, not under a // body, and not on a LIVE token (a crate parked on a gem would strand the chain — the Sokoban // soft-lock this module refuses by construction rather than by search). function _parkAlleyPushDest(st, fromKey, toKey) { const n = st.N, dyn = st.park.dyn, D = dyn && dyn.alley; if (!D || D.crate == null || toKey !== D.crate) return null; const dx = (toKey % n) - (fromKey % n), dy = ((toKey / n) | 0) - ((fromKey / n) | 0); if (Math.abs(dx) + Math.abs(dy) !== 1) return null; const bx = (toKey % n) + dx, by = ((toKey / n) | 0) + dy; if (bx < 0 || by < 0 || bx >= n || by >= n) return null; const bk = by * n + bx; if (st.wall.has(bk)) return null; const ent = dyn.ents && dyn.ents[0]; if (ent && bk === ent.key) return null; if (bk === _parkKey(st, st.pos[0]) || bk === _parkKey(st, st.pos[1])) return null; for (const t of st.tokens) if (t.alive && t.y * n + t.x === bk) return null; return bk; } // _parkAlleyMatePushDest(st, fromKey, toKey): where the COMPANION lands if the walker steps from // fromKey into her at toKey — or null when the shove is refused. The crate predicate above, with two // deliberate differences. A live PAD does not refuse: a pad is a place to stand, not an object, and // refusing it here would rebuild by the back door the very block this action exists to undo. And the // bull's cell refuses absolutely — shoving her under the horns would turn a claim into an execution. function _parkAlleyMatePushDest(st, fromKey, toKey) { const n = st.N, dyn = st.park.dyn; if (toKey !== _parkKey(st, st.pos[1])) return null; if (dyn.downed && !dyn.downed.rescued) return null; // never shove a body on the ground const dx = (toKey % n) - (fromKey % n), dy = ((toKey / n) | 0) - ((fromKey / n) | 0); if (Math.abs(dx) + Math.abs(dy) !== 1) return null; const bx = (toKey % n) + dx, by = ((toKey / n) | 0) + dy; if (bx < 0 || by < 0 || bx >= n || by >= n) return null; const bk = by * n + bx; if (st.wall.has(bk)) return null; const D = dyn.alley; if (D && D.crate != null && bk === D.crate) return null; const ent = dyn.ents && dyn.ents[0]; if (ent && bk === ent.key) return null; return bk; } // _parkAlleySegDist(P, segSet): step distance to the interpose segment over the walkable domain // (non-wall, off the bull, off the crate — you cannot approach through either body). y25's BFS with // the crate added to the blocked set. function _parkAlleySegDist(P, segSet) { const st = P.st, dyn = st.park.dyn, n = st.N; const dist = new Array(n * n).fill(Infinity); const ent = dyn && dyn.ents && dyn.ents[0], bullKey = ent ? ent.key : -1; const crate = dyn && dyn.alley ? dyn.alley.crate : null; const q = []; for (const k of segSet) { if (k !== bullKey && k !== crate && !st.wall.has(k)) { dist[k] = 0; q.push(k); } } for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || nk === bullKey || nk === crate) continue; if (dist[nk] > dist[k] + 1) { dist[nk] = dist[k] + 1; q.push(nk); } } } return dist; } // _parkAlleyCtx(P): the ONE per-state read both facets share — y25's ctx verbatim over the alley // geometry (segSet = locked-lane cells strictly between the bull and the aimed companion). function _parkAlleyCtx(P) { const st = P.st, dyn = st.park.dyn, n = st.N; const ent = dyn && dyn.ents && dyn.ents[0]; const aim = ent && ent.aim ? ent.aim : null; // round 0's yield question does NOT need an aim — it stands for the whole round, which is why it // is read before the early return rather than inside the aim branch. const Y = _parkAlleyYield(P); const ray = _parkAlleyRayCtx(P); if (!aim) return { aim: null, laneSet: new Set(), targetWho: null, segSet: new Set(), dist: new Array(n * n).fill(Infinity), onLaneAdj: false, reach: false, afford: false, yieldKey: Y.yieldKey, takeKey: Y.takeKey, ray }; const lane = aim.lane || [], laneSet = new Set(lane); const here = _parkKey(st, st.pos[0]); let onLaneAdj = laneSet.has(here); if (!onLaneAdj) { const x = here % n, y = (here / n) | 0; for (const d of DIRS) { const nk = (y + d.y) * n + (x + d.x); if ((x + d.x) >= 0 && (x + d.x) < n && (y + d.y) >= 0 && (y + d.y) < n && laneSet.has(nk)) { onLaneAdj = true; break; } } } const segSet = new Set(); if (aim.targetWho === 'mate') { const ci = lane.indexOf(_parkKey(st, st.pos[1])); for (let i = 0; i < ci; i++) segSet.add(lane[i]); // strictly BEFORE the companion (ci<=0 -> empty) } const dist = segSet.size ? _parkAlleySegDist(P, segSet) : new Array(n * n).fill(Infinity); return { aim, laneSet, targetWho: aim.targetWho, segSet, dist, onLaneAdj, reach: segSet.size ? isFinite(dist[here]) : false, afford: P.hearts > PARK_ALLEY_HEART_FLOOR, yieldKey: Y.yieldKey, takeKey: Y.takeKey, ray, }; } // _parkAlleyYield(P): round 0's care question, as a pair of cells. Returns the pad PINK HAS CLAIMED // and the pad left over, or nulls. Care's answer here is not "help her" — she needs no help walking // — it is "do not take the one she said was hers", which is a different and much harder thing for a // copier to fake: an imitator that walks at the nearest bright thing takes the claimed pad every // time, because the claimed one is deliberately the CHEAP one. function _parkAlleyYield(P) { const st = P.st, park = st.park, D = park.dyn && park.dyn.alley; if (!D || !D.goalClaim || (P.dest | 0) !== 0) return { yieldKey: null, takeKey: null }; const any = park.chainAnyOf && park.chainAnyOf[0]; if (!any) return { yieldKey: null, takeKey: null }; const n = st.N, claimed = st.tokens[D.goalClaim.token]; if (!claimed || !claimed.alive) return { yieldKey: null, takeKey: null }; let takeKey = null; for (const ti of any) { const t = st.tokens[ti]; if (t && t.alive && ti !== D.goalClaim.token) { takeKey = t.y * n + t.x; break; } } return { yieldKey: claimed.y * n + claimed.x, takeKey }; } // THE TWO FACET GATES — y25 verbatim (named once, used by BOTH engaged() and prefer(); y8's law), // plus round 0's yield, which is the same gate discipline applied to the new question. function _parkAlleyCLive(P, ctx) { return !!(ctx.aim && ctx.onLaneAdj); } function _parkAlleyNLive(P, ctx) { if (ctx.yieldKey != null && ctx.takeKey != null) return true; return !!(ctx.aim && ctx.targetWho === 'mate' && ctx.reach && ctx.afford); } // _parkAlleyCrawlTick(P): the downed companion's self-recovery — the y3 crawl as transplanted into // y25, re-derived here with the module's own bag (dyn.alley.downDir). Same cadence, same ceiling. function _parkAlleyCrawlTick(P) { const st = P.st, park = st.park, dyn = park.dyn; if (!dyn.downed || dyn.downed.rescued) return; if (dyn.beat % PARK_ALLEY_CRAWL_EVERY !== 0) return; const n = st.N, d = DIRS[dyn.alley.downDir]; const nx = st.pos[1].x + d.x, ny = st.pos[1].y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) { dyn.downed.rescued = true; dyn.downed.by = 'self'; return; } const nk = ny * n + nx; if (st.wall.has(nk) || (dyn.alley.crate != null && nk === dyn.alley.crate)) { dyn.downed.rescued = true; dyn.downed.by = 'self'; return; // a wall (or the crate) stands him up } if (_parkKey(st, st.pos[0]) === nk) return; dyn.downed.trail.push(_parkKey(st, st.pos[1])); st.facing[1] = { dx: d.x, dy: d.y }; st.pos[1] = { x: nx, y: ny }; dyn.downed.crawl++; st.fx.push({ k: 'crawl', x: nx, y: ny }); if (dyn.downed.crawl >= PARK_ALLEY_CRAWL_CAP) { dyn.downed.rescued = true; dyn.downed.by = 'self'; } } // _parkAlleyPlay(cell, persona): the faithful playout that records the signature observables — the // y25 triple (blocks / lane-entries / min approach to the interpose over mate-aim beats) plus this // module's own shields count (REPORTED, never gated: the header's confessed limit). function _parkAlleyPlay(cell, persona) { const P = parkStart(_parkAlleyBuild(cell)); let laneEntries = 0, minSegDist = Infinity; while (!P.over) { const dyn = P.st.park.dyn, B = dyn.ents && dyn.ents[0]; const preAim = B && B.aim ? B.aim : null; if (preAim && preAim.targetWho === 'mate') { const ctx = _parkAlleyCtx(P); if (ctx.segSet.size) { const d = ctx.dist[_parkKey(P.st, P.st.pos[0])]; if (d < minSegDist) minSegDist = d; } } parkStep(P, parkOracleMove(P, persona)); if (preAim && preAim.lane.indexOf(_parkKey(P.st, P.st.pos[0])) !== -1) laneEntries++; } P._alleyBlocks = P.st.park.dyn.alley.blocks; P._alleyShields = P.st.park.dyn.alley.shields; P._alleyLaneEntries = laneEntries; P._alleyMinSegDist = minSegDist; return P; } // _parkAlleySignature(playouts): PATH-TESTIMONY — care is read by how NEAR the walker steps to the // interpose (a landed block is dist 0, the strongest testimony, and on THIS board it is reachable), // never by the price. Safety stays off the street entirely; goal banks strictly more than safety // (the bonus its deep shortcut eats). // // THE PARTITION IS THE PAIR, NOT THE LEAD ATTITUDE (re-derived 2026-08-01, measured). This used to // split care-LED vs goal-LED vs safety-LED and demand careWorst < goalBest. That split cannot // survive a board that poses C-N, and the reason is a LAW of the blind scan rather than an accident // of this yard: a move can only discriminate C against N when the goal mind is silent or split, // because a SINGLETON goal preference g={m} makes both goal-first orders (GCN and GNC) prescribe // exactly {m} — the pairwise scan then has a consistent order on each side of C-vs-N and awards // nothing. And on any beat where the goal is silent or split, the persona that ranks care SECOND // behind goal (goal>care>safety) takes the care move by its own lexical order. So "no goal-led // persona ever approaches" and "every persona poses C-N" are mutually exclusive on any board. // MEASURED, seeds 1..8 x 6 personas = 48 faithful playouts: with the gag pair seated, minSegDist // comes out 0 for all three personas that rank care above caution (gcs/cgs/csg) and 1 for all // three that rank caution above care (gsc/sgc/scg) — a clean 3-3 split ON EXACTLY THE C-N LINE. // The old clause read that same board as a failure (sig 8/8 rejects) while the new one reads the // separation it actually has. This is the stronger claim, not the looser one: the observable now // answers the pair the module exists to pose, instead of a lead-attitude proxy for it. function _parkAlleySignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); // careOver[i] — does this persona rank CARE above CAUTION? (the (C,N) question itself; the att // keys are N=care and C=caution, so this is exactly "N before C" in the att order) const careOver = PARK_PERSONAS.map(p => p.indexOf('care') < p.indexOf('safety')); let gMin = Infinity, sMax = -Infinity; let nearWorst = -Infinity, farBest = Infinity; for (let i = 0; i < playouts.length; i++) { const P = playouts[i], d = P._alleyMinSegDist; if (careOver[i]) nearWorst = Math.max(nearWorst, d); else farBest = Math.min(farBest, d); if (top[i] === 'goal') gMin = Math.min(gMin, P.st.score[0]); if (top[i] === 'safety') { if (!(P._alleyBlocks === 0 && P._alleyLaneEntries === 0)) return false; sMax = Math.max(sMax, P.st.score[0]); } } if (!isFinite(nearWorst) || !isFinite(farBest)) return false; // the scene must be POSED (non-vacuity) if (!(nearWorst < farBest)) return false; // the whole separation, one line return gMin > sMax; } // _parkAlleyAdmissible(cell): generate-then-filter over the full persona set — y25's gate shape // with the whys tallied LOUDLY. complete/dead/matelost per playout, then the signature, then the // posed-pair blind recovery with the C-N scene counted (posed >= 2). const _PARK_ALLEY_WHYS = { complete: 0, dead: 0, matelost: 0, sig: 0, nopose: 0, norec: 0 }; function parkAlleyWhys() { return { ..._PARK_ALLEY_WHYS }; } function _parkAlleyAdmissible(cell) { const playouts = []; for (const persona of PARK_PERSONAS) { const P = _parkAlleyPlay(cell, persona); if (P.reason === 'death') { _PARK_ALLEY_WHYS.dead++; return false; } if (P.reason !== 'complete') { _PARK_ALLEY_WHYS.complete++; return false; } const dyn = P.st.park.dyn; if (dyn.downed && !dyn.downed.rescued) { _PARK_ALLEY_WHYS.matelost++; return false; } playouts.push(P); } if (!_parkAlleySignature(playouts)) { _PARK_ALLEY_WHYS.sig++; return false; } let posed = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_ALLEY_PAIRS) { if (!(parkPairExpressed(_parkAlleyBuild(cell), playouts[i].moves, pair) > 0)) continue; if (pair[0] === 'C' && pair[1] === 'N') posed++; const expect = _parkPushPairDir(persona, pair); if (!parkRecoverPairLex(_parkAlleyBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_ALLEY_WHYS.norec++; return false; } } } if (posed < 2) { _PARK_ALLEY_WHYS.nopose++; return false; } return true; } // ---- THE REGISTRATION. Everything the engine body knows about y50 hangs here. PARK_FIELD_MECHS.alley = { build: _parkAlleyBuild, cell: _parkAlleyCell, admits: _parkAlleyAdmissible, // THE READS — y25's two facets verbatim (the hybrid hardens the YARD, not the minds). reads: { ctx: _parkAlleyCtx, C: { // 두 채널의 OR. 옛 채널은 조준이 걸리고 레인 옆일 때(_parkAlleyCLive), 새 채널은 시연 // 판에서 도즈 시계가 임박을 알릴 때. 모듈 facet 은 마음을 켤 수는 있어도 방향은 못 // 돌린다 — prefer 아래에서 배송 태도의 선호를 교집합으로만 좁힌다. engaged: (P, ctx) => _parkAlleyCLive(P, ctx) || ctx.ray.live, prefer: (P, legal, ctx) => { const live = _parkAlleyCLive(P, ctx); const base = new Set(); for (const c of legal) if (!live || !ctx.laneSet.has(c.key)) base.add(c.k); if (!base.size) for (const c of legal) base.add(c.k); if (!ctx.ray.live) return base; // 광선을 더 뺀다. 전부 빠지면 base 로 되돌린다 — 빈 선호는 _parkLexSet 의 "비지 // 않을 때만 좁힌다"에서 통째로 무시되어, 안전이 말한 적 없는 것과 구별이 안 된다. const out = new Set(); for (const c of legal) if (base.has(c.k) && !ctx.ray.rays.has(c.key)) out.add(c.k); return out.size ? out : base; }, }, N: { engaged: (P, ctx) => _parkAlleyNLive(P, ctx), prefer: (P, legal, ctx) => { const out = new Set(); if (!_parkAlleyNLive(P, ctx)) { for (const c of legal) out.add(c.k); return out; } // ROUND 0 — LEAVE HERS ALONE. Care refuses the pad pink claimed and walks to the other one. // The refusal is the measurable half: a mind that merely "also reaches a pad" is doing what // goal does. What separates them is WHICH pad, and the claimed one is the cheap one on // purpose, so taking it is exactly what a hurried or imitative mind does. if (ctx.yieldKey != null && ctx.takeKey != null) { const st = P.st, n = st.N, tx = ctx.takeKey % n, ty = (ctx.takeKey / n) | 0; const dOf = (x, y) => Math.abs(x - tx) + Math.abs(y - ty); const cur = dOf(st.pos[0].x, st.pos[0].y); for (const c of legal) { if (c.key === ctx.yieldKey) continue; // never step into hers if (dOf(c.x, c.y) < cur) out.add(c.k); } if (out.size) return out; for (const c of legal) if (c.key !== ctx.yieldKey) out.add(c.k); if (out.size) return out; for (const c of legal) out.add(c.k); return out; } const here = _parkKey(P.st, P.st.pos[0]); if (ctx.segSet.has(here)) { out.add('stay'); return out; } const cur = ctx.dist[here]; for (const c of legal) if (ctx.dist[c.key] < cur) out.add(c.k); if (!out.size) out.add('stay'); return out; }, }, }, // THE MASK — the bull's body and the crate are solid to both bodies; the route metric sees the // crate as open ground (ledge law: a masked cell is Infinity to the argmin, and a push that could // never win an argmin is no push at all). The downed freeze is y25/y3 verbatim. legalMask: (P, key, who) => { const dyn = P.st.park.dyn; if (who === 'mate' && dyn && dyn.downed && !dyn.downed.rescued) return true; if (who !== 'me' && who !== 'mate') return false; const D = dyn && dyn.alley; if (D && D.crate != null && key === D.crate) return true; const ent = dyn && dyn.ents && dyn.ents[0]; return !!ent && key === ent.key; }, // THE ADDITIVE OVERRIDE — stepping into the crate is a PUSH when the far cell is free (the y26 // action-idiom: an action wearing a move's clothes). The walker only; the companion never shoves. legalAdd: (P, key, who) => { const st = P.st, D = st.park.dyn && st.park.dyn.alley; if (!D || who !== 'me') return false; // THE SECOND SHOVE (2026-08-04). Same idiom as the crate one line down — an action wearing a // move's clothes — and it exists because round 0 has to stay a question after she can walk. if (st.park.alley && st.park.alley.runner && key === _parkKey(st, st.pos[1])) { return _parkAlleyMatePushDest(st, _parkKey(st, st.pos[0]), key) != null; } if (key !== D.crate) return false; return _parkAlleyPushDest(st, _parkKey(st, st.pos[0]), key) != null; }, // THE CONSEQUENCE — the shove: crate one cell on, pusher put back (never a real move). onEnter: (P, ev) => { const st = P.st, dyn = st.park.dyn, D = dyn && dyn.alley; if (st.park.alley && st.park.alley.runner && ev.fromKey !== ev.toKey && ev.toKey === _parkKey(st, st.pos[1])) { const dest = _parkAlleyMatePushDest(st, ev.fromKey, ev.toKey); if (dest != null) { const nx = dest % st.N, ny = (dest / st.N) | 0; st.facing[1] = { dx: nx - st.pos[1].x, dy: ny - st.pos[1].y }; st.pos[1] = { x: nx, y: ny }; st.fx.push({ k: 'push', x: nx, y: ny }); // the crate's own flash, reused verbatim } return; // THE PUSHER STAYS PUT. The crate hook puts him back; this one must // not, because taking that cell is the entire content of the action. } if (!D || D.crate == null || ev.toKey !== D.crate) return; const dest = _parkAlleyPushDest(st, ev.fromKey, ev.toKey); if (dest != null) { D.crate = dest; st.fx.push({ k: 'push', x: dest % st.N, y: (dest / st.N) | 0 }); // render hook (ZERO-TEXT) } st.pos[0] = { x: ev.from.x, y: ev.from.y }; }, // THE WORLD ADVANCES — y25's three branches with ONE new clause in the resolution scan: the // stored lane is walked in order and the first OBSTACLE found decides — // the CRATE -> the shield: the bull stops one cell short, stuns, ♥ untouched, shields++ // (a crate shoved onto the locked lane mid-fuse is found here: the human's play). // the WALKER -> ♥-1, blocks++, stop one short, stun (the body-block, y25 verbatim). // the MATE -> down in the y3 shape; the crawl above walks him off; stop one short, stun. // nobody -> run the whole lane, stun at the far end. tick: (P, ev) => { const st = P.st, park = st.park, dyn = park.dyn; const B = dyn.ents && dyn.ents[0]; if (!B) return; if (park.alley && park.alley.runner) return _parkAlleyRunnerTick(P); _parkAlleyCrawlTick(P); if (B.stunned > 0) { B.stunned--; return; } if (B.aim) { if (--B.aim.fuse > 0) return; const { lane, dirIdx } = B.aim; const D = dyn.alley; let hitIdx = -1, hitWho = null; for (let i = 0; i < lane.length; i++) { const k = lane[i]; if (D.crate != null && k === D.crate) { hitIdx = i; hitWho = 'crate'; break; } if (_parkKey(st, st.pos[0]) === k) { hitIdx = i; hitWho = 'me'; break; } if (_parkKey(st, st.pos[1]) === k) { hitIdx = i; hitWho = 'mate'; break; } } if (hitWho === 'crate') { D.shields++; B.key = hitIdx > 0 ? lane[hitIdx - 1] : B.key; B.stunned = PARK_ALLEY_STUN; st.fx.push({ k: 'thud', x: lane[hitIdx] % st.N, y: (lane[hitIdx] / st.N) | 0 }); } else if (hitWho === 'me') { P.hearts -= 1; D.blocks++; B.key = hitIdx > 0 ? lane[hitIdx - 1] : B.key; B.stunned = PARK_ALLEY_STUN; st.fx.push({ k: 'thud', x: lane[hitIdx] % st.N, y: (lane[hitIdx] / st.N) | 0 }); } else if (hitWho === 'mate') { dyn.downed = { rescued: false, crawl: 0, by: null, trail: [] }; D.downDir = dirIdx; B.key = hitIdx > 0 ? lane[hitIdx - 1] : B.key; B.stunned = PARK_ALLEY_STUN; st.fx.push({ k: 'thud', x: lane[hitIdx] % st.N, y: (lane[hitIdx] / st.N) | 0 }); } else { B.key = lane.length ? lane[lane.length - 1] : B.key; B.stunned = PARK_ALLEY_STUN; } B.aim = null; return; } const al = _parkAlleyAligned(st); if (al) B.aim = { lane: al.lane, dirIdx: al.dirIdx, targetWho: al.targetWho, fuse: PARK_ALLEY_TELEGRAPH }; }, }; // PARK_ALLEY_SHIP_SEED / _parkAlleyRecovers / PARK_ALLEY_SHIPPABLE — the MODULE bar, read by // CAMP-CROSS-SWEEP's F4 through the FIELD_SHIPPABLE map. DERIVED as of 2026-08-01, replacing the // hand-set `false` literal this block used to carry: the quantity is the same one every other // module pins (PARK_TROLLEY_SHIPPABLE / PARK_SIEGE_SHIPPABLE), the CALIBRATED 6/6 blind order // recovery on the module's shipped-seed cell, read at PARK_CAL_TURNS because that is the skip the // product's own readout uses. // IT COMPUTES false, AND THAT IS THE HONEST ANSWER, NOT A PLACEHOLDER (measured 2026-08-01, // seeds 1..40 x 6 personas = 240 faithful playouts): calibrated 6/6 recovery on 0/40 seeds. // The shortfall is ONE named pair on THREE named personas — G-N is never posed for the three // personas that rank caution above care (gsc/sgc/scg, G-N expressed 0/24 over seeds 1..8), and // parkRecoverOrder returns null the moment ANY pair ties. G-C and C-N are both 48/48 over the // same 48 runs. See docs/superpowers/plans/2026-08-01-y50-second-attempt.md for the lever table. // Deriving it rather than pinning it means the day that scene is built this flag flips ITSELF. const PARK_ALLEY_SHIP_SEED = 1; function _parkAlleyRecovers(cell) { for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkAlleyBuild(cell), persona); if (P.reason !== 'complete') return false; const r = parkRecoverOrder(_parkAlleyBuild(cell), P.moves, PARK_CAL_TURNS); if (!r || r.join() !== persona.join()) return false; } return true; } const PARK_ALLEY_SHIPPABLE = _parkAlleyRecovers(_parkAlleyCell(PARK_ALLEY_SHIP_SEED)); /* ============ END ALLEY FIELD MODULE (the ladder-maze yard, the crate-opaque lane and its telegraphed charge, the ledge-idiom shove and its shield clause in the resolution scan, the y25 reads and path-testimony verbatim on a board whose spawn finally reaches the interpose, and the admission gate) ============ */ /* ============ BEACON FIELD MODULE (y53 "회전 등대", plan 2026-07-27) ============ */ /* MUGUNGHWA 3.0 — THE GAZE GETS A DIRECTION. A lighthouse doll stands on a walled tower at the yard's center and sweeps the four quadrants on a public 8-beat clock. Each facing holds for two beats: a head-TURN beat (free — the telegraph) and then a STARE beat, on which a step that touches the lit quadrant costs one heart. WHERE you stand now matters as much as WHEN you move: a step wholly in the dark is always free, 'stay' is always innocent, and the light is a quarter of the yard rather than all of it — so a faithful plan can cross this yard for zero hearts. That last clause is the economy invariant this board inherits from y51's measured collapse (two dolls billing three beats of six needed five hearts and admitted 0 of 40 seeds). THE HEART CHANNEL IS ONE, and it is the stare (dyn.beacon.spotted). There is no deep field, no water, no second bill. The companion is never killed: caught in the light while the doll stares, he is stunned for exactly PARK_BEACON_STUN beats (1 — y46's measured pin; 2 deletes the very C-vs-N scene this cell exists to pose). THE THREE LEVERS ARE BUILT IN, not retrofitted (y46 spent a promotion session adding them): ① the looking span is SPLIT — the turn telegraphs, only the stare bills; ② the goal mind YIELDS its empty prefer on the turn beat, which is the only beat where caution and care answer alone; ③ his contract gem sits one column OFF his own lane, so "walk toward the shiny thing" and "stand at his shoulder" stop coinciding and the surface imitator has nothing to copy. NO HUMAN-ONLY VERB. No summon, no pull, no click seam — every affordance on this board is one the oracle itself can take, which keeps the module bar, the pairing bar and the mimic control all measuring the same game a person plays. */ const PARK_BEACON_N = 13; // THREE BEATS PER QUADRANT: the beam lands (a free head-TURN, the telegraph), it STARES (the one // billing beat), and it leaves (free again, and the one beat on which no mind here is awake). // Twelve beats to a revolution. // PINNED BY A 1,300-CONFIGURATION SWEEP (2026-07-27, seeds 1..24; the harness sits beside this file // and every number below is reproducible from it). The span is not a taste call — it is the only // axis that moved BOTH bars at once. Spans of 2 and 4 each score admission as well or better in // isolation (the 2-beat span reached 14/24 admitted against this one's 9/24), and both take the // MODULE's calibrated order-recovery bar to ZERO seeds out of 24 — a cell that can never derive a // ship pin, no matter how many of its boards are playable. This span holds 6 seeds at 6/6. const PARK_BEACON_SPAN = 3; const PARK_BEACON_STARE_AT = 1; // the middle beat of the span const PARK_BEACON_PERIOD = PARK_BEACON_SPAN * 4; // 12 beats for a full revolution const PARK_BEACON_STUN = 1; const PARK_BEACON_LEAD = 1; // THE HOLD RADIUS — how close a friend must stand for the light to pass his companion by. // ONE, not two. MEASURED: at radius 2 the yard admits more boards (14 of 24 against 9) and yet // derives NO ship pin at all, because every walker who merely passes his alley on the way to the // finish shelters him by accident — a protection nobody chose is a protection no reading can // attribute. At radius 1 the shelter has to be aimed. const PARK_BEACON_HOLD = 1; const PARK_BEACON_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // CYCLIC order N,E,S,W — spin +1 turns clockwise, -1 anticlockwise. (DIRS elsewhere is the // tiebreak order U,D,L,R and must not be reused here: rotation needs adjacency, not tiebreak.) const _PARK_BEACON_DIRS = [{ x: 0, y: -1 }, { x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }]; // ---- THE PUBLIC CLOCK READS. Pure beat functions; nothing here advances anything. function _parkBeaconFacing(st) { const B = st.park && st.park.beacon; if (!B) return 0; const beat = (st.park.dyn ? st.park.dyn.beat : 0); return (B.f0 + (B.spin * ((beat / PARK_BEACON_SPAN) | 0)) % 4 + 8) % 4; } function _parkBeaconTurning(st) { return ((st.park && st.park.dyn ? st.park.dyn.beat : 0) % PARK_BEACON_SPAN) === 0; } function _parkBeaconStaring(st) { return ((st.park && st.park.dyn ? st.park.dyn.beat : 0) % PARK_BEACON_SPAN) === PARK_BEACON_STARE_AT; } // beats remaining until the next stare (0 when this beat IS the stare) — the care mind's LEAD read. function _parkBeaconTillStare(st) { const ph = (st.park && st.park.dyn ? st.park.dyn.beat : 0) % PARK_BEACON_SPAN; return ph <= PARK_BEACON_STARE_AT ? PARK_BEACON_STARE_AT - ph : PARK_BEACON_SPAN - ph + PARK_BEACON_STARE_AT; } // _parkBeaconNextFace(st): the quadrant the beam turns to NEXT. The ENGINE owns the timetable and // the app's dotted preview draws THIS — seen danger and priced danger never drift (flood's law). function _parkBeaconNextFace(st) { const B = st.park && st.park.beacon; return B ? (_parkBeaconFacing(st) + B.spin + 4) % 4 : 0; } // _parkBeaconCone(st, f): the lit quadrant for facing f — forward > 0 and |lateral| < forward. // THE INEQUALITY IS STRICT, and that is the yard's other piece of architecture: the four diagonals // are lit by NOBODY, so four dark spokes run from the tower to the corners and a walker who wants // to cross without paying has somewhere to stand. MEASURED against the inclusive cone (diagonals // lit by two facings, no dark spokes at all): the strict cone is what carries the module's // order-recovery bar from 0 seeds to 6 of 24, because a caution mind with nowhere safe to stand // cannot answer differently from a hurried one. The tower cell and the border are never lit. function _parkBeaconCone(st, f) { const n = st.N, B = st.park.beacon, d = _PARK_BEACON_DIRS[((f % 4) + 4) % 4]; const cx = B.towerKey % n, cy = (B.towerKey / n) | 0; const out = new Set(); for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) { const k = y * n + x; if (st.wall.has(k)) continue; const fwd = (x - cx) * d.x + (y - cy) * d.y; const lat = Math.abs((x - cx) * -d.y + (y - cy) * d.x); if (fwd > 0 && lat < fwd) out.add(k); } return out; } // _parkBeaconBuild(cell): the y46 yard re-derived (same placement arithmetic, a beacon-own rng salt // so the layout stream is this module's), with the doll moved from the wall to a CENTER TOWER and // two extra seed draws — the starting quadrant and the spin. Those two are the anti-mimic axes: a // copier that learned "hug the left wall" on one seed meets the light coming the other way on the // next. Seed-pure and persona-blind (C1). function _parkBeaconBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_BEACON_N, c = (n - 1) >> 1; const r = rng((seed * 5261 + 4111) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const sd = cs(0, 3), d = _PARK_BEACON_DIRS[sd]; // the yard's advance axis const lat = { x: Math.abs(d.y), y: Math.abs(d.x) }; const fl = cs(0, 1) ? 1 : -1; const f0 = cs(0, 3); // the quadrant the beam starts on const spin = cs(0, 1) ? 1 : -1; // and which way it turns const A = (fwd, side) => ({ x: c + d.x * fwd + lat.x * side * fl, y: c + d.y * fwd + lat.y * side * fl }); const sp = cs(-1, 0); const spawn = A(-4, sp); const finish = A(5, 0); // HIS ERRAND CROSSES THE YARD. The station and the retire seat sit on one flank, his contract gem // on the OTHER — so his walk carries him past the tower and through two opposite quadrants, and // the beam therefore finds him TWICE a revolution on a beat anyone can count to. That turns the // care scene from a coin-flip into an APPOINTMENT that comes round more than once: the light walks // his lane, and a friend either is standing there or is not. // MEASURED against the alternative of keeping the whole errand inside ONE quadrant: the beam then // swept his alley once per revolution, the run often ended before the second sweep, and the cell // rejected on `nobodyHarmed` or on an empty care confession — 3/24 seeds against this crossing // errand's 14/24. // MEASURED with the errand spread across quadrants instead (station forward -3 lateral 2, gem at // the yard's middle): the light found him at unscheduled moments and the care-led walker lost him // on 29 of 40 seeds — the signature's care clause failed on luck, not on choices. MEASURED again // with the alley pushed out to lateral 4-5, one quadrant but far from everything: the care mind // could not cross the yard inside the beam's warning and stood at his shoulder on 0 of 40 seeds. // SO THE ALLEY HUGS THE TOWER. Spawn and finish sit on the axis and the tower is a wall, so every // walker must round it on one side or the other — and one of those sides IS his alley. Care and // haste are then the same two cells apart, which is the only place a choice between them exists. // THE GEM STILL SITS ONE COLUMN OFF HIS LANE (y46's measured anti-mimic pin, adopted on day one // rather than after a failed promotion): "walk toward the shiny thing" must not coincide with // "stand at his shoulder", or a greedy copier reads as care. const station = A(-2, 3), mateGem = A(0, -3), retire = A(2, 3); const tower = { x: c, y: c }; const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) wall.add(y * n + x); wall.add(tower.y * n + tower.x); // the lighthouse itself // NO DEEP FIELD (y29's channel purity, inherited): the heart channel is the stare alone. const deep = new Set(), verge = new Set(), walkway = new Set(); for (let k = 0; k < n * n; k++) if (!wall.has(k)) walkway.add(k); const distDeep = new Array(n * n).fill(Infinity); const tokens = [ { x: finish.x, y: finish.y, v: 1, alive: true, guard: false }, // the finish (chain 0) { x: mateGem.x, y: mateGem.y, v: cs(1, 2), alive: true, guard: false }, // his contract gem ]; const park = { N: n, seed, k: 0, fieldMech: 'beacon', deep, verge, walkway, distDeep, beacon: { side: sd, fl, f0, spin, towerKey: tower.y * n + tower.x, finishTi: 0, finishKey: finish.y * n + finish.x, span: PARK_BEACON_SPAN, stun: PARK_BEACON_STUN, laneKeys: [station.y * n + station.x, mateGem.y * n + mateGem.x] }, clusters: tokens.map(t => ({ x: t.x, y: t.y, v: t.v })), chain: [0], needPairs: true, contracts: [{ gem: 1, station }], retire, spawn, companionSpawn: { x: station.x, y: station.y }, trig: n * 2, cap: 140, minTurns: 6, cautionD: 2, damage: 1, cell: _parkBeaconCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: station.x, y: station.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // every dyn member EXISTS at build (the _parkDeepClone rule). park.dyn.beacon = { spotted: [], litCross: 0, rotations: 0, holds: [], mateCaught: 0, mateStun: 0, matePrev: null, starePrev: false, facePrev: f0 }; return st; } // _parkBeaconCell(seed): the module's PUBLIC play-cell (C1 — no persona parameter exists). function _parkBeaconCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'beacon' } }; } function _parkBeaconNear(st) { return Math.max(Math.abs(st.pos[0].x - st.pos[1].x), Math.abs(st.pos[0].y - st.pos[1].y)); } // _parkBeaconApproach(P): step distance to the COMPANION's cell over ground the walker may use. // y46's shape with nothing subtracted — on this board no ground ever dies, only the light moves. function _parkBeaconApproach(P) { const st = P.st, n = st.N; const dist = new Array(n * n).fill(Infinity); const src = _parkKey(st, st.pos[1]); dist[src] = 0; const q = [src]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk)) continue; if (dist[nk] > dist[k] + 1) { dist[nk] = dist[k] + 1; q.push(nk); } } } return dist; } // _parkBeaconTouches(st, cone, key): is this cell lit, or one step from lit? The minds engage on // PRESENCE, not on the clock — see the ctx header. function _parkBeaconTouches(st, cone, key) { if (cone.has(key)) return true; const n = st.N, x = key % n, y = (key / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; if (cone.has(ny * n + nx)) return true; } return false; } /* _parkBeaconCtx(P) — THE READS' ONE COMPUTATION, and the place this cell parts company with y29. On the statue yards the minds wake on a CLOCK phase (the song is cold, the gaze is hot) because the gaze covers the whole yard. Here the beam always points somewhere, so a clock-keyed engage would be true on every single beat — a mind that is never cold is not a mind, and SEAM-COLD-PREFER would have no state to probe. So the engage is keyed on PRESENCE instead: caution wakes when the light is on him or one step away, care wakes when it is on HIM or one step from him. Both are pure reads of public state (C1), both go quiet on the far side of the yard, and both are exactly the scene each mind is named for. */ function _parkBeaconCtx(P) { const st = P.st, D = st.park.dyn && st.park.dyn.beacon; const stare = _parkBeaconStaring(st), turn = _parkBeaconTurning(st); const face = _parkBeaconFacing(st), cone = _parkBeaconCone(st, face); const near = _parkBeaconNear(st); const live = !!(D && D.mateStun === 0 && P.mode !== 'done'); const here = _parkKey(st, st.pos[0]), mateKey = _parkKey(st, st.pos[1]); const hotHere = cone.has(here); const coneNext = _parkBeaconCone(st, _parkBeaconNextFace(st)); const engagedC = _parkBeaconTouches(st, cone, here); // CARE GETS THREE SPANS OF WARNING, NOT ONE. He does not watch the tower, so the light takes him // wherever it finds him — and a friend who only starts walking when the beam lands can never // arrive in time. So the care mind wakes on the quadrant the beam is on and on the two it turns // to next. MEASURED: two spans of warning leaves the shepherd a step short often enough to cost // two seeds of the module bar. const mateHot = _parkBeaconTouches(st, cone, mateKey); const coneAfter = _parkBeaconCone(st, (_parkBeaconNextFace(st) + st.park.beacon.spin + 4) % 4); const engagedN = live && (mateHot || _parkBeaconTouches(st, coneNext, mateKey) || _parkBeaconTouches(st, coneAfter, mateKey)); return { stare, turn, face, cone, coneNext, coneAfter, near, live, here, hotHere, mateHot, engagedC, engagedN, dist: (engagedN && near > 1) ? _parkBeaconApproach(P) : null }; } /* ---- y53 BEACON — the filter layer: faithful playout, field signature, admissibility. y46's three siblings re-derived, with the hybrid's own non-vacuity clause: a run the beam never swept twice never met this cell at all (norot). */ function _parkBeaconPlay(cell, persona) { const st = _parkBeaconBuild(cell); const P = parkStart(st); while (!P.over) parkStep(P, parkOracleMove(P, persona)); const D = st.park.dyn.beacon; P._beaconSpotted = D.spotted.length; // steps billed by the stare (unit: hearts) P._beaconMateCaught = D.mateCaught; P._beaconHolds = D.holds.length; P._beaconMateHome = st.score[1] > 0; P._beaconRots = D.rotations; // quadrants the beam swept while the run was live P._beaconLitCross = D.litCross; // steps that touched the light at all (billed or not) return P; } // _parkBeaconSignature(playouts): y46's three observables re-derived on the rotating beam — // SAFETY is never billed, GOAL is billed and is strictly faster than caution's worst, CARE holds // and never loses him — plus the harm non-vacuity (somebody does lose him to the light). function _parkBeaconSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); let safeWorst = -Infinity, goalWorst = -Infinity, careSeen = 0, harmed = 0; for (let i = 0; i < playouts.length; i++) { const P = playouts[i]; if (P._beaconMateCaught > 0) harmed++; if (top[i] === 'safety') { if (P._beaconSpotted !== 0) return false; safeWorst = Math.max(safeWorst, P.turns); } if (top[i] === 'goal') { if (P._beaconSpotted < 1) return false; goalWorst = Math.max(goalWorst, P.turns); } if (top[i] === 'care') { careSeen++; if (P._beaconMateCaught !== 0) return false; if (P._beaconHolds < 1) return false; } } if (!isFinite(safeWorst) || !isFinite(goalWorst)) return false; if (!(goalWorst < safeWorst)) return false; if (harmed < 1) return false; return careSeen === 2; } // FIRST-REASON HISTOGRAM (storm's warning, restated): every branch early-returns, one key per reject. const _PARK_BEACON_WHYS = { complete: 0, dead: 0, matelost: 0, sig: 0, nocn: 0, norec: 0, norot: 0 }; function _parkBeaconAdmissible(cell) { const playouts = []; for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = _parkBeaconPlay(cell, PARK_PERSONAS[i]); if (P.reason === 'death') { _PARK_BEACON_WHYS.dead++; return false; } if (P.reason !== 'complete') { _PARK_BEACON_WHYS.complete++; return false; } if (P.hearts < 1) { _PARK_BEACON_WHYS.dead++; return false; } if (PARK_PERSONAS[i][0] === 'care' && !P._beaconMateHome) { _PARK_BEACON_WHYS.matelost++; return false; } // THE HYBRID NON-VACUITY: the beam must sweep at least two quadrants while the run is live, on // EVERY faithful playout — a cell whose runs all end inside one span is a walking cell wearing // a beacon id. if (P._beaconRots < 2) { _PARK_BEACON_WHYS.norot++; return false; } playouts.push(P); } if (!_parkBeaconSignature(playouts)) { _PARK_BEACON_WHYS.sig++; return false; } let posed = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_BEACON_PAIRS) { if (!(parkPairExpressed(_parkBeaconBuild(cell), playouts[i].moves, pair) > 0)) continue; if (pair[0] === 'C' && pair[1] === 'N') posed++; const expect = _parkPushPairDir(persona, pair); if (!parkRecoverPairLex(_parkBeaconBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_BEACON_WHYS.norec++; return false; } } } if (posed < 2) { _PARK_BEACON_WHYS.nocn++; return false; } return true; } function parkBeaconWhys() { return { ..._PARK_BEACON_WHYS }; } // ---- THE REGISTRATION. Everything the engine body knows about y53 hangs here. PARK_FIELD_MECHS.beacon = { build: _parkBeaconBuild, cell: _parkBeaconCell, admits: _parkBeaconAdmissible, reads: { ctx: _parkBeaconCtx, // THE GOAL MIND YIELDS ON THE HEAD-TURN (y17 fire's lever by way of y46, built in from the // first commit rather than found in a promotion session). The arithmetic that licenses it: a // step taken on the turn beat is free either way, and the same cell is still free on the next // song of the clock — nothing the harvest wants can be lost by letting caution and care answer // this one beat. An EMPTY prefer says exactly that, and the seam contract drops a mind from the // turn when its narrowing is empty, so C-vs-N is finally staged. // OUTSIDE THE SPAN IT RETURNS THE FULL LEGAL SET, and that is not defensive coding: a module // prefer() RUNS IN STATES ITS OWN engaged() REJECTED, so an empty set here would silence the // goal mind on every stare beat too. G: { engaged: (P, ctx) => !!ctx && ctx.turn, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.turn) { for (const c of legal) out.add(c.k); return out; } return out; // yield — out of THIS decision, back on the stare }, }, // CAUTION READS THE LIGHT, NOT THE CLOCK. Standing in the lit quadrant when the stare lands, // every step bills — including a step OUT — so the only free answer is to freeze. Everywhere // else the mind simply refuses to walk into the light. C: { engaged: (P, ctx) => !!ctx && ctx.engagedC, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.engagedC) { for (const c of legal) out.add(c.k); return out; } if (ctx.stare && ctx.hotHere) { for (const c of legal) if (c.k === 'stay') out.add(c.k); if (out.size) return out; } for (const c of legal) if (c.k === 'stay' || !ctx.cone.has(c.key)) out.add(c.k); if (out.size) return out; for (const c of legal) out.add(c.k); return out; }, }, // CARE WAKES WHEN THE LIGHT IS ON HIM. y46's measured shape, verbatim in its two hard clauses: // ADJACENCY, NOT INTERPOSITION (the between-cell is usually out of reach inside one beat, and // the mind falls through to 'stay', which stages nothing), and NO DISTANCE-CLOSING FALLBACK on // the billing beat (a copier that walks toward the shiny thing already does that, so care that // is legible to an imitator is not care this cell can measure). The approach branch lives on // the head-turn instead, where it costs nobody a heart. N: { engaged: (P, ctx) => !!ctx && ctx.engagedN, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.engagedN) { for (const c of legal) out.add(c.k); return out; } const co = P.st.pos[1]; const cheb = (c) => Math.max(Math.abs(c.x - co.x), Math.abs(c.y - co.y)); if (ctx.near <= PARK_BEACON_HOLD) { // ALREADY AT HIS SHOULDER: care is SATISFIED, not commanding. Every move that keeps him // sheltered is equally caring, so the mind hands the whole ring back and lets the next // mind choose inside it. MEASURED before this clause, when care answered 'stay' here: // the care-led runs never staged GOAL-versus-CAUTION at all (every unrecovered crossing // run was G-C unposed, and both care-top personas failed on it), because the deciding // mind left the others a set of one. Care that leaves no room decides the whole order // by itself, and an order nobody can read is not a measurement. for (const c of legal) if (cheb(c) <= PARK_BEACON_HOLD) out.add(c.k); if (out.size) return out; } else if (ctx.stare) { for (const c of legal) if (c.k !== 'stay' && cheb(c) <= PARK_BEACON_HOLD) out.add(c.k); if (out.size) return out; for (const c of legal) if (c.k === 'stay') out.add(c.k); if (out.size) return out; } else if (ctx.dist) { const cur = ctx.dist[ctx.here]; if (isFinite(cur)) for (const c of legal) { if (P.prev && P.prev.x === c.x && P.prev.y === c.y) continue; if (ctx.dist[c.key] < cur) out.add(c.k); } if (out.size) return out; } for (const c of legal) out.add(c.k); return out; }, }, }, // THE MASK — the mate domain only, y29's hold grammar: stunned, or standing at the walker's // shoulder while the beam stares. No ground is ever masked on this board, in any domain, which // is what keeps the route-metric Infinity trap structurally out of reach. legalMask: (P, key, who) => { if (who !== 'mate') return false; const st = P.st, D = st.park.dyn && st.park.dyn.beacon; if (!st.park.beacon || !D) return false; if (D.mateStun > 0) return true; return _parkBeaconStaring(st) && _parkBeaconNear(st) <= PARK_BEACON_HOLD; }, // THE STARE TOLL — billed on the beat the walker DECIDED on, for a body that was ALREADY STANDING // in the lit quadrant and moved. 'stay' is always innocent, the head-turn is always free, and // stepping INTO the light is free too. // BOTH ENDS BILL: to move under the stare with either foot in the light costs a heart. An // origin-only toll was measured too — it makes the light free to enter and costly to leave, which // reads well and plays worse: with the dark spokes in place the walker simply steps in and waits, // and the goal mind stops paying at all (admission held, but the module's order-recovery bar fell // by a third). What keeps the heart economy honest here is the STRICT cone and the three-beat // span, not a discount at the doorway. onLeave: (P, ev) => { const st = P.st, dyn = st.park.dyn, D = dyn && dyn.beacon; if (!D) return; if (ev.fromKey === ev.toKey) return; const cone = _parkBeaconCone(st, _parkBeaconFacing(st)); if (cone.has(ev.fromKey) || cone.has(ev.toKey)) D.litCross++; if (!_parkBeaconStaring(st)) return; if (!cone.has(ev.fromKey) && !cone.has(ev.toKey)) return; P.hearts--; D.spotted.push({ beat: dyn.beat, key: ev.toKey }); st.fx.push({ k: 'seen', x: ev.to.x, y: ev.to.y }); }, // THE BEAT — y46's tick order with the water removed: ① the stun expires ② the companion is // judged against the PREVIOUS beat's snapshots (the beat he actually lived through, since the // engine increments dyn.beat before this hook) ③ the sweep is counted ④ re-snapshot. // // HE DOES NOT WATCH THE TOWER. The walker knows the clock and may freeze to hide; his companion // is running an errand and never looks up, so the light takes him wherever it FINDS him — not // only where it catches him moving. That asymmetry is deliberate and it is what makes care an // ACT rather than a sentiment: the one exemption is a friend standing at his shoulder (the same // adjacency the mate mask freezes him with), so on this board "돌봄" is a body in a place, at a // beat, and the confession log records it (holds). // MEASURED before this rule existed, when a catch needed him to be MOVING in the light: over // seeds 1..6 x 6 personas the event fired on 2 seeds only, and on one of those it fired for a // CARE-led walker too — so the signature's harm clause was decided by luck rather than by the // walker's choices, and the cell rejected on `sig` wherever the coin came up wrong. tick: (P) => { const st = P.st, dyn = st.park.dyn, D = dyn && dyn.beacon; if (!D) return; if (D.mateStun > 0) D.mateStun--; const co = st.pos[1]; if (D.starePrev) { const n = st.N, coneP = _parkBeaconCone(st, D.facePrev); const held = _parkBeaconNear(st) <= PARK_BEACON_HOLD; // THE HOLD IS RECORDED WHEREVER HE STANDS, not only where the light happens to fall. Standing // at his shoulder on a billing beat is the ACT; whether the beam picked his alley that time is // the beam's business. MEASURED with the log narrowed to lit beats only: care kept him safe on // 24 of 24 seeds and yet the confession was empty on 21 of them, because a shepherd who is // early is a shepherd whose charge is never in the light — the observable was recording luck. if (held) D.holds.push({ beat: dyn.beat }); else if (coneP.has(co.y * n + co.x)) { D.mateCaught++; D.mateStun = PARK_BEACON_STUN; st.fx.push({ k: 'sent', x: co.x, y: co.y }); } } if (dyn.beat > 0 && (dyn.beat % PARK_BEACON_SPAN) === 0) D.rotations++; D.matePrev = { x: co.x, y: co.y }; D.starePrev = _parkBeaconStaring(st); D.facePrev = _parkBeaconFacing(st); }, }; // PARK_BEACON_SHIP_SEED / _parkBeaconRecovers / PARK_BEACON_SHIPPABLE — the MODULE bar, DERIVED and // never asserted. The quantity is the CALIBRATED 6/6 blind order recovery on this module's // shipped-seed cell — calibrated meaning read at PARK_CAL_TURNS, the same skip the product's own // readout uses, so the bar cannot certify a claim the readout throws away. // MEASURED (2026-07-27, seeds 1..24 x 6 personas): the raw module sweep recovers 90 of 144 runs and // holds 6/6 on seeds 2, 4, 7, 8 and 21 — MISREADS ZERO throughout, so the shortfall is silence and // never error. The crossing sweep exists to select against the quiet boards, and on the boards the // product actually seats — the crossing cells — recovery is 142 of 144 runs with the pairing bar at // 22 of 24 seeds. The two runs that fall short are the same persona (goal>care>safety) on seeds 9 // and 18, and BOTH recover exactly at skip 0: their evidence lands inside the calibration span. A // neutral entry corridor (y8's prescription) was built and MEASURED — it moved neither run and cost // an admitted seed, so it was taken back out rather than kept as decoration. const PARK_BEACON_SHIP_SEED = 7; function _parkBeaconRecovers(cell) { for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkBeaconBuild(cell), persona); if (P.reason !== 'complete') return false; const r = parkRecoverOrder(_parkBeaconBuild(cell), P.moves, PARK_CAL_TURNS); if (!r || r.join() !== persona.join()) return false; } return true; } const PARK_BEACON_SHIPPABLE = _parkBeaconRecovers(_parkBeaconCell(PARK_BEACON_SHIP_SEED)); /* ============ END BEACON FIELD MODULE (the center tower and its four cones, the split looking span, the presence-keyed reads, the stare toll on one confession log, and the admission gate with the norot hybrid clause) ============ */ /* ============ BURST FIELD MODULE (y52 "물풍선 마당", plan 2026-07-27) ============ */ /* CRAZY ARCADE, ON A CLOCK YOU CAN COUNT. Five water balloons sit on the yard with lit fuses. Each one bursts on a beat you can read off its fuse pips, throwing a CROSS of water two cells along each arm; a STEP across that cross on the beat it goes costs a heart (dyn.burst.singed, the ONE heart channel) and crouching is free. Nothing here is hidden: the fuse row counts down in the open, the arms flash the beat before, and a plan that reads the clock crosses this yard for nothing. THE COUNT IS FIVE, measured: at three the yard was quiet enough that the goal-led walker crossed the water by luck and was billed on fewer than half the seeds. The balloon cells themselves are walls. They never move and they never stop returning, so the yard is a rhythm rather than a countdown. THE COMPANION STARTS INSIDE A BUBBLE. He cannot take a step until somebody walks into it and pops it (y3's grammar exactly: legalMask freezes his whole domain, legalAdd opens his cell to the walker, and his planner HOLDS rather than retiring). A burst that catches him puts him back in one. So care on this board is not a sentiment and not a place to stand — it is a COUNT and a CLOCK READING: how early you went to him, and how quickly you went back (dyn.burst.rescues, dyn.burst.freeBeat). Everybody frees him eventually — his errand is half the harvest — so the observable is never WHETHER but WHEN. THE THREE LEVERS ARE BUILT IN, learned from y53 rather than rediscovered: ① the clock is SPLIT (a free warning beat, then the bursting beat); ② the goal mind YIELDS on the warning beat, which is the only beat where caution and care answer alone; ③ his contract gem sits off his own lane so "walk toward the shiny thing" and "go and free him" never coincide. And the fourth, which y53 paid for in measurements: once care is SATISFIED it hands the whole ring back rather than answering 'stay', or the care-led runs never stage goal-versus-caution at all. NO HUMAN-ONLY VERB: popping the bubble is a step, and the oracle takes steps. */ const PARK_BURST_N = 13; const PARK_BURST_PERIOD = 6; const PARK_BURST_ARM = 2; // cross arms, in cells const PARK_BURST_STUN = 1; const PARK_BURST_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; const _PARK_BURST_DIRS = [{ x: 0, y: -1 }, { x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }]; // ---- THE PUBLIC CLOCK READS. Pure beat functions; nothing here advances anything. // _parkBurstDue(st, b): the balloons whose fuse reaches zero on beat b. function _parkBurstDue(st, b) { const B = st.park && st.park.burst; if (!B) return []; return B.balloons.filter(o => ((b % PARK_BURST_PERIOD) === o.phase)); } // _parkBurstArms(st, balloon): the cross this balloon throws — the balloon cell and ARM cells each // way, stopped by walls (a wall eats the rest of that arm, the Bomberman rule everyone already knows). function _parkBurstArms(st, o) { const n = st.N, out = new Set(); out.add(o.y * n + o.x); for (const d of _PARK_BURST_DIRS) { for (let i = 1; i <= PARK_BURST_ARM; i++) { const x = o.x + d.x * i, y = o.y + d.y * i; if (x < 1 || y < 1 || x > n - 2 || y > n - 2) break; const k = y * n + x; if (st.wall.has(k)) break; out.add(k); } } return out; } // _parkBurstHot(st, b): every cell the water reaches on beat b — the ENGINE's timetable, which the // app's flash and the minds' reads both consume so that seen danger and priced danger never drift. function _parkBurstHot(st, b) { const out = new Set(); for (const o of _parkBurstDue(st, b)) for (const k of _parkBurstArms(st, o)) out.add(k); return out; } // the fuse, in beats, for one balloon: 0 means it goes THIS beat. function _parkBurstFuse(st, o) { const b = (st.park.dyn ? st.park.dyn.beat : 0) % PARK_BURST_PERIOD; return (o.phase - b + PARK_BURST_PERIOD) % PARK_BURST_PERIOD; } // _parkBurstWarning(st): is this a beat on which SOME balloon is one beat from bursting? That is the // telegraph beat — free, and the beat the goal mind yields (see the G facet). function _parkBurstWarning(st) { const b = (st.park.dyn ? st.park.dyn.beat : 0); return _parkBurstDue(st, b + 1).length > 0; } function _parkBurstNear(st) { return Math.max(Math.abs(st.pos[0].x - st.pos[1].x), Math.abs(st.pos[0].y - st.pos[1].y)); } function _parkBurstApproach(P) { const st = P.st, n = st.N; const dist = new Array(n * n).fill(Infinity); const src = _parkKey(st, st.pos[1]); dist[src] = 0; const q = [src]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk)) continue; if (dist[nk] > dist[k] + 1) { dist[nk] = dist[k] + 1; q.push(nk); } } } return dist; } // _parkBurstBuild(cell): the yard. Spawn and finish on the axis, the companion's errand CROSSING it // (y53's measured shape: an errand that stays on one flank is visited by the hazard too seldom to // pose anything). Seed-pure and persona-blind (C1). function _parkBurstBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_BURST_N, c = (n - 1) >> 1; const r = rng((seed * 6151 + 2749) >>> 0 || 1); const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const sd = cs(0, 3), d = _PARK_BURST_DIRS[sd]; const lat = { x: Math.abs(d.y), y: Math.abs(d.x) }; const fl = cs(0, 1) ? 1 : -1; const A = (fwd, side) => ({ x: c + d.x * fwd + lat.x * side * fl, y: c + d.y * fwd + lat.y * side * fl }); const sp = cs(-1, 0); const spawn = A(-4, sp); const finish = A(5, 0); // HIS BUBBLE SITS ON THE FLANK AND HIS CONTRACT GEM ACROSS THE YARD. That separation is the // anti-mimic pin, MEASURED rather than assumed: with the gem one step off his own station the // surface imitator expressed the C-N pair on 78 of 144 probes and RECOVERED it on 39 of them, // because "walk toward the shiny thing" and "go and free him" were the same walk. Pulling the gem // to the far flank takes the imitator to expressed 0 and leaks 0 on every measured seed, and the // crossing pairing bar from 11/24 to 24/24. Three neighbouring arrangements were measured too and // all of them collapse the cell: station out at lateral 4 (any of forward 1, -2, 2) admits 0 of // 40 seeds, because a bubble nobody walks past is a bubble nobody pops. const station = A(1, 2), mateGem = A(3, -2), retire = A(0, 2); const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) wall.add(y * n + x); // THE BALLOONS. Their seats are CHOSEN, not assumed: a balloon is a wall, and a wall on the // companion's station or on the finish is a board nobody can play. So the candidates are filtered // against every anchor and against each other, and only the survivors are seated. Their phases // come from the seed and are the anti-mimic axis — a copier that learned one yard's rhythm meets // another one on the next seed. const anchors = [spawn, finish, station, mateGem, retire]; const clashes = (q) => { if (q.x < 2 || q.y < 2 || q.x > n - 3 || q.y > n - 3) return true; // never against the fence for (const a of anchors) if (Math.abs(a.x - q.x) + Math.abs(a.y - q.y) <= 1) return true; return false; }; // THE BALLOONS SIT ON THE WALKER'S OWN LANE, and his companion's errand runs down the flank. That // is the whole geometry: the fast way to the finish is the wet way, the dry way is longer, and the // friend's road is somewhere else entirely. MEASURED with the balloons scattered instead: the // goal-led walker crossed the water by luck and was billed on fewer than half the seeds, so // caution's detour bought nothing a reading could see. const spots = [A(1, 0), A(-2, 0), A(3, 0), A(0, 2), A(2, -2), A(-2, 2), A(3, -2), A(-3, 2), A(2, 2), A(-1, -2), A(-3, -2), A(3, 2)]; const seats = []; for (const q of spots) { if (seats.length >= 5) break; if (clashes(q)) continue; if (seats.some(t => Math.abs(t.x - q.x) + Math.abs(t.y - q.y) <= 2)) continue; // never adjacent crosses seats.push(q); } const ph0 = cs(0, 1) ? 0 : 3; const balloons = seats.map((s0, i) => ({ x: s0.x, y: s0.y, phase: (ph0 + i * 3) % PARK_BURST_PERIOD })); for (const o of balloons) wall.add(o.y * n + o.x); // NO DEEP FIELD (y29's channel purity, inherited): the heart channel is the water alone. const deep = new Set(), verge = new Set(), walkway = new Set(); for (let k = 0; k < n * n; k++) if (!wall.has(k)) walkway.add(k); const distDeep = new Array(n * n).fill(Infinity); const tokens = [ { x: finish.x, y: finish.y, v: 1, alive: true, guard: false }, { x: mateGem.x, y: mateGem.y, v: cs(1, 2), alive: true, guard: false }, ]; const park = { N: n, seed, k: 0, fieldMech: 'burst', deep, verge, walkway, distDeep, burst: { side: sd, fl, balloons, arm: PARK_BURST_ARM, period: PARK_BURST_PERIOD, finishTi: 0, finishKey: finish.y * n + finish.x, laneKeys: [station.y * n + station.x, mateGem.y * n + mateGem.x] }, clusters: tokens.map(t => ({ x: t.x, y: t.y, v: t.v })), chain: [0], needPairs: true, contracts: [{ gem: 1, station }], retire, spawn, companionSpawn: { x: station.x, y: station.y }, trig: n * 2, cap: 140, minTurns: 6, cautionD: 2, damage: 1, cell: _parkBurstCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: station.x, y: station.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); park.dyn.burst = { singed: [], mateSinged: 0, pops: 0, rescues: 0, freeBeat: null, bubbled: true, mateStun: 0, hotCross: 0, shields: 0 }; return st; } // _parkBurstLayoutOk(st): the builder's own refusal. Three balloons must actually have been seated, // and every errand cell must be reachable from every other with the balloon walls in place — // checked STRUCTURALLY (a flood fill), never by trusting the placement arithmetic. function _parkBurstLayoutOk(st) { const B = st.park.burst, n = st.N; if (!B || B.balloons.length < 5) return false; const key = (p) => p.y * n + p.x; const need = [key(st.park.spawn), B.finishKey, key(st.park.companionSpawn), key(st.park.clusters[1]), key(st.park.retire)]; if (new Set(need).size !== need.length) return false; for (const k of need) if (st.wall.has(k)) return false; const seen = new Set([need[0]]), q = [need[0]]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || seen.has(nk)) continue; seen.add(nk); q.push(nk); } } for (const k of need) if (!seen.has(k)) return false; return true; } function _parkBurstCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'burst' } }; } /* _parkBurstCtx(P) — the reads' one computation. y53's lesson: engage on PRESENCE, not on the bare clock, so every mind has states it is cold in and the seam has something to probe. */ function _parkBurstCtx(P) { const st = P.st, D = st.park.dyn && st.park.dyn.burst; const beat = st.park.dyn ? st.park.dyn.beat : 0; const hotNow = _parkBurstHot(st, beat), hotNext = _parkBurstHot(st, beat + 1); const warn = _parkBurstWarning(st); const here = _parkKey(st, st.pos[0]), mateKey = _parkKey(st, st.pos[1]); const near = _parkBurstNear(st); const bubbled = !!(D && D.bubbled); const live = !!(D && D.mateStun === 0 && P.mode !== 'done'); // CAUTION WAKES NEAR THE WATER, not only in it: a mind that only speaks once it is already // standing in the cross has nothing left to decide. MEASURED with the narrow read: caution was // cold on almost every beat, so the care mind's narrowing decided every turn for every persona // and all six playouts came out byte-identical. const nearHot = (k) => { if (hotNow.has(k) || hotNext.has(k)) return true; const n2 = st.N, x = k % n2, y = (k / n2) | 0; for (const d of DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n2 || ny >= n2) continue; const kk = ny * n2 + nx; if (hotNow.has(kk) || hotNext.has(kk)) return true; } return false; }; const engagedC = nearHot(here); // care wakes for a friend who cannot move at all, or one the water is about to reach. const engagedN = bubbled || (live && (hotNow.has(mateKey) || hotNext.has(mateKey))); return { warn, hotNow, hotNext, here, near, bubbled, live, engagedC, engagedN, dist: (engagedN && near > 1) ? _parkBurstApproach(P) : null }; } /* ---- y52 BURST — the filter layer. */ function _parkBurstPlay(cell, persona) { const st = _parkBurstBuild(cell); const P = parkStart(st); while (!P.over) parkStep(P, parkOracleMove(P, persona)); const D = st.park.dyn.burst; P._burstSinged = D.singed.length; // steps billed by the water (unit: hearts) P._burstMateSinged = D.mateSinged; P._burstRescues = D.rescues; P._burstFreeBeat = D.freeBeat; // the beat his bubble first popped (unit: beats) P._burstPops = D.pops; P._burstShields = D.shields; P._burstMateHome = st.score[1] > 0; return P; } // THE SIGNATURE. Three observables, one per mind, plus the harm non-vacuity: // SAFETY never stands in the water · GOAL does, and finishes sooner · CARE frees him FIRST and // never lets the water take him back · and somebody, somewhere, does let it. function _parkBurstSignature(playouts) { const top = PARK_PERSONAS.map(p => p[0]); let safeWorst = -Infinity, goalWorst = -Infinity, careSeen = 0; let careFree = -Infinity, otherFree = Infinity; for (let i = 0; i < playouts.length; i++) { const P = playouts[i]; if (top[i] === 'safety') { if (P._burstSinged !== 0) return false; safeWorst = Math.max(safeWorst, P.turns); } if (top[i] === 'goal') { if (P._burstSinged < 1) return false; goalWorst = Math.max(goalWorst, P.turns); } // THE COMPARISON IS CARE-FIRST AGAINST CARE-LAST, not against everybody else. MEASURED: the // goal>care>safety walker frees him on the very same beat as the two care-led ones, because // care ranked SECOND still turns him aside once the goal mind has nothing to say — so folding // that persona into "the others" made the clause fail on all 24 seeds while the separation it // was looking for was sitting right there. NEVER FREEING HIM IS THE LATEST OF ALL: a walker who // never looks up is not missing evidence, he IS the evidence. if (PARK_PERSONAS[i][2] === 'care') { otherFree = Math.min(otherFree, P._burstFreeBeat == null ? Infinity : P._burstFreeBeat); } if (top[i] === 'care') { careSeen++; if (P._burstFreeBeat == null) return false; careFree = Math.max(careFree, P._burstFreeBeat); } } if (!isFinite(safeWorst) || !isFinite(goalWorst)) return false; if (!(goalWorst < safeWorst)) return false; // CARE GOES FIRST, and that is the whole care observable on this board: the deed itself is open // to anybody, so WHEN is the only thing a reading can hold on to. `otherFree` is Infinity when // nobody else ever went, and that comparison is exactly right. if (!(careFree < otherFree)) return false; // NO HARM CLAUSE HERE, and that is a decision rather than an omission. On the statue yards the // care observable is "he was never taken", which needs somebody to lose him or it says nothing. // On THIS board the observable is a CLOCK — care goes to the bubble first — and that clause is // non-vacuous by construction: it compares two measured beats on every seed. MEASURED: the water // reaches his short errand on only 7 of 24 seeds, so a harm clause would have thrown away // two-thirds of the playable boards to police a claim this cell never makes. return careSeen === 2; } const _PARK_BURST_WHYS = { complete: 0, dead: 0, matelost: 0, sig: 0, nocn: 0, norec: 0, nopop: 0 }; function _parkBurstAdmissible(cell) { if (!_parkBurstLayoutOk(_parkBurstBuild(cell))) { _PARK_BURST_WHYS.nopop++; return false; } const playouts = []; for (let i = 0; i < PARK_PERSONAS.length; i++) { const P = _parkBurstPlay(cell, PARK_PERSONAS[i]); if (P.reason === 'death') { _PARK_BURST_WHYS.dead++; return false; } if (P.reason !== 'complete') { _PARK_BURST_WHYS.complete++; return false; } if (P.hearts < 1) { _PARK_BURST_WHYS.dead++; return false; } if (PARK_PERSONAS[i][0] === 'care' && !P._burstMateHome) { _PARK_BURST_WHYS.matelost++; return false; } // THE HYBRID NON-VACUITY, per playout: the water must have burst at least twice while the run // was live. Whether the bubble was popped is a CROSS-playout question and belongs to the // signature — requiring every persona to free him made a walker who never looked up into a // reason to throw the whole board away, which is the opposite of what this cell measures. if (P._burstPops < 2) { _PARK_BURST_WHYS.nopop++; return false; } playouts.push(P); } if (!_parkBurstSignature(playouts)) { _PARK_BURST_WHYS.sig++; return false; } let posed = 0; for (let i = 0; i < PARK_PERSONAS.length; i++) { const persona = PARK_PERSONAS[i]; for (const pair of PARK_BURST_PAIRS) { if (!(parkPairExpressed(_parkBurstBuild(cell), playouts[i].moves, pair) > 0)) continue; if (pair[0] === 'C' && pair[1] === 'N') posed++; const expect = _parkPushPairDir(persona, pair); if (!parkRecoverPairLex(_parkBurstBuild(cell), playouts[i].moves, pair, { expect }).recovered) { _PARK_BURST_WHYS.norec++; return false; } } } if (posed < 2) { _PARK_BURST_WHYS.nocn++; return false; } return true; } function parkBurstWhys() { return { ..._PARK_BURST_WHYS }; } PARK_FIELD_MECHS.burst = { build: _parkBurstBuild, cell: _parkBurstCell, admits: _parkBurstAdmissible, reads: { ctx: _parkBurstCtx, // THE GOAL MIND YIELDS ON THE WARNING BEAT (y17's lever by way of y46 and y53). A step taken // while a fuse is at one buys nothing the next beat would not give for free, so an EMPTY // prefer says exactly that and the seam drops the goal from this one decision. G: { engaged: (P, ctx) => !!ctx && ctx.warn, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.warn) { for (const c of legal) out.add(c.k); return out; } return out; }, }, // CAUTION READS THE CROSS. Not the beat — the CELLS: the mind wakes when the water is coming // where the body is, and answers with the cells it is not coming to. C: { engaged: (P, ctx) => !!ctx && ctx.engagedC, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.engagedC) { for (const c of legal) out.add(c.k); return out; } // CROUCH FIRST. On this board the toll rides the STEP, so holding still is not merely the // safest answer — it is the FREE one, and a caution mind that walks around the water when // it could simply let the water pass is not being cautious, it is being busy. for (const c of legal) if (c.k === 'stay') out.add(c.k); if (out.size) return out; for (const c of legal) if (!ctx.hotNow.has(c.key) && !ctx.hotNext.has(c.key)) out.add(c.k); if (out.size) return out; for (const c of legal) out.add(c.k); return out; }, }, // CARE GOES TO HIM, AND THEN GETS OUT OF THE WAY. The second half is y53's measured lesson: // a care mind that answers 'stay' once it has arrived decides the whole order by itself, and // the goal-versus-caution scene is never staged on any care-led run. N: { engaged: (P, ctx) => !!ctx && ctx.engagedN, prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx || !ctx.engagedN) { for (const c of legal) out.add(c.k); return out; } const co = P.st.pos[1]; const cheb = (c) => Math.max(Math.abs(c.x - co.x), Math.abs(c.y - co.y)); if (ctx.bubbled) { // pop it if you can reach it this beat; otherwise close the distance for (const c of legal) if (c.x === co.x && c.y === co.y) out.add(c.k); if (out.size) return out; } if (ctx.near <= 1) { // ARRIVED: care is satisfied and hands the ring back, so the next mind can still speak // (y53's measured lesson — a care mind that answers 'stay' decides the order by itself). for (const c of legal) if (cheb(c) <= 1) out.add(c.k); if (out.size) return out; } if (ctx.dist) { const cur = ctx.dist[ctx.here]; if (isFinite(cur)) for (const c of legal) { if (P.prev && P.prev.x === c.x && P.prev.y === c.y) continue; if (ctx.dist[c.key] < cur) out.add(c.k); } if (out.size) return out; } for (const c of legal) out.add(c.k); return out; }, }, }, // THE BUBBLE — y3's freeze, his domain alone. A bubbled man is not a wall: the walker crosses // that ground, and the route metric prices it. legalMask: (P, key, who) => { if (who !== 'mate') return false; const D = P.st.park.dyn && P.st.park.dyn.burst; return !!(D && (D.bubbled || D.mateStun > 0)); }, // THE POP — the one additive lane. His cell has never been a walkable destination, so this is // the only way to say "walk into the bubble and burst it". y3's assist, verbatim in shape. legalAdd: (P, key, who) => { if (who !== 'me') return false; const st = P.st, D = st.park.dyn && st.park.dyn.burst; if (!D || !D.bubbled) return false; return key === _parkKey(st, st.pos[1]); }, // THE WATER'S TOLL — a STEP taken across the bursting cross, on the beat it bursts. Crouching // ('stay') is always innocent, exactly as it is under every doll in this park: the water goes // over a body that holds still and catches one that is moving through it. // MEASURED with the toll on PRESENCE instead (standing on the cross when it went): the goal-led // walker spent all three hearts and died on 12 of 12 seeds at every balloon count and period the // sweep tried — a hazard with no free answer leaves the caution mind nothing to be right about. onLeave: (P, ev) => { const st = P.st, dyn = st.park.dyn, D = dyn && dyn.burst; if (!D) return; if (ev.fromKey === ev.toKey) return; const hot = _parkBurstHot(st, dyn.beat); if (!hot.size) return; if (!hot.has(ev.fromKey) && !hot.has(ev.toKey)) return; P.hearts--; D.singed.push({ beat: dyn.beat, key: ev.toKey }); st.fx.push({ k: 'singed', x: ev.to.x, y: ev.to.y }); }, // ARRIVAL. Walking into the bubble pops it and puts the walker back where he was — an // action-move, the ledge idiom: the step is spent on the deed, not on the ground. onEnter: (P, ev) => { const st = P.st, dyn = st.park.dyn, D = dyn && dyn.burst; if (!D || !D.bubbled) return; if (ev.toKey !== _parkKey(st, st.pos[1])) return; D.bubbled = false; D.rescues++; if (D.freeBeat == null) D.freeBeat = dyn.beat; st.pos[0] = { x: ev.from.x, y: ev.from.y }; st.fx.push({ k: 'pop', x: ev.to.x, y: ev.to.y }); }, // THE BURST. tick runs after dyn.beat++, so the water lands on the beat the fuse pips showed. // ONE heart channel, ONE confession log; the companion is never killed, only bubbled again. tick: (P) => { const st = P.st, dyn = st.park.dyn, D = dyn && dyn.burst; if (!D) return; if (D.mateStun > 0) D.mateStun--; // the burst that just happened is the one for the beat that ENDED — tick runs after dyn.beat++, // and onLeave billed the walker against that same beat, so both hooks read one clock. const b = dyn.beat - 1; const due = _parkBurstDue(st, b); if (!due.length) return; const hot = _parkBurstHot(st, b); D.pops += due.length; const n = st.N; const cok = st.pos[1].y * n + st.pos[1].x; // THE SHIELD — a friend standing beside him when the water comes takes it for him, and it costs // that friend nothing but the place he chose to be. This is the care act that OUTLASTS the // rescue: without it care has one deed at the start of the run and nothing to do afterwards, // and the water soaks his companion on care-led runs exactly as often as on any other. const shielded = Math.max(Math.abs(st.pos[0].x - st.pos[1].x), Math.abs(st.pos[0].y - st.pos[1].y)) <= 1; if (hot.has(cok) && !D.bubbled && shielded) D.shields = (D.shields || 0) + 1; if (hot.has(cok) && !D.bubbled && !shielded) { // A SOAKING, NOT A SECOND BUBBLE. Re-bubbling him was built first and MEASURED: once the // water put him back in, nobody ever came a second time, his errand never finished, and the // care personas rejected on `matelost` across the board. A stun keeps the harm real and // keeps him a person with somewhere to be. D.mateStun = PARK_BURST_STUN; D.mateSinged++; st.fx.push({ k: 'soaked', x: st.pos[1].x, y: st.pos[1].y }); } for (const o of due) st.fx.push({ k: 'burst', x: o.x, y: o.y }); D.hotCross += hot.size; }, }; // PARK_BURST_SHIP_SEED / _parkBurstRecovers / PARK_BURST_SHIPPABLE — the MODULE bar, DERIVED and // never asserted: the CALIBRATED 6/6 blind order recovery on this module's shipped-seed cell, read // at PARK_CAL_TURNS, the skip the product's own readout uses. // MEASURED (2026-07-27, seeds 1..24 x 6 personas): 104 of 144 runs recover, 6/6 holds on seeds 6, 8, // 9, 14, 16, 22 and 24, and MISREADS ARE ZERO — every shortfall is silence, never error. The single // change that bought it was making caution CROUCH (the C facet below): before it, the same yard // recovered 40 of 144 runs and not one seed in 24 reached 6/6, because a caution mind that walks // around the water instead of letting it pass answers almost exactly like a hurried one. const PARK_BURST_SHIP_SEED = 8; function _parkBurstRecovers(cell) { for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkBurstBuild(cell), persona); if (P.reason !== 'complete') return false; const r = parkRecoverOrder(_parkBurstBuild(cell), P.moves, PARK_CAL_TURNS); if (!r || r.join() !== persona.join()) return false; } return true; } const PARK_BURST_SHIPPABLE = _parkBurstRecovers(_parkBurstCell(PARK_BURST_SHIP_SEED)); /* ============ END BURST FIELD MODULE (five balloons on one rhythm, the cross of water billed on the step, the bubble that only a body can pop, and the admission gate with the nopop clause) ============ */ /* ============ SHIFTER (y54) LEFT ON 2026-08-03 with its slot ============ A preview; nothing live was seated on it. Removed whole — module, registration, SHIPPABLE, exports, painter and gates — because unlike ledge and yield (which y26 and y33 shared with the LIVE xp and xs, and which therefore stayed when those two rows went) no other cell used it. ======================================================================== */ /* ============ ROAD FIELD MODULE (y58 "흐르는 도로", design 2026-08-03) ============ */ /* 세로 3차선 위를 흐르는 도로. 진행은 워커의 행(y)이고, 위로 갈수록 앞이다. 네 몸이 도로에 있다 — 워커, 동료, 교통 NPC 둘. 차량 셋(동료 + NPC 둘)의 바로 앞 칸에 토큰이 하나씩 있고, 그 칸에 닿는 것이 곧 추월이다(Task 4). 이 모듈의 주장은 하나다: `stay` 가 값을 물면 세 마음이 같은 자원 위에 올라간다. `↑` 가 아닌 모든 수는 한 칸 뒤로 밀리므로(Task 2) 회피도 양보도 차선 변경도 전부 골을 깎는다. 그 척추가 곧 이 설계의 가장 큰 위험이기도 하다 — 쌍의 상은 `_parkAwardsFor` 가 만장일치일 때만 나오는데, 드리프트는 모든 박자를 G 의 결정으로 만든다. 주기의 마지막 박자(차단 박자, Task 3)에서 전방을 legalMask 로 닫고 드리프트를 면제해 G 를 침묵시킨다. */ const PARK_ROAD_LANES = 5; // L3(2026-08-05): 분리대가 칸을 안 먹으므로 5칸 전부 주행칸 const PARK_ROAD_PERIOD = 6; // 5 자유 박자 + 1 차단 박자 // _parkRoadCell(seed): 모듈의 PUBLIC play-cell. 순수 값 생성자이고 persona 매개변수는 없다(C1). // goalVariant 'reach' — 체인은 차량 앞칸 토큰 셋이고, 완료는 엔진의 generic 경로가 센다. function _parkRoadCell(seed) { // arch: 'road' — 렌더 전용 필드다(선례: engine.js 의 `arch: PARK_SLIDE_ARCH`). 벽 칸 장식을 // 고르는 데만 쓰이고 legal/wall/route 계산 경로 어디에서도 안 읽힌다. 이게 없으면 // app.js 의 _parkWallDecor 가 마지막 else(공원 나무·벤치)로 떨어져 도로 갓길에 벤치가 선다 // (사용자 지적 2026-08-05). rng 를 소비하지 않으므로 보드 기하는 바이트로 동일하다. return { goalVariant: 'reach', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', arch: 'road', lanes: PARK_ROAD_LANES, period: PARK_ROAD_PERIOD, mech: { goalMech: 'reach', safetyMech: 'static', fieldMech: 'road' } }; } // _parkRoadBuild(cell): 도로 보드를 세운다. 차선은 세로 열, 그 사이가 중앙분리대다. function _parkRoadBuild(cell) { const r = rng((cell.seed * 7919 + 4133) >>> 0); // 전역 rng — `_parkRng` 은 없다(확인함) const lanes = cell.lanes || PARK_ROAD_LANES; // L3 기하(2026-08-05): 분리대가 열을 먹던 구조를 버린다. 예전에는 n = lanes*2+1 = 7 이고 // x=2,4 가 통짜 분리대 열이라, 사용자에게는 "5칸 중 3칸만 차선"으로 보였다. 이제 차선은 // 연속 5칸이고 분리선은 칸 사이 모서리에 산다(st.wall 로는 표현 불가 — 벽은 칸이다. // _parkRoadLegalMask 가 표현한다, 아래). // // n=12 는 사용자가 요청한 "상하 12칸"이다. 엔진은 정사각 보드만 쓰므로(N: n 하나) // 폭도 12가 되고, 차선 5칸을 중앙 정렬하면 시작 x = floor((12-5)/2) = 3 이다. // 갓길 7열은 죽은 공간이지만 선례가 있다(flood 17x17 도 벽 비율이 높다) — 그 대신 // L1-4 의 arch:'road' 장식(가드레일·노견 풀)이 그 자리를 도로 갓길로 읽히게 한다. const n = 12; const laneX = []; for (let i = 0; i < lanes; i++) laneX.push(3 + i); // [3,4,5,6,7] // 벽: 차선 밖 전부. 분리대 행-벽이 없어졌다 — 차선끼리의 통행 제한은 이제 모서리 // 실선(_parkRoadSolidEdge + _parkRoadLegalMask)이 맡는다. const wall = new Set(); const laneSet = new Set(laneX); for (let y = 0; y < n; y++) { for (let x = 0; x < n; x++) { if (!laneSet.has(x)) wall.add(y * n + x); } } const midY = Math.floor(n / 2); // 워커 차선은 seed 로 갈린다(0..lanes-1 인덱스). 동료는 반드시 워커와 **이웃한** 차선에서 // 시작한다 — 배열 인덱스 차이가 1(끝 차선이면 가운데뿐이 이웃, 가운데면 양쪽 다 후보). 그래야 // 둘 사이에 중앙분리대 틈이 정확히 하나 있다. 끝 차선끼리(예: 1 과 5)는 사이에 차선이 하나 더 // 있어서 어느 쪽도 그 틈에 한 수로 못 닿는다 — Task 3 의 포켓(차단 박자의 유일한 안전 칸)이 // "워커와 동료 둘 다 한 수로 닿는다"를 요구하므로, 이웃 관계를 확률에 맡기지 않고 여기서 // 기하로 강제한다(모든 시드에서 항상 참이 되도록). const wi = Math.floor(r() * lanes); // L3(2026-08-05): 이웃 차선(±1)이 아니라 **한 칸 건너**(±2)다. 이유는 포켓이다 — // pocketX = (laneX[wi] + laneX[mi]) / 2 는 "두 차선의 중점"인데, 이웃 차선이면 그 중점이 // 옛 구조에서는 분리대 칸(워커가 홀수 행이면 벽 → 실측 27.8% 가 유령 포켓)이었고 // 새 구조에서는 아예 정수가 아니다. 한 칸 건너면 중점이 **실재하는 주행 차선 칸**이 된다. // 공식은 한 글자도 안 바뀐다 — 배치 규칙만 바뀐다. // // 차선 인덱스 0..4 에서 [wi-2, wi+2] 는 항상 비어 있지 않다: // wi=0 -> [2], 1 -> [3], 2 -> [0,4], 3 -> [1], 4 -> [2]. // 5차선이어야 이 자리가 생긴다 — 3차선으로는 못 만든다. const adjIdx = [wi - 2, wi + 2].filter(i => i >= 0 && i < lanes); const mi = adjIdx[Math.floor(r() * adjIdx.length)]; const pocketX = (laneX[wi] + laneX[mi]) / 2; // 둘 사이 중앙분리대의 그 틈 칸(x) const spawn = { x: laneX[wi], y: midY }; // 동료는 바닥 행(n-1)에 붙박이다 — NO CONTRACTS(아래)라 이 보드에서 동료는 원래 안 걷는다. // 드리프트는 워커를 결국 바닥에 눌러 앉힌다(순수 stay 로 3틱 뒤 도달, 그 뒤로 영구 고정 — // Task 2 의 REAREND 노트가 이미 잰 값과 같다, 실측 재확인함). 동료를 거기 놓으면 "차단 // 박자에 둘 다 그 틈에 한 수로 닿는다"가 우연이 아니라 이 보드의 항상-참인 기하가 된다. const mate = { x: laneX[mi], y: n - 1 }; // npc[1] 은 원래 설계로는 midY-4(n=7 이면 -1) 였는데, 그러면 보드 밖이다. 몸이 0행에 있으면 // 그 바로 앞칸(추월 토큰, body.y-1)이 -1행이 되어 영원히 도달 불가한 목적지가 된다(route // mask 의 Infinity 함정). 그래서 두 번째 NPC 는 앞이 아니라 **뒤**에 둔다 — 워커가 앞뒤 // 양쪽에서 차량을 만나는 편이 상하 대칭도 맞고, 네 몸 모두 y>=1 이라 토큰이 전부 보드 안에 // 선다. npc[0] 의 차선은 남은 자유도로 seed 마다 갈린다(어느 차선이든 행이 달라 겹칠 일이 // 없다 — spawn=midY, mate=n-1, npc0=midY-2, npc1=midY+2 는 네 값이 항상 서로 다르다). // npc[0] 의 차선은 npc[1](= 워커 홈 차선 wi)을 **제외한** 나머지에서만 뽑는다. // 예전에는 세 차선 전부에서 뽑아 1/3 확률로 wi 와 같았고(실측 7/24), 그러면 두 NPC 가 // 결국 같은 칸에 겹치면서 추월 토큰 둘(체인 다리 둘)까지 한 칸에 포개졌다. 그 7 시드는 // _parkRoadAdmissible 탈락 10개에 100% 포함됐다. 확률에 맡기지 않고 여기서 기하로 // 강제한다 — 바로 위 adjIdx 가 mi 를 강제하는 것과 같은 관용구다. const npc0Idx = []; for (let i = 0; i < lanes; i++) if (i !== wi) npc0Idx.push(i); const npc = [{ x: laneX[npc0Idx[Math.floor(r() * npc0Idx.length)]], y: midY - 2 }, { x: laneX[wi], y: midY + 2 }]; // xx 위험 액터 (L3-5, 2026-08-05). 다른 NPC 둘은 제 차선에서 y+1 만 하는 스크립트 // 액터인데, 이 액터는 **예고 없이 차선을 바꾼다**. 지금 드럼은 결정적 시간표라 조심(C)이 // 계산으로 환원된다 — 예측 불가 액터가 있어야 조심이 실질을 갖는다. // // 예측 불가는 **플레이어에게만**이다. 일정은 시드에서 파생하므로 엔진에는 완전히 // 결정적이고 Y58-ROAD-SEED-PURE 가 그대로 산다. 사람은 그 일정을 못 보므로 // (화면이 미리 안 알려 준다 — 이게 깜빡이 있는 동료와의 대비다) 조심할 수밖에 없다. // // 위치: 워커·동료·NPC 둘과 겹치지 않는 행. 네 몸의 y 는 각각 midY, n-1, midY-2, midY+2 // 이므로 midY-4 를 쓴다(n=12, midY=6 이면 y=2 — 보드 안이고 넷 다와 다르다). const hazLane = Math.floor(r() * lanes); const hazPlan = []; for (let k = 0; k < 24; k++) hazPlan.push([-1, 0, 0, 1][Math.floor(r() * 4)]); const hazard = { x: laneX[hazLane], y: midY - 4, plan: hazPlan }; // 추월 토큰 — 차량 셋(동료 + NPC 둘)의 **바로 앞 칸**. 차량 위에 두면 목적지가 진입 불가 // 셀이 되어 경로 메트릭이 Infinity 로 죽는다(route mask 의 Infinity 함정). // // pad: true, v: 0 (Task 4, 자체 점검으로 잡은 결함): 이 필드가 없으면 워커가 토큰 칸에 // 들어갈 때 엔진의 GENERIC gem-pickup(engine.js:8258, `!t.pad` 인 살아있는 토큰이면 무조건 // 줍는다)이 onEnter 보다 먼저 토큰을 죽이면서 `st.score[0] += tok.v` 를 건드린다 — v 가 // undefined 라 score 가 NaN 으로 오염됐다(실측 확인). pad:true 면 그 GENERIC gem 경로를 // 건너뛰고 대신 이 goalVariant('reach')용 GENERIC pad-pickup(engine.js:8277-8290, "no // pickup, no score")이 잡는다 — `anyOf ? anyOf : [park.chain[P.dest]]`(engine.js:8281)로 // chainAnyOf 가 없으면 그 다리의 단일 토큰(`park.chain[P.dest]`)을 그대로 후보로 삼는다 // (Fix round 2, 2026-08-04: 이 보드는 이제 chainAnyOf 를 안 쓴다 — 세 다리 순서, 아래 // park.chain 주석 참고). v:0 은 방어적으로 남겨 둔다(y46 의 pad 토큰도 같은 모양). const bodies = [mate, npc[0], npc[1]]; const tokens = bodies.map((b, i) => ({ x: b.x, y: b.y - 1, alive: true, gtype: i, pad: true, v: 0 })); // 동료의 계약 표적 (Task 6 — Step 0 결함 수정): gtype 3, 추월 토큰 셋(0..2, 위)과는 다른 // 넷째 자리다. _parkCompanionPlan(engine.js:7681)은 park.clusters[park.contracts[P.contract] // .gem]을 목표로 잡는다 — gem 이 0..2 중 하나였다면 워커가 그 차를 추월하는 순간 그 토큰이 // 죽어(alive=false) 동료의 계약이 중간에 끊긴다("지울 수 있는 표적", 브리프가 경고한 함정). // 이 넷째 토큰은 세 경로 전부가 비켜 간다: GENERIC gem 픽업(engine.js:8258)은 pad:true 라 // 건너뛰고, reach-pad 픽업(8277-8290)과 dyn.road.passed 장부(_parkRoadOnEnter, 아래에서 // park.chain 으로 좁혔다 — Fix round 2, chainAnyOf 는 더 안 쓴다)는 둘 다 // park.chain=[1,2,0](gtype 0,1,2 의 순열) 만 보므로 gtype 3 은 그 목록 밖이다 // — 워커가 무슨 수를 둬도 안 죽는, 살아남는 표적이다. 동료의 홈 차선(mi) 맨 위 행에 둔다 — // "위로 갈수록 앞"(모듈 서문 그대로), 그의 계획도 워커와 같은 방향으로 도로를 오른다. tokens.push({ x: laneX[mi], y: 0, alive: true, gtype: 3, pad: true, v: 0 }); // 엔진 보편 계약(Task 1 이 빠뜨린 부분, Task 2 가 여기서 발견하고 메운다): park.deep/verge // /walkway/distDeep 는 build 시점에 무조건 있어야 한다. _parkFields(안전/우회 경로 필드)와 // C 마음의 static engaged(park.distDeep 읽음)가 조건 없이 이 넷을 읽으므로, 없으면 parkStep // 첫 호출(어느 수든)에서 그 자리에서 죽는다 — burst/siege 의 "깊은 밭 없음" 선례와 같은 모양 // (park.deep 을 비워 둔다, burst 주석 "NO DEEP FIELD"). 이 보드에는 엔진 지형-깊이 개념이 // 없다 — 밀림·드럼은 dyn.road 의 제 장부(Task 2)와 차단 박자(Task 3)로 다룬다. const deep = new Set(), verge = new Set(), walkway = new Set(); for (let k = 0; k < n * n; k++) if (!wall.has(k)) walkway.add(k); const distDeep = new Array(n * n).fill(Infinity); const park = { // fieldMech: 'road' — 레지스트리 키(다른 모든 필드 모듈의 관용구, 예: engine.js:10946 // "the REGISTRY key (Task 1): what routes every engine dispatch"). _parkMech(st) 는 // st.park.fieldMech 만 읽는다(engine.js:7494) — park.cell.mech.fieldMech 는 안 읽는다. // 이게 없으면 _parkMech 가 항상 null 을 돌려줘 tick/legalMask/onEnter 전부가 등록되고도 // 영원히 안 불린다(Task 2 가 tick 을 붙이려다 여기서 처음 걸렸다). fieldMech: 'road', // pocketX: 워커의 홈 차선과 동료 차선 사이 중앙분리대 틈(build 시점 정적 값, 스크롤과 // 무관 — Task 3 의 _parkRoadPocket 이 차단 박자에 이 값을 그대로 쓴다). // recycleGap: NPC 가 도로 끝에 닿았을 때 어느 줄에서 다시 들어오는가 — // y = -(gap) 에서 재진입한다. **y=0 고정이 아니다**(사용자 결정 2026-08-05): 고정이면 // 두 NPC 의 차간 간격이 영구 고정돼 교통이 메트로놈처럼 읽히고, 그 고정 위상이 지금 // 고치려는 겹침 결함을 재생성 주기마다 재현시킨다. 위 전역 rng 에서 파생하므로 // 시드 결정적이다(Y58-ROAD-SEED-PURE, Y58-ROAD-RECYCLE-SEED-PURE). road: { lanes: laneX, period: cell.period || PARK_ROAD_PERIOD, npc, pocketX, hazard, recycleGap: [1 + Math.floor(r() * 3), 1 + Math.floor(r() * 3)], // solidSeed: 모서리 실선 무늬의 시드. cell.seed 를 그대로 쓰지 않고 섞는 이유는 // 같은 시드의 다른 축(차선 배치·NPC)과 무늬가 상관되지 않게 하려는 것뿐이다. solidSeed: (cell.seed * 2246822519 + 374761393) >>> 0 }, deep, verge, walkway, distDeep, // chain: [1, 2, 0], chainAnyOf 없음 (Fix round 2, 2026-08-04 — Task 6 STOP 이후 컨트롤러 // 지시). Fix round 1(위 이력)은 한 다리·세 대안(`chain:[0], chainAnyOf:[[0,1,2]]`)으로 // "셋 중 아무 하나만 추월하면 끝"을 정직하게 선언했다 — 그 자체는 옳았다(실제 동작과 // 선언이 맞았다). 문제는 그 뒤에서 드러났다: 셋 중 아무 하나만 추월해도 체인이 끝나고 // (대개 turn 3 안쪽, 첫 차단 박자보다도 이르다), 그 순간부터 G(골 마음)는 남은 87+ 턴 // 내내 냉(cold)하다 — `_parkDestCell`이 `P.dest>=chain.length`에서 null을 돌려주기 // 때문이다. 냉한 G는 아무 순서에도 필터 기여가 없으므로 "G가 X보다 앞선다"를 discriminate // 할 방법이 없어져 GC·GN 자체가 그 순간부터 구조적으로 불가능해진다 — Task 6 실측 // (오라클 sweep, 24×6=144)이 GC·GN·CN 셋 다 0/144 로 잡은 게 바로 이것이다(CN 만의 // 문제가 아니었다). // // 고침: 세 다리를 진짜로 세운다 — 세 토큰 인덱스의 순열, chainAnyOf 없음. // `_parkChainStepDone`(engine.js:7266)은 chainAnyOf[i] 가 없으면 // `!st.tokens[park.chain[i]].alive`만 본다 — 겹치는 집합이 없으니 폭포(Fix round 1이 // 잡은 결함)가 재발하지 않는다. `_parkAdvanceDest`의 while 루프는 dest 순서대로 "그 // 다리의 토큰이 죽었나"만 확인하며 전진하고, 이미 죽어 있으면 그 자리에서 바로 다음 // 다리로 넘어간다(순서를 강제로 죽이는 게 아니라, 몇 번째 마리를 먼저 죽였든 dest 는 // 선언 순서대로 밀린다) — "추월 순서 비강제"라는 설계 취지는 그대로 산다, // `_parkDestCell`이 매 순간 보여 주는 목표만 chain[dest] 순서를 따를 뿐이다. 세 다리 // 전부를 죽여야 체인이 끝나므로 G는 게임 대부분에서 계속 무언가를 원한다 — GC·GN이 // 다시 discriminate 가능해질 자리다. y46(engine.js:18883)의 한 다리·세 대안과는 다른 // 모양이지만, 세 다리 순서(engine.js:14163 의 `chain:[0,1,2]`, chainAnyOf 없음)는 // 이 엔진에 이미 있는 관용구다 — 다만 **어느 순열**을 쓰느냐가 이 보드에서는 중요했다 // (바로 아래). // // 순열이 `[0,1,2]`가 아니라 `[1,2,0]`인 이유(동료 자신의 토큰을 맨 뒤로) — 실측으로 // 드러난 진짜 문제 때문이다. `[0,1,2]`(문자 그대로, 동료 자신이 다리 0)로 먼저 재보니 // 24 시드 × 6 페르소나 **144/144 전부**가 // dest=0에서 영원히 멈췄다(_parkFields, engine.js:7541, 의 fast-field BFS는 // st.wall/mask만 보고 **동료의 몸(st.pos[1])은 장애물로 안 본다** — 그런데 // _parkLegal(engine.js:7649)의 companion-occupancy 규칙은 동료가 서 있는 칸으로의 // 이동 자체를 막는다. 다리 0의 표적(gtype 0, 동료 바로 앞 칸)으로 가는 필드-최단 // 경로가 하필 동료 자신이 서 있는 칸(그의 홈, 바닥 행)을 지나면, 필드는 "그리로 가라"고 // 계속 가리키는데 그 수만 매번 불법이라 워커는 그 옆(분리대 틈)에 영원히 멈춘다 — G의 // 선호가 그 자리에서 빈 집합이 되고 다른 마음도 전진을 안 미니 stay 로 고착된다). // NPC 토큰(gtype 1,2)은 이 문제가 없다 — _parkLegal 의 점유 규칙은 오직 동료(st.pos[1]) // 에만 걸리고 NPC 에는 안 걸린다. 그래서 동료 자신의 다리를 맨 뒤로 미루면(그때까지 // NPC 두 다리를 도는 동안 워커가 동료의 트리거 반경(park.trig=2, 포켓 근처)을 지나칠 // 기회가 늘어 그가 먼저 움직여 자리를 비울 수 있다) 실측이 극적으로 바뀐다: [1,2,0] // 으로 재면 144 중 84(14 시드 전부)가 세 다리를 완주한다(아래 _parkRoadAdmissible 이 // 그 14 시드만 통과시킨다). 이 순서는 우연이 아니라 이 모듈 서문("위로 갈수록 앞")과도 // 맞다 — 세 토큰의 y 는 시드와 무관하게 항상 gtype1(y=0, 맨 앞) < gtype2(y=4) < // gtype0(y=5, 맨 뒤, 동료의 홈 바로 앞) 순이다 — "세 대를 도로 순서대로 만난다"는 // 컨트롤러의 말 그대로, 자연스러운 순서이기도 하다. // // 위 수치(144 중 84·14 시드)는 이 순서를 **고르기 위해** 돌린 진단 스윕의 결과다 — // 그 자체가 프로젝트의 공식 최종 측정은 아니다(2026-08-04, 사용자 지시: 세 쌍 // 측정·admitted 시드 수·완주율·C-N co-engagement 는 전부 프로젝트 전체의 단일 // 최종 측정 라운드로 이월한다). 그러니 이 순서의 도달 가능성은 "완전히 확정 측정된 // 것"이 아니라 "이 순서를 [0,1,2] 대신 고른 근거"로만 읽어야 한다 — 공식 수치는 // Task 6 리포트의 "구현 확정 / 측정 이월" 절 참고. clusters: tokens, chain: [1, 2, 0], // needPairs 는 옵트인이 아니라 **필수다**. P.posed 는 항상 존재하지만 needPairs 가 없으면 // 아무도 갱신하지 않는다(engine.js:7220) — 그러면 Task 6 의 C-N 측정이 기하와 무관하게 // 0 이 나온다. 그리고 이게 ⑤(강제 가독성)를 정의상 참으로 만든다: _parkReadDone 이 // "체인이 다 됐고 세 쌍이 전부 한 번은 섰다"를 완주의 정의로 쓴다. needPairs: true, // 계약 하나 (Task 6 — Step 0 결함 수정, 이 값이 "NO CONTRACTS"였다). 동료가 원래 안 // 걷는 설계가 아니라, 계약이 비어 있어 _parkCompanionPlan(engine.js:7681)의 // `park.contracts[P.contract].gem` 인덱싱 자체가 없어 target 이 항상 null 이 되던 결함 // 이었다(실측 확인: 24 시드 × 40 턴 순수 stay, plan non-null 0/144, mate moved 0/25턴). // gem:3 은 위에서 만든 지워지지 않는 넷째 토큰 — station 은 이 보드에 계약이 하나뿐이라 // 안 읽히지만(nextTarget 은 다음 계약이 있을 때만 station 을 본다) 형제 보드의 관용구 // ({gem, station}, engine.js:18567 y33 의 contracts)를 그대로 남긴다. contracts: [{ gem: 3, station: { x: laneX[mi], y: 0 } }], // retire: 계약을 마치면(도로 맨 위 표적에 닿으면) relocate 로 돌아오는 곳 — 제 홈 차선의 // 홈 행이다. 왕복이라 그가 게임 중반까지 살아 움직인다(측정치는 Task 6 리포트). retire: { x: laneX[mi], y: n - 1 }, // trig (Task 6, 실측으로 정함 — 아래 표): Task 1 원래 값(n*2, 전체 보드)을 그대로 두면 // 인접 차선까지의 거리(build 가 강제하는 상수 2, 위 mi 주석)가 turn 1 부터 이미 trig // 안이라 즉시 깨어난다 — 그러면 순수 stay 걸음(GATE-POCKET, 강화된 ADMISSIBLE)에서도 // 차단 박자 전에 동료가 홈(포켓 곁)을 떠나 버린다. 반대로 trig 를 0/1 로 너무 좁히면 // "지워지지 않는" 표적(위)이라도 실제 인격 24 시드 × 6 인격 어느 걸음에서도 단 한 번도 // 안 깨어난다(실측: trig<=1 → moved 0/144 playouts). trig=2 는 그 사이 유일한 값이다 — // Y58-ROAD-CN-DISJOINT(Task 5 의 바, 최소 12/24)가 이 값에서 정확히 12/24 로 서고 // (trig=1 은 13/24 지만 이동 0, trig=3 은 이동은 그대로지만 disjoint 11/24 로 바 미달), // 인격 sweep 은 144/144 playout 전부에서 동료가 움직인다(측정치는 Task 6 리포트 표 참고). trig: 2, cap: 90, minTurns: 12, cautionD: 2, damage: 1, cell: _parkRoadCell(cell.seed), }; const st = { N: n, park, goal: 'reach', round: 0, hazard: new Set(), sacred: new Set(), wall, pos: { 0: spawn, 1: mate }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // 모든 멤버를 여기서 만든다 — _parkDeepClone 은 있는 것만 복제한다. rearendChargedPeriod // (Fix round 1, 2026-08-04): "이번 주기에 이미 하트를 청구했나"를 기억하는 장부 — 설계 // 법칙 1(§3, "드럼은 하트를 쓴다, 죽이지 않는다")을 지키려면 주기당 한 번만 청구해야 하는데, // 이 상태가 없으면 매 박자 무조건 청구다. -1 은 "아직 어느 주기도 청구 안 함" 센티널이다 // (실제 주기 인덱스는 0 이상이라 절대 안 겹친다). park.dyn.road = { scroll: 0, passed: new Set(), rearended: 0, yields: 0, rearendChargedPeriod: -1 }; return st; } // _parkRoadShuffle(arr, rng): 제자리 셔플. 형제 모듈의 헬퍼를 쓰지 않고 여기서 재파생한다. function _parkRoadShuffle(arr, rng) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); const t = arr[i]; arr[i] = arr[j]; arr[j] = t; } return arr; } // _parkRoadAdmissible(cell): 모듈 자신의 generate-then-filter 술어. 플레이 가능성이지 승격 바가 아니다. // // Task 6 이 강화한다 — C-N 을 pairs 로 선언하려면(아래 등록) 그 쌍이 실제로 설 수 있는 시드만 // 통과시켜야 한다("차단 박자에 포켓이 워커·동료 둘 다에게 한 수 거리"). _parkRoadPocket 은 // 이미 그 조건을 스스로 검증해 두므로(Task 3, 벽·사정거리 확인 포함) 여기서는 그 함수가 // 정직한 non-null 을 돌려주는 첫 차단 박자를 찾기만 하면 된다 — 순수 stay 로 30 턴까지 // 걷는다(브리프 원문 그대로). 실측: 24/24 — 동료가 이제 계약을 갖고 움직이지만(Task 6), // trig=2 는 순수 stay(L/R 없음, x 불변)에서 인접 차선까지의 거리(상수 2)보다 좁아 절대 // 안 깨어난다(위 trig 주석) — 그러니 이 admits 는 Task 3 이 쟀던 기하와 바이트가 같다. // 체인 완주 가능성 (Fix round 2, 2026-08-04, 컨트롤러 지시): 세 다리 순서 체인은 시드에 // 따라 도달 불가능할 수 있다 — chain:[1,2,0]으로도(위 park.chain 주석의 실측) 24 시드 중 // 10개는 6 페르소나 **전부**가 dest=2(다리 0, 동료 자신의 토큰)에서 영원히 멈춘다(동료가 // 자기 홈에서 안 비켜서 그 앞칸으로 가는 필드-최단 경로 자체가 막힌다). 이건 페르소나 // 취향이 아니라 시드의 기하다 — 실측(24×6=144)으로 확인: dest 최종값이 시드 안에서 // 페르소나와 무관하게 전부 같다(어느 시드도 페르소나에 따라 갈리지 않았다). 그래서 대표 // 페르소나 하나만 검사해도 충분하지만, `_parkYieldAdmissible`(engine.js:18621)의 선례를 // 따라 여섯 전부를 검사한다 — 한 시드 안에서 우연히 갈리는 페르소나가 미래에 생겨도 // 이 게이트가 그걸 놓치지 않도록. function _parkRoadChainCompletes(cell) { for (const persona of PARK_PERSONAS) { const P = parkStart(_parkRoadBuild(cell)); for (let i = 0; i < 90 && !P.over; i++) parkStep(P, parkOracleMove(P, persona)); if (P.dest !== P.st.park.chain.length) return false; } return true; } function _parkRoadAdmissible(cell) { const st0 = _parkRoadBuild(cell); if (!st0.park.road || st0.park.road.lanes.length !== PARK_ROAD_LANES) return false; const seen = new Set([`${st0.pos[0].x},${st0.pos[0].y}`, `${st0.pos[1].x},${st0.pos[1].y}`]); for (const nn of st0.park.road.npc) seen.add(`${nn.x},${nn.y}`); const hz0 = st0.park.road.hazard; if (hz0) seen.add(`${hz0.x},${hz0.y}`); if (seen.size !== 5) return false; // L3: xx 액터가 다섯째 몸이다 // 토큰 셋이 전부 도달 가능한 칸에 있어야 한다. for (const t of st0.tokens) if (t.y < 0 || st0.wall.has(t.y * st0.N + t.x)) return false; // 세 다리 전부가 실제로 완주되는 시드만 통과시킨다 — 위 _parkRoadChainCompletes 주석 참고. if (!_parkRoadChainCompletes(cell)) return false; // 차단 박자가 최소 한 번 오고, 그때 포켓이 워커·동료 둘 다에게 한 수 거리여야 한다. const P = parkStart(_parkRoadBuild(cell)); for (let i = 0; i < 30 && !P.over; i++) { if (_parkRoadGateBeat(P.st)) { const pk = _parkRoadPocket(P.st); if (!pk) return false; const md = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y); return md(P.st.pos[0], pk) <= 1 && md(P.st.pos[1], pk) <= 1; } parkStep(P, 'stay'); } return false; } // _parkRoadBandAt(st, y): 행 y 가 지금 드럼 밴드인가. dyn.scroll 의 순수 함수 — 보드가 아니라 // 시계를 읽는다. 주기 안에서 밴드는 하나씩 아래로 흐른다. 행 술어로 유지한다(테스트·페인터· // REAREND 장부가 씀 — 드리프트는 어느 차선이든 밀어붙이니 행만 보면 된다). function _parkRoadBandAt(st, y) { const road = st.park.road, dyn = st.park.dyn.road; const period = road.period; return ((y + dyn.scroll) % period) === 0; } // _parkRoadDrumAt(st, x, y): 이 **칸**이 곧 드럼 밴드에 덮이는가 — _parkRoadBandAt(행 술어)와는 // 다른 술어다(Task 5, C 의 prefer 전용). 행 술어를 그대로 "이 수가 드럼으로 가는 수인가"에 // 쓰면 세 차선이 한 행에서 통째로 막힌 걸로 읽혀 분리대 틈(포켓)까지 덩달아 "드럼"이 되고, // C.prefer 가 사실상 전체 합법 집합으로 무너진다 — 그러면 N 과 절대 소가 될 수 없다(실측 // 확인, 아래 _parkRoadReads.C 의 주석과 CN-DISJOINT 게이트 참고). 그래서 여기서는 **차선 // 칸만** 본다(park.road.lanes 의 x) — 분리대 틈은 밴드가 절대 안 훑는다는 뜻이고, 그래야 // 포켓이 "안전"할 자격이 생긴다. scroll+1 로 잰다 — _parkRoadTick 은 스크롤을 제일 먼저 // 올리므로(Task 2 순서 주석), 지금 고르는 수가 실제로 착지할 그 순간의 밴드는 scroll+1 // 기준이다 — engaged 가 말하는 "다음 밴드가 전방을 덮을 예정"과 같은 한 박자 앞선 시선이다. function _parkRoadDrumAt(st, x, y) { const road = st.park.road, dyn = st.park.dyn.road; if (road.lanes.indexOf(x) < 0) return false; // 분리대 틈은 밴드가 안 훑는다 return ((y + dyn.scroll + 1) % road.period) === 0; } // _parkRoadSolidEdge(st, xLo, y): 차선 xLo 와 xLo+1 사이의 모서리가 행 y 에서 실선인가. // xLo 는 차선의 x 값이지 인덱스가 아니다(예: 차선 [3,4,5,6,7] 이면 모서리는 3,4,5,6). // // 실선 = 차선 변경 금지. 파선 = 허용. 이게 배려(N)와 안전(C)이 갈리는 자리다 — // 실선 구간에서 "길을 비켜 준다"와 "규칙을 지킨다"가 같은 수를 못 낸다. // // dyn.scroll 이 인자에 들어가므로 **실선 구간이 도로와 함께 흘러 내려온다** — 흐르는 // 페인트 대시가 이미 dyn.scroll 의 함수인 것과 같은 시계를 탄다(app.js 마크①). // 순수 함수라 Y58-ROAD-SEED-PURE·SCROLL-PURE 를 유지한다: 같은 (시드, scroll) 은 항상 // 같은 답이고, 상태를 안 쓴다. // // 밴드가 아니라 해시인 이유: 밴드(예: y % 4 < 2)는 주기가 눈에 보여 사람이 외운다. // 해시는 시드마다 다른 무늬를 내되 여전히 결정적이다. function _parkRoadSolidEdge(st, xLo, y) { const dyn = st.park.dyn && st.park.dyn.road; const scroll = dyn ? dyn.scroll : 0; const seed = st.park.road.solidSeed >>> 0; // 행은 스크롤과 함께 흐른다 — 도로가 내려오므로 y+scroll 이 "도로 위의 절대 위치"다. const row = (y + scroll) >>> 0; let h = (seed ^ (xLo * 2654435761) ^ (row * 40503)) >>> 0; h = (h ^ (h >>> 13)) >>> 0; h = Math.imul(h, 1274126177) >>> 0; return ((h >>> 8) % 5) < 2; // 약 40% 가 실선 } // _parkRoadGateBeat(st): 주기의 마지막 박자(차단 박자)인가. dyn.scroll 의 순수 함수다. // // 타이밍: legalMask 도, 이 함수를 직접 부르는 코드도, parkStep 이 tick(_parkRoadTick, 아래) 을 // 불러 dyn.scroll++ 을 하기 **전**의 값을 읽는다 — 그 시점에 이번 턴의 합법 목록이 이미 // 확정돼 있으므로("방금 시작된" 다음 박자가 아니라 "지금 결정 중인" 이 박자를 봐야 한다). // _parkRoadTick 안에서 드리프트 면제를 결정할 때는 scroll 을 올리기 **전**에 이 함수를 한 번 // 캐싱해 쓴다(아래) — 증가 후에 다시 읽으면 이미 다음 박자를 보게 돼 legalMask 가 방금 막은 // 그 박자와 어긋난다(브리프가 경고한 함정, Y58-ROAD-GATE-FREE 로 확인). function _parkRoadGateBeat(st) { const road = st.park.road, dyn = st.park.dyn.road; return (dyn.scroll % road.period) === (road.period - 1); } // _parkRoadPocket(st): 차단 박자의 안전 포켓 — 워커의 홈 차선과 동료 차선 사이, 중앙분리대의 // 그 틈 칸(park.road.pocketX, build 시점에 "이웃 차선"으로 강제해 뒀다 — 위 _parkRoadBuild // 참고)이 유일한 후보다. 행은 워커의 **현재** 행이다. // // Fix round 1 (컨트롤러 실측, 2026-08-04): 순수 stay 로만 걸으면(Y58-ROAD-GATE-POCKET) 워커가 // 항상 바닥 행(짝수, 틈)에서 차단 박자를 맞아 후보가 늘 유효했다. 그런데 다양한 수로 걸으면 // (Y58-ROAD-POCKET-REAL) 워커가 **홀수** 행에서 차단 박자를 맞을 수 있고, 그 행은 분리대가 // 벽이다(build 의 파선 규칙, `y%2===1` → wall) — 그러면 그 칸은 아무도 못 들어가는 벽이라 // "안전 포켓"이 아니다(컨트롤러 144 표본 실측: 27.8% 가 벽 포켓). 벽 칸을 그대로 돌려주면 // 아무도 못 들어가니 다툼(C 대 N)이 애초에 안 열리는데도 포켓이 있다고 거짓 보고하는 셈이다. // 그래서 여기서 후보를 실제 보드로 다시 검증한다: 보드 안, 벽 아님, 워커·동료 둘 다 한 수 // 거리(Manhattan ≤1) — 넷 중 하나라도 안 맞으면 정직하게 null 을 돌려준다. null 을 지어내지 // 않는 이유: Task 6 의 admits 가 이 null 로 그 시드/그 박자를 걸러내야지, 존재하지 않는 칸을 // 목표로 삼게 하면 안 된다. function _parkRoadPocket(st) { if (!_parkRoadGateBeat(st)) return null; const n = st.N; const cand = { x: st.park.road.pocketX, y: st.pos[0].y }; if (cand.x < 0 || cand.y < 0 || cand.x >= n || cand.y >= n) return null; // 보드 밖 if (st.wall.has(cand.y * n + cand.x)) return null; // 이 행은 분리대가 벽이다 const md = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y); if (md(st.pos[0], cand) > 1) return null; // 워커가 한 수로 못 닿는다 if (md(st.pos[1], cand) > 1) return null; // 동료가 한 수로 못 닿는다 // 다섯째·여섯째 조건 (L3, 2026-08-05): 거리가 1 이어도 **합법**이 아닐 수 있다. // 모서리 실선이 가로 이동을 막으면 그 칸은 아무도 못 들어가는데, 그걸 "안전 포켓"이라고 // 부르면 거짓말이다. 두 도메인의 마스크를 실제로 불러 확인한다. // // 여기서 P 가 없으므로(이 함수는 st 만 받는다) 마스크를 직접 재파생한다 — 실선 판정은 // _parkRoadSolidEdge(st, ...) 로 st 만 있으면 되므로 P 가 필요 없다. 차단 박자 전방 // 폐쇄는 여기서 안 본다: 그건 워커의 세로 전진을 막는 것이고 포켓은 가로 대피라 // 애초에 겹치지 않는다. const crossBlocked = (body) => { if (body.y !== cand.y) return false; // 세로 이동엔 모서리가 안 걸린다 const d = cand.x - body.x; if (d === 0) return false; // 이미 그 칸에 있다 if (d !== 1 && d !== -1) return true; // 한 수로 못 닿는다(위 md 검사와 중복 안전망) return _parkRoadSolidEdge(st, Math.min(body.x, cand.x), body.y); }; if (crossBlocked(st.pos[0])) return null; // 실선이 워커를 막는다 if (crossBlocked(st.pos[1])) return null; // 실선이 동료를 막는다 (사용자 결정) return cand; } // 두 마스크를 합성한다. **대상 도메인이 다르다**(사용자 결정 2026-08-05): // // ① 차단 박자 전방 폐쇄 — 'me' 전용. 예전 그대로다. 동료까지 얼리면 그건 "실선 구속"이 // 아니라 다른 장치이고, 이 함수의 옛 주석이 지킨 전제("그의 계획이 포켓을 지나는 // 것이 이 장치의 전제")를 이유 없이 깬다. // ② 실선의 가로 차단 — 'me' 와 'mate' 둘 다. 도로 규칙은 사람을 안 가린다. // 선례: y8 통나무("THE LOG IS A WALL FOR HIM, NOT FOR YOU", legalMask 'mate' only), // 얼림 마스크. // // ②가 동료에게 걸리면 그가 포켓에 못 가는 박자가 생기고 그 박자엔 C-N 쌍이 안 선다. // 그래서 _parkRoadPocket 이 이 마스크를 실제로 물어본다 — 못 닿으면 정직하게 // null 이고, admits 가 그 시드를 거른다. 포켓이 있다고 거짓 보고하지 않는다. // // 'route' 는 일부러 안 건다. legalMask 의 계약은 도메인이 **셋**이고('me'/'mate'/'route', // 위 심 주석) 'route' 는 사람이 아니라 persona 의 경로 지표다. 실선은 "사람을 구속한다"는 // 결정이었지 "지표를 좁힌다"가 아니다 — 게다가 이 술어는 몸의 현재 x·행과 scroll 에 // 달려 있어서, 지표에 걸면 매 턴 모양이 바뀌는 마스크로 BFS 를 좁히게 된다(route 에서 // 막은 칸은 오라클이 영원히 못 고른다). 좁히려면 별도 결정으로 별도 게이트와 함께 한다. // // 모양은 엔진의 _parkMaskOf(P,who) 와 같은 (P,who)->((key)=>bool)|null — 등록 자리(아래)에서 // 얇은 어댑터 하나로 엔진의 legalMask(P,key,who) 3-인자 계약에 잇는다. function _parkRoadLegalMask(P, who) { const st = P.st, n = st.N; if (who !== 'me' && who !== 'mate') return null; // 'route' 는 안 좁힌다 const body = who === 'mate' ? st.pos[1] : st.pos[0]; if (!body) return null; const gateFront = (who === 'me' && _parkRoadGateBeat(st)) ? st.pos[0].y - 1 : null; return (key) => { const kx = key % n, ky = Math.floor(key / n); if (gateFront !== null && ky === gateFront) return true; // ① 워커 전용 if (ky !== body.y) return false; // ② 가로 이동만 본다 const d = kx - body.x; if (d !== 1 && d !== -1) return false; // 인접 가로 한 칸만 return _parkRoadSolidEdge(st, Math.min(body.x, kx), body.y); // 실선이면 차단 }; } // _parkRoadTick(P, ev): 세상이 한 박자 나아간다. 순서가 중요하다 — // ① 스크롤(밴드가 내려온다) ② 드리프트(몸이 밀린다) ③ NPC 이동. // ②가 ①보다 뒤인 이유: 밀려난 칸이 드럼인지를 "이번 박자의" 밴드로 판정해야 화면과 물리가 같다. function _parkRoadTick(P, ev) { const st = P.st, dyn = st.park.dyn.road; // 차단 박자 여부는 scroll 을 올리기 전에 한 번 잰다 — legalMask 가 이 턴의 합법 목록을 // 정할 때 본 것과 같은 박자를 봐야 한다(_parkRoadGateBeat 의 타이밍 주석 참고). const wasGateBeat = _parkRoadGateBeat(st); dyn.scroll++; // ② 드리프트. 이동 수 문자열은 엔진 관용구 'U'/'D'/'L'/'R'/'stay'다(_PARK_MOVES) — ↑ 는 // 면제고, 차단 박자도 면제다 — 그 박자에 G 는 얻을 것도 잃을 것도 없어야 만장일치가 산다. if (ev.mvKey !== 'U' && !wasGateBeat) { const ny = st.pos[0].y + 1; const k = ny * st.N + st.pos[0].x; const blocked = ny >= st.N || st.wall.has(k) || (st.pos[1] && st.pos[1].x === st.pos[0].x && st.pos[1].y === ny); if (!blocked) { st.pos[0] = { x: st.pos[0].x, y: ny }; // 밀려서 드럼을 밟았다 — 들어가기로 한 진입이 아니므로 별도 장부에 청구한다. 보편 // deep-entry 청구(park.deep 기반)와는 완전히 별개다 — 이 보드는 park.deep 을 비워 // 두므로(엔진 보편 계약 메모 참고) 그 청구는 애초에 이 보드에서 절대 발생하지 않는다. // // Fix round 1 (컨트롤러 실측, 2026-08-04): 설계 법칙 1(§3) — "드럼은 하트를 쓴다, // 죽이지 않는다". 그런데 매 박자 무조건 -1 을 물리면 하트 예산(3)이 REAREND 만으로 // 정확히 소진돼(실측: 144/144 이 정확히 40턴·정확히 3회 REAREND 에서 사망) 조심이 // 유일하게 중요한 변수가 되고 다른 어떤 선택도 무의미해진다. 그래서 **주기당 한 번만** // 하트를 깎는다 — 장부(dyn.rearended)는 매번 그대로 센다(밀린 사실 자체는 진짜니까, // 게이트가 이 카운터의 non-vacuity 를 이 값에 의존해 잰다). 주기 인덱스는 scroll 이 // 이미 올라간(위 dyn.scroll++) 값으로 잰다 — _parkRoadBandAt 이 드럼 밴드를 판정할 때 // 읽는 것과 같은 scroll 값이라 "이번 박자가 몇 번째 주기인가"가 일관된다. if (_parkRoadBandAt(st, ny)) { dyn.rearended++; const period = Math.floor(dyn.scroll / st.park.road.period); if (dyn.rearendChargedPeriod !== period) { P.hearts--; dyn.rearendChargedPeriod = period; } st.fx.push({ k: 'rearend', x: st.pos[0].x, y: ny }); } } } // ③ NPC 이동. 값 없는 스크립트 액터 — 제 차선에서 한 박자에 한 칸씩 앞으로 흐른다. // // 재생성(2026-08-05): 예전에는 ny >= st.N 이면 그 자리에 **얼어붙었다** — 재생성 경로가 // 없어서 24/24 시드가 정확히 턴 5 에 정지했고, 40턴 중 35턴(87.5%)이 교통 없는 판이었다. // 게다가 두 NPC 가 결국 같은 바닥 행으로 수렴해, 같은 차선이면(실측 7/24) 같은 칸에서 // 겹치고 추월 토큰 두 개(체인 다리 둘)까지 같이 겹쳤다. 그 7 시드는 _parkRoadAdmissible // 탈락 10개에 100% 포함됐다(겹치고 통과한 시드 0개). // // 도로는 트레드밀이다: 끝에 닿은 차는 위로 되돌아 다시 내려온다. 되돌아가는 줄은 // y=0 고정이 아니라 시드 결정적 간격 위(y = -gap)다 — 고정이면 차간 간격이 영구 // 고정돼 교통이 메트로놈처럼 읽힌다. x 는 절대 안 건드린다(차선 변경 없음). const rec = st.park.road.recycleGap || [1, 1]; for (let i = 0; i < st.park.road.npc.length; i++) { const nn = st.park.road.npc[i]; const ny = nn.y + 1; if (ny >= st.N) { nn.y = -(rec[i] || 1); continue; } // 도로 끝 — 위로 되돌린다 if (!st.wall.has(ny * st.N + nn.x)) nn.y = ny; } // xx 액터: 아래로 흐르되, 제 일정대로 차선도 바꾼다. 다른 NPC 와 달리 x 가 움직인다. // 벽(갓길)으로는 못 나가고, 다른 몸 위로도 안 간다 — 규칙을 안 지키는 건 차선 변경 // 예고이지 물리가 아니다. 실선도 무시한다(그게 이 액터가 위험한 이유다). { const hz = st.park.road.hazard; if (hz) { const dir = hz.plan[dyn.scroll % hz.plan.length] | 0; const nx = hz.x + dir; if (dir !== 0 && st.park.road.lanes.indexOf(nx) > -1) hz.x = nx; const hy = hz.y + 1; hz.y = hy >= st.N ? -(st.park.road.recycleGap[0] || 1) : hy; } } // ④ 추월 토큰 추적 (Task 4). 토큰은 제 차량을 따라간다 — 동료(pos[1])와 NPC 둘, build 시점의 // bodies 순서(mate, npc[0], npc[1])와 그대로 맞춘다. 죽은 토큰은 더 안 쫓는다 — 이미 추월한 // 자리를 얼려 두는 편이 (계속 쫓아가 유령처럼 살아 있는 칸을 만드는 것보다) 정직하다. 목적지가 // 다시 진입 불가 셀이 되면(예: 앞차가 벽 쪽으로 밀려도 여긴 벽이 없다) 그 박자는 그냥 건너뛴다 // — route mask 의 Infinity 함정을 다시 만들지 않기 위함이다. const bodies = [st.pos[1], ...st.park.road.npc]; for (let i = 0; i < st.tokens.length; i++) { const t = st.tokens[i], b = bodies[i]; if (!t.alive || !b) continue; const ty = b.y - 1; if (ty >= 0 && !st.wall.has(ty * st.N + b.x)) { t.x = b.x; t.y = ty; } } } // _parkRoadOnEnter(P, ev): 앞차의 앞칸에 닿으면 추월이 성사된다. 'stay' 로는 성사되지 않는다 — // 추월은 능동이다(가만히 있는데 앞차가 밀려와 겹쳐도 그건 추월이 아니다). 토큰의 죽음(alive 를 // false 로)은 엔진의 GENERIC 경로가 이미 한다 — pad:true 라 engine.js:8277-8290 의 reach-pad // 픽업이 onLeave/onEnter(engine.js:8320-8321) 보다 먼저 잡는다. 그래서 여기서 `!t.alive` 로 // 걸러 내려 하면 항상 이미 죽어 있어 아무것도 못 잡는다(자체 점검으로 잡은 결함 — 첫 구현은 // dyn.road.passed 가 영원히 빈 채로 통과됐다). 대신 `!passed.has(t.gtype)` 로 "이 토큰을 아직 // 우리 장부에 안 적었다"를 게이트로 쓴다 — 죽음 자체가 아니라 **기록**을 이 훅의 일로 남긴다. function _parkRoadOnEnter(P, ev) { const st = P.st, park = st.park, passed = park.dyn.road.passed; if (ev.mvKey === 'stay') return; // Task 6: 이 장부는 추월 대상(park.chain = gtype 0,1,2 의 순열)만 잰다. 동료의 계약 // 표적(gtype 3, 위 tokens.push 주석)은 추월 대상이 아니다 — 제한 없이 st.tokens 전부를 // 훑으면 워커가 우연히 그 칸을 밟았을 때 그 표적이 죽어 동료의 "up the road" 여정이 // 중간에 끊긴다. 넷째 토큰이 생기기 전(Task 4)에는 st.tokens 가 곧 park.chain 이라 이 // 제한이 없어도 결과가 같았다 — 지금부터는 다르다. (Fix round 2, 2026-08-04: park.chain // 은 이제 세 다리 [1,2,0] 다, chainAnyOf 는 더 안 쓴다 — 위 park.chain 주석 참고. 순서가 // 바뀌어도 이 코드는 그대로다 — indexOf 는 멤버십만 보지 순서를 안 본다.) const overtakeSet = park.chain; for (const t of st.tokens) { if (overtakeSet && overtakeSet.indexOf(t.gtype) < 0) continue; if (passed.has(t.gtype)) continue; if (t.x === ev.to.x && t.y === ev.to.y) { t.alive = false; // 방어적 — GENERIC 경로가 보통 이미 죽여 뒀다. passed.add(t.gtype); st.fx.push({ k: 'overtake', x: ev.to.x, y: ev.to.y }); break; } } } // _parkRoadReads — 이 메커닉이 C(조심)·N(배려) 두 마음에 대는 몫(Task 5). 엔진의 계약은 하나뿐 // 이다(_parkReads 의 "두 규칙" 주석) — ENGAGEMENT 는 OR, PREFERENCE 는 교집합. 그래서 여기서 // 반환하는 prefer 는 배송 태도의 부분집합으로만 남는다 — 넓히거나 대체할 길이 없다. 대부분의 // 박자에는 두 facet 다 "이 수들을 금한다"를 "나머지 전부"로 반환하는 형태로 쓴다 — 빈 집합은 // 거부가 아니라 그 마음의 침묵이고, 침묵하면 다음 마음이 무제한으로 지배하기 때문이다(그래서 // 아래 두 prefer 모두 끝에 "전부 드럼/전부 포켓이면 그래도 전체를 돌려준다" 폴백이 있다). // **차단 박자(gate ∧ pocket)는 이 모양의 예외다**(Task 6, 2026-08-04): 거기서 C 는 "나머지 // 전부"가 아니라 **양의 단일원소 {pocket}** 을 돌려주고(C.prefer 첫 절), N 은 정확히 // legal ∖ {pocket} 을 돌려준다(N.prefer, 포켓으로 가는 수만 금한다). 이 비대칭 — 한쪽은 // 긍정 지목, 한쪽은 그 하나만 뺀 나머지 — 이 둘을 그 박자에서 구조적으로 서로소로 만든다. // 앞서 있던 "둘 다 나머지 전부" 모양을 차단 박자에도 그대로 쓰면 C 가 N 의 상위집합이 되어 // 여섯 순서 전부 같은 답을 내고 상이 0 이었다(실측 81/81 겹침, 교체 전 구현). const _parkRoadReads = { ctx: (P) => { const st = P.st; return { gate: _parkRoadGateBeat(st), pocket: _parkRoadPocket(st) }; }, C: { // 조심은 전방(내 행-1)이 곧 드럼이거나, 지금이 차단 박자면 깨어난다. 차단 박자에는 항상 // 참이다(_parkRoadGateBeat 정의 그대로) — CN-DISJOINT 게이트가 요구하는 자리다. // // Task 6 이 고친 자리(REGISTRY-SEAM 의 SEAM-COLD-PREFER 가 잡았다 — Task 5 의 tip 커밋 // 412991c 에서 이미 red 였다, 독립 워크트리로 재확인함): Task 5 원문은 여기서 **전방 한 // 칸(내 행-1)** 만, 그것도 **행** 술어 _parkRoadBandAt 을 CURRENT scroll 로 썼는데, 바로 // 아래 prefer() 는 legal **후보 전부**(U/D/L/R/stay 각각의 도착 칸)를 **칸** 술어 // _parkRoadDrumAt 으로 scroll+1 기준으로 훑는다 — engaged 는 "앞으로 한 걸음"만 냉/온을 // 재는데 prefer 는 "이번에 낼 수 있는 모든 수"를 잰 것이었다. 그러니 예컨대 'D'(뒤로 두 // 칸 밀림)가 다음 scroll 에 드럼이 될 행으로 가는 경우, engaged 는 여전히 거짓인데(전방만 // 봤으므로) prefer 는 그 'D' 를 조용히 걸러낸다 — 냉 상태에서 좁혀지는 것 자체가 // SEAM-COLD-PREFER 의 규율 위반이다(실측: 270→234건, 전부 road.C, 전방 U 는 고쳐도 다른 // 방향이 남았다). 고침: engaged 도 prefer 와 정확히 같은 도메인(그 순간의 legal 후보 // 전부)과 같은 술어(_parkRoadDrumAt, scroll+1)로 "이 중 하나라도 곧 드럼이냐"를 묻는다 — // 한 질문을 한 번만 한다. CN-DISJOINT(당시 결정론적 "다양한 수" 걸음, 12/24 **시드** — // 이 수치는 2026-08-04 오라클 걸음으로 교체되며 superseded 됐다; 현재 바는 인격 오라클이 // 실제로 지나는 81/81 **states**, disjoint===posed 다 — 아래 prefer() 의 포켓 절 주석 // 참고)·PREFER-NONEMPTY·SEAM-COLD-PREFER(road 분: 0 offenders) 재실측 완료(리포트 참고). engaged: (P, ctx) => ctx.gate || _parkLegal(P).some(m => _parkRoadDrumAt(P.st, m.x, m.y)), // "드럼 칸으로 가는 수를 뺀 나머지 전부" — 이건 prefer() 의 **두 번째 절**(차단 박자가 // 아니거나 포켓이 없을 때)의 모양이다. 차단 박자에 포켓이 있으면 prefer() 는 이 규칙을 // 아예 안 타고 먼저 {pocket} 하나로 좁혀 돌려준다(첫 번째 절) — 그 예외가 N 과의 서로소를 // 만드는 자리다(위 _parkRoadReads 헤더 주석 참고). 여기 쓰는 것은 행 술어(_parkRoadBandAt)가 아니라 // **칸** 술어 _parkRoadDrumAt 다 — 그 함수 자신의 주석에 이유가 있다: 행 술어를 쓰면 // 분리대 틈(포켓)까지 "드럼"으로 잡혀 이 집합이 전체 합법 집합으로 무너지고, N 과 절대 // 소가 될 수 없다(차단 박자에 워커가 포켓 옆(바닥 행)에 있을 때 실측: C 가 행 술어를 // 쓰면 disjoint 0/18 — 결정을 못 재는 장치가 된다). prefer: (P, legal, ctx) => { const st = P.st, out = new Set(); // 차단 박자에는 조심이 **포켓을 가리킨다** (스펙 §7 원문, 2026-08-04 구현). // 일반 규칙(드럼 아닌 수 전부)을 그 박자에도 쓰면 포켓 아닌 안전한 칸까지 통과시켜 // 조심이 배려의 **상위집합**이 되고, 그러면 배려의 좁힘이 순서와 무관하게 늘 이겨 // 상이 0 이다(실측 81/81 겹침). if (ctx && ctx.gate && ctx.pocket) { for (const m of legal) if (m.x === ctx.pocket.x && m.y === ctx.pocket.y) out.add(m.k); if (out.size) return out; } for (const m of legal) if (!_parkRoadDrumAt(st, m.x, m.y)) out.add(m.k); // 공집합 방지 — 갈 곳이 전부 드럼이면 조심은 "가장 덜 나쁜 것"을 말한다(침묵하지 않는다). if (!out.size) for (const m of legal) out.add(m.k); return out; }, }, N: { // 배려는 안전 포켓이 있으면 깨어난다. 브리프 원문은 "동료의 계획이 포켓을 지난다"를 // _parkCompanionPlan 으로 재려 했지만, 이 보드의 동료는 NO CONTRACTS 라 P.mode 가 'idle' // 을 못 벗어난다(실측 확인) — _parkCompanionPlan 은 이 판에서 항상 null 이고, 그 조건을 // 그대로 쓰면 N 은 영원히 안 깨어난다. 대신 _parkRoadPocket 자신의 정의를 그 "계획"으로 // 쓴다 — 포켓이 non-null 이라는 것 자체가 이미 "워커와 동료 둘 다 그 칸에 한 수로 닿는다" // 를 Task 3 이 검증해 뒀다(벽·사정거리 확인 포함). 붙박이 동료에게 "계획"이란 제 사정거리 // 안의 칸이 전부이고, 포켓이 그 안에 있다는 사실 자체가 그녀의 유일한 "계획"이다. engaged: (P, ctx) => !!ctx.pocket, // 포켓에 들어가는 수만 금한다. stay 는 남는다 — 제자리에서 그가 지나가게 두는 것도 양보다. // // ctx.pocket 널 가드 (Task 6): _parkReads 는 "우리 engaged 가 거짓이어도 걷기판 자신의 // 제너릭 N(PARK_ATTITUDES.N, _parkNCtx 기반)이 이미 참이면" prefer 를 여전히 부른다(교집합 // 자리, engine.js:7930-7937) — 그 제너릭 N 은 `park.contracts`/`_parkCompanionPlan` 을 // 직접 읽으므로, Task 6 이 계약을 채운 지금은 우리 포켓과 무관하게(예: 동료가 걷다가 // 워커의 바로 다음 칸을 막는 순간) 참이 될 수 있다 — 계약이 비어 있던 Task 1~5 에서는 // 제너릭 N 이 구조적으로 항상 거짓이라(gem=null, plan=null) 이 자리가 절대 안 불렸다. // 그때는 널 가드가 없어도 안전했지만 지금은 아니다 — pocket 이 null 인데 그대로 // ctx.pocket.x 를 읽으면 TypeError 다(POCKET-REAL 걸음에서 RED 로 확인). 포켓이 없으면 // 이 마음은 할 말이 없다 — 침묵(전체 legal 반환)한다. prefer: (P, legal, ctx) => { const out = new Set(); if (!ctx.pocket) { for (const m of legal) out.add(m.k); return out; } for (const m of legal) if (!(m.x === ctx.pocket.x && m.y === ctx.pocket.y)) out.add(m.k); if (!out.size) for (const m of legal) out.add(m.k); return out; }, }, }; // PARK_ROAD_SHIP_SEED / _parkRoadRecovers / PARK_ROAD_SHIPPABLE — 모듈 자신의 승격 핀 (Task 6). // DERIVED, 절대 단언하지 않는다(siege 선례, engine.js:18651 의 PARK_YIELD_SHIPPABLE 과 같은 // 모양). 양은 이 모듈의 shipped-seed 셀에서의 보정된 6/6 눈 감은 순서 복원 — 보정이란 // PARK_CAL_TURNS 에서 읽는다는 뜻이고, 그게 제품의 판독 화면이 쓰는 skip 이다. 다른 skip 으로 // 재는 바는 화면이 버리는 주장을 인증한다. // // 실측(Task 6 리포트 참고): 이 함수는 seed=1 에서 false 를 낸다 — 여섯 페르소나 전부 // reason='cap'(90 턴 소진, 'complete' 아님)이라 첫 페르소나에서 이미 return false 다. C-N 이 // 실측에서 expressed=0(아래 PAIRS 게이트 참고)이라 needPairs 의 완주 조건(P.posed.size===3)이 // 이 보드에서 한 번도 안 채워지기 때문이다 — 이 값도 튜닝해 true 로 만들지 않는다, 조건이 // 곧 발견이다. const PARK_ROAD_SHIP_SEED = 1; function _parkRoadRecovers(cell) { for (const persona of PARK_PERSONAS) { const P = parkPlayout(_parkRoadBuild(cell), persona); if (P.reason !== 'complete') return false; const r = parkRecoverOrder(_parkRoadBuild(cell), P.moves, PARK_CAL_TURNS); if (!r || r.join() !== persona.join()) return false; } return true; } const PARK_ROAD_SHIPPABLE = _parkRoadRecovers(_parkRoadCell(PARK_ROAD_SHIP_SEED)); PARK_FIELD_MECHS.road = { build: _parkRoadBuild, cell: _parkRoadCell, admits: _parkRoadAdmissible, tick: _parkRoadTick, onEnter: _parkRoadOnEnter, // 엔진의 legalMask(P,key,who) 3-인자 계약을 _parkRoadLegalMask 의 (P,who)->((key)=>bool)|null // 모양에 잇는 얇은 어댑터 — _parkRoadLegalMask 자체의 주석 참고. legalMask: (P, key, who) => { const f = _parkRoadLegalMask(P, who); return !!f && f(key); }, // C·N 두 마음에 대는 이 메커닉의 몫(Task 5) — _parkRoadReads 자신의 주석 참고. reads: _parkRoadReads, // pairs (Task 6): m4 서명이 den:['gc','gk'] 로 G-C·G-N 을 이미 준다(parkPosedPairs, m4 항목). // 이 모듈은 그 위에 C-N 하나만 얹는다 — 가장 작은 증명 부담. 선언은 admits 가 expressed>0 // 으로 갚아야 인정된다(parkPosedPairs 의 계약, Y58-ROAD-PAIRS 게이트) — 실측은 그 값이 // 0 임을 보인다(리포트 참고), 그래서 이 선언은 코드에는 있지만 아직 갚지 못한 채무다. pairs: [['C', 'N']], }; /* ============ END ROAD FIELD MODULE (Task 6 — pairs: [['C','N']], admits 강화, 승격 핀) ============ */ /* ============ PLAZA FIELD MODULE (y59 "광장 신호수", spec 2026-08-03) ============ */ /* 관점 반전의 계승. y19 관제탑과 마찬가지로 **걷는 것은 내가 아니다** — 색을 가진 주민들이 스스로 광장을 건너 제 색 출구로 가고, pos[0] 은 몸 없는 커서다. 관제탑이 주민 하나·게이트 셋의 나무형 복도였다면 이 판은 교차로가 있는 열린 광장이다: 여러 주민의 동선이 문간에서도 광장 한복판에서도 겹친다. 이 모듈의 주장은 하나다: **커서의 이동 시간이 곧 난이도다.** 이 엔진은 클릭 시임이 원천 금지라 입력이 5키(이동 넷 + `stay`)뿐이고, 그래서 커서의 **출퇴근**이 관제 장르에 없는 자원이 된다. 가장 먼 두 신호기 사이가 정확히 6박이다 — 한쪽 사건을 구하러 가면 반대편 사건은 놓치도록 잰 값이고, 이 상수 하나가 double-bind 를 만든다. 그러니 난이도 다이얼은 편성표가 아니라 **신호기 배치 기하**(커서 왕복 거리 x 스폰 밀도)다. "전 신호 일괄 토글" 같은 원격 편의 기능은 이 경제를 통째로 지우므로 금지다(스펙 §2-⑥). **하트를 쓴다, 죽이지 않는다.** 충돌 사건당 관제사 ♥-1, 얽힌 주민은 1박 기절 후 재개한다 (기본 예산 셋). 터미널이면 조심이 전부가 되어 관제가 헐거워진다 — y58 도로의 설계법과 같은 원리다. 보드에 deep 도 water 도 **한 칸도 없다.** 그래서 커서는 deep 진입 요금을 영원히 물지 않는다 — 요금이 환불되기 때문이 아니라 과금 대상 지형이 애초에 존재하지 않기 때문이다 (관제탑은 그것을 마스킹으로 했고, 여기서는 지형 부재로 더 강하게 다시 세운다). 그래도 park.deep / verge / water 는 **빈 Set 으로 반드시 존재해야 한다**: _parkFields 가 이 셋을 이름으로 읽으므로, 없으면 첫 playout 이 `undefined.has` 로 죽는다. Task 1 은 정적인 절반뿐이다 — 상수, 시드 순수 보드, 시드 순수 편성표, play-cell. 주민의 보행과 신호 토글, reads/admits, 화면, 캠페인 슬롯은 뒤 태스크의 몫이다. PARK_FIELD_MECHS 등록도 admits 가 생기는 다음 태스크로 미룬다: admits 없는 맨 등록은 모듈이 그 바를 견딜 수 있기도 전에 FIELD-REGISTRY-SURFACE 의 수를 바꿔 놓는다. */ const PARK_PLAZA_N = 12; const PARK_PLAZA_SPAWN_GAP = 4; const PARK_PLAZA_COUNT = 10; const PARK_PLAZA_TELEGRAPH = 2; // 스폰 몇 박 전부터 예고 점멸하는가 const PARK_PLAZA_STUN = 1; // 충돌에 얽힌 주민이 쉬는 박 수 const PARK_PLAZA_TAIL = 20; // 박자 상한 = 실제 마지막 스폰이 일어난 박 + TAIL (스펙 §3-7) const PARK_PLAZA_PIPS = 3; // v1.1 patience 핍. 배선은 뒤 태스크, 상수는 지금 못 박는다 const _PARK_PLAZA_DIRS = [{ x: 0, y: -1 }, { x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }]; // 상>우>하>좌 (주민 BFS 타이브레이크, 스펙 §3-5) // _parkPlazaCell(seed): 모듈의 PUBLIC play-cell. 순수 값 생성자이고 persona 매개변수는 없다(C1). // 관제탑의 harvest 문법을 그대로 탄다 — 체인 보석은 두 출구이고, 그 색의 마지막 배달이 // 그 출구의 보석을 끈다(완주 판정은 엔진의 generic 체인 경로가 센다. 본문 0줄). function _parkPlazaCell(seed) { return { goalVariant: 'harvest', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: seed >>> 0, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', fieldMech: 'plaza' } }; } // _parkPlazaBuild(cell): y59 보드. 순수 PUBLIC 기하다 — cell.seed 만 읽고, persona 매개변수는 // 존재하지 않는다(C1). 12x12 열린 광장을 벨트 벽 한 줄이 남북으로 가르고, 그 벽에 뚫린 문간 // 셋만이 유일한 통로다. 문간마다 제 신호기가 하나씩 붙는다. // // x: 0 1 2 3 4 5 6 7 8 9 10 11 // y= 1 entrN(6,1) 북쪽 입구 // y= 3 exitR(10,3) 빨강 출구 (동·북반) // y= 5 sig0(3,5) sig2(9,5) // y= 6 #### door(3,6) door(6,6) door(9,6) #### <- 벨트 벽 // y= 7 sig1(6,7) // y= 9 exitB(1,9) 파랑 출구 (서·남반) // y=10 entrS(6,10) 남쪽 입구 // // sig1 만 벨트 남쪽에 서는 이유: 그래야 sig0<->sig2 가 정확히 6박(레이아웃 게이트의 핀)이고, // sig1 은 반대편에서만 닿으므로 커서 동선이 십자형이 된다. 출구를 대각으로 놓은 결과, 북에서 // 스폰한 파랑과 남에서 스폰한 빨강만 벨트를 건넌다 — 교차 흐름이 문간과 광장 한복판 양쪽에서 // 만난다. 그 "절반"을 매 판 실제로 세우는 것은 아래 편성표의 색 주기 4 다(스펙 §3 확정 문구). // 키오스크 벽은 v1 에 없다: 벨트가 유일한 분리대다. function _parkPlazaBuild(cell) { const seed = ((cell && cell.seed) | 0) >>> 0; const n = PARK_PLAZA_N; const r = rng((seed * 7433 + 2897) >>> 0 || 1); // plaza 고유 소금 (형제 모듈과 겹치지 않는다) // cs(lo,hi): 닫힌 구간 균등 정수. 상한을 넘지 않는 것은 우연이 아니라 **구조**다 — // rng(engine.js:59-64)가 `((s >>> 0) % 1e6) / 1e6` 를 돌려주므로 r() <= 0.999999 가 보장되고, // 따라서 cs(0,5) 는 (0.999999*6)|0 = 5 를 넘을 수 없다. 아래 balancedSwap 의 `[g,g+4,g+8][t/2|0]` // 가 그 상한에 하중을 건다(6 이 나오면 undefined 를 인덱싱한다). rng 의 나눗셈을 바꾸는 사람은 // 이 줄을 먼저 읽을 것. const cs = (lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); const K = (x, y) => y * n + x; const PT = (x, y) => ({ x: x, y: y }); const beltY = 6; const gapX = [3, 6, 9]; // --- 벽: 테두리 전체 + 벨트 한 줄(문간 셋만 열린다) --- const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) wall.add(K(x, y)); for (let x = 1; x <= n - 2; x++) if (gapX.indexOf(x) < 0) wall.add(K(x, beltY)); // 앵커. sigKeys[i] 가 doorKeys[i] 를 막는다 — 배선은 별도 표가 아니라 **인덱스 정렬**이고, // 신호기는 제 문간에서 한 칸 떨어져 선다. 바깥 둘은 벨트 북쪽, 가운데 하나만 남쪽이다. const doorKeys = [K(gapX[0], beltY), K(gapX[1], beltY), K(gapX[2], beltY)]; const sigKeys = [K(gapX[0], beltY - 1), K(gapX[1], beltY + 1), K(gapX[2], beltY - 1)]; const entrN = K(6, 1), entrS = K(6, 10); const exitRP = PT(10, 3), exitBP = PT(1, 9); // --- 지형: 광장은 통째로 walkway 다. deep·water 는 한 칸도 없다(헤더 참고) --- const walkway = new Set(), verge = new Set(), deep = new Set(), water = new Set(); for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) { const kk = K(x, y); if (!wall.has(kk)) walkway.add(kk); } // distDeep 는 deep 집합에서 출발하는 다중출발 BFS 인데, 여기엔 출발점이 하나도 없다 — // 관제탑의 그 BFS 를 그대로 돌려도 큐가 빈 채로 시작해 한 바퀴도 돌지 않고 전 칸이 // Infinity 로 남는다. 결과가 같으므로 절대 돌지 않는 루프를 베끼지 않고 그 답만 세운다. // (전 칸 Infinity 는 "어디도 deep 이 아니다"라는 뜻이고, distDeep >= 2 를 안전 walkway 로 // 읽는 모든 상류 코드가 그대로 통과한다.) const distDeep = new Array(n * n).fill(Infinity); // --- 편성표 (시드 순수) --- 스펙 §3: 10명, 4박 간격, 입구 라운드로빈. // 시드 소비는 딱 넷이다 — 시작 색, 시작 입구, 그리고 입구 조별 균형 교환 둘. 그 밖엔 이 보드에 // 무작위가 없다(기하는 상수다). // // **색의 주기는 4다(쌍 단위 교대). 입구의 주기 2 와 결합하지 않는 것이 이 줄의 존재 이유다.** // v1 은 색도 입구도 i%2 로 교대시켰다. 그러면 (색,입구) 짝이 시드 비트 하나(startR XOR startN) // 로 완전히 결정된다 — 한 판이 통째로 "거의 전원 횡단"이거나 "아무도 안 건넘"이 되고, // 실측 횡단율은 32/100, 판정 시드 10개 중 8개가 횡단자 1~2명이었다(Task 4b 재측정). // 스펙 §3 의 확정 문구는 그 반대를 요구한다: **"절반의 교통이 벨트를 건넌다"** — 시드마다 // 도박이 아니라 매 판 절반이다. 결과가 스펙이고 교대는 그 결과를 내려던 수단이었으므로, // 수단인 색 교대를 주기 4 로 바꾼다(입구 라운드로빈은 스펙이 못 박은 그대로 남는다). // // 산수 — **조 단위로** 센다. (쌍 단위 논증은 쓰지 말 것: "연속한 두 주민은 색이 같다"는 // 아래 balancedSwap 이 깨뜨린다. 결론은 그대로 참이지만 그 모델로는 증명이 안 된다.) // // (1) 입구가 라운드로빈이므로 주민 열은 **두 조**로 갈린다 — i 의 홀짝이 같은 다섯 명씩 // (한 조는 전부 북, 다른 조는 전부 남). // (2) 색은 (i>>1) 의 홀짝이 정한다. 어느 조든 그 다섯 명의 (i>>1) 은 {0,1,2,3,4} 라 // 짝수 셋·홀수 둘이다 → **모든 조가 언제나 색 X 셋 + 색 Y 둘 (3:2)**. startN 과 무관하다. // (3) 출구가 대각이라 벨트를 건너는 것은 (북에서 온 B) 와 (남에서 온 R) 뿐이다. // X = R 이면 두 조가 3R2B 라 횡단 = 2(B@N) + 3(R@S) = 5, 빨강 = 3+3 = 6. // X = B 이면 두 조가 3B2R 라 횡단 = 3(B@N) + 2(R@S) = 5, 빨강 = 2+2 = 4. // → **매 판 정확히 5명이 건너고**, 빨강 수는 {4, 6} 이라 PLAZA-TIMETABLE 의 밴드 [3,7] // 안에 구조적으로 앉는다. // (4) balancedSwap 은 **한 조 안에서** 다수색 하나와 소수색 하나를 맞바꾼다. 조의 색 다중집합 // (3:2) 이 불변이므로 (3) 의 두 수가 둘 다 안 움직인다 — 시드는 "누가" 를 흔들 뿐이다. // 실측 대조(시드 1..3000): 횡단자 {5: 3000}, 빨강 수 {4: 1439, 6: 1561}, 밴드 위반 0건. const startR = cs(0, 1) === 1; // 첫 쌍이 빨강인가 const startN = cs(0, 1) === 1; // 첫 주민이 북쪽 입구인가 const sched = []; for (let i = 0; i < PARK_PLAZA_COUNT; i++) sched.push({ beat: PARK_PLAZA_TELEGRAPH + i * PARK_PLAZA_SPAWN_GAP, // 2, 6, ..., 38 color: (((i >> 1) % 2 === 0) === startR) ? 'R' : 'B', // 주기 4 — XXYYXXYYXX entr: ((i % 2 === 0) === startN) ? 'N' : 'S', // 주기 2 — 라운드로빈 (스펙 §3) }); // 순수 교대는 시드를 몇 가지 편성표로 접어 버린다. 변주는 **균형 교환**으로 준다: 같은 입구로 // 들어오는 다섯 명(= i 의 홀짝이 같은 조) 안에서 다수색 한 명과 소수색 한 명의 색을 맞바꾼다. // 한 조 안의 교환이라 그 조의 색 구성(3:2)이 안 변하고, 따라서 **횡단자 5명도 빨강 수 4/6 도 // 움직이지 않는다** — 누가 어느 색을 지는지만 시드가 흔든다. 색을 그냥 뒤집으면(v1 의 flip) // 횡단 수와 빨강 수가 같이 흔들려 이 태스크가 고치려는 결합이 뒷문으로 돌아온다. const swapN = cs(0, 5), swapS = cs(0, 5); // 조마다 다수 3명 x 소수 2명 = 6가지 const balancedSwap = (g, t) => { const maj = [g, g + 4, g + 8][(t / 2) | 0]; // (i>>1) 이 짝수 — 그 조의 다수색 셋 const min = [g + 2, g + 6][t % 2]; // (i>>1) 이 홀수 — 그 조의 소수색 둘 const c = sched[maj].color; sched[maj].color = sched[min].color; sched[min].color = c; }; balancedSwap(0, swapN); balancedSwap(1, swapS); const remainR = sched.filter(e => e.color === 'R').length; const remainB = PARK_PLAZA_COUNT - remainR; // --- 완주 문법: 출구 둘이 곧 체인 보석 둘 --- // v 는 0 이다. 점수는 배달마다 모듈이 직접 +1 하고(스펙 §3-4 "점수 = 무사 배송 수"), 보석의 // v 는 세지 않는다. 보석은 그 색의 마지막 배달에서 꺼지고, 둘 다 꺼지면 체인이 소진되어 // complete 가 된다 — 관제탑이 쓰는 바로 그 관용구다. const tokens = [ { x: exitRP.x, y: exitRP.y, v: 0, alive: true, guard: false }, // chain 0 — 빨강 출구 { x: exitBP.x, y: exitBP.y, v: 0, alive: true, guard: false }, // chain 1 — 파랑 출구 ]; const clusters = tokens.map(t => ({ x: t.x, y: t.y, v: t.v })); // 커서 시작: 가운데 문간 바로 위. sig0 과 sig2 에서 각각 3박, sig1 에서 2박이라 어느 쪽에도 // 선수를 주지 않는다. 커서는 칸을 점유하지 않으므로 주민 동선 한복판이어도 무방하다. const spawn = PT(6, 5); const mate = PT(1, 1); // 동료: 흐름 밖 북서 구석. 그의 역할은 뒤 태스크가 정한다. const park = { N: n, seed, k: 0, fieldMech: 'plaza', walkway, verge, deep, distDeep, water, plaza: { entrN, entrS, exitR: K(exitRP.x, exitRP.y), exitB: K(exitBP.x, exitBP.y), doorKeys, sigKeys, }, clusters, chain: [0, 1], needTypes: 0, // contracts 는 빈 배열이지 없는 필드가 아니다 — _parkNCtx(engine.js:7752)는 이 배열의 // length 를 조건 없이 읽으므로, 없으면 첫 playout 이 `undefined.length` 로 죽는다. // 동료의 의뢰(배려 축의 재료)는 이 태스크의 몫이 아니라 뒤 태스크가 채운다. contracts: [], spawn, companionSpawn: { x: mate.x, y: mate.y }, // minTurns: 마지막 스폰 박(2 + 9*4 = 38)이 완주 하한의 바닥이다 — 열 번째 주민이 아직 // 판에 없는 동안에는 어떤 충실 플레이도 complete 일 수 없다. 이 판의 자체 박자 상한은 // 그 + TAIL(20) = 58 박이므로 하한 38 은 천장 아래에 안전히 앉는다. park.cap 은 엔진의 // 하드 상한이라 그 58 박에 스폰 지연 여유까지 얹어 넉넉히 잡는다. // trig: **이 보드에서 죽은 값이다.** 커서와는 아무 상관이 없다 — engine.js:8073 의 그 반경은 // _parkCompanionStep 안, 즉 **동반자**가 제 의뢰 보석으로 향하기 시작하는 거리이고, plaza 는 // contracts 가 빈 배열이라(바로 아래) engine.js:8066 의 `P.contract >= park.contracts.length` // 에서 즉시 return 한다. 그러니 이 줄은 한 번도 읽히지 않는다. (Task 3 이 정정: 이전 주석은 // 이것을 "오라클이 보석으로 향하는 반경"이자 "legalMask 가 올 때까지의 완충"이라고 적었는데 // 둘 다 틀렸다.) 커서를 출구 보석에서 빼는 방어는 오직 등록부의 legalMask('me') 에 있다. // 값은 무해하므로 그대로 둔다 — 동반자에게 의뢰가 생기는 날 비로소 살아난다. trig: 5, cap: 120, minTurns: PARK_PLAZA_TELEGRAPH + (PARK_PLAZA_COUNT - 1) * PARK_PLAZA_SPAWN_GAP, cautionD: 2, damage: 1, cell: _parkPlazaCell(seed), }; const st = { N: n, park, goal: 'harvest_max', round: 0, hazard: new Set(deep), sacred: new Set(), wall, pos: { 0: { x: spawn.x, y: spawn.y }, 1: { x: mate.x, y: mate.y } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; park.dyn = _parkDynInit(st); // 모듈이 쓰는 dyn 멤버는 전부 여기서 만든다 — _parkDeepClone 은 있는 것만 복제하므로, // 탐색 포크에서 뒤늦게 생기는 멤버는 갈래마다 다른 물건이 된다. park.dyn.plaza = { signals: [true, true, true], // 시작은 전부 초록 (스펙 §3-2) sched, next: 0, // next: 아직 스폰하지 않은 첫 편성 인덱스 crashes: [], delivered: 0, remainR, remainB, delayed: 0, // 입구가 막혀 스폰이 미뤄진 횟수(스펙 §4-7). 튜닝 계기판이자 게이트의 눈 slip: 0, // 누적 지연분. 예정 박을 row.beat + slip 로 읽어 편성표 꼬리를 통째로 민다 lastSpawnBeat: -1, // 실제 마지막 스폰 박. 박자 상한이 이것 + TAIL 이다 }; park.dyn.ents = []; // 주민들. 스폰이 push 한다 — 관제탑의 ents 시임 재사용 return st; } /* ---- Task 2: 주민의 세계. 걷고, 부딪히고, 배달된다 ------------------------------------ 여기서부터가 이 판의 심장이다. 신호기와 커서의 상호작용은 Task 3 의 몫이라 이 태스크에서는 신호가 **전부 초록으로 남는다** — 문간 셋은 늘 열려 있고 아무도 그것을 토글하지 않는다. 그래도 _parkPlazaDoorShut 은 지금 선다: 주민의 BFS 가 신호를 읽는 자리를 지금 만들어 두어야 Task 3 이 토글만 붙이면 되고, 이 태스크의 게이트도 "닫힌 문간"을 손으로 세워 잴 수 있다. 스펙 §4 의 일곱 걸음 중 이 태스크가 세우는 것은 3·4·6·7 이다. 1(커서 입력)은 엔진이 이미 반영한 뒤에 tick 이 불리므로 공짜이고, 2(platoon 트리거)와 5(patience 소모)는 v1.1 이라 §6 falsifier 가 "전부-닫기 봇 승리"를 보이기 전에는 짓지 않는다(스펙 §5 조건부 투입). */ // _parkPlazaDoorShut(st, key): 그 칸이 **빨간 신호가 막은 문간**인가. 주민에게만 벽이고 // 커서에게는 아무것도 아니다 — 커서는 칸을 점유하지도, 문간에 걸리지도 않는다(스펙 §3-6). // 신호기 i 가 문간 i 를 막는다: 배선은 별도 표가 아니라 인덱스 정렬이다(Task 1 의 앵커 주석). function _parkPlazaDoorShut(st, key) { const Z = st.park.plaza, dz = st.park.dyn.plaza; const i = Z.doorKeys.indexOf(key); return i >= 0 && !dz.signals[i]; } // _parkPlazaResidentIntent(st, ent): 이 주민이 이번 박에 서고 싶은 칸. 열린-신호 그래프 위의 // BFS 최단로 첫 걸음이다(스펙 §3-5). 순수 함수이고 persona 매개변수는 존재하지 않는다(C1) — // 주민 정책이 인격을 읽는 순간 눈 감은 재생이 이 판을 복원하지 못한다. // // 타이브레이크는 _PARK_PLAZA_DIRS 의 상>우>하>좌 순서다. 큐가 그 순서로 펼쳐지고 먼저 잡힌 // 부모가 이기므로, 같은 길이의 길이 여럿이어도 답은 하나로 결정된다. // // **걷는 주민끼리는 서로를 피하지 않는다**(스펙 §3-5 "상호 회피 없음") — 피하기 시작하면 관제가 // 헐거워지고 신호수의 일이 사라진다. 깨어 있는 몸을 막는 것은 길찾기가 아니라 §4-4 의 대기 행렬이다. // // **다만 기절한 주민은 벽이다**(스펙 §4-4 꼬리: "기절 주민은 다음 박 BFS 의 장애물"). 이것은 // §3-5 와의 충돌이 아니라 그 위에 놓인 좁은 특수 규칙이고 — §4 의 제목이 "여기서 못 박는다"이다 — // 설계 의도도 같은 방향을 가리킨다: §3-5 의 괄호가 회피를 금한 이유가 "관제가 헐거워진다"인데, // 기절자를 벽으로 두면 정반대로 사고의 결과가 **교통 정체로 전파되어** 관제가 조여진다. // 퇴장한 주민은 몸이 아니라 막지 않고, 자기 자신도 막지 않는다(기절이면 위에서 이미 빠진다). // 무경로면 제자리다. function _parkPlazaResidentIntent(st, ent) { const n = st.N; if (ent.stun > 0 || ent.done) return ent.y * n + ent.x; // 기절·퇴장은 걷지 않는다 const src = ent.y * n + ent.x; const numb = new Set(); // 쓰러진 몸이 깔고 있는 칸 for (const o of st.park.dyn.ents) if (o !== ent && o.stun > 0 && !o.done) numb.add(o.y * n + o.x); const dist = new Array(n * n).fill(Infinity), parent = new Array(n * n).fill(-1); dist[src] = 0; const q = [src]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of _PARK_PLAZA_DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (dist[nk] < Infinity) continue; if (st.wall.has(nk)) continue; if (_parkPlazaDoorShut(st, nk)) continue; // 스펙 §4-4 꼬리 — 쓰러진 몸은 벽이다. // 인계(Task 3/6): 이 줄은 PLAZA-STUNNED-BLOCKS 가 **손으로 세운 장면**으로만 지켜진다. // 실측(2026-08-04, 시드 1..12 · 커서 유휴): 기절자와 깨어 있는 자가 공존한 박 8 회, 그중 // 이 줄이 걸음을 실제로 바꾼 박 **0 회** — 열린 광장이 같은 길이의 우회로를 주고 첫 걸음을 // 공유하기 때문이다. 물기 시작하는 조건은 "쓰러진 몸이 문간에 있고 뒤 주민이 가까울 때"다. // 신호가 행렬을 문간에 밀어붙이면 그때 실전 발화하니, **라이브 발화 여부를 그때 다시 재라**. if (numb.has(nk)) continue; dist[nk] = dist[kk] + 1; parent[nk] = kk; q.push(nk); } } if (!isFinite(dist[ent.exitKey])) return src; // 무경로 -> 제자리 (스펙 §3-5) let cur = ent.exitKey; while (parent[cur] !== src && parent[cur] >= 0) cur = parent[cur]; return parent[cur] === src ? cur : src; } // _parkPlazaResolve(P, intents): 스펙 §4-4 의 고정점. **보수적이다** — 대상 칸이 이번 박에 // 반드시 빈다고 **증명될 때까지 아무도 움직이지 않는다**. // // 이 한 줄이 이 함수의 전부이고, 가장 값비싼 결정은 순환 고리다. 셋이 서로의 칸으로 도는 // 회전은 "전원 이동"도 "전원 제자리"도 자기일관적이다. 스펙은 **증명 안 되면 제자리**를 고른다 // (최소 고정점): 아무도 첫 증명을 얻지 못하므로 고리는 통째로 멈추고, 하트도 사고도 없다. // 회전을 허용하면 관제사는 볼 수 없는 규칙에 지고, 무엇보다 "빈 칸이 생겨야 들어간다"는 // 이 판의 유일한 물리가 예외를 갖게 된다. // // 청구는 **사건당 한 번**이다(스펙 §3-4). 3 자 뒤엉킴도 ♥-1 이지 ♥-2 가 아니다. function _parkPlazaResolve(P, intents) { const st = P.st, n = st.N, D = st.park.dyn, ents = D.ents, dz = D.plaza; const cellOf = (e) => e.y * n + e.x; // held: 이번 박에 제자리로 확정된 주민. 처음엔 제 칸을 낸 자(기절·무경로)와 이미 퇴장한 자다. const held = ents.map((e, i) => e.done || intents[i] === cellOf(e)); // --- 충돌 ② 맞교환. 서로의 칸으로 들어가려는 둘은 둘 다 제자리 + 사건당 ♥-1 + 전원 1박 기절. // 보수 고정점만으로도 둘 다 못 움직이지만 그건 "증명 안 됨"이지 사고가 아니다. 맞교환은 // 사고이므로 여기서 이름을 붙여 따로 청구한다 — 순환 고리와 갈리는 지점이 정확히 여기다. for (let i = 0; i < ents.length; i++) for (let j = i + 1; j < ents.length; j++) { if (held[i] || held[j]) continue; const ki = cellOf(ents[i]), kj = cellOf(ents[j]); if (intents[i] === kj && intents[j] === ki) { held[i] = held[j] = true; ents[i].stun = ents[j].stun = PARK_PLAZA_STUN; P.hearts--; dz.crashes.push({ beat: D.beat, key: kj, members: [i, j] }); st.fx.push({ k: 'crash', x: ents[j].x, y: ents[j].y }); } } // 지금 어느 칸에 누가 서 있는가. 3 단계 전까지 아무도 움직이지 않으므로 이 표는 불변이다. // 퇴장한 주민은 몸이 아니다 — 장애물로도 세지 않는다. const occupied = new Map(); ents.forEach((e, i) => { if (!e.done) occupied.set(cellOf(e), i); }); // --- 고정점. 두 판정을 **연쇄가 멎을 때까지** 번갈아 돌린다(스펙 §4-4 "fixpoint"): // (i) 풀어 주기 — 대상 칸이 비었거나, 그 점유자가 이번 박에 확실히 나가는 주민일 때만. // (ii) 다툼 — 풀린 주민 둘 이상이 같은 칸을 겨누면 낮은 스폰 순번만 남기고 나머지를 묶는다. // (ii)가 묶은 주민의 칸은 이제 비지 않으므로 (i)을 **다시 돌려야 한다** — 스펙의 // "반동으로 다시 점유된 칸도 같은 규칙으로 재판정"이 이 왕복이다. 이것 없이 (i)-(ii) 를 한 // 번씩만 돌리면 반동으로 되메워진 칸에 후속자가 들어가 **두 몸이 한 칸에 포개진다**. // held 는 단조롭게 늘기만 하고 released 는 그에 따라 줄기만 하므로 왕복은 반드시 멎는다. const contest = new Map(); // 대상 칸 -> 다툰 전원. 승자가 실제로 들어갈 때만 청구된다 let released; // 바깥 왕복이 매 라운드 새로 만든다 — 여기서 채우면 죽은 코드다 for (;;) { released = new Array(ents.length).fill(false); for (let go = true; go;) { go = false; for (let i = 0; i < ents.length; i++) { if (held[i] || released[i]) continue; const occ = occupied.get(intents[i]); // 빈 칸이거나, 점유자가 풀린 주민이며 그가 내 칸으로 오지 않을 때. 서로의 칸으로 도는 // 고리는 아무도 첫 증명을 얻지 못해 영원히 안 풀린다 = 전원 제자리(§4-4 보수 핀). if (occ === undefined || (released[occ] && intents[occ] !== cellOf(ents[i]))) { released[i] = true; go = true; } } } const byTarget = new Map(); for (let i = 0; i < ents.length; i++) if (released[i]) { if (!byTarget.has(intents[i])) byTarget.set(intents[i], []); byTarget.get(intents[i]).push(i); } let again = false; for (const [k, group] of byTarget) { if (group.length < 2) continue; group.sort((a, b) => ents[a].spawnIdx - ents[b].spawnIdx); if (!contest.has(k)) contest.set(k, group.slice()); // 첫 발견이 최대 집합이다(released 는 줄기만 한다) for (let g = 1; g < group.length; g++) if (!held[group[g]]) { held[group[g]] = true; again = true; } } if (!again) break; } // --- 이동 확정. 풀린 주민만, 제 의도대로. for (let i = 0; i < ents.length; i++) if (released[i]) { ents[i].x = intents[i] % n; ents[i].y = (intents[i] / n) | 0; } // --- 충돌 ① 청구. 낮은 스폰 순번이 칸을 갖고 나머지는 원래 칸으로 반동, 뒤엉킨 전원 1박 기절. // 승자마저 (다른 다툼의 여파로) 결국 못 들어갔다면 그 칸은 애초에 비지 않은 것이다 — // 스펙의 충돌 ① 은 "빈 칸(또는 이번 박 비워지는 칸)"에 대한 의도 둘 이상이므로, 그건 사고가 // 아니라 대기 행렬이고 하트를 물리지 않는다. for (const [k, group] of contest) { if (!released[group[0]]) continue; P.hearts--; dz.crashes.push({ beat: D.beat, key: k, members: group.slice() }); st.fx.push({ k: 'crash', x: k % n, y: (k / n) | 0 }); for (const l of group) ents[l].stun = PARK_PLAZA_STUN; // 승자 포함 — 이건 뒤엉킴이다 } } // _parkPlazaDeliver(P): 스펙 §4-6. 제 색 출구에 선 주민은 퇴장하고 점수가 +1 된다. 그 색의 // **마지막** 배달이 그 출구의 체인 보석을 끄고, 둘 다 꺼지면 엔진의 generic 체인 경로가 // complete 를 읽는다(본문 0 줄 — 여기서 세는 것은 remain 뿐이다). function _parkPlazaDeliver(P) { const st = P.st, n = st.N, park = st.park, dz = park.dyn.plaza; for (const e of park.dyn.ents) { if (e.done || e.y * n + e.x !== e.exitKey) continue; e.done = true; dz.delivered++; st.score[0] += 1; st.fx.push({ k: 'deliver', x: e.x, y: e.y }); if (e.color === 'R') dz.remainR--; else dz.remainB--; if ((e.color === 'R' ? dz.remainR : dz.remainB) > 0) continue; const tok = st.tokens.find(t => t.y * n + t.x === e.exitKey); if (tok && tok.alive) { tok.alive = false; st.fx.push({ k: 'gem', x: tok.x, y: tok.y }); } } _parkAdvanceDest(P); } // _parkPlazaSpawnTick(P): 스펙 §4-7. 예정 주민의 입구가 비었으면 세우고, 점유돼 있으면 1박 // 지연한다. // // **편성표 잔량은 뒤로 밀린다** — 밀린 행 하나만이 아니라 꼬리 전체가. 그래서 지연분을 누적 // 오프셋 `slip` 에 얹고 예정 박을 `row.beat + slip` 으로 읽는다. next 를 붙들기만 하면 밀린 행은 // 늦게 서지만 그 다음 행은 **제 예정대로** 서 버려서 4박 간격이 1박으로 무너진다(스펙이 "잔량은 // 뒤로 밀림"이라고 쓴 이유가 바로 그것이다). sched 자체는 건드리지 않는다 — 그건 시드 순수한 // 편성표이고 골든이 물고 있다. // // lastSpawnBeat 은 **실제** 스폰 박을 들고 있으므로 박자 상한도 지연만큼 저절로 뒤로 밀린다(§3-7). function _parkPlazaSpawnTick(P) { const st = P.st, n = st.N, park = st.park, dz = park.dyn.plaza; if (dz.next >= dz.sched.length) return; const row = dz.sched[dz.next]; if (park.dyn.beat < row.beat + dz.slip) return; // 아직 예고 점멸 중이다 const gate = row.entr === 'N' ? park.plaza.entrN : park.plaza.entrS; if (park.dyn.ents.some(e => !e.done && e.y * n + e.x === gate)) { dz.delayed++; dz.slip++; return; // 1박 지연 — 그리고 꼬리 전체가 함께 밀린다 } park.dyn.ents.push({ x: gate % n, y: (gate / n) | 0, color: row.color, exitKey: row.color === 'R' ? park.plaza.exitR : park.plaza.exitB, stun: 0, pips: PARK_PLAZA_PIPS, spawnIdx: park.dyn.ents.length, done: false, }); dz.next++; dz.lastSpawnBeat = park.dyn.beat; st.fx.push({ k: 'spawn', x: gate % n, y: (gate / n) | 0 }); } // _parkPlazaTick(P, ev): 한 박. 스펙 §4 의 번호를 그대로 옮긴 것이 이 함수의 본문이다. // 엔진은 dyn.beat 를 올린 **뒤에** 이걸 부르므로 여기서 읽는 beat 은 "이번 박"이다. function _parkPlazaTick(P, ev) { const st = P.st, park = st.park, D = park.dyn, dz = D.plaza, ents = D.ents; // §4-1 커서 입력 — 엔진이 이미 pos 를 확정한 뒤에 이 훅이 불린다(신호 토글은 Task 3). // §4-2 platoon 트리거 — v1.1, falsifier 판정 전까지 미배선(스펙 §5). // §4-3 의도 수집. **동시에** 모은다 — 아무도 아직 움직이지 않은 판을 전원이 같은 눈으로 본다. const intents = ents.map(e => _parkPlazaResidentIntent(st, e)); const rested = ents.map(e => e.stun > 0); // 이번 박을 이미 기절로 시작한 주민 // §4-4 해소 _parkPlazaResolve(P, intents); // 기절 감쇠는 **해소 뒤에, 이번 박을 기절로 시작한 주민에게만** 건다. 감쇠를 박 맨 앞에 두면 // 방금 켠 기절이 같은 박에 꺼져 STUN 이 단 한 걸음도 막지 못하고 영원히 공허해진다 — // "1박 기절 후 재개"(스펙 §3-4)가 실제로 한 박을 먹으려면 순서가 이래야 한다. for (let i = 0; i < ents.length; i++) if (rested[i] && ents[i].stun > 0) ents[i].stun--; // §4-5 patience 소모 — v1.1, 미배선(스펙 §5). _parkPlazaDeliver(P); // §4-6 _parkPlazaSpawnTick(P); // §4-7 // §3-7 박자 상한: **실제** 마지막 스폰 + TAIL. 편성표가 아직 남았으면 켜지지 않는다 — // lastSpawnBeat 이 -1 인 채로 이 줄이 물면 첫 박에 판이 끝나 버린다. // 상한은 **완주에도 종단에도 진다.** 엔진은 tick 뒤에 죽음(engine.js:8348)과 완주(:8352)를 // 다시 읽는데 둘 다 `!P.over` 가드가 걸려 있다 — 여기서 'cap' 을 세워 버리면 그 재판독이 통째로 // 건너뛰어진다. 그러면 만점으로 끝난 판이 시간초과로 기록되고(delivered 가드), ♥ 를 소진한 판은 // `{reason:'cap', hearts:음수}` 로 기록된다(hearts 가드) — 스펙 §3-4 는 ♥ 소진을 **종단**으로 // 못 박았고, 하트 클램프(P.hearts = 0)마저 그 가드 안에 있어 함께 건너뛰어진다. // 두 오보 모두 Task 6 의 admission 이 읽는 `reason` 을 조용히 더럽힌다. if (!P.over && P.hearts > 0 && dz.delivered < PARK_PLAZA_COUNT && dz.next >= dz.sched.length && D.beat >= dz.lastSpawnBeat + PARK_PLAZA_TAIL) { P.over = true; P.reason = 'cap'; } } /* ---- Task 6: 신호수의 마음. 예지 -> 트리아지 -> 한 걸음 ----------------------------- Task 2·3 이 끝났을 때 이 판에는 **아무도 타고 있지 않았다.** 실측(2026-08-04, 시드 1..12 x 인격 6 = 72 런): 탑재 오라클의 신호 토글 **총 0 회**, 72/72 런이 death, 배달 234/720. 원인 사슬은 등록부 주석이 적어 둔 그대로다 — _parkDestCell 이 살아 있는 출구 보석을 목적지로 주고, legalMask('me') 가 바로 그 칸을 막고, 목표 계량의 argmin 이 보석 인접칸에서 멎어, 그 자리에서 `stay` 가 영원히 최적이 된다. 그것을 고치는 물건이 아래 `_parkPlazaSteer` 다. 정책은 falsifier 하네스의 greedy 봇과 **같은 예지기**를 쓴다(하네스는 게이트 밖 도구라 코드를 공유하지 않지만, 규칙은 셋 다 엔진의 _parkPlazaResidentIntent / _parkPlazaResolve / _parkPlazaSpawnTick 을 그대로 돌린다 — 규칙을 두 번 쓰면 재는 것이 엔진이 아니라 사본이 된다). 하네스가 잰 그 정책의 성적은 시드 1..10 에서 배달 100/100 · 사고 0 · 토글 10 이다. **v1 은 세 마음이 이 read 하나를 공유한다** — 마음별 분화와 쌍 측정은 스펙 §9 가 명시적으로 이연한 별도 세션의 몫이고, 여기서 열지 않는다. 그 결과 여섯 인격의 궤적은 이 판에서 서로 같다(아래 admissible 주석 참고). 착석 최소선(스펙 §2-③)을 넘기는 것이 이 태스크의 일이지 쌍을 세우는 것이 아니다. **prefer 는 좁히기만 한다.** 시임 계약(engine.js:7448)이 못 박은 대로 빈 집합은 거부가 아니라 **침묵**이라 그 마음이 결정에서 통째로 빠진다 — 그래서 아래의 모든 갈래는 좁힐 것이 없으면 legal 전체를 돌려준다. 좁힌 뒤에도 비면 역시 전체다. */ const PARK_PLAZA_HORIZON = 6; // 예지 지평(박). 하네스 greedy 의 H 와 같은 수다 // _parkPlazaSimClone(st): 예지 전용 사본. 규칙 함수 셋이 실제로 읽는 것만 담는다 — 불변 앵커 // (park.plaza·sched)는 공유해도 안전하고(아무도 안 쓴다), 나머지는 전부 새 물건이라 예지가 // 라이브 판을 한 바이트도 건드리지 않는다. function _parkPlazaSimClone(st) { const p = st.park, dz = p.dyn.plaza; return { N: st.N, wall: st.wall, fx: [], park: { plaza: p.plaza, dyn: { beat: p.dyn.beat, ents: p.dyn.ents.map(e => ({ x: e.x, y: e.y, color: e.color, exitKey: e.exitKey, stun: e.stun, pips: e.pips, spawnIdx: e.spawnIdx, done: e.done })), plaza: { signals: dz.signals.slice(), sched: dz.sched, next: dz.next, crashes: [], delivered: dz.delivered, remainR: dz.remainR, remainB: dz.remainB, delayed: dz.delayed, slip: dz.slip, lastSpawnBeat: dz.lastSpawnBeat, }, }, }, }; } // _parkPlazaSimBeat(S): 예지의 한 박. _parkPlazaTick 의 §4-3·4·6·7 을 같은 순서로 부른다. // 배달만 여기서 흉내낸다 — _parkPlazaDeliver 는 점수·보석·_parkAdvanceDest 까지 건드리는데 // 그 셋은 사본에 없고 예지에 필요도 없다(남은 색 수 remainR/remainB 만 세면 된다). function _parkPlazaSimBeat(S) { S.park.dyn.beat++; const ents = S.park.dyn.ents, n = S.N, dz = S.park.dyn.plaza; const intents = ents.map(e => _parkPlazaResidentIntent(S, e)); // §4-3 const rested = ents.map(e => e.stun > 0); const fake = { st: S, hearts: 0 }; // 예지의 하트는 세지 않는다 — 아래 주석 참고 const mark = dz.crashes.length; _parkPlazaResolve(fake, intents); // §4-4 const fresh = dz.crashes.slice(mark); for (let i = 0; i < ents.length; i++) if (rested[i] && ents[i].stun > 0) ents[i].stun--; for (const e of ents) { // §4-6 (점수·보석 없이) if (e.done || e.y * n + e.x !== e.exitKey) continue; e.done = true; dz.delivered++; if (e.color === 'R') dz.remainR--; else dz.remainB--; } _parkPlazaSpawnTick(fake); // §4-7 return fresh; } // _parkPlazaForecast(st, T, flip): 지금 상태에서 T 박을 굴려 그 사이의 사고를 돌려준다. // flip = {at, sig} 이면 offset at (1 = 바로 다음 박) 의 **의도 수집 전에** 그 신호를 뒤집는다 — // 커서가 그 박에 신호기 위에서 stay 를 낸 것과 같은 타이밍이다(엔진에서도 onEnter 가 tick 앞이다). // // **이 예지는 하트 소진도 박자 상한도 모델링하지 않는다.** 종단을 무시하고 T 박을 끝까지 민다. // 그래서 예보는 일어날 수 없는 세계로 넘어갈 수 있다(♥3 이면 3회째에 death 인데 예보는 계속 // 센다). 후보 넷이 **같은 비현실을 공유**하므로 순위는 보존된다 — 하네스가 같은 근사를 쓰고 // 같은 판정을 냈다. 기하나 하트를 움직이는 사람은 여기를 먼저 볼 것. function _parkPlazaForecast(st, T, flip) { const S = _parkPlazaSimClone(st); const out = []; for (let t = 1; t <= T; t++) { if (flip && flip.at === t) { const g = S.park.dyn.plaza.signals; g[flip.sig] = !g[flip.sig]; } for (const c of _parkPlazaSimBeat(S)) out.push({ off: t, key: c.key }); if (S.park.dyn.plaza.delivered >= PARK_PLAZA_COUNT) break; } return out; } // _parkPlazaCursorDist(st, srcKey, mask): 커서 그래프의 다중출발 아닌 단일출발 BFS. 막는 것은 // **등록부의 legalMask('me') 자신**이다 — 규칙을 여기서 다시 쓰면 마스크가 바뀌는 날 조용히 // 갈라지므로, _parkMaskOf 가 묶어 준 그 함수를 그대로 묻는다. 그래프가 무향이라 신호기에서 // 잰 이 거리표 하나가 "커서 -> 신호기"와 "신호기 쪽으로 한 걸음" 둘 다에 쓰인다. function _parkPlazaCursorDist(st, srcKey, mask) { const n = st.N; const dist = new Array(n * n).fill(Infinity); dist[srcKey] = 0; const q = [srcKey]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of _PARK_PLAZA_DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (dist[nk] < Infinity) continue; if (st.wall.has(nk)) continue; if (mask && mask(nk)) continue; dist[nk] = dist[kk] + 1; q.push(nk); } } return dist; } /* _parkPlazaCtx(P) — reads 의 단 한 번의 계산. 여기서 트리아지의 **판단**까지 끝내고, 아래 _parkPlazaSteer 는 그 판단을 걸음으로 옮기기만 한다(마음 셋이 같은 read 를 부르므로, 예지를 prefer 안에 두면 같은 상태에서 세 번 돈다). 정책 — 하네스 greedy 를 그대로 옮긴 것이다: (a) 신호를 동결한 채 H=6 박을 전개한다. 사고가 없으면 아무 일도 하지 않는다(aim = -1). (b) 사고가 보이면 후보 넷을 같은 지평 HE 박으로 전개해 비교한다: 무행동, 그리고 신호기 i 로 **걸어가서**(d_i 박) 토글(+1박). 발효 시점을 offset d_i+1 로 두는 것이 이 판의 핵심이다 — 출퇴근을 공짜로 치면 신호수가 있지도 않은 능력을 갖게 된다. HE = H + max_i d_i + 1 로 모든 후보에 같은 지평을 준다(먼 신호기의 효과가 잘리면 비교가 거리에 편향된다). (c) 점수 = (지평 안 사고 수, 첫 사고를 얼마나 늦췄나) 사전식. 무행동보다 **엄격히** 나은 후보만 채택한다 — 동률 토글은 신호를 무의미하게 되돌리는 짓이다. 동률 후보끼리는 가까운 신호기, 그다음 낮은 인덱스. 매 박 다시 계획한다. 상황이 바뀌면 목표도 바뀐다. 공개 상태만 읽고 persona 는 존재하지도 않는다(C1). dyn.plaza.signals 를 직접 쓰지 않는다 — 신호를 바꾸는 길은 오직 커서의 stay 다. */ function _parkPlazaCtx(P) { const st = P.st, Z = st.park.plaza, dz = st.park.dyn.plaza; const here = st.pos[0].y * st.N + st.pos[0].x; const onSig = Z.sigKeys.indexOf(here); const live = !P.over && dz.delivered < PARK_PLAZA_COUNT; const idle = { live, here, onSig, aim: -1, dist: null }; if (!live) return idle; if (!_parkPlazaForecast(st, PARK_PLAZA_HORIZON, null).length) return idle; // (a) const mask = _parkMaskOf(P, 'me'); const table = Z.sigKeys.map(k => _parkPlazaCursorDist(st, k, mask)); const d = table.map(a => a[here]); let maxD = 0; for (const v of d) if (isFinite(v) && v > maxD) maxD = v; const HE = PARK_PLAZA_HORIZON + maxD + 1; // (b) const score = (list) => { let first = HE + 1; for (const c of list) if (c.off < first) first = c.off; return [list.length, -first]; }; const better = (a, b) => (a[0] !== b[0] ? a[0] < b[0] : a[1] < b[1]); const base = score(_parkPlazaForecast(st, HE, null)); let pick = -1, pickScore = null, pickD = Infinity; for (let i = 0; i < Z.sigKeys.length; i++) { // (c) if (!isFinite(d[i])) continue; const s = score(_parkPlazaForecast(st, HE, { at: d[i] + 1, sig: i })); if (!better(s, base)) continue; if (pick < 0 || better(s, pickScore) || (!better(pickScore, s) && d[i] < pickD)) { pick = i; pickScore = s; pickD = d[i]; } } return { live, here, onSig, aim: pick, dist: pick >= 0 ? table[pick] : null }; } /* _parkPlazaSteer(P, legal, ctx) — 판단을 한 걸음으로. 세 마음이 공유하는 v1 의 유일한 좁힘. 네 갈래뿐이다: * 판이 끝났거나 read 가 없다 -> legal 전체 (아무 말도 하지 않는다) * 목표 신호기 위에 서 있다 -> {stay} = 토글 (스펙 §3-6, 박당 한 번) * 목표가 있고 떨어져 있다 -> 그 신호기까지의 거리를 **엄격히 줄이는** 걸음들 * 목표가 없다(임박 사고 0) -> 제자리 대기. 다만 신호기 위라면 **stay 를 뺀다** — 그 칸의 stay 는 휴식이 아니라 토글이라, 가만히 있으려는 마음이 신호를 뒤집어 버린다. 옆 칸(다른 신호기가 아닌)으로 한 칸 비켜서는 것이 이 판에서 "아무것도 하지 않는다"의 유일한 합법 표현이다. 이 2박(비켜서기 + 되돌아오기)은 신호수의 부담이지 엔진의 결함이 아니다. 좁힌 결과가 비면 언제나 legal 전체다 — 빈 prefer 는 거부가 아니라 침묵이기 때문이다. */ function _parkPlazaSteer(P, legal, ctx) { const all = new Set(); for (const c of legal) all.add(c.k); if (!ctx || !ctx.live) return all; const sigKeys = P.st.park.plaza.sigKeys; const only = (pred) => { const out = new Set(); for (const c of legal) if (pred(c)) out.add(c.k); return out.size ? out : all; }; if (ctx.aim < 0) { if (ctx.onSig >= 0) return only(c => c.k !== 'stay' && sigKeys.indexOf(c.key) < 0); return only(c => c.k === 'stay'); } if (ctx.onSig === ctx.aim) return only(c => c.k === 'stay'); const dist = ctx.dist; if (!dist || !isFinite(dist[ctx.here])) return all; return only(c => isFinite(dist[c.key]) && dist[c.key] < dist[ctx.here]); } // ---- 등록. 엔진이 y59 에 대해 아는 것은 전부 여기에 있다(본문 0 줄). PARK_FIELD_MECHS.plaza = { build: _parkPlazaBuild, cell: _parkPlazaCell, admits: _parkPlazaAdmissible, // 세 마음이 **같은 read 를 공유한다**(v1 한정, 스펙 §9). 셋 다 판이 살아 있는 동안 깨어 있고, // 셋 다 같은 트리아지로 좁힌다 — 마음별 분화는 승격 세션이 연다. 지금 이 자리의 일은 하나뿐이다: // **커서를 실제로 조종하는 것.** 이것이 없으면 오라클은 보석 인접칸에 주차한 채 신호를 한 번도 // 만지지 않고(실측 72 런 0 토글), admits 는 아무도 타지 않은 판을 인증하게 된다. // // 배선 산수(왜 공용 read 하나가 세 인격 순서 전부를 실제로 움직이는가): 이 보드에서 탑재 C 는 // distDeep 가 전부 Infinity 라 engaged 가 거짓이고, 탑재 N 은 contracts 가 비어 engaged 가 // 거짓이다. 그런데 _parkReads 는 engaged 를 OR 하고 **그 뒤에 탑재 preference 를 부른다** — // 그 둘의 preference 는 이 보드에서 legal 전체이므로, 교집합은 아래 steer 가 낸 집합 그대로다. // 탑재 G 만이 진짜로 좁히는데(보석 쪽으로 줄어드는 걸음), 그 교집합이 비면 _parkLexSet 이 // 그 마음을 건너뛰고 다음 마음이 steer 집합으로 좁힌다. 그래서 여섯 순서 전부에서 조종이 산다. reads: { ctx: _parkPlazaCtx, G: { engaged: (P, ctx) => !!ctx && ctx.live, prefer: (P, legal, ctx) => _parkPlazaSteer(P, legal, ctx) }, C: { engaged: (P, ctx) => !!ctx && ctx.live, prefer: (P, legal, ctx) => _parkPlazaSteer(P, legal, ctx) }, N: { engaged: (P, ctx) => !!ctx && ctx.live, prefer: (P, legal, ctx) => _parkPlazaSteer(P, legal, ctx) }, }, // 커서의 영역. 관제탑은 커서를 남쪽 관제실에 **가두어** deep 진입 과금을 구조적으로 지웠지만, // 이 판에서는 **광장 전체가 관제실이다** — 신호기 셋이 벨트 양쪽에 흩어져 있으므로 커서를 // 가두는 순간 출퇴근이라는 이 판의 유일한 비용이 사라진다(스펙 §2-⑥·§3-6 "기본안은 벽만 막는다"). // 과금이 없는 이유는 마스킹이 아니라 **지형 부재**다: v1 보드에는 deep 도 water 도 한 칸도 없다. // 'route' — false. 페르소나 경로 계량은 판 전체를 본다(관제탑과 같다). // 'mate' — 벽뿐. 동료는 흐름 밖 구석에 앉아 있고 이 판이 그에게 거는 금지는 아직 없다. // ★ 그 두 갈래는 **정의상 no-op 이다** — 삼분할은 문서일 뿐 산 코드가 아니다(실측 확인): // 세 호출자가 전부 벽을 마스크보다 **먼저** 거른다 — `_parkLegal` 7642 -> 7648, // `_parkCompanionPlan` 7711 -> 7712, `_parkFields` 7577 -> 7578. 그러니 'route' 가 false 를 // 내든 벽을 내든, 'mate' 가 벽을 내든 false 를 내든 결과 집합은 같다. 다음 사람이 이 둘을 // 산 방어선으로 읽고 그 위에 뭔가를 세우지 않도록 여기 적는다 — 실제로 무언가를 막는 갈래는 // 아래 'me' 하나뿐이다. // 'me' — 벽, **그리고 출구 보석 두 칸**. 문간의 빨간 신호는 **주민에게만** 벽이다 // (_parkPlazaDoorShut 참고) — 커서는 빨간 문간을 그냥 지나간다. // 셋 다 persona 를 읽지 않는다(C1). // // 출구 두 칸을 'me' 에서 빼는 것이 스펙 §3-6 의 "기본안은 벽만"에서 유일하게 벗어나는 지점이고, // 그 문장 자신이 "구현에서 확정"이라고 남겨 둔 자리다. 이유는 취향이 아니라 실측이다: // 이 두 칸은 지형이 아니라 **체인 보석**이라 커서가 밟는 순간 parkStep 의 보편 수확 경로가 // (engine.js:8253) 보석을 꺼 버리고, 둘 다 꺼지면 _parkReadDone 이 complete 를 읽는다. // 실측(2026-08-04, 시드 1..12 x 인격 6 = 72 런): 마스크 없이는 커서가 **72/72** 런에서 출구 // 보석을 밟았고 **60/72** 가 21박에 `complete`(배달 2~4명)로 조기 종료됐다. 이 두 칸만 빼면 // 72/72 -> 0, 60/72 -> 0 이 되고 완주 런이 전부 10/10 배달(44박)이 된다. Task 1 이 남긴 핀 // ("진짜 방어는 뒤 태스크의 legalMask('me') 로 커서를 보석 칸에서 아예 빼는 것") 이 이 줄이다. // 광장 100 개 보행칸 중 둘이라 출퇴근 경제(가장 먼 신호기 6박)는 한 박도 줄지 않는다. // // **이 마스크가 고치지 않은 것 — 다음 사람이 제일 먼저 알아야 할 사실.** 조기 `complete` 는 // 지웠지만 커서를 **관제사로 만들지는 못했다.** 같은 72 런에서 탑재 오라클의 신호 토글은 // **총 0 회**다(시드 2 인격 0: 44수 중 stay 39회, 경로 45칸 중 40칸이 출구 보석 반경 1 이내, // 마지막 12박은 (9,3) 고정). 그러니 위의 `10/10 배달·44박` 은 **커서가 아무것도 하지 않은 // 런**이고, 조종의 증거가 아니라 주민들이 저절로 건너간 기록이다. // // 원인 사슬이 이 마스크와 직접 맞물린다: _parkDestCell(engine.js:7229)이 살아 있는 체인 토큰 // = 출구 보석을 목적지로 주는데 -> 이 줄이 **바로 그 칸을 막으므로** -> 목표 계량의 argmin 이 // 인접칸에서 멎고 -> 그 자리에서 `stay` 가 영원히 최적이 된다. 이 판에서 커서를 실제로 움직이는 // 것은 `reads` 뿐이고 그것은 Task 6 의 몫이다 — **Task 6 의 _parkPlazaSteer 가 정확히 이걸 // 고치러 오는 물건이다.** Task 6 의 admission 이 이 런들을 "살아 있다"로 읽으면 무인 조종 판을 // 인증하게 되니, `reason` 만 보지 말고 토글 수를 함께 재라. legalMask: (P, key, who) => { const Z = P.st.park.plaza; if (who === 'route') return false; if (who === 'mate') return P.st.wall.has(key); return P.st.wall.has(key) || key === Z.exitR || key === Z.exitB; // 'me' }, // THE TOGGLE. 신호기 칸 위에서의 `stay` 한 박이 그 신호를 반전시킨다. 관제탑은 첫 토글에 // dyn.used 로 자물쇠를 걸어 "고백은 한 번"을 만들었지만 **여기엔 자물쇠가 없다** — 토글은 // 무제한이고, 대신 신호기 사이의 거리(가장 먼 둘이 6박)가 값이다. 커서의 출퇴근이 비용이다. // 원격 일괄 토글이 금지인 것도 같은 이유다(스펙 §2-⑥): 그 편의 기능 하나가 이 경제를 지운다. // 연속 stay = 연속 토글(스펙 §3-6) — 박당 한 번이고, 잠기지 않는다. onEnter: (P, ev) => { const st = P.st, D = st.park.dyn; if (ev.mvKey !== 'stay') return; // HOLD 만 토글한다(시임은 stay 에도 onEnter 를 쏜다) const i = st.park.plaza.sigKeys.indexOf(ev.toKey); if (i < 0) return; D.plaza.signals[i] = !D.plaza.signals[i]; st.fx.push({ k: 'toggle', x: ev.to.x, y: ev.to.y }); }, tick: _parkPlazaTick, }; /* ---- Task 6: 착석 필터. 이 셀을 사람이 실제로 끝까지 갈 수 있는가 -------------------- 모듈 자신의 generate-then-filter 술어다(플레이 가능성이지 승격 바가 아니다 — 승격은 이 플랜 밖이고 아래 SHIPPABLE 은 리터럴 false 로 남는다). 바 둘: 1. 여섯 인격의 충실 playout 이 **전부** `complete`. 이 판에서 complete 는 두 출구 보석이 모두 꺼졌다는 뜻이고, 보석은 그 색의 마지막 배달에서만 꺼진다. 2. 배달 >= 8. 2 는 지금 1 에 **구조적으로 포함된다**(complete <=> 배달 10). 그래도 남긴다: 브리프가 못 박은 절이고, 완주 문법이 바뀌는 날(색 셋, 부분 완주, 커서가 보석을 먹는 경로가 다시 열리는 날) 그때 무는 것이 이 줄이다. 실측 델타는 리포트에 정직하게 적혀 있다 — 시드 1..40 스윕에서 `few` 는 0 회다. 공허한 줄을 공허하지 않은 척하지 않는다. **이 술어가 재지 않는 것 — 다음 사람이 먼저 알아야 할 것.** v1 은 세 마음이 read 하나를 공유하고(스펙 §9 이연), 이 보드의 route 계량은 deep 이 없어 safe == fast 다. 그래서 여섯 인격의 궤적이 서로 **같다** — 이 바는 사실상 한 런을 여섯 번 재는 것이고, 인격 간 차이를 증언하지 않는다. 조종이 살아 있다는 증거는 이 술어가 아니라 **토글 수**가 진다: **시드 1..12 x 인격 6 = 72 런에서 0회 -> 72회**(런당 정확히 1회, 라이브 박 3558, 배달 720/720). 재도출은 하네스의 `admission` 모드가 찍는다. admits 만 보고 "마음이 갈린다"고 읽지 말 것. FIRST-REASON 히스토그램(storm 의 경고, 그대로): 모든 가지가 early-return 이고 거절 하나당 키 하나다. 게이트는 이것을 **자기 스윕 주변의 델타**로 읽는다(프로세스 전역이므로 절대값은 앞서 돈 게이트가 남긴 것이다). */ // _PARK_PLAZA_WHYS — 세 키 **전부 거절 사유**다. 다만 `complete` 는 이름이 뜻을 뒤집어 읽히는 // 자리라 여기서 못 박는다(storm 이 같은 함정을 engine.js:14232 에 적어 둔 그 자리): // dead — 그 인격의 런이 `death` 로 끝났다 (♥ 소진) // complete — **완주하지 못했다**(= reason 이 'complete' 도 'death' 도 아니다: 'cap' 등). // "완주한 셀 수"가 **아니다.** 브리프가 못 박은 키 이름이라 그대로 두지만, // 델타를 읽는 사람이 정반대로 읽기 쉬운 유일한 키다. // few — 완주는 했는데 배달이 8 미만 (지금은 도달 불가 — 위 "바 둘" 참고) const _PARK_PLAZA_WHYS = { complete: 0, dead: 0, few: 0 }; function _parkPlazaAdmissible(cell) { for (const persona of PARK_PERSONAS) { const st = _parkPlazaBuild(cell); const P = parkPlayout(st, persona); if (P.reason === 'death') { _PARK_PLAZA_WHYS.dead++; return false; } if (P.reason !== 'complete') { _PARK_PLAZA_WHYS.complete++; return false; } if (st.park.dyn.plaza.delivered < 8) { _PARK_PLAZA_WHYS.few++; return false; } } return true; } // parkPlazaWhys(): 거절 집계의 사본. 게이트가 델타로 읽는다. function parkPlazaWhys() { return { ..._PARK_PLAZA_WHYS }; } // PARK_PLAZA_SHIPPABLE — 모듈 자신의 승격 핀. y59 는 아직 슬롯에 앉지도 않았고 승격 측정은 이 // 플랜 밖이므로, 이것은 **리터럴 false 이지 측정이 아니다**: 파생형(`_parkPlazaRecovers`, // _parkBombRecovers 형태)은 실제로 쌍을 재는 세션의 몫이고, 지금 그것을 적으면 아무도 돌려 본 // 적 없는 슬롯에 숫자를 붙이는 짓이 된다. derive-never-assert 는 양쪽으로 문다 — true 를 단언 // 하지 말 것, 그리고 false 를 파생인 척 꾸미지도 말 것. (y24 lantern·y31 relay 의 관례 그대로.) const PARK_PLAZA_SHIPPABLE = false; /* ============ END PLAZA FIELD MODULE (Task 1 — 상수·보드·편성표·cell / Task 2 — 주민 세계 / Task 3 — 신호 토글 / Task 6 — 공용 트리아지 read·admits·미리보기 핀) ============ */ /* ================================ EXPORTS ============================== */ return { // constants N, ROUNDS, PENALTY, PENALTY_SWAP, SHORTFALL_W, RIVAL_L, MEM_K, HUMAN_MOVES_PER_ROUND, A, O, LIVES, SURVIVAL_BUDGET, isSurvivableBoard, survGoalCompliantMove, survGoalMet, survSafeStep, survGoalTarget, survAvoidHaloMove, G1_CONVERGE_STREAK, G1_MOVE_CAP, NO_PROGRESS_CAP, HARD_STOP_MOVES, RULES, RULE_LIST, RULE_VARIANTS, VARIANT_LIST, DELIVER_VARIANT_LIST, registerVariants, comboPred, // SLICE2 LEVER D phase-clock (public state only — C1) PHASE_VARIANT_LIST, RED_SEG, clockSegOf, advanceClock, validatePhaseEscapable, // SLICE2 LEVER A relational (reads live public positions only — C1) RELATIONAL_VARIANT_LIST, rivalAnchors, minManhattanToRival, nearestRivalTokenKey, validateRelationalEscapable, tutRelational, // SLICE2 LEVER A2 role-play / intention rules (reads public state only — C1) ROLE_VARIANT_LIST, moveKeyOf, consistentMoves, outOfCharacter, roleRef, roleAnchors, validateRoleEscapable, validateRoleTaskBoard, buildRoleDemo, // P3 value-laden yield-aware oracle + chokepoint predicate (daBattery/value path) _isUniqueBlocker, _companionPathSet, yieldAwareCompliantMove, validateValueEscapable, // W1.1 priority-ordering pro-attitudes + lexical-composition primitive (value path) PRO_ATTITUDES, lexFilter, lexEscapable, _pathLen, // W1.3 generic lexical oracle (label-free Discovery answer key; consumes lexFilter) lexicalOracle, // W1.5 persona-enacting value demo (app.js demo render only; rides the Sigma* battery) buildValueDemo, // W1.5 §B CONTINUOUS play-memory demo (app.js render + serializer only; additive, off-score) playTrajectory, playTrajectoryPool, pickRepresentative, pickContinuousWalk, stepCompanion, pruneTrajectorySteps, computeForegone, recoverPairFromMove, // PARK PERSONA GRIDWORLD (design 2026-07-03) — the parkMode overlay substrate (campaign/app only) PARK_AXES, PARK_PERSONAS, PARK_AXIS_ATT, PARK_ATT_AXIS, PARK_ATTITUDES, parkOrderingFor, makeParkBoard, parkStart, parkLexFilter, parkOracleMove, parkStep, parkPlayout, parkRecoverOrder, parkRecoverOrderClosed, // R4 surface-mimic control (Task 4, plan 2026-07-14): the SMARTER anti-mimic — a persona-blind // greedy policy over two learned SURFACE features (terrain-class avoid freq + gem approach). The // CROSS-SURFACE-MIMIC gate + Tasks 5-7 (y17/y18/y19) consume it. Pure fn of public state + demo. _parkSurfaceMimic, // PARK-DYN (Task 0, spec 2026-07-13): the opt-in dyn container + deterministic beat/schedule // substrate the six later park mechanics build their runtime state and timing on. _parkDynInit, _parkBeatOf, _parkSchedule, // P10.5 (spec 2026-07-08 §1/§2): the compliant-SET reads (now exposed), the per-pair marginal // recovery READ, and the diverse-path (equivalence-class) faithful sampler. Consumers only. _parkReads, _parkLexSet, _parkKindPair, parkPairMarginal, parkRecoverPair, parkFaithfulPaths, // CN-FIELD-PAIRS (design 2026-07-23): kind signature pairs ∪ a field mech's DECLARED posed // pairs (PARK_FIELD_MECHS[id].pairs). No declaration => byte-identical to _parkKindPair. parkPosedPairs, _PARK_PAIRS, // triage reads the pair list rather than copying it (drift guard) // P10.5 §2 read-widening: the equivalence-class (lex-set MEMBERSHIP) posterior + its per-pair // recovery predicate — the diverse-path counterpart of parkPosterior/parkRecoverPair (argmin). parkPosteriorSet, parkRecoverPairLex, parkPairExpressed, // CROSS-DOMAIN-COUPLING (constitution principle 3/5): the named park reduction CONTRACT + the // game-descriptor coupling machinery (the chess→poker measurement DEFINED, consumes ANY two // reductions; the shipped gate runs it within the park reduction — cross-ontology is the frontier). parkReduce, parkGame, parkRecoverAxisOrder, parkCoupleEpisode, parkCoupling, // P8.5 completeness constants: calibration-aware task admissibility + the noise terminal PARK_CAL_TURNS, PARK_TASK_CAL, PARK_NOISE_MULT, // P2 scoring rebuild (spec 2026-07-03 §A): blind sigma + posterior readouts, the compliant // C* plan search (pursuit), the loud generator-fallback counter parkSigma, parkPosterior, parkCeiling, _parkCeilingFrom, parkGenFallbacks, // P2b task battery (spec §B): the five minigame generators over the same park physics PARK_TASK_KINDS, makeParkTask, _parkTaskBuild, // P8 safety rule-form palette (design 2026-07-06 §A/§B): the PUBLIC form selector + the // per-kind admissible-form manifest + the live relational taboo read (app.js RED marking). PARK_SAFETY_FORMS, PARK_SAFETY_RULE, parkSafetyForm, _PARK_KIND_FORMS, _PARK_KIND_FORMS_MEASURED, _parkFormOf, _parkFormOfKind, _parkRivalTaboo, _parkGoalOf, // P10 phase-clock safety module (spec §3): the temporal mechanism vocabulary + its OWN // escapability guard + visible-signature facet (buildable/playable, demo-only "coming"). PARK_PHASE_FORM, _PARK_PHASE_KINDS, _PARK_FORMS_ALL, _parkPhaseEscapable, _parkPhaseSignature, // P12 PUSH verb module (scout A 2026-07-10): Sokoban-verb park task — own builder / // generator (loud fallbacks) / joint fields / escapability scan / signature; the blind-read // stack rides the push-gated runtime branches (no-ops on walk boards). PARK_PUSH_N, PARK_PUSH_PAIR, _parkPushBuild, makeParkPushTask, parkPushGenFallbacks, _parkPushAdmissible, _parkPushSignature, _parkPushFields, _parkPushScan, _parkPushEscapable, _parkPushDone, _parkPushSid, _parkPushCandSid, _parkPushPairDir, // P12b SLIDE verb module (scout B 2026-07-10): junction-brake momentum over unchanged park // boards — cell/build/generator (loud fallbacks + reject tallies), slide fields, path/ // junction reads (the app's glide animation + lamp-post glyph hooks), escapability scan, // signature; the blind-read stack rides the slide-gated runtime branches (no-ops on walk). PARK_SLIDE_KIND, PARK_SLIDE_PAIR, PARK_SLIDE_ARCH, _parkSlideCell, _parkSlideBuild, makeParkSlideTask, parkSlideGenFallbacks, parkSlideWhys, _parkSlideAdmissible, _parkSlideSignature, _parkSlideFields, _parkSlideScan, _parkSlideEscapable, _parkSlideLegal, _parkSlidePath, _parkSlideJunction, // PARK_FIELD_MECHS (Task 1): the field-mechanic plug-in registry + its public build entry point. // THE SEAM the five parallel park cells are built through — a mechanic registers ONE hook bundle // { build, legalMask, onEnter, onLeave, tick, reads, oracleCost } from inside its own block and // never edits the engine body (gate: REGISTRY-SEAM-BODY). See the contract at the registry. PARK_FIELD_MECHS, parkFieldBuild, PARK_FIELD_SENTINEL, _parkClone, _parkDeepClone, // _parkMaskOf: 게이트가 메커닉의 지형 마스크를 도메인별로 직접 물어보려면 필요하다 // (y58 L3 의 Y58-ROAD-SOLID-BINDS-BOTH / GATE-CLOSES-WALKER-ONLY 가 'me' 와 'mate' 를 // 따로 물어 비대칭이 실제로 서 있는지 잰다). 읽기 전용 조회라 노출해도 계약이 안 는다. _parkMaskOf, // y12 STONES field module (Task 1, plan 2026-07-13): consumable stepping-stone terrain — the // registry's FIRST CLIENT, and the proof the seam is real. Cell/build, the admission predicate // (+ its readable reject tally), the care-read guard, the signature, and the SHIP GATE (blind 6/6 // order recovery). NO GENERATOR AND NO FALLBACK COUNTER — a FIELD module has no generator of its // own: the campaign sweeps it through mech.cell/mech.admits, so a module-level seed walk would be // unreachable (y12's was written, measured dead, and RETIRED in 121a130 — see the tombstone above // _PARK_STONES_WHYS). The loud counter on this path is the campaign's parkCrossFallbacks(). // It reaches the engine ONLY through its PARK_FIELD_MECHS.stones bundle — every // dispatch point is a no-op on a board that never opted in. PARK_STONES_N, PARK_STONES_PAIRS, PARK_STONES_SHIP_SEED, PARK_STONES_SHIPPABLE, _parkStonesCell, _parkStonesBuild, parkStonesWhys, _parkShoalCell, _parkShoalBuild, PARK_SHOAL_EVERY, // shoal (y12 x y10 병합, Task 1) _parkStonesAdmissible, _parkStonesSignature, _parkStonesRecovers, _parkStonesGuard, // y14 TOLL field module (Task 2, plan 2026-07-13): the gem-priced gate — a hedge with two ways // through (the deep FIELD gap, free but ♥−1; the GATE, safe but one gem), and a SECOND, in-place // toll that buys nothing for the walker and opens the COMPANION's lane. Reaches the engine ONLY // through its PARK_FIELD_MECHS.toll bundle (build / legalMask / onEnter / reads / cell / admits). // No makeParkTollTask / genFallbacks: a FIELD module has no generator of its own — the campaign // sweeps it through the registry (cell/admits) and the loud counter is parkCrossFallbacks(). The // reject telemetry hangs off `admits` itself (parkTollWhys). PARK_TOLL_N, PARK_TOLL_PAIRS, PARK_TOLL_PRICE, PARK_TOLL_SHIP_SEED, PARK_TOLL_SHIPPABLE, _parkTollCell, _parkTollBuild, parkTollWhys, _parkTollAdmissible, _parkTollSignature, _parkTollRecovers, _parkTollShut, _parkTollGateDist, // y3 DOWNED field module (Task 3, plan 2026-07-13): the companion starts COLLAPSED in the meadow // and only an ADJACENT ASSIST (walk into him — legalAdd, the hook this cell forced into the seam) // gets him up; ignored, he crawls to the bank on the beat and rises by himself (the no-deadlock // ceiling). Cell/build, the admission predicate (+ its readable reject tally), the assist + crawl // reads, the signature, and the SHIP GATE (blind 6/6 order recovery). NO GENERATOR AND NO FALLBACK // COUNTER — a FIELD module has no generator of its own (see the stones note above and the // tombstone at parkDownedWhys); the loud counter on this path is the campaign's // parkCrossFallbacks(), which y3 — a SHIPPED slot — is swept against and never bumps. // It reaches the engine ONLY through its PARK_FIELD_MECHS.downed bundle. PARK_DOWNED_N, PARK_DOWNED_PAIRS, PARK_DOWNED_SHIP_SEED, PARK_DOWNED_SHIPPABLE, _parkDownedCell, _parkDownedBuild, parkDownedWhys, _parkDownedAdmissible, _parkDownedSignature, _parkDownedRecovers, _parkDownedState, _parkDownedAssist, _parkDownedCrawlTo, _parkDownedFrame, // y8 LOG field module (Task 4): the beat-driven rolling log + the BODY BLOCK. The seam's first // `tick` client (the log rolls while the walker stays) and its first asymmetric legalMask (a wall // for the companion's planner, open ground for the walker who means to stand in its way). Same // shape as stones: cell/build, the admission predicate (+ its readable reject tally), the named // reads its renderer and gates consume, the care signature, and the SHIP GATE. NO GENERATOR AND // NO FALLBACK COUNTER — a FIELD module has no generator of its own (see the stones note above and // the tombstone at parkLogWhys); the loud counter on this path is the campaign's // parkCrossFallbacks(), which y8 — a SHIPPED slot — is swept against and never bumps. PARK_LOG_N, PARK_LOG_PERIOD, PARK_LOG_PAIRS, PARK_LOG_HEART_FLOOR, PARK_LOG_CORRIDOR, PARK_LOG_SHIP_SEED, PARK_LOG_SHIPPABLE, _parkLogCell, _parkLogBuild, parkLogWhys, _parkLogAdmissible, _parkLogSignature, _parkLogRecovers, _parkLogEnt, _parkLogKey, _parkLogBlockKey, _parkLogRolls, _parkLogRollsAt, _parkLogCtx, _parkLogCLive, _parkLogNLive, _parkLogThrough, // y6 DUCKLING field module (Task 6, plan 2026-07-13): a trail-following companion + PER-BODY // terrain cost — tender ground hurts the DUCK and never the walker, so the safety read and the // care read diverge on the same cell (the C-N pair y12 could not pose). Cell/build, the ADMISSION // predicate (+ its readable reject tally), the tender-move read, the signature, and the SHIP GATE. // NO GENERATOR AND NO FALLBACK COUNTER — deliberately; see the tombstone above _PARK_DUCK_WHYS. // It reaches the engine ONLY through its PARK_FIELD_MECHS.duck bundle. // y10 FLOOD field module (Task 5, plan 2026-07-13): SHRINKING terrain — concentric rings sink on a // seed-pure schedule, the hill never does, and a caught walker's run ENDS (reason 'drown' — a // terminal, never a heart). NO module generator: the campaign sweeps this mechanic through // mech.cell/mech.admits, so a module-level seed-walk would be unreachable (see the note at // parkFloodWhys). The rejection telemetry hangs off `admits`, the predicate the campaign really calls. // Exports are exactly the reached surface: the cell/build pair, the admission + signature + SHIP // gate, the PUBLIC preview read the app's dotted next-ring layer draws, and the measured ship pins. PARK_FLOOD_PAIRS, PARK_FLOOD_SHIP_SEED, PARK_FLOOD_SHIPPABLE, _parkFloodCell, _parkFloodBuild, parkFloodWhys, _parkFloodAdmissible, _parkFloodSignature, _parkFloodRecovers, _parkFloodNext, // y16 CARRY field module (Task 3, plan 2026-07-14): ONE STONE, THREE SPENDS — a scarce, irreversible // TOOL whose spend site (ford / cover / break) is a confession of the mind-ORDER. The three facets // share one two-leg shape (steer to the tool, then to your own site), which is the template the // remaining tool cells copy. Goal grammar is COLLECT (a three-type quota) and it rides the ENGINE's // generic needTypes/gtype path — no body line. No module generator (the y10 decision); the // rejection telemetry hangs off `admits`, the predicate the campaign really calls. PARK_CARRY_PAIRS, PARK_CARRY_SHIP_SEED, PARK_CARRY_SHIPPABLE, _parkCarryCell, _parkCarryBuild, parkCarryWhys, _parkCarryAdmissible, _parkCarrySignature, _parkCarryRecovers, _parkCarryBlocked, // y17 FIRE field module (Task 5, plan 2026-07-14): ONE BUCKET, THREE FRONTS, A CLOCK THAT RE-ASKS — // a fire spreads down three fronts on a _parkSchedule beat; the walker fills a single bucket at the // stream and douses ONE front per fill, so the douse SEQUENCE is his whole mind-ORDER (y16's two-leg // tool oracle with the tool made REFILLABLE + a spread clock, the OOD axis). Goal grammar HARVEST; // no deep field (the fire is a blocked wall, never a heart — design law 3). No module generator; the // reject telemetry hangs off `admits`, the predicate the campaign really calls. PARK_FIRE_N, PARK_FIRE_PERIOD, PARK_FIRE_PAIRS, PARK_FIRE_SHIP_SEED, PARK_FIRE_SHIPPABLE, _parkFireCell, _parkFireBuild, parkFireWhys, _parkFireAdmissible, _parkFireSignature, _parkFireRecovers, _parkFireBlocked, _parkFireBurningSet, // y18 MINE field module (Task 6, plan 2026-07-14): DIG the maze — most of the board is earth the // walker alone opens (legalAdd), buried gas pockets (deep from turn 1) whose danger is made public // by minesweeper PIPS (_parkMinePips, a pure board function). Three dig-routes fork from the spawn: // the ore vein (goal, through a pocket), the pip-0 detour (safety), the walled cavity (care). The // carry prototype ported onto a dig substrate; harvest goal grammar, no engine-body line. PARK_MINE_PAIRS, PARK_MINE_SHIP_SEED, PARK_MINE_SHIPPABLE, _parkMineCell, _parkMineBuild, parkMineWhys, _parkMinePips, _parkMineAdmissible, _parkMineSignature, _parkMineRecovers, _parkMineBlocked, _parkMineCaveFirst, // y19 TOWER field module (Task 7, plan 2026-07-14): the PERSPECTIVE-FLIP cell — pos[0] is a // bodiless CURSOR in a control tower, and what WALKS is an NPC resident (dyn.ents[0]) that steps // one tile/tick toward its nearest gem over the OPEN-GATE graph (persona-independent, C1). A `stay` // on a console toggles the remote gate it wires to (onEnter fires on stay); WHICH console the cursor // commits is the confession of the top mind (the y16 spend-site shape, remote-controlled). No module // generator (the y10 decision); the rejection telemetry hangs off `admits`. PARK_TOWER_N, PARK_TOWER_PAIRS, PARK_TOWER_SHIP_SEED, PARK_TOWER_SHIPPABLE, _parkTowerCell, _parkTowerBuild, parkTowerWhys, _parkTowerAdmissible, _parkTowerSignature, _parkTowerRecovers, _parkTowerNpc, _parkTowerNpcStep, _parkTowerFieldBlocked, _parkTowerCursorBlocked, // y20 BOMB field module (Task 1, plan 2026-07-14): TWO BOMBS, THREE WALLS — a scarce, TIMED tool // used twice. The walls OPENED (in order) are the top two minds; the wall ABANDONED is the lowest // (dyn.opened is the confession). The y16 two-leg tool steering, doubled: pick a bomb, plant on your // mind's face, repeat. The seam-trap-4 cure (a satisfied mind still holding a bomb STEPS ASIDE with an // empty prefer instead of voting all-legal) lets the second fork read the subordinate pair even for a // goal-top persona, so it SHIPS 6/6 (ship:true), joining y16. Harvest goal grammar; no engine-body line. No module // generator (the y10 decision); the rejection telemetry hangs off `admits`, the predicate the campaign // really calls. It reaches the engine ONLY through its PARK_FIELD_MECHS.bomb bundle. PARK_BOMB_N, PARK_BOMB_FUSE, PARK_BOMB_R, PARK_BOMB_PAIRS, PARK_BOMB_SHIP_SEED, PARK_BOMB_SHIPPABLE, _parkBombCell, _parkBombBuild, parkBombWhys, _parkBombAdmissible, _parkBombSignature, _parkBombRecovers, _parkBombBlocked, _parkBombCross, _parkBombCtx, _parkMateKey, // y22 BOMB2 field module (Task 1, plan 2026-07-20): a shallow fork of the bomb block above — the // pen's contract gem sits on DRY ground (the stand) behind a moat, so the walker either wades the // grass route (>= 1 deep entry) or opens the cage wall and walks in dry from the north (the blast // route). Runtime hooks shared BY REFERENCE from PARK_FIELD_MECHS.bomb; only build/cell/admits/ // legalMask are fork-local. It reaches the engine ONLY through its PARK_FIELD_MECHS.bomb2 bundle. // y21 TROLLEY field module (Task 3, plan 2026-07-14): THREE CARTS, THREE FORKS on THREE clocks // (t0=3/12/21). Each track stages ONE pair's shipped-attitude conflict with the third mind inert — // deep-shortcut G-C (track A), contested-gem G-N (track B), deep-lever dive C-N (track C, the y3 // geometry split) — so the three pairwise verdicts reconstruct the full order. Levers are normal // walkable cells that toggle a cart's public branch on step; the carts' channels (gems/hotLane/downed // mate) are the OOD skin, kept off every completion route. Harvest goal grammar; no engine-body line. // It reaches the engine ONLY through its PARK_FIELD_MECHS.trolley bundle. // y23 STORM field module (plan 2026-07-20): flood's ring scheduler ported onto a CLOSING MAGNETIC // FIELD whose two inversions are the cell — the field bills ♥ DoT instead of drowning you, and the // TARGET of that harm swings between the walker and the companion through a console toggle // (dyn.storm.polarity; the flip log is the confession). The three minds only NARROW the shipped // attitudes: C treats the flip as the safest act it can take, N forbids exactly that flip while // the mate is exposed — a geometric C-N PARTITION at the core rim, not a subtraction. It reaches // the engine ONLY through its PARK_FIELD_MECHS.storm bundle. No module generator (the y10 // decision); the rejection telemetry hangs off `admits`. PARK_STORM_N, PARK_STORM_EVERY, PARK_STORM_MARGIN, PARK_STORM_GRACE, PARK_STORM_PAIRS, PARK_STORM_SHIPPABLE, _parkStormBuild, _parkStormCell, _parkStormNextIdx, _parkStormNext, _parkStormMargin, _parkStormPlay, _parkStormSignature, _parkStormAdmissible, _PARK_STORM_WHYS, parkStormWhys, // y25 BULL field module (complete, plan 2026-07-20): board builder + lane/alignment helpers, tick + // legalMask + the PARK_FIELD_MECHS.bull registration, C/N reads, path-testimony signature, and the // admissible gate. // y29 STATUE field module (plan 2026-07-22): the yard, the doll's drawn face, the two pure clock // reads, the gaze toll / adjacency hold / mate-stun hooks, the stay-vs-close reads and the // admission gate. PARK_STATUE_SHIPPABLE is a literal false — this is a PREVIEW module. PARK_STATUE_N, // y29 PARK_STATUE_SING, // y29 PARK_STATUE_GAZE, // y29 PARK_STATUE_PERIOD, // y29 PARK_STATUE_STUN, // y29 PARK_STATUE_LEAD, // y29 PARK_STATUE_PAIRS, // y29 _parkStatueBuild, // y29 _parkStatueCell, // y29 _parkStatueGazing, // y29 _parkStatueTillGaze, // y29 _parkStatueNear, // y29 _parkStatueShadow, // y29 _parkStatueSummonDest, // y29 parkStatueSummon, // y29 _parkStatueSummonStep, // y29 _parkStatueApproach, // y29 _parkStatueCtx, // y29 _parkStatuePlay, // y29 _parkStatueSignature, // y29 _parkStatueAdmissible, // y29 _PARK_STATUE_WHYS, // y29 parkStatueWhys, // y29 PARK_STATUE_SHIPPABLE, // y29 // y27 TRAIL field module (plan 2026-07-20): board builder + the freeze/melt hooks, the // lane-keeping N facet and the self-boxing C facet, the three-openings signature, and the // admission gate. PARK_TRAIL_SHIPPABLE is a literal false — this is a PREVIEW module. // y31 RELAY field module (plan 2026-07-22 + ERRATA): board builder + the masked counter, the // armed shelf and its action-move delivery, the per-shelf mate opening, the relay care facet, // and the admission gate. PARK_RELAY_SHIPPABLE is a literal false — this is a PREVIEW module. PARK_RELAY_N, // y31 PARK_RELAY_REACH, // y31 PARK_RELAY_PAIRS, // y31 _parkRelayBuild, // y31 _parkRelayCell, // y31 _parkRelayServed, // y31 _parkRelayOpen, // y31 _parkRelayApproach, // y31 _parkRelayCtx, // y31 _parkRelayPlay, // y31 _parkRelaySignature, // y31 _parkRelayAdmissible, // y31 _PARK_RELAY_WHYS, // y31 parkRelayWhys, // y31 PARK_RELAY_SHIPPABLE, // y31 PARK_LEDGE_N, // y26 _parkLedgeBuild, // y26 _parkLedgeCell, // y26 _parkLedgeDropKey, // y26 _parkLedgePushDest, // y26 PARK_LEDGE_PAIRS, // y26 _parkLedgeNextPush, // y26 _parkLedgeCtx, // y26 _parkLedgePlay, // y26 _parkLedgeSignature, // y26 _parkLedgeAdmissible, // y26 _PARK_LEDGE_WHYS, // y26 PARK_LEDGE_SHIPPABLE, // y26 PARK_LEDGE_SHIP_SEED, // y26 _parkLedgeRecovers, // y26 PARK_LANT_N, // y24 PARK_LANT_R, // y24 PARK_LANT_REACH, // y24 PARK_LANT_PAIRS, // y24 _parkLantBuild, // y24 _parkLantCell, // y24 _parkLantCenter, // y24 _parkLantLit, // y24 _parkLantApproach, // y24 _parkLantCtx, // y24 _parkLantPlay, // y24 _parkLantSignature, // y24 _parkLantAdmissible, // y24 _PARK_LANT_WHYS, // y24 parkLantWhys, // y24 PARK_LANT_SHIPPABLE, // y24 // y33 YIELD field module (design 2026-07-23): the purpose-built C-vs-N board — one plank over // a water-walled chasm, deep yield pockets on its rim, a rotten mid-span segment with a verge // boardwalk. Reaches the engine ONLY through its PARK_FIELD_MECHS.yield bundle. PARK_YIELD_N, PARK_YIELD_PAIRS, PARK_YIELD_SHIPPABLE, PARK_YIELD_SHIP_SEED, _parkYieldRecovers, _parkYieldCell, _parkYieldBuild, _parkYieldAdmissible, _parkYieldSignature, parkYieldWhys, // y46 SIEGE field module (plan 2026-07-25, PROMOTED 2026-07-26): the statue×storm hybrid — y29's // yard and clock with rear bands that sink on the shared 6-beat period. PARK_SIEGE_SHIPPABLE is // now DERIVED from _parkSiegeRecovers, not a literal. PARK_SIEGE_N, // y46 PARK_SIEGE_SING, // y46 PARK_SIEGE_GAZE, // y46 PARK_SIEGE_PERIOD, // y46 PARK_SIEGE_STUN, // y46 PARK_SIEGE_LEAD, // y46 PARK_SIEGE_RINGS, // y46 PARK_SIEGE_PER_WAVE, // y46 — bands swallowed by ONE wave (v2 raised the speed here, not in EVERY) PARK_SIEGE_EVERY, // y46 PARK_SIEGE_CUT, // y46 PARK_SIEGE_PAIRS, // y46 _parkSiegeBuild, // y46 _parkSiegeCell, // y46 _parkSiegeGazing, // y46 _parkSiegeTurning, // y46 — the head-turn beat (free; the goal mind's yield span) _parkSiegeStare, // y46 — the stare beat (the ONLY beat the gaze toll bills) _parkSiegeTillGaze, // y46 _parkSiegeNext, // y46 _parkRoadBuild, _parkRoadCell, _parkRoadAdmissible, _parkRoadBandAt, // y58 _parkRoadGateBeat, _parkRoadPocket, _parkRoadSolidEdge, // y58 (Task 3 / L3 모서리 실선) _parkRoadDrumAt, // y58 (Task 5 — C.prefer 가 쓰는 칸 술어, _parkRoadBandAt 과 다르다) PARK_ROAD_SHIP_SEED, _parkRoadRecovers, PARK_ROAD_SHIPPABLE, // y58 (Task 6 — derive-never-assert 승격 핀) // _parkDestCell (엔진 제너릭, Task 4 가 처음 노출): road 의 Y58-ROAD-DEST-MOVES 게이트가 쓴다 — // 목적지가 차량을 따라 흐르는지 재려면 P.dest 가 가리키는 셀을 밖에서 직접 읽어야 한다. _parkDestCell, PARK_PLAZA_N, PARK_PLAZA_SPAWN_GAP, PARK_PLAZA_COUNT, PARK_PLAZA_TELEGRAPH, // y59 PARK_PLAZA_STUN, PARK_PLAZA_TAIL, PARK_PLAZA_PIPS, // y59 _parkPlazaCell, _parkPlazaBuild, // y59 _parkPlazaResidentIntent, _parkPlazaResolve, _parkPlazaSpawnTick, // y59 — 주민의 세계 PARK_PLAZA_HORIZON, _parkPlazaSteer, _parkPlazaCtx, _parkPlazaForecast, // y59 — 신호수의 마음 _parkPlazaAdmissible, _PARK_PLAZA_WHYS, parkPlazaWhys, PARK_PLAZA_SHIPPABLE, // y59 — 착석 필터 _parkSiegeClaim, // y46 — take a safe zone (v2, 요구 5) _parkSiegeFreeGoals, // y46 — the zones nobody holds; runners and the care mind read this same list _parkSiegeMateGoal, // y46 — the exit the sleeping companion is nearest to (her claim ring) _parkSiegePushTarget, // y46 — the body standing on a cell that the shoulder may shove _parkSiegePushDest, // y46 — where that body lands (two cells along), or null if refused _parkSiegeDistFrom, // y46 — step distances refusing walls, water and claimed zones _parkSiegeNear, // y46 _parkSiegeShadow, // y46 _parkSiegeSummonStep, // y46 _parkSiegeShove, // y46 _parkSiegeApproach, // y46 _parkSiegeCtx, // y46 _parkSiegePlay, // y46 _parkSiegeSignature, // y46 _parkSiegeAdmissible, // y46 _PARK_SIEGE_WHYS, // y46 parkSiegeWhys, // y46 PARK_SIEGE_SHIP_SEED, // y46 _parkSiegeRecovers, // y46 — the derived module bar (calibrated 6/6 blind order recovery) PARK_SIEGE_SHIPPABLE, // y46 // y50 ALLEY field module (plan 2026-07-25): the push×bull ladder-maze hybrid — y25's telegraphed // charge and reads on a three-alley yard whose spawn reaches the interpose, plus the y26-idiom // crate shove and its shield clause in the resolution scan. PARK_ALLEY_SHIPPABLE is DERIVED from // _parkAlleyRecovers (2026-08-01), not a literal; it reads false today because the calibrated 6/6 // blind order recovery lands on 0/40 seeds (G-N unposed for the three caution-over-care personas) // — a PREVIEW module, honestly. PARK_ALLEY_N, // y50 PARK_ALLEY_DOZE, // y50 — the round-2 bull's wake period (beats); it sleeps through the rest PARK_ALLEY_RAY_WARN, // y50 — 안전이 도즈 시계를 미리 읽는 여유(박자) _parkAlleyRays, // y50 — 황소에서 뻗은 네 직선(벽에서 멈춤) _parkAlleyRayCtx, // y50 — 시계를 읽는 안전 채널 { live, rays }; runner 전용 _parkAlleyYield, // y50 — round 0's claimed/unclaimed pad pair (the goal-vs-care question) PARK_ALLEY_STUN, // y50 PARK_ALLEY_TELEGRAPH, // y50 PARK_ALLEY_CRAWL_EVERY, // y50 PARK_ALLEY_CRAWL_CAP, // y50 PARK_ALLEY_HEART_FLOOR, // y50 PARK_ALLEY_PAIRS, // y50 _parkAlleyBuild, // y50 _parkAlleyCell, // y50 _parkAlleyLane, // y50 _parkAlleyAligned, // y50 _parkAlleyPushDest, // y50 _parkAlleyMatePushDest, // y50 — the companion shove (round 0's answer when she got there first) _parkAlleyTailPad, // y50 — round 2's goal rides the bull's tail PARK_ALLEY_MATE_NEAR, // y50 _parkAlleyMateEvade, // y50 — round 2: she keeps out of the bull's way, and nothing else _parkAlleySegDist, // y50 _parkAlleyCtx, // y50 _parkAlleyPlay, // y50 _parkAlleySignature, // y50 _parkAlleyAdmissible, // y50 _PARK_ALLEY_WHYS, // y50 parkAlleyWhys, // y50 PARK_ALLEY_SHIP_SEED, // y50 _parkAlleyRecovers, // y50 — the derived module bar (calibrated 6/6 blind order recovery) PARK_ALLEY_SHIPPABLE, // y50 // y51 ESCAPE field module (design 2026-07-27): two dolls on a 6-beat and a 3-beat clock, a flood // front that advances on the beats both of them sing through, and the human-only pull that bills // one beat's toll to the companion instead of the walker. PARK_ESCAPE_SHIPPABLE is DERIVED from // _parkEscapeRecovers (Task 6, 2026-07-27), not a literal; it reads false today because module // admission is 0/40 — a PREVIEW module, honestly. PARK_ESCAPE_N, // y51 PARK_ESCAPE_SING, // y51 PARK_ESCAPE_GAZE, // y51 PARK_ESCAPE_PERIOD, // y51 PARK_ESCAPE_SING_B, // y51 PARK_ESCAPE_GAZE_B, // y51 PARK_ESCAPE_PERIOD_B, // y51 PARK_ESCAPE_HEARTS, // y51 PARK_ESCAPE_STUN, // y51 PARK_ESCAPE_EVERY, // y51 PARK_ESCAPE_REACH, // y51 PARK_ESCAPE_PAIRS, // y51 _parkEscapeBuild, // y51 _parkEscapeAssemble, // y51 — Task 3: the pure per-seed layout draw, before the reseed loop _parkEscapeLayoutOk, // y51 — Task 3: the soft-lock filter itself, spec §8's three conditions _parkEscapeCell, // y51 _parkEscapeGazeA, // y51 — the forward doll's looking span _parkEscapeGazeB, // y51 — the left-flank doll's looking span _parkEscapeBills, // y51 — either eye open: the toll's single predicate _parkEscapeFloodBeat, // y51 — the beat the front advances on (tick's own gate) _parkEscapeNext, // y51 _parkEscapeNear, // y51 _parkEscapeShove, // y51 _parkEscapePullDest, // y51 parkEscapePull, // y51 — the HUMAN-ONLY command (app.js is its one caller) _parkEscapeCtx, // y51 _parkEscapePlay, // y51 _parkEscapeSignature, // y51 _parkEscapeAdmissible, // y51 _PARK_ESCAPE_WHYS, // y51 parkEscapeWhys, // y51 PARK_ESCAPE_SHIP_SEED, // y51 — Task 6: the seed the DERIVED module bar is read on _parkEscapeRecovers, // y51 — Task 6: the derived module bar itself (calibrated order recovery) PARK_ESCAPE_SHIPPABLE, // y51 // y53 BEACON field module (plan 2026-07-27): the statue clock given a DIRECTION — a center tower // whose beam sweeps four quadrants on an 8-beat clock, the turn free and the stare billing, with // the three promotion levers built in from the first commit. PARK_BEACON_N, // y53 PARK_BEACON_SPAN, // y53 PARK_BEACON_STARE_AT, // y53 PARK_BEACON_PERIOD, // y53 PARK_BEACON_STUN, // y53 PARK_BEACON_LEAD, // y53 PARK_BEACON_HOLD, // y53 PARK_BEACON_PAIRS, // y53 _parkBeaconBuild, // y53 _parkBeaconCell, // y53 _parkBeaconFacing, // y53 _parkBeaconTurning, // y53 _parkBeaconStaring, // y53 _parkBeaconTillStare, // y53 _parkBeaconNextFace, // y53 _parkBeaconCone, // y53 _parkBeaconNear, // y53 _parkBeaconTouches, // y53 _parkBeaconCtx, // y53 _parkBeaconPlay, // y53 _parkBeaconSignature, // y53 _parkBeaconAdmissible, // y53 _PARK_BEACON_WHYS, // y53 parkBeaconWhys, // y53 PARK_BEACON_SHIP_SEED, // y53 _parkBeaconRecovers, // y53 — the derived module bar (calibrated 6/6 blind order recovery) PARK_BEACON_SHIPPABLE, // y53 // y52 BURST field module (plan 2026-07-27): three water balloons on one public rhythm, the cross // of water they throw, and a companion who starts inside a bubble only a body can pop. PARK_BURST_N, PARK_BURST_PERIOD, PARK_BURST_ARM, PARK_BURST_STUN, PARK_BURST_PAIRS, // y52 _parkBurstBuild, _parkBurstCell, _parkBurstDue, _parkBurstArms, _parkBurstHot, // y52 _parkBurstFuse, _parkBurstWarning, _parkBurstNear, _parkBurstCtx, _parkBurstLayoutOk, // y52 _parkBurstPlay, _parkBurstSignature, _parkBurstAdmissible, _PARK_BURST_WHYS, parkBurstWhys, // y52 PARK_BURST_SHIP_SEED, _parkBurstRecovers, PARK_BURST_SHIPPABLE, // y52 // y54 SHIFTER field module (plan 2026-07-27): two wall rows whose doorways slide on a public // clock — the maze re-forms rather than closing. // the park cell-key helper (y12 gates + the app's stone/water overlays read it) _parkKey, _parkLegal, _parkLegalKeys, _parkCompanionPlan, _parkCompanionStep, // topology archetypes + per-kind signature (spec 2026-07-04 §A/§B) — exposed for the // PARK-SIGNATURE / PARK-TOPOLOGY / PARK-GOALS gates + the feasible-combo probe. _PARK_KIND_ARCHS, _PARK_KIND_ARCHS_X, _PARK_KIND_GOALS, _parkArchOf, _parkTaskAdmissible, _parkSignature, _PARK_TASK_NEED, // the frozen-geometry witness for the yard registry (PARK-YARD-* gates + tools/yard-sweep.mjs) _parkYardSig, // PARK_YARDS — the one table naming every yard in the park (PARK-YARD-REGISTRY gate). // Exposed as a getter because the table is lazily built (PARK_FIELD_MECHS fills in below // this seam); a plain property here would freeze the empty pre-build object in place. get PARK_YARDS() { return _parkYards(); }, parkYardOf, parkBoardBuild, PARK_YARD_SLIDE_ON: PARK_SLIDE_ARCH, // numeric kind ids used to name yard-sig fixture rows stably across kind LETTER renames // (the observer kind was renamed m7 -> m5 while its generator constant stayed 7; see // engine.js:8947 — this table's values are load-bearing, do not "tidy" them) _PARK_KIND_ID, // display-only consumer (app.js foregone-shortcut price tag): the PUBLIC beeline/safe metric // fields over the board — pure geometry of seed + current chain destination (C1; no persona). _parkFields, // the persona-free goal-greedy probe policy (§1.3, used by _parkTaskAdmissible's m2 bar). Exported // 2026-07-16 so a gate or survey can run the SAME mimic proxy the admits layer runs — it is what // makes the y20 active-anti-mimic measurement recorded on Y20-BOMB-SHIP-GATE reproducible by anyone // retrying that track. Filter/gate/survey-only — never a demo or oracle policy. _parkGreedyMove, // P5 value-laden-motive family (daBattery-gated pool admission, value path only) VALUE_VARIANT_LIST, GOAL_LIST, CUBE_GOAL_LIST, ENV_PRESETS, ENV_LIST, EP_MODE, DIRS, // prng + geometry rng, hashStr, key, inb, keyN, inbN, manhattan, adjacent, tokenAt, maxTokenVal, minAliveTokenVal, minManhattanToDark, clamp01, // board makeBoard, applyTopology, penaltyFor, penaltyForMove, // policy / rules legalMoves, violates, rankCompliantTokens, bestCompliantToken, PersonaPolicy, // diagnostic / scoring adjacentTokens, isDiagnostic, newCtx, decisionPoint, recordTemptation, resolveTemptation, resolveIdleTemptation, evaluateAllSeatTemptations, maintenanceTotals, convergenceForSeat, applyMove, goalSeatProgress, // ceilings + metric bfsStep, planMove, nearestCompliantMove, valueOnlyCompliantMove, positionPriced, safeStep, lookahead2CompliantMove, compliantCandidatePolicies, perfectSelfPolicy, ruleOptimalCeiling, greedyBlindCeiling, greedyGrossCeiling, harvestQuota, compliantRoundHarvestMulti, multiAgentCeiling, multiAgentPerfectPolicies, discoveryAcc, discoveryScore, scoreEpisode, // memory forbiddenCellsOf, violatingPolicy, avoidingPolicy, demoPolicy, buildTutorial, tutPhase, buildEpisode, consistentWith, // SLICE2 LEVER C under-determined demos + fairness validator (spec §5) applyDemoBudget, demoIdentifiable, identifyRules, buildMemoryBundle, induceRuleFromMemory, boundedInduceRuleFromMemory, bestCompliantAdjacent, inductionPredLog, // opponents + swap cloneSim, applySim, applySimPenalized, violatesSim, greedyMove, rolloutMove, rolloutValue, mctsO, rolloutMovePeer, rolloutValuePeer, peerMCTS, makeOpponent, opponentMove, rivalRuleFor, canSwap, invokeSwap, swapEV, takeoverClassify, takeoverStats, // lives / endings / G1 convergence Discovery / G3 takeover (DESIGN §3B/§5B) livesFromViolations, eliminated, consecutiveCleanReached, noProgressFor, g1DiscoveryScore, runG1, induceRuleFromTrajectory, runG3, // cube runCell, runCube, aggregateCube, runAxisSweep, focalAgentnessVsOpponent, runOpponentSweep, computeOpponentInvariance, mean, variance, normVar, isMonotone, }; });