From c55926a005485acbe3792bb9c1717c9461557d45 Mon Sep 17 00:00:00 2001 From: projectmoon Date: Sat, 31 Oct 2020 20:29:55 +0000 Subject: [PATCH] Support modifiers for cthulhu rolls, and add tests. --- src/cthulhu/dice.rs | 151 +++++++++++++++++++++++++++++++++++------- src/cthulhu/parser.rs | 85 +++++++++++++++++++++++- 2 files changed, 210 insertions(+), 26 deletions(-) diff --git a/src/cthulhu/dice.rs b/src/cthulhu/dice.rs index 6449a9a..2e9a220 100644 --- a/src/cthulhu/dice.rs +++ b/src/cthulhu/dice.rs @@ -90,6 +90,7 @@ impl fmt::Display for RollResult { } } +//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. @@ -233,6 +234,34 @@ fn roll_percentile_dice(roller: &mut R, unit_roll: u32) -> u32 { } } +fn roll_regular_dice(roll: &DiceRoll, roller: &mut R) -> RolledDice { + use DiceRollModifier::*; + let num_rolls = match roll.modifier { + Normal => 1, + OneBonus | OnePenalty => 2, + TwoBonus | TwoPenalty => 3, + }; + + let unit_roll = roller.roll(); + + let rolls: Vec = (0..num_rolls) + .map(|_| roll_percentile_dice(roller, unit_roll)) + .collect(); + + let num_rolled = match roll.modifier { + Normal => rolls.first(), + OneBonus | TwoBonus => rolls.iter().min(), + OnePenalty | TwoPenalty => rolls.iter().max(), + } + .unwrap(); + + RolledDice { + modifier: roll.modifier, + num_rolled: *num_rolled, + target: roll.target, + } +} + impl DiceRoll { /// Make a roll with a target number and potential modifier. In a /// normal roll, only one percentile die is rolled (1d100). With @@ -243,32 +272,8 @@ impl DiceRoll { /// added to each potential roll before picking the lowest/highest /// result. pub fn roll(&self) -> RolledDice { - use DiceRollModifier::*; - let num_rolls = match self.modifier { - Normal => 1, - OneBonus | OnePenalty => 2, - TwoBonus | TwoPenalty => 3, - }; - let mut roller = RngDieRoller(rand::thread_rng()); - let unit_roll = roller.roll(); - - let rolls: Vec = (0..num_rolls) - .map(|_| roll_percentile_dice(&mut roller, unit_roll)) - .collect(); - - let num_rolled = match self.modifier { - Normal => rolls.first(), - OneBonus | TwoBonus => rolls.iter().min(), - OnePenalty | TwoPenalty => rolls.iter().max(), - } - .unwrap(); - - RolledDice { - modifier: self.modifier, - num_rolled: *num_rolled, - target: self.target, - } + roll_regular_dice(&self, &mut roller) } } @@ -295,3 +300,99 @@ impl AdvancementRoll { } } } + +#[cfg(test)] +mod tests { + use super::*; + + /// 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, + position: usize, + } + + impl SequentialDieRoller { + fn new(results: Vec) -> 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 + } + } + + #[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); + 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); + 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); + 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); + 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); + assert_eq!(30, rolled.num_rolled); + } +} diff --git a/src/cthulhu/parser.rs b/src/cthulhu/parser.rs index 32f88a7..6bb2783 100644 --- a/src/cthulhu/parser.rs +++ b/src/cthulhu/parser.rs @@ -3,14 +3,29 @@ 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> { + if input.ends_with("bb") { + Ok((DiceRollModifier::TwoBonus, input.trim_end_matches("bb"))) + } else if input.ends_with("b") { + Ok((DiceRollModifier::OneBonus, input.trim_end_matches("b"))) + } else if input.ends_with("pp") { + Ok((DiceRollModifier::TwoPenalty, input.trim_end_matches("pp"))) + } else if input.ends_with("p") { + Ok((DiceRollModifier::OnePenalty, input.trim_end_matches("p"))) + } else { + Ok((DiceRollModifier::Normal, input)) + } +} + 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)?; if target <= 100 { Ok(DiceRoll { target: target, - modifier: DiceRollModifier::Normal, + modifier: modifier, }) } else { Err(DiceParsingError::InvalidAmount) @@ -48,11 +63,79 @@ mod tests { ); } + #[test] + fn regular_roll_accepts_two_bonus() { + let result = parse_regular_roll("60bb"); + assert!(result.is_ok()); + assert_eq!( + DiceRoll { + target: 60, + modifier: DiceRollModifier::TwoBonus + }, + result.unwrap() + ); + } + + #[test] + fn regular_roll_accepts_one_bonus() { + let result = parse_regular_roll("60b"); + assert!(result.is_ok()); + assert_eq!( + DiceRoll { + target: 60, + modifier: DiceRollModifier::OneBonus + }, + result.unwrap() + ); + } + + #[test] + fn regular_roll_accepts_two_penalty() { + let result = parse_regular_roll("60pp"); + assert!(result.is_ok()); + assert_eq!( + DiceRoll { + target: 60, + modifier: DiceRollModifier::TwoPenalty + }, + result.unwrap() + ); + } + + #[test] + fn regular_roll_accepts_one_penalty() { + let result = parse_regular_roll("60p"); + assert!(result.is_ok()); + assert_eq!( + DiceRoll { + target: 60, + modifier: DiceRollModifier::OnePenalty + }, + 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()); + + assert!(parse_regular_roll("60bb ").is_ok()); + assert!(parse_regular_roll(" 60bb").is_ok()); + assert!(parse_regular_roll(" 60bb ").is_ok()); + + assert!(parse_regular_roll("60b ").is_ok()); + assert!(parse_regular_roll(" 60b").is_ok()); + assert!(parse_regular_roll(" 60b ").is_ok()); + + assert!(parse_regular_roll("60pp ").is_ok()); + assert!(parse_regular_roll(" 60pp").is_ok()); + assert!(parse_regular_roll(" 60pp ").is_ok()); + + assert!(parse_regular_roll("60p ").is_ok()); + assert!(parse_regular_roll(" 60p").is_ok()); + assert!(parse_regular_roll(" 60p ").is_ok()); } #[test]