diff --git a/docs/BALANCE.md b/docs/BALANCE.md new file mode 100644 index 0000000..7c156f2 --- /dev/null +++ b/docs/BALANCE.md @@ -0,0 +1,51 @@ +# Balance runs + +Whole games played headless by the AI brain on every side (`src/balance.rs`), deterministic per seed: + +```bash +cargo test --release balance_ -- --ignored --nocapture --test-threads=3 +BALANCE_SEEDS=16 cargo test --release balance_difficulty -- --ignored --nocapture # more seeds +BALANCE_PAIR=0,1 ... # one difficulty pairing only (0 easy, 1 medium, 2 hard) +BALANCE_TRACE=1 ... # wave runs: a status line every two minutes +``` + +- **balance_difficulty** – duels of two AI masters (112x112, no hero waves, 45 min); the sides swap + every other seed. Undecided games count as draws. +- **balance_talents** – four AI masters (medium), each held to one talent branch, the branches moving + round the four starts; placement by survival and strength. +- **balance_waves** – one AI master alone against the hero waves (96x96, 40 min). + +## Findings and changes (2026-11-06) + +First runs showed: +- AI masters stopped growing after their build order (99 room tiles, 24 creatures from minute 10). +- They mined one vein at a time and only below 1500 gold. +- They fought hero waves piecemeal and never cast spells. +- "Hard" attacked even when weaker (boldness 0.9) and was no stronger than "easy". + +Changes: +- AI masters keep building lairs and gardens up to 24 / 50 / 80 beds (easy / medium / hard), and a + bigger treasury when the old one is nearly full. +- They mine several veins (one per three imps) whenever gold is below 1.5 paydays + 1000. +- Defence: with intruders in the dungeon the rally flag goes on them, the master casts lightning on + them and heals his hurt creatures; an attack is called off to defend. +- Boldness (own army / enemy army needed to attack): easy 1.0, medium 1.2, hard 1.35. +- Builder talent: imps dig +15% and rooms cost -10% per level (was +10% / -6%). + +Results after the changes: + +| | before | after | +|---|---|---| +| one master vs waves: fallen of 8 (easy/medium/hard) | 2 / 3 / 4 | 2 / 2 / 2 | +| duels, 16 seeds: easy vs medium | 6 : 6 | 3 : 6 | +| medium vs hard | 5 : 6 (8 seeds 3 : 2) | 5 : 6 | +| easy vs hard | 3 : 7 | 3 : 7 | +| talents, average place (builder / warlord / warlock / merchant) | 3.3 / 1.9 / 3.0 / 1.9 | 3.1 / 2.1 / 2.5 / 2.3 | + +Notes: +- The noise is about ±2 wins at 16 seeds; medium vs medium ended 5 : 7. +- The AI uses only lightning and heal, so Warlock is still worth more to a human player than the + tournament shows. +- Builder pays off where space, not gold, limits building; AI dungeons are bounded by their radius. +- One map seed (1333) never brings heroes to the master. It shows as "wave 11, heroes 0" in the + wave table and is a map outlier. diff --git a/src/ai.rs b/src/ai.rs index d0179fa..f9abc18 100644 --- a/src/ai.rs +++ b/src/ai.rs @@ -16,7 +16,7 @@ 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.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 CREATURE_TARGET: [usize; 3] = [24, 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]; diff --git a/src/balance.rs b/src/balance.rs index ebee1a8..55923ae 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -89,7 +89,12 @@ fn seeds() -> Vec { 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)] { + // BALANCE_PAIR=a,b runs one pairing only + let pairs: Vec<(u8, u8)> = match std::env::var("BALANCE_PAIR").ok().and_then(|p| p.split_once(',').map(|(a, b)| (a.parse().ok(), b.parse().ok()))) { + Some((Some(a), Some(b))) => vec![(a, b)], + _ => vec![(0, 1), (1, 2), (0, 2), (1, 1)], + }; + for (a, b) in pairs { 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);