Keep/Drop Function #92

Manually merged
kg333 merged 12 commits from kg333/tenebrous-dicebot:keep_drop_function into master 2021-09-26 14:05:57 +00:00
4 changed files with 186 additions and 38 deletions

View File

@ -118,8 +118,16 @@ expressions.
!r 3d12 - 5d2 + 3 - 7d3 + 20d20 !r 3d12 - 5d2 + 3 - 7d3 + 20d20
``` ```
This system does not yet have the capability to handle things like D&D #### Keep/Drop Dice
5e advantage or disadvantage. The bot supports either keeping the highest dice in a roll, or
dropping the highest dice in a roll. This allows the bot to handle
things like D&D 5e advantage or disadvantage.
```
!roll 2d20k1
!r 2d20dh1 + 5
!r 10d10k5 + 10d10dh5 - 2
```
### Storytelling System ### Storytelling System

View File

@ -12,17 +12,29 @@ use std::ops::{Deref, DerefMut};
pub struct Dice { pub struct Dice {
pub(crate) count: u32, pub(crate) count: u32,
pub(crate) sides: u32, pub(crate) sides: u32,
pub(crate) keep_drop: KeepOrDrop,
} }
impl fmt::Display for Dice { impl fmt::Display for Dice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}d{}", self.count, self.sides) match self.keep_drop {
KeepOrDrop::Keep(keep) => write!(f, "{}d{}k{}", self.count, self.sides, keep),
kg333 marked this conversation as resolved Outdated

Since you have if-else clauses here, it might make more sense to have a third enum member in addition to Keep and Drop, which indicates that we do nothing special with the roll.

Since you have if-else clauses here, it might make more sense to have a third enum member in addition to Keep and Drop, which indicates that we do nothing special with the roll.
Outdated
Review

Added another enum member called None and it's much cleaner. Although members with no type give my C-programmer soul the heebie-jeebies.

Added another enum member called None and it's much cleaner. Although members with no type give my C-programmer soul the heebie-jeebies.
KeepOrDrop::Drop(drop) => write!(f, "{}d{}dh{}", self.count, self.sides, drop),
KeepOrDrop::None => write!(f, "{}d{}", self.count, self.sides),
}
} }
} }
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum KeepOrDrop {
Keep (u32),
Drop (u32),
None,
}
impl Dice { impl Dice {
pub fn new(count: u32, sides: u32) -> Dice { pub fn new(count: u32, sides: u32, keep_drop: KeepOrDrop) -> Dice {
Dice { count, sides } Dice { count, sides, keep_drop }
} }
} }

View File

@ -31,12 +31,53 @@ enum Sign {
Minus, Minus,
} }
// Parse a dice expression. Does not eat whitespace /// Parse a dice expression. Does not eat whitespace
fn parse_dice(input: &str) -> IResult<&str, Dice> { fn parse_dice(input: &str) -> IResult<&str, Dice> {
// parse main dice expression
let (input, (count, _, sides)) = tuple((digit1, tag("d"), digit1))(input)?; let (input, (count, _, sides)) = tuple((digit1, tag("d"), digit1))(input)?;
// check for keep expression to keep highest dice (2d20k1)
kg333 marked this conversation as resolved Outdated

I think it is better to document what the keep expression actually is here, so tacking on "1dXkY" would be useful.

And since pattern matches are expressions, I think we can rewrite these two match blocks. Something like this:

let (keep, input) = match tuple::<&str, _, (_, _), _>((tag("k"), digit1))(input) {
    // if ok, keep expression is present
    Ok(r) => (r.0, r.1.1),
    // otherwise absent and keep all dice
    Err(_) => ("".to_string(), count)
};

The same should be doable for the drop expression. I think this way you can also get rid of the mut on input, because let in rust is not just a variable assignment. It actually creates a whole new variable binding.

Note: I haven't tested the above, but it should work. Or some variation of it should work.

I think it is better to document what the keep expression actually is here, so tacking on "1dXkY" would be useful. And since pattern matches are expressions, I think we can rewrite these two match blocks. Something like this: ```rust let (keep, input) = match tuple::<&str, _, (_, _), _>((tag("k"), digit1))(input) { // if ok, keep expression is present Ok(r) => (r.0, r.1.1), // otherwise absent and keep all dice Err(_) => ("".to_string(), count) }; ``` The same should be doable for the drop expression. I think this way you can also get rid of the `mut` on input, because `let` in rust is not just a variable assignment. It actually creates a whole new variable binding. Note: I haven't tested the above, but it should work. Or some variation of it should work.
let (keep, input) = match tuple::<&str, _, (_, _), _>((tag("k"), digit1))(input) {
// if ok, keep expression is present
Ok((rest, (_, keep_amount))) => (keep_amount, rest),
kg333 marked this conversation as resolved Outdated

Rust supports tuple destructuring in pattern matching, which may be useful here and the other match:

Ok((rest, (_, keep_amount)) => ...

rest being the rest of the input in this case.

Rust supports tuple destructuring in pattern matching, which may be useful here and the other match: ```rust Ok((rest, (_, keep_amount)) => ... ``` `rest` being the rest of the input in this case.
Outdated
Review

Ooh, that's much more human-readable, thanks!

Ooh, that's much more human-readable, thanks!
// otherwise absent and keep all dice
Err(_) => ("", input)
};
// check for drop expression to drop highest dice (2d20dh1)
let (drop, input) = match tuple::<&str, _, (_, _), _>((tag("dh"), digit1))(input) {
// if ok, keep expression is present
Ok((rest, (_, drop_amount))) => (drop_amount, rest),
// otherwise absent and keep all dice
Err(_) => ("", input)
};
let count: u32 = count.parse().unwrap();
// don't allow keep greater than number of dice, and don't allow keep zero
let keep_drop = match keep.parse::<u32>() {
// Ok, there's a keep value, check and create Keep
Ok(i) => match i {
_i if _i > count || _i == 0 => KeepOrDrop::None,
kg333 marked this conversation as resolved Outdated

You can make this a bit more concise with yet another match expression:

Ok(i) => match i {
    _i if _i > count || _i == 0 => KeepOrDrop::Keep(count),
    i => KeepOrDrop::Keep(i)
}

This is mostly a stylistic choice though.

You can make this a bit more concise with yet another match expression: ```rust Ok(i) => match i { _i if _i > count || _i == 0 => KeepOrDrop::Keep(count), i => KeepOrDrop::Keep(i) } ``` This is mostly a stylistic choice though.
i => KeepOrDrop::Keep(i),
},
// Err, check if drop works
Err(_) => {
match drop.parse::<u32>() {
kg333 marked this conversation as resolved Outdated

Instead of unwrap, you should make use of the features of the Result type. While you will see examples of Rust code around the internet littered with unwrap, you don't actually want to use it except under a few circumstances:

  • You know the code will not crash.
  • You are okay (or don't care) with the code crashing.

In this case, might be best to combine parsing and validation:

keep.parse().map(|k| {
    //Could also use match block if it's shorter
    if k > count || k == 0 {
        count
    } else {
        k
    }
})?;

This will send up parsing errors back up the call chain and keep the boundary checks in place for a successful parse.

This comment also apples to the drop parsing.

Instead of `unwrap`, you should make use of the features of the Result type. While you will see examples of Rust code around the internet littered with unwrap, you don't actually want to use it except under a few circumstances: - You know the code will not crash. - You are okay (or don't care) with the code crashing. In this case, might be best to combine parsing and validation: ```rust keep.parse().map(|k| { //Could also use match block if it's shorter if k > count || k == 0 { count } else { k } })?; ``` This will send up parsing errors back up the call chain and keep the boundary checks in place for a successful parse. This comment also apples to the `drop` parsing.
Outdated
Review

This is proving more difficult than I anticipated: it looks like nom is using its own error type? Not sure how to resolve this one, although I agree unwrap is a problem.

error[E0271]: type mismatch resolving `<u32 as std::str::FromStr>::Err == nom::Err<(&str, nom::error::ErrorKind)>`
  --> dicebot/src/basic/parser.rs:56:29
   |
56 |     let count: u32 = (count.parse())?;
   |                             ^^^^^ expected struct `ParseIntError`, found enum `nom::Err`
   |
   = note: expected struct `ParseIntError`
                found enum `nom::Err<(&str, nom::error::ErrorKind)>`

This is proving more difficult than I anticipated: it looks like nom is using its own error type? Not sure how to resolve this one, although I agree unwrap is a problem. ``` error[E0271]: type mismatch resolving `<u32 as std::str::FromStr>::Err == nom::Err<(&str, nom::error::ErrorKind)>` --> dicebot/src/basic/parser.rs:56:29 | 56 | let count: u32 = (count.parse())?; | ^^^^^ expected struct `ParseIntError`, found enum `nom::Err` | = note: expected struct `ParseIntError` found enum `nom::Err<(&str, nom::error::ErrorKind)>` ```
Outdated
Review

After looking at it further, the weird error type is irrelevant. Every possible arm of the match at this point either assigns Drop or Keep, or leaves a default Keep = Count if the parse fails.

After looking at it further, the weird error type is irrelevant. Every possible arm of the match at this point either assigns Drop or Keep, or leaves a default Keep = Count if the parse fails.
// Ok, there's a drop value, check and create Drop
Ok(i) => match i {
_i if i >= count => KeepOrDrop::None,
i => KeepOrDrop::Drop(i),
},
// Err, there's neither keep nor drop
Err(_) => KeepOrDrop::None,
}
},
};
Ok(( Ok((
input, input,
Dice::new(count.parse().unwrap(), sides.parse().unwrap()), Dice::new(count, sides.parse().unwrap(), keep_drop),
)) ))
} }
@ -108,16 +149,29 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn dice_test() { fn dice_test() {
assert_eq!(parse_dice("2d4"), Ok(("", Dice::new(2, 4)))); assert_eq!(parse_dice("2d4"), Ok(("", Dice::new(2, 4, KeepOrDrop::None))));
assert_eq!(parse_dice("20d40"), Ok(("", Dice::new(20, 40)))); assert_eq!(parse_dice("20d40"), Ok(("", Dice::new(20, 40, KeepOrDrop::None))));
assert_eq!(parse_dice("8d7"), Ok(("", Dice::new(8, 7)))); assert_eq!(parse_dice("8d7"), Ok(("", Dice::new(8, 7, KeepOrDrop::None))));
assert_eq!(parse_dice("2d20k1"), Ok(("", Dice::new(2, 20, KeepOrDrop::Keep(1)))));
assert_eq!(parse_dice("100d10k90"), Ok(("", Dice::new(100, 10, KeepOrDrop::Keep(90)))));
assert_eq!(parse_dice("11d10k10"), Ok(("", Dice::new(11, 10, KeepOrDrop::Keep(10)))));
assert_eq!(parse_dice("12d10k11"), Ok(("", Dice::new(12, 10, KeepOrDrop::Keep(11)))));
assert_eq!(parse_dice("12d10k13"), Ok(("", Dice::new(12, 10, KeepOrDrop::None))));
assert_eq!(parse_dice("12d10k0"), Ok(("", Dice::new(12, 10, KeepOrDrop::None))));
assert_eq!(parse_dice("20d40dh5"), Ok(("", Dice::new(20, 40, KeepOrDrop::Drop(5)))));
assert_eq!(parse_dice("8d7dh9"), Ok(("", Dice::new(8, 7, KeepOrDrop::None))));
assert_eq!(parse_dice("8d7dh8"), Ok(("", Dice::new(8, 7, KeepOrDrop::None))));
} }
#[test] #[test]
fn element_test() { fn element_test() {
assert_eq!( assert_eq!(
parse_element(" \t\n\r\n 8d7 \n"), parse_element(" \t\n\r\n 8d7 \n"),
Ok((" \n", Element::Dice(Dice::new(8, 7)))) Ok((" \n", Element::Dice(Dice::new(8, 7, KeepOrDrop::None))))
);
assert_eq!(
parse_element(" \t\n\r\n 3d20k2 \n"),
Ok((" \n", Element::Dice(Dice::new(3, 20, KeepOrDrop::Keep(2)))))
); );
assert_eq!( assert_eq!(
parse_element(" \t\n\r\n 8 \n"), parse_element(" \t\n\r\n 8 \n"),
@ -139,14 +193,21 @@ mod tests {
parse_signed_element(" \t\n\r\n- 8d4 \n"), parse_signed_element(" \t\n\r\n- 8d4 \n"),
Ok(( Ok((
" \n", " \n",
SignedElement::Negative(Element::Dice(Dice::new(8, 4))) SignedElement::Negative(Element::Dice(Dice::new(8, 4, KeepOrDrop::None)))
))
);
assert_eq!(
parse_signed_element(" \t\n\r\n- 8d4k4 \n"),
Ok((
" \n",
SignedElement::Negative(Element::Dice(Dice::new(8, 4, KeepOrDrop::Keep(4))))
)) ))
); );
assert_eq!( assert_eq!(
parse_signed_element(" \t\n\r\n+ 8d4 \n"), parse_signed_element(" \t\n\r\n+ 8d4 \n"),
Ok(( Ok((
" \n", " \n",
SignedElement::Positive(Element::Dice(Dice::new(8, 4))) SignedElement::Positive(Element::Dice(Dice::new(8, 4, KeepOrDrop::None)))
)) ))
); );
} }
@ -158,29 +219,39 @@ mod tests {
Ok(( Ok((
"", "",
ElementExpression(vec![SignedElement::Positive(Element::Dice(Dice::new( ElementExpression(vec![SignedElement::Positive(Element::Dice(Dice::new(
8, 4 8, 4, KeepOrDrop::None
)))]) )))])
)) ))
); );
assert_eq!(
parse_element_expression("\t2d20k1 + 5"),
Ok((
"",
ElementExpression(vec![
SignedElement::Positive(Element::Dice(Dice::new(2, 20, KeepOrDrop::Keep(1)))),
SignedElement::Positive(Element::Bonus(5)),
])
))
);
assert_eq!( assert_eq!(
parse_element_expression(" - 8d4 \n "), parse_element_expression(" - 8d4 \n "),
Ok(( Ok((
" \n ", " \n ",
ElementExpression(vec![SignedElement::Negative(Element::Dice(Dice::new( ElementExpression(vec![SignedElement::Negative(Element::Dice(Dice::new(
8, 4 8, 4, KeepOrDrop::None
)))]) )))])
)) ))
); );
assert_eq!( assert_eq!(
parse_element_expression("\t3d4 + 7 - 5 - 6d12 + 1d1 + 53 1d5 "), parse_element_expression("\t3d4k2 + 7 - 5 - 6d12dh3 + 1d1 + 53 1d5 "),
Ok(( Ok((
" 1d5 ", " 1d5 ",
ElementExpression(vec![ ElementExpression(vec![
SignedElement::Positive(Element::Dice(Dice::new(3, 4))), SignedElement::Positive(Element::Dice(Dice::new(3, 4, KeepOrDrop::Keep(2)))),
SignedElement::Positive(Element::Bonus(7)), SignedElement::Positive(Element::Bonus(7)),
SignedElement::Negative(Element::Bonus(5)), SignedElement::Negative(Element::Bonus(5)),
SignedElement::Negative(Element::Dice(Dice::new(6, 12))), SignedElement::Negative(Element::Dice(Dice::new(6, 12, KeepOrDrop::Drop(3)))),
SignedElement::Positive(Element::Dice(Dice::new(1, 1))), SignedElement::Positive(Element::Dice(Dice::new(1, 1, KeepOrDrop::None))),
SignedElement::Positive(Element::Bonus(53)), SignedElement::Positive(Element::Bonus(53)),
]) ])
)) ))

View File

@ -4,6 +4,7 @@
* project. * project.
*/ */
use crate::basic::dice; use crate::basic::dice;
use crate::basic::dice::KeepOrDrop;
use rand::prelude::*; use rand::prelude::*;
use std::fmt; use std::fmt;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
@ -19,15 +20,27 @@ pub trait Rolled {
} }
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
kg333 marked this conversation as resolved Outdated

Add a third / to get the Rustdoc working. It may also be good to more thoroughly describe what keep and drop mean here, namely that they mean keeping the highest or dropping the highest rolls.

Add a third `/` to get the Rustdoc working. It may also be good to more thoroughly describe what keep and drop mean here, namely that they mean keeping the highest or dropping the highest rolls.
pub struct DiceRoll(pub Vec<u32>); /// array of rolls in order, how many dice to keep, and how many to drop
/// keep indicates how many of the highest dice to keep
/// drop indicates how many of the highest dice to drop
pub struct DiceRoll (pub Vec<u32>, usize, usize);
impl DiceRoll { impl DiceRoll {
pub fn rolls(&self) -> &[u32] { pub fn rolls(&self) -> &[u32] {
&self.0 &self.0
} }
pub fn keep(&self) -> usize {
self.1
}
pub fn drop(&self) -> usize {
self.2
}
// only count kept dice in total
pub fn total(&self) -> u32 { pub fn total(&self) -> u32 {
self.0.iter().sum() self.0[self.2..self.1].iter().sum()
} }
} }
@ -41,11 +54,21 @@ impl fmt::Display for DiceRoll {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.rolled_value())?; write!(f, "{}", self.rolled_value())?;
let rolls = self.rolls(); let rolls = self.rolls();
let mut iter = rolls.iter(); let keep = self.keep();
let drop = self.drop();
let mut iter = rolls.iter().enumerate();
if let Some(first) = iter.next() { if let Some(first) = iter.next() {
write!(f, " ({}", first)?; if drop != 0 {
write!(f, " ([{}]", first.1)?;
} else {
write!(f, " ({}", first.1)?;
}
for roll in iter { for roll in iter {
write!(f, " + {}", roll)?; if roll.0 >= keep || roll.0 < drop {
write!(f, " + [{}]", roll.1)?;
} else {
write!(f, " + {}", roll.1)?;
}
} }
write!(f, ")")?; write!(f, ")")?;
} }
@ -58,11 +81,17 @@ impl Roll for dice::Dice {
fn roll(&self) -> DiceRoll { fn roll(&self) -> DiceRoll {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let rolls: Vec<_> = (0..self.count) let mut rolls: Vec<_> = (0..self.count)
.map(|_| rng.gen_range(1..=self.sides)) .map(|_| rng.gen_range(1..=self.sides))
.collect(); .collect();
// sort rolls in descending order
rolls.sort_by(|a, b| b.cmp(a));
DiceRoll(rolls) match self.keep_drop {
KeepOrDrop::Keep(k) => DiceRoll(rolls,k as usize, 0),
KeepOrDrop::Drop(dh) => DiceRoll(rolls,self.count as usize, dh as usize),
KeepOrDrop::None => DiceRoll(rolls,self.count as usize, 0),
}
} }
} }
@ -198,18 +227,26 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn dice_roll_display_test() { fn dice_roll_display_test() {
assert_eq!(DiceRoll(vec![1, 3, 4]).to_string(), "8 (1 + 3 + 4)"); assert_eq!(DiceRoll(vec![1, 3, 4], 3, 0).to_string(), "8 (1 + 3 + 4)");
assert_eq!(DiceRoll(vec![]).to_string(), "0"); assert_eq!(DiceRoll(vec![], 0, 0).to_string(), "0");
assert_eq!( assert_eq!(
DiceRoll(vec![4, 7, 2, 10]).to_string(), DiceRoll(vec![4, 7, 2, 10], 4, 0).to_string(),
"23 (4 + 7 + 2 + 10)" "23 (4 + 7 + 2 + 10)"
); );
assert_eq!(
DiceRoll(vec![20, 13, 11, 10], 3, 0).to_string(),
"44 (20 + 13 + 11 + [10])"
);
assert_eq!(
DiceRoll(vec![20, 13, 11, 10], 4, 1).to_string(),
"34 ([20] + 13 + 11 + 10)"
);
} }
#[test] #[test]
fn element_roll_display_test() { fn element_roll_display_test() {
assert_eq!( assert_eq!(
ElementRoll::Dice(DiceRoll(vec![1, 3, 4])).to_string(), ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0)).to_string(),
"8 (1 + 3 + 4)" "8 (1 + 3 + 4)"
); );
assert_eq!(ElementRoll::Bonus(7).to_string(), "7"); assert_eq!(ElementRoll::Bonus(7).to_string(), "7");
@ -218,11 +255,11 @@ mod tests {
#[test] #[test]
fn signed_element_roll_display_test() { fn signed_element_roll_display_test() {
assert_eq!( assert_eq!(
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))).to_string(), SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))).to_string(),
"8 (1 + 3 + 4)" "8 (1 + 3 + 4)"
); );
assert_eq!( assert_eq!(
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))).to_string(), SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))).to_string(),
"-8 (1 + 3 + 4)" "-8 (1 + 3 + 4)"
); );
assert_eq!( assert_eq!(
@ -239,14 +276,14 @@ mod tests {
fn element_expression_roll_display_test() { fn element_expression_roll_display_test() {
assert_eq!( assert_eq!(
ElementExpressionRoll(vec![SignedElementRoll::Positive(ElementRoll::Dice( ElementExpressionRoll(vec![SignedElementRoll::Positive(ElementRoll::Dice(
DiceRoll(vec![1, 3, 4]) DiceRoll(vec![1, 3, 4], 3, 0)
)),]) )),])
.to_string(), .to_string(),
"8 (1 + 3 + 4)" "8 (1 + 3 + 4)"
); );
assert_eq!( assert_eq!(
ElementExpressionRoll(vec![SignedElementRoll::Negative(ElementRoll::Dice( ElementExpressionRoll(vec![SignedElementRoll::Negative(ElementRoll::Dice(
DiceRoll(vec![1, 3, 4]) DiceRoll(vec![1, 3, 4], 3, 0)
)),]) )),])
.to_string(), .to_string(),
"-8 (1 + 3 + 4)" "-8 (1 + 3 + 4)"
@ -263,8 +300,8 @@ mod tests {
); );
assert_eq!( assert_eq!(
ElementExpressionRoll(vec![ ElementExpressionRoll(vec![
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))), SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))),
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 2]))), SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 2], 2, 0))),
SignedElementRoll::Positive(ElementRoll::Bonus(4)), SignedElementRoll::Positive(ElementRoll::Bonus(4)),
SignedElementRoll::Negative(ElementRoll::Bonus(7)), SignedElementRoll::Negative(ElementRoll::Bonus(7)),
]) ])
@ -273,13 +310,33 @@ mod tests {
); );
assert_eq!( assert_eq!(
ElementExpressionRoll(vec![ ElementExpressionRoll(vec![
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))), SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))),
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 2]))), SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 2], 2, 0))),
SignedElementRoll::Negative(ElementRoll::Bonus(4)), SignedElementRoll::Negative(ElementRoll::Bonus(4)),
SignedElementRoll::Positive(ElementRoll::Bonus(7)), SignedElementRoll::Positive(ElementRoll::Bonus(7)),
]) ])
.to_string(), .to_string(),
"-2 (-8 (1 + 3 + 4) + 3 (1 + 2) - 4 + 7)" "-2 (-8 (1 + 3 + 4) + 3 (1 + 2) - 4 + 7)"
); );
assert_eq!(
ElementExpressionRoll(vec![
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![4, 3, 1], 3, 0))),
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![12, 2], 1, 0))),
SignedElementRoll::Negative(ElementRoll::Bonus(4)),
SignedElementRoll::Positive(ElementRoll::Bonus(7)),
])
.to_string(),
"7 (-8 (4 + 3 + 1) + 12 (12 + [2]) - 4 + 7)"
);
assert_eq!(
ElementExpressionRoll(vec![
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![4, 3, 1], 3, 1))),
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![12, 2], 2, 0))),
SignedElementRoll::Negative(ElementRoll::Bonus(4)),
SignedElementRoll::Positive(ElementRoll::Bonus(7)),
])
.to_string(),
"13 (-4 ([4] + 3 + 1) + 14 (12 + 2) - 4 + 7)"
);
} }
} }