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>
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:
newGamewith a deck of 5Test Fillerfor player 0. SummonLoot Hoarder(id H), thenArmorsmith(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 inhand(0). H is at 0 Health from the firstDamagedevent, 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.
- The log is exactly, in this order:
test_03_chained_death_phases
- Setup: an empty game. Summon
Harvest Golem(id G), thenUnstable Ghoul(id U), thenTest 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 inboard(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)andzoneOf(G)are graveyard.health(R)is 4.
- The log is exactly, in this order:
test_03_simultaneous_removal
- Setup:
newGamewith a deck of 5Test Fillerfor player 0. SummonLoot Hoarder(id H1), then anotherLoot 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)andzoneOf(H2)are graveyard, andboard(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 inboard(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".
- The log is exactly, in this order:
test_03_summon_position_after_simultaneous_deaths
- Setup:
newGamewith a deck of 5Test Fillerfor player 0. SummonLoot Hoarder(id H), thenHarvest Golem(id G), thenTest 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 oneSummonedevent; 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)andzoneOf(G)are graveyard.
test_03_simultaneous_summons_keep_board_order
- Setup: an empty game. Summon
Harvest Golem(id A), then anotherHarvest Golem(id B), thenTest Reactor(id R).dealDamage(A, 2)anddealDamage(B, 2), which leave both at 1 Health. - Action: player 0 casts
Whirlwind. - Assertions:
- The log has exactly two
Summonedevents; 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)andnameOf(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)andzoneOf(B)are graveyard.
- The log has exactly two