From d5034c11dc17237a5bc4a681551857d15d51eaeb Mon Sep 17 00:00:00 2001 From: projectmoon Date: Fri, 23 Oct 2020 20:23:13 +0000 Subject: [PATCH] Basic implementation of CoC dice rolling. Not hooked up to parser or commands yet. --- src/cthulhu.rs | 1 + src/cthulhu/dice.rs | 203 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 3 files changed, 205 insertions(+) create mode 100644 src/cthulhu.rs create mode 100644 src/cthulhu/dice.rs diff --git a/src/cthulhu.rs b/src/cthulhu.rs new file mode 100644 index 0000000..99ad3e8 --- /dev/null +++ b/src/cthulhu.rs @@ -0,0 +1 @@ +pub mod dice; diff --git a/src/cthulhu/dice.rs b/src/cthulhu/dice.rs new file mode 100644 index 0000000..805e9af --- /dev/null +++ b/src/cthulhu/dice.rs @@ -0,0 +1,203 @@ +/// 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, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 8828a20..cb466db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod cofd; pub mod commands; pub mod config; pub mod context; +pub mod cthulhu; pub mod db; pub mod dice; pub mod error;