@yipe/dice 0.2.22 → 0.3.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/README.md +17 -0
- package/dist/builder/index.cjs +392 -295
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.d.cts +9 -9
- package/dist/builder/index.d.ts +9 -9
- package/dist/builder/index.js +392 -296
- package/dist/builder/index.js.map +1 -1
- package/dist/index.cjs +356 -256
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.js +356 -257
- package/dist/index.js.map +1 -1
- package/dist/{pmf-BC1poIqF.d.cts → pmf-DqUCnYN9.d.cts} +89 -10
- package/dist/{pmf-BC1poIqF.d.ts → pmf-DqUCnYN9.d.ts} +89 -10
- package/package.json +11 -13
package/dist/index.cjs
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
var
|
|
5
|
-
|
|
3
|
+
// src/common/errors.ts
|
|
4
|
+
var DiceParseError = class _DiceParseError extends Error {
|
|
5
|
+
constructor(message, options) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "DiceParseError";
|
|
8
|
+
this.expression = options?.expression;
|
|
9
|
+
this.cause = options?.cause;
|
|
10
|
+
Object.setPrototypeOf(this, _DiceParseError.prototype);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
6
13
|
|
|
7
14
|
// src/common/lru-cache.ts
|
|
8
15
|
var LRUCache = class {
|
|
9
16
|
constructor(maxSize = 1e3) {
|
|
10
17
|
this.maxSize = maxSize;
|
|
11
|
-
|
|
18
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
12
19
|
}
|
|
13
20
|
get(key) {
|
|
14
21
|
const value = this.cache.get(key);
|
|
@@ -60,15 +67,30 @@ var onPotentCantripOnly = ["pc"];
|
|
|
60
67
|
// src/pmf/query.ts
|
|
61
68
|
var _DiceQuery = class _DiceQuery {
|
|
62
69
|
constructor(singles, combined, eps = EPS) {
|
|
63
|
-
__publicField(this, "singles");
|
|
64
|
-
__publicField(this, "combined");
|
|
65
|
-
__publicField(this, "_combinedWithAttr");
|
|
66
70
|
this.singles = Array.isArray(singles) ? singles : [singles];
|
|
67
71
|
if (this.singles.some((s) => s === void 0)) {
|
|
68
72
|
throw new Error("DiceQuery contains undefined singles");
|
|
69
73
|
}
|
|
70
|
-
|
|
71
|
-
this.
|
|
74
|
+
this._eps = eps;
|
|
75
|
+
this._combinedProvided = combined !== void 0;
|
|
76
|
+
if (combined !== void 0) {
|
|
77
|
+
this._combined = Math.abs(combined.mass() - 1) <= eps ? combined : combined.normalize();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The combined damage distribution of all single PMFs (their convolution),
|
|
82
|
+
* normalized to total probability 1.
|
|
83
|
+
*
|
|
84
|
+
* Computed lazily on first access and cached. Queries that only need
|
|
85
|
+
* additive statistics — {@link DiceQuery.mean}, {@link DiceQuery.variance},
|
|
86
|
+
* {@link DiceQuery.stddev} — never trigger this convolution.
|
|
87
|
+
*/
|
|
88
|
+
get combined() {
|
|
89
|
+
if (this._combined === void 0) {
|
|
90
|
+
const c = PMF.convolveMany(this.singles);
|
|
91
|
+
this._combined = Math.abs(c.mass() - 1) <= this._eps ? c : c.normalize();
|
|
92
|
+
}
|
|
93
|
+
return this._combined;
|
|
72
94
|
}
|
|
73
95
|
/**
|
|
74
96
|
* Returns a new PMF with damage attribution metadata populated.
|
|
@@ -93,6 +115,10 @@ var _DiceQuery = class _DiceQuery {
|
|
|
93
115
|
if (this._combinedWithAttr) {
|
|
94
116
|
return this._combinedWithAttr;
|
|
95
117
|
}
|
|
118
|
+
if (this.singles.every((pmf) => pmf.hasAttribution())) {
|
|
119
|
+
this._combinedWithAttr = this.combined;
|
|
120
|
+
return this._combinedWithAttr;
|
|
121
|
+
}
|
|
96
122
|
const singlesWithAttr = this.singles.map((pmf) => pmf.withAttribution());
|
|
97
123
|
const combined = PMF.convolveMany(singlesWithAttr, this.combined.epsilon);
|
|
98
124
|
const normalized = Math.abs(combined.mass() - 1) <= this.combined.epsilon ? combined : combined.normalize();
|
|
@@ -106,11 +132,18 @@ var _DiceQuery = class _DiceQuery {
|
|
|
106
132
|
* Use case: "What's my average damage per round?"
|
|
107
133
|
*/
|
|
108
134
|
mean() {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
135
|
+
if (this._combinedProvided) {
|
|
136
|
+
let m = 0;
|
|
137
|
+
for (const [damageValue, bin] of this.combined) m += damageValue * bin.p;
|
|
138
|
+
return m;
|
|
139
|
+
}
|
|
140
|
+
let totalMean = 0;
|
|
141
|
+
for (const single of this.singles) {
|
|
142
|
+
const mass = single.mass();
|
|
143
|
+
if (mass <= 0) continue;
|
|
144
|
+
totalMean += Math.abs(mass - 1) <= this._eps ? single.mean() : single.mean() / mass;
|
|
112
145
|
}
|
|
113
|
-
return
|
|
146
|
+
return totalMean;
|
|
114
147
|
}
|
|
115
148
|
/**
|
|
116
149
|
* Returns the variance of the damage distribution.
|
|
@@ -120,13 +153,33 @@ var _DiceQuery = class _DiceQuery {
|
|
|
120
153
|
* High variance means higher risk/reward. Lower variance means more consistent damage.
|
|
121
154
|
*/
|
|
122
155
|
variance() {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
156
|
+
if (this._combinedProvided) {
|
|
157
|
+
const mu = this.mean();
|
|
158
|
+
let v = 0;
|
|
159
|
+
for (const [damageValue, bin] of this.combined) {
|
|
160
|
+
const dev = damageValue - mu;
|
|
161
|
+
v += dev * dev * bin.p;
|
|
162
|
+
}
|
|
163
|
+
return v;
|
|
164
|
+
}
|
|
165
|
+
let totalVariance = 0;
|
|
166
|
+
for (const single of this.singles) {
|
|
167
|
+
const mass = single.mass();
|
|
168
|
+
if (mass <= 0) continue;
|
|
169
|
+
if (Math.abs(mass - 1) <= this._eps) {
|
|
170
|
+
totalVariance += single.variance();
|
|
171
|
+
} else {
|
|
172
|
+
let mu = 0;
|
|
173
|
+
for (const [d, b] of single) mu += d * (b.p / mass);
|
|
174
|
+
let v = 0;
|
|
175
|
+
for (const [d, b] of single) {
|
|
176
|
+
const dev = d - mu;
|
|
177
|
+
v += dev * dev * (b.p / mass);
|
|
178
|
+
}
|
|
179
|
+
totalVariance += v;
|
|
180
|
+
}
|
|
128
181
|
}
|
|
129
|
-
return
|
|
182
|
+
return totalVariance;
|
|
130
183
|
}
|
|
131
184
|
/**
|
|
132
185
|
* Returns the standard deviation of the damage distribution.
|
|
@@ -138,6 +191,10 @@ var _DiceQuery = class _DiceQuery {
|
|
|
138
191
|
stddev() {
|
|
139
192
|
return Math.sqrt(this.variance());
|
|
140
193
|
}
|
|
194
|
+
/** Alias of {@link DiceQuery.stddev}, matching {@link PMF.stdev}. */
|
|
195
|
+
stdev() {
|
|
196
|
+
return this.stddev();
|
|
197
|
+
}
|
|
141
198
|
/**
|
|
142
199
|
* Returns the Cumulative Distribution Function.
|
|
143
200
|
*/
|
|
@@ -228,20 +285,49 @@ var _DiceQuery = class _DiceQuery {
|
|
|
228
285
|
return this.combined.max();
|
|
229
286
|
}
|
|
230
287
|
singleProb(diceIndex, label) {
|
|
288
|
+
const single = this.singles[diceIndex];
|
|
231
289
|
let probabilitySum = 0;
|
|
232
|
-
for (const [, probabilityBin] of
|
|
290
|
+
for (const [, probabilityBin] of single) {
|
|
233
291
|
probabilitySum += probabilityBin.count[label] || 0;
|
|
234
292
|
}
|
|
235
|
-
|
|
293
|
+
const mass = single.mass();
|
|
294
|
+
return mass > 0 ? probabilitySum / mass : 0;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Full count distribution [P(0), P(1), …, P(n)] for "an attack succeeds if it
|
|
298
|
+
* carries ANY of `labels`", over the n independent singles.
|
|
299
|
+
*
|
|
300
|
+
* Each single's per-event success probability is the Poisson-binomial
|
|
301
|
+
* marginal P(≥1 of labels) from {@link probabilityOf} (i.e. probAtLeastOne),
|
|
302
|
+
* computed exactly once. The binomial DP then runs once to produce the whole
|
|
303
|
+
* distribution, so the array-label paths of probExactlyK / probAtLeastK /
|
|
304
|
+
* probAtMostK can slice or sum from it instead of rebuilding a DiceQuery and
|
|
305
|
+
* re-running the DP per requested k.
|
|
306
|
+
*/
|
|
307
|
+
countDistribution(labels) {
|
|
308
|
+
const n = this.singles.length;
|
|
309
|
+
const successProbabilities = this.singles.map(
|
|
310
|
+
(single) => new _DiceQuery([single]).probabilityOf(labels)
|
|
311
|
+
);
|
|
312
|
+
const dist = new Array(n + 1).fill(0);
|
|
313
|
+
dist[0] = 1;
|
|
314
|
+
for (const successProb of successProbabilities) {
|
|
315
|
+
for (let outcomeCount = n; outcomeCount >= 1; outcomeCount--) {
|
|
316
|
+
dist[outcomeCount] = dist[outcomeCount] * (1 - successProb) + dist[outcomeCount - 1] * successProb;
|
|
317
|
+
}
|
|
318
|
+
dist[0] *= 1 - successProb;
|
|
319
|
+
}
|
|
320
|
+
return dist;
|
|
236
321
|
}
|
|
237
322
|
probAtLeastK(labels, k) {
|
|
238
323
|
const L = Array.isArray(labels) ? [...new Set(labels)] : [labels];
|
|
239
324
|
const n = this.singles.length;
|
|
240
325
|
if (k <= 0) return 1;
|
|
241
326
|
if (k > n) return 0;
|
|
327
|
+
const dist = this.countDistribution(L);
|
|
242
328
|
let tail = 0;
|
|
243
329
|
for (let i = k; i <= n; i++) {
|
|
244
|
-
tail +=
|
|
330
|
+
tail += dist[i];
|
|
245
331
|
}
|
|
246
332
|
if (tail < 0) return 0;
|
|
247
333
|
if (tail > 1) return 1;
|
|
@@ -273,9 +359,12 @@ var _DiceQuery = class _DiceQuery {
|
|
|
273
359
|
for (const label of labels) {
|
|
274
360
|
combinedProbability += this.singleProb(diceIndex, label);
|
|
275
361
|
}
|
|
362
|
+
if (combinedProbability < 0) combinedProbability = 0;
|
|
363
|
+
else if (combinedProbability > 1) combinedProbability = 1;
|
|
276
364
|
productOfNonOccurrence *= 1 - combinedProbability;
|
|
277
365
|
}
|
|
278
|
-
|
|
366
|
+
const result = 1 - productOfNonOccurrence;
|
|
367
|
+
return result < 0 ? 0 : result > 1 ? 1 : result;
|
|
279
368
|
}
|
|
280
369
|
/**
|
|
281
370
|
* Computes binomial probabilities for exactly 0, 1, 2, ..., maxK occurrences of a label.
|
|
@@ -316,7 +405,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
316
405
|
* Array examples:
|
|
317
406
|
* - probExactlyK(['hit', 'crit'], 2) = probability exactly 2 attacks succeed
|
|
318
407
|
* - probExactlyK(['hit', 'crit'], 1) = probability exactly 1 attack succeeds
|
|
319
|
-
* - probExactlyK(['
|
|
408
|
+
* - probExactlyK(['missDamage', 'missNone'], 0) = probability no attacks miss
|
|
320
409
|
*
|
|
321
410
|
* Use cases:
|
|
322
411
|
* - "What's the chance exactly one of my attacks hits?"
|
|
@@ -331,19 +420,8 @@ var _DiceQuery = class _DiceQuery {
|
|
|
331
420
|
const probabilityArray = this.computeBinomialProbabilities(labels, k);
|
|
332
421
|
return probabilityArray[k];
|
|
333
422
|
}
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
return singleQuery.probabilityOf(labels);
|
|
337
|
-
});
|
|
338
|
-
const binomialProbs = new Array(k + 1).fill(0);
|
|
339
|
-
binomialProbs[0] = 1;
|
|
340
|
-
for (const successProb of successProbabilities) {
|
|
341
|
-
for (let outcomeCount = k; outcomeCount >= 1; outcomeCount--) {
|
|
342
|
-
binomialProbs[outcomeCount] = binomialProbs[outcomeCount] * (1 - successProb) + binomialProbs[outcomeCount - 1] * successProb;
|
|
343
|
-
}
|
|
344
|
-
binomialProbs[0] *= 1 - successProb;
|
|
345
|
-
}
|
|
346
|
-
return binomialProbs[k];
|
|
423
|
+
const dist = this.countDistribution(labels);
|
|
424
|
+
return k >= 0 && k < dist.length ? dist[k] : 0;
|
|
347
425
|
}
|
|
348
426
|
/**
|
|
349
427
|
* Returns the probability that AT MOST K attacks result in the specified outcome(s).
|
|
@@ -351,7 +429,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
351
429
|
* Single label examples:
|
|
352
430
|
* - probAtMostK('hit', 1) = probability 0 or 1 attacks hit (at most 1)
|
|
353
431
|
* - probAtMostK('crit', 0) = probability no attacks crit
|
|
354
|
-
* - probAtMostK('
|
|
432
|
+
* - probAtMostK('missDamage', 2) = probability at most 2 attacks miss
|
|
355
433
|
*
|
|
356
434
|
* Array examples:
|
|
357
435
|
* - probAtMostK(['hit', 'crit'], 1) = probability at most 1 attack succeeds
|
|
@@ -372,9 +450,11 @@ var _DiceQuery = class _DiceQuery {
|
|
|
372
450
|
}
|
|
373
451
|
return cumulativeSum2;
|
|
374
452
|
}
|
|
453
|
+
const dist = this.countDistribution(labels);
|
|
454
|
+
const upper = Math.min(k, dist.length - 1);
|
|
375
455
|
let cumulativeSum = 0;
|
|
376
|
-
for (let outcomeCount = 0; outcomeCount <=
|
|
377
|
-
cumulativeSum +=
|
|
456
|
+
for (let outcomeCount = 0; outcomeCount <= upper; outcomeCount++) {
|
|
457
|
+
cumulativeSum += dist[outcomeCount];
|
|
378
458
|
}
|
|
379
459
|
return cumulativeSum;
|
|
380
460
|
}
|
|
@@ -420,7 +500,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
420
500
|
*
|
|
421
501
|
* Array examples:
|
|
422
502
|
* - damageStatsFrom(['hit', 'crit']) = damage range when at least one attack succeeds
|
|
423
|
-
* - damageStatsFrom(['
|
|
503
|
+
* - damageStatsFrom(['missDamage', 'missNone']) = damage range when at least one attack misses
|
|
424
504
|
*
|
|
425
505
|
* Tactical Use Cases:
|
|
426
506
|
* - "Given that I don't completely whiff (99% of turns), what damage should I expect?"
|
|
@@ -438,6 +518,12 @@ var _DiceQuery = class _DiceQuery {
|
|
|
438
518
|
* This includes mixed scenarios (2 hits + 1 crit, 3 hits + 1 miss, etc.) which
|
|
439
519
|
* occur far more frequently than pure scenarios. For pure scenarios, use combinedDamageStats.
|
|
440
520
|
*
|
|
521
|
+
* KNOWN LIMITATION (multi-attack, single label): the returned `count` is an
|
|
522
|
+
* EXPECTED COUNT (E[#label], so > 1 for N≥2 attacks, not a probability), and
|
|
523
|
+
* `avg` is the size-biased conditional mean E[dmg·#label]/E[#label] rather than
|
|
524
|
+
* E[dmg | the label occurs]. For a single attack both are the plain
|
|
525
|
+
* conditional figures. Use {@link probAtLeastOne} for the scenario probability.
|
|
526
|
+
*
|
|
441
527
|
* @example
|
|
442
528
|
* // High-level tactical planning
|
|
443
529
|
* const successStats = query.damageStatsFrom('hit')
|
|
@@ -542,7 +628,8 @@ var _DiceQuery = class _DiceQuery {
|
|
|
542
628
|
};
|
|
543
629
|
}
|
|
544
630
|
/**
|
|
545
|
-
* Returns the probability that
|
|
631
|
+
* Returns the probability that at least one attack carries ANY of the
|
|
632
|
+
* specified labels (the marginal P(≥1) across the independent attacks).
|
|
546
633
|
*
|
|
547
634
|
* Examples:
|
|
548
635
|
* - `query.probabilityOf('hit')` → 0.88 (probability at least one hit occurs)
|
|
@@ -551,25 +638,15 @@ var _DiceQuery = class _DiceQuery {
|
|
|
551
638
|
* Use cases:
|
|
552
639
|
* - "What's the chance my resolution includes a success label?"
|
|
553
640
|
* - "How likely am I to get any hits or crits across all attacks?"
|
|
641
|
+
*
|
|
642
|
+
* Note: this must NOT be computed by summing `combined` bin probabilities. A
|
|
643
|
+
* single combined damage total is reachable by many outcome combinations and
|
|
644
|
+
* a bin can hold several labels at once, so summing `bin.p` over bins that
|
|
645
|
+
* contain a label over-counts. The correct marginal is the Poisson-binomial
|
|
646
|
+
* complement over the per-attack probabilities, i.e. {@link probAtLeastOne}.
|
|
554
647
|
*/
|
|
555
648
|
probabilityOf(labels) {
|
|
556
|
-
|
|
557
|
-
labels = [labels];
|
|
558
|
-
}
|
|
559
|
-
let totalProbability = 0;
|
|
560
|
-
for (const [, probabilityBin] of this.combined) {
|
|
561
|
-
let binHasAnyLabel = false;
|
|
562
|
-
for (const label of labels) {
|
|
563
|
-
if (probabilityBin.count[label] && probabilityBin.count[label] > 0) {
|
|
564
|
-
binHasAnyLabel = true;
|
|
565
|
-
break;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
if (binHasAnyLabel) {
|
|
569
|
-
totalProbability += probabilityBin.p;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
return totalProbability;
|
|
649
|
+
return this.probAtLeastOne(labels);
|
|
573
650
|
}
|
|
574
651
|
/**
|
|
575
652
|
* Returns the probability of missing (any type of miss).
|
|
@@ -629,10 +706,6 @@ var _DiceQuery = class _DiceQuery {
|
|
|
629
706
|
*/
|
|
630
707
|
toStackedChartData(labels = [], epsilon = EPS) {
|
|
631
708
|
const damageValues = this.combined.support();
|
|
632
|
-
damageValues.map((dmg) => {
|
|
633
|
-
const bin = this.combined.map.get(dmg);
|
|
634
|
-
return labels.reduce((sum, lab) => sum + (bin.count[lab] || 0), 0);
|
|
635
|
-
});
|
|
636
709
|
const datasets = labels.map((outcomeLabel) => ({
|
|
637
710
|
label: outcomeLabel,
|
|
638
711
|
data: damageValues.map((dmg) => {
|
|
@@ -1069,6 +1142,16 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1069
1142
|
* Snapshot of the distribution in the exact shape the UI consumes.
|
|
1070
1143
|
* - outcome probabilities are "at least one" (and equal to "all" for a single PMF)
|
|
1071
1144
|
* - damageRange is conditional on the outcome occurring
|
|
1145
|
+
*
|
|
1146
|
+
* The outcome probabilities use the correct Poisson-binomial marginals
|
|
1147
|
+
* (`atLeastOneProbability` = P(≥1 attack has it), `allProbability` = P(all do)),
|
|
1148
|
+
* so they are always valid probabilities in [0,1].
|
|
1149
|
+
*
|
|
1150
|
+
* KNOWN LIMITATION (multi-attack): `damageRange.avg` is still aggregated from
|
|
1151
|
+
* the combined PMF's `count`, which the convolution accumulates as an EXPECTED
|
|
1152
|
+
* COUNT, so for N≥2 attacks it is the size-biased mean E[dmg·#label]/E[#label]
|
|
1153
|
+
* rather than a clean conditional expectation. It is correct for a single
|
|
1154
|
+
* attack.
|
|
1072
1155
|
*/
|
|
1073
1156
|
snapshot(order) {
|
|
1074
1157
|
const discovered = /* @__PURE__ */ new Set();
|
|
@@ -1090,10 +1173,8 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1090
1173
|
);
|
|
1091
1174
|
}
|
|
1092
1175
|
const rows = this.toLabeledTable(outcomes);
|
|
1093
|
-
const totals = /* @__PURE__ */ new Map();
|
|
1094
1176
|
const rangeAcc = /* @__PURE__ */ new Map();
|
|
1095
1177
|
for (const ot of outcomes) {
|
|
1096
|
-
totals.set(ot, 0);
|
|
1097
1178
|
rangeAcc.set(ot, { sum: 0, mass: 0 });
|
|
1098
1179
|
}
|
|
1099
1180
|
for (const row of rows) {
|
|
@@ -1101,7 +1182,6 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1101
1182
|
for (const ot of outcomes) {
|
|
1102
1183
|
const p = row[ot] || 0;
|
|
1103
1184
|
if (p <= 0) continue;
|
|
1104
|
-
totals.set(ot, (totals.get(ot) || 0) + p);
|
|
1105
1185
|
const r = rangeAcc.get(ot);
|
|
1106
1186
|
r.sum += dmg * p;
|
|
1107
1187
|
r.mass += p;
|
|
@@ -1109,15 +1189,14 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1109
1189
|
if (r.max === void 0 || dmg > r.max) r.max = dmg;
|
|
1110
1190
|
}
|
|
1111
1191
|
}
|
|
1192
|
+
const n = this.singles.length;
|
|
1112
1193
|
const outcomeMap = /* @__PURE__ */ new Map();
|
|
1113
1194
|
for (const ot of outcomes) {
|
|
1114
|
-
const total = totals.get(ot) || 0;
|
|
1115
1195
|
const r = rangeAcc.get(ot);
|
|
1116
1196
|
const avg = r.mass > 0 ? r.sum / r.mass : 0;
|
|
1117
1197
|
outcomeMap.set(ot, {
|
|
1118
|
-
atLeastOneProbability:
|
|
1119
|
-
allProbability:
|
|
1120
|
-
// single aggregate PMF: same value
|
|
1198
|
+
atLeastOneProbability: this.probAtLeastOne(ot),
|
|
1199
|
+
allProbability: this.probAtLeastK(ot, n),
|
|
1121
1200
|
damageRange: { min: r.min ?? 0, avg, max: r.max ?? 0 }
|
|
1122
1201
|
});
|
|
1123
1202
|
}
|
|
@@ -1302,11 +1381,11 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1302
1381
|
return [a, b, any, none];
|
|
1303
1382
|
}
|
|
1304
1383
|
};
|
|
1305
|
-
|
|
1384
|
+
_DiceQuery.DEFAULT_OUTCOMES = [
|
|
1306
1385
|
"hit",
|
|
1307
1386
|
"crit",
|
|
1308
1387
|
"missNone"
|
|
1309
|
-
]
|
|
1388
|
+
];
|
|
1310
1389
|
var DiceQuery = _DiceQuery;
|
|
1311
1390
|
var pmfCache = new LRUCache(1e3);
|
|
1312
1391
|
var _PMF = class _PMF {
|
|
@@ -1316,14 +1395,6 @@ var _PMF = class _PMF {
|
|
|
1316
1395
|
this.normalized = normalized;
|
|
1317
1396
|
this.identifier = identifier;
|
|
1318
1397
|
this._preservedProvenance = _preservedProvenance;
|
|
1319
|
-
// Cached computed values
|
|
1320
|
-
__publicField(this, "_support");
|
|
1321
|
-
__publicField(this, "_min");
|
|
1322
|
-
__publicField(this, "_max");
|
|
1323
|
-
__publicField(this, "_totalMass");
|
|
1324
|
-
__publicField(this, "_mean");
|
|
1325
|
-
__publicField(this, "_variance");
|
|
1326
|
-
__publicField(this, "_stdev");
|
|
1327
1398
|
}
|
|
1328
1399
|
static empty(epsilon = EPS, identifier = "empty") {
|
|
1329
1400
|
return new _PMF(/* @__PURE__ */ new Map(), epsilon, false, identifier);
|
|
@@ -1363,8 +1434,14 @@ var _PMF = class _PMF {
|
|
|
1363
1434
|
if (p === 1) return successPMF.scaleMass(1);
|
|
1364
1435
|
const eps = successPMF.epsilon ?? failurePMF.epsilon;
|
|
1365
1436
|
const id = `branch(${failurePMF.identifier}*${q.toFixed(6)} + ${successPMF.identifier}*${p.toFixed(6)})`;
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1437
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
1438
|
+
for (const [damageValue, bin] of failurePMF.map) {
|
|
1439
|
+
_PMF.mergeInto(resultMap, damageValue, _PMF.scaleBin(bin, q));
|
|
1440
|
+
}
|
|
1441
|
+
for (const [damageValue, bin] of successPMF.map) {
|
|
1442
|
+
_PMF.mergeInto(resultMap, damageValue, _PMF.scaleBin(bin, p));
|
|
1443
|
+
}
|
|
1444
|
+
return new _PMF(resultMap, eps, false, id);
|
|
1368
1445
|
}
|
|
1369
1446
|
/**
|
|
1370
1447
|
* withProbability()
|
|
@@ -1412,8 +1489,8 @@ var _PMF = class _PMF {
|
|
|
1412
1489
|
* @param fallback PMF to apply when this PMF is *not* selected.
|
|
1413
1490
|
* @returns A new PMF representing the weighted mixture of this PMF and the fallback.
|
|
1414
1491
|
*/
|
|
1415
|
-
gate(p,
|
|
1416
|
-
return _PMF.branch(this,
|
|
1492
|
+
gate(p, fallback) {
|
|
1493
|
+
return _PMF.branch(this, fallback, p);
|
|
1417
1494
|
}
|
|
1418
1495
|
/**
|
|
1419
1496
|
* PMF.exclusive()
|
|
@@ -1496,13 +1573,23 @@ var _PMF = class _PMF {
|
|
|
1496
1573
|
*
|
|
1497
1574
|
* @returns New PMF with attr field populated in each bin
|
|
1498
1575
|
*/
|
|
1499
|
-
|
|
1576
|
+
/**
|
|
1577
|
+
* Returns true if this PMF already carries damage attribution metadata.
|
|
1578
|
+
*
|
|
1579
|
+
* Only the first positive-damage bin is inspected (parser-generated PMFs
|
|
1580
|
+
* populate `attr` uniformly), so this is O(1) in practice.
|
|
1581
|
+
*/
|
|
1582
|
+
hasAttribution() {
|
|
1500
1583
|
for (const [damage, bin] of this.map) {
|
|
1501
1584
|
if (damage !== 0 && bin.attr && Object.keys(bin.attr).length > 0) {
|
|
1502
|
-
return
|
|
1585
|
+
return true;
|
|
1503
1586
|
}
|
|
1504
1587
|
if (damage > 0) break;
|
|
1505
1588
|
}
|
|
1589
|
+
return false;
|
|
1590
|
+
}
|
|
1591
|
+
withAttribution() {
|
|
1592
|
+
if (this.hasAttribution()) return this;
|
|
1506
1593
|
const newMap = /* @__PURE__ */ new Map();
|
|
1507
1594
|
for (const [damage, bin] of this.map) {
|
|
1508
1595
|
const attr = {};
|
|
@@ -1619,7 +1706,7 @@ var _PMF = class _PMF {
|
|
|
1619
1706
|
*/
|
|
1620
1707
|
replicate(n) {
|
|
1621
1708
|
if (!Number.isInteger(n) || n <= 0) {
|
|
1622
|
-
throw new Error("
|
|
1709
|
+
throw new Error("replicate(n): n must be a positive integer");
|
|
1623
1710
|
}
|
|
1624
1711
|
if (n === 1) return [this];
|
|
1625
1712
|
return Array.from({ length: n }, () => this);
|
|
@@ -1651,7 +1738,6 @@ var _PMF = class _PMF {
|
|
|
1651
1738
|
if (normalizationFactor === 0) return this;
|
|
1652
1739
|
const normalizedMap = /* @__PURE__ */ new Map();
|
|
1653
1740
|
for (const [damageValue, probabilityBin] of this.map) {
|
|
1654
|
-
const normalizedProbability = probabilityBin.p / normalizationFactor;
|
|
1655
1741
|
const normalizedCount = {};
|
|
1656
1742
|
for (const labelKey in probabilityBin.count) {
|
|
1657
1743
|
normalizedCount[labelKey] = probabilityBin.count[labelKey] / normalizationFactor;
|
|
@@ -1664,7 +1750,7 @@ var _PMF = class _PMF {
|
|
|
1664
1750
|
}
|
|
1665
1751
|
}
|
|
1666
1752
|
normalizedMap.set(damageValue, {
|
|
1667
|
-
p:
|
|
1753
|
+
p: probabilityBin.p / normalizationFactor,
|
|
1668
1754
|
count: normalizedCount,
|
|
1669
1755
|
attr: normalizedAttributes
|
|
1670
1756
|
});
|
|
@@ -1687,22 +1773,23 @@ var _PMF = class _PMF {
|
|
|
1687
1773
|
for (const [damageValue, probabilityBin] of this.map) {
|
|
1688
1774
|
const shouldKeep = probabilityBin.p >= eps || keepFinalBin && damageValue === maxKey;
|
|
1689
1775
|
if (!shouldKeep) continue;
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1776
|
+
const cleanedBin = _PMF.cloneBin(probabilityBin);
|
|
1777
|
+
for (const labelKey in cleanedBin.count) {
|
|
1778
|
+
if (Math.abs(cleanedBin.count[labelKey] || 0) < eps) {
|
|
1779
|
+
delete cleanedBin.count[labelKey];
|
|
1693
1780
|
}
|
|
1694
1781
|
}
|
|
1695
|
-
if (
|
|
1696
|
-
for (const labelKey in
|
|
1697
|
-
if (Math.abs(
|
|
1698
|
-
delete
|
|
1782
|
+
if (cleanedBin.attr) {
|
|
1783
|
+
for (const labelKey in cleanedBin.attr) {
|
|
1784
|
+
if (Math.abs(cleanedBin.attr[labelKey] || 0) < eps) {
|
|
1785
|
+
delete cleanedBin.attr[labelKey];
|
|
1699
1786
|
}
|
|
1700
1787
|
}
|
|
1701
|
-
if (Object.keys(
|
|
1702
|
-
|
|
1788
|
+
if (Object.keys(cleanedBin.attr).length === 0) {
|
|
1789
|
+
cleanedBin.attr = void 0;
|
|
1703
1790
|
}
|
|
1704
1791
|
}
|
|
1705
|
-
compactedMap.set(damageValue,
|
|
1792
|
+
compactedMap.set(damageValue, cleanedBin);
|
|
1706
1793
|
}
|
|
1707
1794
|
return new _PMF(compactedMap, eps, this.normalized, this.identifier);
|
|
1708
1795
|
}
|
|
@@ -1769,14 +1856,33 @@ var _PMF = class _PMF {
|
|
|
1769
1856
|
}
|
|
1770
1857
|
return this._stdev;
|
|
1771
1858
|
}
|
|
1859
|
+
/** Deep-copies a Bin, cloning its count and (optional) attr maps. */
|
|
1860
|
+
static cloneBin(bin) {
|
|
1861
|
+
return {
|
|
1862
|
+
p: bin.p,
|
|
1863
|
+
count: { ...bin.count },
|
|
1864
|
+
attr: bin.attr ? { ...bin.attr } : void 0
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
/** Returns a new Bin with p, count, and attr all multiplied by `factor`. */
|
|
1868
|
+
static scaleBin(bin, factor) {
|
|
1869
|
+
const count = {};
|
|
1870
|
+
for (const k in bin.count) {
|
|
1871
|
+
count[k] = bin.count[k] * factor;
|
|
1872
|
+
}
|
|
1873
|
+
let attr;
|
|
1874
|
+
if (bin.attr) {
|
|
1875
|
+
attr = {};
|
|
1876
|
+
for (const k in bin.attr) {
|
|
1877
|
+
attr[k] = bin.attr[k] * factor;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return { p: bin.p * factor, count, attr };
|
|
1881
|
+
}
|
|
1772
1882
|
static mergeInto(destinationMap, damageValue, binToAdd) {
|
|
1773
1883
|
const existingBin = destinationMap.get(damageValue);
|
|
1774
1884
|
if (!existingBin) {
|
|
1775
|
-
destinationMap.set(damageValue,
|
|
1776
|
-
p: binToAdd.p,
|
|
1777
|
-
count: { ...binToAdd.count },
|
|
1778
|
-
attr: binToAdd.attr ? { ...binToAdd.attr } : void 0
|
|
1779
|
-
});
|
|
1885
|
+
destinationMap.set(damageValue, _PMF.cloneBin(binToAdd));
|
|
1780
1886
|
return;
|
|
1781
1887
|
}
|
|
1782
1888
|
existingBin.p += binToAdd.p;
|
|
@@ -1807,29 +1913,14 @@ var _PMF = class _PMF {
|
|
|
1807
1913
|
if (probability === 0) return this;
|
|
1808
1914
|
const resultMap = /* @__PURE__ */ new Map();
|
|
1809
1915
|
for (const [dmg, bin] of this.map) {
|
|
1810
|
-
resultMap.set(dmg,
|
|
1811
|
-
p: bin.p,
|
|
1812
|
-
count: { ...bin.count },
|
|
1813
|
-
attr: bin.attr ? { ...bin.attr } : void 0
|
|
1814
|
-
});
|
|
1916
|
+
resultMap.set(dmg, _PMF.cloneBin(bin));
|
|
1815
1917
|
}
|
|
1816
1918
|
for (const [damageValue, probabilityBin] of branch.map) {
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
if (probabilityBin.attr) {
|
|
1823
|
-
scaledAttributes = {};
|
|
1824
|
-
for (const k in probabilityBin.attr) {
|
|
1825
|
-
scaledAttributes[k] = probability * probabilityBin.attr[k];
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
_PMF.mergeInto(resultMap, damageValue, {
|
|
1829
|
-
p: probability * probabilityBin.p,
|
|
1830
|
-
count: scaledCount,
|
|
1831
|
-
attr: scaledAttributes
|
|
1832
|
-
});
|
|
1919
|
+
_PMF.mergeInto(
|
|
1920
|
+
resultMap,
|
|
1921
|
+
damageValue,
|
|
1922
|
+
_PMF.scaleBin(probabilityBin, probability)
|
|
1923
|
+
);
|
|
1833
1924
|
}
|
|
1834
1925
|
return new _PMF(
|
|
1835
1926
|
resultMap,
|
|
@@ -1842,22 +1933,7 @@ var _PMF = class _PMF {
|
|
|
1842
1933
|
if (factor === 1) return this;
|
|
1843
1934
|
const scaledMap = /* @__PURE__ */ new Map();
|
|
1844
1935
|
for (const [damageValue, probabilityBin] of this.map) {
|
|
1845
|
-
|
|
1846
|
-
for (const labelKey in probabilityBin.count) {
|
|
1847
|
-
scaledCount[labelKey] = probabilityBin.count[labelKey] * factor;
|
|
1848
|
-
}
|
|
1849
|
-
let scaledAttributes;
|
|
1850
|
-
if (probabilityBin.attr) {
|
|
1851
|
-
scaledAttributes = {};
|
|
1852
|
-
for (const labelKey in probabilityBin.attr) {
|
|
1853
|
-
scaledAttributes[labelKey] = probabilityBin.attr[labelKey] * factor;
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
scaledMap.set(damageValue, {
|
|
1857
|
-
p: probabilityBin.p * factor,
|
|
1858
|
-
count: scaledCount,
|
|
1859
|
-
attr: scaledAttributes
|
|
1860
|
-
});
|
|
1936
|
+
scaledMap.set(damageValue, _PMF.scaleBin(probabilityBin, factor));
|
|
1861
1937
|
}
|
|
1862
1938
|
return new _PMF(
|
|
1863
1939
|
scaledMap,
|
|
@@ -1870,11 +1946,11 @@ var _PMF = class _PMF {
|
|
|
1870
1946
|
const transformedMap = /* @__PURE__ */ new Map();
|
|
1871
1947
|
for (const [originalDamage, probabilityBin] of this.map) {
|
|
1872
1948
|
const transformedDamage = damageTransformFunction(originalDamage);
|
|
1873
|
-
_PMF.mergeInto(
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1949
|
+
_PMF.mergeInto(
|
|
1950
|
+
transformedMap,
|
|
1951
|
+
transformedDamage,
|
|
1952
|
+
_PMF.cloneBin(probabilityBin)
|
|
1953
|
+
);
|
|
1878
1954
|
}
|
|
1879
1955
|
return new _PMF(
|
|
1880
1956
|
transformedMap,
|
|
@@ -1889,14 +1965,21 @@ var _PMF = class _PMF {
|
|
|
1889
1965
|
}
|
|
1890
1966
|
getPMFCombineCacheKey(p1, p2, eps, raw) {
|
|
1891
1967
|
const [id1, id2] = [p1.identifier, p2.identifier].sort();
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1968
|
+
return `v4:${raw ? "RAW" : "N"}:${id1}+${id2}@${eps}|${p1.fingerprint()}|${p2.fingerprint()}`;
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* A small content fingerprint (mass + bin count + face sum) so convolution
|
|
1972
|
+
* cache keys change if the underlying numbers do. Memoized because a PMF is
|
|
1973
|
+
* immutable once constructed — this avoids re-summing every key on each
|
|
1974
|
+
* convolve() call (including cache hits).
|
|
1975
|
+
*/
|
|
1976
|
+
fingerprint() {
|
|
1977
|
+
if (this._fingerprint === void 0) {
|
|
1895
1978
|
let faceSum = 0;
|
|
1896
|
-
for (const k of
|
|
1897
|
-
|
|
1898
|
-
}
|
|
1899
|
-
return
|
|
1979
|
+
for (const k of this.map.keys()) faceSum += k;
|
|
1980
|
+
this._fingerprint = `${this.mass().toFixed(12)}|${this.map.size}|${faceSum}`;
|
|
1981
|
+
}
|
|
1982
|
+
return this._fingerprint;
|
|
1900
1983
|
}
|
|
1901
1984
|
convolve(other, eps, raw = false) {
|
|
1902
1985
|
const epsilon = eps ?? this.epsilon;
|
|
@@ -1909,25 +1992,34 @@ var _PMF = class _PMF {
|
|
|
1909
1992
|
if (cached) return cached;
|
|
1910
1993
|
const combinedMap = /* @__PURE__ */ new Map();
|
|
1911
1994
|
for (const [aVal, aBin] of A.map) {
|
|
1995
|
+
const ap = aBin.p;
|
|
1996
|
+
const aCount = aBin.count;
|
|
1997
|
+
const aAttr = aBin.attr;
|
|
1912
1998
|
for (const [bVal, bBin] of B.map) {
|
|
1913
|
-
const
|
|
1999
|
+
const bp = bBin.p;
|
|
1914
2000
|
const dmg = aVal + bVal;
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2001
|
+
let dest = combinedMap.get(dmg);
|
|
2002
|
+
if (dest === void 0) {
|
|
2003
|
+
dest = { p: 0, count: {} };
|
|
2004
|
+
combinedMap.set(dmg, dest);
|
|
2005
|
+
}
|
|
2006
|
+
dest.p += ap * bp;
|
|
2007
|
+
const dc = dest.count;
|
|
2008
|
+
for (const k in aCount) dc[k] = (dc[k] || 0) + aCount[k] * bp;
|
|
1918
2009
|
for (const k in bBin.count)
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2010
|
+
dc[k] = (dc[k] || 0) + bBin.count[k] * ap;
|
|
2011
|
+
if (aAttr || bBin.attr) {
|
|
2012
|
+
let da = dest.attr;
|
|
2013
|
+
if (da === void 0) {
|
|
2014
|
+
da = {};
|
|
2015
|
+
dest.attr = da;
|
|
2016
|
+
}
|
|
2017
|
+
if (aAttr)
|
|
2018
|
+
for (const k in aAttr) da[k] = (da[k] || 0) + aAttr[k] * bp;
|
|
1926
2019
|
if (bBin.attr)
|
|
1927
2020
|
for (const k in bBin.attr)
|
|
1928
|
-
|
|
2021
|
+
da[k] = (da[k] || 0) + bBin.attr[k] * ap;
|
|
1929
2022
|
}
|
|
1930
|
-
_PMF.mergeInto(combinedMap, dmg, { p, count, attr });
|
|
1931
2023
|
}
|
|
1932
2024
|
}
|
|
1933
2025
|
let result = new _PMF(
|
|
@@ -1938,10 +2030,10 @@ var _PMF = class _PMF {
|
|
|
1938
2030
|
);
|
|
1939
2031
|
const mExp = (raw ? A.mass() : 1) * (raw ? B.mass() : 1);
|
|
1940
2032
|
const mGot = result.mass();
|
|
1941
|
-
if (mExp !== 0 && Math.abs(mGot - mExp) > epsilon) {
|
|
2033
|
+
if (mExp !== 0 && mGot !== 0 && Math.abs(mGot - mExp) > epsilon) {
|
|
1942
2034
|
result = result.scaleMass(mExp / mGot);
|
|
1943
2035
|
}
|
|
1944
|
-
if (!raw && Math.abs(result.mass() - 1) > epsilon)
|
|
2036
|
+
if (!raw && mGot !== 0 && Math.abs(result.mass() - 1) > epsilon)
|
|
1945
2037
|
result = result.normalize();
|
|
1946
2038
|
pmfCache?.set(cacheKey, result);
|
|
1947
2039
|
return result;
|
|
@@ -1950,27 +2042,6 @@ var _PMF = class _PMF {
|
|
|
1950
2042
|
combineRaw(other, eps) {
|
|
1951
2043
|
return this.convolve(other, eps, true);
|
|
1952
2044
|
}
|
|
1953
|
-
// Collapse repeated identical PMFs using power() and return a sorted list
|
|
1954
|
-
// Temporarily disabled for now. This was used for a performance optimization, but can lose data provenance.
|
|
1955
|
-
// private static collapseIdentical(pmfList: PMF[], eps: number): PMF[] {
|
|
1956
|
-
// const grouped = new Map<string, { pmf: PMF; count: number }>();
|
|
1957
|
-
// for (const pmf of pmfList) {
|
|
1958
|
-
// const id = pmf.identifier;
|
|
1959
|
-
// const g = grouped.get(id);
|
|
1960
|
-
// if (g) g.count++;
|
|
1961
|
-
// else grouped.set(id, { pmf, count: 1 });
|
|
1962
|
-
// }
|
|
1963
|
-
// if (grouped.size >= pmfList.length) return pmfList;
|
|
1964
|
-
// const collapsed: PMF[] = [];
|
|
1965
|
-
// for (const { pmf, count } of grouped.values()) {
|
|
1966
|
-
// collapsed.push(count > 1 ? pmf.power(count, eps) : pmf);
|
|
1967
|
-
// }
|
|
1968
|
-
// // Stable order for better cache locality
|
|
1969
|
-
// collapsed.sort((a, b) =>
|
|
1970
|
-
// a.identifier < b.identifier ? -1 : a.identifier > b.identifier ? 1 : 0
|
|
1971
|
-
// );
|
|
1972
|
-
// return collapsed;
|
|
1973
|
-
// }
|
|
1974
2045
|
// Reduce a list of PMFs by left-folding convolve() with the given eps
|
|
1975
2046
|
static reduceConvolveLeft(pmfList, eps) {
|
|
1976
2047
|
let result = pmfList[0];
|
|
@@ -1994,12 +2065,23 @@ var _PMF = class _PMF {
|
|
|
1994
2065
|
if (pmfList.length === 1) return pmfList[0];
|
|
1995
2066
|
return _PMF.reduceConvolveLeft(pmfList, eps);
|
|
1996
2067
|
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Returns a plain, JSON-serializable representation of this PMF.
|
|
2070
|
+
*
|
|
2071
|
+
* Follows the standard `toJSON` contract, so `JSON.stringify(pmf)` produces
|
|
2072
|
+
* the expected output (no double-encoding). Use {@link PMF.fromJSON} to
|
|
2073
|
+
* reconstruct, or {@link PMF.toJSONString} if you need the string directly.
|
|
2074
|
+
*/
|
|
1997
2075
|
toJSON() {
|
|
1998
|
-
return
|
|
2076
|
+
return {
|
|
1999
2077
|
bins: [...this.map.entries()],
|
|
2000
2078
|
normalized: this.normalized,
|
|
2001
2079
|
identifier: this.identifier
|
|
2002
|
-
}
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
/** Serializes this PMF to a JSON string (equivalent to `JSON.stringify(pmf)`). */
|
|
2083
|
+
toJSONString() {
|
|
2084
|
+
return JSON.stringify(this);
|
|
2003
2085
|
}
|
|
2004
2086
|
static fromJSON(jsonData) {
|
|
2005
2087
|
return new _PMF(
|
|
@@ -2070,7 +2152,6 @@ var _PMF = class _PMF {
|
|
|
2070
2152
|
}
|
|
2071
2153
|
return new _PMF(prunedMap, epsRel, false, `prune(${this.identifier})`);
|
|
2072
2154
|
}
|
|
2073
|
-
/** NEW - REVIEW IF THESE ARE USEFUL OR DUPLCIATIVE? */
|
|
2074
2155
|
/** Probability mass at exactly x. */
|
|
2075
2156
|
pAt(x) {
|
|
2076
2157
|
return this.map.get(x)?.p ?? 0;
|
|
@@ -2152,9 +2233,8 @@ var _PMF = class _PMF {
|
|
|
2152
2233
|
}
|
|
2153
2234
|
tailProbGE(t) {
|
|
2154
2235
|
let s = 0;
|
|
2155
|
-
for (const [x,
|
|
2156
|
-
|
|
2157
|
-
if (p > 0 && x >= t) s += p;
|
|
2236
|
+
for (const [x, bin] of this) {
|
|
2237
|
+
if (bin.p > 0 && x >= t) s += bin.p;
|
|
2158
2238
|
}
|
|
2159
2239
|
return s;
|
|
2160
2240
|
}
|
|
@@ -2215,6 +2295,11 @@ var _PMF = class _PMF {
|
|
|
2215
2295
|
* - pAny: Probability that at least one success occurred
|
|
2216
2296
|
*/
|
|
2217
2297
|
static firstSuccessWeights(pSuccess, pSpecial, n) {
|
|
2298
|
+
if (!Number.isFinite(pSuccess) || !Number.isFinite(pSpecial) || pSuccess < 0 || pSuccess > 1 || pSpecial < 0 || pSpecial - pSuccess > EPS) {
|
|
2299
|
+
throw new Error(
|
|
2300
|
+
`firstSuccessWeights: require 0 <= pSpecial <= pSuccess <= 1 (got pSuccess=${pSuccess}, pSpecial=${pSpecial})`
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2218
2303
|
const pFail = 1 - pSuccess;
|
|
2219
2304
|
const pFailAll = Math.pow(pFail, n);
|
|
2220
2305
|
const pAny = 1 - pFailAll;
|
|
@@ -2230,13 +2315,12 @@ var _PMF = class _PMF {
|
|
|
2230
2315
|
const round = (x) => rounding === "floor" ? Math.floor(x) : rounding === "ceil" ? Math.ceil(x) : rounding === "round" ? Math.round(x) : x;
|
|
2231
2316
|
const probs = /* @__PURE__ */ new Map();
|
|
2232
2317
|
const counts = /* @__PURE__ */ new Map();
|
|
2233
|
-
for (const [v,
|
|
2234
|
-
if (Math.abs(
|
|
2318
|
+
for (const [v, bin] of this) {
|
|
2319
|
+
if (Math.abs(bin.p) < eps) continue;
|
|
2235
2320
|
const u = round(f(v));
|
|
2236
|
-
probs.set(u, (probs.get(u) ?? 0) +
|
|
2321
|
+
probs.set(u, (probs.get(u) ?? 0) + bin.p);
|
|
2237
2322
|
if (preserveCounts) {
|
|
2238
|
-
const
|
|
2239
|
-
const src = typeof rec2 === "number" ? void 0 : rec2?.count;
|
|
2323
|
+
const src = bin.count;
|
|
2240
2324
|
if (src) {
|
|
2241
2325
|
const dest = counts.get(u) ?? {};
|
|
2242
2326
|
for (const k in src) {
|
|
@@ -2289,17 +2373,20 @@ var _PMF = class _PMF {
|
|
|
2289
2373
|
}
|
|
2290
2374
|
};
|
|
2291
2375
|
// Unique ID generator for anonymous PMFs to avoid cache key collisions
|
|
2292
|
-
|
|
2376
|
+
_PMF.__anonIdCounter = 1;
|
|
2293
2377
|
var PMF = _PMF;
|
|
2294
2378
|
|
|
2295
2379
|
// src/parser/dice.ts
|
|
2380
|
+
var MAX_BINARY_OUTCOMES = 1e8;
|
|
2296
2381
|
var Dice = class _Dice {
|
|
2297
2382
|
constructor(x = 0) {
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2383
|
+
this.faces = {};
|
|
2384
|
+
this.privateData = {};
|
|
2385
|
+
// Partial: the object starts empty and gains keys as outcomes are recorded,
|
|
2386
|
+
// so the type must not claim every OutcomeType is present. (Previously typed
|
|
2387
|
+
// as a full Record via an `as` cast, which lied about missing keys.)
|
|
2388
|
+
this.outcomeData = {};
|
|
2389
|
+
this.hasHitDistributionCalculated = false;
|
|
2303
2390
|
if (x <= 0) return;
|
|
2304
2391
|
for (let i = 1; i <= x; i++) {
|
|
2305
2392
|
this.faces[i] = 1;
|
|
@@ -2350,27 +2437,27 @@ var Dice = class _Dice {
|
|
|
2350
2437
|
// TODO this can be private later if we change how testing works
|
|
2351
2438
|
calculateHitDistribution() {
|
|
2352
2439
|
const hitValues = {};
|
|
2440
|
+
const subtractedOutcomes = [
|
|
2441
|
+
this.outcomeData.crit,
|
|
2442
|
+
this.outcomeData.missNone,
|
|
2443
|
+
this.outcomeData.missDamage,
|
|
2444
|
+
this.outcomeData.saveHalf,
|
|
2445
|
+
this.outcomeData.saveFail,
|
|
2446
|
+
this.outcomeData.pc
|
|
2447
|
+
];
|
|
2353
2448
|
for (const [face, totalCount] of Object.entries(this.faces)) {
|
|
2354
2449
|
const numFace = Number(face);
|
|
2355
2450
|
let hitCount = totalCount;
|
|
2356
|
-
for (const
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
"saveHalf",
|
|
2361
|
-
"saveFail",
|
|
2362
|
-
"pc"
|
|
2363
|
-
]) {
|
|
2364
|
-
const distribution = this.getOutcomeDistribution(outcomeType);
|
|
2365
|
-
if (distribution && distribution[numFace]) {
|
|
2366
|
-
hitCount -= distribution[numFace];
|
|
2451
|
+
for (const distribution of subtractedOutcomes) {
|
|
2452
|
+
const outcomeCount = distribution?.[numFace];
|
|
2453
|
+
if (outcomeCount) {
|
|
2454
|
+
hitCount -= outcomeCount;
|
|
2367
2455
|
}
|
|
2368
2456
|
}
|
|
2369
2457
|
if (numFace === 0) {
|
|
2370
2458
|
hitCount = 0;
|
|
2371
2459
|
}
|
|
2372
2460
|
if (hitCount < 0) {
|
|
2373
|
-
console.error("hitCount is <=0?", face, totalCount, hitCount);
|
|
2374
2461
|
hitCount = 0;
|
|
2375
2462
|
}
|
|
2376
2463
|
hitValues[numFace] = hitCount;
|
|
@@ -2389,13 +2476,18 @@ var Dice = class _Dice {
|
|
|
2389
2476
|
const result = diceConstructor ? diceConstructor() : new _Dice();
|
|
2390
2477
|
const isScalar = typeof other === "number";
|
|
2391
2478
|
const keys1 = this.keys();
|
|
2479
|
+
const keys2 = isScalar ? [] : other.keys();
|
|
2480
|
+
if (!isScalar && keys1.length * keys2.length > MAX_BINARY_OUTCOMES) {
|
|
2481
|
+
throw new DiceParseError(
|
|
2482
|
+
`Dice operation over ${keys1.length}\xD7${keys2.length} face pairs exceeds the maximum of ${MAX_BINARY_OUTCOMES}`
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2392
2485
|
for (const key1 of keys1) {
|
|
2393
2486
|
const value1 = this.faces[key1];
|
|
2394
2487
|
if (isScalar) {
|
|
2395
2488
|
const resultKey = op(key1, other);
|
|
2396
2489
|
result.increment(resultKey, value1);
|
|
2397
2490
|
} else {
|
|
2398
|
-
const keys2 = other.keys();
|
|
2399
2491
|
for (const key2 of keys2) {
|
|
2400
2492
|
const value2 = other.faces[key2];
|
|
2401
2493
|
const resultKey = op(key1, key2);
|
|
@@ -2417,7 +2509,7 @@ var Dice = class _Dice {
|
|
|
2417
2509
|
result.outcomeData = { ...this.outcomeData };
|
|
2418
2510
|
return result;
|
|
2419
2511
|
}
|
|
2420
|
-
// PUBLIC
|
|
2512
|
+
// PUBLIC FUNCTIONS
|
|
2421
2513
|
getFaceEntries() {
|
|
2422
2514
|
return Object.entries(this.faces).map(([k, v]) => [Number(k), v]);
|
|
2423
2515
|
}
|
|
@@ -2547,10 +2639,12 @@ var Dice = class _Dice {
|
|
|
2547
2639
|
}
|
|
2548
2640
|
reroll(toReroll) {
|
|
2549
2641
|
const rerollDice = typeof toReroll === "number" ? _Dice.scalar(toReroll) : toReroll;
|
|
2550
|
-
const
|
|
2642
|
+
const rerollKeys = rerollDice.keys();
|
|
2643
|
+
const rerollSet = new Set(rerollKeys);
|
|
2644
|
+
const removed = this.removeFaces(rerollKeys);
|
|
2551
2645
|
let result = new _Dice();
|
|
2552
2646
|
for (const face of this.keys()) {
|
|
2553
|
-
const wasRerolled =
|
|
2647
|
+
const wasRerolled = rerollSet.has(face);
|
|
2554
2648
|
result = result.combine(removed);
|
|
2555
2649
|
if (wasRerolled) {
|
|
2556
2650
|
result = result.combine(this);
|
|
@@ -2618,17 +2712,10 @@ var Dice = class _Dice {
|
|
|
2618
2712
|
const missDistro = this.getOutcomeDistribution("missDamage") || {};
|
|
2619
2713
|
const saveDistro = this.getOutcomeDistribution("saveHalf") || {};
|
|
2620
2714
|
const pcDistro = this.getOutcomeDistribution("pc") || {};
|
|
2621
|
-
|
|
2622
|
-
for (const halfDamage of Object.keys(saveDistro).map(Number)) {
|
|
2623
|
-
const fullDamage = halfDamage * 2;
|
|
2624
|
-
if (fullDamage > 0 && hitDistro[fullDamage]) {
|
|
2625
|
-
isSaveHalf = true;
|
|
2626
|
-
break;
|
|
2627
|
-
}
|
|
2628
|
-
}
|
|
2715
|
+
const isSaveHalf = Object.keys(saveDistro).length > 0;
|
|
2629
2716
|
const isDCCheck = this.privateData.isDCCheck === true;
|
|
2630
2717
|
const clampNonNeg = (x) => x < 0 && x > -1e-15 ? 0 : x;
|
|
2631
|
-
for (const [faceStr, faceCountRaw] of Object.entries(this.
|
|
2718
|
+
for (const [faceStr, faceCountRaw] of Object.entries(this.faces)) {
|
|
2632
2719
|
const face = Number(faceStr);
|
|
2633
2720
|
const faceCount = Number(faceCountRaw);
|
|
2634
2721
|
if (faceCount <= 0) continue;
|
|
@@ -2702,14 +2789,14 @@ var Dice = class _Dice {
|
|
|
2702
2789
|
map.set(face, bin);
|
|
2703
2790
|
}
|
|
2704
2791
|
const identifier = this.identifier || "ERROR";
|
|
2705
|
-
if (identifier === "ERROR") {
|
|
2706
|
-
console.error("Dice identifier is undefined", this);
|
|
2707
|
-
}
|
|
2708
2792
|
return new PMF(map, numEpsilon, true, identifier).compact(numEpsilon, true);
|
|
2709
2793
|
}
|
|
2710
2794
|
};
|
|
2711
2795
|
|
|
2712
2796
|
// src/parser/parser.ts
|
|
2797
|
+
var MAX_DIE_SIDES = 1e6;
|
|
2798
|
+
var MAX_DICE_COUNT = 1e4;
|
|
2799
|
+
var MAX_KEEP_OUTCOMES = 1e6;
|
|
2713
2800
|
var parseCache = new LRUCache(1e3);
|
|
2714
2801
|
var cachingEnabled = true;
|
|
2715
2802
|
function setCachingEnabled(enabled) {
|
|
@@ -2730,20 +2817,21 @@ function parse(expression, n = 0) {
|
|
|
2730
2817
|
if (cached) return cached;
|
|
2731
2818
|
}
|
|
2732
2819
|
const chars = [...cleaned];
|
|
2733
|
-
let result
|
|
2820
|
+
let result;
|
|
2734
2821
|
try {
|
|
2735
2822
|
result = parseExpression(chars, n);
|
|
2736
2823
|
} catch (error) {
|
|
2737
|
-
throw new
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
result.identifier = cleaned;
|
|
2742
|
-
} catch {
|
|
2824
|
+
throw new DiceParseError(
|
|
2825
|
+
`Cannot parse dice expression [${expression}]: ${error}`,
|
|
2826
|
+
{ expression, cause: error }
|
|
2827
|
+
);
|
|
2743
2828
|
}
|
|
2829
|
+
result.privateData = result.privateData || {};
|
|
2830
|
+
result.identifier = cleaned;
|
|
2744
2831
|
if (chars.length > 0) {
|
|
2745
|
-
throw new
|
|
2746
|
-
`Unexpected token: '${chars[0]}' from expression: '${expression}'
|
|
2832
|
+
throw new DiceParseError(
|
|
2833
|
+
`Unexpected token: '${chars[0]}' from expression: '${expression}'`,
|
|
2834
|
+
{ expression }
|
|
2747
2835
|
);
|
|
2748
2836
|
}
|
|
2749
2837
|
const resultPMF = result.toPMF(-1);
|
|
@@ -2897,7 +2985,7 @@ function multiplyDiceByDice(d1, d2) {
|
|
|
2897
2985
|
if (typeof d1 === "number") d1 = Dice.scalar(d1);
|
|
2898
2986
|
if (typeof d2 === "number") d2 = Dice.scalar(d2);
|
|
2899
2987
|
const result = new Dice();
|
|
2900
|
-
const faces =
|
|
2988
|
+
const faces = /* @__PURE__ */ new Map();
|
|
2901
2989
|
let normalizationFactor = 1;
|
|
2902
2990
|
for (const key of d1.keys()) {
|
|
2903
2991
|
let face;
|
|
@@ -2905,17 +2993,21 @@ function multiplyDiceByDice(d1, d2) {
|
|
|
2905
2993
|
continue;
|
|
2906
2994
|
}
|
|
2907
2995
|
if (d2.privateData.keep) {
|
|
2996
|
+
const faceCount = d2.keys().length;
|
|
2997
|
+
if (Math.pow(faceCount, key) > MAX_KEEP_OUTCOMES) {
|
|
2998
|
+
throw new DiceParseError(
|
|
2999
|
+
`Keep enumeration of ${faceCount}^${key} outcomes exceeds the maximum of ${MAX_KEEP_OUTCOMES}`
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
2908
3002
|
const repeat = Array(key).fill(d2);
|
|
2909
3003
|
face = opDice(repeat, d2.privateData.keep);
|
|
2910
3004
|
} else {
|
|
2911
3005
|
face = multiplyDice(key, d2);
|
|
2912
3006
|
}
|
|
2913
3007
|
normalizationFactor *= face.total();
|
|
2914
|
-
faces
|
|
3008
|
+
faces.set(key, face);
|
|
2915
3009
|
}
|
|
2916
|
-
for (const
|
|
2917
|
-
const k = parseFloat(key);
|
|
2918
|
-
const face = faces[k];
|
|
3010
|
+
for (const [k, face] of faces) {
|
|
2919
3011
|
const count = d1.get(k);
|
|
2920
3012
|
result.combineInPlace(
|
|
2921
3013
|
face.normalize(count * normalizationFactor / face.total())
|
|
@@ -2925,6 +3017,11 @@ function multiplyDiceByDice(d1, d2) {
|
|
|
2925
3017
|
return result;
|
|
2926
3018
|
}
|
|
2927
3019
|
function multiplyDice(n, d) {
|
|
3020
|
+
if (n > MAX_DICE_COUNT) {
|
|
3021
|
+
throw new DiceParseError(
|
|
3022
|
+
`Dice count ${n} exceeds the maximum of ${MAX_DICE_COUNT}`
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
2928
3025
|
if (n === 0) return new Dice(0);
|
|
2929
3026
|
if (n === 1) return d;
|
|
2930
3027
|
const half = Math.floor(n / 2);
|
|
@@ -3007,9 +3104,14 @@ function parseDice(s, n) {
|
|
|
3007
3104
|
return;
|
|
3008
3105
|
}
|
|
3009
3106
|
const sides = parseNumber(s, n);
|
|
3107
|
+
if (sides > MAX_DIE_SIDES) {
|
|
3108
|
+
throw new DiceParseError(
|
|
3109
|
+
`Die size ${sides} exceeds the maximum of ${MAX_DIE_SIDES}`
|
|
3110
|
+
);
|
|
3111
|
+
}
|
|
3010
3112
|
let result = new Dice(sides);
|
|
3011
3113
|
if (rerollOne) {
|
|
3012
|
-
result = result.
|
|
3114
|
+
result = result.reroll(1);
|
|
3013
3115
|
}
|
|
3014
3116
|
return result;
|
|
3015
3117
|
}
|
|
@@ -3124,11 +3226,9 @@ function parseOperation(s) {
|
|
|
3124
3226
|
// src/pmf/mixture.ts
|
|
3125
3227
|
var Mixture = class _Mixture {
|
|
3126
3228
|
constructor(eps = EPS) {
|
|
3127
|
-
|
|
3229
|
+
this.totals = /* @__PURE__ */ new Map();
|
|
3128
3230
|
// raw mass per outcome (pre-normalization)
|
|
3129
|
-
|
|
3130
|
-
// raw mass per outcome per label
|
|
3131
|
-
__publicField(this, "eps");
|
|
3231
|
+
this.labelMass = /* @__PURE__ */ new Map();
|
|
3132
3232
|
this.eps = Number.isFinite(eps) ? eps : EPS;
|
|
3133
3233
|
}
|
|
3134
3234
|
/** Remove all accumulated state. */
|
|
@@ -3152,9 +3252,8 @@ var Mixture = class _Mixture {
|
|
|
3152
3252
|
*/
|
|
3153
3253
|
add(label, pmf, weight = 1) {
|
|
3154
3254
|
if (!Number.isFinite(weight) || weight <= 0) return this;
|
|
3155
|
-
for (const [v,
|
|
3156
|
-
const
|
|
3157
|
-
const p = isNumber ? binOrNumber : binOrNumber?.p ?? 0;
|
|
3255
|
+
for (const [v, bin] of pmf) {
|
|
3256
|
+
const p = bin.p;
|
|
3158
3257
|
if (p <= 0) continue;
|
|
3159
3258
|
const add = weight * p;
|
|
3160
3259
|
if (!Number.isFinite(add) || Math.abs(add) < this.eps) continue;
|
|
@@ -3242,6 +3341,7 @@ var Mixture = class _Mixture {
|
|
|
3242
3341
|
}
|
|
3243
3342
|
};
|
|
3244
3343
|
|
|
3344
|
+
exports.DiceParseError = DiceParseError;
|
|
3245
3345
|
exports.DiceQuery = DiceQuery;
|
|
3246
3346
|
exports.EPS = EPS;
|
|
3247
3347
|
exports.LRUCache = LRUCache;
|