The tear waits for counteractions (rules rev 6)

STRENGTH's card says the grab "would be an attack," but the engine
resolved it instantly while punches opened counteraction windows — the
defender never got their say. At rev 6 the tear rides the stack like
any physical attack: the clutch roll waits out the counters, and only
an unstopped grab still in arm's reach of a still-laden defender gets
its wrestle. A TELEPORT escape now genuinely saves the chest (FULL
SHIELD does not — the card stops spell attacks only, and the grab is
physical). The automatons counter tears at their carried gold by not
being there when the hand closes. Narrowing, so rev-gated: games
pinned at rev 5 keep their instant tears and replay unchanged — all 29
ledgers verify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-27 22:40:12 -04:00
co-authored by Claude Fable 5
parent cbd4d80dea
commit e388691858
6 changed files with 144 additions and 8 deletions
+69
View File
@@ -1196,3 +1196,72 @@ describe("the bent sight-trace shows AROUND THE CORNER's legs", () => {
expect(traced?.bend?.mid).toBeDefined();
});
});
describe("the tear is an attack (rev 6)", () => {
function tearRig(deckRev?: number) {
let { state } = createGame({
playerIds: ["att", "def"], seed: 42, sets: ["basic", "expansion1"],
...(deckRev != null ? { deckRev } : {}),
});
state = toRound2(state);
while (activePlayer(state).id !== "att") {
state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
}
const att = state.players.find((p) => p.id === "att")!;
const def = state.players.find((p) => p.id === "def")!;
def.position = { ...att.position };
const gold = state.treasures.find((t) => t.owner === "att" && t.position)!;
gold.position = null;
gold.carriedBy = "def";
def.carriedTreasureId = gold.id;
state.sustained.push({
id: "fx-t", cardId: "strength", casterId: "att", targetId: "att",
remainingTurns: 3, data: {},
});
return { state, att, def, gold };
}
it("opens a counteraction window; a TELEPORT escape keeps the chest", () => {
let { state, gold } = tearRig();
const r = applyCommand(state, "att", { type: "tearTreasure", targetId: "def" });
if (!r.ok) throw new Error(r.error);
state = r.state;
expect(state.stack?.tearTreasure).toBe(true);
expect(state.stack?.waitingOn).toBe("def");
const def = state.players.find((p) => p.id === "def")!;
const view = boardView(state);
const away = SIDES.map((s) => stepTarget(view, def.position, s))
.find((t) => t.kind === "step")!;
giveCard(state, "def", "teleport", "TP", 0);
state = must(state, "def", { type: "counteract", instanceId: "teleport#TP", params: { cell: (away as { to: Cell }).to } });
state = must(state, "att", { type: "pass" });
if (state.stack) state = must(state, state.stack.waitingOn, { type: "pass" });
expect(state.stack).toBeNull();
expect(state.players.find((p) => p.id === "def")!.carriedTreasureId).toBe(gold.id);
});
it("an uncountered tear wrestles after the window closes", () => {
let { state, gold } = tearRig();
const r = applyCommand(state, "att", { type: "tearTreasure", targetId: "def" });
if (!r.ok) throw new Error(r.error);
state = r.state;
const res = applyCommand(state, "def", { type: "pass" });
if (!res.ok) throw new Error(res.error);
state = res.state;
const rolled = res.events.find((e) => e.type === "dieRolled");
expect(rolled).toBeTruthy();
const kept = rolled!.type === "dieRolled" && rolled!.roll === 1;
const att = state.players.find((p) => p.id === "att")!;
const def = state.players.find((p) => p.id === "def")!;
if (kept) expect(def.carriedTreasureId).toBe(gold.id);
else expect(att.carriedTreasureId).toBe(gold.id);
});
it("rev 5 games keep the instant tear, no stack", () => {
let { state } = tearRig(5);
const r = applyCommand(state, "att", { type: "tearTreasure", targetId: "def" });
if (!r.ok) throw new Error(r.error);
expect(r.state.stack).toBeNull();
expect(r.events.some((e) => e.type === "treasureTorn" || e.type === "tearResisted")).toBe(true);
});
});