IDIOT trusts the feet and yields to a jailbreak drop (rules rev 37)

The per-step steering recalculated greedily, ping-ponging between routes
that tie at a corner and able to wall the victim off from the cure; rev 37
retires it — the march is the player's to honor, and the automatons steer
themselves. DROP OBJECT that shakes the victim's OWN treasure out of a
thief's arms is now an aid, not an attack: while carried, that treasure
cannot even be stood upon, so the curse could be unsatisfiable. Older
ledgers replay steered and strict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-21 11:11:25 -04:00
co-authored by Claude Fable 5
parent 6adcbe23b5
commit 118bfd236b
6 changed files with 185 additions and 15 deletions
@@ -198,6 +198,64 @@ describe("expansion combat cards", () => {
expect(applyCommand(state, defender, { type: "counteract", instanceId: "absorb-spell#AB" }).ok).toBe(true);
});
it("idiot at rev 37 no longer steers the march — the step goes where asked", () => {
let { state } = createGame({
playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 37,
});
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const id = giveCard(state, attacker, "idiot");
state = castAt(state, attacker, defender, id);
expect(sustainedOn(state, defender, "idiot").length).toBe(1);
state = must(state, attacker, { type: "endTurn", draw: 0 });
const d = state.players.find((p) => p.id === defender)!;
const board = boardView(state);
const open = (["N", "S", "E", "W"] as const)
.filter((s) => stepTarget(board, d.position, s).kind === "step");
expect(open.length).toBeGreaterThanOrEqual(2);
// Plant the victim's own gold one step out one way; walk the other way.
// Pre-37 steering would have overridden the request and taken the gold.
const toward = stepTarget(board, d.position, open[0]!);
const away = stepTarget(board, d.position, open[1]!);
if (toward.kind === "blocked" || away.kind === "blocked") throw new Error("unreachable");
const own = state.treasures.find((t) => t.owner === defender)!;
own.carriedBy = null;
own.position = toward.to;
state = must(state, defender, { type: "move", direction: open[1]! });
expect(cellKey(state.players.find((p) => p.id === defender)!.position)).toBe(cellKey(away.to));
});
it("idiot at rev 37 permits DROP OBJECT that frees the victim's own treasure", () => {
for (const deckRev of [36, 37]) {
let { state } = createGame({
playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev,
});
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const thief = state.players.find((p) => p.id === attacker)!;
const own = state.treasures.find((t) => t.owner === defender)!;
own.carriedBy = attacker;
own.position = null;
thief.carriedTreasureId = own.id;
const id = giveCard(state, attacker, "idiot");
state = castAt(state, attacker, defender, id);
state = must(state, attacker, { type: "endTurn", draw: 0 });
// An attack for its own sake stays forbidden at every revision.
const fb = giveCard(state, defender, "fireball", "F", 0);
expect(applyCommand(state, defender, {
type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: attacker },
}).ok).toBe(false);
// Shaking YOUR OWN gold out of the thief's arms serves the march —
// while carried it cannot be stood upon — but only from rev 37 on.
const dr = giveCard(state, defender, "drop-object", "D", 1);
const r = applyCommand(state, defender, {
type: "cast", instanceId: dr.instanceId,
target: { kind: "player", playerId: attacker }, params: { cardId: "treasure" },
});
expect(r.ok).toBe(deckRev >= 37);
}
});
it("idiot at rev 23 has no effect on a victim carrying their own treasure", () => {
let { state } = createGame({
playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 23,