Implement parsing of Cthulhu dice, only basic for now.

Does not understand anything besides single numbers at the moment.
This commit is contained in:
projectmoon 2020-10-31 14:03:18 +00:00 committed by ProjectMoon
parent c290393ddf
commit 08b0e58193
6 changed files with 163 additions and 29 deletions

View File

@ -11,7 +11,7 @@ impl Command for CthRoll {
"roll percentile pool" "roll percentile pool"
} }
async fn execute(&self, ctx: &Context) -> Execution { async fn execute(&self, _ctx: &Context) -> Execution {
//TODO this will be converted to a result when supporting variables. //TODO this will be converted to a result when supporting variables.
let roll = self.0.roll(); let roll = self.0.roll();
let plain = format!("Roll: {}\nResult: {}", self.0, roll); let plain = format!("Roll: {}\nResult: {}", self.0, roll);
@ -25,3 +25,22 @@ impl Command for CthRoll {
} }
pub struct CthAdvanceRoll(pub AdvancementRoll); pub struct CthAdvanceRoll(pub AdvancementRoll);
#[async_trait]
impl Command for CthAdvanceRoll {
fn name(&self) -> &'static str {
"roll percentile pool"
}
async fn execute(&self, _ctx: &Context) -> Execution {
//TODO this will be converted to a result when supporting variables.
let roll = self.0.roll();
let plain = format!("Roll: {}\nResult: {}", self.0, roll);
let html = format!(
"<p><strong>Roll:</strong> {}</p><p><strong>Result</strong>: {}</p>",
self.0, roll
);
Execution { plain, html }
}
}

View File

@ -2,14 +2,14 @@ use crate::cofd::parser::{create_chance_die, parse_dice_pool};
use crate::commands::{ use crate::commands::{
basic_rolling::RollCommand, basic_rolling::RollCommand,
cofd::PoolRollCommand, cofd::PoolRollCommand,
cthulhu::CthRoll, cthulhu::{CthAdvanceRoll, CthRoll},
misc::HelpCommand, misc::HelpCommand,
variables::{ variables::{
DeleteVariableCommand, GetAllVariablesCommand, GetVariableCommand, SetVariableCommand, DeleteVariableCommand, GetAllVariablesCommand, GetVariableCommand, SetVariableCommand,
}, },
Command, Command,
}; };
use crate::cthulhu::dice::{DiceRoll, DiceRollModifier}; use crate::cthulhu::parser::{parse_advancement_roll, parse_regular_roll};
use crate::dice::parser::parse_element_expression; use crate::dice::parser::parse_element_expression;
use crate::error::BotError; use crate::error::BotError;
use crate::help::parse_help_topic; use crate::help::parse_help_topic;
@ -57,13 +57,15 @@ fn parse_pool_roll(input: &str) -> Result<Box<dyn Command>, BotError> {
} }
fn parse_cth_roll(input: &str) -> Result<Box<dyn Command>, BotError> { fn parse_cth_roll(input: &str) -> Result<Box<dyn Command>, BotError> {
let roll = DiceRoll { let roll = parse_regular_roll(input)?;
target: 50,
modifier: DiceRollModifier::Normal,
};
Ok(Box::new(CthRoll(roll))) Ok(Box::new(CthRoll(roll)))
} }
fn parse_cth_advancement_roll(input: &str) -> Result<Box<dyn Command>, BotError> {
let roll = parse_advancement_roll(input)?;
Ok(Box::new(CthAdvanceRoll(roll)))
}
fn chance_die() -> Result<Box<dyn Command>, BotError> { fn chance_die() -> Result<Box<dyn Command>, BotError> {
let pool = create_chance_die()?; let pool = create_chance_die()?;
Ok(Box::new(PoolRollCommand(pool))) Ok(Box::new(PoolRollCommand(pool)))
@ -121,7 +123,10 @@ pub fn parse_command(input: &str) -> Result<Option<Box<dyn Command>>, BotError>
"del" => parse_delete_variable_command(&cmd_input).map(|command| Some(command)), "del" => parse_delete_variable_command(&cmd_input).map(|command| Some(command)),
"r" | "roll" => parse_roll(&cmd_input).map(|command| Some(command)), "r" | "roll" => parse_roll(&cmd_input).map(|command| Some(command)),
"rp" | "pool" => parse_pool_roll(&cmd_input).map(|command| Some(command)), "rp" | "pool" => parse_pool_roll(&cmd_input).map(|command| Some(command)),
"cthroll" => parse_cth_roll(&cmd_input).map(|command| Some(command)), "cthroll" | "cthRoll" => parse_cth_roll(&cmd_input).map(|command| Some(command)),
"cthadv" | "cthARoll" => {
parse_cth_advancement_roll(&cmd_input).map(|command| Some(command))
}
"chance" => chance_die().map(|command| Some(command)), "chance" => chance_die().map(|command| Some(command)),
"help" => help(&cmd_input).map(|command| Some(command)), "help" => help(&cmd_input).map(|command| Some(command)),
// No recognized command, ignore this. // No recognized command, ignore this.

View File

@ -1 +1,2 @@
pub mod dice; pub mod dice;
pub mod parser;

View File

@ -1,7 +1,7 @@
use std::fmt; use std::fmt;
/// A planned dice roll. /// A planned dice roll.
#[derive(Clone, Copy)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct DiceRoll { pub struct DiceRoll {
pub target: u32, pub target: u32,
pub modifier: DiceRollModifier, pub modifier: DiceRollModifier,
@ -9,14 +9,14 @@ pub struct DiceRoll {
impl fmt::Display for DiceRoll { impl fmt::Display for DiceRoll {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = format!("target: {}, modifiers: {}", self.target, self.modifier); let message = format!("target: {}, with {}", self.target, self.modifier);
write!(f, "{}", message)?; write!(f, "{}", message)?;
Ok(()) Ok(())
} }
} }
/// Potential modifier on the die roll to be made. /// Potential modifier on the die roll to be made.
#[derive(Clone, Copy)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum DiceRollModifier { pub enum DiceRollModifier {
/// No bonuses or penalties. /// No bonuses or penalties.
Normal, Normal,
@ -37,11 +37,11 @@ pub enum DiceRollModifier {
impl fmt::Display for DiceRollModifier { impl fmt::Display for DiceRollModifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self { let message = match self {
Self::Normal => "none", Self::Normal => "no modifiers",
Self::OneBonus => "one bonus", Self::OneBonus => "one bonus die",
Self::TwoBonus => "two bonus", Self::TwoBonus => "two bonus dice",
Self::OnePenalty => "one penalty", Self::OnePenalty => "one penalty die",
Self::TwoPenalty => "two penalty", Self::TwoPenalty => "two penalty dice",
}; };
write!(f, "{}", message)?; write!(f, "{}", message)?;
@ -100,6 +100,7 @@ pub struct RolledDice {
target: u32, target: u32,
/// Stored for informational purposes in display. /// Stored for informational purposes in display.
#[allow(dead_code)]
modifier: DiceRollModifier, modifier: DiceRollModifier,
} }
@ -141,15 +142,25 @@ impl fmt::Display for RolledDice {
/// A planned advancement roll, where the target number is the /// A planned advancement roll, where the target number is the
/// existing skill amount. /// existing skill amount.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AdvancementRoll { pub struct AdvancementRoll {
/// The amount (0 to 100) of the existing skill. We must beat this /// The amount (0 to 100) of the existing skill. We must beat this
/// target number to advance the skill, or roll above a 95. /// target number to advance the skill, or roll above a 95.
pub existing_skill: u32, pub existing_skill: u32,
} }
impl fmt::Display for AdvancementRoll {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = format!("advancement for skill of {}", self.existing_skill);
write!(f, "{}", message)?;
Ok(())
}
}
/// A completed advancement roll. /// A completed advancement roll.
pub struct RolledAdvancement { pub struct RolledAdvancement {
existing_skill: u32, existing_skill: u32,
num_rolled: u32,
advancement: u32, advancement: u32,
successful: bool, successful: bool,
} }
@ -173,6 +184,27 @@ impl RolledAdvancement {
} }
} }
impl fmt::Display for RolledAdvancement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = if self.successful {
format!(
"success! new skill is {} (advanced by {}).",
self.new_skill_amount(),
self.advancement
)
} else {
format!("failure! skill remains at {}", self.existing_skill)
};
write!(
f,
"rolled {} against {}: {}",
self.num_rolled, self.existing_skill, message
)?;
Ok(())
}
}
trait DieRoller { trait DieRoller {
fn roll(&mut self) -> u32; fn roll(&mut self) -> u32;
} }
@ -248,12 +280,14 @@ impl AdvancementRoll {
if percentile_roll < self.existing_skill || percentile_roll > 95 { if percentile_roll < self.existing_skill || percentile_roll > 95 {
RolledAdvancement { RolledAdvancement {
num_rolled: percentile_roll,
existing_skill: self.existing_skill, existing_skill: self.existing_skill,
advancement: roller.roll() + 1, advancement: roller.roll() + 1,
successful: true, successful: true,
} }
} else { } else {
RolledAdvancement { RolledAdvancement {
num_rolled: percentile_roll,
existing_skill: self.existing_skill, existing_skill: self.existing_skill,
advancement: 0, advancement: 0,
successful: false, successful: false,

View File

@ -1,16 +1,91 @@
use super::dice::{AdvancementRoll, DiceRoll}; use super::dice::{AdvancementRoll, DiceRoll, DiceRollModifier};
use crate::error::BotError; use crate::parser::DiceParsingError;
use combine::error::StringStreamError;
use combine::parser::char::{digit, letter, spaces, string};
use combine::{choice, count, many, many1, one_of, Parser};
pub fn parse_roll(input: &str) -> Result<DiceRoll, ParsingError> { //TOOD convert these to use parse_amounts from the common dice code.
Ok(DiceRoll {
target: 50, pub fn parse_regular_roll(input: &str) -> Result<DiceRoll, DiceParsingError> {
modifier: DiceRollModifier::Normal, let input = input.trim();
}) let target: u32 = input.parse().map_err(|_| DiceParsingError::InvalidAmount)?;
if target <= 100 {
Ok(DiceRoll {
target: target,
modifier: DiceRollModifier::Normal,
})
} else {
Err(DiceParsingError::InvalidAmount)
}
} }
pub fn parse_advancement_roll(input: &str) -> Result<AdvancementRoll, ParsingError> { pub fn parse_advancement_roll(input: &str) -> Result<AdvancementRoll, DiceParsingError> {
Ok(AdvancementRoll { existing_skill: 50 }) let input = input.trim();
let target: u32 = input.parse().map_err(|_| DiceParsingError::InvalidAmount)?;
if target <= 100 {
Ok(AdvancementRoll {
existing_skill: target,
})
} else {
Err(DiceParsingError::InvalidAmount)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn regular_roll_accepts_single_number() {
let result = parse_regular_roll("60");
assert!(result.is_ok());
assert_eq!(
DiceRoll {
target: 60,
modifier: DiceRollModifier::Normal
},
result.unwrap()
);
}
#[test]
fn regular_roll_accepts_whitespacen() {
assert!(parse_regular_roll("60 ").is_ok());
assert!(parse_regular_roll(" 60").is_ok());
assert!(parse_regular_roll(" 60 ").is_ok());
}
#[test]
fn advancement_roll_accepts_whitespacen() {
assert!(parse_advancement_roll("60 ").is_ok());
assert!(parse_advancement_roll(" 60").is_ok());
assert!(parse_advancement_roll(" 60 ").is_ok());
}
#[test]
fn advancement_roll_accepts_single_number() {
let result = parse_advancement_roll("60");
assert!(result.is_ok());
assert_eq!(AdvancementRoll { existing_skill: 60 }, result.unwrap());
}
#[test]
fn regular_roll_rejects_big_numbers() {
assert!(parse_regular_roll("3000").is_err());
}
#[test]
fn advancement_roll_rejects_big_numbers() {
assert!(parse_advancement_roll("3000").is_err());
}
#[test]
fn regular_roll_rejects_invalid_input() {
assert!(parse_regular_roll("abc").is_err());
}
#[test]
fn advancement_roll_rejects_invalid_input() {
assert!(parse_advancement_roll("abc").is_err());
}
} }

View File

@ -8,7 +8,7 @@ use thiserror::Error;
//****************************** //******************************
#[derive(Debug, Clone, Copy, PartialEq, Error)] #[derive(Debug, Clone, Copy, PartialEq, Error)]
pub enum DiceParsingError { pub enum DiceParsingError {
#[error("invalid amount of dice")] #[error("invalid amount")]
InvalidAmount, InvalidAmount,
#[error("modifiers not specified properly")] #[error("modifiers not specified properly")]