@yipe/dice 0.1.2 → 0.1.4

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 CHANGED
@@ -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
@@ -51,15 +51,15 @@ var EPS = 1e-12;
51
51
 
52
52
  // src/query.ts
53
53
  var _DiceQuery = class _DiceQuery {
54
- constructor(singles, combined) {
54
+ constructor(singles, combined, eps = EPS) {
55
55
  __publicField(this, "singles");
56
56
  __publicField(this, "combined");
57
57
  this.singles = Array.isArray(singles) ? singles : [singles];
58
- const c = combined ?? PMF.convolveMany(this.singles);
59
- this.combined = Math.abs(c.mass() - 1) <= 1e-12 ? c : c.normalize();
60
58
  if (this.singles.some((s) => s === void 0)) {
61
59
  throw new Error("DiceQuery contains undefined singles");
62
60
  }
61
+ const c = combined ?? PMF.convolveMany(this.singles);
62
+ this.combined = Math.abs(c.mass() - 1) <= eps ? c : c.normalize();
63
63
  }
64
64
  /**
65
65
  * Returns the expected damage across all possible outcomes.
@@ -357,23 +357,16 @@ var _DiceQuery = class _DiceQuery {
357
357
  * - "How much damage comes from miss effects (like save-for-half spells)?"
358
358
  */
359
359
  expectedDamageFrom(labels) {
360
- if (typeof labels === "string") {
361
- return this._expectedDamageFromSingle(labels);
362
- }
363
- let totalExpectedDamage = 0;
364
- for (const [, probabilityBin] of this.combined) {
365
- for (const label of labels) {
366
- totalExpectedDamage += probabilityBin.attr?.[label] || 0;
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;
367
367
  }
368
368
  }
369
- return totalExpectedDamage;
370
- }
371
- _expectedDamageFromSingle(label) {
372
- let totalExpectedDamage = 0;
373
- for (const [, probabilityBin] of this.combined) {
374
- totalExpectedDamage += probabilityBin.attr?.[label] || 0;
375
- }
376
- return totalExpectedDamage;
369
+ return total;
377
370
  }
378
371
  /**
379
372
  * Returns damage statistics for scenarios where AT LEAST ONE attack results in
@@ -596,7 +589,7 @@ var _DiceQuery = class _DiceQuery {
596
589
  * query.toStackedChartData(['hit', 'crit'])
597
590
  * // → {labels: [0, 6, 12, ...], datasets: [{label: 'hit', data: [0, 0.03, ...]}, ...]}
598
591
  */
599
- toStackedChartData(labels = [], epsilon = 1e-12) {
592
+ toStackedChartData(labels = [], epsilon = EPS) {
600
593
  const damageValues = this.combined.support();
601
594
  damageValues.map((dmg) => {
602
595
  const bin = this.combined.map.get(dmg);
@@ -1279,12 +1272,12 @@ __publicField(_DiceQuery, "DEFAULT_OUTCOMES", [
1279
1272
  var DiceQuery = _DiceQuery;
1280
1273
  var pmfCache = new LRUCache(1e3);
1281
1274
  var _PMF = class _PMF {
1282
- constructor(map = /* @__PURE__ */ new Map(), epsilon = EPS, normalized = false, identifier = `anon#${_PMF.__anonIdCounter++}`, _preservedProvidence = true) {
1275
+ constructor(map = /* @__PURE__ */ new Map(), epsilon = EPS, normalized = false, identifier = `anon#${_PMF.__anonIdCounter++}`, _preservedProvenance = true) {
1283
1276
  this.map = map;
1284
1277
  this.epsilon = epsilon;
1285
1278
  this.normalized = normalized;
1286
1279
  this.identifier = identifier;
1287
- this._preservedProvidence = _preservedProvidence;
1280
+ this._preservedProvenance = _preservedProvenance;
1288
1281
  // Cached computed values
1289
1282
  __publicField(this, "_support");
1290
1283
  __publicField(this, "_min");
@@ -1303,6 +1296,9 @@ var _PMF = class _PMF {
1303
1296
  m.set(0, { p: 1, count: { miss: 1 }, attr: {} });
1304
1297
  return new _PMF(m, epsilon, false, "zero");
1305
1298
  }
1299
+ static delta(value, epsilon = EPS) {
1300
+ return _PMF.fromMap(/* @__PURE__ */ new Map([[value, 1]]), epsilon);
1301
+ }
1306
1302
  // This creates a single bin at value 0, but with weight 0.
1307
1303
  static emptyMass() {
1308
1304
  return _PMF.zero().scaleMass(0);
@@ -1405,7 +1401,7 @@ var _PMF = class _PMF {
1405
1401
  * @param eps Optional tolerance for floating point rounding
1406
1402
  * @returns A single PMF representing the exclusive mixture of all options.
1407
1403
  */
1408
- static exclusive(options, eps = 1e-12) {
1404
+ static exclusive(options, eps = EPS) {
1409
1405
  const items = options.map(
1410
1406
  (o) => Array.isArray(o) ? { pmf: o[0], weight: o[1] } : o
1411
1407
  );
@@ -1435,8 +1431,7 @@ var _PMF = class _PMF {
1435
1431
  * [pCrit, critPMF],
1436
1432
  * ]);
1437
1433
  */
1438
- static mixN(weights) {
1439
- const eps = 1e-12;
1434
+ static mixN(weights, eps = EPS) {
1440
1435
  const filtered = weights.filter(([w]) => w > eps);
1441
1436
  if (filtered.length === 0) {
1442
1437
  return _PMF.emptyMass();
@@ -1458,17 +1453,17 @@ var _PMF = class _PMF {
1458
1453
  // This is a convenience method for when we use power
1459
1454
  // TODO: It can be smarter in the future, and we can also add it to query
1460
1455
  // That way statistics operations on invalid PMFs can throw an error
1461
- // TODO… how can we detect if manually merging two queries' combined PMFs, as that loses providence?
1462
- setPreservedProvidence(preserved) {
1463
- if (!this._preservedProvidence && preserved) {
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) {
1464
1459
  throw new Error(
1465
- "Preserved providence is already set to false, cannot fix that"
1460
+ "Preserved provenance is already set to false, cannot fix that"
1466
1461
  );
1467
1462
  }
1468
- this._preservedProvidence = preserved;
1463
+ this._preservedProvenance = preserved;
1469
1464
  }
1470
- preservedProvidence() {
1471
- return this._preservedProvidence;
1465
+ preservedProvenance() {
1466
+ return this._preservedProvenance;
1472
1467
  }
1473
1468
  getPowerCacheKey(n, eps) {
1474
1469
  const id = this.identifier;
@@ -1509,7 +1504,7 @@ var _PMF = class _PMF {
1509
1504
  base = base.convolve(base, epsilon);
1510
1505
  }
1511
1506
  }
1512
- result.setPreservedProvidence(false);
1507
+ result.setPreservedProvenance(false);
1513
1508
  {
1514
1509
  pmfCache?.set(key, result);
1515
1510
  }
@@ -1538,7 +1533,7 @@ var _PMF = class _PMF {
1538
1533
  outcomeMass(outcome) {
1539
1534
  let totalProbabilityMass = 0;
1540
1535
  for (const { p, count } of this.map.values()) {
1541
- totalProbabilityMass += p * count[outcome];
1536
+ totalProbabilityMass += p * (count[outcome] ?? 0);
1542
1537
  }
1543
1538
  return totalProbabilityMass;
1544
1539
  }
@@ -1801,7 +1796,7 @@ var _PMF = class _PMF {
1801
1796
  }
1802
1797
  convolve(other, eps, raw = false) {
1803
1798
  const epsilon = eps ?? this.epsilon;
1804
- const norm = (x) => raw ? x : Math.abs(x.mass() - 1) <= 1e-12 ? x : x.normalize();
1799
+ const norm = (x) => raw ? x : Math.abs(x.mass() - 1) <= epsilon ? x : x.normalize();
1805
1800
  const A0 = norm(this);
1806
1801
  const B0 = norm(other);
1807
1802
  const [A, B] = A0.identifier <= B0.identifier ? [A0, B0] : [B0, A0];
@@ -1836,14 +1831,13 @@ var _PMF = class _PMF {
1836
1831
  epsilon,
1837
1832
  !raw,
1838
1833
  `${A.identifier}${raw ? "*" : "+"}${B.identifier}`
1839
- // ← canonical order
1840
1834
  );
1841
1835
  const mExp = (raw ? A.mass() : 1) * (raw ? B.mass() : 1);
1842
1836
  const mGot = result.mass();
1843
- if (mExp !== 0 && Math.abs(mGot - mExp) > 1e-12) {
1837
+ if (mExp !== 0 && Math.abs(mGot - mExp) > epsilon) {
1844
1838
  result = result.scaleMass(mExp / mGot);
1845
1839
  }
1846
- if (!raw && Math.abs(result.mass() - 1) > 1e-12)
1840
+ if (!raw && Math.abs(result.mass() - 1) > epsilon)
1847
1841
  result = result.normalize();
1848
1842
  pmfCache?.set(cacheKey, result);
1849
1843
  return result;
@@ -2052,6 +2046,21 @@ var _PMF = class _PMF {
2052
2046
  }
2053
2047
  return false;
2054
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
+ }
2055
2064
  /**
2056
2065
  * Returns a new PMF containing only bins where the specified outcome has non-zero probability.
2057
2066
  * This creates a marginal distribution for the given outcome type, with probabilities
@@ -2111,6 +2120,66 @@ var _PMF = class _PMF {
2111
2120
  const pNone = 1 - pSpecificSuccess - pGeneralSuccess;
2112
2121
  return { pSpecificSuccess, pGeneralSuccess, pNone, pAny };
2113
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
+ }
2114
2183
  query() {
2115
2184
  return new DiceQuery(this);
2116
2185
  }
@@ -2119,6 +2188,127 @@ var _PMF = class _PMF {
2119
2188
  __publicField(_PMF, "__anonIdCounter", 1);
2120
2189
  var PMF = _PMF;
2121
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("Mixture: 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
+ };
2311
+
2122
2312
  // src/dice.ts
2123
2313
  var Dice = class _Dice {
2124
2314
  constructor(x = 0) {
@@ -2612,7 +2802,7 @@ function parseExpression(arr, n) {
2612
2802
  }
2613
2803
  critNorm = crit.total();
2614
2804
  crit = op.call(crit, parseBinaryArgument(arg, arr, n));
2615
- critNorm = critNorm ? crit.total() / critNorm : 1;
2805
+ critNorm = crit && critNorm ? crit.total() / critNorm : 1;
2616
2806
  }
2617
2807
  let save;
2618
2808
  let saveNorm = 1;
@@ -2627,7 +2817,7 @@ function parseExpression(arr, n) {
2627
2817
  saveNorm = save.total();
2628
2818
  finalResult = finalResult.deleteFace(min);
2629
2819
  save = op.call(save, parseBinaryArgument(arg, arr, n));
2630
- saveNorm = saveNorm ? save.total() / saveNorm : 1;
2820
+ saveNorm = save && saveNorm ? save.total() / saveNorm : 1;
2631
2821
  }
2632
2822
  let pc;
2633
2823
  let pcNorm = 1;
@@ -2640,7 +2830,7 @@ function parseExpression(arr, n) {
2640
2830
  const missBefore = pc.total();
2641
2831
  finalResult = finalResult.deleteFace(min);
2642
2832
  pc = op.call(pc, parseBinaryArgument(arg, arr, n)).divideRoundDown(2);
2643
- const missAfter = pc.total();
2833
+ const missAfter = pc ? pc.total() : 0;
2644
2834
  pcNorm = missBefore ? missAfter / missBefore : 1;
2645
2835
  }
2646
2836
  let miss;
@@ -2656,7 +2846,7 @@ function parseExpression(arr, n) {
2656
2846
  missNorm = miss.total();
2657
2847
  finalResult = finalResult.deleteFace(min);
2658
2848
  miss = op.call(miss, parseBinaryArgument(arg, arr, n));
2659
- missNorm = missNorm ? miss.total() / missNorm : 1;
2849
+ missNorm = miss && missNorm ? miss.total() / missNorm : 1;
2660
2850
  }
2661
2851
  let norm = finalResult.total();
2662
2852
  finalResult = op.call(finalResult, arg);
@@ -2946,10 +3136,10 @@ function parseOperation(s) {
2946
3136
  return;
2947
3137
  }
2948
3138
 
2949
- exports.Dice = Dice;
2950
3139
  exports.DiceQuery = DiceQuery;
2951
3140
  exports.EPS = EPS;
2952
3141
  exports.LRUCache = LRUCache;
3142
+ exports.Mixture = Mixture;
2953
3143
  exports.PMF = PMF;
2954
3144
  exports.clearParserCache = clearParserCache;
2955
3145
  exports.getCachingEnabled = getCachingEnabled;