@mcptoolshop/roll 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.es.md +204 -0
  4. package/README.fr.md +204 -0
  5. package/README.hi.md +204 -0
  6. package/README.it.md +204 -0
  7. package/README.ja.md +204 -0
  8. package/README.md +204 -0
  9. package/README.pt-BR.md +204 -0
  10. package/README.zh.md +204 -0
  11. package/dist/analyze/distribution.d.ts +6 -0
  12. package/dist/analyze/distribution.d.ts.map +1 -0
  13. package/dist/analyze/distribution.js +252 -0
  14. package/dist/analyze/distribution.js.map +1 -0
  15. package/dist/analyze/montecarlo.d.ts +5 -0
  16. package/dist/analyze/montecarlo.d.ts.map +1 -0
  17. package/dist/analyze/montecarlo.js +19 -0
  18. package/dist/analyze/montecarlo.js.map +1 -0
  19. package/dist/analyze/stats.d.ts +16 -0
  20. package/dist/analyze/stats.d.ts.map +1 -0
  21. package/dist/analyze/stats.js +66 -0
  22. package/dist/analyze/stats.js.map +1 -0
  23. package/dist/bin.d.ts +3 -0
  24. package/dist/bin.d.ts.map +1 -0
  25. package/dist/bin.js +267 -0
  26. package/dist/bin.js.map +1 -0
  27. package/dist/display/box.d.ts +5 -0
  28. package/dist/display/box.d.ts.map +1 -0
  29. package/dist/display/box.js +26 -0
  30. package/dist/display/box.js.map +1 -0
  31. package/dist/display/color.d.ts +12 -0
  32. package/dist/display/color.d.ts.map +1 -0
  33. package/dist/display/color.js +19 -0
  34. package/dist/display/color.js.map +1 -0
  35. package/dist/display/format.d.ts +13 -0
  36. package/dist/display/format.d.ts.map +1 -0
  37. package/dist/display/format.js +134 -0
  38. package/dist/display/format.js.map +1 -0
  39. package/dist/display/histogram.d.ts +7 -0
  40. package/dist/display/histogram.d.ts.map +1 -0
  41. package/dist/display/histogram.js +48 -0
  42. package/dist/display/histogram.js.map +1 -0
  43. package/dist/engine/random.d.ts +6 -0
  44. package/dist/engine/random.d.ts.map +1 -0
  45. package/dist/engine/random.js +17 -0
  46. package/dist/engine/random.js.map +1 -0
  47. package/dist/engine/roller.d.ts +19 -0
  48. package/dist/engine/roller.d.ts.map +1 -0
  49. package/dist/engine/roller.js +168 -0
  50. package/dist/engine/roller.js.map +1 -0
  51. package/dist/index.d.ts +27 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +37 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/loot/table.d.ts +27 -0
  56. package/dist/loot/table.d.ts.map +1 -0
  57. package/dist/loot/table.js +92 -0
  58. package/dist/loot/table.js.map +1 -0
  59. package/dist/parser/ast.d.ts +27 -0
  60. package/dist/parser/ast.d.ts.map +1 -0
  61. package/dist/parser/ast.js +2 -0
  62. package/dist/parser/ast.js.map +1 -0
  63. package/dist/parser/lexer.d.ts +7 -0
  64. package/dist/parser/lexer.d.ts.map +1 -0
  65. package/dist/parser/lexer.js +126 -0
  66. package/dist/parser/lexer.js.map +1 -0
  67. package/dist/parser/parser.d.ts +7 -0
  68. package/dist/parser/parser.d.ts.map +1 -0
  69. package/dist/parser/parser.js +188 -0
  70. package/dist/parser/parser.js.map +1 -0
  71. package/dist/parser/tokens.d.ts +25 -0
  72. package/dist/parser/tokens.d.ts.map +1 -0
  73. package/dist/parser/tokens.js +21 -0
  74. package/dist/parser/tokens.js.map +1 -0
  75. package/package.json +55 -0
@@ -0,0 +1,204 @@
1
+ <p align="center">
2
+ <a href="README.ja.md">日本語</a> | <a href="README.zh.md">中文</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.hi.md">हिन्दी</a> | <a href="README.it.md">Italiano</a> | <a href="README.md">English</a>
3
+ </p>
4
+
5
+ <p align="center"><img src="https://raw.githubusercontent.com/mcp-tool-shop-org/brand/main/logos/roll/readme.png" width="400" alt="Roll"></p>
6
+
7
+ <p align="center">
8
+ <a href="https://github.com/mcp-tool-shop-org/roll/actions"><img src="https://github.com/mcp-tool-shop-org/roll/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
10
+ <a href="https://mcp-tool-shop-org.github.io/roll/"><img src="https://img.shields.io/badge/Landing_Page-online-brightgreen" alt="Landing Page"></a>
11
+ <a href="https://www.npmjs.com/package/@mcptoolshop/roll"><img src="https://img.shields.io/npm/v/@mcptoolshop/roll" alt="npm version"></a>
12
+ </p>
13
+
14
+ <p align="center">RPG dice engine with probability analysis, loot tables, and beautiful terminal output.</p>
15
+
16
+ ```
17
+ npx @mcptoolshop/roll 4d6dl1 --analyze
18
+ ```
19
+
20
+ ```
21
+ Distribution
22
+
23
+ 3 0.08%
24
+ 4 █ 0.31%
25
+ 5 ███ 0.77%
26
+ 6 ███████ 1.62%
27
+ 7 █████████████ 2.93%
28
+ 8 ██████████████████████ 4.78%
29
+ 9 ████████████████████████████████ 7.02%
30
+ 10 ███████████████████████████████████████████ 9.41%
31
+ 11 ████████████████████████████████████████████████████ 11.42%
32
+ 12 ██████████████████████████████████████████████████████████ 12.89%
33
+ 13 ████████████████████████████████████████████████████████████ 13.27%
34
+ 14 ████████████████████████████████████████████████████████ 12.35%
35
+ 15 ██████████████████████████████████████████████ 10.11%
36
+ 16 █████████████████████████████████ 7.25%
37
+ 17 ███████████████████ 4.17%
38
+ 18 ███████ 1.62%
39
+
40
+ ┌─ Statistics ─────────────────────────────────┐
41
+ │ Mean: 12.24 │
42
+ │ Median: 12 │
43
+ │ Mode: 13 │
44
+ │ Std Dev: 2.85 │
45
+ │ Range: 3–18 │
46
+ │ Entropy: 3.53 bits │
47
+ │ │
48
+ │ Percentiles: │
49
+ │ p10:8 p25:10 p50:12 p75:14 p90:16 p95:17│
50
+ └───────────────────────────────────────────────┘
51
+ ```
52
+
53
+ ## Instalação
54
+
55
+ ```bash
56
+ npm install @mcptoolshop/roll
57
+ ```
58
+
59
+ Requer Node.js >= 22.
60
+
61
+ ## Uso da Linha de Comando
62
+
63
+ ### Lançar dados
64
+
65
+ ```bash
66
+ roll 2d6+3
67
+ roll d20+5
68
+ roll 4d6kh3
69
+ roll 1d6!
70
+ roll d%
71
+ roll 4dF
72
+ roll "(2d6+3)*2"
73
+ ```
74
+
75
+ ### Analisar probabilidade
76
+
77
+ ```bash
78
+ roll 2d6 --analyze # Full distribution + statistics
79
+ roll d20+5 --at-least 15 # P(result >= 15)
80
+ ```
81
+
82
+ ### Comparar distribuições
83
+
84
+ ```bash
85
+ roll --compare "4d6dl1" "3d6"
86
+ ```
87
+
88
+ Estatísticas lado a lado (média, mediana, moda, desvio padrão, intervalo, entropia) com coluna de diferença, além de ambos os histogramas.
89
+
90
+ ### Tabelas de recompensas
91
+
92
+ ```bash
93
+ roll --loot treasure.json
94
+ ```
95
+
96
+ Formato JSON:
97
+
98
+ ```json
99
+ {
100
+ "tables": [
101
+ {
102
+ "table": "Treasure",
103
+ "items": [
104
+ { "name": "Gold", "weight": 40, "roll": "2d6*10" },
105
+ { "name": "Potion of Healing", "weight": 30 },
106
+ { "name": "Scroll", "weight": 15, "quantity": "1d3" },
107
+ { "name": "Rare Item", "weight": 5, "table": "Rare Weapons" }
108
+ ]
109
+ },
110
+ {
111
+ "table": "Rare Weapons",
112
+ "items": [
113
+ { "name": "Vorpal Blade", "weight": 5 },
114
+ { "name": "Frost Brand", "weight": 25 }
115
+ ]
116
+ }
117
+ ]
118
+ }
119
+ ```
120
+
121
+ Características: seleção ponderada, referências de tabelas aninhadas, expressões de dados para quantidade e valor.
122
+
123
+ ### Outras opções
124
+
125
+ ```bash
126
+ roll 2d6+3 --times 5 # Roll 5 times
127
+ roll 2d6+3 --json # Machine-readable output
128
+ roll --help # Full usage
129
+ roll --version # Version
130
+ ```
131
+
132
+ ## Notação de Dados
133
+
134
+ | Notação | Significado |
135
+ |----------|---------|
136
+ | `2d6` | Lançar 2 dados de seis lados |
137
+ | `d20` | Lançar 1 dado de vinte lados |
138
+ | `4d6kh3` | Lançar 4d6, manter os 3 maiores |
139
+ | `4d6dl1` | Lançar 4d6, descartar o menor |
140
+ | `1d6!` | Dado explosivo (relançar no máximo, adicionar) |
141
+ | `1d6!>4` | Explodir em 4 ou mais |
142
+ | `d%` | Dado de percentil (1-100) |
143
+ | `4dF` | Dados Fate/Fudge (-1, 0, +1 cada) |
144
+ | `(2d6+3)*2` | Aritmética com agrupamento |
145
+ | `2d6+1d4+3` | Expressões encadeadas |
146
+
147
+ ## API da Biblioteca
148
+
149
+ ```typescript
150
+ import { roll, analyze, parse, evaluate, computeDistribution } from '@mcptoolshop/roll';
151
+
152
+ // Quick roll
153
+ const result = roll('4d6kh3');
154
+ console.log(result.total); // 14
155
+ console.log(result.groups[0].dice); // per-die breakdown
156
+
157
+ // Full analysis
158
+ const analysis = analyze('2d6+3');
159
+ console.log(analysis.stats.mean); // 10
160
+ console.log(analysis.stats.percentiles[95]); // 14
161
+ console.log(analysis.probabilityAtLeast(12)); // 0.2778
162
+
163
+ // Low-level: parse → AST → evaluate
164
+ import { seededRng } from '@mcptoolshop/roll';
165
+ const ast = parse('4d6dl1');
166
+ const r = evaluate(ast, seededRng(42)); // deterministic
167
+
168
+ // Loot tables
169
+ import { rollLootTable } from '@mcptoolshop/roll';
170
+ const tables = [{ table: "Loot", items: [{ name: "Gold", weight: 50, roll: "2d6*10" }] }];
171
+ const drops = rollLootTable(tables);
172
+ ```
173
+
174
+ ## Motor de Probabilidade
175
+
176
+ - **Distribuições exatas** via convolução polinomial para NdM básicos
177
+ - **Enumeração completa** para mecânicas de manter/descartar (4d6 = 1.296 estados)
178
+ - **Recursão truncada** para dados explosivos (limitado a 10 explosões)
179
+ - **Fallback de Monte Carlo** (100 mil amostras) quando o cálculo exato excede 10 milhões de estados
180
+
181
+ ## Sem Dependências
182
+
183
+ Construído inteiramente com os recursos nativos do Node.js 22+:
184
+ - `util.styleText` para cores no terminal
185
+ - `util.parseArgs` para análise de argumentos da linha de comando
186
+ - `crypto.randomInt` para geração de números aleatórios criptograficamente seguros
187
+
188
+ ## Segurança e Confiança
189
+
190
+ O `@mcptoolshop/roll` processa apenas expressões de dados e nada mais. Não faz solicitações de rede, não escreve arquivos e não coleta dados. O único acesso ao sistema de arquivos é a flag `--loot`, que lê um único arquivo JSON especificado pelo usuário.
191
+
192
+ Não há telemetria, análise ou rastreamento de qualquer tipo. Nenhum segredo, token ou credencial está envolvido em nenhuma operação.
193
+
194
+ Todos os lançamentos de dados usam `crypto.randomInt` do módulo `crypto` do Node.js, fornecendo aleatoriedade criptograficamente segura, adequada para resultados justos.
195
+
196
+ Consulte [SECURITY.md](./SECURITY.md) para a política de relatório de vulnerabilidades.
197
+
198
+ ## Licença
199
+
200
+ MIT
201
+
202
+ ---
203
+
204
+ Desenvolvido por <a href="https://mcp-tool-shop.github.io/">MCP Tool Shop</a>
package/README.zh.md ADDED
@@ -0,0 +1,204 @@
1
+ <p align="center">
2
+ <a href="README.ja.md">日本語</a> | <a href="README.md">English</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.hi.md">हिन्दी</a> | <a href="README.it.md">Italiano</a> | <a href="README.pt-BR.md">Português (BR)</a>
3
+ </p>
4
+
5
+ <p align="center"><img src="https://raw.githubusercontent.com/mcp-tool-shop-org/brand/main/logos/roll/readme.png" width="400" alt="Roll"></p>
6
+
7
+ <p align="center">
8
+ <a href="https://github.com/mcp-tool-shop-org/roll/actions"><img src="https://github.com/mcp-tool-shop-org/roll/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
10
+ <a href="https://mcp-tool-shop-org.github.io/roll/"><img src="https://img.shields.io/badge/Landing_Page-online-brightgreen" alt="Landing Page"></a>
11
+ <a href="https://www.npmjs.com/package/@mcptoolshop/roll"><img src="https://img.shields.io/npm/v/@mcptoolshop/roll" alt="npm version"></a>
12
+ </p>
13
+
14
+ <p align="center">RPG dice engine with probability analysis, loot tables, and beautiful terminal output.</p>
15
+
16
+ ```
17
+ npx @mcptoolshop/roll 4d6dl1 --analyze
18
+ ```
19
+
20
+ ```
21
+ Distribution
22
+
23
+ 3 0.08%
24
+ 4 █ 0.31%
25
+ 5 ███ 0.77%
26
+ 6 ███████ 1.62%
27
+ 7 █████████████ 2.93%
28
+ 8 ██████████████████████ 4.78%
29
+ 9 ████████████████████████████████ 7.02%
30
+ 10 ███████████████████████████████████████████ 9.41%
31
+ 11 ████████████████████████████████████████████████████ 11.42%
32
+ 12 ██████████████████████████████████████████████████████████ 12.89%
33
+ 13 ████████████████████████████████████████████████████████████ 13.27%
34
+ 14 ████████████████████████████████████████████████████████ 12.35%
35
+ 15 ██████████████████████████████████████████████ 10.11%
36
+ 16 █████████████████████████████████ 7.25%
37
+ 17 ███████████████████ 4.17%
38
+ 18 ███████ 1.62%
39
+
40
+ ┌─ Statistics ─────────────────────────────────┐
41
+ │ Mean: 12.24 │
42
+ │ Median: 12 │
43
+ │ Mode: 13 │
44
+ │ Std Dev: 2.85 │
45
+ │ Range: 3–18 │
46
+ │ Entropy: 3.53 bits │
47
+ │ │
48
+ │ Percentiles: │
49
+ │ p10:8 p25:10 p50:12 p75:14 p90:16 p95:17│
50
+ └───────────────────────────────────────────────┘
51
+ ```
52
+
53
+ ## 安装
54
+
55
+ ```bash
56
+ npm install @mcptoolshop/roll
57
+ ```
58
+
59
+ 需要 Node.js >= 22。
60
+
61
+ ## 命令行用法
62
+
63
+ ### 投骰子
64
+
65
+ ```bash
66
+ roll 2d6+3
67
+ roll d20+5
68
+ roll 4d6kh3
69
+ roll 1d6!
70
+ roll d%
71
+ roll 4dF
72
+ roll "(2d6+3)*2"
73
+ ```
74
+
75
+ ### 分析概率
76
+
77
+ ```bash
78
+ roll 2d6 --analyze # Full distribution + statistics
79
+ roll d20+5 --at-least 15 # P(result >= 15)
80
+ ```
81
+
82
+ ### 比较分布
83
+
84
+ ```bash
85
+ roll --compare "4d6dl1" "3d6"
86
+ ```
87
+
88
+ 并排显示统计数据(均值、中位数、众数、标准差、范围、熵),包含差异列,以及直方图。
89
+
90
+ ### 掉落表
91
+
92
+ ```bash
93
+ roll --loot treasure.json
94
+ ```
95
+
96
+ JSON 格式:
97
+
98
+ ```json
99
+ {
100
+ "tables": [
101
+ {
102
+ "table": "Treasure",
103
+ "items": [
104
+ { "name": "Gold", "weight": 40, "roll": "2d6*10" },
105
+ { "name": "Potion of Healing", "weight": 30 },
106
+ { "name": "Scroll", "weight": 15, "quantity": "1d3" },
107
+ { "name": "Rare Item", "weight": 5, "table": "Rare Weapons" }
108
+ ]
109
+ },
110
+ {
111
+ "table": "Rare Weapons",
112
+ "items": [
113
+ { "name": "Vorpal Blade", "weight": 5 },
114
+ { "name": "Frost Brand", "weight": 25 }
115
+ ]
116
+ }
117
+ ]
118
+ }
119
+ ```
120
+
121
+ 特性:加权选择、嵌套表引用、用于数量和值的骰子表达式。
122
+
123
+ ### 其他选项
124
+
125
+ ```bash
126
+ roll 2d6+3 --times 5 # Roll 5 times
127
+ roll 2d6+3 --json # Machine-readable output
128
+ roll --help # Full usage
129
+ roll --version # Version
130
+ ```
131
+
132
+ ## 骰子表示法
133
+
134
+ | 表示法 | 含义 |
135
+ |----------|---------|
136
+ | `2d6` | 投掷 2 个六面骰子 |
137
+ | `d20` | 投掷 1 个二十面骰子 |
138
+ | `4d6kh3` | 投掷 4d6,保留最高的 3 个 |
139
+ | `4d6dl1` | 投掷 4d6,舍弃最低的 1 个 |
140
+ | `1d6!` | 爆炸骰子(最大值时重新投掷,并加总) |
141
+ | `1d6!>4` | 当结果为 4 或更高时爆炸 |
142
+ | `d%` | 百分位骰子(1-100) |
143
+ | `4dF` | 命运/模糊骰子(-1、0、+1 各一个) |
144
+ | `(2d6+3)*2` | 带有分组的算术运算 |
145
+ | `2d6+1d4+3` | 链式表达式 |
146
+
147
+ ## 库 API
148
+
149
+ ```typescript
150
+ import { roll, analyze, parse, evaluate, computeDistribution } from '@mcptoolshop/roll';
151
+
152
+ // Quick roll
153
+ const result = roll('4d6kh3');
154
+ console.log(result.total); // 14
155
+ console.log(result.groups[0].dice); // per-die breakdown
156
+
157
+ // Full analysis
158
+ const analysis = analyze('2d6+3');
159
+ console.log(analysis.stats.mean); // 10
160
+ console.log(analysis.stats.percentiles[95]); // 14
161
+ console.log(analysis.probabilityAtLeast(12)); // 0.2778
162
+
163
+ // Low-level: parse → AST → evaluate
164
+ import { seededRng } from '@mcptoolshop/roll';
165
+ const ast = parse('4d6dl1');
166
+ const r = evaluate(ast, seededRng(42)); // deterministic
167
+
168
+ // Loot tables
169
+ import { rollLootTable } from '@mcptoolshop/roll';
170
+ const tables = [{ table: "Loot", items: [{ name: "Gold", weight: 50, roll: "2d6*10" }] }];
171
+ const drops = rollLootTable(tables);
172
+ ```
173
+
174
+ ## 概率引擎
175
+
176
+ - **精确分布**:通过多项式卷积计算基本 NdM 概率。
177
+ - **完整枚举**:用于保留/舍弃机制(4d6 = 1296 种状态)。
178
+ - **截断递归**:用于爆炸骰子(最多 10 次爆炸)。
179
+ - **蒙特卡洛方法**(10 万个样本):当精确计算超过 1000 万种状态时使用。
180
+
181
+ ## 无外部依赖
182
+
183
+ 完全基于 Node.js 22+ 的内置模块:
184
+ - `util.styleText` 用于终端颜色。
185
+ - `util.parseArgs` 用于命令行参数解析。
186
+ - `crypto.randomInt` 用于密码学安全的骰子投掷。
187
+
188
+ ## 安全与信任
189
+
190
+ `@mcptoolshop/roll` 只处理骰子表达式,不做其他任何操作。它不进行任何网络请求,不写入任何文件,也不收集任何数据。唯一的系统文件访问是 `--loot` 选项,它读取单个用户指定的 JSON 文件。
191
+
192
+ 没有遥测数据,没有分析,也没有任何形式的跟踪。没有涉及任何秘密、令牌或凭据。
193
+
194
+ 所有骰子投掷都使用 Node.js `crypto` 模块中的 `crypto.randomInt`,提供密码学安全的随机数,适用于公平的结果。
195
+
196
+ 请参阅 [SECURITY.md](./SECURITY.md) 以获取漏洞报告政策。
197
+
198
+ ## 许可证
199
+
200
+ MIT
201
+
202
+ ---
203
+
204
+ 由 <a href="https://mcp-tool-shop.github.io/">MCP Tool Shop</a> 构建。
@@ -0,0 +1,6 @@
1
+ import type { ASTNode } from "../parser/ast.js";
2
+ /** A probability distribution: value → probability (0..1) */
3
+ export type Distribution = Map<number, number>;
4
+ /** Compute the full probability distribution for an AST. Falls back to Monte Carlo for complex cases. */
5
+ export declare function computeDistribution(ast: ASTNode): Distribution;
6
+ //# sourceMappingURL=distribution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distribution.d.ts","sourceRoot":"","sources":["../../src/analyze/distribution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAuB,MAAM,kBAAkB,CAAC;AAGrE,6DAA6D;AAC7D,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AA+L/C,yGAAyG;AACzG,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,CAI9D"}
@@ -0,0 +1,252 @@
1
+ import { monteCarloDistribution } from "./montecarlo.js";
2
+ const MAX_EXACT_STATES = 10_000_000;
3
+ function sideCount(sides) {
4
+ if (sides === "%")
5
+ return 100;
6
+ if (sides === "F")
7
+ return 3;
8
+ return sides;
9
+ }
10
+ /** Distribution for a single die roll. */
11
+ function singleDieDistribution(sides) {
12
+ const dist = new Map();
13
+ if (sides === "F") {
14
+ dist.set(-1, 1 / 3);
15
+ dist.set(0, 1 / 3);
16
+ dist.set(1, 1 / 3);
17
+ }
18
+ else {
19
+ const n = sides === "%" ? 100 : sides;
20
+ const p = 1 / n;
21
+ for (let i = 1; i <= n; i++) {
22
+ dist.set(i, p);
23
+ }
24
+ }
25
+ return dist;
26
+ }
27
+ /** Convolve two distributions (sum of independent random variables). */
28
+ function convolve(a, b) {
29
+ const result = new Map();
30
+ for (const [va, pa] of a) {
31
+ for (const [vb, pb] of b) {
32
+ const sum = va + vb;
33
+ result.set(sum, (result.get(sum) ?? 0) + pa * pb);
34
+ }
35
+ }
36
+ return result;
37
+ }
38
+ /** Distribution for NdM via iterative convolution. */
39
+ function convolveDice(count, sides) {
40
+ const single = singleDieDistribution(sides);
41
+ let dist = new Map([[0, 1]]); // identity: always 0
42
+ for (let i = 0; i < count; i++) {
43
+ dist = convolve(dist, single);
44
+ }
45
+ return dist;
46
+ }
47
+ /** Distribution with keep/drop via full enumeration. */
48
+ function enumerateKeepDrop(node) {
49
+ const s = sideCount(node.sides);
50
+ const totalStates = Math.pow(s, node.count);
51
+ if (totalStates > MAX_EXACT_STATES)
52
+ return null;
53
+ const isFate = node.sides === "F";
54
+ const dist = new Map();
55
+ const prob = 1 / totalStates;
56
+ // Enumerate all outcomes
57
+ const rolls = new Array(node.count);
58
+ function enumerate(depth) {
59
+ if (depth === node.count) {
60
+ // Apply keep/drop
61
+ const sorted = [...rolls].sort((a, b) => a - b);
62
+ let kept = sorted.slice(); // start with all
63
+ for (const mod of node.modifiers) {
64
+ if (mod.kind === "explode")
65
+ continue;
66
+ const n = mod.value ?? 1;
67
+ switch (mod.kind) {
68
+ case "kh":
69
+ kept = kept.slice(kept.length - n);
70
+ break;
71
+ case "kl":
72
+ kept = kept.slice(0, n);
73
+ break;
74
+ case "dh":
75
+ kept = kept.slice(0, kept.length - n);
76
+ break;
77
+ case "dl":
78
+ kept = kept.slice(n);
79
+ break;
80
+ }
81
+ }
82
+ const total = kept.reduce((a, b) => a + b, 0);
83
+ dist.set(total, (dist.get(total) ?? 0) + prob);
84
+ return;
85
+ }
86
+ if (isFate) {
87
+ for (let v = -1; v <= 1; v++) {
88
+ rolls[depth] = v;
89
+ enumerate(depth + 1);
90
+ }
91
+ }
92
+ else {
93
+ const max = sideCount(node.sides);
94
+ for (let v = 1; v <= max; v++) {
95
+ rolls[depth] = v;
96
+ enumerate(depth + 1);
97
+ }
98
+ }
99
+ }
100
+ enumerate(0);
101
+ return dist;
102
+ }
103
+ /** Distribution for exploding dice via iterative depth expansion. */
104
+ function explodingDistribution(node) {
105
+ if (node.sides === "F")
106
+ return null;
107
+ const s = sideCount(node.sides);
108
+ const explodeMod = node.modifiers.find((m) => m.kind === "explode");
109
+ if (!explodeMod)
110
+ return null;
111
+ const threshold = explodeMod.value ?? s;
112
+ const maxExplosions = 10;
113
+ function singleExploding() {
114
+ const dist = new Map();
115
+ const pFace = 1 / s;
116
+ // Build depth-by-depth: accumulated tracks (sum → probability) for chains
117
+ // that haven't terminated yet
118
+ let accumulated = new Map([[0, 1]]);
119
+ for (let depth = 0; depth <= maxExplosions; depth++) {
120
+ const nextAccumulated = new Map();
121
+ for (const [accSum, accProb] of accumulated) {
122
+ for (let face = 1; face <= s; face++) {
123
+ const total = accSum + face;
124
+ const p = accProb * pFace;
125
+ if (face < threshold || depth === maxExplosions) {
126
+ // Terminating roll (or forced cap)
127
+ dist.set(total, (dist.get(total) ?? 0) + p);
128
+ }
129
+ else {
130
+ // Exploding — carry forward
131
+ nextAccumulated.set(total, (nextAccumulated.get(total) ?? 0) + p);
132
+ }
133
+ }
134
+ }
135
+ accumulated = nextAccumulated;
136
+ if (accumulated.size === 0)
137
+ break;
138
+ }
139
+ return dist;
140
+ }
141
+ const single = singleExploding();
142
+ let dist = new Map([[0, 1]]);
143
+ for (let i = 0; i < node.count; i++) {
144
+ dist = convolve(dist, single);
145
+ }
146
+ return dist;
147
+ }
148
+ /** Shift distribution by constant. */
149
+ function shiftDistribution(dist, offset) {
150
+ const result = new Map();
151
+ for (const [v, p] of dist) {
152
+ result.set(v + offset, (result.get(v + offset) ?? 0) + p);
153
+ }
154
+ return result;
155
+ }
156
+ /** Scale distribution by constant. */
157
+ function scaleDistribution(dist, factor) {
158
+ const result = new Map();
159
+ for (const [v, p] of dist) {
160
+ const scaled = factor >= 0 ? v * factor : v * factor;
161
+ result.set(scaled, (result.get(scaled) ?? 0) + p);
162
+ }
163
+ return result;
164
+ }
165
+ /** Floor-divide distribution by constant. */
166
+ function divideDistribution(dist, divisor) {
167
+ if (divisor === 0)
168
+ return new Map([[0, 1]]);
169
+ const result = new Map();
170
+ for (const [v, p] of dist) {
171
+ const divided = Math.floor(v / divisor);
172
+ result.set(divided, (result.get(divided) ?? 0) + p);
173
+ }
174
+ return result;
175
+ }
176
+ /** Compute the full probability distribution for an AST. Falls back to Monte Carlo for complex cases. */
177
+ export function computeDistribution(ast) {
178
+ const result = tryExact(ast);
179
+ if (result)
180
+ return result;
181
+ return monteCarloDistribution(ast);
182
+ }
183
+ function tryExact(node) {
184
+ switch (node.type) {
185
+ case "number":
186
+ return new Map([[node.value, 1]]);
187
+ case "dice": {
188
+ const hasKeepDrop = node.modifiers.some((m) => m.kind === "kh" || m.kind === "kl" || m.kind === "dh" || m.kind === "dl");
189
+ const hasExplode = node.modifiers.some((m) => m.kind === "explode");
190
+ if (hasExplode && !hasKeepDrop) {
191
+ return explodingDistribution(node);
192
+ }
193
+ if (hasKeepDrop) {
194
+ return enumerateKeepDrop(node);
195
+ }
196
+ // Plain NdM — use convolution
197
+ return convolveDice(node.count, node.sides);
198
+ }
199
+ case "binary": {
200
+ const left = tryExact(node.left);
201
+ const right = tryExact(node.right);
202
+ if (!left || !right)
203
+ return null;
204
+ // Optimize: if one side is a constant
205
+ if (right.size === 1) {
206
+ const [rv] = [...right.keys()];
207
+ switch (node.op) {
208
+ case "+":
209
+ return shiftDistribution(left, rv);
210
+ case "-":
211
+ return shiftDistribution(left, -rv);
212
+ case "*":
213
+ return scaleDistribution(left, rv);
214
+ case "/":
215
+ return divideDistribution(left, rv);
216
+ }
217
+ }
218
+ if (left.size === 1) {
219
+ const [lv] = [...left.keys()];
220
+ switch (node.op) {
221
+ case "+":
222
+ return shiftDistribution(right, lv);
223
+ case "-": {
224
+ // lv - right → negate right, shift by lv
225
+ const negated = scaleDistribution(right, -1);
226
+ return shiftDistribution(negated, lv);
227
+ }
228
+ case "*":
229
+ return scaleDistribution(right, lv);
230
+ case "/":
231
+ return null; // constant / distribution is unusual, use MC
232
+ }
233
+ }
234
+ // Both sides are distributions — convolve for +/-, fall back for */÷
235
+ if (node.op === "+")
236
+ return convolve(left, right);
237
+ if (node.op === "-") {
238
+ const negated = scaleDistribution(right, -1);
239
+ return convolve(left, negated);
240
+ }
241
+ // Multiplication/division of two distributions — Monte Carlo
242
+ return null;
243
+ }
244
+ case "unary_minus": {
245
+ const inner = tryExact(node.operand);
246
+ if (!inner)
247
+ return null;
248
+ return scaleDistribution(inner, -1);
249
+ }
250
+ }
251
+ }
252
+ //# sourceMappingURL=distribution.js.map