Refactor keep-drop parsing into function, better error handling. #93
|
@ -6,8 +6,9 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
//Old stuff, for regular dice rolling. To be moved elsewhere.
|
/// A basic dice roll, in XdY notation, like "1d4" or "3d6".
|
||||||
|
/// Optionally supports D&D advantage/disadvantge keep-or-drop
|
||||||
|
/// functionality.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub struct Dice {
|
pub struct Dice {
|
||||||
pub(crate) count: u32,
|
pub(crate) count: u32,
|
||||||
|
@ -15,6 +16,25 @@ pub struct Dice {
|
||||||
pub(crate) keep_drop: KeepOrDrop,
|
pub(crate) keep_drop: KeepOrDrop,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enum indicating how to handle bonuses or penalties using extra
|
||||||
|
/// dice. If set to Keep, the roll will keep the highest X number of
|
||||||
|
/// dice in the roll, and add those together. If set to Drop, the
|
||||||
|
/// opposite is performed, and the lowest X number of dice are added
|
||||||
|
/// instead. If set to None, then all dice in the roll are added up as
|
||||||
|
/// normal.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum KeepOrDrop {
|
||||||
|
/// Keep only the X highest dice for adding up to the total.
|
||||||
|
Keep(u32),
|
||||||
|
|
||||||
|
/// Keep only the X lowest dice (i.e. drop the highest) for adding
|
||||||
|
/// up to the total.
|
||||||
|
Drop(u32),
|
||||||
|
|
||||||
|
/// Add up all dice in the roll for the total.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for Dice {
|
impl fmt::Display for Dice {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self.keep_drop {
|
match self.keep_drop {
|
||||||
|
@ -25,16 +45,13 @@ impl fmt::Display for Dice {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
||||||
pub enum KeepOrDrop {
|
|
||||||
Keep (u32),
|
|
||||||
Drop (u32),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Dice {
|
impl Dice {
|
||||||
pub fn new(count: u32, sides: u32, keep_drop: KeepOrDrop) -> Dice {
|
pub fn new(count: u32, sides: u32, keep_drop: KeepOrDrop) -> Dice {
|
||||||
Dice { count, sides, keep_drop }
|
Dice {
|
||||||
|
count,
|
||||||
|
sides,
|
||||||
|
keep_drop,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,14 @@ enum Sign {
|
||||||
Minus,
|
Minus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Intermediate parsed value for a keep-drop expression to indicate
|
||||||
|
/// which one it is.
|
||||||
|
enum ParsedKeepOrDrop<'a> {
|
||||||
|
Keep(&'a str),
|
||||||
|
Drop(&'a str),
|
||||||
|
NotPresent,
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! too_big {
|
macro_rules! too_big {
|
||||||
($input: expr) => {
|
($input: expr) => {
|
||||||
NomErr::Error(($input, NomErrorKind::TooLarge))
|
NomErr::Error(($input, NomErrorKind::TooLarge))
|
||||||
|
@ -41,50 +49,58 @@ macro_rules! too_big {
|
||||||
|
|
||||||
/// Parse a dice expression. Does not eat whitespace
|
/// Parse a dice expression. Does not eat whitespace
|
||||||
fn parse_dice(input: &str) -> IResult<&str, Dice> {
|
fn parse_dice(input: &str) -> IResult<&str, Dice> {
|
||||||
// parse main dice expression
|
|
||||||
let (input, (count, _, sides)) = tuple((digit1, tag("d"), digit1))(input)?;
|
let (input, (count, _, sides)) = tuple((digit1, tag("d"), digit1))(input)?;
|
||||||
|
|
||||||
// check for keep expression to keep highest dice (2d20k1)
|
|
||||||
let (keep, input) = match tuple::<&str, _, (_, _), _>((tag("k"), digit1))(input) {
|
|
||||||
// if ok, keep expression is present
|
|
||||||
Ok((rest, (_, keep_amount))) => (keep_amount, rest),
|
|
||||||
// otherwise absent and keep all dice
|
|
||||||
Err(_) => ("", input),
|
|
||||||
};
|
|
||||||
|
|
||||||
// check for drop expression to drop highest dice (2d20dh1)
|
|
||||||
let (drop, input) = match tuple::<&str, _, (_, _), _>((tag("dh"), digit1))(input) {
|
|
||||||
// if ok, keep expression is present
|
|
||||||
Ok((rest, (_, drop_amount))) => (drop_amount, rest),
|
|
||||||
// otherwise absent and keep all dice
|
|
||||||
Err(_) => ("", input),
|
|
||||||
};
|
|
||||||
|
|
||||||
let count: u32 = count.parse().map_err(|_| too_big!(count))?;
|
let count: u32 = count.parse().map_err(|_| too_big!(count))?;
|
||||||
|
let sides = sides.parse().map_err(|_| too_big!(sides))?;
|
||||||
|
let (input, keep_drop) = parse_keep_or_drop(input, count)?;
|
||||||
|
Ok((input, Dice::new(count, sides, keep_drop)))
|
||||||
|
}
|
||||||
|
|
||||||
// don't allow keep greater than number of dice, and don't allow keep zero
|
/// Extract keep/drop number as a string. Fails if the value is not a
|
||||||
let keep_drop = match keep.parse::<u32>() {
|
/// string.
|
||||||
// Ok, there's a keep value, check and create Keep
|
fn parse_keep_or_drop_text<'a>(
|
||||||
Ok(i) => match i {
|
symbol: &'a str,
|
||||||
_i if _i > count || _i == 0 => KeepOrDrop::None,
|
input: &'a str,
|
||||||
i => KeepOrDrop::Keep(i),
|
) -> IResult<&'a str, ParsedKeepOrDrop<'a>> {
|
||||||
|
let (parsed_kd, input) = match tuple::<&str, _, (_, _), _>((tag(symbol), digit1))(input) {
|
||||||
|
// if ok, one of the expressions is present
|
||||||
|
Ok((rest, (_, kd_expr))) => match symbol {
|
||||||
|
"k" => (ParsedKeepOrDrop::Keep(kd_expr), rest),
|
||||||
|
"dh" => (ParsedKeepOrDrop::Drop(kd_expr), rest),
|
||||||
|
_ => panic!("Unrecogized keep-drop symbol: {}", symbol),
|
||||||
},
|
},
|
||||||
// Err, check if drop works
|
// otherwise absent (attempt to keep all dice)
|
||||||
Err(_) => {
|
Err(_) => (ParsedKeepOrDrop::NotPresent, input),
|
||||||
match drop.parse::<u32>() {
|
|
||||||
// Ok, there's a drop value, check and create Drop
|
|
||||||
Ok(i) => match i {
|
|
||||||
_i if i >= count => KeepOrDrop::None,
|
|
||||||
i => KeepOrDrop::Drop(i),
|
|
||||||
},
|
|
||||||
// Err, there's neither keep nor drop
|
|
||||||
Err(_) => KeepOrDrop::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let sides = sides.parse().map_err(|_| too_big!(sides))?;
|
Ok((input, parsed_kd))
|
||||||
Ok((input, Dice::new(count, sides, keep_drop)))
|
}
|
||||||
|
|
||||||
|
/// Parse keep/drop expression, which consits of "k" or "dh" following
|
||||||
|
/// a dice expression. For example, "1d4h3" or "1d4dh2".
|
||||||
|
fn parse_keep_or_drop<'a>(input: &'a str, count: u32) -> IResult<&'a str, KeepOrDrop> {
|
||||||
|
let (input, keep) = parse_keep_or_drop_text("k", input)?;
|
||||||
|
let (input, drop) = parse_keep_or_drop_text("dh", input)?;
|
||||||
|
|
||||||
|
use ParsedKeepOrDrop::*;
|
||||||
|
let keep_drop: KeepOrDrop = match (keep, drop) {
|
||||||
|
//Potential valid Keep expression.
|
||||||
|
(Keep(keep), NotPresent) => match keep.parse().map_err(|_| too_big!(input))? {
|
||||||
|
_i if _i > count || _i == 0 => Ok(KeepOrDrop::None),
|
||||||
|
i => Ok(KeepOrDrop::Keep(i)),
|
||||||
|
},
|
||||||
|
//Potential valid Drop expression.
|
||||||
|
(NotPresent, Drop(drop)) => match drop.parse().map_err(|_| too_big!(input))? {
|
||||||
|
_i if _i >= count => Ok(KeepOrDrop::None),
|
||||||
|
i => Ok(KeepOrDrop::Drop(i)),
|
||||||
|
},
|
||||||
|
//No Keep or Drop specified; regular behavior.
|
||||||
|
(NotPresent, NotPresent) => Ok(KeepOrDrop::None),
|
||||||
|
//Anything else is an error.
|
||||||
|
_ => Err(NomErr::Error((input, NomErrorKind::Many1))),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok((input, keep_drop))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse a single digit expression. Does not eat whitespace
|
// Parse a single digit expression. Does not eat whitespace
|
||||||
|
@ -205,6 +221,18 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cant_have_both_keep_and_drop_test() {
|
||||||
|
let res = parse_dice("1d4k3dh2");
|
||||||
|
assert!(res.is_err());
|
||||||
|
match res {
|
||||||
|
Err(NomErr::Error((_, kind))) => {
|
||||||
|
assert_eq!(kind, NomErrorKind::Many1);
|
||||||
|
}
|
||||||
|
_ => panic!("Got success, expected error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn big_number_of_dice_doesnt_crash_test() {
|
fn big_number_of_dice_doesnt_crash_test() {
|
||||||
let res = parse_dice("64378631476346123874527551481376547657868536d4");
|
let res = parse_dice("64378631476346123874527551481376547657868536d4");
|
||||||
|
|
Loading…
Reference in New Issue