@yipe/dice 0.2.23 → 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 +391 -295
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.d.cts +8 -8
- package/dist/builder/index.d.ts +8 -8
- package/dist/builder/index.js +391 -295
- 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/builder/index.cjs
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
5
|
-
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
6
|
-
|
|
7
3
|
// src/common/lru-cache.ts
|
|
8
4
|
var LRUCache = class {
|
|
9
5
|
constructor(maxSize = 1e3) {
|
|
10
6
|
this.maxSize = maxSize;
|
|
11
|
-
|
|
7
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
12
8
|
}
|
|
13
9
|
get(key) {
|
|
14
10
|
const value = this.cache.get(key);
|
|
@@ -52,15 +48,30 @@ var EPS = 1e-12;
|
|
|
52
48
|
// src/pmf/query.ts
|
|
53
49
|
var _DiceQuery = class _DiceQuery {
|
|
54
50
|
constructor(singles, combined, eps = EPS) {
|
|
55
|
-
__publicField(this, "singles");
|
|
56
|
-
__publicField(this, "combined");
|
|
57
|
-
__publicField(this, "_combinedWithAttr");
|
|
58
51
|
this.singles = Array.isArray(singles) ? singles : [singles];
|
|
59
52
|
if (this.singles.some((s) => s === void 0)) {
|
|
60
53
|
throw new Error("DiceQuery contains undefined singles");
|
|
61
54
|
}
|
|
62
|
-
|
|
63
|
-
this.
|
|
55
|
+
this._eps = eps;
|
|
56
|
+
this._combinedProvided = combined !== void 0;
|
|
57
|
+
if (combined !== void 0) {
|
|
58
|
+
this._combined = Math.abs(combined.mass() - 1) <= eps ? combined : combined.normalize();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The combined damage distribution of all single PMFs (their convolution),
|
|
63
|
+
* normalized to total probability 1.
|
|
64
|
+
*
|
|
65
|
+
* Computed lazily on first access and cached. Queries that only need
|
|
66
|
+
* additive statistics — {@link DiceQuery.mean}, {@link DiceQuery.variance},
|
|
67
|
+
* {@link DiceQuery.stddev} — never trigger this convolution.
|
|
68
|
+
*/
|
|
69
|
+
get combined() {
|
|
70
|
+
if (this._combined === void 0) {
|
|
71
|
+
const c = PMF.convolveMany(this.singles);
|
|
72
|
+
this._combined = Math.abs(c.mass() - 1) <= this._eps ? c : c.normalize();
|
|
73
|
+
}
|
|
74
|
+
return this._combined;
|
|
64
75
|
}
|
|
65
76
|
/**
|
|
66
77
|
* Returns a new PMF with damage attribution metadata populated.
|
|
@@ -85,6 +96,10 @@ var _DiceQuery = class _DiceQuery {
|
|
|
85
96
|
if (this._combinedWithAttr) {
|
|
86
97
|
return this._combinedWithAttr;
|
|
87
98
|
}
|
|
99
|
+
if (this.singles.every((pmf) => pmf.hasAttribution())) {
|
|
100
|
+
this._combinedWithAttr = this.combined;
|
|
101
|
+
return this._combinedWithAttr;
|
|
102
|
+
}
|
|
88
103
|
const singlesWithAttr = this.singles.map((pmf) => pmf.withAttribution());
|
|
89
104
|
const combined = PMF.convolveMany(singlesWithAttr, this.combined.epsilon);
|
|
90
105
|
const normalized = Math.abs(combined.mass() - 1) <= this.combined.epsilon ? combined : combined.normalize();
|
|
@@ -98,11 +113,18 @@ var _DiceQuery = class _DiceQuery {
|
|
|
98
113
|
* Use case: "What's my average damage per round?"
|
|
99
114
|
*/
|
|
100
115
|
mean() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
if (this._combinedProvided) {
|
|
117
|
+
let m = 0;
|
|
118
|
+
for (const [damageValue, bin] of this.combined) m += damageValue * bin.p;
|
|
119
|
+
return m;
|
|
104
120
|
}
|
|
105
|
-
|
|
121
|
+
let totalMean = 0;
|
|
122
|
+
for (const single of this.singles) {
|
|
123
|
+
const mass = single.mass();
|
|
124
|
+
if (mass <= 0) continue;
|
|
125
|
+
totalMean += Math.abs(mass - 1) <= this._eps ? single.mean() : single.mean() / mass;
|
|
126
|
+
}
|
|
127
|
+
return totalMean;
|
|
106
128
|
}
|
|
107
129
|
/**
|
|
108
130
|
* Returns the variance of the damage distribution.
|
|
@@ -112,13 +134,33 @@ var _DiceQuery = class _DiceQuery {
|
|
|
112
134
|
* High variance means higher risk/reward. Lower variance means more consistent damage.
|
|
113
135
|
*/
|
|
114
136
|
variance() {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
137
|
+
if (this._combinedProvided) {
|
|
138
|
+
const mu = this.mean();
|
|
139
|
+
let v = 0;
|
|
140
|
+
for (const [damageValue, bin] of this.combined) {
|
|
141
|
+
const dev = damageValue - mu;
|
|
142
|
+
v += dev * dev * bin.p;
|
|
143
|
+
}
|
|
144
|
+
return v;
|
|
145
|
+
}
|
|
146
|
+
let totalVariance = 0;
|
|
147
|
+
for (const single of this.singles) {
|
|
148
|
+
const mass = single.mass();
|
|
149
|
+
if (mass <= 0) continue;
|
|
150
|
+
if (Math.abs(mass - 1) <= this._eps) {
|
|
151
|
+
totalVariance += single.variance();
|
|
152
|
+
} else {
|
|
153
|
+
let mu = 0;
|
|
154
|
+
for (const [d2, b] of single) mu += d2 * (b.p / mass);
|
|
155
|
+
let v = 0;
|
|
156
|
+
for (const [d2, b] of single) {
|
|
157
|
+
const dev = d2 - mu;
|
|
158
|
+
v += dev * dev * (b.p / mass);
|
|
159
|
+
}
|
|
160
|
+
totalVariance += v;
|
|
161
|
+
}
|
|
120
162
|
}
|
|
121
|
-
return
|
|
163
|
+
return totalVariance;
|
|
122
164
|
}
|
|
123
165
|
/**
|
|
124
166
|
* Returns the standard deviation of the damage distribution.
|
|
@@ -130,6 +172,10 @@ var _DiceQuery = class _DiceQuery {
|
|
|
130
172
|
stddev() {
|
|
131
173
|
return Math.sqrt(this.variance());
|
|
132
174
|
}
|
|
175
|
+
/** Alias of {@link DiceQuery.stddev}, matching {@link PMF.stdev}. */
|
|
176
|
+
stdev() {
|
|
177
|
+
return this.stddev();
|
|
178
|
+
}
|
|
133
179
|
/**
|
|
134
180
|
* Returns the Cumulative Distribution Function.
|
|
135
181
|
*/
|
|
@@ -220,20 +266,49 @@ var _DiceQuery = class _DiceQuery {
|
|
|
220
266
|
return this.combined.max();
|
|
221
267
|
}
|
|
222
268
|
singleProb(diceIndex, label) {
|
|
269
|
+
const single = this.singles[diceIndex];
|
|
223
270
|
let probabilitySum = 0;
|
|
224
|
-
for (const [, probabilityBin] of
|
|
271
|
+
for (const [, probabilityBin] of single) {
|
|
225
272
|
probabilitySum += probabilityBin.count[label] || 0;
|
|
226
273
|
}
|
|
227
|
-
|
|
274
|
+
const mass = single.mass();
|
|
275
|
+
return mass > 0 ? probabilitySum / mass : 0;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Full count distribution [P(0), P(1), …, P(n)] for "an attack succeeds if it
|
|
279
|
+
* carries ANY of `labels`", over the n independent singles.
|
|
280
|
+
*
|
|
281
|
+
* Each single's per-event success probability is the Poisson-binomial
|
|
282
|
+
* marginal P(≥1 of labels) from {@link probabilityOf} (i.e. probAtLeastOne),
|
|
283
|
+
* computed exactly once. The binomial DP then runs once to produce the whole
|
|
284
|
+
* distribution, so the array-label paths of probExactlyK / probAtLeastK /
|
|
285
|
+
* probAtMostK can slice or sum from it instead of rebuilding a DiceQuery and
|
|
286
|
+
* re-running the DP per requested k.
|
|
287
|
+
*/
|
|
288
|
+
countDistribution(labels) {
|
|
289
|
+
const n = this.singles.length;
|
|
290
|
+
const successProbabilities = this.singles.map(
|
|
291
|
+
(single) => new _DiceQuery([single]).probabilityOf(labels)
|
|
292
|
+
);
|
|
293
|
+
const dist = new Array(n + 1).fill(0);
|
|
294
|
+
dist[0] = 1;
|
|
295
|
+
for (const successProb of successProbabilities) {
|
|
296
|
+
for (let outcomeCount = n; outcomeCount >= 1; outcomeCount--) {
|
|
297
|
+
dist[outcomeCount] = dist[outcomeCount] * (1 - successProb) + dist[outcomeCount - 1] * successProb;
|
|
298
|
+
}
|
|
299
|
+
dist[0] *= 1 - successProb;
|
|
300
|
+
}
|
|
301
|
+
return dist;
|
|
228
302
|
}
|
|
229
303
|
probAtLeastK(labels, k) {
|
|
230
304
|
const L = Array.isArray(labels) ? [...new Set(labels)] : [labels];
|
|
231
305
|
const n = this.singles.length;
|
|
232
306
|
if (k <= 0) return 1;
|
|
233
307
|
if (k > n) return 0;
|
|
308
|
+
const dist = this.countDistribution(L);
|
|
234
309
|
let tail = 0;
|
|
235
310
|
for (let i = k; i <= n; i++) {
|
|
236
|
-
tail +=
|
|
311
|
+
tail += dist[i];
|
|
237
312
|
}
|
|
238
313
|
if (tail < 0) return 0;
|
|
239
314
|
if (tail > 1) return 1;
|
|
@@ -265,9 +340,12 @@ var _DiceQuery = class _DiceQuery {
|
|
|
265
340
|
for (const label of labels) {
|
|
266
341
|
combinedProbability += this.singleProb(diceIndex, label);
|
|
267
342
|
}
|
|
343
|
+
if (combinedProbability < 0) combinedProbability = 0;
|
|
344
|
+
else if (combinedProbability > 1) combinedProbability = 1;
|
|
268
345
|
productOfNonOccurrence *= 1 - combinedProbability;
|
|
269
346
|
}
|
|
270
|
-
|
|
347
|
+
const result = 1 - productOfNonOccurrence;
|
|
348
|
+
return result < 0 ? 0 : result > 1 ? 1 : result;
|
|
271
349
|
}
|
|
272
350
|
/**
|
|
273
351
|
* Computes binomial probabilities for exactly 0, 1, 2, ..., maxK occurrences of a label.
|
|
@@ -308,7 +386,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
308
386
|
* Array examples:
|
|
309
387
|
* - probExactlyK(['hit', 'crit'], 2) = probability exactly 2 attacks succeed
|
|
310
388
|
* - probExactlyK(['hit', 'crit'], 1) = probability exactly 1 attack succeeds
|
|
311
|
-
* - probExactlyK(['
|
|
389
|
+
* - probExactlyK(['missDamage', 'missNone'], 0) = probability no attacks miss
|
|
312
390
|
*
|
|
313
391
|
* Use cases:
|
|
314
392
|
* - "What's the chance exactly one of my attacks hits?"
|
|
@@ -323,19 +401,8 @@ var _DiceQuery = class _DiceQuery {
|
|
|
323
401
|
const probabilityArray = this.computeBinomialProbabilities(labels, k);
|
|
324
402
|
return probabilityArray[k];
|
|
325
403
|
}
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
return singleQuery.probabilityOf(labels);
|
|
329
|
-
});
|
|
330
|
-
const binomialProbs = new Array(k + 1).fill(0);
|
|
331
|
-
binomialProbs[0] = 1;
|
|
332
|
-
for (const successProb of successProbabilities) {
|
|
333
|
-
for (let outcomeCount = k; outcomeCount >= 1; outcomeCount--) {
|
|
334
|
-
binomialProbs[outcomeCount] = binomialProbs[outcomeCount] * (1 - successProb) + binomialProbs[outcomeCount - 1] * successProb;
|
|
335
|
-
}
|
|
336
|
-
binomialProbs[0] *= 1 - successProb;
|
|
337
|
-
}
|
|
338
|
-
return binomialProbs[k];
|
|
404
|
+
const dist = this.countDistribution(labels);
|
|
405
|
+
return k >= 0 && k < dist.length ? dist[k] : 0;
|
|
339
406
|
}
|
|
340
407
|
/**
|
|
341
408
|
* Returns the probability that AT MOST K attacks result in the specified outcome(s).
|
|
@@ -343,7 +410,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
343
410
|
* Single label examples:
|
|
344
411
|
* - probAtMostK('hit', 1) = probability 0 or 1 attacks hit (at most 1)
|
|
345
412
|
* - probAtMostK('crit', 0) = probability no attacks crit
|
|
346
|
-
* - probAtMostK('
|
|
413
|
+
* - probAtMostK('missDamage', 2) = probability at most 2 attacks miss
|
|
347
414
|
*
|
|
348
415
|
* Array examples:
|
|
349
416
|
* - probAtMostK(['hit', 'crit'], 1) = probability at most 1 attack succeeds
|
|
@@ -364,9 +431,11 @@ var _DiceQuery = class _DiceQuery {
|
|
|
364
431
|
}
|
|
365
432
|
return cumulativeSum2;
|
|
366
433
|
}
|
|
434
|
+
const dist = this.countDistribution(labels);
|
|
435
|
+
const upper = Math.min(k, dist.length - 1);
|
|
367
436
|
let cumulativeSum = 0;
|
|
368
|
-
for (let outcomeCount = 0; outcomeCount <=
|
|
369
|
-
cumulativeSum +=
|
|
437
|
+
for (let outcomeCount = 0; outcomeCount <= upper; outcomeCount++) {
|
|
438
|
+
cumulativeSum += dist[outcomeCount];
|
|
370
439
|
}
|
|
371
440
|
return cumulativeSum;
|
|
372
441
|
}
|
|
@@ -412,7 +481,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
412
481
|
*
|
|
413
482
|
* Array examples:
|
|
414
483
|
* - damageStatsFrom(['hit', 'crit']) = damage range when at least one attack succeeds
|
|
415
|
-
* - damageStatsFrom(['
|
|
484
|
+
* - damageStatsFrom(['missDamage', 'missNone']) = damage range when at least one attack misses
|
|
416
485
|
*
|
|
417
486
|
* Tactical Use Cases:
|
|
418
487
|
* - "Given that I don't completely whiff (99% of turns), what damage should I expect?"
|
|
@@ -430,6 +499,12 @@ var _DiceQuery = class _DiceQuery {
|
|
|
430
499
|
* This includes mixed scenarios (2 hits + 1 crit, 3 hits + 1 miss, etc.) which
|
|
431
500
|
* occur far more frequently than pure scenarios. For pure scenarios, use combinedDamageStats.
|
|
432
501
|
*
|
|
502
|
+
* KNOWN LIMITATION (multi-attack, single label): the returned `count` is an
|
|
503
|
+
* EXPECTED COUNT (E[#label], so > 1 for N≥2 attacks, not a probability), and
|
|
504
|
+
* `avg` is the size-biased conditional mean E[dmg·#label]/E[#label] rather than
|
|
505
|
+
* E[dmg | the label occurs]. For a single attack both are the plain
|
|
506
|
+
* conditional figures. Use {@link probAtLeastOne} for the scenario probability.
|
|
507
|
+
*
|
|
433
508
|
* @example
|
|
434
509
|
* // High-level tactical planning
|
|
435
510
|
* const successStats = query.damageStatsFrom('hit')
|
|
@@ -534,7 +609,8 @@ var _DiceQuery = class _DiceQuery {
|
|
|
534
609
|
};
|
|
535
610
|
}
|
|
536
611
|
/**
|
|
537
|
-
* Returns the probability that
|
|
612
|
+
* Returns the probability that at least one attack carries ANY of the
|
|
613
|
+
* specified labels (the marginal P(≥1) across the independent attacks).
|
|
538
614
|
*
|
|
539
615
|
* Examples:
|
|
540
616
|
* - `query.probabilityOf('hit')` → 0.88 (probability at least one hit occurs)
|
|
@@ -543,25 +619,15 @@ var _DiceQuery = class _DiceQuery {
|
|
|
543
619
|
* Use cases:
|
|
544
620
|
* - "What's the chance my resolution includes a success label?"
|
|
545
621
|
* - "How likely am I to get any hits or crits across all attacks?"
|
|
622
|
+
*
|
|
623
|
+
* Note: this must NOT be computed by summing `combined` bin probabilities. A
|
|
624
|
+
* single combined damage total is reachable by many outcome combinations and
|
|
625
|
+
* a bin can hold several labels at once, so summing `bin.p` over bins that
|
|
626
|
+
* contain a label over-counts. The correct marginal is the Poisson-binomial
|
|
627
|
+
* complement over the per-attack probabilities, i.e. {@link probAtLeastOne}.
|
|
546
628
|
*/
|
|
547
629
|
probabilityOf(labels) {
|
|
548
|
-
|
|
549
|
-
labels = [labels];
|
|
550
|
-
}
|
|
551
|
-
let totalProbability = 0;
|
|
552
|
-
for (const [, probabilityBin] of this.combined) {
|
|
553
|
-
let binHasAnyLabel = false;
|
|
554
|
-
for (const label of labels) {
|
|
555
|
-
if (probabilityBin.count[label] && probabilityBin.count[label] > 0) {
|
|
556
|
-
binHasAnyLabel = true;
|
|
557
|
-
break;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
if (binHasAnyLabel) {
|
|
561
|
-
totalProbability += probabilityBin.p;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
return totalProbability;
|
|
630
|
+
return this.probAtLeastOne(labels);
|
|
565
631
|
}
|
|
566
632
|
/**
|
|
567
633
|
* Returns the probability of missing (any type of miss).
|
|
@@ -621,10 +687,6 @@ var _DiceQuery = class _DiceQuery {
|
|
|
621
687
|
*/
|
|
622
688
|
toStackedChartData(labels = [], epsilon = EPS) {
|
|
623
689
|
const damageValues = this.combined.support();
|
|
624
|
-
damageValues.map((dmg) => {
|
|
625
|
-
const bin = this.combined.map.get(dmg);
|
|
626
|
-
return labels.reduce((sum, lab) => sum + (bin.count[lab] || 0), 0);
|
|
627
|
-
});
|
|
628
690
|
const datasets = labels.map((outcomeLabel) => ({
|
|
629
691
|
label: outcomeLabel,
|
|
630
692
|
data: damageValues.map((dmg) => {
|
|
@@ -1061,6 +1123,16 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1061
1123
|
* Snapshot of the distribution in the exact shape the UI consumes.
|
|
1062
1124
|
* - outcome probabilities are "at least one" (and equal to "all" for a single PMF)
|
|
1063
1125
|
* - damageRange is conditional on the outcome occurring
|
|
1126
|
+
*
|
|
1127
|
+
* The outcome probabilities use the correct Poisson-binomial marginals
|
|
1128
|
+
* (`atLeastOneProbability` = P(≥1 attack has it), `allProbability` = P(all do)),
|
|
1129
|
+
* so they are always valid probabilities in [0,1].
|
|
1130
|
+
*
|
|
1131
|
+
* KNOWN LIMITATION (multi-attack): `damageRange.avg` is still aggregated from
|
|
1132
|
+
* the combined PMF's `count`, which the convolution accumulates as an EXPECTED
|
|
1133
|
+
* COUNT, so for N≥2 attacks it is the size-biased mean E[dmg·#label]/E[#label]
|
|
1134
|
+
* rather than a clean conditional expectation. It is correct for a single
|
|
1135
|
+
* attack.
|
|
1064
1136
|
*/
|
|
1065
1137
|
snapshot(order) {
|
|
1066
1138
|
const discovered = /* @__PURE__ */ new Set();
|
|
@@ -1082,10 +1154,8 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1082
1154
|
);
|
|
1083
1155
|
}
|
|
1084
1156
|
const rows = this.toLabeledTable(outcomes);
|
|
1085
|
-
const totals = /* @__PURE__ */ new Map();
|
|
1086
1157
|
const rangeAcc = /* @__PURE__ */ new Map();
|
|
1087
1158
|
for (const ot of outcomes) {
|
|
1088
|
-
totals.set(ot, 0);
|
|
1089
1159
|
rangeAcc.set(ot, { sum: 0, mass: 0 });
|
|
1090
1160
|
}
|
|
1091
1161
|
for (const row of rows) {
|
|
@@ -1093,7 +1163,6 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1093
1163
|
for (const ot of outcomes) {
|
|
1094
1164
|
const p = row[ot] || 0;
|
|
1095
1165
|
if (p <= 0) continue;
|
|
1096
|
-
totals.set(ot, (totals.get(ot) || 0) + p);
|
|
1097
1166
|
const r = rangeAcc.get(ot);
|
|
1098
1167
|
r.sum += dmg * p;
|
|
1099
1168
|
r.mass += p;
|
|
@@ -1101,15 +1170,14 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1101
1170
|
if (r.max === void 0 || dmg > r.max) r.max = dmg;
|
|
1102
1171
|
}
|
|
1103
1172
|
}
|
|
1173
|
+
const n = this.singles.length;
|
|
1104
1174
|
const outcomeMap = /* @__PURE__ */ new Map();
|
|
1105
1175
|
for (const ot of outcomes) {
|
|
1106
|
-
const total = totals.get(ot) || 0;
|
|
1107
1176
|
const r = rangeAcc.get(ot);
|
|
1108
1177
|
const avg = r.mass > 0 ? r.sum / r.mass : 0;
|
|
1109
1178
|
outcomeMap.set(ot, {
|
|
1110
|
-
atLeastOneProbability:
|
|
1111
|
-
allProbability:
|
|
1112
|
-
// single aggregate PMF: same value
|
|
1179
|
+
atLeastOneProbability: this.probAtLeastOne(ot),
|
|
1180
|
+
allProbability: this.probAtLeastK(ot, n),
|
|
1113
1181
|
damageRange: { min: r.min ?? 0, avg, max: r.max ?? 0 }
|
|
1114
1182
|
});
|
|
1115
1183
|
}
|
|
@@ -1294,11 +1362,11 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1294
1362
|
return [a, b, any, none];
|
|
1295
1363
|
}
|
|
1296
1364
|
};
|
|
1297
|
-
|
|
1365
|
+
_DiceQuery.DEFAULT_OUTCOMES = [
|
|
1298
1366
|
"hit",
|
|
1299
1367
|
"crit",
|
|
1300
1368
|
"missNone"
|
|
1301
|
-
]
|
|
1369
|
+
];
|
|
1302
1370
|
var DiceQuery = _DiceQuery;
|
|
1303
1371
|
var pmfCache = new LRUCache(1e3);
|
|
1304
1372
|
var _PMF = class _PMF {
|
|
@@ -1308,14 +1376,6 @@ var _PMF = class _PMF {
|
|
|
1308
1376
|
this.normalized = normalized;
|
|
1309
1377
|
this.identifier = identifier;
|
|
1310
1378
|
this._preservedProvenance = _preservedProvenance;
|
|
1311
|
-
// Cached computed values
|
|
1312
|
-
__publicField(this, "_support");
|
|
1313
|
-
__publicField(this, "_min");
|
|
1314
|
-
__publicField(this, "_max");
|
|
1315
|
-
__publicField(this, "_totalMass");
|
|
1316
|
-
__publicField(this, "_mean");
|
|
1317
|
-
__publicField(this, "_variance");
|
|
1318
|
-
__publicField(this, "_stdev");
|
|
1319
1379
|
}
|
|
1320
1380
|
static empty(epsilon = EPS, identifier = "empty") {
|
|
1321
1381
|
return new _PMF(/* @__PURE__ */ new Map(), epsilon, false, identifier);
|
|
@@ -1355,8 +1415,14 @@ var _PMF = class _PMF {
|
|
|
1355
1415
|
if (p === 1) return successPMF.scaleMass(1);
|
|
1356
1416
|
const eps = successPMF.epsilon ?? failurePMF.epsilon;
|
|
1357
1417
|
const id = `branch(${failurePMF.identifier}*${q.toFixed(6)} + ${successPMF.identifier}*${p.toFixed(6)})`;
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1418
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
1419
|
+
for (const [damageValue, bin] of failurePMF.map) {
|
|
1420
|
+
_PMF.mergeInto(resultMap, damageValue, _PMF.scaleBin(bin, q));
|
|
1421
|
+
}
|
|
1422
|
+
for (const [damageValue, bin] of successPMF.map) {
|
|
1423
|
+
_PMF.mergeInto(resultMap, damageValue, _PMF.scaleBin(bin, p));
|
|
1424
|
+
}
|
|
1425
|
+
return new _PMF(resultMap, eps, false, id);
|
|
1360
1426
|
}
|
|
1361
1427
|
/**
|
|
1362
1428
|
* withProbability()
|
|
@@ -1404,8 +1470,8 @@ var _PMF = class _PMF {
|
|
|
1404
1470
|
* @param fallback PMF to apply when this PMF is *not* selected.
|
|
1405
1471
|
* @returns A new PMF representing the weighted mixture of this PMF and the fallback.
|
|
1406
1472
|
*/
|
|
1407
|
-
gate(p,
|
|
1408
|
-
return _PMF.branch(this,
|
|
1473
|
+
gate(p, fallback) {
|
|
1474
|
+
return _PMF.branch(this, fallback, p);
|
|
1409
1475
|
}
|
|
1410
1476
|
/**
|
|
1411
1477
|
* PMF.exclusive()
|
|
@@ -1488,13 +1554,23 @@ var _PMF = class _PMF {
|
|
|
1488
1554
|
*
|
|
1489
1555
|
* @returns New PMF with attr field populated in each bin
|
|
1490
1556
|
*/
|
|
1491
|
-
|
|
1557
|
+
/**
|
|
1558
|
+
* Returns true if this PMF already carries damage attribution metadata.
|
|
1559
|
+
*
|
|
1560
|
+
* Only the first positive-damage bin is inspected (parser-generated PMFs
|
|
1561
|
+
* populate `attr` uniformly), so this is O(1) in practice.
|
|
1562
|
+
*/
|
|
1563
|
+
hasAttribution() {
|
|
1492
1564
|
for (const [damage, bin] of this.map) {
|
|
1493
1565
|
if (damage !== 0 && bin.attr && Object.keys(bin.attr).length > 0) {
|
|
1494
|
-
return
|
|
1566
|
+
return true;
|
|
1495
1567
|
}
|
|
1496
1568
|
if (damage > 0) break;
|
|
1497
1569
|
}
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1572
|
+
withAttribution() {
|
|
1573
|
+
if (this.hasAttribution()) return this;
|
|
1498
1574
|
const newMap = /* @__PURE__ */ new Map();
|
|
1499
1575
|
for (const [damage, bin] of this.map) {
|
|
1500
1576
|
const attr = {};
|
|
@@ -1611,7 +1687,7 @@ var _PMF = class _PMF {
|
|
|
1611
1687
|
*/
|
|
1612
1688
|
replicate(n) {
|
|
1613
1689
|
if (!Number.isInteger(n) || n <= 0) {
|
|
1614
|
-
throw new Error("
|
|
1690
|
+
throw new Error("replicate(n): n must be a positive integer");
|
|
1615
1691
|
}
|
|
1616
1692
|
if (n === 1) return [this];
|
|
1617
1693
|
return Array.from({ length: n }, () => this);
|
|
@@ -1643,7 +1719,6 @@ var _PMF = class _PMF {
|
|
|
1643
1719
|
if (normalizationFactor === 0) return this;
|
|
1644
1720
|
const normalizedMap = /* @__PURE__ */ new Map();
|
|
1645
1721
|
for (const [damageValue, probabilityBin] of this.map) {
|
|
1646
|
-
const normalizedProbability = probabilityBin.p / normalizationFactor;
|
|
1647
1722
|
const normalizedCount = {};
|
|
1648
1723
|
for (const labelKey in probabilityBin.count) {
|
|
1649
1724
|
normalizedCount[labelKey] = probabilityBin.count[labelKey] / normalizationFactor;
|
|
@@ -1656,7 +1731,7 @@ var _PMF = class _PMF {
|
|
|
1656
1731
|
}
|
|
1657
1732
|
}
|
|
1658
1733
|
normalizedMap.set(damageValue, {
|
|
1659
|
-
p:
|
|
1734
|
+
p: probabilityBin.p / normalizationFactor,
|
|
1660
1735
|
count: normalizedCount,
|
|
1661
1736
|
attr: normalizedAttributes
|
|
1662
1737
|
});
|
|
@@ -1679,22 +1754,23 @@ var _PMF = class _PMF {
|
|
|
1679
1754
|
for (const [damageValue, probabilityBin] of this.map) {
|
|
1680
1755
|
const shouldKeep = probabilityBin.p >= eps || keepFinalBin && damageValue === maxKey;
|
|
1681
1756
|
if (!shouldKeep) continue;
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1757
|
+
const cleanedBin = _PMF.cloneBin(probabilityBin);
|
|
1758
|
+
for (const labelKey in cleanedBin.count) {
|
|
1759
|
+
if (Math.abs(cleanedBin.count[labelKey] || 0) < eps) {
|
|
1760
|
+
delete cleanedBin.count[labelKey];
|
|
1685
1761
|
}
|
|
1686
1762
|
}
|
|
1687
|
-
if (
|
|
1688
|
-
for (const labelKey in
|
|
1689
|
-
if (Math.abs(
|
|
1690
|
-
delete
|
|
1763
|
+
if (cleanedBin.attr) {
|
|
1764
|
+
for (const labelKey in cleanedBin.attr) {
|
|
1765
|
+
if (Math.abs(cleanedBin.attr[labelKey] || 0) < eps) {
|
|
1766
|
+
delete cleanedBin.attr[labelKey];
|
|
1691
1767
|
}
|
|
1692
1768
|
}
|
|
1693
|
-
if (Object.keys(
|
|
1694
|
-
|
|
1769
|
+
if (Object.keys(cleanedBin.attr).length === 0) {
|
|
1770
|
+
cleanedBin.attr = void 0;
|
|
1695
1771
|
}
|
|
1696
1772
|
}
|
|
1697
|
-
compactedMap.set(damageValue,
|
|
1773
|
+
compactedMap.set(damageValue, cleanedBin);
|
|
1698
1774
|
}
|
|
1699
1775
|
return new _PMF(compactedMap, eps, this.normalized, this.identifier);
|
|
1700
1776
|
}
|
|
@@ -1761,14 +1837,33 @@ var _PMF = class _PMF {
|
|
|
1761
1837
|
}
|
|
1762
1838
|
return this._stdev;
|
|
1763
1839
|
}
|
|
1840
|
+
/** Deep-copies a Bin, cloning its count and (optional) attr maps. */
|
|
1841
|
+
static cloneBin(bin) {
|
|
1842
|
+
return {
|
|
1843
|
+
p: bin.p,
|
|
1844
|
+
count: { ...bin.count },
|
|
1845
|
+
attr: bin.attr ? { ...bin.attr } : void 0
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
/** Returns a new Bin with p, count, and attr all multiplied by `factor`. */
|
|
1849
|
+
static scaleBin(bin, factor) {
|
|
1850
|
+
const count = {};
|
|
1851
|
+
for (const k in bin.count) {
|
|
1852
|
+
count[k] = bin.count[k] * factor;
|
|
1853
|
+
}
|
|
1854
|
+
let attr;
|
|
1855
|
+
if (bin.attr) {
|
|
1856
|
+
attr = {};
|
|
1857
|
+
for (const k in bin.attr) {
|
|
1858
|
+
attr[k] = bin.attr[k] * factor;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
return { p: bin.p * factor, count, attr };
|
|
1862
|
+
}
|
|
1764
1863
|
static mergeInto(destinationMap, damageValue, binToAdd) {
|
|
1765
1864
|
const existingBin = destinationMap.get(damageValue);
|
|
1766
1865
|
if (!existingBin) {
|
|
1767
|
-
destinationMap.set(damageValue,
|
|
1768
|
-
p: binToAdd.p,
|
|
1769
|
-
count: { ...binToAdd.count },
|
|
1770
|
-
attr: binToAdd.attr ? { ...binToAdd.attr } : void 0
|
|
1771
|
-
});
|
|
1866
|
+
destinationMap.set(damageValue, _PMF.cloneBin(binToAdd));
|
|
1772
1867
|
return;
|
|
1773
1868
|
}
|
|
1774
1869
|
existingBin.p += binToAdd.p;
|
|
@@ -1799,29 +1894,14 @@ var _PMF = class _PMF {
|
|
|
1799
1894
|
if (probability === 0) return this;
|
|
1800
1895
|
const resultMap = /* @__PURE__ */ new Map();
|
|
1801
1896
|
for (const [dmg, bin] of this.map) {
|
|
1802
|
-
resultMap.set(dmg,
|
|
1803
|
-
p: bin.p,
|
|
1804
|
-
count: { ...bin.count },
|
|
1805
|
-
attr: bin.attr ? { ...bin.attr } : void 0
|
|
1806
|
-
});
|
|
1897
|
+
resultMap.set(dmg, _PMF.cloneBin(bin));
|
|
1807
1898
|
}
|
|
1808
1899
|
for (const [damageValue, probabilityBin] of branch.map) {
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
if (probabilityBin.attr) {
|
|
1815
|
-
scaledAttributes = {};
|
|
1816
|
-
for (const k in probabilityBin.attr) {
|
|
1817
|
-
scaledAttributes[k] = probability * probabilityBin.attr[k];
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
_PMF.mergeInto(resultMap, damageValue, {
|
|
1821
|
-
p: probability * probabilityBin.p,
|
|
1822
|
-
count: scaledCount,
|
|
1823
|
-
attr: scaledAttributes
|
|
1824
|
-
});
|
|
1900
|
+
_PMF.mergeInto(
|
|
1901
|
+
resultMap,
|
|
1902
|
+
damageValue,
|
|
1903
|
+
_PMF.scaleBin(probabilityBin, probability)
|
|
1904
|
+
);
|
|
1825
1905
|
}
|
|
1826
1906
|
return new _PMF(
|
|
1827
1907
|
resultMap,
|
|
@@ -1834,22 +1914,7 @@ var _PMF = class _PMF {
|
|
|
1834
1914
|
if (factor === 1) return this;
|
|
1835
1915
|
const scaledMap = /* @__PURE__ */ new Map();
|
|
1836
1916
|
for (const [damageValue, probabilityBin] of this.map) {
|
|
1837
|
-
|
|
1838
|
-
for (const labelKey in probabilityBin.count) {
|
|
1839
|
-
scaledCount[labelKey] = probabilityBin.count[labelKey] * factor;
|
|
1840
|
-
}
|
|
1841
|
-
let scaledAttributes;
|
|
1842
|
-
if (probabilityBin.attr) {
|
|
1843
|
-
scaledAttributes = {};
|
|
1844
|
-
for (const labelKey in probabilityBin.attr) {
|
|
1845
|
-
scaledAttributes[labelKey] = probabilityBin.attr[labelKey] * factor;
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
scaledMap.set(damageValue, {
|
|
1849
|
-
p: probabilityBin.p * factor,
|
|
1850
|
-
count: scaledCount,
|
|
1851
|
-
attr: scaledAttributes
|
|
1852
|
-
});
|
|
1917
|
+
scaledMap.set(damageValue, _PMF.scaleBin(probabilityBin, factor));
|
|
1853
1918
|
}
|
|
1854
1919
|
return new _PMF(
|
|
1855
1920
|
scaledMap,
|
|
@@ -1862,11 +1927,11 @@ var _PMF = class _PMF {
|
|
|
1862
1927
|
const transformedMap = /* @__PURE__ */ new Map();
|
|
1863
1928
|
for (const [originalDamage, probabilityBin] of this.map) {
|
|
1864
1929
|
const transformedDamage = damageTransformFunction(originalDamage);
|
|
1865
|
-
_PMF.mergeInto(
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1930
|
+
_PMF.mergeInto(
|
|
1931
|
+
transformedMap,
|
|
1932
|
+
transformedDamage,
|
|
1933
|
+
_PMF.cloneBin(probabilityBin)
|
|
1934
|
+
);
|
|
1870
1935
|
}
|
|
1871
1936
|
return new _PMF(
|
|
1872
1937
|
transformedMap,
|
|
@@ -1881,14 +1946,21 @@ var _PMF = class _PMF {
|
|
|
1881
1946
|
}
|
|
1882
1947
|
getPMFCombineCacheKey(p1, p2, eps, raw) {
|
|
1883
1948
|
const [id1, id2] = [p1.identifier, p2.identifier].sort();
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1949
|
+
return `v4:${raw ? "RAW" : "N"}:${id1}+${id2}@${eps}|${p1.fingerprint()}|${p2.fingerprint()}`;
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* A small content fingerprint (mass + bin count + face sum) so convolution
|
|
1953
|
+
* cache keys change if the underlying numbers do. Memoized because a PMF is
|
|
1954
|
+
* immutable once constructed — this avoids re-summing every key on each
|
|
1955
|
+
* convolve() call (including cache hits).
|
|
1956
|
+
*/
|
|
1957
|
+
fingerprint() {
|
|
1958
|
+
if (this._fingerprint === void 0) {
|
|
1887
1959
|
let faceSum = 0;
|
|
1888
|
-
for (const k of
|
|
1889
|
-
|
|
1890
|
-
}
|
|
1891
|
-
return
|
|
1960
|
+
for (const k of this.map.keys()) faceSum += k;
|
|
1961
|
+
this._fingerprint = `${this.mass().toFixed(12)}|${this.map.size}|${faceSum}`;
|
|
1962
|
+
}
|
|
1963
|
+
return this._fingerprint;
|
|
1892
1964
|
}
|
|
1893
1965
|
convolve(other, eps, raw = false) {
|
|
1894
1966
|
const epsilon = eps ?? this.epsilon;
|
|
@@ -1901,25 +1973,34 @@ var _PMF = class _PMF {
|
|
|
1901
1973
|
if (cached) return cached;
|
|
1902
1974
|
const combinedMap = /* @__PURE__ */ new Map();
|
|
1903
1975
|
for (const [aVal, aBin] of A.map) {
|
|
1976
|
+
const ap = aBin.p;
|
|
1977
|
+
const aCount = aBin.count;
|
|
1978
|
+
const aAttr = aBin.attr;
|
|
1904
1979
|
for (const [bVal, bBin] of B.map) {
|
|
1905
|
-
const
|
|
1980
|
+
const bp = bBin.p;
|
|
1906
1981
|
const dmg = aVal + bVal;
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1982
|
+
let dest = combinedMap.get(dmg);
|
|
1983
|
+
if (dest === void 0) {
|
|
1984
|
+
dest = { p: 0, count: {} };
|
|
1985
|
+
combinedMap.set(dmg, dest);
|
|
1986
|
+
}
|
|
1987
|
+
dest.p += ap * bp;
|
|
1988
|
+
const dc = dest.count;
|
|
1989
|
+
for (const k in aCount) dc[k] = (dc[k] || 0) + aCount[k] * bp;
|
|
1910
1990
|
for (const k in bBin.count)
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1991
|
+
dc[k] = (dc[k] || 0) + bBin.count[k] * ap;
|
|
1992
|
+
if (aAttr || bBin.attr) {
|
|
1993
|
+
let da = dest.attr;
|
|
1994
|
+
if (da === void 0) {
|
|
1995
|
+
da = {};
|
|
1996
|
+
dest.attr = da;
|
|
1997
|
+
}
|
|
1998
|
+
if (aAttr)
|
|
1999
|
+
for (const k in aAttr) da[k] = (da[k] || 0) + aAttr[k] * bp;
|
|
1918
2000
|
if (bBin.attr)
|
|
1919
2001
|
for (const k in bBin.attr)
|
|
1920
|
-
|
|
2002
|
+
da[k] = (da[k] || 0) + bBin.attr[k] * ap;
|
|
1921
2003
|
}
|
|
1922
|
-
_PMF.mergeInto(combinedMap, dmg, { p, count, attr });
|
|
1923
2004
|
}
|
|
1924
2005
|
}
|
|
1925
2006
|
let result = new _PMF(
|
|
@@ -1930,10 +2011,10 @@ var _PMF = class _PMF {
|
|
|
1930
2011
|
);
|
|
1931
2012
|
const mExp = (raw ? A.mass() : 1) * (raw ? B.mass() : 1);
|
|
1932
2013
|
const mGot = result.mass();
|
|
1933
|
-
if (mExp !== 0 && Math.abs(mGot - mExp) > epsilon) {
|
|
2014
|
+
if (mExp !== 0 && mGot !== 0 && Math.abs(mGot - mExp) > epsilon) {
|
|
1934
2015
|
result = result.scaleMass(mExp / mGot);
|
|
1935
2016
|
}
|
|
1936
|
-
if (!raw && Math.abs(result.mass() - 1) > epsilon)
|
|
2017
|
+
if (!raw && mGot !== 0 && Math.abs(result.mass() - 1) > epsilon)
|
|
1937
2018
|
result = result.normalize();
|
|
1938
2019
|
pmfCache?.set(cacheKey, result);
|
|
1939
2020
|
return result;
|
|
@@ -1942,27 +2023,6 @@ var _PMF = class _PMF {
|
|
|
1942
2023
|
combineRaw(other, eps) {
|
|
1943
2024
|
return this.convolve(other, eps, true);
|
|
1944
2025
|
}
|
|
1945
|
-
// Collapse repeated identical PMFs using power() and return a sorted list
|
|
1946
|
-
// Temporarily disabled for now. This was used for a performance optimization, but can lose data provenance.
|
|
1947
|
-
// private static collapseIdentical(pmfList: PMF[], eps: number): PMF[] {
|
|
1948
|
-
// const grouped = new Map<string, { pmf: PMF; count: number }>();
|
|
1949
|
-
// for (const pmf of pmfList) {
|
|
1950
|
-
// const id = pmf.identifier;
|
|
1951
|
-
// const g = grouped.get(id);
|
|
1952
|
-
// if (g) g.count++;
|
|
1953
|
-
// else grouped.set(id, { pmf, count: 1 });
|
|
1954
|
-
// }
|
|
1955
|
-
// if (grouped.size >= pmfList.length) return pmfList;
|
|
1956
|
-
// const collapsed: PMF[] = [];
|
|
1957
|
-
// for (const { pmf, count } of grouped.values()) {
|
|
1958
|
-
// collapsed.push(count > 1 ? pmf.power(count, eps) : pmf);
|
|
1959
|
-
// }
|
|
1960
|
-
// // Stable order for better cache locality
|
|
1961
|
-
// collapsed.sort((a, b) =>
|
|
1962
|
-
// a.identifier < b.identifier ? -1 : a.identifier > b.identifier ? 1 : 0
|
|
1963
|
-
// );
|
|
1964
|
-
// return collapsed;
|
|
1965
|
-
// }
|
|
1966
2026
|
// Reduce a list of PMFs by left-folding convolve() with the given eps
|
|
1967
2027
|
static reduceConvolveLeft(pmfList, eps) {
|
|
1968
2028
|
let result = pmfList[0];
|
|
@@ -1986,12 +2046,23 @@ var _PMF = class _PMF {
|
|
|
1986
2046
|
if (pmfList.length === 1) return pmfList[0];
|
|
1987
2047
|
return _PMF.reduceConvolveLeft(pmfList, eps);
|
|
1988
2048
|
}
|
|
2049
|
+
/**
|
|
2050
|
+
* Returns a plain, JSON-serializable representation of this PMF.
|
|
2051
|
+
*
|
|
2052
|
+
* Follows the standard `toJSON` contract, so `JSON.stringify(pmf)` produces
|
|
2053
|
+
* the expected output (no double-encoding). Use {@link PMF.fromJSON} to
|
|
2054
|
+
* reconstruct, or {@link PMF.toJSONString} if you need the string directly.
|
|
2055
|
+
*/
|
|
1989
2056
|
toJSON() {
|
|
1990
|
-
return
|
|
2057
|
+
return {
|
|
1991
2058
|
bins: [...this.map.entries()],
|
|
1992
2059
|
normalized: this.normalized,
|
|
1993
2060
|
identifier: this.identifier
|
|
1994
|
-
}
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
/** Serializes this PMF to a JSON string (equivalent to `JSON.stringify(pmf)`). */
|
|
2064
|
+
toJSONString() {
|
|
2065
|
+
return JSON.stringify(this);
|
|
1995
2066
|
}
|
|
1996
2067
|
static fromJSON(jsonData) {
|
|
1997
2068
|
return new _PMF(
|
|
@@ -2062,7 +2133,6 @@ var _PMF = class _PMF {
|
|
|
2062
2133
|
}
|
|
2063
2134
|
return new _PMF(prunedMap, epsRel, false, `prune(${this.identifier})`);
|
|
2064
2135
|
}
|
|
2065
|
-
/** NEW - REVIEW IF THESE ARE USEFUL OR DUPLCIATIVE? */
|
|
2066
2136
|
/** Probability mass at exactly x. */
|
|
2067
2137
|
pAt(x) {
|
|
2068
2138
|
return this.map.get(x)?.p ?? 0;
|
|
@@ -2144,9 +2214,8 @@ var _PMF = class _PMF {
|
|
|
2144
2214
|
}
|
|
2145
2215
|
tailProbGE(t) {
|
|
2146
2216
|
let s = 0;
|
|
2147
|
-
for (const [x,
|
|
2148
|
-
|
|
2149
|
-
if (p > 0 && x >= t) s += p;
|
|
2217
|
+
for (const [x, bin] of this) {
|
|
2218
|
+
if (bin.p > 0 && x >= t) s += bin.p;
|
|
2150
2219
|
}
|
|
2151
2220
|
return s;
|
|
2152
2221
|
}
|
|
@@ -2207,6 +2276,11 @@ var _PMF = class _PMF {
|
|
|
2207
2276
|
* - pAny: Probability that at least one success occurred
|
|
2208
2277
|
*/
|
|
2209
2278
|
static firstSuccessWeights(pSuccess, pSpecial, n) {
|
|
2279
|
+
if (!Number.isFinite(pSuccess) || !Number.isFinite(pSpecial) || pSuccess < 0 || pSuccess > 1 || pSpecial < 0 || pSpecial - pSuccess > EPS) {
|
|
2280
|
+
throw new Error(
|
|
2281
|
+
`firstSuccessWeights: require 0 <= pSpecial <= pSuccess <= 1 (got pSuccess=${pSuccess}, pSpecial=${pSpecial})`
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2210
2284
|
const pFail = 1 - pSuccess;
|
|
2211
2285
|
const pFailAll = Math.pow(pFail, n);
|
|
2212
2286
|
const pAny = 1 - pFailAll;
|
|
@@ -2222,13 +2296,12 @@ var _PMF = class _PMF {
|
|
|
2222
2296
|
const round = (x) => rounding === "floor" ? Math.floor(x) : rounding === "ceil" ? Math.ceil(x) : rounding === "round" ? Math.round(x) : x;
|
|
2223
2297
|
const probs = /* @__PURE__ */ new Map();
|
|
2224
2298
|
const counts = /* @__PURE__ */ new Map();
|
|
2225
|
-
for (const [v,
|
|
2226
|
-
if (Math.abs(
|
|
2299
|
+
for (const [v, bin] of this) {
|
|
2300
|
+
if (Math.abs(bin.p) < eps) continue;
|
|
2227
2301
|
const u = round(f(v));
|
|
2228
|
-
probs.set(u, (probs.get(u) ?? 0) +
|
|
2302
|
+
probs.set(u, (probs.get(u) ?? 0) + bin.p);
|
|
2229
2303
|
if (preserveCounts) {
|
|
2230
|
-
const
|
|
2231
|
-
const src = typeof rec2 === "number" ? void 0 : rec2?.count;
|
|
2304
|
+
const src = bin.count;
|
|
2232
2305
|
if (src) {
|
|
2233
2306
|
const dest = counts.get(u) ?? {};
|
|
2234
2307
|
for (const k in src) {
|
|
@@ -2281,17 +2354,15 @@ var _PMF = class _PMF {
|
|
|
2281
2354
|
}
|
|
2282
2355
|
};
|
|
2283
2356
|
// Unique ID generator for anonymous PMFs to avoid cache key collisions
|
|
2284
|
-
|
|
2357
|
+
_PMF.__anonIdCounter = 1;
|
|
2285
2358
|
var PMF = _PMF;
|
|
2286
2359
|
|
|
2287
2360
|
// src/pmf/mixture.ts
|
|
2288
2361
|
var Mixture = class _Mixture {
|
|
2289
2362
|
constructor(eps = EPS) {
|
|
2290
|
-
|
|
2363
|
+
this.totals = /* @__PURE__ */ new Map();
|
|
2291
2364
|
// raw mass per outcome (pre-normalization)
|
|
2292
|
-
|
|
2293
|
-
// raw mass per outcome per label
|
|
2294
|
-
__publicField(this, "eps");
|
|
2365
|
+
this.labelMass = /* @__PURE__ */ new Map();
|
|
2295
2366
|
this.eps = Number.isFinite(eps) ? eps : EPS;
|
|
2296
2367
|
}
|
|
2297
2368
|
/** Remove all accumulated state. */
|
|
@@ -2315,9 +2386,8 @@ var Mixture = class _Mixture {
|
|
|
2315
2386
|
*/
|
|
2316
2387
|
add(label, pmf, weight = 1) {
|
|
2317
2388
|
if (!Number.isFinite(weight) || weight <= 0) return this;
|
|
2318
|
-
for (const [v,
|
|
2319
|
-
const
|
|
2320
|
-
const p = isNumber ? binOrNumber : binOrNumber?.p ?? 0;
|
|
2389
|
+
for (const [v, bin] of pmf) {
|
|
2390
|
+
const p = bin.p;
|
|
2321
2391
|
if (p <= 0) continue;
|
|
2322
2392
|
const add = weight * p;
|
|
2323
2393
|
if (!Number.isFinite(add) || Math.abs(add) < this.eps) continue;
|
|
@@ -2405,14 +2475,28 @@ var Mixture = class _Mixture {
|
|
|
2405
2475
|
}
|
|
2406
2476
|
};
|
|
2407
2477
|
|
|
2478
|
+
// src/common/errors.ts
|
|
2479
|
+
var DiceParseError = class _DiceParseError extends Error {
|
|
2480
|
+
constructor(message, options) {
|
|
2481
|
+
super(message);
|
|
2482
|
+
this.name = "DiceParseError";
|
|
2483
|
+
this.expression = options?.expression;
|
|
2484
|
+
this.cause = options?.cause;
|
|
2485
|
+
Object.setPrototypeOf(this, _DiceParseError.prototype);
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
|
|
2408
2489
|
// src/parser/dice.ts
|
|
2490
|
+
var MAX_BINARY_OUTCOMES = 1e8;
|
|
2409
2491
|
var Dice = class _Dice {
|
|
2410
2492
|
constructor(x = 0) {
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2493
|
+
this.faces = {};
|
|
2494
|
+
this.privateData = {};
|
|
2495
|
+
// Partial: the object starts empty and gains keys as outcomes are recorded,
|
|
2496
|
+
// so the type must not claim every OutcomeType is present. (Previously typed
|
|
2497
|
+
// as a full Record via an `as` cast, which lied about missing keys.)
|
|
2498
|
+
this.outcomeData = {};
|
|
2499
|
+
this.hasHitDistributionCalculated = false;
|
|
2416
2500
|
if (x <= 0) return;
|
|
2417
2501
|
for (let i = 1; i <= x; i++) {
|
|
2418
2502
|
this.faces[i] = 1;
|
|
@@ -2463,27 +2547,27 @@ var Dice = class _Dice {
|
|
|
2463
2547
|
// TODO this can be private later if we change how testing works
|
|
2464
2548
|
calculateHitDistribution() {
|
|
2465
2549
|
const hitValues = {};
|
|
2550
|
+
const subtractedOutcomes = [
|
|
2551
|
+
this.outcomeData.crit,
|
|
2552
|
+
this.outcomeData.missNone,
|
|
2553
|
+
this.outcomeData.missDamage,
|
|
2554
|
+
this.outcomeData.saveHalf,
|
|
2555
|
+
this.outcomeData.saveFail,
|
|
2556
|
+
this.outcomeData.pc
|
|
2557
|
+
];
|
|
2466
2558
|
for (const [face, totalCount] of Object.entries(this.faces)) {
|
|
2467
2559
|
const numFace = Number(face);
|
|
2468
2560
|
let hitCount = totalCount;
|
|
2469
|
-
for (const
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
"saveHalf",
|
|
2474
|
-
"saveFail",
|
|
2475
|
-
"pc"
|
|
2476
|
-
]) {
|
|
2477
|
-
const distribution = this.getOutcomeDistribution(outcomeType);
|
|
2478
|
-
if (distribution && distribution[numFace]) {
|
|
2479
|
-
hitCount -= distribution[numFace];
|
|
2561
|
+
for (const distribution of subtractedOutcomes) {
|
|
2562
|
+
const outcomeCount = distribution?.[numFace];
|
|
2563
|
+
if (outcomeCount) {
|
|
2564
|
+
hitCount -= outcomeCount;
|
|
2480
2565
|
}
|
|
2481
2566
|
}
|
|
2482
2567
|
if (numFace === 0) {
|
|
2483
2568
|
hitCount = 0;
|
|
2484
2569
|
}
|
|
2485
2570
|
if (hitCount < 0) {
|
|
2486
|
-
console.error("hitCount is <=0?", face, totalCount, hitCount);
|
|
2487
2571
|
hitCount = 0;
|
|
2488
2572
|
}
|
|
2489
2573
|
hitValues[numFace] = hitCount;
|
|
@@ -2502,13 +2586,18 @@ var Dice = class _Dice {
|
|
|
2502
2586
|
const result = diceConstructor ? diceConstructor() : new _Dice();
|
|
2503
2587
|
const isScalar = typeof other === "number";
|
|
2504
2588
|
const keys1 = this.keys();
|
|
2589
|
+
const keys2 = isScalar ? [] : other.keys();
|
|
2590
|
+
if (!isScalar && keys1.length * keys2.length > MAX_BINARY_OUTCOMES) {
|
|
2591
|
+
throw new DiceParseError(
|
|
2592
|
+
`Dice operation over ${keys1.length}\xD7${keys2.length} face pairs exceeds the maximum of ${MAX_BINARY_OUTCOMES}`
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2505
2595
|
for (const key1 of keys1) {
|
|
2506
2596
|
const value1 = this.faces[key1];
|
|
2507
2597
|
if (isScalar) {
|
|
2508
2598
|
const resultKey = op(key1, other);
|
|
2509
2599
|
result.increment(resultKey, value1);
|
|
2510
2600
|
} else {
|
|
2511
|
-
const keys2 = other.keys();
|
|
2512
2601
|
for (const key2 of keys2) {
|
|
2513
2602
|
const value2 = other.faces[key2];
|
|
2514
2603
|
const resultKey = op(key1, key2);
|
|
@@ -2530,7 +2619,7 @@ var Dice = class _Dice {
|
|
|
2530
2619
|
result.outcomeData = { ...this.outcomeData };
|
|
2531
2620
|
return result;
|
|
2532
2621
|
}
|
|
2533
|
-
// PUBLIC
|
|
2622
|
+
// PUBLIC FUNCTIONS
|
|
2534
2623
|
getFaceEntries() {
|
|
2535
2624
|
return Object.entries(this.faces).map(([k, v]) => [Number(k), v]);
|
|
2536
2625
|
}
|
|
@@ -2660,10 +2749,12 @@ var Dice = class _Dice {
|
|
|
2660
2749
|
}
|
|
2661
2750
|
reroll(toReroll) {
|
|
2662
2751
|
const rerollDice = typeof toReroll === "number" ? _Dice.scalar(toReroll) : toReroll;
|
|
2663
|
-
const
|
|
2752
|
+
const rerollKeys = rerollDice.keys();
|
|
2753
|
+
const rerollSet = new Set(rerollKeys);
|
|
2754
|
+
const removed = this.removeFaces(rerollKeys);
|
|
2664
2755
|
let result = new _Dice();
|
|
2665
2756
|
for (const face of this.keys()) {
|
|
2666
|
-
const wasRerolled =
|
|
2757
|
+
const wasRerolled = rerollSet.has(face);
|
|
2667
2758
|
result = result.combine(removed);
|
|
2668
2759
|
if (wasRerolled) {
|
|
2669
2760
|
result = result.combine(this);
|
|
@@ -2731,17 +2822,10 @@ var Dice = class _Dice {
|
|
|
2731
2822
|
const missDistro = this.getOutcomeDistribution("missDamage") || {};
|
|
2732
2823
|
const saveDistro = this.getOutcomeDistribution("saveHalf") || {};
|
|
2733
2824
|
const pcDistro = this.getOutcomeDistribution("pc") || {};
|
|
2734
|
-
|
|
2735
|
-
for (const halfDamage of Object.keys(saveDistro).map(Number)) {
|
|
2736
|
-
const fullDamage = halfDamage * 2;
|
|
2737
|
-
if (fullDamage > 0 && hitDistro[fullDamage]) {
|
|
2738
|
-
isSaveHalf = true;
|
|
2739
|
-
break;
|
|
2740
|
-
}
|
|
2741
|
-
}
|
|
2825
|
+
const isSaveHalf = Object.keys(saveDistro).length > 0;
|
|
2742
2826
|
const isDCCheck = this.privateData.isDCCheck === true;
|
|
2743
2827
|
const clampNonNeg = (x) => x < 0 && x > -1e-15 ? 0 : x;
|
|
2744
|
-
for (const [faceStr, faceCountRaw] of Object.entries(this.
|
|
2828
|
+
for (const [faceStr, faceCountRaw] of Object.entries(this.faces)) {
|
|
2745
2829
|
const face = Number(faceStr);
|
|
2746
2830
|
const faceCount = Number(faceCountRaw);
|
|
2747
2831
|
if (faceCount <= 0) continue;
|
|
@@ -2815,14 +2899,14 @@ var Dice = class _Dice {
|
|
|
2815
2899
|
map.set(face, bin);
|
|
2816
2900
|
}
|
|
2817
2901
|
const identifier = this.identifier || "ERROR";
|
|
2818
|
-
if (identifier === "ERROR") {
|
|
2819
|
-
console.error("Dice identifier is undefined", this);
|
|
2820
|
-
}
|
|
2821
2902
|
return new PMF(map, numEpsilon, true, identifier).compact(numEpsilon, true);
|
|
2822
2903
|
}
|
|
2823
2904
|
};
|
|
2824
2905
|
|
|
2825
2906
|
// src/parser/parser.ts
|
|
2907
|
+
var MAX_DIE_SIDES = 1e6;
|
|
2908
|
+
var MAX_DICE_COUNT = 1e4;
|
|
2909
|
+
var MAX_KEEP_OUTCOMES = 1e6;
|
|
2826
2910
|
var parseCache = new LRUCache(1e3);
|
|
2827
2911
|
function parse(expression, n = 0) {
|
|
2828
2912
|
const cleaned = expression.replace(/ /g, "").toLowerCase();
|
|
@@ -2832,20 +2916,21 @@ function parse(expression, n = 0) {
|
|
|
2832
2916
|
if (cached) return cached;
|
|
2833
2917
|
}
|
|
2834
2918
|
const chars = [...cleaned];
|
|
2835
|
-
let result
|
|
2919
|
+
let result;
|
|
2836
2920
|
try {
|
|
2837
2921
|
result = parseExpression(chars, n);
|
|
2838
2922
|
} catch (error) {
|
|
2839
|
-
throw new
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
result.identifier = cleaned;
|
|
2844
|
-
} catch {
|
|
2923
|
+
throw new DiceParseError(
|
|
2924
|
+
`Cannot parse dice expression [${expression}]: ${error}`,
|
|
2925
|
+
{ expression, cause: error }
|
|
2926
|
+
);
|
|
2845
2927
|
}
|
|
2928
|
+
result.privateData = result.privateData || {};
|
|
2929
|
+
result.identifier = cleaned;
|
|
2846
2930
|
if (chars.length > 0) {
|
|
2847
|
-
throw new
|
|
2848
|
-
`Unexpected token: '${chars[0]}' from expression: '${expression}'
|
|
2931
|
+
throw new DiceParseError(
|
|
2932
|
+
`Unexpected token: '${chars[0]}' from expression: '${expression}'`,
|
|
2933
|
+
{ expression }
|
|
2849
2934
|
);
|
|
2850
2935
|
}
|
|
2851
2936
|
const resultPMF = result.toPMF(-1);
|
|
@@ -2999,7 +3084,7 @@ function multiplyDiceByDice(d1, d2) {
|
|
|
2999
3084
|
if (typeof d1 === "number") d1 = Dice.scalar(d1);
|
|
3000
3085
|
if (typeof d2 === "number") d2 = Dice.scalar(d2);
|
|
3001
3086
|
const result = new Dice();
|
|
3002
|
-
const faces =
|
|
3087
|
+
const faces = /* @__PURE__ */ new Map();
|
|
3003
3088
|
let normalizationFactor = 1;
|
|
3004
3089
|
for (const key of d1.keys()) {
|
|
3005
3090
|
let face;
|
|
@@ -3007,17 +3092,21 @@ function multiplyDiceByDice(d1, d2) {
|
|
|
3007
3092
|
continue;
|
|
3008
3093
|
}
|
|
3009
3094
|
if (d2.privateData.keep) {
|
|
3095
|
+
const faceCount = d2.keys().length;
|
|
3096
|
+
if (Math.pow(faceCount, key) > MAX_KEEP_OUTCOMES) {
|
|
3097
|
+
throw new DiceParseError(
|
|
3098
|
+
`Keep enumeration of ${faceCount}^${key} outcomes exceeds the maximum of ${MAX_KEEP_OUTCOMES}`
|
|
3099
|
+
);
|
|
3100
|
+
}
|
|
3010
3101
|
const repeat = Array(key).fill(d2);
|
|
3011
3102
|
face = opDice(repeat, d2.privateData.keep);
|
|
3012
3103
|
} else {
|
|
3013
3104
|
face = multiplyDice(key, d2);
|
|
3014
3105
|
}
|
|
3015
3106
|
normalizationFactor *= face.total();
|
|
3016
|
-
faces
|
|
3107
|
+
faces.set(key, face);
|
|
3017
3108
|
}
|
|
3018
|
-
for (const
|
|
3019
|
-
const k = parseFloat(key);
|
|
3020
|
-
const face = faces[k];
|
|
3109
|
+
for (const [k, face] of faces) {
|
|
3021
3110
|
const count = d1.get(k);
|
|
3022
3111
|
result.combineInPlace(
|
|
3023
3112
|
face.normalize(count * normalizationFactor / face.total())
|
|
@@ -3027,6 +3116,11 @@ function multiplyDiceByDice(d1, d2) {
|
|
|
3027
3116
|
return result;
|
|
3028
3117
|
}
|
|
3029
3118
|
function multiplyDice(n, d2) {
|
|
3119
|
+
if (n > MAX_DICE_COUNT) {
|
|
3120
|
+
throw new DiceParseError(
|
|
3121
|
+
`Dice count ${n} exceeds the maximum of ${MAX_DICE_COUNT}`
|
|
3122
|
+
);
|
|
3123
|
+
}
|
|
3030
3124
|
if (n === 0) return new Dice(0);
|
|
3031
3125
|
if (n === 1) return d2;
|
|
3032
3126
|
const half = Math.floor(n / 2);
|
|
@@ -3109,9 +3203,14 @@ function parseDice(s, n) {
|
|
|
3109
3203
|
return;
|
|
3110
3204
|
}
|
|
3111
3205
|
const sides = parseNumber(s, n);
|
|
3206
|
+
if (sides > MAX_DIE_SIDES) {
|
|
3207
|
+
throw new DiceParseError(
|
|
3208
|
+
`Die size ${sides} exceeds the maximum of ${MAX_DIE_SIDES}`
|
|
3209
|
+
);
|
|
3210
|
+
}
|
|
3112
3211
|
let result = new Dice(sides);
|
|
3113
3212
|
if (rerollOne) {
|
|
3114
|
-
result = result.
|
|
3213
|
+
result = result.reroll(1);
|
|
3115
3214
|
}
|
|
3116
3215
|
return result;
|
|
3117
3216
|
}
|
|
@@ -3323,15 +3422,14 @@ var configComplexityScore = (config) => {
|
|
|
3323
3422
|
};
|
|
3324
3423
|
var RollBuilder = class _RollBuilder {
|
|
3325
3424
|
constructor(countOrConfigs = 1) {
|
|
3326
|
-
__publicField(this, "subRollConfigs");
|
|
3327
3425
|
// --- Dice Shortcut Methods ---
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3426
|
+
this.d4 = () => this.d(4);
|
|
3427
|
+
this.d6 = () => this.d(6);
|
|
3428
|
+
this.d8 = () => this.d(8);
|
|
3429
|
+
this.d10 = () => this.d(10);
|
|
3430
|
+
this.d12 = () => this.d(12);
|
|
3431
|
+
this.d20 = () => this.d(20);
|
|
3432
|
+
this.d100 = () => this.d(100);
|
|
3335
3433
|
if (typeof countOrConfigs === "number") {
|
|
3336
3434
|
const count = countOrConfigs;
|
|
3337
3435
|
if (isNaN(count)) throw new Error("Invalid NaN value for count");
|
|
@@ -3838,10 +3936,10 @@ var RollBuilder = class _RollBuilder {
|
|
|
3838
3936
|
}
|
|
3839
3937
|
// These methods are implemented via prototype augmentation in ac.ts and dc.ts
|
|
3840
3938
|
// They are declared here to provide proper TypeScript types
|
|
3841
|
-
ac(
|
|
3939
|
+
ac(_targetAC) {
|
|
3842
3940
|
throw new Error("ac() should be implemented via prototype augmentation");
|
|
3843
3941
|
}
|
|
3844
|
-
dc(
|
|
3942
|
+
dc(_saveDC) {
|
|
3845
3943
|
throw new Error("dc() should be implemented via prototype augmentation");
|
|
3846
3944
|
}
|
|
3847
3945
|
};
|
|
@@ -3958,7 +4056,6 @@ var AlwaysHitBuilder = class _AlwaysHitBuilder extends RollBuilder {
|
|
|
3958
4056
|
);
|
|
3959
4057
|
}
|
|
3960
4058
|
super(baseRoll.getSubRollConfigs());
|
|
3961
|
-
__publicField(this, "attackConfig");
|
|
3962
4059
|
if (attackConfig) {
|
|
3963
4060
|
this.attackConfig = { ...attackConfig };
|
|
3964
4061
|
} else {
|
|
@@ -4008,8 +4105,6 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
|
|
|
4008
4105
|
);
|
|
4009
4106
|
}
|
|
4010
4107
|
super(baseRoll.getSubRollConfigs());
|
|
4011
|
-
__publicField(this, "attackConfig");
|
|
4012
|
-
__publicField(this, "fromAlwaysHit");
|
|
4013
4108
|
if (attackConfig) {
|
|
4014
4109
|
this.attackConfig = { ...attackConfig };
|
|
4015
4110
|
} else {
|
|
@@ -4051,8 +4146,6 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
|
|
|
4051
4146
|
var ParsedRollBuilder = class _ParsedRollBuilder extends RollBuilder {
|
|
4052
4147
|
constructor(expression) {
|
|
4053
4148
|
super([]);
|
|
4054
|
-
__publicField(this, "cachedPMF");
|
|
4055
|
-
__publicField(this, "originalExpression");
|
|
4056
4149
|
this.originalExpression = expression;
|
|
4057
4150
|
this.cachedPMF = parse(expression, 0);
|
|
4058
4151
|
}
|
|
@@ -4302,7 +4395,11 @@ function astFromRollConfigs(configs) {
|
|
|
4302
4395
|
if (trials === 1) {
|
|
4303
4396
|
node = perTrial;
|
|
4304
4397
|
} else {
|
|
4305
|
-
node = {
|
|
4398
|
+
node = {
|
|
4399
|
+
type: "maxOf",
|
|
4400
|
+
count: trials,
|
|
4401
|
+
child: perTrial
|
|
4402
|
+
};
|
|
4306
4403
|
}
|
|
4307
4404
|
} else if (trials === baseCount) {
|
|
4308
4405
|
const base = { type: "sum", count: trials, child: node };
|
|
@@ -4507,7 +4604,7 @@ function computeMaxOfPMF(pmf, count, eps = defaultEps) {
|
|
|
4507
4604
|
const support = pmf.support();
|
|
4508
4605
|
const out = /* @__PURE__ */ new Map();
|
|
4509
4606
|
if (count <= 6 && support.length <= 20) {
|
|
4510
|
-
let
|
|
4607
|
+
let dfs2 = function(rollsLeft, currentMax, probability) {
|
|
4511
4608
|
if (rollsLeft === 0) {
|
|
4512
4609
|
out.set(currentMax, (out.get(currentMax) || 0) + probability);
|
|
4513
4610
|
return;
|
|
@@ -4516,17 +4613,18 @@ function computeMaxOfPMF(pmf, count, eps = defaultEps) {
|
|
|
4516
4613
|
const p = pmf.pAt(value);
|
|
4517
4614
|
if (p > 0) {
|
|
4518
4615
|
const newMax = Math.max(currentMax, value);
|
|
4519
|
-
|
|
4616
|
+
dfs2(rollsLeft - 1, newMax, probability * p);
|
|
4520
4617
|
}
|
|
4521
4618
|
}
|
|
4522
4619
|
};
|
|
4523
|
-
|
|
4620
|
+
dfs2(count, -Infinity, 1);
|
|
4524
4621
|
} else {
|
|
4525
4622
|
const sortedSupport = [...support].sort((a, b) => a - b);
|
|
4623
|
+
let runningCdf = 0;
|
|
4526
4624
|
for (const value of sortedSupport) {
|
|
4527
|
-
const
|
|
4528
|
-
|
|
4529
|
-
const probMax = Math.pow(
|
|
4625
|
+
const prevCdf = runningCdf;
|
|
4626
|
+
runningCdf += pmf.pAt(value);
|
|
4627
|
+
const probMax = Math.pow(runningCdf, count) - Math.pow(prevCdf, count);
|
|
4530
4628
|
if (probMax > eps) {
|
|
4531
4629
|
out.set(value, probMax);
|
|
4532
4630
|
}
|
|
@@ -4553,7 +4651,8 @@ function keepSumPMF(single, total, keep, highest, eps = defaultEps) {
|
|
|
4553
4651
|
}
|
|
4554
4652
|
}
|
|
4555
4653
|
let state = /* @__PURE__ */ new Map();
|
|
4556
|
-
const
|
|
4654
|
+
const stride = total + 1;
|
|
4655
|
+
const keyOf = (used, r) => used * stride + r;
|
|
4557
4656
|
state.set(keyOf(0, total), /* @__PURE__ */ new Map([[0, 1]]));
|
|
4558
4657
|
const valuesDesc = highest ? [...sortedSupport].sort((a, b) => b - a) : [...sortedSupport].sort((a, b) => a - b);
|
|
4559
4658
|
const binomPMF = (r, p) => {
|
|
@@ -4602,9 +4701,8 @@ function keepSumPMF(single, total, keep, highest, eps = defaultEps) {
|
|
|
4602
4701
|
const pCond = Math.min(1, p / q);
|
|
4603
4702
|
const next = /* @__PURE__ */ new Map();
|
|
4604
4703
|
for (const [k, m] of state) {
|
|
4605
|
-
const
|
|
4606
|
-
const
|
|
4607
|
-
const r = parseInt(rStr, 10);
|
|
4704
|
+
const used = Math.floor(k / stride);
|
|
4705
|
+
const r = k - used * stride;
|
|
4608
4706
|
if (r === 0) {
|
|
4609
4707
|
const destKey = keyOf(used, 0);
|
|
4610
4708
|
const dest = next.get(destKey) ?? /* @__PURE__ */ new Map();
|
|
@@ -4775,8 +4873,8 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
4775
4873
|
const bonusPMF2 = bonusDicePMFs2.length ? PMF.convolveMany(bonusDicePMFs2, eps) : PMF.delta(0, eps);
|
|
4776
4874
|
let pcrit2 = 0;
|
|
4777
4875
|
let pmiss2 = 0;
|
|
4778
|
-
for (const [r,
|
|
4779
|
-
const pr =
|
|
4876
|
+
for (const [r, bin] of d202) {
|
|
4877
|
+
const pr = bin.p;
|
|
4780
4878
|
if (pr <= 0) continue;
|
|
4781
4879
|
if (r === 1) {
|
|
4782
4880
|
pmiss2 += pr;
|
|
@@ -4791,8 +4889,8 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
4791
4889
|
}
|
|
4792
4890
|
if (check instanceof AlwaysHitBuilder) {
|
|
4793
4891
|
let pCrit = 0;
|
|
4794
|
-
for (const [r,
|
|
4795
|
-
const pr =
|
|
4892
|
+
for (const [r, bin] of d202) {
|
|
4893
|
+
const pr = bin.p;
|
|
4796
4894
|
if (pr <= 0) continue;
|
|
4797
4895
|
if (r >= critThreshold) pCrit += pr;
|
|
4798
4896
|
}
|
|
@@ -4807,8 +4905,8 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
4807
4905
|
let pcrit = 0;
|
|
4808
4906
|
let phit = 0;
|
|
4809
4907
|
let pmiss = 0;
|
|
4810
|
-
for (const [r,
|
|
4811
|
-
const pr =
|
|
4908
|
+
for (const [r, bin] of d202) {
|
|
4909
|
+
const pr = bin.p;
|
|
4812
4910
|
if (pr <= 0) continue;
|
|
4813
4911
|
if (r === 1) {
|
|
4814
4912
|
pmiss += pr;
|
|
@@ -4888,7 +4986,6 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
4888
4986
|
var ACBuilder = class _ACBuilder extends RollBuilder {
|
|
4889
4987
|
constructor(baseRoll, ac, attackConfig) {
|
|
4890
4988
|
super(baseRoll.getSubRollConfigs());
|
|
4891
|
-
__publicField(this, "attackConfig");
|
|
4892
4989
|
if (attackConfig) {
|
|
4893
4990
|
this.attackConfig = { ...attackConfig, ac };
|
|
4894
4991
|
} else {
|
|
@@ -5017,8 +5114,8 @@ function resolveProbabilities(check) {
|
|
|
5017
5114
|
const baseReroll = check.baseReroll;
|
|
5018
5115
|
const die = d20RollPMF(d20Type, baseReroll > 0);
|
|
5019
5116
|
const faceP = /* @__PURE__ */ new Map();
|
|
5020
|
-
for (const [r,
|
|
5021
|
-
const pr =
|
|
5117
|
+
for (const [r, bin] of die) {
|
|
5118
|
+
const pr = bin.p;
|
|
5022
5119
|
if (pr > 0) faceP.set(r, pr);
|
|
5023
5120
|
}
|
|
5024
5121
|
const eps = 0;
|
|
@@ -5039,7 +5136,6 @@ function resolveProbabilities(check) {
|
|
|
5039
5136
|
var DCBuilder = class _DCBuilder extends RollBuilder {
|
|
5040
5137
|
constructor(baseRoll, saveConfig) {
|
|
5041
5138
|
super(baseRoll.getSubRollConfigs());
|
|
5042
|
-
__publicField(this, "saveConfig");
|
|
5043
5139
|
this.saveConfig = saveConfig ? { ...saveConfig } : { dc: 10 };
|
|
5044
5140
|
}
|
|
5045
5141
|
dc(saveDC) {
|
|
@@ -5088,8 +5184,8 @@ var DCBuilder = class _DCBuilder extends RollBuilder {
|
|
|
5088
5184
|
);
|
|
5089
5185
|
const bonusPMF = bonusDicePMFs.length ? PMF.convolveMany(bonusDicePMFs, eps) : PMF.delta(0, eps);
|
|
5090
5186
|
let psuccess = 0;
|
|
5091
|
-
for (const [r,
|
|
5092
|
-
const pr =
|
|
5187
|
+
for (const [r, bin] of d202) {
|
|
5188
|
+
const pr = bin.p;
|
|
5093
5189
|
if (pr <= 0) continue;
|
|
5094
5190
|
const need = saveDC - staticMod - r;
|
|
5095
5191
|
psuccess += pr * bonusPMF.tailProbGE(need);
|