#![forbid(unsafe_code)]
use rand::{thread_rng, Rng};
use std::convert::{TryFrom, TryInto};
use std::error::Error;
use std::fmt::Display;
use std::fmt::Formatter;
mod parse;
use parse::{wrap_dice, Die, Expr, ParseError, Sign, Term};
pub mod util;
pub(crate) type TResult = Result<i64, RollError>;
impl Display for Term {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
Term::Die(x) => write!(f, "{}d{}", x.number, x.size),
Term::Constant(x) => write!(f, "{}", x),
}
}
}
impl Display for Expr {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
let mut nstr = String::new();
match self.sign {
Sign::Positive => (),
Sign::Negative => nstr.push_str("-"),
}
nstr.push_str(&format!("{}", self.term));
write!(f, "{}", nstr)
}
}
#[derive(Debug, Clone)]
pub struct ExpressionResult {
pairs: Vec<(Expr, i64)>,
total: i64,
}
impl ExpressionResult {
pub fn total(&self) -> i64 {
self.total
}
}
impl Display for ExpressionResult {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
let mut nstr = self.total.to_string();
if self.pairs.len() > 1 {
nstr.push_str(" = (");
let mut iter = self.pairs.iter();
let first = iter.next().unwrap();
let form = |prior: Expr, val: i64| match prior.term {
Term::Constant(_) => format!("{}", val),
Term::Die(_) => format!("{} → {}", prior, val),
};
nstr.push_str(&form(first.0, first.1));
for x in iter {
nstr.push_str(&format!(", {}", form(x.0, x.1)));
}
nstr.push_str(")");
}
write!(f, "{}", nstr)
}
}
#[derive(Debug, Clone, Copy)]
pub enum RollError {
InvalidDie,
OverflowPositive,
OverflowNegative,
InvalidExpression,
}
impl From<ParseError> for RollError {
fn from(e: ParseError) -> Self {
match e {
ParseError::InvalidExpression => RollError::InvalidExpression,
}
}
}
impl Display for RollError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
RollError::InvalidDie => write!(f, "Invalid die"),
RollError::OverflowPositive => write!(f, "sum is too high for `i64`"),
RollError::OverflowNegative => write!(f, "sum is too low for `i64`"),
RollError::InvalidExpression => {
write!(f, "you've specified an invalid dice expression.")
}
}
}
}
impl Error for RollError {}
type EResult = Result<ExpressionResult, RollError>;
fn roll_die_with<R>(a: &Die, rng: &mut R) -> Result<i64, RollError>
where
R: Rng,
{
if a.size == 1 {
Ok(a.number)
} else if a.size < 1 {
Err(RollError::InvalidDie)
} else {
let mut acc: i64 = 0;
for n in (0..a.number).map(|_| rng.gen_range(1, a.size + 1)) {
acc = match acc.checked_add(n) {
Some(x) => x,
None => return Err(RollError::OverflowPositive),
}
}
Ok(acc)
}
}
fn eval_term_with<R>(a: &Expr, rng: &mut R) -> TResult
where
R: Rng,
{
let t = match a.term {
Term::Die(x) => roll_die_with(&x, rng),
Term::Constant(x) => Ok(x),
};
let p = match a.sign {
Sign::Positive => match t {
x => x,
},
Sign::Negative => match t {
Ok(x) => Ok(x),
Err(e) => match e {
RollError::OverflowPositive => Err(RollError::OverflowNegative),
x => Err(x),
},
},
};
match p {
Ok(x) => match x.try_into() {
Ok(x) => match a.sign {
Sign::Positive => Ok(x),
Sign::Negative => Ok(-x),
},
Err(_) => Err(RollError::OverflowPositive),
},
Err(x) => Err(x),
}
}
pub fn roll(input: &str) -> EResult {
match wrap_dice(input) {
Ok(x) => Ok(roll_expr_iter(x.into_iter())?),
Err(x) => Err(RollError::from(x)),
}
}
type ExprTuple = (i64, i64);
pub fn tupl_vec(input: &str) -> Result<Vec<ExprTuple>, RollError> {
let e = wrap_dice(input)?;
Ok(e.into_iter().map(|x| x.into()).collect())
}
impl TryFrom<ExprTuple> for Expr {
type Error = RollError;
fn try_from(tup: ExprTuple) -> Result<Self, RollError> {
let (mut n, s) = tup;
let sign = if n < 0 {
n = -n;
Sign::Negative
} else {
Sign::Positive
};
Ok(Self {
term: if s > 1 {
Term::Die(Die::new(n, s)?)
} else {
Term::Constant(n)
},
sign,
})
}
}
impl From<Expr> for ExprTuple {
fn from(e: Expr) -> ExprTuple {
let t = match e.term {
Term::Die(x) => (x.number, x.size),
Term::Constant(x) => (x, 1),
};
match e.sign {
Sign::Positive => t,
Sign::Negative => (-t.0, t.1),
}
}
}
fn try_roll_expr_iter<I>(input: I) -> EResult
where
I: Iterator<Item = Result<Expr, RollError>>,
{
let mut rng = thread_rng();
let mut pairs = Vec::new();
let mut total: i64 = 0;
for x in input {
match x {
Ok(x) => {
let res = match eval_term_with(&x, &mut rng) {
Ok(x) => x,
Err(x) => return Err(x),
};
pairs.push((x, res));
match total.checked_add(res) {
Some(x) => total = x,
None => {
return if res > 0 {
Err(RollError::OverflowPositive)
} else {
Err(RollError::OverflowNegative)
}
}
}
}
Err(x) => return Err(x),
}
}
Ok(ExpressionResult { pairs, total })
}
fn roll_expr_iter<I>(input: I) -> EResult
where
I: Iterator<Item = Expr>,
{
try_roll_expr_iter(input.map(Ok))
}
fn roll_tupl_iter<'a, I>(input: I) -> EResult
where
I: Iterator<Item = &'a ExprTuple>,
{
let terms = input.map(|x| Expr::try_from(*x));
try_roll_expr_iter(terms)
}
pub fn roll_tupls(input: &[ExprTuple]) -> EResult {
roll_tupl_iter(input.iter())
}
#[cfg(test)]
mod tests {
use crate::{roll, Die};
#[test]
fn arithmetic() {
assert_eq!(roll("5 + 3").unwrap().total, 8);
assert_eq!(roll("5 - 3").unwrap().total, 2);
}
#[test]
fn dice() {
let mut good = true;
match Die::new(0, 0) {
Ok(_) => good = false,
Err(_) => (),
}
if !good {
panic!()
}
}
}