diff --git a/README.md b/README.md index 7fe3ccb..5882dfc 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,16 @@ expressions. !r 3d12 - 5d2 + 3 - 7d3 + 20d20 ``` -This system does not yet have the capability to handle things like D&D -5e advantage or disadvantage. +#### Keep/Drop Dice +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 diff --git a/dicebot/src/basic/dice.rs b/dicebot/src/basic/dice.rs index 24b8bbf..e35a744 100644 --- a/dicebot/src/basic/dice.rs +++ b/dicebot/src/basic/dice.rs @@ -12,17 +12,29 @@ use std::ops::{Deref, DerefMut}; pub struct Dice { pub(crate) count: u32, pub(crate) sides: u32, + pub(crate) keep_drop: KeepOrDrop, } impl fmt::Display for Dice { 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), + 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 { - pub fn new(count: u32, sides: u32) -> Dice { - Dice { count, sides } + pub fn new(count: u32, sides: u32, keep_drop: KeepOrDrop) -> Dice { + Dice { count, sides, keep_drop } } } diff --git a/dicebot/src/basic/parser.rs b/dicebot/src/basic/parser.rs index c16bf23..2be8590 100644 --- a/dicebot/src/basic/parser.rs +++ b/dicebot/src/basic/parser.rs @@ -31,12 +31,53 @@ enum Sign { 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> { + // parse main dice expression let (input, (count, _, sides)) = tuple((digit1, tag("d"), digit1))(input)?; + + // check for keep expression to keep highest dice (2d20k1) + let (keep, input) = match tuple::<&str, _, (_, _), _>((tag("k"), digit1))(input) { + // if ok, keep expression is present + Ok((rest, (_, keep_amount))) => (keep_amount, rest), + // 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::() { + // Ok, there's a keep value, check and create Keep + Ok(i) => match i { + _i if _i > count || _i == 0 => KeepOrDrop::None, + i => KeepOrDrop::Keep(i), + }, + // Err, check if drop works + Err(_) => { + match drop.parse::() { + // 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(( 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::*; #[test] fn dice_test() { - assert_eq!(parse_dice("2d4"), Ok(("", Dice::new(2, 4)))); - assert_eq!(parse_dice("20d40"), Ok(("", Dice::new(20, 40)))); - assert_eq!(parse_dice("8d7"), Ok(("", Dice::new(8, 7)))); + assert_eq!(parse_dice("2d4"), Ok(("", Dice::new(2, 4, KeepOrDrop::None)))); + assert_eq!(parse_dice("20d40"), Ok(("", Dice::new(20, 40, KeepOrDrop::None)))); + 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] fn element_test() { assert_eq!( 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!( parse_element(" \t\n\r\n 8 \n"), @@ -139,14 +193,21 @@ mod tests { parse_signed_element(" \t\n\r\n- 8d4 \n"), Ok(( " \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!( parse_signed_element(" \t\n\r\n+ 8d4 \n"), Ok(( " \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(( "", 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!( parse_element_expression(" - 8d4 \n "), Ok(( " \n ", ElementExpression(vec![SignedElement::Negative(Element::Dice(Dice::new( - 8, 4 + 8, 4, KeepOrDrop::None )))]) )) ); assert_eq!( - parse_element_expression("\t3d4 + 7 - 5 - 6d12 + 1d1 + 53 1d5 "), + parse_element_expression("\t3d4k2 + 7 - 5 - 6d12dh3 + 1d1 + 53 1d5 "), Ok(( " 1d5 ", 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::Negative(Element::Bonus(5)), - SignedElement::Negative(Element::Dice(Dice::new(6, 12))), - SignedElement::Positive(Element::Dice(Dice::new(1, 1))), + SignedElement::Negative(Element::Dice(Dice::new(6, 12, KeepOrDrop::Drop(3)))), + SignedElement::Positive(Element::Dice(Dice::new(1, 1, KeepOrDrop::None))), SignedElement::Positive(Element::Bonus(53)), ]) )) diff --git a/dicebot/src/basic/roll.rs b/dicebot/src/basic/roll.rs index 5614f73..42e89f2 100644 --- a/dicebot/src/basic/roll.rs +++ b/dicebot/src/basic/roll.rs @@ -4,6 +4,7 @@ * project. */ use crate::basic::dice; +use crate::basic::dice::KeepOrDrop; use rand::prelude::*; use std::fmt; use std::ops::{Deref, DerefMut}; @@ -19,15 +20,27 @@ pub trait Rolled { } #[derive(Debug, PartialEq, Eq, Clone)] -pub struct DiceRoll(pub Vec); +/// 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, usize, usize); impl DiceRoll { pub fn rolls(&self) -> &[u32] { &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 { - 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 { write!(f, "{}", self.rolled_value())?; 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() { - write!(f, " ({}", first)?; + if drop != 0 { + write!(f, " ([{}]", first.1)?; + } else { + write!(f, " ({}", first.1)?; + } 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, ")")?; } @@ -58,11 +81,17 @@ impl Roll for dice::Dice { fn roll(&self) -> DiceRoll { 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)) .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::*; #[test] fn dice_roll_display_test() { - assert_eq!(DiceRoll(vec![1, 3, 4]).to_string(), "8 (1 + 3 + 4)"); - assert_eq!(DiceRoll(vec![]).to_string(), "0"); + assert_eq!(DiceRoll(vec![1, 3, 4], 3, 0).to_string(), "8 (1 + 3 + 4)"); + assert_eq!(DiceRoll(vec![], 0, 0).to_string(), "0"); 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)" ); + 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] fn element_roll_display_test() { 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)" ); assert_eq!(ElementRoll::Bonus(7).to_string(), "7"); @@ -218,11 +255,11 @@ mod tests { #[test] fn signed_element_roll_display_test() { 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)" ); 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)" ); assert_eq!( @@ -239,14 +276,14 @@ mod tests { fn element_expression_roll_display_test() { assert_eq!( ElementExpressionRoll(vec![SignedElementRoll::Positive(ElementRoll::Dice( - DiceRoll(vec![1, 3, 4]) + DiceRoll(vec![1, 3, 4], 3, 0) )),]) .to_string(), "8 (1 + 3 + 4)" ); assert_eq!( ElementExpressionRoll(vec![SignedElementRoll::Negative(ElementRoll::Dice( - DiceRoll(vec![1, 3, 4]) + DiceRoll(vec![1, 3, 4], 3, 0) )),]) .to_string(), "-8 (1 + 3 + 4)" @@ -263,8 +300,8 @@ mod tests { ); assert_eq!( ElementExpressionRoll(vec![ - SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))), - SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 2]))), + SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))), + SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 2], 2, 0))), SignedElementRoll::Positive(ElementRoll::Bonus(4)), SignedElementRoll::Negative(ElementRoll::Bonus(7)), ]) @@ -273,13 +310,33 @@ mod tests { ); assert_eq!( ElementExpressionRoll(vec![ - SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))), - SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 2]))), + SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))), + SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 2], 2, 0))), SignedElementRoll::Negative(ElementRoll::Bonus(4)), SignedElementRoll::Positive(ElementRoll::Bonus(7)), ]) .to_string(), "-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)" + ); } }