dice_expression 0.1.6

A library to help you parse and execute some dice expressions.
Documentation
//! A library to help you parse and execute some dice expressions like:
//! - `1d6 + 1`
//! - `2d20`
//! - `3d20k2 - 5`
//! - `10d20>10`
//!
//! # Examples
//!
//! A simple dice roll might look like:
//! ```
//! use dice_expression::{Dice, Evaluable};
//!
//! let d20 = Dice::from(20);
//! let roll_result = d20.evaluate()
//!     .unwrap();
//! ```
//!
//! A more complete expression that requires parsing would look like:
//! ```
//! use std::str::FromStr;
//! use dice_expression::{DiceExpression, Evaluable};
//!
//! let expr = DiceExpression::from_str("2d20k1min3 - 1d4 + 3").unwrap();
//! let roll_result = expr.evaluate().unwrap();
//! ```
//!
//! # Supported operations and modifiers
//! The following operations can be parsed:
//! - Basic operations: `+`, `-`, `*`
//! - Negation: `-`
//! - Parenthesis: `(` and `)`
//! - Dices: `d6` or `1d15` or `2d20`
//! - Dice modifiers:
//!     - Keep highest: `2d20kH`, `2d20k1` or `5d20k2`
//!     - Keep lowest: `2d20klL`, `2d20kl1` or `5d8kl3`
//!     - Drop highest: `2d20dhH`, `2d20dh1` or `5d20dh2`
//!     - Drop lowest: `2d20dL`, `2d20d1` or `5d8d3`
//!     - Reroll: `2d20r5` will reroll all results lower or equal to 5 until it gets results strictly superior to 5.
//!     - Reroll Once: `2d20ro5` will reroll all results lower or equal to 5, but only once.
//!     - Minimum: `2d20mi4` or `2d20min4` will change all results lower than 4 to the minimum value of 4.
//!     - Maximum: `2d20ma17` or `2d20max17` will change all results greater than 17 to the minimum value of 17.
//!     - Count number of results greater than: `2d20>10` will count the number of results that are greater or equal to 10.
//!     - Count number of results lower than: `2d20<8` will count the number of results that are lower or equal to 8.
//!
//! All dice modifiers can be combined, like: `3d20r3k2max18`
//!

use crate::ast::{Expr, ParserError};
use crate::ast_evaluation::{FixedSource, FixedValue, RandCrateSource, StochasticEvaluable};
use std::fmt::{Debug, Display};
use std::str::FromStr;
use thiserror::Error;

mod ast;
mod ast_evaluation;

/// Might be returned during the evaluation of an expression.
#[derive(Error, Debug)]
pub enum EvaluateError {
    /// The reroll value is higher than the number of sides on the given dice, making it impossible to reach a value that's above the reroll value.
    #[error("Reroll value ({reroll}) is higher than the number of dice sides ({dice_sides})")]
    RerollValueHigherThanDiceSides {
        /// A rolled value needs to be rerolled if they are lower or equal than the "reroll" value.
        reroll: u32,
        /// Number of sides for the current dice to be rerolled.
        dice_sides: u32,
    },
    /// The minimum value is higher than the number of sides on the given dice, making it impossible to reach a value that's above the minimum value with a dice roll.
    #[error("Minimum value ({minimum}) is higher than the dice sides ({dice_size})")]
    MinimumValueHigherThanDiceSides {
        /// The minimum value to reach.
        minimum: u32,
        /// Number of sides for the current dice.
        dice_size: u32,
    },
    /// The maximum value specified is 0, but it needs to be at least 1.
    ///
    /// Dices rolls in the range \[1 ... \<Sides>], making '0' an impossible value to reach.
    #[error("Maximum value cannot be 0")]
    MaximumValueCannotBe0,
    /// Catch any other errors, most probably coming from RandomSource
    #[error(transparent)]
    OtherErrors(#[from] Box<dyn std::error::Error + Send + Sync>),
}

/// A trait marking something that can be evaluated to a specific value.
pub trait Evaluable<Out> {
    /// Compute the current represented expression.
    fn evaluate(&self) -> Result<Out, EvaluateError>;
}

/// A single, simple dice.
///
/// Use it if you want a lightweight dice that doesn't require parsing a string.
///
/// Evaluating the dice will result in a [u32] between 1 and the given sides (included).
pub struct Dice {
    /// Number of sides for the current dice.
    pub sides: u32,
}

impl From<u32> for Dice {
    fn from(sides: u32) -> Self {
        Dice { sides }
    }
}

impl Evaluable<u32> for Dice {
    #[mutants::skip] // This function is using a real random source, and cannot be tested correctly against consistent results
    fn evaluate(&self) -> Result<u32, EvaluateError> {
        let result = rand::random_range(1..self.sides + 1);
        Ok(result)
    }
}

/// Might be returned if an attempt to parse an invalid string to a DiceExpression.
#[derive(Debug, Error)]
pub enum DiceExpressionParsingError {
    /// The current string could not be parsed to a [DiceExpression]
    #[error("Could not parse '{parsed}: {err}'")]
    ExpressionParsingFailed {
        /// The invalid string that could not be parsed.
        parsed: String,
        err: ParserError,
    },
}

/// A dice expression parsed from a string.
///
/// Use the [FromStr] trait to parse a string expression to this struct.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
pub struct DiceExpression {
    expr: Expr,
}

impl Display for DiceExpression {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.expr)
    }
}

impl FromStr for DiceExpression {
    type Err = DiceExpressionParsingError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let expr = ast::Parser::parse(s).map_err(|err| {
            DiceExpressionParsingError::ExpressionParsingFailed {
                parsed: s.to_string(),
                err,
            }
        })?;
        Ok(DiceExpression { expr })
    }
}

impl Evaluable<i64> for DiceExpression {
    #[mutants::skip] // This function is using a real random source, and cannot be tested correctly against consistent results
    fn evaluate(&self) -> Result<i64, EvaluateError> {
        self.expr.evaluate(&mut RandCrateSource::new())
    }
}

impl DiceExpression {
    pub fn minimum(&self) -> Result<i64, EvaluateError> {
        self.expr.evaluate(&mut FixedSource {
            value: FixedValue::Minimum,
        })
    }

    pub fn maximum(&self) -> Result<i64, EvaluateError> {
        self.expr.evaluate(&mut FixedSource {
            value: FixedValue::Maximum,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_minimum() {
        let expr = DiceExpression::from_str("d6").unwrap();
        let minimum = expr.minimum().unwrap();
        assert_eq!(1, minimum);
    }

    #[test]
    fn test_minimum_addition() {
        let expr = DiceExpression::from_str("d6 + 2").unwrap();
        let minimum = expr.minimum().unwrap();
        assert_eq!(3, minimum);
    }

    #[test]
    fn test_maximum() {
        let expr = DiceExpression::from_str("d6").unwrap();
        let minimum = expr.maximum().unwrap();
        assert_eq!(6, minimum);
    }
}