Engine events carry the nesting depth they were recorded at; the harness prints the log as an indented tree. New tests test_02_log_tree_shows_nesting and test_03_log_tree_death_phase; both SpellGrid CLIs print the tree of the last command's Sequences under a Log: line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
210 lines
6.8 KiB
TypeScript
210 lines
6.8 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_log_tree_death_phase", () => {
|
|
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.logTree()).toBe(
|
|
[
|
|
`Damaged{target: ${ghoul}, amount: 3}`,
|
|
`Destroyed{id: ${ghoul}}`,
|
|
`DeathPhase{round: 1}`,
|
|
`Triggered{source: ${ghoul}, trigger: "deathrattle"}`,
|
|
` Damaged{target: ${golem}, amount: 1}`,
|
|
` Damaged{target: ${r}, amount: 1}`,
|
|
`Destroyed{id: ${golem}}`,
|
|
`DeathPhase{round: 2}`,
|
|
`Triggered{source: ${golem}, trigger: "deathrattle"}`,
|
|
` Summoned{id: ${d}}`,
|
|
].join("\n") + "\n",
|
|
);
|
|
});
|
|
|
|
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");
|
|
});
|