From 39e6eb9b4671e522379ff4ccb511323c7b1394e1 Mon Sep 17 00:00:00 2001 From: projectmoon Date: Wed, 4 Nov 2020 20:09:39 +0000 Subject: [PATCH] Implement support for user variables in CoC dice rolling. Also comes with reorganization of the dice rolling code to centralize the variable -> dice amount logic, and changes the way the results of those rolls are displayed. --- src/cofd/dice.rs | 47 ++------- src/commands/cthulhu.rs | 29 ++++-- src/cthulhu/dice.rs | 214 ++++++++++++++++++---------------------- src/cthulhu/parser.rs | 111 ++++++++++++--------- src/dice.rs | 33 +++++++ src/error.rs | 15 ++- 6 files changed, 231 insertions(+), 218 deletions(-) diff --git a/src/cofd/dice.rs b/src/cofd/dice.rs index ff645ee..93e3586 100644 --- a/src/cofd/dice.rs +++ b/src/cofd/dice.rs @@ -1,22 +1,10 @@ use crate::context::Context; -use crate::db::variables::UserAndRoom; -use crate::error::BotError; +use crate::error::{BotError, DiceRollingError}; use crate::parser::{Amount, Element, Operator}; use crate::roll::Rolled; -use futures::stream::{self, StreamExt, TryStreamExt}; use itertools::Itertools; use std::convert::TryFrom; use std::fmt; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum DiceRollingError { - #[error("variable not found: {0}")] - VariableNotFound(String), - - #[error("dice pool expression too large")] - ExpressionTooLarge, -} #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum DicePoolQuality { @@ -94,29 +82,6 @@ pub struct DicePool { pub(crate) modifiers: DicePoolModifiers, } -async fn calculate_dice_amount(pool: &DicePoolWithContext<'_>) -> Result { - let stream = stream::iter(&pool.0.amounts); - let key = UserAndRoom(&pool.1.username, &pool.1.room_id); - let variables = &pool.1.db.variables.get_user_variables(&key)?; - - use DiceRollingError::VariableNotFound; - let dice_amount: Result = stream - .then(|amount| async move { - match &amount.element { - Element::Number(num_dice) => Ok(*num_dice * amount.operator.mult()), - Element::Variable(variable) => variables - .get(variable) - .ok_or(VariableNotFound(variable.clone().to_string())) - .map(|i| *i) - .map_err(|e| e.into()), - } - }) - .try_fold(0, |total, num_dice| async move { Ok(total + num_dice) }) - .await; - - dice_amount -} - impl DicePool { pub fn easy_pool(dice_amount: i32, quality: DicePoolQuality) -> DicePool { DicePool { @@ -346,7 +311,7 @@ pub async fn roll_pool(pool: &DicePoolWithContext<'_>) -> Result 0 { @@ -563,9 +528,13 @@ mod tests { }]; let pool = DicePool::new(amounts, DicePoolModifiers::default()); - let pool_with_ctx = DicePoolWithContext(&pool, &ctx); - assert_eq!(calculate_dice_amount(&pool_with_ctx).await.unwrap(), 10); + assert_eq!( + crate::dice::calculate_dice_amount(&pool.amounts, &ctx) + .await + .unwrap(), + 10 + ); } //DicePool tests diff --git a/src/commands/cthulhu.rs b/src/commands/cthulhu.rs index 7ccdb24..e353d66 100644 --- a/src/commands/cthulhu.rs +++ b/src/commands/cthulhu.rs @@ -1,6 +1,6 @@ use super::{Command, Execution}; use crate::context::Context; -use crate::cthulhu::dice::{AdvancementRoll, DiceRoll}; +use crate::cthulhu::dice::{regular_roll, AdvancementRoll, DiceRoll, DiceRollWithContext}; use async_trait::async_trait; pub struct CthRoll(pub DiceRoll); @@ -11,14 +11,25 @@ impl Command for CthRoll { "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!( - "

Roll: {}

Result: {}

", - self.0, roll - ); + async fn execute(&self, ctx: &Context<'_>) -> Execution { + let roll_with_ctx = DiceRollWithContext(&self.0, ctx); + let roll = regular_roll(&roll_with_ctx).await; + + let (plain, html) = match roll { + Ok(executed_roll) => { + let plain = format!("Roll: {}\nResult: {}", executed_roll, executed_roll.roll); + let html = format!( + "

Roll: {}

Result: {}

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

Error: {}

", e); + (plain, html) + } + }; Execution { plain, html } } diff --git a/src/cthulhu/dice.rs b/src/cthulhu/dice.rs index 5919d10..6f2a35a 100644 --- a/src/cthulhu/dice.rs +++ b/src/cthulhu/dice.rs @@ -1,19 +1,17 @@ +use crate::context::Context; +use crate::error::{BotError, DiceRollingError}; +use crate::parser::Amount; +use std::convert::TryFrom; use std::fmt; /// A planned dice roll. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct DiceRoll { - pub target: u32, + pub amounts: Vec, pub modifier: DiceRollModifier, } -impl fmt::Display for DiceRoll { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let message = format!("target: {}, with {}", self.target, self.modifier); - write!(f, "{}", message)?; - Ok(()) - } -} +pub struct DiceRollWithContext<'a>(pub &'a DiceRoll, pub &'a Context<'a>); /// Potential modifier on the die roll to be made. #[derive(Clone, Copy, Debug, PartialEq)] @@ -91,6 +89,26 @@ impl fmt::Display for RollResult { } } +pub struct ExecutedDiceRoll { + /// The number we must meet for the roll to be considered a + /// success. + pub target: u32, + + /// Stored for informational purposes in display. + pub modifier: DiceRollModifier, + + /// The actual roll result. + pub roll: RolledDice, +} + +impl fmt::Display for ExecutedDiceRoll { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = format!("target: {}, with {}", self.target, self.modifier); + write!(f, "{}", message)?; + Ok(()) + } +} + //TODO need to keep track of all rolled numbers for informational purposes! /// The outcome of a roll. pub struct RolledDice { @@ -100,10 +118,6 @@ pub struct RolledDice { /// The number we must meet for the roll to be considered a /// success. target: u32, - - /// Stored for informational purposes in display. - #[allow(dead_code)] - modifier: DiceRollModifier, } impl RolledDice { @@ -235,9 +249,14 @@ fn roll_percentile_dice(roller: &mut R, unit_roll: u32) -> u32 { } } -fn roll_regular_dice(roll: &DiceRoll, roller: &mut R) -> RolledDice { +fn roll_regular_dice( + modifier: &DiceRollModifier, + target: u32, + roller: &mut R, +) -> RolledDice { use DiceRollModifier::*; - let num_rolls = match roll.modifier { + + let num_rolls = match modifier { Normal => 1, OneBonus | OnePenalty => 2, TwoBonus | TwoPenalty => 3, @@ -249,7 +268,7 @@ fn roll_regular_dice(roll: &DiceRoll, roller: &mut R) -> RolledDic .map(|_| roll_percentile_dice(roller, unit_roll)) .collect(); - let num_rolled = match roll.modifier { + let num_rolled = match modifier { Normal => rolls.first(), OneBonus | TwoBonus => rolls.iter().min(), OnePenalty | TwoPenalty => rolls.iter().max(), @@ -257,9 +276,8 @@ fn roll_regular_dice(roll: &DiceRoll, roller: &mut R) -> RolledDic .unwrap(); RolledDice { - modifier: roll.modifier, num_rolled: *num_rolled, - target: roll.target, + target: target, } } @@ -287,19 +305,29 @@ fn roll_advancement_dice( } } -impl DiceRoll { - /// Make a roll with a target number and potential modifier. In a - /// normal roll, only one percentile die is rolled (1d100). With - /// bonuses or penalties, more dice are rolled, and either the lowest - /// (in case of bonus) or highest (in case of penalty) result is - /// picked. Rolls are not simply d100; the unit roll (ones place) is - /// rolled separately from the tens place, and then the unit number is - /// added to each potential roll before picking the lowest/highest - /// result. - pub fn roll(&self) -> RolledDice { - let mut roller = RngDieRoller(rand::thread_rng()); - roll_regular_dice(&self, &mut roller) - } +/// Make a roll with a target number and potential modifier. In a +/// normal roll, only one percentile die is rolled (1d100). With +/// bonuses or penalties, more dice are rolled, and either the lowest +/// (in case of bonus) or highest (in case of penalty) result is +/// picked. Rolls are not simply d100; the unit roll (ones place) is +/// rolled separately from the tens place, and then the unit number is +/// added to each potential roll before picking the lowest/highest +/// result. +pub async fn regular_roll( + roll_with_ctx: &DiceRollWithContext<'_>, +) -> Result { + let target = + crate::dice::calculate_dice_amount(&roll_with_ctx.0.amounts, roll_with_ctx.1).await?; + + let target = u32::try_from(target).map_err(|_| DiceRollingError::InvalidAmount)?; + let mut roller = RngDieRoller(rand::thread_rng()); + let rolled_dice = roll_regular_dice(&roll_with_ctx.0.modifier, target, &mut roller); + + Ok(ExecutedDiceRoll { + target: target, + modifier: roll_with_ctx.0.modifier, + roll: rolled_dice, + }) } impl AdvancementRoll { @@ -312,6 +340,8 @@ impl AdvancementRoll { #[cfg(test)] mod tests { use super::*; + use crate::db::Database; + use crate::parser::{Amount, Element, Operator}; /// Generate a series of numbers manually for testing. For this /// die system, the first roll in the Vec should be the unit roll, @@ -339,195 +369,141 @@ mod tests { } } - #[test] - fn regular_roll_succeeds_when_below_target() { + #[tokio::test] + async fn regular_roll_converts_u32_safely() { let roll = DiceRoll { - target: 50, + amounts: vec![Amount { + operator: Operator::Plus, + element: Element::Number(-10), + }], modifier: DiceRollModifier::Normal, }; + let db = Database::new_temp().unwrap(); + let ctx = Context::new(&db, "roomid", "username", "message"); + let roll_with_ctx = DiceRollWithContext(&roll, &ctx); + let result = regular_roll(&roll_with_ctx).await; + assert!(result.is_err()); + assert!(matches!( + result, + Err(BotError::DiceRollingError(DiceRollingError::InvalidAmount)) + )); + } + + #[test] + fn regular_roll_succeeds_when_below_target() { //Roll 30, succeeding. let mut roller = SequentialDieRoller::new(vec![0, 3]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller); assert_eq!(RollResult::Success, rolled.result()); } #[test] fn regular_roll_hard_success_when_rolling_half() { - let roll = DiceRoll { - target: 50, - modifier: DiceRollModifier::Normal, - }; - //Roll 25, succeeding. let mut roller = SequentialDieRoller::new(vec![5, 2]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller); assert_eq!(RollResult::HardSuccess, rolled.result()); } #[test] fn regular_roll_extreme_success_when_rolling_one_fifth() { - let roll = DiceRoll { - target: 50, - modifier: DiceRollModifier::Normal, - }; - //Roll 10, succeeding extremely. let mut roller = SequentialDieRoller::new(vec![0, 1]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller); assert_eq!(RollResult::ExtremeSuccess, rolled.result()); } #[test] fn regular_roll_extreme_success_target_above_100() { - let roll = DiceRoll { - target: 150, - modifier: DiceRollModifier::Normal, - }; - //Roll 30, succeeding extremely. let mut roller = SequentialDieRoller::new(vec![0, 3]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::Normal, 150, &mut roller); assert_eq!(RollResult::ExtremeSuccess, rolled.result()); } #[test] fn regular_roll_critical_success_on_one() { - let roll = DiceRoll { - target: 50, - modifier: DiceRollModifier::Normal, - }; - //Roll 1. let mut roller = SequentialDieRoller::new(vec![1, 0]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller); assert_eq!(RollResult::CriticalSuccess, rolled.result()); } #[test] fn regular_roll_fail_when_above_target() { - let roll = DiceRoll { - target: 50, - modifier: DiceRollModifier::Normal, - }; - //Roll 60. let mut roller = SequentialDieRoller::new(vec![0, 6]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller); assert_eq!(RollResult::Failure, rolled.result()); } #[test] fn regular_roll_is_fumble_when_skill_below_50_and_roll_at_least_96() { - let roll = DiceRoll { - target: 49, - modifier: DiceRollModifier::Normal, - }; - //Roll 96. let mut roller = SequentialDieRoller::new(vec![6, 9]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::Normal, 49, &mut roller); assert_eq!(RollResult::Fumble, rolled.result()); } #[test] fn regular_roll_is_failure_when_skill_at_or_above_50_and_roll_at_least_96() { - let roll = DiceRoll { - target: 50, - modifier: DiceRollModifier::Normal, - }; - //Roll 96. let mut roller = SequentialDieRoller::new(vec![6, 9]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller); assert_eq!(RollResult::Failure, rolled.result()); - let roll = DiceRoll { - target: 68, - modifier: DiceRollModifier::Normal, - }; - //Roll 96. let mut roller = SequentialDieRoller::new(vec![6, 9]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::Normal, 68, &mut roller); assert_eq!(RollResult::Failure, rolled.result()); } #[test] fn regular_roll_always_fumble_on_100() { - let roll = DiceRoll { - target: 100, - modifier: DiceRollModifier::Normal, - }; - //Roll 100. let mut roller = SequentialDieRoller::new(vec![0, 0]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::Normal, 100, &mut roller); assert_eq!(RollResult::Fumble, rolled.result()); } #[test] fn one_penalty_picks_highest_of_two() { - let roll = DiceRoll { - target: 50, - modifier: DiceRollModifier::OnePenalty, - }; - //Should only roll 30 and 40, not 50. let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 5]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::OnePenalty, 50, &mut roller); assert_eq!(40, rolled.num_rolled); } #[test] fn two_penalty_picks_highest_of_three() { - let roll = DiceRoll { - target: 50, - modifier: DiceRollModifier::TwoPenalty, - }; - //Should only roll 30, 40, 50, and not 60. let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 5, 6]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::TwoPenalty, 50, &mut roller); assert_eq!(50, rolled.num_rolled); } #[test] fn one_bonus_picks_lowest_of_two() { - let roll = DiceRoll { - target: 50, - modifier: DiceRollModifier::OneBonus, - }; - //Should only roll 30 and 40, not 20. let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 2]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::OneBonus, 50, &mut roller); assert_eq!(30, rolled.num_rolled); } #[test] fn two_bonus_picks_lowest_of_three() { - let roll = DiceRoll { - target: 50, - modifier: DiceRollModifier::TwoBonus, - }; - //Should only roll 30, 40, 50, and not 20. let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 5, 2]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::TwoBonus, 50, &mut roller); assert_eq!(30, rolled.num_rolled); } #[test] fn normal_modifier_rolls_once() { - let roll = DiceRoll { - target: 50, - modifier: DiceRollModifier::Normal, - }; - //Should only roll 30, not 40. let mut roller = SequentialDieRoller::new(vec![0, 3, 4]); - let rolled = roll_regular_dice(&roll, &mut roller); + let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller); assert_eq!(30, rolled.num_rolled); } diff --git a/src/cthulhu/parser.rs b/src/cthulhu/parser.rs index 6bb2783..97cde30 100644 --- a/src/cthulhu/parser.rs +++ b/src/cthulhu/parser.rs @@ -3,33 +3,40 @@ use crate::parser::DiceParsingError; //TOOD convert these to use parse_amounts from the common dice code. -fn parse_modifier(input: &str) -> Result<(DiceRollModifier, &str), DiceParsingError> { +fn parse_modifier(input: &str) -> Result { if input.ends_with("bb") { - Ok((DiceRollModifier::TwoBonus, input.trim_end_matches("bb"))) + Ok(DiceRollModifier::TwoBonus) } else if input.ends_with("b") { - Ok((DiceRollModifier::OneBonus, input.trim_end_matches("b"))) + Ok(DiceRollModifier::OneBonus) } else if input.ends_with("pp") { - Ok((DiceRollModifier::TwoPenalty, input.trim_end_matches("pp"))) + Ok(DiceRollModifier::TwoPenalty) } else if input.ends_with("p") { - Ok((DiceRollModifier::OnePenalty, input.trim_end_matches("p"))) + Ok(DiceRollModifier::OnePenalty) } else { - Ok((DiceRollModifier::Normal, input)) + Ok(DiceRollModifier::Normal) } } -pub fn parse_regular_roll(input: &str) -> Result { - let input = input.trim(); - let (modifier, input) = parse_modifier(input)?; - let target: u32 = input.parse().map_err(|_| DiceParsingError::InvalidAmount)?; +//Make diceroll take a vec of Amounts +//Split based on :, send first part to parse_modifier. +//Send second part to parse_amounts - if target <= 100 { - Ok(DiceRoll { - target: target, - modifier: modifier, - }) - } else { - Err(DiceParsingError::InvalidAmount) - } +pub fn parse_regular_roll(input: &str) -> Result { + let input: Vec<&str> = input.trim().split(":").collect(); + + let (modifiers_str, amounts_str) = match input[..] { + [amounts] => Ok(("", amounts)), + [modifiers, amounts] => Ok((modifiers, amounts)), + _ => Err(DiceParsingError::UnconsumedInput), + }?; + + let modifier = parse_modifier(modifiers_str)?; + let amounts = crate::parser::parse_amounts(amounts_str)?; + + Ok(DiceRoll { + amounts: amounts, + modifier: modifier, + }) } pub fn parse_advancement_roll(input: &str) -> Result { @@ -49,6 +56,7 @@ pub fn parse_advancement_roll(input: &str) -> Result) -> Result { + let stream = stream::iter(amounts); + let key = UserAndRoom(&ctx.username, &ctx.room_id); + let variables = &ctx.db.variables.get_user_variables(&key)?; + + use DiceRollingError::VariableNotFound; + let dice_amount: Result = stream + .then(|amount| async move { + match &amount.element { + NewElement::Number(num_dice) => Ok(*num_dice * amount.operator.mult()), + NewElement::Variable(variable) => variables + .get(variable) + .ok_or(VariableNotFound(variable.clone().to_string())) + .map(|i| *i) + .map_err(|e| e.into()), + } + }) + .try_fold(0, |total, num_dice| async move { Ok(total + num_dice) }) + .await; + + dice_amount +} + +//Old stuff, for regular dice rolling. To be moved elsewhere. + #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct Dice { pub(crate) count: u32, diff --git a/src/error.rs b/src/error.rs index 5a8e7c2..68fcf4b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,3 @@ -use crate::cofd::dice::DiceRollingError; use crate::commands::CommandError; use crate::config::ConfigError; use crate::db::errors::DataError; @@ -50,7 +49,7 @@ pub enum BotError { #[error("command parsing error: {0}")] CommandParsingError(#[from] crate::commands::parser::CommandParsingError), - #[error("dice pool roll error: {0}")] + #[error("dice rolling error: {0}")] DiceRollingError(#[from] DiceRollingError), #[error("variable parsing error: {0}")] @@ -68,3 +67,15 @@ pub enum BotError { #[error("database error")] DatabaseErrror(#[from] sled::Error), } + +#[derive(Error, Debug)] +pub enum DiceRollingError { + #[error("variable not found: {0}")] + VariableNotFound(String), + + #[error("invalid amount")] + InvalidAmount, + + #[error("dice pool expression too large")] + ExpressionTooLarge, +}