Skip to main content

terminals_core/substrate/
world.rs

1//! WorldGrid — Zone physics for the orbital shell world.
2//!
3//! 41 zones across 4 shells (0, 6, 12, 22) mapped to substrate atoms 23-63.
4//! Each zone has topology, active operations (bitfield), resonance, and stability.
5
6use serde::{Deserialize, Serialize};
7
8/// Total zones in the orbital world
9pub const ZONE_COUNT: usize = 41;
10
11/// Atom offset — zones map to substrate atoms 23-63
12pub const ZONE_ATOM_OFFSET: usize = 23;
13
14/// Shell sizes: [1, 6, 12, 22]
15pub const SHELL_SIZES: [usize; 4] = [1, 6, 12, 22];
16
17/// Shell base resonance values (inner = stable, outer = chaotic)
18pub const SHELL_BASE_RESONANCE: [f32; 4] = [1.0, 0.8, 0.5, 0.2];
19
20/// Shell stability scaling factors
21pub const SHELL_STABILITY_SCALE: [f32; 4] = [1.0, 0.9, 0.6, 0.3];
22
23/// Shell radii in world units
24pub const SHELL_RADII: [f32; 4] = [0.0, 8.0, 18.0, 30.0];
25
26/// A single zone in the orbital world.
27#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
28pub struct WorldAtom {
29    pub zone_id: u8,
30    pub shell: u8,
31    /// Which of the 6 topology types (0-5)
32    pub topology: u8,
33    /// Bitfield for 55 operations (fits in u64)
34    pub active_ops: u64,
35    /// Audio coupling strength [0, 1]
36    pub resonance: f32,
37    /// Derived from substrate R
38    pub stability: f32,
39    /// Hash of the player who owns this zone (0 = unowned)
40    pub owner_hash: u32,
41    /// Angle on shell ring (radians)
42    pub angle: f32,
43}
44
45impl Default for WorldAtom {
46    fn default() -> Self {
47        Self {
48            zone_id: 0,
49            shell: 0,
50            topology: 0,
51            active_ops: 0,
52            resonance: 0.0,
53            stability: 0.0,
54            owner_hash: 0,
55            angle: 0.0,
56        }
57    }
58}
59
60/// The complete world grid — 41 zones.
61pub struct WorldGrid {
62    pub zones: [WorldAtom; ZONE_COUNT],
63}
64
65impl WorldGrid {
66    /// Create a new world grid with zones placed on orbital shells.
67    pub fn new() -> Self {
68        let mut zones = [WorldAtom::default(); ZONE_COUNT];
69        let mut zone_id: u8 = 0;
70
71        for (shell, &count) in SHELL_SIZES.iter().enumerate() {
72            let shell_u8 = shell as u8;
73            for i in 0..count {
74                let angle = if count > 1 {
75                    2.0 * std::f32::consts::PI * (i as f32) / (count as f32)
76                } else {
77                    0.0
78                };
79
80                zones[zone_id as usize] = WorldAtom {
81                    zone_id,
82                    shell: shell_u8,
83                    topology: (i % 6) as u8,
84                    active_ops: 0,
85                    resonance: SHELL_BASE_RESONANCE[shell],
86                    stability: SHELL_STABILITY_SCALE[shell],
87                    owner_hash: 0,
88                    angle,
89                };
90
91                zone_id += 1;
92            }
93        }
94
95        Self { zones }
96    }
97
98    /// Apply an operation to a zone. Returns true if the op was new.
99    pub fn apply_op(&mut self, zone_id: u32, op_id: u32) -> bool {
100        if zone_id as usize >= ZONE_COUNT || op_id >= 55 {
101            return false;
102        }
103        let zone = &mut self.zones[zone_id as usize];
104        let mask = 1u64 << op_id;
105        let was_new = zone.active_ops & mask == 0;
106        zone.active_ops |= mask;
107        was_new
108    }
109
110    /// Check if an op is active in a zone.
111    pub fn has_op(&self, zone_id: u32, op_id: u32) -> bool {
112        if zone_id as usize >= ZONE_COUNT || op_id >= 55 {
113            return false;
114        }
115        self.zones[zone_id as usize].active_ops & (1u64 << op_id) != 0
116    }
117
118    /// Count active ops in a zone.
119    pub fn op_count(&self, zone_id: u32) -> u32 {
120        if zone_id as usize >= ZONE_COUNT {
121            return 0;
122        }
123        self.zones[zone_id as usize].active_ops.count_ones()
124    }
125
126    /// Get all active op IDs for a zone as a vec.
127    pub fn active_ops(&self, zone_id: u32) -> Vec<u32> {
128        if zone_id as usize >= ZONE_COUNT {
129            return vec![];
130        }
131        let bits = self.zones[zone_id as usize].active_ops;
132        (0..55).filter(|&i| bits & (1u64 << i) != 0).collect()
133    }
134
135    /// Update zone stability based on substrate coherence R.
136    pub fn update_stability(&mut self, substrate_r: f32) {
137        for zone in &mut self.zones {
138            let shell = zone.shell as usize;
139            if shell < 4 {
140                zone.stability = substrate_r * SHELL_STABILITY_SCALE[shell];
141                zone.resonance = SHELL_BASE_RESONANCE[shell] * (0.5 + substrate_r * 0.5);
142            }
143        }
144    }
145
146    /// Apply a bass shockwave — ripples outward, attenuating per shell.
147    pub fn apply_shockwave(&mut self, bass_peak: f32) {
148        if bass_peak < 0.3 {
149            return;
150        }
151        for zone in &mut self.zones {
152            let shell = zone.shell as usize;
153            if shell < 4 {
154                let intensity = bass_peak * (1.0 - shell as f32 * 0.2);
155                zone.resonance = (zone.resonance + intensity * 0.3).min(1.0);
156            }
157        }
158    }
159
160    /// Tick all zones — decay resonance toward base values.
161    pub fn tick(&mut self, dt: f32) {
162        let decay = (-dt * 2.0).exp(); // Exponential decay toward base
163        for zone in &mut self.zones {
164            let shell = zone.shell as usize;
165            if shell < 4 {
166                let base = SHELL_BASE_RESONANCE[shell];
167                zone.resonance = base + (zone.resonance - base) * decay;
168            }
169        }
170    }
171
172    /// Get the zone witness as a flat f32 array: [R, entropy, stability, topology] per zone.
173    /// Total length = ZONE_COUNT * 4 = 164.
174    pub fn witness_flat(&self) -> Vec<f32> {
175        let mut out = Vec::with_capacity(ZONE_COUNT * 4);
176        for zone in &self.zones {
177            out.push(zone.resonance);
178            out.push(zone.stability);
179            out.push(zone.active_ops.count_ones() as f32 / 55.0); // Normalized op density
180            out.push(zone.topology as f32);
181        }
182        out
183    }
184
185    /// Get the shell that a zone belongs to.
186    pub fn zone_shell(zone_id: u32) -> u8 {
187        if zone_id == 0 { return 0; }
188        if zone_id <= 6 { return 1; }
189        if zone_id <= 18 { return 2; }
190        if zone_id <= 40 { return 3; }
191        3 // Clamp
192    }
193}
194
195impl Default for WorldGrid {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_world_creation() {
207        let world = WorldGrid::new();
208        assert_eq!(world.zones.len(), ZONE_COUNT);
209        assert_eq!(world.zones[0].shell, 0); // Shell 0
210        assert_eq!(world.zones[1].shell, 1); // Shell 1
211        assert_eq!(world.zones[7].shell, 2); // Shell 2
212        assert_eq!(world.zones[19].shell, 3); // Shell 3
213    }
214
215    #[test]
216    fn test_apply_op() {
217        let mut world = WorldGrid::new();
218        assert!(world.apply_op(0, 5)); // New op
219        assert!(!world.apply_op(0, 5)); // Already active
220        assert!(world.has_op(0, 5));
221        assert!(!world.has_op(0, 6));
222        assert_eq!(world.op_count(0), 1);
223    }
224
225    #[test]
226    fn test_apply_op_bounds() {
227        let mut world = WorldGrid::new();
228        assert!(!world.apply_op(50, 0)); // Zone out of bounds
229        assert!(!world.apply_op(0, 55)); // Op out of bounds
230        assert!(!world.apply_op(0, 60)); // Op out of bounds
231    }
232
233    #[test]
234    fn test_active_ops() {
235        let mut world = WorldGrid::new();
236        world.apply_op(5, 0);
237        world.apply_op(5, 10);
238        world.apply_op(5, 54);
239        let ops = world.active_ops(5);
240        assert_eq!(ops, vec![0, 10, 54]);
241    }
242
243    #[test]
244    fn test_update_stability() {
245        let mut world = WorldGrid::new();
246        world.update_stability(0.5);
247        assert!((world.zones[0].stability - 0.5).abs() < 1e-6); // Shell 0: 0.5 * 1.0
248        assert!((world.zones[1].stability - 0.45).abs() < 1e-6); // Shell 1: 0.5 * 0.9
249    }
250
251    #[test]
252    fn test_shockwave() {
253        let mut world = WorldGrid::new();
254        // Shell 0 base resonance is 1.0 (already at max), use shell 3 zone (base 0.2)
255        let zone_idx = 19; // First zone in shell 3
256        let pre_res = world.zones[zone_idx].resonance;
257        world.apply_shockwave(0.8);
258        assert!(world.zones[zone_idx].resonance > pre_res,
259            "resonance {} should exceed pre {}", world.zones[zone_idx].resonance, pre_res);
260    }
261
262    #[test]
263    fn test_shockwave_below_threshold() {
264        let mut world = WorldGrid::new();
265        let pre_res = world.zones[0].resonance;
266        world.apply_shockwave(0.2); // Below 0.3 threshold
267        assert_eq!(world.zones[0].resonance, pre_res);
268    }
269
270    #[test]
271    fn test_tick_decay() {
272        let mut world = WorldGrid::new();
273        world.zones[1].resonance = 1.0; // Spike above base (0.8)
274        world.tick(1.0); // 1 second
275        assert!(world.zones[1].resonance < 1.0); // Decayed toward 0.8
276        assert!(world.zones[1].resonance > 0.8); // But not below base
277    }
278
279    #[test]
280    fn test_witness_flat() {
281        let world = WorldGrid::new();
282        let flat = world.witness_flat();
283        assert_eq!(flat.len(), ZONE_COUNT * 4);
284    }
285
286    #[test]
287    fn test_zone_shell() {
288        assert_eq!(WorldGrid::zone_shell(0), 0);
289        assert_eq!(WorldGrid::zone_shell(1), 1);
290        assert_eq!(WorldGrid::zone_shell(6), 1);
291        assert_eq!(WorldGrid::zone_shell(7), 2);
292        assert_eq!(WorldGrid::zone_shell(18), 2);
293        assert_eq!(WorldGrid::zone_shell(19), 3);
294        assert_eq!(WorldGrid::zone_shell(40), 3);
295    }
296}