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