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
+6
View File
@@ -1239,6 +1239,12 @@ export function automatonCommand(
if (spell) return spell.cmd;
const misery = tier.afflictions ? bestAffliction(view, target.id, thief?.id === target.id) : null;
if (misery) return misery;
// STRENGTH in force and the treasure in reach: wrest it from their
// arms — worth the attack even for the timid.
if (cellKey(target.position) === here && target.carriedTreasureId &&
view.sustained.some((e) => e.cardId === "strength" && e.targetId === view.you)) {
return { type: "tearTreasure", targetId: target.id };
}
if (cellKey(target.position) === here && style !== "worrier") {
return { type: "punch", targetId: target.id };
}
+88
View File
@@ -605,6 +605,8 @@ export type GameEvent =
| { type: "handTaken"; from: PlayerId; to: PlayerId; count: number }
| { type: "handTakenPrivate"; visibleTo: PlayerId; cards: CardInstance[] }
| { type: "treasurePickedUp"; player: PlayerId; treasureId: string; owner: PlayerId; at: Cell }
| { type: "treasureTorn"; attacker: PlayerId; defender: PlayerId; treasureId: string; toFloor: boolean }
| { type: "tearResisted"; attacker: PlayerId; defender: PlayerId }
| { type: "treasureDropped"; player: PlayerId; treasureId: string; at: Cell; onHomeOf: PlayerId | null }
| { type: "playerEliminated"; player: PlayerId; reason: "killed" | "treasuresLost" }
| { type: "cardsDiscarded"; player: PlayerId; cards: CardInstance[] }
@@ -632,6 +634,7 @@ export type Command =
| { type: "move"; direction: Side; over?: boolean }
| { type: "playNumberForMovement"; instanceId: string; addInstanceId?: string }
| { type: "punch"; targetId: PlayerId }
| { type: "tearTreasure"; targetId: PlayerId }
| { type: "punchWall"; cell: Cell; side: Side }
| { type: "testIllusion"; cell: Cell; side: Side }
| { type: "armWard" }
@@ -3659,6 +3662,7 @@ function applyCommandInner(state: GameState, playerId: PlayerId, command: Comman
case "move": return doMove(state, command.direction, command.over === true);
case "playNumberForMovement": return doPlayNumberForMovement(state, command.instanceId, command.addInstanceId);
case "punch": return doPunch(state, command.targetId);
case "tearTreasure": return doTearTreasure(state, command.targetId);
case "punchWall": return doPunchWall(state, command.cell, command.side);
case "testIllusion": return doTestIllusion(state, command.cell, command.side);
case "armWard": return doArmWard();
@@ -4325,6 +4329,90 @@ function doTestIllusion(prev: GameState, cell: Cell, side: Side): CommandResult
return { ok: true, state, events };
}
/**
* STRENGTH's grip: "physically tear a treasure out of the grasp of another
* player if you occupy the same space (this would be an attack). The other
* player must roll a 1 on the D4 to retain the treasure." The struggle is
* bodily the defender's D4 is the defense, not a counteraction window.
* FAQ: tearing is not picking up an object (the turn's actions continue),
* and a wizard whose arms are full or too weak to carry tears it loose
* onto the floor instead.
*/
function doTearTreasure(prev: GameState, targetId: PlayerId): CommandResult {
const pre = attackPreconditions(prev) ?? idiotBlocked(prev, activePlayer(prev).id);
if (pre) return err(pre);
const state = clone(prev);
const attacker = activePlayer(state);
if (sustainedOn(state, attacker.id, "strength").length === 0) {
return err("only STRENGTH lets you tear a treasure from a wizard's grasp");
}
if (targetId === attacker.id) return err("you cannot attack yourself");
const target = state.players.find((p) => p.id === targetId);
if (!target || !target.alive) return err("no such living player");
if (cellKey(target.position) !== cellKey(attacker.position)) {
return err("you must be in the same square to tear it away");
}
if (!target.carriedTreasureId) return err("they carry no treasure");
const bushOrMist = attackBlockedByStatus(state, attacker, target);
if (bushOrMist) return err(bushOrMist);
state.sustained = state.sustained.filter(
(s) => !(s.cardId === "buddy" && s.casterId === attacker.id && s.targetId === target.id),
);
const events: GameEvent[] = [];
// BLIND: same convention as a punch — the grab finds its grip on a 1.
if (isBlinded(state, attacker)) {
const roll = rollD4(state, events, attacker.id, "a blind grab — only a 1 finds the grip");
if (roll !== 1) {
state.turn.attackUsed = true;
events.push({
type: "attackMisdirected", attacker: attacker.id, intended: target.id,
rolledDirection: SIDES[roll - 1]!, newTarget: null,
});
return { ok: true, state, events };
}
}
if (state.turn.attackUsed) state.turn.secondAttackUsed = true;
state.turn.attackUsed = true;
const roll = rollD4(state, events, target.id, "clutching the treasure — a 1 keeps it");
if (roll === 1) {
events.push({ type: "tearResisted", attacker: attacker.id, defender: target.id });
return { ok: true, state, events };
}
const t = state.treasures.find((t) => t.id === target.carriedTreasureId)!;
target.carriedTreasureId = null;
const laden = attacker.carriedTreasureId != null ||
sustainedOn(state, attacker.id, "weakness").length > 0;
if (laden) {
t.carriedBy = null;
t.position = { ...attacker.position };
events.push({ type: "treasureTorn", attacker: attacker.id, defender: target.id, treasureId: t.id, toFloor: true });
events.push({
type: "treasureDropped", player: attacker.id, treasureId: t.id,
at: attacker.position, onHomeOf: homeOwnerAt(state, attacker.position),
});
} else {
t.carriedBy = attacker.id;
t.position = null;
attacker.carriedTreasureId = t.id;
events.push({ type: "treasureTorn", attacker: attacker.id, defender: target.id, treasureId: t.id, toFloor: false });
}
// WARD guards the treasure against ANY seizure: its owner's window opens.
const owner = state.players.find((q) => q.id === t.owner);
if (owner && owner.alive && owner.id !== attacker.id) {
if (owner.hand.some((c) => c.cardId === "ward")) {
state.wardPending = { ownerId: owner.id, takerId: attacker.id };
events.push({ type: "wardWindow", owner: owner.id, victim: attacker.id });
}
}
checkVictory(state, events);
return { ok: true, state, events };
}
function doPunchWall(prev: GameState, cell: Cell, side: Side): CommandResult {
const pre = attackPreconditions(prev);
if (pre) return err(pre);