From b8bf2b8503a26dfa9c4f0694fb13145ebacabf0b Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Tue, 3 Nov 2026 11:37:32 +0100 Subject: [PATCH] Campaign tests: every mission runs a simulated minute undecided; missions 1 and 2 are won by a scripted player Co-Authored-By: Claude Opus 5.5 --- src/campaign.rs | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/campaign.rs b/src/campaign.rs index 6ffa626..1e5925c 100644 --- a/src/campaign.rs +++ b/src/campaign.rs @@ -421,6 +421,89 @@ mod tests { } } + #[test] + fn missions_run_and_are_not_decided_at_once() { + for (i, d) in missions().iter().enumerate() { + let mut g = start_mission(i, None); + let m = g.mission.clone().unwrap(); + assert!(!m.goals.iter().all(|&goal| g.goal_done(goal)), "{}: goals done at the start", d.key); + let t = std::time::Instant::now(); + for _ in 0..(60 * 20) { + g.update(0.05); + } + println!("{}: 60 s simulated in {} ms, state {}, core {:.0}", d.key, t.elapsed().as_millis(), g.state, g.core_hp); + assert_eq!(g.state, 0, "{}: decided without the player", d.key); + } + } + + // A simple scripted player: digs a corridor to the shard, bridges rivers on the way and + // sends a few strong creatures along with the flag. + fn play_to_shard(i: usize) -> (u8, f32) { + let mut g = start_mission(i, None); + let (cx, cy) = g.map.core; + let shard = g.map.t.iter().position(|t| t.kind == Kind::Shard).unwrap(); + let path = g + .map + .search(idx(cx + 1, cy - 2), |j| { + let t = &g.map.t[j]; + match t.kind { + Kind::Rock | Kind::Core | Kind::Tower => None, + Kind::Water | Kind::Lava => Some(3), + k if t.solid() => Some(if t.reinforced { 6 } else { 2 }), + _ => Some(1), + } + }, |j| j == shard, false) + .expect("a way to the shard"); + for &j in &path { + if g.map.t[j].diggable() { + g.map.t[j].marked = true; + } + } + for k in [EK::Ogre, EK::Ogre, EK::Mage, EK::Ogre] { + g.spawn(k, false, cx as f32 + 4.5, cy as f32 + 1.5, 4); + } + g.gold = 3000.0; + // lair and mushroom garden at the sides of the start area + g.build_rect(1, cx - 3, cy - 3, cx - 2, cy + 5); + g.build_rect(2, cx + 4, cy - 3, cx + 5, cy + 5); + g.gold = 2500.0; // as if the player had mined some gold + let mut t = 0.0; + while t < 1500.0 && g.state == 0 { + for _ in 0..20 { + g.update(0.05); + } + t += 1.0; + // fallen imps are replaced + if t as i32 % 10 == 0 && g.worker_count() < 4 { + g.summon(cx as f32 + 1.5, cy as f32 - 1.5); + } + // bridges: every few seconds try to bridge the water on the path + if t as i32 % 5 == 0 { + for &j in &path { + if g.map.t[j].liquid() { + let (x, y) = xy(j); + g.build_bridges(x, y, x, y); + } + } + // the creatures follow the digging: flag at the furthest open tile of the path + if let Some(&j) = path.iter().rev().find(|&&j| g.map.t[j].walkable()) { + let (x, y) = xy(j); + g.set_flag(x, y); + } + } + } + (g.state, t) + } + + #[test] + fn first_missions_can_be_won() { + for i in [0, 1] { + let (state, t) = play_to_shard(i); + println!("mission {}: state {} after {} s", i + 1, state, t); + assert_eq!(state, 1, "mission {} not won", i + 1); + } + } + #[test] fn carry_over_takes_the_strongest() { let mut g = start_mission(0, None);