Files
systemic-game-design-course/course/03-death-step/lesson.md
T
Lucas WintherandClaude Opus 5.5 fb9d0a4eb4 feat: Module 12, Combat (attack as one Sequence, Taunt, defender-first as a queue priority, end of turn; Bonechewer Brawler as the Expansion Drop)
Adds course/12-combat: lesson, scenarios, nine test_12 tests in both tracks,
the cumulative reference (Module 11's plus combat) and the drop. The Damage
action carries per-target hits with a priority; react stable-sorts its Queue by
priority before order of play, so the triggers answering the defender's Damage
Event go first. orderAttack/canAttack/endTurn join the harness (orderAttack
because attack(id) already reads Attack). endOfTurn triggers fire, so Drakkari
Enchanter's data is live. Test Bell is scoped to Module 12's cards. Harness
contract, catalog, research notes and earlier lessons' forward pointers follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:34:20 +01:00

412 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Module 3: Deferred death
At the end of Module 2, a minion at 0 Health stays on the board forever. That was deliberate: it still has to react to the damage that put it there. In this module your engine decides *when* the dead leave play and what happens next. It adds one checkpoint at the end of every Sequence that removes all the dead at once, then resolves their Deathrattles, then checks again.
You need your Module 2 workspace with all Module 1 and Module 2 tests passing, under git.
## Observe
Three cards ([Loot Hoarder](https://hearthstone.wiki.gg/wiki/Loot_Hoarder), [Harvest Golem](https://hearthstone.wiki.gg/wiki/Harvest_Golem), [Unstable Ghoul](https://hearthstone.wiki.gg/wiki/Unstable_Ghoul)):
- Loot Hoarder, 2-mana 2/1: "Deathrattle: Draw a card."
- Harvest Golem, 3-mana 2/3: "Deathrattle: Summon a 2/1 Damaged Golem."
- Unstable Ghoul, 2-mana 1/3: "Taunt. Deathrattle: Deal 1 damage to all minions."
**Damage does not kill on the spot.** Module 2 already used this: a minion at 0 or less Health is *mortally wounded*, and it stays in play and keeps triggering. The rulebook says when that ends: "mortally wounded and pending destroy are ONLY converted into dead once the outermost Phase ends!" Phases nest (every reaction opens one inside the Phase that caused it), "but only the outermost Phase ending initiates the Death Creation Step." ([Advanced rulebook](https://hearthstone.wiki.gg/wiki/Advanced_rulebook)).
**The Death Creation Step removes everyone at once.** It "Looks for all mortally wounded (0 or less Health)/pending destroy (hit with a destroy effect) Entities", then "sends them from play to the graveyard and creates Death Events for each." (same source). *Pending destroy* is the second way to die: a destroy effect such as Twisting Nether doesn't touch Health, it marks the minion, and the step collects it with the rest.
**Then a Death Phase.** "Then if any Deaths occurred, a Death Phase starts, resolving Death Events in order of play." Deathrattles are queued there, oldest first, and resolve depth-first like any Module 2 trigger. A Death Phase is itself an outermost Phase, so when it ends the step runs again: "This continues until there are no new Deaths." (same source).
**Summons remember where the dead stood.** A Deathrattle summon appears "in the board position the Death Event remembered from when it was created." (same source). How an engine should *remember* a position turns out to be the subtle part of this module: see Predict question 5.
The rulebook's own example uses Unstable Ghoul. With three Ghouls dying in one Phase and an Acolyte of Pain on the board, the Acolyte "is hit, triggered and draws three cards" before it dies (same source): every Ghoul's damage lands, every Acolyte trigger resolves, and only then does the next Death Creation Step take the Acolyte.
> **A stale example**
> That example was written for an Acolyte of Pain with 3 Health. The card's page records "Now has 4 health (Up from 3)" in Patch 27.2.0.183876, 22 August 2023 ([Acolyte of Pain](https://hearthstone.wiki.gg/wiki/Acolyte_of_Pain)), and the course uses the current 1/4. Today the Acolyte would draw its three cards and survive. The rule the example shows has not changed; one number in it has. A rules document and the card data it mentions are edited by different people at different times, which is worth remembering whenever you use an example as a specification.
Now a smaller chain. You played Harvest Golem, then Unstable Ghoul. The Golem has taken 2 damage and has 1 Health left. You deal 3 damage to the Ghoul.
## Predict
Write your answers down before you open the discussion, for example in `notes/03.md` in your workspace.
1. Walk through the Golem and Ghoul case event by event. How many Death Phases are there? Where on the board does the Damaged Golem appear? Does the Ghoul's Deathrattle damage the Ghoul?
2. You played Loot Hoarder, then Armorsmith ("Whenever a friendly minion takes damage, gain 1 Armor."), and cast Whirlwind. How much Armor do you gain? When does the Hoarder leave the board: before or after the Armor?
3. Two Loot Hoarders die to the same Whirlwind. Does the first Hoarder's card get drawn before the second Hoarder has left the board? Name a card for which the answer would change the outcome.
4. Your Module 2 engine has one entry point per player action. Where should "remove the dead" live, and why not inside `damage`?
5. You played Loot Hoarder, then Harvest Golem, then Test Reactor (a 1/5), so they stand in that order, left to right. The Golem has 1 Health left. You cast Whirlwind: the Hoarder and the Golem die together. Where does the Damaged Golem appear? Now suppose your engine remembered the Golem's position as "board index 1" when the Death Event was created. Where does *that* put it?
<details><summary>Discussion</summary>
1. Two. Damage the Ghoul (0 Health). The outermost Phase ends; the Death Creation Step removes the Ghoul: Death Phase 1. Its Deathrattle damages all minions, which by now means only the Golem (and anything else still in play). The Ghoul has already left play, so its own Deathrattle doesn't hit it. The Golem drops to 0 but stays: it is only mortally wounded, and this Death Phase is still running. When Death Phase 1 ends, the step runs again, removes the Golem, and Death Phase 2 resolves its Deathrattle. The Damaged Golem appears where the Golem stood when its Death Event was created, not at the right end.
2. 2 Armor. Whirlwind creates both Damage Events (Hoarder, then Armorsmith), Armorsmith reacts to each, and only after that whole Phase does the step remove the Hoarder. The rulebook's Flamestrike example is the same shape: "the Armorsmiths will be mortally wounded but remain in play, triggering on each Damage Event" ([Advanced rulebook](https://hearthstone.wiki.gg/wiki/Advanced_rulebook)).
3. No. The step removes both Hoarders and creates both Death Events before the Death Phase resolves either Deathrattle. It matters for any Deathrattle that looks at the board. Unstable Ghoul is one: two Ghouls dying together never hit each other, because both have left play before either Deathrattle resolves. If your engine removed and resolved them one at a time, the second Ghoul would take the first one's damage while still on the board.
4. In the Sequence's entry point, after the action and all its reactions have resolved. `damage` runs inside nested Phases: if it removed the minion, the minion would miss the reactions it is still owed (Module 2's mortally wounded Acolyte), and a removal there would happen mid-Phase, in the middle of somebody else's Queue. One checkpoint at the end of the outermost Phase means one place where minions leave play.
5. Left of the Reactor: that is where the Golem stood. An engine that remembered "index 1" gets it wrong. When the step began the board was `[Hoarder, Golem, Reactor]` and the Golem was at index 1. The step then removes the Hoarder *and* the Golem, so the board is `[Reactor]`, and index 1 is now the right end: the token lands to the right of the Reactor.
This is Module 1's Predict question 2 again. "A board index goes stale as soon as anything moves", and a Death Creation Step moves several things at once. The first version of this course's own reference engine had exactly this bug, and the original four tests passed, because none of them had a minion die to the left of a summoning Deathrattle in the same step. The cure is the same as in Module 1: remember a handle. See Build.
</details>
## Spec
From the course repository, copy the module's tests into your workspace:
```sh
node tools/start-module.mjs 03 ~/my-engine
```
This module adds one harness function (the M3 line in `course/harness.md`):
| TypeScript | Rust |
|---|---|
| `destroy(target)` | `destroy(&mut self, target: Id)` |
`destroy` runs one Sequence whose only action marks `target` pending destroy. It deals no damage and does not move the minion itself; the Death Creation Step at the end of that Sequence does.
Two `GameEvent`s, already in your `GameEvent` type since Module 2, now get recorded:
- `Destroyed{id}`: the Death Creation Step moved `id` from play to the graveyard. One per minion, in order of play.
- `DeathPhase{round}`: a Death Phase begins. `round` counts from 1 within one Sequence.
A Deathrattle reports `Triggered{source, trigger: "deathrattle"}`, with the dead minion as `source`.
The seven tests (full statements in `course/03-death-step/scenarios.md`) all compare the exact log:
- `test_03_removed_only_after_sequence`: Loot Hoarder then Armorsmith, Whirlwind. `Destroyed(Hoarder)` comes after both Armorsmith triggers; then `DeathPhase 1`, the Hoarder's Deathrattle and its draw.
- `test_03_chained_death_phases`: Harvest Golem at 1 Health, Unstable Ghoul, Test Reactor; `dealDamage(ghoul, 3)`. Two Death Phases, and the Damaged Golem appears at the Golem's old position, left of the Reactor.
- `test_03_log_tree_death_phase`: the same scenario as a tree, from `logTree()`. The Death Creation Step and the Death Phase run after the outermost Phase, so `Destroyed`, `DeathPhase` and each Deathrattle's `Triggered` are at depth 0, and the Deathrattle's effects at depth 1.
- `test_03_simultaneous_removal`: two Loot Hoarders die to Whirlwind. Both `Destroyed` events come before either Deathrattle; the Deathrattles resolve in order of play in one Death Phase.
- `test_03_destroy_marks_pending`: `destroy` on a full-Health Harvest Golem. It leaves through the Death Creation Step with its Health untouched.
- `test_03_summon_position_after_simultaneous_deaths`: Predict question 5. The Damaged Golem ends up left of the Reactor although a minion to the Golem's left died in the same step.
- `test_03_simultaneous_summons_keep_board_order`: two Harvest Golems side by side die together, left of a Reactor. Their tokens stand in the Golems' order, both left of the Reactor.
Test Reactor is the fictional "Whenever a hero takes damage, gain +1 Attack." minion from Module 2. No hero takes damage here, so it only stands on the board and gives the Damaged Golem something to land left of.
The chain test is the heart of the module:
```rust
#[test]
fn test_03_chained_death_phases() {
let mut g = new_game(GameOpts::default());
let golem = g.summon("Harvest Golem", 0);
let ghoul = g.summon("Unstable Ghoul", 0);
let r = g.summon("Test Reactor", 0);
g.deal_damage(golem, 2);
g.clear_log();
g.deal_damage(ghoul, 3);
let d = g.board(0)[0];
assert_eq!(
g.log(),
vec![
damaged(ghoul, 3),
GameEvent::Destroyed { id: ghoul },
GameEvent::DeathPhase { round: 1 },
triggered(ghoul, "deathrattle"),
damaged(golem, 1),
damaged(r, 1),
GameEvent::Destroyed { id: golem },
GameEvent::DeathPhase { round: 2 },
triggered(golem, "deathrattle"),
GameEvent::Summoned { id: d },
]
);
assert_eq!(g.name_of(d), "Damaged Golem");
assert_eq!(g.board(0), vec![d, r]);
assert_eq!(g.zone_of(ghoul), Zone::Graveyard);
assert_eq!(g.zone_of(golem), Zone::Graveyard);
assert_eq!(g.health(r), 4);
}
```
```ts
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);
});
```
`damaged` and `triggered` are small helpers at the top of the Rust test file that build those two events.
Add a `destroy` stub and run `cargo test` or `npm test`. The seven Module 3 tests fail; Modules 1 and 2 still pass. That is RED.
## Build
Make the seven tests pass without breaking Modules 1 and 2. The rules for where code goes still hold:
- The death step, the Death Phase loop, pending destroy, "summon at a position" and the Deathrattle trigger condition are generic machinery: `src/engine/`.
- Loot Hoarder, Harvest Golem, Damaged Golem and Unstable Ghoul are cards: `src/content/`. The engine must not name them. "Deathrattle" is rules vocabulary, like "a friendly minion takes damage" was in Module 2.
- `src/harness` stays thin. `destroy` hands one action to the engine, exactly as `dealDamage` does.
`test_02_mortally_wounded_still_triggers` must keep passing. It now ends with the Acolyte in the graveyard, which that test doesn't check: what it checks, that a mortally wounded minion still draws, is still true.
<details><summary>Hint 1 — nudge</summary>
Your Module 2 `runSequence` has a comment where deaths go. After `perform` returns, every Queue of the Sequence has been resolved. The course treats that point as the end of the outermost Phase (see "How the real game differs" below). What should happen there? And what must happen *again* after the Deathrattles you resolve there have finished?
</details>
<details><summary>Hint 2 — concept</summary>
- **Marks, not removals.** Damage lowers Health. A destroy action sets a `pendingDestroy` flag. Neither moves anything.
- **Death Creation Step:** one function. Collect every minion in play, in order of play, that has Health ≤ 0 or `pendingDestroy`. For each one, *first* note where it stands, then move them all to the graveyard and record `Destroyed` for each. Return their Death Events, each holding the id and the remembered place.
- **Remember a neighbour, not an index.** An index into the board is wrong as soon as another minion further left leaves in the same step (Predict question 5). Remember instead the nearest minion to the right that is *not* dying in this step, by id, or "none". Nothing leaves play during the Death Phase that follows, so that minion is still on the board when the Deathrattle resolves, and "immediately left of it" (or the right end, for "none") is where the dead minion stood.
- **Death Phase:** if the step returned anything, record `DeathPhase{round}` and hand the Death Events to the same `react` you wrote in Module 2. A Deathrattle is a trigger whose condition is "the Death Event is mine". The dead minion is no longer in play, so `react` must also consider the dead minion itself as a listener for its own Death Event.
- **Loop:** after the Death Phase, run the step again. Stop when it returns nothing.
- **Position:** an effect needs to know where to summon. When a Deathrattle starts to resolve, turn the remembered neighbour into a board index *then*, not earlier, and pass it along with the trigger (in the reference it's a field of the effect's context). Give the summon action an optional board index.
</details>
<details><summary>Hint 3 — pseudo-code</summary>
```text
runSequence(action):
perform(action)
deaths = endOfOutermostPhase()
round = 0
while deaths not empty:
round += 1
log DeathPhase(round)
react(deaths) // the Module 2 Phase, unchanged in shape
deaths = endOfOutermostPhase()
endOfOutermostPhase(): // Module 4 adds aura updates around this
return deathCreationStep()
deathCreationStep():
dying = [m for m in minionsInPlay (order of play) if m.health <= 0 or m.pendingDestroy]
events = [Destroyed(m, before: nearest minion right of m that is not in dying, or none)
for m in dying] // neighbours first
for e in events:
move e.id to graveyard; log Destroyed(e.id)
return events
perform(action):
...
Destroy(targets): for t in targets: t.pendingDestroy = true
Summon(card, p, at): create card on p's board at index min(at, len), or the right end
react(events):
for e in events:
listeners = minionsInPlay, plus e.id if e is Destroyed // sorted by order of play
for m in listeners, for trig in m.triggers:
if responds(trig.on, m, e): queue.push((m, trig, e))
for (m, trig, e) in queue:
log Triggered(m, label(trig.on))
position = (index of e.before on m's owner's board, or the board's length if none)
if e is m's Death Event else boardIndex(m)
for a in trig.effect(ctx(owner = m.owner, source = m, position)):
perform(a)
responds(deathrattle, m, Destroyed(id)) -> id == m
```
</details>
When your tests pass, compare with the reference engine in `course/03-death-step/reference/rust/src/engine/` or `course/03-death-step/reference/ts/src/engine/`. The step is `death.rs` / `death.ts`; the loop is `run_sequence` / `runSequence` in `queue.rs` / `queue.ts`.
Three decisions in the reference are worth naming, because the rulebook raises them and this module's cards don't force them:
- **The remembered position is a handle.** A Death Event holds `before`, the id of the nearest surviving minion to the right, and `react` turns it into an index when the Deathrattle resolves. Tokens from the same gap line up left to right in the order they are summoned, which is the order their Deathrattles resolve: order of play. The rulebook says only that the position is "remembered"; it doesn't say how, and it doesn't cover two dying neighbours whose order of play is the reverse of their board order. In that case the reference puts the older minion's token on the left. That is the course's choice.
- **A queued trigger whose source has left play.** In this engine that cannot happen to anything but a Deathrattle. Nothing leaves play mid-Phase, and the Death Creation Step runs only after the outermost Phase, when every Queue has been emptied. Deathrattles are the deliberate exception: their source has always left play by the time they resolve, and they resolve anyway. A minion that is only mortally wounded is still in play and still triggers (Module 2's Acolyte). The course's sources don't say what happens to a queued trigger whose source is removed some other way mid-Phase. No card in this course does that, so the engine doesn't re-check a source before resolving its trigger. That is a course simplification.
- **"A trigger cannot be nested inside itself"** ([Advanced rulebook](https://hearthstone.wiki.gg/wiki/Advanced_rulebook)). The references of Modules 3 and 4 have no guard for this; it arrives in [Module 5](../05-assemble/lesson.md#the-harness). No card in this module can nest a trigger inside itself: a Ghoul whose Deathrattle kills another Ghoul doesn't nest anything, because the second Ghoul's Deathrattle waits for the next Death Phase. Two Module 2-style cards that deal damage whenever a minion takes damage *would* recurse forever, and the rule is the cure. Add it when a card needs it: in the course, that is Module 5, where a test can write such a card as data.
> **How the real game differs**
> - The course treats each player action (a `dealDamage`, a `cast`, from Module 4 a `play`) as one outermost Phase, and processes deaths once, after the whole action. In the real game "A Sequence is what begins when a Player Action is taken" and a Sequence is made of Phases, and "death processing is not done in the middle of a Phase, but is done between Phases" ([Advanced rulebook](https://hearthstone.wiki.gg/wiki/Advanced_rulebook)). So one player action can have deaths processed more than once, between its Phases. The course does not model which Phases each action has; none of the course's cards can tell the difference.
> - The Death Creation Step is one of several steps at the end of the outermost Phase: "After the outermost Phase ends, Hearthstone does an Aura Update (Health/Attack), then does the Death Creation Step", and an Aura Update (Other) follows it ([Advanced rulebook](https://hearthstone.wiki.gg/wiki/Advanced_rulebook)). The course has no auras until Module 4, which adds both updates around the step.
> - The rulebook says "negative (damaging/destroying/hindering/misdirecting) triggers ignore mortally wounded and pending destroy characters", while "AoE effects count mortally wounded" (same source). The course's area effects count them, which matches; nothing in the course is a targeted negative trigger, so the first rule is not modelled.
> - A Death Phase queues "Deathrattles, on-Death Secrets and on-Death triggered effects" (same source). The course has only Deathrattles.
> - "Feign Death and similar effects summon minions at the right side of the board, not the minion's position" (same source). The course has no such effect; every Deathrattle summon appears where its minion stood.
> - Unstable Ghoul has Taunt. The course has no combat until [Module 12](../12-combat/lesson.md), so Taunt does nothing here. Heroes never die in the course either: no game ends.
## Name it
The pattern is a **deferred state-based check**. Effects only change state (Health, a pending-destroy flag). Whether that state means "dead" is decided at one defined checkpoint, for everything at once, and the check repeats until it finds nothing.
Magic: The Gathering has the same rule under that name. The [Comprehensive Rules](https://magic.wizards.com/en/rules), rule 704: "State-based actions are game actions that happen automatically whenever certain conditions (listed below) are met." The game "performs all applicable state-based actions simultaneously as a single event", and "If any state-based actions are performed as a result of a check, the check is repeated". One of the conditions (704.5f): "If a creature has toughness 0 or less, it's put into its owner's graveyard." That is a Death Creation Step and its loop, in a different game.
In the reference engine, the end of a Sequence:
```rust
/// Run one Sequence: a player action and everything it causes.
pub fn run_sequence(world: &mut World, action: Action) {
perform(world, action);
// The outermost Phase has ended. Each Death Phase is itself an outermost
// Phase, so the end-of-Phase steps run again after it, until nothing dies.
let mut deaths = end_of_outermost_phase(world);
let mut round = 0;
while !deaths.is_empty() {
round += 1;
world.record(Event::DeathPhase { round });
react(world, &deaths);
deaths = end_of_outermost_phase(world);
}
}
/// The steps that run only when the outermost Phase ends; returns the Death Events.
/// Module 4 adds an Aura Update (Health/Attack) before the Death Creation Step
/// and an Aura Update (Other) after it, here.
fn end_of_outermost_phase(world: &mut World) -> Vec<Event> {
death_creation_step(world)
}
```
```ts
/** Run one Sequence: a player action and everything it causes. */
export function runSequence(world: World, action: Action): void {
perform(world, action);
// The outermost Phase has ended. Each Death Phase is itself an outermost
// Phase, so the end-of-Phase steps run again after it, until nothing dies.
let deaths = endOfOutermostPhase(world);
for (let round = 1; deaths.length > 0; round++) {
world.record({ type: "DeathPhase", round });
react(world, deaths);
deaths = endOfOutermostPhase(world);
}
}
/**
* The steps that run only when the outermost Phase ends; returns the Death Events.
* Module 4 adds an Aura Update (Health/Attack) before the Death Creation Step
* and an Aura Update (Other) after it, here.
*/
function endOfOutermostPhase(world: World): Event[] {
return deathCreationStep(world);
}
```
Notice what didn't change: a Death Phase is the Module 2 `react`, fed Death Events instead of Damage Events. A Deathrattle is one more trigger condition. Depth-first resolution, order of play and the locked Queue all carry over for free.
Fireplace, a Hearthstone simulator in Python, has the same checkpoint. In [`fireplace/game.py`](https://github.com/jleclanche/fireplace/blob/47a2572/fireplace/game.py) (commit `47a2572`), `action_end` checks `if not self._action_stack:`, meaning the outermost action block has just closed, and only then refreshes auras and calls `process_deaths`.
**Run the coupling audit** again: `node tools/coupling-audit.mjs ~/my-engine` should print `coupling-audit: ok`. Your engine now knows "Deathrattle", "summon at a position" and "destroy". Those are rules vocabulary. "Harvest Golem" is not.
## Expansion Drop
A new card arrives: Twisting Nether, 8-mana spell, "Destroy all minions and locations." ([Twisting Nether](https://hearthstone.wiki.gg/wiki/Twisting_Nether)).
> **How the real game differs**
> Locations are a card type this course doesn't have. Here, Twisting Nether destroys all minions.
The constraint, as always: **do not edit anything under `src/engine/`**. The harness doesn't need to change either. Only `src/content/` should.
1. Commit your workspace and note the commit: `git add -A && git commit -m "module 3 done"`.
2. Copy the expansion test for your track from `course/03-death-step/expansion/tests/rust/test_03x.rs` or `course/03-death-step/expansion/tests/ts/test_03x.test.ts` into your workspace's `tests/`. Run it and watch it fail.
3. Add Twisting Nether to your content.
4. When the tests pass, run the check from the course repository:
```sh
node tools/expansion-check.mjs ~/my-engine HEAD
```
The test, `test_03x_twisting_nether_destroys_all`, summons Loot Hoarder and Harvest Golem for player 0 and a `Test Filler` for player 1, then casts Twisting Nether. All three are destroyed in one Death Creation Step, before either Deathrattle resolves, with their Health untouched. The Damaged Golem arrives afterwards and survives: the destroy marked only the minions that were in play when it resolved. The full scenario is in `course/03-death-step/expansion/README.md`.
<details><summary>Discussion: what this drop measures</summary>
If `destroy` is an action your effects can return, Twisting Nether is one entry in your content library:
```rust
// Expansion Drop: "Destroy all minions and locations." The course has no locations.
CardDef::spell("Twisting Nether", |c| {
vec![Action::Destroy {
targets: c.world.minions_in_play(),
}]
}),
```
```ts
// Expansion Drop: "Destroy all minions and locations." The course has no locations.
spell("Twisting Nether", (c) => [{ type: "destroy", targets: c.world.minionsInPlay() }]),
```
That is the reference answer in `course/03-death-step/expansion/solution/`. The drop is fair: the harness's `destroy` already needed a destroy action, and Whirlwind already showed how a spell targets every minion. Twisting Nether combines them.
Shapes that fail this drop:
- **`destroy` lives in the harness only.** If the harness marks the minion itself instead of handing the engine an action, a card can't return "destroy", and Twisting Nether needs a new engine action.
- **Destroy removes immediately.** If the destroy action moves each minion to the graveyard and resolves its Deathrattle on the spot, the Hoarder's draw happens before the Golem is destroyed, and the log order is wrong.
- **The death step only looks at Health.** Pending-destroy minions at full Health are never collected.
If the check fails, fix your engine (a normal commit, outside the drop), then redo the drop from step 1.
</details>
## Stress it
Optional. Each question pushes the design past what this module's tests check. Predict first, then open the discussion.
1. `runSequence` loops "until there are no new Deaths". With the cards of Modules 1–3, can that loop run forever? Don't answer "obviously not": find a quantity that gets strictly smaller every time round the loop and can't go below zero.
<details><summary>Discussion</summary>
Every pass needs at least one death, and a death removes a minion. The only way minions come back is a summon, and the only card that summons is Harvest Golem, whose token has no Deathrattle. So count the pair (Harvest Golems in play, minions in play) and compare pairs left to right. A pass in which a Golem dies lowers the first number. A pass in which no Golem dies summons nothing, so it lowers the second. The pair can't fall forever, so the loop ends.
That argument is about the *card set*, not the engine. Break it with one card: Test Ember, a 1/0 with "Deathrattle: Summon a Test Ember." It arrives mortally wounded, dies in the next Death Creation Step, and summons its replacement, every round, with nothing else on the board. Your engine will loop until something external stops it.
So termination is a property the designers of a card set have to maintain, or the engine has to enforce. Magic: The Gathering does the second, as a rule of the game: 104.4b, "If a game ... somehow enters a 'loop' of mandatory actions, repeating a sequence of events with no way to stop, the game is a draw." ([Comprehensive Rules](https://magic.wizards.com/en/rules)). The course's sources don't say what Hearthstone does. The cheapest engine-side guard is a cap on `round` that fails loudly; a failure with a message is better than a hang, and far better than a silent cut-off that makes one game in a million resolve differently.
The references of Modules 3 and 4 have no cap: given Test Ember, they loop until something external stops them. The cap arrives in [Module 5](../05-assemble/lesson.md#the-harness), where cards are data and a test can write the Ember: more than 64 Death Phases in one Sequence is an error whose message contains `Death Phase limit` (`test_05_death_phase_limit`, with an Ember that also deals 1 damage to all minions). From Module 5 on, every reference has it.
</details>
2. Swap two lines in your Death Creation Step so that it removes each dying minion and creates its Death Event one at a time, in order of play, instead of noting every position first and removing afterwards. Which tests fail?
<details><summary>Discussion</summary>
It depends on how you remember positions, and the experiment is worth running for that reason.
If you remember a board index, `test_03_summon_position_after_simultaneous_deaths` fails in the *other* direction from Predict question 5, or passes by accident, depending on the order of the two dead minions. Indexes read during a removal loop are indexes into a list that is changing under you.
If you remember the surviving right-hand neighbour, the result depends on what "surviving" means halfway through the loop. The reference asks "is that minion doomed?", a question about marks, which has the same answer before and after any removal. So the one-at-a-time version gives the same result. That is not luck. The step is simultaneous *because every decision in it reads only the marks*, and the marks don't change during the step. "Decide from a snapshot, then apply" is the pattern; "do everything first, then everything else" is one way to get it, and reading only data that the loop doesn't change is another. Module 8 and Module 9 lean on the same idea.
</details>
3. Hearthstone's rulebook example for this module has three Unstable Ghouls dying "in one Phase" and an Acolyte of Pain that draws three cards. Build it: three Ghouls and an Acolyte with a deck. What is the smallest single action that kills all three Ghouls in one Death Creation Step? How many Death Phases follow, and what kills the Acolyte in the version of the game where it has 3 Health?
<details><summary>Discussion</summary>
Twisting Nether would kill the Acolyte too, so use damage: bring each Ghoul to 1 Health first (three earlier Sequences), then cast Whirlwind. One Death Creation Step removes all three; Death Phase 1 resolves three Deathrattles, each dealing 1 to all minions. The Acolyte takes 1 from Whirlwind and 3 from the Ghouls and draws four cards in all.
With 4 Health (the course's card, and today's) it is at 0 after the third Ghoul and leaves in the next step: one more Death Creation Step, an empty Death Phase 2 (no Deathrattle), and the loop ends. Counting Whirlwind's damage, a 3-Health Acolyte was already at 0 after the *second* Ghoul, and still drew for the third: mortally wounded minions keep triggering. The rulebook's "draws three cards" counts only the Ghouls' damage. Writing an example out as a test, with every number in it, is how you find out which of its numbers the example was actually about.
</details>
## Further reading
- [Advanced rulebook, hearthstone.wiki.gg](https://hearthstone.wiki.gg/wiki/Advanced_rulebook): the glossary entries for Sequence, Phase and Death, and the passages on what runs when the outermost Phase ends (the Death Creation Step and the two Aura Updates). Read the Unstable Ghoul / Acolyte of Pain example in full.
- Wizards of the Coast, [Magic: The Gathering Comprehensive Rules](https://magic.wizards.com/en/rules), rule 704 (state-based actions). Short, precise, and written for a game with far more ways to die. Read 704.3 for the loop and skim 704.5 for the list of conditions.
- Fireplace, [`fireplace/game.py`](https://github.com/jleclanche/fireplace/blob/47a2572/fireplace/game.py) at commit `47a2572`: read `action_end` and `process_deaths`, around lines 127–192. Compare where it calls `refresh_auras()` with the Module 4 seam in your `endOfOutermostPhase`.
- Jon Parham, [*Make a CCG – Action System*](https://theliquidfire.com/2017/09/11/make-a-ccg-action-system/) (The Liquid Fire, 2017). The "death reaper" loop that runs "to take care of any mortally wounded cards" after the root action, previewed in Module 2, is this module's loop.