The RNRX coma: tacks are scattered, not thrown

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-26 12:04:57 -04:00
co-authored by Claude Fable 5
parent 6d88b35516
commit 75f1217c77
2 changed files with 39 additions and 2 deletions
+7 -2
View File
@@ -370,7 +370,12 @@ function pathDenial(view: GameView): Command | null {
); );
} }
// Never brick the square the treasure needs to stay reachable on. // 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( consider(
{ type: "cast", instanceId: filler.instanceId, target: { kind: "cell", cell: b } }, { type: "cast", instanceId: filler.instanceId, target: { kind: "cell", cell: b } },
{ avoidCell: cellKey(b) }, { avoidCell: cellKey(b) },
@@ -609,7 +614,7 @@ function pathToward(
const hazard = view.squareContents[k]?.kind; const hazard = view.squareContents[k]?.kind;
if (!opts.throughHazards && if (!opts.throughHazards &&
(hazard === "pit" || hazard === "ooze" || hazard === "thornbush" || (hazard === "pit" || hazard === "ooze" || hazard === "thornbush" ||
hazard === "rosebush" || hazard === "slime")) continue; hazard === "rosebush" || hazard === "slime" || hazard === "tacks")) continue;
if (opts.avoidNearEnemies && nearEnemy(to)) continue; if (opts.avoidNearEnemies && nearEnemy(to)) continue;
next.push(to); next.push(to);
} }
+32
View File
@@ -922,3 +922,35 @@ describe("lessons from the human duels", () => {
expect(cmd).toMatchObject({ type: "cast", instanceId: "LB" }); 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;
}
});
});