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}