From 2c08eb41adbe80e34ec4c615565059a316f5d98b Mon Sep 17 00:00:00 2001 From: projectmoon Date: Fri, 21 Aug 2020 21:49:22 +0000 Subject: [PATCH] Implement Chronicles of Darkness dice system, improve error handling. Adds the Chronicles of Darkness 2E dice system to the bot, and also somewhat improves the error handling when weird commands are received. --- Cargo.lock | 37 +++++ Cargo.toml | 2 + src/cofd.rs | 315 +++++++++++++++++++++++++++++++++++++++++ src/cofd/parser.rs | 232 ++++++++++++++++++++++++++++++ src/commands.rs | 56 +++++++- src/commands/parser.rs | 30 +++- src/lib.rs | 1 + src/roll.rs | 5 +- 8 files changed, 669 insertions(+), 9 deletions(-) create mode 100644 src/cofd.rs create mode 100644 src/cofd/parser.rs diff --git a/Cargo.lock b/Cargo.lock index fcc811e..b6cc4f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,11 +22,13 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" name = "axfive-matrix-dicebot" version = "0.1.2" dependencies = [ + "itertools", "nom", "rand", "reqwest", "serde", "serde_json", + "thiserror", "tokio", "toml", ] @@ -89,6 +91,12 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" +[[package]] +name = "either" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" + [[package]] name = "encoding_rs" version = "0.8.23" @@ -313,6 +321,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.6" @@ -815,6 +832,26 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "thiserror" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.1.43" diff --git a/Cargo.toml b/Cargo.toml index 5f8f93c..0cd3e0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ serde_json = "1" toml = "0.5" nom = "5" rand = "0.7" +thiserror = "1.0" +itertools = "0.9" [dependencies.serde] version = "1" diff --git a/src/cofd.rs b/src/cofd.rs new file mode 100644 index 0000000..aa81ba1 --- /dev/null +++ b/src/cofd.rs @@ -0,0 +1,315 @@ +use crate::roll::{Roll, Rolled}; +use itertools::Itertools; +use std::convert::TryFrom; +use std::fmt; + +pub mod parser; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum DicePoolQuality { + TenAgain, + NineAgain, + EightAgain, + Rote, + ChanceDie, +} + +impl fmt::Display for DicePoolQuality { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DicePoolQuality::TenAgain => write!(f, "ten-again"), + DicePoolQuality::NineAgain => write!(f, "nine-again"), + DicePoolQuality::EightAgain => write!(f, "eight-again"), + DicePoolQuality::Rote => write!(f, "rote quality"), + DicePoolQuality::ChanceDie => write!(f, "chance die"), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct DicePool { + pub(crate) count: u32, + pub(crate) sides: u32, + pub(crate) success_on: u32, + pub(crate) exceptional_success: u32, + pub(crate) quality: DicePoolQuality, +} + +impl fmt::Display for DicePool { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} dice ({}, exceptional on {} successes)", + self.count, self.quality, self.exceptional_success + ) + } +} + +impl DicePool { + fn new(count: u32, successes_for_exceptional: u32, quality: DicePoolQuality) -> DicePool { + DicePool { + count: count, + sides: 10, //TODO make configurable + success_on: 8, //TODO make configurable + exceptional_success: successes_for_exceptional, + quality: quality, + } + } + + pub fn chance_die() -> DicePool { + DicePool { + count: 1, + sides: 10, + success_on: 10, + exceptional_success: 5, + quality: DicePoolQuality::ChanceDie, + } + } +} + +///Store all rolls of the dice pool dice into one struct. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct DicePoolRoll { + success_on: u32, + exceptional_on: u32, + rolls: Vec, +} + +impl DicePoolRoll { + pub fn rolls(&self) -> &[u32] { + &self.rolls + } + + pub fn successes(&self) -> i32 { + let successes = self + .rolls + .iter() + .cloned() + .filter(|&roll| roll >= self.success_on) + .count(); + i32::try_from(successes).unwrap_or(0) + } + + pub fn is_exceptional(&self) -> bool { + self.successes() >= (self.exceptional_on as i32) + } +} + +impl Roll for DicePool { + type Output = DicePoolRoll; + + fn roll(&self) -> DicePoolRoll { + roll_dice(self) + } +} + +impl Rolled for DicePoolRoll { + fn rolled_value(&self) -> i32 { + self.successes() + } +} + +impl fmt::Display for DicePoolRoll { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let successes = self.successes(); + if successes > 0 { + let success_msg = if self.is_exceptional() { + format!("{} successes (exceptional!)", successes) + } else { + format!("{} successes", successes) + }; + + write!(f, "{} ({})", success_msg, self.rolls().iter().join(", "))?; + } else { + write!(f, "failure! ({})", self.rolls().iter().join(", "))?; + } + + Ok(()) + } +} + +trait DieRoller { + fn roll_number(&mut self, sides: u32) -> u32; +} + +///A version of DieRoller that uses a rand::Rng to roll numbers. +struct RngDieRoller(R); + +impl DieRoller for RngDieRoller { + fn roll_number(&mut self, sides: u32) -> u32 { + self.0.gen_range(1, sides + 1) + } +} + +///Roll a die in the pool, that "explodes" on a given number or higher. Dice will keep +///being rolled until the result is lower than the explode number, which is normally 10. +///Statistically speaking, usually one result will be returned from this function. +fn roll_exploding_die( + roller: &mut R, + sides: u32, + explode_on_or_higher: u32, +) -> Vec { + let mut results = vec![]; + loop { + let roll = roller.roll_number(sides); + results.push(roll); + if roll < explode_on_or_higher { + break; + } + } + results +} + +///A die with the rote quality is re-rolled once if the roll fails. Otherwise, it obeys +///all normal rules (re-roll 10s). Re-rolled dice are appended to the result set, so we +///can keep track of the actual dice that were rolled. +fn roll_rote_die(roller: &mut R, sides: u32) -> Vec { + let mut rolls = roll_exploding_die(roller, sides, 10); + if rolls.len() == 1 && rolls[0] < 8 { + rolls.append(&mut roll_exploding_die(roller, sides, 10)); + } + + rolls +} + +///Roll a single die in the pool, potentially rolling additional dice depending on pool +///behavior. The default ten-again will "explode" the die if the result is 10 (repeatedly, if +///there are multiple 10s). Nine- and eight-again will explode similarly if the result is +///at least that number. Rote quality will re-roll a failure once, while also exploding +///on 10. The function returns a Vec of all rolled dice (usually 1). +fn roll_die(roller: &mut R, sides: u32, quality: DicePoolQuality) -> Vec { + let mut results = vec![]; + match quality { + DicePoolQuality::TenAgain => results.append(&mut roll_exploding_die(roller, sides, 10)), + DicePoolQuality::NineAgain => results.append(&mut roll_exploding_die(roller, sides, 9)), + DicePoolQuality::EightAgain => results.append(&mut roll_exploding_die(roller, sides, 8)), + DicePoolQuality::Rote => results.append(&mut roll_rote_die(roller, sides)), + DicePoolQuality::ChanceDie => results.push(roller.roll_number(sides)), + } + + results +} + +///Roll the dice in a dice pool, according to behavior documented in the various rolling +///methods. +fn roll_dice(pool: &DicePool) -> DicePoolRoll { + let mut roller = RngDieRoller(rand::thread_rng()); + + let rolls: Vec = (0..pool.count) + .flat_map(|_| roll_die(&mut roller, pool.sides, pool.quality)) + .collect(); + + DicePoolRoll { + rolls: rolls, + exceptional_on: pool.exceptional_success, + success_on: pool.success_on, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + ///Instead of being random, generate a series of numbers we have complete + ///control over. + struct SequentialDieRoller { + results: Vec, + position: usize, + } + + impl SequentialDieRoller { + fn new(results: Vec) -> SequentialDieRoller { + SequentialDieRoller { + results: results, + position: 0, + } + } + } + + impl DieRoller for SequentialDieRoller { + fn roll_number(&mut self, _sides: u32) -> u32 { + let roll = self.results[self.position]; + self.position += 1; + roll + } + } + + //Dice rolling tests. + #[test] + pub fn ten_again_test() { + let mut roller = SequentialDieRoller::new(vec![10, 8, 1]); + let rolls = roll_exploding_die(&mut roller, 10, 10); + assert_eq!(vec![10, 8], rolls); + } + + #[test] + pub fn nine_again_test() { + let mut roller = SequentialDieRoller::new(vec![10, 9, 8, 1]); + let rolls = roll_exploding_die(&mut roller, 10, 9); + assert_eq!(vec![10, 9, 8], rolls); + } + + #[test] + pub fn eight_again_test() { + let mut roller = SequentialDieRoller::new(vec![10, 9, 8, 8, 1]); + let rolls = roll_exploding_die(&mut roller, 10, 8); + assert_eq!(vec![10, 9, 8, 8, 1], rolls); + } + + #[test] + pub fn rote_quality_fail_then_succeed_test() { + let mut roller = SequentialDieRoller::new(vec![5, 8, 1]); + let rolls = roll_rote_die(&mut roller, 10); + assert_eq!(vec![5, 8], rolls); + } + + #[test] + pub fn rote_quality_fail_twice_test() { + let mut roller = SequentialDieRoller::new(vec![5, 6, 10]); + let rolls = roll_rote_die(&mut roller, 10); + assert_eq!(vec![5, 6], rolls); + } + + #[test] + pub fn rote_quality_fail_then_explode_test() { + let mut roller = SequentialDieRoller::new(vec![5, 10, 8, 1]); + let rolls = roll_rote_die(&mut roller, 10); + assert_eq!(vec![5, 10, 8], rolls); + } + + //DiceRoll tests + #[test] + fn is_successful_on_equal_test() { + let result = DicePoolRoll { + rolls: vec![8], + exceptional_on: 5, + success_on: 8, + }; + + assert_eq!(1, result.successes()); + } + + #[test] + fn is_exceptional_test() { + let result = DicePoolRoll { + rolls: vec![8, 8, 9, 10, 8], + exceptional_on: 5, + success_on: 8, + }; + + assert_eq!(5, result.successes()); + assert_eq!(true, result.is_exceptional()); + } + + #[test] + fn is_not_exceptional_test() { + let result = DicePoolRoll { + rolls: vec![8, 8, 9, 10], + exceptional_on: 5, + success_on: 8, + }; + + assert_eq!(4, result.successes()); + assert_eq!(false, result.is_exceptional()); + } +} diff --git a/src/cofd/parser.rs b/src/cofd/parser.rs new file mode 100644 index 0000000..71ca0da --- /dev/null +++ b/src/cofd/parser.rs @@ -0,0 +1,232 @@ +use nom::{ + alt, bytes::complete::tag, character::complete::digit1, complete, many0, named, + sequence::tuple, tag, IResult, +}; + +use crate::cofd::{DicePool, DicePoolQuality}; +use crate::parser::eat_whitespace; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum DicePoolElement { + NumberOfDice(u32), + SuccessesForExceptional(u32), + DicePoolQuality(DicePoolQuality), +} + +// Parse a single digit expression. Does not eat whitespace +fn parse_digit(input: &str) -> IResult<&str, u32> { + let (input, num) = digit1(input)?; + Ok((input, num.parse().unwrap())) +} + +fn parse_quality(input: &str) -> IResult<&str, DicePoolQuality> { + let (input, _) = eat_whitespace(input)?; + named!(quality(&str) -> DicePoolQuality, alt!( + complete!(tag!("n")) => { |_| DicePoolQuality::NineAgain } | + complete!(tag!("e")) => { |_| DicePoolQuality::EightAgain } | + complete!(tag!("r")) => { |_| DicePoolQuality::Rote } + )); + + let (input, dice_pool_quality) = quality(input)?; + Ok((input, dice_pool_quality)) +} + +fn parse_exceptional_requirement(input: &str) -> IResult<&str, u32> { + let (input, _) = eat_whitespace(input)?; + let (input, (_, successes)) = tuple((tag("s"), digit1))(input)?; + Ok((input, successes.parse().unwrap())) +} + +// Parse a dice pool element expression. Eats whitespace. +fn parse_dice_pool_element(input: &str) -> IResult<&str, DicePoolElement> { + let (input, _) = eat_whitespace(input)?; + named!(element(&str) -> DicePoolElement, alt!( + parse_digit => { |num| DicePoolElement::NumberOfDice(num) } | + parse_quality => { |qual| DicePoolElement::DicePoolQuality(qual) } | + parse_exceptional_requirement => { |succ| DicePoolElement::SuccessesForExceptional(succ) } + )); + + let (input, element) = element(input)?; + Ok((input, element)) +} + +fn find_elements(elements: Vec) -> (Option, DicePoolQuality, u32) { + let mut found_quality: Option = None; + let mut found_count: Option = None; + let mut found_successes_required: Option = None; + + for element in elements { + if found_quality.is_some() && found_count.is_some() && found_successes_required.is_some() { + break; + } + + match element { + DicePoolElement::NumberOfDice(found) => { + if found_count.is_none() { + found_count = Some(found); + } + } + DicePoolElement::DicePoolQuality(found) => { + if found_quality.is_none() { + found_quality = Some(found); + } + } + DicePoolElement::SuccessesForExceptional(found) => { + if found_successes_required.is_none() { + found_successes_required = Some(found); + } + } + }; + } + + let quality: DicePoolQuality = found_quality.unwrap_or(DicePoolQuality::TenAgain); + let successes_for_exceptional: u32 = found_successes_required.unwrap_or(5); + (found_count, quality, successes_for_exceptional) +} + +fn convert_to_dice_pool(input: &str, elements: Vec) -> IResult<&str, DicePool> { + let (count, quality, successes_for_exceptional) = find_elements(elements); + + if count.is_some() { + Ok(( + input, + DicePool::new(count.unwrap(), successes_for_exceptional, quality), + )) + } else { + use nom::error::ErrorKind; + use nom::Err; + Err(Err::Error((input, ErrorKind::Alt))) + } +} + +pub fn parse_dice_pool(input: &str) -> IResult<&str, DicePool> { + named!(first_element(&str) -> DicePoolElement, alt!( + parse_dice_pool_element => { |e| e } + )); + let (input, first) = first_element(input)?; + let (input, elements) = if input.trim().is_empty() { + (input, vec![first]) + } else { + named!(rest_elements(&str) -> Vec, many0!(parse_dice_pool_element)); + let (input, mut rest) = rest_elements(input)?; + rest.insert(0, first); + (input, rest) + }; + + convert_to_dice_pool(input, elements) +} + +pub fn create_chance_die() -> IResult<&'static str, DicePool> { + Ok(("", DicePool::chance_die())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_digit_test() { + use nom::error::ErrorKind; + use nom::Err; + assert_eq!(parse_digit("1"), Ok(("", 1))); + assert_eq!(parse_digit("10"), Ok(("", 10))); + assert_eq!( + parse_digit("adsf"), + Err(Err::Error(("adsf", ErrorKind::Digit))) + ); + } + + #[test] + fn quality_test() { + use nom::error::ErrorKind; + use nom::Err; + assert_eq!(parse_quality("n"), Ok(("", DicePoolQuality::NineAgain))); + assert_eq!(parse_quality("e"), Ok(("", DicePoolQuality::EightAgain))); + assert_eq!(parse_quality("r"), Ok(("", DicePoolQuality::Rote))); + assert_eq!(parse_quality("b"), Err(Err::Error(("b", ErrorKind::Alt)))); + } + + #[test] + fn multiple_quality_test() { + assert_eq!(parse_quality("ner"), Ok(("er", DicePoolQuality::NineAgain))); + } + + #[test] + fn exceptional_success_test() { + use nom::error::ErrorKind; + use nom::Err; + assert_eq!(parse_exceptional_requirement("s3"), Ok(("", 3))); + assert_eq!(parse_exceptional_requirement("s10"), Ok(("", 10))); + assert_eq!(parse_exceptional_requirement("s20b"), Ok(("b", 20))); + assert_eq!( + parse_exceptional_requirement("sab10"), + Err(Err::Error(("ab10", ErrorKind::Digit))) + ); + } + + #[test] + fn dice_pool_element_expression_test() { + use nom::error::ErrorKind; + use nom::Err; + + assert_eq!( + parse_dice_pool_element("8"), + Ok(("", DicePoolElement::NumberOfDice(8))) + ); + + assert_eq!( + parse_dice_pool_element("n"), + Ok(( + "", + DicePoolElement::DicePoolQuality(DicePoolQuality::NineAgain) + )) + ); + + assert_eq!( + parse_dice_pool_element("s3"), + Ok(("", DicePoolElement::SuccessesForExceptional(3))) + ); + + assert_eq!( + parse_dice_pool_element("8ns3"), + Ok(("ns3", DicePoolElement::NumberOfDice(8))) + ); + + assert_eq!( + parse_dice_pool_element("totallynotvalid"), + Err(Err::Error(("totallynotvalid", ErrorKind::Alt))) + ); + } + + #[test] + fn dice_pool_number_only_test() { + assert_eq!( + parse_dice_pool("8"), + Ok(("", DicePool::new(8, 5, DicePoolQuality::TenAgain))) + ); + } + + #[test] + fn dice_pool_number_with_quality() { + assert_eq!( + parse_dice_pool("8n"), + Ok(("", DicePool::new(8, 5, DicePoolQuality::NineAgain))) + ); + } + + #[test] + fn dice_pool_number_with_success_change() { + assert_eq!( + parse_dice_pool("8s3"), + Ok(("", DicePool::new(8, 3, DicePoolQuality::TenAgain))) + ); + } + + #[test] + fn dice_pool_with_quality_and_success_change() { + assert_eq!( + parse_dice_pool("8rs3"), + Ok(("", DicePool::new(8, 3, DicePoolQuality::Rote))) + ); + } +} diff --git a/src/commands.rs b/src/commands.rs index 557dff5..fd8a8f2 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,3 +1,4 @@ +use crate::cofd::DicePool; use crate::dice::ElementExpression; use crate::roll::Roll; @@ -18,13 +19,18 @@ impl Execution { } } -pub struct RollCommand(ElementExpression); - pub trait Command { fn execute(&self) -> Execution; + fn name(&self) -> &'static str; } +pub struct RollCommand(ElementExpression); + impl Command for RollCommand { + fn name(&self) -> &'static str { + "roll regular dice" + } + fn execute(&self) -> Execution { let roll = self.0.roll(); let plain = format!("Dice: {}\nResult: {}", self.0, roll); @@ -36,13 +42,55 @@ impl Command for RollCommand { } } +pub struct PoolRollCommand(DicePool); + +impl Command for PoolRollCommand { + fn name(&self) -> &'static str { + "roll dice pool" + } + + fn execute(&self) -> Execution { + let roll = self.0.roll(); + let plain = format!("Pool: {}\nResult: {}", self.0, roll); + let html = format!( + "

Pool: {}

Result: {}

", + self.0, roll + ); + 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(s: &str) -> Result>, String> { - // Ignore trailing input, if any. match parser::parse_command(s) { - Ok((_, result)) => Ok(result), + //The first clause prevents bot from spamming messages to itself + //after executing a previous command. + Ok((input, result)) => match (input, &result) { + ("", Some(_)) | (_, None) => Ok(result), + _ => Err(format!("{}: malformed dice expression", s)), + }, Err(err) => Err(err.to_string()), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn chance_die_is_not_malformed() { + assert!(parse_command("!chance").is_ok()); + } + + #[test] + fn roll_malformed_expression_test() { + assert!(parse_command("!roll 1d20asdlfkj").is_err()); + } + + #[test] + fn roll_dice_pool_expression_test() { + assert!(parse_command("!pool 8abc").is_err()); + } +} diff --git a/src/commands/parser.rs b/src/commands/parser.rs index 84cf416..cf709da 100644 --- a/src/commands/parser.rs +++ b/src/commands/parser.rs @@ -1,6 +1,7 @@ -use nom::{complete, named, tag, take_while, tuple, IResult}; +use nom::{alt, complete, named, tag, take_while, tuple, IResult}; -use crate::commands::{Command, RollCommand}; +use crate::cofd::parser::{create_chance_die, parse_dice_pool}; +use crate::commands::{Command, PoolRollCommand, RollCommand}; use crate::dice::parser::parse_element_expression; use crate::parser::eat_whitespace; @@ -11,18 +12,41 @@ fn parse_roll(input: &str) -> IResult<&str, Box> { Ok((input, Box::new(RollCommand(expression)))) } +fn parse_pool_roll(input: &str) -> IResult<&str, Box> { + let (input, _) = eat_whitespace(input)?; + let (input, pool) = parse_dice_pool(input)?; + Ok((input, Box::new(PoolRollCommand(pool)))) +} + +fn chance_die() -> IResult<&'static str, Box> { + let (input, pool) = create_chance_die()?; + Ok((input, Box::new(PoolRollCommand(pool)))) +} + /// 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>> { let (input, _) = eat_whitespace(original_input)?; - named!(command(&str) -> (&str, &str), tuple!(complete!(tag!("!")), complete!(take_while!(char::is_alphabetic)))); + + //Parser understands either specific !commands with no input, or any !command with extra input. + named!(command(&str) -> (&str, &str), tuple!( + complete!(tag!("!")), + alt!( + complete!(tag!("chance")) | //TODO figure out how to just have it read single commands. + complete!(take_while!(char::is_alphabetic)) + ) + )); + let (input, command) = match command(input) { // Strip the exclamation mark Ok((input, (_, result))) => (input, result), 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))), // No recognized command, ignore this. _ => Ok((original_input, None)), } diff --git a/src/lib.rs b/src/lib.rs index 6e7d7f9..ad467d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod bot; +pub mod cofd; pub mod commands; pub mod dice; pub mod matrix; diff --git a/src/roll.rs b/src/roll.rs index fc26293..3352270 100644 --- a/src/roll.rs +++ b/src/roll.rs @@ -14,7 +14,7 @@ pub trait Rolled { } #[derive(Debug, PartialEq, Eq, Clone)] -pub struct DiceRoll(Vec); +pub struct DiceRoll(pub Vec); impl DiceRoll { pub fn rolls(&self) -> &[u32] { @@ -53,9 +53,10 @@ impl Roll for dice::Dice { fn roll(&self) -> DiceRoll { let mut rng = rand::thread_rng(); - let rolls = (0..self.count) + let rolls: Vec<_> = (0..self.count) .map(|_| rng.gen_range(1, self.sides + 1)) .collect(); + DiceRoll(rolls) } }