diff --git a/src/dice.rs b/src/dice.rs index f17ec8f..c034882 100644 --- a/src/dice.rs +++ b/src/dice.rs @@ -33,6 +33,8 @@ pub struct DiceFormula { pub(crate) x: bool, } +const DICE_POOL_LIMIT: usize = 100_000; + impl Expression { pub fn evaluate(&self, mut rng: impl Rng) -> anyhow::Result { let mut result = String::new(); @@ -85,7 +87,7 @@ impl Expression { } else { 1 }; - if count > 1_000_000 { + if count > DICE_POOL_LIMIT { bail!("Too many dice.") }; output.push_str("d"); @@ -102,7 +104,7 @@ impl Expression { if size < 2 { bail!("Infinite explosion.") } - for i in 0..1_000_000 { + for i in 0..DICE_POOL_LIMIT { if i >= rolls.len() { break; } @@ -148,7 +150,7 @@ impl Expression { let print_all_rolls = rolls.len() <= 100; if print_all_rolls { - output.push_str(" [ "); + output.push_str(" ‹ "); } for idx in 0..rolls.len() { let mut skip = false; @@ -174,27 +176,25 @@ impl Expression { skip = true; } } - if skip { - if print_all_rolls { - write!(output, "~~`{}`~~", roll)?; - } - continue; + if !skip { + result_set.push(roll as f64); } - result_set.push(roll as f64); if print_all_rolls { - let bold = size > 2 && (roll == 1 || roll == size); - if bold { + if skip { + write!(output, "~~`{}`~~", roll)?; + } else if size > 2 && (roll == 1 || roll == size) { write!(output, "**`{}`**", roll)?; } else { write!(output, "`{}`", roll)?; } + if idx != rolls.len() - 1 { - output.push_str(", "); + output.push_str(" "); } } } if print_all_rolls { - output.push_str(" ]"); + output.push_str(" ›"); } result_set.iter().sum() @@ -253,7 +253,7 @@ impl Expression { } else { 1 }; - if count > 1_000_000 { + if count > DICE_POOL_LIMIT { bail!("Too many dice.") }; let size = self.node_sample(rng, *size)? as i64; @@ -268,7 +268,7 @@ impl Expression { if size < 2 { bail!("Infinite explosion.") } - for i in 0..1_000_000 { + for i in 0..DICE_POOL_LIMIT { if i >= rolls.len() { break; } diff --git a/src/main.rs b/src/main.rs index 61acf71..c442fdc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,8 @@ use anyhow::bail; use dotenv::dotenv; use rand::{Rng, SeedableRng}; use serenity::all::{ - Command, CommandInteraction, CommandOptionType, CreateCommandOption, CreateComponent, FullEvent, Interaction, MessageFlags, ResolvedOption, ResolvedValue, + Command, CommandInteraction, CommandOptionType, CreateCommandOption, CreateComponent, + FullEvent, Interaction, MessageFlags, ResolvedOption, ResolvedValue, }; use serenity::builder::{ CreateCommand, CreateContainer, CreateContainerComponent, CreateInteractionResponse, @@ -80,7 +81,7 @@ impl EventHandler for Handler { ).required(true)) .add_option(CreateCommandOption::new( CommandOptionType::Integer, - "multi", + "repeat", "Repeat the rolled formula this many times. Useful if you're controlling too many units!", )) .add_option(CreateCommandOption::new( @@ -92,6 +93,11 @@ impl EventHandler for Handler { CommandOptionType::Boolean, "private", "Keep the roll private and hidden, only for yourself.", + )) + .add_option(CreateCommandOption::new( + CommandOptionType::String, + "seed", + "Roll with a fixed seed. This bot uses xoshiro256++.", )), ) .await; @@ -114,12 +120,12 @@ pub async fn roll(ctx: &Context, command: &CommandInteraction) -> anyhow::Result else { bail!("Formula was not provided.") }; - let multi = options + let repeat = options .iter() - .find(|option| option.name == "multi") + .find(|option| option.name == "repeat") .and_then(|option| { - if let ResolvedValue::Integer(multi) = option.value { - Some(multi) + if let ResolvedValue::Integer(repeat) = option.value { + Some(repeat) } else { None } @@ -147,6 +153,17 @@ pub async fn roll(ctx: &Context, command: &CommandInteraction) -> anyhow::Result None } }); + let fixed_seed = options + .iter() + .find(|option| option.name == "seed") + .and_then(|option| { + if let ResolvedValue::String(fixed_seed) = option.value { + Some(fixed_seed) + } else { + None + } + }) + .and_then(|s| s.parse::().ok()); let result = parsing::parse(&formula); let expression = match result { @@ -171,24 +188,27 @@ pub async fn roll(ctx: &Context, command: &CommandInteraction) -> anyhow::Result command.defer(&ctx.http).await?; } - let seed = rand::rng().next_u64(); + let seed = fixed_seed.unwrap_or_else(|| rand::rng().next_u64()); let mut rng = rand_xoshiro::Xoshiro256PlusPlus::seed_from_u64(seed); let ev = (expression.average(&mut rng)? * 10.0).round() / 10.0; - let mut text_components = Vec::with_capacity((multi + 2) as usize); - text_components.push(CreateContainerComponent::TextDisplay( - CreateTextDisplay::new(if let Some(description) = description { - format!("### {} rolled: *{description}*\n", command.user.mention()) - } else { - format!("### {} rolled\n", command.user.mention()) - }), - )); - for _ in 0..multi { + let mut text_components = Vec::with_capacity((repeat + 2) as usize); + if let Some(description) = description { + text_components.push(CreateContainerComponent::TextDisplay( + CreateTextDisplay::new(format!(">>> {description}\n")), + )); + } + for _ in 0..repeat { text_components.push(CreateContainerComponent::TextDisplay( CreateTextDisplay::new(format!("{}\n", expression.evaluate(&mut rng)?)), )); } + if fixed_seed.is_some() { + text_components.push(CreateContainerComponent::TextDisplay( + CreateTextDisplay::new("***⚠️ This is not a fair roll. The result was generated based on a fixed seed and is thus fully reproducible.***".to_string()), + )); + } text_components.push(CreateContainerComponent::TextDisplay( CreateTextDisplay::new(format!("-# Expected value: {ev} | Seed: {seed}")), ));