// Module 10: SpellGrid. Each test matches a heading in scenarios.md. use engine::harness::*; /// A level from its JSON text (the course's level format). fn level_json(json: &str) -> LevelDef { serde_json::from_str(json).expect("a level in the course's level format") } /// A level `width` wide and `height` high: text written as `(word, x, y)`, other /// objects as `(kind, x, y)`, minion cards as `(card, x, y)`, and a hand. fn level( width: i32, height: i32, words: &[(&str, i32, i32)], objects: &[(&str, i32, i32)], cards: &[(&str, i32, i32)], hand: &[&str], ) -> LevelDef { let words = words.iter().map(|&(w, x, y)| (format!("text:{w}"), x, y)); let objects = objects.iter().map(|&(k, x, y)| (k.to_string(), x, y)); LevelDef { width, height, objects: words .chain(objects) .map(|(kind, x, y)| LevelObject { kind, x, y }) .collect(), cards: cards .iter() .map(|&(card, x, y)| LevelCard { card: card.to_string(), x, y, }) .collect(), hand: hand.iter().map(|c| c.to_string()).collect(), } } /// The minion on tile (x, y); fails the test if there is none. fn minion(g: &SpellGrid, x: i32, y: i32) -> Id { g.minion_at(x, y) .unwrap_or_else(|| panic!("no minion at ({x}, {y})")) } /// How many times `source`'s Deathrattle triggered in the log. fn deathrattles(g: &SpellGrid, source: Id) -> usize { g.log() .iter() .filter(|e| { matches!(e, GameEvent::Triggered { source: s, trigger } if *s == source && trigger == "deathrattle") }) .count() } fn has(rules: &[String], rule: &str) -> bool { rules.iter().any(|r| r == rule) } #[test] fn test_10_spatial_rule_feeds_interceptor() { let mut g = new_spell_grid( level( 6, 3, &[("DEATHRATTLE", 0, 0), ("IS", 1, 0)], &[], &[("Powder Keg", 0, 2), ("Bramble Wall", 2, 2)], &["Spark"], ), vec![], ); let keg = minion(&g, 0, 2); let bramble = minion(&g, 2, 2); assert!(!has(&g.active_rules(), "DEATHRATTLE IS DOUBLE")); g.place("text:DOUBLE", 2, 0); assert!(has(&g.active_rules(), "DEATHRATTLE IS DOUBLE")); g.clear_log(); g.cast("Spark", Some(keg)); assert_eq!(deathrattles(&g, keg), 2); assert_eq!(g.health(bramble), 1); assert_eq!(g.pos_of(keg), None); assert_eq!(g.pos_of(bramble), Some((2, 2))); assert_eq!(g.hand(), Vec::::new()); } #[test] fn test_10_not_cancels_spatial_modifier() { let mut g = new_spell_grid( level( 6, 4, &[ ("DEATHRATTLE", 0, 0), ("IS", 1, 0), ("DOUBLE", 2, 0), ("DEATHRATTLE", 0, 1), ("IS", 1, 1), ("NOT", 2, 1), ("DOUBLE", 3, 1), ], &[], &[ ("Grave Warden", 0, 3), ("Powder Keg", 1, 3), ("Bramble Wall", 2, 3), ], &["Spark"], ), vec![], ); let warden = minion(&g, 0, 3); let keg = minion(&g, 1, 3); let bramble = minion(&g, 2, 3); assert!(has(&g.rules(), "DEATHRATTLE IS DOUBLE")); assert!(has(&g.rules(), "DEATHRATTLE IS NOT DOUBLE")); assert!(has(&g.active_rules(), "DEATHRATTLE IS NOT DOUBLE")); assert!(!has(&g.active_rules(), "DEATHRATTLE IS DOUBLE")); g.clear_log(); g.cast("Spark", Some(keg)); assert_eq!(deathrattles(&g, keg), 1); assert_eq!(g.health(bramble), 2); assert_eq!(g.pos_of(warden), None); } #[test] fn test_10_grid_and_card_doublers_take_max() { let mut g = new_spell_grid( level( 6, 3, &[("DEATHRATTLE", 0, 0), ("IS", 1, 0), ("DOUBLE", 2, 0)], &[], &[ ("Grave Warden", 0, 2), ("Powder Keg", 1, 2), ("Bramble Wall", 2, 2), ], &["Spark"], ), vec![], ); let keg = minion(&g, 1, 2); let bramble = minion(&g, 2, 2); g.clear_log(); g.cast("Spark", Some(keg)); assert_eq!(deathrattles(&g, keg), 2); assert_eq!(g.health(bramble), 1); } #[test] fn test_10_hazard_feeds_death_step() { let mut g = new_spell_grid( level( 5, 4, &[ ("KEG", 0, 0), ("IS", 1, 0), ("YOU", 2, 0), ("AND", 3, 0), ("MELT", 4, 0), ("LAVA", 0, 1), ("IS", 1, 1), ("HOT", 2, 1), ], &[("lava", 2, 3)], &[("Powder Keg", 1, 3), ("Bramble Wall", 4, 3)], &[], ), vec![], ); let keg = minion(&g, 1, 3); let bramble = minion(&g, 4, 3); g.clear_log(); g.step(Dir::Right); assert_eq!( g.log(), vec![ GameEvent::Destroyed { id: keg }, GameEvent::DeathPhase { round: 1 }, GameEvent::Triggered { source: keg, trigger: "deathrattle".to_string() }, GameEvent::Damaged { target: bramble, amount: 1 }, ] ); assert_eq!(g.pos_of(keg), None); assert_eq!(g.kinds_at(2, 3), vec!["lava"]); assert_eq!(g.health(bramble), 2); assert!(g.lost()); } #[test] fn test_10_spawned_text_triggers_reparse() { let mut g = new_spell_grid( level( 5, 3, &[ ("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0), ("BABA", 0, 1), ("IS", 1, 1), ], &[], &[("Rune Scribe", 2, 1)], &["Spark"], ), vec![], ); let scribe = minion(&g, 2, 1); let baba = g.place("baba", 4, 2); assert!(!has(&g.rules(), "BABA IS WIN")); g.cast("Spark", Some(scribe)); assert_eq!(g.kinds_at(2, 1), vec!["text:WIN"]); assert!(has(&g.rules(), "BABA IS WIN")); assert!(g.has_property(baba, "WIN")); assert!(!g.won()); g.step(Dir::Left); assert!(g.won()); } #[test] fn test_10_targeted_spell_needs_target() { let mut g = new_spell_grid( level(3, 1, &[], &[], &[("Bramble Wall", 0, 0)], &["Spark"]), vec![], ); let bramble = minion(&g, 0, 0); let cast = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| g.cast("Spark", None))); assert!(cast.is_err()); assert_eq!(g.hand(), vec!["Spark"]); assert_eq!(g.health(bramble), 3); g.cast("Spark", Some(bramble)); assert_eq!(g.health(bramble), 2); assert_eq!(g.hand(), Vec::::new()); } #[test] fn test_10_level_1_solution() { let mut g = new_spell_grid( level_json(include_str!("../levels/l1-double-tap.json")), vec![], ); let keg = minion(&g, 0, 4); let bramble = minion(&g, 5, 6); g.step(Dir::Up); g.step(Dir::Up); assert!(has(&g.active_rules(), "DEATHRATTLE IS DOUBLE")); g.cast("Whirlwind", None); assert_eq!(deathrattles(&g, keg), 2); assert_eq!(g.pos_of(bramble), None); for dir in [ Dir::Down, Dir::Down, Dir::Left, Dir::Down, Dir::Down, Dir::Right, ] { g.step(dir); } assert!(!g.won()); assert_eq!(g.kinds_at(5, 6), vec!["baba"]); g.step(Dir::Right); assert!(g.won()); } #[test] fn test_10_level_2_solution() { let mut g = new_spell_grid( level_json(include_str!("../levels/l2-spliced-corpse.json")), vec![], ); g.set_chooser(|kind, _| { match kind { "base" => "Ember Moth", "donor" => "Rune Scribe", _ => "bottom", } .to_string() }); g.cast("Graft", None); assert_eq!(g.hand(), vec!["Ember Moth (Assembled)"]); let moth = g.play("Ember Moth (Assembled)", 4, 3); assert_eq!(g.kinds_at(4, 3), vec!["moth"]); assert_eq!(g.name_of(moth), "Ember Moth (Assembled)"); g.step(Dir::Left); g.step(Dir::Left); assert_eq!(g.pos_of(moth), None); assert_eq!(g.kinds_at(2, 3), vec!["lava", "text:WIN"]); assert!(has(&g.rules(), "BABA IS WIN")); assert!(g.won()); } /// Level 3's first input, one Whirlwind, after clearing the log. Returns the /// knight, keg, warden and hedge. fn cast_level_3_whirlwind(g: &mut SpellGrid) -> (Id, Id, Id, Id) { let ids = ( minion(g, 0, 4), minion(g, 1, 5), minion(g, 2, 5), minion(g, 4, 4), ); g.clear_log(); g.cast("Whirlwind", None); ids } #[test] fn test_10_level_3_solution() { let mut g = new_spell_grid( level_json(include_str!("../levels/l3-simultaneous-paradox.json")), vec![], ); let (knight, keg, warden, hedge) = cast_level_3_whirlwind(&mut g); assert_eq!(deathrattles(&g, keg), 1); assert_eq!(g.health(knight), 1); assert_eq!(g.pos_of(hedge), None); assert_eq!(g.pos_of(keg), None); assert_eq!(g.pos_of(warden), None); for _ in 0..5 { g.step(Dir::Right); } assert!(g.won()); } #[test] fn test_10_level_3_naive_would_differ() { let mut g = new_spell_grid( level_json(include_str!("../levels/l3-simultaneous-paradox.json")), vec![], ); g.with_naive_aura_timing(); let (knight, keg, _, _) = cast_level_3_whirlwind(&mut g); for _ in 0..5 { g.step(Dir::Right); } assert_eq!(deathrattles(&g, keg), 2); assert_eq!(g.pos_of(knight), None); assert!(g.lost()); assert!(!g.won()); }