1use super::properties::{PropertyAssignment, PropertyPredicate};
2use serde::{Deserialize, Serialize};
3
4#[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#[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#[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 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 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}