saikoro/lib.rs
1//! A parser and evaluator for dice notation expression
2//! # Basic Usage example
3//! ```rust
4//! # fn main() -> Result<(), saikoro::error::ParsingError> {
5//! // roll for fireball damage
6//! let damage = saikoro::evaluate("8d6")?;
7//! println!("Fireball deals {} fire damage", damage.value);
8//! # Ok(())}
9//! ```
10
11pub mod error;
12pub mod evaluation;
13pub mod operator;
14mod parsing;
15#[cfg(feature = "stats")]
16mod statistics;
17mod tokenization;
18
19use error::ParsingError;
20use evaluation::DiceEvaluation;
21use rand::{Rng, RngCore, SeedableRng};
22use std::ops::Range;
23use tokenization::TokenStream;
24
25/// Evaluates a string in format similar to [Standard Dice Notation](https://en.wikipedia.org/wiki/Dice_notation)
26/// evaluated with [`rand::thread_rng`]. Equivalent to [`eval_with_rand`] called with `&mut
27/// rand::thread_rng()` as the second parameter
28/// # Examples
29/// ```rust
30/// # fn main() -> Result<(), saikoro::error::ParsingError> {
31/// let evaluation = saikoro::evaluate("2d6")?;
32/// let final_value = evaluation.value;
33/// // the result of rolling 2d6 will be between 2 and 12
34/// assert!(final_value >= 2.0 && final_value <= 12.0);
35/// # Ok(())
36/// # }
37/// ```
38/// # Errors
39/// An error variant will be returned if the expression is unable to be parsed, or the evaluation
40/// function produces an error
41pub fn evaluate(input: &str) -> Result<DiceEvaluation, ParsingError>
42{
43 eval_with_rand(input, &mut rand::thread_rng())
44}
45
46/// A utility wrapper function for seeding a dice roll with the given u64 as the seed
47/// (see [`saikoro::eval_with_rand`][`eval_with_rand`] for more information)
48pub fn eval_with_seed(input: &str, seed: u64) -> Result<DiceEvaluation, ParsingError>
49{
50 let mut seeded_random = rand::rngs::StdRng::seed_from_u64(seed);
51 eval_with_rand(input, &mut seeded_random)
52}
53
54/// Evaluates a string in format similar to [Standard Dice Notation](https://en.wikipedia.org/wiki/Dice_notation)
55/// evaluated with the given [`RangeRng`]
56/// # Examples
57/// ```rust
58/// # fn main() -> Result<(), saikoro::error::ParsingError>
59/// # {
60/// use rand::{rngs::StdRng, SeedableRng};
61///
62/// // this seed will generate a 4 and a 5 from the first two rolls
63/// let mut seeded = StdRng::seed_from_u64(2024);
64/// let evaluation = saikoro::eval_with_rand("2d6", &mut seeded)?;
65/// assert_eq!(evaluation.value, 9.0);
66/// # Ok(())
67/// # }
68/// ```
69/// # Errors
70/// An error variant will be returned if the expression is unable to be parsed, or the evaluation
71/// function produces an error
72/// #
73/// # Rng Notes
74/// Importantly, the evaluation function for dice rolls will generate a number in the range [0,
75/// faces) (note exclusive upper bound) and add 1 to the value, so if the [`RangeRng::rng_range`]
76/// function will always return the same value regardless of provided range, (eg. for testing
77/// purposes) the value produced from dice rolls may be 1 more than expected.
78/// # See Also
79/// For simply seeding a roll with a u64 seed, see [`saikoro::eval_with_seed`][`eval_with_seed`]
80/// [`saikoro::RangeRng`][`RangeRng`]
81pub fn eval_with_rand<R>(input: &str, rand: &mut R) -> Result<DiceEvaluation, ParsingError>
82where
83 R: RangeRng,
84{
85 evaluation::evaluate_tree(
86 parsing::parse_tree_from(&mut TokenStream::new(input))?,
87 rand,
88 )
89}
90
91/// A utility trait for allowing flexibility for testing or rigging saikoro's random number
92/// generation. All implementers of [`rand::RngCore`] (i.e. all RNGs from the [`rand`] therefore
93/// ones one is likely to use) get an implementation of this trait for free, so most will not need
94/// to implement this trait, but it is availale publicly for those who do
95/// # Examples
96/// ```rust
97/// # use saikoro::RangeRng;
98/// use std::collections::VecDeque;
99/// # use std::ops::Range;
100///
101/// struct RiggedRng
102/// {
103/// // we store the rigged numbers in a queue
104/// roll_queue: VecDeque<u32>
105/// }
106/// impl RangeRng for RiggedRng
107/// {
108/// // "generating a random number" is just popping the next number from the queue
109/// fn rng_range(&mut self, range: Range<u32>) -> u32
110/// {
111/// self.roll_queue.pop_front().expect("Queue was empty!")
112/// }
113/// }
114///
115/// fn main()
116/// {
117/// // the first three numbers produced from the generator should be 4, 1, 2
118/// let mut rigged_rng = RiggedRng {
119/// roll_queue: VecDeque::from([4, 1, 2])
120/// };
121/// assert_eq!(rigged_rng.rng_range(0..6), 4);
122/// assert_eq!(rigged_rng.rng_range(0..6), 1);
123/// assert_eq!(rigged_rng.rng_range(0..6), 2);
124/// }
125/// ```
126/// # Note to Implementers
127/// In practice, when used by [`saikoro::eval_with_rand`][`crate::eval_with_rand`],
128/// the [`rng_range`][`RangeRng::rng_range`] function will generate a number in the range [0, faces)
129/// (note exclusive upper bound) and add 1 to the value, so if the
130/// [`rng_range`][`RangeRng::rng_range`] function will always return the same value regardless of
131/// provided range, (eg. for testing purposes) the value produced from dice rolls may be 1 more than
132/// expected.
133pub trait RangeRng
134{
135 /// Generates a random number within the bounds of the [`Range`]
136 fn rng_range(&mut self, range: Range<u32>) -> u32;
137}
138impl<T: RngCore> RangeRng for T
139{
140 #[doc(hidden)]
141 fn rng_range(&mut self, range: Range<u32>) -> u32
142 {
143 self.gen_range(range)
144 }
145}
146
147#[cfg(test)]
148pub(crate) mod test_helpers
149{
150 use crate::{eval_with_rand, RangeRng};
151 use std::collections::VecDeque;
152
153 pub struct RiggedRandom
154 {
155 pub roll_queue: VecDeque<u32>,
156 }
157 impl RiggedRandom
158 {
159 pub fn new<I>(values: I) -> Self
160 where
161 I: IntoIterator<Item = u32>,
162 {
163 Self {
164 roll_queue: values.into_iter().map(|it| it.saturating_sub(1)).collect(),
165 }
166 }
167
168 fn pop(&mut self) -> u32
169 {
170 self.roll_queue.pop_front().expect("roll queue empty!")
171 }
172 }
173 impl RangeRng for RiggedRandom
174 {
175 fn rng_range(&mut self, range: std::ops::Range<u32>) -> u32
176 {
177 self.pop().clamp(range.start, range.end - 1)
178 }
179 }
180
181 pub fn flip_result<T, E>(result: Result<T, E>) -> Result<E, T>
182 {
183 match result
184 {
185 Ok(ok) => Err(ok),
186 Err(err) => Ok(err),
187 }
188 }
189
190 macro_rules! assert_approx_eq {
191 ($left:expr, $right:expr) => {
192 match (&$left, &$right)
193 {
194 (left_val, right_val) =>
195 {
196 if !(f64::abs(*left_val - *right_val) < f64::EPSILON)
197 {
198 std::panic!("assertion that `left` approx equals `right` failed\nleft: {}\nright: {}", &*left_val, &*right_val);
199 }
200 }
201 }
202 };
203 ($left:expr, $right:expr, $error:expr) => {
204 match (&$left, &$right, &$error)
205 {
206 (left_val, right_val, error_val) =>
207 {
208 if !(f64::abs(*left_val - *right_val) < *error_val)
209 {
210 std::panic!("assertion that `left` approx equals `right` failed\nleft: {}\nright: {}\nmax error: {}", &*left_val, &*right_val, &*error_val);
211 }
212 }
213 }
214 }
215 }
216 pub(crate) use assert_approx_eq;
217
218 #[test]
219 fn rigged_random_test()
220 {
221 let rolls = eval_with_rand("3d6", &mut RiggedRandom::new([3, 5, 2])).unwrap();
222
223 assert_approx_eq!(rolls.value, 10.0);
224 assert_eq!(
225 vec![3, 5, 2],
226 rolls
227 .ungrouped_rolls()
228 .map(|it| it.original_value)
229 .collect::<Vec<_>>()
230 );
231 }
232}