Skip to main content

proof_engine/entity/
ai.rs

1//! Entity AI — Finite State Machine, Behavior Tree, and Utility AI.
2//!
3//! Three complementary AI models:
4//! - `StateMachine<S>`: enum-driven FSM with transition guards
5//! - `BehaviorTree`: composable node tree (Sequence, Selector, Decorator)
6//! - `UtilityAI`: scores candidate actions and picks the highest
7
8use std::collections::HashMap;
9use glam::Vec3;
10
11// ── Blackboard ────────────────────────────────────────────────────────────────
12
13/// Shared working memory for AI nodes.
14#[derive(Clone, Debug, Default)]
15pub struct Blackboard {
16    floats:  HashMap<String, f32>,
17    bools:   HashMap<String, bool>,
18    vec3s:   HashMap<String, Vec3>,
19    strings: HashMap<String, String>,
20}
21
22impl Blackboard {
23    pub fn new() -> Self { Self::default() }
24
25    pub fn set_float(&mut self, k: &str, v: f32)      { self.floats.insert(k.into(), v); }
26    pub fn get_float(&self, k: &str) -> f32           { self.floats.get(k).copied().unwrap_or(0.0) }
27    pub fn set_bool(&mut self, k: &str, v: bool)      { self.bools.insert(k.into(), v); }
28    pub fn get_bool(&self, k: &str) -> bool           { self.bools.get(k).copied().unwrap_or(false) }
29    pub fn set_vec3(&mut self, k: &str, v: Vec3)      { self.vec3s.insert(k.into(), v); }
30    pub fn get_vec3(&self, k: &str) -> Vec3           { self.vec3s.get(k).copied().unwrap_or(Vec3::ZERO) }
31    pub fn set_str(&mut self, k: &str, v: &str)       { self.strings.insert(k.into(), v.into()); }
32    pub fn get_str(&self, k: &str) -> &str            { self.strings.get(k).map(|s| s.as_str()).unwrap_or("") }
33
34    pub fn has_float(&self, k: &str) -> bool { self.floats.contains_key(k) }
35    pub fn has_bool(&self, k: &str)  -> bool { self.bools.contains_key(k)  }
36    pub fn has_vec3(&self, k: &str)  -> bool { self.vec3s.contains_key(k)  }
37
38    pub fn clear(&mut self) {
39        self.floats.clear();
40        self.bools.clear();
41        self.vec3s.clear();
42        self.strings.clear();
43    }
44}
45
46// ── Finite State Machine ──────────────────────────────────────────────────────
47
48/// Outcome of a state's update tick.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum StateResult {
51    /// Stay in this state.
52    Continue,
53    /// Transition to the given state index.
54    Transition(usize),
55    /// Signal that the FSM should pop (for hierarchical FSMs).
56    Pop,
57}
58
59/// A single FSM state.
60pub trait FsmState<Context>: Send + Sync {
61    fn name(&self) -> &str;
62    fn on_enter(&mut self, _ctx: &mut Context, _bb: &mut Blackboard) {}
63    fn on_exit(&mut self,  _ctx: &mut Context, _bb: &mut Blackboard) {}
64    fn tick(&mut self,      ctx: &mut Context,  bb: &mut Blackboard, dt: f32) -> StateResult;
65}
66
67/// A generic Finite State Machine.
68pub struct StateMachine<Context> {
69    states:  Vec<Box<dyn FsmState<Context>>>,
70    current: usize,
71    pub bb:  Blackboard,
72    history: Vec<usize>,
73    pub active: bool,
74}
75
76impl<Context> StateMachine<Context> {
77    pub fn new() -> Self {
78        Self {
79            states:  Vec::new(),
80            current: 0,
81            bb:      Blackboard::new(),
82            history: Vec::new(),
83            active:  false,
84        }
85    }
86
87    /// Add a state. First added = index 0.
88    pub fn add_state(&mut self, state: Box<dyn FsmState<Context>>) -> usize {
89        let idx = self.states.len();
90        self.states.push(state);
91        idx
92    }
93
94    /// Start the FSM in state `idx`.
95    pub fn start(&mut self, ctx: &mut Context, idx: usize) {
96        self.current = idx;
97        self.active  = true;
98        self.states[self.current].on_enter(ctx, &mut self.bb);
99    }
100
101    /// Tick the current state.  Handles transitions automatically.
102    pub fn tick(&mut self, ctx: &mut Context, dt: f32) {
103        if !self.active || self.states.is_empty() { return; }
104        let result = self.states[self.current].tick(ctx, &mut self.bb, dt);
105        match result {
106            StateResult::Continue => {}
107            StateResult::Transition(next) if next < self.states.len() && next != self.current => {
108                self.states[self.current].on_exit(ctx, &mut self.bb);
109                self.history.push(self.current);
110                self.current = next;
111                self.states[self.current].on_enter(ctx, &mut self.bb);
112            }
113            StateResult::Transition(_) => {}
114            StateResult::Pop => {
115                if let Some(prev) = self.history.pop() {
116                    self.states[self.current].on_exit(ctx, &mut self.bb);
117                    self.current = prev;
118                    self.states[self.current].on_enter(ctx, &mut self.bb);
119                } else {
120                    self.active = false;
121                }
122            }
123        }
124    }
125
126    pub fn current_state_name(&self) -> &str {
127        self.states.get(self.current).map(|s| s.name()).unwrap_or("none")
128    }
129
130    pub fn current_index(&self) -> usize { self.current }
131}
132
133impl<C> Default for StateMachine<C> {
134    fn default() -> Self { Self::new() }
135}
136
137// ── Behavior Tree ─────────────────────────────────────────────────────────────
138
139/// Status returned by a BT node each tick.
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141pub enum BtStatus {
142    Success,
143    Failure,
144    Running,
145}
146
147/// A node in a behavior tree.
148pub trait BtNode: Send + Sync {
149    fn tick(&mut self, bb: &mut Blackboard, dt: f32) -> BtStatus;
150    fn reset(&mut self) {}
151    fn name(&self) -> &str { "BtNode" }
152}
153
154// ── Composite nodes ───────────────────────────────────────────────────────────
155
156/// Sequence: runs children left-to-right; fails on first child failure.
157pub struct Sequence {
158    children:   Vec<Box<dyn BtNode>>,
159    current:    usize,
160}
161
162impl Sequence {
163    pub fn new(children: Vec<Box<dyn BtNode>>) -> Self {
164        Self { children, current: 0 }
165    }
166}
167
168impl BtNode for Sequence {
169    fn name(&self) -> &str { "Sequence" }
170
171    fn tick(&mut self, bb: &mut Blackboard, dt: f32) -> BtStatus {
172        while self.current < self.children.len() {
173            match self.children[self.current].tick(bb, dt) {
174                BtStatus::Success => self.current += 1,
175                BtStatus::Failure => { self.current = 0; return BtStatus::Failure; }
176                BtStatus::Running => return BtStatus::Running,
177            }
178        }
179        self.current = 0;
180        BtStatus::Success
181    }
182
183    fn reset(&mut self) {
184        self.current = 0;
185        for c in &mut self.children { c.reset(); }
186    }
187}
188
189/// Selector: runs children left-to-right; succeeds on first child success.
190pub struct Selector {
191    children: Vec<Box<dyn BtNode>>,
192    current:  usize,
193}
194
195impl Selector {
196    pub fn new(children: Vec<Box<dyn BtNode>>) -> Self {
197        Self { children, current: 0 }
198    }
199}
200
201impl BtNode for Selector {
202    fn name(&self) -> &str { "Selector" }
203
204    fn tick(&mut self, bb: &mut Blackboard, dt: f32) -> BtStatus {
205        while self.current < self.children.len() {
206            match self.children[self.current].tick(bb, dt) {
207                BtStatus::Failure => self.current += 1,
208                BtStatus::Success => { self.current = 0; return BtStatus::Success; }
209                BtStatus::Running => return BtStatus::Running,
210            }
211        }
212        self.current = 0;
213        BtStatus::Failure
214    }
215
216    fn reset(&mut self) {
217        self.current = 0;
218        for c in &mut self.children { c.reset(); }
219    }
220}
221
222/// Parallel: runs all children every tick. Succeeds when `n` succeed.
223pub struct Parallel {
224    children:      Vec<Box<dyn BtNode>>,
225    success_count: usize,
226}
227
228impl Parallel {
229    pub fn new(children: Vec<Box<dyn BtNode>>, success_count: usize) -> Self {
230        Self { children, success_count }
231    }
232
233    pub fn all(children: Vec<Box<dyn BtNode>>) -> Self {
234        let n = children.len();
235        Self::new(children, n)
236    }
237
238    pub fn any(children: Vec<Box<dyn BtNode>>) -> Self {
239        Self::new(children, 1)
240    }
241}
242
243impl BtNode for Parallel {
244    fn name(&self) -> &str { "Parallel" }
245
246    fn tick(&mut self, bb: &mut Blackboard, dt: f32) -> BtStatus {
247        let mut successes = 0;
248        let mut failures  = 0;
249        for child in &mut self.children {
250            match child.tick(bb, dt) {
251                BtStatus::Success => successes += 1,
252                BtStatus::Failure => failures  += 1,
253                BtStatus::Running => {}
254            }
255        }
256        let remaining = self.children.len() - failures;
257        if successes >= self.success_count { return BtStatus::Success; }
258        if remaining < self.success_count  { return BtStatus::Failure; }
259        BtStatus::Running
260    }
261
262    fn reset(&mut self) {
263        for c in &mut self.children { c.reset(); }
264    }
265}
266
267/// RandomSelector: like Selector but shuffles children on each activation.
268pub struct RandomSelector {
269    children: Vec<Box<dyn BtNode>>,
270    order:    Vec<usize>,
271    current:  usize,
272    rng:      u64,
273}
274
275impl RandomSelector {
276    pub fn new(children: Vec<Box<dyn BtNode>>) -> Self {
277        let n = children.len();
278        Self { children, order: (0..n).collect(), current: 0, rng: 9876543210 }
279    }
280
281    fn shuffle(&mut self) {
282        let n = self.order.len();
283        for i in (1..n).rev() {
284            self.rng ^= self.rng << 13;
285            self.rng ^= self.rng >> 7;
286            self.rng ^= self.rng << 17;
287            let j = (self.rng as usize) % (i + 1);
288            self.order.swap(i, j);
289        }
290    }
291}
292
293impl BtNode for RandomSelector {
294    fn name(&self) -> &str { "RandomSelector" }
295
296    fn tick(&mut self, bb: &mut Blackboard, dt: f32) -> BtStatus {
297        if self.current == 0 { self.shuffle(); }
298        while self.current < self.order.len() {
299            let idx = self.order[self.current];
300            match self.children[idx].tick(bb, dt) {
301                BtStatus::Failure => self.current += 1,
302                BtStatus::Success => { self.current = 0; return BtStatus::Success; }
303                BtStatus::Running => return BtStatus::Running,
304            }
305        }
306        self.current = 0;
307        BtStatus::Failure
308    }
309
310    fn reset(&mut self) {
311        self.current = 0;
312        for c in &mut self.children { c.reset(); }
313    }
314}
315
316// ── Decorator nodes ───────────────────────────────────────────────────────────
317
318/// Inverter: flips Success/Failure.
319pub struct Inverter { pub child: Box<dyn BtNode> }
320
321impl BtNode for Inverter {
322    fn name(&self) -> &str { "Inverter" }
323    fn tick(&mut self, bb: &mut Blackboard, dt: f32) -> BtStatus {
324        match self.child.tick(bb, dt) {
325            BtStatus::Success => BtStatus::Failure,
326            BtStatus::Failure => BtStatus::Success,
327            BtStatus::Running => BtStatus::Running,
328        }
329    }
330    fn reset(&mut self) { self.child.reset(); }
331}
332
333/// Succeeder: always returns Success.
334pub struct Succeeder { pub child: Box<dyn BtNode> }
335
336impl BtNode for Succeeder {
337    fn name(&self) -> &str { "Succeeder" }
338    fn tick(&mut self, bb: &mut Blackboard, dt: f32) -> BtStatus {
339        self.child.tick(bb, dt);
340        BtStatus::Success
341    }
342    fn reset(&mut self) { self.child.reset(); }
343}
344
345/// Repeater: runs child `n` times (0 = infinite).
346pub struct Repeater {
347    pub child:   Box<dyn BtNode>,
348    pub max:     u32,
349    count:       u32,
350    until_fail:  bool,
351}
352
353impl Repeater {
354    pub fn n_times(child: Box<dyn BtNode>, n: u32) -> Self {
355        Self { child, max: n, count: 0, until_fail: false }
356    }
357
358    pub fn until_failure(child: Box<dyn BtNode>) -> Self {
359        Self { child, max: 0, count: 0, until_fail: true }
360    }
361
362    pub fn forever(child: Box<dyn BtNode>) -> Self {
363        Self { child, max: 0, count: 0, until_fail: false }
364    }
365}
366
367impl BtNode for Repeater {
368    fn name(&self) -> &str { "Repeater" }
369    fn tick(&mut self, bb: &mut Blackboard, dt: f32) -> BtStatus {
370        loop {
371            let status = self.child.tick(bb, dt);
372            if status == BtStatus::Running { return BtStatus::Running; }
373            if self.until_fail && status == BtStatus::Failure { return BtStatus::Success; }
374            self.child.reset();
375            self.count += 1;
376            if self.max > 0 && self.count >= self.max {
377                self.count = 0;
378                return BtStatus::Success;
379            }
380            // Infinite repeater — only run once per tick to avoid infinite loops
381            if self.max == 0 { return BtStatus::Running; }
382        }
383    }
384    fn reset(&mut self) { self.count = 0; self.child.reset(); }
385}
386
387/// Cooldown: child can only run once every `cooldown` seconds.
388pub struct Cooldown {
389    pub child:    Box<dyn BtNode>,
390    pub cooldown: f32,
391    timer:        f32,
392}
393
394impl Cooldown {
395    pub fn new(child: Box<dyn BtNode>, cooldown: f32) -> Self {
396        Self { child, cooldown, timer: 0.0 }
397    }
398}
399
400impl BtNode for Cooldown {
401    fn name(&self) -> &str { "Cooldown" }
402    fn tick(&mut self, bb: &mut Blackboard, dt: f32) -> BtStatus {
403        if self.timer > 0.0 {
404            self.timer = (self.timer - dt).max(0.0);
405            return BtStatus::Failure;
406        }
407        let status = self.child.tick(bb, dt);
408        if status == BtStatus::Success {
409            self.timer = self.cooldown;
410        }
411        status
412    }
413    fn reset(&mut self) { self.timer = 0.0; self.child.reset(); }
414}
415
416// ── Leaf nodes ────────────────────────────────────────────────────────────────
417
418/// Always-success leaf.
419pub struct AlwaysSuccess;
420impl BtNode for AlwaysSuccess {
421    fn name(&self) -> &str { "AlwaysSuccess" }
422    fn tick(&mut self, _: &mut Blackboard, _: f32) -> BtStatus { BtStatus::Success }
423}
424
425/// Always-failure leaf.
426pub struct AlwaysFailure;
427impl BtNode for AlwaysFailure {
428    fn name(&self) -> &str { "AlwaysFailure" }
429    fn tick(&mut self, _: &mut Blackboard, _: f32) -> BtStatus { BtStatus::Failure }
430}
431
432/// Condition: reads a bool from the blackboard.
433pub struct CheckFlag { pub key: String }
434impl BtNode for CheckFlag {
435    fn name(&self) -> &str { "CheckFlag" }
436    fn tick(&mut self, bb: &mut Blackboard, _: f32) -> BtStatus {
437        if bb.get_bool(&self.key) { BtStatus::Success } else { BtStatus::Failure }
438    }
439}
440
441/// Condition: checks if a float exceeds a threshold.
442pub struct CheckFloat { pub key: String, pub threshold: f32, pub above: bool }
443impl BtNode for CheckFloat {
444    fn name(&self) -> &str { "CheckFloat" }
445    fn tick(&mut self, bb: &mut Blackboard, _: f32) -> BtStatus {
446        let v = bb.get_float(&self.key);
447        let ok = if self.above { v >= self.threshold } else { v < self.threshold };
448        if ok { BtStatus::Success } else { BtStatus::Failure }
449    }
450}
451
452/// Action: set a blackboard flag.
453pub struct SetFlag { pub key: String, pub value: bool }
454impl BtNode for SetFlag {
455    fn name(&self) -> &str { "SetFlag" }
456    fn tick(&mut self, bb: &mut Blackboard, _: f32) -> BtStatus {
457        bb.set_bool(&self.key, self.value);
458        BtStatus::Success
459    }
460}
461
462/// Action: wait for `duration` seconds.
463pub struct Wait { pub duration: f32, elapsed: f32 }
464impl Wait {
465    pub fn new(duration: f32) -> Self { Self { duration, elapsed: 0.0 } }
466}
467impl BtNode for Wait {
468    fn name(&self) -> &str { "Wait" }
469    fn tick(&mut self, _: &mut Blackboard, dt: f32) -> BtStatus {
470        self.elapsed += dt;
471        if self.elapsed >= self.duration {
472            self.elapsed = 0.0;
473            BtStatus::Success
474        } else {
475            BtStatus::Running
476        }
477    }
478    fn reset(&mut self) { self.elapsed = 0.0; }
479}
480
481// ── Behavior tree root ────────────────────────────────────────────────────────
482
483/// A complete behavior tree with its own blackboard.
484pub struct BehaviorTree {
485    root:    Box<dyn BtNode>,
486    pub bb:  Blackboard,
487    pub status: BtStatus,
488}
489
490impl BehaviorTree {
491    pub fn new(root: Box<dyn BtNode>) -> Self {
492        Self { root, bb: Blackboard::new(), status: BtStatus::Running }
493    }
494
495    pub fn tick(&mut self, dt: f32) -> BtStatus {
496        self.status = self.root.tick(&mut self.bb, dt);
497        self.status
498    }
499
500    pub fn reset(&mut self) {
501        self.root.reset();
502        self.status = BtStatus::Running;
503    }
504}
505
506// ── Utility AI ────────────────────────────────────────────────────────────────
507
508/// An action that utility AI can evaluate and execute.
509pub trait UtilityAction: Send + Sync {
510    fn name(&self) -> &str;
511    /// Score this action [0, 1]. Higher = more desirable.
512    fn score(&self, bb: &Blackboard) -> f32;
513    /// Execute this action. Returns true if complete.
514    fn execute(&mut self, bb: &mut Blackboard, dt: f32) -> bool;
515    /// Reset execution state.
516    fn reset(&mut self) {}
517}
518
519/// Scoring curve applied to a raw utility value.
520#[derive(Clone, Copy, Debug)]
521pub enum UtilityCurve {
522    Linear { m: f32, b: f32 },           // y = m*x + b
523    Quadratic { m: f32, k: f32, b: f32 }, // y = m*(x-k)^2 + b
524    Logistic { k: f32, x0: f32 },         // sigmoid
525    Exponential { k: f32 },               // e^(k*x)
526    Constant(f32),
527}
528
529impl UtilityCurve {
530    pub fn evaluate(&self, x: f32) -> f32 {
531        let y = match self {
532            UtilityCurve::Linear { m, b }         => m * x + b,
533            UtilityCurve::Quadratic { m, k, b }   => m * (x - k).powi(2) + b,
534            UtilityCurve::Logistic { k, x0 }      => 1.0 / (1.0 + (-k * (x - x0)).exp()),
535            UtilityCurve::Exponential { k }        => (k * x).exp().min(1.0),
536            UtilityCurve::Constant(c)              => *c,
537        };
538        y.clamp(0.0, 1.0)
539    }
540}
541
542/// Consideration: maps a blackboard value through a curve to a partial score.
543pub struct Consideration {
544    pub name:   String,
545    pub key:    String,   // blackboard key
546    pub min:    f32,
547    pub max:    f32,
548    pub curve:  UtilityCurve,
549    pub weight: f32,
550}
551
552impl Consideration {
553    pub fn new(name: &str, key: &str, min: f32, max: f32, curve: UtilityCurve) -> Self {
554        Self { name: name.into(), key: key.into(), min, max, curve, weight: 1.0 }
555    }
556
557    pub fn with_weight(mut self, w: f32) -> Self { self.weight = w; self }
558
559    pub fn evaluate(&self, bb: &Blackboard) -> f32 {
560        let raw = bb.get_float(&self.key);
561        let t   = ((raw - self.min) / (self.max - self.min).max(f32::EPSILON)).clamp(0.0, 1.0);
562        self.curve.evaluate(t) * self.weight
563    }
564}
565
566/// An action defined by its considerations and execution function.
567pub struct UtilityActionDef {
568    pub name:           String,
569    pub considerations: Vec<Consideration>,
570    /// How to combine multiple consideration scores.
571    pub combine:        ConsiderationCombine,
572    /// Cooldown timer.
573    pub cooldown:       f32,
574    cooldown_timer:     f32,
575    /// Custom execution callback.
576    execute_fn:         Box<dyn Fn(&mut Blackboard, f32) -> bool + Send + Sync>,
577    elapsed:            f32,
578    pub max_duration:   f32,
579}
580
581#[derive(Clone, Copy, Debug)]
582pub enum ConsiderationCombine {
583    /// Multiply all scores (any 0 = disqualified).
584    Multiply,
585    /// Average all scores.
586    Average,
587    /// Minimum score.
588    Min,
589    /// Maximum score.
590    Max,
591}
592
593impl UtilityActionDef {
594    pub fn new(
595        name: &str,
596        considerations: Vec<Consideration>,
597        execute_fn: impl Fn(&mut Blackboard, f32) -> bool + Send + Sync + 'static,
598    ) -> Self {
599        Self {
600            name: name.into(),
601            considerations,
602            combine: ConsiderationCombine::Multiply,
603            cooldown: 0.0,
604            cooldown_timer: 0.0,
605            execute_fn: Box::new(execute_fn),
606            elapsed: 0.0,
607            max_duration: f32::MAX,
608        }
609    }
610
611    pub fn with_cooldown(mut self, c: f32) -> Self { self.cooldown = c; self }
612    pub fn with_max_duration(mut self, d: f32) -> Self { self.max_duration = d; self }
613    pub fn with_combine(mut self, c: ConsiderationCombine) -> Self { self.combine = c; self }
614}
615
616impl UtilityAction for UtilityActionDef {
617    fn name(&self) -> &str { &self.name }
618
619    fn score(&self, bb: &Blackboard) -> f32 {
620        if self.cooldown_timer > 0.0 { return 0.0; }
621        if self.considerations.is_empty() { return 0.5; }
622
623        let scores: Vec<f32> = self.considerations.iter().map(|c| c.evaluate(bb)).collect();
624        match self.combine {
625            ConsiderationCombine::Multiply => scores.iter().product(),
626            ConsiderationCombine::Average  => scores.iter().sum::<f32>() / scores.len() as f32,
627            ConsiderationCombine::Min      => scores.iter().cloned().fold(f32::MAX, f32::min),
628            ConsiderationCombine::Max      => scores.iter().cloned().fold(0.0_f32, f32::max),
629        }
630    }
631
632    fn execute(&mut self, bb: &mut Blackboard, dt: f32) -> bool {
633        self.elapsed += dt;
634        let done = (self.execute_fn)(bb, dt) || self.elapsed >= self.max_duration;
635        if done {
636            self.cooldown_timer = self.cooldown;
637            self.elapsed = 0.0;
638        }
639        done
640    }
641
642    fn reset(&mut self) { self.elapsed = 0.0; }
643}
644
645/// Selects the highest-scoring available action and runs it.
646pub struct UtilityAI {
647    actions:        Vec<Box<dyn UtilityAction>>,
648    pub bb:         Blackboard,
649    current_action: Option<usize>,
650    /// Re-evaluate scores every `reeval_interval` seconds.
651    pub reeval_interval: f32,
652    reeval_timer:   f32,
653    /// Inertia: current action needs to be beaten by this margin to switch.
654    pub inertia:    f32,
655}
656
657impl UtilityAI {
658    pub fn new() -> Self {
659        Self {
660            actions:        Vec::new(),
661            bb:             Blackboard::new(),
662            current_action: None,
663            reeval_interval: 0.1,
664            reeval_timer:   0.0,
665            inertia:        0.05,
666        }
667    }
668
669    pub fn add_action(&mut self, action: Box<dyn UtilityAction>) {
670        self.actions.push(action);
671    }
672
673    pub fn tick(&mut self, dt: f32) {
674        self.reeval_timer -= dt;
675
676        // Update cooldown timers
677        // (UtilityActionDef handles its own timers internally)
678
679        // Re-evaluate
680        let should_reeval = self.reeval_timer <= 0.0;
681        if should_reeval { self.reeval_timer = self.reeval_interval; }
682
683        if should_reeval || self.current_action.is_none() {
684            let bb         = &self.bb;
685            let inertia    = self.inertia;
686            let current    = self.current_action;
687            let mut best_score = -1.0_f32;
688            let mut best_idx   = None;
689
690            for (i, action) in self.actions.iter().enumerate() {
691                let mut score = action.score(bb);
692                // Boost current action by inertia to prevent thrashing
693                if Some(i) == current { score += inertia; }
694                if score > best_score {
695                    best_score = score;
696                    best_idx   = Some(i);
697                }
698            }
699
700            if best_idx != self.current_action {
701                if let Some(old) = self.current_action {
702                    self.actions[old].reset();
703                }
704                self.current_action = best_idx;
705            }
706        }
707
708        // Execute current action
709        if let Some(idx) = self.current_action {
710            let bb = &mut self.bb;
711            self.actions[idx].execute(bb, dt);
712        }
713    }
714
715    pub fn current_action_name(&self) -> Option<&str> {
716        self.current_action.and_then(|i| self.actions.get(i)).map(|a| a.name())
717    }
718
719    pub fn scores(&self) -> Vec<(&str, f32)> {
720        self.actions.iter().map(|a| (a.name(), a.score(&self.bb))).collect()
721    }
722}
723
724impl Default for UtilityAI {
725    fn default() -> Self { Self::new() }
726}
727
728// ── Common AI contexts ────────────────────────────────────────────────────────
729
730/// Standard blackboard keys used by built-in behaviors.
731pub mod keys {
732    pub const HEALTH:           &str = "health";
733    pub const MAX_HEALTH:       &str = "max_health";
734    pub const DISTANCE_TO_PLAYER: &str = "dist_player";
735    pub const DISTANCE_TO_COVER:  &str = "dist_cover";
736    pub const AMMO:             &str = "ammo";
737    pub const IN_COVER:         &str = "in_cover";
738    pub const PLAYER_VISIBLE:   &str = "player_visible";
739    pub const TARGET_POS:       &str = "target_pos";
740    pub const SELF_POS:         &str = "self_pos";
741    pub const ALERT_LEVEL:      &str = "alert_level";
742    pub const AGGRESSION:       &str = "aggression";
743    pub const CAN_ATTACK:       &str = "can_attack";
744    pub const IS_FLEEING:       &str = "is_fleeing";
745    pub const TIME_IN_STATE:    &str = "time_in_state";
746}
747
748// ── Tests ─────────────────────────────────────────────────────────────────────
749
750#[cfg(test)]
751mod tests {
752    use super::*;
753
754    // ── Blackboard ──
755
756    #[test]
757    fn blackboard_set_get() {
758        let mut bb = Blackboard::new();
759        bb.set_float("hp", 80.0);
760        bb.set_bool("alive", true);
761        bb.set_vec3("pos", Vec3::new(1.0, 2.0, 3.0));
762        assert!((bb.get_float("hp") - 80.0).abs() < 1e-5);
763        assert!(bb.get_bool("alive"));
764        assert_eq!(bb.get_vec3("pos"), Vec3::new(1.0, 2.0, 3.0));
765    }
766
767    // ── BT nodes ──
768
769    #[test]
770    fn sequence_succeeds_when_all_succeed() {
771        let mut seq = Sequence::new(vec![
772            Box::new(AlwaysSuccess),
773            Box::new(AlwaysSuccess),
774        ]);
775        let mut bb = Blackboard::new();
776        assert_eq!(seq.tick(&mut bb, 0.016), BtStatus::Success);
777    }
778
779    #[test]
780    fn sequence_fails_on_child_failure() {
781        let mut seq = Sequence::new(vec![
782            Box::new(AlwaysSuccess),
783            Box::new(AlwaysFailure),
784            Box::new(AlwaysSuccess),
785        ]);
786        let mut bb = Blackboard::new();
787        assert_eq!(seq.tick(&mut bb, 0.016), BtStatus::Failure);
788    }
789
790    #[test]
791    fn selector_succeeds_on_first_success() {
792        let mut sel = Selector::new(vec![
793            Box::new(AlwaysFailure),
794            Box::new(AlwaysSuccess),
795        ]);
796        let mut bb = Blackboard::new();
797        assert_eq!(sel.tick(&mut bb, 0.016), BtStatus::Success);
798    }
799
800    #[test]
801    fn selector_fails_when_all_fail() {
802        let mut sel = Selector::new(vec![
803            Box::new(AlwaysFailure),
804            Box::new(AlwaysFailure),
805        ]);
806        let mut bb = Blackboard::new();
807        assert_eq!(sel.tick(&mut bb, 0.016), BtStatus::Failure);
808    }
809
810    #[test]
811    fn inverter_flips_success() {
812        let mut inv = Inverter { child: Box::new(AlwaysSuccess) };
813        let mut bb = Blackboard::new();
814        assert_eq!(inv.tick(&mut bb, 0.016), BtStatus::Failure);
815    }
816
817    #[test]
818    fn wait_runs_and_completes() {
819        let mut w  = Wait::new(0.1);
820        let mut bb = Blackboard::new();
821        assert_eq!(w.tick(&mut bb, 0.05), BtStatus::Running);
822        assert_eq!(w.tick(&mut bb, 0.06), BtStatus::Success);
823    }
824
825    #[test]
826    fn check_flag_reads_blackboard() {
827        let mut bb = Blackboard::new();
828        bb.set_bool("alive", true);
829        let mut node = CheckFlag { key: "alive".into() };
830        assert_eq!(node.tick(&mut bb, 0.016), BtStatus::Success);
831        bb.set_bool("alive", false);
832        assert_eq!(node.tick(&mut bb, 0.016), BtStatus::Failure);
833    }
834
835    #[test]
836    fn check_float_threshold() {
837        let mut bb = Blackboard::new();
838        bb.set_float("hp", 20.0);
839        let mut node = CheckFloat { key: "hp".into(), threshold: 50.0, above: false };
840        assert_eq!(node.tick(&mut bb, 0.016), BtStatus::Success); // 20 < 50
841    }
842
843    #[test]
844    fn cooldown_blocks_repeat() {
845        let mut cd = Cooldown::new(Box::new(AlwaysSuccess), 1.0);
846        let mut bb = Blackboard::new();
847        assert_eq!(cd.tick(&mut bb, 0.016), BtStatus::Success);  // fires
848        assert_eq!(cd.tick(&mut bb, 0.016), BtStatus::Failure);  // on cooldown
849        // Advance past cooldown
850        cd.tick(&mut bb, 1.0);
851        assert_eq!(cd.tick(&mut bb, 0.016), BtStatus::Success);  // fires again
852    }
853
854    // ── Utility AI ──
855
856    #[test]
857    fn utility_curve_linear() {
858        let c = UtilityCurve::Linear { m: 1.0, b: 0.0 };
859        assert!((c.evaluate(0.5) - 0.5).abs() < 1e-5);
860    }
861
862    #[test]
863    fn utility_curve_clamps() {
864        let c = UtilityCurve::Linear { m: 2.0, b: 0.0 };
865        assert!((c.evaluate(1.0) - 1.0).abs() < 1e-5); // clamped to 1
866        assert!((c.evaluate(-1.0) - 0.0).abs() < 1e-5); // clamped to 0
867    }
868
869    #[test]
870    fn utility_ai_selects_best() {
871        let mut ai = UtilityAI::new();
872        // Action A scores 0.9 if "prefer_a" is true
873        ai.add_action(Box::new(UtilityActionDef::new(
874            "action_a",
875            vec![Consideration::new("pref_a", "prefer_a",
876                0.0, 1.0, UtilityCurve::Linear { m: 1.0, b: 0.0 })],
877            |_, _| true,
878        )));
879        // Action B always scores 0.1
880        ai.add_action(Box::new(UtilityActionDef::new(
881            "action_b",
882            vec![Consideration::new("const", "dummy",
883                0.0, 1.0, UtilityCurve::Constant(0.1))],
884            |_, _| true,
885        )));
886
887        ai.bb.set_float("prefer_a", 0.9);
888        ai.bb.set_float("dummy", 0.5);
889        ai.tick(0.016);
890
891        assert_eq!(ai.current_action_name(), Some("action_a"));
892    }
893}