From dd6ba592208ae656224c7f40976fc08ce8932f41 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sat, 29 Aug 2026 22:23:53 -0400 Subject: [PATCH] Reflected permanent curses return to their caster (rules rev 11) SLOW DEATH, WALKING DEAD, and IDIOT ride no damage or duration pipe, so a FULL REFLECTION used to evaporate them; per the FAQ reflections act on the cast, so the returned curse now opens its stack against the caster like any reflected spell. Marked permanentCurse on the three defs; older ledgers keep the evaporation. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF --- packages/engine/src/game.ts | 17 ++++++++++++++--- packages/engine/test/stones.test.ts | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index 0b2f14a..bfecb2d 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -271,7 +271,9 @@ export interface GameConfig { * corridors on the turn's legal legs (three plus numbers) and spends * them. * Rev 11: a REVERSE against SLOW DEATH turns the whole curse (FAQ) — - * the victim gains a point for every card drawn, permanently. + * the victim gains a point for every card drawn, permanently — and a + * FULL REFLECTION returns a permanent curse (SLOW DEATH, WALKING DEAD, + * IDIOT) onto its caster instead of letting it evaporate. */ deckRev?: number; } @@ -826,6 +828,10 @@ type AttackEffect = { baseDamage: (numberValue: number | null, params: CastParams | null) => number; /** A duration spell: attach a sustained effect on resolution. */ sustains?: boolean; + /** A permanent curse attached by onResolved (SLOW DEATH, WALKING DEAD, + * IDIOT): it rides no damage or duration pipe, so a FULL REFLECTION + * must return it explicitly or the curse would evaporate. */ + permanentCurse?: true; /** Card stays in hand and is displayed rather than discarded (WIZARDBLADE). */ keepInHand?: boolean; validate?: (state: GameState, cmd: Extract, caster?: PlayerState, target?: PlayerState) => string | null; @@ -1720,6 +1726,7 @@ const CARD_EFFECTS: Record kind: "attack", requiresLos: true, baseDamage: () => 0, + permanentCurse: true, // "This is permanent. Once SLOW DEATH is on, it can't be turned off." // Rev 11, per the FAQ: "If SLOW DEATH is REVERSED, the player receives // a point for every card drawn" — the whole curse attaches backward, @@ -2210,6 +2217,7 @@ const CARD_EFFECTS: Record kind: "attack", requiresLos: true, baseDamage: () => 0, + permanentCurse: true, // "1/2 point of damage for every space moved. This spell is permanent." onResolved: (ctx) => { if (ctx.fullyStopped || !ctx.defender.alive) return; @@ -2554,6 +2562,7 @@ const CARD_EFFECTS: Record kind: "attack", requiresLos: true, baseDamage: () => 0, + permanentCurse: true, // "Opponent heads straight for the nearest of his own treasures ... This // lasts until opponent is on his own treasure." sustains: false, @@ -6043,9 +6052,11 @@ function resolveStack(state: GameState, events: GameEvent[]): void { return; } // The returned spell is a fresh attack on its own caster — who gets a - // defender's counteraction window against it. + // defender's counteraction window against it. Rev 11: permanent curses + // ride the return too; older games let a reflected curse evaporate. + const returnsCurse = effect?.permanentCurse === true && (state.config.deckRev ?? 1) >= 11; if (!stack.trapped && !attackingCreature && - attacker.alive && (pipe.damage > 0 || pipe.duration > 0)) { + attacker.alive && (pipe.damage > 0 || pipe.duration > 0 || returnsCurse)) { state.stack = { attackerId: defender.id, defenderId: attacker.id, diff --git a/packages/engine/test/stones.test.ts b/packages/engine/test/stones.test.ts index f063770..0091b74 100644 --- a/packages/engine/test/stones.test.ts +++ b/packages/engine/test/stones.test.ts @@ -170,6 +170,26 @@ describe("slow death", () => { expect(state.players.find((p) => p.id === defender)!.life).toBe(17); }); + it("a full reflection returns the curse onto its caster (rev 11)", () => { + let { state } = newGame(); + state = toRound2(state); + const { attacker, defender } = faceOff(state); + const sd = giveCard(state, attacker, "slow-death"); + giveCard(state, defender, "full-reflection", "FR", 0); + const r = applyCommand(state, attacker, { + type: "cast", instanceId: sd.instanceId, target: { kind: "player", playerId: defender }, + }); + if (!r.ok) throw new Error(r.error); + state = must(r.state, defender, { type: "counteract", instanceId: "full-reflection#FR" }); + state = drain(state); + expect(sustainedOn(state, defender, "slow-death").length).toBe(0); + expect(sustainedOn(state, attacker, "slow-death").length).toBe(1); + + // The caster now bleeds for their own draws. + state = must(state, attacker, { type: "endTurn", draw: 1 }); + expect(state.players.find((p) => p.id === attacker)!.life).toBe(14); + }); + it("older decks attach the biting curse through a reverse (rev ≤10)", () => { let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 10 }); state = toRound2(state);