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