Stone Dead counts only the stones in play (rules rev 34)

Same reading as fireball's rev 33: 'carrying' means displayed. Counting
hidden hand stones both inflated the damage and broadcast the secret
count in the total. The automatons' estimate (displayed as the visible
floor) is now simply the truth. All production ledgers verified clean
before deploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-19 22:22:45 -04:00
co-authored by Claude Fable 5
parent b67f2a4c8a
commit e8c25ddc19
4 changed files with 33 additions and 4 deletions
+8 -2
View File
@@ -5402,9 +5402,15 @@ function resolveStack(state: GameState, events: GameEvent[]): void {
if (attackId === "fireball" && sustainedOn(state, defender.id, "sticky-web").length > 0) {
base += 2;
}
// STONE DEAD: number x the stones the defender carries.
// STONE DEAD: number x the stones the defender carries — and "carrying"
// means displayed, as with fireball's burning (rules rev 34): a hidden
// stone is a secret card, and counting it would broadcast the secret in
// the damage total. Earlier games counted the whole hand and replay so.
if (attackId === "stone-dead") {
base = (stack.numberValue ?? 1) * defender.hand.filter((c) => isMagicStone(c.cardId)).length;
const inPlay = (state.config.deckRev ?? 1) >= 34
? defender.hand.filter((c) => isMagicStone(c.cardId) && defender.displayed.includes(c.instanceId))
: defender.hand.filter((c) => isMagicStone(c.cardId));
base = (stack.numberValue ?? 1) * inPlay.length;
}
base *= stack.amplifyFactor;
base += stack.powerAttackPoints;
+23
View File
@@ -1083,3 +1083,26 @@ describe("fireball burns only the stones in play (rules rev 33)", () => {
expect(after.hand.some((c) => c.instanceId === "speedstone#T")).toBe(true);
});
});
describe("stone dead counts only the stones in play (rules rev 34)", () => {
it("hidden stones neither add damage nor betray their count", () => {
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"], deckRev: 34 });
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const sd = giveCard(state, attacker, "stone-dead");
giveCard(state, attacker, "number-3", "N", 1);
const d = state.players.find((p) => p.id === defender)!;
d.hand[0] = { instanceId: "powerstone#T", cardId: "powerstone" };
d.hand[1] = { instanceId: "speedstone#T", cardId: "speedstone" };
d.hand[2] = { instanceId: "bloodstone#T", cardId: "bloodstone" };
d.displayed = ["powerstone#T"]; // one on the table, two secret
state = must(state, attacker, {
type: "cast", instanceId: sd.instanceId, numberInstanceIds: ["number-3#N"],
target: { kind: "player", playerId: defender },
});
state = must(state, defender, { type: "pass" });
// 3 x 1 displayed = 3 damage... minus the displayed bloodstone? It is
// hidden, so no soak either: 15 - 3 = 12.
expect(state.players.find((p) => p.id === defender)!.life).toBe(12);
});
});
+1 -1
View File
@@ -54,7 +54,7 @@ export interface Room {
const rooms = new Map<string, Room>();
/** Rules revision new games are dealt under (stored games keep their own). */
const RULES_REV = 33;
const RULES_REV = 34;
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
+1 -1
View File
@@ -136,7 +136,7 @@ class LocalGame {
seed,
sets: expansion ? ["basic", "expansion1"] : ["basic"],
...(colors ? { colors } : {}),
deckRev: 33,
deckRev: 34,
};
const { state, events } = createGame(config);
for (const e of events) {