fix: module 04 review round 1 (golem order, lesson accuracy)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-29 02:58:58 +01:00
co-authored by Claude Opus 5.5
parent bcdaca0cd2
commit 55fe5a71d7
4 changed files with 31 additions and 13 deletions
+4 -4
View File
@@ -17,7 +17,7 @@ Baron Rivendare, 4-mana 1/7 Legendary ([Baron Rivendare](https://hearthstone.wik
The card page adds the details that matter for an engine (same source):
- **Queued once, activated twice.** "The two activations of the Deathrattle do not queue/resolve separately." "The second activation of the Deathrattle is immediately after the first activation resolves", before the next queued trigger.
- **Queued once, activated twice.** The two activations do not queue or resolve separately: the Deathrattle is queued once and activates twice. "The second activation of the Deathrattle is immediately after the first activation resolves", before the next queued trigger.
- **Two Barons are not four.** "Rivendare's ability is not duplicated or multiplied by having more than one Baron Rivendare in play."
- **Dying with Baron.** "if he dies at the same time as a minion with a Deathrattle, the Deathrattle is not doubled."
- **Why.** "Baron Rivendare's effect, being an aura, only appears and disappears between Phases when Aura Update (Other) runs".
@@ -45,11 +45,11 @@ Write your answers down before you open the discussion, for example in `notes/04
<details><summary>Discussion</summary>
1. Hoarder, Hoarder, Golem, Golem. Baron survives the Whirlwind (1/7), so his aura is in force when the Death Phase starts. Each Deathrattle is queued once, and its two activations run back to back before the next entry in the Queue. The Hoarder draws twice, then the Golem summons twice. Both Damaged Golems go where the Golem stood: the first lands at that index, the second at the same index, which is now just right of the first.
1. Hoarder, Hoarder, Golem, Golem. Baron survives the Whirlwind (1/7), so his aura is in force when the Death Phase starts. Each Deathrattle is queued once, and its two activations run back to back before the next entry in the Queue. The Hoarder draws twice, then the Golem summons twice. Both Damaged Golems appear where the Golem stood.
2. Two. Baron's multiplier doesn't stack. If your Module 0 fix looped over the board and ran the Deathrattle once more per Baron, you drew three. If it checked "is there a Baron", you got two.
3. One. Both leave play in the same Death Creation Step; the Aura Update (Other) runs after the step, finds no Baron, and the Deathrattle resolves once. An engine gets this wrong if it decides "twice or once" while it creates each Death Event, or if it updates auras before the Death Creation Step. In both cases Baron is still in play at the moment of the decision, so the order of play of the two minions decides the answer, exactly as in your Module 0 engine.
3. One. Both leave play in the same Death Creation Step; the Aura Update (Other) runs after the step, finds no Baron, and the Deathrattle resolves once. An engine gets this wrong in two ways. If it decides "twice or once" while it creates each Death Event, Baron is still in play when the older Hoarder's event is created, so the order of play of the two minions decides the answer, exactly as in your Module 0 engine. If it updates auras before the Death Creation Step, Baron is still in the snapshot whatever the order, and the Deathrattle always doubles.
4. The Phase function (`react` in the reference): it takes each queued trigger and runs its effect. To decide how many times, it needs the trigger's kind (Deathrattle, Battlecry, ...), whose minion it is, and which "trigger N times" effects are in force. None of these name a card. "Which effects are in force" is something the cards in play *declare*, as data.
@@ -211,7 +211,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.
- **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 anything else can happen.
- **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.
> **How the real game differs**
+2 -1
View File
@@ -20,9 +20,10 @@ A trigger multiplier is an aura: data on a card that says "triggers of this kind
- Setup: Summon `Baron Rivendare` (id B), then `Loot Hoarder` (id H), then `Harvest Golem` (id G). `dealDamage(G, 2)`, which leaves G at 1 Health.
- Action: player 0 casts `Whirlwind`.
- Assertions:
- `hand(0)` has 2 cards; call them C1 and C2, in hand order. `board(0)` has 3 minions: B, then D1, then D2.
- `hand(0)` has 2 cards; call them C1 and C2, in hand order. The log has exactly 2 `Summoned` events; call their ids D1 and D2, in log order.
- The log is exactly, in this order: `SpellCast{card: "Whirlwind"}`, `Damaged{target: B, amount: 1}`, `Damaged{target: H, amount: 1}`, `Damaged{target: G, amount: 1}`, `Destroyed{id: H}`, `Destroyed{id: G}`, `DeathPhase{round: 1}`, `Triggered{source: H, trigger: "deathrattle"}`, `Drew{player: 0, id: C1}`, `Triggered{source: H, trigger: "deathrattle"}`, `Drew{player: 0, id: C2}`, `Triggered{source: G, trigger: "deathrattle"}`, `Summoned{id: D1}`, `Triggered{source: G, trigger: "deathrattle"}`, `Summoned{id: D2}`. Each Deathrattle is queued once and activates twice back to back: the Hoarder's two activations both finish before the Golem's first one starts.
- `nameOf(D1)` and `nameOf(D2)` are `"Damaged Golem"`.
- `board(0)` has 3 minions: B first, then D1 and D2 in either order. Both Damaged Golems appear where the Golem stood; this test doesn't fix their order relative to each other.
- `health(B)` is 6: Baron survives the Whirlwind, so his aura is still active when the Death Phase starts.
### test_04_simultaneous_death_not_doubled
+18 -4
View File
@@ -38,10 +38,16 @@ fn test_04_baron_doubles() {
let hand = g.hand(0);
assert_eq!(hand.len(), 2);
let (c1, c2) = (hand[0], hand[1]);
let board = g.board(0);
assert_eq!(board.len(), 3);
assert_eq!(board[0], b);
let (d1, d2) = (board[1], board[2]);
let summoned: Vec<Id> = g
.log()
.into_iter()
.filter_map(|e| match e {
GameEvent::Summoned { id } => Some(id),
_ => None,
})
.collect();
assert_eq!(summoned.len(), 2);
let (d1, d2) = (summoned[0], summoned[1]);
assert_eq!(
g.log(),
vec![
@@ -64,6 +70,14 @@ fn test_04_baron_doubles() {
);
assert_eq!(g.name_of(d1), "Damaged Golem");
assert_eq!(g.name_of(d2), "Damaged Golem");
let board = g.board(0);
assert_eq!(board.len(), 3);
assert_eq!(board[0], b);
let mut golems = vec![board[1], board[2]];
golems.sort();
let mut expected = vec![d1, d2];
expected.sort();
assert_eq!(golems, expected);
assert_eq!(g.health(b), 6);
}
@@ -17,10 +17,9 @@ test("test_04_baron_doubles", () => {
const hand = g.hand(0);
expect(hand).toHaveLength(2);
const [c1, c2] = hand;
const board = g.board(0);
expect(board).toHaveLength(3);
expect(board[0]).toBe(b);
const [, d1, d2] = board;
const summoned = g.log().flatMap((e) => (e.type === "Summoned" ? [e.id] : []));
expect(summoned).toHaveLength(2);
const [d1, d2] = summoned;
expect(g.log()).toEqual([
{ type: "SpellCast", card: "Whirlwind" },
{ type: "Damaged", target: b, amount: 1 },
@@ -40,6 +39,10 @@ test("test_04_baron_doubles", () => {
]);
expect(g.nameOf(d1)).toBe("Damaged Golem");
expect(g.nameOf(d2)).toBe("Damaged Golem");
const board = g.board(0);
expect(board).toHaveLength(3);
expect(board[0]).toBe(b);
expect([board[1], board[2]].sort((x, y) => x - y)).toEqual([d1, d2].sort((x, y) => x - y));
expect(g.health(b)).toBe(6);
});