Automated balance runs; AI masters keep growing, mine more, defend with the rally flag and spells; Builder talent stronger
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
24ba7fb5e3
commit
66eaedf016
8 files changed
+320
-21
No files matched your search
@@ -14,8 +14,9 @@ const THINK: [f32; 3] = [5.2, 3.25, 1.95]; // seconds between two thoughts (a ca
|
||||
const MAX_RADIUS: [i32; 3] = [11, 15, 19]; // how far the dungeon grows around the heart
|
||||
const MAX_IMPS: [usize; 3] = [5, 7, 9];
|
||||
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.0, 1.2, 1.35]; // own army / enemy army needed to attack (easy is reckless, hard waits)
|
||||
const MARK_REACH: i32 = 5; // an AI master marks only this close to its own ground
|
||||
const CREATURE_TARGET: [usize; 3] = [30, 50, 80]; // beds an AI master builds up to
|
||||
const MAX_TOWERS: [usize; 3] = [1, 2, 4];
|
||||
const START_GOLD: [f32; 3] = [1500.0, 2500.0, 3500.0];
|
||||
|
||||
@@ -51,10 +52,11 @@ impl Game {
|
||||
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_rooms(d, cx, cy);
|
||||
self.ai_towers(d, cx, cy);
|
||||
self.ai_fight(k, d, side, cx, cy);
|
||||
self.ai_talents();
|
||||
let focus = self.rivals[k].focus;
|
||||
self.ai_talents(focus);
|
||||
}
|
||||
|
||||
// the tiles an AI master may mark: within MARK_REACH tiles of its own ground
|
||||
@@ -161,18 +163,23 @@ impl Game {
|
||||
|
||||
// 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 {
|
||||
// gold for one and a half paydays and a little more: enough
|
||||
if self.gold > self.next_payday_cost() * 1.5 + 1000.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
|
||||
// marked veins the imps can reach (next to own ground) are being worked already; one vein
|
||||
// for every three imps at a time
|
||||
let side = me.side().min(7);
|
||||
let busy = (0..self.map.t.len())
|
||||
.filter(|&i| {
|
||||
let t = &self.map.t[i];
|
||||
let (x, y) = xy(i);
|
||||
t.marked && t.mark_by == side && 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()))
|
||||
})
|
||||
.count();
|
||||
if busy >= (self.worker_count() / 3).max(1) {
|
||||
return;
|
||||
}
|
||||
self.ai_mark_tunnel(cx, cy, |g: &Game, i: usize| {
|
||||
let t = &g.map.t[i];
|
||||
@@ -220,7 +227,7 @@ 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) {
|
||||
fn ai_rooms(&mut self, d: usize, 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)) {
|
||||
@@ -239,6 +246,29 @@ impl Game {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// the base is done: a bigger treasury when it is nearly full
|
||||
if self.gold > self.gold_cap() * 0.85 {
|
||||
let cost = ROOM_COST[0] as f32 * 9.0;
|
||||
if self.gold >= cost + 400.0 {
|
||||
if let Some((x0, y0)) = self.ai_free_rect(cx, cy, 3, 3) {
|
||||
self.build_rect(0, x0, y0, x0 + 2, y0 + 2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// more beds and food while the army may still grow (by difficulty)
|
||||
if self.counts[1] >= CREATURE_TARGET[d] || self.free_beds() > 2 {
|
||||
return;
|
||||
}
|
||||
let room = if self.counts[2] * 10 < self.counts[1] * 6 { 2 } else { 1 };
|
||||
let (w, h) = (4, 3);
|
||||
let cost = ROOM_COST[room] as f32 * (w * h) as f32;
|
||||
if self.gold < cost + 400.0 {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// has this master researched the tool (rooms, towers)?
|
||||
@@ -295,6 +325,11 @@ impl Game {
|
||||
self.place_mark(cx + 1, cy + 3, false);
|
||||
}
|
||||
let army = self.side_army(side);
|
||||
// heroes or enemy units in the dungeon: everybody goes for them (the rally flag) and the
|
||||
// master casts lightning on them and heals his hurt creatures
|
||||
if !self.is_partner(side) && self.ai_defend(k) {
|
||||
return;
|
||||
}
|
||||
// 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));
|
||||
@@ -363,6 +398,49 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
// the defence of the dungeon; true while there are intruders
|
||||
fn ai_defend(&mut self, k: usize) -> bool {
|
||||
let me = self.me;
|
||||
let intruders: Vec<(f32, f32)> = self
|
||||
.ents
|
||||
.iter()
|
||||
.filter(|e| e.alive && !e.held && e.captive == 0 && self.is_hostile(e) && self.map.get(e.x as i32, e.y as i32).map_or(false, |t| t.owner == me))
|
||||
.map(|e| (e.x, e.y))
|
||||
.collect();
|
||||
if intruders.is_empty() {
|
||||
if self.rivals[k].defending {
|
||||
self.rivals[k].defending = false;
|
||||
self.flag = None;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// the middle of the intruders, on walkable ground
|
||||
let n = intruders.len() as f32;
|
||||
let (mx, my) = intruders.iter().fold((0.0, 0.0), |a, p| (a.0 + p.0 / n, a.1 + p.1 / n));
|
||||
let near = intruders.iter().min_by(|a, b| ((a.0 - mx).powi(2) + (a.1 - my).powi(2)).partial_cmp(&((b.0 - mx).powi(2) + (b.1 - my).powi(2))).unwrap()).copied().unwrap();
|
||||
let tile = idx(near.0 as i32, near.1 as i32);
|
||||
if self.rivals[k].attacking {
|
||||
self.rivals[k].attacking = false; // home first
|
||||
}
|
||||
self.rivals[k].defending = true;
|
||||
if self.map.t[tile].walkable() && self.flag != Some(tile) {
|
||||
self.flag = Some(tile);
|
||||
}
|
||||
// lightning on the thickest of them, healing for the hurt
|
||||
let bolt = 50.0 * self.talents.spell_cost();
|
||||
if self.mana >= bolt + 40.0 && self.known(crate::game::LIGHTNING_TOOL) {
|
||||
self.lightning(near.0, near.1);
|
||||
}
|
||||
let heal = 60.0 * self.talents.spell_cost();
|
||||
let hurt = self.ents.iter().filter(|e| e.alive && e.creature() && e.hp < e.max_hp * 0.5 && (e.x - near.0).powi(2) + (e.y - near.1).powi(2) < 36.0).map(|e| (e.x, e.y)).next();
|
||||
if let Some((hx, hy)) = hurt {
|
||||
if self.mana >= heal + 40.0 && self.known(crate::game::HEAL_TOOL) {
|
||||
self.heal(hx, hy);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// is an enemy heart near tile f?
|
||||
fn ai_enemy_heart_near(&self, side: u8, f: usize) -> bool {
|
||||
let (fx, fy) = xy(f);
|
||||
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
// Automated balancing: whole games played by the AI brain on every side, headless and
|
||||
// deterministic, many seeds each. Run with
|
||||
// cargo test --release balance_ -- --ignored --nocapture --test-threads=4
|
||||
// and read the tables (docs/BALANCE.md keeps the findings).
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use crate::game::*;
|
||||
use crate::rival::{Rival, PARTNER_GIFT_EVERY, RIVAL_CORE_HP};
|
||||
|
||||
const STEP: f32 = 0.05;
|
||||
|
||||
// the master on side 0 gets the same brain as the rivals
|
||||
fn pilot(g: &mut Game, difficulty: u8, focus: Option<u8>) {
|
||||
let core = g.map.core;
|
||||
// like every AI master: the 9x9 starting area and a portal of its own beside it
|
||||
g.carve_master_area(0, core);
|
||||
g.add_master_portal(0, core);
|
||||
g.rivals.push(Rival { side: 0, core, hp: RIVAL_CORE_HP, alive: true, radius: 5, grow_t: 0.0, spawn_t: 0.0, attack_t: 0.0, difficulty, power: 0.0, gift_t: PARTNER_GIFT_EVERY, think_t: 1.0, attacking: false, army0: 0.0, focus, defending: false });
|
||||
}
|
||||
|
||||
// the other corners of a square map for up to three more masters (centre of the core)
|
||||
fn corners(w: i32, h: i32) -> [(i32, i32); 3] {
|
||||
[(w - 29, 13), (w - 29, h - 15), (15, h - 15)]
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Outcome {
|
||||
out_at: Vec<(u8, f32)>, // side, minute it fell
|
||||
score: Vec<(u8, f32)>, // side, strength at the end (alive ones)
|
||||
winner: Option<u8>,
|
||||
minutes: f32,
|
||||
}
|
||||
|
||||
fn alive(g: &Game, side: u8) -> bool {
|
||||
g.core_hp_of(side) > 0.0 && g.rivals.iter().any(|r| r.side == side && r.alive)
|
||||
}
|
||||
|
||||
fn score(g: &Game, side: u8) -> f32 {
|
||||
g.core_hp_of(side) * 2.0 + g.side_army(side) + g.gold_of(side) * 0.2
|
||||
}
|
||||
|
||||
// plays until one side is left or the time is up; side 0 falling does not stop it
|
||||
fn play(g: &mut Game, sides: &[u8], max_min: f32) -> Outcome {
|
||||
let mut o = Outcome::default();
|
||||
let steps = (max_min * 60.0 / STEP) as usize;
|
||||
for k in 0..steps {
|
||||
g.update(STEP);
|
||||
if g.state != 0 {
|
||||
g.state = 0; // the harness decides the end
|
||||
}
|
||||
if k % 20 == 0 {
|
||||
for &s in sides {
|
||||
if !o.out_at.iter().any(|x| x.0 == s) && !alive(g, s) {
|
||||
o.out_at.push((s, g.time / 60.0));
|
||||
}
|
||||
}
|
||||
let left: Vec<u8> = sides.iter().copied().filter(|&s| !o.out_at.iter().any(|x| x.0 == s)).collect();
|
||||
if left.len() <= 1 {
|
||||
o.winner = left.first().copied();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
o.minutes = g.time / 60.0;
|
||||
o.score = sides.iter().copied().filter(|&s| !o.out_at.iter().any(|x| x.0 == s)).map(|s| (s, score(g, s))).collect();
|
||||
if o.winner.is_none() {
|
||||
o.winner = o.score.iter().max_by(|a, b| a.1.partial_cmp(&b.1).unwrap()).map(|x| x.0);
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
fn arena(seed: u32, size: usize) -> Game {
|
||||
let mut g = Game::new(seed, size, size);
|
||||
g.gate_on = false; // no hero waves: masters against each other
|
||||
g.ai_difficulty = 1;
|
||||
g
|
||||
}
|
||||
|
||||
fn seeds() -> Vec<u32> {
|
||||
let n: u32 = std::env::var("BALANCE_SEEDS").ok().and_then(|s| s.parse().ok()).unwrap_or(6);
|
||||
(1..=n).map(|k| 1000 + k * 37).collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- difficulty duels
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn balance_difficulty() {
|
||||
println!("\n== difficulty duels (2 masters, 112x112, no waves, 45 min) ==");
|
||||
println!("{:<14} {:>5} {:>5} {:>5} {:>8}", "pair", "A", "B", "draw", "min");
|
||||
for (a, b) in [(0u8, 1u8), (1, 2), (0, 2), (1, 1)] {
|
||||
let (mut wa, mut wb, mut dr, mut mins) = (0, 0, 0, 0.0);
|
||||
for (k, seed) in seeds().into_iter().enumerate() {
|
||||
let mut g = arena(seed, 112);
|
||||
// swap who plays which side every other seed (side 0 has the map's own start)
|
||||
let (d0, d2) = if k % 2 == 0 { (a, b) } else { (b, a) };
|
||||
pilot(&mut g, d0, None);
|
||||
let (x, y) = corners(112, 112)[k % 3];
|
||||
g.add_rival(2, x, y, d2);
|
||||
let o = play(&mut g, &[0, 2], 45.0);
|
||||
mins += o.minutes;
|
||||
let a_side = if k % 2 == 0 { 0 } else { 2 };
|
||||
let decisive = o.out_at.len() == 1;
|
||||
match o.winner {
|
||||
Some(w) if decisive && w == a_side => wa += 1,
|
||||
Some(_) if decisive => wb += 1,
|
||||
_ => dr += 1,
|
||||
}
|
||||
}
|
||||
let n = seeds().len() as f32;
|
||||
println!("{:<14} {:>5} {:>5} {:>5} {:>8.1}", format!("{} vs {}", a, b), wa, wb, dr, mins / n);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- talent branches
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn balance_talents() {
|
||||
use crate::talents::*;
|
||||
println!("\n== talent tournament (4 masters, medium, 128x128, no waves, 50 min) ==");
|
||||
let names = ["builder", "warlord", "warlock", "merchant"];
|
||||
let mut wins = [0usize; 4];
|
||||
let mut place_sum = [0.0f32; 4];
|
||||
let mut lv_sum = [0.0f32; 4];
|
||||
let sides = [0u8, 2, 3, 4];
|
||||
for (k, seed) in seeds().into_iter().enumerate() {
|
||||
let mut g = arena(seed, 128);
|
||||
// the branches move round the four starts from seed to seed
|
||||
let branch_of = |s: usize| ((s + k) % 4) as u8;
|
||||
pilot(&mut g, 1, Some(branch_of(0)));
|
||||
for (n, &(x, y)) in corners(128, 128).iter().enumerate() {
|
||||
g.add_rival(sides[n + 1], x, y, 1);
|
||||
g.rivals.last_mut().unwrap().focus = Some(branch_of(n + 1));
|
||||
}
|
||||
let o = play(&mut g, &sides, 50.0);
|
||||
// placement: the winner 1st, the survivors by score, the fallen by how long they lasted
|
||||
let mut order: Vec<u8> = Vec::new();
|
||||
let mut alive_sorted = o.score.clone();
|
||||
alive_sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
|
||||
order.extend(alive_sorted.iter().map(|x| x.0));
|
||||
let mut fallen = o.out_at.clone();
|
||||
fallen.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
|
||||
order.extend(fallen.iter().map(|x| x.0));
|
||||
for (place, &s) in order.iter().enumerate() {
|
||||
let n = sides.iter().position(|&x| x == s).unwrap();
|
||||
let b = branch_of(n) as usize;
|
||||
place_sum[b] += place as f32 + 1.0;
|
||||
if place == 0 {
|
||||
wins[b] += 1;
|
||||
}
|
||||
lv_sum[b] += g.talents_of(s).lv[b] as f32;
|
||||
}
|
||||
let _ = BRANCHES;
|
||||
}
|
||||
let n = seeds().len() as f32;
|
||||
println!("{:<10} {:>5} {:>10} {:>10}", "branch", "wins", "avg place", "avg level");
|
||||
for b in 0..4 {
|
||||
println!("{:<10} {:>5} {:>10.2} {:>10.1}", names[b], wins[b], place_sum[b] / n, lv_sum[b] / n);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- one master against the waves
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn balance_waves() {
|
||||
println!("\n== one AI master against the hero waves (96x96, 40 min) ==");
|
||||
println!("{:<6} {:>8} {:>6} {:>7} {:>7} {:>7} {:>6}", "diff", "fell@min", "wave", "heroes", "lost", "gold", "army");
|
||||
for d in 0..3u8 {
|
||||
for seed in seeds() {
|
||||
let mut g = Game::new(seed, 96, 96);
|
||||
g.ai_difficulty = d;
|
||||
pilot(&mut g, d, None);
|
||||
let mut fell = None;
|
||||
for k in 0..((40.0 * 60.0 / STEP) as usize) {
|
||||
g.update(STEP);
|
||||
if g.state != 0 {
|
||||
if g.core_hp <= 0.0 {
|
||||
fell = Some(g.time / 60.0);
|
||||
break;
|
||||
}
|
||||
g.state = 0;
|
||||
}
|
||||
if std::env::var("BALANCE_TRACE").is_ok() && k % (120.0 / STEP) as usize == 0 {
|
||||
let rooms: usize = g.counts.iter().sum();
|
||||
let portal = (0..g.map.portals.len()).any(|p| g.map.portal_owner(p) == crate::map::Owner::Player);
|
||||
println!(" seed {} d{} min {:>4.1}: creatures {:>3} imps {:>2} gold {:>6} rooms {:>3} lair {:>3} farm {:>3} wave {:>2} core {:>5} portal {} heroes near {}",
|
||||
seed, d, g.time / 60.0, g.creature_count(), g.worker_count(), g.gold as i32, rooms, g.counts[1], g.counts[2], g.wave, g.core_hp as i32, portal,
|
||||
g.ents.iter().filter(|e| e.alive && e.active_hero()).count());
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{:<6} {:>8} {:>6} {:>7} {:>7} {:>7} {:>6}",
|
||||
d,
|
||||
fell.map_or("-".to_string(), |m| format!("{:.1}", m)),
|
||||
g.wave,
|
||||
g.stats.get(crate::stats::HEROES) as i32,
|
||||
g.stats.get(crate::stats::LOST) as i32,
|
||||
g.gold as i32,
|
||||
g.side_army(0) as i32
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,6 +161,8 @@ pub struct St {
|
||||
|
||||
pub const RUNE_TOOL: usize = 20; // index of the rune forge tower in ui::TOOLS
|
||||
pub const SUMMON_TOOL: usize = 11; // index of summoning in ui::TOOLS
|
||||
pub const LIGHTNING_TOOL: usize = 9; // lightning in ui::TOOLS
|
||||
pub const HEAL_TOOL: usize = 10; // healing in ui::TOOLS
|
||||
|
||||
pub fn st(k: EK) -> St {
|
||||
let s = |name, hp, atk, range, speed, interval, wage, bounty| St { name, hp, atk, range, speed, interval, wage, bounty };
|
||||
|
||||
@@ -30,6 +30,7 @@ mod props;
|
||||
mod research;
|
||||
mod rival;
|
||||
mod rogues;
|
||||
mod balance;
|
||||
mod talents;
|
||||
mod weather;
|
||||
mod names;
|
||||
|
||||
+1
-1
@@ -365,7 +365,7 @@ impl Game {
|
||||
// the seat keeps its master (dungeon, gold, creatures): the brain (ai.rs) plays it
|
||||
let core = self.core_of_side(side);
|
||||
let difficulty = self.ai_difficulty;
|
||||
self.rivals.push(crate::rival::Rival { side, core, hp: crate::rival::RIVAL_CORE_HP, alive: true, radius: 6, grow_t: 0.0, spawn_t: 0.0, attack_t: 0.0, difficulty, power: 0.0, gift_t: crate::rival::PARTNER_GIFT_EVERY, think_t: 1.0, attacking: false, army0: 0.0 });
|
||||
self.rivals.push(crate::rival::Rival { side, core, hp: crate::rival::RIVAL_CORE_HP, alive: true, radius: 6, grow_t: 0.0, spawn_t: 0.0, attack_t: 0.0, difficulty, power: 0.0, gift_t: crate::rival::PARTNER_GIFT_EVERY, think_t: 1.0, attacking: false, army0: 0.0, focus: None, defending: false });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -33,6 +33,10 @@ pub struct Rival {
|
||||
pub attacking: bool, // the attack flag is set on an enemy heart
|
||||
#[serde(default)]
|
||||
pub army0: f32, // army strength when the attack began
|
||||
#[serde(default)]
|
||||
pub focus: Option<u8>, // balance runs: this master learns only one talent branch
|
||||
#[serde(default)]
|
||||
pub defending: bool, // the rally flag is on intruders in the dungeon (ai.rs)
|
||||
}
|
||||
|
||||
impl Rival {
|
||||
@@ -98,7 +102,7 @@ impl Game {
|
||||
let core = (cx - 1, cy - 1);
|
||||
self.carve_master_area(side, core);
|
||||
self.add_master_portal(side, core);
|
||||
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.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, focus: None, defending: false });
|
||||
self.adopt_master(side, core, difficulty, true);
|
||||
}
|
||||
|
||||
|
||||
+13
-5
@@ -73,10 +73,10 @@ impl Talents {
|
||||
}
|
||||
// Builder: imps dig and claim faster, rooms cost less
|
||||
pub fn dig_speed(&self) -> f32 {
|
||||
1.0 + 0.1 * self.level(BUILDER)
|
||||
1.0 + 0.15 * self.level(BUILDER)
|
||||
}
|
||||
pub fn room_cost(&self) -> f32 {
|
||||
1.0 - 0.06 * self.level(BUILDER)
|
||||
1.0 - 0.1 * self.level(BUILDER)
|
||||
}
|
||||
// Warlord: creatures hit harder and take less damage
|
||||
pub fn damage(&self) -> f32 {
|
||||
@@ -136,8 +136,16 @@ impl Game {
|
||||
self.sfx_ui(Sfx::LevelUp);
|
||||
}
|
||||
|
||||
// an AI master spends his points by himself: the branches in turn, the lowest first
|
||||
pub(crate) fn ai_talents(&mut self) {
|
||||
// an AI master spends his points by himself: the branches in turn, the lowest first (a balance
|
||||
// run may hold him to one branch)
|
||||
pub(crate) fn ai_talents(&mut self, focus: Option<u8>) {
|
||||
if let Some(b) = focus {
|
||||
let b = b as usize;
|
||||
while self.talents.free() > 0 && self.talents.lv[b] < MAX_LEVEL {
|
||||
self.talents.lv[b] += 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
while self.talents.free() > 0 {
|
||||
let order = [WARLORD, BUILDER, MERCHANT, WARLOCK];
|
||||
let Some(&b) = order.iter().filter(|&&b| self.talents.lv[b] < MAX_LEVEL).min_by_key(|&&b| self.talents.lv[b]) else { break };
|
||||
@@ -213,7 +221,7 @@ mod tests {
|
||||
fn ai_masters_spend_their_points() {
|
||||
let mut g = Game::new(9, 64, 64);
|
||||
g.talents.xp = point_xp(6);
|
||||
g.ai_talents();
|
||||
g.ai_talents(None);
|
||||
assert_eq!(g.talents.free(), 0);
|
||||
assert!(g.talents.lv.iter().all(|&l| l >= 1), "{:?}", g.talents.lv);
|
||||
}
|
||||
|
||||
@@ -559,7 +559,7 @@ fn branch_text(t: &crate::talents::Talents, b: usize) -> String {
|
||||
let desc = |lv: u8| {
|
||||
let l = lv as i32;
|
||||
let (a, c) = match b {
|
||||
BUILDER => (10 * l, 6 * l),
|
||||
BUILDER => (15 * l, 10 * l),
|
||||
WARLORD => (5 * l, 5 * l),
|
||||
WARLOCK => (15 * l, 6 * l),
|
||||
_ => (6 * l, 6 * l),
|
||||
|
||||
Reference in new issue
Block a user