/// A planned dice roll. pub struct DiceRoll { target: u32, modifier: DiceRollModifier, } /// Potential modifier on the die roll to be made. 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, } /// The outcome of a die roll, either some kind of success or failure. 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, } /// 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, /// Stored for informational purposes in display. modifier: DiceRollModifier, } impl RolledDice { /// Calculate what type of success or failure this roll is. /// Consult the RollResult enum for descriptions of what each /// result requires. 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 } } } /// A planned advancement roll, where the target number is the /// existing skill amount. 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. existing_skill: u32, } /// A completed advancement roll. pub struct RolledAdvancement { existing_skill: 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 } } trait DieRoller { fn roll(&mut self) -> u32; } ///A version of DieRoller that uses a rand::Rng to roll numbers. struct RngDieRoller(R); impl DieRoller for RngDieRoller { 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(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 } } /// 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. fn roll(roll: DiceRoll) -> RolledDice { use DiceRollModifier::*; let num_rolls = match roll.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 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, } } fn advancement_roll(roll: AdvancementRoll) -> RolledAdvancement { let mut roller = RngDieRoller(rand::thread_rng()); let unit_roll = roller.roll(); let percentile_roll = roll_percentile_dice(&mut roller, unit_roll); if percentile_roll < roll.existing_skill || percentile_roll > 95 { RolledAdvancement { existing_skill: roll.existing_skill, advancement: roller.roll() + 1, successful: true, } } else { RolledAdvancement { existing_skill: roll.existing_skill, advancement: 0, successful: false, } } }