Applying a Damage to one target now runs DAMAGE_STEPS = [redirect, armor]
first. A damageRedirect aura (Bolf Ramshield, fetched from the wiki) sends
damage meant for its owner's hero to the oldest such minion; Armor absorbs
damage to a hero before Health, logged as the new ArmorLost{player, amount}
just before a full-amount Damaged. Four new Module 4 tests; propagated to the
references of Modules 4-10 in both tracks; card schema, catalog, card JSON,
harness.md, Lessons 2, 4 and 5 updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
295 lines
7.3 KiB
Rust
295 lines
7.3 KiB
Rust
// Module 4: Interceptors and "trigger twice". Each test matches a heading in scenarios.md.
|
|
use engine::harness::*;
|
|
|
|
fn game_with_deck() -> Game {
|
|
new_game(GameOpts {
|
|
decks: Some([vec!["Test Filler".to_string(); 5], vec![]]),
|
|
})
|
|
}
|
|
|
|
fn damaged(target: Id, amount: i32) -> GameEvent {
|
|
GameEvent::Damaged { target, amount }
|
|
}
|
|
|
|
fn triggered(source: Id, trigger: &str) -> GameEvent {
|
|
GameEvent::Triggered {
|
|
source,
|
|
trigger: trigger.to_string(),
|
|
}
|
|
}
|
|
|
|
fn spell_cast(card: &str) -> GameEvent {
|
|
GameEvent::SpellCast {
|
|
card: card.to_string(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_04_baron_doubles() {
|
|
let mut g = game_with_deck();
|
|
let b = g.summon("Baron Rivendare", 0);
|
|
let h = g.summon("Loot Hoarder", 0);
|
|
let golem = g.summon("Harvest Golem", 0);
|
|
g.deal_damage(golem, 2);
|
|
g.clear_log();
|
|
|
|
g.cast("Whirlwind", 0, None);
|
|
|
|
let hand = g.hand(0);
|
|
assert_eq!(hand.len(), 2);
|
|
let (c1, c2) = (hand[0], hand[1]);
|
|
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![
|
|
spell_cast("Whirlwind"),
|
|
damaged(b, 1),
|
|
damaged(h, 1),
|
|
damaged(golem, 1),
|
|
GameEvent::Destroyed { id: h },
|
|
GameEvent::Destroyed { id: golem },
|
|
GameEvent::DeathPhase { round: 1 },
|
|
triggered(h, "deathrattle"),
|
|
GameEvent::Drew { player: 0, id: c1 },
|
|
triggered(h, "deathrattle"),
|
|
GameEvent::Drew { player: 0, id: c2 },
|
|
triggered(golem, "deathrattle"),
|
|
GameEvent::Summoned { id: d1 },
|
|
triggered(golem, "deathrattle"),
|
|
GameEvent::Summoned { id: d2 },
|
|
]
|
|
);
|
|
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);
|
|
}
|
|
|
|
#[test]
|
|
fn test_04_simultaneous_death_not_doubled() {
|
|
let mut g = game_with_deck();
|
|
let b = g.summon("Baron Rivendare", 0);
|
|
let h = g.summon("Loot Hoarder", 0);
|
|
g.clear_log();
|
|
|
|
g.cast("Twisting Nether", 0, None);
|
|
|
|
assert_eq!(g.hand(0).len(), 1);
|
|
let hoarder_triggers = g
|
|
.log()
|
|
.into_iter()
|
|
.filter(|e| *e == triggered(h, "deathrattle"))
|
|
.count();
|
|
assert_eq!(hoarder_triggers, 1);
|
|
assert_eq!(g.zone_of(b), Zone::Graveyard);
|
|
assert_eq!(g.zone_of(h), Zone::Graveyard);
|
|
}
|
|
|
|
#[test]
|
|
fn test_04_two_barons_do_not_stack() {
|
|
let mut g = game_with_deck();
|
|
g.summon("Baron Rivendare", 0);
|
|
g.summon("Baron Rivendare", 0);
|
|
let h = g.summon("Loot Hoarder", 0);
|
|
g.clear_log();
|
|
|
|
g.deal_damage(h, 1);
|
|
|
|
let hand = g.hand(0);
|
|
assert_eq!(hand.len(), 2);
|
|
let (c1, c2) = (hand[0], hand[1]);
|
|
assert_eq!(
|
|
g.log(),
|
|
vec![
|
|
damaged(h, 1),
|
|
GameEvent::Destroyed { id: h },
|
|
GameEvent::DeathPhase { round: 1 },
|
|
triggered(h, "deathrattle"),
|
|
GameEvent::Drew { player: 0, id: c1 },
|
|
triggered(h, "deathrattle"),
|
|
GameEvent::Drew { player: 0, id: c2 },
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_04_aura_timing_order() {
|
|
let mut g = game_with_deck();
|
|
let h = g.summon("Loot Hoarder", 0);
|
|
let b = g.summon("Baron Rivendare", 0);
|
|
g.clear_log();
|
|
|
|
g.cast("Twisting Nether", 0, None);
|
|
|
|
let hand = g.hand(0);
|
|
assert_eq!(hand.len(), 1);
|
|
let c = hand[0];
|
|
assert_eq!(
|
|
g.log(),
|
|
vec![
|
|
spell_cast("Twisting Nether"),
|
|
GameEvent::Destroyed { id: h },
|
|
GameEvent::Destroyed { id: b },
|
|
GameEvent::DeathPhase { round: 1 },
|
|
triggered(h, "deathrattle"),
|
|
GameEvent::Drew { player: 0, id: c },
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_04_battlecry_on_play() {
|
|
let mut g = game_with_deck();
|
|
let e = g.add_to_hand("Novice Engineer", 0);
|
|
g.clear_log();
|
|
|
|
g.play(e);
|
|
|
|
let hand = g.hand(0);
|
|
assert_eq!(hand.len(), 1);
|
|
let c = hand[0];
|
|
assert_eq!(
|
|
g.log(),
|
|
vec![
|
|
GameEvent::Summoned { id: e },
|
|
triggered(e, "battlecry"),
|
|
GameEvent::Drew { player: 0, id: c },
|
|
]
|
|
);
|
|
assert_eq!(g.zone_of(e), Zone::Board);
|
|
assert_eq!(g.board(0), vec![e]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_04_armor_absorbs_before_health() {
|
|
let mut g = game_with_deck();
|
|
let a = g.summon("Armorsmith", 0);
|
|
let r = g.summon("Test Reactor", 0);
|
|
g.deal_damage(a, 1);
|
|
g.deal_damage(a, 1);
|
|
let hero = g.hero(0);
|
|
assert_eq!(g.armor(0), 2);
|
|
g.clear_log();
|
|
|
|
g.deal_damage(hero, 1);
|
|
|
|
assert_eq!(
|
|
g.log(),
|
|
vec![
|
|
GameEvent::ArmorLost {
|
|
player: 0,
|
|
amount: 1
|
|
},
|
|
damaged(hero, 1),
|
|
triggered(r, "damaged"),
|
|
GameEvent::StatsChanged {
|
|
id: r,
|
|
attack: 2,
|
|
health: 5
|
|
},
|
|
]
|
|
);
|
|
assert_eq!(g.armor(0), 1);
|
|
assert_eq!(g.health(hero), 30);
|
|
g.clear_log();
|
|
|
|
g.deal_damage(hero, 5);
|
|
|
|
assert_eq!(
|
|
g.log(),
|
|
vec![
|
|
GameEvent::ArmorLost {
|
|
player: 0,
|
|
amount: 1
|
|
},
|
|
damaged(hero, 5),
|
|
triggered(r, "damaged"),
|
|
GameEvent::StatsChanged {
|
|
id: r,
|
|
attack: 3,
|
|
health: 5
|
|
},
|
|
]
|
|
);
|
|
assert_eq!(g.armor(0), 0);
|
|
assert_eq!(g.health(hero), 26);
|
|
}
|
|
|
|
#[test]
|
|
fn test_04_redirect_replaces_hero_damage() {
|
|
let mut g = game_with_deck();
|
|
let b = g.summon("Bolf Ramshield", 0);
|
|
let a = g.summon("Armorsmith", 0);
|
|
g.summon("Test Reactor", 0);
|
|
let hero = g.hero(0);
|
|
g.clear_log();
|
|
|
|
g.deal_damage(hero, 4);
|
|
|
|
assert_eq!(
|
|
g.log(),
|
|
vec![
|
|
damaged(b, 4),
|
|
triggered(a, "damaged"),
|
|
GameEvent::ArmorGained {
|
|
player: 0,
|
|
amount: 1
|
|
},
|
|
]
|
|
);
|
|
assert_eq!(g.health(hero), 30);
|
|
assert_eq!(g.health(b), 5);
|
|
assert_eq!(g.armor(0), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_04_redirect_applies_once() {
|
|
let mut g = game_with_deck();
|
|
let b1 = g.summon("Bolf Ramshield", 0);
|
|
let b2 = g.summon("Bolf Ramshield", 0);
|
|
let hero = g.hero(0);
|
|
g.clear_log();
|
|
|
|
g.deal_damage(hero, 3);
|
|
|
|
assert_eq!(g.log(), vec![damaged(b1, 3)]);
|
|
assert_eq!(g.health(b1), 6);
|
|
assert_eq!(g.health(b2), 9);
|
|
assert_eq!(g.health(hero), 30);
|
|
}
|
|
|
|
#[test]
|
|
fn test_04_redirect_before_armor() {
|
|
let mut g = game_with_deck();
|
|
let a = g.summon("Armorsmith", 0);
|
|
g.deal_damage(a, 1);
|
|
g.destroy(a);
|
|
let b = g.summon("Bolf Ramshield", 0);
|
|
let hero = g.hero(0);
|
|
assert_eq!(g.armor(0), 1);
|
|
g.clear_log();
|
|
|
|
g.deal_damage(hero, 2);
|
|
|
|
assert_eq!(g.log(), vec![damaged(b, 2)]);
|
|
assert_eq!(g.armor(0), 1);
|
|
assert_eq!(g.health(hero), 30);
|
|
assert_eq!(g.health(b), 7);
|
|
}
|