bracket-random 0.8.7

Random number generator (xorshift based), focused on dice rolling. Optionally includes parsing of RPG-style dice strings (e.g. "3d6+12"). Part of the bracket-lib family.
Documentation
#[cfg(feature = "parsing")]
use crate::prelude::{parse_dice_string, DiceParseError, DiceType};
use rand::{Rng, RngCore, SeedableRng};
use rand_xorshift::XorShiftRng;

#[cfg(feature = "serde")]
use serde_crate::{Deserialize, Serialize};

#[cfg(target_arch = "wasm32")]
fn get_seed() -> u64 {
    let mut buf = [0u8; 8];
    if crate::js_seed::getrandom_inner(&mut buf).is_ok() {
        u64::from_be_bytes(buf)
    } else {
        js_sys::Date::now() as u64
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn get_seed() -> u64 {
    let mut buf = [0u8; 8];
    if getrandom::getrandom(&mut buf).is_ok() {
        u64::from_be_bytes(buf)
    } else {
        use std::time::{SystemTime, UNIX_EPOCH};
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs() as u64
    }
}

#[derive(Clone)]
#[cfg_attr(
    feature = "serde",
    derive(Serialize, Deserialize),
    serde(crate = "serde_crate")
)]
pub struct RandomNumberGenerator {
    rng: XorShiftRng,
}

impl RandomNumberGenerator {
    /// Creates a new RNG from a randomly generated seed
    #[allow(clippy::new_without_default)] // XorShiftRng doesn't have a Default, so we don't either
    pub fn new() -> RandomNumberGenerator {
        let rng: XorShiftRng = SeedableRng::seed_from_u64(get_seed());
        RandomNumberGenerator { rng }
    }

    /// Creates a new RNG from a specific seed
    pub fn seeded(seed: u64) -> RandomNumberGenerator {
        let rng: XorShiftRng = SeedableRng::seed_from_u64(seed);
        RandomNumberGenerator { rng }
    }

    /// Returns a random value of whatever type you specify
    pub fn rand<T>(&mut self) -> T
    where
        rand::distributions::Standard: rand::distributions::Distribution<T>,
    {
        self.rng.gen::<T>()
    }

    /// Returns a random value in the specified range, of type specified at the call site.
    /// This is INCLUSIVE of the first parameter, and EXCLUSIVE of the second.
    /// So range(1,6) will give you numbers from 1 to 5.
    pub fn range<T>(&mut self, min: T, max: T) -> T
    where
        T: rand::distributions::uniform::SampleUniform + PartialOrd,
    {
        self.rng.gen_range(min..max)
    }

    /// Rolls dice, using the classic 3d6 type of format: n is the number of dice, die_type is the size of the dice.
    pub fn roll_dice(&mut self, n: i32, die_type: i32) -> i32 {
        (0..n).map(|_| self.range(1, die_type + 1)).sum()
    }

    /// Returns the RNG's next unsigned-64 type
    pub fn next_u64(&mut self) -> u64 {
        self.rng.next_u64()
    }

    /// Rolls dice based on a DiceType struct, including application of the bonus
    #[cfg(feature = "parsing")]
    pub fn roll(&mut self, dice: DiceType) -> i32 {
        self.roll_dice(dice.n_dice, dice.die_type) + dice.bonus
    }

    /// Rolls dice based on passing in a string, such as roll_str("1d12")
    #[cfg(feature = "parsing")]
    pub fn roll_str<S: ToString>(&mut self, dice: S) -> Result<i32, DiceParseError> {
        match parse_dice_string(&dice.to_string()) {
            Ok(dt) => Ok(self.roll(dt)),
            Err(e) => Err(e),
        }
    }

    /// Returns a random index into a slice
    pub fn random_slice_index<T>(&mut self, slice: &[T]) -> Option<usize> {
        if slice.is_empty() {
            None
        } else {
            let sz = slice.len();
            if sz == 1 {
                Some(0)
            } else {
                Some(self.roll_dice(1, sz as i32) as usize - 1)
            }
        }
    }

    /// Returns a random entry in a slice (or none if empty)
    pub fn random_slice_entry<'a, T>(&mut self, slice: &'a [T]) -> Option<&'a T> {
        if slice.is_empty() {
            None
        } else {
            let sz = slice.len();
            if sz == 1 {
                Some(&slice[0])
            } else {
                Some(&slice[self.roll_dice(1, sz as i32) as usize - 1])
            }
        }
    }

    /// Get underlying RNG implementation for use in traits / algorithms exposed by
    /// other crates (eg. `rand` itself)
    pub fn get_rng(&mut self) -> &mut XorShiftRng {
        &mut self.rng
    }
}

#[cfg(test)]
mod tests {
    use crate::prelude::RandomNumberGenerator;

    #[test]
    fn roll_str_1d6() {
        let mut rng = RandomNumberGenerator::new();
        assert!(rng.roll_str("1d6").is_ok());
    }

    #[test]
    fn roll_str_3d6plus1() {
        let mut rng = RandomNumberGenerator::new();
        assert!(rng.roll_str("3d6+1").is_ok());
    }

    #[test]
    fn roll_str_3d20minus1() {
        let mut rng = RandomNumberGenerator::new();
        assert!(rng.roll_str("3d20-1").is_ok());
    }

    #[test]
    fn roll_str_error() {
        let mut rng = RandomNumberGenerator::new();
        assert!(rng.roll_str("blah").is_err());
    }

    #[test]
    fn test_roll_range() {
        let mut rng = RandomNumberGenerator::new();
        for _ in 0..100 {
            let n = rng.roll_dice(1, 20);
            assert!(n > 0 && n < 21);
        }
    }

    #[test]
    fn random_slice_index_empty() {
        let mut rng = RandomNumberGenerator::new();
        let test_data: Vec<i32> = Vec::new();
        assert!(rng.random_slice_index(&test_data).is_none());
    }

    #[test]
    fn random_slice_index_valid() {
        let mut rng = RandomNumberGenerator::new();
        let test_data: Vec<i32> = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        for _ in 0..100 {
            match rng.random_slice_index(&test_data) {
                None => assert!(1 == 2),
                Some(idx) => assert!(idx < test_data.len()),
            }
        }
    }

    #[test]
    fn random_slice_entry_empty() {
        let mut rng = RandomNumberGenerator::new();
        let test_data: Vec<i32> = Vec::new();
        assert!(rng.random_slice_entry(&test_data).is_none());
    }

    #[test]
    fn random_slice_entry_valid() {
        let mut rng = RandomNumberGenerator::new();
        let test_data: Vec<i32> = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        for _ in 0..100 {
            match rng.random_slice_entry(&test_data) {
                None => assert!(1 == 2),
                Some(e) => assert!(*e > 0 && *e < 11),
            }
        }
    }

    #[cfg(feature = "serde")]
    #[test]
    fn serialize_rng() {
        use serde_crate::{Deserialize, Serialize};
        let mut rng = RandomNumberGenerator::seeded(1000);
        let serialized = serde_json::to_string(&rng).unwrap();
        let n = rng.range(0, 100);
        let mut deserialized: RandomNumberGenerator = serde_json::from_str(&serialized).unwrap();
        let n2 = deserialized.range(0, 100);
        assert!(n == n2);
    }
}