From cccb6b64c3f3a856cba92eec1c94433f5936c045 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Fri, 6 Nov 2026 09:54:19 +0100 Subject: [PATCH] AI masters build only rooms and towers they have researched Co-Authored-By: Claude Opus 5.5 --- src/ai.rs | 11 ++++++++++- src/rival.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/ai.rs b/src/ai.rs index 65c00ad..eb094d9 100644 --- a/src/ai.rs +++ b/src/ai.rs @@ -221,6 +221,10 @@ impl Game { // the next room of the build order, where there is free own floor for it fn ai_rooms(&mut self, cx: i32, cy: i32) { for &(room, w, h, n) in BUILD.iter() { + // only what this master has researched (the same rules as the player) + if !self.ai_knows(crate::ui::Tool::Room(room)) { + continue; + } let tiles = self.counts[room]; if tiles >= (w * h) as usize * n { continue; @@ -236,6 +240,11 @@ impl Game { } } + // has this master researched the tool (rooms, towers)? + fn ai_knows(&self, tool: crate::ui::Tool) -> bool { + crate::ui::TOOLS.iter().position(|&t| t == tool).map_or(false, |i| self.tool_allowed(i)) + } + // a rectangle of own plain floor, as near the heart as possible (not right next to it) fn ai_free_rect(&self, cx: i32, cy: i32, w: i32, h: i32) -> Option<(i32, i32)> { let me = self.me; @@ -260,7 +269,7 @@ impl Game { fn ai_towers(&mut self, d: usize, cx: i32, cy: i32) { let me = self.me; let mine = self.towers.iter().filter(|t| !t.hero && self.map.t[t.tile].owner == me).count(); - if mine >= MAX_TOWERS[d] || self.gold < 1800.0 || self.counts[0] == 0 { + if mine >= MAX_TOWERS[d] || self.gold < 1800.0 || self.counts[0] == 0 || !self.ai_knows(crate::ui::Tool::Tower(TowerKind::Arrow)) { return; } let spot = (0..self.map.t.len()).filter(|&i| { diff --git a/src/rival.rs b/src/rival.rs index 89cd7d3..93afb44 100644 --- a/src/rival.rs +++ b/src/rival.rs @@ -600,6 +600,37 @@ mod tests { g } + #[test] + fn the_ai_partner_builds_only_what_is_researched() { + // the first missions of both co-op campaigns: no library, no forge yet + let doc = crate::editor::coop_campaign::coop_campaign(); + let games = [ + crate::campaign::start_editor_coop_mission(&doc.missions[0], 0, 77), + crate::campaign::start_coop_mission(crate::campaign::coop_mission_indexes()[0], None), + ]; + let lib = crate::ui::TOOLS.iter().position(|&t| t == crate::ui::Tool::Room(4)).unwrap(); + let forge = crate::ui::TOOLS.iter().position(|&t| t == crate::ui::Tool::Room(8)).unwrap(); + for mut g in games { + g.coop = true; + assert!(g.start_match(&[0, 2], 0)); + g.sync_ai_sides(&[2], 1); + g.end_match(); + let mine_knows = (g.tool_allowed(lib), g.tool_allowed(forge)); + for _ in 0..(10 * 60 * 20) { + g.update(0.05); + } + let owner = Owner::of_side(2); + let has = |k: Kind| g.map.t.iter().any(|t| t.kind == k && t.owner == owner); + if !mine_knows.0 { + assert!(!has(Kind::Library), "the partner has no library either"); + } + if !mine_knows.1 { + assert!(!has(Kind::Forge), "the partner has no forge either"); + } + assert!(has(Kind::Lair) || has(Kind::Treasury), "but builds what it knows"); + } + } + #[test] fn imps_take_ground_from_enemies_but_never_from_allies() { let mut g = offline_league_game();