Real AI masters: rivals, AI seats and the co-op partner play by the player's rules (imps dig, mine and claim, rooms, towers, tunnels and the attack flag); other masters are saved; portals belong to whoever claims them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b940a959f1
commit
a00ae6a06b
11 files changed
+456
-223
No files matched your search
@@ -0,0 +1,313 @@
|
||||
// AI dungeon masters: a rival (free play, campaign), an AI seat of a match or the co-op partner is
|
||||
// a real master with gold, mana, imps, rooms and research - the same rules as the player. This
|
||||
// brain gives him the player's orders every few seconds: summon imps, mark earth and veins to dig
|
||||
// in growing rings, build rooms in a sensible order, put towers at the border, set the attack
|
||||
// flag on an enemy heart once the army is strong enough (and take it back after heavy losses),
|
||||
// guard its own heart with a shield. The difficulty sets how often it thinks and how bold it is.
|
||||
// Everything runs in the shared step with the shared random numbers (the same on every client).
|
||||
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
|
||||
// per difficulty (easy, medium, hard)
|
||||
const THINK: [f32; 3] = [4.0, 2.5, 1.5]; // seconds between two thoughts
|
||||
const MAX_RADIUS: [i32; 3] = [11, 15, 19]; // how far the dungeon grows around the heart
|
||||
const MAX_IMPS: [usize; 3] = [6, 9, 12];
|
||||
const FIRST_ATTACK: [f32; 3] = [720.0, 480.0, 330.0]; // no attack before (seconds)
|
||||
const BOLDNESS: [f32; 3] = [1.5, 1.15, 0.9]; // own army / enemy army needed to attack
|
||||
const MAX_TOWERS: [usize; 3] = [1, 2, 4];
|
||||
const START_GOLD: [f32; 3] = [1500.0, 2500.0, 3500.0];
|
||||
|
||||
// the rooms a master builds, in this order: (room index, width, height, how many of them)
|
||||
const BUILD: [(usize, i32, i32, usize); 10] = [
|
||||
(0, 3, 3, 1), // treasury
|
||||
(1, 4, 3, 1), // lair
|
||||
(2, 4, 3, 1), // mushroom garden
|
||||
(3, 3, 3, 1), // training hall
|
||||
(4, 3, 3, 1), // library
|
||||
(1, 4, 3, 2), // a second lair
|
||||
(2, 4, 3, 2), // a second garden
|
||||
(8, 3, 3, 1), // forge
|
||||
(6, 3, 3, 1), // temple
|
||||
(9, 3, 2, 1), // prison
|
||||
];
|
||||
|
||||
pub fn start_gold(difficulty: u8) -> f32 {
|
||||
START_GOLD[difficulty.min(2) as usize]
|
||||
}
|
||||
|
||||
impl Game {
|
||||
// the brain of rival k (its master is the one being processed)
|
||||
pub(crate) fn ai_think(&mut self, k: usize, dt: f32) {
|
||||
let d = self.rivals[k].difficulty.min(2) as usize;
|
||||
self.rivals[k].think_t -= dt;
|
||||
if self.rivals[k].think_t > 0.0 {
|
||||
return;
|
||||
}
|
||||
self.rivals[k].think_t = THINK[d] * (0.85 + self.rng.f() * 0.3);
|
||||
let side = self.rivals[k].side;
|
||||
let (cx, cy) = (self.map.core.0 + 1, self.map.core.1 + 1);
|
||||
self.ai_imps(d, cx, cy);
|
||||
self.ai_dig(k, d, cx, cy);
|
||||
self.ai_gold(cx, cy);
|
||||
self.ai_rooms(cx, cy);
|
||||
self.ai_towers(d, cx, cy);
|
||||
self.ai_fight(k, d, side, cx, cy);
|
||||
}
|
||||
|
||||
// enough imps for the dungeon's size
|
||||
fn ai_imps(&mut self, d: usize, cx: i32, cy: i32) {
|
||||
let claimed = self.map.t.iter().filter(|t| t.owner == self.me).count();
|
||||
let want = (4 + claimed / 30).min(MAX_IMPS[d]);
|
||||
if self.worker_count() < want && self.mana >= self.summon_cost() + 20.0 {
|
||||
self.summon(cx as f32 + 0.5, cy as f32 + 2.5);
|
||||
}
|
||||
}
|
||||
|
||||
// dig: rings of earth around the heart, veins first; the ring grows when it is dug out
|
||||
fn ai_dig(&mut self, k: usize, d: usize, cx: i32, cy: i32) {
|
||||
let me = self.me;
|
||||
let r = self.rivals[k].radius.max(5);
|
||||
let marked_mine = |g: &Game| {
|
||||
let mut n = 0;
|
||||
for y in cy - r - 4..=cy + r + 4 {
|
||||
for x in cx - r - 4..=cx + r + 4 {
|
||||
if g.map.get(x, y).map_or(false, |t| t.marked) {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
n
|
||||
};
|
||||
let open = marked_mine(self);
|
||||
if open > self.worker_count() * 4 {
|
||||
return; // the imps have enough to do
|
||||
}
|
||||
let mut added = 0;
|
||||
let diggable = |t: &Tile| t.diggable() && !t.plant() && !t.reinforced && t.kind != Kind::Steel && (t.owner == Owner::None || t.owner == me);
|
||||
// veins a little beyond the ring, then the ring itself
|
||||
for pass in 0..2 {
|
||||
let reach = if pass == 0 { r + 3 } else { r };
|
||||
for y in cy - reach..=cy + reach {
|
||||
for x in cx - reach..=cx + reach {
|
||||
if added >= 16 || !inb(x, y) || x < 2 || y < 2 || x >= mw() as i32 - 2 || y >= mh() as i32 - 2 {
|
||||
continue;
|
||||
}
|
||||
let i = idx(x, y);
|
||||
let t = self.map.t[i];
|
||||
if t.marked || !diggable(&t) || (pass == 0 && t.vein().is_none()) {
|
||||
continue;
|
||||
}
|
||||
self.map.t[i].marked = true;
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// the ring is done: grow
|
||||
if added == 0 && open == 0 && r < MAX_RADIUS[d] {
|
||||
self.rivals[k].radius = r + 2;
|
||||
}
|
||||
}
|
||||
|
||||
// gold running low: a tunnel to the nearest vein that nobody works on yet
|
||||
fn ai_gold(&mut self, cx: i32, cy: i32) {
|
||||
if self.gold > 1500.0 {
|
||||
return;
|
||||
}
|
||||
let me = self.me;
|
||||
// a marked vein the imps can reach (next to own ground) is being worked already
|
||||
let busy = (0..self.map.t.len()).any(|i| {
|
||||
let t = &self.map.t[i];
|
||||
let (x, y) = xy(i);
|
||||
t.marked && t.vein().is_some() && DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == me && n.walkable()))
|
||||
});
|
||||
if busy {
|
||||
return; // a vein is being worked already
|
||||
}
|
||||
self.ai_mark_tunnel(cx, cy, |g: &Game, i: usize| {
|
||||
let t = &g.map.t[i];
|
||||
t.vein().is_some() && !t.marked && (t.owner == Owner::None || t.owner == me)
|
||||
}, 40);
|
||||
}
|
||||
|
||||
// marks the diggable tiles on the cheapest way from the heart to a goal tile (and the goal
|
||||
// with the veins next to it); false if there is none within `reach` tiles
|
||||
fn ai_mark_tunnel(&mut self, cx: i32, cy: i32, goal: impl Fn(&Game, usize) -> bool, reach: i32) -> bool {
|
||||
let start = [(cx, cy + 2), (cx, cy - 2), (cx + 2, cy), (cx - 2, cy)].into_iter().find(|&(x, y)| self.map.walk(x, y));
|
||||
let Some((sx, sy)) = start else { return false };
|
||||
let found = {
|
||||
let cost = self.map.hero_cost_as(false);
|
||||
let near = |i: usize| {
|
||||
let (x, y) = xy(i);
|
||||
(x - cx).abs().max((y - cy).abs()) <= reach
|
||||
};
|
||||
self.map.search(idx(sx, sy), |i| if near(i) { cost(i) } else { None }, |i| goal(self, i), false)
|
||||
};
|
||||
let Some(path) = found else { return false };
|
||||
let me = self.me;
|
||||
let mut cluster = Vec::new();
|
||||
for &i in &path {
|
||||
let t = self.map.t[i];
|
||||
if t.diggable() && !t.plant() && t.kind != Kind::Steel && (t.owner == Owner::None || t.owner == me || t.reinforced) {
|
||||
self.map.t[i].marked = true;
|
||||
if t.vein().is_some() {
|
||||
cluster.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
// the rest of the vein
|
||||
for i in cluster {
|
||||
let (x, y) = xy(i);
|
||||
for &(dx, dy) in DIR8.iter() {
|
||||
if let Some(t) = self.map.get(x + dx, y + dy) {
|
||||
if t.vein().is_some() && t.owner == Owner::None {
|
||||
self.map.t[idx(x + dx, y + dy)].marked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// 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() {
|
||||
let tiles = self.counts[room];
|
||||
if tiles >= (w * h) as usize * n {
|
||||
continue;
|
||||
}
|
||||
let cost = ROOM_COST[room] as f32 * (w * h) as f32;
|
||||
if self.gold < cost + 200.0 {
|
||||
return; // saving for it
|
||||
}
|
||||
if let Some((x0, y0)) = self.ai_free_rect(cx, cy, w, h) {
|
||||
self.build_rect(room, x0, y0, x0 + w - 1, y0 + h - 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
let free = |x: i32, y: i32| self.map.get(x, y).map_or(false, |t| t.kind == Kind::Floor && t.owner == me) && !self.ents.iter().any(|e| e.alive && e.tile() == idx(x, y) && e.kind == EK::Worker && false);
|
||||
let mut best: Option<((i32, i32), i32)> = None;
|
||||
for y0 in cy - 22..=cy + 22 {
|
||||
for x0 in cx - 22..=cx + 22 {
|
||||
let (mx, my) = (x0 + w / 2, y0 + h / 2);
|
||||
let dist = (mx - cx).abs() + (my - cy).abs();
|
||||
if dist < 4 || best.map_or(false, |b| b.1 <= dist) {
|
||||
continue;
|
||||
}
|
||||
if (y0..y0 + h).all(|y| (x0..x0 + w).all(|x| free(x, y))) {
|
||||
best = Some(((x0, y0), dist));
|
||||
}
|
||||
}
|
||||
}
|
||||
best.map(|b| b.0)
|
||||
}
|
||||
|
||||
// a tower or two at the border of the dungeon (towards the open ground)
|
||||
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 {
|
||||
return;
|
||||
}
|
||||
let spot = (0..self.map.t.len()).filter(|&i| {
|
||||
let (x, y) = xy(i);
|
||||
let t = &self.map.t[i];
|
||||
t.kind == Kind::Floor && t.owner == me && (x - cx).abs().max((y - cy).abs()) >= 4
|
||||
&& DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner != me && n.walkable()))
|
||||
}).min_by_key(|&i| {
|
||||
let (x, y) = xy(i);
|
||||
(x - cx).abs() + (y - cy).abs()
|
||||
});
|
||||
if let Some(i) = spot {
|
||||
let (x, y) = xy(i);
|
||||
self.build_tower(TowerKind::Arrow, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
// fight: a shield at the heart; the attack flag on the nearest enemy heart once strong enough,
|
||||
// taken back after heavy losses. A co-op partner follows the attack flag of his allies.
|
||||
fn ai_fight(&mut self, k: usize, d: usize, side: u8, cx: i32, cy: i32) {
|
||||
if self.guard_marks.is_empty() && self.creature_count() >= 3 && d >= 1 {
|
||||
self.place_mark(cx + 1, cy + 3, false);
|
||||
}
|
||||
let army = self.side_army(side);
|
||||
// co-op: go where the allies go
|
||||
if self.is_partner(side) {
|
||||
let lead = self.master_sides().into_iter().find(|&s| s != side && !self.is_out(s) && self.allied(side, s));
|
||||
let flag = lead.and_then(|s| self.masters.iter().find(|m| m.side == s).and_then(|m| m.flag));
|
||||
if flag != self.flag {
|
||||
self.flag = flag;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.rivals[k].attacking {
|
||||
// too many lost, or the target is gone: back home
|
||||
let target_gone = self.flag.map_or(true, |f| !self.ai_enemy_heart_near(side, f));
|
||||
if army < self.rivals[k].army0 * 0.45 || target_gone {
|
||||
self.rivals[k].attacking = false;
|
||||
self.flag = None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.time < FIRST_ATTACK[d] || self.creature_count() < 4 {
|
||||
return;
|
||||
}
|
||||
// the nearest enemy heart (players, other AI masters)
|
||||
let target = self
|
||||
.master_sides()
|
||||
.into_iter()
|
||||
.filter(|&s| s != side && !self.is_out(s) && !self.allied(side, s) && self.core_hp_of(s) > 0.0)
|
||||
.min_by_key(|&s| {
|
||||
let (x, y) = self.core_of_side(s);
|
||||
(x - cx).abs() + (y - cy).abs()
|
||||
});
|
||||
let Some(t) = target else { return };
|
||||
if army < self.side_army(t) * BOLDNESS[d] {
|
||||
return;
|
||||
}
|
||||
let (tx, ty) = self.core_of_side(t);
|
||||
let spot = DIR8.iter().map(|&(dx, dy)| (tx + 1 + dx * 2, ty + 1 + dy * 2)).find(|&(x, y)| self.map.walk(x, y));
|
||||
// creatures only walk: first a tunnel to the enemy (the imps dig it)
|
||||
let from = [(cx, cy + 2), (cx, cy - 2), (cx + 2, cy), (cx - 2, cy)].into_iter().find(|&(x, y)| self.map.walk(x, y));
|
||||
let open = match (from, spot) {
|
||||
(Some((fx, fy)), Some((x, y))) => self.map.creature_path(false, idx(fx, fy), idx(x, y)).is_some(),
|
||||
_ => false,
|
||||
};
|
||||
if !open {
|
||||
let target_owner = Owner::of_side(t);
|
||||
if !self.map.t.iter().any(|tl| tl.marked && tl.reinforced) {
|
||||
self.ai_mark_tunnel(cx, cy, |g: &Game, i: usize| {
|
||||
let (x, y) = xy(i);
|
||||
g.map.t[i].owner == target_owner && g.map.t[i].walkable() || ((x - tx - 1).abs() <= 2 && (y - ty - 1).abs() <= 2 && g.map.t[i].walkable())
|
||||
}, 200);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some((x, y)) = spot {
|
||||
self.flag = Some(idx(x, y));
|
||||
self.rivals[k].attacking = true;
|
||||
self.rivals[k].army0 = army;
|
||||
if t == self.local_side {
|
||||
let key = if side == 2 { "msg.rival1_attack" } else { "msg.rival2_attack" };
|
||||
let n = self.creature_count().to_string();
|
||||
self.tell_local(key, vec![n], Some(Sfx::WaveAlarm));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// is an enemy heart near tile f?
|
||||
fn ai_enemy_heart_near(&self, side: u8, f: usize) -> bool {
|
||||
let (fx, fy) = xy(f);
|
||||
self.master_sides().into_iter().any(|s| {
|
||||
s != side && !self.is_out(s) && self.core_hp_of(s) > 0.0 && {
|
||||
let (x, y) = self.core_of_side(s);
|
||||
(x + 1 - fx).abs() <= 4 && (y + 1 - fy).abs() <= 4
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user