irregular6612's picture
deploy: Space λ₯Ό main 4709b8a 트리둜 λ§žμΆ˜λ‹€
884b387
Raw
History Blame Contribute Delete
46.6 kB
/* ============ 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));