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:
co-authored by
Claude Fable 5
parent
6adcbe23b5
commit
118bfd236b
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
applyCommand,
|
||||
createGame,
|
||||
sustainedOn,
|
||||
type GameState,
|
||||
type PlayerId,
|
||||
} from "../src/game";
|
||||
@@ -223,6 +224,65 @@ describe("the clockwork guards gold only within sight", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the clockwork honors IDIOT's march", () => {
|
||||
// Rev 37 retired the engine's steering, so the duty falls to the brain.
|
||||
function cursedBot() {
|
||||
let { state } = createGame({ playerIds: ["bot", "foe"], seed: 7, sets: ["basic", "expansion1"], deckRev: 37 });
|
||||
// Past round 1 (no combat) and around to the bot's turn.
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = r.state;
|
||||
}
|
||||
while (actingSeat(state) !== "bot") {
|
||||
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = r.state;
|
||||
}
|
||||
state.sustained.push({
|
||||
id: "fx-idiot", cardId: "idiot", casterId: "foe", targetId: "bot",
|
||||
remainingTurns: 9999, data: {},
|
||||
} as never);
|
||||
return state;
|
||||
}
|
||||
|
||||
it("marches to its own gold until the curse lifts", () => {
|
||||
let state = cursedBot();
|
||||
state.players.find((p) => p.id === "bot")!.hand = [];
|
||||
// A walled route can cost more steps than one turn's allowance; the
|
||||
// march may span turns (the foe just passes).
|
||||
for (let i = 0; i < 30 && sustainedOn(state, "bot", "idiot").length > 0; i++) {
|
||||
const seat = actingSeat(state);
|
||||
const cmd = seat === "bot"
|
||||
? automatonCommand(viewFor(state, "bot"), "hunter", "archmage")
|
||||
?? automatonFallback(viewFor(state, "bot"), "archmage")
|
||||
: { type: "endTurn", draw: 0 } as const;
|
||||
const r = applyCommand(state, seat, cmd);
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = r.state;
|
||||
}
|
||||
expect(sustainedOn(state, "bot", "idiot").length).toBe(0);
|
||||
});
|
||||
|
||||
it("shakes its gold from a thief's arms with DROP OBJECT", () => {
|
||||
const state = cursedBot();
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
const thief = state.players.find((p) => p.id === "foe")!;
|
||||
thief.position = { ...bot.position };
|
||||
const own = state.treasures.find((t) => t.owner === "bot")!;
|
||||
own.carriedBy = "foe";
|
||||
own.position = null;
|
||||
thief.carriedTreasureId = own.id;
|
||||
bot.hand = [{ instanceId: "drop-object#T", cardId: "drop-object" }];
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
expect(cmd).toEqual({
|
||||
type: "cast", instanceId: "drop-object#T",
|
||||
target: { kind: "player", playerId: "foe" }, params: { cardId: "treasure" },
|
||||
});
|
||||
expect(applyCommand(state, "bot", cmd!).ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a clogged hand gets shed, not hoarded", () => {
|
||||
it("the bot discards dead weight so the end-of-turn draw has room", () => {
|
||||
let { state } = createGame({ playerIds: ["bot", "other"], seed: 42, sets: ["basic"], deckRev: 13 });
|
||||
|
||||
Reference in New Issue
Block a user