clay-borg/crates/cb-kernel/src/rng.rs

83 lines
2.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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 FisherYates shuffle of `items`.
fn shuffle<T>(&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
}
}
#[cfg(test)]
mod tests {
use super::*;
/// 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<u32> = (0..64).map(|_| a.draw(1000)).collect();
let draws_b: Vec<u32> = (0..64).map(|_| b.draw(1000)).collect();
let draws_c: Vec<u32> = (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];
// FisherYates 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);
}
}