Files
systemic-game-design-course/course/03-death-step/tests/ts/test_03.test.ts
T
Lucas WintherandClaude Fable 5.2 429137a6ef fix(reference): Death Events remember a right-hand neighbour, not a board index
A Deathrattle summon used the dying minion's board index from before the
Death Creation Step removed anything. That index is stale when a minion to
its left dies in the same step, so the token landed one place too far right.

Each Death Event now records `before`: the nearest minion to the right that
is not dying in the same step. The position handed to the effect context is
resolved when the trigger resolves: the current index of `before`, or the
right end. Applied to the Rust and TS references of Modules 3-10.

Module 3 gains two tests, documented in scenarios.md:
test_03_summon_position_after_simultaneous_deaths (fails before the fix) and
test_03_simultaneous_summons_keep_board_order.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 09:15:48 +01:00

183 lines
6.0 KiB
TypeScript

// Module 3: Deferred death. Each test matches a heading in scenarios.md.
import { expect, test } from "vitest";
import { newGame } from "../src/harness";
const fillers = (n: number): string[] => Array(n).fill("Test Filler");
test("test_03_removed_only_after_sequence", () => {
const g = newGame({ decks: [fillers(5), []] });
const h = g.summon("Loot Hoarder", 0);
const s = g.summon("Armorsmith", 0);
g.clearLog();
g.cast("Whirlwind", 0);
const hand = g.hand(0);
expect(hand).toHaveLength(1);
const c = hand[0];
expect(g.log()).toEqual([
{ type: "SpellCast", card: "Whirlwind" },
{ type: "Damaged", target: h, amount: 1 },
{ type: "Damaged", target: s, amount: 1 },
{ type: "Triggered", source: s, trigger: "damaged" },
{ type: "ArmorGained", player: 0, amount: 1 },
{ type: "Triggered", source: s, trigger: "damaged" },
{ type: "ArmorGained", player: 0, amount: 1 },
{ type: "Destroyed", id: h },
{ type: "DeathPhase", round: 1 },
{ type: "Triggered", source: h, trigger: "deathrattle" },
{ type: "Drew", player: 0, id: c },
]);
expect(g.zoneOf(h)).toBe("graveyard");
expect(g.board(0)).toEqual([s]);
expect(g.armor(0)).toBe(2);
});
test("test_03_chained_death_phases", () => {
const g = newGame();
const golem = g.summon("Harvest Golem", 0);
const ghoul = g.summon("Unstable Ghoul", 0);
const r = g.summon("Test Reactor", 0);
g.dealDamage(golem, 2);
g.clearLog();
g.dealDamage(ghoul, 3);
const d = g.board(0)[0];
expect(g.log()).toEqual([
{ type: "Damaged", target: ghoul, amount: 3 },
{ type: "Destroyed", id: ghoul },
{ type: "DeathPhase", round: 1 },
{ type: "Triggered", source: ghoul, trigger: "deathrattle" },
{ type: "Damaged", target: golem, amount: 1 },
{ type: "Damaged", target: r, amount: 1 },
{ type: "Destroyed", id: golem },
{ type: "DeathPhase", round: 2 },
{ type: "Triggered", source: golem, trigger: "deathrattle" },
{ type: "Summoned", id: d },
]);
expect(g.nameOf(d)).toBe("Damaged Golem");
expect(g.board(0)).toEqual([d, r]);
expect(g.zoneOf(ghoul)).toBe("graveyard");
expect(g.zoneOf(golem)).toBe("graveyard");
expect(g.health(r)).toBe(4);
});
test("test_03_simultaneous_removal", () => {
const g = newGame({ decks: [fillers(5), []] });
const h1 = g.summon("Loot Hoarder", 0);
const h2 = g.summon("Loot Hoarder", 0);
g.clearLog();
g.cast("Whirlwind", 0);
const hand = g.hand(0);
expect(hand).toHaveLength(2);
const [c1, c2] = hand;
expect(g.log()).toEqual([
{ type: "SpellCast", card: "Whirlwind" },
{ type: "Damaged", target: h1, amount: 1 },
{ type: "Damaged", target: h2, amount: 1 },
{ type: "Destroyed", id: h1 },
{ type: "Destroyed", id: h2 },
{ type: "DeathPhase", round: 1 },
{ type: "Triggered", source: h1, trigger: "deathrattle" },
{ type: "Drew", player: 0, id: c1 },
{ type: "Triggered", source: h2, trigger: "deathrattle" },
{ type: "Drew", player: 0, id: c2 },
]);
expect(g.zoneOf(h1)).toBe("graveyard");
expect(g.zoneOf(h2)).toBe("graveyard");
expect(g.board(0)).toEqual([]);
});
test("test_03_destroy_marks_pending", () => {
const g = newGame();
const golem = g.summon("Harvest Golem", 0);
g.clearLog();
g.destroy(golem);
const board = g.board(0);
expect(board).toHaveLength(1);
const d = board[0];
expect(g.log()).toEqual([
{ type: "Destroyed", id: golem },
{ type: "DeathPhase", round: 1 },
{ type: "Triggered", source: golem, trigger: "deathrattle" },
{ type: "Summoned", id: d },
]);
expect(g.health(golem)).toBe(3);
expect(g.zoneOf(golem)).toBe("graveyard");
expect(g.nameOf(d)).toBe("Damaged Golem");
});
test("test_03_summon_position_after_simultaneous_deaths", () => {
const g = newGame({ decks: [fillers(5), []] });
const h = g.summon("Loot Hoarder", 0);
const golem = g.summon("Harvest Golem", 0);
const r = g.summon("Test Reactor", 0);
g.dealDamage(golem, 2);
g.clearLog();
g.cast("Whirlwind", 0);
const hand = g.hand(0);
expect(hand).toHaveLength(1);
const c = hand[0];
const summoned = g.log().flatMap((e) => (e.type === "Summoned" ? [e.id] : []));
expect(summoned).toHaveLength(1);
const d = summoned[0];
expect(g.log()).toEqual([
{ type: "SpellCast", card: "Whirlwind" },
{ type: "Damaged", target: h, amount: 1 },
{ type: "Damaged", target: golem, amount: 1 },
{ type: "Damaged", target: r, amount: 1 },
{ type: "Destroyed", id: h },
{ type: "Destroyed", id: golem },
{ type: "DeathPhase", round: 1 },
{ type: "Triggered", source: h, trigger: "deathrattle" },
{ type: "Drew", player: 0, id: c },
{ type: "Triggered", source: golem, trigger: "deathrattle" },
{ type: "Summoned", id: d },
]);
expect(g.nameOf(d)).toBe("Damaged Golem");
expect(g.board(0)).toEqual([d, r]);
expect(g.zoneOf(h)).toBe("graveyard");
expect(g.zoneOf(golem)).toBe("graveyard");
});
test("test_03_simultaneous_summons_keep_board_order", () => {
const g = newGame();
const a = g.summon("Harvest Golem", 0);
const b = g.summon("Harvest Golem", 0);
const r = g.summon("Test Reactor", 0);
g.dealDamage(a, 2);
g.dealDamage(b, 2);
g.clearLog();
g.cast("Whirlwind", 0);
const summoned = g.log().flatMap((e) => (e.type === "Summoned" ? [e.id] : []));
expect(summoned).toHaveLength(2);
const [da, db] = summoned;
expect(g.log()).toEqual([
{ type: "SpellCast", card: "Whirlwind" },
{ type: "Damaged", target: a, amount: 1 },
{ type: "Damaged", target: b, amount: 1 },
{ type: "Damaged", target: r, amount: 1 },
{ type: "Destroyed", id: a },
{ type: "Destroyed", id: b },
{ type: "DeathPhase", round: 1 },
{ type: "Triggered", source: a, trigger: "deathrattle" },
{ type: "Summoned", id: da },
{ type: "Triggered", source: b, trigger: "deathrattle" },
{ type: "Summoned", id: db },
]);
expect(g.nameOf(da)).toBe("Damaged Golem");
expect(g.nameOf(db)).toBe("Damaged Golem");
expect(g.board(0)).toEqual([da, db, r]);
expect(g.zoneOf(a)).toBe("graveyard");
expect(g.zoneOf(b)).toBe("graveyard");
});