pub mod modifier;
pub mod roller;
use alloc::{
borrow::Cow,
format,
string::{String, ToString},
vec::Vec,
};
use core::{cmp, fmt};
use self::modifier::Condition;
pub use self::{modifier::Modifier, roller::Roller};
use crate::expr::Describe;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[allow(clippy::exhaustive_structs)]
pub struct Dice {
pub count: u8,
pub sides: u8,
pub modifiers: Vec<Modifier>,
}
impl Dice {
#[must_use]
#[inline]
pub const fn plain(&self) -> Self {
Self::new(self.count, self.sides)
}
#[must_use]
pub const fn new(count: u8, sides: u8) -> Self {
Self {
count,
sides,
modifiers: Vec::new(),
}
}
#[must_use]
#[inline]
pub fn builder() -> Builder {
Builder::default()
}
}
impl Default for Dice {
#[inline]
fn default() -> Self {
Self::new(1, 20)
}
}
impl fmt::Display for Dice {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}d{}{}",
self.count,
self.sides,
self.modifiers.iter().map(ToString::to_string).collect::<String>()
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DieRoll {
pub val: u8,
pub added_by: Option<Modifier>,
pub dropped_by: Option<Modifier>,
pub changes: Vec<ValChange>,
}
impl DieRoll {
pub fn add(&mut self, from: Modifier) {
assert!(
self.added_by.is_none(),
"marking a die as added that has already been marked as added by another modifier"
);
self.added_by = Some(from);
}
pub fn drop(&mut self, from: Modifier) {
assert!(
self.dropped_by.is_none(),
"marking a die as dropped that has already been marked as dropped by another modifier"
);
self.dropped_by = Some(from);
}
pub fn change(&mut self, from: Modifier, new_val: u8) {
self.changes.push(ValChange {
before: self.val,
after: new_val,
cause: from,
});
self.val = new_val;
}
#[must_use]
#[inline]
pub const fn is_original(&self) -> bool {
self.added_by.is_none()
}
#[must_use]
#[inline]
pub const fn is_additional(&self) -> bool {
self.added_by.is_some()
}
#[must_use]
#[inline]
pub const fn is_dropped(&self) -> bool {
self.dropped_by.is_some()
}
#[must_use]
#[inline]
pub const fn is_kept(&self) -> bool {
self.dropped_by.is_none()
}
#[must_use]
#[inline]
pub fn is_changed(&self) -> bool {
!self.changes.is_empty()
}
#[must_use]
pub const fn new(val: u8) -> Self {
Self {
val,
added_by: None,
dropped_by: None,
changes: Vec::new(),
}
}
}
impl PartialOrd for DieRoll {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DieRoll {
fn cmp(&self, other: &Self) -> cmp::Ordering {
self.val.cmp(&other.val)
}
}
impl fmt::Display for DieRoll {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}{}{}",
self.val,
if self.is_changed() { " (m)" } else { "" },
if self.is_dropped() { " (d)" } else { "" }
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::exhaustive_structs)]
pub struct ValChange {
pub before: u8,
pub after: u8,
pub cause: Modifier,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(clippy::exhaustive_structs)]
pub struct Rolled<'a> {
pub rolls: Vec<DieRoll>,
pub dice: Cow<'a, Dice>,
}
impl Rolled<'_> {
pub fn total(&self) -> Result<u16, Error> {
let mut sum: u16 = 0;
for r in self.rolls.iter().filter(|roll| roll.is_kept()) {
sum = sum
.checked_add(u16::from(r.val))
.ok_or_else(|| Error::Overflow(self.clone().into_owned()))?;
}
Ok(sum)
}
#[must_use]
pub fn into_owned(self) -> Rolled<'static> {
Rolled {
rolls: self.rolls,
dice: Cow::Owned(self.dice.into_owned()),
}
}
#[must_use]
pub fn from_dice_and_rolls(dice: &Dice, rolls: impl IntoIterator<Item = u8>) -> Rolled {
Rolled {
rolls: rolls.into_iter().map(DieRoll::new).collect(),
dice: Cow::Borrowed(dice),
}
}
}
impl Describe for Rolled<'_> {
fn describe(&self, list_limit: Option<usize>) -> String {
let list_limit = list_limit.unwrap_or(usize::MAX);
let total_rolls = self.rolls.len();
let truncated_rolls = total_rolls.saturating_sub(list_limit);
format!(
"{}[{}{}]",
self.dice,
self.rolls
.iter()
.take(list_limit)
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", "),
if truncated_rolls > 0 {
format!(", {truncated_rolls} more...")
} else {
String::new()
}
)
}
}
impl fmt::Display for Rolled<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.describe(None))
}
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("integer overflow")]
Overflow(Rolled<'static>),
#[error("{0} would result in infinite rolls")]
InfiniteRolls(Dice),
#[error("unknown condition symbol: {0}")]
UnknownCondition(String),
}
#[derive(Debug, Clone, Default)]
pub struct Builder(Dice);
impl Builder {
#[must_use]
pub const fn count(mut self, count: u8) -> Self {
self.0.count = count;
self
}
#[must_use]
pub const fn sides(mut self, sides: u8) -> Self {
self.0.sides = sides;
self
}
#[must_use]
pub fn reroll(mut self, cond: Condition, recurse: bool) -> Self {
self.0.modifiers.push(Modifier::Reroll { cond, recurse });
self
}
#[must_use]
pub fn explode(mut self, cond: Option<Condition>, recurse: bool) -> Self {
self.0.modifiers.push(Modifier::Explode { cond, recurse });
self
}
#[must_use]
pub fn keep_high(mut self, count: u8) -> Self {
self.0.modifiers.push(Modifier::KeepHigh(count));
self
}
#[must_use]
pub fn keep_low(mut self, count: u8) -> Self {
self.0.modifiers.push(Modifier::KeepLow(count));
self
}
#[must_use]
pub fn min(mut self, min: u8) -> Self {
self.0.modifiers.push(Modifier::Min(min));
self
}
#[must_use]
pub fn max(mut self, max: u8) -> Self {
self.0.modifiers.push(Modifier::Max(max));
self
}
#[must_use]
pub fn build(self) -> Dice {
self.0
}
}