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}