Compare commits

..

No commits in common. "0eec30fe82a84bd066c90829b7d8536810e3268a" and "9dd6bee0e350a89be3eec67caefc1bb6ff089685" have entirely different histories.

3 changed files with 180 additions and 246 deletions

View file

@ -1,10 +1,8 @@
use crate::limits::{MAX_DICE_COUNT_PER_ROLL, MAX_DISPLAYED_DICE_PER_MESSAGE, RESULT_CULL_CHARACTER_THRESHOLD};
use anyhow::bail;
use rand::{Rng, RngExt};
use std::convert::Infallible;
use std::error::Error;
use std::fmt::{Write, format};
use std::sync::{Arc, RwLock};
use thiserror::Error;
pub type ExpBox = Box<Expression>;
@ -42,7 +40,16 @@ pub enum CompareFragment {
Le(ExpBox),
}
const DICE_POOL_LIMIT: usize = 10_000;
impl Expression {
pub fn collect_evaluation(&self, mut rng: impl Rng) -> anyhow::Result<String> {
let mut witness = DiscordMdWitness::default();
let total = self.evaluate(&mut rng, &mut witness, 100)?;
let result_text = witness.buffer;
Ok(format!("**{total}** = {result_text}"))
}
pub fn average(&self, rng: &mut impl Rng) -> anyhow::Result<f64> {
if let Ok(avg) = self.avg() {
return Ok(avg);
@ -54,7 +61,7 @@ impl Expression {
Ok(average)
}
pub(crate) fn evaluate<W, E>(
fn evaluate<W, E>(
&self,
rng: &mut impl Rng,
w: &mut W,
@ -64,7 +71,6 @@ impl Expression {
W: Witness<Error = E>,
E: Error + Send + Sync + 'static,
{
use crate::limits::MAX_DICE_COUNT_PER_ROLL;
use Expression::*;
let precedence = self.precedence();
let needs_parens = precedence > outer_precedence;
@ -90,7 +96,7 @@ impl Expression {
} else {
1
};
if count > MAX_DICE_COUNT_PER_ROLL {
if count > DICE_POOL_LIMIT {
bail!("Too many dice.")
};
write!(w, "d")?;
@ -122,7 +128,7 @@ impl Expression {
}
let mut i = 0;
while i < rolls.len() {
if i > MAX_DICE_COUNT_PER_ROLL {
if i > DICE_POOL_LIMIT {
bail!("Explosion added too many dice.")
}
if comparers.len() == 0 {
@ -292,7 +298,7 @@ impl Expression {
} else {
1
};
if count > MAX_DICE_COUNT_PER_ROLL {
if count > DICE_POOL_LIMIT {
bail!("Too many dice.")
};
let size = size.avg()? as i64;
@ -384,7 +390,7 @@ impl Expression {
}
}
pub(crate) trait Witness {
trait Witness {
type Ok;
type Error: Error;
type WitnessSet<'a>: WitnessSet<Ok = Self::Ok, Error = Self::Error>
@ -399,10 +405,9 @@ pub(crate) trait Witness {
{
self.witness_source_text(&format(args))
}
fn witness_total_result(&mut self, result: f64) -> Result<Self::Ok, Self::Error>;
}
pub(crate) trait WitnessSet {
trait WitnessSet {
type Ok;
type Error: Error;
@ -411,7 +416,7 @@ pub(crate) trait WitnessSet {
}
#[derive(Debug)]
pub(crate) struct DiceRoll {
struct DiceRoll {
size: usize,
roll: usize,
is_admitted: bool,
@ -431,56 +436,27 @@ impl Default for DiceRoll {
}
}
#[derive(Default)]
pub(crate) struct DiscordMd {
pub(crate) buffers: Vec<String>,
pub(crate) dice_written: usize,
struct DiscordMdWitness {
buffer: String,
dice_written: usize,
}
pub(crate) struct DiscordMdWitness {
parent: Arc<RwLock<DiscordMd>>,
buffer_idx: usize,
result: Option<f64>,
}
pub(crate) struct DiscordMdWitnessSet<'a> {
witness: &'a mut DiscordMdWitness,
struct DiscordMdWitnessSet<'a> {
parent: &'a mut DiscordMdWitness,
index: usize,
dice_elided: bool,
}
#[derive(Error, Debug)]
pub(crate) enum DiscordMdWitnessError {
#[error("Formatting failed.")]
enum DiscordMdWitnessError {
#[error("formatting failed")]
Format(#[from] std::fmt::Error),
}
impl DiscordMd {
pub(crate) fn create_witness(md: &Arc<RwLock<Self>>) -> DiscordMdWitness {
let mut parent = md.write().unwrap();
parent.buffers.push(String::new());
DiscordMdWitness {
parent: md.clone(),
buffer_idx: parent.buffers.len() - 1,
result: None,
}
}
pub(crate) fn character_count(&self) -> usize {
self.buffers.iter().map(|buf| buf.chars().count()).sum()
}
}
impl DiscordMdWitness {
pub(crate) fn end(self) {
let mut parent = self.parent.write().unwrap();
if parent.character_count() > RESULT_CULL_CHARACTER_THRESHOLD {
parent.buffers[self.buffer_idx] = self
.result
.map(|result| format!("**{result} = …**"))
.unwrap_or_else(|| String::new());
} else if let Some(result) = self.result {
parent.buffers[self.buffer_idx]
.insert_str(0, &format!("**{result}** = "));
impl Default for DiscordMdWitness {
fn default() -> Self {
Self {
buffer: String::new(),
dice_written: 0,
}
}
}
@ -491,87 +467,65 @@ impl Witness for DiscordMdWitness {
type WitnessSet<'a> = DiscordMdWitnessSet<'a>;
fn witness_source_text(&mut self, text: &str) -> Result<Self::Ok, Self::Error> {
self.parent.write().unwrap().buffers[self.buffer_idx].push_str(text);
self.buffer.push_str(text);
Ok(())
}
fn witness_set(&mut self) -> Result<Self::WitnessSet<'_>, Self::Error> {
write!(self.parent.write().unwrap().buffers[self.buffer_idx], "‹ ")?;
write!(self.buffer, "‹ ")?;
Ok(DiscordMdWitnessSet {
witness: self,
parent: self,
index: 0,
dice_elided: false,
})
}
fn witness_total_result(&mut self, result: f64) -> Result<Self::Ok, Self::Error> {
self.result = Some(result);
Ok(())
}
}
impl<'a> WitnessSet for DiscordMdWitnessSet<'a> {
type Ok = ();
type Error = DiscordMdWitnessError;
fn witness_roll(&mut self, dice: DiceRoll) -> Result<Self::Ok, Self::Error> {
let mut parent = self.witness.parent.write().unwrap();
if parent.dice_written >= MAX_DISPLAYED_DICE_PER_MESSAGE {
if self.parent.dice_written >= 50 {
if !self.dice_elided {
write!(
parent.buffers[self.witness.buffer_idx],
"…"
)?;
write!(self.parent.buffer, "…")?;
self.dice_elided = true;
}
return Ok(());
}
let buf = &mut parent.buffers[self.witness.buffer_idx];
let is_extreme = dice.size >= 4 && (dice.roll == 1 || dice.roll == dice.size);
// A visible separator is required despite the monospace boxing,
// due to iOS Discord being weird.
if self.index > 0 {
write!(buf, ",")?;
write!(self.parent.buffer, " ")?;
}
if !dice.is_admitted {
write!(buf, "~~")?;
write!(self.parent.buffer, "~~")?;
}
if dice.is_from_proliferation {
write!(buf, "*")?;
write!(self.parent.buffer, "*")?;
}
if is_extreme {
write!(buf, "**")?;
}
if dice.did_proliferate {
write!(buf, "__")?;
if is_extreme || dice.did_proliferate {
write!(self.parent.buffer, "**")?;
}
write!(buf, "`{}`", dice.roll)?;
write!(self.parent.buffer, "`{}`", dice.roll)?;
if dice.did_proliferate {
write!(buf, "__")?;
}
if is_extreme {
write!(buf, "**")?;
if is_extreme || dice.did_proliferate {
write!(self.parent.buffer, "**")?;
}
if dice.is_from_proliferation {
write!(buf, "*")?;
write!(self.parent.buffer, "*")?;
}
if !dice.is_admitted {
write!(buf, "~~")?;
write!(self.parent.buffer, "~~")?;
}
self.index += 1;
parent.dice_written += 1;
self.parent.dice_written += 1;
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
write!(
self.witness.parent.write().unwrap().buffers[self.witness.buffer_idx],
" ›"
)?;
write!(self.parent.buffer, " ›")?;
Ok(())
}
}
@ -591,10 +545,6 @@ impl Witness for std::io::Sink {
fn witness_set(&mut self) -> Result<Self::WitnessSet<'_>, Self::Error> {
Ok(self)
}
fn witness_total_result(&mut self, _result: f64) -> Result<Self::Ok, Self::Error> {
Ok(())
}
}
impl WitnessSet for &mut std::io::Sink {

View file

@ -1,4 +0,0 @@
pub const MAX_DICE_COUNT_PER_ROLL: usize = 20_000;
pub const RESULT_CULL_CHARACTER_THRESHOLD: usize = 3000;
pub const MAX_DISPLAYED_DICE_PER_MESSAGE: usize = 100;
pub const MAX_ROLL_REPEATS: usize = 35;

View file

@ -1,18 +1,13 @@
mod dice;
mod limits;
mod parsing;
use crate::dice::{DiscordMd, Witness};
use anyhow::{Context as AnyCtx, bail};
use anyhow::bail;
use dotenv::dotenv;
use limits::*;
use rand::{Rng, SeedableRng};
use serenity::all::{
Command, CommandInteraction, CommandOptionType, CreateCommandOption, CreateComponent,
FullEvent, Interaction, InteractionContext, MessageFlags, ResolvedOption, ResolvedValue,
};
use serenity::all::{Command, CommandInteraction, CommandOptionType, CreateCommandOption, CreateComponent, FullEvent, Interaction, InteractionContext, MessageFlags, ResolvedOption, ResolvedValue};
use serenity::builder::{
CreateCommand, CreateContainer, CreateContainerComponent, CreateInteractionResponse, CreateInteractionResponseMessage, CreateTextDisplay,
CreateCommand, CreateContainer, CreateContainerComponent, CreateInteractionResponse,
CreateInteractionResponseFollowup, CreateInteractionResponseMessage, CreateTextDisplay,
};
use serenity::{async_trait, prelude::*};
use std::sync::Arc;
@ -115,7 +110,6 @@ impl EventHandler for Handler {
}
pub async fn roll(ctx: &Context, command: &CommandInteraction) -> anyhow::Result<()> {
pub async fn inner(ctx: &Context, command: &CommandInteraction) -> anyhow::Result<()> {
let options = command.data.options();
let Some(ResolvedOption {
value: ResolvedValue::String(formula),
@ -135,7 +129,7 @@ pub async fn roll(ctx: &Context, command: &CommandInteraction) -> anyhow::Result
}
})
.unwrap_or(1)
.min(MAX_ROLL_REPEATS as i64);
.min(20);
let private = options
.iter()
.find(|option| option.name == "private")
@ -169,94 +163,88 @@ pub async fn roll(ctx: &Context, command: &CommandInteraction) -> anyhow::Result
})
.and_then(|s| s.parse::<u64>().ok());
let expression = parsing::parse(&formula).context("Dice formula could not be parsed.")?;
let seed = fixed_seed.unwrap_or_else(|| rand::rng().next_u64());
let mut rng = rand_xoshiro::Xoshiro256PlusPlus::seed_from_u64(seed);
let average = expression.average(&mut rng)?;
let ev = (average * 10.0).round() / 10.0;
let discord_md = Arc::new(std::sync::RwLock::new(DiscordMd {
buffers: vec![],
dice_written: 0,
}));
if let Some(description) = description {
discord_md
.write()
.unwrap()
.buffers
.push(format!(">>> {description}\n"));
}
for _ in 0..repeat {
let mut witness = DiscordMd::create_witness(&discord_md);
let result = expression.evaluate(&mut rng, &mut witness, i64::MAX)?;
witness.witness_total_result(result)?;
witness.end();
}
if fixed_seed.is_some() {
discord_md
.write()
.unwrap()
.buffers
.push("***⚠️ This is not a fair roll. The result was generated based on a fixed seed and is thus fully reproducible.***".to_string());
}
discord_md
.write()
.unwrap()
.buffers
.push(format!("-# Expected value: {ev} | Seed: {seed}"));
let text_components = Arc::into_inner(discord_md)
.unwrap()
.into_inner()?
.buffers
.into_iter()
.map(|text| CreateContainerComponent::TextDisplay(CreateTextDisplay::new(text)))
.collect::<Vec<_>>();
let response = CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.components(vec![CreateComponent::Container(CreateContainer::new(
text_components,
))])
.ephemeral(private)
.flags(
MessageFlags::IS_COMPONENTS_V2 | MessageFlags::SUPPRESS_NOTIFICATIONS,
),
);
debug!("Returning response: {response:#?}");
command
.create_response(
&ctx.http,
response,
)
.await?;
Ok(())
}
if let Ok(_) = inner(ctx, command).await {
} else if let Err(err) = inner(ctx, command).await {
warn!("Error encountered: {err:?}");
let result = parsing::parse(&formula);
let expression = match result {
Ok(expression) => expression,
Err(error) => {
command
.create_response(
&ctx.http,
CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.components(vec![CreateComponent::Container(CreateContainer::new(
vec![CreateContainerComponent::TextDisplay(
CreateTextDisplay::new(err.to_string()),
)],
))])
.ephemeral(true)
.flags(
MessageFlags::IS_COMPONENTS_V2 | MessageFlags::SUPPRESS_NOTIFICATIONS,
),
CreateInteractionResponseMessage::default()
.content(format!("Dice formula could not be parsed.\n{}", error))
.ephemeral(true),
),
)
.await?
.await?;
return Ok(());
}
};
if private {
command.defer_ephemeral(&ctx.http).await?;
} else {
command.defer(&ctx.http).await?;
}
let seed = fixed_seed.unwrap_or_else(|| rand::rng().next_u64());
let mut rng = rand_xoshiro::Xoshiro256PlusPlus::seed_from_u64(seed);
let average = match expression.average(&mut rng) {
Ok(res) => res,
Err(err) => {
command
.create_followup(
&ctx.http,
CreateInteractionResponseFollowup::new().content(format!("{}", err)),
)
.await?;
return Err(err);
}
};
let ev = (average * 10.0).round() / 10.0;
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 {
let result = match expression.collect_evaluation(&mut rng) {
Ok(res) => res,
Err(err) => {
command
.create_followup(
&ctx.http,
CreateInteractionResponseFollowup::new().content(format!("{}", err)),
)
.await?;
return Err(err);
}
};
text_components.push(CreateContainerComponent::TextDisplay(
CreateTextDisplay::new(format!("{}\n", result)),
));
}
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}")),
));
command
.create_followup(
&ctx.http,
CreateInteractionResponseFollowup::new()
.components(vec![CreateComponent::Container(CreateContainer::new(
text_components,
))])
.ephemeral(private)
.flags(MessageFlags::IS_COMPONENTS_V2 | MessageFlags::SUPPRESS_NOTIFICATIONS),
)
.await?;
Ok(())
}