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
+111
-190
@@ -1,6 +1,7 @@
|
||||
// Rival dungeon masters (simplified): a dungeon around their own soul core that grows over
|
||||
// time, creatures that appear there, guard it and attack in groups - the player, the other
|
||||
// master or the humans. A master whose core falls is out; its ground becomes free.
|
||||
// Rival dungeon masters: each is a real master (gold, imps, rooms, research - see add_rival)
|
||||
// played by the brain in ai.rs; this keeps their record (side, heart, difficulty, alive) for the
|
||||
// interface, the goals and the victory. A master whose heart falls is out; his ground becomes
|
||||
// free. Units of a rival without a master (old savegames) still use rival_ai until he gets one.
|
||||
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
@@ -26,23 +27,12 @@ pub struct Rival {
|
||||
pub power: f32, // grows with the time: level of the new creatures
|
||||
#[serde(default)]
|
||||
pub gift_t: f32, // co-op AI partner: seconds until it looks whether its allies need gold
|
||||
}
|
||||
|
||||
struct Pace {
|
||||
spawn: f32, // seconds between new creatures
|
||||
grow: f32, // seconds between growth steps
|
||||
attack: f32, // seconds between attacks
|
||||
cap: usize, // most creatures at a time
|
||||
max_radius: i32, // largest dungeon
|
||||
power: f32, // levels per minute
|
||||
}
|
||||
|
||||
fn pace(d: u8) -> Pace {
|
||||
match d {
|
||||
0 => Pace { spawn: 55.0, grow: 45.0, attack: 330.0, cap: 6, max_radius: 8, power: 0.25 },
|
||||
2 => Pace { spawn: 26.0, grow: 22.0, attack: 170.0, cap: 13, max_radius: 13, power: 0.6 },
|
||||
_ => Pace { spawn: 38.0, grow: 30.0, attack: 240.0, cap: 9, max_radius: 11, power: 0.4 },
|
||||
}
|
||||
#[serde(default)]
|
||||
pub think_t: f32, // the brain (ai.rs): seconds until the next thought
|
||||
#[serde(default)]
|
||||
pub attacking: bool, // the attack flag is set on an enemy heart
|
||||
#[serde(default)]
|
||||
pub army0: f32, // army strength when the attack began
|
||||
}
|
||||
|
||||
impl Rival {
|
||||
@@ -102,77 +92,81 @@ impl Game {
|
||||
self.rivals.iter().any(|r| r.side == side && r.alive)
|
||||
}
|
||||
|
||||
// sets up a rival dungeon around (cx, cy)
|
||||
// sets up a rival master around (cx, cy): a real master (ai.rs) with his heart, a little
|
||||
// ground, a portal of his own, four imps and gold by the difficulty
|
||||
pub fn add_rival(&mut self, side: u8, cx: i32, cy: i32, difficulty: u8) {
|
||||
let owner = Owner::of_side(side);
|
||||
let core = (cx - 1, cy - 1);
|
||||
let r = 4;
|
||||
for y in cy - r..=cy + r {
|
||||
for x in cx - r..=cx + r {
|
||||
if !inb(x, y) || x < 2 || y < 2 || x >= mw() as i32 - 2 || y >= mh() as i32 - 2 {
|
||||
continue;
|
||||
self.carve_master_area(side, core);
|
||||
// his own portal beside the dungeon (if there is room)
|
||||
let owner = Owner::of_side(side);
|
||||
let (px, py) = (core.0 + 6, core.1);
|
||||
if px + 3 < mw() as i32 - 2 && py >= 2 && py + 3 < mh() as i32 - 2 {
|
||||
for y in py..py + 3 {
|
||||
for x in px..px + 3 {
|
||||
let t = &mut self.map.t[idx(x, y)];
|
||||
if !matches!(t.kind, Kind::Core | Kind::Gate) {
|
||||
t.kind = Kind::Portal;
|
||||
t.owner = owner;
|
||||
t.gold = 0;
|
||||
t.reinforced = false;
|
||||
}
|
||||
}
|
||||
let t = &mut self.map.t[idx(x, y)];
|
||||
if matches!(t.kind, Kind::Portal | Kind::Gate | Kind::Core) {
|
||||
continue; // portals and other cores stay
|
||||
}
|
||||
t.kind = Kind::Floor;
|
||||
t.owner = owner;
|
||||
t.gold = 0;
|
||||
t.marked = false;
|
||||
t.reinforced = false;
|
||||
}
|
||||
if !self.map.portals.contains(&(px, py)) {
|
||||
self.map.portals.push((px, py));
|
||||
}
|
||||
}
|
||||
for y in 0..3 {
|
||||
for x in 0..3 {
|
||||
let t = &mut self.map.t[idx(core.0 + x, core.1 + y)];
|
||||
t.kind = Kind::Core;
|
||||
t.owner = owner;
|
||||
}
|
||||
}
|
||||
// a lair and a treasury (the player can take it)
|
||||
for y in cy - 3..=cy + 3 {
|
||||
let t = &mut self.map.t[idx(cx - 3, y)];
|
||||
t.kind = Kind::Lair;
|
||||
}
|
||||
for y in cy - 1..=cy + 1 {
|
||||
let t = &mut self.map.t[idx(cx + 3, y)];
|
||||
t.kind = Kind::Treasury;
|
||||
t.gold = 300;
|
||||
t.res = 0;
|
||||
}
|
||||
self.rivals.push(Rival { side, core, hp: RIVAL_CORE_HP, alive: true, radius: r, grow_t: 20.0, spawn_t: 5.0, attack_t: pace(difficulty).attack, difficulty, power: 0.0, gift_t: PARTNER_GIFT_EVERY });
|
||||
for k in 0..3 {
|
||||
self.spawn_rival_unit(side, EK::Kobold, 1, k as f32);
|
||||
}
|
||||
self.rivals.push(Rival { side, core, hp: RIVAL_CORE_HP, alive: true, radius: 5, grow_t: 20.0, spawn_t: 5.0, attack_t: 0.0, difficulty, power: 0.0, gift_t: PARTNER_GIFT_EVERY, think_t: 2.0, attacking: false, army0: 0.0 });
|
||||
self.adopt_master(side, core, difficulty, true);
|
||||
}
|
||||
|
||||
fn spawn_rival_unit(&mut self, side: u8, kind: EK, level: u32, k: f32) {
|
||||
let Some(rv) = self.rivals.iter().find(|r| r.side == side) else { return };
|
||||
let (cx, cy) = rv.centre();
|
||||
let a = k * 1.7 + self.rng.f() * 2.0;
|
||||
let (x, y) = (cx + a.cos() * 2.4, cy + a.sin() * 2.4);
|
||||
let id = self.spawn(kind, false, x, y, level);
|
||||
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.side = side;
|
||||
e.home = Some((cx, cy));
|
||||
// the rival on `side` gets (or has) a master of his own. `fresh`: a new one with imps and gold
|
||||
pub(crate) fn adopt_master(&mut self, side: u8, core: (i32, i32), difficulty: u8, fresh: bool) {
|
||||
if self.is_master(side) {
|
||||
return;
|
||||
}
|
||||
let me = self.me_side;
|
||||
self.add_master(side, core);
|
||||
self.switch_to(side);
|
||||
self.gold = crate::ai::start_gold(difficulty);
|
||||
self.mana = 150.0;
|
||||
self.recount();
|
||||
if fresh {
|
||||
let (cx, cy) = core;
|
||||
for j in 0..4 {
|
||||
self.spawn(EK::Worker, false, cx as f32 - 0.5, cy as f32 + j as f32 * 0.7 + 0.3, 1);
|
||||
}
|
||||
}
|
||||
// the creatures of that side are his
|
||||
for e in self.ents.iter_mut().filter(|e| e.alive && !e.hero && e.side == side) {
|
||||
e.home = None;
|
||||
e.target = None;
|
||||
}
|
||||
self.switch_to(me);
|
||||
}
|
||||
|
||||
|
||||
pub fn update_rivals(&mut self, dt: f32) {
|
||||
let back = self.me_side;
|
||||
for k in 0..self.rivals.len() {
|
||||
if !self.rivals[k].alive {
|
||||
continue;
|
||||
}
|
||||
let p = pace(self.rivals[k].difficulty);
|
||||
let side = self.rivals[k].side;
|
||||
{
|
||||
let r = &mut self.rivals[k];
|
||||
r.power += dt / 60.0 * p.power;
|
||||
r.grow_t -= dt;
|
||||
r.spawn_t -= dt;
|
||||
r.attack_t -= dt;
|
||||
// a rival from an older savegame or a painted map: he gets his master now
|
||||
if !self.is_master(side) {
|
||||
let (core, diff) = (self.rivals[k].core, self.rivals[k].difficulty);
|
||||
self.adopt_master(side, core, diff, false);
|
||||
}
|
||||
// his heart fell: he is out
|
||||
if self.core_hp_of(side) <= 0.0 {
|
||||
if !self.out_sides.contains(&side) {
|
||||
self.out_sides.push(side);
|
||||
}
|
||||
self.eliminate_rival(k);
|
||||
continue;
|
||||
}
|
||||
self.rivals[k].hp = self.core_hp_of(side) / CORE_HP * RIVAL_CORE_HP;
|
||||
// a co-op partner sends gold to allied masters who run short
|
||||
if self.is_partner(side) {
|
||||
self.rivals[k].gift_t -= dt;
|
||||
@@ -181,112 +175,12 @@ impl Game {
|
||||
self.partner_gift(side);
|
||||
}
|
||||
}
|
||||
// growth: one more ring of ground (no imps needed)
|
||||
if self.rivals[k].grow_t <= 0.0 {
|
||||
self.rivals[k].grow_t = p.grow * (0.8 + self.rng.f() * 0.4);
|
||||
if self.rivals[k].radius < p.max_radius {
|
||||
self.rivals[k].radius += 1;
|
||||
self.rival_grow(k);
|
||||
}
|
||||
}
|
||||
// new creatures
|
||||
let units = self.ents.iter().filter(|e| e.alive && e.side == side && !e.hero).count();
|
||||
if self.rivals[k].spawn_t <= 0.0 {
|
||||
self.rivals[k].spawn_t = p.spawn * (0.8 + self.rng.f() * 0.4);
|
||||
if units < p.cap {
|
||||
let power = self.rivals[k].power;
|
||||
let mut pool = vec![EK::Kobold, EK::Spider];
|
||||
if power >= 1.5 {
|
||||
pool.extend([EK::Mage, EK::Ogre]);
|
||||
}
|
||||
if power >= 3.0 {
|
||||
pool.push(EK::Gargoyle);
|
||||
}
|
||||
let kind = pool[(self.rng.next() as usize) % pool.len()];
|
||||
let level = (1.0 + power) as u32;
|
||||
self.spawn_rival_unit(side, kind, level.clamp(1, 10), units as f32);
|
||||
}
|
||||
}
|
||||
// attack: about two thirds of the creatures march on an enemy
|
||||
if self.rivals[k].attack_t <= 0.0 {
|
||||
self.rivals[k].attack_t = p.attack * (0.8 + self.rng.f() * 0.4);
|
||||
let mut targets: Vec<(u8, u32)> = Vec::new();
|
||||
if self.coop {
|
||||
if self.gate_on || self.ents.iter().any(|e| e.alive && e.hero) {
|
||||
targets.push((1, 3));
|
||||
}
|
||||
} else {
|
||||
// the masters (the player, or the human masters of a match) that are not allies
|
||||
for s in self.master_sides() {
|
||||
if !self.is_out(s) && !self.allied(side, s) {
|
||||
targets.push((s, 3));
|
||||
}
|
||||
}
|
||||
for o in &self.rivals {
|
||||
if o.alive && o.side != side && !self.allied(side, o.side) {
|
||||
targets.push((o.side, 1));
|
||||
}
|
||||
}
|
||||
if self.has_gate() && self.gate_hp > 0.0 {
|
||||
targets.push((1, 1));
|
||||
}
|
||||
}
|
||||
let total: u32 = targets.iter().map(|t| t.1).sum();
|
||||
if total == 0 {
|
||||
continue;
|
||||
}
|
||||
let mut r = self.rng.next() % total;
|
||||
let mut target = 0;
|
||||
for (s, w) in targets {
|
||||
if r < w {
|
||||
target = s;
|
||||
break;
|
||||
}
|
||||
r -= w;
|
||||
}
|
||||
let mut n = 0;
|
||||
for e in self.ents.iter_mut() {
|
||||
if e.alive && !e.hero && e.side == side && e.target.is_none() && n * 3 < units * 2 {
|
||||
e.target = Some(target);
|
||||
e.path.clear();
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
if n > 0 && target == 0 {
|
||||
self.msg(if side == 2 { "msg.rival1_attack" } else { "msg.rival2_attack" }, vec![n.to_string()]);
|
||||
self.sfx_ui(Sfx::WaveAlarm);
|
||||
}
|
||||
}
|
||||
self.switch_to(side);
|
||||
self.ai_think(k, dt);
|
||||
self.switch_to(back);
|
||||
}
|
||||
}
|
||||
|
||||
// a ring of earth around the rival dungeon becomes its ground
|
||||
fn rival_grow(&mut self, k: usize) {
|
||||
let rv = self.rivals[k].clone();
|
||||
let owner = Owner::of_side(rv.side);
|
||||
let (cx, cy) = (rv.core.0 + 1, rv.core.1 + 1);
|
||||
let r = rv.radius;
|
||||
for y in cy - r..=cy + r {
|
||||
for x in cx - r..=cx + r {
|
||||
if (x - cx).abs() != r && (y - cy).abs() != r {
|
||||
continue;
|
||||
}
|
||||
if !inb(x, y) || x < 2 || y < 2 || x >= mw() as i32 - 2 || y >= mh() as i32 - 2 {
|
||||
continue;
|
||||
}
|
||||
let t = self.map.t[idx(x, y)];
|
||||
let free = matches!(t.kind, Kind::Earth | Kind::Floor) && !t.reinforced && t.owner == Owner::None;
|
||||
let touches = DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == owner && n.walkable()));
|
||||
if free && touches {
|
||||
let tl = &mut self.map.t[idx(x, y)];
|
||||
tl.kind = Kind::Floor;
|
||||
tl.owner = owner;
|
||||
tl.gold = 0;
|
||||
tl.marked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// damage to the core of a rival master
|
||||
pub fn rival_core_hit(&mut self, side: u8, dmg: f32) {
|
||||
@@ -533,21 +427,24 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rival_grows_spawns_and_attacks() {
|
||||
fn an_ai_master_builds_its_dungeon() {
|
||||
let mut g = rival_game(2);
|
||||
let r0 = g.rivals[0].radius;
|
||||
let core0 = g.core_hp;
|
||||
let mut attacked = false;
|
||||
for _ in 0..(12 * 60 * 20) {
|
||||
let side = g.rivals[0].side;
|
||||
let owner = Owner::of_side(side);
|
||||
assert!(g.is_master(side), "a real master");
|
||||
let ground0 = g.map.t.iter().filter(|t| t.owner == owner).count();
|
||||
for _ in 0..(8 * 60 * 20) {
|
||||
g.update(0.05);
|
||||
attacked |= g.ents.iter().any(|e| e.rival() && e.target == Some(0));
|
||||
if g.core_hp < core0 - 50.0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(g.rivals[0].radius > r0, "the rival dungeon grows");
|
||||
assert!(attacked, "the rival sends creatures");
|
||||
assert!(g.core_hp < core0, "they reach the player's core");
|
||||
let ground = g.map.t.iter().filter(|t| t.owner == owner).count();
|
||||
assert!(ground > ground0 + 30, "its imps dig and claim: {} -> {}", ground0, ground);
|
||||
g.switch_to(side);
|
||||
let rooms: usize = g.counts.iter().sum();
|
||||
let imps = g.worker_count();
|
||||
g.switch_to(0);
|
||||
assert!(rooms >= 9, "it builds rooms ({} tiles)", rooms);
|
||||
assert!(imps >= 4, "and keeps its imps");
|
||||
assert!(g.torches.iter().any(|t| g.map.t[t.wall].owner == owner || g.map.t[t.wall].solid()), "torches");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -633,7 +530,7 @@ mod tests {
|
||||
#[test]
|
||||
fn an_offline_coop_partner_is_an_allied_ai_master() {
|
||||
let mut g = offline_league_game();
|
||||
assert!(g.netplay.is_none() && g.masters.is_empty());
|
||||
assert!(g.netplay.is_none() && g.is_master(2), "the partner is a real master without the server");
|
||||
assert!(g.rival_alive(2) && g.is_partner(2));
|
||||
assert!(g.allied(0, 2) && g.allied(2, 0));
|
||||
// short of gold: the partner helps out within a minute
|
||||
@@ -674,4 +571,28 @@ mod tests {
|
||||
}
|
||||
assert!(near, "partner creatures gather at the flag");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn ai_master_report() {
|
||||
for d in 0..3u8 {
|
||||
let mut g = (1..200u32).map(|s| Game::new_free(s, 96, 96, d)).find(|g| g.rivals.len() == 1 && !g.gate_on).unwrap();
|
||||
let side = g.rivals[0].side;
|
||||
for m in 1..=15 {
|
||||
for _ in 0..(60 * 20) {
|
||||
g.update(0.05);
|
||||
}
|
||||
g.switch_to(side);
|
||||
let (rooms, imps, cr, gold) = (g.counts.iter().sum::<usize>(), g.worker_count(), g.creature_count(), g.gold);
|
||||
let flag = g.flag.is_some();
|
||||
let earned = g.stats.get(crate::stats::GOLD);
|
||||
let me = g.me;
|
||||
let veins = (0..g.map.t.len()).filter(|&i| g.map.t[i].marked && g.map.t[i].vein().is_some()).count();
|
||||
let marked = g.map.t.iter().filter(|t| t.marked).count();
|
||||
let near = (0..g.map.t.len()).filter(|&i| { let (x, y) = xy(i); g.map.t[i].marked && DIR4.iter().any(|&(dx, dy)| g.map.get(x + dx, y + dy).map_or(false, |n| n.owner == me && n.walkable())) }).count();
|
||||
g.switch_to(0);
|
||||
println!("diff {} min {:2}: rooms {:3} imps {:2} creatures {:2} gold {:5.0} attacking {} | marked {} veins {} reachable {} earned {:.0} | player core {:.0}", d, m, rooms, imps, cr, gold, flag, marked, veins, near, earned, g.core_hp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user