dicelib/
lib.rs

1//! The RPG Dice Rust crate. A combination command line dice roller and library
2//! for evaluating dice roll expressions.
3
4// Enables a lot of annoying warnings to keep us honest.
5#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
6// Warnings we sincerely don't care about.
7#![allow(clippy::module_name_repetitions)]
8// Won't compile if there are missing docs.
9#![deny(missing_docs)]
10
11#[macro_use]
12extern crate lazy_static;
13
14pub mod dice;
15pub mod error;
16
17use anyhow::Result;
18use dice::DiceRoll;
19use evalexpr::eval;
20use rand::{rngs::SmallRng, SeedableRng};
21use regex::{Captures, Regex};
22use std::borrow::Cow;
23
24/// Solves a dice expression string by rolling each dice in-place and then
25/// evaluating the resulting arithmetic expression.
26///
27/// ```
28/// use dicelib::solve_dice_expression;
29/// let result = solve_dice_expression(&"2d5 + 4".to_string(), None).unwrap();
30/// assert!(result >= 6 && result <= 14);
31/// ```
32///
33/// # Errors
34/// - Integer overflow from huge dice rolls.
35///
36pub fn solve_dice_expression(expression: &str, random_seed: Option<u64>) -> Result<i64> {
37    lazy_static! {
38        static ref PATTERN: Regex = Regex::new(r"(\d+)d(\d+)").expect("Problem compiling regex");
39    }
40
41    // Initialize our RNG
42    let mut rng = match random_seed {
43        Some(inner) => SmallRng::seed_from_u64(inner),
44        None => SmallRng::from_entropy(),
45    };
46
47    // In order to bubble up errors from Regex::replace, we capture this Option
48    // to smuggle it out.
49    let mut error = None;
50
51    // For every match on the DiceRoll expression regex, roll it in-place.
52    let rolled_expression = PATTERN.replace_all(expression, |caps: &Captures| {
53        // FIXME: the unwrap here can cause a panic
54        let diceroll_str = &caps.get(0).unwrap().as_str().to_string();
55        match DiceRoll::from_string(diceroll_str) {
56            Ok(dice) => match dice.roll(&mut rng) {
57                Ok(roll_result) => Cow::Owned(format!("{}", roll_result)),
58                Err(e) => {
59                    error = Some(e.context(diceroll_str.clone()));
60                    Cow::Borrowed("")
61                }
62            },
63            Err(e) => {
64                error = Some(e.context(diceroll_str.clone()));
65                Cow::Borrowed("")
66            }
67        }
68    });
69
70    if let Some(e) = error {
71        Err(e)
72    } else {
73        // Calculate the result
74        let result = eval(&rolled_expression)?.as_int()?;
75        Ok(result)
76    }
77}