diff --git a/.gitignore b/.gitignore index 9f92ef6..05b7944 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ target/ dicebot-config todo +todo.org cache *.tar *.tar.gz diff --git a/Cargo.lock b/Cargo.lock index e6b568e..ef29325 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -277,6 +277,7 @@ dependencies = [ "actix", "actix-rt", "async-trait", + "combine", "dirs", "env_logger", "indoc", @@ -287,6 +288,7 @@ dependencies = [ "matrix-sdk-common 0.1.0 (git+https://github.com/matrix-org/matrix-rust-sdk?rev=0.1.0)", "matrix-sdk-common-macros", "nom", + "once_cell", "rand", "serde", "thiserror", @@ -325,6 +327,17 @@ dependencies = [ "cc", ] +[[package]] +name = "combine" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2809f67365382d65fd2b6d9c22577231b954ed27400efeafbe687bda75abcc0b" +dependencies = [ + "bytes", + "memchr", + "pin-project-lite", +] + [[package]] name = "const-random" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index b566bfe..d46badc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ dirs = "3.0" indoc = "1.0" actix = "0.10" actix-rt = "1.1" +combine = "4.3" +once_cell = "1.4" # The versioning of the matrix SDK follows its Cargo.toml. The SDK and # macros are on master, but it imports the common and base from 0.1.0. diff --git a/src/cofd/dice.rs b/src/cofd/dice.rs index d877e81..71dbc07 100644 --- a/src/cofd/dice.rs +++ b/src/cofd/dice.rs @@ -1,8 +1,36 @@ +use crate::error::BotError; use crate::roll::{Roll, Rolled}; use itertools::Itertools; use std::convert::TryFrom; use std::fmt; +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Operator { + Plus, + Minus, +} + +impl Operator { + pub fn mult(&self) -> i32 { + match self { + Operator::Plus => 1, + Operator::Minus => -1, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Element { + Variable(String), + Number(i32), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Amount { + pub operator: Operator, + pub element: Element, +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum DicePoolQuality { TenAgain, @@ -26,58 +54,145 @@ impl fmt::Display for DicePoolQuality { } } -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct DicePool { - pub(crate) count: u32, - pub(crate) sides: u32, - pub(crate) success_on: u32, - pub(crate) exceptional_success: u32, +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct DicePoolModifiers { + pub(crate) success_on: i32, + pub(crate) exceptional_on: i32, pub(crate) quality: DicePoolQuality, } -impl fmt::Display for DicePool { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} dice ({}, exceptional on {} successes)", - self.count, self.quality, self.exceptional_success - ) +impl DicePoolModifiers { + pub fn default() -> DicePoolModifiers { + DicePoolModifiers { + success_on: 8, + exceptional_on: 5, + quality: DicePoolQuality::TenAgain, + } } -} -impl DicePool { - pub fn new(count: u32, successes_for_exceptional: u32, quality: DicePoolQuality) -> DicePool { - DicePool { - count: count, - sides: 10, //TODO make configurable - //TODO make configurable - success_on: match quality { - DicePoolQuality::ChanceDie => 10, - _ => 8, - }, - exceptional_success: successes_for_exceptional, + pub fn custom_quality(quality: DicePoolQuality) -> DicePoolModifiers { + let success_on = if quality != DicePoolQuality::ChanceDie { + 8 + } else { + 10 + }; + DicePoolModifiers { + success_on: success_on, + exceptional_on: 5, quality: quality, } } - pub fn chance_die() -> DicePool { - DicePool { - count: 1, - sides: 10, - success_on: 10, - exceptional_success: 5, - quality: DicePoolQuality::ChanceDie, + pub fn custom_exceptional_on(exceptional_on: i32) -> DicePoolModifiers { + DicePoolModifiers { + success_on: 8, + exceptional_on: exceptional_on, + quality: DicePoolQuality::TenAgain, } } + + pub fn custom(quality: DicePoolQuality, exceptional_on: i32) -> DicePoolModifiers { + DicePoolModifiers { + success_on: 8, + exceptional_on: exceptional_on, + quality: quality, + } + } +} + +#[derive(Debug, PartialEq)] +pub struct DicePool { + pub(crate) amounts: Vec, + pub(crate) sides: i32, + pub(crate) modifiers: DicePoolModifiers, +} + +fn calculate_dice_amount(amounts: &Vec) -> Result { + let dice_amount: Result = amounts + .iter() + .map(|amount| match &amount.element { + Element::Number(num_dice) => Ok(*num_dice * amount.operator.mult()), + Element::Variable(variable) => handle_variable(&variable), + }) + .collect::, _>>() + .map(|numbers| numbers.iter().sum()); + + dice_amount +} + +impl DicePool { + pub fn easy_pool(dice_amount: i32, quality: DicePoolQuality) -> DicePool { + DicePool { + amounts: vec![Amount { + operator: Operator::Plus, + element: Element::Number(dice_amount), + }], + sides: 10, + modifiers: DicePoolModifiers::custom_quality(quality), + } + } + + pub fn easy_with_modifiers(dice_amount: i32, modifiers: DicePoolModifiers) -> DicePool { + DicePool { + amounts: vec![Amount { + operator: Operator::Plus, + element: Element::Number(dice_amount), + }], + sides: 10, + modifiers: modifiers, + } + } + + pub fn new(amounts: Vec, modifiers: DicePoolModifiers) -> DicePool { + DicePool { + amounts: amounts, + sides: 10, //TODO make configurable + //TODO make configurable + modifiers: modifiers, + } + } + + pub fn chance_die() -> DicePool { + DicePool::easy_pool(1, DicePoolQuality::ChanceDie) + } +} + +///The result of a successfully executed roll of a dice pool. Does not +///contain the heavy information of the DicePool instance. +pub struct RolledDicePool { + pub(crate) num_dice: i32, + pub(crate) roll: DicePoolRoll, + pub(crate) modifiers: DicePoolModifiers, +} + +impl RolledDicePool { + fn from(pool: &DicePool, num_dice: i32, rolls: Vec) -> RolledDicePool { + RolledDicePool { + modifiers: pool.modifiers, + num_dice: num_dice, + roll: DicePoolRoll { + rolls: rolls, + modifiers: pool.modifiers, + }, + } + } +} + +impl fmt::Display for RolledDicePool { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} dice ({}, exceptional on {} successes)", + self.num_dice, self.modifiers.quality, self.modifiers.exceptional_on + ) + } } ///Store all rolls of the dice pool dice into one struct. #[derive(Debug, PartialEq, Eq, Clone)] pub struct DicePoolRoll { - quality: DicePoolQuality, - success_on: u32, - exceptional_on: u32, - rolls: Vec, + modifiers: DicePoolModifiers, + rolls: Vec, } fn fmt_rolls(pool: &DicePoolRoll) -> String { @@ -96,7 +211,7 @@ fn fmt_rolls(pool: &DicePoolRoll) -> String { } fn fmt_for_failure(pool: &DicePoolRoll) -> String { - match pool.quality { + match pool.modifiers.quality { //There should only be 1 die in a chance die roll. DicePoolQuality::ChanceDie if pool.rolls().first() == Some(&1) => { String::from("dramatic failure!") @@ -106,7 +221,7 @@ fn fmt_for_failure(pool: &DicePoolRoll) -> String { } impl DicePoolRoll { - pub fn rolls(&self) -> &[u32] { + pub fn rolls(&self) -> &[i32] { &self.rolls } @@ -115,20 +230,20 @@ impl DicePoolRoll { .rolls .iter() .cloned() - .filter(|&roll| roll >= self.success_on) + .filter(|&roll| roll >= self.modifiers.success_on) .count(); i32::try_from(successes).unwrap_or(0) } pub fn is_exceptional(&self) -> bool { - self.successes() >= (self.exceptional_on as i32) + self.successes() >= self.modifiers.exceptional_on } } impl Roll for DicePool { - type Output = DicePoolRoll; + type Output = Result; - fn roll(&self) -> DicePoolRoll { + fn roll(&self) -> Result { roll_dice(self, &mut RngDieRoller(rand::thread_rng())) } } @@ -159,14 +274,14 @@ impl fmt::Display for DicePoolRoll { } trait DieRoller { - fn roll_number(&mut self, sides: u32) -> u32; + fn roll_number(&mut self, sides: i32) -> i32; } ///A version of DieRoller that uses a rand::Rng to roll numbers. struct RngDieRoller(R); impl DieRoller for RngDieRoller { - fn roll_number(&mut self, sides: u32) -> u32 { + fn roll_number(&mut self, sides: i32) -> i32 { self.0.gen_range(1, sides + 1) } } @@ -176,9 +291,9 @@ impl DieRoller for RngDieRoller { ///Statistically speaking, usually one result will be returned from this function. fn roll_exploding_die( roller: &mut R, - sides: u32, - explode_on_or_higher: u32, -) -> Vec { + sides: i32, + explode_on_or_higher: i32, +) -> Vec { let mut results = vec![]; loop { let roll = roller.roll_number(sides); @@ -193,7 +308,7 @@ fn roll_exploding_die( ///A die with the rote quality is re-rolled once if the roll fails. Otherwise, it obeys ///all normal rules (re-roll 10s). Re-rolled dice are appended to the result set, so we ///can keep track of the actual dice that were rolled. -fn roll_rote_die(roller: &mut R, sides: u32, success_on: u32) -> Vec { +fn roll_rote_die(roller: &mut R, sides: i32, success_on: i32) -> Vec { let mut rolls = roll_exploding_die(roller, sides, 10); if rolls.len() == 1 && rolls[0] < success_on { @@ -208,15 +323,16 @@ fn roll_rote_die(roller: &mut R, sides: u32, success_on: u32) -> V ///there are multiple 10s). Nine- and eight-again will explode similarly if the result is ///at least that number. Rote quality will re-roll a failure once, while also exploding ///on 10. The function returns a Vec of all rolled dice (usually 1). -fn roll_die(roller: &mut R, pool: &DicePool) -> Vec { +fn roll_die(roller: &mut R, pool: &DicePool) -> Vec { let mut results = vec![]; let sides = pool.sides; + let success_on = pool.modifiers.success_on; - match pool.quality { + match pool.modifiers.quality { DicePoolQuality::TenAgain => results.append(&mut roll_exploding_die(roller, sides, 10)), DicePoolQuality::NineAgain => results.append(&mut roll_exploding_die(roller, sides, 9)), DicePoolQuality::EightAgain => results.append(&mut roll_exploding_die(roller, sides, 8)), - DicePoolQuality::Rote => results.append(&mut roll_rote_die(roller, sides, pool.success_on)), + DicePoolQuality::Rote => results.append(&mut roll_rote_die(roller, sides, success_on)), DicePoolQuality::ChanceDie | DicePoolQuality::NoExplode => { results.push(roller.roll_number(sides)) } @@ -225,19 +341,16 @@ fn roll_die(roller: &mut R, pool: &DicePool) -> Vec { results } +fn handle_variable(_variable: &str) -> Result { + Err(BotError::VariablesNotSupported) +} + ///Roll the dice in a dice pool, according to behavior documented in the various rolling ///methods. -fn roll_dice(pool: &DicePool, roller: &mut R) -> DicePoolRoll { - let rolls: Vec = (0..pool.count) - .flat_map(|_| roll_die(roller, pool)) - .collect(); - - DicePoolRoll { - quality: pool.quality, - rolls: rolls, - exceptional_on: pool.exceptional_success, - success_on: pool.success_on, - } +fn roll_dice(pool: &DicePool, roller: &mut R) -> Result { + let num_dice = calculate_dice_amount(&pool.amounts)?; + let rolls: Vec = (0..num_dice).flat_map(|_| roll_die(roller, pool)).collect(); + Ok(RolledDicePool::from(pool, num_dice, rolls)) } #[cfg(test)] @@ -247,12 +360,12 @@ mod tests { ///Instead of being random, generate a series of numbers we have complete ///control over. struct SequentialDieRoller { - results: Vec, + results: Vec, position: usize, } impl SequentialDieRoller { - fn new(results: Vec) -> SequentialDieRoller { + fn new(results: Vec) -> SequentialDieRoller { SequentialDieRoller { results: results, position: 0, @@ -261,7 +374,7 @@ mod tests { } impl DieRoller for SequentialDieRoller { - fn roll_number(&mut self, _sides: u32) -> u32 { + fn roll_number(&mut self, _sides: i32) -> i32 { let roll = self.results[self.position]; self.position += 1; roll @@ -271,16 +384,18 @@ mod tests { //Sanity checks #[test] pub fn chance_die_has_success_on_10_test() { - assert_eq!( - 10, - DicePool::new(1, 5, DicePoolQuality::ChanceDie).success_on - ); + assert_eq!(10, DicePool::chance_die().modifiers.success_on); } #[test] pub fn non_chance_die_has_success_on_8_test() { fn check_success_on(quality: DicePoolQuality) { - assert_eq!(8, DicePool::new(1, 5, quality).success_on); + let modifiers = DicePoolModifiers::custom_quality(quality); + let amount = vec![Amount { + operator: Operator::Plus, + element: Element::Number(1), + }]; + assert_eq!(8, DicePool::new(amount, modifiers).modifiers.success_on); } check_success_on(DicePoolQuality::TenAgain); @@ -346,22 +461,73 @@ mod tests { assert_eq!(vec![8, 7], rolls); } + #[test] + fn dice_pool_modifiers_chance_die_test() { + let modifiers = DicePoolModifiers::custom_quality(DicePoolQuality::ChanceDie); + assert_eq!(10, modifiers.success_on); + } + + #[test] + fn dice_pool_modifiers_default_sanity_check() { + let modifiers = DicePoolModifiers::default(); + assert_eq!(8, modifiers.success_on); + assert_eq!(5, modifiers.exceptional_on); + assert_eq!(DicePoolQuality::TenAgain, modifiers.quality); + } + #[test] pub fn no_explode_roll_test() { - let pool = DicePool::new(1, 5, DicePoolQuality::NoExplode); + let pool = DicePool::easy_pool(1, DicePoolQuality::NoExplode); let mut roller = SequentialDieRoller::new(vec![10, 8]); - let roll: DicePoolRoll = roll_dice(&pool, &mut roller); + let result = roll_dice(&pool, &mut roller); + assert!(result.is_ok()); + + let roll = result.unwrap().roll; assert_eq!(vec![10], roll.rolls()); } + #[test] + pub fn number_of_dice_equality_test() { + let pool = DicePool::easy_pool(5, DicePoolQuality::NoExplode); + let mut roller = SequentialDieRoller::new(vec![1, 2, 3, 4, 5]); + let result = roll_dice(&pool, &mut roller); + assert!(result.is_ok()); + + let roll = result.unwrap(); + assert_eq!(5, roll.num_dice); + } + //DicePool tests + #[test] + fn easy_pool_chance_die_test() { + let pool = DicePool::easy_pool(1, DicePoolQuality::ChanceDie); + assert_eq!(10, pool.modifiers.success_on); + } + + #[test] + fn easy_pool_quality_test() { + fn check_quality(quality: DicePoolQuality) { + let pool = DicePool::easy_pool(1, quality); + assert_eq!(quality, pool.modifiers.quality); + } + + check_quality(DicePoolQuality::TenAgain); + check_quality(DicePoolQuality::NineAgain); + check_quality(DicePoolQuality::EightAgain); + check_quality(DicePoolQuality::Rote); + check_quality(DicePoolQuality::ChanceDie); + check_quality(DicePoolQuality::NoExplode); + } + #[test] fn is_successful_on_equal_test() { let result = DicePoolRoll { - quality: DicePoolQuality::TenAgain, rolls: vec![8], - exceptional_on: 5, - success_on: 8, + modifiers: DicePoolModifiers { + exceptional_on: 5, + success_on: 8, + quality: DicePoolQuality::TenAgain, + }, }; assert_eq!(1, result.successes()); @@ -370,10 +536,12 @@ mod tests { #[test] fn chance_die_success_test() { let result = DicePoolRoll { - quality: DicePoolQuality::TenAgain, rolls: vec![10], - exceptional_on: 5, - success_on: 10, + modifiers: DicePoolModifiers { + exceptional_on: 5, + success_on: 10, + quality: DicePoolQuality::ChanceDie, + }, }; assert_eq!(1, result.successes()); @@ -382,10 +550,12 @@ mod tests { #[test] fn chance_die_fail_test() { let result = DicePoolRoll { - quality: DicePoolQuality::TenAgain, rolls: vec![9], - exceptional_on: 5, - success_on: 10, + modifiers: DicePoolModifiers { + exceptional_on: 5, + success_on: 10, + quality: DicePoolQuality::ChanceDie, + }, }; assert_eq!(0, result.successes()); @@ -394,10 +564,12 @@ mod tests { #[test] fn is_exceptional_test() { let result = DicePoolRoll { - quality: DicePoolQuality::TenAgain, rolls: vec![8, 8, 9, 10, 8], - exceptional_on: 5, - success_on: 8, + modifiers: DicePoolModifiers { + exceptional_on: 5, + success_on: 8, + quality: DicePoolQuality::TenAgain, + }, }; assert_eq!(5, result.successes()); @@ -407,10 +579,8 @@ mod tests { #[test] fn is_not_exceptional_test() { let result = DicePoolRoll { - quality: DicePoolQuality::TenAgain, rolls: vec![8, 8, 9, 10], - exceptional_on: 5, - success_on: 8, + modifiers: DicePoolModifiers::default(), }; assert_eq!(4, result.successes()); @@ -421,10 +591,8 @@ mod tests { #[test] fn formats_dramatic_failure_test() { let result = DicePoolRoll { - quality: DicePoolQuality::ChanceDie, rolls: vec![1], - exceptional_on: 5, - success_on: 10, + modifiers: DicePoolModifiers::custom_quality(DicePoolQuality::ChanceDie), }; assert_eq!("dramatic failure!", fmt_for_failure(&result)); @@ -433,10 +601,12 @@ mod tests { #[test] fn formats_regular_failure_when_not_chance_die_test() { let result = DicePoolRoll { - quality: DicePoolQuality::TenAgain, rolls: vec![1], - exceptional_on: 5, - success_on: 10, + modifiers: DicePoolModifiers { + quality: DicePoolQuality::TenAgain, + exceptional_on: 5, + success_on: 10, + }, }; assert_eq!("failure!", fmt_for_failure(&result)); @@ -445,10 +615,8 @@ mod tests { #[test] fn formats_lots_of_dice_test() { let result = DicePoolRoll { - quality: DicePoolQuality::TenAgain, + modifiers: DicePoolModifiers::default(), rolls: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9], - exceptional_on: 5, - success_on: 10, }; assert_eq!( diff --git a/src/cofd/parser.rs b/src/cofd/parser.rs index 959d782..6d653b7 100644 --- a/src/cofd/parser.rs +++ b/src/cofd/parser.rs @@ -1,124 +1,205 @@ -use nom::{ - alt, bytes::complete::tag, character::complete::digit1, complete, many0, named, - sequence::tuple, tag, IResult, -}; +use crate::cofd::dice::{Amount, DicePool, DicePoolModifiers, DicePoolQuality, Element, Operator}; +use crate::error::BotError; +use combine::error::StringStreamError; +use combine::parser::char::{digit, letter, spaces, string}; +use combine::{choice, count, many, many1, one_of, Parser}; -use crate::cofd::dice::{DicePool, DicePoolQuality}; -use crate::parser::eat_whitespace; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum DicePoolElement { - NumberOfDice(u32), - SuccessesForExceptional(u32), - DicePoolQuality(DicePoolQuality), +#[derive(Debug, Clone, Copy, PartialEq)] +enum ParsedInfo { + Quality(DicePoolQuality), + ExceptionalOn(i32), } -// Parse a single digit expression. Does not eat whitespace -fn parse_digit(input: &str) -> IResult<&str, u32> { - let (input, num) = digit1(input)?; - Ok((input, num.parse().unwrap())) +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DiceParsingError { + InvalidAmount, + InvalidModifiers, + UnconsumedInput, } -fn parse_quality(input: &str) -> IResult<&str, DicePoolQuality> { - let (input, _) = eat_whitespace(input)?; - named!(quality(&str) -> DicePoolQuality, alt!( - complete!(tag!("n")) => { |_| DicePoolQuality::NineAgain } | - complete!(tag!("e")) => { |_| DicePoolQuality::EightAgain } | - complete!(tag!("r")) => { |_| DicePoolQuality::Rote } | - complete!(tag!("x")) => { |_| DicePoolQuality::NoExplode } - )); - - let (input, dice_pool_quality) = quality(input)?; - Ok((input, dice_pool_quality)) +impl std::fmt::Display for DiceParsingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } } -fn parse_exceptional_requirement(input: &str) -> IResult<&str, u32> { - let (input, _) = eat_whitespace(input)?; - let (input, (_, successes)) = tuple((tag("s"), digit1))(input)?; - Ok((input, successes.parse().unwrap())) +impl std::error::Error for DiceParsingError { + fn description(&self) -> &str { + self.as_str() + } } -// Parse a dice pool element expression. Eats whitespace. -fn parse_dice_pool_element(input: &str) -> IResult<&str, DicePoolElement> { - let (input, _) = eat_whitespace(input)?; - named!(element(&str) -> DicePoolElement, alt!( - parse_digit => { |num| DicePoolElement::NumberOfDice(num) } | - parse_quality => { |qual| DicePoolElement::DicePoolQuality(qual) } | - parse_exceptional_requirement => { |succ| DicePoolElement::SuccessesForExceptional(succ) } - )); - - let (input, element) = element(input)?; - Ok((input, element)) -} - -fn find_elements(elements: Vec) -> (Option, DicePoolQuality, u32) { - let mut found_quality: Option = None; - let mut found_count: Option = None; - let mut found_successes_required: Option = None; - - for element in elements { - if found_quality.is_some() && found_count.is_some() && found_successes_required.is_some() { - break; +impl DiceParsingError { + fn as_str(&self) -> &str { + use self::DiceParsingError::*; + match *self { + InvalidAmount => "invalid amount of dice", + InvalidModifiers => "dice pool modifiers not specified properly", + UnconsumedInput => "extraneous input detected", } - - match element { - DicePoolElement::NumberOfDice(found) => { - if found_count.is_none() { - found_count = Some(found); - } - } - DicePoolElement::DicePoolQuality(found) => { - if found_quality.is_none() { - found_quality = Some(found); - } - } - DicePoolElement::SuccessesForExceptional(found) => { - if found_successes_required.is_none() { - found_successes_required = Some(found); - } - } - }; } - - let quality: DicePoolQuality = found_quality.unwrap_or(DicePoolQuality::TenAgain); - let successes_for_exceptional: u32 = found_successes_required.unwrap_or(5); - (found_count, quality, successes_for_exceptional) } -fn convert_to_dice_pool(input: &str, elements: Vec) -> IResult<&str, DicePool> { - let (count, quality, successes_for_exceptional) = find_elements(elements); +pub fn parse_modifiers(input: &str) -> Result { + if input.len() == 0 { + return Ok(DicePoolModifiers::default()); + } - if count.is_some() { - Ok(( - input, - DicePool::new(count.unwrap(), successes_for_exceptional, quality), + let input = input.trim(); + + let quality = one_of("nerx".chars()) + .skip(spaces().silent()) + .map(|quality| match quality { + 'n' => ParsedInfo::Quality(DicePoolQuality::NineAgain), + 'e' => ParsedInfo::Quality(DicePoolQuality::EightAgain), + 'r' => ParsedInfo::Quality(DicePoolQuality::Rote), + 'x' => ParsedInfo::Quality(DicePoolQuality::NoExplode), + _ => ParsedInfo::Quality(DicePoolQuality::TenAgain), //TODO add warning log + }); + + let exceptional_on = string("s") + .and(many1(digit())) + .map(|s| s.1) //Discard the s; only need the number + .skip(spaces().silent()) + .map(|num_as_str: String| { + ParsedInfo::ExceptionalOn(match num_as_str.parse::() { + Ok(success_on) => success_on, + Err(_) => 5, //TODO add warning log + }) + }); + + let mut parser = count(2, choice((quality, exceptional_on))) + .skip(spaces().silent()) + .map(|modifiers: Vec| modifiers); + + let (result, rest) = parser.parse(input)?; + + if rest.len() == 0 { + convert_to_info(&result) + } else { + Err(BotError::DiceParsingError( + DiceParsingError::UnconsumedInput, )) - } else { - use nom::error::ErrorKind; - use nom::Err; - Err(Err::Error((input, ErrorKind::Alt))) } } -pub fn parse_dice_pool(input: &str) -> IResult<&str, DicePool> { - named!(first_element(&str) -> DicePoolElement, alt!( - parse_dice_pool_element => { |e| e } - )); - let (input, first) = first_element(input)?; - let (input, elements) = if input.trim().is_empty() { - (input, vec![first]) +fn convert_to_info(parsed: &Vec) -> Result { + use ParsedInfo::*; + if parsed.len() == 0 { + Ok(DicePoolModifiers::default()) + } else if parsed.len() == 1 { + match parsed[0] { + ExceptionalOn(exceptional_on) => { + Ok(DicePoolModifiers::custom_exceptional_on(exceptional_on)) + } + Quality(quality) => Ok(DicePoolModifiers::custom_quality(quality)), + } + } else if parsed.len() == 2 { + match parsed[..] { + [ExceptionalOn(exceptional_on), Quality(quality)] => { + Ok(DicePoolModifiers::custom(quality, exceptional_on)) + } + [Quality(quality), ExceptionalOn(exceptional_on)] => { + Ok(DicePoolModifiers::custom(quality, exceptional_on)) + } + _ => Err(DiceParsingError::InvalidModifiers.into()), + } } else { - named!(rest_elements(&str) -> Vec, many0!(parse_dice_pool_element)); - let (input, mut rest) = rest_elements(input)?; - rest.insert(0, first); - (input, rest) - }; - - convert_to_dice_pool(input, elements) + //We don't expect this clause to be hit, because the parser works 0 to 2 times. + Err(DiceParsingError::InvalidModifiers.into()) + } } -pub fn create_chance_die() -> IResult<&'static str, DicePool> { - Ok(("", DicePool::chance_die())) +/// Parse dice pool amounts into elements coupled with operators, +/// where an operator is "+" or "-", and an element is either a number +/// or variable name. The first element should not have an operator, +/// but every one after that should. Accepts expressions like "8", "10 +/// + variablename", "variablename - 3", etc. +fn parse_pool_amount(input: &str) -> Result, BotError> { + let input = input.trim(); + + let plus_or_minus = one_of("+-".chars()); + let maybe_sign = plus_or_minus.map(|sign: char| match sign { + '+' => Operator::Plus, + '-' => Operator::Minus, + _ => Operator::Plus, + }); + + //TODO make this a macro or something + let first = many1(letter()) + .or(many1(digit())) + .skip(spaces().silent()) //Consume any space after first amount + .map(|value: String| match value.parse::() { + Ok(num) => Amount { + operator: Operator::Plus, + element: Element::Number(num), + }, + _ => Amount { + operator: Operator::Plus, + element: Element::Variable(value), + }, + }); + + let variable_or_number = + many1(letter()) + .or(many1(digit())) + .map(|value: String| match value.parse::() { + Ok(num) => Element::Number(num), + _ => Element::Variable(value), + }); + + let sign_and_word = maybe_sign + .skip(spaces().silent()) + .and(variable_or_number) + .skip(spaces().silent()) + .map(|parsed: (Operator, Element)| Amount { + operator: parsed.0, + element: parsed.1, + }); + + let rest = many(sign_and_word).map(|expr: Vec<_>| expr); + + let mut parser = first.and(rest); + + //Maps the found expression into a Vec of Amount instances, + //tacking the first one on. + type ParsedAmountExpr = (Amount, Vec); + let (results, rest) = parser + .parse(input) + .map(|mut results: (ParsedAmountExpr, &str)| { + let mut amounts = vec![(results.0).0]; + amounts.append(&mut (results.0).1); + (amounts, results.1) + })?; + + if rest.len() == 0 { + Ok(results) + } else { + Err(BotError::DiceParsingError( + DiceParsingError::UnconsumedInput, + )) + } +} + +pub fn parse_dice_pool(input: &str) -> Result { + //The "modifiers:" part is optional. Assume amounts if no modifier + //section found. + let split = input.split(":").collect::>(); + let (modifiers_str, amounts_str) = (match split[..] { + [amounts] => Ok(("", amounts)), + [modifiers, amounts] => Ok((modifiers, amounts)), + _ => Err(BotError::DiceParsingError( + DiceParsingError::UnconsumedInput, + )), + })?; + + let modifiers = parse_modifiers(modifiers_str)?; + let amounts = parse_pool_amount(&amounts_str)?; + Ok(DicePool::new(amounts, modifiers)) +} + +pub fn create_chance_die() -> Result { + Ok(DicePool::chance_die()) } #[cfg(test)] @@ -126,109 +207,206 @@ mod tests { use super::*; #[test] - fn parse_digit_test() { - use nom::error::ErrorKind; - use nom::Err; - assert_eq!(parse_digit("1"), Ok(("", 1))); - assert_eq!(parse_digit("10"), Ok(("", 10))); + fn parse_single_number_amount_test() { + let result = parse_pool_amount("1"); + assert!(result.is_ok()); assert_eq!( - parse_digit("adsf"), - Err(Err::Error(("adsf", ErrorKind::Digit))) + result.unwrap(), + vec![Amount { + operator: Operator::Plus, + element: Element::Number(1) + }] ); + + let result = parse_pool_amount("10"); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + vec![Amount { + operator: Operator::Plus, + element: Element::Number(10) + }] + ); + } + + #[test] + fn parse_single_variable_amount_test() { + let result = parse_pool_amount("asdf"); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + vec![Amount { + operator: Operator::Plus, + element: Element::Variable("asdf".to_string()) + }] + ); + + let result = parse_pool_amount("nosis"); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + vec![Amount { + operator: Operator::Plus, + element: Element::Variable("nosis".to_string()) + }] + ); + } + + #[test] + fn parse_complex_amount_expression() { + assert!(parse_pool_amount("1 + myvariable - 2").is_ok()); } #[test] fn quality_test() { - use nom::error::ErrorKind; - use nom::Err; - assert_eq!(parse_quality("n"), Ok(("", DicePoolQuality::NineAgain))); - assert_eq!(parse_quality("e"), Ok(("", DicePoolQuality::EightAgain))); - assert_eq!(parse_quality("r"), Ok(("", DicePoolQuality::Rote))); - assert_eq!(parse_quality("x"), Ok(("", DicePoolQuality::NoExplode))); - assert_eq!(parse_quality("b"), Err(Err::Error(("b", ErrorKind::Alt)))); + let result = parse_modifiers("n"); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + DicePoolModifiers::custom_quality(DicePoolQuality::NineAgain) + ); + + let result = parse_modifiers("e"); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + DicePoolModifiers::custom_quality(DicePoolQuality::EightAgain) + ); + + let result = parse_modifiers("r"); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + DicePoolModifiers::custom_quality(DicePoolQuality::Rote) + ); + + let result = parse_modifiers("x"); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + DicePoolModifiers::custom_quality(DicePoolQuality::NoExplode) + ); + + let result = parse_modifiers("b"); + assert!(result.is_err()); + assert!(matches!( + result, + Err(BotError::DiceParsingError( + DiceParsingError::UnconsumedInput + )) + )); } #[test] - fn multiple_quality_test() { - assert_eq!(parse_quality("ner"), Ok(("er", DicePoolQuality::NineAgain))); + fn multiple_quality_failure_test() { + let result = parse_modifiers("ne"); + assert!(result.is_err()); + assert!(matches!( + result, + Err(BotError::DiceParsingError( + DiceParsingError::InvalidModifiers + )) + )); } #[test] fn exceptional_success_test() { - use nom::error::ErrorKind; - use nom::Err; - assert_eq!(parse_exceptional_requirement("s3"), Ok(("", 3))); - assert_eq!(parse_exceptional_requirement("s10"), Ok(("", 10))); - assert_eq!(parse_exceptional_requirement("s20b"), Ok(("b", 20))); - assert_eq!( - parse_exceptional_requirement("sab10"), - Err(Err::Error(("ab10", ErrorKind::Digit))) - ); - } - - #[test] - fn dice_pool_element_expression_test() { - use nom::error::ErrorKind; - use nom::Err; + let result = parse_modifiers("s3"); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), DicePoolModifiers::custom_exceptional_on(3)); + let result = parse_modifiers("s33"); + assert!(result.is_ok()); assert_eq!( - parse_dice_pool_element("8"), - Ok(("", DicePoolElement::NumberOfDice(8))) + result.unwrap(), + DicePoolModifiers::custom_exceptional_on(33) ); - assert_eq!( - parse_dice_pool_element("n"), - Ok(( - "", - DicePoolElement::DicePoolQuality(DicePoolQuality::NineAgain) + let result = parse_modifiers("s3q"); + assert!(result.is_err()); + assert!(matches!( + result, + Err(BotError::DiceParsingError( + DiceParsingError::UnconsumedInput )) - ); - - assert_eq!( - parse_dice_pool_element("s3"), - Ok(("", DicePoolElement::SuccessesForExceptional(3))) - ); - - assert_eq!( - parse_dice_pool_element("8ns3"), - Ok(("ns3", DicePoolElement::NumberOfDice(8))) - ); - - assert_eq!( - parse_dice_pool_element("totallynotvalid"), - Err(Err::Error(("totallynotvalid", ErrorKind::Alt))) - ); + )); } #[test] fn dice_pool_number_only_test() { + let result = parse_dice_pool("8"); + assert!(result.is_ok()); assert_eq!( - parse_dice_pool("8"), - Ok(("", DicePool::new(8, 5, DicePoolQuality::TenAgain))) + result.unwrap(), + DicePool::easy_pool(8, DicePoolQuality::TenAgain) ); } #[test] fn dice_pool_number_with_quality() { + let result = parse_dice_pool("n:8"); + assert!(result.is_ok()); assert_eq!( - parse_dice_pool("8n"), - Ok(("", DicePool::new(8, 5, DicePoolQuality::NineAgain))) + result.unwrap(), + DicePool::easy_pool(8, DicePoolQuality::NineAgain) ); } #[test] fn dice_pool_number_with_success_change() { - assert_eq!( - parse_dice_pool("8s3"), - Ok(("", DicePool::new(8, 3, DicePoolQuality::TenAgain))) - ); + let modifiers = DicePoolModifiers::custom_exceptional_on(3); + let result = parse_dice_pool("s3:8"); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), DicePool::easy_with_modifiers(8, modifiers)); } #[test] fn dice_pool_with_quality_and_success_change() { - assert_eq!( - parse_dice_pool("8rs3"), - Ok(("", DicePool::new(8, 3, DicePoolQuality::Rote))) - ); + let modifiers = DicePoolModifiers::custom(DicePoolQuality::Rote, 3); + let result = parse_dice_pool("rs3:8"); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), DicePool::easy_with_modifiers(8, modifiers)); + } + + #[test] + fn dice_pool_complex_expression_test() { + let modifiers = DicePoolModifiers::custom(DicePoolQuality::Rote, 3); + let amounts = vec![ + Amount { + operator: Operator::Plus, + element: Element::Number(8), + }, + Amount { + operator: Operator::Plus, + element: Element::Number(10), + }, + Amount { + operator: Operator::Minus, + element: Element::Number(2), + }, + Amount { + operator: Operator::Plus, + element: Element::Variable("varname".to_owned()), + }, + ]; + + let expected = DicePool::new(amounts, modifiers); + + let result = parse_dice_pool("rs3:8+10-2+varname"); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected); + + let result = parse_dice_pool("rs3:8+10- 2 + varname"); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected); + + let result = parse_dice_pool("rs3 : 8+ 10 -2 + varname"); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected); + + //This one has tabs in it. + let result = parse_dice_pool(" r s3 : 8 + 10 -2 + varname"); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected); } } diff --git a/src/commands.rs b/src/commands.rs index 85e2519..7153c82 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,5 +1,6 @@ use crate::cofd::dice::DicePool; use crate::dice::ElementExpression; +use crate::error::BotError; use crate::help::HelpTopic; use crate::roll::Roll; @@ -51,12 +52,24 @@ impl Command for PoolRollCommand { } fn execute(&self) -> Execution { - let roll = self.0.roll(); - let plain = format!("Pool: {}\nResult: {}", self.0, roll); - let html = format!( - "

Pool: {}

Result: {}

", - self.0, roll - ); + let roll_result = self.0.roll(); + + let (plain, html) = match roll_result { + Ok(rolled_pool) => { + let plain = format!("Pool: {}\nResult: {}", rolled_pool, rolled_pool.roll); + let html = format!( + "

Pool: {}

Result: {}

", + rolled_pool, rolled_pool.roll + ); + (plain, html) + } + Err(e) => { + let plain = format!("Error: {}", e); + let html = format!("

Error: {}

", e); + (plain, html) + } + }; + Execution { plain, html } } } @@ -83,20 +96,21 @@ impl Command for HelpCommand { /// Parse a command string into a dynamic command execution trait /// object. Returns an error if a command was recognized but not /// parsed correctly. Returns Ok(None) if no command was recognized. -pub fn parse_command(s: &str) -> Result>, String> { - match parser::parse_command(s) { - Ok((input, command)) => match (input, &command) { - //Any command, or text transformed into non-command is - //sent upwards. - ("", Some(_)) | (_, None) => Ok(command), +pub fn parse_command(s: &str) -> Result>, BotError> { + // match parser::parse_command(s) { + // Ok(Some(command)) => match &command { + // //Any command, or text transformed into non-command is + // //sent upwards. + // ("", Some(_)) | (_, None) => Ok(command), - //TODO replcae with nom all_consuming? - //Any unconsumed input (whitespace should already be - // stripped) is considered a parsing error. - _ => Err(format!("{}: malformed expression", s)), - }, - Err(err) => Err(err.to_string()), - } + // //TODO replcae with nom all_consuming? + // //Any unconsumed input (whitespace should already be + // // stripped) is considered a parsing error. + // _ => Err(format!("{}: malformed expression", s)), + // }, + // Err(err) => Err(err), + // } + parser::parse_command(s) } #[cfg(test)] @@ -122,13 +136,13 @@ mod tests { #[test] fn pool_whitespace_test() { - assert!(parse_command("!pool 8ns3 ") + assert!(parse_command("!pool ns3:8 ") .map(|p| p.is_some()) .expect("was error")); - assert!(parse_command(" !pool 8ns3") + assert!(parse_command(" !pool ns3:8") .map(|p| p.is_some()) .expect("was error")); - assert!(parse_command(" !pool 8ns3 ") + assert!(parse_command(" !pool ns3:8 ") .map(|p| p.is_some()) .expect("was error")); } diff --git a/src/commands/parser.rs b/src/commands/parser.rs index 3ad9c6a..fb01daa 100644 --- a/src/commands/parser.rs +++ b/src/commands/parser.rs @@ -1,68 +1,84 @@ use crate::cofd::parser::{create_chance_die, parse_dice_pool}; use crate::commands::{Command, HelpCommand, PoolRollCommand, RollCommand}; use crate::dice::parser::parse_element_expression; +use crate::error::BotError; use crate::help::parse_help_topic; -use nom::bytes::streaming::tag; -use nom::error::ErrorKind as NomErrorKind; +use combine::parser::char::{char, letter, space}; +use combine::{any, many1, optional, Parser}; use nom::Err as NomErr; -use nom::{character::complete::alpha1, IResult}; // Parse a roll expression. -fn parse_roll(input: &str) -> IResult<&str, Box> { - let (input, expression) = parse_element_expression(input)?; - Ok((input, Box::new(RollCommand(expression)))) +fn parse_roll(input: &str) -> Result, BotError> { + let result = parse_element_expression(input); + match result { + Ok((rest, expression)) if rest.len() == 0 => Ok(Box::new(RollCommand(expression))), + //Legacy code boundary translates nom errors into BotErrors. + Ok(_) => Err(BotError::NomParserIncomplete), + Err(NomErr::Error(e)) => Err(BotError::NomParserError(e.1)), + Err(NomErr::Failure(e)) => Err(BotError::NomParserError(e.1)), + Err(NomErr::Incomplete(_)) => Err(BotError::NomParserIncomplete), + } } -fn parse_pool_roll(input: &str) -> IResult<&str, Box> { - let (input, pool) = parse_dice_pool(input)?; - Ok((input, Box::new(PoolRollCommand(pool)))) +fn parse_pool_roll(input: &str) -> Result, BotError> { + let pool = parse_dice_pool(input)?; + Ok(Box::new(PoolRollCommand(pool))) } -fn chance_die() -> IResult<&'static str, Box> { - let (input, pool) = create_chance_die()?; - Ok((input, Box::new(PoolRollCommand(pool)))) +fn chance_die() -> Result, BotError> { + let pool = create_chance_die()?; + Ok(Box::new(PoolRollCommand(pool))) } -fn help(topic: &str) -> IResult<&str, Box> { +fn help(topic: &str) -> Result, BotError> { let topic = parse_help_topic(topic); - Ok(("", Box::new(HelpCommand(topic)))) + Ok(Box::new(HelpCommand(topic))) } /// Split an input string into its constituent command and "everything /// else" parts. Extracts the command separately from its input (i.e. /// rest of the line) and returns a tuple of (command_input, command). /// Whitespace at the start and end of the command input is removed. -fn split_command(input: &str) -> IResult<&str, &str> { - let input = input.trim_start(); - let (input, _) = tag("!")(input)?; +fn split_command(input: &str) -> Result<(String, String), BotError> { + let input = input.trim(); - let (mut command_input, command) = alpha1(input)?; - command_input = command_input.trim(); - Ok((command_input, command)) + let exclamation = char('!'); + let word = many1(letter()).map(|value: String| value); + let at_least_one_space = many1(space().silent()).map(|value: String| value); + let cmd_input = optional(at_least_one_space.and(many1(any()).map(|value: String| value))); + + let mut parser = exclamation.and(word).and(cmd_input); + + //TODO make less wacky, possibly by mapping it into a struct and + // making use of skip. This super-wacky tuple is: + // (parsed_input, rest) + //Where parsed_input is: + // (!command, option) + //Where !command is: + // ('!', command) + //Were option is: + // Option tuple of (whitespace, arguments) + let (command, command_input) = match parser.parse(input)? { + (((_, command), Some((_, command_input))), _) => (command, command_input), + (((_, command), None), _) => (command, "".to_string()), + }; + + Ok((command, command_input)) } /// Potentially parse a command expression. If we recognize the /// command, an error should be raised if the command is misparsed. If /// we don't recognize the command, ignore it and return None. -pub fn parse_command(input: &str) -> IResult<&str, Option>> { +pub fn parse_command(input: &str) -> Result>, BotError> { match split_command(input) { - Ok((cmd_input, cmd)) => match cmd { - "r" | "roll" => parse_roll(cmd_input).map(|(input, command)| (input, Some(command))), - "rp" | "pool" => { - parse_pool_roll(cmd_input).map(|(input, command)| (input, Some(command))) - } - "chance" => chance_die().map(|(input, command)| (input, Some(command))), - "help" => help(cmd_input).map(|(input, command)| (input, Some(command))), + Ok((cmd, cmd_input)) => match cmd.as_ref() { + "r" | "roll" => parse_roll(&cmd_input).map(|command| Some(command)), + "rp" | "pool" => parse_pool_roll(&cmd_input).map(|command| Some(command)), + "chance" => chance_die().map(|command| Some(command)), + "help" => help(&cmd_input).map(|command| Some(command)), // No recognized command, ignore this. - _ => Ok((input, None)), + _ => Ok(None), }, - - //TODO better way to do this? - //If the input is not a command, or the message is incomplete - //(empty), we declare this to be a non-command, and don't do - //anything else with it. - Err(NomErr::Error((_, NomErrorKind::Tag))) | Err(NomErr::Incomplete(_)) => Ok(("", None)), - //All other errors passed up. Err(e) => Err(e), } @@ -72,18 +88,19 @@ pub fn parse_command(input: &str) -> IResult<&str, Option>> { mod tests { use super::*; + //TODO these errors don't seem to implement the right traits to do + //eq checks or even unwrap_err! + #[test] fn non_command_test() { let result = parse_command("not a command"); - assert!(result.is_ok()); - assert!(result.unwrap().1.is_none()); + assert!(result.is_err()); } #[test] fn empty_message_test() { let result = parse_command(""); - assert!(result.is_ok()); - assert!(result.unwrap().1.is_none()); + assert!(result.is_err()); } #[test] @@ -95,22 +112,19 @@ mod tests { #[test] fn word_with_exclamation_mark_test() { let result1 = parse_command("hello !notacommand"); - assert!(result1.is_ok()); - assert!(result1.unwrap().1.is_none()); + assert!(result1.is_err()); let result2 = parse_command("hello!"); - assert!(result2.is_ok()); - assert!(result2.unwrap().1.is_none()); + assert!(result2.is_err()); let result3 = parse_command("hello!notacommand"); - assert!(result3.is_ok()); - assert!(result3.unwrap().1.is_none()); + assert!(result3.is_err()); } #[test] fn basic_command_test() { assert_eq!( - ("1d4", "roll"), + ("roll".to_string(), "1d4".to_string()), split_command("!roll 1d4").expect("got parsing error") ); } @@ -118,7 +132,7 @@ mod tests { #[test] fn whitespace_at_start_test() { assert_eq!( - ("1d4", "roll"), + ("roll".to_string(), "1d4".to_string()), split_command(" !roll 1d4").expect("got parsing error") ); } @@ -126,7 +140,7 @@ mod tests { #[test] fn whitespace_at_end_test() { assert_eq!( - ("1d4", "roll"), + ("roll".to_string(), "1d4".to_string()), split_command("!roll 1d4 ").expect("got parsing error") ); } @@ -134,7 +148,7 @@ mod tests { #[test] fn whitespace_on_both_ends_test() { assert_eq!( - ("1d4", "roll"), + ("roll".to_string(), "1d4".to_string()), split_command(" !roll 1d4 ").expect("got parsing error") ); } @@ -142,12 +156,12 @@ mod tests { #[test] fn single_command_test() { assert_eq!( - ("", "roll"), + ("roll".to_string(), "".to_string()), split_command("!roll").expect("got parsing error") ); assert_eq!( - ("", "thisdoesnotexist"), + ("thisdoesnotexist".to_string(), "".to_string()), split_command("!thisdoesnotexist").expect("got parsing error") ); } diff --git a/src/error.rs b/src/error.rs index bbe0122..2afbde8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -33,4 +33,19 @@ pub enum BotError { #[error("i/o error")] IoError(#[from] std::io::Error), + + #[error("parsing error")] + ParserError(#[from] combine::error::StringStreamError), + + #[error("dice parsing error")] + DiceParsingError(#[from] crate::cofd::parser::DiceParsingError), + + #[error("legacy parsing error")] + NomParserError(nom::error::ErrorKind), + + #[error("legacy parsing error: not enough data")] + NomParserIncomplete, + + #[error("variables not yet supported")] + VariablesNotSupported, }