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