vortex-core 0.1.0

Core types and deterministic scheduler for Vortex simulation engine
Documentation
//! Deterministic random number generator.
//!
//! Seeded PRNG using a PCG-style LCG. Same seed produces identical sequences
//! across runs, platforms, and Rust versions. No external entropy is ever used.

/// A deterministic pseudo-random number generator.
///
/// Uses a 64-bit LCG (Linear Congruential Generator) with PCG constants.
/// Same seed always produces the same sequence — this is the foundation
/// of Vortex's reproducibility guarantee.
pub struct DetRng {
    state: u64,
}

impl DetRng {
    /// Create a new RNG with the given seed.
    pub fn new(seed: u64) -> Self {
        Self { state: seed }
    }

    /// Create a child RNG for a subsystem by mixing the master seed with a domain tag.
    ///
    /// This ensures different subsystems (network, storage, clock) get independent
    /// but deterministic random streams from the same master seed.
    ///
    /// ```
    /// # use vortex_core::DetRng;
    /// let master = 42u64;
    /// let net_rng = DetRng::derive(master, "network");
    /// let disk_rng = DetRng::derive(master, "storage");
    /// // net_rng and disk_rng produce different sequences
    /// ```
    pub fn derive(master_seed: u64, domain: &str) -> Self {
        let mut hash: u64 = 14695981039346656037; // FNV-1a offset basis
        // Mix in master seed bytes
        for byte in master_seed.to_le_bytes() {
            hash ^= byte as u64;
            hash = hash.wrapping_mul(1099511628211);
        }
        // Mix in domain string
        for byte in domain.as_bytes() {
            hash ^= *byte as u64;
            hash = hash.wrapping_mul(1099511628211);
        }
        Self { state: hash }
    }

    /// Advance the RNG state and return the next raw u64.
    #[inline]
    pub fn next_u64(&mut self) -> u64 {
        self.state = self
            .state
            .wrapping_mul(6364136223846793005)
            .wrapping_add(1442695040888963407);
        self.state
    }

    /// Return a random f64 in [0.0, 1.0).
    #[inline]
    pub fn next_f64(&mut self) -> f64 {
        let val = self.next_u64();
        (val >> 11) as f64 / (1u64 << 53) as f64
    }

    /// Return a random u64 in [0, max) (exclusive upper bound).
    #[inline]
    pub fn next_u64_below(&mut self, max: u64) -> u64 {
        if max == 0 {
            return 0;
        }
        self.next_u64() % max
    }

    /// Return a random u64 in [min, max] (inclusive both bounds).
    #[inline]
    pub fn next_u64_range(&mut self, min: u64, max: u64) -> u64 {
        if min >= max {
            return min;
        }
        let range = max - min + 1;
        min + self.next_u64_below(range)
    }

    /// Return true with the given probability [0.0, 1.0].
    #[inline]
    pub fn chance(&mut self, probability: f64) -> bool {
        self.next_f64() < probability
    }

    /// Shuffle a slice in-place (Fisher-Yates).
    pub fn shuffle<T>(&mut self, slice: &mut [T]) {
        let len = slice.len();
        if len < 2 {
            return;
        }
        for i in (1..len).rev() {
            let j = self.next_u64_below(i as u64 + 1) as usize;
            slice.swap(i, j);
        }
    }

    /// Get the current internal state (for debugging/logging).
    pub fn state(&self) -> u64 {
        self.state
    }
}

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

    #[test]
    fn test_deterministic_same_seed() {
        let mut rng1 = DetRng::new(42);
        let mut rng2 = DetRng::new(42);
        let seq1: Vec<u64> = (0..100).map(|_| rng1.next_u64()).collect();
        let seq2: Vec<u64> = (0..100).map(|_| rng2.next_u64()).collect();
        assert_eq!(seq1, seq2, "Same seed must produce identical sequences");
    }

    #[test]
    fn test_different_seeds_differ() {
        let mut rng1 = DetRng::new(42);
        let mut rng2 = DetRng::new(99);
        let seq1: Vec<u64> = (0..10).map(|_| rng1.next_u64()).collect();
        let seq2: Vec<u64> = (0..10).map(|_| rng2.next_u64()).collect();
        assert_ne!(seq1, seq2);
    }

    #[test]
    fn test_derive_produces_different_streams() {
        let mut net = DetRng::derive(42, "network");
        let mut disk = DetRng::derive(42, "storage");
        let a = net.next_u64();
        let b = disk.next_u64();
        assert_ne!(a, b);
    }

    #[test]
    fn test_derive_is_deterministic() {
        let mut r1 = DetRng::derive(42, "network");
        let mut r2 = DetRng::derive(42, "network");
        let seq1: Vec<u64> = (0..50).map(|_| r1.next_u64()).collect();
        let seq2: Vec<u64> = (0..50).map(|_| r2.next_u64()).collect();
        assert_eq!(seq1, seq2);
    }

    #[test]
    fn test_f64_in_range() {
        let mut rng = DetRng::new(42);
        for _ in 0..10_000 {
            let v = rng.next_f64();
            assert!((0.0..1.0).contains(&v), "f64 out of [0,1): {v}");
        }
    }

    #[test]
    fn test_range_in_bounds() {
        let mut rng = DetRng::new(42);
        for _ in 0..10_000 {
            let v = rng.next_u64_range(10, 50);
            assert!((10..=50).contains(&v), "Range out of [10,50]: {v}");
        }
    }

    #[test]
    fn test_chance_probability() {
        let mut rng = DetRng::new(42);
        let total = 10_000;
        let hits: usize = (0..total).filter(|_| rng.chance(0.5)).count();
        let rate = hits as f64 / total as f64;
        assert!(
            rate > 0.45 && rate < 0.55,
            "50% chance should be ~50%, got {:.1}%",
            rate * 100.0
        );
    }

    #[test]
    fn test_shuffle_deterministic() {
        let mut rng1 = DetRng::new(42);
        let mut rng2 = DetRng::new(42);
        let mut a: Vec<i32> = (0..20).collect();
        let mut b: Vec<i32> = (0..20).collect();
        rng1.shuffle(&mut a);
        rng2.shuffle(&mut b);
        assert_eq!(a, b, "Same seed shuffle must match");
    }

    #[test]
    fn test_shuffle_actually_shuffles() {
        let mut rng = DetRng::new(42);
        let original: Vec<i32> = (0..20).collect();
        let mut shuffled = original.clone();
        rng.shuffle(&mut shuffled);
        assert_ne!(original, shuffled, "Shuffle should reorder elements");
    }
}