Waves spend their force as they travel (rev 24); bots wield Destroy Wall

Rules rev 24: a waterwall wave that finds a victim dist cells from its
source has only range-dist spaces of push left — a range-2 wave throws its
adjacent victim two spaces but a victim at its far edge only one, and
spent force never converts into crush damage. Applies to WATERWALL and
both STONE TO WATER waves; older revisions keep the flat full-range wash
so stored games replay unchanged.

Automatons now use DESTROY WALL on the march: two BFS distance maps (from
the bot, from its objectives) price every visible wall by the shortcut its
removal opens; the bot blasts when it saves 4+ steps of walking — or when
no road exists at all — never standing beside the blast unless trapped
and healthy. The card also leaves the shed pile: discardValue 2 -> 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-17 19:40:41 -04:00
co-authored by Claude Fable 5
parent dfd7a2ff56
commit 2d53ba4bc0
6 changed files with 215 additions and 27 deletions
+46
View File
@@ -5,6 +5,7 @@ import {
type GameState,
type PlayerId,
} from "../src/game";
import { edgeKey } from "../src/board";
import { viewFor } from "../src/view";
import { automatonCommand, automatonFallback, type AutomatonStyle, type AutomatonTier } from "../src/automaton";
@@ -214,3 +215,48 @@ describe("the clockwork honors absorb's fine print", () => {
expect(cmd).toEqual({ type: "counteract", instanceId: "blunt#T" });
});
});
describe("the clockwork wields destroy wall", () => {
it("blasts its way out when no road leads to the gold", () => {
let { state } = createGame({ playerIds: ["bot", "other"], seed: 42, sets: ["basic"], deckRev: 24 });
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;
}
const bot = state.players.find((p) => p.id === "bot")!;
// Brick the bot into a one-square cell far from everything.
bot.position = { x: 4, y: 4 };
for (const side of ["N", "S", "E", "W"] as const) {
state.edgeOverrides[edgeKey(bot.position, side)] = "wall";
}
state.players.find((p) => p.id === "other")!.position = { x: 0, y: 0 };
bot.hand[0] = { instanceId: "destroy-wall#T", cardId: "destroy-wall" };
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
expect(cmd).toMatchObject({ type: "cast", instanceId: "destroy-wall#T" });
// And the engine accepts the blast it chose.
const r = applyCommand(state, "bot", cmd!);
expect(r.ok).toBe(true);
});
it("values destroy wall above the chaff when forced to discard", () => {
let { state } = createGame({ playerIds: ["bot", "other"], seed: 42, sets: ["basic"], deckRev: 24 });
const bot = state.players.find((p) => p.id === "bot")!;
bot.hand = [
{ instanceId: "destroy-wall#T", cardId: "destroy-wall" },
{ instanceId: "buddy#T", cardId: "buddy" },
{ instanceId: "ugly#T", cardId: "ugly" },
{ instanceId: "fear#T", cardId: "fear" },
{ instanceId: "full-shield#T", cardId: "full-shield" },
{ instanceId: "fireball#T", cardId: "fireball" },
{ instanceId: "number-3#T", cardId: "number-3" },
{ instanceId: "troll#T", cardId: "troll" },
];
state.pendingDiscard = "bot";
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
expect(cmd?.type).toBe("discard");
if (cmd?.type === "discard") {
expect(cmd.instanceIds).not.toContain("destroy-wall#T");
}
});
});