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:
co-authored by
Claude Fable 5
parent
4a91d56d24
commit
76207c6264
@@ -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"] });
|
||||
|
||||
Reference in New Issue
Block a user