Fireball burns only the stones in play (rules rev 33)

'Destroys all magical stones an opponent is CARRYING' — the owner's
ruling: a stone hidden in the hand is a secret card, not a carried
stone. Only displayed stones burn; the old all-hand burning also leaked
hidden information by announcing cards nobody knew existed. Earlier
games replay their broader fire. All 42 production ledgers verified
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:19:20 -04:00
co-authored by Claude Fable 5
parent 1cf5d0a68d
commit b67f2a4c8a
4 changed files with 31 additions and 4 deletions
+11 -2
View File
@@ -840,9 +840,18 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
baseDamage: () => 5,
onResolved: (ctx) => {
if (ctx.damageDealt <= 0) return;
const stones = ctx.defender.hand.filter((c) => isMagicStone(c.cardId));
// "Destroys all magical stones an opponent is CARRYING": a stone
// still hidden in the hand is a secret card, not a carried stone —
// only displayed stones burn (rules rev 33; earlier games burned
// the hand's hidden stones too and replay so).
const rev33 = (ctx.state.config.deckRev ?? 1) >= 33;
const stones = ctx.defender.hand.filter((c) =>
isMagicStone(c.cardId) &&
(!rev33 || ctx.defender.displayed.includes(c.instanceId)));
if (stones.length === 0) return;
ctx.defender.hand = ctx.defender.hand.filter((c) => !isMagicStone(c.cardId));
ctx.defender.hand = ctx.defender.hand.filter(
(c) => !stones.some((s) => s.instanceId === c.instanceId),
);
ctx.defender.displayed = ctx.defender.displayed.filter(
(id) => !stones.some((s) => s.instanceId === id),
);
+18
View File
@@ -1065,3 +1065,21 @@ describe("a reflected lightning blast ends the caster's turn (rules rev 28)", ()
expect(state.players.find((p) => p.id === attacker)!.hand.length).toBeGreaterThan(before);
});
});
describe("fireball burns only the stones in play (rules rev 33)", () => {
it("a displayed stone dies; a hidden one stays secret and safe", () => {
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic"], deckRev: 33 });
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const fb = giveCard(state, attacker, "fireball");
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.displayed = ["powerstone#T"]; // only the powerstone is on the table
state = must(state, attacker, { type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: defender } });
state = must(state, defender, { type: "pass" });
const after = state.players.find((p) => p.id === defender)!;
expect(after.hand.some((c) => c.instanceId === "powerstone#T")).toBe(false);
expect(after.hand.some((c) => c.instanceId === "speedstone#T")).toBe(true);
});
});
+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 = 32;
const RULES_REV = 33;
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
+1 -1
View File
@@ -136,7 +136,7 @@ class LocalGame {
seed,
sets: expansion ? ["basic", "expansion1"] : ["basic"],
...(colors ? { colors } : {}),
deckRev: 32,
deckRev: 33,
};
const { state, events } = createGame(config);
for (const e of events) {