nice_dice/
lib.rs

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/// Present the comma-separated expressions as a table, formatted as a column chart by Charts.css.
46///
47/// Returns a string of HTML indicating. On error, returns HTML indicating the error.
48#[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
59/// Present the comma-separated expressions as a table, formatted as a column chart by Charts.css.
60///
61/// On success, returns a string of HTML indicating.
62pub 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        // Manual rather than doctest, because it's also a normal .md file
83        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}