diff --git a/course/04-interceptors/lesson.md b/course/04-interceptors/lesson.md index 646eb91..4670ce7 100644 --- a/course/04-interceptors/lesson.md +++ b/course/04-interceptors/lesson.md @@ -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
Discussion -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** diff --git a/course/04-interceptors/scenarios.md b/course/04-interceptors/scenarios.md index 170f427..4f5c662 100644 --- a/course/04-interceptors/scenarios.md +++ b/course/04-interceptors/scenarios.md @@ -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 diff --git a/course/04-interceptors/tests/rust/test_04.rs b/course/04-interceptors/tests/rust/test_04.rs index 473f103..d20f383 100644 --- a/course/04-interceptors/tests/rust/test_04.rs +++ b/course/04-interceptors/tests/rust/test_04.rs @@ -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 = 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); } diff --git a/course/04-interceptors/tests/ts/test_04.test.ts b/course/04-interceptors/tests/ts/test_04.test.ts index 560e195..9981fb5 100644 --- a/course/04-interceptors/tests/ts/test_04.test.ts +++ b/course/04-interceptors/tests/ts/test_04.test.ts @@ -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); });