Files
systemic-game-design-course/course/03-death-step/scenarios.md
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

7.2 KiB

Module 3 scenarios: Deferred death

Each heading is a test name. The Rust test lives in tests/rust/test_03.rs and the TypeScript test in tests/ts/test_03.test.ts; both state exactly what is written here. Player 0 is the first player, player 1 the second. Every minion in these tests is summoned for player 0, in the order listed, so the first one listed has the lowest order of play and stands furthest left. Each test calls clearLog() after its setup, so log() holds only the events of the action under test.

Cards used (text from course/catalog.json):

  • Loot Hoarder, 2/1: "Deathrattle: Draw a card."
  • Harvest Golem, 2/3: "Deathrattle: Summon a 2/1 Damaged Golem."
  • Damaged Golem, 2/1, no text. Only Harvest Golem creates it.
  • Unstable Ghoul, 1/3: "Taunt. Deathrattle: Deal 1 damage to all minions." Taunt has no effect in these tests.
  • Armorsmith, 1/4: "Whenever a friendly minion takes damage, gain 1 Armor."
  • Test Reactor (fictional), 1/5: "Whenever a hero takes damage, gain +1 Attack." No hero takes damage in these tests, so it never triggers; it is here to stand on the board.
  • Whirlwind, spell: "Deal 1 damage to ALL minions."
  • Test Filler (fictional), 1/1, no text. Used to fill decks.

A minion is removed from play only in the Death Creation Step, which runs when the Sequence's outermost Phase ends. The step removes every minion at 0 or less Health and every minion marked by a destroy effect, all at once, and records Destroyed{id} for each in order of play. If it removed anything, a Death Phase follows: it records DeathPhase{round} (1 for the first Death Phase of the Sequence, 2 for the next, and so on) and resolves the Deathrattles of the removed minions in order of play. When the Death Phase ends, the step runs again, until it removes nothing.

test_03_removed_only_after_sequence

  • Setup: newGame with a deck of 5 Test Filler for player 0. Summon Loot Hoarder (id H), then Armorsmith (id S).
  • Action: player 0 casts Whirlwind.
  • Assertions:
    • The log is exactly, in this order: SpellCast{card: "Whirlwind"}, Damaged{target: H, amount: 1}, Damaged{target: S, amount: 1}, Triggered{source: S, trigger: "damaged"}, ArmorGained{player: 0, amount: 1}, Triggered{source: S, trigger: "damaged"}, ArmorGained{player: 0, amount: 1}, Destroyed{id: H}, DeathPhase{round: 1}, Triggered{source: H, trigger: "deathrattle"}, Drew{player: 0, id: C}, where C is the only card in hand(0). H is at 0 Health from the first Damaged event, but it is removed only after every reaction to the Whirlwind has resolved.
    • zoneOf(H) is graveyard.
    • board(0) is [S].
    • armor(0) is 2.

test_03_chained_death_phases

  • Setup: an empty game. Summon Harvest Golem (id G), then Unstable Ghoul (id U), then Test Reactor (id R). dealDamage(G, 2), which leaves G at 1 Health.
  • Action: dealDamage(U, 3).
  • Assertions:
    • The log is exactly, in this order: Damaged{target: U, amount: 3}, Destroyed{id: U}, DeathPhase{round: 1}, Triggered{source: U, trigger: "deathrattle"}, Damaged{target: G, amount: 1}, Damaged{target: R, amount: 1}, Destroyed{id: G}, DeathPhase{round: 2}, Triggered{source: G, trigger: "deathrattle"}, Summoned{id: D}, where D is the first id in board(0). The Ghoul's Deathrattle does not hit the Ghoul: it has already left play. Its damage leaves G at 0 Health, and G is removed by the second Death Creation Step, after the first Death Phase has ended.
    • nameOf(D) is "Damaged Golem".
    • board(0) is [D, R]: the Damaged Golem takes the board position G had when it died, left of R, not the right end.
    • zoneOf(U) and zoneOf(G) are graveyard.
    • health(R) is 4.

test_03_simultaneous_removal

  • Setup: newGame with a deck of 5 Test Filler for player 0. Summon Loot Hoarder (id H1), then another Loot Hoarder (id H2).
  • Action: player 0 casts Whirlwind.
  • Assertions:
    • hand(0) has 2 cards; call them C1 and C2, in hand order.
    • The log is exactly, in this order: SpellCast{card: "Whirlwind"}, Damaged{target: H1, amount: 1}, Damaged{target: H2, amount: 1}, Destroyed{id: H1}, Destroyed{id: H2}, DeathPhase{round: 1}, Triggered{source: H1, trigger: "deathrattle"}, Drew{player: 0, id: C1}, Triggered{source: H2, trigger: "deathrattle"}, Drew{player: 0, id: C2}. Both minions leave play before either Deathrattle resolves, and the Deathrattles resolve in order of play, in one Death Phase.
    • zoneOf(H1) and zoneOf(H2) are graveyard, and board(0) is empty.

test_03_destroy_marks_pending

  • Setup: an empty game. Summon Harvest Golem (id G).
  • Action: destroy(G).
  • Assertions:
    • The log is exactly, in this order: Destroyed{id: G}, DeathPhase{round: 1}, Triggered{source: G, trigger: "deathrattle"}, Summoned{id: D}, where D is the only id in board(0). A destroy effect deals no damage and does not remove the minion itself: it marks G, and the Death Creation Step removes it like any other death.
    • health(G) is 3: G was at full Health when it was removed.
    • zoneOf(G) is graveyard.
    • nameOf(D) is "Damaged Golem".

test_03_summon_position_after_simultaneous_deaths

  • Setup: newGame with a deck of 5 Test Filler for player 0. Summon Loot Hoarder (id H), then Harvest Golem (id G), then Test Reactor (id R). dealDamage(G, 2), which leaves G at 1 Health.
  • Action: player 0 casts Whirlwind.
  • Assertions:
    • hand(0) has 1 card; call it C. The log has exactly one Summoned event; call its id D.
    • The log is exactly, in this order: SpellCast{card: "Whirlwind"}, Damaged{target: H, amount: 1}, Damaged{target: G, amount: 1}, Damaged{target: R, amount: 1}, Destroyed{id: H}, Destroyed{id: G}, DeathPhase{round: 1}, Triggered{source: H, trigger: "deathrattle"}, Drew{player: 0, id: C}, Triggered{source: G, trigger: "deathrattle"}, Summoned{id: D}. H and G leave play in the same Death Creation Step.
    • nameOf(D) is "Damaged Golem".
    • board(0) is [D, R]: the Damaged Golem stands where G stood, left of R. G was second from the left when the step began, but H, to its left, left play in the same step, so that board index no longer points at G's place.
    • zoneOf(H) and zoneOf(G) are graveyard.

test_03_simultaneous_summons_keep_board_order

  • Setup: an empty game. Summon Harvest Golem (id A), then another Harvest Golem (id B), then Test Reactor (id R). dealDamage(A, 2) and dealDamage(B, 2), which leave both at 1 Health.
  • Action: player 0 casts Whirlwind.
  • Assertions:
    • The log has exactly two Summoned events; call their ids DA and DB, in log order.
    • The log is exactly, in this order: SpellCast{card: "Whirlwind"}, Damaged{target: A, amount: 1}, Damaged{target: B, amount: 1}, Damaged{target: R, amount: 1}, Destroyed{id: A}, Destroyed{id: B}, DeathPhase{round: 1}, Triggered{source: A, trigger: "deathrattle"}, Summoned{id: DA}, Triggered{source: B, trigger: "deathrattle"}, Summoned{id: DB}. A's Deathrattle resolves first, by order of play, so DA is A's Damaged Golem and DB is B's.
    • nameOf(DA) and nameOf(DB) are "Damaged Golem".
    • board(0) is [DA, DB, R]: the two Damaged Golems stand in the order A and B stood, both left of R.
    • zoneOf(A) and zoneOf(B) are graveyard.