99 lines
2.8 KiB
Rust
99 lines
2.8 KiB
Rust
//! Quantum dice rolling using QTRNG bytes.
|
|
//!
|
|
//! Configurable sides (d4, d6, d8, d12, d20, etc.) and count.
|
|
//! Uses rejection sampling for uniform distribution.
|
|
|
|
/// Default number of sides (d6).
|
|
pub const DEFAULT_SIDES: u32 = 6;
|
|
|
|
/// Default number of dice to roll.
|
|
pub const DEFAULT_COUNT: usize = 1;
|
|
|
|
/// Max dice per request to avoid excessive entropy use.
|
|
pub const MAX_COUNT: usize = 100;
|
|
|
|
/// Min/max sides (d2 = coin, up to d256).
|
|
pub const MIN_SIDES: u32 = 2;
|
|
pub const MAX_SIDES: u32 = 256;
|
|
|
|
/// Roll one die with given sides using bytes from a RNG.
|
|
/// Returns value in 1..=sides. Uses rejection sampling for uniformity.
|
|
#[inline]
|
|
fn roll_one(sides: u32, bytes: &[u8], byte_index: &mut usize) -> Option<u32> {
|
|
if sides < MIN_SIDES || sides > MAX_SIDES {
|
|
return None;
|
|
}
|
|
let n = sides as u64;
|
|
let threshold = (u32::MAX as u64 / n) * n; // largest multiple of n that fits in u32
|
|
loop {
|
|
if *byte_index + 4 > bytes.len() {
|
|
return None;
|
|
}
|
|
let word = u32::from_be_bytes([
|
|
bytes[*byte_index],
|
|
bytes[*byte_index + 1],
|
|
bytes[*byte_index + 2],
|
|
bytes[*byte_index + 3],
|
|
]);
|
|
*byte_index += 4;
|
|
if (word as u64) < threshold {
|
|
return Some((word as u32 % sides as u32) + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Roll `count` dice with `sides` each, using the provided random bytes.
|
|
/// Returns `None` if not enough bytes or invalid params; otherwise `Some(vec of rolls)`.
|
|
pub fn roll_dice(sides: u32, count: usize, bytes: &[u8]) -> Option<Vec<u32>> {
|
|
if count == 0 || count > MAX_COUNT {
|
|
return None;
|
|
}
|
|
if sides < MIN_SIDES || sides > MAX_SIDES {
|
|
return None;
|
|
}
|
|
let mut out = Vec::with_capacity(count);
|
|
let mut idx = 0;
|
|
for _ in 0..count {
|
|
let v = roll_one(sides, bytes, &mut idx)?;
|
|
out.push(v);
|
|
}
|
|
Some(out)
|
|
}
|
|
|
|
/// Estimate bytes needed for `count` dice of `sides` (worst-case rejection).
|
|
#[allow(dead_code)]
|
|
pub fn bytes_needed(sides: u32, count: usize) -> usize {
|
|
let n = sides as u64;
|
|
let threshold = (u32::MAX as u64 / n) * n;
|
|
let accept_prob = threshold as f64 / u32::MAX as f64;
|
|
let per_roll = 4.0_f64 / accept_prob;
|
|
(per_roll * count as f64).ceil() as usize
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn roll_d6_one() {
|
|
let bytes = [0u8; 64];
|
|
let r = roll_dice(6, 1, &bytes);
|
|
assert!(r.is_some());
|
|
let r = r.unwrap();
|
|
assert_eq!(r.len(), 1);
|
|
assert!(r[0] >= 1 && r[0] <= 6);
|
|
}
|
|
|
|
#[test]
|
|
fn roll_d20_five() {
|
|
let bytes = [0u8; 128];
|
|
let r = roll_dice(20, 5, &bytes);
|
|
assert!(r.is_some());
|
|
let r = r.unwrap();
|
|
assert_eq!(r.len(), 5);
|
|
for &v in &r {
|
|
assert!(v >= 1 && v <= 20);
|
|
}
|
|
}
|
|
}
|