use std::fmt;
use crate::dice::{roller::Roller, Dice, Error as DiceError, Rolled};
macro_rules! op_type_impl {
($name:ty) => {
impl HasOpType for $name {
fn op_type(&self) -> OpType {
match self {
Self::Num(..) | Self::Dice(..) => OpType::Value,
Self::Neg(..) => OpType::Unary,
Self::Add(..) | Self::Sub(..) => OpType::Additive,
Self::Mul(..) | Self::DivDown(..) | Self::DivUp(..) => OpType::Multiplicative,
}
}
fn is_value(&self) -> bool {
matches!(self, Self::Num(..) | Self::Dice(..))
}
fn is_unary(&self) -> bool {
matches!(self, Self::Neg(..))
}
fn is_additive(&self) -> bool {
matches!(self, Self::Add(..) | Self::Sub(..))
}
fn is_multiplicative(&self) -> bool {
matches!(self, Self::Mul(..) | Self::DivDown(..) | Self::DivUp(..))
}
}
};
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Expr {
Num(i32),
Dice(Dice),
Neg(Box<Self>),
Add(Box<Self>, Box<Self>),
Sub(Box<Self>, Box<Self>),
Mul(Box<Self>, Box<Self>),
DivDown(Box<Self>, Box<Self>),
DivUp(Box<Self>, Box<Self>),
}
op_type_impl!(Expr);
impl Expr {
pub fn eval(&self, rng: &mut impl Roller) -> Result<Evaled, EvalError> {
Ok(match self {
Self::Num(x) => Evaled::Num(*x),
Self::Dice(dice) => Evaled::Dice(rng.roll(dice, true).map_err(|err| EvalError::Dice(self.clone(), err))?),
Self::Neg(x) => Evaled::Neg(Box::new(x.eval(rng)?)),
Self::Add(a, b) => Evaled::Add(Box::new(a.eval(rng)?), Box::new(b.eval(rng)?)),
Self::Sub(a, b) => Evaled::Sub(Box::new(a.eval(rng)?), Box::new(b.eval(rng)?)),
Self::Mul(a, b) => Evaled::Mul(Box::new(a.eval(rng)?), Box::new(b.eval(rng)?)),
Self::DivDown(a, b) => Evaled::DivDown(Box::new(a.eval(rng)?), Box::new(b.eval(rng)?)),
Self::DivUp(a, b) => Evaled::DivUp(Box::new(a.eval(rng)?), Box::new(b.eval(rng)?)),
})
}
#[must_use]
pub fn is_deterministic(&self) -> bool {
match self {
Self::Num(..) => true,
Self::Dice(dice) => dice.sides == 1,
Self::Neg(x) => x.is_deterministic(),
Self::Add(a, b) | Self::Sub(a, b) | Self::Mul(a, b) | Self::DivDown(a, b) | Self::DivUp(a, b) => {
a.is_deterministic() && b.is_deterministic()
}
}
}
}
impl Describe for Expr {
fn describe(&self, _list_limit: Option<usize>) -> String {
match self {
Self::Num(x) => x.to_string(),
Self::Dice(dice) => dice.to_string(),
Self::Neg(x) => match x.as_ref() {
Self::Num(..) | Self::Dice(..) => format!("-{}", x.describe(None)),
_ => format!("-({})", x.describe(None)),
},
Self::Add(a, b) => self.describe_binary_expr('+', a.as_ref(), b.as_ref(), None),
Self::Sub(a, b) => self.describe_binary_expr('-', a.as_ref(), b.as_ref(), None),
Self::Mul(a, b) => self.describe_binary_expr('*', a.as_ref(), b.as_ref(), None),
Self::DivDown(a, b) => self.describe_binary_expr('/', a.as_ref(), b.as_ref(), None),
Self::DivUp(a, b) => self.describe_binary_expr('\\', a.as_ref(), b.as_ref(), None),
}
}
}
impl fmt::Display for Expr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.describe(None))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Evaled<'a> {
Num(i32),
Dice(Rolled<'a>),
Neg(Box<Self>),
Add(Box<Self>, Box<Self>),
Sub(Box<Self>, Box<Self>),
Mul(Box<Self>, Box<Self>),
DivDown(Box<Self>, Box<Self>),
DivUp(Box<Self>, Box<Self>),
}
op_type_impl!(Evaled<'_>);
impl Evaled<'_> {
pub fn calc(&self) -> Result<i32, CalcError> {
match self {
Self::Num(x) => Ok(*x),
Self::Dice(rolled) => Ok(rolled
.total()
.map_err(|err| CalcError::Dice(self.clone().into_owned(), err))?
.into()),
Self::Neg(x) => Ok(x
.calc()?
.checked_neg()
.ok_or_else(|| CalcError::Overflow(self.clone().into_owned()))?),
Self::Add(a, b) => a
.calc()?
.checked_add(b.calc()?)
.ok_or_else(|| CalcError::Overflow(self.clone().into_owned())),
Self::Sub(a, b) => a
.calc()?
.checked_sub(b.calc()?)
.ok_or_else(|| CalcError::Overflow(self.clone().into_owned())),
Self::Mul(a, b) => a
.calc()?
.checked_mul(b.calc()?)
.ok_or_else(|| CalcError::Overflow(self.clone().into_owned())),
Self::DivDown(a, b) => a
.calc()?
.checked_div(b.calc()?)
.ok_or_else(|| CalcError::Division(self.clone().into_owned())),
Self::DivUp(a, b) => {
let a_val = a.calc()?;
let b_val = b.calc()?;
let result = a_val
.checked_div(b_val)
.ok_or_else(|| CalcError::Division(self.clone().into_owned()))?;
let remainder = a_val
.checked_rem(b_val)
.ok_or_else(|| CalcError::Division(self.clone().into_owned()))?;
if remainder != 0 {
Ok(result
.checked_add(1)
.ok_or_else(|| CalcError::Overflow(self.clone().into_owned()))?)
} else {
Ok(result)
}
}
}
}
#[must_use]
pub fn into_owned(self) -> Evaled<'static> {
match self {
Self::Num(x) => Evaled::Num(x),
Self::Dice(rolled) => Evaled::Dice(rolled.into_owned()),
Self::Neg(x) => Evaled::Neg(Box::new(x.into_owned())),
Self::Add(a, b) => Evaled::Add(Box::new(a.into_owned()), Box::new(b.into_owned())),
Self::Sub(a, b) => Evaled::Sub(Box::new(a.into_owned()), Box::new(b.into_owned())),
Self::Mul(a, b) => Evaled::Mul(Box::new(a.into_owned()), Box::new(b.into_owned())),
Self::DivDown(a, b) => Evaled::DivDown(Box::new(a.into_owned()), Box::new(b.into_owned())),
Self::DivUp(a, b) => Evaled::DivUp(Box::new(a.into_owned()), Box::new(b.into_owned())),
}
}
}
impl Describe for Evaled<'_> {
fn describe(&self, list_limit: Option<usize>) -> String {
match self {
Self::Num(x) => x.to_string(),
Self::Dice(roll) => roll.describe(list_limit),
Self::Neg(x) => match x.as_ref() {
Self::Num(..) | Self::Dice(..) => format!("-{}", x.describe(list_limit)),
_ => format!("-({})", x.describe(list_limit)),
},
Self::Add(a, b) => self.describe_binary_expr('+', a.as_ref(), b.as_ref(), list_limit),
Self::Sub(a, b) => self.describe_binary_expr('-', a.as_ref(), b.as_ref(), list_limit),
Self::Mul(a, b) => self.describe_binary_expr('*', a.as_ref(), b.as_ref(), list_limit),
Self::DivDown(a, b) => self.describe_binary_expr('/', a.as_ref(), b.as_ref(), list_limit),
Self::DivUp(a, b) => self.describe_binary_expr('\\', a.as_ref(), b.as_ref(), list_limit),
}
}
}
impl fmt::Display for Evaled<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.describe(None))
}
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum EvalError {
#[error("dice error while evaluating \"{0}\": {1}")]
Dice(Expr, #[source] DiceError),
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum CalcError {
#[error("dice error while calculating ({0}): {1}")]
Dice(Evaled<'static>, #[source] DiceError),
#[error("integer overflow while calculating {0}")]
Overflow(Evaled<'static>),
#[error("division error while calculating {0}")]
Division(Evaled<'static>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::exhaustive_enums)]
pub enum OpType {
Value,
Unary,
Additive,
Multiplicative,
}
pub trait HasOpType {
fn op_type(&self) -> OpType;
fn is_value(&self) -> bool;
fn is_unary(&self) -> bool;
fn is_additive(&self) -> bool;
fn is_multiplicative(&self) -> bool;
fn is_binary(&self) -> bool {
self.is_additive() || self.is_multiplicative()
}
}
pub trait Describe {
#[must_use]
fn describe(&self, list_limit: Option<usize>) -> String;
}
trait DescribeBinaryExpr: HasOpType + Describe {
fn describe_binary_expr(
&self,
op: char,
a: &impl DescribeBinaryExpr,
b: &impl DescribeBinaryExpr,
list_limit: Option<usize>,
) -> String {
format!(
"{} {} {}",
match (self.op_type(), a.op_type()) {
(OpType::Additive | OpType::Unary, OpType::Multiplicative)
| (OpType::Multiplicative | OpType::Unary, OpType::Additive)
| (OpType::Unary, OpType::Unary) => paren_wrap(a.describe(list_limit)),
_ => a.describe(list_limit),
},
op,
match (self.op_type(), b.op_type()) {
(OpType::Additive | OpType::Unary, OpType::Multiplicative)
| (OpType::Multiplicative | OpType::Unary, OpType::Additive)
| (OpType::Unary, OpType::Unary) => paren_wrap(b.describe(list_limit)),
_ => b.describe(list_limit),
}
)
}
}
impl<T: HasOpType + Describe> DescribeBinaryExpr for T {}
#[must_use]
fn paren_wrap(mut text: String) -> String {
text.insert(0, '(');
text.push(')');
text
}