dicelib/dice.rs
1//! Represents a Dice.
2
3use anyhow::Result;
4use rand::{prelude::Rng, RngCore};
5use regex::Regex;
6
7use crate::error::{Error, RollError};
8
9/// A dice roll expressed in RPG term e.g. 3d6 means "roll a 6-sided die 3 times".
10pub struct DiceRoll {
11 /// How many times the dice will be rolled.
12 pub rolls: u32,
13
14 /// The maximum number of sides the dice has. If it has 6 sides, the most it
15 /// can roll for at any one time is 6.
16 pub sides: u32,
17}
18
19impl DiceRoll {
20 /// All dice must be rollable at least once.
21 pub const MINIMUM_ROLLS: u32 = 1;
22
23 /// All dice must have at least 2 sides.
24 pub const MINIMUM_SIDES: u32 = 2;
25
26 /// Create a `DiceRoll`.
27 ///
28 /// # Errors
29 /// - `rolls` is less than 1
30 /// - `sides` is less than 2
31 pub fn new(rolls: u32, sides: u32) -> Result<Self> {
32 if rolls < Self::MINIMUM_ROLLS {
33 return Err(Error::InvalidRolls(rolls).into());
34 }
35
36 if sides < Self::MINIMUM_SIDES {
37 return Err(Error::InvalidSides(sides).into());
38 }
39
40 Ok(Self { rolls, sides })
41 }
42
43 /// Create a `DiceRoll` from a string.
44 /// ```
45 /// use dicelib::dice::DiceRoll;
46 /// let dice = DiceRoll::from_string(&"3d6".to_string()).unwrap();
47 /// ```
48 ///
49 /// # Errors
50 /// - See `parse_rolls_and_sides()`
51 // TODO: use &str instead of &String
52 pub fn from_string(string: &str) -> Result<Self> {
53 let (rolls, sides) = Self::parse_rolls_and_sides(string)?;
54 Self::new(rolls, sides)
55 }
56
57 /// Utility function to parse the rolls and sides of a dice roll string
58 /// into a pair of u32s. If you want a `DiceRoll`, use `from_string()` instead.
59 /// ```
60 /// use dicelib::dice::DiceRoll;
61 /// let (rolls, sides) = DiceRoll::parse_rolls_and_sides(&"1d4".to_string()).unwrap();
62 /// ```
63 ///
64 /// # Errors
65 /// - If rolls or sides cannot be matched (expression is malformed)
66 /// - If the matched rolls and sides are not parseable as `u32`
67 pub fn parse_rolls_and_sides(string: &str) -> Result<(u32, u32)> {
68 // parse into rolls and sides, with regex validation
69 lazy_static! {
70 static ref PATTERN: Regex = Regex::new(r"^(\d+)d(\d+)$").unwrap();
71 }
72
73 // Parse the captures as u32s.
74 let captures = PATTERN
75 .captures(string)
76 .ok_or_else(|| Error::InvalidExpression(string.to_string()))?;
77
78 // The error handling here is more of a formality because if we got this
79 // far, we probably matched two ints.
80 let rolls = captures
81 .get(1)
82 .ok_or_else(|| Error::FailedToParseRolls(string.to_string()))?
83 .as_str()
84 .parse::<u32>()?;
85 let sides = captures
86 .get(2)
87 .ok_or_else(|| Error::FailedToParseSides(string.to_string()))?
88 .as_str()
89 .parse::<u32>()?;
90
91 Ok((rolls, sides))
92 }
93
94 // TODO: this function is the hot path for very large numbers of rolls.
95 /// Performs the `DiceRoll` and returns the sum of all rolls.
96 ///
97 /// # Errors
98 /// - `IntegerOverFlow` if the rolls and sides are very, very big numbers.
99 pub fn roll(&self, rng: &mut impl RngCore) -> Result<u32> {
100 let mut result: u32 = 0;
101
102 // TODO: experiment with a bigint implementation, benchmark against native
103 // integers.
104 for _ in 0..self.rolls {
105 // TODO: benchmark this against unchecked +=
106 let roll = rng.gen_range(1, self.sides + 1);
107 result = result
108 .checked_add(roll)
109 .ok_or(RollError::IntegerOverFlow(result, roll))?;
110 }
111
112 Ok(result)
113 }
114}
115
116#[cfg(test)]
117mod dice_unit_tests {
118 use super::DiceRoll;
119
120 #[test]
121 fn dice_from_string() {
122 let _d = DiceRoll::from_string(&"1d6".to_string());
123 assert!(true);
124 }
125}