terminals-core 0.1.0

Core runtime primitives for Terminals OS: phase dynamics, AXON wire protocol, substrate engine, and sematonic types
Documentation
//! WorldGrid — Zone physics for the orbital shell world.
//!
//! 41 zones across 4 shells (0, 6, 12, 22) mapped to substrate atoms 23-63.
//! Each zone has topology, active operations (bitfield), resonance, and stability.

use serde::{Deserialize, Serialize};

/// Total zones in the orbital world
pub const ZONE_COUNT: usize = 41;

/// Atom offset — zones map to substrate atoms 23-63
pub const ZONE_ATOM_OFFSET: usize = 23;

/// Shell sizes: [1, 6, 12, 22]
pub const SHELL_SIZES: [usize; 4] = [1, 6, 12, 22];

/// Shell base resonance values (inner = stable, outer = chaotic)
pub const SHELL_BASE_RESONANCE: [f32; 4] = [1.0, 0.8, 0.5, 0.2];

/// Shell stability scaling factors
pub const SHELL_STABILITY_SCALE: [f32; 4] = [1.0, 0.9, 0.6, 0.3];

/// Shell radii in world units
pub const SHELL_RADII: [f32; 4] = [0.0, 8.0, 18.0, 30.0];

/// A single zone in the orbital world.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct WorldAtom {
    pub zone_id: u8,
    pub shell: u8,
    /// Which of the 6 topology types (0-5)
    pub topology: u8,
    /// Bitfield for 55 operations (fits in u64)
    pub active_ops: u64,
    /// Audio coupling strength [0, 1]
    pub resonance: f32,
    /// Derived from substrate R
    pub stability: f32,
    /// Hash of the player who owns this zone (0 = unowned)
    pub owner_hash: u32,
    /// Angle on shell ring (radians)
    pub angle: f32,
}

impl Default for WorldAtom {
    fn default() -> Self {
        Self {
            zone_id: 0,
            shell: 0,
            topology: 0,
            active_ops: 0,
            resonance: 0.0,
            stability: 0.0,
            owner_hash: 0,
            angle: 0.0,
        }
    }
}

/// The complete world grid — 41 zones.
pub struct WorldGrid {
    pub zones: [WorldAtom; ZONE_COUNT],
}

impl WorldGrid {
    /// Create a new world grid with zones placed on orbital shells.
    pub fn new() -> Self {
        let mut zones = [WorldAtom::default(); ZONE_COUNT];
        let mut zone_id: u8 = 0;

        for (shell, &count) in SHELL_SIZES.iter().enumerate() {
            let shell_u8 = shell as u8;
            for i in 0..count {
                let angle = if count > 1 {
                    2.0 * std::f32::consts::PI * (i as f32) / (count as f32)
                } else {
                    0.0
                };

                zones[zone_id as usize] = WorldAtom {
                    zone_id,
                    shell: shell_u8,
                    topology: (i % 6) as u8,
                    active_ops: 0,
                    resonance: SHELL_BASE_RESONANCE[shell],
                    stability: SHELL_STABILITY_SCALE[shell],
                    owner_hash: 0,
                    angle,
                };

                zone_id += 1;
            }
        }

        Self { zones }
    }

    /// Apply an operation to a zone. Returns true if the op was new.
    pub fn apply_op(&mut self, zone_id: u32, op_id: u32) -> bool {
        if zone_id as usize >= ZONE_COUNT || op_id >= 55 {
            return false;
        }
        let zone = &mut self.zones[zone_id as usize];
        let mask = 1u64 << op_id;
        let was_new = zone.active_ops & mask == 0;
        zone.active_ops |= mask;
        was_new
    }

    /// Check if an op is active in a zone.
    pub fn has_op(&self, zone_id: u32, op_id: u32) -> bool {
        if zone_id as usize >= ZONE_COUNT || op_id >= 55 {
            return false;
        }
        self.zones[zone_id as usize].active_ops & (1u64 << op_id) != 0
    }

    /// Count active ops in a zone.
    pub fn op_count(&self, zone_id: u32) -> u32 {
        if zone_id as usize >= ZONE_COUNT {
            return 0;
        }
        self.zones[zone_id as usize].active_ops.count_ones()
    }

    /// Get all active op IDs for a zone as a vec.
    pub fn active_ops(&self, zone_id: u32) -> Vec<u32> {
        if zone_id as usize >= ZONE_COUNT {
            return vec![];
        }
        let bits = self.zones[zone_id as usize].active_ops;
        (0..55).filter(|&i| bits & (1u64 << i) != 0).collect()
    }

    /// Update zone stability based on substrate coherence R.
    pub fn update_stability(&mut self, substrate_r: f32) {
        for zone in &mut self.zones {
            let shell = zone.shell as usize;
            if shell < 4 {
                zone.stability = substrate_r * SHELL_STABILITY_SCALE[shell];
                zone.resonance = SHELL_BASE_RESONANCE[shell] * (0.5 + substrate_r * 0.5);
            }
        }
    }

    /// Apply a bass shockwave — ripples outward, attenuating per shell.
    pub fn apply_shockwave(&mut self, bass_peak: f32) {
        if bass_peak < 0.3 {
            return;
        }
        for zone in &mut self.zones {
            let shell = zone.shell as usize;
            if shell < 4 {
                let intensity = bass_peak * (1.0 - shell as f32 * 0.2);
                zone.resonance = (zone.resonance + intensity * 0.3).min(1.0);
            }
        }
    }

    /// Tick all zones — decay resonance toward base values.
    pub fn tick(&mut self, dt: f32) {
        let decay = (-dt * 2.0).exp(); // Exponential decay toward base
        for zone in &mut self.zones {
            let shell = zone.shell as usize;
            if shell < 4 {
                let base = SHELL_BASE_RESONANCE[shell];
                zone.resonance = base + (zone.resonance - base) * decay;
            }
        }
    }

    /// Get the zone witness as a flat f32 array: [R, entropy, stability, topology] per zone.
    /// Total length = ZONE_COUNT * 4 = 164.
    pub fn witness_flat(&self) -> Vec<f32> {
        let mut out = Vec::with_capacity(ZONE_COUNT * 4);
        for zone in &self.zones {
            out.push(zone.resonance);
            out.push(zone.stability);
            out.push(zone.active_ops.count_ones() as f32 / 55.0); // Normalized op density
            out.push(zone.topology as f32);
        }
        out
    }

    /// Get the shell that a zone belongs to.
    pub fn zone_shell(zone_id: u32) -> u8 {
        if zone_id == 0 { return 0; }
        if zone_id <= 6 { return 1; }
        if zone_id <= 18 { return 2; }
        if zone_id <= 40 { return 3; }
        3 // Clamp
    }
}

impl Default for WorldGrid {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_world_creation() {
        let world = WorldGrid::new();
        assert_eq!(world.zones.len(), ZONE_COUNT);
        assert_eq!(world.zones[0].shell, 0); // Shell 0
        assert_eq!(world.zones[1].shell, 1); // Shell 1
        assert_eq!(world.zones[7].shell, 2); // Shell 2
        assert_eq!(world.zones[19].shell, 3); // Shell 3
    }

    #[test]
    fn test_apply_op() {
        let mut world = WorldGrid::new();
        assert!(world.apply_op(0, 5)); // New op
        assert!(!world.apply_op(0, 5)); // Already active
        assert!(world.has_op(0, 5));
        assert!(!world.has_op(0, 6));
        assert_eq!(world.op_count(0), 1);
    }

    #[test]
    fn test_apply_op_bounds() {
        let mut world = WorldGrid::new();
        assert!(!world.apply_op(50, 0)); // Zone out of bounds
        assert!(!world.apply_op(0, 55)); // Op out of bounds
        assert!(!world.apply_op(0, 60)); // Op out of bounds
    }

    #[test]
    fn test_active_ops() {
        let mut world = WorldGrid::new();
        world.apply_op(5, 0);
        world.apply_op(5, 10);
        world.apply_op(5, 54);
        let ops = world.active_ops(5);
        assert_eq!(ops, vec![0, 10, 54]);
    }

    #[test]
    fn test_update_stability() {
        let mut world = WorldGrid::new();
        world.update_stability(0.5);
        assert!((world.zones[0].stability - 0.5).abs() < 1e-6); // Shell 0: 0.5 * 1.0
        assert!((world.zones[1].stability - 0.45).abs() < 1e-6); // Shell 1: 0.5 * 0.9
    }

    #[test]
    fn test_shockwave() {
        let mut world = WorldGrid::new();
        // Shell 0 base resonance is 1.0 (already at max), use shell 3 zone (base 0.2)
        let zone_idx = 19; // First zone in shell 3
        let pre_res = world.zones[zone_idx].resonance;
        world.apply_shockwave(0.8);
        assert!(world.zones[zone_idx].resonance > pre_res,
            "resonance {} should exceed pre {}", world.zones[zone_idx].resonance, pre_res);
    }

    #[test]
    fn test_shockwave_below_threshold() {
        let mut world = WorldGrid::new();
        let pre_res = world.zones[0].resonance;
        world.apply_shockwave(0.2); // Below 0.3 threshold
        assert_eq!(world.zones[0].resonance, pre_res);
    }

    #[test]
    fn test_tick_decay() {
        let mut world = WorldGrid::new();
        world.zones[1].resonance = 1.0; // Spike above base (0.8)
        world.tick(1.0); // 1 second
        assert!(world.zones[1].resonance < 1.0); // Decayed toward 0.8
        assert!(world.zones[1].resonance > 0.8); // But not below base
    }

    #[test]
    fn test_witness_flat() {
        let world = WorldGrid::new();
        let flat = world.witness_flat();
        assert_eq!(flat.len(), ZONE_COUNT * 4);
    }

    #[test]
    fn test_zone_shell() {
        assert_eq!(WorldGrid::zone_shell(0), 0);
        assert_eq!(WorldGrid::zone_shell(1), 1);
        assert_eq!(WorldGrid::zone_shell(6), 1);
        assert_eq!(WorldGrid::zone_shell(7), 2);
        assert_eq!(WorldGrid::zone_shell(18), 2);
        assert_eq!(WorldGrid::zone_shell(19), 3);
        assert_eq!(WorldGrid::zone_shell(40), 3);
    }
}