Rev 20: a wave names its victims before it pushes (LK97)

Kestrel stood against a wall Mad Hywel turned to water, was washed one
square back into stone, and was crushed twice. The sweep walked the
way it pushed: having shoved him onto the next square, its next probe
found him there and shoved him again with the force left. From rev 20
a sweep names every victim along its line first, then pushes each
once; older games replay the double shove. A test washes a wizard one
square into a wall under both readings.

With it, a client fix: a tap on a bare square while holding a card no
longer walks there. Kestrel tapped a slime with LIGHTNING BLAST in hand
and strode into it. The tap now says the card is aimed at a wizard or
creature and leaves the feet alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-07 14:38:48 -05:00
co-authored by Claude Fable 5.1
parent e5d82a94af
commit 3f71b04c3b
3 changed files with 92 additions and 42 deletions
@@ -636,3 +636,42 @@ describe("a pit on the board's rim (M4Q7)", () => {
}
});
});
describe("a wave names its victims before it pushes (LK97)", () => {
/** A wall between two squares, a wizard on its near side with one open
* square behind them and a wall beyond it, and a caster's square beside. */
function site(state: GameState) {
const view = boardView(state);
for (const k of Object.keys(view.cells)) {
const [x, y] = k.split(",").map(Number) as [number, number];
const a = { x, y };
if (state.squareContents[k] || view.homes.some((h) => cellKey(h) === k)) continue;
if ((view.edges[edgeKey(a, "S")] ?? "open") !== "wall" || !view.cells[cellKey(neighbor(a, "S"))]) continue;
const back = stepTarget(view, a, "N");
if (back.kind !== "step" || state.squareContents[cellKey(back.to)] || view.homes.some((h) => cellKey(h) === cellKey(back.to))) continue;
if (stepTarget(view, back.to, "N").kind !== "blocked") continue;
const beside = stepTarget(view, a, "E");
if (beside.kind !== "step" || state.squareContents[cellKey(beside.to)]) continue;
return { a, back: back.to, beside: beside.to };
}
throw new Error("no such wall on this board");
}
for (const [deckRev, crush] of [[20, 1], [19, 2]] as const) {
it(`washed one square into a wall: rev ${deckRev} costs ${crush}`, () => {
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev });
const caster = activePlayer(state);
const victim = state.players.find((p) => p.id !== caster.id)!;
const s = site(state);
caster.position = { ...s.beside };
victim.position = { ...s.a };
const card = giveCard(state, caster.id, "stone-to-water");
const r = applyCommand(state, caster.id, { type: "cast", instanceId: card.instanceId, target: { kind: "edge", cell: s.a, side: "S" } });
expect(r.ok).toBe(true);
if (!r.ok) return;
const v = r.state.players.find((p) => p.id === victim.id)!;
expect(cellKey(v.position)).toBe(cellKey(s.back));
expect(v.life).toBe(15 - crush);
expect(r.events.filter((e) => e.type === "washedBack" && e.player === victim.id).length).toBe(crush === 1 ? 1 : 2);
});
}
});