From 0d920fcdc134300a811c39bf9cb19977052e5751 Mon Sep 17 00:00:00 2001 From: Jontze <42588836+jontze@users.noreply.github.com> Date: Thu, 11 Jan 2024 22:07:46 +0100 Subject: [PATCH] feat(roll): Roll dice with a p&p style pattern (e.g. `2d6+3`) --- .../src/{roll.rs => roll/command.rs} | 25 +-- cadency_commands/src/roll/dice.rs | 204 ++++++++++++++++++ cadency_commands/src/roll/mod.rs | 4 + 3 files changed, 221 insertions(+), 12 deletions(-) rename cadency_commands/src/{roll.rs => roll/command.rs} (58%) create mode 100644 cadency_commands/src/roll/dice.rs create mode 100644 cadency_commands/src/roll/mod.rs diff --git a/cadency_commands/src/roll.rs b/cadency_commands/src/roll/command.rs similarity index 58% rename from cadency_commands/src/roll.rs rename to cadency_commands/src/roll/command.rs index eacb97b..a2e6d08 100644 --- a/cadency_commands/src/roll.rs +++ b/cadency_commands/src/roll/command.rs @@ -1,25 +1,20 @@ +use super::dice::{RollDice, Throw}; use cadency_core::{ response::{Response, ResponseBuilder}, CadencyCommand, CadencyError, }; -use rand::Rng; use serenity::{async_trait, client::Context, model::application::CommandInteraction}; #[derive(CommandBaseline, Default)] #[description = "Roll a dice of n sides"] #[argument( - name = "sides", - description = "The number of sides on the dice", - kind = "Integer" + name = "roll", + description = "Dice(s) to roll. Only the following patterns are supported: `d6`, `2d6`, 2d6+1` or `2d6-1`", + kind = "String" )] pub struct Roll {} -impl Roll { - fn roll_dice(&self, sides: &i64) -> i64 { - let mut rng = rand::thread_rng(); - rng.gen_range(1..=*sides) as i64 - } -} +impl Roll {} #[async_trait] impl CadencyCommand for Roll { @@ -29,8 +24,14 @@ impl CadencyCommand for Roll { command: &'a mut CommandInteraction, response_builder: &'a mut ResponseBuilder, ) -> Result { - let roll = self.roll_dice(&self.arg_sides(command)); - let roll_msg = format!("**:dice_cube: You rolled a `{roll}`**"); + let throw_str = self.arg_roll(command); + let throw = throw_str.parse::()?; + + throw.validate()?; + + let roll = throw.roll(); + + let roll_msg = format!("**{throw_str} :ice_cube: You rolled a `{roll}`**"); Ok(response_builder.message(Some(roll_msg)).build()?) } } diff --git a/cadency_commands/src/roll/dice.rs b/cadency_commands/src/roll/dice.rs new file mode 100644 index 0000000..57ffde6 --- /dev/null +++ b/cadency_commands/src/roll/dice.rs @@ -0,0 +1,204 @@ +use cadency_core::CadencyError; +use rand::Rng; + +pub(crate) trait RollDice { + fn roll(&self) -> i64; +} + +struct Dice { + sides: i64, +} + +impl Dice { + fn new(sides: i64) -> Self { + Self { sides } + } +} + +impl RollDice for Dice { + fn roll(&self) -> i64 { + let mut rng = rand::thread_rng(); + rng.gen_range(1..=self.sides) as i64 + } +} + +pub(crate) struct Throw { + dices: Vec, + bonus: i64, +} + +impl Throw { + fn new(dices: Vec, bonus: i64) -> Self { + Self { dices, bonus } + } + + pub(crate) fn validate(&self) -> Result<(), CadencyError> { + for dice in &self.dices { + if dice.sides <= 1 { + return Err(CadencyError::Command { + message: "Amount of sides must be greater then `1`".to_string(), + }); + } else if dice.sides.gt(&100) { + return Err(CadencyError::Command { + message: "Amount of sides must be at most `100`".to_string(), + }); + } + } + Ok(()) + } +} + +impl RollDice for Throw { + fn roll(&self) -> i64 { + self.dices.iter().map(|dice| dice.roll()).sum::() + self.bonus + } +} + +impl std::str::FromStr for Throw { + type Err = CadencyError; + + fn from_str(s: &str) -> Result { + const UNSUPPORTED_PATTERN_ERROR: &str = + "Unsupported pattern. Only the following patterns are supported: e.g. `d6`, `2d6`, 2d6+1` or `2d6-1`"; + + let has_multiple_dices = !s.starts_with('d') + && s.contains('d') + && s.chars() + .next() + .ok_or(CadencyError::Command { + message: UNSUPPORTED_PATTERN_ERROR.to_string(), + })? + .is_ascii_digit(); + + let bonus_parser = |throw_str: &str| { + let has_positive_bonus = + throw_str.contains('+') && !throw_str.ends_with('+') && !throw_str.starts_with('+'); + let has_negative_bonus = + throw_str.contains('-') && !throw_str.ends_with('-') && !throw_str.starts_with('-'); + let bonus_sign = if has_positive_bonus { + '+' + } else if has_negative_bonus { + '-' + } else if !has_positive_bonus && !has_negative_bonus { + // No bonus - Finish early and return 0 + return Ok(0); + } else { + unreachable!("Bonus sign is either + or -") + }; + + throw_str.split(bonus_sign).collect::>()[1] + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect::() + .parse::() + .map(|bonus| if has_negative_bonus { -bonus } else { bonus }) + .map_err(|_| CadencyError::Command { + message: UNSUPPORTED_PATTERN_ERROR.to_string(), + }) + }; + + let throw = if has_multiple_dices { + let amount = s + .chars() + .take_while(|c| c.is_ascii_digit() && c != &'d') + .collect::() + .parse::() + .map_err(|_| CadencyError::Command { + message: UNSUPPORTED_PATTERN_ERROR.to_string(), + })?; + + let dice_sides = s.split('d').collect::>()[1] + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect::() + .parse::() + .map_err(|_| CadencyError::Command { + message: UNSUPPORTED_PATTERN_ERROR.to_string(), + })?; + let dices = (0..amount) + .map(|_| Dice::new(dice_sides)) + .collect::>(); + Throw::new(dices, bonus_parser(s)?) + } else { + let dice_sides = s + .replace('d', "") + .trim() + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect::() + .parse::() + .map_err(|_| CadencyError::Command { + message: UNSUPPORTED_PATTERN_ERROR.to_string(), + })?; + + Throw::new(vec![Dice::new(dice_sides)], bonus_parser(s)?) + }; + Ok(throw) + } +} + +#[cfg(test)] +mod test_throw { + use super::*; + + #[test] + fn test_multiple_dices_with_bonus_single_digit() { + let throw = "2d6+1".parse::().unwrap(); + assert_eq!(throw.dices.len(), 2); + assert_eq!(throw.dices[0].sides, 6); + assert_eq!(throw.dices[1].sides, 6); + assert_eq!(throw.bonus, 1); + } + + #[test] + fn test_multiple_dices_with_negativ_bonus() { + let throw = "2d6-1".parse::().unwrap(); + assert_eq!(throw.dices.len(), 2); + assert_eq!(throw.dices[0].sides, 6); + assert_eq!(throw.dices[1].sides, 6); + assert_eq!(throw.bonus, -1); + } + + #[test] + fn test_multiple_dices_with_bonus_multiple_digits() { + let throw = "2d100+10".parse::().unwrap(); + assert_eq!(throw.dices.len(), 2); + assert_eq!(throw.dices[0].sides, 100); + assert_eq!(throw.dices[1].sides, 100); + assert_eq!(throw.bonus, 10); + } + + #[test] + fn test_single_dice_single_digit() { + let throw = "d6".parse::().unwrap(); + assert_eq!(throw.dices.len(), 1); + assert_eq!(throw.dices[0].sides, 6); + assert_eq!(throw.bonus, 0); + } + + #[test] + fn test_single_dice_multiple_digits() { + let throw = "d100".parse::().unwrap(); + assert_eq!(throw.dices.len(), 1); + assert_eq!(throw.dices[0].sides, 100); + assert_eq!(throw.bonus, 0); + } + + #[test] + fn test_multiple_dices_single_digit() { + let throw = "2d6".parse::().unwrap(); + assert_eq!(throw.dices.len(), 2); + assert_eq!(throw.dices[0].sides, 6); + assert_eq!(throw.dices[1].sides, 6); + assert_eq!(throw.bonus, 0); + } + + #[test] + fn test_multiple_dices_multiple_digits() { + let throw = "2d100".parse::().unwrap(); + assert_eq!(throw.dices.len(), 2); + assert_eq!(throw.dices[0].sides, 100); + assert_eq!(throw.dices[1].sides, 100); + assert_eq!(throw.bonus, 0); + } +} diff --git a/cadency_commands/src/roll/mod.rs b/cadency_commands/src/roll/mod.rs new file mode 100644 index 0000000..e5fdda4 --- /dev/null +++ b/cadency_commands/src/roll/mod.rs @@ -0,0 +1,4 @@ +mod command; +mod dice; + +pub use command::Roll;