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, Debug, PartialEq)] pub struct DiceRoll { pub amounts: Vec<Amount>, pub modifier: DiceRollModifier, } 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)] pub enum DiceRollModifier { /// No bonuses or penalties. Normal, /// Roll one extra die and pick the lower of two results. OneBonus, /// Roll two extra dice and pick the lower of all results. TwoBonus, /// Roll one extra die and pick the higher of two results. OnePenalty, /// Roll two extra dice and pick the higher of all results. TwoPenalty, } impl fmt::Display for DiceRollModifier { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let message = match self { Self::Normal => "no modifiers", Self::OneBonus => "one bonus die", Self::TwoBonus => "two bonus dice", Self::OnePenalty => "one penalty die", Self::TwoPenalty => "two penalty dice", }; write!(f, "{}", message)?; Ok(()) } } /// The outcome of a die roll, either some kind of success or failure. #[derive(Clone, Copy, Debug, PartialEq)] pub enum RollResult { /// Basic success. The rolled number was equal to or less than the target number. Success, /// Hard success means the rolled number was equal to or less than /// the target number divided by 2 (rounded down). HardSuccess, /// Extreme success means the rolled number was equal to or less /// than the target number divided by 5 (rounded down). ExtremeSuccess, /// A critical success occurs on a roll of 1. CriticalSuccess, /// A basic failure means that the roll was above the target number. Failure, /// A fumble occurs if the target number is below 50 and the roll /// was 96 - 100, OR if the roll result was 100. This means lower /// target numbers are more likely to produce a fumble. Fumble, } impl fmt::Display for RollResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let message = match self { Self::Success => "success!", Self::HardSuccess => "hard success!", Self::ExtremeSuccess => "extreme success!", Self::CriticalSuccess => "critical success!", Self::Failure => "failure!", Self::Fumble => "fumble!", }; write!(f, "{}", message)?; Ok(()) } } 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 { /// The d100 result actually rolled. num_rolled: u32, /// The number we must meet for the roll to be considered a /// success. target: u32, } impl RolledDice { /// Calculate what type of success or failure this roll is. /// Consult the RollResult enum for descriptions of what each /// result requires. pub fn result(&self) -> RollResult { let hard_target = self.target / 2u32; let extreme_target = self.target / 5u32; if (self.target < 50 && self.num_rolled > 95) || self.num_rolled == 100 { RollResult::Fumble } else if self.num_rolled == 1 { RollResult::CriticalSuccess } else if self.num_rolled <= extreme_target { RollResult::ExtremeSuccess } else if self.num_rolled <= hard_target { RollResult::HardSuccess } else if self.num_rolled <= self.target { RollResult::Success } else { RollResult::Failure } } } impl fmt::Display for RolledDice { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let message = format!( "{} against {}: {}", self.num_rolled, self.target, self.result() ); write!(f, "{}", message)?; Ok(()) } } /// A planned advancement roll, where the target number is the /// existing skill amount. #[derive(Clone, Copy, Debug, PartialEq)] pub struct AdvancementRoll { /// The amount (0 to 100) of the existing skill. We must beat this /// target number to advance the skill, or roll above a 95. 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. pub struct RolledAdvancement { existing_skill: u32, num_rolled: u32, advancement: u32, successful: bool, } impl RolledAdvancement { /// The new skill amount, which will be the same if the roll was a /// failure. pub fn new_skill_amount(&self) -> u32 { self.existing_skill + self.advancement } /// How much the skill advanced (1 to 10). 0 if the advancement /// roll failed. pub fn advancement(&self) -> u32 { self.advancement } /// Whether or not the advancement roll was successful. pub fn successful(&self) -> bool { self.successful } } 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 { fn roll(&mut self) -> u32; } ///A version of DieRoller that uses a rand::Rng to roll numbers. struct RngDieRoller<R: rand::Rng>(R); impl<R: rand::Rng> DieRoller for RngDieRoller<R> { fn roll(&mut self) -> u32 { self.0.gen_range(0, 10) } } /// Roll a single percentile die according to the rules. We cannot /// simply roll a d100 due to the way the game calculates roll results /// with bonus/penalty dice. The unit roll (ones place) is added to /// the tens roll, unless both results are 0, in which case the result /// is 100. fn roll_percentile_dice<R: DieRoller>(roller: &mut R, unit_roll: u32) -> u32 { let tens_roll = roller.roll() * 10; if tens_roll == 0 && unit_roll == 0 { 100 } else { tens_roll + unit_roll } } fn roll_regular_dice<R: DieRoller>( modifier: &DiceRollModifier, target: u32, roller: &mut R, ) -> RolledDice { use DiceRollModifier::*; let num_rolls = match modifier { Normal => 1, OneBonus | OnePenalty => 2, TwoBonus | TwoPenalty => 3, }; let unit_roll = roller.roll(); let rolls: Vec<u32> = (0..num_rolls) .map(|_| roll_percentile_dice(roller, unit_roll)) .collect(); let num_rolled = match modifier { Normal => rolls.first(), OneBonus | TwoBonus => rolls.iter().min(), OnePenalty | TwoPenalty => rolls.iter().max(), } .unwrap(); RolledDice { num_rolled: *num_rolled, target: target, } } fn roll_advancement_dice<R: DieRoller>( roll: &AdvancementRoll, roller: &mut R, ) -> RolledAdvancement { let unit_roll = roller.roll(); let percentile_roll = roll_percentile_dice(roller, unit_roll); if percentile_roll > roll.existing_skill || percentile_roll > 95 { RolledAdvancement { num_rolled: percentile_roll, existing_skill: roll.existing_skill, advancement: roller.roll() + 1, successful: true, } } else { RolledAdvancement { num_rolled: percentile_roll, existing_skill: roll.existing_skill, advancement: 0, successful: false, } } } /// 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<ExecutedDiceRoll, BotError> { 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 { pub fn roll(&self) -> RolledAdvancement { let mut roller = RngDieRoller(rand::thread_rng()); roll_advancement_dice(self, &mut roller) } } #[cfg(test)] mod tests { use super::*; use crate::db::Database; use crate::parser::{Amount, Element, Operator}; /// Create a dummy room instance. fn dummy_room() -> matrix_sdk::Room { matrix_sdk::Room::new( &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"), &matrix_sdk::identifiers::user_id!("@fakeuserid:example.com"), ) } /// Generate a series of numbers manually for testing. For this /// die system, the first roll in the Vec should be the unit roll, /// and any subsequent rolls should be the tens place roll. The /// results rolled must come from a d10 (0 to 9). struct SequentialDieRoller { results: Vec<u32>, position: usize, } impl SequentialDieRoller { fn new(results: Vec<u32>) -> SequentialDieRoller { SequentialDieRoller { results: results, position: 0, } } } impl DieRoller for SequentialDieRoller { fn roll(&mut self) -> u32 { let roll = self.results[self.position]; self.position += 1; roll } } #[tokio::test] async fn regular_roll_converts_u32_safely() { let roll = DiceRoll { amounts: vec![Amount { operator: Operator::Plus, element: Element::Number(-10), }], modifier: DiceRollModifier::Normal, }; let db = Database::new_temp().unwrap(); let ctx = Context { db: db, matrix_client: &matrix_sdk::Client::new("https://example.com").unwrap(), room: &dummy_room(), username: "username", message_body: "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(&DiceRollModifier::Normal, 50, &mut roller); assert_eq!(RollResult::Success, rolled.result()); } #[test] fn regular_roll_hard_success_when_rolling_half() { //Roll 25, succeeding. let mut roller = SequentialDieRoller::new(vec![5, 2]); 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() { //Roll 10, succeeding extremely. let mut roller = SequentialDieRoller::new(vec![0, 1]); 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() { //Roll 30, succeeding extremely. let mut roller = SequentialDieRoller::new(vec![0, 3]); let rolled = roll_regular_dice(&DiceRollModifier::Normal, 150, &mut roller); assert_eq!(RollResult::ExtremeSuccess, rolled.result()); } #[test] fn regular_roll_critical_success_on_one() { //Roll 1. let mut roller = SequentialDieRoller::new(vec![1, 0]); let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller); assert_eq!(RollResult::CriticalSuccess, rolled.result()); } #[test] fn regular_roll_fail_when_above_target() { //Roll 60. let mut roller = SequentialDieRoller::new(vec![0, 6]); 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() { //Roll 96. let mut roller = SequentialDieRoller::new(vec![6, 9]); 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() { //Roll 96. let mut roller = SequentialDieRoller::new(vec![6, 9]); let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller); assert_eq!(RollResult::Failure, rolled.result()); //Roll 96. let mut roller = SequentialDieRoller::new(vec![6, 9]); let rolled = roll_regular_dice(&DiceRollModifier::Normal, 68, &mut roller); assert_eq!(RollResult::Failure, rolled.result()); } #[test] fn regular_roll_always_fumble_on_100() { //Roll 100. let mut roller = SequentialDieRoller::new(vec![0, 0]); let rolled = roll_regular_dice(&DiceRollModifier::Normal, 100, &mut roller); assert_eq!(RollResult::Fumble, rolled.result()); } #[test] fn one_penalty_picks_highest_of_two() { //Should only roll 30 and 40, not 50. let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 5]); let rolled = roll_regular_dice(&DiceRollModifier::OnePenalty, 50, &mut roller); assert_eq!(40, rolled.num_rolled); } #[test] fn two_penalty_picks_highest_of_three() { //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(&DiceRollModifier::TwoPenalty, 50, &mut roller); assert_eq!(50, rolled.num_rolled); } #[test] fn one_bonus_picks_lowest_of_two() { //Should only roll 30 and 40, not 20. let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 2]); let rolled = roll_regular_dice(&DiceRollModifier::OneBonus, 50, &mut roller); assert_eq!(30, rolled.num_rolled); } #[test] fn two_bonus_picks_lowest_of_three() { //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(&DiceRollModifier::TwoBonus, 50, &mut roller); assert_eq!(30, rolled.num_rolled); } #[test] fn normal_modifier_rolls_once() { //Should only roll 30, not 40. let mut roller = SequentialDieRoller::new(vec![0, 3, 4]); let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller); assert_eq!(30, rolled.num_rolled); } #[test] fn advancement_succeeds_on_above_skill() { let roll = AdvancementRoll { existing_skill: 30 }; //Roll 52, then advance skill by 5. (advancement adds +1 to 0-9 roll) let mut roller = SequentialDieRoller::new(vec![2, 5, 4]); let rolled = roll_advancement_dice(&roll, &mut roller); assert!(rolled.successful()); assert_eq!(5, rolled.advancement()); assert_eq!(35, rolled.new_skill_amount()); } #[test] fn advancement_succeeds_on_above_95() { let roll = AdvancementRoll { existing_skill: 97 }; //Roll 96, then advance skill by 1. (advancement adds +1 to 0-9 roll) let mut roller = SequentialDieRoller::new(vec![6, 9, 0]); let rolled = roll_advancement_dice(&roll, &mut roller); assert!(rolled.successful()); assert_eq!(1, rolled.advancement()); assert_eq!(98, rolled.new_skill_amount()); } #[test] fn advancement_fails_on_below_skill() { let roll = AdvancementRoll { existing_skill: 30 }; //Roll 25, failing. let mut roller = SequentialDieRoller::new(vec![5, 2]); let rolled = roll_advancement_dice(&roll, &mut roller); assert!(!rolled.successful()); assert_eq!(0, rolled.advancement()); } }