Support modifiers for cthulhu rolls, and add tests.
This commit is contained in:
parent
08b0e58193
commit
c55926a005
|
@ -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.
|
/// The outcome of a roll.
|
||||||
pub struct RolledDice {
|
pub struct RolledDice {
|
||||||
/// The d100 result actually rolled.
|
/// The d100 result actually rolled.
|
||||||
|
@ -233,6 +234,34 @@ fn roll_percentile_dice<R: DieRoller>(roller: &mut R, unit_roll: u32) -> u32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn roll_regular_dice<R: DieRoller>(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<u32> = (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 {
|
impl DiceRoll {
|
||||||
/// Make a roll with a target number and potential modifier. In a
|
/// Make a roll with a target number and potential modifier. In a
|
||||||
/// normal roll, only one percentile die is rolled (1d100). With
|
/// 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
|
/// added to each potential roll before picking the lowest/highest
|
||||||
/// result.
|
/// result.
|
||||||
pub fn roll(&self) -> RolledDice {
|
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 mut roller = RngDieRoller(rand::thread_rng());
|
||||||
let unit_roll = roller.roll();
|
roll_regular_dice(&self, &mut roller)
|
||||||
|
|
||||||
let rolls: Vec<u32> = (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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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<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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,14 +3,29 @@ use crate::parser::DiceParsingError;
|
||||||
|
|
||||||
//TOOD convert these to use parse_amounts from the common dice code.
|
//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<DiceRoll, DiceParsingError> {
|
pub fn parse_regular_roll(input: &str) -> Result<DiceRoll, DiceParsingError> {
|
||||||
let input = input.trim();
|
let input = input.trim();
|
||||||
|
let (modifier, input) = parse_modifier(input)?;
|
||||||
let target: u32 = input.parse().map_err(|_| DiceParsingError::InvalidAmount)?;
|
let target: u32 = input.parse().map_err(|_| DiceParsingError::InvalidAmount)?;
|
||||||
|
|
||||||
if target <= 100 {
|
if target <= 100 {
|
||||||
Ok(DiceRoll {
|
Ok(DiceRoll {
|
||||||
target: target,
|
target: target,
|
||||||
modifier: DiceRollModifier::Normal,
|
modifier: modifier,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(DiceParsingError::InvalidAmount)
|
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]
|
#[test]
|
||||||
fn regular_roll_accepts_whitespacen() {
|
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(" 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]
|
#[test]
|
||||||
|
|
Loading…
Reference in New Issue