terminals-core 0.1.0

Core runtime primitives for Terminals OS: phase dynamics, AXON wire protocol, substrate engine, and sematonic types
Documentation
//! Emergence Engine — Interaction net reduction for op pair composition.
//!
//! When two different operations are active in the same zone, they compose
//! into an emergent result. Rarity is derived from category distance.

use serde::{Deserialize, Serialize};

/// Operation categories (must match TypeScript OpCategory)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum OpCategory {
    Structural = 0,
    Environmental = 1,
    Biological = 2,
    Informational = 3,
    Temporal = 4,
    Acoustic = 5,
}

/// Emergence rarity levels
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[repr(u8)]
pub enum EmergenceRarity {
    Common = 0,
    Uncommon = 1,
    Rare = 2,
    Legendary = 3,
}

/// Result of composing two operations
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmergenceResult {
    pub op_a: u32,
    pub op_b: u32,
    pub rarity: EmergenceRarity,
    /// Index into a behavior table (0-15)
    pub behavior_index: u8,
    /// Visual override: [hue_shift, saturation_boost, glow_intensity]
    pub visual_override: [f32; 3],
}

/// Category assignment for each of the 55 operations.
/// Distribution: structural=12, environmental=10, biological=9, informational=8, temporal=8, acoustic=8
const OP_CATEGORIES: [OpCategory; 55] = [
    // 0-11: structural
    OpCategory::Structural, OpCategory::Structural, OpCategory::Structural,
    OpCategory::Structural, OpCategory::Structural, OpCategory::Structural,
    OpCategory::Structural, OpCategory::Structural, OpCategory::Structural,
    OpCategory::Structural, OpCategory::Structural, OpCategory::Structural,
    // 12-21: environmental
    OpCategory::Environmental, OpCategory::Environmental, OpCategory::Environmental,
    OpCategory::Environmental, OpCategory::Environmental, OpCategory::Environmental,
    OpCategory::Environmental, OpCategory::Environmental, OpCategory::Environmental,
    OpCategory::Environmental,
    // 22-30: biological
    OpCategory::Biological, OpCategory::Biological, OpCategory::Biological,
    OpCategory::Biological, OpCategory::Biological, OpCategory::Biological,
    OpCategory::Biological, OpCategory::Biological, OpCategory::Biological,
    // 31-38: informational
    OpCategory::Informational, OpCategory::Informational, OpCategory::Informational,
    OpCategory::Informational, OpCategory::Informational, OpCategory::Informational,
    OpCategory::Informational, OpCategory::Informational,
    // 39-46: temporal
    OpCategory::Temporal, OpCategory::Temporal, OpCategory::Temporal,
    OpCategory::Temporal, OpCategory::Temporal, OpCategory::Temporal,
    OpCategory::Temporal, OpCategory::Temporal,
    // 47-54: acoustic
    OpCategory::Acoustic, OpCategory::Acoustic, OpCategory::Acoustic,
    OpCategory::Acoustic, OpCategory::Acoustic, OpCategory::Acoustic,
    OpCategory::Acoustic, OpCategory::Acoustic,
];

/// Get the category for an operation ID (0-54).
pub fn op_category(op_id: u32) -> OpCategory {
    if (op_id as usize) < OP_CATEGORIES.len() {
        OP_CATEGORIES[op_id as usize]
    } else {
        OpCategory::Structural // Fallback
    }
}

/// Compute the "distance" between two categories.
/// Same category = 0, adjacent = 1, distant = 2-3, temporal+acoustic = max (4).
fn category_distance(a: OpCategory, b: OpCategory) -> u32 {
    // Special case: temporal(4) + acoustic(5) = maximum distance for legendary
    if (a == OpCategory::Temporal && b == OpCategory::Acoustic)
        || (a == OpCategory::Acoustic && b == OpCategory::Temporal)
    {
        return 4;
    }
    let ai = a as i32;
    let bi = b as i32;
    (ai - bi).unsigned_abs()
}

/// Determine emergence rarity from category distance.
fn distance_to_rarity(distance: u32) -> EmergenceRarity {
    match distance {
        0 => EmergenceRarity::Common,
        1 => EmergenceRarity::Uncommon,
        2..=3 => EmergenceRarity::Rare,
        _ => EmergenceRarity::Legendary,
    }
}

/// Compose two operations into an emergent result.
/// Deterministic — same inputs always produce the same output.
pub fn compose_ops(op_a: u32, op_b: u32) -> EmergenceResult {
    let (lo, hi) = if op_a <= op_b { (op_a, op_b) } else { (op_b, op_a) };

    let cat_a = op_category(lo);
    let cat_b = op_category(hi);
    let distance = category_distance(cat_a, cat_b);
    let rarity = distance_to_rarity(distance);

    // Deterministic behavior index from pair hash
    let pair_hash = lo.wrapping_mul(7919) ^ hi.wrapping_mul(104729);
    let behavior_index = (pair_hash % 16) as u8;

    // Visual override based on rarity
    let visual_override = match rarity {
        EmergenceRarity::Common => [0.0, 0.1, 0.2],
        EmergenceRarity::Uncommon => [0.15, 0.2, 0.4],
        EmergenceRarity::Rare => [0.3, 0.4, 0.7],
        EmergenceRarity::Legendary => [0.5, 0.6, 1.0],
    };

    EmergenceResult {
        op_a: lo,
        op_b: hi,
        rarity,
        behavior_index,
        visual_override,
    }
}

/// Compose all active ops in a zone (given as a bitfield) and return
/// the highest-rarity emergence found.
/// Returns None if fewer than 2 ops are active.
pub fn best_emergence(active_ops: u64) -> Option<EmergenceResult> {
    let ops: Vec<u32> = (0u32..55).filter(|&i| active_ops & (1u64 << i) != 0).collect();
    if ops.len() < 2 {
        return None;
    }

    let mut best: Option<EmergenceResult> = None;
    for i in 0..ops.len() {
        for j in (i + 1)..ops.len() {
            let result = compose_ops(ops[i], ops[j]);
            if best.as_ref().is_none_or(|b| result.rarity > b.rarity) {
                best = Some(result);
            }
        }
    }
    best
}

/// Serialize an emergence result to JSON.
pub fn emergence_to_json(result: &EmergenceResult) -> String {
    serde_json::to_string(result).unwrap_or_default()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_same_category_common() {
        // Ops 0 and 1 are both structural
        let result = compose_ops(0, 1);
        assert_eq!(result.rarity, EmergenceRarity::Common);
    }

    #[test]
    fn test_adjacent_category_uncommon() {
        // Op 11 (structural) + op 12 (environmental)
        let result = compose_ops(11, 12);
        assert_eq!(result.rarity, EmergenceRarity::Uncommon);
    }

    #[test]
    fn test_distant_category_rare() {
        // Op 0 (structural) + op 31 (informational) — distance 3
        let result = compose_ops(0, 31);
        assert_eq!(result.rarity, EmergenceRarity::Rare);
    }

    #[test]
    fn test_temporal_acoustic_legendary() {
        // Op 39 (temporal) + op 47 (acoustic)
        let result = compose_ops(39, 47);
        assert_eq!(result.rarity, EmergenceRarity::Legendary);
    }

    #[test]
    fn test_compose_deterministic() {
        let r1 = compose_ops(5, 20);
        let r2 = compose_ops(5, 20);
        assert_eq!(r1.rarity, r2.rarity);
        assert_eq!(r1.behavior_index, r2.behavior_index);
    }

    #[test]
    fn test_compose_symmetric() {
        let r1 = compose_ops(5, 20);
        let r2 = compose_ops(20, 5);
        assert_eq!(r1.rarity, r2.rarity);
        assert_eq!(r1.op_a, r2.op_a);
        assert_eq!(r1.op_b, r2.op_b);
    }

    #[test]
    fn test_best_emergence_no_ops() {
        assert!(best_emergence(0).is_none());
    }

    #[test]
    fn test_best_emergence_single_op() {
        assert!(best_emergence(1 << 5).is_none());
    }

    #[test]
    fn test_best_emergence_multiple() {
        // Ops 0 (structural), 39 (temporal), 47 (acoustic)
        let bits = (1u64 << 0) | (1u64 << 39) | (1u64 << 47);
        let best = best_emergence(bits).unwrap();
        assert_eq!(best.rarity, EmergenceRarity::Legendary); // temporal + acoustic
    }

    #[test]
    fn test_emergence_to_json() {
        let result = compose_ops(0, 47);
        let json = emergence_to_json(&result);
        assert!(json.contains("rarity"));
        assert!(json.contains("visual_override"));
    }

    #[test]
    fn test_op_category_bounds() {
        assert_eq!(op_category(0), OpCategory::Structural);
        assert_eq!(op_category(54), OpCategory::Acoustic);
        assert_eq!(op_category(100), OpCategory::Structural); // Fallback
    }
}