@yipe/dice 0.2.23 → 0.4.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.
- package/.claude/worktrees/amazing-matsumoto-27220c/LICENSE +21 -0
- package/.claude/worktrees/amazing-matsumoto-27220c/README.md +518 -0
- package/.claude/worktrees/vibrant-lovelace-0cc9e7/LICENSE +21 -0
- package/.claude/worktrees/vibrant-lovelace-0cc9e7/README.md +518 -0
- package/.claude/worktrees/wizardly-mclean-e375de/LICENSE +21 -0
- package/.claude/worktrees/wizardly-mclean-e375de/README.md +518 -0
- package/CHANGELOG.md +224 -0
- package/README.md +17 -0
- package/dist/builder/index.cjs +555 -295
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.d.cts +9 -10
- package/dist/builder/index.d.ts +9 -10
- package/dist/builder/index.js +555 -295
- package/dist/builder/index.js.map +1 -1
- package/dist/index.cjs +619 -256
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -3
- package/dist/index.d.ts +61 -3
- package/dist/index.js +613 -257
- package/dist/index.js.map +1 -1
- package/dist/{pmf-BC1poIqF.d.cts → pmf-D5VRghZI.d.cts} +204 -10
- package/dist/{pmf-BC1poIqF.d.ts → pmf-D5VRghZI.d.ts} +204 -10
- package/package.json +12 -14
package/dist/builder/index.cjs
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
5
|
-
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
6
|
-
|
|
7
3
|
// src/common/lru-cache.ts
|
|
8
4
|
var LRUCache = class {
|
|
9
5
|
constructor(maxSize = 1e3) {
|
|
10
6
|
this.maxSize = maxSize;
|
|
11
|
-
|
|
7
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
12
8
|
}
|
|
13
9
|
get(key) {
|
|
14
10
|
const value = this.cache.get(key);
|
|
@@ -48,19 +44,35 @@ var LRUCache = class {
|
|
|
48
44
|
|
|
49
45
|
// src/common/types.ts
|
|
50
46
|
var EPS = 1e-12;
|
|
47
|
+
var MISS_NONE_OUTCOME = "missNone";
|
|
51
48
|
|
|
52
49
|
// src/pmf/query.ts
|
|
53
50
|
var _DiceQuery = class _DiceQuery {
|
|
54
51
|
constructor(singles, combined, eps = EPS) {
|
|
55
|
-
__publicField(this, "singles");
|
|
56
|
-
__publicField(this, "combined");
|
|
57
|
-
__publicField(this, "_combinedWithAttr");
|
|
58
52
|
this.singles = Array.isArray(singles) ? singles : [singles];
|
|
59
53
|
if (this.singles.some((s) => s === void 0)) {
|
|
60
54
|
throw new Error("DiceQuery contains undefined singles");
|
|
61
55
|
}
|
|
62
|
-
|
|
63
|
-
this.
|
|
56
|
+
this._eps = eps;
|
|
57
|
+
this._combinedProvided = combined !== void 0;
|
|
58
|
+
if (combined !== void 0) {
|
|
59
|
+
this._combined = Math.abs(combined.mass() - 1) <= eps ? combined : combined.normalize();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The combined damage distribution of all single PMFs (their convolution),
|
|
64
|
+
* normalized to total probability 1.
|
|
65
|
+
*
|
|
66
|
+
* Computed lazily on first access and cached. Queries that only need
|
|
67
|
+
* additive statistics — {@link DiceQuery.mean}, {@link DiceQuery.variance},
|
|
68
|
+
* {@link DiceQuery.stddev} — never trigger this convolution.
|
|
69
|
+
*/
|
|
70
|
+
get combined() {
|
|
71
|
+
if (this._combined === void 0) {
|
|
72
|
+
const c = PMF.convolveMany(this.singles);
|
|
73
|
+
this._combined = Math.abs(c.mass() - 1) <= this._eps ? c : c.normalize();
|
|
74
|
+
}
|
|
75
|
+
return this._combined;
|
|
64
76
|
}
|
|
65
77
|
/**
|
|
66
78
|
* Returns a new PMF with damage attribution metadata populated.
|
|
@@ -85,12 +97,39 @@ var _DiceQuery = class _DiceQuery {
|
|
|
85
97
|
if (this._combinedWithAttr) {
|
|
86
98
|
return this._combinedWithAttr;
|
|
87
99
|
}
|
|
100
|
+
if (this.singles.every((pmf) => pmf.hasAttribution())) {
|
|
101
|
+
this._combinedWithAttr = this.combined;
|
|
102
|
+
return this._combinedWithAttr;
|
|
103
|
+
}
|
|
88
104
|
const singlesWithAttr = this.singles.map((pmf) => pmf.withAttribution());
|
|
89
105
|
const combined = PMF.convolveMany(singlesWithAttr, this.combined.epsilon);
|
|
90
106
|
const normalized = Math.abs(combined.mass() - 1) <= this.combined.epsilon ? combined : combined.normalize();
|
|
91
107
|
this._combinedWithAttr = normalized;
|
|
92
108
|
return normalized;
|
|
93
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Per-label `damage value → probability mass` series for the combined,
|
|
112
|
+
* attribution-carrying distribution — the provenance core of the stacked
|
|
113
|
+
* damage-attribution chart. Convenience for
|
|
114
|
+
* `combinedWithAttribution().attributionByValue()`; see
|
|
115
|
+
* {@link PMF.attributionByValue}.
|
|
116
|
+
*/
|
|
117
|
+
attributionByValue() {
|
|
118
|
+
return this.combinedWithAttribution().attributionByValue();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* How many of the independent single PMFs can produce the given outcome
|
|
122
|
+
* label. Useful for "all of them succeeded" style probabilities where the
|
|
123
|
+
* exponent is the number of contributing attacks (see
|
|
124
|
+
* {@link DiceQuery.probExactlyK}).
|
|
125
|
+
*/
|
|
126
|
+
countSinglesWith(label) {
|
|
127
|
+
let count = 0;
|
|
128
|
+
for (const single of this.singles) {
|
|
129
|
+
if (single.hasOutcome(label)) count++;
|
|
130
|
+
}
|
|
131
|
+
return count;
|
|
132
|
+
}
|
|
94
133
|
/**
|
|
95
134
|
* Returns the expected damage across all possible outcomes.
|
|
96
135
|
*
|
|
@@ -98,11 +137,18 @@ var _DiceQuery = class _DiceQuery {
|
|
|
98
137
|
* Use case: "What's my average damage per round?"
|
|
99
138
|
*/
|
|
100
139
|
mean() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
140
|
+
if (this._combinedProvided) {
|
|
141
|
+
let m = 0;
|
|
142
|
+
for (const [damageValue, bin] of this.combined) m += damageValue * bin.p;
|
|
143
|
+
return m;
|
|
144
|
+
}
|
|
145
|
+
let totalMean = 0;
|
|
146
|
+
for (const single of this.singles) {
|
|
147
|
+
const mass = single.mass();
|
|
148
|
+
if (mass <= 0) continue;
|
|
149
|
+
totalMean += Math.abs(mass - 1) <= this._eps ? single.mean() : single.mean() / mass;
|
|
104
150
|
}
|
|
105
|
-
return
|
|
151
|
+
return totalMean;
|
|
106
152
|
}
|
|
107
153
|
/**
|
|
108
154
|
* Returns the variance of the damage distribution.
|
|
@@ -112,13 +158,33 @@ var _DiceQuery = class _DiceQuery {
|
|
|
112
158
|
* High variance means higher risk/reward. Lower variance means more consistent damage.
|
|
113
159
|
*/
|
|
114
160
|
variance() {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
161
|
+
if (this._combinedProvided) {
|
|
162
|
+
const mu = this.mean();
|
|
163
|
+
let v = 0;
|
|
164
|
+
for (const [damageValue, bin] of this.combined) {
|
|
165
|
+
const dev = damageValue - mu;
|
|
166
|
+
v += dev * dev * bin.p;
|
|
167
|
+
}
|
|
168
|
+
return v;
|
|
169
|
+
}
|
|
170
|
+
let totalVariance = 0;
|
|
171
|
+
for (const single of this.singles) {
|
|
172
|
+
const mass = single.mass();
|
|
173
|
+
if (mass <= 0) continue;
|
|
174
|
+
if (Math.abs(mass - 1) <= this._eps) {
|
|
175
|
+
totalVariance += single.variance();
|
|
176
|
+
} else {
|
|
177
|
+
let mu = 0;
|
|
178
|
+
for (const [d2, b] of single) mu += d2 * (b.p / mass);
|
|
179
|
+
let v = 0;
|
|
180
|
+
for (const [d2, b] of single) {
|
|
181
|
+
const dev = d2 - mu;
|
|
182
|
+
v += dev * dev * (b.p / mass);
|
|
183
|
+
}
|
|
184
|
+
totalVariance += v;
|
|
185
|
+
}
|
|
120
186
|
}
|
|
121
|
-
return
|
|
187
|
+
return totalVariance;
|
|
122
188
|
}
|
|
123
189
|
/**
|
|
124
190
|
* Returns the standard deviation of the damage distribution.
|
|
@@ -130,6 +196,10 @@ var _DiceQuery = class _DiceQuery {
|
|
|
130
196
|
stddev() {
|
|
131
197
|
return Math.sqrt(this.variance());
|
|
132
198
|
}
|
|
199
|
+
/** Alias of {@link DiceQuery.stddev}, matching {@link PMF.stdev}. */
|
|
200
|
+
stdev() {
|
|
201
|
+
return this.stddev();
|
|
202
|
+
}
|
|
133
203
|
/**
|
|
134
204
|
* Returns the Cumulative Distribution Function.
|
|
135
205
|
*/
|
|
@@ -220,20 +290,49 @@ var _DiceQuery = class _DiceQuery {
|
|
|
220
290
|
return this.combined.max();
|
|
221
291
|
}
|
|
222
292
|
singleProb(diceIndex, label) {
|
|
293
|
+
const single = this.singles[diceIndex];
|
|
223
294
|
let probabilitySum = 0;
|
|
224
|
-
for (const [, probabilityBin] of
|
|
295
|
+
for (const [, probabilityBin] of single) {
|
|
225
296
|
probabilitySum += probabilityBin.count[label] || 0;
|
|
226
297
|
}
|
|
227
|
-
|
|
298
|
+
const mass = single.mass();
|
|
299
|
+
return mass > 0 ? probabilitySum / mass : 0;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Full count distribution [P(0), P(1), …, P(n)] for "an attack succeeds if it
|
|
303
|
+
* carries ANY of `labels`", over the n independent singles.
|
|
304
|
+
*
|
|
305
|
+
* Each single's per-event success probability is the Poisson-binomial
|
|
306
|
+
* marginal P(≥1 of labels) from {@link probabilityOf} (i.e. probAtLeastOne),
|
|
307
|
+
* computed exactly once. The binomial DP then runs once to produce the whole
|
|
308
|
+
* distribution, so the array-label paths of probExactlyK / probAtLeastK /
|
|
309
|
+
* probAtMostK can slice or sum from it instead of rebuilding a DiceQuery and
|
|
310
|
+
* re-running the DP per requested k.
|
|
311
|
+
*/
|
|
312
|
+
countDistribution(labels) {
|
|
313
|
+
const n = this.singles.length;
|
|
314
|
+
const successProbabilities = this.singles.map(
|
|
315
|
+
(single) => new _DiceQuery([single]).probabilityOf(labels)
|
|
316
|
+
);
|
|
317
|
+
const dist = new Array(n + 1).fill(0);
|
|
318
|
+
dist[0] = 1;
|
|
319
|
+
for (const successProb of successProbabilities) {
|
|
320
|
+
for (let outcomeCount = n; outcomeCount >= 1; outcomeCount--) {
|
|
321
|
+
dist[outcomeCount] = dist[outcomeCount] * (1 - successProb) + dist[outcomeCount - 1] * successProb;
|
|
322
|
+
}
|
|
323
|
+
dist[0] *= 1 - successProb;
|
|
324
|
+
}
|
|
325
|
+
return dist;
|
|
228
326
|
}
|
|
229
327
|
probAtLeastK(labels, k) {
|
|
230
328
|
const L = Array.isArray(labels) ? [...new Set(labels)] : [labels];
|
|
231
329
|
const n = this.singles.length;
|
|
232
330
|
if (k <= 0) return 1;
|
|
233
331
|
if (k > n) return 0;
|
|
332
|
+
const dist = this.countDistribution(L);
|
|
234
333
|
let tail = 0;
|
|
235
334
|
for (let i = k; i <= n; i++) {
|
|
236
|
-
tail +=
|
|
335
|
+
tail += dist[i];
|
|
237
336
|
}
|
|
238
337
|
if (tail < 0) return 0;
|
|
239
338
|
if (tail > 1) return 1;
|
|
@@ -265,9 +364,12 @@ var _DiceQuery = class _DiceQuery {
|
|
|
265
364
|
for (const label of labels) {
|
|
266
365
|
combinedProbability += this.singleProb(diceIndex, label);
|
|
267
366
|
}
|
|
367
|
+
if (combinedProbability < 0) combinedProbability = 0;
|
|
368
|
+
else if (combinedProbability > 1) combinedProbability = 1;
|
|
268
369
|
productOfNonOccurrence *= 1 - combinedProbability;
|
|
269
370
|
}
|
|
270
|
-
|
|
371
|
+
const result = 1 - productOfNonOccurrence;
|
|
372
|
+
return result < 0 ? 0 : result > 1 ? 1 : result;
|
|
271
373
|
}
|
|
272
374
|
/**
|
|
273
375
|
* Computes binomial probabilities for exactly 0, 1, 2, ..., maxK occurrences of a label.
|
|
@@ -308,7 +410,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
308
410
|
* Array examples:
|
|
309
411
|
* - probExactlyK(['hit', 'crit'], 2) = probability exactly 2 attacks succeed
|
|
310
412
|
* - probExactlyK(['hit', 'crit'], 1) = probability exactly 1 attack succeeds
|
|
311
|
-
* - probExactlyK(['
|
|
413
|
+
* - probExactlyK(['missDamage', 'missNone'], 0) = probability no attacks miss
|
|
312
414
|
*
|
|
313
415
|
* Use cases:
|
|
314
416
|
* - "What's the chance exactly one of my attacks hits?"
|
|
@@ -323,19 +425,8 @@ var _DiceQuery = class _DiceQuery {
|
|
|
323
425
|
const probabilityArray = this.computeBinomialProbabilities(labels, k);
|
|
324
426
|
return probabilityArray[k];
|
|
325
427
|
}
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
return singleQuery.probabilityOf(labels);
|
|
329
|
-
});
|
|
330
|
-
const binomialProbs = new Array(k + 1).fill(0);
|
|
331
|
-
binomialProbs[0] = 1;
|
|
332
|
-
for (const successProb of successProbabilities) {
|
|
333
|
-
for (let outcomeCount = k; outcomeCount >= 1; outcomeCount--) {
|
|
334
|
-
binomialProbs[outcomeCount] = binomialProbs[outcomeCount] * (1 - successProb) + binomialProbs[outcomeCount - 1] * successProb;
|
|
335
|
-
}
|
|
336
|
-
binomialProbs[0] *= 1 - successProb;
|
|
337
|
-
}
|
|
338
|
-
return binomialProbs[k];
|
|
428
|
+
const dist = this.countDistribution(labels);
|
|
429
|
+
return k >= 0 && k < dist.length ? dist[k] : 0;
|
|
339
430
|
}
|
|
340
431
|
/**
|
|
341
432
|
* Returns the probability that AT MOST K attacks result in the specified outcome(s).
|
|
@@ -343,7 +434,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
343
434
|
* Single label examples:
|
|
344
435
|
* - probAtMostK('hit', 1) = probability 0 or 1 attacks hit (at most 1)
|
|
345
436
|
* - probAtMostK('crit', 0) = probability no attacks crit
|
|
346
|
-
* - probAtMostK('
|
|
437
|
+
* - probAtMostK('missDamage', 2) = probability at most 2 attacks miss
|
|
347
438
|
*
|
|
348
439
|
* Array examples:
|
|
349
440
|
* - probAtMostK(['hit', 'crit'], 1) = probability at most 1 attack succeeds
|
|
@@ -364,9 +455,11 @@ var _DiceQuery = class _DiceQuery {
|
|
|
364
455
|
}
|
|
365
456
|
return cumulativeSum2;
|
|
366
457
|
}
|
|
458
|
+
const dist = this.countDistribution(labels);
|
|
459
|
+
const upper = Math.min(k, dist.length - 1);
|
|
367
460
|
let cumulativeSum = 0;
|
|
368
|
-
for (let outcomeCount = 0; outcomeCount <=
|
|
369
|
-
cumulativeSum +=
|
|
461
|
+
for (let outcomeCount = 0; outcomeCount <= upper; outcomeCount++) {
|
|
462
|
+
cumulativeSum += dist[outcomeCount];
|
|
370
463
|
}
|
|
371
464
|
return cumulativeSum;
|
|
372
465
|
}
|
|
@@ -412,7 +505,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
412
505
|
*
|
|
413
506
|
* Array examples:
|
|
414
507
|
* - damageStatsFrom(['hit', 'crit']) = damage range when at least one attack succeeds
|
|
415
|
-
* - damageStatsFrom(['
|
|
508
|
+
* - damageStatsFrom(['missDamage', 'missNone']) = damage range when at least one attack misses
|
|
416
509
|
*
|
|
417
510
|
* Tactical Use Cases:
|
|
418
511
|
* - "Given that I don't completely whiff (99% of turns), what damage should I expect?"
|
|
@@ -430,6 +523,12 @@ var _DiceQuery = class _DiceQuery {
|
|
|
430
523
|
* This includes mixed scenarios (2 hits + 1 crit, 3 hits + 1 miss, etc.) which
|
|
431
524
|
* occur far more frequently than pure scenarios. For pure scenarios, use combinedDamageStats.
|
|
432
525
|
*
|
|
526
|
+
* KNOWN LIMITATION (multi-attack, single label): the returned `count` is an
|
|
527
|
+
* EXPECTED COUNT (E[#label], so > 1 for N≥2 attacks, not a probability), and
|
|
528
|
+
* `avg` is the size-biased conditional mean E[dmg·#label]/E[#label] rather than
|
|
529
|
+
* E[dmg | the label occurs]. For a single attack both are the plain
|
|
530
|
+
* conditional figures. Use {@link probAtLeastOne} for the scenario probability.
|
|
531
|
+
*
|
|
433
532
|
* @example
|
|
434
533
|
* // High-level tactical planning
|
|
435
534
|
* const successStats = query.damageStatsFrom('hit')
|
|
@@ -534,7 +633,8 @@ var _DiceQuery = class _DiceQuery {
|
|
|
534
633
|
};
|
|
535
634
|
}
|
|
536
635
|
/**
|
|
537
|
-
* Returns the probability that
|
|
636
|
+
* Returns the probability that at least one attack carries ANY of the
|
|
637
|
+
* specified labels (the marginal P(≥1) across the independent attacks).
|
|
538
638
|
*
|
|
539
639
|
* Examples:
|
|
540
640
|
* - `query.probabilityOf('hit')` → 0.88 (probability at least one hit occurs)
|
|
@@ -543,25 +643,15 @@ var _DiceQuery = class _DiceQuery {
|
|
|
543
643
|
* Use cases:
|
|
544
644
|
* - "What's the chance my resolution includes a success label?"
|
|
545
645
|
* - "How likely am I to get any hits or crits across all attacks?"
|
|
646
|
+
*
|
|
647
|
+
* Note: this must NOT be computed by summing `combined` bin probabilities. A
|
|
648
|
+
* single combined damage total is reachable by many outcome combinations and
|
|
649
|
+
* a bin can hold several labels at once, so summing `bin.p` over bins that
|
|
650
|
+
* contain a label over-counts. The correct marginal is the Poisson-binomial
|
|
651
|
+
* complement over the per-attack probabilities, i.e. {@link probAtLeastOne}.
|
|
546
652
|
*/
|
|
547
653
|
probabilityOf(labels) {
|
|
548
|
-
|
|
549
|
-
labels = [labels];
|
|
550
|
-
}
|
|
551
|
-
let totalProbability = 0;
|
|
552
|
-
for (const [, probabilityBin] of this.combined) {
|
|
553
|
-
let binHasAnyLabel = false;
|
|
554
|
-
for (const label of labels) {
|
|
555
|
-
if (probabilityBin.count[label] && probabilityBin.count[label] > 0) {
|
|
556
|
-
binHasAnyLabel = true;
|
|
557
|
-
break;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
if (binHasAnyLabel) {
|
|
561
|
-
totalProbability += probabilityBin.p;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
return totalProbability;
|
|
654
|
+
return this.probAtLeastOne(labels);
|
|
565
655
|
}
|
|
566
656
|
/**
|
|
567
657
|
* Returns the probability of missing (any type of miss).
|
|
@@ -621,10 +711,6 @@ var _DiceQuery = class _DiceQuery {
|
|
|
621
711
|
*/
|
|
622
712
|
toStackedChartData(labels = [], epsilon = EPS) {
|
|
623
713
|
const damageValues = this.combined.support();
|
|
624
|
-
damageValues.map((dmg) => {
|
|
625
|
-
const bin = this.combined.map.get(dmg);
|
|
626
|
-
return labels.reduce((sum, lab) => sum + (bin.count[lab] || 0), 0);
|
|
627
|
-
});
|
|
628
714
|
const datasets = labels.map((outcomeLabel) => ({
|
|
629
715
|
label: outcomeLabel,
|
|
630
716
|
data: damageValues.map((dmg) => {
|
|
@@ -1061,6 +1147,16 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1061
1147
|
* Snapshot of the distribution in the exact shape the UI consumes.
|
|
1062
1148
|
* - outcome probabilities are "at least one" (and equal to "all" for a single PMF)
|
|
1063
1149
|
* - damageRange is conditional on the outcome occurring
|
|
1150
|
+
*
|
|
1151
|
+
* The outcome probabilities use the correct Poisson-binomial marginals
|
|
1152
|
+
* (`atLeastOneProbability` = P(≥1 attack has it), `allProbability` = P(all do)),
|
|
1153
|
+
* so they are always valid probabilities in [0,1].
|
|
1154
|
+
*
|
|
1155
|
+
* KNOWN LIMITATION (multi-attack): `damageRange.avg` is still aggregated from
|
|
1156
|
+
* the combined PMF's `count`, which the convolution accumulates as an EXPECTED
|
|
1157
|
+
* COUNT, so for N≥2 attacks it is the size-biased mean E[dmg·#label]/E[#label]
|
|
1158
|
+
* rather than a clean conditional expectation. It is correct for a single
|
|
1159
|
+
* attack.
|
|
1064
1160
|
*/
|
|
1065
1161
|
snapshot(order) {
|
|
1066
1162
|
const discovered = /* @__PURE__ */ new Set();
|
|
@@ -1082,10 +1178,8 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1082
1178
|
);
|
|
1083
1179
|
}
|
|
1084
1180
|
const rows = this.toLabeledTable(outcomes);
|
|
1085
|
-
const totals = /* @__PURE__ */ new Map();
|
|
1086
1181
|
const rangeAcc = /* @__PURE__ */ new Map();
|
|
1087
1182
|
for (const ot of outcomes) {
|
|
1088
|
-
totals.set(ot, 0);
|
|
1089
1183
|
rangeAcc.set(ot, { sum: 0, mass: 0 });
|
|
1090
1184
|
}
|
|
1091
1185
|
for (const row of rows) {
|
|
@@ -1093,7 +1187,6 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1093
1187
|
for (const ot of outcomes) {
|
|
1094
1188
|
const p = row[ot] || 0;
|
|
1095
1189
|
if (p <= 0) continue;
|
|
1096
|
-
totals.set(ot, (totals.get(ot) || 0) + p);
|
|
1097
1190
|
const r = rangeAcc.get(ot);
|
|
1098
1191
|
r.sum += dmg * p;
|
|
1099
1192
|
r.mass += p;
|
|
@@ -1101,15 +1194,14 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1101
1194
|
if (r.max === void 0 || dmg > r.max) r.max = dmg;
|
|
1102
1195
|
}
|
|
1103
1196
|
}
|
|
1197
|
+
const n = this.singles.length;
|
|
1104
1198
|
const outcomeMap = /* @__PURE__ */ new Map();
|
|
1105
1199
|
for (const ot of outcomes) {
|
|
1106
|
-
const total = totals.get(ot) || 0;
|
|
1107
1200
|
const r = rangeAcc.get(ot);
|
|
1108
1201
|
const avg = r.mass > 0 ? r.sum / r.mass : 0;
|
|
1109
1202
|
outcomeMap.set(ot, {
|
|
1110
|
-
atLeastOneProbability:
|
|
1111
|
-
allProbability:
|
|
1112
|
-
// single aggregate PMF: same value
|
|
1203
|
+
atLeastOneProbability: this.probAtLeastOne(ot),
|
|
1204
|
+
allProbability: this.probAtLeastK(ot, n),
|
|
1113
1205
|
damageRange: { min: r.min ?? 0, avg, max: r.max ?? 0 }
|
|
1114
1206
|
});
|
|
1115
1207
|
}
|
|
@@ -1294,11 +1386,11 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1294
1386
|
return [a, b, any, none];
|
|
1295
1387
|
}
|
|
1296
1388
|
};
|
|
1297
|
-
|
|
1389
|
+
_DiceQuery.DEFAULT_OUTCOMES = [
|
|
1298
1390
|
"hit",
|
|
1299
1391
|
"crit",
|
|
1300
1392
|
"missNone"
|
|
1301
|
-
]
|
|
1393
|
+
];
|
|
1302
1394
|
var DiceQuery = _DiceQuery;
|
|
1303
1395
|
var pmfCache = new LRUCache(1e3);
|
|
1304
1396
|
var _PMF = class _PMF {
|
|
@@ -1308,14 +1400,6 @@ var _PMF = class _PMF {
|
|
|
1308
1400
|
this.normalized = normalized;
|
|
1309
1401
|
this.identifier = identifier;
|
|
1310
1402
|
this._preservedProvenance = _preservedProvenance;
|
|
1311
|
-
// Cached computed values
|
|
1312
|
-
__publicField(this, "_support");
|
|
1313
|
-
__publicField(this, "_min");
|
|
1314
|
-
__publicField(this, "_max");
|
|
1315
|
-
__publicField(this, "_totalMass");
|
|
1316
|
-
__publicField(this, "_mean");
|
|
1317
|
-
__publicField(this, "_variance");
|
|
1318
|
-
__publicField(this, "_stdev");
|
|
1319
1403
|
}
|
|
1320
1404
|
static empty(epsilon = EPS, identifier = "empty") {
|
|
1321
1405
|
return new _PMF(/* @__PURE__ */ new Map(), epsilon, false, identifier);
|
|
@@ -1329,6 +1413,20 @@ var _PMF = class _PMF {
|
|
|
1329
1413
|
static delta(value, epsilon = EPS) {
|
|
1330
1414
|
return _PMF.fromMap(/* @__PURE__ */ new Map([[value, 1]]), epsilon);
|
|
1331
1415
|
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Point mass at damage 0 tagged with the canonical `missNone` outcome.
|
|
1418
|
+
*
|
|
1419
|
+
* Differs from {@link PMF.zero}, which labels its zero bin `miss` — the
|
|
1420
|
+
* builder's attack-resolution vocabulary. This uses the `missNone`
|
|
1421
|
+
* {@link OutcomeType} that the attribution charts and outcome stats key on,
|
|
1422
|
+
* so it is the correct "clean miss / no damage" delta for provenance-aware
|
|
1423
|
+
* mixtures feeding those consumers.
|
|
1424
|
+
*/
|
|
1425
|
+
static missNone(epsilon = EPS) {
|
|
1426
|
+
const m = /* @__PURE__ */ new Map();
|
|
1427
|
+
m.set(0, { p: 1, count: { [MISS_NONE_OUTCOME]: 1 }, attr: {} });
|
|
1428
|
+
return new _PMF(m, epsilon, false, "missNone");
|
|
1429
|
+
}
|
|
1332
1430
|
// This creates a single bin at value 0, but with weight 0.
|
|
1333
1431
|
static emptyMass() {
|
|
1334
1432
|
return _PMF.zero().scaleMass(0);
|
|
@@ -1355,8 +1453,14 @@ var _PMF = class _PMF {
|
|
|
1355
1453
|
if (p === 1) return successPMF.scaleMass(1);
|
|
1356
1454
|
const eps = successPMF.epsilon ?? failurePMF.epsilon;
|
|
1357
1455
|
const id = `branch(${failurePMF.identifier}*${q.toFixed(6)} + ${successPMF.identifier}*${p.toFixed(6)})`;
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1456
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
1457
|
+
for (const [damageValue, bin] of failurePMF.map) {
|
|
1458
|
+
_PMF.mergeInto(resultMap, damageValue, _PMF.scaleBin(bin, q));
|
|
1459
|
+
}
|
|
1460
|
+
for (const [damageValue, bin] of successPMF.map) {
|
|
1461
|
+
_PMF.mergeInto(resultMap, damageValue, _PMF.scaleBin(bin, p));
|
|
1462
|
+
}
|
|
1463
|
+
return new _PMF(resultMap, eps, false, id);
|
|
1360
1464
|
}
|
|
1361
1465
|
/**
|
|
1362
1466
|
* withProbability()
|
|
@@ -1404,8 +1508,8 @@ var _PMF = class _PMF {
|
|
|
1404
1508
|
* @param fallback PMF to apply when this PMF is *not* selected.
|
|
1405
1509
|
* @returns A new PMF representing the weighted mixture of this PMF and the fallback.
|
|
1406
1510
|
*/
|
|
1407
|
-
gate(p,
|
|
1408
|
-
return _PMF.branch(this,
|
|
1511
|
+
gate(p, fallback) {
|
|
1512
|
+
return _PMF.branch(this, fallback, p);
|
|
1409
1513
|
}
|
|
1410
1514
|
/**
|
|
1411
1515
|
* PMF.exclusive()
|
|
@@ -1488,13 +1592,23 @@ var _PMF = class _PMF {
|
|
|
1488
1592
|
*
|
|
1489
1593
|
* @returns New PMF with attr field populated in each bin
|
|
1490
1594
|
*/
|
|
1491
|
-
|
|
1595
|
+
/**
|
|
1596
|
+
* Returns true if this PMF already carries damage attribution metadata.
|
|
1597
|
+
*
|
|
1598
|
+
* Only the first positive-damage bin is inspected (parser-generated PMFs
|
|
1599
|
+
* populate `attr` uniformly), so this is O(1) in practice.
|
|
1600
|
+
*/
|
|
1601
|
+
hasAttribution() {
|
|
1492
1602
|
for (const [damage, bin] of this.map) {
|
|
1493
1603
|
if (damage !== 0 && bin.attr && Object.keys(bin.attr).length > 0) {
|
|
1494
|
-
return
|
|
1604
|
+
return true;
|
|
1495
1605
|
}
|
|
1496
1606
|
if (damage > 0) break;
|
|
1497
1607
|
}
|
|
1608
|
+
return false;
|
|
1609
|
+
}
|
|
1610
|
+
withAttribution() {
|
|
1611
|
+
if (this.hasAttribution()) return this;
|
|
1498
1612
|
const newMap = /* @__PURE__ */ new Map();
|
|
1499
1613
|
for (const [damage, bin] of this.map) {
|
|
1500
1614
|
const attr = {};
|
|
@@ -1611,7 +1725,7 @@ var _PMF = class _PMF {
|
|
|
1611
1725
|
*/
|
|
1612
1726
|
replicate(n) {
|
|
1613
1727
|
if (!Number.isInteger(n) || n <= 0) {
|
|
1614
|
-
throw new Error("
|
|
1728
|
+
throw new Error("replicate(n): n must be a positive integer");
|
|
1615
1729
|
}
|
|
1616
1730
|
if (n === 1) return [this];
|
|
1617
1731
|
return Array.from({ length: n }, () => this);
|
|
@@ -1643,7 +1757,6 @@ var _PMF = class _PMF {
|
|
|
1643
1757
|
if (normalizationFactor === 0) return this;
|
|
1644
1758
|
const normalizedMap = /* @__PURE__ */ new Map();
|
|
1645
1759
|
for (const [damageValue, probabilityBin] of this.map) {
|
|
1646
|
-
const normalizedProbability = probabilityBin.p / normalizationFactor;
|
|
1647
1760
|
const normalizedCount = {};
|
|
1648
1761
|
for (const labelKey in probabilityBin.count) {
|
|
1649
1762
|
normalizedCount[labelKey] = probabilityBin.count[labelKey] / normalizationFactor;
|
|
@@ -1656,7 +1769,7 @@ var _PMF = class _PMF {
|
|
|
1656
1769
|
}
|
|
1657
1770
|
}
|
|
1658
1771
|
normalizedMap.set(damageValue, {
|
|
1659
|
-
p:
|
|
1772
|
+
p: probabilityBin.p / normalizationFactor,
|
|
1660
1773
|
count: normalizedCount,
|
|
1661
1774
|
attr: normalizedAttributes
|
|
1662
1775
|
});
|
|
@@ -1679,22 +1792,23 @@ var _PMF = class _PMF {
|
|
|
1679
1792
|
for (const [damageValue, probabilityBin] of this.map) {
|
|
1680
1793
|
const shouldKeep = probabilityBin.p >= eps || keepFinalBin && damageValue === maxKey;
|
|
1681
1794
|
if (!shouldKeep) continue;
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1795
|
+
const cleanedBin = _PMF.cloneBin(probabilityBin);
|
|
1796
|
+
for (const labelKey in cleanedBin.count) {
|
|
1797
|
+
if (Math.abs(cleanedBin.count[labelKey] || 0) < eps) {
|
|
1798
|
+
delete cleanedBin.count[labelKey];
|
|
1685
1799
|
}
|
|
1686
1800
|
}
|
|
1687
|
-
if (
|
|
1688
|
-
for (const labelKey in
|
|
1689
|
-
if (Math.abs(
|
|
1690
|
-
delete
|
|
1801
|
+
if (cleanedBin.attr) {
|
|
1802
|
+
for (const labelKey in cleanedBin.attr) {
|
|
1803
|
+
if (Math.abs(cleanedBin.attr[labelKey] || 0) < eps) {
|
|
1804
|
+
delete cleanedBin.attr[labelKey];
|
|
1691
1805
|
}
|
|
1692
1806
|
}
|
|
1693
|
-
if (Object.keys(
|
|
1694
|
-
|
|
1807
|
+
if (Object.keys(cleanedBin.attr).length === 0) {
|
|
1808
|
+
cleanedBin.attr = void 0;
|
|
1695
1809
|
}
|
|
1696
1810
|
}
|
|
1697
|
-
compactedMap.set(damageValue,
|
|
1811
|
+
compactedMap.set(damageValue, cleanedBin);
|
|
1698
1812
|
}
|
|
1699
1813
|
return new _PMF(compactedMap, eps, this.normalized, this.identifier);
|
|
1700
1814
|
}
|
|
@@ -1761,14 +1875,33 @@ var _PMF = class _PMF {
|
|
|
1761
1875
|
}
|
|
1762
1876
|
return this._stdev;
|
|
1763
1877
|
}
|
|
1878
|
+
/** Deep-copies a Bin, cloning its count and (optional) attr maps. */
|
|
1879
|
+
static cloneBin(bin) {
|
|
1880
|
+
return {
|
|
1881
|
+
p: bin.p,
|
|
1882
|
+
count: { ...bin.count },
|
|
1883
|
+
attr: bin.attr ? { ...bin.attr } : void 0
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
/** Returns a new Bin with p, count, and attr all multiplied by `factor`. */
|
|
1887
|
+
static scaleBin(bin, factor) {
|
|
1888
|
+
const count = {};
|
|
1889
|
+
for (const k in bin.count) {
|
|
1890
|
+
count[k] = bin.count[k] * factor;
|
|
1891
|
+
}
|
|
1892
|
+
let attr;
|
|
1893
|
+
if (bin.attr) {
|
|
1894
|
+
attr = {};
|
|
1895
|
+
for (const k in bin.attr) {
|
|
1896
|
+
attr[k] = bin.attr[k] * factor;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
return { p: bin.p * factor, count, attr };
|
|
1900
|
+
}
|
|
1764
1901
|
static mergeInto(destinationMap, damageValue, binToAdd) {
|
|
1765
1902
|
const existingBin = destinationMap.get(damageValue);
|
|
1766
1903
|
if (!existingBin) {
|
|
1767
|
-
destinationMap.set(damageValue,
|
|
1768
|
-
p: binToAdd.p,
|
|
1769
|
-
count: { ...binToAdd.count },
|
|
1770
|
-
attr: binToAdd.attr ? { ...binToAdd.attr } : void 0
|
|
1771
|
-
});
|
|
1904
|
+
destinationMap.set(damageValue, _PMF.cloneBin(binToAdd));
|
|
1772
1905
|
return;
|
|
1773
1906
|
}
|
|
1774
1907
|
existingBin.p += binToAdd.p;
|
|
@@ -1799,29 +1932,14 @@ var _PMF = class _PMF {
|
|
|
1799
1932
|
if (probability === 0) return this;
|
|
1800
1933
|
const resultMap = /* @__PURE__ */ new Map();
|
|
1801
1934
|
for (const [dmg, bin] of this.map) {
|
|
1802
|
-
resultMap.set(dmg,
|
|
1803
|
-
p: bin.p,
|
|
1804
|
-
count: { ...bin.count },
|
|
1805
|
-
attr: bin.attr ? { ...bin.attr } : void 0
|
|
1806
|
-
});
|
|
1935
|
+
resultMap.set(dmg, _PMF.cloneBin(bin));
|
|
1807
1936
|
}
|
|
1808
1937
|
for (const [damageValue, probabilityBin] of branch.map) {
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
if (probabilityBin.attr) {
|
|
1815
|
-
scaledAttributes = {};
|
|
1816
|
-
for (const k in probabilityBin.attr) {
|
|
1817
|
-
scaledAttributes[k] = probability * probabilityBin.attr[k];
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
_PMF.mergeInto(resultMap, damageValue, {
|
|
1821
|
-
p: probability * probabilityBin.p,
|
|
1822
|
-
count: scaledCount,
|
|
1823
|
-
attr: scaledAttributes
|
|
1824
|
-
});
|
|
1938
|
+
_PMF.mergeInto(
|
|
1939
|
+
resultMap,
|
|
1940
|
+
damageValue,
|
|
1941
|
+
_PMF.scaleBin(probabilityBin, probability)
|
|
1942
|
+
);
|
|
1825
1943
|
}
|
|
1826
1944
|
return new _PMF(
|
|
1827
1945
|
resultMap,
|
|
@@ -1830,26 +1948,54 @@ var _PMF = class _PMF {
|
|
|
1830
1948
|
`${this.identifier}+scaled(${branch.identifier},${probability})`
|
|
1831
1949
|
);
|
|
1832
1950
|
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Redistributes probability mass to model an effect that only occurs with
|
|
1953
|
+
* probability `frequency` — a conditional attack, an on-hit rider, or a
|
|
1954
|
+
* sub-one AoE target fraction.
|
|
1955
|
+
*
|
|
1956
|
+
* Every hit outcome (damage > 0) is scaled by `frequency` — probability mass,
|
|
1957
|
+
* per-label `count`, AND per-label `attr` — and the freed mass is moved into
|
|
1958
|
+
* the miss bin at damage 0, tagged with the canonical `missNone` outcome.
|
|
1959
|
+
* Total probability mass is preserved.
|
|
1960
|
+
*
|
|
1961
|
+
* Unlike a bare {@link scaleMass} or {@link mapDamage}, this keeps damage
|
|
1962
|
+
* attribution (`attr`) intact, so a frequency-scaled PMF still renders
|
|
1963
|
+
* correctly in the damage-attribution charts.
|
|
1964
|
+
*
|
|
1965
|
+
* `frequency >= 1` (or non-finite) returns this PMF unchanged; `frequency <= 0`
|
|
1966
|
+
* collapses all mass into the miss bin. The miss outcome is assumed to be
|
|
1967
|
+
* encoded at damage value 0.
|
|
1968
|
+
*
|
|
1969
|
+
* @param frequency Probability in [0, 1] that the effect occurs.
|
|
1970
|
+
*/
|
|
1971
|
+
applyHitFrequency(frequency) {
|
|
1972
|
+
if (!Number.isFinite(frequency) || frequency >= 1) return this;
|
|
1973
|
+
const freq = Math.max(0, frequency);
|
|
1974
|
+
const pMiss = this.pAt(0);
|
|
1975
|
+
const pHit = 1 - pMiss;
|
|
1976
|
+
const newMissMass = pMiss + (1 - freq) * pHit;
|
|
1977
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
1978
|
+
newMap.set(0, {
|
|
1979
|
+
p: newMissMass,
|
|
1980
|
+
count: { [MISS_NONE_OUTCOME]: newMissMass },
|
|
1981
|
+
attr: {}
|
|
1982
|
+
});
|
|
1983
|
+
for (const [damage, bin] of this.map) {
|
|
1984
|
+
if (damage <= 0) continue;
|
|
1985
|
+
newMap.set(damage, _PMF.scaleBin(bin, freq));
|
|
1986
|
+
}
|
|
1987
|
+
return new _PMF(
|
|
1988
|
+
newMap,
|
|
1989
|
+
this.epsilon,
|
|
1990
|
+
false,
|
|
1991
|
+
`freq(${this.identifier},${freq})`
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1833
1994
|
scaleMass(factor) {
|
|
1834
1995
|
if (factor === 1) return this;
|
|
1835
1996
|
const scaledMap = /* @__PURE__ */ new Map();
|
|
1836
1997
|
for (const [damageValue, probabilityBin] of this.map) {
|
|
1837
|
-
|
|
1838
|
-
for (const labelKey in probabilityBin.count) {
|
|
1839
|
-
scaledCount[labelKey] = probabilityBin.count[labelKey] * factor;
|
|
1840
|
-
}
|
|
1841
|
-
let scaledAttributes;
|
|
1842
|
-
if (probabilityBin.attr) {
|
|
1843
|
-
scaledAttributes = {};
|
|
1844
|
-
for (const labelKey in probabilityBin.attr) {
|
|
1845
|
-
scaledAttributes[labelKey] = probabilityBin.attr[labelKey] * factor;
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
scaledMap.set(damageValue, {
|
|
1849
|
-
p: probabilityBin.p * factor,
|
|
1850
|
-
count: scaledCount,
|
|
1851
|
-
attr: scaledAttributes
|
|
1852
|
-
});
|
|
1998
|
+
scaledMap.set(damageValue, _PMF.scaleBin(probabilityBin, factor));
|
|
1853
1999
|
}
|
|
1854
2000
|
return new _PMF(
|
|
1855
2001
|
scaledMap,
|
|
@@ -1862,11 +2008,11 @@ var _PMF = class _PMF {
|
|
|
1862
2008
|
const transformedMap = /* @__PURE__ */ new Map();
|
|
1863
2009
|
for (const [originalDamage, probabilityBin] of this.map) {
|
|
1864
2010
|
const transformedDamage = damageTransformFunction(originalDamage);
|
|
1865
|
-
_PMF.mergeInto(
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2011
|
+
_PMF.mergeInto(
|
|
2012
|
+
transformedMap,
|
|
2013
|
+
transformedDamage,
|
|
2014
|
+
_PMF.cloneBin(probabilityBin)
|
|
2015
|
+
);
|
|
1870
2016
|
}
|
|
1871
2017
|
return new _PMF(
|
|
1872
2018
|
transformedMap,
|
|
@@ -1881,14 +2027,21 @@ var _PMF = class _PMF {
|
|
|
1881
2027
|
}
|
|
1882
2028
|
getPMFCombineCacheKey(p1, p2, eps, raw) {
|
|
1883
2029
|
const [id1, id2] = [p1.identifier, p2.identifier].sort();
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
2030
|
+
return `v4:${raw ? "RAW" : "N"}:${id1}+${id2}@${eps}|${p1.fingerprint()}|${p2.fingerprint()}`;
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* A small content fingerprint (mass + bin count + face sum) so convolution
|
|
2034
|
+
* cache keys change if the underlying numbers do. Memoized because a PMF is
|
|
2035
|
+
* immutable once constructed — this avoids re-summing every key on each
|
|
2036
|
+
* convolve() call (including cache hits).
|
|
2037
|
+
*/
|
|
2038
|
+
fingerprint() {
|
|
2039
|
+
if (this._fingerprint === void 0) {
|
|
1887
2040
|
let faceSum = 0;
|
|
1888
|
-
for (const k of
|
|
1889
|
-
|
|
1890
|
-
}
|
|
1891
|
-
return
|
|
2041
|
+
for (const k of this.map.keys()) faceSum += k;
|
|
2042
|
+
this._fingerprint = `${this.mass().toFixed(12)}|${this.map.size}|${faceSum}`;
|
|
2043
|
+
}
|
|
2044
|
+
return this._fingerprint;
|
|
1892
2045
|
}
|
|
1893
2046
|
convolve(other, eps, raw = false) {
|
|
1894
2047
|
const epsilon = eps ?? this.epsilon;
|
|
@@ -1901,25 +2054,34 @@ var _PMF = class _PMF {
|
|
|
1901
2054
|
if (cached) return cached;
|
|
1902
2055
|
const combinedMap = /* @__PURE__ */ new Map();
|
|
1903
2056
|
for (const [aVal, aBin] of A.map) {
|
|
2057
|
+
const ap = aBin.p;
|
|
2058
|
+
const aCount = aBin.count;
|
|
2059
|
+
const aAttr = aBin.attr;
|
|
1904
2060
|
for (const [bVal, bBin] of B.map) {
|
|
1905
|
-
const
|
|
2061
|
+
const bp = bBin.p;
|
|
1906
2062
|
const dmg = aVal + bVal;
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2063
|
+
let dest = combinedMap.get(dmg);
|
|
2064
|
+
if (dest === void 0) {
|
|
2065
|
+
dest = { p: 0, count: {} };
|
|
2066
|
+
combinedMap.set(dmg, dest);
|
|
2067
|
+
}
|
|
2068
|
+
dest.p += ap * bp;
|
|
2069
|
+
const dc = dest.count;
|
|
2070
|
+
for (const k in aCount) dc[k] = (dc[k] || 0) + aCount[k] * bp;
|
|
1910
2071
|
for (const k in bBin.count)
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2072
|
+
dc[k] = (dc[k] || 0) + bBin.count[k] * ap;
|
|
2073
|
+
if (aAttr || bBin.attr) {
|
|
2074
|
+
let da = dest.attr;
|
|
2075
|
+
if (da === void 0) {
|
|
2076
|
+
da = {};
|
|
2077
|
+
dest.attr = da;
|
|
2078
|
+
}
|
|
2079
|
+
if (aAttr)
|
|
2080
|
+
for (const k in aAttr) da[k] = (da[k] || 0) + aAttr[k] * bp;
|
|
1918
2081
|
if (bBin.attr)
|
|
1919
2082
|
for (const k in bBin.attr)
|
|
1920
|
-
|
|
2083
|
+
da[k] = (da[k] || 0) + bBin.attr[k] * ap;
|
|
1921
2084
|
}
|
|
1922
|
-
_PMF.mergeInto(combinedMap, dmg, { p, count, attr });
|
|
1923
2085
|
}
|
|
1924
2086
|
}
|
|
1925
2087
|
let result = new _PMF(
|
|
@@ -1930,10 +2092,10 @@ var _PMF = class _PMF {
|
|
|
1930
2092
|
);
|
|
1931
2093
|
const mExp = (raw ? A.mass() : 1) * (raw ? B.mass() : 1);
|
|
1932
2094
|
const mGot = result.mass();
|
|
1933
|
-
if (mExp !== 0 && Math.abs(mGot - mExp) > epsilon) {
|
|
2095
|
+
if (mExp !== 0 && mGot !== 0 && Math.abs(mGot - mExp) > epsilon) {
|
|
1934
2096
|
result = result.scaleMass(mExp / mGot);
|
|
1935
2097
|
}
|
|
1936
|
-
if (!raw && Math.abs(result.mass() - 1) > epsilon)
|
|
2098
|
+
if (!raw && mGot !== 0 && Math.abs(result.mass() - 1) > epsilon)
|
|
1937
2099
|
result = result.normalize();
|
|
1938
2100
|
pmfCache?.set(cacheKey, result);
|
|
1939
2101
|
return result;
|
|
@@ -1942,27 +2104,6 @@ var _PMF = class _PMF {
|
|
|
1942
2104
|
combineRaw(other, eps) {
|
|
1943
2105
|
return this.convolve(other, eps, true);
|
|
1944
2106
|
}
|
|
1945
|
-
// Collapse repeated identical PMFs using power() and return a sorted list
|
|
1946
|
-
// Temporarily disabled for now. This was used for a performance optimization, but can lose data provenance.
|
|
1947
|
-
// private static collapseIdentical(pmfList: PMF[], eps: number): PMF[] {
|
|
1948
|
-
// const grouped = new Map<string, { pmf: PMF; count: number }>();
|
|
1949
|
-
// for (const pmf of pmfList) {
|
|
1950
|
-
// const id = pmf.identifier;
|
|
1951
|
-
// const g = grouped.get(id);
|
|
1952
|
-
// if (g) g.count++;
|
|
1953
|
-
// else grouped.set(id, { pmf, count: 1 });
|
|
1954
|
-
// }
|
|
1955
|
-
// if (grouped.size >= pmfList.length) return pmfList;
|
|
1956
|
-
// const collapsed: PMF[] = [];
|
|
1957
|
-
// for (const { pmf, count } of grouped.values()) {
|
|
1958
|
-
// collapsed.push(count > 1 ? pmf.power(count, eps) : pmf);
|
|
1959
|
-
// }
|
|
1960
|
-
// // Stable order for better cache locality
|
|
1961
|
-
// collapsed.sort((a, b) =>
|
|
1962
|
-
// a.identifier < b.identifier ? -1 : a.identifier > b.identifier ? 1 : 0
|
|
1963
|
-
// );
|
|
1964
|
-
// return collapsed;
|
|
1965
|
-
// }
|
|
1966
2107
|
// Reduce a list of PMFs by left-folding convolve() with the given eps
|
|
1967
2108
|
static reduceConvolveLeft(pmfList, eps) {
|
|
1968
2109
|
let result = pmfList[0];
|
|
@@ -1986,12 +2127,23 @@ var _PMF = class _PMF {
|
|
|
1986
2127
|
if (pmfList.length === 1) return pmfList[0];
|
|
1987
2128
|
return _PMF.reduceConvolveLeft(pmfList, eps);
|
|
1988
2129
|
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Returns a plain, JSON-serializable representation of this PMF.
|
|
2132
|
+
*
|
|
2133
|
+
* Follows the standard `toJSON` contract, so `JSON.stringify(pmf)` produces
|
|
2134
|
+
* the expected output (no double-encoding). Use {@link PMF.fromJSON} to
|
|
2135
|
+
* reconstruct, or {@link PMF.toJSONString} if you need the string directly.
|
|
2136
|
+
*/
|
|
1989
2137
|
toJSON() {
|
|
1990
|
-
return
|
|
2138
|
+
return {
|
|
1991
2139
|
bins: [...this.map.entries()],
|
|
1992
2140
|
normalized: this.normalized,
|
|
1993
2141
|
identifier: this.identifier
|
|
1994
|
-
}
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
/** Serializes this PMF to a JSON string (equivalent to `JSON.stringify(pmf)`). */
|
|
2145
|
+
toJSONString() {
|
|
2146
|
+
return JSON.stringify(this);
|
|
1995
2147
|
}
|
|
1996
2148
|
static fromJSON(jsonData) {
|
|
1997
2149
|
return new _PMF(
|
|
@@ -2062,11 +2214,43 @@ var _PMF = class _PMF {
|
|
|
2062
2214
|
}
|
|
2063
2215
|
return new _PMF(prunedMap, epsRel, false, `prune(${this.identifier})`);
|
|
2064
2216
|
}
|
|
2065
|
-
/** NEW - REVIEW IF THESE ARE USEFUL OR DUPLCIATIVE? */
|
|
2066
2217
|
/** Probability mass at exactly x. */
|
|
2067
2218
|
pAt(x) {
|
|
2068
2219
|
return this.map.get(x)?.p ?? 0;
|
|
2069
2220
|
}
|
|
2221
|
+
/**
|
|
2222
|
+
* P(any damage) — the mass on all non-zero outcomes, i.e. `1 - P(0)`.
|
|
2223
|
+
* Assumes a miss is encoded as the damage-0 bin (the convention used across
|
|
2224
|
+
* attack/save PMFs). The dual of {@link missProbability}.
|
|
2225
|
+
*/
|
|
2226
|
+
hitProbability() {
|
|
2227
|
+
return 1 - this.pAt(0);
|
|
2228
|
+
}
|
|
2229
|
+
/** P(no damage) — the mass at damage 0. The dual of {@link hitProbability}. */
|
|
2230
|
+
missProbability() {
|
|
2231
|
+
return this.pAt(0);
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Coarsen the distribution into at most `maxBuckets` contiguous, equal-width
|
|
2235
|
+
* damage buckets, aggregating probability mass (and `count`/`attr`
|
|
2236
|
+
* provenance) into each bucket's start value. Returns this PMF unchanged when
|
|
2237
|
+
* its integer support already fits within `maxBuckets`.
|
|
2238
|
+
*
|
|
2239
|
+
* This is a lossy display/downsampling transform (bucket start replaces the
|
|
2240
|
+
* exact damage value) — use it for charting wide distributions, not for DPR
|
|
2241
|
+
* math.
|
|
2242
|
+
*/
|
|
2243
|
+
rebin(maxBuckets) {
|
|
2244
|
+
if (!(maxBuckets > 0)) return this;
|
|
2245
|
+
const support = this.support();
|
|
2246
|
+
if (support.length === 0) return this;
|
|
2247
|
+
const min = support[0];
|
|
2248
|
+
const max = support[support.length - 1];
|
|
2249
|
+
const range = max - min;
|
|
2250
|
+
if (range + 1 <= maxBuckets) return this;
|
|
2251
|
+
const binSize = Math.ceil((range + 1) / maxBuckets);
|
|
2252
|
+
return this.mapDamage((d2) => min + Math.floor((d2 - min) / binSize) * binSize);
|
|
2253
|
+
}
|
|
2070
2254
|
/** Dense integer support from min..max (inclusive).
|
|
2071
2255
|
* Useful for showing empty bars in charts.
|
|
2072
2256
|
*/
|
|
@@ -2142,11 +2326,60 @@ var _PMF = class _PMF {
|
|
|
2142
2326
|
}
|
|
2143
2327
|
return false;
|
|
2144
2328
|
}
|
|
2329
|
+
/**
|
|
2330
|
+
* Split each damage value's probability mass across outcome labels, returning
|
|
2331
|
+
* per-label maps of `damage value → probability mass attributable to that
|
|
2332
|
+
* label`. Summing over labels at a given value recovers that value's `p`.
|
|
2333
|
+
*
|
|
2334
|
+
* Damage-bearing bins are split by `attr` weight (the share of damage each
|
|
2335
|
+
* outcome contributed); the clean-miss bin at 0 is split by `count` weight
|
|
2336
|
+
* (there is no damage to attribute). Attribution is computed on demand via
|
|
2337
|
+
* {@link withAttribution} when absent, so builder-generated PMFs work too.
|
|
2338
|
+
*
|
|
2339
|
+
* This is the provenance core of the stacked damage-attribution chart — the
|
|
2340
|
+
* caller only maps these series into its rendering format (colors, binning,
|
|
2341
|
+
* axis labels).
|
|
2342
|
+
*/
|
|
2343
|
+
attributionByValue() {
|
|
2344
|
+
const src = this.hasAttribution() ? this : this.withAttribution();
|
|
2345
|
+
const result = /* @__PURE__ */ new Map();
|
|
2346
|
+
const add = (label, damage, mass) => {
|
|
2347
|
+
if (!(mass > 0)) return;
|
|
2348
|
+
let series = result.get(label);
|
|
2349
|
+
if (!series) {
|
|
2350
|
+
series = /* @__PURE__ */ new Map();
|
|
2351
|
+
result.set(label, series);
|
|
2352
|
+
}
|
|
2353
|
+
series.set(damage, (series.get(damage) ?? 0) + mass);
|
|
2354
|
+
};
|
|
2355
|
+
for (const [damage, bin] of src.map) {
|
|
2356
|
+
const p = bin.p || 0;
|
|
2357
|
+
if (p <= 0) continue;
|
|
2358
|
+
const isMissBin = damage === 0;
|
|
2359
|
+
if (isMissBin) {
|
|
2360
|
+
let totalCount = 0;
|
|
2361
|
+
for (const k in bin.count) totalCount += bin.count[k] || 0;
|
|
2362
|
+
if (totalCount > 0) {
|
|
2363
|
+
const c = bin.count[MISS_NONE_OUTCOME] || 0;
|
|
2364
|
+
add(MISS_NONE_OUTCOME, damage, c / totalCount * p);
|
|
2365
|
+
}
|
|
2366
|
+
continue;
|
|
2367
|
+
}
|
|
2368
|
+
let totalAttr = 0;
|
|
2369
|
+
if (bin.attr) for (const k in bin.attr) totalAttr += bin.attr[k] || 0;
|
|
2370
|
+
if (bin.attr && totalAttr > 0) {
|
|
2371
|
+
for (const k in bin.attr) {
|
|
2372
|
+
if (k === MISS_NONE_OUTCOME) continue;
|
|
2373
|
+
add(k, damage, (bin.attr[k] || 0) / totalAttr * p);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
return result;
|
|
2378
|
+
}
|
|
2145
2379
|
tailProbGE(t) {
|
|
2146
2380
|
let s = 0;
|
|
2147
|
-
for (const [x,
|
|
2148
|
-
|
|
2149
|
-
if (p > 0 && x >= t) s += p;
|
|
2381
|
+
for (const [x, bin] of this) {
|
|
2382
|
+
if (bin.p > 0 && x >= t) s += bin.p;
|
|
2150
2383
|
}
|
|
2151
2384
|
return s;
|
|
2152
2385
|
}
|
|
@@ -2207,6 +2440,11 @@ var _PMF = class _PMF {
|
|
|
2207
2440
|
* - pAny: Probability that at least one success occurred
|
|
2208
2441
|
*/
|
|
2209
2442
|
static firstSuccessWeights(pSuccess, pSpecial, n) {
|
|
2443
|
+
if (!Number.isFinite(pSuccess) || !Number.isFinite(pSpecial) || pSuccess < 0 || pSuccess > 1 || pSpecial < 0 || pSpecial - pSuccess > EPS) {
|
|
2444
|
+
throw new Error(
|
|
2445
|
+
`firstSuccessWeights: require 0 <= pSpecial <= pSuccess <= 1 (got pSuccess=${pSuccess}, pSpecial=${pSpecial})`
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2210
2448
|
const pFail = 1 - pSuccess;
|
|
2211
2449
|
const pFailAll = Math.pow(pFail, n);
|
|
2212
2450
|
const pAny = 1 - pFailAll;
|
|
@@ -2222,13 +2460,12 @@ var _PMF = class _PMF {
|
|
|
2222
2460
|
const round = (x) => rounding === "floor" ? Math.floor(x) : rounding === "ceil" ? Math.ceil(x) : rounding === "round" ? Math.round(x) : x;
|
|
2223
2461
|
const probs = /* @__PURE__ */ new Map();
|
|
2224
2462
|
const counts = /* @__PURE__ */ new Map();
|
|
2225
|
-
for (const [v,
|
|
2226
|
-
if (Math.abs(
|
|
2463
|
+
for (const [v, bin] of this) {
|
|
2464
|
+
if (Math.abs(bin.p) < eps) continue;
|
|
2227
2465
|
const u = round(f(v));
|
|
2228
|
-
probs.set(u, (probs.get(u) ?? 0) +
|
|
2466
|
+
probs.set(u, (probs.get(u) ?? 0) + bin.p);
|
|
2229
2467
|
if (preserveCounts) {
|
|
2230
|
-
const
|
|
2231
|
-
const src = typeof rec2 === "number" ? void 0 : rec2?.count;
|
|
2468
|
+
const src = bin.count;
|
|
2232
2469
|
if (src) {
|
|
2233
2470
|
const dest = counts.get(u) ?? {};
|
|
2234
2471
|
for (const k in src) {
|
|
@@ -2281,17 +2518,15 @@ var _PMF = class _PMF {
|
|
|
2281
2518
|
}
|
|
2282
2519
|
};
|
|
2283
2520
|
// Unique ID generator for anonymous PMFs to avoid cache key collisions
|
|
2284
|
-
|
|
2521
|
+
_PMF.__anonIdCounter = 1;
|
|
2285
2522
|
var PMF = _PMF;
|
|
2286
2523
|
|
|
2287
2524
|
// src/pmf/mixture.ts
|
|
2288
2525
|
var Mixture = class _Mixture {
|
|
2289
2526
|
constructor(eps = EPS) {
|
|
2290
|
-
|
|
2527
|
+
this.totals = /* @__PURE__ */ new Map();
|
|
2291
2528
|
// raw mass per outcome (pre-normalization)
|
|
2292
|
-
|
|
2293
|
-
// raw mass per outcome per label
|
|
2294
|
-
__publicField(this, "eps");
|
|
2529
|
+
this.labelMass = /* @__PURE__ */ new Map();
|
|
2295
2530
|
this.eps = Number.isFinite(eps) ? eps : EPS;
|
|
2296
2531
|
}
|
|
2297
2532
|
/** Remove all accumulated state. */
|
|
@@ -2315,9 +2550,8 @@ var Mixture = class _Mixture {
|
|
|
2315
2550
|
*/
|
|
2316
2551
|
add(label, pmf, weight = 1) {
|
|
2317
2552
|
if (!Number.isFinite(weight) || weight <= 0) return this;
|
|
2318
|
-
for (const [v,
|
|
2319
|
-
const
|
|
2320
|
-
const p = isNumber ? binOrNumber : binOrNumber?.p ?? 0;
|
|
2553
|
+
for (const [v, bin] of pmf) {
|
|
2554
|
+
const p = bin.p;
|
|
2321
2555
|
if (p <= 0) continue;
|
|
2322
2556
|
const add = weight * p;
|
|
2323
2557
|
if (!Number.isFinite(add) || Math.abs(add) < this.eps) continue;
|
|
@@ -2405,14 +2639,28 @@ var Mixture = class _Mixture {
|
|
|
2405
2639
|
}
|
|
2406
2640
|
};
|
|
2407
2641
|
|
|
2642
|
+
// src/common/errors.ts
|
|
2643
|
+
var DiceParseError = class _DiceParseError extends Error {
|
|
2644
|
+
constructor(message, options) {
|
|
2645
|
+
super(message);
|
|
2646
|
+
this.name = "DiceParseError";
|
|
2647
|
+
this.expression = options?.expression;
|
|
2648
|
+
this.cause = options?.cause;
|
|
2649
|
+
Object.setPrototypeOf(this, _DiceParseError.prototype);
|
|
2650
|
+
}
|
|
2651
|
+
};
|
|
2652
|
+
|
|
2408
2653
|
// src/parser/dice.ts
|
|
2654
|
+
var MAX_BINARY_OUTCOMES = 1e8;
|
|
2409
2655
|
var Dice = class _Dice {
|
|
2410
2656
|
constructor(x = 0) {
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2657
|
+
this.faces = {};
|
|
2658
|
+
this.privateData = {};
|
|
2659
|
+
// Partial: the object starts empty and gains keys as outcomes are recorded,
|
|
2660
|
+
// so the type must not claim every OutcomeType is present. (Previously typed
|
|
2661
|
+
// as a full Record via an `as` cast, which lied about missing keys.)
|
|
2662
|
+
this.outcomeData = {};
|
|
2663
|
+
this.hasHitDistributionCalculated = false;
|
|
2416
2664
|
if (x <= 0) return;
|
|
2417
2665
|
for (let i = 1; i <= x; i++) {
|
|
2418
2666
|
this.faces[i] = 1;
|
|
@@ -2463,27 +2711,27 @@ var Dice = class _Dice {
|
|
|
2463
2711
|
// TODO this can be private later if we change how testing works
|
|
2464
2712
|
calculateHitDistribution() {
|
|
2465
2713
|
const hitValues = {};
|
|
2714
|
+
const subtractedOutcomes = [
|
|
2715
|
+
this.outcomeData.crit,
|
|
2716
|
+
this.outcomeData.missNone,
|
|
2717
|
+
this.outcomeData.missDamage,
|
|
2718
|
+
this.outcomeData.saveHalf,
|
|
2719
|
+
this.outcomeData.saveFail,
|
|
2720
|
+
this.outcomeData.pc
|
|
2721
|
+
];
|
|
2466
2722
|
for (const [face, totalCount] of Object.entries(this.faces)) {
|
|
2467
2723
|
const numFace = Number(face);
|
|
2468
2724
|
let hitCount = totalCount;
|
|
2469
|
-
for (const
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
"saveHalf",
|
|
2474
|
-
"saveFail",
|
|
2475
|
-
"pc"
|
|
2476
|
-
]) {
|
|
2477
|
-
const distribution = this.getOutcomeDistribution(outcomeType);
|
|
2478
|
-
if (distribution && distribution[numFace]) {
|
|
2479
|
-
hitCount -= distribution[numFace];
|
|
2725
|
+
for (const distribution of subtractedOutcomes) {
|
|
2726
|
+
const outcomeCount = distribution?.[numFace];
|
|
2727
|
+
if (outcomeCount) {
|
|
2728
|
+
hitCount -= outcomeCount;
|
|
2480
2729
|
}
|
|
2481
2730
|
}
|
|
2482
2731
|
if (numFace === 0) {
|
|
2483
2732
|
hitCount = 0;
|
|
2484
2733
|
}
|
|
2485
2734
|
if (hitCount < 0) {
|
|
2486
|
-
console.error("hitCount is <=0?", face, totalCount, hitCount);
|
|
2487
2735
|
hitCount = 0;
|
|
2488
2736
|
}
|
|
2489
2737
|
hitValues[numFace] = hitCount;
|
|
@@ -2502,13 +2750,18 @@ var Dice = class _Dice {
|
|
|
2502
2750
|
const result = diceConstructor ? diceConstructor() : new _Dice();
|
|
2503
2751
|
const isScalar = typeof other === "number";
|
|
2504
2752
|
const keys1 = this.keys();
|
|
2753
|
+
const keys2 = isScalar ? [] : other.keys();
|
|
2754
|
+
if (!isScalar && keys1.length * keys2.length > MAX_BINARY_OUTCOMES) {
|
|
2755
|
+
throw new DiceParseError(
|
|
2756
|
+
`Dice operation over ${keys1.length}\xD7${keys2.length} face pairs exceeds the maximum of ${MAX_BINARY_OUTCOMES}`
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2505
2759
|
for (const key1 of keys1) {
|
|
2506
2760
|
const value1 = this.faces[key1];
|
|
2507
2761
|
if (isScalar) {
|
|
2508
2762
|
const resultKey = op(key1, other);
|
|
2509
2763
|
result.increment(resultKey, value1);
|
|
2510
2764
|
} else {
|
|
2511
|
-
const keys2 = other.keys();
|
|
2512
2765
|
for (const key2 of keys2) {
|
|
2513
2766
|
const value2 = other.faces[key2];
|
|
2514
2767
|
const resultKey = op(key1, key2);
|
|
@@ -2530,7 +2783,7 @@ var Dice = class _Dice {
|
|
|
2530
2783
|
result.outcomeData = { ...this.outcomeData };
|
|
2531
2784
|
return result;
|
|
2532
2785
|
}
|
|
2533
|
-
// PUBLIC
|
|
2786
|
+
// PUBLIC FUNCTIONS
|
|
2534
2787
|
getFaceEntries() {
|
|
2535
2788
|
return Object.entries(this.faces).map(([k, v]) => [Number(k), v]);
|
|
2536
2789
|
}
|
|
@@ -2660,10 +2913,12 @@ var Dice = class _Dice {
|
|
|
2660
2913
|
}
|
|
2661
2914
|
reroll(toReroll) {
|
|
2662
2915
|
const rerollDice = typeof toReroll === "number" ? _Dice.scalar(toReroll) : toReroll;
|
|
2663
|
-
const
|
|
2916
|
+
const rerollKeys = rerollDice.keys();
|
|
2917
|
+
const rerollSet = new Set(rerollKeys);
|
|
2918
|
+
const removed = this.removeFaces(rerollKeys);
|
|
2664
2919
|
let result = new _Dice();
|
|
2665
2920
|
for (const face of this.keys()) {
|
|
2666
|
-
const wasRerolled =
|
|
2921
|
+
const wasRerolled = rerollSet.has(face);
|
|
2667
2922
|
result = result.combine(removed);
|
|
2668
2923
|
if (wasRerolled) {
|
|
2669
2924
|
result = result.combine(this);
|
|
@@ -2731,17 +2986,10 @@ var Dice = class _Dice {
|
|
|
2731
2986
|
const missDistro = this.getOutcomeDistribution("missDamage") || {};
|
|
2732
2987
|
const saveDistro = this.getOutcomeDistribution("saveHalf") || {};
|
|
2733
2988
|
const pcDistro = this.getOutcomeDistribution("pc") || {};
|
|
2734
|
-
|
|
2735
|
-
for (const halfDamage of Object.keys(saveDistro).map(Number)) {
|
|
2736
|
-
const fullDamage = halfDamage * 2;
|
|
2737
|
-
if (fullDamage > 0 && hitDistro[fullDamage]) {
|
|
2738
|
-
isSaveHalf = true;
|
|
2739
|
-
break;
|
|
2740
|
-
}
|
|
2741
|
-
}
|
|
2989
|
+
const isSaveHalf = Object.keys(saveDistro).length > 0;
|
|
2742
2990
|
const isDCCheck = this.privateData.isDCCheck === true;
|
|
2743
2991
|
const clampNonNeg = (x) => x < 0 && x > -1e-15 ? 0 : x;
|
|
2744
|
-
for (const [faceStr, faceCountRaw] of Object.entries(this.
|
|
2992
|
+
for (const [faceStr, faceCountRaw] of Object.entries(this.faces)) {
|
|
2745
2993
|
const face = Number(faceStr);
|
|
2746
2994
|
const faceCount = Number(faceCountRaw);
|
|
2747
2995
|
if (faceCount <= 0) continue;
|
|
@@ -2815,14 +3063,14 @@ var Dice = class _Dice {
|
|
|
2815
3063
|
map.set(face, bin);
|
|
2816
3064
|
}
|
|
2817
3065
|
const identifier = this.identifier || "ERROR";
|
|
2818
|
-
if (identifier === "ERROR") {
|
|
2819
|
-
console.error("Dice identifier is undefined", this);
|
|
2820
|
-
}
|
|
2821
3066
|
return new PMF(map, numEpsilon, true, identifier).compact(numEpsilon, true);
|
|
2822
3067
|
}
|
|
2823
3068
|
};
|
|
2824
3069
|
|
|
2825
3070
|
// src/parser/parser.ts
|
|
3071
|
+
var MAX_DIE_SIDES = 1e6;
|
|
3072
|
+
var MAX_DICE_COUNT = 1e4;
|
|
3073
|
+
var MAX_KEEP_OUTCOMES = 1e6;
|
|
2826
3074
|
var parseCache = new LRUCache(1e3);
|
|
2827
3075
|
function parse(expression, n = 0) {
|
|
2828
3076
|
const cleaned = expression.replace(/ /g, "").toLowerCase();
|
|
@@ -2832,20 +3080,21 @@ function parse(expression, n = 0) {
|
|
|
2832
3080
|
if (cached) return cached;
|
|
2833
3081
|
}
|
|
2834
3082
|
const chars = [...cleaned];
|
|
2835
|
-
let result
|
|
3083
|
+
let result;
|
|
2836
3084
|
try {
|
|
2837
3085
|
result = parseExpression(chars, n);
|
|
2838
3086
|
} catch (error) {
|
|
2839
|
-
throw new
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
result.identifier = cleaned;
|
|
2844
|
-
} catch {
|
|
3087
|
+
throw new DiceParseError(
|
|
3088
|
+
`Cannot parse dice expression [${expression}]: ${error}`,
|
|
3089
|
+
{ expression, cause: error }
|
|
3090
|
+
);
|
|
2845
3091
|
}
|
|
3092
|
+
result.privateData = result.privateData || {};
|
|
3093
|
+
result.identifier = cleaned;
|
|
2846
3094
|
if (chars.length > 0) {
|
|
2847
|
-
throw new
|
|
2848
|
-
`Unexpected token: '${chars[0]}' from expression: '${expression}'
|
|
3095
|
+
throw new DiceParseError(
|
|
3096
|
+
`Unexpected token: '${chars[0]}' from expression: '${expression}'`,
|
|
3097
|
+
{ expression }
|
|
2849
3098
|
);
|
|
2850
3099
|
}
|
|
2851
3100
|
const resultPMF = result.toPMF(-1);
|
|
@@ -2999,7 +3248,7 @@ function multiplyDiceByDice(d1, d2) {
|
|
|
2999
3248
|
if (typeof d1 === "number") d1 = Dice.scalar(d1);
|
|
3000
3249
|
if (typeof d2 === "number") d2 = Dice.scalar(d2);
|
|
3001
3250
|
const result = new Dice();
|
|
3002
|
-
const faces =
|
|
3251
|
+
const faces = /* @__PURE__ */ new Map();
|
|
3003
3252
|
let normalizationFactor = 1;
|
|
3004
3253
|
for (const key of d1.keys()) {
|
|
3005
3254
|
let face;
|
|
@@ -3007,17 +3256,21 @@ function multiplyDiceByDice(d1, d2) {
|
|
|
3007
3256
|
continue;
|
|
3008
3257
|
}
|
|
3009
3258
|
if (d2.privateData.keep) {
|
|
3259
|
+
const faceCount = d2.keys().length;
|
|
3260
|
+
if (Math.pow(faceCount, key) > MAX_KEEP_OUTCOMES) {
|
|
3261
|
+
throw new DiceParseError(
|
|
3262
|
+
`Keep enumeration of ${faceCount}^${key} outcomes exceeds the maximum of ${MAX_KEEP_OUTCOMES}`
|
|
3263
|
+
);
|
|
3264
|
+
}
|
|
3010
3265
|
const repeat = Array(key).fill(d2);
|
|
3011
3266
|
face = opDice(repeat, d2.privateData.keep);
|
|
3012
3267
|
} else {
|
|
3013
3268
|
face = multiplyDice(key, d2);
|
|
3014
3269
|
}
|
|
3015
3270
|
normalizationFactor *= face.total();
|
|
3016
|
-
faces
|
|
3271
|
+
faces.set(key, face);
|
|
3017
3272
|
}
|
|
3018
|
-
for (const
|
|
3019
|
-
const k = parseFloat(key);
|
|
3020
|
-
const face = faces[k];
|
|
3273
|
+
for (const [k, face] of faces) {
|
|
3021
3274
|
const count = d1.get(k);
|
|
3022
3275
|
result.combineInPlace(
|
|
3023
3276
|
face.normalize(count * normalizationFactor / face.total())
|
|
@@ -3027,6 +3280,11 @@ function multiplyDiceByDice(d1, d2) {
|
|
|
3027
3280
|
return result;
|
|
3028
3281
|
}
|
|
3029
3282
|
function multiplyDice(n, d2) {
|
|
3283
|
+
if (n > MAX_DICE_COUNT) {
|
|
3284
|
+
throw new DiceParseError(
|
|
3285
|
+
`Dice count ${n} exceeds the maximum of ${MAX_DICE_COUNT}`
|
|
3286
|
+
);
|
|
3287
|
+
}
|
|
3030
3288
|
if (n === 0) return new Dice(0);
|
|
3031
3289
|
if (n === 1) return d2;
|
|
3032
3290
|
const half = Math.floor(n / 2);
|
|
@@ -3109,9 +3367,14 @@ function parseDice(s, n) {
|
|
|
3109
3367
|
return;
|
|
3110
3368
|
}
|
|
3111
3369
|
const sides = parseNumber(s, n);
|
|
3370
|
+
if (sides > MAX_DIE_SIDES) {
|
|
3371
|
+
throw new DiceParseError(
|
|
3372
|
+
`Die size ${sides} exceeds the maximum of ${MAX_DIE_SIDES}`
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3112
3375
|
let result = new Dice(sides);
|
|
3113
3376
|
if (rerollOne) {
|
|
3114
|
-
result = result.
|
|
3377
|
+
result = result.reroll(1);
|
|
3115
3378
|
}
|
|
3116
3379
|
return result;
|
|
3117
3380
|
}
|
|
@@ -3323,15 +3586,14 @@ var configComplexityScore = (config) => {
|
|
|
3323
3586
|
};
|
|
3324
3587
|
var RollBuilder = class _RollBuilder {
|
|
3325
3588
|
constructor(countOrConfigs = 1) {
|
|
3326
|
-
__publicField(this, "subRollConfigs");
|
|
3327
3589
|
// --- Dice Shortcut Methods ---
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3590
|
+
this.d4 = () => this.d(4);
|
|
3591
|
+
this.d6 = () => this.d(6);
|
|
3592
|
+
this.d8 = () => this.d(8);
|
|
3593
|
+
this.d10 = () => this.d(10);
|
|
3594
|
+
this.d12 = () => this.d(12);
|
|
3595
|
+
this.d20 = () => this.d(20);
|
|
3596
|
+
this.d100 = () => this.d(100);
|
|
3335
3597
|
if (typeof countOrConfigs === "number") {
|
|
3336
3598
|
const count = countOrConfigs;
|
|
3337
3599
|
if (isNaN(count)) throw new Error("Invalid NaN value for count");
|
|
@@ -3838,10 +4100,10 @@ var RollBuilder = class _RollBuilder {
|
|
|
3838
4100
|
}
|
|
3839
4101
|
// These methods are implemented via prototype augmentation in ac.ts and dc.ts
|
|
3840
4102
|
// They are declared here to provide proper TypeScript types
|
|
3841
|
-
ac(
|
|
4103
|
+
ac(_targetAC) {
|
|
3842
4104
|
throw new Error("ac() should be implemented via prototype augmentation");
|
|
3843
4105
|
}
|
|
3844
|
-
dc(
|
|
4106
|
+
dc(_saveDC) {
|
|
3845
4107
|
throw new Error("dc() should be implemented via prototype augmentation");
|
|
3846
4108
|
}
|
|
3847
4109
|
};
|
|
@@ -3958,7 +4220,6 @@ var AlwaysHitBuilder = class _AlwaysHitBuilder extends RollBuilder {
|
|
|
3958
4220
|
);
|
|
3959
4221
|
}
|
|
3960
4222
|
super(baseRoll.getSubRollConfigs());
|
|
3961
|
-
__publicField(this, "attackConfig");
|
|
3962
4223
|
if (attackConfig) {
|
|
3963
4224
|
this.attackConfig = { ...attackConfig };
|
|
3964
4225
|
} else {
|
|
@@ -4008,8 +4269,6 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
|
|
|
4008
4269
|
);
|
|
4009
4270
|
}
|
|
4010
4271
|
super(baseRoll.getSubRollConfigs());
|
|
4011
|
-
__publicField(this, "attackConfig");
|
|
4012
|
-
__publicField(this, "fromAlwaysHit");
|
|
4013
4272
|
if (attackConfig) {
|
|
4014
4273
|
this.attackConfig = { ...attackConfig };
|
|
4015
4274
|
} else {
|
|
@@ -4051,8 +4310,6 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
|
|
|
4051
4310
|
var ParsedRollBuilder = class _ParsedRollBuilder extends RollBuilder {
|
|
4052
4311
|
constructor(expression) {
|
|
4053
4312
|
super([]);
|
|
4054
|
-
__publicField(this, "cachedPMF");
|
|
4055
|
-
__publicField(this, "originalExpression");
|
|
4056
4313
|
this.originalExpression = expression;
|
|
4057
4314
|
this.cachedPMF = parse(expression, 0);
|
|
4058
4315
|
}
|
|
@@ -4302,7 +4559,11 @@ function astFromRollConfigs(configs) {
|
|
|
4302
4559
|
if (trials === 1) {
|
|
4303
4560
|
node = perTrial;
|
|
4304
4561
|
} else {
|
|
4305
|
-
node = {
|
|
4562
|
+
node = {
|
|
4563
|
+
type: "maxOf",
|
|
4564
|
+
count: trials,
|
|
4565
|
+
child: perTrial
|
|
4566
|
+
};
|
|
4306
4567
|
}
|
|
4307
4568
|
} else if (trials === baseCount) {
|
|
4308
4569
|
const base = { type: "sum", count: trials, child: node };
|
|
@@ -4507,7 +4768,7 @@ function computeMaxOfPMF(pmf, count, eps = defaultEps) {
|
|
|
4507
4768
|
const support = pmf.support();
|
|
4508
4769
|
const out = /* @__PURE__ */ new Map();
|
|
4509
4770
|
if (count <= 6 && support.length <= 20) {
|
|
4510
|
-
let
|
|
4771
|
+
let dfs2 = function(rollsLeft, currentMax, probability) {
|
|
4511
4772
|
if (rollsLeft === 0) {
|
|
4512
4773
|
out.set(currentMax, (out.get(currentMax) || 0) + probability);
|
|
4513
4774
|
return;
|
|
@@ -4516,17 +4777,18 @@ function computeMaxOfPMF(pmf, count, eps = defaultEps) {
|
|
|
4516
4777
|
const p = pmf.pAt(value);
|
|
4517
4778
|
if (p > 0) {
|
|
4518
4779
|
const newMax = Math.max(currentMax, value);
|
|
4519
|
-
|
|
4780
|
+
dfs2(rollsLeft - 1, newMax, probability * p);
|
|
4520
4781
|
}
|
|
4521
4782
|
}
|
|
4522
4783
|
};
|
|
4523
|
-
|
|
4784
|
+
dfs2(count, -Infinity, 1);
|
|
4524
4785
|
} else {
|
|
4525
4786
|
const sortedSupport = [...support].sort((a, b) => a - b);
|
|
4787
|
+
let runningCdf = 0;
|
|
4526
4788
|
for (const value of sortedSupport) {
|
|
4527
|
-
const
|
|
4528
|
-
|
|
4529
|
-
const probMax = Math.pow(
|
|
4789
|
+
const prevCdf = runningCdf;
|
|
4790
|
+
runningCdf += pmf.pAt(value);
|
|
4791
|
+
const probMax = Math.pow(runningCdf, count) - Math.pow(prevCdf, count);
|
|
4530
4792
|
if (probMax > eps) {
|
|
4531
4793
|
out.set(value, probMax);
|
|
4532
4794
|
}
|
|
@@ -4553,7 +4815,8 @@ function keepSumPMF(single, total, keep, highest, eps = defaultEps) {
|
|
|
4553
4815
|
}
|
|
4554
4816
|
}
|
|
4555
4817
|
let state = /* @__PURE__ */ new Map();
|
|
4556
|
-
const
|
|
4818
|
+
const stride = total + 1;
|
|
4819
|
+
const keyOf = (used, r) => used * stride + r;
|
|
4557
4820
|
state.set(keyOf(0, total), /* @__PURE__ */ new Map([[0, 1]]));
|
|
4558
4821
|
const valuesDesc = highest ? [...sortedSupport].sort((a, b) => b - a) : [...sortedSupport].sort((a, b) => a - b);
|
|
4559
4822
|
const binomPMF = (r, p) => {
|
|
@@ -4602,9 +4865,8 @@ function keepSumPMF(single, total, keep, highest, eps = defaultEps) {
|
|
|
4602
4865
|
const pCond = Math.min(1, p / q);
|
|
4603
4866
|
const next = /* @__PURE__ */ new Map();
|
|
4604
4867
|
for (const [k, m] of state) {
|
|
4605
|
-
const
|
|
4606
|
-
const
|
|
4607
|
-
const r = parseInt(rStr, 10);
|
|
4868
|
+
const used = Math.floor(k / stride);
|
|
4869
|
+
const r = k - used * stride;
|
|
4608
4870
|
if (r === 0) {
|
|
4609
4871
|
const destKey = keyOf(used, 0);
|
|
4610
4872
|
const dest = next.get(destKey) ?? /* @__PURE__ */ new Map();
|
|
@@ -4775,8 +5037,8 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
4775
5037
|
const bonusPMF2 = bonusDicePMFs2.length ? PMF.convolveMany(bonusDicePMFs2, eps) : PMF.delta(0, eps);
|
|
4776
5038
|
let pcrit2 = 0;
|
|
4777
5039
|
let pmiss2 = 0;
|
|
4778
|
-
for (const [r,
|
|
4779
|
-
const pr =
|
|
5040
|
+
for (const [r, bin] of d202) {
|
|
5041
|
+
const pr = bin.p;
|
|
4780
5042
|
if (pr <= 0) continue;
|
|
4781
5043
|
if (r === 1) {
|
|
4782
5044
|
pmiss2 += pr;
|
|
@@ -4791,8 +5053,8 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
4791
5053
|
}
|
|
4792
5054
|
if (check instanceof AlwaysHitBuilder) {
|
|
4793
5055
|
let pCrit = 0;
|
|
4794
|
-
for (const [r,
|
|
4795
|
-
const pr =
|
|
5056
|
+
for (const [r, bin] of d202) {
|
|
5057
|
+
const pr = bin.p;
|
|
4796
5058
|
if (pr <= 0) continue;
|
|
4797
5059
|
if (r >= critThreshold) pCrit += pr;
|
|
4798
5060
|
}
|
|
@@ -4807,8 +5069,8 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
4807
5069
|
let pcrit = 0;
|
|
4808
5070
|
let phit = 0;
|
|
4809
5071
|
let pmiss = 0;
|
|
4810
|
-
for (const [r,
|
|
4811
|
-
const pr =
|
|
5072
|
+
for (const [r, bin] of d202) {
|
|
5073
|
+
const pr = bin.p;
|
|
4812
5074
|
if (pr <= 0) continue;
|
|
4813
5075
|
if (r === 1) {
|
|
4814
5076
|
pmiss += pr;
|
|
@@ -4888,7 +5150,6 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
4888
5150
|
var ACBuilder = class _ACBuilder extends RollBuilder {
|
|
4889
5151
|
constructor(baseRoll, ac, attackConfig) {
|
|
4890
5152
|
super(baseRoll.getSubRollConfigs());
|
|
4891
|
-
__publicField(this, "attackConfig");
|
|
4892
5153
|
if (attackConfig) {
|
|
4893
5154
|
this.attackConfig = { ...attackConfig, ac };
|
|
4894
5155
|
} else {
|
|
@@ -5017,8 +5278,8 @@ function resolveProbabilities(check) {
|
|
|
5017
5278
|
const baseReroll = check.baseReroll;
|
|
5018
5279
|
const die = d20RollPMF(d20Type, baseReroll > 0);
|
|
5019
5280
|
const faceP = /* @__PURE__ */ new Map();
|
|
5020
|
-
for (const [r,
|
|
5021
|
-
const pr =
|
|
5281
|
+
for (const [r, bin] of die) {
|
|
5282
|
+
const pr = bin.p;
|
|
5022
5283
|
if (pr > 0) faceP.set(r, pr);
|
|
5023
5284
|
}
|
|
5024
5285
|
const eps = 0;
|
|
@@ -5039,7 +5300,6 @@ function resolveProbabilities(check) {
|
|
|
5039
5300
|
var DCBuilder = class _DCBuilder extends RollBuilder {
|
|
5040
5301
|
constructor(baseRoll, saveConfig) {
|
|
5041
5302
|
super(baseRoll.getSubRollConfigs());
|
|
5042
|
-
__publicField(this, "saveConfig");
|
|
5043
5303
|
this.saveConfig = saveConfig ? { ...saveConfig } : { dc: 10 };
|
|
5044
5304
|
}
|
|
5045
5305
|
dc(saveDC) {
|
|
@@ -5088,8 +5348,8 @@ var DCBuilder = class _DCBuilder extends RollBuilder {
|
|
|
5088
5348
|
);
|
|
5089
5349
|
const bonusPMF = bonusDicePMFs.length ? PMF.convolveMany(bonusDicePMFs, eps) : PMF.delta(0, eps);
|
|
5090
5350
|
let psuccess = 0;
|
|
5091
|
-
for (const [r,
|
|
5092
|
-
const pr =
|
|
5351
|
+
for (const [r, bin] of d202) {
|
|
5352
|
+
const pr = bin.p;
|
|
5093
5353
|
if (pr <= 0) continue;
|
|
5094
5354
|
const need = saveDC - staticMod - r;
|
|
5095
5355
|
psuccess += pr * bonusPMF.tailProbGE(need);
|