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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-29 22:23:53 -04:00
co-authored by Claude Fable 5
parent e885325098
commit dd6ba59220
2 changed files with 34 additions and 3 deletions
+14 -3
View File
@@ -271,7 +271,9 @@ export interface GameConfig {
* corridors on the turn's legal legs (three plus numbers) and spends * corridors on the turn's legal legs (three plus numbers) and spends
* them. * them.
* Rev 11: a REVERSE against SLOW DEATH turns the whole curse (FAQ) * 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; deckRev?: number;
} }
@@ -826,6 +828,10 @@ type AttackEffect = {
baseDamage: (numberValue: number | null, params: CastParams | null) => number; baseDamage: (numberValue: number | null, params: CastParams | null) => number;
/** A duration spell: attach a sustained effect on resolution. */ /** A duration spell: attach a sustained effect on resolution. */
sustains?: boolean; 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). */ /** Card stays in hand and is displayed rather than discarded (WIZARDBLADE). */
keepInHand?: boolean; keepInHand?: boolean;
validate?: (state: GameState, cmd: Extract<Command, { type: "cast" }>, caster?: PlayerState, target?: PlayerState) => string | null; validate?: (state: GameState, cmd: Extract<Command, { type: "cast" }>, caster?: PlayerState, target?: PlayerState) => string | null;
@@ -1720,6 +1726,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
kind: "attack", kind: "attack",
requiresLos: true, requiresLos: true,
baseDamage: () => 0, baseDamage: () => 0,
permanentCurse: true,
// "This is permanent. Once SLOW DEATH is on, it can't be turned off." // "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 // Rev 11, per the FAQ: "If SLOW DEATH is REVERSED, the player receives
// a point for every card drawn" — the whole curse attaches backward, // a point for every card drawn" — the whole curse attaches backward,
@@ -2210,6 +2217,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
kind: "attack", kind: "attack",
requiresLos: true, requiresLos: true,
baseDamage: () => 0, baseDamage: () => 0,
permanentCurse: true,
// "1/2 point of damage for every space moved. This spell is permanent." // "1/2 point of damage for every space moved. This spell is permanent."
onResolved: (ctx) => { onResolved: (ctx) => {
if (ctx.fullyStopped || !ctx.defender.alive) return; if (ctx.fullyStopped || !ctx.defender.alive) return;
@@ -2554,6 +2562,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
kind: "attack", kind: "attack",
requiresLos: true, requiresLos: true,
baseDamage: () => 0, baseDamage: () => 0,
permanentCurse: true,
// "Opponent heads straight for the nearest of his own treasures ... This // "Opponent heads straight for the nearest of his own treasures ... This
// lasts until opponent is on his own treasure." // lasts until opponent is on his own treasure."
sustains: false, sustains: false,
@@ -6043,9 +6052,11 @@ function resolveStack(state: GameState, events: GameEvent[]): void {
return; return;
} }
// The returned spell is a fresh attack on its own caster — who gets a // 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 && if (!stack.trapped && !attackingCreature &&
attacker.alive && (pipe.damage > 0 || pipe.duration > 0)) { attacker.alive && (pipe.damage > 0 || pipe.duration > 0 || returnsCurse)) {
state.stack = { state.stack = {
attackerId: defender.id, attackerId: defender.id,
defenderId: attacker.id, defenderId: attacker.id,
+20
View File
@@ -170,6 +170,26 @@ describe("slow death", () => {
expect(state.players.find((p) => p.id === defender)!.life).toBe(17); 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)", () => { it("older decks attach the biting curse through a reverse (rev ≤10)", () => {
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 10 }); let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 10 });
state = toRound2(state); state = toRound2(state);