mod parser;
mod tokenizer;
use crate::die::{Roll, Rollable};
use crate::expr::parser::{ExpressionParser, Op, RollOperation};
use anyhow::Result;
use std::fmt::{self, Display, Formatter};
#[derive(Debug)]
pub struct Expression {
rollables: Vec<RollOperation>,
}
impl Expression {
pub fn parse(value: impl AsRef<str>) -> Result<Expression> {
let rollables = ExpressionParser::new(value).parse()?;
let rollables = ExpressionVector::new(rollables).normalize().collect();
Ok(Self { rollables })
}
pub fn roll(&self) -> Roll {
self.rollables
.iter()
.fold(0, |acc, RollOperation(rhs, op)| match op {
Op::Add => acc + rhs.roll(),
Op::Sub => acc - rhs.roll(),
})
}
}
impl Display for Expression {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let s = self
.rollables
.iter()
.fold(String::new(), |acc, RollOperation(r, o)| {
if acc.is_empty() {
r.to_string()
} else {
format!("{acc} {o} {r}")
}
});
write!(f, "{s}")
}
}
impl TryFrom<String> for Expression {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::parse(&value)
}
}
impl TryFrom<&str> for Expression {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
struct ExpressionVector {
rollables: Vec<RollOperation>,
}
impl ExpressionVector {
fn new(rollables: Vec<RollOperation>) -> Self {
Self { rollables }
}
fn collect(self) -> Vec<RollOperation> {
self.rollables
}
fn normalize(self) -> Self {
self.sort().collapse()
}
fn sort(mut self) -> Self {
self.rollables.sort();
self.rollables.reverse();
self
}
fn collapse(self) -> Self {
let fixed_vals: Vec<&RollOperation> = self
.rollables
.iter()
.filter(|RollOperation(r, _)| matches!(r, Rollable::Fixed(_)))
.collect();
if fixed_vals.is_empty() {
self
} else {
let fixed_val = fixed_vals
.iter()
.fold(0, |acc, RollOperation(r, o)| match r {
Rollable::Fixed(n) => acc + o.operand(n),
_ => acc,
});
let (fixed_val, op) = if fixed_val < 0 {
(fixed_val.abs(), Op::Sub)
} else {
(fixed_val, Op::Add)
};
let new_fixed = Rollable::Fixed(fixed_val);
let new_op = RollOperation(new_fixed, op);
let mut rollables: Vec<RollOperation> = self
.rollables
.into_iter()
.filter(|RollOperation(r, _)| !matches!(r, Rollable::Fixed(_)))
.collect();
rollables.push(new_op);
Self { rollables }
}
}
}
#[cfg(test)]
mod tests {
mod fixed {
mod parsing {
use super::super::super::{Expression, parser::Op};
use crate::die::{Die, Rollable};
use crate::expr::parser::RollOperation;
type Result = anyhow::Result<()>;
macro_rules! assert_range {
($result:expr, $lo:expr, $hi:expr) => {
assert!(
$result >= $lo && $result <= $hi,
"! {} <= {} <= {}",
$lo,
$result,
$hi
);
};
}
macro_rules! assert_die {
($rollable:expr, $die:ident) => {
assert!(
matches!($rollable, Rollable::Dice(Die::$die, 1)),
"die rollable: expected {}, got {} instead",
Die::$die,
$rollable,
)
};
}
macro_rules! assert_dice {
($rollable:expr, $die:ident, $num:expr) => {
assert!(
matches!($rollable, Rollable::Dice(Die::$die, $num)),
"dice rollable: expected {}{}, got {} instead",
$num,
Die::$die,
$rollable,
)
};
}
macro_rules! assert_fixed {
($rollable:expr, $fixed:literal) => {
assert!(
matches!($rollable, Rollable::Fixed($fixed)),
"fixed rollable: expected {}, got {} instead",
$fixed,
$rollable,
)
};
}
macro_rules! assert_op {
($op:expr, $item:ident) => {
assert!(
matches!($op, Op::$item),
"op: expected {}, got {} instead",
Op::$item,
$op
)
};
}
#[test]
fn parses_a_simple_die_expression() -> Result {
let expr = Expression::parse("d20")?;
assert_eq!(expr.rollables.len(), 1);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_die!(rollable, D20);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 1, 20);
Ok(())
}
#[test]
fn parses_a_simple_dice_expression() -> Result {
let expr = Expression::parse("3d6")?;
assert_eq!(expr.rollables.len(), 1);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D6, 3);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 3, 18);
Ok(())
}
#[test]
fn parses_a_simple_dice_expression_with_a_quantity_of_1() -> Result {
let expr = Expression::parse("1d20")?;
assert_eq!(expr.rollables.len(), 1);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_die!(rollable, D20);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 1, 20);
Ok(())
}
#[test]
fn parses_a_fixed_expression_with_a_leading_plus_sign() -> Result {
let expr = Expression::parse("+4")?;
assert_eq!(expr.rollables.len(), 1);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Add);
let result = expr.roll();
assert_eq!(result, 4);
Ok(())
}
#[test]
fn parses_a_fixed_expression_without_a_leading_plus_sign() -> Result {
let expr = Expression::parse("8")?;
assert_eq!(expr.rollables.len(), 1);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_fixed!(rollable, 8);
assert_op!(op, Add);
let result = expr.roll();
assert_eq!(result, 8);
Ok(())
}
#[test]
fn parses_a_fixed_negative_expression() -> Result {
let expr = Expression::parse("-2")?;
assert_eq!(expr.rollables.len(), 1);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_fixed!(rollable, 2);
assert_op!(op, Sub);
let result = expr.roll();
assert_eq!(result, -2);
Ok(())
}
#[test]
fn parses_a_die_with_a_fixed_expression() -> Result {
let expr = Expression::parse("d20 + 4")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_die!(rollable, D20);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 5, 24);
Ok(())
}
#[test]
fn parses_a_die_with_a_fixed_expression_and_a_quantity() -> Result {
let expr = Expression::parse("1d20 + 4")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_die!(rollable, D20);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 5, 24);
Ok(())
}
#[test]
fn parses_a_die_with_a_fixed_expression_without_spaces() -> Result {
let expr = Expression::parse("d20+4")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_die!(rollable, D20);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 5, 24);
Ok(())
}
#[test]
fn parses_a_die_with_a_fixed_expression_with_a_quantity_and_without_spaces() -> Result {
let expr = Expression::parse("1d20+4")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_die!(rollable, D20);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 5, 24);
Ok(())
}
#[test]
fn parses_a_die_with_a_leading_fixed_expression() -> Result {
let expr = Expression::parse("4 + 1d20")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_die!(rollable, D20);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 5, 24);
Ok(())
}
#[test]
fn parses_a_die_with_a_fixed_negative_expression() -> Result {
let expr = Expression::parse("d20 - 4")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_die!(rollable, D20);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Sub);
let result = expr.roll();
assert_range!(result, -3, 16);
Ok(())
}
#[test]
fn parses_a_die_with_a_fixed_negative_expression_without_spaces() -> Result {
let expr = Expression::parse("d20-4")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_die!(rollable, D20);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Sub);
let result = expr.roll();
assert_range!(result, -3, 16);
Ok(())
}
#[test]
fn parses_a_die_with_a_leading_fixed_negative_expression() -> Result {
let expr = Expression::parse("-4 + 1d20")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_die!(rollable, D20);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Sub);
let result = expr.roll();
assert_range!(result, -3, 16);
Ok(())
}
#[test]
fn parses_dice_with_a_fixed_expression() -> Result {
let expr = Expression::parse("2d10 + 4")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 6, 24);
Ok(())
}
#[test]
fn parses_dice_with_a_fixed_expression_without_spaces() -> Result {
let expr = Expression::parse("2d10+4")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 6, 24);
Ok(())
}
#[test]
fn parses_dice_with_a_leading_fixed_expression_without_spaces() -> Result {
let expr = Expression::parse("4+2d10")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 6, 24);
Ok(())
}
#[test]
fn parses_dice_with_a_fixed_negative_expression() -> Result {
let expr = Expression::parse("2d10 - 4")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Sub);
let result = expr.roll();
assert_range!(result, -2, 16);
Ok(())
}
#[test]
fn parses_dice_with_a_fixed_negative_expression_without_spaces() -> Result {
let expr = Expression::parse("2d10-4")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Sub);
let result = expr.roll();
assert_range!(result, -2, 16);
Ok(())
}
#[test]
fn parses_with_with_a_leading_fixed_negative_expression() -> Result {
let expr = Expression::parse("-4 + 2d10")?;
assert_eq!(expr.rollables.len(), 2, "{:?}", expr.rollables);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Sub);
let result = expr.roll();
assert_range!(result, -2, 16);
Ok(())
}
#[test]
fn parses_dice_with_multiple_fixed_expressions() -> Result {
let expr = Expression::parse("2d10 + 4 + 6 + 2")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 12);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 14, 32);
Ok(())
}
#[test]
fn parses_dice_with_multiple_fixed_expressions_without_spaces() -> Result {
let expr = Expression::parse("2d10+4+6+2")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 12);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 14, 32);
Ok(())
}
#[test]
fn parses_dice_with_multiple_leading_fixed_expressions() -> Result {
let expr = Expression::parse("4 + 6 + 2 + 2d10")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 12);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 14, 32);
Ok(())
}
#[test]
fn parses_dice_with_multiple_interspersed_fixed_expressions() -> Result {
let expr = Expression::parse("4 + 2d10 + 2 + 6")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 12);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 14, 32);
Ok(())
}
#[test]
fn parses_dice_with_multiple_fixed_negative_expressions() -> Result {
let expr = Expression::parse("2d10 - 4 - 6 - 2")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 12);
assert_op!(op, Sub);
let result = expr.roll();
assert_range!(result, -10, 8);
Ok(())
}
#[test]
fn parses_dice_with_multiple_fixed_negative_expressions_without_spaces() -> Result {
let expr = Expression::parse("2d10-4-6-2")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 12);
assert_op!(op, Sub);
let result = expr.roll();
assert_range!(result, -10, 8);
Ok(())
}
#[test]
fn parses_dice_with_multiple_leading_fixed_negative_expressions() -> Result {
let expr = Expression::parse("-4 - 6 - 2 + 2d10")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 12);
assert_op!(op, Sub);
let result = expr.roll();
assert_range!(result, -10, 8);
Ok(())
}
#[test]
fn parses_dice_with_multiple_interspersed_fixed_negative_expressions() -> Result {
let expr = Expression::parse("-4 + 2d10 - 2 - 6")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 12);
assert_op!(op, Sub);
let result = expr.roll();
assert_range!(result, -10, 8);
Ok(())
}
#[test]
fn parses_dice_with_multiple_fixed_mixed_expressions() -> Result {
let expr = Expression::parse("2d10 - 4 + 6 + 2")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 4, 24);
Ok(())
}
#[test]
fn parses_dice_with_multiple_fixed_mixed_expressions_without_spaces() -> Result {
let expr = Expression::parse("2d10-4+6+2")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 4, 24);
Ok(())
}
#[test]
fn parses_dice_with_multiple_leading_fixed_mixed_expressions() -> Result {
let expr = Expression::parse("-4 + 6 + 2 + 2d10")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 4);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 4, 24);
Ok(())
}
#[test]
fn parses_dice_with_multiple_interspersed_fixed_mixed_expressions() -> Result {
let expr = Expression::parse("4 - 2d10 + 2 - 6")?;
assert_eq!(expr.rollables.len(), 2);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Sub);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_fixed!(rollable, 0);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, -20, -2);
Ok(())
}
#[test]
fn parses_heterogeneous_dice() -> Result {
let expr = Expression::parse("2d10 + 1d8 + d6")?;
assert_eq!(expr.rollables.len(), 3);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_die!(rollable, D8);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(2).unwrap();
assert_die!(rollable, D6);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 4, 34);
Ok(())
}
#[test]
fn parses_heterogeneous_dice_with_fixed_expressions() -> Result {
let expr = Expression::parse("2d10 + 1d8 + d6 + 4 + 6")?;
assert_eq!(expr.rollables.len(), 4);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D10, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_die!(rollable, D8);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(2).unwrap();
assert_die!(rollable, D6);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(3).unwrap();
assert_fixed!(rollable, 10);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 14, 44);
Ok(())
}
#[test]
fn parses_heterogeneous_dice_with_subtraction() -> Result {
let expr = Expression::parse("1d10 - 1d4 - 1d8")?;
assert_eq!(expr.rollables.len(), 3);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_die!(rollable, D10);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_die!(rollable, D8);
assert_op!(op, Sub);
let RollOperation(rollable, op) = expr.rollables.get(2).unwrap();
assert_die!(rollable, D4);
assert_op!(op, Sub);
let result = expr.roll();
assert_range!(result, -11, 8);
Ok(())
}
#[test]
fn parses_heterogeneous_dice_with_mixed_operations() -> Result {
let expr = Expression::parse("1d10 - 1d4 + 1d8")?;
assert_eq!(expr.rollables.len(), 3);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_die!(rollable, D10);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_die!(rollable, D8);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(2).unwrap();
assert_die!(rollable, D4);
assert_op!(op, Sub);
let result = expr.roll();
assert_range!(result, -2, 17);
Ok(())
}
#[test]
fn parses_an_expression_with_and_without_spaces() -> Result {
let expr = Expression::parse("2d20+1d10 + 10")?;
assert_eq!(
expr.rollables.len(),
3,
"length {}: {:?}",
expr.rollables.len(),
expr.rollables
);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D20, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_die!(rollable, D10);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(2).unwrap();
assert_fixed!(rollable, 10);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 13, 60);
Ok(())
}
#[test]
#[ignore = "something wrong with sorting"]
fn parses_an_expression_with_and_without_spaces_in_two_sets() -> Result {
let expr = Expression::parse("2d20+1d10 + 10+2d8-4d4")?;
assert_eq!(
expr.rollables.len(),
5,
"length {}: {:?}",
expr.rollables.len(),
expr.rollables
);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D20, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(1).unwrap();
assert_die!(rollable, D10);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(3).unwrap();
assert_dice!(rollable, D8, 2);
assert_op!(op, Add);
let RollOperation(rollable, op) = expr.rollables.get(4).unwrap();
assert_dice!(rollable, D4, 4);
assert_op!(op, Sub);
let RollOperation(rollable, op) = expr.rollables.get(2).unwrap();
assert_fixed!(rollable, 10);
assert_op!(op, Add);
let result = expr.roll();
assert_range!(result, 13, 60);
Ok(())
}
#[test]
#[ignore = "TOD: collapse homogenous dice"]
fn parses_multiple_homogenous_dice() -> Result {
let expr = Expression::parse("1d20 + 1d20")?;
assert_eq!(
expr.rollables.len(),
1,
"length {}: {:?}",
expr.rollables.len(),
expr.rollables
);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D20, 2);
assert_op!(op, Add);
Ok(())
}
#[test]
#[ignore = "TODO: collapse homogenous dice"]
fn parses_multiple_homogenous_dice_with_no_quantities() -> Result {
let expr = Expression::parse("d20 + d20")?;
assert_eq!(
expr.rollables.len(),
1,
"length {}: {:?}",
expr.rollables.len(),
expr.rollables
);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D20, 2);
assert_op!(op, Add);
Ok(())
}
#[test]
#[ignore = "TOD: collapse homogenous dice"]
fn parses_multiple_homogenous_dice_with_one_quantity() -> Result {
let expr = Expression::parse("d20 + 1d20")?;
assert_eq!(
expr.rollables.len(),
1,
"length {}: {:?}",
expr.rollables.len(),
expr.rollables
);
let RollOperation(rollable, op) = expr.rollables.get(0).unwrap();
assert_dice!(rollable, D20, 2);
assert_op!(op, Add);
Ok(())
}
#[test]
fn fails_for_signed_modifiers_after_operators() {
let expr = Expression::parse("1d10 + +4");
assert!(expr.is_err(), "error: 1d10 + +4 ({expr:?})");
let expr = Expression::parse("1d10 - -4");
assert!(expr.is_err(), "error: 1d10 - -4 ({expr:?})");
let expr = Expression::parse("1d10 + -4");
assert!(expr.is_err(), "error: 1d10 + -4 ({expr:?})");
let expr = Expression::parse("1d10 - +4");
assert!(expr.is_err(), "error: 1d10 - +4 ({expr:?})");
}
#[test]
fn fails_for_floating_point_modifiers() {
let expr = Expression::parse("-4.0");
assert!(expr.is_err());
}
#[test]
fn fails_for_invalid_dice() {
let expr = Expression::parse("1d21 + 10");
assert!(expr.is_err());
}
#[test]
fn fails_for_gobbledy_gook() {
let expr = Expression::parse("daaabbbccccddddxxxxyyyyzzzz");
assert!(expr.is_err());
}
#[test]
fn fails_for_empty_string() {
let expr = Expression::parse("");
assert!(expr.is_err());
}
}
mod display {}
mod from {}
}
mod hypothesis {
mod parsing {}
mod display {}
mod from {}
}
}