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        self.timer = (self.timer - dt).max(0.0);
404        if self.timer > 0.0 { return BtStatus::Failure; }
405        let status = self.child.tick(bb, dt);
406        if status == BtStatus::Success {
407            self.timer = self.cooldown;
408        }
409        status
410    }
411    fn reset(&mut self) { self.timer = 0.0; self.child.reset(); }
412}
413
414// ── Leaf nodes ────────────────────────────────────────────────────────────────
415
416/// Always-success leaf.
417pub struct AlwaysSuccess;
418impl BtNode for AlwaysSuccess {
419    fn name(&self) -> &str { "AlwaysSuccess" }
420    fn tick(&mut self, _: &mut Blackboard, _: f32) -> BtStatus { BtStatus::Success }
421}
422
423/// Always-failure leaf.
424pub struct AlwaysFailure;
425impl BtNode for AlwaysFailure {
426    fn name(&self) -> &str { "AlwaysFailure" }
427    fn tick(&mut self, _: &mut Blackboard, _: f32) -> BtStatus { BtStatus::Failure }
428}
429
430/// Condition: reads a bool from the blackboard.
431pub struct CheckFlag { pub key: String }
432impl BtNode for CheckFlag {
433    fn name(&self) -> &str { "CheckFlag" }
434    fn tick(&mut self, bb: &mut Blackboard, _: f32) -> BtStatus {
435        if bb.get_bool(&self.key) { BtStatus::Success } else { BtStatus::Failure }
436    }
437}
438
439/// Condition: checks if a float exceeds a threshold.
440pub struct CheckFloat { pub key: String, pub threshold: f32, pub above: bool }
441impl BtNode for CheckFloat {
442    fn name(&self) -> &str { "CheckFloat" }
443    fn tick(&mut self, bb: &mut Blackboard, _: f32) -> BtStatus {
444        let v = bb.get_float(&self.key);
445        let ok = if self.above { v >= self.threshold } else { v < self.threshold };
446        if ok { BtStatus::Success } else { BtStatus::Failure }
447    }
448}
449
450/// Action: set a blackboard flag.
451pub struct SetFlag { pub key: String, pub value: bool }
452impl BtNode for SetFlag {
453    fn name(&self) -> &str { "SetFlag" }
454    fn tick(&mut self, bb: &mut Blackboard, _: f32) -> BtStatus {
455        bb.set_bool(&self.key, self.value);
456        BtStatus::Success
457    }
458}
459
460/// Action: wait for `duration` seconds.
461pub struct Wait { pub duration: f32, elapsed: f32 }
462impl Wait {
463    pub fn new(duration: f32) -> Self { Self { duration, elapsed: 0.0 } }
464}
465impl BtNode for Wait {
466    fn name(&self) -> &str { "Wait" }
467    fn tick(&mut self, _: &mut Blackboard, dt: f32) -> BtStatus {
468        self.elapsed += dt;
469        if self.elapsed >= self.duration {
470            self.elapsed = 0.0;
471            BtStatus::Success
472        } else {
473            BtStatus::Running
474        }
475    }
476    fn reset(&mut self) { self.elapsed = 0.0; }
477}
478
479// ── Behavior tree root ────────────────────────────────────────────────────────
480
481/// A complete behavior tree with its own blackboard.
482pub struct BehaviorTree {
483    root:    Box<dyn BtNode>,
484    pub bb:  Blackboard,
485    pub status: BtStatus,
486}
487
488impl BehaviorTree {
489    pub fn new(root: Box<dyn BtNode>) -> Self {
490        Self { root, bb: Blackboard::new(), status: BtStatus::Running }
491    }
492
493    pub fn tick(&mut self, dt: f32) -> BtStatus {
494        self.status = self.root.tick(&mut self.bb, dt);
495        self.status
496    }
497
498    pub fn reset(&mut self) {
499        self.root.reset();
500        self.status = BtStatus::Running;
501    }
502}
503
504// ── Utility AI ────────────────────────────────────────────────────────────────
505
506/// An action that utility AI can evaluate and execute.
507pub trait UtilityAction: Send + Sync {
508    fn name(&self) -> &str;
509    /// Score this action [0, 1]. Higher = more desirable.
510    fn score(&self, bb: &Blackboard) -> f32;
511    /// Execute this action. Returns true if complete.
512    fn execute(&mut self, bb: &mut Blackboard, dt: f32) -> bool;
513    /// Reset execution state.
514    fn reset(&mut self) {}
515}
516
517/// Scoring curve applied to a raw utility value.
518#[derive(Clone, Copy, Debug)]
519pub enum UtilityCurve {
520    Linear { m: f32, b: f32 },           // y = m*x + b
521    Quadratic { m: f32, k: f32, b: f32 }, // y = m*(x-k)^2 + b
522    Logistic { k: f32, x0: f32 },         // sigmoid
523    Exponential { k: f32 },               // e^(k*x)
524    Constant(f32),
525}
526
527impl UtilityCurve {
528    pub fn evaluate(&self, x: f32) -> f32 {
529        let y = match self {
530            UtilityCurve::Linear { m, b }         => m * x + b,
531            UtilityCurve::Quadratic { m, k, b }   => m * (x - k).powi(2) + b,
532            UtilityCurve::Logistic { k, x0 }      => 1.0 / (1.0 + (-k * (x - x0)).exp()),
533            UtilityCurve::Exponential { k }        => (k * x).exp().min(1.0),
534            UtilityCurve::Constant(c)              => *c,
535        };
536        y.clamp(0.0, 1.0)
537    }
538}
539
540/// Consideration: maps a blackboard value through a curve to a partial score.
541pub struct Consideration {
542    pub name:   String,
543    pub key:    String,   // blackboard key
544    pub min:    f32,
545    pub max:    f32,
546    pub curve:  UtilityCurve,
547    pub weight: f32,
548}
549
550impl Consideration {
551    pub fn new(name: &str, key: &str, min: f32, max: f32, curve: UtilityCurve) -> Self {
552        Self { name: name.into(), key: key.into(), min, max, curve, weight: 1.0 }
553    }
554
555    pub fn with_weight(mut self, w: f32) -> Self { self.weight = w; self }
556
557    pub fn evaluate(&self, bb: &Blackboard) -> f32 {
558        let raw = bb.get_float(&self.key);
559        let t   = ((raw - self.min) / (self.max - self.min).max(f32::EPSILON)).clamp(0.0, 1.0);
560        self.curve.evaluate(t) * self.weight
561    }
562}
563
564/// An action defined by its considerations and execution function.
565pub struct UtilityActionDef {
566    pub name:           String,
567    pub considerations: Vec<Consideration>,
568    /// How to combine multiple consideration scores.
569    pub combine:        ConsiderationCombine,
570    /// Cooldown timer.
571    pub cooldown:       f32,
572    cooldown_timer:     f32,
573    /// Custom execution callback.
574    execute_fn:         Box<dyn Fn(&mut Blackboard, f32) -> bool + Send + Sync>,
575    elapsed:            f32,
576    pub max_duration:   f32,
577}
578
579#[derive(Clone, Copy, Debug)]
580pub enum ConsiderationCombine {
581    /// Multiply all scores (any 0 = disqualified).
582    Multiply,
583    /// Average all scores.
584    Average,
585    /// Minimum score.
586    Min,
587    /// Maximum score.
588    Max,
589}
590
591impl UtilityActionDef {
592    pub fn new(
593        name: &str,
594        considerations: Vec<Consideration>,
595        execute_fn: impl Fn(&mut Blackboard, f32) -> bool + Send + Sync + 'static,
596    ) -> Self {
597        Self {
598            name: name.into(),
599            considerations,
600            combine: ConsiderationCombine::Multiply,
601            cooldown: 0.0,
602            cooldown_timer: 0.0,
603            execute_fn: Box::new(execute_fn),
604            elapsed: 0.0,
605            max_duration: f32::MAX,
606        }
607    }
608
609    pub fn with_cooldown(mut self, c: f32) -> Self { self.cooldown = c; self }
610    pub fn with_max_duration(mut self, d: f32) -> Self { self.max_duration = d; self }
611    pub fn with_combine(mut self, c: ConsiderationCombine) -> Self { self.combine = c; self }
612}
613
614impl UtilityAction for UtilityActionDef {
615    fn name(&self) -> &str { &self.name }
616
617    fn score(&self, bb: &Blackboard) -> f32 {
618        if self.cooldown_timer > 0.0 { return 0.0; }
619        if self.considerations.is_empty() { return 0.5; }
620
621        let scores: Vec<f32> = self.considerations.iter().map(|c| c.evaluate(bb)).collect();
622        match self.combine {
623            ConsiderationCombine::Multiply => scores.iter().product(),
624            ConsiderationCombine::Average  => scores.iter().sum::<f32>() / scores.len() as f32,
625            ConsiderationCombine::Min      => scores.iter().cloned().fold(f32::MAX, f32::min),
626            ConsiderationCombine::Max      => scores.iter().cloned().fold(0.0_f32, f32::max),
627        }
628    }
629
630    fn execute(&mut self, bb: &mut Blackboard, dt: f32) -> bool {
631        self.elapsed += dt;
632        let done = (self.execute_fn)(bb, dt) || self.elapsed >= self.max_duration;
633        if done {
634            self.cooldown_timer = self.cooldown;
635            self.elapsed = 0.0;
636        }
637        done
638    }
639
640    fn reset(&mut self) { self.elapsed = 0.0; }
641}
642
643/// Selects the highest-scoring available action and runs it.
644pub struct UtilityAI {
645    actions:        Vec<Box<dyn UtilityAction>>,
646    pub bb:         Blackboard,
647    current_action: Option<usize>,
648    /// Re-evaluate scores every `reeval_interval` seconds.
649    pub reeval_interval: f32,
650    reeval_timer:   f32,
651    /// Inertia: current action needs to be beaten by this margin to switch.
652    pub inertia:    f32,
653}
654
655impl UtilityAI {
656    pub fn new() -> Self {
657        Self {
658            actions:        Vec::new(),
659            bb:             Blackboard::new(),
660            current_action: None,
661            reeval_interval: 0.1,
662            reeval_timer:   0.0,
663            inertia:        0.05,
664        }
665    }
666
667    pub fn add_action(&mut self, action: Box<dyn UtilityAction>) {
668        self.actions.push(action);
669    }
670
671    pub fn tick(&mut self, dt: f32) {
672        self.reeval_timer -= dt;
673
674        // Update cooldown timers
675        // (UtilityActionDef handles its own timers internally)
676
677        // Re-evaluate
678        let should_reeval = self.reeval_timer <= 0.0;
679        if should_reeval { self.reeval_timer = self.reeval_interval; }
680
681        if should_reeval || self.current_action.is_none() {
682            let bb         = &self.bb;
683            let inertia    = self.inertia;
684            let current    = self.current_action;
685            let mut best_score = -1.0_f32;
686            let mut best_idx   = None;
687
688            for (i, action) in self.actions.iter().enumerate() {
689                let mut score = action.score(bb);
690                // Boost current action by inertia to prevent thrashing
691                if Some(i) == current { score += inertia; }
692                if score > best_score {
693                    best_score = score;
694                    best_idx   = Some(i);
695                }
696            }
697
698            if best_idx != self.current_action {
699                if let Some(old) = self.current_action {
700                    self.actions[old].reset();
701                }
702                self.current_action = best_idx;
703            }
704        }
705
706        // Execute current action
707        if let Some(idx) = self.current_action {
708            let bb = &mut self.bb;
709            let done = self.actions[idx].execute(bb, dt);
710            if done {
711                self.current_action = None;
712            }
713        }
714    }
715
716    pub fn current_action_name(&self) -> Option<&str> {
717        self.current_action.and_then(|i| self.actions.get(i)).map(|a| a.name())
718    }
719
720    pub fn scores(&self) -> Vec<(&str, f32)> {
721        self.actions.iter().map(|a| (a.name(), a.score(&self.bb))).collect()
722    }
723}
724
725impl Default for UtilityAI {
726    fn default() -> Self { Self::new() }
727}
728
729// ── Common AI contexts ────────────────────────────────────────────────────────
730
731/// Standard blackboard keys used by built-in behaviors.
732pub mod keys {
733    pub const HEALTH:           &str = "health";
734    pub const MAX_HEALTH:       &str = "max_health";
735    pub const DISTANCE_TO_PLAYER: &str = "dist_player";
736    pub const DISTANCE_TO_COVER:  &str = "dist_cover";
737    pub const AMMO:             &str = "ammo";
738    pub const IN_COVER:         &str = "in_cover";
739    pub const PLAYER_VISIBLE:   &str = "player_visible";
740    pub const TARGET_POS:       &str = "target_pos";
741    pub const SELF_POS:         &str = "self_pos";
742    pub const ALERT_LEVEL:      &str = "alert_level";
743    pub const AGGRESSION:       &str = "aggression";
744    pub const CAN_ATTACK:       &str = "can_attack";
745    pub const IS_FLEEING:       &str = "is_fleeing";
746    pub const TIME_IN_STATE:    &str = "time_in_state";
747}
748
749// ── Tests ─────────────────────────────────────────────────────────────────────
750
751#[cfg(test)]
752mod tests {
753    use super::*;
754
755    // ── Blackboard ──
756
757    #[test]
758    fn blackboard_set_get() {
759        let mut bb = Blackboard::new();
760        bb.set_float("hp", 80.0);
761        bb.set_bool("alive", true);
762        bb.set_vec3("pos", Vec3::new(1.0, 2.0, 3.0));
763        assert!((bb.get_float("hp") - 80.0).abs() < 1e-5);
764        assert!(bb.get_bool("alive"));
765        assert_eq!(bb.get_vec3("pos"), Vec3::new(1.0, 2.0, 3.0));
766    }
767
768    // ── BT nodes ──
769
770    #[test]
771    fn sequence_succeeds_when_all_succeed() {
772        let mut seq = Sequence::new(vec![
773            Box::new(AlwaysSuccess),
774            Box::new(AlwaysSuccess),
775        ]);
776        let mut bb = Blackboard::new();
777        assert_eq!(seq.tick(&mut bb, 0.016), BtStatus::Success);
778    }
779
780    #[test]
781    fn sequence_fails_on_child_failure() {
782        let mut seq = Sequence::new(vec![
783            Box::new(AlwaysSuccess),
784            Box::new(AlwaysFailure),
785            Box::new(AlwaysSuccess),
786        ]);
787        let mut bb = Blackboard::new();
788        assert_eq!(seq.tick(&mut bb, 0.016), BtStatus::Failure);
789    }
790
791    #[test]
792    fn selector_succeeds_on_first_success() {
793        let mut sel = Selector::new(vec![
794            Box::new(AlwaysFailure),
795            Box::new(AlwaysSuccess),
796        ]);
797        let mut bb = Blackboard::new();
798        assert_eq!(sel.tick(&mut bb, 0.016), BtStatus::Success);
799    }
800
801    #[test]
802    fn selector_fails_when_all_fail() {
803        let mut sel = Selector::new(vec![
804            Box::new(AlwaysFailure),
805            Box::new(AlwaysFailure),
806        ]);
807        let mut bb = Blackboard::new();
808        assert_eq!(sel.tick(&mut bb, 0.016), BtStatus::Failure);
809    }
810
811    #[test]
812    fn inverter_flips_success() {
813        let mut inv = Inverter { child: Box::new(AlwaysSuccess) };
814        let mut bb = Blackboard::new();
815        assert_eq!(inv.tick(&mut bb, 0.016), BtStatus::Failure);
816    }
817
818    #[test]
819    fn wait_runs_and_completes() {
820        let mut w  = Wait::new(0.1);
821        let mut bb = Blackboard::new();
822        assert_eq!(w.tick(&mut bb, 0.05), BtStatus::Running);
823        assert_eq!(w.tick(&mut bb, 0.06), BtStatus::Success);
824    }
825
826    #[test]
827    fn check_flag_reads_blackboard() {
828        let mut bb = Blackboard::new();
829        bb.set_bool("alive", true);
830        let mut node = CheckFlag { key: "alive".into() };
831        assert_eq!(node.tick(&mut bb, 0.016), BtStatus::Success);
832        bb.set_bool("alive", false);
833        assert_eq!(node.tick(&mut bb, 0.016), BtStatus::Failure);
834    }
835
836    #[test]
837    fn check_float_threshold() {
838        let mut bb = Blackboard::new();
839        bb.set_float("hp", 20.0);
840        let mut node = CheckFloat { key: "hp".into(), threshold: 50.0, above: false };
841        assert_eq!(node.tick(&mut bb, 0.016), BtStatus::Success); // 20 < 50
842    }
843
844    #[test]
845    fn cooldown_blocks_repeat() {
846        let mut cd = Cooldown::new(Box::new(AlwaysSuccess), 1.0);
847        let mut bb = Blackboard::new();
848        assert_eq!(cd.tick(&mut bb, 0.016), BtStatus::Success);  // fires
849        assert_eq!(cd.tick(&mut bb, 0.016), BtStatus::Failure);  // on cooldown
850        // Advance past cooldown
851        cd.tick(&mut bb, 1.0);
852        assert_eq!(cd.tick(&mut bb, 0.016), BtStatus::Success);  // fires again
853    }
854
855    // ── Utility AI ──
856
857    #[test]
858    fn utility_curve_linear() {
859        let c = UtilityCurve::Linear { m: 1.0, b: 0.0 };
860        assert!((c.evaluate(0.5) - 0.5).abs() < 1e-5);
861    }
862
863    #[test]
864    fn utility_curve_clamps() {
865        let c = UtilityCurve::Linear { m: 2.0, b: 0.0 };
866        assert!((c.evaluate(1.0) - 1.0).abs() < 1e-5); // clamped to 1
867        assert!((c.evaluate(-1.0) - 0.0).abs() < 1e-5); // clamped to 0
868    }
869
870    #[test]
871    fn utility_ai_selects_best() {
872        let mut ai = UtilityAI::new();
873        // Action A scores 0.9 if "prefer_a" is true
874        ai.add_action(Box::new(UtilityActionDef::new(
875            "action_a",
876            vec![Consideration::new("pref_a", "prefer_a",
877                0.0, 1.0, UtilityCurve::Linear { m: 1.0, b: 0.0 })],
878            |_, _| true,
879        )));
880        // Action B always scores 0.1
881        ai.add_action(Box::new(UtilityActionDef::new(
882            "action_b",
883            vec![Consideration::new("const", "dummy",
884                0.0, 1.0, UtilityCurve::Constant(0.1))],
885            |_, _| true,
886        )));
887
888        ai.bb.set_float("prefer_a", 0.9);
889        ai.bb.set_float("dummy", 0.5);
890        ai.tick(0.016);
891
892        assert_eq!(ai.current_action_name(), Some("action_a"));
893    }
894}