Compare commits

...
2 Commits
6 changed files with 161 additions and 37 deletions

No files matched your search

+1 -1
View File
@@ -1 +1 @@
v202611-00057 v202611-00058
+59 -13
View File
@@ -10,11 +10,12 @@ use crate::game::*;
use crate::map::*; use crate::map::*;
// per difficulty (easy, medium, hard) // per difficulty (easy, medium, hard)
const THINK: [f32; 3] = [4.0, 2.5, 1.5]; // seconds between two thoughts const THINK: [f32; 3] = [5.2, 3.25, 1.95]; // seconds between two thoughts (a calm pace)
const MAX_RADIUS: [i32; 3] = [11, 15, 19]; // how far the dungeon grows around the heart 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 MAX_IMPS: [usize; 3] = [5, 7, 9];
const FIRST_ATTACK: [f32; 3] = [720.0, 480.0, 330.0]; // no attack before (seconds) const FIRST_ATTACK: [f32; 3] = [940.0, 625.0, 430.0]; // no attack before (seconds)
const BOLDNESS: [f32; 3] = [1.5, 1.15, 0.9]; // own army / enemy army needed to attack const BOLDNESS: [f32; 3] = [1.5, 1.15, 0.9]; // own army / enemy army needed to attack
const MARK_REACH: i32 = 5; // an AI master marks only this close to its own ground
const MAX_TOWERS: [usize; 3] = [1, 2, 4]; const MAX_TOWERS: [usize; 3] = [1, 2, 4];
const START_GOLD: [f32; 3] = [1500.0, 2500.0, 3500.0]; const START_GOLD: [f32; 3] = [1500.0, 2500.0, 3500.0];
@@ -55,11 +56,56 @@ impl Game {
self.ai_fight(k, d, side, cx, cy); self.ai_fight(k, d, side, cx, cy);
} }
// the tiles an AI master may mark: within MARK_REACH tiles of its own ground
fn ai_reach_mask(&self) -> Vec<bool> {
let me = self.me;
let mut dist = vec![u8::MAX; self.map.t.len()];
let mut queue = std::collections::VecDeque::new();
for (i, t) in self.map.t.iter().enumerate() {
if t.owner == me {
dist[i] = 0;
queue.push_back(i);
}
}
while let Some(i) = queue.pop_front() {
let d = dist[i];
if d as i32 >= MARK_REACH {
continue;
}
let (x, y) = xy(i);
for &(dx, dy) in DIR8.iter() {
if inb(x + dx, y + dy) {
let j = idx(x + dx, y + dy);
if dist[j] > d + 1 {
dist[j] = d + 1;
queue.push_back(j);
}
}
}
}
dist.into_iter().map(|d| d as i32 <= MARK_REACH).collect()
}
// marks a tile for this master's imps (only near its own ground)
fn ai_mark(&mut self, i: usize, mask: &[bool]) -> bool {
if !mask.get(i).copied().unwrap_or(false) {
return false;
}
let side = self.me.side().min(7);
let t = &mut self.map.t[i];
if t.marked && t.mark_by != side {
return false; // another master's plan
}
t.marked = true;
t.mark_by = side;
true
}
// enough imps for the dungeon's size // enough imps for the dungeon's size
fn ai_imps(&mut self, d: usize, cx: i32, cy: i32) { 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 claimed = self.map.t.iter().filter(|t| t.owner == self.me).count();
let want = (4 + claimed / 30).min(MAX_IMPS[d]); let want = (4 + claimed / 30).min(MAX_IMPS[d]);
if self.worker_count() < want && self.mana >= self.summon_cost() + 20.0 { if self.worker_count() < want && self.mana >= self.summon_cost() {
self.summon(cx as f32 + 0.5, cy as f32 + 2.5); self.summon(cx as f32 + 0.5, cy as f32 + 2.5);
} }
} }
@@ -67,12 +113,13 @@ impl Game {
// dig: rings of earth around the heart, veins first; the ring grows when it is dug out // 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) { fn ai_dig(&mut self, k: usize, d: usize, cx: i32, cy: i32) {
let me = self.me; let me = self.me;
let side = me.side().min(7);
let r = self.rivals[k].radius.max(5); let r = self.rivals[k].radius.max(5);
let marked_mine = |g: &Game| { let marked_mine = |g: &Game| {
let mut n = 0; let mut n = 0;
for y in cy - r - 4..=cy + r + 4 { for y in cy - r - 4..=cy + r + 4 {
for x in cx - r - 4..=cx + r + 4 { for x in cx - r - 4..=cx + r + 4 {
if g.map.get(x, y).map_or(false, |t| t.marked) { if g.map.get(x, y).map_or(false, |t| t.marked && t.mark_by == side) {
n += 1; n += 1;
} }
} }
@@ -84,13 +131,14 @@ impl Game {
return; // the imps have enough to do return; // the imps have enough to do
} }
let mut added = 0; let mut added = 0;
let mask = self.ai_reach_mask();
let diggable = |t: &Tile| t.diggable() && !t.plant() && !t.reinforced && t.kind != Kind::Steel && (t.owner == Owner::None || t.owner == me); 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 // veins a little beyond the ring, then the ring itself
for pass in 0..2 { for pass in 0..2 {
let reach = if pass == 0 { r + 3 } else { r }; let reach = if pass == 0 { r + 3 } else { r };
for y in cy - reach..=cy + reach { for y in cy - reach..=cy + reach {
for x in cx - reach..=cx + 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 { if added >= 11 || !inb(x, y) || x < 2 || y < 2 || x >= mw() as i32 - 2 || y >= mh() as i32 - 2 {
continue; continue;
} }
let i = idx(x, y); let i = idx(x, y);
@@ -98,12 +146,12 @@ impl Game {
if t.marked || !diggable(&t) || (pass == 0 && t.vein().is_none()) { if t.marked || !diggable(&t) || (pass == 0 && t.vein().is_none()) {
continue; continue;
} }
self.map.t[i].marked = true; if self.ai_mark(i, &mask) {
self.map.t[i].mark_by = self.me.side().min(7);
added += 1; added += 1;
} }
} }
} }
}
// the ring is done: grow // the ring is done: grow
if added == 0 && open == 0 && r < MAX_RADIUS[d] { if added == 0 && open == 0 && r < MAX_RADIUS[d] {
self.rivals[k].radius = r + 2; self.rivals[k].radius = r + 2;
@@ -146,12 +194,11 @@ impl Game {
}; };
let Some(path) = found else { return false }; let Some(path) = found else { return false };
let me = self.me; let me = self.me;
let mask = self.ai_reach_mask();
let mut cluster = Vec::new(); let mut cluster = Vec::new();
for &i in &path { for &i in &path {
let t = self.map.t[i]; let t = self.map.t[i];
if t.diggable() && !t.plant() && t.kind != Kind::Steel && (t.owner == Owner::None || t.owner == me || t.reinforced) { if t.diggable() && !t.plant() && t.kind != Kind::Steel && (t.owner == Owner::None || t.owner == me || t.reinforced) && self.ai_mark(i, &mask) {
self.map.t[i].marked = true;
self.map.t[i].mark_by = self.me.side().min(7);
if t.vein().is_some() { if t.vein().is_some() {
cluster.push(i); cluster.push(i);
} }
@@ -163,8 +210,7 @@ impl Game {
for &(dx, dy) in DIR8.iter() { for &(dx, dy) in DIR8.iter() {
if let Some(t) = self.map.get(x + dx, y + dy) { if let Some(t) = self.map.get(x + dx, y + dy) {
if t.vein().is_some() && t.owner == Owner::None { if t.vein().is_some() && t.owner == Owner::None {
self.map.t[idx(x + dx, y + dy)].marked = true; self.ai_mark(idx(x + dx, y + dy), &mask);
self.map.t[idx(x + dx, y + dy)].mark_by = self.me.side().min(7);
} }
} }
} }
+14 -9
View File
@@ -3269,12 +3269,13 @@ impl Game {
// ---------------------------------------------------------------- pit imps (workers) // ---------------------------------------------------------------- pit imps (workers)
fn dig_neighbor(&self, i: usize, me: u32) -> Option<usize> { // a neighbour of tile i that this imp may dig: marked by its own master (side)
fn dig_neighbor(&self, i: usize, me: u32, side: u8) -> Option<usize> {
let (x, y) = xy(i); let (x, y) = xy(i);
let mut found = None; let mut found = None;
for &(dx, dy) in &DIR4 { for &(dx, dy) in &DIR4 {
if let Some(t) = self.map.get(x + dx, y + dy) { if let Some(t) = self.map.get(x + dx, y + dy) {
if t.marked && t.diggable() { if t.marked && t.mark_by == side && t.diggable() {
if t.reserved == me { if t.reserved == me {
return Some(idx(x + dx, y + dy)); return Some(idx(x + dx, y + dy));
} }
@@ -3312,14 +3313,16 @@ impl Game {
None None
} }
fn claimable(&self, i: usize, me: u32) -> bool { pub(crate) fn claimable(&self, i: usize, me: u32) -> bool {
let t = &self.map.t[i]; let t = &self.map.t[i];
let ok = if t.owner == self.me { let ok = if t.owner == self.me {
false false
} else if t.owner == Owner::None { } else if t.owner == Owner::None {
matches!(t.kind, Kind::Floor | Kind::Portal | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Treasury | Kind::ManaSpring | Kind::RuneChest) matches!(t.kind, Kind::Floor | Kind::Portal | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Treasury | Kind::ManaSpring | Kind::RuneChest)
} else if t.owner != Owner::Hero && self.allied(self.me_side, t.owner.side()) {
false // an ally's ground is never taken
} else { } else {
// knight and other masters' ground (also vaults and shard chambers), but not while enemies fight there // knight and enemy masters' ground (also vaults and shard chambers), but not while enemies fight there
matches!(t.kind, Kind::Floor | Kind::Treasury | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Lair | Kind::Training) && !self.contested.get(i).copied().unwrap_or(false) matches!(t.kind, Kind::Floor | Kind::Treasury | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Lair | Kind::Training) && !self.contested.get(i).copied().unwrap_or(false)
}; };
if !ok { if !ok {
@@ -3412,7 +3415,7 @@ impl Game {
match e.job { match e.job {
Job::Dig(t) => { Job::Dig(t) => {
let tile = self.map.t[t]; let tile = self.map.t[t];
if !tile.marked || !tile.diggable() || !adj4(cur, t) { if !tile.marked || tile.mark_by != e.side || !tile.diggable() || !adj4(cur, t) {
self.unreserve(me); self.unreserve(me);
e.job = Job::None; e.job = Job::None;
return; return;
@@ -3647,9 +3650,9 @@ impl Game {
return; return;
} }
} }
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.dig_neighbor(i, me).is_some(), true) { if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.dig_neighbor(i, me, e.side).is_some(), true) {
let end = p.last().copied().unwrap_or(cur); let end = p.last().copied().unwrap_or(cur);
if let Some(t) = self.dig_neighbor(end, me) { if let Some(t) = self.dig_neighbor(end, me, e.side) {
self.map.t[t].reserved = me; self.map.t[t].reserved = me;
e.path = p; e.path = p;
e.job = Job::Dig(t); e.job = Job::Dig(t);
@@ -4851,9 +4854,11 @@ impl Game {
for x in x0.min(x1)..=x0.max(x1) { for x in x0.min(x1)..=x0.max(x1) {
if inb(x, y) { if inb(x, y) {
let t = &mut self.map.t[idx(x, y)]; let t = &mut self.map.t[idx(x, y)];
if t.diggable() { // a master sets and clears only his own marks
let side = self.me.side().min(7);
if t.diggable() && (!t.marked || t.mark_by == side) {
t.marked = on; t.marked = on;
t.mark_by = self.me.side().min(7); t.mark_by = side;
} }
} }
} }
+5 -1
View File
@@ -2097,7 +2097,11 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
let ground = app.cam.ground_point(mx, my); let ground = app.cam.ground_point(mx, my);
clear_background(Color::from_rgba(4, 3, 5, 255)); clear_background(Color::from_rgba(4, 3, 5, 255));
let (color, radius) = match ui.tool { let (color, radius) = match ui.tool {
Tool::Dig => (Color::new(1.0, 0.85, 0.3, 1.0), None), Tool::Dig => {
// the dig selection in the master's colour (like the marks)
let c = render3d::master_rgb(app.game.local_side);
(Color::new(c.x, c.y, c.z, 1.0), None)
}
Tool::Hand => (Color::new(0.9, 0.75, 0.6, 1.0), None), Tool::Hand => (Color::new(0.9, 0.75, 0.6, 1.0), None),
Tool::Room(_) => (Color::new(0.4, 0.85, 1.0, 1.0), None), Tool::Room(_) => (Color::new(0.4, 0.85, 1.0, 1.0), None),
Tool::Sell => (Color::new(1.0, 0.5, 0.3, 1.0), None), Tool::Sell => (Color::new(1.0, 0.5, 0.3, 1.0), None),
+5 -13
View File
@@ -675,18 +675,10 @@ fn view_side() -> u8 {
VIEW_SIDE.with(|v| v.get()) VIEW_SIDE.with(|v| v.get())
} }
// colour of a dig mark: own marks yellow, other masters' marks in their colour // colour of a dig mark: always the colour of the master who set it
fn mark_rgb(by: u8) -> [u8; 3] { fn mark_rgb(by: u8) -> [u8; 3] {
if by == view_side() { let c = master_rgb(by);
return [255, 205, 50]; [(c.x * 255.0) as u8, (c.y * 255.0) as u8, (c.z * 255.0) as u8]
}
match by {
0 => [235, 90, 125], // the first master, seen by another one
2 => [80, 240, 110], // green rival
3 => [100, 150, 255], // blue rival
4 => [255, 150, 50], // the fourth master
_ => [255, 205, 50],
}
} }
// Glowing box in the marker's colour around a block that is marked for digging // Glowing box in the marker's colour around a block that is marked for digging
@@ -768,9 +760,9 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T
let (xf, zf) = (x as f32, z as f32); let (xf, zf) = (x as f32, z as f32);
if !map.shown(idx(x, z)) { if !map.shown(idx(x, z)) {
// fog of war: a black block, with black sides towards known open ground; // fog of war: a black block, with black sides towards known open ground;
// tiles marked for digging glow dark yellow so the plan stays visible // own tiles marked for digging glow darkly in the master's colour so the plan stays visible
let black = col(Vec3::splat(0.0), 1.0); let black = col(Vec3::splat(0.0), 1.0);
let top = if t.marked && t.mark_by == view_side() { col(vec3(0.42, 0.33, 0.06), 1.0) } else { black }; let top = if t.marked && t.mark_by == view_side() { col(master_rgb(t.mark_by) * 0.42, 1.0) } else { black };
mb.quad([vec3(xf, WALL_H, zf), vec3(xf + 1.0, WALL_H, zf), vec3(xf + 1.0, WALL_H, zf + 1.0), vec3(xf, WALL_H, zf + 1.0)], [ua, ua, ua, ua], [top; 4], [Vec3::Y; 4]); mb.quad([vec3(xf, WALL_H, zf), vec3(xf + 1.0, WALL_H, zf), vec3(xf + 1.0, WALL_H, zf + 1.0), vec3(xf, WALL_H, zf + 1.0)], [ua, ua, ua, ua], [top; 4], [Vec3::Y; 4]);
for (d, &(dx, dz)) in DIR4.iter().enumerate() { for (d, &(dx, dz)) in DIR4.iter().enumerate() {
let open = map.get(x + dx, z + dz).map_or(false, |o| !o.solid()) && map.shown(idx((x + dx).max(0), (z + dz).max(0))); let open = map.get(x + dx, z + dz).map_or(false, |o| !o.solid()) && map.shown(idx((x + dx).max(0), (z + dz).max(0)));
+77
View File
@@ -468,6 +468,60 @@ mod tests {
assert!(ground > ground0 + 20, "the yellow master digs and claims: {} -> {}", ground0, ground); assert!(ground > ground0 + 20, "the yellow master digs and claims: {} -> {}", ground0, ground);
} }
#[test]
fn ai_marks_stay_near_its_ground_and_imps_dig_only_their_masters_marks() {
let mut g = rival_game(2);
let side = g.rivals[0].side;
let owner = Owner::of_side(side);
for _ in 0..(10 * 60 * 20) {
g.update(0.05);
if g.time as i32 % 20 == 0 {
for (i, t) in g.map.t.iter().enumerate() {
if t.marked && t.mark_by == side {
let (x, y) = crate::map::xy(i);
let near = (-5..=5).any(|dy| (-5..=5).any(|dx| g.map.get(x + dx, y + dy).map_or(false, |o| o.owner == owner)));
assert!(near, "AI mark at {},{} far from its ground", x, y);
}
}
}
}
// a foreign mark next to the player's imps stays untouched
let (cx, cy) = g.map.core;
let spot = (0..g.map.t.len()).find(|&i| {
let (x, y) = crate::map::xy(i);
(x - cx).abs() < 10 && (y - cy).abs() < 10 && g.map.t[i].kind == Kind::Earth && !g.map.t[i].marked
&& DIR4.iter().any(|&(dx, dy)| g.map.get(x + dx, y + dy).map_or(false, |o| o.owner == Owner::Player && o.walkable()))
}).expect("earth beside the player's floor");
g.map.t[spot].reinforced = false;
g.map.t[spot].marked = true;
g.map.t[spot].mark_by = side;
for _ in 0..(60 * 20) {
g.update(0.05);
}
assert_eq!(g.map.t[spot].kind, Kind::Earth, "the player's imps leave the AI's mark alone");
}
#[test]
fn the_ai_summons_new_imps_when_its_imps_die() {
let mut g = rival_game(1);
let side = g.rivals[0].side;
for _ in 0..(60 * 20) {
g.update(0.05);
}
for e in g.ents.iter_mut().filter(|e| e.alive && e.side == side && e.kind == EK::Worker) {
e.hp = 0.0;
}
for _ in 0..20 {
g.update(0.05);
}
assert_eq!(g.ents.iter().filter(|e| e.alive && e.side == side && e.kind == EK::Worker).count(), 0, "all its imps are dead");
for _ in 0..(3 * 60 * 20) {
g.update(0.05);
}
let imps = g.ents.iter().filter(|e| e.alive && e.side == side && e.kind == EK::Worker).count();
assert!(imps >= 2, "new imps (one per 80 mana, like the player): {}", imps);
}
#[test] #[test]
fn ai_dig_marks_carry_the_masters_side() { fn ai_dig_marks_carry_the_masters_side() {
let mut g = rival_game(1); let mut g = rival_game(1);
@@ -564,6 +618,29 @@ mod tests {
g g
} }
#[test]
fn imps_take_ground_from_enemies_but_never_from_allies() {
let mut g = offline_league_game();
assert!(g.allied(0, 2));
// a partner floor tile right beside the player's floor
let spot = (0..g.map.t.len()).find(|&i| {
let (x, y) = crate::map::xy(i);
g.map.t[i].kind == Kind::Floor && g.map.t[i].owner == Owner::Player
&& DIR4.iter().any(|&(dx, dy)| g.map.get(x + dx, y + dy).map_or(false, |o| o.kind == Kind::Floor && o.owner == Owner::Player))
&& x > 2 && y > 2
}).expect("player floor");
let (x, y) = crate::map::xy(spot);
let n = DIR4.iter().map(|&(dx, dy)| crate::map::idx(x + dx, y + dy)).find(|&j| g.map.t[j].owner == Owner::Player).unwrap();
g.map.t[n].owner = Owner::of_side(2);
g.map.t[n].reserved = 0;
assert!(!g.claimable(n, 999), "an ally's floor stays his");
// the same tile of an enemy master may be taken
g.coop = false;
g.teams = vec![(0, 0), (2, 1)];
assert!(!g.allied(0, 2));
assert!(g.claimable(n, 999), "an enemy's floor can be claimed");
}
#[test] #[test]
fn an_offline_coop_partner_is_an_allied_ai_master() { fn an_offline_coop_partner_is_an_allied_ai_master() {
let mut g = offline_league_game(); let mut g = offline_league_game();