forked from projectmoon/tenebrous-dicebot
Implement support for user variables in CoC dice rolling.
Also comes with reorganization of the dice rolling code to centralize the variable -> dice amount logic, and changes the way the results of those rolls are displayed.
This commit is contained in:
parent
b142b87d65
commit
39e6eb9b46
|
@ -1,22 +1,10 @@
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::db::variables::UserAndRoom;
|
use crate::error::{BotError, DiceRollingError};
|
||||||
use crate::error::BotError;
|
|
||||||
use crate::parser::{Amount, Element, Operator};
|
use crate::parser::{Amount, Element, Operator};
|
||||||
use crate::roll::Rolled;
|
use crate::roll::Rolled;
|
||||||
use futures::stream::{self, StreamExt, TryStreamExt};
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum DiceRollingError {
|
|
||||||
#[error("variable not found: {0}")]
|
|
||||||
VariableNotFound(String),
|
|
||||||
|
|
||||||
#[error("dice pool expression too large")]
|
|
||||||
ExpressionTooLarge,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum DicePoolQuality {
|
pub enum DicePoolQuality {
|
||||||
|
@ -94,29 +82,6 @@ pub struct DicePool {
|
||||||
pub(crate) modifiers: DicePoolModifiers,
|
pub(crate) modifiers: DicePoolModifiers,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn calculate_dice_amount(pool: &DicePoolWithContext<'_>) -> Result<i32, BotError> {
|
|
||||||
let stream = stream::iter(&pool.0.amounts);
|
|
||||||
let key = UserAndRoom(&pool.1.username, &pool.1.room_id);
|
|
||||||
let variables = &pool.1.db.variables.get_user_variables(&key)?;
|
|
||||||
|
|
||||||
use DiceRollingError::VariableNotFound;
|
|
||||||
let dice_amount: Result<i32, BotError> = stream
|
|
||||||
.then(|amount| async move {
|
|
||||||
match &amount.element {
|
|
||||||
Element::Number(num_dice) => Ok(*num_dice * amount.operator.mult()),
|
|
||||||
Element::Variable(variable) => variables
|
|
||||||
.get(variable)
|
|
||||||
.ok_or(VariableNotFound(variable.clone().to_string()))
|
|
||||||
.map(|i| *i)
|
|
||||||
.map_err(|e| e.into()),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.try_fold(0, |total, num_dice| async move { Ok(total + num_dice) })
|
|
||||||
.await;
|
|
||||||
|
|
||||||
dice_amount
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DicePool {
|
impl DicePool {
|
||||||
pub fn easy_pool(dice_amount: i32, quality: DicePoolQuality) -> DicePool {
|
pub fn easy_pool(dice_amount: i32, quality: DicePoolQuality) -> DicePool {
|
||||||
DicePool {
|
DicePool {
|
||||||
|
@ -346,7 +311,7 @@ pub async fn roll_pool(pool: &DicePoolWithContext<'_>) -> Result<RolledDicePool,
|
||||||
return Err(DiceRollingError::ExpressionTooLarge.into());
|
return Err(DiceRollingError::ExpressionTooLarge.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let num_dice = calculate_dice_amount(&pool).await?;
|
let num_dice = crate::dice::calculate_dice_amount(&pool.0.amounts, &pool.1).await?;
|
||||||
let mut roller = RngDieRoller(rand::thread_rng());
|
let mut roller = RngDieRoller(rand::thread_rng());
|
||||||
|
|
||||||
if num_dice > 0 {
|
if num_dice > 0 {
|
||||||
|
@ -563,9 +528,13 @@ mod tests {
|
||||||
}];
|
}];
|
||||||
|
|
||||||
let pool = DicePool::new(amounts, DicePoolModifiers::default());
|
let pool = DicePool::new(amounts, DicePoolModifiers::default());
|
||||||
let pool_with_ctx = DicePoolWithContext(&pool, &ctx);
|
|
||||||
|
|
||||||
assert_eq!(calculate_dice_amount(&pool_with_ctx).await.unwrap(), 10);
|
assert_eq!(
|
||||||
|
crate::dice::calculate_dice_amount(&pool.amounts, &ctx)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
10
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//DicePool tests
|
//DicePool tests
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::{Command, Execution};
|
use super::{Command, Execution};
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::cthulhu::dice::{AdvancementRoll, DiceRoll};
|
use crate::cthulhu::dice::{regular_roll, AdvancementRoll, DiceRoll, DiceRollWithContext};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
pub struct CthRoll(pub DiceRoll);
|
pub struct CthRoll(pub DiceRoll);
|
||||||
|
@ -11,14 +11,25 @@ impl Command for CthRoll {
|
||||||
"roll percentile pool"
|
"roll percentile pool"
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, _ctx: &Context<'_>) -> Execution {
|
async fn execute(&self, ctx: &Context<'_>) -> Execution {
|
||||||
//TODO this will be converted to a result when supporting variables.
|
let roll_with_ctx = DiceRollWithContext(&self.0, ctx);
|
||||||
let roll = self.0.roll();
|
let roll = regular_roll(&roll_with_ctx).await;
|
||||||
let plain = format!("Roll: {}\nResult: {}", self.0, roll);
|
|
||||||
|
let (plain, html) = match roll {
|
||||||
|
Ok(executed_roll) => {
|
||||||
|
let plain = format!("Roll: {}\nResult: {}", executed_roll, executed_roll.roll);
|
||||||
let html = format!(
|
let html = format!(
|
||||||
"<p><strong>Roll:</strong> {}</p><p><strong>Result</strong>: {}</p>",
|
"<p><strong>Roll:</strong> {}</p><p><strong>Result</strong>: {}</p>",
|
||||||
self.0, roll
|
executed_roll, executed_roll.roll
|
||||||
);
|
);
|
||||||
|
(plain, html)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let plain = format!("Error: {}", e);
|
||||||
|
let html = format!("<p><strong>Error:</strong> {}</p>", e);
|
||||||
|
(plain, html)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Execution { plain, html }
|
Execution { plain, html }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::error::{BotError, DiceRollingError};
|
||||||
|
use crate::parser::Amount;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
/// A planned dice roll.
|
/// A planned dice roll.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct DiceRoll {
|
pub struct DiceRoll {
|
||||||
pub target: u32,
|
pub amounts: Vec<Amount>,
|
||||||
pub modifier: DiceRollModifier,
|
pub modifier: DiceRollModifier,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for DiceRoll {
|
pub struct DiceRollWithContext<'a>(pub &'a DiceRoll, pub &'a Context<'a>);
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let message = format!("target: {}, with {}", self.target, self.modifier);
|
|
||||||
write!(f, "{}", message)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Potential modifier on the die roll to be made.
|
/// Potential modifier on the die roll to be made.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
@ -91,6 +89,26 @@ impl fmt::Display for RollResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ExecutedDiceRoll {
|
||||||
|
/// The number we must meet for the roll to be considered a
|
||||||
|
/// success.
|
||||||
|
pub target: u32,
|
||||||
|
|
||||||
|
/// Stored for informational purposes in display.
|
||||||
|
pub modifier: DiceRollModifier,
|
||||||
|
|
||||||
|
/// The actual roll result.
|
||||||
|
pub roll: RolledDice,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExecutedDiceRoll {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let message = format!("target: {}, with {}", self.target, self.modifier);
|
||||||
|
write!(f, "{}", message)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//TODO need to keep track of all rolled numbers for informational purposes!
|
//TODO need to keep track of all rolled numbers for informational purposes!
|
||||||
/// The outcome of a roll.
|
/// The outcome of a roll.
|
||||||
pub struct RolledDice {
|
pub struct RolledDice {
|
||||||
|
@ -100,10 +118,6 @@ pub struct RolledDice {
|
||||||
/// The number we must meet for the roll to be considered a
|
/// The number we must meet for the roll to be considered a
|
||||||
/// success.
|
/// success.
|
||||||
target: u32,
|
target: u32,
|
||||||
|
|
||||||
/// Stored for informational purposes in display.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
modifier: DiceRollModifier,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RolledDice {
|
impl RolledDice {
|
||||||
|
@ -235,9 +249,14 @@ fn roll_percentile_dice<R: DieRoller>(roller: &mut R, unit_roll: u32) -> u32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn roll_regular_dice<R: DieRoller>(roll: &DiceRoll, roller: &mut R) -> RolledDice {
|
fn roll_regular_dice<R: DieRoller>(
|
||||||
|
modifier: &DiceRollModifier,
|
||||||
|
target: u32,
|
||||||
|
roller: &mut R,
|
||||||
|
) -> RolledDice {
|
||||||
use DiceRollModifier::*;
|
use DiceRollModifier::*;
|
||||||
let num_rolls = match roll.modifier {
|
|
||||||
|
let num_rolls = match modifier {
|
||||||
Normal => 1,
|
Normal => 1,
|
||||||
OneBonus | OnePenalty => 2,
|
OneBonus | OnePenalty => 2,
|
||||||
TwoBonus | TwoPenalty => 3,
|
TwoBonus | TwoPenalty => 3,
|
||||||
|
@ -249,7 +268,7 @@ fn roll_regular_dice<R: DieRoller>(roll: &DiceRoll, roller: &mut R) -> RolledDic
|
||||||
.map(|_| roll_percentile_dice(roller, unit_roll))
|
.map(|_| roll_percentile_dice(roller, unit_roll))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let num_rolled = match roll.modifier {
|
let num_rolled = match modifier {
|
||||||
Normal => rolls.first(),
|
Normal => rolls.first(),
|
||||||
OneBonus | TwoBonus => rolls.iter().min(),
|
OneBonus | TwoBonus => rolls.iter().min(),
|
||||||
OnePenalty | TwoPenalty => rolls.iter().max(),
|
OnePenalty | TwoPenalty => rolls.iter().max(),
|
||||||
|
@ -257,9 +276,8 @@ fn roll_regular_dice<R: DieRoller>(roll: &DiceRoll, roller: &mut R) -> RolledDic
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
RolledDice {
|
RolledDice {
|
||||||
modifier: roll.modifier,
|
|
||||||
num_rolled: *num_rolled,
|
num_rolled: *num_rolled,
|
||||||
target: roll.target,
|
target: target,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,7 +305,6 @@ fn roll_advancement_dice<R: DieRoller>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiceRoll {
|
|
||||||
/// Make a roll with a target number and potential modifier. In a
|
/// Make a roll with a target number and potential modifier. In a
|
||||||
/// normal roll, only one percentile die is rolled (1d100). With
|
/// normal roll, only one percentile die is rolled (1d100). With
|
||||||
/// bonuses or penalties, more dice are rolled, and either the lowest
|
/// bonuses or penalties, more dice are rolled, and either the lowest
|
||||||
|
@ -296,10 +313,21 @@ impl DiceRoll {
|
||||||
/// rolled separately from the tens place, and then the unit number is
|
/// rolled separately from the tens place, and then the unit number is
|
||||||
/// added to each potential roll before picking the lowest/highest
|
/// added to each potential roll before picking the lowest/highest
|
||||||
/// result.
|
/// result.
|
||||||
pub fn roll(&self) -> RolledDice {
|
pub async fn regular_roll(
|
||||||
|
roll_with_ctx: &DiceRollWithContext<'_>,
|
||||||
|
) -> Result<ExecutedDiceRoll, BotError> {
|
||||||
|
let target =
|
||||||
|
crate::dice::calculate_dice_amount(&roll_with_ctx.0.amounts, roll_with_ctx.1).await?;
|
||||||
|
|
||||||
|
let target = u32::try_from(target).map_err(|_| DiceRollingError::InvalidAmount)?;
|
||||||
let mut roller = RngDieRoller(rand::thread_rng());
|
let mut roller = RngDieRoller(rand::thread_rng());
|
||||||
roll_regular_dice(&self, &mut roller)
|
let rolled_dice = roll_regular_dice(&roll_with_ctx.0.modifier, target, &mut roller);
|
||||||
}
|
|
||||||
|
Ok(ExecutedDiceRoll {
|
||||||
|
target: target,
|
||||||
|
modifier: roll_with_ctx.0.modifier,
|
||||||
|
roll: rolled_dice,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AdvancementRoll {
|
impl AdvancementRoll {
|
||||||
|
@ -312,6 +340,8 @@ impl AdvancementRoll {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::db::Database;
|
||||||
|
use crate::parser::{Amount, Element, Operator};
|
||||||
|
|
||||||
/// Generate a series of numbers manually for testing. For this
|
/// Generate a series of numbers manually for testing. For this
|
||||||
/// die system, the first roll in the Vec should be the unit roll,
|
/// die system, the first roll in the Vec should be the unit roll,
|
||||||
|
@ -339,195 +369,141 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn regular_roll_succeeds_when_below_target() {
|
async fn regular_roll_converts_u32_safely() {
|
||||||
let roll = DiceRoll {
|
let roll = DiceRoll {
|
||||||
target: 50,
|
amounts: vec![Amount {
|
||||||
|
operator: Operator::Plus,
|
||||||
|
element: Element::Number(-10),
|
||||||
|
}],
|
||||||
modifier: DiceRollModifier::Normal,
|
modifier: DiceRollModifier::Normal,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let db = Database::new_temp().unwrap();
|
||||||
|
let ctx = Context::new(&db, "roomid", "username", "message");
|
||||||
|
let roll_with_ctx = DiceRollWithContext(&roll, &ctx);
|
||||||
|
let result = regular_roll(&roll_with_ctx).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Err(BotError::DiceRollingError(DiceRollingError::InvalidAmount))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn regular_roll_succeeds_when_below_target() {
|
||||||
//Roll 30, succeeding.
|
//Roll 30, succeeding.
|
||||||
let mut roller = SequentialDieRoller::new(vec![0, 3]);
|
let mut roller = SequentialDieRoller::new(vec![0, 3]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller);
|
||||||
assert_eq!(RollResult::Success, rolled.result());
|
assert_eq!(RollResult::Success, rolled.result());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_hard_success_when_rolling_half() {
|
fn regular_roll_hard_success_when_rolling_half() {
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 50,
|
|
||||||
modifier: DiceRollModifier::Normal,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Roll 25, succeeding.
|
//Roll 25, succeeding.
|
||||||
let mut roller = SequentialDieRoller::new(vec![5, 2]);
|
let mut roller = SequentialDieRoller::new(vec![5, 2]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller);
|
||||||
assert_eq!(RollResult::HardSuccess, rolled.result());
|
assert_eq!(RollResult::HardSuccess, rolled.result());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_extreme_success_when_rolling_one_fifth() {
|
fn regular_roll_extreme_success_when_rolling_one_fifth() {
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 50,
|
|
||||||
modifier: DiceRollModifier::Normal,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Roll 10, succeeding extremely.
|
//Roll 10, succeeding extremely.
|
||||||
let mut roller = SequentialDieRoller::new(vec![0, 1]);
|
let mut roller = SequentialDieRoller::new(vec![0, 1]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller);
|
||||||
assert_eq!(RollResult::ExtremeSuccess, rolled.result());
|
assert_eq!(RollResult::ExtremeSuccess, rolled.result());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_extreme_success_target_above_100() {
|
fn regular_roll_extreme_success_target_above_100() {
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 150,
|
|
||||||
modifier: DiceRollModifier::Normal,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Roll 30, succeeding extremely.
|
//Roll 30, succeeding extremely.
|
||||||
let mut roller = SequentialDieRoller::new(vec![0, 3]);
|
let mut roller = SequentialDieRoller::new(vec![0, 3]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::Normal, 150, &mut roller);
|
||||||
assert_eq!(RollResult::ExtremeSuccess, rolled.result());
|
assert_eq!(RollResult::ExtremeSuccess, rolled.result());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_critical_success_on_one() {
|
fn regular_roll_critical_success_on_one() {
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 50,
|
|
||||||
modifier: DiceRollModifier::Normal,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Roll 1.
|
//Roll 1.
|
||||||
let mut roller = SequentialDieRoller::new(vec![1, 0]);
|
let mut roller = SequentialDieRoller::new(vec![1, 0]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller);
|
||||||
assert_eq!(RollResult::CriticalSuccess, rolled.result());
|
assert_eq!(RollResult::CriticalSuccess, rolled.result());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_fail_when_above_target() {
|
fn regular_roll_fail_when_above_target() {
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 50,
|
|
||||||
modifier: DiceRollModifier::Normal,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Roll 60.
|
//Roll 60.
|
||||||
let mut roller = SequentialDieRoller::new(vec![0, 6]);
|
let mut roller = SequentialDieRoller::new(vec![0, 6]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller);
|
||||||
assert_eq!(RollResult::Failure, rolled.result());
|
assert_eq!(RollResult::Failure, rolled.result());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_is_fumble_when_skill_below_50_and_roll_at_least_96() {
|
fn regular_roll_is_fumble_when_skill_below_50_and_roll_at_least_96() {
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 49,
|
|
||||||
modifier: DiceRollModifier::Normal,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Roll 96.
|
//Roll 96.
|
||||||
let mut roller = SequentialDieRoller::new(vec![6, 9]);
|
let mut roller = SequentialDieRoller::new(vec![6, 9]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::Normal, 49, &mut roller);
|
||||||
assert_eq!(RollResult::Fumble, rolled.result());
|
assert_eq!(RollResult::Fumble, rolled.result());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_is_failure_when_skill_at_or_above_50_and_roll_at_least_96() {
|
fn regular_roll_is_failure_when_skill_at_or_above_50_and_roll_at_least_96() {
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 50,
|
|
||||||
modifier: DiceRollModifier::Normal,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Roll 96.
|
//Roll 96.
|
||||||
let mut roller = SequentialDieRoller::new(vec![6, 9]);
|
let mut roller = SequentialDieRoller::new(vec![6, 9]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller);
|
||||||
assert_eq!(RollResult::Failure, rolled.result());
|
assert_eq!(RollResult::Failure, rolled.result());
|
||||||
|
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 68,
|
|
||||||
modifier: DiceRollModifier::Normal,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Roll 96.
|
//Roll 96.
|
||||||
let mut roller = SequentialDieRoller::new(vec![6, 9]);
|
let mut roller = SequentialDieRoller::new(vec![6, 9]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::Normal, 68, &mut roller);
|
||||||
assert_eq!(RollResult::Failure, rolled.result());
|
assert_eq!(RollResult::Failure, rolled.result());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_always_fumble_on_100() {
|
fn regular_roll_always_fumble_on_100() {
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 100,
|
|
||||||
modifier: DiceRollModifier::Normal,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Roll 100.
|
//Roll 100.
|
||||||
let mut roller = SequentialDieRoller::new(vec![0, 0]);
|
let mut roller = SequentialDieRoller::new(vec![0, 0]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::Normal, 100, &mut roller);
|
||||||
assert_eq!(RollResult::Fumble, rolled.result());
|
assert_eq!(RollResult::Fumble, rolled.result());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn one_penalty_picks_highest_of_two() {
|
fn one_penalty_picks_highest_of_two() {
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 50,
|
|
||||||
modifier: DiceRollModifier::OnePenalty,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Should only roll 30 and 40, not 50.
|
//Should only roll 30 and 40, not 50.
|
||||||
let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 5]);
|
let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 5]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::OnePenalty, 50, &mut roller);
|
||||||
assert_eq!(40, rolled.num_rolled);
|
assert_eq!(40, rolled.num_rolled);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn two_penalty_picks_highest_of_three() {
|
fn two_penalty_picks_highest_of_three() {
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 50,
|
|
||||||
modifier: DiceRollModifier::TwoPenalty,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Should only roll 30, 40, 50, and not 60.
|
//Should only roll 30, 40, 50, and not 60.
|
||||||
let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 5, 6]);
|
let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 5, 6]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::TwoPenalty, 50, &mut roller);
|
||||||
assert_eq!(50, rolled.num_rolled);
|
assert_eq!(50, rolled.num_rolled);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn one_bonus_picks_lowest_of_two() {
|
fn one_bonus_picks_lowest_of_two() {
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 50,
|
|
||||||
modifier: DiceRollModifier::OneBonus,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Should only roll 30 and 40, not 20.
|
//Should only roll 30 and 40, not 20.
|
||||||
let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 2]);
|
let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 2]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::OneBonus, 50, &mut roller);
|
||||||
assert_eq!(30, rolled.num_rolled);
|
assert_eq!(30, rolled.num_rolled);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn two_bonus_picks_lowest_of_three() {
|
fn two_bonus_picks_lowest_of_three() {
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 50,
|
|
||||||
modifier: DiceRollModifier::TwoBonus,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Should only roll 30, 40, 50, and not 20.
|
//Should only roll 30, 40, 50, and not 20.
|
||||||
let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 5, 2]);
|
let mut roller = SequentialDieRoller::new(vec![0, 3, 4, 5, 2]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::TwoBonus, 50, &mut roller);
|
||||||
assert_eq!(30, rolled.num_rolled);
|
assert_eq!(30, rolled.num_rolled);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normal_modifier_rolls_once() {
|
fn normal_modifier_rolls_once() {
|
||||||
let roll = DiceRoll {
|
|
||||||
target: 50,
|
|
||||||
modifier: DiceRollModifier::Normal,
|
|
||||||
};
|
|
||||||
|
|
||||||
//Should only roll 30, not 40.
|
//Should only roll 30, not 40.
|
||||||
let mut roller = SequentialDieRoller::new(vec![0, 3, 4]);
|
let mut roller = SequentialDieRoller::new(vec![0, 3, 4]);
|
||||||
let rolled = roll_regular_dice(&roll, &mut roller);
|
let rolled = roll_regular_dice(&DiceRollModifier::Normal, 50, &mut roller);
|
||||||
assert_eq!(30, rolled.num_rolled);
|
assert_eq!(30, rolled.num_rolled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,33 +3,40 @@ use crate::parser::DiceParsingError;
|
||||||
|
|
||||||
//TOOD convert these to use parse_amounts from the common dice code.
|
//TOOD convert these to use parse_amounts from the common dice code.
|
||||||
|
|
||||||
fn parse_modifier(input: &str) -> Result<(DiceRollModifier, &str), DiceParsingError> {
|
fn parse_modifier(input: &str) -> Result<DiceRollModifier, DiceParsingError> {
|
||||||
if input.ends_with("bb") {
|
if input.ends_with("bb") {
|
||||||
Ok((DiceRollModifier::TwoBonus, input.trim_end_matches("bb")))
|
Ok(DiceRollModifier::TwoBonus)
|
||||||
} else if input.ends_with("b") {
|
} else if input.ends_with("b") {
|
||||||
Ok((DiceRollModifier::OneBonus, input.trim_end_matches("b")))
|
Ok(DiceRollModifier::OneBonus)
|
||||||
} else if input.ends_with("pp") {
|
} else if input.ends_with("pp") {
|
||||||
Ok((DiceRollModifier::TwoPenalty, input.trim_end_matches("pp")))
|
Ok(DiceRollModifier::TwoPenalty)
|
||||||
} else if input.ends_with("p") {
|
} else if input.ends_with("p") {
|
||||||
Ok((DiceRollModifier::OnePenalty, input.trim_end_matches("p")))
|
Ok(DiceRollModifier::OnePenalty)
|
||||||
} else {
|
} else {
|
||||||
Ok((DiceRollModifier::Normal, input))
|
Ok(DiceRollModifier::Normal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Make diceroll take a vec of Amounts
|
||||||
|
//Split based on :, send first part to parse_modifier.
|
||||||
|
//Send second part to parse_amounts
|
||||||
|
|
||||||
pub fn parse_regular_roll(input: &str) -> Result<DiceRoll, DiceParsingError> {
|
pub fn parse_regular_roll(input: &str) -> Result<DiceRoll, DiceParsingError> {
|
||||||
let input = input.trim();
|
let input: Vec<&str> = input.trim().split(":").collect();
|
||||||
let (modifier, input) = parse_modifier(input)?;
|
|
||||||
let target: u32 = input.parse().map_err(|_| DiceParsingError::InvalidAmount)?;
|
let (modifiers_str, amounts_str) = match input[..] {
|
||||||
|
[amounts] => Ok(("", amounts)),
|
||||||
|
[modifiers, amounts] => Ok((modifiers, amounts)),
|
||||||
|
_ => Err(DiceParsingError::UnconsumedInput),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let modifier = parse_modifier(modifiers_str)?;
|
||||||
|
let amounts = crate::parser::parse_amounts(amounts_str)?;
|
||||||
|
|
||||||
if target <= 100 {
|
|
||||||
Ok(DiceRoll {
|
Ok(DiceRoll {
|
||||||
target: target,
|
amounts: amounts,
|
||||||
modifier: modifier,
|
modifier: modifier,
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
Err(DiceParsingError::InvalidAmount)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_advancement_roll(input: &str) -> Result<AdvancementRoll, DiceParsingError> {
|
pub fn parse_advancement_roll(input: &str) -> Result<AdvancementRoll, DiceParsingError> {
|
||||||
|
@ -49,6 +56,7 @@ pub fn parse_advancement_roll(input: &str) -> Result<AdvancementRoll, DiceParsin
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::parser::{Amount, Element, Operator};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_accepts_single_number() {
|
fn regular_roll_accepts_single_number() {
|
||||||
|
@ -56,7 +64,10 @@ mod tests {
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DiceRoll {
|
DiceRoll {
|
||||||
target: 60,
|
amounts: vec![Amount {
|
||||||
|
operator: Operator::Plus,
|
||||||
|
element: Element::Number(60)
|
||||||
|
}],
|
||||||
modifier: DiceRollModifier::Normal
|
modifier: DiceRollModifier::Normal
|
||||||
},
|
},
|
||||||
result.unwrap()
|
result.unwrap()
|
||||||
|
@ -65,11 +76,14 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_accepts_two_bonus() {
|
fn regular_roll_accepts_two_bonus() {
|
||||||
let result = parse_regular_roll("60bb");
|
let result = parse_regular_roll("bb:60");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DiceRoll {
|
DiceRoll {
|
||||||
target: 60,
|
amounts: vec![Amount {
|
||||||
|
operator: Operator::Plus,
|
||||||
|
element: Element::Number(60)
|
||||||
|
}],
|
||||||
modifier: DiceRollModifier::TwoBonus
|
modifier: DiceRollModifier::TwoBonus
|
||||||
},
|
},
|
||||||
result.unwrap()
|
result.unwrap()
|
||||||
|
@ -78,11 +92,14 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_accepts_one_bonus() {
|
fn regular_roll_accepts_one_bonus() {
|
||||||
let result = parse_regular_roll("60b");
|
let result = parse_regular_roll("b:60");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DiceRoll {
|
DiceRoll {
|
||||||
target: 60,
|
amounts: vec![Amount {
|
||||||
|
operator: Operator::Plus,
|
||||||
|
element: Element::Number(60)
|
||||||
|
}],
|
||||||
modifier: DiceRollModifier::OneBonus
|
modifier: DiceRollModifier::OneBonus
|
||||||
},
|
},
|
||||||
result.unwrap()
|
result.unwrap()
|
||||||
|
@ -91,11 +108,14 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_accepts_two_penalty() {
|
fn regular_roll_accepts_two_penalty() {
|
||||||
let result = parse_regular_roll("60pp");
|
let result = parse_regular_roll("pp:60");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DiceRoll {
|
DiceRoll {
|
||||||
target: 60,
|
amounts: vec![Amount {
|
||||||
|
operator: Operator::Plus,
|
||||||
|
element: Element::Number(60)
|
||||||
|
}],
|
||||||
modifier: DiceRollModifier::TwoPenalty
|
modifier: DiceRollModifier::TwoPenalty
|
||||||
},
|
},
|
||||||
result.unwrap()
|
result.unwrap()
|
||||||
|
@ -104,11 +124,14 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_accepts_one_penalty() {
|
fn regular_roll_accepts_one_penalty() {
|
||||||
let result = parse_regular_roll("60p");
|
let result = parse_regular_roll("p:60");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DiceRoll {
|
DiceRoll {
|
||||||
target: 60,
|
amounts: vec![Amount {
|
||||||
|
operator: Operator::Plus,
|
||||||
|
element: Element::Number(60)
|
||||||
|
}],
|
||||||
modifier: DiceRollModifier::OnePenalty
|
modifier: DiceRollModifier::OnePenalty
|
||||||
},
|
},
|
||||||
result.unwrap()
|
result.unwrap()
|
||||||
|
@ -121,21 +144,21 @@ mod tests {
|
||||||
assert!(parse_regular_roll(" 60").is_ok());
|
assert!(parse_regular_roll(" 60").is_ok());
|
||||||
assert!(parse_regular_roll(" 60 ").is_ok());
|
assert!(parse_regular_roll(" 60 ").is_ok());
|
||||||
|
|
||||||
assert!(parse_regular_roll("60bb ").is_ok());
|
assert!(parse_regular_roll("bb:60 ").is_ok());
|
||||||
assert!(parse_regular_roll(" 60bb").is_ok());
|
assert!(parse_regular_roll(" bb:60").is_ok());
|
||||||
assert!(parse_regular_roll(" 60bb ").is_ok());
|
assert!(parse_regular_roll(" bb:60 ").is_ok());
|
||||||
|
|
||||||
assert!(parse_regular_roll("60b ").is_ok());
|
assert!(parse_regular_roll("b:60 ").is_ok());
|
||||||
assert!(parse_regular_roll(" 60b").is_ok());
|
assert!(parse_regular_roll(" b:60").is_ok());
|
||||||
assert!(parse_regular_roll(" 60b ").is_ok());
|
assert!(parse_regular_roll(" b:60 ").is_ok());
|
||||||
|
|
||||||
assert!(parse_regular_roll("60pp ").is_ok());
|
assert!(parse_regular_roll("pp:60 ").is_ok());
|
||||||
assert!(parse_regular_roll(" 60pp").is_ok());
|
assert!(parse_regular_roll(" pp:60").is_ok());
|
||||||
assert!(parse_regular_roll(" 60pp ").is_ok());
|
assert!(parse_regular_roll(" pp:60 ").is_ok());
|
||||||
|
|
||||||
assert!(parse_regular_roll("60p ").is_ok());
|
assert!(parse_regular_roll("p:60 ").is_ok());
|
||||||
assert!(parse_regular_roll(" 60p").is_ok());
|
assert!(parse_regular_roll(" p:60").is_ok());
|
||||||
assert!(parse_regular_roll(" 60p ").is_ok());
|
assert!(parse_regular_roll(" p:60 ").is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -152,21 +175,11 @@ mod tests {
|
||||||
assert_eq!(AdvancementRoll { existing_skill: 60 }, result.unwrap());
|
assert_eq!(AdvancementRoll { existing_skill: 60 }, result.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn regular_roll_rejects_big_numbers() {
|
|
||||||
assert!(parse_regular_roll("3000").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn advancement_roll_rejects_big_numbers() {
|
fn advancement_roll_rejects_big_numbers() {
|
||||||
assert!(parse_advancement_roll("3000").is_err());
|
assert!(parse_advancement_roll("3000").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn regular_roll_rejects_invalid_input() {
|
|
||||||
assert!(parse_regular_roll("abc").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn advancement_roll_rejects_invalid_input() {
|
fn advancement_roll_rejects_invalid_input() {
|
||||||
assert!(parse_advancement_roll("abc").is_err());
|
assert!(parse_advancement_roll("abc").is_err());
|
||||||
|
|
33
src/dice.rs
33
src/dice.rs
|
@ -1,8 +1,41 @@
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
|
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::db::variables::UserAndRoom;
|
||||||
|
use crate::error::BotError;
|
||||||
|
use crate::error::DiceRollingError;
|
||||||
|
use crate::parser::Amount;
|
||||||
|
use crate::parser::Element as NewElement;
|
||||||
|
use futures::stream::{self, StreamExt, TryStreamExt};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
//New hotness
|
||||||
|
pub async fn calculate_dice_amount(amounts: &[Amount], ctx: &Context<'_>) -> Result<i32, BotError> {
|
||||||
|
let stream = stream::iter(amounts);
|
||||||
|
let key = UserAndRoom(&ctx.username, &ctx.room_id);
|
||||||
|
let variables = &ctx.db.variables.get_user_variables(&key)?;
|
||||||
|
|
||||||
|
use DiceRollingError::VariableNotFound;
|
||||||
|
let dice_amount: Result<i32, BotError> = stream
|
||||||
|
.then(|amount| async move {
|
||||||
|
match &amount.element {
|
||||||
|
NewElement::Number(num_dice) => Ok(*num_dice * amount.operator.mult()),
|
||||||
|
NewElement::Variable(variable) => variables
|
||||||
|
.get(variable)
|
||||||
|
.ok_or(VariableNotFound(variable.clone().to_string()))
|
||||||
|
.map(|i| *i)
|
||||||
|
.map_err(|e| e.into()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.try_fold(0, |total, num_dice| async move { Ok(total + num_dice) })
|
||||||
|
.await;
|
||||||
|
|
||||||
|
dice_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
//Old stuff, for regular dice rolling. To be moved elsewhere.
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub struct Dice {
|
pub struct Dice {
|
||||||
pub(crate) count: u32,
|
pub(crate) count: u32,
|
||||||
|
|
15
src/error.rs
15
src/error.rs
|
@ -1,4 +1,3 @@
|
||||||
use crate::cofd::dice::DiceRollingError;
|
|
||||||
use crate::commands::CommandError;
|
use crate::commands::CommandError;
|
||||||
use crate::config::ConfigError;
|
use crate::config::ConfigError;
|
||||||
use crate::db::errors::DataError;
|
use crate::db::errors::DataError;
|
||||||
|
@ -50,7 +49,7 @@ pub enum BotError {
|
||||||
#[error("command parsing error: {0}")]
|
#[error("command parsing error: {0}")]
|
||||||
CommandParsingError(#[from] crate::commands::parser::CommandParsingError),
|
CommandParsingError(#[from] crate::commands::parser::CommandParsingError),
|
||||||
|
|
||||||
#[error("dice pool roll error: {0}")]
|
#[error("dice rolling error: {0}")]
|
||||||
DiceRollingError(#[from] DiceRollingError),
|
DiceRollingError(#[from] DiceRollingError),
|
||||||
|
|
||||||
#[error("variable parsing error: {0}")]
|
#[error("variable parsing error: {0}")]
|
||||||
|
@ -68,3 +67,15 @@ pub enum BotError {
|
||||||
#[error("database error")]
|
#[error("database error")]
|
||||||
DatabaseErrror(#[from] sled::Error),
|
DatabaseErrror(#[from] sled::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum DiceRollingError {
|
||||||
|
#[error("variable not found: {0}")]
|
||||||
|
VariableNotFound(String),
|
||||||
|
|
||||||
|
#[error("invalid amount")]
|
||||||
|
InvalidAmount,
|
||||||
|
|
||||||
|
#[error("dice pool expression too large")]
|
||||||
|
ExpressionTooLarge,
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue