84 lines
2.5 KiB
Rust
84 lines
2.5 KiB
Rust
|
|
//! 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<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];
|
|||
|
|
// 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);
|
|||
|
|
}
|
|||
|
|
}
|