Skip to main content

terminals_core/substrate/
emergence.rs

1//! Emergence Engine — Interaction net reduction for op pair composition.
2//!
3//! When two different operations are active in the same zone, they compose
4//! into an emergent result. Rarity is derived from category distance.
5
6use serde::{Deserialize, Serialize};
7
8/// Operation categories (must match TypeScript OpCategory)
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[repr(u8)]
11pub enum OpCategory {
12    Structural = 0,
13    Environmental = 1,
14    Biological = 2,
15    Informational = 3,
16    Temporal = 4,
17    Acoustic = 5,
18}
19
20/// Emergence rarity levels
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
22#[repr(u8)]
23pub enum EmergenceRarity {
24    Common = 0,
25    Uncommon = 1,
26    Rare = 2,
27    Legendary = 3,
28}
29
30/// Result of composing two operations
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct EmergenceResult {
33    pub op_a: u32,
34    pub op_b: u32,
35    pub rarity: EmergenceRarity,
36    /// Index into a behavior table (0-15)
37    pub behavior_index: u8,
38    /// Visual override: [hue_shift, saturation_boost, glow_intensity]
39    pub visual_override: [f32; 3],
40}
41
42/// Category assignment for each of the 55 operations.
43/// Distribution: structural=12, environmental=10, biological=9, informational=8, temporal=8, acoustic=8
44const OP_CATEGORIES: [OpCategory; 55] = [
45    // 0-11: structural
46    OpCategory::Structural, OpCategory::Structural, OpCategory::Structural,
47    OpCategory::Structural, OpCategory::Structural, OpCategory::Structural,
48    OpCategory::Structural, OpCategory::Structural, OpCategory::Structural,
49    OpCategory::Structural, OpCategory::Structural, OpCategory::Structural,
50    // 12-21: environmental
51    OpCategory::Environmental, OpCategory::Environmental, OpCategory::Environmental,
52    OpCategory::Environmental, OpCategory::Environmental, OpCategory::Environmental,
53    OpCategory::Environmental, OpCategory::Environmental, OpCategory::Environmental,
54    OpCategory::Environmental,
55    // 22-30: biological
56    OpCategory::Biological, OpCategory::Biological, OpCategory::Biological,
57    OpCategory::Biological, OpCategory::Biological, OpCategory::Biological,
58    OpCategory::Biological, OpCategory::Biological, OpCategory::Biological,
59    // 31-38: informational
60    OpCategory::Informational, OpCategory::Informational, OpCategory::Informational,
61    OpCategory::Informational, OpCategory::Informational, OpCategory::Informational,
62    OpCategory::Informational, OpCategory::Informational,
63    // 39-46: temporal
64    OpCategory::Temporal, OpCategory::Temporal, OpCategory::Temporal,
65    OpCategory::Temporal, OpCategory::Temporal, OpCategory::Temporal,
66    OpCategory::Temporal, OpCategory::Temporal,
67    // 47-54: acoustic
68    OpCategory::Acoustic, OpCategory::Acoustic, OpCategory::Acoustic,
69    OpCategory::Acoustic, OpCategory::Acoustic, OpCategory::Acoustic,
70    OpCategory::Acoustic, OpCategory::Acoustic,
71];
72
73/// Get the category for an operation ID (0-54).
74pub fn op_category(op_id: u32) -> OpCategory {
75    if (op_id as usize) < OP_CATEGORIES.len() {
76        OP_CATEGORIES[op_id as usize]
77    } else {
78        OpCategory::Structural // Fallback
79    }
80}
81
82/// Compute the "distance" between two categories.
83/// Same category = 0, adjacent = 1, distant = 2-3, temporal+acoustic = max (4).
84fn category_distance(a: OpCategory, b: OpCategory) -> u32 {
85    // Special case: temporal(4) + acoustic(5) = maximum distance for legendary
86    if (a == OpCategory::Temporal && b == OpCategory::Acoustic)
87        || (a == OpCategory::Acoustic && b == OpCategory::Temporal)
88    {
89        return 4;
90    }
91    let ai = a as i32;
92    let bi = b as i32;
93    (ai - bi).unsigned_abs()
94}
95
96/// Determine emergence rarity from category distance.
97fn distance_to_rarity(distance: u32) -> EmergenceRarity {
98    match distance {
99        0 => EmergenceRarity::Common,
100        1 => EmergenceRarity::Uncommon,
101        2..=3 => EmergenceRarity::Rare,
102        _ => EmergenceRarity::Legendary,
103    }
104}
105
106/// Compose two operations into an emergent result.
107/// Deterministic — same inputs always produce the same output.
108pub fn compose_ops(op_a: u32, op_b: u32) -> EmergenceResult {
109    let (lo, hi) = if op_a <= op_b { (op_a, op_b) } else { (op_b, op_a) };
110
111    let cat_a = op_category(lo);
112    let cat_b = op_category(hi);
113    let distance = category_distance(cat_a, cat_b);
114    let rarity = distance_to_rarity(distance);
115
116    // Deterministic behavior index from pair hash
117    let pair_hash = lo.wrapping_mul(7919) ^ hi.wrapping_mul(104729);
118    let behavior_index = (pair_hash % 16) as u8;
119
120    // Visual override based on rarity
121    let visual_override = match rarity {
122        EmergenceRarity::Common => [0.0, 0.1, 0.2],
123        EmergenceRarity::Uncommon => [0.15, 0.2, 0.4],
124        EmergenceRarity::Rare => [0.3, 0.4, 0.7],
125        EmergenceRarity::Legendary => [0.5, 0.6, 1.0],
126    };
127
128    EmergenceResult {
129        op_a: lo,
130        op_b: hi,
131        rarity,
132        behavior_index,
133        visual_override,
134    }
135}
136
137/// Compose all active ops in a zone (given as a bitfield) and return
138/// the highest-rarity emergence found.
139/// Returns None if fewer than 2 ops are active.
140pub fn best_emergence(active_ops: u64) -> Option<EmergenceResult> {
141    let ops: Vec<u32> = (0u32..55).filter(|&i| active_ops & (1u64 << i) != 0).collect();
142    if ops.len() < 2 {
143        return None;
144    }
145
146    let mut best: Option<EmergenceResult> = None;
147    for i in 0..ops.len() {
148        for j in (i + 1)..ops.len() {
149            let result = compose_ops(ops[i], ops[j]);
150            if best.as_ref().is_none_or(|b| result.rarity > b.rarity) {
151                best = Some(result);
152            }
153        }
154    }
155    best
156}
157
158/// Serialize an emergence result to JSON.
159pub fn emergence_to_json(result: &EmergenceResult) -> String {
160    serde_json::to_string(result).unwrap_or_default()
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_same_category_common() {
169        // Ops 0 and 1 are both structural
170        let result = compose_ops(0, 1);
171        assert_eq!(result.rarity, EmergenceRarity::Common);
172    }
173
174    #[test]
175    fn test_adjacent_category_uncommon() {
176        // Op 11 (structural) + op 12 (environmental)
177        let result = compose_ops(11, 12);
178        assert_eq!(result.rarity, EmergenceRarity::Uncommon);
179    }
180
181    #[test]
182    fn test_distant_category_rare() {
183        // Op 0 (structural) + op 31 (informational) — distance 3
184        let result = compose_ops(0, 31);
185        assert_eq!(result.rarity, EmergenceRarity::Rare);
186    }
187
188    #[test]
189    fn test_temporal_acoustic_legendary() {
190        // Op 39 (temporal) + op 47 (acoustic)
191        let result = compose_ops(39, 47);
192        assert_eq!(result.rarity, EmergenceRarity::Legendary);
193    }
194
195    #[test]
196    fn test_compose_deterministic() {
197        let r1 = compose_ops(5, 20);
198        let r2 = compose_ops(5, 20);
199        assert_eq!(r1.rarity, r2.rarity);
200        assert_eq!(r1.behavior_index, r2.behavior_index);
201    }
202
203    #[test]
204    fn test_compose_symmetric() {
205        let r1 = compose_ops(5, 20);
206        let r2 = compose_ops(20, 5);
207        assert_eq!(r1.rarity, r2.rarity);
208        assert_eq!(r1.op_a, r2.op_a);
209        assert_eq!(r1.op_b, r2.op_b);
210    }
211
212    #[test]
213    fn test_best_emergence_no_ops() {
214        assert!(best_emergence(0).is_none());
215    }
216
217    #[test]
218    fn test_best_emergence_single_op() {
219        assert!(best_emergence(1 << 5).is_none());
220    }
221
222    #[test]
223    fn test_best_emergence_multiple() {
224        // Ops 0 (structural), 39 (temporal), 47 (acoustic)
225        let bits = (1u64 << 0) | (1u64 << 39) | (1u64 << 47);
226        let best = best_emergence(bits).unwrap();
227        assert_eq!(best.rarity, EmergenceRarity::Legendary); // temporal + acoustic
228    }
229
230    #[test]
231    fn test_emergence_to_json() {
232        let result = compose_ops(0, 47);
233        let json = emergence_to_json(&result);
234        assert!(json.contains("rarity"));
235        assert!(json.contains("visual_override"));
236    }
237
238    #[test]
239    fn test_op_category_bounds() {
240        assert_eq!(op_category(0), OpCategory::Structural);
241        assert_eq!(op_category(54), OpCategory::Acoustic);
242        assert_eq!(op_category(100), OpCategory::Structural); // Fallback
243    }
244}