darkforge_rng 0.1.0

Dark Forge is a library and extension for Godot engine that implements the Blades in the Dark SRD by One Seven Design.
Documentation
/*
 * Copyright (C) 2025 Pierre Fouilloux, Hibiscus Collective
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License along with this program.
 * If not, see https://www.gnu.org/licenses/.
 */
//! # Random Number Generation
//!
//! Random number generation utilities for tabletop gaming applications.
//!
//! The module offers:
//! - A [`Random`] trait for random number generators
//! - An implementation of this trait using thread-local random number generation ([`UniformThreadRandom`])
//! - Test utilities for predictable random number generation
//!
//! ## Examples
//!
//! ```
//! use darkforge_rng::rng::{Random, UniformThreadRandom};
//!
//! // Create a random number generator for values between 1 and 100
//! let mut rng = UniformThreadRandom::new(1, 100).unwrap();
//!
//! // Generate a single random number
//! let value = rng.next();
//! assert!(value >= 1 && value <= 100);
//!
//! // Generate multiple random numbers
//! let values = rng.take(5);
//! assert_eq!(values.len(), 5);
//! for &value in &values {
//!     assert!(value >= 1 && value <= 100);
//! }
//! ```

#[cfg(test)]
pub mod test;

use core::fmt::Debug;
use std::fmt;

use fmt::Formatter;
use rand::{
    distr::{Distribution as _, Uniform, uniform::SampleUniform},
    prelude::ThreadRng,
};
use thiserror::Error;

use crate::Result;

/// Error type for random number generation operations.
///
/// This enum represents the various errors that can occur during random number generation
/// operations in this module.
///
/// # Examples
///
/// ```
/// use darkforge_rng::rng::{UniformThreadRandom, RngError};
/// use rand::distr::uniform::Error;
/// use darkforge_rng::DFRngError;
///
/// // This will fail because the lower bound is greater than the upper bound
/// let err = UniformThreadRandom::<u8>::new(10, 5).expect_err("should have failed");
/// assert_eq!(DFRngError::RngError(RngError::InvalidDistribution(Error::EmptyRange)), err);
/// ```
#[derive(Debug, Error, PartialEq)]
pub enum RngError {
    /// An error occurred in the uniform distribution.
    ///
    /// For example, the lower bound is greater than the upper bound.
    #[error("invalid distribution: {0}")]
    InvalidDistribution(#[from] rand::distr::uniform::Error),
}

/// Trait for random number generators.
///
/// Allows different implementations to be used interchangeably.
///
/// # Type Parameters
///
/// * `T` - The values generated by this random number generator
///
/// # Examples
///
/// ```
/// use darkforge_rng::rng::{Random, UniformThreadRandom};
///
/// // Create a random number generator
/// let mut rng = UniformThreadRandom::new(1, 6).unwrap();
///
/// // Generate a random number
/// let value = rng.next();
/// assert!(value >= 1 && value <= 6);
/// ```
pub trait Random<T> {
    /// Generates the next random value.
    ///
    /// # Returns
    ///
    /// A random value of type `T`
    ///
    /// # Examples
    ///
    /// ```
    /// use darkforge_rng::rng::{Random, UniformThreadRandom};
    ///
    /// let mut rng = UniformThreadRandom::new(1, 20).unwrap();
    /// let value = rng.next();
    /// assert!(value >= 1 && value <= 20);
    /// ```
    fn next(&mut self) -> T;

    /// Generates multiple random values.
    ///
    /// # Arguments
    ///
    /// * `n` - The number of random values to generate
    ///
    /// # Returns
    ///
    /// A vector containing `n` random values of type `T`
    ///
    /// # Examples
    ///
    /// ```
    /// use darkforge_rng::rng::{Random, UniformThreadRandom};
    ///
    /// let mut rng = UniformThreadRandom::new(1, 10).unwrap();
    /// let values = rng.take(5);
    /// assert_eq!(values.len(), 5);
    /// for &value in &values {
    ///     assert!(value >= 1 && value <= 10);
    /// }
    /// ```
    fn take(&mut self, n: usize) -> Vec<T>;
}

/// A random number generator that produces uniformly distributed values using thread-local randomness.
///
/// This struct uses the `rand` crate's `ThreadRng` to generate random numbers with a uniform
/// distribution between specified bounds.
///
/// # Type Parameters
///
/// * `T` - The type of values generated by this random number generator. `T` must implement `SampleUniform`
///
/// # Examples
///
/// ```
/// use darkforge_rng::rng::{Random, UniformThreadRandom};
///
/// // Create a random number generator for values between 1 and 100
/// let mut rng = UniformThreadRandom::new(1, 100).unwrap();
///
/// // Generate a random number
/// let value = rng.next();
/// assert!(value >= 1 && value <= 100);
/// ```
pub struct UniformThreadRandom<T: SampleUniform> {
    /// The uniform distribution used to generate random values
    distribution: Uniform<T>,

    /// The thread-local random number generator used to generate random values
    rng: ThreadRng,
}

impl<T: SampleUniform> UniformThreadRandom<T> {
    /// Creates a new random number generator with the specified bounds.
    ///
    /// # Arguments
    ///
    /// * `low` - The lower bound (inclusive)
    /// * `high` - The upper bound (inclusive)
    ///
    /// # Returns
    ///
    /// A `Result` containing either the new random number generator or an error
    /// if the bounds are invalid (e.g. if `low > high`).
    ///
    /// # Examples
    ///
    /// ```
    /// use darkforge_rng::rng::UniformThreadRandom;
    ///
    /// // Create a random number generator for values between 1 and 6
    /// let rng = UniformThreadRandom::new(1, 6).unwrap();
    ///
    /// // This will fail because the lower bound is greater than the upper bound
    /// let result = UniformThreadRandom::<u8>::new(10, 5);
    /// assert!(result.is_err());
    /// ```
    ///
    /// # Errors
    ///
    /// The `UniformThreadRandom` struct uses the `Uniform` distribution from the `rand` crate
    /// to generate random numbers. Errors can occur if the bounds are invalid, such as if
    /// `low > high`. In this case, the `UniformThreadRandom::new` constructor will return
    /// an error.
    #[inline]
    pub fn new(low: T, high: T) -> Result<Self> {
        let distribution = Uniform::new_inclusive(low, high).map_err(RngError::InvalidDistribution)?;
        Ok(Self {
            distribution,
            rng: ThreadRng::default(),
        })
    }
}

impl<T: SampleUniform> Debug for UniformThreadRandom<T> {
    #[inline]
    #[expect(clippy::min_ident_chars, reason = "Conflicts with lint requiring same names as trait")]
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("UniformThreadRandom").finish()
    }
}

impl<T: SampleUniform> Random<T> for UniformThreadRandom<T> {
    /// Generates the next random value within the configured bounds.
    ///
    /// # Returns
    ///
    /// A random value of type `T` between the lower and upper bounds (inclusive)
    #[inline]
    fn next(&mut self) -> T {
        self.distribution.sample(&mut self.rng)
    }

    /// Generates multiple random values within the configured bounds.
    ///
    /// # Arguments
    ///
    /// * `n` - The number of random values to generate
    ///
    /// # Returns
    ///
    /// A vector containing `n` random values of type `T`, each between the lower and upper bounds (inclusive)
    #[inline]
    fn take(&mut self, n: usize) -> Vec<T> {
        (&self.distribution).sample_iter(&mut self.rng).take(n).collect()
    }
}

#[cfg(test)]
mod tests {
    use rand::distr::uniform::Error;

    use super::*;
    use crate::DFRngError;

    #[test]
    fn should_return_error_when_uniform_distribution_upper_bound_is_smaller_than_lower_bound() {
        let err = UniformThreadRandom::new(10, 5).expect_err("should have failed");
        assert_eq!(DFRngError::RngError(RngError::InvalidDistribution(Error::EmptyRange)), err);
    }
}