added conditional fragments to exploding dice

This commit is contained in:
Lilith Schier 2026-08-11 18:56:51 +02:00
parent 25d72e9633
commit 0ca5db21f9
2 changed files with 132 additions and 137 deletions

View file

@ -1,6 +1,6 @@
use anyhow::bail; use anyhow::bail;
use rand::{Rng, RngExt}; use rand::{Rng, RngExt};
use std::fmt::Write; use std::io::Write;
pub type ExpBox = Box<Expression>; pub type ExpBox = Box<Expression>;
@ -23,24 +23,26 @@ pub struct DiceFormula {
pub(crate) kl: Option<ExpBox>, pub(crate) kl: Option<ExpBox>,
pub(crate) dh: Option<ExpBox>, pub(crate) dh: Option<ExpBox>,
pub(crate) dl: Option<ExpBox>, pub(crate) dl: Option<ExpBox>,
pub(crate) x: bool, pub(crate) x: Option<Vec<CompareFragment>>,
} }
#[derive(Debug, PartialEq, Clone)]
pub enum CompareFragment { pub enum CompareFragment {
Eq(ExpBox), Eq(ExpBox),
Gt(ExpBox), Gt(ExpBox),
Gte(ExpBox), Ge(ExpBox),
Lt(ExpBox), Lt(ExpBox),
Lte(ExpBox), Le(ExpBox),
} }
const DICE_POOL_LIMIT: usize = 100_000; const DICE_POOL_LIMIT: usize = 100_000;
impl Expression { impl Expression {
pub fn collect_evaluation(&self, mut rng: impl Rng) -> anyhow::Result<String> { pub fn collect_evaluation(&self, mut rng: impl Rng) -> anyhow::Result<String> {
let mut result = String::new(); let mut buffer = Vec::new();
let total = self.evaluate(&mut rng, &mut result, 100)?; let total = self.evaluate(&mut rng, &mut buffer, 100)?;
Ok(format!("**{total}** = {result}")) let result_text = String::from_utf8(buffer)?;
Ok(format!("**{total}** = {result_text}"))
} }
pub fn average(&self, rng: &mut impl Rng) -> anyhow::Result<f64> { pub fn average(&self, rng: &mut impl Rng) -> anyhow::Result<f64> {
@ -49,8 +51,7 @@ impl Expression {
} }
let mut average = self.sample(rng)?; let mut average = self.sample(rng)?;
for idx in 1..3000 { for idx in 1..3000 {
average = average = (average * idx as f64 + self.sample(rng)?) / (idx as f64 + 1f64);
(average * idx as f64 + self.sample(rng)?) / (idx as f64 + 1f64);
} }
Ok(average) Ok(average)
} }
@ -58,19 +59,18 @@ impl Expression {
fn evaluate( fn evaluate(
&self, &self,
rng: &mut impl Rng, rng: &mut impl Rng,
output: &mut String, writer: &mut impl Write,
outer_precedence: i64, outer_precedence: i64,
) -> anyhow::Result<f64> { ) -> anyhow::Result<f64> {
use Expression::*; use Expression::*;
let node = self; let precedence = self.precedence();
let precedence = node.precedence();
let needs_parens = precedence > outer_precedence; let needs_parens = precedence > outer_precedence;
if needs_parens { if needs_parens {
output.push('(') write!(writer, "(")?;
} }
let result = match node { let result = match self {
Const(x) => { Const(x) => {
write!(output, "{}", x).unwrap(); write!(writer, "{}", x)?;
*x *x
} }
Dice(DiceFormula { Dice(DiceFormula {
@ -83,15 +83,15 @@ impl Expression {
x, x,
}) => { }) => {
let count = if let Some(count_node) = count_node { let count = if let Some(count_node) = count_node {
count_node.evaluate(rng, output, precedence)? as usize count_node.evaluate(rng, writer, precedence)? as usize
} else { } else {
1 1
}; };
if count > DICE_POOL_LIMIT { if count > DICE_POOL_LIMIT {
bail!("Too many dice.") bail!("Too many dice.")
}; };
output.push_str("d"); write!(writer, "d")?;
let size = size_node.evaluate(rng, output, precedence)? as i64; let size = size_node.evaluate(rng, writer, precedence)? as i64;
if size < 1 { if size < 1 {
bail!("Invalid die size.") bail!("Invalid die size.")
} }
@ -99,42 +99,57 @@ impl Expression {
let mut rolls = (0..count) let mut rolls = (0..count)
.map(|_| rng.random_range(1..=size)) .map(|_| rng.random_range(1..=size))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if *x { if let Some(fragments) = x {
output.push_str("x"); write!(writer, "x")?;
if size < 2 { if size < 2 {
bail!("Infinite explosion.") bail!("Infinite explosion.")
} }
let mut comparers = Vec::with_capacity(fragments.len());
for fragment in fragments {
comparers.push(fragment.comparer(rng, writer)?);
}
for i in 0..DICE_POOL_LIMIT { for i in 0..DICE_POOL_LIMIT {
if i >= rolls.len() { if i >= rolls.len() {
break; break;
} }
if comparers.len() == 0 {
// Explode on max size dice
if rolls[i] == size { if rolls[i] == size {
rolls.push(rng.random_range(1..=size)); rolls.push(rng.random_range(1..=size));
} }
} else {
// Explode based on comparers provided
for comparer in comparers.iter() {
if comparer(rolls[i] as f64) {
rolls.push(rng.random_range(1..=size));
break;
}
}
}
} }
} }
let kh = if let Some(child) = kh { let kh = if let Some(child) = kh {
output.push_str("kh"); write!(writer, "kh")?;
Some(child.evaluate(rng, output, precedence)? as usize) Some(child.evaluate(rng, writer, precedence)? as usize)
} else { } else {
None None
}; };
let kl = if let Some(child) = kl { let kl = if let Some(child) = kl {
output.push_str("kl"); write!(writer, "kl")?;
Some(child.evaluate(rng, output, precedence)? as usize) Some(child.evaluate(rng, writer, precedence)? as usize)
} else { } else {
None None
}; };
let dh = if let Some(child) = dh { let dh = if let Some(child) = dh {
output.push_str("dh"); write!(writer, "dh")?;
Some(child.evaluate(rng, output, precedence)? as usize) Some(child.evaluate(rng, writer, precedence)? as usize)
} else { } else {
None None
}; };
let dl = if let Some(child) = dl { let dl = if let Some(child) = dl {
output.push_str("dl"); write!(writer, "dl")?;
Some(child.evaluate(rng, output, precedence)? as usize) Some(child.evaluate(rng, writer, precedence)? as usize)
} else { } else {
None None
}; };
@ -150,7 +165,7 @@ impl Expression {
let print_all_rolls = rolls.len() <= 100; let print_all_rolls = rolls.len() <= 100;
if print_all_rolls { if print_all_rolls {
output.push_str(" ‹ "); write!(writer, " ‹ ")?;
} }
for idx in 0..rolls.len() { for idx in 0..rolls.len() {
let mut skip = false; let mut skip = false;
@ -181,140 +196,62 @@ impl Expression {
} }
if print_all_rolls { if print_all_rolls {
if skip { if skip {
write!(output, "~~`{}`~~", roll)?; write!(writer, "~~`{}`~~", roll)?;
} else if size > 2 && (roll == 1 || roll == size) { } else if size > 2 && (roll == 1 || roll == size) {
write!(output, "**`{}`**", roll)?; write!(writer, "**`{}`**", roll)?;
} else { } else {
write!(output, "`{}`", roll)?; write!(writer, "`{}`", roll)?;
} }
if idx != rolls.len() - 1 { if idx != rolls.len() - 1 {
output.push_str(" "); write!(writer, " ")?;
} }
} }
} }
if print_all_rolls { if print_all_rolls {
output.push_str(" ›"); write!(writer, " ›")?;
} }
result_set.iter().sum() result_set.iter().sum()
} }
Neg(inner_node) => { Neg(inner_node) => {
output.push_str("-"); write!(writer, "-")?;
let inner = inner_node.evaluate(rng, output, precedence)?; let inner = inner_node.evaluate(rng, writer, precedence)?;
-inner -inner
} }
Add(lhs_node, rhs_node) => { Add(lhs_node, rhs_node) => {
let lhs = lhs_node.evaluate(rng, output, precedence)?; let lhs = lhs_node.evaluate(rng, writer, precedence)?;
output.push_str(" + "); write!(writer, " + ")?;
let rhs = rhs_node.evaluate(rng, output, precedence)?; let rhs = rhs_node.evaluate(rng, writer, precedence)?;
lhs + rhs lhs + rhs
} }
Sub(lhs_node, rhs_node) => { Sub(lhs_node, rhs_node) => {
let lhs = lhs_node.evaluate(rng, output, precedence)?; let lhs = lhs_node.evaluate(rng, writer, precedence)?;
output.push_str(" - "); write!(writer, " - ")?;
let rhs = rhs_node.evaluate(rng, output, precedence)?; let rhs = rhs_node.evaluate(rng, writer, precedence)?;
lhs - rhs lhs - rhs
} }
Mul(lhs_node, rhs_node) => { Mul(lhs_node, rhs_node) => {
let lhs = lhs_node.evaluate(rng, output, precedence)?; let lhs = lhs_node.evaluate(rng, writer, precedence)?;
output.push_str(" × "); write!(writer, " × ")?;
let rhs = rhs_node.evaluate(rng, output, precedence)?; let rhs = rhs_node.evaluate(rng, writer, precedence)?;
lhs * rhs lhs * rhs
} }
Div(lhs_node, rhs_node) => { Div(lhs_node, rhs_node) => {
let lhs = lhs_node.evaluate(rng, output, precedence)?; let lhs = lhs_node.evaluate(rng, writer, precedence)?;
output.push_str(" ÷ "); write!(writer, " ÷ ")?;
let rhs = rhs_node.evaluate(rng, output, precedence)?; let rhs = rhs_node.evaluate(rng, writer, precedence)?;
lhs / rhs lhs / rhs
} }
}; };
if needs_parens { if needs_parens {
output.push(')') write!(writer, ")")?;
} }
Ok(result) Ok(result)
} }
fn sample(&self, rng: &mut impl Rng) -> anyhow::Result<f64> { fn sample(&self, rng: &mut impl Rng) -> anyhow::Result<f64> {
let node = self; self.evaluate(rng, &mut std::io::sink(), 100)
let result = match node {
Expression::Const(x) => *x,
Expression::Dice(DiceFormula {
count,
size,
kh,
kl,
dh,
dl,
x,
}) => {
let count = if let Some(count) = count {
count.sample(rng)? as usize
} else {
1
};
if count > DICE_POOL_LIMIT {
bail!("Too many dice.")
};
let size = size.sample(rng)? as i64;
if size < 1 {
bail!("Invalid die size.")
}
let mut rolls = (0..count)
.map(|_| rng.random_range(1..=size))
.collect::<Vec<_>>();
if *x {
if size < 2 {
bail!("Infinite explosion.")
}
for i in 0..DICE_POOL_LIMIT {
if i >= rolls.len() {
break;
}
if rolls[i] == size {
rolls.push(rng.random_range(1..=size));
}
}
}
rolls.sort();
let kh = if let Some(child) = kh {
child.sample(rng)? as usize
} else {
rolls.len()
};
let kl = if let Some(child) = kl {
child.sample(rng)? as usize
} else {
rolls.len()
};
let dh = if let Some(child) = dh {
child.sample(rng)? as usize
} else {
0
};
let dl = if let Some(child) = dl {
child.sample(rng)? as usize
} else {
0
};
let rolls_len = rolls.len();
rolls
.into_iter()
.skip(dl.max(rolls_len - kh))
.take(rolls_len - (dh.max(rolls_len - kl)))
.map(|x| x as f64)
.sum::<f64>()
}
Expression::Neg(id) => -id.sample(rng)?,
Expression::Add(lhs, rhs) => lhs.sample(rng)? + rhs.sample(rng)?,
Expression::Sub(lhs, rhs) => lhs.sample(rng)? - rhs.sample(rng)?,
Expression::Mul(lhs, rhs) => lhs.sample(rng)? * rhs.sample(rng)?,
Expression::Div(lhs, rhs) => lhs.sample(rng)? / rhs.sample(rng)?,
};
Ok(result)
} }
fn avg(&self) -> anyhow::Result<f64> { fn avg(&self) -> anyhow::Result<f64> {
@ -343,7 +280,7 @@ impl Expression {
bail!("Invalid die size.") bail!("Invalid die size.")
} }
if *x { if let Some(_) = x {
bail!("Not implemented yet"); bail!("Not implemented yet");
} }
if let Some(_) = kh { if let Some(_) = kh {
@ -371,6 +308,42 @@ impl Expression {
} }
} }
impl CompareFragment {
fn comparer(
&self,
rng: &mut impl Rng,
writer: &mut impl Write,
) -> anyhow::Result<Box<dyn Fn(f64) -> bool>> {
Ok(match self {
CompareFragment::Eq(inner) => {
write!(writer, "=")?;
let comp = inner.evaluate(rng, writer, 2)?;
Box::new(move |value| value == comp)
}
CompareFragment::Gt(inner) => {
write!(writer, ">")?;
let comp = inner.evaluate(rng, writer, 2)?;
Box::new(move |value| value > comp)
}
CompareFragment::Ge(inner) => {
write!(writer, ">=")?;
let comp = inner.evaluate(rng, writer, 2)?;
Box::new(move |value| value >= comp)
}
CompareFragment::Lt(inner) => {
write!(writer, "<")?;
let comp = inner.evaluate(rng, writer, 2)?;
Box::new(move |value| value < comp)
}
CompareFragment::Le(inner) => {
write!(writer, "<=")?;
let comp = inner.evaluate(rng, writer, 2)?;
Box::new(move |value| value <= comp)
}
})
}
}
impl Expression { impl Expression {
fn precedence(&self) -> i64 { fn precedence(&self) -> i64 {
use Expression::*; use Expression::*;

View file

@ -1,7 +1,4 @@
use crate::dice::{ use crate::dice::{CompareFragment, DiceFormula, Expression::{self, Const}};
DiceFormula,
Expression::{self, Const},
};
use Assoc::Left; use Assoc::Left;
use nom::{ use nom::{
Parser, Parser,
@ -73,7 +70,7 @@ fn expr<'c, 'i>(
opt(preceded(tag("kl"), opt(basic_operand(ctx)))), opt(preceded(tag("kl"), opt(basic_operand(ctx)))),
opt(preceded(tag("dh"), opt(basic_operand(ctx)))), opt(preceded(tag("dh"), opt(basic_operand(ctx)))),
opt(preceded(tag("dl"), opt(basic_operand(ctx)))), opt(preceded(tag("dl"), opt(basic_operand(ctx)))),
opt(preceded(tag("x"), opt(basic_operand(ctx)))), opt(preceded(tag("x"), many0(compare_fragment(ctx)))),
)), )),
) )
.map(|(count, size, (kh, kl, dh, dl, x))| { .map(|(count, size, (kh, kl, dh, dl, x))| {
@ -84,7 +81,7 @@ fn expr<'c, 'i>(
kl: kl.map(|n| Box::new(n.unwrap_or(Const(1f64)))), kl: kl.map(|n| Box::new(n.unwrap_or(Const(1f64)))),
dh: dh.map(|n| Box::new(n.unwrap_or(Const(1f64)))), dh: dh.map(|n| Box::new(n.unwrap_or(Const(1f64)))),
dl: dl.map(|n| Box::new(n.unwrap_or(Const(1f64)))), dl: dl.map(|n| Box::new(n.unwrap_or(Const(1f64)))),
x: x.is_some(), x,
}) })
}); });
let operand = alt((dice_formula, basic_operand(ctx))); let operand = alt((dice_formula, basic_operand(ctx)));
@ -127,6 +124,19 @@ fn basic_operand<'c, 'i>(
alt((number, delimited(tag("("), expr(ctx), tag(")")))) alt((number, delimited(tag("("), expr(ctx), tag(")"))))
} }
fn compare_fragment<'c, 'i>(
ctx: &'c Context,
) -> impl Parser<&'i str, Output = CompareFragment, Error = InternalError<&'i str>> + use<'c, 'i> {
use CompareFragment::*;
alt((
preceded(tag("="), basic_operand(ctx)).map(|x| Eq(Box::new(x))),
preceded(tag(">"), basic_operand(ctx)).map(|x| Gt(Box::new(x))),
preceded(tag(">="), basic_operand(ctx)).map(|x| Ge(Box::new(x))),
preceded(tag("<"), basic_operand(ctx)).map(|x| Lt(Box::new(x))),
preceded(tag("<="), basic_operand(ctx)).map(|x| Le(Box::new(x))),
))
}
fn spaced_op(t: &str) -> impl Parser<&str, Output = &str, Error = VerboseError<&str>> { fn spaced_op(t: &str) -> impl Parser<&str, Output = &str, Error = VerboseError<&str>> {
delimited(many0(tag(" ")), tag(t), many0(tag(" "))) delimited(many0(tag(" ")), tag(t), many0(tag(" ")))
} }
@ -183,4 +193,16 @@ mod test {
let expression = parse("2d20dl1").unwrap(); let expression = parse("2d20dl1").unwrap();
assert_matches!(expression, Expression::Dice { .. }); assert_matches!(expression, Expression::Dice { .. });
} }
#[test]
pub fn parse_x() {
let expression = parse("3d4x").unwrap();
assert_matches!(expression, Expression::Dice { .. });
}
#[test]
pub fn parse_xgt() {
let expression = parse("3d4x>=3").unwrap();
assert_matches!(expression, Expression::Dice { .. });
}
} }