Strength gets its grip: tear a treasure from a wizard's arms

The card's second power was never built. New tearTreasure command: with
STRENGTH on you, in the same square as a carrier, the tear spends your
attack; the victim keeps the treasure only on a D4 roll of 1. Per the
FAQ it is not a pickup (your turn's actions continue), and full or
weakened arms tear it loose onto the floor instead. A torn treasure's
warded owner gets their window, blind grabs flail like blind punches,
and the automatons wrestle too. A new command breaks no stored ledger,
so live games gain the grip at once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-21 13:44:50 -04:00
co-authored by Claude Fable 5
parent 4a91d56d24
commit 76207c6264
6 changed files with 211 additions and 0 deletions
@@ -394,6 +394,105 @@ describe("ambushes (async interrupts)", () => {
});
});
describe("strength tears treasures from wizards' arms", () => {
function grip(seed: number) {
let { state } = createGame({ playerIds: ["alice", "bob"], seed, sets: ["basic", "expansion1"] });
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const d = state.players.find((p) => p.id === defender)!;
// The defender carries the ATTACKER's own treasure (no ward window).
const stolen = state.treasures.find((t) => t.owner === attacker)!;
stolen.carriedBy = defender;
stolen.position = null;
d.carriedTreasureId = stolen.id;
const st = giveCard(state, attacker, "strength");
giveCard(state, attacker, "number-2", "N", 1);
state = must(state, attacker, {
type: "cast", instanceId: st.instanceId, numberInstanceIds: ["number-2#N"],
});
return { state, attacker, defender, treasure: stolen.id };
}
it("the grip is an attack: the victim keeps the treasure only on a 1", () => {
let torn = 0, kept = 0;
for (let seed = 1; seed <= 12; seed++) {
const { state, attacker, defender, treasure } = grip(seed);
const r = applyCommand(state, attacker, { type: "tearTreasure", targetId: defender });
if (!r.ok) throw new Error(r.error);
expect(r.state.turn.attackUsed).toBe(true);
// FAQ: tearing is not picking up — the turn's actions continue.
expect(r.state.turn.actionsEnded).toBe(false);
const a = r.state.players.find((p) => p.id === attacker)!;
const d = r.state.players.find((p) => p.id === defender)!;
if (a.carriedTreasureId === treasure) {
torn++;
expect(d.carriedTreasureId).toBeNull();
} else {
kept++;
expect(d.carriedTreasureId).toBe(treasure);
}
// One attack per turn: a second wrench is refused.
expect(applyCommand(r.state, attacker, { type: "tearTreasure", targetId: defender }).ok).toBe(false);
}
expect(torn + kept).toBe(12);
expect(torn).toBeGreaterThan(0);
expect(kept).toBeGreaterThan(0);
});
it("without STRENGTH the grip is refused", () => {
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"] });
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const d = state.players.find((p) => p.id === defender)!;
const t = state.treasures.find((t) => t.owner === attacker)!;
t.carriedBy = defender;
t.position = null;
d.carriedTreasureId = t.id;
const r = applyCommand(state, attacker, { type: "tearTreasure", targetId: defender });
expect(r.ok).toBe(false);
});
it("laden arms tear it loose onto the floor — and the ward's owner gets their window", () => {
for (let seed = 1; seed <= 12; seed++) {
let { state } = createGame({ playerIds: ["alice", "bob", "cara"], seed, sets: ["basic", "expansion1"] });
state = toRound2(state);
while (activePlayer(state).id !== "alice") {
state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
}
const alice = state.players.find((p) => p.id === "alice")!;
const bob = state.players.find((p) => p.id === "bob")!;
bob.position = { ...alice.position };
// Alice already hauls her own gold; Bob carries CARA's — and Cara
// holds a WARD over it.
const mine = state.treasures.find((t) => t.owner === "alice")!;
mine.carriedBy = "alice";
mine.position = null;
alice.carriedTreasureId = mine.id;
const caras = state.treasures.find((t) => t.owner === "cara")!;
caras.carriedBy = "bob";
caras.position = null;
bob.carriedTreasureId = caras.id;
giveCard(state, "cara", "ward", "W", 0);
const st = giveCard(state, "alice", "strength");
giveCard(state, "alice", "number-2", "N", 1);
state = must(state, "alice", {
type: "cast", instanceId: st.instanceId, numberInstanceIds: ["number-2#N"],
});
const r = applyCommand(state, "alice", { type: "tearTreasure", targetId: "bob" });
if (!r.ok) throw new Error(r.error);
const after = r.state.treasures.find((t) => t.id === caras.id)!;
if (after.carriedBy === "bob") continue; // Bob rolled his 1 — try again
// Torn loose: Alice's arms are full, so it lands at her feet…
expect(after.position).toEqual(alice.position);
expect(r.state.players.find((p) => p.id === "alice")!.carriedTreasureId).toBe(mine.id);
// …and the seizure hangs on Cara's ward.
expect(r.state.wardPending).toEqual({ ownerId: "cara", takerId: "alice" });
return;
}
throw new Error("every seed rolled a 1 — the rig is wrong");
});
});
describe("swap meet trades carried items", () => {
function rig() {
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"] });