Skip to main content

dreamwell_engine/physics/
heuristics.rs

1use super::properties::{PropertyAssignment, PropertyPredicate};
2use serde::{Deserialize, Serialize};
3
4/// Heuristic rule — infers interactions from tags and properties.
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct HeuristicRule {
7    pub name: String,
8    pub when_tags_all: Vec<String>,
9    pub when_tags_any: Vec<String>,
10    pub when_props: Vec<PropertyPredicate>,
11    pub then_emit_signals: Vec<String>,
12    pub then_set_props: Vec<PropertyAssignment>,
13    pub then_trigger_emitters: Vec<String>,
14    pub then_raise_event: Option<String>,
15}
16
17/// Result of evaluating heuristic rules against properties.
18#[derive(Debug, Clone)]
19pub struct HeuristicResult {
20    pub rule_name: String,
21    pub emitted_signals: Vec<String>,
22    pub property_mutations: Vec<PropertyAssignment>,
23    pub triggered_emitters: Vec<String>,
24    pub raised_event: Option<String>,
25}
26
27/// Heuristic engine — evaluates rules against semantic properties.
28#[derive(Debug, Clone, Default)]
29pub struct HeuristicEngine {
30    pub rules: Vec<HeuristicRule>,
31}
32
33impl HeuristicEngine {
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Load built-in heuristic rules.
39    pub fn with_builtins() -> Self {
40        let rules = vec![
41            HeuristicRule {
42                name: "flammable_ignition".into(),
43                when_tags_all: vec!["isFlammable".into()],
44                when_tags_any: vec!["receive.fire".into(), "receive.heat".into()],
45                when_props: vec![PropertyPredicate::GreaterThan("temperature".into(), 100.0)],
46                then_emit_signals: vec!["signal.ignited".into()],
47                then_set_props: vec![],
48                then_trigger_emitters: vec!["fire_spread".into()],
49                then_raise_event: Some("ignition".into()),
50            },
51            HeuristicRule {
52                name: "destructible_fracture".into(),
53                when_tags_all: vec!["isDestructible".into()],
54                when_tags_any: vec!["receive.force".into(), "receive.explosion".into()],
55                when_props: vec![],
56                then_emit_signals: vec!["signal.fractured".into()],
57                then_set_props: vec![],
58                then_trigger_emitters: vec![],
59                then_raise_event: Some("fracture".into()),
60            },
61            HeuristicRule {
62                name: "conductive_electrify".into(),
63                when_tags_all: vec!["isConductive".into(), "receive.electricity".into()],
64                when_tags_any: vec![],
65                when_props: vec![],
66                then_emit_signals: vec!["signal.electrified".into()],
67                then_set_props: vec![],
68                then_trigger_emitters: vec!["electrical_arc".into()],
69                then_raise_event: None,
70            },
71            HeuristicRule {
72                name: "glass_shatter".into(),
73                when_tags_all: vec!["isGlass".into(), "receive.force".into()],
74                when_tags_any: vec![],
75                when_props: vec![],
76                then_emit_signals: vec!["signal.fractured".into(), "signal.destroyed".into()],
77                then_set_props: vec![],
78                then_trigger_emitters: vec!["glass_shatter".into()],
79                then_raise_event: Some("shatter".into()),
80            },
81            HeuristicRule {
82                name: "frozen_shatter".into(),
83                when_tags_all: vec!["isFrozen".into(), "receive.force".into()],
84                when_tags_any: vec![],
85                when_props: vec![],
86                then_emit_signals: vec!["signal.fractured".into()],
87                then_set_props: vec![],
88                then_trigger_emitters: vec!["ice_shatter".into()],
89                then_raise_event: None,
90            },
91            HeuristicRule {
92                name: "explosive_detonate".into(),
93                when_tags_all: vec!["isExplosive".into()],
94                when_tags_any: vec!["signal.ignited".into(), "signal.impacted".into()],
95                when_props: vec![],
96                then_emit_signals: vec!["signal.destroyed".into()],
97                then_set_props: vec![],
98                then_trigger_emitters: vec!["explosion_large".into()],
99                then_raise_event: Some("detonation".into()),
100            },
101            HeuristicRule {
102                name: "corruptible_contaminate".into(),
103                when_tags_all: vec!["isCorruptible".into(), "receive.corruption".into()],
104                when_tags_any: vec![],
105                when_props: vec![],
106                then_emit_signals: vec!["signal.contaminated".into()],
107                then_set_props: vec![],
108                then_trigger_emitters: vec!["corruption_spread".into()],
109                then_raise_event: None,
110            },
111            HeuristicRule {
112                name: "organic_harvest".into(),
113                when_tags_all: vec!["isOrganic".into(), "receive.harvest".into()],
114                when_tags_any: vec![],
115                when_props: vec![],
116                then_emit_signals: vec!["signal.harvested".into()],
117                then_set_props: vec![],
118                then_trigger_emitters: vec![],
119                then_raise_event: Some("harvest".into()),
120            },
121            HeuristicRule {
122                name: "metal_salvage".into(),
123                when_tags_all: vec!["isMetal".into(), "isDebris".into(), "receive.salvage".into()],
124                when_tags_any: vec![],
125                when_props: vec![],
126                then_emit_signals: vec!["signal.pickup_spawned".into()],
127                then_set_props: vec![],
128                then_trigger_emitters: vec![],
129                then_raise_event: Some("salvage".into()),
130            },
131            HeuristicRule {
132                name: "liquid_freeze".into(),
133                when_tags_all: vec!["isLiquid".into(), "receive.freeze".into()],
134                when_tags_any: vec![],
135                when_props: vec![],
136                then_emit_signals: vec!["signal.frozen".into()],
137                then_set_props: vec![],
138                then_trigger_emitters: vec!["freeze_effect".into()],
139                then_raise_event: None,
140            },
141        ];
142        Self { rules }
143    }
144
145    /// Evaluate all rules against given tags. Returns matching rule results.
146    pub fn evaluate_by_names(
147        &self,
148        trait_tags: &[&str],
149        signal_tags: &[&str],
150        receiver_tags: &[&str],
151        props: &[(String, f32)],
152    ) -> Vec<HeuristicResult> {
153        let all_tags: Vec<&str> = trait_tags
154            .iter()
155            .chain(signal_tags.iter())
156            .chain(receiver_tags.iter())
157            .copied()
158            .collect();
159
160        let mut results = Vec::new();
161        for rule in &self.rules {
162            let all_match = rule.when_tags_all.iter().all(|t| all_tags.contains(&t.as_str()));
163            if !all_match {
164                continue;
165            }
166            let any_match =
167                rule.when_tags_any.is_empty() || rule.when_tags_any.iter().any(|t| all_tags.contains(&t.as_str()));
168            if !any_match {
169                continue;
170            }
171            let props_match = rule.when_props.iter().all(|pred| match pred {
172                PropertyPredicate::GreaterThan(key, threshold) => props.iter().any(|(k, v)| k == key && v > threshold),
173                PropertyPredicate::LessThan(key, threshold) => props.iter().any(|(k, v)| k == key && v < threshold),
174                PropertyPredicate::EqualsBool(_, _) | PropertyPredicate::EqualsEnum(_, _) => true,
175            });
176            if !props_match {
177                continue;
178            }
179            results.push(HeuristicResult {
180                rule_name: rule.name.clone(),
181                emitted_signals: rule.then_emit_signals.clone(),
182                property_mutations: rule.then_set_props.clone(),
183                triggered_emitters: rule.then_trigger_emitters.clone(),
184                raised_event: rule.then_raise_event.clone(),
185            });
186        }
187        results
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn heuristic_engine_builtins() {
197        let engine = HeuristicEngine::with_builtins();
198        assert_eq!(engine.rules.len(), 10);
199    }
200
201    #[test]
202    fn evaluate_flammable_ignition() {
203        let engine = HeuristicEngine::with_builtins();
204        let results = engine.evaluate_by_names(
205            &["isFlammable"],
206            &[],
207            &["receive.fire"],
208            &[("temperature".into(), 200.0)],
209        );
210        assert_eq!(results.len(), 1);
211        assert_eq!(results[0].rule_name, "flammable_ignition");
212        assert!(results[0].emitted_signals.contains(&"signal.ignited".to_string()));
213    }
214
215    #[test]
216    fn evaluate_no_match() {
217        let engine = HeuristicEngine::with_builtins();
218        let results = engine.evaluate_by_names(&["isWalkable"], &[], &[], &[]);
219        assert!(results.is_empty());
220    }
221
222    #[test]
223    fn evaluate_destructible_fracture() {
224        let engine = HeuristicEngine::with_builtins();
225        let results = engine.evaluate_by_names(&["isDestructible"], &[], &["receive.force"], &[]);
226        assert_eq!(results.len(), 1);
227        assert_eq!(results[0].rule_name, "destructible_fracture");
228    }
229
230    #[test]
231    fn evaluate_glass_shatter() {
232        let engine = HeuristicEngine::with_builtins();
233        let results = engine.evaluate_by_names(&["isGlass"], &[], &["receive.force"], &[]);
234        assert_eq!(results.len(), 1);
235        assert_eq!(results[0].rule_name, "glass_shatter");
236        assert_eq!(results[0].emitted_signals.len(), 2);
237    }
238
239    #[test]
240    fn evaluate_multiple_rules_match() {
241        let engine = HeuristicEngine::with_builtins();
242        let results = engine.evaluate_by_names(&["isDestructible", "isGlass"], &[], &["receive.force"], &[]);
243        assert_eq!(results.len(), 2);
244    }
245}