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>
10 KiB
Module 12 scenarios: Combat
Each heading is a test name. The Rust test lives in tests/rust/test_12.rs and the TypeScript test in tests/ts/test_12.test.ts; both state exactly what is written here. Player 0 is the first player, player 1 the second. Minions are summoned in the order listed, so the first one listed has the lowest order of play; each test says whose each minion is. Each test calls clearLog() after its setup, so log() holds only the events of the actions under test. Every test starts with newGame and a deck of 5 Test Filler for each player.
This module adds three harness functions:
orderAttack(attacker, defender)runs one Sequence:attackerattacksdefender. If the attack is illegal, nothing happens: TypeScript throws anErrorwhose message iscanAttack's reason, and Rust returnsErr(reason).canAttack(attacker, defender)returns why the attack is illegal (a string), ornull(Rust:None) if it is legal.endTurn(player)runs one Sequence: the end ofplayer's turn.
The rules of an attack:
- Legality. The attacker is a minion in play. The defender is an enemy minion in play or the enemy hero. The attacker's Attack is more than 0. While the defender's side has a minion with Taunt, only a minion with Taunt may be attacked: the reason then contains
Taunt. A reason for 0 Attack containsAttack. - Damage. The attacker deals its Attack to the defender and, at the same time, the defender deals its Attack to the attacker: one Damage action with two targets, each with its own amount, so both
Damagedevents come before any trigger. As for all area damage, theDamagedevents are in order of play. A hero has no Attack and deals nothing back, nor does a minion with 0 Attack. - Priority. The triggers answering the defender's
Damagedevent go before those answering the attacker's, whatever their order of play. Within each, the usual order holds. - Then the usual end of the outermost Phase: the dead leave play in one Death Creation Step, and their Deathrattles resolve in a Death Phase.
The end of a turn: every trigger of player's minions that listens for endOfTurn responds, in order of play, and resolves as any trigger does. Its Triggered event says trigger: "endOfTurn". A trigger multiplier for endOfTurn (Drakkari Enchanter) is an aura and follows the Aura Update schedule of Module 4. The course has no mana, no limit of one attack per turn and no summoning sickness.
Cards used (text from course/catalog.json):
Loot Hoarder, 2/1: "Deathrattle: Draw a card."Acolyte of Pain, 1/4: "Whenever this minion takes damage, draw a card."Harvest Golem, 2/3: "Deathrattle: Summon a 2/1 Damaged Golem."Unstable Ghoul, 1/3: "Taunt. Deathrattle: Deal 1 damage to all minions."Armorsmith, 1/4: "Whenever a friendly minion takes damage, gain 1 Armor."Bolf Ramshield, 3/9: "Whenever your hero takes damage, this minion takes it instead."Drakkari Enchanter, 1/5: "Your end of turn effects trigger twice."Test Filler(fictional), 1/1, no text.Test Bell(fictional), 0/3: "At the end of your turn, deal 1 damage to the enemy hero."Test Conjurer(fictional, written as data in the test), 1/1: "At the end of your turn, summon a Drakkari Enchanter."
test_12_combat_is_simultaneous
- Setup: Summon
Loot Hoarderfor player 0 (id A), thenLoot Hoarderfor player 1 (id D). - Action:
orderAttack(A, D). - Assertions:
- Call the card in
hand(0)C0 and the card inhand(1)C1. - The log is exactly, in this order:
Damaged{target: A, amount: 2},Damaged{target: D, amount: 2},Destroyed{id: A},Destroyed{id: D},DeathPhase{round: 1},Triggered{source: A, trigger: "deathrattle"},Drew{player: 0, id: C0},Triggered{source: D, trigger: "deathrattle"},Drew{player: 1, id: C1}. zoneOf(A)andzoneOf(D)are graveyard.- Both Hoarders deal their damage, though each one's damage kills the other: the damage is simultaneous. Both die in the same Death Creation Step, and their Deathrattles resolve in order of play in one Death Phase.
- Call the card in
test_12_defender_triggers_first
- Setup: Summon
Acolyte of Painfor player 0 (id A), thenAcolyte of Painfor player 1 (id D). The attacker, A, is older. - Action 1:
orderAttack(A, D). - Assertions 1:
- Call the card in
hand(0)C0 and the card inhand(1)C1. - The log is exactly, in this order:
Damaged{target: A, amount: 1},Damaged{target: D, amount: 1},Triggered{source: D, trigger: "damaged"},Drew{player: 1, id: C1},Triggered{source: A, trigger: "damaged"},Drew{player: 0, id: C0}. - The
Damagedevents are in order of play, but the defender's trigger goes first. In order of play alone, A's would.
- Call the card in
- Action 2:
clearLog(), thenorderAttack(D, A): now the attacker is the younger one. - Assertions 2:
- The sources of the
Triggeredevents, in order, are A, D: the defender's first again. health(A)andhealth(D)are both 2.
- The sources of the
test_12_taunt_restricts_targets
- Setup: Summon
Loot Hoarderfor player 1 (id H), thenUnstable Ghoulfor player 1 (id T, Taunt), thenHarvest Golemfor player 0 (id A). - Assertions 1:
canAttack(A, H)is a string containingTaunt.canAttack(A, hero(1))is a string containingTaunt: Taunt protects the hero too.canAttack(A, T)is null.
- Action 1:
orderAttack(A, H). - Assertions 2: it fails with
canAttack(A, H)'s reason (TypeScript: it throws anErrorwith that message; Rust: it returnsErrwith that string). The log is empty andhealth(H)is 1: nothing happened. - Action 2:
orderAttack(A, T). - Assertions 3: the log is exactly
Damaged{target: T, amount: 2},Damaged{target: A, amount: 1}(order of play: T is older).health(T)is 1 andhealth(A)is 2.
test_12_zero_attack_cannot_attack
- Setup: Summon
Test Bellfor player 0 (id B, 0 Attack), thenTest Fillerfor player 0 (id F0), thenTest Fillerfor player 1 (id F1). - Assertions:
canAttack(B, F1)is a string containingAttack, andorderAttack(B, F1)fails.canAttack(F0, B)is not null: a friendly minion can't be attacked.canAttack(hero(0), F1)is not null: in the course only minions attack.canAttack(F0, F1)andcanAttack(F0, hero(1))are null.- The log is empty.
test_12_attack_hero_through_armor
- Setup: Summon
Armorsmithfor player 1 (id S).dealDamage(S, 1)twice, which gives player 1 2 Armor, thendestroy(S). SummonBolf Ramshieldfor player 0 (id B, 3 Attack).armor(1)is 2. - Action:
orderAttack(B, hero(1)). - Assertions:
- The log is exactly
ArmorLost{player: 1, amount: 2},Damaged{target: hero(1), amount: 3}. Combat damage to a hero goes throughDAMAGE_STEPSlike any other damage (Module 4). armor(1)is 0,health(hero(1))is 29.health(B)is 9: the hero dealt no damage back.
- The log is exactly
test_12_redirect_only_for_the_hero
- Setup: Summon
Bolf Ramshieldfor player 1 (id R), thenLoot Hoarderfor player 1 (id H), thenHarvest Golemfor player 0 (id A). - Action 1:
orderAttack(A, H). - Assertions 1:
- Call the card in
hand(1)C. - The log is exactly, in this order:
Damaged{target: H, amount: 2},Damaged{target: A, amount: 2},Destroyed{id: H},DeathPhase{round: 1},Triggered{source: H, trigger: "deathrattle"},Drew{player: 1, id: C}. health(R)is 9: Bolf's redirect is for damage to its owner's hero, and this damage was aimed at a minion.
- Call the card in
- Action 2:
clearLog(), thenorderAttack(A, hero(1)). - Assertions 2:
- The log is exactly
Damaged{target: R, amount: 2}: the redirect takes the attack's damage. health(hero(1))is 30,health(R)is 7.health(A)is 1: the Golem took no damage. Bolf took the damage in the hero's place, but Bolf was not the defender, so it deals nothing back.
- The log is exactly
test_12_end_of_turn_trigger
- Setup: Summon
Test Bellfor player 0 (id B). - Action 1:
endTurn(1). - Assertions 1: the log is empty. The Bell answers only the end of its owner's turn.
- Action 2:
endTurn(0). - Assertions 2: the log is exactly
Triggered{source: B, trigger: "endOfTurn"},Damaged{target: hero(1), amount: 1}.health(hero(1))is 29 andhealth(hero(0))is 30.
test_12_drakkari_doubles_end_of_turn
- Setup: Summon
Test Bellfor player 0 (id B0), thenDrakkari Enchanterfor player 0, thenTest Bellfor player 1 (id B1). - Action 1:
endTurn(0). - Assertions 1: the log is exactly
Triggered{source: B0, trigger: "endOfTurn"},Damaged{target: hero(1), amount: 1},Triggered{source: B0, trigger: "endOfTurn"},Damaged{target: hero(1), amount: 1}. The Bell's trigger is queued once and activates twice, back to back, as Baron's Deathrattles do. - Action 2:
clearLog(), thenendTurn(1). - Assertions 2: the log is exactly
Triggered{source: B1, trigger: "endOfTurn"},Damaged{target: hero(0), amount: 1}. Drakkari doubles its owner's end of turn effects only.health(hero(1))is 28 andhealth(hero(0))is 29.
test_12_drakkari_summoned_at_end_of_turn
Test Conjurer is registered in the test with registerCards: a minion, 1/1, {trigger: endOfTurn} then {effect: summon "Drakkari Enchanter", count 1}.
- Case A, setup: Summon
Test Conjurerfor player 0 (id C), thenTest Bellfor player 0 (id B). - Action:
endTurn(0). - Assertions A:
- The log has exactly one
Summonedevent; call its id K.nameOf(K)is"Drakkari Enchanter". - The log is exactly, in this order:
Triggered{source: C, trigger: "endOfTurn"},Summoned{id: K},Triggered{source: B, trigger: "endOfTurn"},Damaged{target: hero(1), amount: 1},Triggered{source: B, trigger: "endOfTurn"},Damaged{target: hero(1), amount: 1}. - The Conjurer's own entry counted its activations before Drakkari existed, so it activates once. A summon runs an Aura Update, so when the Bell's entry starts, Drakkari is in the snapshot and the Bell activates twice.
- The log has exactly one
- Case B, a new game, setup: Summon
Test Bellfor player 0 (id B), thenTest Conjurerfor player 0 (id C). - Action:
endTurn(0). - Assertions B:
- The log has exactly one
Summonedevent; call its id K. - The log is exactly, in this order:
Triggered{source: B, trigger: "endOfTurn"},Damaged{target: hero(1), amount: 1},Triggered{source: C, trigger: "endOfTurn"},Summoned{id: K}. health(hero(1))is 29. The Bell's entry started before any Drakkari existed.
- The log has exactly one