Compare commits
3
Commits
66eaedf016
...
dab31579df
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dab31579df | ||
|
|
e85e602491 | ||
|
|
28eac52953 |
No files matched your search
@@ -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.
|
||||||
+2
-2
@@ -140,8 +140,8 @@ grid), guard and attack markers (shields and swords), savegames with the game st
|
|||||||
or trees with lightning every 6-20 s. Dimmed daylight, rain streaks, fog veils, flashes; a HUD label.
|
or trees with lightning every 6-20 s. Dimmed daylight, rain streaks, fog veils, flashes; a HUD label.
|
||||||
6. **Named creatures** (done, v202611-00068, names.rs) - every creature gets a name from its kind's list in the
|
6. **Named creatures** (done, v202611-00068, names.rs) - every creature gets a name from its kind's list in the
|
||||||
language files (20 per kind, per language), a humorous epithet from level 5 and a grand one from level 8;
|
language files (20 per kind, per language), a humorous epithet from level 5 and a grand one from level 8;
|
||||||
names in the hover info, the creature overview and the messages. A creature of level 3+ that falls leaves a
|
names in the hover info, the creature overview and the messages. A creature of level 10 that falls leaves a
|
||||||
memorial stone on its master's ground (hover: who lies there; at most 24).
|
memorial stone on its master's ground (hover: who lies there; at most 30 per map).
|
||||||
7. **Hall of the fallen** (done, v202611-00073) - every named creature of level 3+ that falls is remembered
|
7. **Hall of the fallen** (done, v202611-00073) - every named creature of level 3+ that falls is remembered
|
||||||
with its level, heroes slain and how it fell (slain by a kind, lava, water, snare, lightning): in the game
|
with its level, heroes slain and how it fell (slain by a kind, lava, water, snare, lightning): in the game
|
||||||
(creature overview "Fallen", or a click with the hand on a memorial stone) and over all games on the
|
(creature overview "Fallen", or a click with the hand on a memorial stone) and over all games on the
|
||||||
|
|||||||
@@ -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 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 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 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 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];
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -89,7 +89,12 @@ fn seeds() -> Vec<u32> {
|
|||||||
fn balance_difficulty() {
|
fn balance_difficulty() {
|
||||||
println!("\n== difficulty duels (2 masters, 112x112, no waves, 45 min) ==");
|
println!("\n== difficulty duels (2 masters, 112x112, no waves, 45 min) ==");
|
||||||
println!("{:<14} {:>5} {:>5} {:>5} {:>8}", "pair", "A", "B", "draw", "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);
|
let (mut wa, mut wb, mut dr, mut mins) = (0, 0, 0, 0.0);
|
||||||
for (k, seed) in seeds().into_iter().enumerate() {
|
for (k, seed) in seeds().into_iter().enumerate() {
|
||||||
let mut g = arena(seed, 112);
|
let mut g = arena(seed, 112);
|
||||||
|
|||||||
+19
-7
@@ -8,9 +8,11 @@ use crate::i18n::{tr, trf};
|
|||||||
use crate::map::*;
|
use crate::map::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
// the fallen get a stone from this level on; at most MAX_MEMORIALS stones (the oldest go)
|
// the fallen are remembered in the hall from HALL_LEVEL on; only the greatest (MEMORIAL_LEVEL)
|
||||||
pub const MEMORIAL_LEVEL: u32 = 3;
|
// get a stone on the map, at most MAX_MEMORIALS of them (the oldest go)
|
||||||
pub const MAX_MEMORIALS: usize = 24;
|
pub const HALL_LEVEL: u32 = 3;
|
||||||
|
pub const MEMORIAL_LEVEL: u32 = 10;
|
||||||
|
pub const MAX_MEMORIALS: usize = 30;
|
||||||
|
|
||||||
// what killed a creature besides another unit (Ent::bane)
|
// what killed a creature besides another unit (Ent::bane)
|
||||||
pub const BANE_SNARE: u8 = 250;
|
pub const BANE_SNARE: u8 = 250;
|
||||||
@@ -134,13 +136,17 @@ impl Game {
|
|||||||
// a named creature of some standing fell: a stone with its name on its master's ground, as
|
// a named creature of some standing fell: a stone with its name on its master's ground, as
|
||||||
// near to where it fell as there is plain floor
|
// near to where it fell as there is plain floor
|
||||||
pub(crate) fn raise_memorial(&mut self, e: &Ent) -> bool {
|
pub(crate) fn raise_memorial(&mut self, e: &Ent) -> bool {
|
||||||
if e.hero || e.name == 0 || e.level < MEMORIAL_LEVEL || !Owner::MASTER_SIDES.contains(&e.side) {
|
if e.hero || e.name == 0 || e.level < HALL_LEVEL || !Owner::MASTER_SIDES.contains(&e.side) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// the hall of the fallen keeps the local master's (also without room for a stone)
|
// the hall of the fallen keeps the local master's (also without room for a stone)
|
||||||
if e.side == self.local_side {
|
if e.side == self.local_side {
|
||||||
self.hall.push(Fallen { kind: e.kind, name: e.name, level: e.level, kills: e.kills, bane: e.bane, time: self.time, when: 0 });
|
self.hall.push(Fallen { kind: e.kind, name: e.name, level: e.level, kills: e.kills, bane: e.bane, time: self.time, when: 0 });
|
||||||
}
|
}
|
||||||
|
// a stone only for the greatest
|
||||||
|
if e.level < MEMORIAL_LEVEL {
|
||||||
|
return false; // the usual message tells of it
|
||||||
|
}
|
||||||
let owner = Owner::of_side(e.side);
|
let owner = Owner::of_side(e.side);
|
||||||
let (x, y) = (e.x as i32, e.y as i32);
|
let (x, y) = (e.x as i32, e.y as i32);
|
||||||
let taken: Vec<usize> = self.memorials.iter().map(|m| m.tile).collect();
|
let taken: Vec<usize> = self.memorials.iter().map(|m| m.tile).collect();
|
||||||
@@ -261,12 +267,18 @@ mod tests {
|
|||||||
t.owner = Owner::Player;
|
t.owner = Owner::Player;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let id = g.spawn(EK::Ogre, false, cx as f32 + 1.5, cy as f32 + 5.5, 4);
|
let id = g.spawn(EK::Ogre, false, cx as f32 + 1.5, cy as f32 + 5.5, 10);
|
||||||
let j = g.ents.iter().position(|e| e.id == id).unwrap();
|
let j = g.ents.iter().position(|e| e.id == id).unwrap();
|
||||||
g.kill(j);
|
g.kill(j);
|
||||||
assert_eq!(g.memorials.len(), 1);
|
assert_eq!(g.memorials.len(), 1);
|
||||||
let m = &g.memorials[0];
|
let m = g.memorials[0].clone();
|
||||||
assert_eq!((m.kind, m.level), (EK::Ogre, 4));
|
assert_eq!((m.kind, m.level), (EK::Ogre, 10));
|
||||||
|
// level 4: in the hall, but no stone on the map
|
||||||
|
let o4 = g.spawn(EK::Ogre, false, cx as f32 + 3.5, cy as f32 + 5.5, 4);
|
||||||
|
let j = g.ents.iter().position(|e| e.id == o4).unwrap();
|
||||||
|
g.kill(j);
|
||||||
|
assert_eq!(g.memorials.len(), 1);
|
||||||
|
assert_eq!(g.hall.len(), 2);
|
||||||
assert!(g.memorial_at(m.tile).is_some());
|
assert!(g.memorial_at(m.tile).is_some());
|
||||||
// a level 1 kobold is not remembered
|
// a level 1 kobold is not remembered
|
||||||
let k = g.spawn(EK::Kobold, false, cx as f32 + 2.5, cy as f32 + 5.5, 1);
|
let k = g.spawn(EK::Kobold, false, cx as f32 + 2.5, cy as f32 + 5.5, 1);
|
||||||
|
|||||||
Reference in new issue
Block a user