Rev 12: vanishing and water answer the call to arms

INVISIBLE's corner reads NEUTRAL/COUNTERACTION, and ANTI-ANTI's own
face names it an escape — yet the engine refused it at the door. Now
it counters: the vanishing goes up mid-stack (duration from a NUMBER
card riding the counteract command), the attacker's 1-in-4 hit roll
happens at resolution, the spell lingers after the exchange, and
ANTI-ANTI cannot pin it. WATERWALL — "Acts as counteraction to
FIREBALL" — now mirrors WALL OF FIRE's waterbolt-stopping role and
counts as a total stop. WALL OF FIRE itself joins the total-stop set
at rev 12 only: stored rev-10/11 games bounced after it and replay so.

While responding, tapping a number card holds it to ride the next
counter (unless a displayed Shieldstone makes the number itself the
counteraction, as before). The automatons learn the trick too: vanish
when the blow is heavy and the shields are spent, and never waste
ANTI-ANTI against an escape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 21:07:47 -04:00
co-authored by Claude Fable 5
parent fb2266b401
commit 4d8584ae44
6 changed files with 141 additions and 16 deletions
+62
View File
@@ -844,3 +844,65 @@ describe("total stops end the exchange (rules rev 10)", () => {
expect(state.players.find((p) => p.id === defender)!.life).toBeLessThan(15);
});
});
describe("escapes and elemental walls as counteractions", () => {
function rigged(attackId: string, defenderCards: { instanceId: string; cardId: string }[]) {
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 12 });
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const atk = giveCard(state, attacker, attackId);
const d = state.players.find((p) => p.id === defender)!;
defenderCards.forEach((c, i) => { d.hand[i] = c; });
state = must(state, attacker, {
type: "cast", instanceId: atk.instanceId, target: { kind: "player", playerId: defender },
});
return { state, attacker, defender, atk };
}
it("invisible vanishes mid-stack: the sustained goes up, the hit needs a 1", () => {
let { state, defender } = rigged("fireball", [
{ instanceId: "invisible#T", cardId: "invisible" },
{ instanceId: "number-3#T", cardId: "number-3" },
]);
state = must(state, defender, {
type: "counteract", instanceId: "invisible#T", numberInstanceIds: ["number-3#T"],
});
const fx = state.sustained.find((s) => s.cardId === "invisible" && s.targetId === defender);
expect(fx).toBeDefined();
expect(fx!.remainingTurns).toBe(3);
// The attached number left the hand with the spell.
const d = state.players.find((p) => p.id === defender)!;
expect(d.hand.some((c) => c.instanceId === "number-3#T")).toBe(false);
});
it("anti-anti cannot pin the vanishing — escape is escape", () => {
let { state, attacker, defender } = rigged("fireball", [
{ instanceId: "invisible#T", cardId: "invisible" },
]);
state = must(state, defender, { type: "counteract", instanceId: "invisible#T" });
const a = state.players.find((p) => p.id === attacker)!;
a.hand[1] = { instanceId: "anti-anti#T", cardId: "anti-anti" };
const refused = applyCommand(state, attacker, { type: "counteract", instanceId: "anti-anti#T" });
expect(refused.ok).toBe(false);
});
it("waterwall meets the fireball and stops it whole", () => {
let { state, attacker, defender } = rigged("fireball", [
{ instanceId: "waterwall#T", cardId: "waterwall" },
]);
const lifeBefore = state.players.find((p) => p.id === defender)!.life;
state = must(state, defender, { type: "counteract", instanceId: "waterwall#T" });
state = must(state, attacker, { type: "pass" });
// A total stop: the attacker's declined answer resolves at once (rev 10+).
expect(state.stack).toBeNull();
expect(state.players.find((p) => p.id === defender)!.life).toBe(lifeBefore);
});
it("waterwall refuses everything that is not a fireball", () => {
let { state, defender } = rigged("dagger", [
{ instanceId: "waterwall#T", cardId: "waterwall" },
]);
const refused = applyCommand(state, defender, { type: "counteract", instanceId: "waterwall#T" });
expect(refused.ok).toBe(false);
});
});