1use glam::{Vec2, Vec3};
25use std::collections::HashMap;
26use std::sync::{Arc, RwLock};
27
28#[derive(Debug, Clone, PartialEq)]
34pub enum BlackboardValue {
35 Bool(bool),
36 Int(i64),
37 Float(f64),
38 Vec2(Vec2),
39 Vec3(Vec3),
40 Str(String),
41 Entity(u64),
42 List(Vec<BlackboardValue>),
43}
44
45impl BlackboardValue {
46 pub fn as_float(&self) -> Option<f64> {
48 match self {
49 BlackboardValue::Float(v) => Some(*v),
50 BlackboardValue::Int(v) => Some(*v as f64),
51 BlackboardValue::Bool(v) => Some(if *v { 1.0 } else { 0.0 }),
52 _ => None,
53 }
54 }
55
56 pub fn as_bool(&self) -> Option<bool> {
57 match self {
58 BlackboardValue::Bool(v) => Some(*v),
59 BlackboardValue::Int(v) => Some(*v != 0),
60 BlackboardValue::Float(v) => Some(*v != 0.0),
61 _ => None,
62 }
63 }
64
65 pub fn as_int(&self) -> Option<i64> {
66 match self {
67 BlackboardValue::Int(v) => Some(*v),
68 BlackboardValue::Float(v) => Some(*v as i64),
69 BlackboardValue::Bool(v) => Some(if *v { 1 } else { 0 }),
70 _ => None,
71 }
72 }
73
74 pub fn as_str(&self) -> Option<&str> {
75 match self {
76 BlackboardValue::Str(s) => Some(s.as_str()),
77 _ => None,
78 }
79 }
80
81 pub fn as_vec2(&self) -> Option<Vec2> {
82 match self { BlackboardValue::Vec2(v) => Some(*v), _ => None }
83 }
84
85 pub fn as_vec3(&self) -> Option<Vec3> {
86 match self { BlackboardValue::Vec3(v) => Some(*v), _ => None }
87 }
88
89 pub fn as_entity(&self) -> Option<u64> {
90 match self { BlackboardValue::Entity(e) => Some(*e), _ => None }
91 }
92
93 pub fn as_list(&self) -> Option<&Vec<BlackboardValue>> {
94 match self { BlackboardValue::List(l) => Some(l), _ => None }
95 }
96
97 pub fn type_name(&self) -> &'static str {
99 match self {
100 BlackboardValue::Bool(_) => "bool",
101 BlackboardValue::Int(_) => "int",
102 BlackboardValue::Float(_) => "float",
103 BlackboardValue::Vec2(_) => "vec2",
104 BlackboardValue::Vec3(_) => "vec3",
105 BlackboardValue::Str(_) => "str",
106 BlackboardValue::Entity(_) => "entity",
107 BlackboardValue::List(_) => "list",
108 }
109 }
110}
111
112impl From<bool> for BlackboardValue { fn from(v: bool) -> Self { BlackboardValue::Bool(v) } }
113impl From<i64> for BlackboardValue { fn from(v: i64) -> Self { BlackboardValue::Int(v) } }
114impl From<i32> for BlackboardValue { fn from(v: i32) -> Self { BlackboardValue::Int(v as i64) } }
115impl From<f64> for BlackboardValue { fn from(v: f64) -> Self { BlackboardValue::Float(v) } }
116impl From<f32> for BlackboardValue { fn from(v: f32) -> Self { BlackboardValue::Float(v as f64) } }
117impl From<Vec2> for BlackboardValue { fn from(v: Vec2) -> Self { BlackboardValue::Vec2(v) } }
118impl From<Vec3> for BlackboardValue { fn from(v: Vec3) -> Self { BlackboardValue::Vec3(v) } }
119impl From<String> for BlackboardValue { fn from(v: String) -> Self { BlackboardValue::Str(v) } }
120impl From<&str> for BlackboardValue { fn from(v: &str) -> Self { BlackboardValue::Str(v.into()) } }
121impl From<u64> for BlackboardValue {
122 fn from(v: u64) -> Self { BlackboardValue::Entity(v) }
123}
124
125#[derive(Debug, Clone)]
131pub struct BlackboardEntry {
132 pub value: BlackboardValue,
133 pub timestamp: f64,
135 pub ttl: Option<f64>,
137}
138
139impl BlackboardEntry {
140 pub fn new(value: BlackboardValue, timestamp: f64) -> Self {
141 BlackboardEntry { value, timestamp, ttl: None }
142 }
143
144 pub fn with_ttl(mut self, ttl: f64) -> Self {
145 self.ttl = Some(ttl);
146 self
147 }
148
149 pub fn is_expired(&self, now: f64) -> bool {
151 match self.ttl {
152 Some(ttl) => now > self.timestamp + ttl,
153 None => false,
154 }
155 }
156
157 pub fn remaining_ttl(&self, now: f64) -> f64 {
159 match self.ttl {
160 Some(ttl) => (self.timestamp + ttl - now).max(0.0),
161 None => f64::INFINITY,
162 }
163 }
164}
165
166#[derive(Debug, Clone, Default)]
172pub struct Blackboard {
173 pub entries: HashMap<String, BlackboardEntry>,
174 pub current_time: f64,
175}
176
177impl Blackboard {
178 pub fn new() -> Self { Blackboard::default() }
179
180 pub fn set(&mut self, key: impl Into<String>, value: impl Into<BlackboardValue>) {
182 let entry = BlackboardEntry::new(value.into(), self.current_time);
183 self.entries.insert(key.into(), entry);
184 }
185
186 pub fn set_with_ttl(
188 &mut self,
189 key: impl Into<String>,
190 value: impl Into<BlackboardValue>,
191 ttl: f64,
192 ) {
193 let entry = BlackboardEntry::new(value.into(), self.current_time).with_ttl(ttl);
194 self.entries.insert(key.into(), entry);
195 }
196
197 pub fn get(&self, key: &str) -> Option<&BlackboardValue> {
199 self.entries.get(key).and_then(|e| {
200 if e.is_expired(self.current_time) { None } else { Some(&e.value) }
201 })
202 }
203
204 pub fn get_entry(&self, key: &str) -> Option<&BlackboardEntry> {
206 self.entries.get(key).filter(|e| !e.is_expired(self.current_time))
207 }
208
209 pub fn get_bool(&self, key: &str) -> Option<bool> {
212 self.get(key)?.as_bool()
213 }
214
215 pub fn get_int(&self, key: &str) -> Option<i64> {
216 self.get(key)?.as_int()
217 }
218
219 pub fn get_float(&self, key: &str) -> Option<f64> {
220 self.get(key)?.as_float()
221 }
222
223 pub fn get_vec2(&self, key: &str) -> Option<Vec2> {
224 self.get(key)?.as_vec2()
225 }
226
227 pub fn get_vec3(&self, key: &str) -> Option<Vec3> {
228 self.get(key)?.as_vec3()
229 }
230
231 pub fn get_str(&self, key: &str) -> Option<&str> {
232 self.get(key)?.as_str()
233 }
234
235 pub fn get_entity(&self, key: &str) -> Option<u64> {
236 self.get(key)?.as_entity()
237 }
238
239 pub fn remove(&mut self, key: &str) -> Option<BlackboardValue> {
243 self.entries.remove(key).map(|e| e.value)
244 }
245
246 pub fn update(&mut self, dt: f64) {
248 self.current_time += dt;
249 self.entries.retain(|_, e| !e.is_expired(self.current_time));
250 }
251
252 pub fn set_time(&mut self, t: f64) {
254 self.current_time = t;
255 self.entries.retain(|_, e| !e.is_expired(t));
256 }
257
258 pub fn contains(&self, key: &str) -> bool { self.get(key).is_some() }
261
262 pub fn len(&self) -> usize {
263 self.entries.values().filter(|e| !e.is_expired(self.current_time)).count()
264 }
265
266 pub fn is_empty(&self) -> bool { self.len() == 0 }
267
268 pub fn keys(&self) -> impl Iterator<Item = &String> {
270 self.entries.iter()
271 .filter(|(_, e)| !e.is_expired(self.current_time))
272 .map(|(k, _)| k)
273 }
274
275 pub fn clear(&mut self) { self.entries.clear(); }
277
278 pub fn merge(&mut self, other: &Blackboard) {
281 for (k, e) in &other.entries {
282 if !e.is_expired(other.current_time) {
283 self.entries.insert(k.clone(), e.clone());
284 }
285 }
286 }
287
288 pub fn increment_int(&mut self, key: &str, delta: i64) {
290 let current = self.get_int(key).unwrap_or(0);
291 self.set(key, BlackboardValue::Int(current + delta));
292 }
293
294 pub fn increment_float(&mut self, key: &str, delta: f64) {
296 let current = self.get_float(key).unwrap_or(0.0);
297 self.set(key, BlackboardValue::Float(current + delta));
298 }
299
300 pub fn push_to_list(&mut self, key: &str, value: BlackboardValue) {
302 let mut list = match self.get(key) {
303 Some(BlackboardValue::List(l)) => l.clone(),
304 _ => Vec::new(),
305 };
306 list.push(value);
307 self.set(key, BlackboardValue::List(list));
308 }
309
310 pub fn snapshot(&self) -> Vec<(&String, &BlackboardValue)> {
312 self.entries.iter()
313 .filter(|(_, e)| !e.is_expired(self.current_time))
314 .map(|(k, e)| (k, &e.value))
315 .collect()
316 }
317}
318
319#[derive(Debug, Clone)]
325pub struct SharedBlackboard(pub Arc<RwLock<Blackboard>>);
326
327impl SharedBlackboard {
328 pub fn new() -> Self {
329 SharedBlackboard(Arc::new(RwLock::new(Blackboard::new())))
330 }
331
332 pub fn set(&self, key: impl Into<String>, value: impl Into<BlackboardValue>) {
333 if let Ok(mut bb) = self.0.write() {
334 bb.set(key, value);
335 }
336 }
337
338 pub fn set_with_ttl(&self, key: impl Into<String>, value: impl Into<BlackboardValue>, ttl: f64) {
339 if let Ok(mut bb) = self.0.write() {
340 bb.set_with_ttl(key, value, ttl);
341 }
342 }
343
344 pub fn get_float(&self, key: &str) -> Option<f64> {
345 self.0.read().ok()?.get_float(key)
346 }
347
348 pub fn get_bool(&self, key: &str) -> Option<bool> {
349 self.0.read().ok()?.get_bool(key)
350 }
351
352 pub fn get_vec2(&self, key: &str) -> Option<Vec2> {
353 self.0.read().ok()?.get_vec2(key)
354 }
355
356 pub fn get_entity(&self, key: &str) -> Option<u64> {
357 self.0.read().ok()?.get_entity(key)
358 }
359
360 pub fn contains(&self, key: &str) -> bool {
361 self.0.read().map(|bb| bb.contains(key)).unwrap_or(false)
362 }
363
364 pub fn update(&self, dt: f64) {
365 if let Ok(mut bb) = self.0.write() {
366 bb.update(dt);
367 }
368 }
369
370 pub fn remove(&self, key: &str) {
371 if let Ok(mut bb) = self.0.write() {
372 bb.remove(key);
373 }
374 }
375
376 pub fn read<T, F: FnOnce(&Blackboard) -> T>(&self, f: F) -> Option<T> {
378 self.0.read().ok().map(|bb| f(&*bb))
379 }
380
381 pub fn write<T, F: FnOnce(&mut Blackboard) -> T>(&self, f: F) -> Option<T> {
382 self.0.write().ok().map(|mut bb| f(&mut *bb))
383 }
384}
385
386impl Default for SharedBlackboard {
387 fn default() -> Self { Self::new() }
388}
389
390#[derive(Debug, Clone, PartialEq)]
397pub enum BlackboardCondition {
398 HasKey(String),
400 NotHasKey(String),
402 KeyEquals(String, BlackboardValue),
404 KeyGreaterThan(String, f64),
406 KeyLessThan(String, f64),
408 KeyInRange(String, f64, f64),
410 IsTrue(String),
412 IsFalse(String),
414 And(Box<BlackboardCondition>, Box<BlackboardCondition>),
416 Or(Box<BlackboardCondition>, Box<BlackboardCondition>),
418 Not(Box<BlackboardCondition>),
420}
421
422impl BlackboardCondition {
423 pub fn evaluate(&self, bb: &Blackboard) -> bool {
425 match self {
426 BlackboardCondition::HasKey(k) => bb.contains(k),
427 BlackboardCondition::NotHasKey(k) => !bb.contains(k),
428 BlackboardCondition::KeyEquals(k, v) => bb.get(k) == Some(v),
429 BlackboardCondition::KeyGreaterThan(k, threshold) => {
430 bb.get_float(k).map(|f| f > *threshold).unwrap_or(false)
431 }
432 BlackboardCondition::KeyLessThan(k, threshold) => {
433 bb.get_float(k).map(|f| f < *threshold).unwrap_or(false)
434 }
435 BlackboardCondition::KeyInRange(k, min, max) => {
436 bb.get_float(k).map(|f| f >= *min && f <= *max).unwrap_or(false)
437 }
438 BlackboardCondition::IsTrue(k) => {
439 bb.get_bool(k).unwrap_or(false)
440 }
441 BlackboardCondition::IsFalse(k) => {
442 !bb.get_bool(k).unwrap_or(false)
443 }
444 BlackboardCondition::And(a, b) => a.evaluate(bb) && b.evaluate(bb),
445 BlackboardCondition::Or(a, b) => a.evaluate(bb) || b.evaluate(bb),
446 BlackboardCondition::Not(c) => !c.evaluate(bb),
447 }
448 }
449
450 pub fn and(self, other: BlackboardCondition) -> BlackboardCondition {
453 BlackboardCondition::And(Box::new(self), Box::new(other))
454 }
455
456 pub fn or(self, other: BlackboardCondition) -> BlackboardCondition {
457 BlackboardCondition::Or(Box::new(self), Box::new(other))
458 }
459
460 pub fn not(self) -> BlackboardCondition {
461 BlackboardCondition::Not(Box::new(self))
462 }
463}
464
465#[derive(Debug, Clone, Default)]
471pub struct BlackboardObserver {
472 watched_keys: Vec<String>,
474 last_values: HashMap<String, BlackboardValue>,
476 changed: Vec<String>,
478}
479
480impl BlackboardObserver {
481 pub fn new() -> Self { BlackboardObserver::default() }
482
483 pub fn watch(&mut self, key: impl Into<String>) {
485 self.watched_keys.push(key.into());
486 }
487
488 pub fn poll(&mut self, bb: &Blackboard) {
490 for key in &self.watched_keys {
491 let current = bb.get(key).cloned();
492 let previous = self.last_values.get(key).cloned();
493 if current != previous {
494 self.changed.push(key.clone());
495 match current {
496 Some(v) => { self.last_values.insert(key.clone(), v); }
497 None => { self.last_values.remove(key); }
498 }
499 }
500 }
501 }
502
503 pub fn drain_changes(&mut self) -> Vec<String> {
505 std::mem::take(&mut self.changed)
506 }
507
508 pub fn has_changed(&self, key: &str) -> bool {
510 self.changed.iter().any(|k| k == key)
511 }
512}
513
514impl Blackboard {
519 pub fn debug_dump(&self) -> String {
521 let mut entries: Vec<_> = self.entries.iter()
522 .filter(|(_, e)| !e.is_expired(self.current_time))
523 .collect();
524 entries.sort_by_key(|(k, _)| k.as_str());
525
526 let mut out = String::from("Blackboard {\n");
527 for (k, e) in &entries {
528 let ttl_info = match e.ttl {
529 Some(_) => format!(" [TTL: {:.2}s]", e.remaining_ttl(self.current_time)),
530 None => String::new(),
531 };
532 out += &format!(" {} = {:?}{}\n", k, e.value, ttl_info);
533 }
534 out += "}";
535 out
536 }
537}
538
539#[cfg(test)]
544mod tests {
545 use super::*;
546 use glam::Vec2;
547
548 #[test]
549 fn test_set_and_get() {
550 let mut bb = Blackboard::new();
551 bb.set("health", BlackboardValue::Float(100.0));
552 assert_eq!(bb.get_float("health"), Some(100.0));
553 }
554
555 #[test]
556 fn test_set_and_get_bool() {
557 let mut bb = Blackboard::new();
558 bb.set("alive", true);
559 assert_eq!(bb.get_bool("alive"), Some(true));
560 }
561
562 #[test]
563 fn test_set_and_get_int() {
564 let mut bb = Blackboard::new();
565 bb.set("kills", BlackboardValue::Int(5));
566 assert_eq!(bb.get_int("kills"), Some(5));
567 }
568
569 #[test]
570 fn test_set_and_get_vec2() {
571 let mut bb = Blackboard::new();
572 bb.set("pos", Vec2::new(1.0, 2.0));
573 assert_eq!(bb.get_vec2("pos"), Some(Vec2::new(1.0, 2.0)));
574 }
575
576 #[test]
577 fn test_set_and_get_entity() {
578 let mut bb = Blackboard::new();
579 bb.set("target", BlackboardValue::Entity(42));
580 assert_eq!(bb.get_entity("target"), Some(42));
581 }
582
583 #[test]
584 fn test_set_and_get_str() {
585 let mut bb = Blackboard::new();
586 bb.set("state", "patrolling");
587 assert_eq!(bb.get_str("state"), Some("patrolling"));
588 }
589
590 #[test]
591 fn test_ttl_expiry() {
592 let mut bb = Blackboard::new();
593 bb.set_with_ttl("temp", BlackboardValue::Float(1.0), 2.0);
594 assert!(bb.contains("temp"));
595 bb.update(3.0);
596 assert!(!bb.contains("temp"), "entry should have expired");
597 }
598
599 #[test]
600 fn test_ttl_not_yet_expired() {
601 let mut bb = Blackboard::new();
602 bb.set_with_ttl("temp", BlackboardValue::Float(1.0), 10.0);
603 bb.update(5.0);
604 assert!(bb.contains("temp"), "should still be valid");
605 }
606
607 #[test]
608 fn test_permanent_entry_not_expired() {
609 let mut bb = Blackboard::new();
610 bb.set("permanent", BlackboardValue::Bool(true));
611 bb.update(9999.0);
612 assert!(bb.contains("permanent"));
613 }
614
615 #[test]
616 fn test_remove() {
617 let mut bb = Blackboard::new();
618 bb.set("x", BlackboardValue::Int(1));
619 assert!(bb.contains("x"));
620 bb.remove("x");
621 assert!(!bb.contains("x"));
622 }
623
624 #[test]
625 fn test_len() {
626 let mut bb = Blackboard::new();
627 bb.set("a", BlackboardValue::Bool(true));
628 bb.set("b", BlackboardValue::Bool(false));
629 assert_eq!(bb.len(), 2);
630 }
631
632 #[test]
633 fn test_clear() {
634 let mut bb = Blackboard::new();
635 bb.set("a", BlackboardValue::Bool(true));
636 bb.clear();
637 assert_eq!(bb.len(), 0);
638 }
639
640 #[test]
641 fn test_merge() {
642 let mut bb1 = Blackboard::new();
643 bb1.set("a", BlackboardValue::Int(1));
644 let mut bb2 = Blackboard::new();
645 bb2.set("b", BlackboardValue::Int(2));
646 bb2.set("a", BlackboardValue::Int(99)); bb1.merge(&bb2);
648 assert_eq!(bb1.get_int("a"), Some(99));
649 assert_eq!(bb1.get_int("b"), Some(2));
650 }
651
652 #[test]
653 fn test_condition_has_key() {
654 let mut bb = Blackboard::new();
655 bb.set("x", BlackboardValue::Bool(true));
656 assert!(BlackboardCondition::HasKey("x".into()).evaluate(&bb));
657 assert!(!BlackboardCondition::HasKey("y".into()).evaluate(&bb));
658 }
659
660 #[test]
661 fn test_condition_not_has_key() {
662 let bb = Blackboard::new();
663 assert!(BlackboardCondition::NotHasKey("missing".into()).evaluate(&bb));
664 }
665
666 #[test]
667 fn test_condition_key_equals() {
668 let mut bb = Blackboard::new();
669 bb.set("mode", "attack");
670 let cond = BlackboardCondition::KeyEquals("mode".into(), BlackboardValue::Str("attack".into()));
671 assert!(cond.evaluate(&bb));
672 let cond2 = BlackboardCondition::KeyEquals("mode".into(), BlackboardValue::Str("flee".into()));
673 assert!(!cond2.evaluate(&bb));
674 }
675
676 #[test]
677 fn test_condition_greater_than() {
678 let mut bb = Blackboard::new();
679 bb.set("health", BlackboardValue::Float(75.0));
680 assert!(BlackboardCondition::KeyGreaterThan("health".into(), 50.0).evaluate(&bb));
681 assert!(!BlackboardCondition::KeyGreaterThan("health".into(), 80.0).evaluate(&bb));
682 }
683
684 #[test]
685 fn test_condition_less_than() {
686 let mut bb = Blackboard::new();
687 bb.set("health", BlackboardValue::Float(25.0));
688 assert!(BlackboardCondition::KeyLessThan("health".into(), 50.0).evaluate(&bb));
689 assert!(!BlackboardCondition::KeyLessThan("health".into(), 10.0).evaluate(&bb));
690 }
691
692 #[test]
693 fn test_condition_in_range() {
694 let mut bb = Blackboard::new();
695 bb.set("ammo", BlackboardValue::Int(5));
696 assert!(BlackboardCondition::KeyInRange("ammo".into(), 1.0, 10.0).evaluate(&bb));
697 assert!(!BlackboardCondition::KeyInRange("ammo".into(), 6.0, 10.0).evaluate(&bb));
698 }
699
700 #[test]
701 fn test_condition_is_true_false() {
702 let mut bb = Blackboard::new();
703 bb.set("armed", true);
704 assert!(BlackboardCondition::IsTrue("armed".into()).evaluate(&bb));
705 assert!(!BlackboardCondition::IsFalse("armed".into()).evaluate(&bb));
706 }
707
708 #[test]
709 fn test_condition_and() {
710 let mut bb = Blackboard::new();
711 bb.set("a", true);
712 bb.set("b", true);
713 let cond = BlackboardCondition::IsTrue("a".into())
714 .and(BlackboardCondition::IsTrue("b".into()));
715 assert!(cond.evaluate(&bb));
716 bb.set("b", false);
717 assert!(!cond.evaluate(&bb));
718 }
719
720 #[test]
721 fn test_condition_or() {
722 let mut bb = Blackboard::new();
723 bb.set("a", true);
724 bb.set("b", false);
725 let cond = BlackboardCondition::IsTrue("a".into())
726 .or(BlackboardCondition::IsTrue("b".into()));
727 assert!(cond.evaluate(&bb));
728 }
729
730 #[test]
731 fn test_condition_not() {
732 let mut bb = Blackboard::new();
733 bb.set("enemy_visible", false);
734 let cond = BlackboardCondition::IsTrue("enemy_visible".into()).not();
735 assert!(cond.evaluate(&bb));
736 }
737
738 #[test]
739 fn test_increment_int() {
740 let mut bb = Blackboard::new();
741 bb.increment_int("score", 10);
742 bb.increment_int("score", 5);
743 assert_eq!(bb.get_int("score"), Some(15));
744 }
745
746 #[test]
747 fn test_increment_float() {
748 let mut bb = Blackboard::new();
749 bb.increment_float("damage", 12.5);
750 bb.increment_float("damage", 7.5);
751 assert!((bb.get_float("damage").unwrap() - 20.0).abs() < 0.001);
752 }
753
754 #[test]
755 fn test_push_to_list() {
756 let mut bb = Blackboard::new();
757 bb.push_to_list("log", BlackboardValue::Str("event1".into()));
758 bb.push_to_list("log", BlackboardValue::Str("event2".into()));
759 let list = bb.get("log").unwrap().as_list().unwrap();
760 assert_eq!(list.len(), 2);
761 }
762
763 #[test]
764 fn test_shared_blackboard() {
765 let shared = SharedBlackboard::new();
766 shared.set("hp", BlackboardValue::Float(80.0));
767 assert_eq!(shared.get_float("hp"), Some(80.0));
768 }
769
770 #[test]
771 fn test_shared_blackboard_ttl_update() {
772 let shared = SharedBlackboard::new();
773 shared.set_with_ttl("temp", BlackboardValue::Bool(true), 1.0);
774 assert!(shared.contains("temp"));
775 shared.update(2.0);
776 assert!(!shared.contains("temp"));
777 }
778
779 #[test]
780 fn test_observer_poll() {
781 let mut bb = Blackboard::new();
782 let mut obs = BlackboardObserver::new();
783 obs.watch("health");
784 bb.set("health", BlackboardValue::Float(100.0));
785 obs.poll(&bb);
786 let changes = obs.drain_changes();
787 assert!(changes.contains(&"health".to_string()));
788 }
789
790 #[test]
791 fn test_observer_no_change() {
792 let mut bb = Blackboard::new();
793 bb.set("health", BlackboardValue::Float(100.0));
794 let mut obs = BlackboardObserver::new();
795 obs.watch("health");
796 obs.poll(&bb); obs.drain_changes();
798 obs.poll(&bb); assert!(obs.drain_changes().is_empty());
800 }
801
802 #[test]
803 fn test_debug_dump() {
804 let mut bb = Blackboard::new();
805 bb.set("x", BlackboardValue::Int(42));
806 let dump = bb.debug_dump();
807 assert!(dump.contains("x"));
808 }
809
810 #[test]
811 fn test_snapshot() {
812 let mut bb = Blackboard::new();
813 bb.set("a", BlackboardValue::Bool(true));
814 bb.set("b", BlackboardValue::Float(3.14));
815 let snap = bb.snapshot();
816 assert_eq!(snap.len(), 2);
817 }
818
819 #[test]
820 fn test_value_from_impls() {
821 let v: BlackboardValue = true.into();
822 assert_eq!(v, BlackboardValue::Bool(true));
823 let v: BlackboardValue = 42i64.into();
824 assert_eq!(v, BlackboardValue::Int(42));
825 let v: BlackboardValue = 3.14f64.into();
826 assert_eq!(v, BlackboardValue::Float(3.14));
827 }
828
829 #[test]
830 fn test_remaining_ttl() {
831 let mut bb = Blackboard::new();
832 bb.set_with_ttl("item", BlackboardValue::Bool(true), 10.0);
833 let entry = bb.get_entry("item").unwrap();
834 assert!((entry.remaining_ttl(0.0) - 10.0).abs() < 0.001);
835 assert!((entry.remaining_ttl(5.0) - 5.0).abs() < 0.001);
836 assert_eq!(entry.remaining_ttl(15.0), 0.0);
837 }
838}