tyche/dice/mod.rs
1//! All functionality for directly creating dice, rolling them, and working with their resulting rolls.
2//!
3//! This is the home of the dice "primitives". For using as part of a larger expression, see [`Expr::dice`].
4//!
5//! [`Expr::dice`]: crate::expr::Expr::Dice
6
7pub mod modifier;
8pub mod roller;
9
10use alloc::{
11 borrow::Cow,
12 format,
13 string::{String, ToString},
14 vec::Vec,
15};
16use core::{cmp, fmt};
17
18use self::modifier::Condition;
19pub use self::{modifier::Modifier, roller::Roller};
20use crate::expr::Describe;
21
22/// A set of one or more rollable dice with a specific number of sides, along with a collection of modifiers to apply to
23/// any resulting rolls from them.
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25#[allow(clippy::exhaustive_structs)]
26pub struct Dice {
27 /// Number of dice to roll
28 pub count: u8,
29
30 /// Number of sides for each die
31 pub sides: u8,
32
33 /// Modifiers to automatically apply to rolls from this set of dice
34 pub modifiers: Vec<Modifier>,
35}
36
37impl Dice {
38 /// Creates a new set of dice matching this one but without any modifiers.
39 #[must_use]
40 #[inline]
41 pub const fn plain(&self) -> Self {
42 Self::new(self.count, self.sides)
43 }
44
45 /// Creates a new set of dice with a given count and number of sides.
46 #[must_use]
47 pub const fn new(count: u8, sides: u8) -> Self {
48 Self {
49 count,
50 sides,
51 modifiers: Vec::new(),
52 }
53 }
54
55 /// Creates a new dice builder.
56 #[must_use]
57 #[inline]
58 pub fn builder() -> Builder {
59 Builder::default()
60 }
61}
62
63impl Default for Dice {
64 /// Creates the default dice (1d20).
65 #[inline]
66 fn default() -> Self {
67 Self::new(1, 20)
68 }
69}
70
71impl fmt::Display for Dice {
72 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
73 write!(
74 f,
75 "{}d{}{}",
76 self.count,
77 self.sides,
78 self.modifiers.iter().map(ToString::to_string).collect::<String>()
79 )
80 }
81}
82
83/// Single die produced from rolling [`Dice`] and optionally applying [`Modifier`]s
84#[derive(Debug, Clone, PartialEq, Eq)]
85#[non_exhaustive]
86pub struct DieRoll {
87 /// Value that was rolled
88 pub val: u8,
89
90 /// Modifier that caused the addition of this die, if any
91 pub added_by: Option<Modifier>,
92
93 /// Modifier that caused the drop of this die, if any
94 pub dropped_by: Option<Modifier>,
95
96 /// Modifications that were made to the value of the roll
97 pub changes: Vec<ValChange>,
98}
99
100impl DieRoll {
101 /// Marks this die roll as added by a given modifier, setting [`Self::added_by`].
102 ///
103 /// # Panics
104 /// Panics if `Self::added_by` is already [`Some`].
105 pub fn add(&mut self, from: Modifier) {
106 assert!(
107 self.added_by.is_none(),
108 "marking a die as added that has already been marked as added by another modifier"
109 );
110 self.added_by = Some(from);
111 }
112
113 /// Marks this die roll as dropped by a given modifier, setting [`Self::dropped_by`].
114 ///
115 /// # Panics
116 /// Panics if `Self::dropped_by` is already [`Some`].
117 pub fn drop(&mut self, from: Modifier) {
118 assert!(
119 self.dropped_by.is_none(),
120 "marking a die as dropped that has already been marked as dropped by another modifier"
121 );
122 self.dropped_by = Some(from);
123 }
124
125 /// Replaces the die roll's value and logs the change made.
126 pub fn change(&mut self, from: Modifier, new_val: u8) {
127 self.changes.push(ValChange {
128 before: self.val,
129 after: new_val,
130 cause: from,
131 });
132 self.val = new_val;
133 }
134
135 /// Indicates whether this die roll was part of the original set (not added by a modifier).
136 #[must_use]
137 #[inline]
138 pub const fn is_original(&self) -> bool {
139 self.added_by.is_none()
140 }
141
142 /// Indicates whether this die roll was added as the result of a modifier being applied.
143 /// This is the direct inverse of [`DieRoll::is_original()`].
144 #[must_use]
145 #[inline]
146 pub const fn is_additional(&self) -> bool {
147 self.added_by.is_some()
148 }
149
150 /// Indicates whether this die roll has been dropped by a modifier.
151 #[must_use]
152 #[inline]
153 pub const fn is_dropped(&self) -> bool {
154 self.dropped_by.is_some()
155 }
156
157 /// Indicates whether this die roll is being kept (has *not* been dropped by a modifier).
158 /// This is the direct inverse of [`DieRoll::is_dropped()`].
159 #[must_use]
160 #[inline]
161 pub const fn is_kept(&self) -> bool {
162 self.dropped_by.is_none()
163 }
164
165 /// Indicates whether this die roll's value has been directly changed by a modifier.
166 #[must_use]
167 #[inline]
168 pub fn is_changed(&self) -> bool {
169 !self.changes.is_empty()
170 }
171
172 /// Creates a new die roll with the given value.
173 #[must_use]
174 pub const fn new(val: u8) -> Self {
175 Self {
176 val,
177 added_by: None,
178 dropped_by: None,
179 changes: Vec::new(),
180 }
181 }
182}
183
184impl PartialOrd for DieRoll {
185 fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
186 Some(self.cmp(other))
187 }
188}
189
190impl Ord for DieRoll {
191 fn cmp(&self, other: &Self) -> cmp::Ordering {
192 self.val.cmp(&other.val)
193 }
194}
195
196impl fmt::Display for DieRoll {
197 /// Formats the value using the given formatter. [Read more][core::fmt::Debug::fmt()]
198 ///
199 /// The format of a die roll is simply the plain numeric value of the roll.
200 /// If the roll was dropped, it is appended with ` (d)`.
201 ///
202 /// # Examples
203 /// ```
204 /// use tyche::dice::DieRoll;
205 ///
206 /// let roll = DieRoll::new(4);
207 /// assert_eq!(roll.to_string(), "4");
208 /// ```
209 ///
210 /// ```
211 /// use tyche::dice::{DieRoll, Modifier};
212 ///
213 /// let mut roll = DieRoll::new(16);
214 /// let kh_mod = Modifier::KeepHigh(1);
215 /// roll.drop(kh_mod);
216 /// assert_eq!(roll.to_string(), "16 (d)");
217 /// ```
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 write!(
220 f,
221 "{}{}{}",
222 self.val,
223 if self.is_changed() { " (m)" } else { "" },
224 if self.is_dropped() { " (d)" } else { "" }
225 )
226 }
227}
228
229/// Details about a modification made to a [`DieRoll`] as a result of a [`Modifier`] being applied to it
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231#[allow(clippy::exhaustive_structs)]
232pub struct ValChange {
233 /// Roll value before the change was made
234 pub before: u8,
235
236 /// Roll value after the change was made
237 pub after: u8,
238
239 /// Modifier that caused the change
240 pub cause: Modifier,
241}
242
243/// Representation of the result from rolling [`Dice`]
244#[derive(Debug, Clone, PartialEq, Eq)]
245#[allow(clippy::exhaustive_structs)]
246pub struct Rolled<'a> {
247 /// Each individual die roll that was made
248 pub rolls: Vec<DieRoll>,
249
250 /// Dice that were rolled to produce this
251 pub dice: Cow<'a, Dice>,
252}
253
254impl Rolled<'_> {
255 /// Calculates the total of all roll values.
256 ///
257 /// # Errors
258 /// If there is an integer overflow while summing the die rolls, an error variant is returned.
259 ///
260 /// # Examples
261 /// ```
262 /// use tyche::dice::{roller::{FastRand as FastRandRoller, Roller}, Dice};
263 ///
264 /// let dice = Dice::new(4, 8);
265 /// let rolled = FastRandRoller::default().roll(&dice, true)?;
266 /// let total = rolled.total()?;
267 /// assert_eq!(total, rolled.rolls.iter().map(|roll| roll.val as u16).sum());
268 /// # Ok::<(), tyche::dice::Error>(())
269 /// ```
270 pub fn total(&self) -> Result<u16, Error> {
271 let mut sum: u16 = 0;
272
273 // Sum all rolls that haven't been dropped
274 for r in self.rolls.iter().filter(|roll| roll.is_kept()) {
275 sum = sum
276 .checked_add(u16::from(r.val))
277 .ok_or_else(|| Error::Overflow(self.clone().into_owned()))?;
278 }
279
280 Ok(sum)
281 }
282
283 /// Moves all of self's owned data into a new instance and clones any unowned data in order to create a `'static`
284 /// instance of self.
285 #[must_use]
286 pub fn into_owned(self) -> Rolled<'static> {
287 Rolled {
288 rolls: self.rolls,
289 dice: Cow::Owned(self.dice.into_owned()),
290 }
291 }
292
293 /// Creates a new rolled set of dice from a given set of dice and an iterator of values.
294 #[must_use]
295 pub fn from_dice_and_rolls(dice: &Dice, rolls: impl IntoIterator<Item = u8>) -> Rolled {
296 Rolled {
297 rolls: rolls.into_iter().map(DieRoll::new).collect(),
298 dice: Cow::Borrowed(dice),
299 }
300 }
301}
302
303impl Describe for Rolled<'_> {
304 /// Builds a string of the dice the roll is from and a list of all of the individual rolled dice
305 /// (see [`DieRoll::fmt()`]).
306 ///
307 /// If `list_limit` is specified and there are more rolls than it, the list of rolled dice will be truncated and
308 /// appended with "X more..." (where X is the remaining roll count past the max).
309 ///
310 /// # Examples
311 /// ```
312 /// use std::borrow::Cow;
313 /// use tyche::{dice::{Dice, DieRoll, Rolled}, expr::Describe};
314 ///
315 /// let dice = Dice::builder().count(4).sides(6).keep_high(2).build();
316 /// let kh_mod = dice.modifiers[0];
317 /// let rolled = Rolled {
318 /// rolls: vec![
319 /// DieRoll::new(6),
320 /// {
321 /// let mut roll = DieRoll::new(2);
322 /// roll.drop(kh_mod);
323 /// roll
324 /// },
325 /// DieRoll::new(5),
326 /// {
327 /// let mut roll = DieRoll::new(3);
328 /// roll.drop(kh_mod);
329 /// roll
330 /// },
331 /// ],
332 /// dice: Cow::Borrowed(&dice),
333 /// };
334 ///
335 /// assert_eq!(rolled.describe(None), "4d6kh2[6, 2 (d), 5, 3 (d)]");
336 /// assert_eq!(rolled.describe(Some(2)), "4d6kh2[6, 2 (d), 2 more...]");
337 /// ```
338 ///
339 /// [`DieRoll::fmt()`]: ./struct.DieRoll.html#method.fmt
340 fn describe(&self, list_limit: Option<usize>) -> String {
341 let list_limit = list_limit.unwrap_or(usize::MAX);
342 let total_rolls = self.rolls.len();
343 let truncated_rolls = total_rolls.saturating_sub(list_limit);
344
345 format!(
346 "{}[{}{}]",
347 self.dice,
348 self.rolls
349 .iter()
350 .take(list_limit)
351 .map(ToString::to_string)
352 .collect::<Vec<_>>()
353 .join(", "),
354 if truncated_rolls > 0 {
355 format!(", {truncated_rolls} more...")
356 } else {
357 String::new()
358 }
359 )
360 }
361}
362
363impl fmt::Display for Rolled<'_> {
364 /// Formats the value using the given formatter. [Read more][core::fmt::Debug::fmt()]
365 ///
366 /// The output is equivalent to calling [`Self::describe(None)`].
367 ///
368 /// [`Self::describe(None)`]: Self::describe()
369 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
370 write!(f, "{}", self.describe(None))
371 }
372}
373
374/// An error resulting from a dice operation
375#[derive(thiserror::Error, Debug)]
376#[non_exhaustive]
377pub enum Error {
378 /// There was an integer overflow when performing mathematical operations on roll values.
379 /// This normally should not ever happen given the types used for die counts, sides, and totals.
380 #[error("integer overflow")]
381 Overflow(Rolled<'static>),
382
383 /// Rolling the dice specified would result in infinite rolls.
384 ///
385 /// # Examples
386 /// ```
387 /// use tyche::dice::{roller::{FastRand as FastRandRoller, Roller}, Dice, Error};
388 ///
389 /// let dice = Dice::builder().count(4).sides(1).explode(None, true).build();
390 /// assert!(matches!(FastRandRoller::default().roll(&dice, true), Err(Error::InfiniteRolls(..))));
391 /// ```
392 #[error("{0} would result in infinite rolls")]
393 InfiniteRolls(Dice),
394
395 /// The provided symbol doesn't match to a known condition.
396 ///
397 /// # Examples
398 /// ```
399 /// use tyche::dice::{modifier::Condition, Error};
400 ///
401 /// let cond = Condition::from_symbol_and_val("!", 4);
402 /// assert!(matches!(cond, Err(Error::UnknownCondition(..))));
403 /// ```
404 #[error("unknown condition symbol: {0}")]
405 UnknownCondition(String),
406}
407
408/// Builds [`Dice`] with a fluent interface.
409///
410/// # Examples
411///
412/// ## Basic dice
413/// ```
414/// use tyche::Dice;
415///
416/// let dice = Dice::builder().count(2).sides(6).build();
417/// assert_eq!(dice, Dice::new(2, 6));
418/// ```
419///
420/// ## Single modifier
421/// ```
422/// use tyche::dice::{Dice, Modifier};
423///
424/// let dice = Dice::builder().count(6).sides(8).explode(None, true).build();
425/// assert_eq!(
426/// dice,
427/// Dice {
428/// count: 6,
429/// sides: 8,
430/// modifiers: vec![Modifier::Explode {
431/// cond: None,
432/// recurse: true,
433/// }],
434/// },
435/// );
436/// ```
437///
438/// ## Multiple modifiers
439/// ```
440/// use tyche::dice::{modifier::{Condition, Modifier}, Dice};
441///
442/// let dice = Dice::builder()
443/// .count(6)
444/// .sides(8)
445/// .reroll(Condition::Eq(1), false)
446/// .keep_high(4)
447/// .build();
448/// assert_eq!(
449/// dice,
450/// Dice {
451/// count: 6,
452/// sides: 8,
453/// modifiers: vec![
454/// Modifier::Reroll {
455/// cond: Condition::Eq(1),
456/// recurse: false
457/// },
458/// Modifier::KeepHigh(4),
459/// ],
460/// },
461/// );
462/// ```
463#[derive(Debug, Clone, Default)]
464pub struct Builder(Dice);
465
466impl Builder {
467 /// Sets the number of dice to roll.
468 #[must_use]
469 pub const fn count(mut self, count: u8) -> Self {
470 self.0.count = count;
471 self
472 }
473
474 /// Sets the number of sides per die.
475 #[must_use]
476 pub const fn sides(mut self, sides: u8) -> Self {
477 self.0.sides = sides;
478 self
479 }
480
481 /// Adds a reroll modifier to the dice.
482 #[must_use]
483 pub fn reroll(mut self, cond: Condition, recurse: bool) -> Self {
484 self.0.modifiers.push(Modifier::Reroll { cond, recurse });
485 self
486 }
487
488 /// Adds an exploding modifier to the dice.
489 #[must_use]
490 pub fn explode(mut self, cond: Option<Condition>, recurse: bool) -> Self {
491 self.0.modifiers.push(Modifier::Explode { cond, recurse });
492 self
493 }
494
495 /// Adds a keep highest modifier to the dice.
496 #[must_use]
497 pub fn keep_high(mut self, count: u8) -> Self {
498 self.0.modifiers.push(Modifier::KeepHigh(count));
499 self
500 }
501
502 /// Adds a keep lowest modifier to the dice.
503 #[must_use]
504 pub fn keep_low(mut self, count: u8) -> Self {
505 self.0.modifiers.push(Modifier::KeepLow(count));
506 self
507 }
508
509 /// Adds a minimum modifier to the dice.
510 #[must_use]
511 pub fn min(mut self, min: u8) -> Self {
512 self.0.modifiers.push(Modifier::Min(min));
513 self
514 }
515
516 /// Adds a maximum modifier to the dice.
517 #[must_use]
518 pub fn max(mut self, max: u8) -> Self {
519 self.0.modifiers.push(Modifier::Max(max));
520 self
521 }
522
523 /// Finalizes the dice.
524 #[must_use]
525 pub fn build(self) -> Dice {
526 self.0
527 }
528}