From 75f1217c77b4a832a8f1d8fea20b9c4cc2b3ba0f Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Wed, 26 Aug 2026 12:04:57 -0400 Subject: [PATCH] The RNRX coma: tacks are scattered, not thrown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automaton II froze for three turns of game four: pathDenial proposed HANDFUL OF TACKS on a square four steps away, the engine refused the cast every turn ("you must be adjacent to scatter tacks"), and the refusal-fallback loop read as a coma. The denial planner now offers tacks only at the caster's feet — and the walker learns tacks are a floor hazard, so a bot no longer mines its own doorstep and strolls straight into it (also RNRX, seq 194-195). The frozen ledger moment replays cured; a regression pins the planner's proposals to engine-accepted commands. 288 tests; 24 ledgers verified. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF --- packages/engine/src/automaton.ts | 9 ++++++-- packages/engine/test/automaton.test.ts | 32 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/automaton.ts b/packages/engine/src/automaton.ts index 394eb99..c6ef254 100644 --- a/packages/engine/src/automaton.ts +++ b/packages/engine/src/automaton.ts @@ -370,7 +370,12 @@ function pathDenial(view: GameView): Command | null { ); } // Never brick the square the treasure needs to stay reachable on. - if (filler && cellKey(b) !== cellKey(goal) && cellCastable(cellKey(b))) { + // TACKS are scattered at the caster's feet ("you must be adjacent"), + // not thrown — offering a distant square wedges the whole brain on + // an eternally refused cast. + const fillerReaches = !filler || filler.cardId !== "handful-of-tacks" || + Math.abs(b.x - me(view).position.x) + Math.abs(b.y - me(view).position.y) <= 1; + if (filler && fillerReaches && cellKey(b) !== cellKey(goal) && cellCastable(cellKey(b))) { consider( { type: "cast", instanceId: filler.instanceId, target: { kind: "cell", cell: b } }, { avoidCell: cellKey(b) }, @@ -609,7 +614,7 @@ function pathToward( const hazard = view.squareContents[k]?.kind; if (!opts.throughHazards && (hazard === "pit" || hazard === "ooze" || hazard === "thornbush" || - hazard === "rosebush" || hazard === "slime")) continue; + hazard === "rosebush" || hazard === "slime" || hazard === "tacks")) continue; if (opts.avoidNearEnemies && nearEnemy(to)) continue; next.push(to); } diff --git a/packages/engine/test/automaton.test.ts b/packages/engine/test/automaton.test.ts index 628bd6f..43dfc10 100644 --- a/packages/engine/test/automaton.test.ts +++ b/packages/engine/test/automaton.test.ts @@ -922,3 +922,35 @@ describe("lessons from the human duels", () => { expect(cmd).toMatchObject({ type: "cast", instanceId: "LB" }); }); }); + +describe("the denial planner offers only castable blocks", () => { + it("tacks at range are never proposed — the RNRX coma", () => { + // Room RNRX froze Automaton II for three turns: pathDenial proposed + // scattering tacks four squares away, the engine refused ("you must + // be adjacent"), and the refusal-fallback loop read as a coma. + let { state } = createGame({ playerIds: ["foe", "bot"], seed: 42, sets: ["basic", "expansion1"] }); + while (state.turn.round < 2 || state.players[state.turn.activeIndex]!.id !== "bot") { + const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 }); + if (!r.ok) throw new Error(r.error); + state = r.state; + } + const bot = state.players.find((p) => p.id === "bot")!; + const foe = state.players.find((p) => p.id === "foe")!; + // The bot's floor gold with a raider closing on it, and only TACKS + // in hand to deny the road — from a stand-off distance. + const gold = state.treasures.find((t) => t.owner === "bot" && t.position)!; + foe.position = { ...gold.position! }; + bot.position = { ...bot.home }; + bot.hand = [{ cardId: "handful-of-tacks", instanceId: "HT" } as never]; + for (let guard = 0; guard < 6; guard++) { + const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage"); + if (!cmd) break; + const r = applyCommand(state, "bot", cmd); + // Whatever the brain proposes, the engine must accept it. + expect(r.ok, `refused: ${JSON.stringify(cmd)} — ${!r.ok ? r.error : ""}`).toBe(true); + if (!r.ok) break; + state = r.state; + if (cmd.type === "endTurn") break; + } + }); +});