@yipe/dice 0.1.1 → 0.1.3
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 +1 -3
- package/dist/index.cjs +248 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -97
- package/dist/index.d.ts +72 -97
- package/dist/index.js +246 -58
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
[](LICENSE)
|
|
3
3
|

|
|
4
4
|

|
|
5
|
-

|
|
5
|
+

|
|
6
6
|
|
|
7
7
|

|
|
8
8
|

|
|
@@ -33,8 +33,6 @@ console.log("DPR:", attack.mean());
|
|
|
33
33
|
|
|
34
34
|
## 🚀 Installation
|
|
35
35
|
|
|
36
|
-
(COMING SOON!)
|
|
37
|
-
|
|
38
36
|
```bash
|
|
39
37
|
# Install with npm or yarn
|
|
40
38
|
npm install @yipe/dice
|
package/dist/index.cjs
CHANGED
|
@@ -47,20 +47,19 @@ var LRUCache = class {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
// src/types.ts
|
|
50
|
-
var
|
|
51
|
-
var TEST_EPS = 1e-10;
|
|
50
|
+
var EPS = 1e-12;
|
|
52
51
|
|
|
53
52
|
// src/query.ts
|
|
54
53
|
var _DiceQuery = class _DiceQuery {
|
|
55
|
-
constructor(singles, combined) {
|
|
54
|
+
constructor(singles, combined, eps = EPS) {
|
|
56
55
|
__publicField(this, "singles");
|
|
57
56
|
__publicField(this, "combined");
|
|
58
57
|
this.singles = Array.isArray(singles) ? singles : [singles];
|
|
59
|
-
const c = combined ?? PMF.convolveMany(this.singles);
|
|
60
|
-
this.combined = Math.abs(c.mass() - 1) <= 1e-12 ? c : c.normalize();
|
|
61
58
|
if (this.singles.some((s) => s === void 0)) {
|
|
62
59
|
throw new Error("DiceQuery contains undefined singles");
|
|
63
60
|
}
|
|
61
|
+
const c = combined ?? PMF.convolveMany(this.singles);
|
|
62
|
+
this.combined = Math.abs(c.mass() - 1) <= eps ? c : c.normalize();
|
|
64
63
|
}
|
|
65
64
|
/**
|
|
66
65
|
* Returns the expected damage across all possible outcomes.
|
|
@@ -358,23 +357,16 @@ var _DiceQuery = class _DiceQuery {
|
|
|
358
357
|
* - "How much damage comes from miss effects (like save-for-half spells)?"
|
|
359
358
|
*/
|
|
360
359
|
expectedDamageFrom(labels) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
360
|
+
const wanted = Array.isArray(labels) ? labels : [labels];
|
|
361
|
+
let total = 0;
|
|
362
|
+
for (const single of this.singles) {
|
|
363
|
+
for (const [dmg, bin] of single) {
|
|
364
|
+
let p = 0;
|
|
365
|
+
for (const label of wanted) p += bin.count[label] ?? 0;
|
|
366
|
+
total += dmg * p;
|
|
368
367
|
}
|
|
369
368
|
}
|
|
370
|
-
return
|
|
371
|
-
}
|
|
372
|
-
_expectedDamageFromSingle(label) {
|
|
373
|
-
let totalExpectedDamage = 0;
|
|
374
|
-
for (const [, probabilityBin] of this.combined) {
|
|
375
|
-
totalExpectedDamage += probabilityBin.attr?.[label] || 0;
|
|
376
|
-
}
|
|
377
|
-
return totalExpectedDamage;
|
|
369
|
+
return total;
|
|
378
370
|
}
|
|
379
371
|
/**
|
|
380
372
|
* Returns damage statistics for scenarios where AT LEAST ONE attack results in
|
|
@@ -597,7 +589,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
597
589
|
* query.toStackedChartData(['hit', 'crit'])
|
|
598
590
|
* // → {labels: [0, 6, 12, ...], datasets: [{label: 'hit', data: [0, 0.03, ...]}, ...]}
|
|
599
591
|
*/
|
|
600
|
-
toStackedChartData(labels = [], epsilon =
|
|
592
|
+
toStackedChartData(labels = [], epsilon = EPS) {
|
|
601
593
|
const damageValues = this.combined.support();
|
|
602
594
|
damageValues.map((dmg) => {
|
|
603
595
|
const bin = this.combined.map.get(dmg);
|
|
@@ -1229,7 +1221,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1229
1221
|
*
|
|
1230
1222
|
* Returns tuple: [pFirstNonSubset, pFirstSubset, pAnySuccess, pNone]
|
|
1231
1223
|
*/
|
|
1232
|
-
firstSuccessSplit(successOutcome, subsetOutcome, eps =
|
|
1224
|
+
firstSuccessSplit(successOutcome, subsetOutcome, eps = EPS) {
|
|
1233
1225
|
const pmfs = this.singles;
|
|
1234
1226
|
if (!pmfs.length) {
|
|
1235
1227
|
throw new Error("firstSuccessSplitFromPMFs: pmfs must be non-empty");
|
|
@@ -1278,15 +1270,14 @@ __publicField(_DiceQuery, "DEFAULT_OUTCOMES", [
|
|
|
1278
1270
|
"missNone"
|
|
1279
1271
|
]);
|
|
1280
1272
|
var DiceQuery = _DiceQuery;
|
|
1281
|
-
var EPSILON = COMPUTATIONAL_EPS;
|
|
1282
1273
|
var pmfCache = new LRUCache(1e3);
|
|
1283
1274
|
var _PMF = class _PMF {
|
|
1284
|
-
constructor(map = /* @__PURE__ */ new Map(), epsilon =
|
|
1275
|
+
constructor(map = /* @__PURE__ */ new Map(), epsilon = EPS, normalized = false, identifier = `anon#${_PMF.__anonIdCounter++}`, _preservedProvenance = true) {
|
|
1285
1276
|
this.map = map;
|
|
1286
1277
|
this.epsilon = epsilon;
|
|
1287
1278
|
this.normalized = normalized;
|
|
1288
1279
|
this.identifier = identifier;
|
|
1289
|
-
this.
|
|
1280
|
+
this._preservedProvenance = _preservedProvenance;
|
|
1290
1281
|
// Cached computed values
|
|
1291
1282
|
__publicField(this, "_support");
|
|
1292
1283
|
__publicField(this, "_min");
|
|
@@ -1296,15 +1287,18 @@ var _PMF = class _PMF {
|
|
|
1296
1287
|
__publicField(this, "_variance");
|
|
1297
1288
|
__publicField(this, "_stdev");
|
|
1298
1289
|
}
|
|
1299
|
-
static empty(epsilon =
|
|
1290
|
+
static empty(epsilon = EPS, identifier = "empty") {
|
|
1300
1291
|
return new _PMF(/* @__PURE__ */ new Map(), epsilon, false, identifier);
|
|
1301
1292
|
}
|
|
1302
1293
|
// This has a single bin at value 0, mass of 1
|
|
1303
|
-
static zero(epsilon =
|
|
1294
|
+
static zero(epsilon = EPS) {
|
|
1304
1295
|
const m = /* @__PURE__ */ new Map();
|
|
1305
1296
|
m.set(0, { p: 1, count: { miss: 1 }, attr: {} });
|
|
1306
1297
|
return new _PMF(m, epsilon, false, "zero");
|
|
1307
1298
|
}
|
|
1299
|
+
static delta(value, epsilon = EPS) {
|
|
1300
|
+
return _PMF.fromMap(/* @__PURE__ */ new Map([[value, 1]]), epsilon);
|
|
1301
|
+
}
|
|
1308
1302
|
// This creates a single bin at value 0, but with weight 0.
|
|
1309
1303
|
static emptyMass() {
|
|
1310
1304
|
return _PMF.zero().scaleMass(0);
|
|
@@ -1407,7 +1401,7 @@ var _PMF = class _PMF {
|
|
|
1407
1401
|
* @param eps Optional tolerance for floating point rounding
|
|
1408
1402
|
* @returns A single PMF representing the exclusive mixture of all options.
|
|
1409
1403
|
*/
|
|
1410
|
-
static exclusive(options, eps =
|
|
1404
|
+
static exclusive(options, eps = EPS) {
|
|
1411
1405
|
const items = options.map(
|
|
1412
1406
|
(o) => Array.isArray(o) ? { pmf: o[0], weight: o[1] } : o
|
|
1413
1407
|
);
|
|
@@ -1437,8 +1431,7 @@ var _PMF = class _PMF {
|
|
|
1437
1431
|
* [pCrit, critPMF],
|
|
1438
1432
|
* ]);
|
|
1439
1433
|
*/
|
|
1440
|
-
static mixN(weights) {
|
|
1441
|
-
const eps = 1e-12;
|
|
1434
|
+
static mixN(weights, eps = EPS) {
|
|
1442
1435
|
const filtered = weights.filter(([w]) => w > eps);
|
|
1443
1436
|
if (filtered.length === 0) {
|
|
1444
1437
|
return _PMF.emptyMass();
|
|
@@ -1460,17 +1453,23 @@ var _PMF = class _PMF {
|
|
|
1460
1453
|
// This is a convenience method for when we use power
|
|
1461
1454
|
// TODO: It can be smarter in the future, and we can also add it to query
|
|
1462
1455
|
// That way statistics operations on invalid PMFs can throw an error
|
|
1463
|
-
// TODO… how can we detect if manually merging two queries' combined PMFs, as that loses
|
|
1464
|
-
|
|
1465
|
-
if (!this.
|
|
1456
|
+
// TODO… how can we detect if manually merging two queries' combined PMFs, as that loses provenance?
|
|
1457
|
+
setPreservedProvenance(preserved) {
|
|
1458
|
+
if (!this._preservedProvenance && preserved) {
|
|
1466
1459
|
throw new Error(
|
|
1467
|
-
"Preserved
|
|
1460
|
+
"Preserved provenance is already set to false, cannot fix that"
|
|
1468
1461
|
);
|
|
1469
1462
|
}
|
|
1470
|
-
this.
|
|
1463
|
+
this._preservedProvenance = preserved;
|
|
1471
1464
|
}
|
|
1472
|
-
|
|
1473
|
-
return this.
|
|
1465
|
+
preservedProvenance() {
|
|
1466
|
+
return this._preservedProvenance;
|
|
1467
|
+
}
|
|
1468
|
+
getPowerCacheKey(n, eps) {
|
|
1469
|
+
const id = this.identifier;
|
|
1470
|
+
let key = `${id}`;
|
|
1471
|
+
for (let i = 1; i < n; i++) key += `+${id}`;
|
|
1472
|
+
return `${key}@${eps}`;
|
|
1474
1473
|
}
|
|
1475
1474
|
/**
|
|
1476
1475
|
* Efficiently computes this PMF convolved with itself `n` times.
|
|
@@ -1488,7 +1487,7 @@ var _PMF = class _PMF {
|
|
|
1488
1487
|
}
|
|
1489
1488
|
if (n === 1) return this;
|
|
1490
1489
|
const epsilon = eps ?? this.epsilon;
|
|
1491
|
-
const key = getPowerCacheKey(
|
|
1490
|
+
const key = this.getPowerCacheKey(n, epsilon);
|
|
1492
1491
|
{
|
|
1493
1492
|
const cached = pmfCache?.get(key);
|
|
1494
1493
|
if (cached) return cached;
|
|
@@ -1505,7 +1504,7 @@ var _PMF = class _PMF {
|
|
|
1505
1504
|
base = base.convolve(base, epsilon);
|
|
1506
1505
|
}
|
|
1507
1506
|
}
|
|
1508
|
-
result.
|
|
1507
|
+
result.setPreservedProvenance(false);
|
|
1509
1508
|
{
|
|
1510
1509
|
pmfCache?.set(key, result);
|
|
1511
1510
|
}
|
|
@@ -1534,7 +1533,7 @@ var _PMF = class _PMF {
|
|
|
1534
1533
|
outcomeMass(outcome) {
|
|
1535
1534
|
let totalProbabilityMass = 0;
|
|
1536
1535
|
for (const { p, count } of this.map.values()) {
|
|
1537
|
-
totalProbabilityMass += p * count[outcome];
|
|
1536
|
+
totalProbabilityMass += p * (count[outcome] ?? 0);
|
|
1538
1537
|
}
|
|
1539
1538
|
return totalProbabilityMass;
|
|
1540
1539
|
}
|
|
@@ -1797,7 +1796,7 @@ var _PMF = class _PMF {
|
|
|
1797
1796
|
}
|
|
1798
1797
|
convolve(other, eps, raw = false) {
|
|
1799
1798
|
const epsilon = eps ?? this.epsilon;
|
|
1800
|
-
const norm = (x) => raw ? x : Math.abs(x.mass() - 1) <=
|
|
1799
|
+
const norm = (x) => raw ? x : Math.abs(x.mass() - 1) <= epsilon ? x : x.normalize();
|
|
1801
1800
|
const A0 = norm(this);
|
|
1802
1801
|
const B0 = norm(other);
|
|
1803
1802
|
const [A, B] = A0.identifier <= B0.identifier ? [A0, B0] : [B0, A0];
|
|
@@ -1832,14 +1831,13 @@ var _PMF = class _PMF {
|
|
|
1832
1831
|
epsilon,
|
|
1833
1832
|
!raw,
|
|
1834
1833
|
`${A.identifier}${raw ? "*" : "+"}${B.identifier}`
|
|
1835
|
-
// ← canonical order
|
|
1836
1834
|
);
|
|
1837
1835
|
const mExp = (raw ? A.mass() : 1) * (raw ? B.mass() : 1);
|
|
1838
1836
|
const mGot = result.mass();
|
|
1839
|
-
if (mExp !== 0 && Math.abs(mGot - mExp) >
|
|
1837
|
+
if (mExp !== 0 && Math.abs(mGot - mExp) > epsilon) {
|
|
1840
1838
|
result = result.scaleMass(mExp / mGot);
|
|
1841
1839
|
}
|
|
1842
|
-
if (!raw && Math.abs(result.mass() - 1) >
|
|
1840
|
+
if (!raw && Math.abs(result.mass() - 1) > epsilon)
|
|
1843
1841
|
result = result.normalize();
|
|
1844
1842
|
pmfCache?.set(cacheKey, result);
|
|
1845
1843
|
return result;
|
|
@@ -1887,7 +1885,7 @@ var _PMF = class _PMF {
|
|
|
1887
1885
|
* - Similar PMF lists share common prefixes (A+B, (A+B)+C, etc.)
|
|
1888
1886
|
* - Order-independent cache keys work better with consistent build patterns
|
|
1889
1887
|
*/
|
|
1890
|
-
static convolveMany(pmfList, eps =
|
|
1888
|
+
static convolveMany(pmfList, eps = EPS) {
|
|
1891
1889
|
if (pmfList.length === 0) return _PMF.empty(eps);
|
|
1892
1890
|
if (pmfList.length === 1) return pmfList[0];
|
|
1893
1891
|
return _PMF.reduceConvolveLeft(pmfList, eps);
|
|
@@ -1902,7 +1900,7 @@ var _PMF = class _PMF {
|
|
|
1902
1900
|
static fromJSON(jsonData) {
|
|
1903
1901
|
return new _PMF(
|
|
1904
1902
|
new Map(jsonData.bins),
|
|
1905
|
-
|
|
1903
|
+
EPS,
|
|
1906
1904
|
!!jsonData.normalized,
|
|
1907
1905
|
jsonData.identifier || "fromJSON"
|
|
1908
1906
|
);
|
|
@@ -2048,6 +2046,21 @@ var _PMF = class _PMF {
|
|
|
2048
2046
|
}
|
|
2049
2047
|
return false;
|
|
2050
2048
|
}
|
|
2049
|
+
tailProbGE(t) {
|
|
2050
|
+
let s = 0;
|
|
2051
|
+
for (const [x, rec] of this) {
|
|
2052
|
+
const p = typeof rec === "number" ? rec : rec.p;
|
|
2053
|
+
if (p > 0 && x >= t) s += p;
|
|
2054
|
+
}
|
|
2055
|
+
return s;
|
|
2056
|
+
}
|
|
2057
|
+
tailProbGT(t) {
|
|
2058
|
+
let s = 0;
|
|
2059
|
+
for (const [x, rec] of this) {
|
|
2060
|
+
if (x > t) s += rec.p;
|
|
2061
|
+
}
|
|
2062
|
+
return s;
|
|
2063
|
+
}
|
|
2051
2064
|
/**
|
|
2052
2065
|
* Returns a new PMF containing only bins where the specified outcome has non-zero probability.
|
|
2053
2066
|
* This creates a marginal distribution for the given outcome type, with probabilities
|
|
@@ -2107,6 +2120,66 @@ var _PMF = class _PMF {
|
|
|
2107
2120
|
const pNone = 1 - pSpecificSuccess - pGeneralSuccess;
|
|
2108
2121
|
return { pSpecificSuccess, pGeneralSuccess, pNone, pAny };
|
|
2109
2122
|
}
|
|
2123
|
+
mapValues(f, eps = EPS, opts) {
|
|
2124
|
+
const rounding = opts?.rounding ?? "none";
|
|
2125
|
+
const preserveCounts = opts?.preserveCounts ?? true;
|
|
2126
|
+
const round = (x) => rounding === "floor" ? Math.floor(x) : rounding === "ceil" ? Math.ceil(x) : rounding === "round" ? Math.round(x) : x;
|
|
2127
|
+
const probs = /* @__PURE__ */ new Map();
|
|
2128
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2129
|
+
for (const [v, rec] of this) {
|
|
2130
|
+
if (Math.abs(rec.p) < eps) continue;
|
|
2131
|
+
const u = round(f(v));
|
|
2132
|
+
probs.set(u, (probs.get(u) ?? 0) + rec.p);
|
|
2133
|
+
if (preserveCounts) {
|
|
2134
|
+
const rec2 = this.map.get(v);
|
|
2135
|
+
const src = typeof rec2 === "number" ? void 0 : rec2?.count;
|
|
2136
|
+
if (src) {
|
|
2137
|
+
const dest = counts.get(u) ?? {};
|
|
2138
|
+
for (const k in src) {
|
|
2139
|
+
dest[k] = (dest[k] ?? 0) + src[k];
|
|
2140
|
+
}
|
|
2141
|
+
counts.set(u, dest);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
const internal = /* @__PURE__ */ new Map();
|
|
2146
|
+
for (const [u, p] of probs) {
|
|
2147
|
+
internal.set(u, { p, count: counts.get(u) ?? {} });
|
|
2148
|
+
}
|
|
2149
|
+
return _PMF.fromMap(
|
|
2150
|
+
new Map(Array.from(internal, ([u, b]) => [u, b.p])),
|
|
2151
|
+
eps
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
static fromMap(m, eps = EPS, { requireIntegerValues = true } = {}) {
|
|
2155
|
+
const filtered = [];
|
|
2156
|
+
for (const [v, p] of m) {
|
|
2157
|
+
if (!Number.isFinite(v) || !Number.isFinite(p)) continue;
|
|
2158
|
+
if (p <= 0 || Math.abs(p) < eps) continue;
|
|
2159
|
+
if (requireIntegerValues && !Number.isInteger(v)) {
|
|
2160
|
+
throw new Error(`fromMap: non-integer outcome ${v}`);
|
|
2161
|
+
}
|
|
2162
|
+
filtered.push([v, p]);
|
|
2163
|
+
}
|
|
2164
|
+
if (filtered.length === 0) {
|
|
2165
|
+
throw new Error("fromMap: empty or invalid input map");
|
|
2166
|
+
}
|
|
2167
|
+
let sum = 0;
|
|
2168
|
+
let c = 0;
|
|
2169
|
+
for (const [, p] of filtered) {
|
|
2170
|
+
const y = p - c;
|
|
2171
|
+
const t = sum + y;
|
|
2172
|
+
c = t - sum - y;
|
|
2173
|
+
sum = t;
|
|
2174
|
+
}
|
|
2175
|
+
if (sum <= 0) throw new Error("pmfFromMap: probabilities sum to 0");
|
|
2176
|
+
filtered.sort((a, b) => a[0] - b[0]);
|
|
2177
|
+
const internal = /* @__PURE__ */ new Map();
|
|
2178
|
+
for (const [v, p] of filtered) {
|
|
2179
|
+
internal.set(v, { p: p / sum, count: {} });
|
|
2180
|
+
}
|
|
2181
|
+
return new _PMF(internal, eps);
|
|
2182
|
+
}
|
|
2110
2183
|
query() {
|
|
2111
2184
|
return new DiceQuery(this);
|
|
2112
2185
|
}
|
|
@@ -2114,12 +2187,127 @@ var _PMF = class _PMF {
|
|
|
2114
2187
|
// Unique ID generator for anonymous PMFs to avoid cache key collisions
|
|
2115
2188
|
__publicField(_PMF, "__anonIdCounter", 1);
|
|
2116
2189
|
var PMF = _PMF;
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2190
|
+
|
|
2191
|
+
// src/mixture.ts
|
|
2192
|
+
var Mixture = class _Mixture {
|
|
2193
|
+
constructor(eps = EPS) {
|
|
2194
|
+
__publicField(this, "totals", /* @__PURE__ */ new Map());
|
|
2195
|
+
// raw mass per outcome (pre-normalization)
|
|
2196
|
+
__publicField(this, "labelMass", /* @__PURE__ */ new Map());
|
|
2197
|
+
// raw mass per outcome per label
|
|
2198
|
+
__publicField(this, "eps");
|
|
2199
|
+
this.eps = Number.isFinite(eps) ? eps : EPS;
|
|
2200
|
+
}
|
|
2201
|
+
/** Remove all accumulated state. */
|
|
2202
|
+
clear() {
|
|
2203
|
+
this.totals.clear();
|
|
2204
|
+
this.labelMass.clear();
|
|
2205
|
+
return this;
|
|
2206
|
+
}
|
|
2207
|
+
/** Number of distinct outcome values currently accumulated. */
|
|
2208
|
+
size() {
|
|
2209
|
+
return this.totals.size;
|
|
2210
|
+
}
|
|
2211
|
+
/** Whether a label was ever added. */
|
|
2212
|
+
hasLabel(label) {
|
|
2213
|
+
for (const bag of this.labelMass.values()) if (bag[label]) return true;
|
|
2214
|
+
return false;
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Add a labeled component with a mixture weight.
|
|
2218
|
+
* Weight can be any positive finite number. Very small contributions are pruned by eps.
|
|
2219
|
+
*/
|
|
2220
|
+
add(label, pmf, weight = 1) {
|
|
2221
|
+
if (!Number.isFinite(weight) || weight <= 0) return this;
|
|
2222
|
+
for (const [v, binOrNumber] of pmf) {
|
|
2223
|
+
const isNumber = typeof binOrNumber === "number";
|
|
2224
|
+
const p = isNumber ? binOrNumber : binOrNumber?.p ?? 0;
|
|
2225
|
+
if (p <= 0) continue;
|
|
2226
|
+
const add = weight * p;
|
|
2227
|
+
if (!Number.isFinite(add) || Math.abs(add) < this.eps) continue;
|
|
2228
|
+
this.totals.set(v, (this.totals.get(v) ?? 0) + add);
|
|
2229
|
+
const bag = this.labelMass.get(v) ?? {};
|
|
2230
|
+
bag[label] = (bag[label] ?? 0) + add;
|
|
2231
|
+
this.labelMass.set(v, bag);
|
|
2232
|
+
}
|
|
2233
|
+
return this;
|
|
2234
|
+
}
|
|
2235
|
+
buildPMF(eps = EPS) {
|
|
2236
|
+
let grand = 0;
|
|
2237
|
+
let c = 0;
|
|
2238
|
+
for (const m of this.totals.values()) {
|
|
2239
|
+
const y = m - c;
|
|
2240
|
+
const t = grand + y;
|
|
2241
|
+
c = t - grand - y;
|
|
2242
|
+
grand = t;
|
|
2243
|
+
}
|
|
2244
|
+
if (!(grand > 0)) throw new Error("LabeledMixture: zero total mass");
|
|
2245
|
+
const internal = /* @__PURE__ */ new Map();
|
|
2246
|
+
for (const [v, m] of this.totals) {
|
|
2247
|
+
if (m <= 0 || Math.abs(m) < this.eps) continue;
|
|
2248
|
+
const count = this.labelMass.get(v) ?? {};
|
|
2249
|
+
internal.set(v, { p: m / grand, count });
|
|
2250
|
+
}
|
|
2251
|
+
return new PMF(internal, eps);
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* Produce normalized *per-label* PMFs (labels independent).
|
|
2255
|
+
* These are unlabeled PMFs built from the raw mass of that label alone.
|
|
2256
|
+
*/
|
|
2257
|
+
byOutcome() {
|
|
2258
|
+
const labels = /* @__PURE__ */ new Set();
|
|
2259
|
+
for (const bag of this.labelMass.values()) {
|
|
2260
|
+
for (const k of Object.keys(bag)) labels.add(k);
|
|
2261
|
+
}
|
|
2262
|
+
const out = {};
|
|
2263
|
+
for (const label of labels) {
|
|
2264
|
+
const m = /* @__PURE__ */ new Map();
|
|
2265
|
+
for (const [v, bag] of this.labelMass) {
|
|
2266
|
+
const w = bag[label];
|
|
2267
|
+
if (w && Math.abs(w) >= this.eps) m.set(v, w);
|
|
2268
|
+
}
|
|
2269
|
+
if (m.size > 0) out[label] = PMF.fromMap(m, this.eps);
|
|
2270
|
+
}
|
|
2271
|
+
return out;
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Mixture weights per label, normalized to sum to 1 over labels that appeared.
|
|
2275
|
+
* Uses raw mass before per-outcome normalization.
|
|
2276
|
+
*/
|
|
2277
|
+
weights() {
|
|
2278
|
+
const res = {};
|
|
2279
|
+
for (const [, bag] of this.labelMass) {
|
|
2280
|
+
for (const [lab, w] of Object.entries(bag)) {
|
|
2281
|
+
if (!Number.isFinite(w) || w <= 0) continue;
|
|
2282
|
+
res[lab] = (res[lab] ?? 0) + w;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
let total = 0;
|
|
2286
|
+
let c = 0;
|
|
2287
|
+
for (const v of Object.values(res)) {
|
|
2288
|
+
const y = v - c;
|
|
2289
|
+
const t = total + y;
|
|
2290
|
+
c = t - total - y;
|
|
2291
|
+
total = t;
|
|
2292
|
+
}
|
|
2293
|
+
if (total > 0) {
|
|
2294
|
+
for (const k in res) res[k] = res[k] / total;
|
|
2295
|
+
}
|
|
2296
|
+
return res;
|
|
2297
|
+
}
|
|
2298
|
+
toJSON() {
|
|
2299
|
+
return {
|
|
2300
|
+
totals: Array.from(this.totals.entries()).sort((a, b) => a[0] - b[0]),
|
|
2301
|
+
labels: Array.from(this.labelMass.entries()).sort((a, b) => a[0] - b[0]),
|
|
2302
|
+
eps: this.eps
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
static mix(items, eps = EPS) {
|
|
2306
|
+
const mix = new _Mixture(eps);
|
|
2307
|
+
for (const [lab, pmf, w] of items) mix.add(lab, pmf, w);
|
|
2308
|
+
return mix.buildPMF();
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2123
2311
|
|
|
2124
2312
|
// src/dice.ts
|
|
2125
2313
|
var Dice = class _Dice {
|
|
@@ -2435,7 +2623,7 @@ var Dice = class _Dice {
|
|
|
2435
2623
|
* Convert dice to PMF using OutcomeType labels directly from damage distribution.
|
|
2436
2624
|
* This is much cleaner than the original complex distribution conversion.
|
|
2437
2625
|
*/
|
|
2438
|
-
toPMF(numEpsilon =
|
|
2626
|
+
toPMF(numEpsilon = EPS) {
|
|
2439
2627
|
const total = this.total();
|
|
2440
2628
|
if (total === 0) return PMF.empty(numEpsilon);
|
|
2441
2629
|
this.ensureHitDistribution();
|
|
@@ -2614,7 +2802,7 @@ function parseExpression(arr, n) {
|
|
|
2614
2802
|
}
|
|
2615
2803
|
critNorm = crit.total();
|
|
2616
2804
|
crit = op.call(crit, parseBinaryArgument(arg, arr, n));
|
|
2617
|
-
critNorm = critNorm ? crit.total() / critNorm : 1;
|
|
2805
|
+
critNorm = crit && critNorm ? crit.total() / critNorm : 1;
|
|
2618
2806
|
}
|
|
2619
2807
|
let save;
|
|
2620
2808
|
let saveNorm = 1;
|
|
@@ -2629,7 +2817,7 @@ function parseExpression(arr, n) {
|
|
|
2629
2817
|
saveNorm = save.total();
|
|
2630
2818
|
finalResult = finalResult.deleteFace(min);
|
|
2631
2819
|
save = op.call(save, parseBinaryArgument(arg, arr, n));
|
|
2632
|
-
saveNorm = saveNorm ? save.total() / saveNorm : 1;
|
|
2820
|
+
saveNorm = save && saveNorm ? save.total() / saveNorm : 1;
|
|
2633
2821
|
}
|
|
2634
2822
|
let pc;
|
|
2635
2823
|
let pcNorm = 1;
|
|
@@ -2642,7 +2830,7 @@ function parseExpression(arr, n) {
|
|
|
2642
2830
|
const missBefore = pc.total();
|
|
2643
2831
|
finalResult = finalResult.deleteFace(min);
|
|
2644
2832
|
pc = op.call(pc, parseBinaryArgument(arg, arr, n)).divideRoundDown(2);
|
|
2645
|
-
const missAfter = pc.total();
|
|
2833
|
+
const missAfter = pc ? pc.total() : 0;
|
|
2646
2834
|
pcNorm = missBefore ? missAfter / missBefore : 1;
|
|
2647
2835
|
}
|
|
2648
2836
|
let miss;
|
|
@@ -2658,7 +2846,7 @@ function parseExpression(arr, n) {
|
|
|
2658
2846
|
missNorm = miss.total();
|
|
2659
2847
|
finalResult = finalResult.deleteFace(min);
|
|
2660
2848
|
miss = op.call(miss, parseBinaryArgument(arg, arr, n));
|
|
2661
|
-
missNorm = missNorm ? miss.total() / missNorm : 1;
|
|
2849
|
+
missNorm = miss && missNorm ? miss.total() / missNorm : 1;
|
|
2662
2850
|
}
|
|
2663
2851
|
let norm = finalResult.total();
|
|
2664
2852
|
finalResult = op.call(finalResult, arg);
|
|
@@ -2948,16 +3136,15 @@ function parseOperation(s) {
|
|
|
2948
3136
|
return;
|
|
2949
3137
|
}
|
|
2950
3138
|
|
|
2951
|
-
exports.COMPUTATIONAL_EPS = COMPUTATIONAL_EPS;
|
|
2952
|
-
exports.Dice = Dice;
|
|
2953
3139
|
exports.DiceQuery = DiceQuery;
|
|
3140
|
+
exports.EPS = EPS;
|
|
2954
3141
|
exports.LRUCache = LRUCache;
|
|
3142
|
+
exports.Mixture = Mixture;
|
|
2955
3143
|
exports.PMF = PMF;
|
|
2956
|
-
exports.TEST_EPS = TEST_EPS;
|
|
2957
3144
|
exports.clearParserCache = clearParserCache;
|
|
2958
3145
|
exports.getCachingEnabled = getCachingEnabled;
|
|
2959
|
-
exports.getPowerCacheKey = getPowerCacheKey;
|
|
2960
3146
|
exports.parse = parse;
|
|
3147
|
+
exports.pmfCache = pmfCache;
|
|
2961
3148
|
exports.setCachingEnabled = setCachingEnabled;
|
|
2962
3149
|
//# sourceMappingURL=index.cjs.map
|
|
2963
3150
|
//# sourceMappingURL=index.cjs.map
|