1#![doc=include_str!("../README.md")]
2
3use std::collections::HashSet;
4
5use maud::PreEscaped;
6use peg::{error::ParseError, str::LineCol};
7use symbolic::Symbol;
8
9mod analysis;
10mod discrete;
11mod parse;
12mod symbolic;
13
14pub mod html;
15pub use analysis::Closed;
16pub use discrete::{Distribution, Evaluator};
17use wasm_bindgen::prelude::wasm_bindgen;
18
19#[cfg(test)]
20mod properties;
21
22#[derive(thiserror::Error, Debug)]
23pub enum Error {
24 #[error("parse error; in expression {0}; {1}")]
25 ParseError(String, ParseError<LineCol>),
26 #[error("count cannot be negative; in expression {0}")]
27 NegativeCount(String),
28 #[error("asked to keep {0} rolls, but the expression {1} may not generate that many")]
29 KeepTooFew(usize, String),
30 #[error("denominator contains 0 in its range; in expression {0}")]
31 DivideByZero(String),
32 #[error("invalid character {0} in symbol; symbols may only contain A-Z")]
33 InvalidSymbolCharacter(char),
34 #[error("symbol(s) used when not bound: {}", list_symbols(.0))]
35 UnboundSymbols(HashSet<Symbol>),
36 #[error("d0 is not a valid die")]
37 ZeroFacedDie(),
38}
39
40fn list_symbols(s: &HashSet<Symbol>) -> String {
41 let strs: Vec<_> = s.iter().map(|v| v.to_string()).collect();
42 strs.join(", ")
43}
44
45#[wasm_bindgen]
49pub async fn distribution_table(input: String) -> String {
50 match distribution_table_inner(input) {
51 Ok(v) => v,
52 Err(e) => maud::html!(
53 p{ "Error: " (e) }
54 ),
55 }
56 .into()
57}
58
59pub fn distribution_table_inner(input: String) -> Result<PreEscaped<String>, Error> {
63 let items = input.split(",");
64 let res: Result<Vec<_>, _> = items
65 .map(|v| {
66 let expr: Closed = v.parse()?;
67 let distr = expr.distribution()?;
68 Ok((expr.to_string(), distr))
69 })
70 .collect();
71 let res = res?;
72
73 Ok(html::table_multi_dist(&res))
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn readme_examples() {
82 for expr in [
84 "(1d20 + 4 >= 12) * (1d4 + 1)",
85 "[ATK: 1d20] (ATK + 4 >= 12) * (1d4 + 1)",
86 "[ATK: 1d20] (ATK > 1) * (ATK + 4 >= 12) * (1d4 + 1)",
87 "(1d20 > 1) * (1d20 + 4 >= 12) * (1d4 + 1)",
88 "[ATK: 1d20] (ATK = 20) * (2d4 + 1) + (ATK < 20) * (ATK > 1) * (ATK + 4 >= 12) * (1d4 + 1)",
89 "2([ATK: 1d20] (ATK = 20) * (2d4 + 1) + (ATK < 20) * (ATK > 1) * (ATK + 4 >= 12) * (1d4 + 1))",
90 r#"[MOD: +5] [PROFICIENCY: +3] [AC: 12]
912 (
92 [ATK: 2d20kl] [DIE: 1d10] [CRIT: 1d10]
93 (ATK = 20) * (DIE + CRIT + MOD) +
94 (ATK < 20) * (ATK > 1) (ATK + MOD + PROFICIENCY >= AC) * (DIE + MOD)
95)"#,
96 ] {
97 let _: Closed = expr.parse().unwrap();
98 }
99 }
100}