//! Seeded RNG service (GameKernel K5) with the null/reference impl pair //! required by AM-11. The only randomness source available to game code. use crate::ids::Seed; use rand_core::{RngCore, SeedableRng}; /// Kernel randomness: deterministic draws from the game seed. pub trait KernelRng { /// Uniform draw in `0..bound` (bound ≥ 1). fn draw(&mut self, bound: u32) -> u32; /// Deterministic Fisher–Yates shuffle of `items`. fn shuffle(&mut self, items: &mut [T]) { for i in (1..items.len()).rev() { #[allow(clippy::cast_possible_truncation)] let j = self.draw(i as u32 + 1) as usize; items.swap(i, j); } } } /// Reference implementation: ChaCha12, seeded from the game `Seed`. pub struct ChaChaRng(rand_chacha::ChaCha12Rng); impl ChaChaRng { pub fn from_seed(seed: Seed) -> Self { Self(rand_chacha::ChaCha12Rng::seed_from_u64(seed.0)) } } impl KernelRng for ChaChaRng { fn draw(&mut self, bound: u32) -> u32 { assert!(bound >= 1, "draw bound must be >= 1"); // Rejection sampling for uniformity. let zone = u32::MAX - (u32::MAX % bound); loop { let v = self.0.next_u32(); if v < zone { return v % bound; } } } } /// Null implementation for tests: always returns 0 (first choice, no /// shuffle movement). Makes scenario fixtures fully predictable. #[derive(Default)] pub struct NullRng; impl KernelRng for NullRng { fn draw(&mut self, _bound: u32) -> u32 { 0 } } /// The AM-11 conformance suite for [`KernelRng`] — **one** suite, driven by /// every impl. /// /// Before CB-WP-0006 T05 the pair was exercised by two *separate*, /// non-shared tests (`chacha_is_deterministic_per_seed` and /// `null_rng_never_moves_a_shuffle`). M-D4-SWAP is a bool over impls /// "passing the same conformance suite", so a pair with no shared suite /// never earned it; ADR-0005 §4 downgraded AM-11 to unmet on exactly that /// reading. /// /// The assertions are the properties true of **both** a real CSPRNG and a /// null draw — determinism, bounds, and shuffle integrity. An impl that /// violates any of them cannot be substituted for the other, which is what /// the port claims. pub fn conformance(label: &str, mut make: impl FnMut() -> R) { // Bounds. `draw(n)` must land in `0..n` for every impl, or callers // indexing with it are unsound. let mut r = make(); for bound in [1u32, 2, 3, 7, 52, 1000] { for _ in 0..64 { let v = r.draw(bound); assert!( v < bound, "{label}: draw({bound}) returned {v}, outside 0..{bound}" ); } } // draw(1) has exactly one legal answer. let mut r = make(); assert_eq!(r.draw(1), 0, "{label}: draw(1) must be 0"); // Determinism: two fresh instances must produce identical sequences. // This is K5 for ChaCha and trivially true for the null impl — which // is the point of a shared suite. let (mut a, mut b) = (make(), make()); let seq_a: Vec = (0..32).map(|_| a.draw(97)).collect(); let seq_b: Vec = (0..32).map(|_| b.draw(97)).collect(); assert_eq!( seq_a, seq_b, "{label}: two fresh instances must draw identically" ); // Shuffle preserves the multiset — it may reorder, never invent or // drop. A shuffle that loses an element would corrupt a deal. let mut r = make(); let original: Vec = (0..52).collect(); let mut items = original.clone(); r.shuffle(&mut items); let mut sorted = items.clone(); sorted.sort_unstable(); assert_eq!( sorted, original, "{label}: shuffle must preserve the multiset" ); // Shuffling is deterministic too, or replay diverges (K8). let (mut c, mut d) = (make(), make()); let (mut x, mut y) = (original.clone(), original.clone()); c.shuffle(&mut x); d.shuffle(&mut y); assert_eq!(x, y, "{label}: shuffle must be deterministic per impl"); } #[cfg(test)] mod tests { use super::*; /// AM-11: the SAME suite, both impls. Two separate tests are what the /// pair had before, and why the metric was never earned. #[test] fn chacha_passes_the_conformance_suite() { conformance("ChaChaRng", || ChaChaRng::from_seed(Seed(42))); } #[test] fn null_rng_passes_the_conformance_suite() { conformance("NullRng", NullRng::default); } /// AM-8 seed determinism at the unit level: same seed, same draws. #[test] fn chacha_is_deterministic_per_seed() { let mut a = ChaChaRng::from_seed(Seed(42)); let mut b = ChaChaRng::from_seed(Seed(42)); let mut c = ChaChaRng::from_seed(Seed(43)); let draws_a: Vec = (0..64).map(|_| a.draw(1000)).collect(); let draws_b: Vec = (0..64).map(|_| b.draw(1000)).collect(); let draws_c: Vec = (0..64).map(|_| c.draw(1000)).collect(); assert_eq!(draws_a, draws_b); assert_ne!(draws_a, draws_c); } #[test] fn null_rng_never_moves_a_shuffle() { let mut rng = NullRng; let mut items = vec![1, 2, 3, 4]; // Fisher–Yates with j=0 each step rotates deterministically. rng.shuffle(&mut items); let mut again = vec![1, 2, 3, 4]; NullRng.shuffle(&mut again); assert_eq!(items, again); } }