Files
systemic-game-design-course/course/04-interceptors/tests/ts/test_04.test.ts
T
Lucas WintherandClaude Opus 5.5 372c99c306 feat: replacement effects in Module 4 (Armor absorption, Bolf Ramshield redirect)
Applying a Damage to one target now runs DAMAGE_STEPS = [redirect, armor]
first. A damageRedirect aura (Bolf Ramshield, fetched from the wiki) sends
damage meant for its owner's hero to the oldest such minion; Armor absorbs
damage to a hero before Health, logged as the new ArmorLost{player, amount}
just before a full-amount Damaged. Four new Module 4 tests; propagated to the
references of Modules 4-10 in both tracks; card schema, catalog, card JSON,
harness.md, Lessons 2, 4 and 5 updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 10:44:06 +01:00

213 lines
6.5 KiB
TypeScript

// Module 4: Interceptors and "trigger twice". 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_04_baron_doubles", () => {
const g = newGame({ decks: [fillers(5), []] });
const b = g.summon("Baron Rivendare", 0);
const h = g.summon("Loot Hoarder", 0);
const golem = g.summon("Harvest Golem", 0);
g.dealDamage(golem, 2);
g.clearLog();
g.cast("Whirlwind", 0);
const hand = g.hand(0);
expect(hand).toHaveLength(2);
const [c1, c2] = hand;
const summoned = g.log().flatMap((e) => (e.type === "Summoned" ? [e.id] : []));
expect(summoned).toHaveLength(2);
const [d1, d2] = summoned;
expect(g.log()).toEqual([
{ type: "SpellCast", card: "Whirlwind" },
{ type: "Damaged", target: b, amount: 1 },
{ type: "Damaged", target: h, amount: 1 },
{ type: "Damaged", target: golem, 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: c1 },
{ type: "Triggered", source: h, trigger: "deathrattle" },
{ type: "Drew", player: 0, id: c2 },
{ type: "Triggered", source: golem, trigger: "deathrattle" },
{ type: "Summoned", id: d1 },
{ type: "Triggered", source: golem, trigger: "deathrattle" },
{ type: "Summoned", id: d2 },
]);
expect(g.nameOf(d1)).toBe("Damaged Golem");
expect(g.nameOf(d2)).toBe("Damaged Golem");
const board = g.board(0);
expect(board).toHaveLength(3);
expect(board[0]).toBe(b);
expect([board[1], board[2]].sort((x, y) => x - y)).toEqual([d1, d2].sort((x, y) => x - y));
expect(g.health(b)).toBe(6);
});
test("test_04_simultaneous_death_not_doubled", () => {
const g = newGame({ decks: [fillers(5), []] });
const b = g.summon("Baron Rivendare", 0);
const h = g.summon("Loot Hoarder", 0);
g.clearLog();
g.cast("Twisting Nether", 0);
expect(g.hand(0)).toHaveLength(1);
const triggers = g.log().filter((e) => e.type === "Triggered" && e.source === h && e.trigger === "deathrattle");
expect(triggers).toHaveLength(1);
expect(g.zoneOf(b)).toBe("graveyard");
expect(g.zoneOf(h)).toBe("graveyard");
});
test("test_04_two_barons_do_not_stack", () => {
const g = newGame({ decks: [fillers(5), []] });
g.summon("Baron Rivendare", 0);
g.summon("Baron Rivendare", 0);
const h = g.summon("Loot Hoarder", 0);
g.clearLog();
g.dealDamage(h, 1);
const hand = g.hand(0);
expect(hand).toHaveLength(2);
const [c1, c2] = hand;
expect(g.log()).toEqual([
{ type: "Damaged", target: h, amount: 1 },
{ type: "Destroyed", id: h },
{ type: "DeathPhase", round: 1 },
{ type: "Triggered", source: h, trigger: "deathrattle" },
{ type: "Drew", player: 0, id: c1 },
{ type: "Triggered", source: h, trigger: "deathrattle" },
{ type: "Drew", player: 0, id: c2 },
]);
});
test("test_04_aura_timing_order", () => {
const g = newGame({ decks: [fillers(5), []] });
const h = g.summon("Loot Hoarder", 0);
const b = g.summon("Baron Rivendare", 0);
g.clearLog();
g.cast("Twisting Nether", 0);
const hand = g.hand(0);
expect(hand).toHaveLength(1);
const c = hand[0];
expect(g.log()).toEqual([
{ type: "SpellCast", card: "Twisting Nether" },
{ type: "Destroyed", id: h },
{ type: "Destroyed", id: b },
{ type: "DeathPhase", round: 1 },
{ type: "Triggered", source: h, trigger: "deathrattle" },
{ type: "Drew", player: 0, id: c },
]);
});
test("test_04_battlecry_on_play", () => {
const g = newGame({ decks: [fillers(5), []] });
const e = g.addToHand("Novice Engineer", 0);
g.clearLog();
g.play(e);
const hand = g.hand(0);
expect(hand).toHaveLength(1);
const c = hand[0];
expect(g.log()).toEqual([
{ type: "Summoned", id: e },
{ type: "Triggered", source: e, trigger: "battlecry" },
{ type: "Drew", player: 0, id: c },
]);
expect(g.zoneOf(e)).toBe("board");
expect(g.board(0)).toEqual([e]);
});
test("test_04_armor_absorbs_before_health", () => {
const g = newGame({ decks: [fillers(5), []] });
const a = g.summon("Armorsmith", 0);
const r = g.summon("Test Reactor", 0);
g.dealDamage(a, 1);
g.dealDamage(a, 1);
const hero = g.hero(0);
expect(g.armor(0)).toBe(2);
g.clearLog();
g.dealDamage(hero, 1);
expect(g.log()).toEqual([
{ type: "ArmorLost", player: 0, amount: 1 },
{ type: "Damaged", target: hero, amount: 1 },
{ type: "Triggered", source: r, trigger: "damaged" },
{ type: "StatsChanged", id: r, attack: 2, health: 5 },
]);
expect(g.armor(0)).toBe(1);
expect(g.health(hero)).toBe(30);
g.clearLog();
g.dealDamage(hero, 5);
expect(g.log()).toEqual([
{ type: "ArmorLost", player: 0, amount: 1 },
{ type: "Damaged", target: hero, amount: 5 },
{ type: "Triggered", source: r, trigger: "damaged" },
{ type: "StatsChanged", id: r, attack: 3, health: 5 },
]);
expect(g.armor(0)).toBe(0);
expect(g.health(hero)).toBe(26);
});
test("test_04_redirect_replaces_hero_damage", () => {
const g = newGame({ decks: [fillers(5), []] });
const b = g.summon("Bolf Ramshield", 0);
const a = g.summon("Armorsmith", 0);
g.summon("Test Reactor", 0);
const hero = g.hero(0);
g.clearLog();
g.dealDamage(hero, 4);
expect(g.log()).toEqual([
{ type: "Damaged", target: b, amount: 4 },
{ type: "Triggered", source: a, trigger: "damaged" },
{ type: "ArmorGained", player: 0, amount: 1 },
]);
expect(g.health(hero)).toBe(30);
expect(g.health(b)).toBe(5);
expect(g.armor(0)).toBe(1);
});
test("test_04_redirect_applies_once", () => {
const g = newGame({ decks: [fillers(5), []] });
const b1 = g.summon("Bolf Ramshield", 0);
const b2 = g.summon("Bolf Ramshield", 0);
const hero = g.hero(0);
g.clearLog();
g.dealDamage(hero, 3);
expect(g.log()).toEqual([{ type: "Damaged", target: b1, amount: 3 }]);
expect(g.health(b1)).toBe(6);
expect(g.health(b2)).toBe(9);
expect(g.health(hero)).toBe(30);
});
test("test_04_redirect_before_armor", () => {
const g = newGame({ decks: [fillers(5), []] });
const a = g.summon("Armorsmith", 0);
g.dealDamage(a, 1);
g.destroy(a);
const b = g.summon("Bolf Ramshield", 0);
const hero = g.hero(0);
expect(g.armor(0)).toBe(1);
g.clearLog();
g.dealDamage(hero, 2);
expect(g.log()).toEqual([{ type: "Damaged", target: b, amount: 2 }]);
expect(g.armor(0)).toBe(1);
expect(g.health(hero)).toBe(30);
expect(g.health(b)).toBe(7);
});