// Module 12: Combat. Each test matches a heading in scenarios.md. use engine::harness::*; fn game_with_decks() -> Game { new_game(GameOpts { decks: Some([ vec!["Test Filler".to_string(); 5], vec!["Test Filler".to_string(); 5], ]), }) } 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 card(json: &str) -> CardDef { serde_json::from_str(json).unwrap() } /// The `source` of each `Triggered` event, in log order. fn triggered_sources(log: &[GameEvent]) -> Vec { log.iter() .filter_map(|e| match e { GameEvent::Triggered { source, .. } => Some(*source), _ => None, }) .collect() } /// The id of the one `Summoned` event in a log. fn summoned(log: &[GameEvent]) -> Id { let ids: Vec = log .iter() .filter_map(|e| match e { GameEvent::Summoned { id } => Some(*id), _ => None, }) .collect(); assert_eq!(ids.len(), 1, "{log:?}"); ids[0] } #[test] fn test_12_combat_is_simultaneous() { let mut g = game_with_decks(); let a = g.summon("Loot Hoarder", 0); let d = g.summon("Loot Hoarder", 1); g.clear_log(); g.order_attack(a, d).unwrap(); let (c0, c1) = (g.hand(0)[0], g.hand(1)[0]); assert_eq!( g.log(), vec![ damaged(a, 2), damaged(d, 2), GameEvent::Destroyed { id: a }, GameEvent::Destroyed { id: d }, GameEvent::DeathPhase { round: 1 }, triggered(a, "deathrattle"), GameEvent::Drew { player: 0, id: c0 }, triggered(d, "deathrattle"), GameEvent::Drew { player: 1, id: c1 }, ] ); assert_eq!(g.zone_of(a), Zone::Graveyard); assert_eq!(g.zone_of(d), Zone::Graveyard); } #[test] fn test_12_defender_triggers_first() { let mut g = game_with_decks(); let a = g.summon("Acolyte of Pain", 0); let d = g.summon("Acolyte of Pain", 1); g.clear_log(); // The attacker is older: order of play alone would put its trigger first. g.order_attack(a, d).unwrap(); let (c0, c1) = (g.hand(0)[0], g.hand(1)[0]); assert_eq!( g.log(), vec![ damaged(a, 1), damaged(d, 1), triggered(d, "damaged"), GameEvent::Drew { player: 1, id: c1 }, triggered(a, "damaged"), GameEvent::Drew { player: 0, id: c0 }, ] ); // The other way round: the attacker is younger, and the defender still goes first. g.clear_log(); g.order_attack(d, a).unwrap(); assert_eq!(triggered_sources(&g.log()), vec![a, d]); assert_eq!(g.health(a), 2); assert_eq!(g.health(d), 2); } #[test] fn test_12_taunt_restricts_targets() { let mut g = game_with_decks(); let h = g.summon("Loot Hoarder", 1); let t = g.summon("Unstable Ghoul", 1); let a = g.summon("Harvest Golem", 0); let enemy_hero = g.hero(1); g.clear_log(); let reason = g.can_attack(a, h).expect("the Hoarder is behind a Taunt"); assert!(reason.contains("Taunt"), "{reason}"); let reason = g .can_attack(a, enemy_hero) .expect("the hero is behind a Taunt"); assert!(reason.contains("Taunt"), "{reason}"); assert_eq!(g.can_attack(a, t), None); assert_eq!(g.order_attack(a, h), Err(g.can_attack(a, h).unwrap())); assert_eq!(g.log(), vec![]); assert_eq!(g.health(h), 1); g.order_attack(a, t).unwrap(); assert_eq!(g.log(), vec![damaged(t, 2), damaged(a, 1)]); assert_eq!(g.health(t), 1); assert_eq!(g.health(a), 2); } #[test] fn test_12_zero_attack_cannot_attack() { let mut g = game_with_decks(); let bell = g.summon("Test Bell", 0); let f0 = g.summon("Test Filler", 0); let f1 = g.summon("Test Filler", 1); let (hero0, hero1) = (g.hero(0), g.hero(1)); g.clear_log(); let reason = g.can_attack(bell, f1).expect("0 Attack"); assert!(reason.contains("Attack"), "{reason}"); assert!(g.order_attack(bell, f1).is_err()); assert!(g.can_attack(f0, bell).is_some()); assert!(g.can_attack(hero0, f1).is_some()); assert_eq!(g.can_attack(f0, f1), None); assert_eq!(g.can_attack(f0, hero1), None); assert_eq!(g.log(), vec![]); } #[test] fn test_12_attack_hero_through_armor() { let mut g = game_with_decks(); let s = g.summon("Armorsmith", 1); g.deal_damage(s, 1); g.deal_damage(s, 1); g.destroy(s); let b = g.summon("Bolf Ramshield", 0); let enemy_hero = g.hero(1); assert_eq!(g.armor(1), 2); g.clear_log(); g.order_attack(b, enemy_hero).unwrap(); assert_eq!( g.log(), vec![ GameEvent::ArmorLost { player: 1, amount: 2 }, damaged(enemy_hero, 3), ] ); assert_eq!(g.armor(1), 0); assert_eq!(g.health(enemy_hero), 29); assert_eq!(g.health(b), 9); } #[test] fn test_12_redirect_only_for_the_hero() { let mut g = game_with_decks(); let r = g.summon("Bolf Ramshield", 1); let h = g.summon("Loot Hoarder", 1); let a = g.summon("Harvest Golem", 0); let enemy_hero = g.hero(1); g.clear_log(); g.order_attack(a, h).unwrap(); let c = g.hand(1)[0]; assert_eq!( g.log(), vec![ damaged(h, 2), damaged(a, 2), GameEvent::Destroyed { id: h }, GameEvent::DeathPhase { round: 1 }, triggered(h, "deathrattle"), GameEvent::Drew { player: 1, id: c }, ] ); assert_eq!(g.health(r), 9); g.clear_log(); g.order_attack(a, enemy_hero).unwrap(); assert_eq!(g.log(), vec![damaged(r, 2)]); assert_eq!(g.health(enemy_hero), 30); assert_eq!(g.health(r), 7); assert_eq!(g.health(a), 1); } #[test] fn test_12_end_of_turn_trigger() { let mut g = game_with_decks(); let b = g.summon("Test Bell", 0); let (hero0, hero1) = (g.hero(0), g.hero(1)); g.clear_log(); g.end_turn(1); assert_eq!(g.log(), vec![]); g.end_turn(0); assert_eq!(g.log(), vec![triggered(b, "endOfTurn"), damaged(hero1, 1)]); assert_eq!(g.health(hero1), 29); assert_eq!(g.health(hero0), 30); } #[test] fn test_12_drakkari_doubles_end_of_turn() { let mut g = game_with_decks(); let b0 = g.summon("Test Bell", 0); g.summon("Drakkari Enchanter", 0); let b1 = g.summon("Test Bell", 1); let (hero0, hero1) = (g.hero(0), g.hero(1)); g.clear_log(); g.end_turn(0); assert_eq!( g.log(), vec![ triggered(b0, "endOfTurn"), damaged(hero1, 1), triggered(b0, "endOfTurn"), damaged(hero1, 1), ] ); // Drakkari doubles its owner's end of turn effects only. g.clear_log(); g.end_turn(1); assert_eq!(g.log(), vec![triggered(b1, "endOfTurn"), damaged(hero0, 1)]); assert_eq!(g.health(hero1), 28); assert_eq!(g.health(hero0), 29); } /// "At the end of your turn, summon a Drakkari Enchanter." const TEST_CONJURER: &str = r#"{ "name": "Test Conjurer", "type": "minion", "cost": 1, "attack": 1, "health": 1, "split": 1, "text": [{ "kind": "trigger", "on": "endOfTurn" }, { "kind": "effect", "effect": { "type": "summon", "card": "Drakkari Enchanter", "count": 1 } }] }"#; #[test] fn test_12_drakkari_summoned_at_end_of_turn() { // Conjurer first: the Bell's entry starts after Drakkari's summon updated the auras. let mut g = game_with_decks(); g.register_cards(vec![card(TEST_CONJURER)]); let c = g.summon("Test Conjurer", 0); let b = g.summon("Test Bell", 0); let hero1 = g.hero(1); g.clear_log(); g.end_turn(0); let k = summoned(&g.log()); assert_eq!(g.name_of(k), "Drakkari Enchanter"); assert_eq!( g.log(), vec![ triggered(c, "endOfTurn"), GameEvent::Summoned { id: k }, triggered(b, "endOfTurn"), damaged(hero1, 1), triggered(b, "endOfTurn"), damaged(hero1, 1), ] ); // Bell first: its entry has started, and counted, before any Drakkari exists. let mut g = game_with_decks(); g.register_cards(vec![card(TEST_CONJURER)]); let b = g.summon("Test Bell", 0); let c = g.summon("Test Conjurer", 0); let hero1 = g.hero(1); g.clear_log(); g.end_turn(0); let k = summoned(&g.log()); assert_eq!( g.log(), vec![ triggered(b, "endOfTurn"), damaged(hero1, 1), triggered(c, "endOfTurn"), GameEvent::Summoned { id: k }, ] ); assert_eq!(g.health(hero1), 29); }