Skip to main content

dice_parser/
roller.rs

1//! All rng and rolling logic.
2
3use std::{
4    num::TryFromIntError,
5    ops::{Add, Neg, Sub},
6};
7
8use crate::{
9    ast::{DiceExpr, Keep, RollSpec},
10    error::DiceError,
11};
12use rand::{Rng, rngs::ThreadRng};
13
14/// An rng for rolling dice.
15///
16/// * `rng`: The rng base which is `rand::Rng`.
17pub struct Roller<R: Rng> {
18    rng: R,
19}
20
21impl Default for Roller<ThreadRng> {
22    fn default() -> Self {
23        Roller { rng: rand::rng() }
24    }
25}
26
27/// Representation of the result of rolling 0 or more dice. Note that a 0-sided dice is interpreted
28/// as a constant. See also `dice-parser::ast::DiceExpr::roll()`.
29///
30/// * `total`: The sum of the rolls
31/// * `detail`: A `RollDetail` containing the terms that made the `total`.
32#[derive(Debug, Clone)]
33pub struct RollResult {
34    pub total: u32,
35    pub detail: RollDetail,
36}
37impl RollResult {
38    pub fn new(total: u32, detail: RollDetail) -> Self {
39        RollResult { total, detail }
40    }
41}
42
43/// Represents the dice or constant of a `RollResult`.
44///
45/// * `Dice(Vec<u32>)`: The roll was based on dice, and the Vec<u32> are their individual rolls in
46///   the order they were rolled.
47/// * `Constant(i32)`: The roll was a constant value, and no rng was done. The i32 contains that
48///   value.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum RollDetail {
51    Dice(Vec<u32>),
52    Constant(i32),
53}
54
55/// The result of evaluating a dice expression.
56///
57/// This struct contains the final total, all individual die rolls (with their signs),
58/// and the sum of all constant modifiers in the expression.
59///
60/// # Fields
61///
62/// - `total`: The final result after all rolls and modifiers are applied
63/// - `rolls`: All individual die rolls. Rolls from subtracted expressions are negative.
64/// - `modifier`: The sum of all constant (non-dice) terms in the expression
65///
66/// # Examples
67///
68/// ```
69/// use dice_parser::DiceExpr;
70///
71/// let expr = DiceExpr::parse("2d6+3").unwrap();
72/// let result = expr.roll().unwrap();
73///
74/// // Access the components of the result
75/// println!("Total: {}", result.total);           // e.g., 10
76/// println!("Dice rolls: {:?}", result.rolls);    // e.g., [3, 4]
77/// println!("Modifier: {}", result.modifier);     // 3
78///
79/// // Total equals sum of rolls plus modifier
80/// assert_eq!(result.total, result.rolls.iter().sum::<i32>() + result.modifier);
81/// ```
82///
83/// ## Subtraction Example
84///
85/// ```
86/// use dice_parser::DiceExpr;
87///
88/// let expr = DiceExpr::parse("10 - 2d6").unwrap();
89/// let result = expr.roll().unwrap();
90///
91/// // Subtracted rolls are negative in the rolls vec
92/// assert_eq!(result.modifier, 10);
93/// assert_eq!(result.rolls.len(), 2);
94/// // Both rolls should be negative
95/// assert!(result.rolls[0] < 0 && result.rolls[1] < 0);
96/// ```
97#[derive(Debug, Clone)]
98pub struct ExprResult {
99    /// The total result of the expression.
100    pub total: i32,
101    /// All individual die rolls. Subtracted rolls are negative.
102    pub rolls: Vec<i32>,
103    /// The sum of all constant modifiers in the expression.
104    pub modifier: i32,
105}
106
107impl TryFrom<RollResult> for ExprResult {
108    type Error = DiceError;
109    fn try_from(val: RollResult) -> Result<Self, DiceError> {
110        let expr = ExprResult {
111            // Checked try making total signed
112            total: val.total.try_into()?,
113            rolls: match &val.detail {
114                RollDetail::Dice(d) => d
115                    .iter()
116                    .map(|&x| x.try_into())
117                    .collect::<Result<Vec<i32>, TryFromIntError>>()?,
118                RollDetail::Constant(_) => Vec::new(),
119            },
120            modifier: if let RollDetail::Constant(n) = &val.detail {
121                *n
122            } else {
123                0
124            },
125        };
126        Ok(expr)
127    }
128}
129
130impl Add<ExprResult> for ExprResult {
131    type Output = Self;
132    fn add(mut self, other: ExprResult) -> Self {
133        self.rolls.extend(other.rolls.iter());
134        self.modifier += other.modifier;
135        self.total += other.total;
136        self
137    }
138}
139
140impl Neg for ExprResult {
141    type Output = Self;
142    fn neg(mut self) -> Self {
143        // Negate each roll
144        self.rolls.iter_mut().for_each(|x| *x = -*x);
145        self.modifier = -self.modifier;
146        self.total = -self.total;
147        self
148    }
149}
150
151impl Sub<ExprResult> for ExprResult {
152    type Output = Self;
153    fn sub(self, other: ExprResult) -> Self {
154        self + (-other)
155    }
156}
157
158impl ExprResult {
159    pub fn new(total: i32, rolls: Vec<i32>, modifier: i32) -> Self {
160        ExprResult {
161            total,
162            rolls,
163            modifier,
164        }
165    }
166}
167
168impl<R: Rng> Roller<R> {
169    /// Instantiate a `Roller` from a custom `rand::Rng` object.
170    ///
171    /// * `rng`: The `rand::Rng` object to be used as the generator.
172    pub fn from_rng(rng: R) -> Self {
173        Roller { rng }
174    }
175
176    /// Roll a single die with the given number of sides.
177    ///
178    /// * `sides`: Number of sides on the dice.
179    ///
180    /// # Panics
181    /// Panics if `sides == 0`.
182    fn roll_die(&mut self, sides: u32) -> u32 {
183        debug_assert!(sides > 0);
184        self.rng.random_range(1..=sides)
185    }
186
187    /// Roll 1d100 correctly using 2d10, one for the ones and a percentile die for the tens.
188    fn roll_d100(&mut self) -> u32 {
189        let ones = self.rng.random_range(1..=10);
190        let tens = self.rng.random_range(0..=9);
191        if tens == 0 { ones } else { ones + tens * 10 }
192    }
193
194    /// Roll an arbitrary number of dice with the same number of sides.
195    ///
196    /// * `sides`: Number of sides.
197    /// * `count`: Number of dice.
198    ///
199    /// # Panics
200    /// Panics in debug if `sides == 0` or `count == 0`
201    fn roll_dice(&mut self, sides: u32, count: u32) -> Vec<u32> {
202        debug_assert!(sides > 0 && count > 0);
203        (0..count)
204            .map(|_| match sides {
205                100 => self.roll_d100(),
206                _ => self.roll_die(sides),
207            })
208            .collect()
209    }
210
211    /// Evaluate a `RollSpec`.
212    ///
213    /// * `spec`: the `dice-parser::ast::RollSpec` to be rolled.
214    ///
215    /// # Returns
216    /// Except the self-evident totals (where)
217    /// * `Constant(0)`: if `spec.count == 0`
218    /// * `Constant(spec.count)`: if `spec.sides == 0`
219    /// * `Dice(...)`: if `spec.count > 0 && spec.sides > 0` and contains the rolled dice.
220    ///
221    /// # Examples
222    pub fn roll_spec(&mut self, spec: &RollSpec) -> Result<RollResult, DiceError> {
223        if spec.count == 0 {
224            return Ok(RollResult::new(0, RollDetail::Constant(0)));
225        }
226
227        if spec.sides == 0 {
228            return Ok(RollResult::new(
229                spec.count,
230                RollDetail::Constant(spec.count as i32),
231            ));
232        }
233
234        let mut rolls = self.roll_dice(spec.sides, spec.count);
235        let rolled = RollDetail::Dice(rolls.clone());
236
237        if let Some(keep) = &spec.keep {
238            rolls.sort_unstable();
239            let total: u32 = match keep {
240                Keep::Highest(n) => {
241                    if *n as usize > rolls.len() {
242                        return Err(DiceError::InvalidSpec(
243                            spec.clone(),
244                            String::from("tried to keep more than total amount of rolled dice"),
245                        ));
246                    }
247                    rolls[(rolls.len() - *n as usize)..].iter().sum()
248                }
249                Keep::Lowest(n) => {
250                    if *n as usize > rolls.len() {
251                        return Err(DiceError::InvalidSpec(
252                            spec.clone(),
253                            String::from("tried to keep more than total amount of rolled dice"),
254                        ));
255                    }
256
257                    rolls[..*n as usize].iter().sum()
258                }
259            };
260
261            Ok(RollResult::new(total, rolled))
262        } else {
263            let total = rolls.iter().sum();
264            Ok(RollResult::new(total, rolled))
265        }
266    }
267
268    /// Evaluate a DiceExpr.
269    ///
270    /// * `expr`: The epression to be evaluated.
271    ///
272    /// # Returns
273    /// * `Ok(ExprResult)` if successful.
274    /// * `Err(DiceError)` if evaluation failed.
275    pub fn roll_expr(&mut self, expr: &DiceExpr) -> Result<ExprResult, DiceError> {
276        match expr {
277            DiceExpr::Sum(lhs, rhs) => Ok(self.roll_expr(lhs)? + self.roll_expr(rhs)?),
278            DiceExpr::Difference(lhs, rhs) => Ok(self.roll_expr(lhs)? - self.roll_expr(rhs)?),
279            DiceExpr::Roll(spec) => self.roll_spec(spec)?.try_into(),
280            DiceExpr::Literal(lit) => Ok(ExprResult {
281                total: *lit,
282                rolls: Vec::new(),
283                modifier: *lit,
284            }),
285        }
286    }
287}
288
289#[cfg(test)]
290mod test {
291    use super::*;
292    use rand::{self, SeedableRng, rngs::StdRng};
293
294    // ==== Tests for rolling ====
295
296    #[test]
297    fn test_roll() {
298        let mut roller = Roller::from_rng(StdRng::seed_from_u64(12));
299        let spec = RollSpec::new(2, 20, None);
300
301        let res = roller.roll_spec(&spec).unwrap();
302
303        assert_eq!(res.total, 10 + 9);
304        if let RollDetail::Dice(dice) = res.detail {
305            assert_eq!(dice, vec![9, 10])
306        }
307    }
308
309    #[test]
310    fn test_0_sides() {
311        let mut roller = Roller::from_rng(StdRng::seed_from_u64(42));
312        let spec = RollSpec::new(3, 0, None);
313        let res = roller.roll_spec(&spec).unwrap();
314        assert_eq!(res.total, 3);
315        assert_eq!(res.detail, RollDetail::Constant(3));
316    }
317
318    #[test]
319    fn test_0_count() {
320        let mut roller = Roller::from_rng(StdRng::seed_from_u64(36));
321        let spec = RollSpec::new(0, 12, None);
322
323        let res = roller.roll_spec(&spec).unwrap();
324
325        assert_eq!(res.total, 0);
326        assert_eq!(res.detail, RollDetail::Constant(0))
327    }
328
329    #[test]
330    fn test_keep_highest() {
331        // Keep Highest 1
332        let mut roller = Roller::from_rng(StdRng::seed_from_u64(12));
333
334        let spec = RollSpec::new(5, 20, Some(Keep::Highest(1)));
335        let res = roller.roll_spec(&spec).unwrap();
336
337        assert_eq!(res.total, 20);
338        if let RollDetail::Dice(d) = res.detail {
339            assert_eq!(d, vec![9, 10, 14, 12, 20])
340        } else {
341            panic!()
342        }
343
344        // Keep Highest 2
345        let mut roller = Roller::from_rng(StdRng::seed_from_u64(12));
346        let spec = RollSpec::new(5, 20, Some(Keep::Highest(2)));
347        let res = roller.roll_spec(&spec).unwrap();
348
349        assert_eq!(res.total, 34);
350        if let RollDetail::Dice(d) = res.detail {
351            assert_eq!(d, vec![9, 10, 14, 12, 20])
352        } else {
353            panic!()
354        }
355    }
356
357    #[test]
358    fn test_keep_lowest() {
359        // Keep Lowest 1
360        let mut roller = Roller::from_rng(StdRng::seed_from_u64(12));
361
362        let spec = RollSpec::new(5, 20, Some(Keep::Lowest(1)));
363        let res = roller.roll_spec(&spec).unwrap();
364
365        assert_eq!(res.total, 9);
366        if let RollDetail::Dice(d) = res.detail {
367            assert_eq!(d, vec![9, 10, 14, 12, 20])
368        } else {
369            panic!()
370        }
371
372        // Keep Lowest 2
373        let mut roller = Roller::from_rng(StdRng::seed_from_u64(12));
374        let spec = RollSpec::new(5, 20, Some(Keep::Lowest(2)));
375        let res = roller.roll_spec(&spec).unwrap();
376
377        assert_eq!(res.total, 19);
378        if let RollDetail::Dice(d) = res.detail {
379            assert_eq!(d, vec![9, 10, 14, 12, 20])
380        } else {
381            panic!()
382        }
383    }
384
385    #[test]
386    fn test_keep_all_dice() {
387        // Keep highest/lowest equal to count (should keep all)
388        let mut roller = Roller::from_rng(StdRng::seed_from_u64(42));
389        let spec = RollSpec::new(4, 6, Some(Keep::Highest(4)));
390        let res = roller.roll_spec(&spec).unwrap();
391        if let RollDetail::Dice(d) = res.detail {
392            assert_eq!(d.len(), 4);
393        } else {
394            panic!("Expected Dice variant")
395        }
396        // Total should equal sum of all rolls
397    }
398
399    #[test]
400    fn test_keep_single_from_many() {
401        // Keep 1 from 10d20
402        let mut roller = Roller::from_rng(StdRng::seed_from_u64(42));
403        let spec = RollSpec::new(10, 20, Some(Keep::Highest(1)));
404        let res = roller.roll_spec(&spec).unwrap();
405        // Total should equal max roll
406        if let RollDetail::Dice(rolls) = &res.detail {
407            assert_eq!(rolls.len(), 10);
408            let max = rolls.iter().max().unwrap();
409            assert_eq!(res.total, *max);
410        }
411    }
412
413    #[test]
414    fn test_keep_too_many_high() {
415        let mut roller = Roller::from_rng(StdRng::seed_from_u64(42));
416        let spec = RollSpec::new(4, 20, Some(Keep::Highest(5)));
417        let res = roller.roll_spec(&spec);
418
419        match res {
420            Ok(_) => panic!("Expected Err variant"),
421            Err(e) => {
422                if !matches!(e, DiceError::InvalidSpec(_, _)) {
423                    panic!("expected `DiceError::InvdalidSpec` variant")
424                }
425            }
426        }
427    }
428
429    #[test]
430    fn test_keep_too_many_low() {
431        let mut roller = Roller::from_rng(StdRng::seed_from_u64(42));
432        let spec = RollSpec::new(4, 20, Some(Keep::Lowest(5)));
433        let res = roller.roll_spec(&spec);
434
435        match res {
436            Ok(_) => panic!("Expected Err variant"),
437            Err(e) => {
438                if !matches!(e, DiceError::InvalidSpec(_, _)) {
439                    panic!("expected `DiceError::InvdalidSpec` variant")
440                }
441            }
442        }
443    }
444
445    #[test]
446    fn test_keep_too_few_high() {
447        let mut roller = Roller::from_rng(StdRng::seed_from_u64(42));
448        let spec = RollSpec::new(6, 8, Some(Keep::Highest(0)));
449        let res = roller.roll_spec(&spec).unwrap();
450
451        assert_eq!(res.total, 0);
452        if let RollDetail::Dice(d) = &res.detail {
453            assert_eq!(d.len(), 6);
454        } else {
455            panic!("expected Dice variant")
456        }
457    }
458
459    #[test]
460    fn test_keep_too_few_low() {
461        let mut roller = Roller::from_rng(StdRng::seed_from_u64(42));
462        let spec = RollSpec::new(4, 12, Some(Keep::Lowest(0)));
463        let res = roller.roll_spec(&spec).unwrap();
464
465        assert_eq!(res.total, 0);
466        if let RollDetail::Dice(d) = &res.detail {
467            assert_eq!(d.len(), 4);
468        } else {
469            panic!("expected Dice variant")
470        }
471    }
472    #[test]
473    fn test_d100_range_validation() {
474        let mut roller = Roller::from_rng(StdRng::seed_from_u64(500));
475        let spec = RollSpec::new(1000000, 100, None);
476        let res = roller.roll_spec(&spec).unwrap();
477
478        if let RollDetail::Dice(rolls) = res.detail {
479            for roll in rolls {
480                assert!(
481                    (1_u32..=100_u32).contains(&roll),
482                    "D100 roll out of range: {}",
483                    roll
484                );
485            }
486        }
487    }
488    // ==== Tests for DiceExpr evaluation ====
489
490    #[test]
491    fn test_expr_literal() {
492        let mut roller = Roller::from_rng(StdRng::seed_from_u64(1));
493        let expr = DiceExpr::Literal(7);
494        let result = roller.roll_expr(&expr).unwrap();
495        assert_eq!(result.total, 7);
496        assert_eq!(result.rolls, vec![]);
497        assert_eq!(result.modifier, 7);
498    }
499
500    #[test]
501    fn test_expr_literal_negative() {
502        let mut roller = Roller::from_rng(StdRng::seed_from_u64(1));
503        let expr = DiceExpr::Literal(-15);
504        let result = roller.roll_expr(&expr).unwrap();
505        assert_eq!(result.total, -15);
506        assert_eq!(result.rolls, vec![]);
507        assert_eq!(result.modifier, -15);
508    }
509
510    #[test]
511    fn test_expr_literal_zero() {
512        let mut roller = Roller::from_rng(StdRng::seed_from_u64(1));
513        let expr = DiceExpr::Literal(0);
514        let result = roller.roll_expr(&expr).unwrap();
515        assert_eq!(result.total, 0);
516        assert_eq!(result.rolls, vec![]);
517        assert_eq!(result.modifier, 0);
518    }
519
520    #[test]
521    fn test_expr_roll_basic() {
522        let mut roller = Roller::from_rng(StdRng::seed_from_u64(42));
523        let expr = DiceExpr::Roll(RollSpec::new(2, 6, None));
524        let result = roller.roll_expr(&expr).unwrap();
525        assert_eq!(result.rolls.len(), 2);
526        assert_eq!(result.total, result.rolls.iter().sum());
527        assert_eq!(result.modifier, 0);
528        // All rolls should be in range [1, 6]
529        for roll in &result.rolls {
530            assert!(*roll >= 1 && *roll <= 6);
531        }
532    }
533
534    #[test]
535    fn test_expr_roll_d20() {
536        let mut roller = Roller::from_rng(StdRng::seed_from_u64(999));
537        let expr = DiceExpr::Roll(RollSpec::new(1, 20, None));
538        let result = roller.roll_expr(&expr).unwrap();
539        assert_eq!(result.rolls.len(), 1);
540        assert!(result.rolls[0] >= 1 && result.rolls[0] <= 20);
541        assert_eq!(result.total, result.rolls[0]);
542        assert_eq!(result.modifier, 0);
543    }
544
545    #[test]
546    fn test_expr_roll_multiple_d100() {
547        let mut roller = Roller::from_rng(StdRng::seed_from_u64(777));
548        let expr = DiceExpr::Roll(RollSpec::new(3, 100, None));
549        let result = roller.roll_expr(&expr).unwrap();
550        assert_eq!(result.rolls.len(), 3);
551        for roll in &result.rolls {
552            assert!(*roll >= 1 && *roll <= 100);
553        }
554        assert_eq!(result.total, result.rolls.iter().sum());
555    }
556
557    #[test]
558    fn test_expr_sum_literal_and_roll() {
559        let mut roller = Roller::from_rng(StdRng::seed_from_u64(100));
560        let left = DiceExpr::Roll(RollSpec::new(1, 20, None));
561        let right = DiceExpr::Literal(5);
562        let sum_expr = DiceExpr::Sum(Box::new(left), Box::new(right));
563        let result = roller.roll_expr(&sum_expr).unwrap();
564
565        // Result should be die roll + 5
566        assert_eq!(result.rolls.len(), 1);
567        assert_eq!(result.modifier, 5);
568        assert_eq!(result.total, result.rolls[0] + 5);
569    }
570
571    #[test]
572    fn test_expr_sum_multiple_literals() {
573        let mut roller = Roller::from_rng(StdRng::seed_from_u64(1));
574        let left = DiceExpr::Literal(10);
575        let right = DiceExpr::Literal(20);
576        let sum_expr = DiceExpr::Sum(Box::new(left), Box::new(right));
577        let result = roller.roll_expr(&sum_expr).unwrap();
578
579        assert_eq!(result.total, 30);
580        assert_eq!(result.rolls, vec![]);
581        assert_eq!(result.modifier, 30);
582    }
583
584    #[test]
585    fn test_expr_sum_multiple_rolls() {
586        let mut roller = Roller::from_rng(StdRng::seed_from_u64(333));
587        let left = DiceExpr::Roll(RollSpec::new(2, 6, None));
588        let right = DiceExpr::Roll(RollSpec::new(1, 8, None));
589        let sum_expr = DiceExpr::Sum(Box::new(left), Box::new(right));
590        let result = roller.roll_expr(&sum_expr).unwrap();
591
592        // Should have 3 total rolls (2 d6 + 1 d8)
593        assert_eq!(result.rolls.len(), 3);
594        assert_eq!(result.modifier, 0);
595        assert_eq!(result.total, result.rolls.iter().sum());
596    }
597
598    #[test]
599    fn test_expr_difference_roll_minus_literal() {
600        let mut roller = Roller::from_rng(StdRng::seed_from_u64(200));
601        let left = DiceExpr::Roll(RollSpec::new(1, 20, None));
602        let right = DiceExpr::Literal(5);
603        let diff_expr = DiceExpr::Difference(Box::new(left), Box::new(right));
604        let result = roller.roll_expr(&diff_expr).unwrap();
605
606        // Result should be die roll - 5
607        assert_eq!(result.rolls.len(), 1);
608        assert_eq!(result.modifier, -5);
609        assert_eq!(result.total, result.rolls[0] - 5);
610    }
611
612    #[test]
613    fn test_expr_difference_literal_minus_roll() {
614        let mut roller = Roller::from_rng(StdRng::seed_from_u64(201));
615        let left = DiceExpr::Literal(10);
616        let right = DiceExpr::Roll(RollSpec::new(1, 6, None));
617        let diff_expr = DiceExpr::Difference(Box::new(left), Box::new(right));
618        let result = roller.roll_expr(&diff_expr).unwrap();
619
620        // Result should be 10 - die roll
621        assert_eq!(result.rolls.len(), 1);
622        assert!(result.rolls[0].abs() >= 1 && result.rolls[0].abs() <= 6);
623        assert_eq!(result.total, result.modifier + result.rolls[0]);
624        assert_eq!(result.modifier, 10);
625    }
626
627    #[test]
628    fn test_expr_difference_multiple_literals() {
629        let mut roller = Roller::from_rng(StdRng::seed_from_u64(1));
630        let left = DiceExpr::Literal(50);
631        let right = DiceExpr::Literal(20);
632        let diff_expr = DiceExpr::Difference(Box::new(left), Box::new(right));
633        let result = roller.roll_expr(&diff_expr).unwrap();
634
635        assert_eq!(result.total, 30);
636        assert_eq!(result.rolls, vec![]);
637        assert_eq!(result.modifier, 30);
638    }
639
640    #[test]
641    fn test_expr_nested_sum_difference() {
642        // Evaluate (3 + 1d8) - 1d8
643        let mut roller = Roller::from_rng(StdRng::seed_from_u64(555));
644        let left = DiceExpr::Roll(RollSpec::new(1, 8, None));
645        let inner_sum = DiceExpr::Sum(Box::new(DiceExpr::Literal(3)), Box::new(left.clone()));
646        let expr = DiceExpr::Difference(Box::new(inner_sum), Box::new(left));
647        let result = roller.roll_expr(&expr).unwrap();
648
649        // Get the values rolled
650        let mut ref_rng = StdRng::seed_from_u64(555);
651        let mut rolls: Vec<i32> = (1..=2).map(|_| ref_rng.random_range(1..=8)).collect();
652
653        rolls[1] = -rolls[1];
654
655        // The two d8 rolls cancel out, leaving 3
656        assert_eq!(result.total, 3 + rolls.iter().sum::<i32>());
657        assert_eq!(result.rolls, rolls);
658        assert_eq!(result.modifier, 3);
659        assert_eq!(result.rolls.len(), 2);
660    }
661
662    #[test]
663    fn test_expr_complex_nested() {
664        let mut roller = Roller::from_rng(StdRng::seed_from_u64(666));
665        // ((2d6 + 5) - 3) + 1d4
666        let d2d6 = DiceExpr::Roll(RollSpec::new(2, 6, None));
667        let sum1 = DiceExpr::Sum(Box::new(d2d6), Box::new(DiceExpr::Literal(5)));
668        let diff = DiceExpr::Difference(Box::new(sum1), Box::new(DiceExpr::Literal(3)));
669        let d1d4 = DiceExpr::Roll(RollSpec::new(1, 4, None));
670        let final_expr = DiceExpr::Sum(Box::new(diff), Box::new(d1d4));
671
672        let result = roller.roll_expr(&final_expr).unwrap();
673
674        // Should have 3 rolls: 2 d6 + 1 d4
675        assert_eq!(result.rolls.len(), 3);
676        // Modifier should be 5 - 3 = 2
677        assert_eq!(result.modifier, 2);
678        // Total should be sum of all rolls + modifier
679        assert_eq!(
680            result.total,
681            result.rolls.iter().sum::<i32>() + result.modifier
682        );
683    }
684
685    #[test]
686    fn test_expr_keep_highest() {
687        let mut roller = Roller::from_rng(StdRng::seed_from_u64(123));
688        let expr = DiceExpr::Roll(RollSpec::new(4, 10, Some(Keep::Highest(2))));
689        let result = roller.roll_expr(&expr).unwrap();
690
691        // Should have all 4 rolls recorded
692        assert_eq!(result.rolls.len(), 4);
693        assert_eq!(result.modifier, 0);
694
695        // Total should be the sum of the 2 highest rolls
696        let mut rolls_sorted = result.rolls.clone();
697        rolls_sorted.sort_unstable();
698        let expected_total = rolls_sorted[2] + rolls_sorted[3];
699        assert_eq!(result.total, expected_total);
700    }
701
702    #[test]
703    fn test_expr_keep_lowest() {
704        let mut roller = Roller::from_rng(StdRng::seed_from_u64(124));
705        let expr = DiceExpr::Roll(RollSpec::new(4, 10, Some(Keep::Lowest(2))));
706        let result = roller.roll_expr(&expr).unwrap();
707
708        // Should have all 4 rolls recorded
709        assert_eq!(result.rolls.len(), 4);
710        assert_eq!(result.modifier, 0);
711
712        // Total should be the sum of the 2 lowest rolls
713        let mut rolls_sorted = result.rolls.clone();
714        rolls_sorted.sort_unstable();
715        let expected_total = rolls_sorted[0] + rolls_sorted[1];
716        assert_eq!(result.total, expected_total);
717    }
718
719    #[test]
720    fn test_expr_keep_highest_with_sum() {
721        let mut roller = Roller::from_rng(StdRng::seed_from_u64(125));
722        let keep_roll = DiceExpr::Roll(RollSpec::new(3, 6, Some(Keep::Highest(1))));
723        let literal = DiceExpr::Literal(10);
724        let expr = DiceExpr::Sum(Box::new(keep_roll), Box::new(literal));
725        let result = roller.roll_expr(&expr).unwrap();
726
727        // Should have 3 rolls (all rolls are recorded)
728        assert_eq!(result.rolls.len(), 3);
729        assert_eq!(result.modifier, 10);
730
731        // The total should be the highest roll + 10
732        let mut rolls_sorted = result.rolls.clone();
733        rolls_sorted.sort_unstable();
734        let expected_total = rolls_sorted[2] + 10;
735        assert_eq!(result.total, expected_total);
736    }
737
738    #[test]
739    fn test_exprresult_add_preserves_order() {
740        let left = ExprResult::new(10, vec![3, 7], 0);
741        let right = ExprResult::new(15, vec![5, 10], 0);
742        let sum = left + right;
743
744        assert_eq!(sum.rolls.len(), 4);
745        assert_eq!(sum.total, 25);
746        assert_eq!(sum.modifier, 0);
747    }
748
749    #[test]
750    fn test_exprresult_negation_flips_signs() {
751        let expr = ExprResult::new(10, vec![3, 7], 5);
752        let neg = -expr;
753
754        assert_eq!(neg.total, -10);
755        assert_eq!(neg.rolls, vec![-3, -7]);
756        assert_eq!(neg.modifier, -5);
757    }
758
759    #[test]
760    fn test_exprresult_sub_uses_negation() {
761        let left = ExprResult::new(20, vec![10, 10], 0);
762        let right = ExprResult::new(5, vec![5], 0);
763        let diff = left - right;
764
765        assert_eq!(diff.total, 15);
766        assert_eq!(diff.rolls.len(), 3);
767        assert_eq!(diff.rolls, vec![10, 10, -5])
768    }
769
770    #[test]
771    fn test_deeply_nested_expressions() {
772        // Build a deeply nested expression: (((1d6 + 1) + 1) + 1) ...
773        let mut expr = DiceExpr::Roll(RollSpec::new(1, 6, None));
774        for _ in 0..10 {
775            expr = DiceExpr::Sum(Box::new(expr), Box::new(DiceExpr::Literal(1)));
776        }
777
778        let mut roller = Roller::from_rng(StdRng::seed_from_u64(42));
779        let result = roller.roll_expr(&expr).unwrap();
780
781        assert_eq!(result.modifier, 10);
782        assert_eq!(result.rolls.len(), 1);
783    }
784
785    // ==== Tests for RollResult to ExprResult conversion ====
786
787    #[test]
788    fn test_rollresult_to_exprresult_constant_positive() {
789        let rr = RollResult::new(42, RollDetail::Constant(42));
790        let expr_result = ExprResult::try_from(rr).unwrap();
791
792        assert_eq!(expr_result.total, 42);
793        assert_eq!(expr_result.rolls, vec![]);
794        assert_eq!(expr_result.modifier, 42);
795    }
796
797    #[test]
798    fn test_rollresult_to_exprresult_constant_negative() {
799        let rr = RollResult::new(5, RollDetail::Constant(-20));
800        let expr_result = ExprResult::try_from(rr).unwrap();
801
802        assert_eq!(expr_result.total, 5);
803        assert_eq!(expr_result.rolls, vec![]);
804        assert_eq!(expr_result.modifier, -20);
805    }
806
807    #[test]
808    fn test_rollresult_to_exprresult_constant_zero() {
809        let rr = RollResult::new(0, RollDetail::Constant(0));
810        let expr_result = ExprResult::try_from(rr).unwrap();
811
812        assert_eq!(expr_result.total, 0);
813        assert_eq!(expr_result.rolls, vec![]);
814        assert_eq!(expr_result.modifier, 0);
815    }
816
817    #[test]
818    fn test_rollresult_to_exprresult_dice_simple() {
819        let rr = RollResult::new(9, RollDetail::Dice(vec![4, 5]));
820        let expr_result = ExprResult::try_from(rr).unwrap();
821
822        assert_eq!(expr_result.total, 9);
823        assert_eq!(expr_result.rolls, vec![4, 5]);
824        assert_eq!(expr_result.modifier, 0);
825    }
826
827    #[test]
828    fn test_rollresult_to_exprresult_dice_multiple() {
829        let rr = RollResult::new(21, RollDetail::Dice(vec![3, 7, 5, 6]));
830        let expr_result = ExprResult::try_from(rr).unwrap();
831
832        assert_eq!(expr_result.total, 21);
833        assert_eq!(expr_result.rolls, vec![3, 7, 5, 6]);
834        assert_eq!(expr_result.modifier, 0);
835    }
836
837    #[test]
838    fn test_rollresult_to_exprresult_dice_single() {
839        let rr = RollResult::new(12, RollDetail::Dice(vec![12]));
840        let expr_result = ExprResult::try_from(rr).unwrap();
841
842        assert_eq!(expr_result.total, 12);
843        assert_eq!(expr_result.rolls, vec![12]);
844        assert_eq!(expr_result.modifier, 0);
845    }
846
847    #[test]
848    #[should_panic = "Overflow as expected"]
849    fn test_rollresult_to_exprresult_overflow() {
850        let rr = RollResult::new(u32::MAX, RollDetail::Dice(vec![u32::MAX]));
851        let result = ExprResult::try_from(rr);
852
853        assert!(result.is_err());
854        match result.unwrap_err() {
855            DiceError::Overflow(_) => panic!("Overflow as expected"),
856            _ => panic!("Expected Overflow Variant"),
857        }
858    }
859
860    #[test]
861    #[should_panic = "Overflow as expected"]
862    fn test_rollresult_to_exprresult_dice_with_overflow() {
863        let rr = RollResult::new(100, RollDetail::Dice(vec![u32::MAX - 1, 2]));
864        let result = ExprResult::try_from(rr);
865
866        assert!(result.is_err());
867        match result.unwrap_err() {
868            DiceError::Overflow(_) => panic!("Overflow as expected"),
869            _ => {
870                panic!("Expected Overflow variant.")
871            }
872        }
873    }
874
875    #[test]
876    fn test_rollresult_to_exprresult_empty_dice() {
877        let rr = RollResult::new(0, RollDetail::Dice(vec![]));
878        let expr_result = ExprResult::try_from(rr).unwrap();
879
880        assert_eq!(expr_result.total, 0);
881        assert_eq!(expr_result.rolls, vec![]);
882        assert_eq!(expr_result.modifier, 0);
883    }
884
885    #[test]
886    fn test_rollresult_to_exprresult_large_valid_values() {
887        let rr = RollResult::new(1000, RollDetail::Dice(vec![200, 300, 500]));
888        let expr_result = ExprResult::try_from(rr).unwrap();
889
890        assert_eq!(expr_result.total, 1000);
891        assert_eq!(expr_result.rolls, vec![200, 300, 500]);
892        assert_eq!(expr_result.modifier, 0);
893    }
894
895    #[test]
896    fn test_rollresult_from_keep_highest_preserves_detail() {
897        let mut roller = Roller::from_rng(StdRng::seed_from_u64(777));
898        let spec = RollSpec::new(5, 12, Some(Keep::Highest(2)));
899        let rr = roller.roll_spec(&spec).unwrap();
900
901        // The detail should contain all 5 rolls
902        if let RollDetail::Dice(ref rolls) = rr.detail {
903            assert_eq!(rolls.len(), 5);
904        } else {
905            panic!("Expected Dice variant");
906        }
907
908        let expr_result = ExprResult::try_from(rr).unwrap();
909        // ExprResult should have all 5 rolls, even though total only includes 2
910        assert_eq!(expr_result.rolls.len(), 5);
911    }
912}