Skip to main content

dice_parser/
ast.rs

1use std::str::FromStr;
2
3use rand::Rng;
4
5use crate::{
6    error::DiceError,
7    parser::Parser,
8    roller::{ExprResult, Roller},
9};
10
11/// A parsed dice expression that can be evaluated to produce a result.
12///
13/// This is the main type for working with dice expressions. It represents
14/// a tree structure of dice rolls, literals, and arithmetic operations.
15///
16/// # Variants
17///
18/// - `Sum`: Addition of two sub-expressions (e.g., "2d6 + 3")
19/// - `Difference`: Subtraction of two sub-expressions (e.g., "1d20 - 2")
20/// - `Roll`: A dice roll specification (e.g., "2d6")
21/// - `Literal`: A constant integer value (e.g., "5")
22///
23/// # Examples
24///
25/// ## Parsing from a string
26///
27/// ```
28/// use dice_parser::DiceExpr;
29///
30/// let expr = DiceExpr::parse("2d6+3").unwrap();
31/// let result = expr.roll().unwrap();
32/// assert!(result.total >= 5 && result.total <= 15); // 2-12 from dice + 3
33/// ```
34///
35/// ## Manual construction
36///
37/// ```
38/// use dice_parser::{DiceExpr, RollSpec};
39///
40/// // Create "1d20 + 5"
41/// let roll = DiceExpr::Roll(RollSpec::new(1, 20, None));
42/// let modifier = DiceExpr::Literal(5);
43/// let expr = DiceExpr::Sum(Box::new(roll), Box::new(modifier));
44///
45/// let result = expr.roll().unwrap();
46/// assert!(result.total >= 6 && result.total <= 25);
47/// ```
48#[derive(Debug, Clone)]
49pub enum DiceExpr {
50    /// Addition of two dice expressions.
51    Sum(Box<DiceExpr>, Box<DiceExpr>),
52    /// Subtraction of two dice expressions.
53    Difference(Box<DiceExpr>, Box<DiceExpr>),
54    /// A dice roll specification.
55    Roll(RollSpec),
56    /// A constant integer literal.
57    Literal(i32),
58}
59
60impl FromStr for DiceExpr {
61    type Err = DiceError;
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        let mut parser = Parser::new(s);
64        parser.parse()
65    }
66}
67
68impl DiceExpr {
69    /// Evaluate the dice expression using the default random number generator provided by the `rand` crate.
70    ///
71    /// This method rolls all dice in the expression and computes the final result,
72    /// including individual roll values and any modifiers.
73    ///
74    /// # Returns
75    ///
76    /// - `Ok(ExprResult)`: The result of evaluating the expression
77    /// - `Err(DiceError)`: If the roll specification is invalid or an overflow occurs
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use dice_parser::DiceExpr;
83    ///
84    /// let expr = DiceExpr::parse("2d6+3").unwrap();
85    /// let result = expr.roll().unwrap();
86    ///
87    /// // Result contains total, individual rolls, and modifier
88    /// assert!(result.total >= 5 && result.total <= 15);
89    /// assert_eq!(result.rolls.len(), 2); // Two d6 rolls
90    /// assert_eq!(result.modifier, 3);
91    /// ```
92    pub fn roll(&self) -> Result<ExprResult, DiceError> {
93        let mut roller = Roller::default();
94        roller.roll_expr(self)
95    }
96
97    /// Evaluate the dice expression using a custom random number generator.
98    ///
99    /// This method is useful for deterministic testing or when you want to
100    /// control the randomness source.
101    ///
102    /// # Parameters
103    ///
104    /// - `r`: Any type implementing the `rand::Rng` trait
105    ///
106    /// # Returns
107    ///
108    /// - `Ok(ExprResult)`: The result of evaluating the expression
109    /// - `Err(DiceError)`: If the roll specification is invalid or an overflow occurs
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use dice_parser::DiceExpr;
115    /// use rand::{SeedableRng, rngs::StdRng};
116    ///
117    /// let expr = DiceExpr::parse("1d20").unwrap();
118    ///
119    /// // Use a seeded RNG for deterministic results
120    /// let rng = StdRng::seed_from_u64(42);
121    /// let result = expr.roll_with_rng(rng).unwrap();
122    /// assert!(result.total >= 1 && result.total <= 20);
123    /// ```
124    pub fn roll_with_rng<T: Rng>(&self, r: T) -> Result<ExprResult, DiceError> {
125        let mut roller = Roller::from_rng(r);
126        roller.roll_expr(self)
127    }
128
129    /// Parse a dice expression from a string.
130    ///
131    /// This is the prefred way to create a `DiceExpr`. This is especially useful when parsin user input.
132    /// The parser supports a subset standard dice notation with addition and subtraction.
133    ///
134    /// # Supported Syntax
135    ///
136    /// - Dice rolls: `NdS` where N is the number of dice and S is the number of sides
137    /// - Literals: Any integer (positive or negative)
138    /// - Addition: `expr + expr`
139    /// - Subtraction: `expr - expr`
140    /// - Whitespace is ignored
141    ///
142    /// Note: Keep mechanics (e.g., "2d20kh" for keep highest, "6d6kl3" for keep lowest 3)
143    /// are planned for a future release. Currently, use manual construction with
144    /// `RollSpec` and `Keep` for keep functionality.
145    ///
146    /// # Parameters
147    ///
148    /// - `input`: A string slice containing the dice expression
149    ///
150    /// # Returns
151    ///
152    /// - `Ok(DiceExpr)`: The parsed expression
153    /// - `Err(DiceError)`: If the input is malformed or contains syntax errors
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use dice_parser::DiceExpr;
159    ///
160    /// // Simple dice roll
161    /// let expr = DiceExpr::parse("2d6").unwrap();
162    ///
163    /// // With addition
164    /// let expr = DiceExpr::parse("1d20 + 5").unwrap();
165    ///
166    /// // Complex expression
167    /// let expr = DiceExpr::parse("2d6 + 1d4 - 2").unwrap();
168    ///
169    /// // Negative dice count is not allowed
170    /// assert!(DiceExpr::parse("-2d6").is_err());
171    /// ```
172    pub fn parse(input: &str) -> Result<DiceExpr, DiceError> {
173        DiceExpr::from_str(input)
174    }
175}
176
177/// A specification for rolling one or more dice.
178///
179/// This struct defines how many dice to roll, how many sides each die has,
180/// and optionally which dice to keep (for advantage/disadvantage mechanics).
181///
182/// # Fields
183///
184/// - `count`: The number of dice to roll
185/// - `sides`: The number of sides on each die
186/// - `keep`: Optional keep modifier (keep highest N or lowest N)
187///
188/// # Examples
189///
190/// ## Basic dice roll
191///
192/// ```
193/// use dice_parser::RollSpec;
194///
195/// // Roll 2 six-sided dice
196/// let spec = RollSpec::new(2, 6, None);
197/// ```
198///
199/// ## Keep highest (advantage)
200///
201/// ```
202/// use dice_parser::{RollSpec, Keep};
203///
204/// // Roll 4d6, keep highest 3 (common for D&D ability scores)
205/// let spec = RollSpec::new(4, 6, Some(Keep::Highest(3)));
206/// ```
207///
208/// ## Keep lowest (disadvantage)
209///
210/// ```
211/// use dice_parser::{RollSpec, Keep};
212///
213/// // Roll 2d20, keep lowest 1 (disadvantage in D&D)
214/// let spec = RollSpec::new(2, 20, Some(Keep::Lowest(1)));
215/// ```
216#[derive(Debug, Clone)]
217pub struct RollSpec {
218    /// The number of dice to roll.
219    pub count: u32,
220    /// The number of sides on each die.
221    pub sides: u32,
222    /// Optional modifier to keep only highest or lowest N dice.
223    pub keep: Option<Keep>,
224}
225
226impl RollSpec {
227    /// Create a new roll specification.
228    ///
229    /// # Parameters
230    ///
231    /// - `count`: The number of dice to roll
232    /// - `sides`: The number of sides on each die
233    /// - `keep`: Optional keep modifier
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// use dice_parser::{RollSpec, Keep};
239    ///
240    /// // Simple 2d6 roll
241    /// let spec = RollSpec::new(2, 6, None);
242    ///
243    /// // 4d6 keep highest 3
244    /// let spec = RollSpec::new(4, 6, Some(Keep::Highest(3)));
245    /// ```
246    pub fn new(count: u32, sides: u32, keep: Option<Keep>) -> Self {
247        RollSpec { count, sides, keep }
248    }
249}
250
251/// Specifies which dice to keep from a roll.
252///
253/// This enum is used with `RollSpec` to implement advantage/disadvantage
254/// mechanics or other "keep best/worst N" scenarios.
255///
256/// # Variants
257///
258/// - `Highest(N)`: Keep the N highest dice from the roll
259/// - `Lowest(N)`: Keep the N lowest dice from the roll
260///
261/// # Examples
262///
263/// ```
264/// use dice_parser::{DiceExpr, RollSpec, Keep};
265///
266/// // D&D 5e advantage: roll 2d20, keep highest 1
267/// let advantage = RollSpec::new(2, 20, Some(Keep::Highest(1)));
268/// let expr = DiceExpr::Roll(advantage);
269///
270/// // D&D ability scores: roll 4d6, keep highest 3
271/// let ability_roll = RollSpec::new(4, 6, Some(Keep::Highest(3)));
272/// let expr = DiceExpr::Roll(ability_roll);
273///
274/// // Keep the lowest roll (disadvantage)
275/// let disadvantage = RollSpec::new(2, 20, Some(Keep::Lowest(1)));
276/// let expr = DiceExpr::Roll(disadvantage);
277/// ```
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub enum Keep {
280    /// Keep the N highest dice from the roll.
281    Highest(u32),
282    /// Keep the N lowest dice from the roll.
283    Lowest(u32),
284}