use nom::bytes::complete::tag;
use nom::character::complete::{char, digit1};
use nom::combinator::{map, map_res};
use nom::sequence::separated_pair;
use nom::{branch, IResult};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, PartialEq)]
pub struct Roll {
pub number_of_sides: u16,
pub number_of_dice: u16,
pub modifier: i32,
}
impl Roll {
pub fn new(number_of_sides: u16, number_of_dice: u16, modifier: i32) -> Self {
Self {
number_of_sides,
number_of_dice,
modifier,
}
}
fn parse_modified_roll(input: &str) -> Result<Roll, RollError> {
let whitespaceless = input.replace(' ', "");
let (remainder, (number_of_dice, number_of_sides)) =
match parse_simple_roll(&whitespaceless) {
Ok(v) => v,
Err(_) => return Err(RollError::ParsingError),
};
let (_, modifier) = match parse_modifier(remainder) {
Ok(v) => v,
Err(_) => return Err(RollError::ParsingError),
};
Ok(Roll {
number_of_dice,
number_of_sides,
modifier,
})
}
fn check_roll_validity(&self, max_dice: u16) -> Result<(), RollError> {
match self.number_of_sides {
2 => (),
4 => (),
6 => (),
8 => (),
10 => (),
12 => (),
20 => (),
100 => (),
_ => return Err(RollError::DieTypeInvalid),
}
if self.number_of_dice > max_dice && !max_dice != 0 {
return Err(RollError::DiceExceedLimit);
} else if self.number_of_dice == 0 {
return Err(RollError::NoDiceToRoll);
}
Ok(())
}
pub fn parse_roll(input: &str) -> Result<Roll, RollError> {
let result = Roll::parse_modified_roll(input)?;
match result.check_roll_validity(100) {
Ok(()) => Ok(result),
Err(e) => Err(e),
}
}
pub fn parse_roll_with_limit(input: &str, max_dice: u16) -> Result<Roll, RollError> {
let result = Roll::parse_modified_roll(input)?;
match result.check_roll_validity(max_dice) {
Ok(()) => Ok(result),
Err(e) => Err(e),
}
}
}
#[derive(Debug, PartialEq)]
pub enum RollError {
DieTypeInvalid,
DiceExceedLimit,
NoDiceToRoll,
ParsingError,
}
impl fmt::Display for RollError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::DieTypeInvalid => write!(f, "The requested type of die is invalid."),
Self::DiceExceedLimit => write!(f, "Amount of dice exceeds the specified limit."),
Self::NoDiceToRoll => write!(f, "Can't roll less than 1 die."),
Self::ParsingError => write!(f, "Failed to parse the input string."),
}
}
}
impl std::error::Error for RollError {}
fn parse_numbers(input: &str) -> IResult<&str, u16> {
map_res(digit1, u16::from_str)(input)
}
fn parse_simple_roll(s: &str) -> IResult<&str, (u16, u16)> {
let parser = separated_pair(parse_numbers, char('d'), parse_numbers);
map(parser, |(number_of_dice, number_of_sides)| {
(number_of_dice, number_of_sides)
})(s)
}
fn parse_operator(s: &str) -> IResult<&str, &str> {
branch::alt((tag("+"), tag("-"), tag("")))(s)
}
fn parse_modifier(s: &str) -> IResult<&str, i32> {
let (remainder, operator) = parse_operator(s).unwrap();
match operator {
"+" => map(parse_numbers, |modifier| modifier as i32)(remainder),
"-" => map(parse_numbers, |modifier| -(modifier as i32))(remainder),
_ => Ok((remainder, 0)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_roll() {
let tests = [
("4d20", (4, 20), ""),
("4d20remainder_text", (4, 20), "remainder_text"),
];
for (input, expected_output, expected_remaining_input) in tests {
let (remaining_input, output) = parse_simple_roll(input).unwrap();
assert_eq!(remaining_input, expected_remaining_input);
assert_eq!(output, expected_output);
}
}
#[test]
fn test_parse_modifier() {
let tests_positive = [
("+5", 5, ""),
("+5remainder_text", 5, "remainder_text"),
("", 0, ""),
("random_unparsable", 0, "random_unparsable"),
];
let tests_negative = [
("-5", -5, ""),
("-5remainder_text", -5, "remainder_text"),
("", 0, ""),
("random_unparsable", 0, "random_unparsable"),
];
for (input, expected_output, expected_remaining_input) in tests_positive {
let (remaining_input, output) = parse_modifier(input).unwrap();
assert_eq!(remaining_input, expected_remaining_input);
assert_eq!(output, expected_output);
}
for (input, expected_output, expected_remaining_input) in tests_negative {
let (remaining_input, output) = parse_modifier(input).unwrap();
assert_eq!(remaining_input, expected_remaining_input);
assert_eq!(output, expected_output);
}
}
#[test]
fn test_parse_modified_roll() {
let tests = [
("4d10+3", Roll::new(10, 4, 3)),
("4d10-3", Roll::new(10, 4, -3)),
("4 d 10 + 3", Roll::new(10, 4, 3)),
("4 d 10 - 3", Roll::new(10, 4, -3)),
("4d10+3 random_stuff", Roll::new(10, 4, 3)),
("4d10-3 random_stuff", Roll::new(10, 4, -3)),
];
for (input, expected_output) in tests {
let output = Roll::parse_modified_roll(input).unwrap();
assert_eq!(output, expected_output);
}
}
#[test]
fn test_err_modified_roll() {
let tests = [
("4d10+unparsable_modifier", RollError::ParsingError),
("4d10-unparsable_modifier", RollError::ParsingError),
("4d10 + unparsable_modifier", RollError::ParsingError),
("4d10 - unparsable_modifier", RollError::ParsingError),
("4dinvalid_die_type", RollError::ParsingError),
("invalid_die_amountd20", RollError::ParsingError),
];
for (input, expected_output) in tests {
let output = Roll::parse_modified_roll(input).unwrap_err();
assert_eq!(output, expected_output);
}
}
#[test]
fn test_err_parse_roll() {
let tests = [
("4d5", RollError::DieTypeInvalid),
("0d20", RollError::NoDiceToRoll),
("0d5", RollError::DieTypeInvalid),
("9001d20", RollError::DiceExceedLimit),
];
for (input, expected_output) in tests {
let output = Roll::parse_roll(input).unwrap_err();
assert_eq!(output, expected_output);
}
}
}