From fc1331fc8593ec88d122918e7f97e59d1d51b3fe Mon Sep 17 00:00:00 2001 From: Nathan Perry Date: Sun, 3 Mar 2019 20:56:35 -0500 Subject: initial rework of rolling framework --- src/commands/roll.rs | 253 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 207 insertions(+), 46 deletions(-) (limited to 'src/commands/roll.rs') diff --git a/src/commands/roll.rs b/src/commands/roll.rs index b3f7a38..8087d80 100644 --- a/src/commands/roll.rs +++ b/src/commands/roll.rs @@ -1,63 +1,224 @@ +use failure::err_msg; +use nom::{ + self, + digit, + types::CompleteStr, +}; use rand::prelude::*; -use regex::Regex; use serenity::{ framework::standard::Args, model::channel::Message, prelude::*, }; +use statrs; use crate::{ commands::send, Result, }; -lazy_static! { - static ref ROLL_REGEX: Regex = Regex::new(r"([0-9]+)?(?:d([0-9]+)(?:\s+\+\s+([0-9]+))?)") - .expect("error parsing roll regex"); +#[derive(Clone, Debug, PartialEq)] +enum CalcExpr { + Binary(BinOp, Box, Box), + Unary(UnaryOp, Box), + Term(f64), } -pub fn roll(_ctx: &mut Context, msg: &Message, args: Args) -> Result<()> { - let captures = match ROLL_REGEX.captures(args.full()) { - Some(captures) => captures, - None => return send(msg.channel_id, "conway is a goldfish", msg.tts), - }; - - let dice_count = match captures.get(1) { - Some(x) => { - match x.as_str().parse::() { - Ok(x) => x, - Err(e) => { - send(msg.channel_id, "conway is a goldfish", msg.tts)?; - return Err(e.into()); - }, - } - }, - None => 1, - }; - - if dice_count > 1000000 { - send(msg.channel_id, "no.", msg.tts)?; - return Ok(()); +impl CalcExpr { + fn parse>(input: S) -> Result> { + parse_expr(CompleteStr(input.as_ref())) + .map(|(_, res)| res) + .map_err(|e| err_msg(format!("couldn't parse: {}", e))) + } + + fn compute(self: Box) -> f64 { + use self::CalcExpr::*; + use self::BinOp::*; + use self::UnaryOp::*; + + let s = *self; + match s { + Binary(bop, e1, e2) => { + let r1 = e1.compute(); + let r2 = e2.compute(); + match bop { + Add => r1 + r2, + Sub => r1 - r2, + Mul => r1 * r2, + Div => r1 / r2, + Mod => r1 % r2, + Pow => r1.powf(r2), + Min => r1.min(r2), + Max => r1.max(r2), + DiceRoll => { + let dice_count = r1 as usize; + let dice_faces = r2 as usize; + + let mut rng = thread_rng(); + (0..dice_count).map(|_| rng.gen_range(1, dice_faces + 1)).sum::() as f64 + } + } + }, + Unary(uop, e) => { + let r = e.compute(); + + match uop { + Neg => -r, + Log => r.ln(), + Sqrt => r.sqrt(), + Sgn => r.signum(), + Sin => r.sin(), + Cos => r.cos(), + Tan => r.tan(), + Factorial => statrs::function::gamma::gamma(r), + Exp => r.exp(), + } + }, + Term(v) => v, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum BinOp { + Add, + Sub, + Mul, + Div, + Mod, + Pow, + Min, + Max, + DiceRoll, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum UnaryOp { + Log, + Sqrt, + Sgn, + Exp, + Sin, + Cos, + Tan, + Factorial, + Neg, +} + +fn parse_expr(input: CompleteStr) -> nom::IResult> { + ws!(input, alt_complete!( + parse_infix | + parse_dice | + parse_binary_prefix | + parse_suffix | + parse_prefix | + parse_term_or_paren + )) +} + +fn parse_term_or_paren(input: CompleteStr) -> nom::IResult> { + ws!(input, alt_complete!( + delimited!(char!('('), parse_expr, char!(')')) | + do_parse!( + dat: flat_map!(digit, parse_to!(f64)) >> + (Box::new(CalcExpr::Term(dat))) + ) + )) +} + +fn parse_dice(input: CompleteStr) -> nom::IResult> { + ws!(input, do_parse!( + tpl: separated_pair!(parse_term_or_paren, ws!(char!('d')), parse_term_or_paren) >> + ({ + let (expr1, expr2) = tpl; + Box::new(CalcExpr::Binary(BinOp::DiceRoll, expr1, expr2)) + }) + )) +} + +fn parse_infix(input: CompleteStr) -> nom::IResult> { + ws!(input, do_parse!( + tpl: tuple!(parse_term_or_paren, ws!(one_of!("+-*/%^")), parse_term_or_paren) >> + ({ + let (expr1, op, expr2) = tpl; + let op = match op { + '+' => BinOp::Add, + '-' => BinOp::Sub, + '*' => BinOp::Mul, + '/' => BinOp::Div, + '%' => BinOp::Mod, + '^' => BinOp::Pow, + _ => unreachable!(), + }; + Box::new(CalcExpr::Binary(op, expr1, expr2)) + }) + )) +} + +fn parse_prefix(input: CompleteStr) -> nom::IResult> { + ws!(input, do_parse!( + op: ws!(alt_complete!( + tag!("log") => { |_| UnaryOp::Log } + | tag!("sqrt") => { |_| UnaryOp::Sqrt } + | tag!("sin") => { |_| UnaryOp::Sin } + | tag!("cos") => { |_| UnaryOp::Cos } + | tag!("tan") => { |_| UnaryOp::Tan } + | tag!("sgn") => { |_| UnaryOp::Sgn } + | tag!("exp") => { |_| UnaryOp::Exp } + | char!('-') => { |_| UnaryOp::Neg } + )) >> + expr: parse_term_or_paren >> + (Box::new(CalcExpr::Unary(op, expr))) + )) +} + +fn parse_binary_prefix(input: CompleteStr) -> nom::IResult> { + ws!(input, do_parse!( + op: ws!(alt_complete!( + tag!("min") => { |_| BinOp::Min } | + tag!("max") => { |_| BinOp::Max } + )) >> + expr1: parse_term_or_paren >> + expr2: parse_term_or_paren >> + (Box::new(CalcExpr::Binary(op, expr1, expr2))) + )) +} + +fn parse_suffix(input: CompleteStr) -> nom::IResult> { + ws!(input, do_parse!( + expr: terminated!(parse_term_or_paren, ws!(tag!("!"))) >> + (Box::new(CalcExpr::Unary(UnaryOp::Factorial, expr))) + )) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse_usize() { + let (s, expr) = parse_expr("123".into()).unwrap(); + assert_eq!(s.0, ""); + assert_eq!(expr, box CalcExpr::Term(123.)); } - let faces = match captures.get(2).unwrap().as_str().parse::() { - Ok(faces) => faces, - Err(e) => { - send(msg.channel_id, "conway is a goldfish", msg.tts)?; - return Err(e.into()) - }, - }; - - let adjust = match captures.get(3).map(|adjust| adjust.as_str().parse::()).transpose() { - Ok(adjust) => adjust.unwrap_or(0), - Err(e) => { - send(msg.channel_id, "conway is a goldfish", msg.tts)?; - return Err(e.into()) - }, - }; - - let mut rng = thread_rng(); - let total = (0..dice_count).map(|_| rng.gen_range(0, faces)).sum::() + adjust + dice_count; - - send(msg.channel_id, &format!("{}", total), msg.tts) + #[test] + fn test_parens() { + let (s, expr) = parse_expr("(123)".into()).unwrap(); + assert_eq!(s.0, ""); + assert_eq!(expr, box CalcExpr::Term(123.)); + } + + #[test] + fn test_infix() { + let (s, expr) = parse_expr("1 + 2".into()).unwrap(); + assert_eq!(s.0, ""); + assert_eq!(expr, box CalcExpr::Binary(BinOp::Add, box CalcExpr::Term(1.), box CalcExpr::Term(2.))) + } + +} + +pub fn roll(_ctx: &mut Context, msg: &Message, args: Args) -> Result<()> { + let expr = CalcExpr::parse(args.rest())?; + send(msg.channel_id, &format!("{}", expr.compute()), msg.tts) } -- cgit v1.3.1