@yipe/dice 0.1.2 → 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/dist/index.js CHANGED
@@ -49,15 +49,15 @@ var EPS = 1e-12;
49
49
 
50
50
  // src/query.ts
51
51
  var _DiceQuery = class _DiceQuery {
52
- constructor(singles, combined) {
52
+ constructor(singles, combined, eps = EPS) {
53
53
  __publicField(this, "singles");
54
54
  __publicField(this, "combined");
55
55
  this.singles = Array.isArray(singles) ? singles : [singles];
56
- const c = combined ?? PMF.convolveMany(this.singles);
57
- this.combined = Math.abs(c.mass() - 1) <= 1e-12 ? c : c.normalize();
58
56
  if (this.singles.some((s) => s === void 0)) {
59
57
  throw new Error("DiceQuery contains undefined singles");
60
58
  }
59
+ const c = combined ?? PMF.convolveMany(this.singles);
60
+ this.combined = Math.abs(c.mass() - 1) <= eps ? c : c.normalize();
61
61
  }
62
62
  /**
63
63
  * Returns the expected damage across all possible outcomes.
@@ -355,23 +355,16 @@ var _DiceQuery = class _DiceQuery {
355
355
  * - "How much damage comes from miss effects (like save-for-half spells)?"
356
356
  */
357
357
  expectedDamageFrom(labels) {
358
- if (typeof labels === "string") {
359
- return this._expectedDamageFromSingle(labels);
360
- }
361
- let totalExpectedDamage = 0;
362
- for (const [, probabilityBin] of this.combined) {
363
- for (const label of labels) {
364
- totalExpectedDamage += probabilityBin.attr?.[label] || 0;
358
+ const wanted = Array.isArray(labels) ? labels : [labels];
359
+ let total = 0;
360
+ for (const single of this.singles) {
361
+ for (const [dmg, bin] of single) {
362
+ let p = 0;
363
+ for (const label of wanted) p += bin.count[label] ?? 0;
364
+ total += dmg * p;
365
365
  }
366
366
  }
367
- return totalExpectedDamage;
368
- }
369
- _expectedDamageFromSingle(label) {
370
- let totalExpectedDamage = 0;
371
- for (const [, probabilityBin] of this.combined) {
372
- totalExpectedDamage += probabilityBin.attr?.[label] || 0;
373
- }
374
- return totalExpectedDamage;
367
+ return total;
375
368
  }
376
369
  /**
377
370
  * Returns damage statistics for scenarios where AT LEAST ONE attack results in
@@ -594,7 +587,7 @@ var _DiceQuery = class _DiceQuery {
594
587
  * query.toStackedChartData(['hit', 'crit'])
595
588
  * // → {labels: [0, 6, 12, ...], datasets: [{label: 'hit', data: [0, 0.03, ...]}, ...]}
596
589
  */
597
- toStackedChartData(labels = [], epsilon = 1e-12) {
590
+ toStackedChartData(labels = [], epsilon = EPS) {
598
591
  const damageValues = this.combined.support();
599
592
  damageValues.map((dmg) => {
600
593
  const bin = this.combined.map.get(dmg);
@@ -1277,12 +1270,12 @@ __publicField(_DiceQuery, "DEFAULT_OUTCOMES", [
1277
1270
  var DiceQuery = _DiceQuery;
1278
1271
  var pmfCache = new LRUCache(1e3);
1279
1272
  var _PMF = class _PMF {
1280
- constructor(map = /* @__PURE__ */ new Map(), epsilon = EPS, normalized = false, identifier = `anon#${_PMF.__anonIdCounter++}`, _preservedProvidence = true) {
1273
+ constructor(map = /* @__PURE__ */ new Map(), epsilon = EPS, normalized = false, identifier = `anon#${_PMF.__anonIdCounter++}`, _preservedProvenance = true) {
1281
1274
  this.map = map;
1282
1275
  this.epsilon = epsilon;
1283
1276
  this.normalized = normalized;
1284
1277
  this.identifier = identifier;
1285
- this._preservedProvidence = _preservedProvidence;
1278
+ this._preservedProvenance = _preservedProvenance;
1286
1279
  // Cached computed values
1287
1280
  __publicField(this, "_support");
1288
1281
  __publicField(this, "_min");
@@ -1301,6 +1294,9 @@ var _PMF = class _PMF {
1301
1294
  m.set(0, { p: 1, count: { miss: 1 }, attr: {} });
1302
1295
  return new _PMF(m, epsilon, false, "zero");
1303
1296
  }
1297
+ static delta(value, epsilon = EPS) {
1298
+ return _PMF.fromMap(/* @__PURE__ */ new Map([[value, 1]]), epsilon);
1299
+ }
1304
1300
  // This creates a single bin at value 0, but with weight 0.
1305
1301
  static emptyMass() {
1306
1302
  return _PMF.zero().scaleMass(0);
@@ -1403,7 +1399,7 @@ var _PMF = class _PMF {
1403
1399
  * @param eps Optional tolerance for floating point rounding
1404
1400
  * @returns A single PMF representing the exclusive mixture of all options.
1405
1401
  */
1406
- static exclusive(options, eps = 1e-12) {
1402
+ static exclusive(options, eps = EPS) {
1407
1403
  const items = options.map(
1408
1404
  (o) => Array.isArray(o) ? { pmf: o[0], weight: o[1] } : o
1409
1405
  );
@@ -1433,8 +1429,7 @@ var _PMF = class _PMF {
1433
1429
  * [pCrit, critPMF],
1434
1430
  * ]);
1435
1431
  */
1436
- static mixN(weights) {
1437
- const eps = 1e-12;
1432
+ static mixN(weights, eps = EPS) {
1438
1433
  const filtered = weights.filter(([w]) => w > eps);
1439
1434
  if (filtered.length === 0) {
1440
1435
  return _PMF.emptyMass();
@@ -1456,17 +1451,17 @@ var _PMF = class _PMF {
1456
1451
  // This is a convenience method for when we use power
1457
1452
  // TODO: It can be smarter in the future, and we can also add it to query
1458
1453
  // That way statistics operations on invalid PMFs can throw an error
1459
- // TODO… how can we detect if manually merging two queries' combined PMFs, as that loses providence?
1460
- setPreservedProvidence(preserved) {
1461
- if (!this._preservedProvidence && preserved) {
1454
+ // TODO… how can we detect if manually merging two queries' combined PMFs, as that loses provenance?
1455
+ setPreservedProvenance(preserved) {
1456
+ if (!this._preservedProvenance && preserved) {
1462
1457
  throw new Error(
1463
- "Preserved providence is already set to false, cannot fix that"
1458
+ "Preserved provenance is already set to false, cannot fix that"
1464
1459
  );
1465
1460
  }
1466
- this._preservedProvidence = preserved;
1461
+ this._preservedProvenance = preserved;
1467
1462
  }
1468
- preservedProvidence() {
1469
- return this._preservedProvidence;
1463
+ preservedProvenance() {
1464
+ return this._preservedProvenance;
1470
1465
  }
1471
1466
  getPowerCacheKey(n, eps) {
1472
1467
  const id = this.identifier;
@@ -1507,7 +1502,7 @@ var _PMF = class _PMF {
1507
1502
  base = base.convolve(base, epsilon);
1508
1503
  }
1509
1504
  }
1510
- result.setPreservedProvidence(false);
1505
+ result.setPreservedProvenance(false);
1511
1506
  {
1512
1507
  pmfCache?.set(key, result);
1513
1508
  }
@@ -1536,7 +1531,7 @@ var _PMF = class _PMF {
1536
1531
  outcomeMass(outcome) {
1537
1532
  let totalProbabilityMass = 0;
1538
1533
  for (const { p, count } of this.map.values()) {
1539
- totalProbabilityMass += p * count[outcome];
1534
+ totalProbabilityMass += p * (count[outcome] ?? 0);
1540
1535
  }
1541
1536
  return totalProbabilityMass;
1542
1537
  }
@@ -1799,7 +1794,7 @@ var _PMF = class _PMF {
1799
1794
  }
1800
1795
  convolve(other, eps, raw = false) {
1801
1796
  const epsilon = eps ?? this.epsilon;
1802
- const norm = (x) => raw ? x : Math.abs(x.mass() - 1) <= 1e-12 ? x : x.normalize();
1797
+ const norm = (x) => raw ? x : Math.abs(x.mass() - 1) <= epsilon ? x : x.normalize();
1803
1798
  const A0 = norm(this);
1804
1799
  const B0 = norm(other);
1805
1800
  const [A, B] = A0.identifier <= B0.identifier ? [A0, B0] : [B0, A0];
@@ -1834,14 +1829,13 @@ var _PMF = class _PMF {
1834
1829
  epsilon,
1835
1830
  !raw,
1836
1831
  `${A.identifier}${raw ? "*" : "+"}${B.identifier}`
1837
- // ← canonical order
1838
1832
  );
1839
1833
  const mExp = (raw ? A.mass() : 1) * (raw ? B.mass() : 1);
1840
1834
  const mGot = result.mass();
1841
- if (mExp !== 0 && Math.abs(mGot - mExp) > 1e-12) {
1835
+ if (mExp !== 0 && Math.abs(mGot - mExp) > epsilon) {
1842
1836
  result = result.scaleMass(mExp / mGot);
1843
1837
  }
1844
- if (!raw && Math.abs(result.mass() - 1) > 1e-12)
1838
+ if (!raw && Math.abs(result.mass() - 1) > epsilon)
1845
1839
  result = result.normalize();
1846
1840
  pmfCache?.set(cacheKey, result);
1847
1841
  return result;
@@ -2050,6 +2044,21 @@ var _PMF = class _PMF {
2050
2044
  }
2051
2045
  return false;
2052
2046
  }
2047
+ tailProbGE(t) {
2048
+ let s = 0;
2049
+ for (const [x, rec] of this) {
2050
+ const p = typeof rec === "number" ? rec : rec.p;
2051
+ if (p > 0 && x >= t) s += p;
2052
+ }
2053
+ return s;
2054
+ }
2055
+ tailProbGT(t) {
2056
+ let s = 0;
2057
+ for (const [x, rec] of this) {
2058
+ if (x > t) s += rec.p;
2059
+ }
2060
+ return s;
2061
+ }
2053
2062
  /**
2054
2063
  * Returns a new PMF containing only bins where the specified outcome has non-zero probability.
2055
2064
  * This creates a marginal distribution for the given outcome type, with probabilities
@@ -2109,6 +2118,66 @@ var _PMF = class _PMF {
2109
2118
  const pNone = 1 - pSpecificSuccess - pGeneralSuccess;
2110
2119
  return { pSpecificSuccess, pGeneralSuccess, pNone, pAny };
2111
2120
  }
2121
+ mapValues(f, eps = EPS, opts) {
2122
+ const rounding = opts?.rounding ?? "none";
2123
+ const preserveCounts = opts?.preserveCounts ?? true;
2124
+ const round = (x) => rounding === "floor" ? Math.floor(x) : rounding === "ceil" ? Math.ceil(x) : rounding === "round" ? Math.round(x) : x;
2125
+ const probs = /* @__PURE__ */ new Map();
2126
+ const counts = /* @__PURE__ */ new Map();
2127
+ for (const [v, rec] of this) {
2128
+ if (Math.abs(rec.p) < eps) continue;
2129
+ const u = round(f(v));
2130
+ probs.set(u, (probs.get(u) ?? 0) + rec.p);
2131
+ if (preserveCounts) {
2132
+ const rec2 = this.map.get(v);
2133
+ const src = typeof rec2 === "number" ? void 0 : rec2?.count;
2134
+ if (src) {
2135
+ const dest = counts.get(u) ?? {};
2136
+ for (const k in src) {
2137
+ dest[k] = (dest[k] ?? 0) + src[k];
2138
+ }
2139
+ counts.set(u, dest);
2140
+ }
2141
+ }
2142
+ }
2143
+ const internal = /* @__PURE__ */ new Map();
2144
+ for (const [u, p] of probs) {
2145
+ internal.set(u, { p, count: counts.get(u) ?? {} });
2146
+ }
2147
+ return _PMF.fromMap(
2148
+ new Map(Array.from(internal, ([u, b]) => [u, b.p])),
2149
+ eps
2150
+ );
2151
+ }
2152
+ static fromMap(m, eps = EPS, { requireIntegerValues = true } = {}) {
2153
+ const filtered = [];
2154
+ for (const [v, p] of m) {
2155
+ if (!Number.isFinite(v) || !Number.isFinite(p)) continue;
2156
+ if (p <= 0 || Math.abs(p) < eps) continue;
2157
+ if (requireIntegerValues && !Number.isInteger(v)) {
2158
+ throw new Error(`fromMap: non-integer outcome ${v}`);
2159
+ }
2160
+ filtered.push([v, p]);
2161
+ }
2162
+ if (filtered.length === 0) {
2163
+ throw new Error("fromMap: empty or invalid input map");
2164
+ }
2165
+ let sum = 0;
2166
+ let c = 0;
2167
+ for (const [, p] of filtered) {
2168
+ const y = p - c;
2169
+ const t = sum + y;
2170
+ c = t - sum - y;
2171
+ sum = t;
2172
+ }
2173
+ if (sum <= 0) throw new Error("pmfFromMap: probabilities sum to 0");
2174
+ filtered.sort((a, b) => a[0] - b[0]);
2175
+ const internal = /* @__PURE__ */ new Map();
2176
+ for (const [v, p] of filtered) {
2177
+ internal.set(v, { p: p / sum, count: {} });
2178
+ }
2179
+ return new _PMF(internal, eps);
2180
+ }
2112
2181
  query() {
2113
2182
  return new DiceQuery(this);
2114
2183
  }
@@ -2117,6 +2186,127 @@ var _PMF = class _PMF {
2117
2186
  __publicField(_PMF, "__anonIdCounter", 1);
2118
2187
  var PMF = _PMF;
2119
2188
 
2189
+ // src/mixture.ts
2190
+ var Mixture = class _Mixture {
2191
+ constructor(eps = EPS) {
2192
+ __publicField(this, "totals", /* @__PURE__ */ new Map());
2193
+ // raw mass per outcome (pre-normalization)
2194
+ __publicField(this, "labelMass", /* @__PURE__ */ new Map());
2195
+ // raw mass per outcome per label
2196
+ __publicField(this, "eps");
2197
+ this.eps = Number.isFinite(eps) ? eps : EPS;
2198
+ }
2199
+ /** Remove all accumulated state. */
2200
+ clear() {
2201
+ this.totals.clear();
2202
+ this.labelMass.clear();
2203
+ return this;
2204
+ }
2205
+ /** Number of distinct outcome values currently accumulated. */
2206
+ size() {
2207
+ return this.totals.size;
2208
+ }
2209
+ /** Whether a label was ever added. */
2210
+ hasLabel(label) {
2211
+ for (const bag of this.labelMass.values()) if (bag[label]) return true;
2212
+ return false;
2213
+ }
2214
+ /**
2215
+ * Add a labeled component with a mixture weight.
2216
+ * Weight can be any positive finite number. Very small contributions are pruned by eps.
2217
+ */
2218
+ add(label, pmf, weight = 1) {
2219
+ if (!Number.isFinite(weight) || weight <= 0) return this;
2220
+ for (const [v, binOrNumber] of pmf) {
2221
+ const isNumber = typeof binOrNumber === "number";
2222
+ const p = isNumber ? binOrNumber : binOrNumber?.p ?? 0;
2223
+ if (p <= 0) continue;
2224
+ const add = weight * p;
2225
+ if (!Number.isFinite(add) || Math.abs(add) < this.eps) continue;
2226
+ this.totals.set(v, (this.totals.get(v) ?? 0) + add);
2227
+ const bag = this.labelMass.get(v) ?? {};
2228
+ bag[label] = (bag[label] ?? 0) + add;
2229
+ this.labelMass.set(v, bag);
2230
+ }
2231
+ return this;
2232
+ }
2233
+ buildPMF(eps = EPS) {
2234
+ let grand = 0;
2235
+ let c = 0;
2236
+ for (const m of this.totals.values()) {
2237
+ const y = m - c;
2238
+ const t = grand + y;
2239
+ c = t - grand - y;
2240
+ grand = t;
2241
+ }
2242
+ if (!(grand > 0)) throw new Error("LabeledMixture: zero total mass");
2243
+ const internal = /* @__PURE__ */ new Map();
2244
+ for (const [v, m] of this.totals) {
2245
+ if (m <= 0 || Math.abs(m) < this.eps) continue;
2246
+ const count = this.labelMass.get(v) ?? {};
2247
+ internal.set(v, { p: m / grand, count });
2248
+ }
2249
+ return new PMF(internal, eps);
2250
+ }
2251
+ /**
2252
+ * Produce normalized *per-label* PMFs (labels independent).
2253
+ * These are unlabeled PMFs built from the raw mass of that label alone.
2254
+ */
2255
+ byOutcome() {
2256
+ const labels = /* @__PURE__ */ new Set();
2257
+ for (const bag of this.labelMass.values()) {
2258
+ for (const k of Object.keys(bag)) labels.add(k);
2259
+ }
2260
+ const out = {};
2261
+ for (const label of labels) {
2262
+ const m = /* @__PURE__ */ new Map();
2263
+ for (const [v, bag] of this.labelMass) {
2264
+ const w = bag[label];
2265
+ if (w && Math.abs(w) >= this.eps) m.set(v, w);
2266
+ }
2267
+ if (m.size > 0) out[label] = PMF.fromMap(m, this.eps);
2268
+ }
2269
+ return out;
2270
+ }
2271
+ /**
2272
+ * Mixture weights per label, normalized to sum to 1 over labels that appeared.
2273
+ * Uses raw mass before per-outcome normalization.
2274
+ */
2275
+ weights() {
2276
+ const res = {};
2277
+ for (const [, bag] of this.labelMass) {
2278
+ for (const [lab, w] of Object.entries(bag)) {
2279
+ if (!Number.isFinite(w) || w <= 0) continue;
2280
+ res[lab] = (res[lab] ?? 0) + w;
2281
+ }
2282
+ }
2283
+ let total = 0;
2284
+ let c = 0;
2285
+ for (const v of Object.values(res)) {
2286
+ const y = v - c;
2287
+ const t = total + y;
2288
+ c = t - total - y;
2289
+ total = t;
2290
+ }
2291
+ if (total > 0) {
2292
+ for (const k in res) res[k] = res[k] / total;
2293
+ }
2294
+ return res;
2295
+ }
2296
+ toJSON() {
2297
+ return {
2298
+ totals: Array.from(this.totals.entries()).sort((a, b) => a[0] - b[0]),
2299
+ labels: Array.from(this.labelMass.entries()).sort((a, b) => a[0] - b[0]),
2300
+ eps: this.eps
2301
+ };
2302
+ }
2303
+ static mix(items, eps = EPS) {
2304
+ const mix = new _Mixture(eps);
2305
+ for (const [lab, pmf, w] of items) mix.add(lab, pmf, w);
2306
+ return mix.buildPMF();
2307
+ }
2308
+ };
2309
+
2120
2310
  // src/dice.ts
2121
2311
  var Dice = class _Dice {
2122
2312
  constructor(x = 0) {
@@ -2610,7 +2800,7 @@ function parseExpression(arr, n) {
2610
2800
  }
2611
2801
  critNorm = crit.total();
2612
2802
  crit = op.call(crit, parseBinaryArgument(arg, arr, n));
2613
- critNorm = critNorm ? crit.total() / critNorm : 1;
2803
+ critNorm = crit && critNorm ? crit.total() / critNorm : 1;
2614
2804
  }
2615
2805
  let save;
2616
2806
  let saveNorm = 1;
@@ -2625,7 +2815,7 @@ function parseExpression(arr, n) {
2625
2815
  saveNorm = save.total();
2626
2816
  finalResult = finalResult.deleteFace(min);
2627
2817
  save = op.call(save, parseBinaryArgument(arg, arr, n));
2628
- saveNorm = saveNorm ? save.total() / saveNorm : 1;
2818
+ saveNorm = save && saveNorm ? save.total() / saveNorm : 1;
2629
2819
  }
2630
2820
  let pc;
2631
2821
  let pcNorm = 1;
@@ -2638,7 +2828,7 @@ function parseExpression(arr, n) {
2638
2828
  const missBefore = pc.total();
2639
2829
  finalResult = finalResult.deleteFace(min);
2640
2830
  pc = op.call(pc, parseBinaryArgument(arg, arr, n)).divideRoundDown(2);
2641
- const missAfter = pc.total();
2831
+ const missAfter = pc ? pc.total() : 0;
2642
2832
  pcNorm = missBefore ? missAfter / missBefore : 1;
2643
2833
  }
2644
2834
  let miss;
@@ -2654,7 +2844,7 @@ function parseExpression(arr, n) {
2654
2844
  missNorm = miss.total();
2655
2845
  finalResult = finalResult.deleteFace(min);
2656
2846
  miss = op.call(miss, parseBinaryArgument(arg, arr, n));
2657
- missNorm = missNorm ? miss.total() / missNorm : 1;
2847
+ missNorm = miss && missNorm ? miss.total() / missNorm : 1;
2658
2848
  }
2659
2849
  let norm = finalResult.total();
2660
2850
  finalResult = op.call(finalResult, arg);
@@ -2944,6 +3134,6 @@ function parseOperation(s) {
2944
3134
  return;
2945
3135
  }
2946
3136
 
2947
- export { Dice, DiceQuery, EPS, LRUCache, PMF, clearParserCache, getCachingEnabled, parse, pmfCache, setCachingEnabled };
3137
+ export { DiceQuery, EPS, LRUCache, Mixture, PMF, clearParserCache, getCachingEnabled, parse, pmfCache, setCachingEnabled };
2948
3138
  //# sourceMappingURL=index.js.map
2949
3139
  //# sourceMappingURL=index.js.map