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}