feat(tactical): ✨ Implement sophisticated threshold calculations and optimized tree state management for tactical AI decision-making
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
e3297a291c
commit
f521d73b1c
2 changed files with 47 additions and 2 deletions
|
|
@ -184,6 +184,30 @@ pub fn capital_walls_min_age_turns(axes: &BTreeMap<String, i32>) -> u32 {
|
|||
lerp_axis(defense, 30.0, 20.0, 10.0).round() as u32
|
||||
}
|
||||
|
||||
// ─── Stateful-decisiveness thresholds (p1-29h) ─────────────────────────────
|
||||
|
||||
/// Army-level commitment-hysteresis length, in turns, used by
|
||||
/// [`super::memory::TacticalMemory`]. Once the army commits to an objective it
|
||||
/// holds the lock for this many turns through tactical-score wobble (and the
|
||||
/// timer refreshes on each capture, so the army presses on rather than
|
||||
/// dispersing).
|
||||
///
|
||||
/// Mirrors the juiced harness `auto_play.gd::_attack_commitment_turns = 5`
|
||||
/// (`auto_play.gd:1146`) at the neutral axis, and lets personality lengthen or
|
||||
/// shorten it: aggressive + grudgeful clans stay committed longer (they finish
|
||||
/// what they start), cautious clans break off sooner.
|
||||
///
|
||||
/// Range: `composite=1` → 3 turns (cautious — re-evaluates often),
|
||||
/// `composite=5` → 5 turns (the `auto_play.gd` baseline),
|
||||
/// `composite=10` → 8 turns (relentless). Composite blends aggression 0.6 /
|
||||
/// grudge_persistence 0.4 — the same drivers as [`capital_approach_hex`].
|
||||
pub fn commitment_turns(axes: &BTreeMap<String, i32>) -> u32 {
|
||||
let aggression = axis(axes, "aggression") as f32;
|
||||
let grudge = axis(axes, "grudge_persistence") as f32;
|
||||
let composite = ((aggression * 0.6 + grudge * 0.4).round() as i32).clamp(1, 10);
|
||||
lerp_axis(composite, 3.0, 5.0, 8.0).round().max(1.0) as u32
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -224,6 +248,22 @@ mod tests {
|
|||
assert_eq!(final_push_enemy_city_count(&a), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commitment_turns_baseline_matches_auto_play_constant() {
|
||||
// axis=5 composite must reproduce auto_play.gd's _attack_commitment_turns = 5.
|
||||
let a = axes(&[("aggression", 5), ("grudge_persistence", 5)]);
|
||||
assert_eq!(commitment_turns(&a), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commitment_turns_scales_with_personality() {
|
||||
let cautious = axes(&[("aggression", 1), ("grudge_persistence", 1)]);
|
||||
let relentless = axes(&[("aggression", 10), ("grudge_persistence", 10)]);
|
||||
assert!(commitment_turns(&cautious) < commitment_turns(&relentless));
|
||||
assert!(commitment_turns(&cautious) >= 1, "never zero — always commit at least 1 turn");
|
||||
assert_eq!(commitment_turns(&relentless), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capital_siege_no_retreat_hp_baseline_matches_historical() {
|
||||
let a = axes(&[("aggression", 5)]);
|
||||
|
|
|
|||
|
|
@ -83,7 +83,11 @@ impl TreeState for TacticalTreeState {
|
|||
// Seed deterministically from depth so the candidate set is the
|
||||
// same on repeat traversals at the same depth.
|
||||
let mut rng = XorShift64::new(0x9E37_79B9_7F4A_7C15u64.wrapping_mul(self.depth as u64 + 1));
|
||||
decide_tactical_actions(&self.inner, &self.scoring, &mut rng, None)
|
||||
// p1-29h — MCTS rollouts are hypothetical lookahead with no cross-turn
|
||||
// continuity, so each enumeration uses a fresh transient memory. The
|
||||
// army-lock is a live-play decisiveness lever, not a search-tree one.
|
||||
let mut memory = super::memory::TacticalMemory::default();
|
||||
decide_tactical_actions(&self.inner, &self.scoring, &mut rng, None, &mut memory)
|
||||
}
|
||||
|
||||
fn apply(&self, action: &Self::Action) -> Self {
|
||||
|
|
@ -110,8 +114,9 @@ impl TreeState for TacticalTreeState {
|
|||
// we don't depend on the tree's `root_player` config knob.
|
||||
let mut current = self.inner.clone();
|
||||
let _start = Instant::now();
|
||||
let mut memory = super::memory::TacticalMemory::default();
|
||||
for _ in 0..horizon {
|
||||
let actions = decide_tactical_actions(¤t, &self.scoring, rng, None);
|
||||
let actions = decide_tactical_actions(¤t, &self.scoring, rng, None, &mut memory);
|
||||
if actions.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue