dreamwell-engine 1.0.0

Dreamwell pure-logic engine library — transforms, hierarchy, canon pipeline, spatial math, hashing, tile rules, validation, waymark schema, material/lighting descriptors. No SpacetimeDB dependency.
Documentation
use super::properties::{PropertyAssignment, PropertyPredicate};
use serde::{Deserialize, Serialize};

/// Heuristic rule — infers interactions from tags and properties.
#[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>,
}

/// Result of evaluating heuristic rules against properties.
#[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>,
}

/// Heuristic engine — evaluates rules against semantic properties.
#[derive(Debug, Clone, Default)]
pub struct HeuristicEngine {
    pub rules: Vec<HeuristicRule>,
}

impl HeuristicEngine {
    pub fn new() -> Self {
        Self::default()
    }

    /// Load built-in heuristic rules.
    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 }
    }

    /// Evaluate all rules against given tags. Returns matching rule results.
    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);
    }
}