diff --git a/Cargo.lock b/Cargo.lock index 32c83fe..5a8a2e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,6 +172,7 @@ dependencies = [ "async-trait", "dirs", "env_logger", + "indoc", "itertools", "log", "matrix-sdk", @@ -680,6 +681,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644defcefee68d7805653a682e99a2e2a5014a1fc3cc9be7059a215844eeea6f" +dependencies = [ + "unindent", +] + [[package]] name = "instant" version = "0.1.6" @@ -2051,6 +2061,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "unindent" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af41d708427f8fd0e915dcebb2cae0f0e6acb2a939b2d399c265c39a38a18942" + [[package]] name = "url" version = "2.1.1" diff --git a/Cargo.toml b/Cargo.toml index 369562d..c4c99a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ itertools = "0.9" async-trait = "0.1" url = "2.1" dirs = "3.0" +indoc = "1.0" # The versioning of the matrix SDK follows its Cargo.toml. The SDK and # macros are on master, but it imports the common and base from 0.1.0. diff --git a/src/commands.rs b/src/commands.rs index 8e3bd3c..9880469 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,5 +1,6 @@ use crate::cofd::dice::DicePool; use crate::dice::ElementExpression; +use crate::help::HelpTopic; use crate::parser::trim; use crate::roll::Roll; @@ -61,10 +62,29 @@ impl Command for PoolRollCommand { } } -/// Parse a command string into a dynamic command execution trait object. -/// Returns an error if a command was recognized but not parsed correctly. Returns None if no -/// command was recognized. -pub fn parse_command(s: &str) -> Result>, String> { +pub struct HelpCommand(Option); + +impl Command for HelpCommand { + fn name(&self) -> &'static str { + "help information" + } + + fn execute(&self) -> Execution { + let help = match &self.0 { + Some(topic) => topic.message(), + _ => "There is no help for this topic", + }; + + let plain = format!("Help: {}", help); + let html = format!("

Help: {}", help.replace("\n", "
")); + Execution { plain, html } + } +} + +/// Parse a command string into a dynamic command execution trait +/// object. Returns an error if a command was recognized but not +/// parsed correctly. Returns None if no command was recognized. +pub fn parse_command<'a>(s: &'a str) -> Result>, String> { match parser::parse_command(s) { Ok((input, command)) => match (input, &command) { //This clause prevents bot from spamming messages to itself @@ -115,6 +135,19 @@ mod tests { .expect("was error")); } + #[test] + fn help_whitespace_test() { + assert!(parse_command("!help stuff ") + .map(|p| p.is_some()) + .expect("was error")); + assert!(parse_command(" !help stuff") + .map(|p| p.is_some()) + .expect("was error")); + assert!(parse_command(" !help stuff ") + .map(|p| p.is_some()) + .expect("was error")); + } + #[test] fn roll_whitespace_test() { assert!(parse_command("!roll 1d4 + 5d6 -3 ") diff --git a/src/commands/parser.rs b/src/commands/parser.rs index cf709da..afd3e21 100644 --- a/src/commands/parser.rs +++ b/src/commands/parser.rs @@ -1,9 +1,10 @@ use nom::{alt, complete, named, tag, take_while, tuple, IResult}; use crate::cofd::parser::{create_chance_die, parse_dice_pool}; -use crate::commands::{Command, PoolRollCommand, RollCommand}; +use crate::commands::{Command, HelpCommand, PoolRollCommand, RollCommand}; use crate::dice::parser::parse_element_expression; -use crate::parser::eat_whitespace; +use crate::help::parse_help_topic; +use crate::parser::{eat_whitespace, trim}; // Parse a roll expression. fn parse_roll(input: &str) -> IResult<&str, Box> { @@ -23,6 +24,12 @@ fn chance_die() -> IResult<&'static str, Box> { Ok((input, Box::new(PoolRollCommand(pool)))) } +fn help(topic: &str) -> IResult<&str, Box> { + let (topic, _) = eat_whitespace(topic)?; + let topic = parse_help_topic(&trim(topic)); + Ok(("", Box::new(HelpCommand(topic)))) +} + /// Potentially parse a command expression. If we recognize the command, an error should be raised /// if the command is misparsed. If we don't recognize the command, ignore it and return none pub fn parse_command(original_input: &str) -> IResult<&str, Option>> { @@ -32,7 +39,9 @@ pub fn parse_command(original_input: &str) -> IResult<&str, Option (&str, &str), tuple!( complete!(tag!("!")), alt!( - complete!(tag!("chance")) | //TODO figure out how to just have it read single commands. + //TODO figure out how to gracefully handle arbitrary single commands. + complete!(tag!("chance")) | + complete!(tag!("help")) | complete!(take_while!(char::is_alphabetic)) ) )); @@ -40,13 +49,16 @@ pub fn parse_command(original_input: &str) -> IResult<&str, Option (input, result), - Err(_e) => return Ok((original_input, None)), + Err(_e) => { + return Ok((original_input, None)); + } }; match command { "r" | "roll" => parse_roll(input).map(|(input, command)| (input, Some(command))), "rp" | "pool" => parse_pool_roll(input).map(|(input, command)| (input, Some(command))), "chance" => chance_die().map(|(input, command)| (input, Some(command))), + "help" => help(input).map(|(input, command)| (input, Some(command))), // No recognized command, ignore this. _ => Ok((original_input, None)), } diff --git a/src/help.rs b/src/help.rs new file mode 100644 index 0000000..bb34cbc --- /dev/null +++ b/src/help.rs @@ -0,0 +1,86 @@ +use indoc::indoc; + +pub fn parse_help_topic(input: &str) -> Option { + match input { + "cofd" => Some(HelpTopic::ChroniclesOfDarkness), + "dicepool" => Some(HelpTopic::DicePool), + "dice" => Some(HelpTopic::RollingDice), + "" => Some(HelpTopic::General), + _ => None, + } +} + +pub enum HelpTopic { + ChroniclesOfDarkness, + DicePool, + RollingDice, + General, +} + +const COFD_HELP: &'static str = indoc! {" +Chronicles of Darkness + +Commands available: + !pool, !rp: roll a dice pool + !chance: roll a chance die + +See also: + !help dicepool +"}; + +const DICE_HELP: &'static str = indoc! {" +Rolling basic dice + +Command: !roll, !r + +Syntax !roll + +Dice expression can be a basic die (e.g. 1d4), with a bonus (1d4+3), +or a more complex series of dice rolls or arbitrary numbers. +Parentheses are not supported. + +Examples: + !roll 1d4 + !roll 1d4+5 + !roll 2d6+8 + !roll 2d8 + 4d6 - 3 +"}; + +const DICEPOOL_HELP: &'static str = indoc! {" +Rolling dice pools + +Command: !pool, !rp + +Syntax: !pool + +Modifiers: + n = nine-again + e = eight-again + r = rote quality + x = do not re-roll 10s + s = number of successes for exceptional + +Examples: + !pool 8 (roll a regular pool of 8 dice) + !pool 5n (roll dice pool of 5, nine-again) + !pool 6rs3 (roll dice pool of 6, rote quality, 3 successes for exceptional) +"}; + +const GENERAL_HELP: &'static str = indoc! {" +General Help + +Try these help commands: + !help cofd + !help dice +"}; + +impl HelpTopic { + pub fn message(&self) -> &str { + match self { + HelpTopic::ChroniclesOfDarkness => COFD_HELP, + HelpTopic::DicePool => DICEPOOL_HELP, + HelpTopic::RollingDice => DICE_HELP, + HelpTopic::General => GENERAL_HELP, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 5d6b456..1bb1f4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,5 +2,6 @@ pub mod bot; pub mod cofd; pub mod commands; pub mod dice; +mod help; mod parser; pub mod roll;