use super::properties::{PropertyAssignment, PropertyPredicate};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeuristicRule {
pub name: String,
pub when_tags_all: Vec<String>,
pub when_tags_any: Vec<String>,
pub when_props: Vec<PropertyPredicate>,
pub then_emit_signals: Vec<String>,
pub then_set_props: Vec<PropertyAssignment>,
pub then_trigger_emitters: Vec<String>,
pub then_raise_event: Option<String>,
}
#[derive(Debug, Clone)]
pub struct HeuristicResult {
pub rule_name: String,
pub emitted_signals: Vec<String>,
pub property_mutations: Vec<PropertyAssignment>,
pub triggered_emitters: Vec<String>,
pub raised_event: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct HeuristicEngine {
pub rules: Vec<HeuristicRule>,
}
impl HeuristicEngine {
pub fn new() -> Self {
Self::default()
}
pub fn with_builtins() -> Self {
let rules = vec![
HeuristicRule {
name: "flammable_ignition".into(),
when_tags_all: vec!["isFlammable".into()],
when_tags_any: vec!["receive.fire".into(), "receive.heat".into()],
when_props: vec![PropertyPredicate::GreaterThan("temperature".into(), 100.0)],
then_emit_signals: vec!["signal.ignited".into()],
then_set_props: vec![],
then_trigger_emitters: vec!["fire_spread".into()],
then_raise_event: Some("ignition".into()),
},
HeuristicRule {
name: "destructible_fracture".into(),
when_tags_all: vec!["isDestructible".into()],
when_tags_any: vec!["receive.force".into(), "receive.explosion".into()],
when_props: vec![],
then_emit_signals: vec!["signal.fractured".into()],
then_set_props: vec![],
then_trigger_emitters: vec![],
then_raise_event: Some("fracture".into()),
},
HeuristicRule {
name: "conductive_electrify".into(),
when_tags_all: vec!["isConductive".into(), "receive.electricity".into()],
when_tags_any: vec![],
when_props: vec![],
then_emit_signals: vec!["signal.electrified".into()],
then_set_props: vec![],
then_trigger_emitters: vec!["electrical_arc".into()],
then_raise_event: None,
},
HeuristicRule {
name: "glass_shatter".into(),
when_tags_all: vec!["isGlass".into(), "receive.force".into()],
when_tags_any: vec![],
when_props: vec![],
then_emit_signals: vec!["signal.fractured".into(), "signal.destroyed".into()],
then_set_props: vec![],
then_trigger_emitters: vec!["glass_shatter".into()],
then_raise_event: Some("shatter".into()),
},
HeuristicRule {
name: "frozen_shatter".into(),
when_tags_all: vec!["isFrozen".into(), "receive.force".into()],
when_tags_any: vec![],
when_props: vec![],
then_emit_signals: vec!["signal.fractured".into()],
then_set_props: vec![],
then_trigger_emitters: vec!["ice_shatter".into()],
then_raise_event: None,
},
HeuristicRule {
name: "explosive_detonate".into(),
when_tags_all: vec!["isExplosive".into()],
when_tags_any: vec!["signal.ignited".into(), "signal.impacted".into()],
when_props: vec![],
then_emit_signals: vec!["signal.destroyed".into()],
then_set_props: vec![],
then_trigger_emitters: vec!["explosion_large".into()],
then_raise_event: Some("detonation".into()),
},
HeuristicRule {
name: "corruptible_contaminate".into(),
when_tags_all: vec!["isCorruptible".into(), "receive.corruption".into()],
when_tags_any: vec![],
when_props: vec![],
then_emit_signals: vec!["signal.contaminated".into()],
then_set_props: vec![],
then_trigger_emitters: vec!["corruption_spread".into()],
then_raise_event: None,
},
HeuristicRule {
name: "organic_harvest".into(),
when_tags_all: vec!["isOrganic".into(), "receive.harvest".into()],
when_tags_any: vec![],
when_props: vec![],
then_emit_signals: vec!["signal.harvested".into()],
then_set_props: vec![],
then_trigger_emitters: vec![],
then_raise_event: Some("harvest".into()),
},
HeuristicRule {
name: "metal_salvage".into(),
when_tags_all: vec!["isMetal".into(), "isDebris".into(), "receive.salvage".into()],
when_tags_any: vec![],
when_props: vec![],
then_emit_signals: vec!["signal.pickup_spawned".into()],
then_set_props: vec![],
then_trigger_emitters: vec![],
then_raise_event: Some("salvage".into()),
},
HeuristicRule {
name: "liquid_freeze".into(),
when_tags_all: vec!["isLiquid".into(), "receive.freeze".into()],
when_tags_any: vec![],
when_props: vec![],
then_emit_signals: vec!["signal.frozen".into()],
then_set_props: vec![],
then_trigger_emitters: vec!["freeze_effect".into()],
then_raise_event: None,
},
];
Self { rules }
}
pub fn evaluate_by_names(
&self,
trait_tags: &[&str],
signal_tags: &[&str],
receiver_tags: &[&str],
props: &[(String, f32)],
) -> Vec<HeuristicResult> {
let all_tags: Vec<&str> = trait_tags
.iter()
.chain(signal_tags.iter())
.chain(receiver_tags.iter())
.copied()
.collect();
let mut results = Vec::new();
for rule in &self.rules {
let all_match = rule.when_tags_all.iter().all(|t| all_tags.contains(&t.as_str()));
if !all_match {
continue;
}
let any_match =
rule.when_tags_any.is_empty() || rule.when_tags_any.iter().any(|t| all_tags.contains(&t.as_str()));
if !any_match {
continue;
}
let props_match = rule.when_props.iter().all(|pred| match pred {
PropertyPredicate::GreaterThan(key, threshold) => props.iter().any(|(k, v)| k == key && v > threshold),
PropertyPredicate::LessThan(key, threshold) => props.iter().any(|(k, v)| k == key && v < threshold),
PropertyPredicate::EqualsBool(_, _) | PropertyPredicate::EqualsEnum(_, _) => true,
});
if !props_match {
continue;
}
results.push(HeuristicResult {
rule_name: rule.name.clone(),
emitted_signals: rule.then_emit_signals.clone(),
property_mutations: rule.then_set_props.clone(),
triggered_emitters: rule.then_trigger_emitters.clone(),
raised_event: rule.then_raise_event.clone(),
});
}
results
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn heuristic_engine_builtins() {
let engine = HeuristicEngine::with_builtins();
assert_eq!(engine.rules.len(), 10);
}
#[test]
fn evaluate_flammable_ignition() {
let engine = HeuristicEngine::with_builtins();
let results = engine.evaluate_by_names(
&["isFlammable"],
&[],
&["receive.fire"],
&[("temperature".into(), 200.0)],
);
assert_eq!(results.len(), 1);
assert_eq!(results[0].rule_name, "flammable_ignition");
assert!(results[0].emitted_signals.contains(&"signal.ignited".to_string()));
}
#[test]
fn evaluate_no_match() {
let engine = HeuristicEngine::with_builtins();
let results = engine.evaluate_by_names(&["isWalkable"], &[], &[], &[]);
assert!(results.is_empty());
}
#[test]
fn evaluate_destructible_fracture() {
let engine = HeuristicEngine::with_builtins();
let results = engine.evaluate_by_names(&["isDestructible"], &[], &["receive.force"], &[]);
assert_eq!(results.len(), 1);
assert_eq!(results[0].rule_name, "destructible_fracture");
}
#[test]
fn evaluate_glass_shatter() {
let engine = HeuristicEngine::with_builtins();
let results = engine.evaluate_by_names(&["isGlass"], &[], &["receive.force"], &[]);
assert_eq!(results.len(), 1);
assert_eq!(results[0].rule_name, "glass_shatter");
assert_eq!(results[0].emitted_signals.len(), 2);
}
#[test]
fn evaluate_multiple_rules_match() {
let engine = HeuristicEngine::with_builtins();
let results = engine.evaluate_by_names(&["isDestructible", "isGlass"], &[], &["receive.force"], &[]);
assert_eq!(results.len(), 2);
}
}