The clockwork stops absorbing nothing

An automaton answered DROP OBJECT with an Absorb, then a Blunt —
two counters drained against a spell that carries no points. The
brain's price table listed only the classic damage spells; anything
unknown was guessed at 2 points, which tripped every damage-counter
rung. Utility attacks (dropping, dragging, swapping, peeking) now
route past those rungs entirely: the bot passes, or spends a full
stop when the effect actually threatens something — a shield for the
treasure it carries, a worrier's refusal to be dragged. Stone Dead,
Butt Head, Heave-Ho, and Illusionary Attack join the price table
while the hood is up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 21:39:34 -04:00
co-authored by Claude Fable 5
parent 8a460a0eed
commit 2950380e23
2 changed files with 74 additions and 1 deletions
+49
View File
@@ -116,3 +116,52 @@ describe("automaton vs automaton", () => {
}
});
});
describe("the clockwork does not waste counters on pointless targets", () => {
function underAttack(attackId: string, defenderHand: { instanceId: string; cardId: string }[], rig?: (s: any, defender: string) => void) {
let { state } = createGame({ playerIds: ["human", "bot"], seed: 42, sets: ["basic", "expansion1"], deckRev: 13 });
// burn round 1
for (let i = 0; i < 2; i++) {
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
state = r.state;
}
while (actingSeat(state) !== "human") {
state = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 }).state;
}
const human = state.players.find((p: { id: string }) => p.id === "human")!;
const bot = state.players.find((p: { id: string }) => p.id === "bot")!;
bot.position = { ...human.position };
defenderHand.forEach((c, i) => { bot.hand[i] = c; });
human.hand[0] = { instanceId: `${attackId}#A`, cardId: attackId };
rig?.(state, "bot");
const r = applyCommand(state, "human", {
type: "cast", instanceId: `${attackId}#A`, target: { kind: "player", playerId: "bot" },
...(attackId === "drop-object" ? { params: { cardId: "dagger" } } : {}),
});
if (!r.ok) throw new Error(`setup: ${r.error}`);
return r.state;
}
it("passes on drop-object rather than absorbing nothing", () => {
const state = underAttack("drop-object", [
{ instanceId: "absorb#T", cardId: "absorb" },
{ instanceId: "blunt#T", cardId: "blunt" },
{ instanceId: "dagger#T", cardId: "dagger" },
]);
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
expect(cmd).toEqual({ type: "pass" });
});
it("shields a drop-object aimed at its carried treasure", () => {
const state = underAttack("drop-object", [
{ instanceId: "full-shield#T", cardId: "full-shield" },
{ instanceId: "dagger#T", cardId: "dagger" },
], (s, defender) => {
const d = s.players.find((p: { id: string }) => p.id === defender)!;
const t = s.treasures.find((t: { owner: string }) => t.owner !== defender)!;
t.position = null; t.carriedBy = defender; d.carriedTreasureId = t.id;
});
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
expect(cmd).toEqual({ type: "counteract", instanceId: "full-shield#T" });
});
});