tyche/dice/roller.rs
1//! Abstractions for rolling [`DieRoll`]s using various means.
2
3use alloc::{borrow::Cow, vec::Vec};
4use core::iter::Peekable;
5
6#[cfg(feature = "fastrand")]
7use fastrand::Rng;
8
9use super::{Dice, DieRoll, Error, Rolled};
10
11/// Rolls dice - what else is there to say?
12pub trait Roller {
13 /// Rolls a single die.
14 #[must_use]
15 fn roll_die(&mut self, sides: u8) -> DieRoll;
16
17 /// Rolls a set of dice and optionally applies all of its modifiers to the rolls.
18 ///
19 /// # Errors
20 /// If any errors are encountered while applying the dice's modifiers, an error variant is returned.
21 fn roll<'d, 'r>(&mut self, dice: &'d Dice, apply_mods: bool) -> Result<Rolled<'r>, Error>
22 where
23 'd: 'r,
24 Self: Sized,
25 {
26 // Roll the dice!
27 let mut rolls = Vec::with_capacity(dice.count as usize);
28 for _ in 0..dice.count {
29 rolls.push(self.roll_die(dice.sides));
30 }
31
32 let mut rolled = Rolled {
33 rolls,
34 dice: Cow::Borrowed(dice),
35 };
36
37 // Apply all of the dice's modifiers
38 if apply_mods {
39 for modifier in &dice.modifiers {
40 modifier.apply(&mut rolled, self)?;
41 }
42 }
43
44 Ok(rolled)
45 }
46}
47
48/// Generates rolls with random values using [fastrand]. Requires the `fastrand` feature (enabled by default).
49///
50/// # Examples
51///
52/// ## Default fastrand roller
53/// ```
54/// use tyche::dice::{roller::{FastRand as FastRandRoller, Roller}, Dice};
55///
56/// let mut roller = FastRandRoller::default();
57///
58/// let dice = Dice::new(4, 6);
59/// let _ = roller.roll(&dice, true)?;
60/// let _ = roller.roll(&dice, true)?;
61/// # Ok::<(), tyche::dice::Error>(())
62/// ```
63///
64/// ## Manually seeded fastrand roller
65/// ```
66/// use tyche::dice::{roller::{FastRand as FastRandRoller, Roller}, Dice};
67///
68/// let mut roller = FastRandRoller::with_seed(0x750c38d574400);
69///
70/// let dice = Dice::new(4, 6);
71/// let _ = roller.roll(&dice, true)?;
72/// let _ = roller.roll(&dice, true)?;
73/// # Ok::<(), tyche::dice::Error>(())
74/// ```
75///
76/// ## Custom fastrand roller
77/// ```
78/// use tyche::dice::{roller::{FastRand as FastRandRoller, Roller}, Dice};
79/// use fastrand::Rng;
80///
81/// let rng = Rng::with_seed(0x750c38d574400);
82/// let mut roller = FastRandRoller::new(rng);
83///
84/// let dice = Dice::new(4, 6);
85/// let _ = roller.roll(&dice, true)?;
86/// let _ = roller.roll(&dice, true)?;
87/// # Ok::<(), tyche::dice::Error>(())
88/// ```
89#[cfg(feature = "fastrand")]
90#[derive(Debug, Clone)]
91#[cfg_attr(feature = "std", derive(Default))]
92pub struct FastRand(Rng);
93
94#[cfg(feature = "fastrand")]
95impl FastRand {
96 /// Creates a new fastrand roller that uses the given RNG instance to generate rolls.
97 #[must_use]
98 #[inline]
99 pub const fn new(rng: Rng) -> Self {
100 Self(rng)
101 }
102
103 /// Creates a new fastrand roller that uses a pre-seeded RNG instance to generate rolls.
104 #[must_use]
105 #[inline]
106 pub fn with_seed(seed: u64) -> Self {
107 Self(Rng::with_seed(seed))
108 }
109}
110
111#[cfg(feature = "fastrand")]
112impl Roller for FastRand {
113 /// Rolls a single die using the [`fastrand::Rng`] the roller was created with.
114 #[inline]
115 fn roll_die(&mut self, sides: u8) -> DieRoll {
116 if sides > 0 {
117 DieRoll::new(self.0.u8(1..=sides))
118 } else {
119 DieRoll::new(0)
120 }
121 }
122}
123
124/// Generates rolls that always have a specific value.
125///
126/// # Examples
127/// ```
128/// use tyche::dice::{roller::{Roller, Val as ValRoller}, Dice};
129///
130/// let mut roller = ValRoller(42);
131///
132/// let dice = Dice::new(4, 6);
133/// let rolled = roller.roll(&dice, true)?;
134/// assert!(rolled.rolls.iter().all(|roll| roll.val == 42));
135///
136/// let dice = Dice::new(2, 20);
137/// let rolled = roller.roll(&dice, true)?;
138/// assert!(rolled.rolls.iter().all(|roll| roll.val == 42));
139/// # Ok::<(), tyche::dice::Error>(())
140/// ```
141#[derive(Debug, Default, Clone)]
142#[allow(clippy::exhaustive_structs)]
143pub struct Val(pub u8);
144
145impl Roller for Val {
146 /// Rolls a single die, always with one specific value.
147 #[inline]
148 fn roll_die(&mut self, _sides: u8) -> DieRoll {
149 DieRoll::new(self.0)
150 }
151}
152
153/// Generates rolls that always have their max value.
154///
155/// # Examples
156/// ```
157/// use tyche::dice::{roller::{Max as MaxRoller, Roller}, Dice};
158///
159/// let mut roller = MaxRoller;
160///
161/// let dice = Dice::new(4, 6);
162/// let rolled = roller.roll(&dice, true)?;
163/// assert!(rolled.rolls.iter().all(|roll| roll.val == 6));
164///
165/// let dice = Dice::new(2, 20);
166/// let rolled = roller.roll(&dice, true)?;
167/// assert!(rolled.rolls.iter().all(|roll| roll.val == 20));
168/// # Ok::<(), tyche::dice::Error>(())
169/// ```
170#[derive(Debug, Default, Clone)]
171#[allow(clippy::exhaustive_structs)]
172pub struct Max;
173
174impl Roller for Max {
175 /// Rolls a single die, always with the max value (same as the number of sides).
176 #[inline]
177 fn roll_die(&mut self, sides: u8) -> DieRoll {
178 DieRoll::new(sides)
179 }
180}
181
182/// Generates rolls from an iterator of values. Mainly useful for testing purposes.
183///
184/// # Examples
185/// ```
186/// use tyche::dice::{roller::{Iter as IterRoller, Roller}, Dice, DieRoll};
187///
188/// let mut roller = IterRoller::new(vec![1, 2, 3, 4, 10]);
189/// let dice = Dice::new(5, 6);
190/// assert_eq!(
191/// roller.roll(&dice, true)?.rolls,
192/// vec![DieRoll::new(1), DieRoll::new(2), DieRoll::new(3), DieRoll::new(4), DieRoll::new(10)]
193/// );
194/// # Ok::<(), tyche::dice::Error>(())
195/// ```
196#[derive(Debug, Clone)]
197pub struct Iter<I: Iterator<Item = u8>>(Peekable<I>);
198
199impl<I: Iterator<Item = u8>> Iter<I> {
200 /// Checks whether the iterator still has values available.
201 #[inline]
202 pub fn can_roll(&mut self) -> bool {
203 self.0.peek().is_some()
204 }
205
206 /// Creates a new roller that uses the given iterator to provide roll values.
207 #[must_use]
208 #[inline]
209 pub fn new(iter: impl IntoIterator<IntoIter = I>) -> Self {
210 Self(iter.into_iter().peekable())
211 }
212}
213
214impl<I: Iterator<Item = u8>> Roller for Iter<I> {
215 /// Rolls a die with the value from the next iteration.
216 ///
217 /// # Panics
218 /// If the iterator has finished, this will panic.
219 #[inline]
220 #[allow(clippy::expect_used)]
221 fn roll_die(&mut self, _sides: u8) -> DieRoll {
222 DieRoll::new(self.0.next().expect("iterator is finished"))
223 }
224}