diff --git a/course/02-action-queue/lesson.md b/course/02-action-queue/lesson.md
index 8be4adb..1ec578a 100644
--- a/course/02-action-queue/lesson.md
+++ b/course/02-action-queue/lesson.md
@@ -329,9 +329,75 @@ If the check fails, fix your engine (a normal commit, outside the drop), then re
+## Stress it
+
+Optional. Each question pushes the design past what this module's tests check. Predict first, then open the discussion.
+
+1. Add a fictional card to your content: Test Bomb, 1/50, "Whenever a minion takes damage, deal 1 damage to all minions." Summon one and deal it 1 damage. Before you run it: what does your engine do, and what is in the log when it stops?
+
+Discussion
+
+It never stops on its own. The Bomb's trigger damages the Bomb, which queues the Bomb's trigger, inside itself. The reference engine at this module recurses until the language runtime gives up: in the TypeScript track that is a `RangeError` after about two thousand log events, with the Bomb at −936 Health. A stack overflow in the middle of a Sequence also leaves the world half-changed, which is worse than the crash.
+
+Depth-first resolution by plain recursion is the right model and it has this cost: nothing bounds the depth. Two real games answer in two different ways.
+
+- **Forbid it by rule.** The Advanced rulebook: "A trigger cannot be nested inside itself" ([Advanced rulebook](https://hearthstone.wiki.gg/wiki/Advanced_rulebook)). In engine terms: keep the set of (source, trigger) pairs that are resolving right now, and don't queue an entry that is already in it. Try it. With the guard, the Bomb damages everything once and the Sequence ends: its trigger is on the stack when its own damage lands, so it isn't queued again.
+- **Allow it and name the outcome.** Magic: The Gathering has no such guard. Rule 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)). An engine for that game has to *detect* the loop, which is harder than preventing it.
+
+Either way the decision belongs in the engine's one resolution loop, and it is a rule of the game, not an implementation detail: two Bombs under the guard still hit each other once each, and a player can see that.
+
+The guard is not part of the course's tests, and the reference doesn't have it; [Module 3](../03-death-step/lesson.md) says why it can wait. If you add it, add a test with it.
+
+
+
+2. Make your engine breadth-first on purpose. Keep one list of pending triggers for the whole Sequence; when a trigger's action causes new triggers, append them to the end. Which of the module's four tests fails, and with what order? What did you gain?
+
+Discussion
+
+Only `test_02_depth_first_nesting`: the triggers come out Echo, Armorsmith, Reactor, and `ArmorGained` precedes Reactor's `StatsChanged`. The other three pass, which is worth noticing. Area damage first, mortally wounded minions that still react, and fatigue are all independent of the depth-first rule. One test in the module pins it.
+
+What you gained is that the breadth-first engine can't overflow the stack: question 1's Bomb fills a list instead (forever, but politely). That is why queues are the pattern people reach for first, and why Nystrom's advice in Name it is to avoid raising events from handlers. If you want depth-first order *and* no recursion, keep an explicit stack and push a trigger's consequences on top of it, to be resolved before what was already waiting. That is Slay the Spire's "add to top" from Further reading, and it is the same order your recursion produces.
+
+
+
+3. Your log is a flat list. The real game's is not. HearthSim's parser for Hearthstone's `Power.log` describes it: "The `BLOCK_START` and `BLOCK_END` packets are serialized into a Block packet, which is nestable." ([python-hslog README](https://github.com/HearthSim/python-hslog)). The block types include `PLAY`, `POWER`, `TRIGGER` and `DEATHS` (`BlockType` in [python-hearthstone's `enums.py`](https://github.com/HearthSim/python-hearthstone/blob/master/hearthstone/enums.py)). What would your engine have to record so that the Echo / Armorsmith / Reactor log could be printed as an indented tree? Add it, and print the tree.
+
+Discussion
+
+One number: the nesting depth at which each event was recorded (or, equivalently, a "block opens" and a "block closes" marker around each trigger's resolution). Your `react` already knows it; it is the depth of the recursion.
+
+```text
+Damaged(Armorsmith, 1)
+ Triggered(Echo)
+ Damaged(enemy hero, 1)
+ Triggered(Reactor)
+ StatsChanged(Reactor, 2, 5)
+ Triggered(Armorsmith)
+ ArmorGained(0, 1)
+```
+
+Printed this way the depth-first rule is visible at a glance: Reactor's line is *inside* Echo's block, and Armorsmith's block starts only after Echo's has closed. A breadth-first engine can't print this tree at all, because its events have no single parent block that is still open. The nested blocks in the game's own log are consistent with the rulebook's "similar to a depth-first search"; that is the course's reading of the log format, not a statement from Blizzard.
+
+Keep the tree printer. It is the most useful debugging tool you will have in Modules 3 and 4.
+
+
+
+4. This module's Armor is only a number. In the real game, "Damage taken will always be deducted from Armor before Health." and "Damage taken while protected by Armor still counts as taking damage for game purposes." ([Armor](https://hearthstone.wiki.gg/wiki/Armor)). Player 0 has 2 Armor and their hero takes 3 damage, with Test Reactor ("Whenever a hero takes damage, gain +1 Attack.") in play. Where in your engine does the absorption go: in the card that deals the damage, in `perform`, or in a trigger? What does the `Damaged` event say?
+
+Discussion
+
+In `perform`, between "an effect asked for 3 damage" and "the world changed": lower Armor by 2, lower Health by 1, and record one `Damaged` event, so Reactor triggers once. Not in the card, because every source of damage would have to remember to do it. Not in a trigger, because triggers react to events that have already happened, and this changes what happens.
+
+That makes Armor the course's first example of a rule that **rewrites an action before it is performed**, rather than reacting after. Module 4 builds a different rule of that family (how many times a trigger runs), and names the general one: a replacement effect. The questions you just had to answer are the ones every such rule raises. Does the event report the amount asked for or the amount that got through? (The wiki's second quote suggests "it still counts", so the event exists even if Health didn't change.) And if two rewriting rules apply to the same action, which goes first?
+
+The course doesn't build this: no test damages a hero that has Armor. If you build it, decide what `Damaged.amount` means and write that down next to the event type.
+
+
+
## Further reading
- Jon Parham, [*Make a CCG – Action System*](https://theliquidfire.com/2017/09/11/make-a-ccg-action-system/) (The Liquid Fire, 2017). The most readable worked implementation of a Hearthstone-style reaction tree, in C#. Read `SortActions`: reactions are sorted by `priority` first and `orderOfPlay` only as the tie-breaker, which is where an engine would put Poisonous or Redemption. Also note the loop at the end, the "death reaper", which runs "to take care of any mortally wounded cards" after the root action. That is Module 3.
- Robert Nystrom, [*Event Queue*](https://gameprogrammingpatterns.com/event-queue.html) (*Game Programming Patterns*). The general pattern, and its warnings. Read the sections on feedback loops and on the world state having changed by the time an event is handled: "you have to be careful not to assume the current state of the world reflects how the world was". Your ids from Module 1 are part of the answer.
- Alchyr, [*Action Queue*](https://github.com/Alchyr/BasicMod/wiki/Action-Queue) (BasicMod wiki, Slay the Spire modding). A short practical page about a different game's queue, where the modder chooses where a new action goes: "In a card's use: add to bottom. In an action's update: add to top." Compare it with the depth-first rule: "add to top" is how Slay the Spire makes a reaction happen before the next sibling.
- [Advanced rulebook, hearthstone.wiki.gg](https://hearthstone.wiki.gg/wiki/Advanced_rulebook): Rule 2 (Queues), the depth-first section, and Rule DH1 on area damage.
+- HearthSim, [python-hslog](https://github.com/HearthSim/python-hslog): "a powerful Hearthstone Power.log deserializer". Read the README's Concepts section on the packet tree. It is the closest thing to seeing the real game's resolution as data, and it is a tree of nested blocks.
diff --git a/course/03-death-step/lesson.md b/course/03-death-step/lesson.md
index 35c83f6..37136e6 100644
--- a/course/03-death-step/lesson.md
+++ b/course/03-death-step/lesson.md
@@ -18,10 +18,13 @@ Three cards ([Loot Hoarder](https://hearthstone.wiki.gg/wiki/Loot_Hoarder), [Har
**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).
+**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
@@ -32,6 +35,7 @@ Write your answers down before you open the discussion, for example in `notes/03
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?
Discussion
@@ -43,6 +47,10 @@ Write your answers down before you open the discussion, for example in `notes/03
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.
+
## Spec
@@ -68,12 +76,14 @@ Two `GameEvent`s, already in your `GameEvent` type since Module 2, now get recor
A Deathrattle reports `Triggered{source, trigger: "deathrattle"}`, with the dead minion as `source`.
-The four tests (full statements in `course/03-death-step/scenarios.md`) all compare the exact log:
+The six 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_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.
@@ -149,11 +159,11 @@ test("test_03_chained_death_phases", () => {
`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 four Module 3 tests fail; Modules 1 and 2 still pass. That is RED.
+Add a `destroy` stub and run `cargo test` or `npm test`. The six Module 3 tests fail; Modules 1 and 2 still pass. That is RED.
## Build
-Make the four tests pass without breaking Modules 1 and 2. The rules for where code goes still hold:
+Make the six 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.
@@ -170,10 +180,11 @@ Your Module 2 `runSequence` has a comment where deaths go. After `perform` retur
Hint 2 — concept
- **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`. Read each one's board index *first*, then move them all to the graveyard and record `Destroyed` for each. Return their Death Events, each holding the id and the remembered position.
+- **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. Pass the remembered position along with the trigger (in the reference it's a field of the effect's context), and give the summon action an optional board index.
+- **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.
@@ -195,7 +206,8 @@ endOfOutermostPhase(): // Module 4 adds aura updates around
deathCreationStep():
dying = [m for m in minionsInPlay (order of play) if m.health <= 0 or m.pendingDestroy]
- events = [Destroyed(m, boardIndex(m)) for m in dying] // positions first
+ 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
@@ -212,7 +224,8 @@ react(events):
if responds(trig.on, m, e): queue.push((m, trig, e))
for (m, trig, e) in queue:
log Triggered(m, label(trig.on))
- position = e.position if e is m's Death Event else boardIndex(m)
+ 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)
@@ -223,8 +236,9 @@ responds(deathrattle, m, Destroyed(id)) -> id == m
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`.
-Two decisions in the reference are worth naming, because the rulebook raises them and this module's cards don't force them:
+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 reference has no guard for this yet. 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.
@@ -233,7 +247,7 @@ Two decisions in the reference are worth naming, because the rulebook raises the
> - 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 uses the remembered position.
+> - "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, so Taunt does nothing here. Heroes never die in the course either: no game ends.
## Name it
@@ -348,6 +362,44 @@ If the check fails, fix your engine (a normal commit, outside the drop), then re
+## 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.
+
+Discussion
+
+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.
+
+
+
+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?
+
+Discussion
+
+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.
+
+
+
+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?
+
+Discussion
+
+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.
+
+
+
## 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.
diff --git a/course/04-interceptors/lesson.md b/course/04-interceptors/lesson.md
index 4670ce7..f993bf3 100644
--- a/course/04-interceptors/lesson.md
+++ b/course/04-interceptors/lesson.md
@@ -210,7 +210,7 @@ When your tests pass, compare with the reference engine in `course/04-intercepto
Decisions in the reference that the tests don't force, but the rules suggest:
-- **The count is read once per queue entry, as it starts.** The rulebook says repeat-trigger cards "check before an Event starts", and adds: "if one Battlecry/Deathrattle/End of turn effect summons them, it will not trigger again. But Deathrattles/End of turn effects that follow can benefit, for Aura (Other) is updated when you summon them." ([Advanced rulebook](https://hearthstone.wiki.gg/wiki/Advanced_rulebook)). Reading the count before the first activation, and updating the snapshot on summon, gives exactly that: an effect that summons a Baron has already decided its own count, and the next queue entry sees him. No test checks this, because no card in the course summons a multiplier.
+- **The count is read once per queue entry, as it starts.** The rulebook says repeat-trigger cards "check before an Event starts", and adds: "if one Battlecry/Deathrattle/End of turn effect summons them, it will not trigger again. But Deathrattles/End of turn effects that follow can benefit, for Aura (Other) is updated when you summon them." ([Advanced rulebook](https://hearthstone.wiki.gg/wiki/Advanced_rulebook)). Reading the count before the first activation, and updating the snapshot on summon, gives exactly that: an effect that summons a Baron has already decided its own count, and the next queue entry sees him. No Module 4 test checks this, because no Module 4 card summons a multiplier; Module 5 adds one (see Stress it, question 1).
- **Playing is not summoning.** The rulebook's update point is "whenever a minion is summoned (but not played)" (same source). So `play` doesn't run an Aura Update; the played minion's auras join at the end of the outermost Phase, which is still before any Death Phase it could affect. Its own Battlecry has already resolved by then.
- **The `additive` branch is there, and no Hearthstone card in the course uses it.** It is two lines, and it is what makes `stacking` a real choice rather than a constant: see Name it.
@@ -300,6 +300,8 @@ minion("Baron Rivendare", 1, 7, [], [
The Argentum engine (an MTG rules engine in Kotlin) implements it the way this module does. The card [`Panharmonicon.kt`](https://github.com/wingedsheep/argentum-engine/blob/main/mtg-sets/2008-2016/src/main/kotlin/com/wingedsheep/mtg/sets/definitions/kld/cards/Panharmonicon.kt) declares a static ability `AdditionalETBOrLTBTriggers(...)`: pure data, with a filter for which objects' enter/leave triggers it applies to. One generic routine, `duplicateETBOrLTBTriggers` in [`TriggerDetector.kt`](https://github.com/wingedsheep/argentum-engine/blob/main/rules-engine/src/main/kotlin/com/wingedsheep/engine/event/TriggerDetector.kt), consumes every such ability. Its doc comment states the stacking policy: "Multiple copies are additive: N copies add N extra copies of each trigger." Baron's page says the opposite for Baron. Same mechanism, different policy, which is why `stacking` is a field of the modifier and not a rule of the engine.
+Hearthstone's own data appears to draw the same line. python-hearthstone's `GameTag` (the tag names from Module 0) has `EXTRA_DEATHRATTLES_BASE = 882` next to `EXTRA_DEATHRATTLES_ADDITIONAL = 1131`, and the same pair for Battlecries: `EXTRA_BATTLECRIES_BASE = 411` and `EXTRA_BATTLECRIES_ADDITIONAL = 1126` ([`enums.py`](https://github.com/HearthSim/python-hearthstone/blob/master/hearthstone/enums.py)). The file gives names and numbers and nothing else, so what follows is the course's reading of the names: a *base* count that a second Baron can't raise, and *additional* activations that add up. If that reading is right, the game met the stacking question and answered it the way this module does, with two kinds of data and one consumer.
+
Argentum's Panharmonicon file also carries a ruling: "Replacement effects are unaffected by Panharmonicon's ability". That is the line between two kinds of interceptor. A trigger multiplier changes how many times a trigger fires. A **replacement effect** rewrites an event before it happens: Comprehensive Rules 614.1a, "Effects that use the word 'instead' are replacement effects." 614.5 adds the guard every rewrite system needs: "A replacement effect doesn't invoke itself repeatedly; it gets only one opportunity to affect an event". And when many modifiers touch the same values, rule 613 orders them in layers: "all applicable continuous effects are applied in a series of layers in the following order". The course needs only the first kind; the other two are the same idea (data consulted by one step, with an explicit ordering) at a larger scale.
**Run the coupling audit** again: `node tools/coupling-audit.mjs ~/my-engine` should print `coupling-audit: ok`. Your engine now knows "aura", "trigger multiplier", "Battlecry" and "play". It must not know "Baron Rivendare".
@@ -341,7 +343,7 @@ minion("Brann Bronzebeard", 2, 4, [], [
]),
```
-That is the reference answer in `course/04-interceptors/expansion/solution/`. It is Baron with one word changed. The course's sources don't say how two Branns combine, and the test uses one, so the reference simply reuses Baron's `max`.
+That is the reference answer in `course/04-interceptors/expansion/solution/`. It is Baron with one word changed, `max` included: the card's page says "Brann's effect does not stack with itself" ([Brann Bronzebeard](https://hearthstone.wiki.gg/wiki/Brann_Bronzebeard)). The test uses one Brann.
Shapes that fail this drop:
@@ -353,6 +355,62 @@ If the check fails, fix your engine (a normal commit, outside the drop), then re
+## Stress it
+
+Optional. Each question pushes the design past what this module's tests check. Predict first, then open the discussion.
+
+1. Add a fictional card: Test Herald, 1/1, "Deathrattle: Summon a Baron Rivendare." Case A: you played Herald, then Loot Hoarder, and cast Twisting Nether. Case B: Loot Hoarder first, then Herald, then Twisting Nether. How many cards do you draw in each?
+
+Discussion
+
+Two in case A, one in case B. Both minions leave in the same Death Creation Step and both Deathrattles sit in one locked Queue, in order of play. In A the Herald's resolves first and summons Baron; a summon runs an Aura Update, so when the Hoarder's entry *starts*, Baron is in the snapshot and it activates twice. In B the Hoarder's entry starts before any Baron exists.
+
+This is the rulebook passage quoted under Build: a multiplier summoned by one Deathrattle doesn't help that Deathrattle, "But Deathrattles/End of turn effects that follow can benefit, for Aura (Other) is updated when you summon them." The reference has behaved this way since this module and no Module 4 test checks it, because no Module 4 card summons a multiplier. Module 5 adds the test (`test_05_summoned_multiplier_counts_for_later_entries`) once a card can be written as data inside a test.
+
+Note what decides the answer: order of play, again. The same six words of card text, the same spell, and the draw count depends on which minion was played first.
+
+
+
+2. Replace your snapshot with a live scan: make `activations` look at the board every time it is called, and delete the Aura Updates. Run the Module 1–4 tests. Then explain the result.
+
+Discussion
+
+They all pass. In the reference, the same change breaks no test in Modules 1 to 9.
+
+The snapshot differs from a live scan only when a multiplier's presence on the board changes *between* Aura Updates. In this engine minions leave play only in the Death Creation Step, which is followed at once by an update, and a summon runs an update too. That leaves one gap: a minion that is **played**. Its aura isn't in the snapshot until the outermost Phase ends, and a live scan would see it at once. So the two engines differ only if something asks "how many times?" between the play and the end of the Phase, about a trigger the new minion multiplies. That something is the played minion's own Battlecry.
+
+Module 4 has no card with both a Battlecry and a Battlecry multiplier, so the difference is invisible here. Module 5 can write one as data, and its `test_05_own_aura_misses_own_battlecry` does: "Battlecry: Draw a card. Your Battlecries trigger twice." draws one card under the snapshot and two under a live scan. The real game sides with the snapshot: "The double Battlecry effect will only take place if Brann was already present on the board." ([Brann Bronzebeard](https://hearthstone.wiki.gg/wiki/Brann_Bronzebeard)).
+
+The method matters more than the card. You changed a design decision, the tests stayed green, and that told you the tests didn't pin the decision. Then you worked out the smallest scenario that tells the two designs apart. That is **mutation testing** done by hand, and [Module 11](../11-break-your-engine/lesson.md) does it to the whole course.
+
+
+
+3. Brann is in play. You play a minion whose Battlecry destroys Brann. The wiki says the second Battlecry still happens: "This is because Auras like Brann do not update mid-Phase." ([Brann Bronzebeard](https://hearthstone.wiki.gg/wiki/Brann_Bronzebeard)). Your engine agrees. Name every separate reason it agrees.
+
+Discussion
+
+There are three, and any one would be enough:
+
+- **Deferred death** (Module 3). "Destroy" only marks Brann. He is on the board until the Death Creation Step, after the whole Battlecry entry has finished.
+- **The snapshot.** Even if Brann left the board at once, the list of auras in force isn't recomputed until an Aura Update.
+- **The count is read once.** `activations` is asked when the queue entry starts, and the loop runs that many times whatever happens to the snapshot meanwhile.
+
+Three mechanisms guarding one behaviour means none of them is tested by it. Question 2 found the same thing from the other side. When you want a test to pin *one* decision, build the scenario in which the other safeguards don't apply.
+
+
+
+4. `activations` returns `max + extra`: the largest `max` multiplier, plus every additive one's extra activations. Baron (`max`, 2) with two Test Chorus (`additive`, 2) gives 4. Is that order-independent: does it matter which minion's aura the loop reads first? Now invent a third policy, `multiply` ("your Deathrattles trigger three times as often"). Can you keep the answer order-independent?
+
+Discussion
+
+`max + extra` is order-independent because it never applies one aura *to the result of* another. It folds all the `max` values with `max` and all the extras with `+`, each of which gives the same answer in any order, and combines the two totals once at the end.
+
+`multiply` breaks that unless you decide where it sits. With one additive +1 and one ×3 you can get (1 + 1) × 3 = 6 or 1 × 3 + 1 = 4. The fix is the same move again: don't let the order of the *auras* matter; fix the order of the *kinds*. "All max, then all additive, then all multiply" is a three-entry list, written once. That list is a tiny version of Magic's rule 613, quoted in Name it: continuous effects are "applied in a series of layers", and only inside a layer does a timestamp break ties.
+
+The reference's choice (`max + extra`) is itself only one of the defensible ones. "Additive extras, then at least the base" (`max(base, 1 + extra)`) is another, and it gives 3 for Baron plus two Chorus. No source decides between them, since Test Chorus is the course's card. What a design must not do is leave the choice to the order of a loop.
+
+
+
## Further reading
- [Baron Rivendare, hearthstone.wiki.gg](https://hearthstone.wiki.gg/wiki/Baron_Rivendare): the notes section, on queuing, stacking and simultaneous death.
diff --git a/course/05-assemble/lesson.md b/course/05-assemble/lesson.md
index b95585e..a4f06f9 100644
--- a/course/05-assemble/lesson.md
+++ b/course/05-assemble/lesson.md
@@ -114,6 +114,7 @@ The full statements are in `course/05-assemble/scenarios.md`.
- `test_05_top_half_swap`: Loot Hoarder with Novice Engineer's top half. Playing it draws once; destroying it draws nothing.
- `test_05_assemble_effect_with_chooser`: cast `Test Workshop` ("Assemble a Battlecry minion. Give all minions in your hand +1/+1.") with an injected chooser. The base options are exactly the Battlecry minions in `cards/`; the new minion and a `Test Filler` already in hand both get +1/+1.
- `test_05_additive_multipliers_stack`: two `Test Chorus` ("Your minions trigger their Deathrattles an additional time.", stacking `additive`) and a Loot Hoarder: three activations. Module 4 built the `additive` branch; this is the first card that uses it, and it needed no code.
+- `test_05_summoned_multiplier_counts_for_later_entries` and `test_05_own_aura_misses_own_battlecry`: two timing rules from Module 4 that no Module 4 card could reach. Each test registers a one-off card written as data inside the test (a Deathrattle that summons Baron Rivendare; a minion with a Battlecry and its own Battlecry multiplier). If your Module 4 engine followed the lesson's Aura Update schedule, both pass without new code. See Module 4's [Stress it](../04-interceptors/lesson.md#stress-it), questions 1 and 2.
The top-half test:
@@ -194,7 +195,7 @@ Run `cargo test` or `npm test`. The Module 5 tests fail to compile (Rust) or fai
## Build
-Make the seven tests pass, and keep Modules 1–4 green at every step. Where code goes:
+Make the nine tests pass, and keep Modules 1–4 green at every step. Where code goes:
- **No card in code.** Your content directory becomes a loader: it reads `cards/*.json` and hands the definitions to the engine. Delete the card functions from Modules 2–4 once the data versions pass the old tests.
- **The engine interprets.** `src/engine/` gets types that mirror the card format, and one routine that turns an effect plus its context (owner, source, target, position) into the actions you already have. That routine is the only code that knows what `draw` or `dealDamage` means. It names effect kinds and targets, never cards.
@@ -421,9 +422,57 @@ If the check fails, fix your engine (a normal commit, outside the drop), then re
+## Stress it
+
+Optional. Each question pushes the design past what this module's tests check. Predict first, then open the discussion.
+
+1. `assemble("Loot Hoarder", "Baron Rivendare", "top")` and `assemble("Loot Hoarder", "Baron Rivendare", "bottom")`. Baron's `split` is 0: his top half is empty and his bottom half is the aura. Write out the `text` of each result, then say what each card should *do*. Then summon each one in your engine, destroy it, and read the log.
+
+Discussion
+
+- **Top:** `[] + [effect: draw]`. A minion whose whole text is "Draw a card." with no trigger in front of it. In the reference the clause compiles into the "effects before any trigger" list, which only a spell ever runs. The card does nothing, ever, and nothing reports that.
+- **Bottom:** `[trigger: deathrattle] + [aura: Deathrattles ×2]`. It reads "Deathrattle: Your minions trigger their Deathrattles twice." In the reference the trigger gets no effects, so on death the log shows a `Triggered(deathrattle)` followed by nothing; and the aura is in force the whole time the minion is in play, exactly like Baron's, because an aura clause isn't attached to the trigger before it.
+
+Neither result is what the text says to a player, and `assemble` accepted both. The function is closed over *lists of clauses*, but a card's text has a grammar (an effect belongs to a trigger; a trigger needs an effect; an aura stands alone), and cutting two well-formed texts in half and swapping can leave that grammar. `text == base.text` is the only check, and it is about a different question.
+
+A design has three honest ways out:
+
+- **Curate the pool**, so that every pair of halves fits. This is what the real game does: the base comes from minions "whose effects can be split in half" ([Assemble](https://hearthstone.wiki.gg/wiki/Assemble), paraphrased in Observe). Curation is data, and it costs designer time per card.
+- **Check the result.** Write `wellFormed(text)` and have the `assemble` effect offer only donors and halves whose result passes. The rule lives in one function, next to the must-change rule. Alexis King's [*Parse, don't validate*](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/) argues for going one step further: make the compiled form unable to represent a trigger without effects, so the check can't be skipped.
+- **Give every list a meaning.** Decide that an aura after a trigger is granted when the trigger fires, and that a bare effect on a minion is a Battlecry. Now nothing is ill-formed, and you have designed several new mechanics by accident.
+
+The course takes none of them: its pool is small and its tests only assemble pairs that fit. Write `wellFormed` for the course's four clause kinds; it is about ten lines. Then count how many of the base × donor × half combinations in `cards/` it rejects.
+
+
+
+2. `course/catalog.json` has a `text` field for every card: the words a player reads. Your engine has the same card as `text` clauses. Write `render(def)`, a function from the data to the English sentence, and test it against the catalog for every card in `cards/`.
+
+Discussion
+
+A dozen cases cover the course: one phrase per trigger ("Deathrattle:", "Whenever this minion takes damage,"), one per effect, one per target, and the joining punctuation. The test is a loop over the catalog.
+
+It is worth doing for what it proves. The card file is now the single source of both what the card *does* and what it *says*, so the two can't drift apart. And Assemble gets its rules text for free: render the assembled clause list. In the real game the assembled minion's "text changes"; with behaviour as data, that is the same operation as changing what it does. Try `render` on the two cards from question 1. A renderer that can't produce a sensible sentence is a second detector for ill-formed text.
+
+You will find cards where the catalog's sentence and your rendering can't match word for word (Harvest Golem's "Summon a 2/1 Damaged Golem." needs the token's stats). Decide whether `render` looks the token up or the test allows a difference, and note which. That is the gap Argentum's "inspectable AST nodes" in Further reading are there to close.
+
+
+
+3. Every random or player choice in your engine goes through the chooser. Write a chooser that records each answer it gives, play a short game with three Assembles, then start a new game and feed the recorded answers back in. Do you get the same log? What else, besides the answers, did you have to record?
+
+Discussion
+
+The same log, provided you also recorded the *inputs*: every harness call that started a Sequence, in order, with its arguments. Inputs plus choices is everything that enters the engine from outside. Everything else is computed. That makes a game **replayable** from a short list, and it is why the chooser was worth the trouble: one port for nondeterminism instead of a `random()` call wherever a card needed one.
+
+Two things break replay, and both are worth trying. Iterating over an unordered map somewhere in the engine (TypeScript objects keep insertion order; a Rust `HashMap` does not, so on the Rust track this can bite) and ids that depend on something outside the game. If your replayed log differs, the first differing event points at the place.
+
+Replay is the foundation for three things the course doesn't build: saving a game as its inputs, undo by replaying all but the last input, and reproducing a bug report exactly. Robert Nystrom's [*Command*](https://gameprogrammingpatterns.com/command.html) chapter makes the same observation about recorded input: "We can take the player's input, push it over the network to another machine, and then replay it." [Module 11](../11-break-your-engine/lesson.md) uses replay to build a solver.
+
+
+
## Further reading
- Robert Nystrom, [*Bytecode*](https://gameprogrammingpatterns.com/bytecode.html), *Game Programming Patterns*: read it for the case for behaviour as data and for the sandboxing argument. The course stops at the Interpreter pattern the chapter starts from, on purpose.
- Argentum, [`docs/architecture-principles.md`](https://github.com/wingedsheep/argentum-engine/blob/main/docs/architecture-principles.md), §1.1 ("Cards are pure, serializable data structures") and §1.2 (amounts as inspectable AST nodes). Compare §1.2 with the course's integer `amount`: what would "deal damage equal to your hand size" need?
- Forge wiki, [Card scripting API](https://github.com/Card-Forge/forge/wiki/Card-scripting-API) and [Creating a custom Card](https://github.com/Card-Forge/forge/wiki/Creating-a-custom-Card): abilities as data at the scale of a whole game's card pool, with triggers, statics and replacement effects as separate kinds of line.
- [Assemble, hearthstone.wiki.gg](https://hearthstone.wiki.gg/wiki/Assemble): the rules, the 13 cards with the keyword, and what the page leaves undefined. Check whether it defines "half" by the time you read it.
+- Alexis King, [*Parse, don't validate*](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/) (2019). Written about Haskell types, and it applies to the `register` step of this module: turn loose input into a form that can't be wrong, once, at the boundary. Read it with Stress it question 1 in mind.