@yipe/dice 0.9.0 → 0.11.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.
Files changed (41) hide show
  1. package/dist/builder/ac.d.ts.map +1 -1
  2. package/dist/builder/ast.d.ts +32 -2
  3. package/dist/builder/ast.d.ts.map +1 -1
  4. package/dist/builder/attack.d.ts +19 -0
  5. package/dist/builder/attack.d.ts.map +1 -1
  6. package/dist/builder/dc.d.ts.map +1 -1
  7. package/dist/builder/example.d.ts +4 -4
  8. package/dist/builder/example.d.ts.map +1 -1
  9. package/dist/builder/index.cjs +614 -121
  10. package/dist/builder/index.cjs.map +1 -1
  11. package/dist/builder/index.js +614 -122
  12. package/dist/builder/index.js.map +1 -1
  13. package/dist/builder/nodes.d.ts +1 -0
  14. package/dist/builder/nodes.d.ts.map +1 -1
  15. package/dist/builder/roll.d.ts +13 -1
  16. package/dist/builder/roll.d.ts.map +1 -1
  17. package/dist/builder/save.d.ts.map +1 -1
  18. package/dist/builder/types.d.ts +1 -0
  19. package/dist/builder/types.d.ts.map +1 -1
  20. package/dist/common/bounce.d.ts +45 -0
  21. package/dist/common/bounce.d.ts.map +1 -1
  22. package/dist/common/types.d.ts +33 -0
  23. package/dist/common/types.d.ts.map +1 -1
  24. package/dist/index.cjs +235 -15
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.js +232 -16
  27. package/dist/index.js.map +1 -1
  28. package/dist/parser/dice.d.ts +14 -0
  29. package/dist/parser/dice.d.ts.map +1 -1
  30. package/dist/pmf/pmf.d.ts +22 -4
  31. package/dist/pmf/pmf.d.ts.map +1 -1
  32. package/dist/turn/plan.d.ts +29 -6
  33. package/dist/turn/plan.d.ts.map +1 -1
  34. package/dist/turn/state.d.ts +16 -8
  35. package/dist/turn/state.d.ts.map +1 -1
  36. package/dist/turn/turn.d.ts +35 -0
  37. package/dist/turn/turn.d.ts.map +1 -1
  38. package/dist/turn/types.d.ts +32 -10
  39. package/dist/turn/types.d.ts.map +1 -1
  40. package/package.json +2 -2
  41. package/CHANGELOG.md +0 -456
package/dist/index.js CHANGED
@@ -1,4 +1,25 @@
1
1
  // src/common/bounce.ts
2
+ function faceWeights(faces, minimum = 0, reroll = 0) {
3
+ const f = Math.max(0, Math.floor(faces));
4
+ if (f <= 0) return [];
5
+ let weights = new Array(f).fill(1 / f);
6
+ const r = Math.max(0, Math.min(Math.floor(reroll), f));
7
+ if (r > 0) {
8
+ const rerollMass = r / f;
9
+ const uniformReroll = rerollMass / f;
10
+ weights = weights.map((_, i) => (i < r ? 0 : 1 / f) + uniformReroll);
11
+ }
12
+ const minV = Math.max(0, Math.floor(minimum));
13
+ if (minV > 1) {
14
+ const collapsed = new Array(f).fill(0);
15
+ for (let v = 1; v <= f; v++) {
16
+ const target = Math.min(f, Math.max(v, minV));
17
+ collapsed[target - 1] += weights[v - 1];
18
+ }
19
+ weights = collapsed;
20
+ }
21
+ return weights;
22
+ }
2
23
  function binom(n, k) {
3
24
  if (k < 0 || k > n) return 0;
4
25
  let result = 1;
@@ -47,6 +68,128 @@ function calculateBounceOdds(diceCount, dieFaces, options) {
47
68
  );
48
69
  return Math.min(1, pMatchFirst + pNoMatchFirst * pMatchAfterReroll);
49
70
  }
71
+ function diceSumDistribution(dice, weights) {
72
+ let dist = /* @__PURE__ */ new Map([[0, 1]]);
73
+ for (let die = 0; die < dice; die++) {
74
+ const next = /* @__PURE__ */ new Map();
75
+ for (const [sum, mass] of dist) {
76
+ for (let face = 1; face <= weights.length; face++) {
77
+ const w = weights[face - 1] ?? 0;
78
+ if (w <= 0) continue;
79
+ const s = sum + face;
80
+ next.set(s, (next.get(s) ?? 0) + mass * w);
81
+ }
82
+ }
83
+ dist = next;
84
+ }
85
+ return dist;
86
+ }
87
+ function sumAllDistinctDistribution(dice, weights) {
88
+ const faceCount = weights.length;
89
+ let dp = /* @__PURE__ */ new Map([[0, /* @__PURE__ */ new Map([[0, 1]])]]);
90
+ for (let face = 1; face <= faceCount; face++) {
91
+ const w = weights[face - 1] ?? 0;
92
+ const next = /* @__PURE__ */ new Map();
93
+ for (const [count, sumMap] of dp) next.set(count, new Map(sumMap));
94
+ if (w > 0) {
95
+ for (const [count, sumMap] of dp) {
96
+ const nextCount = count + 1;
97
+ if (nextCount > dice) continue;
98
+ const target = next.get(nextCount) ?? /* @__PURE__ */ new Map();
99
+ for (const [sum, mass] of sumMap) {
100
+ const s = sum + face;
101
+ target.set(s, (target.get(s) ?? 0) + mass * w);
102
+ }
103
+ next.set(nextCount, target);
104
+ }
105
+ }
106
+ dp = next;
107
+ }
108
+ let factorial = 1;
109
+ for (let i = 2; i <= dice; i++) factorial *= i;
110
+ const chosen = dp.get(dice) ?? /* @__PURE__ */ new Map();
111
+ const result = /* @__PURE__ */ new Map();
112
+ for (const [sum, mass] of chosen) result.set(sum, mass * factorial);
113
+ return result;
114
+ }
115
+ function jointSumAndMatch(dice, weights) {
116
+ if (dice <= 1) return /* @__PURE__ */ new Map();
117
+ const total = diceSumDistribution(dice, weights);
118
+ const distinct = sumAllDistinctDistribution(dice, weights);
119
+ const result = /* @__PURE__ */ new Map();
120
+ for (const [sum, mass] of total) {
121
+ const matchMass = Math.max(0, mass - (distinct.get(sum) ?? 0));
122
+ if (matchMass > 0) result.set(sum, matchMass);
123
+ }
124
+ return result;
125
+ }
126
+ function explodingPoolMkDistribution(pMax, count, budget) {
127
+ const binomial = (n, p) => {
128
+ const result = new Array(n + 1).fill(0);
129
+ result[0] = 1;
130
+ for (let trial = 0; trial < n; trial++) {
131
+ const next = new Array(n + 1).fill(0);
132
+ for (let successes = 0; successes <= trial; successes++) {
133
+ const mass = result[successes];
134
+ if (mass <= 0) continue;
135
+ next[successes] += mass * (1 - p);
136
+ next[successes + 1] += mass * p;
137
+ }
138
+ for (let i = 0; i <= n; i++) result[i] = next[i];
139
+ }
140
+ return result;
141
+ };
142
+ const memo = /* @__PURE__ */ new Map();
143
+ const f = (pending, remainingBudget) => {
144
+ if (pending === 0) return /* @__PURE__ */ new Map([["0,0", 1]]);
145
+ if (remainingBudget === 0) {
146
+ const binom2 = binomial(pending, pMax);
147
+ const result2 = /* @__PURE__ */ new Map();
148
+ for (let m = 0; m <= pending; m++) {
149
+ const mass = binom2[m];
150
+ if (mass > 0) result2.set(`${m},${pending - m}`, mass);
151
+ }
152
+ return result2;
153
+ }
154
+ const key = `${pending},${remainingBudget}`;
155
+ const cached = memo.get(key);
156
+ if (cached) return cached;
157
+ const result = /* @__PURE__ */ new Map();
158
+ const accumulate = (mk, mass) => {
159
+ result.set(mk, (result.get(mk) ?? 0) + mass);
160
+ };
161
+ for (const [mk, mass] of f(pending, remainingBudget - 1)) {
162
+ const [m, k] = mk.split(",").map(Number);
163
+ accumulate(`${m + 1},${k}`, mass * pMax);
164
+ }
165
+ for (const [mk, mass] of f(pending - 1, remainingBudget)) {
166
+ const [m, k] = mk.split(",").map(Number);
167
+ accumulate(`${m},${k + 1}`, mass * (1 - pMax));
168
+ }
169
+ memo.set(key, result);
170
+ return result;
171
+ };
172
+ return f(count, budget);
173
+ }
174
+ function explodingPoolMatchProbability(pMax, faces, count, budget) {
175
+ if (count <= 1) return 0;
176
+ const mkDistribution = explodingPoolMkDistribution(pMax, count, budget);
177
+ const nonMaxFaces = Math.max(1, faces - 1);
178
+ let pMatchTotal = 0;
179
+ for (const [mk, weight] of mkDistribution) {
180
+ const [m, k] = mk.split(",").map(Number);
181
+ if (m >= 2) {
182
+ pMatchTotal += weight;
183
+ continue;
184
+ }
185
+ let pAllDistinctAmongNonMax = 1;
186
+ for (let i = 0; i < k; i++) {
187
+ pAllDistinctAmongNonMax *= (nonMaxFaces - i) / nonMaxFaces;
188
+ }
189
+ pMatchTotal += weight * (1 - Math.max(0, pAllDistinctAmongNonMax));
190
+ }
191
+ return Math.min(1, Math.max(0, pMatchTotal));
192
+ }
50
193
 
51
194
  // src/common/errors.ts
52
195
  var DiceParseError = class _DiceParseError extends Error {
@@ -1566,7 +1709,7 @@ var _PMF = class _PMF {
1566
1709
  const id = this.identifier;
1567
1710
  let key = `${id}`;
1568
1711
  for (let i = 1; i < n; i++) key += `+${id}`;
1569
- return `${key}@${eps}`;
1712
+ return `${key}@${eps}|${this.fingerprint()}`;
1570
1713
  }
1571
1714
  /**
1572
1715
  * Efficiently computes this PMF convolved with itself `n` times.
@@ -1878,6 +2021,30 @@ var _PMF = class _PMF {
1878
2021
  `freq(${this.identifier},${freq})`
1879
2022
  );
1880
2023
  }
2024
+ /**
2025
+ * Splits this PMF into two complementary PMFs by an arbitrary per-damage-value factor in
2026
+ * `[0, 1]` — bin `d`'s mass, `count`, and `attr` split `factor(d)` / `1 - factor(d)` between the
2027
+ * two results (via the same proportional scaling {@link applyHitFrequency} uses, `scaleBin`), so
2028
+ * `a.add(b)` recovers this PMF exactly and both halves stay chart-attributable. Unlike
2029
+ * {@link applyHitFrequency}, mass is NOT redistributed to a miss bin at 0 — each bin stays at its
2030
+ * own damage value in whichever half it lands in. `factor` outside `[0, 1]` is clamped.
2031
+ *
2032
+ * Built for `dice-match` trigger slicing: splitting a hit/crit sub-PMF into "matched" and
2033
+ * "did not match" halves by the exact per-damage-value match probability.
2034
+ */
2035
+ splitByFactor(factor) {
2036
+ const a = /* @__PURE__ */ new Map();
2037
+ const b = /* @__PURE__ */ new Map();
2038
+ for (const [damage, bin] of this.map) {
2039
+ const f = Math.min(1, Math.max(0, factor(damage)));
2040
+ if (f > 0) a.set(damage, _PMF.scaleBin(bin, f));
2041
+ if (f < 1) b.set(damage, _PMF.scaleBin(bin, 1 - f));
2042
+ }
2043
+ return [
2044
+ new _PMF(a, this.epsilon, false, `split+(${this.identifier})`),
2045
+ new _PMF(b, this.epsilon, false, `split-(${this.identifier})`)
2046
+ ];
2047
+ }
1881
2048
  scaleMass(factor) {
1882
2049
  if (factor === 1) return this;
1883
2050
  const scaledMap = /* @__PURE__ */ new Map();
@@ -1917,16 +2084,27 @@ var _PMF = class _PMF {
1917
2084
  return `v4:${raw ? "RAW" : "N"}:${id1}+${id2}@${eps}|${p1.fingerprint()}|${p2.fingerprint()}`;
1918
2085
  }
1919
2086
  /**
1920
- * A small content fingerprint (mass + bin count + face sum) so convolution
1921
- * cache keys change if the underlying numbers do. Memoized because a PMF is
1922
- * immutable once constructed — this avoids re-summing every key on each
1923
- * convolve() call (including cache hits).
2087
+ * A content fingerprint of every bin (probability, per-label `count`, per-label `attr`) plus
2088
+ * the `normalized` flag, so convolution/power cache keys change whenever the underlying
2089
+ * numbers do. Mass/bin-count/face-sum alone are not content-unique: `mapDamage` variants can
2090
+ * keep the same identifier, support, mass, and face sum while differing in per-bin
2091
+ * probabilities or in the `count`/`attr` channels `convolve()`/`power()` actually propagate --
2092
+ * that previously let `power()` return one PMF's cached result for a different PMF. Memoized
2093
+ * because a PMF is immutable once constructed -- this avoids re-deriving the key on every
2094
+ * convolve()/power() call (including cache hits). Bin order is sorted by damage value (and
2095
+ * label keys sorted within each bin) so two equal-content PMFs built via different code paths
2096
+ * fingerprint identically regardless of Map insertion order.
1924
2097
  */
1925
2098
  fingerprint() {
1926
2099
  if (this._fingerprint === void 0) {
1927
- let faceSum = 0;
1928
- for (const k of this.map.keys()) faceSum += k;
1929
- this._fingerprint = `${this.mass().toFixed(12)}|${this.map.size}|${faceSum}`;
2100
+ const bins = [...this.map.entries()].sort((a, b) => a[0] - b[0]);
2101
+ const parts = [];
2102
+ for (const [damageValue, bin] of bins) {
2103
+ const countStr = Object.keys(bin.count).sort().map((k) => `${k}:${bin.count[k]}`).join(",");
2104
+ const attrStr = bin.attr ? Object.keys(bin.attr).sort().map((k) => `${k}:${bin.attr[k]}`).join(",") : "";
2105
+ parts.push(`${damageValue}:${bin.p}[${countStr}]{${attrStr}}`);
2106
+ }
2107
+ this._fingerprint = `${this.normalized ? 1 : 0}|${parts.join(";")}`;
1930
2108
  }
1931
2109
  return this._fingerprint;
1932
2110
  }
@@ -2158,11 +2336,13 @@ var _PMF = class _PMF {
2158
2336
  /** Quantile / inverse CDF for p in [0,1]. Returns smallest x with CDF ≥ p. */
2159
2337
  quantile(p) {
2160
2338
  if (this.map.size === 0) return 0;
2339
+ const totalMass = this.mass();
2340
+ if (totalMass <= 0) return 0;
2161
2341
  const s = this.support().sort((a, b) => a - b);
2162
2342
  let acc = 0;
2163
2343
  for (const x of s) {
2164
2344
  acc += this.pAt(x);
2165
- if (acc >= p) return x;
2345
+ if (acc / totalMass >= p) return x;
2166
2346
  }
2167
2347
  return s[s.length - 1];
2168
2348
  }
@@ -3023,6 +3203,12 @@ function combineDiceWithNormalization(dice, normValue, outcomeType, currentNorm,
3023
3203
  finalResult = finalResult.combine(dice);
3024
3204
  return { newNorm: currentNorm * normValue, updatedResult: finalResult };
3025
3205
  }
3206
+ function subtractCounts(a, b) {
3207
+ const result = new Dice();
3208
+ for (const [key, value] of a.getFaceEntries()) result.increment(key, value);
3209
+ for (const [key, value] of b.getFaceEntries()) result.increment(key, -value);
3210
+ return result;
3211
+ }
3026
3212
  function parseExpression(arr, n) {
3027
3213
  const result = (() => {
3028
3214
  const res = parseArgument(arr, n);
@@ -3030,8 +3216,29 @@ function parseExpression(arr, n) {
3030
3216
  })();
3031
3217
  let op = parseOperation(arr);
3032
3218
  let finalResult = result;
3219
+ let baseDieMeta = result.privateData?.checkDie && !result.privateData.checkDie.rerollOne ? result.privateData.checkDie : void 0;
3220
+ let bonusOnly = Dice.scalar(0);
3033
3221
  while (op != null) {
3034
3222
  const arg = !op.unary ? parseArgument(arr, n) : finalResult;
3223
+ let acAlreadyApplied = false;
3224
+ if (baseDieMeta) {
3225
+ if (op === Dice.prototype.addNonZero) {
3226
+ bonusOnly = bonusOnly.add(arg);
3227
+ } else if (op === Dice.prototype.subtract) {
3228
+ bonusOnly = bonusOnly.subtract(arg);
3229
+ } else if (op === Dice.prototype.ac && typeof arg === "number") {
3230
+ const natMaxSlice = bonusOnly.add(baseDieMeta.sides);
3231
+ const restSlice = subtractCounts(finalResult, natMaxSlice);
3232
+ const gatedNatMaxSlice = natMaxSlice.ac(arg);
3233
+ finalResult = restSlice.ac(arg).combine(gatedNatMaxSlice);
3234
+ finalResult.privateData.checkDie = baseDieMeta;
3235
+ finalResult.privateData.natMaxCritSlice = gatedNatMaxSlice;
3236
+ acAlreadyApplied = true;
3237
+ baseDieMeta = void 0;
3238
+ } else {
3239
+ baseDieMeta = void 0;
3240
+ }
3241
+ }
3035
3242
  let crit;
3036
3243
  let critNorm = 1;
3037
3244
  if (arr[0] === "x" || arr[0] === "c") {
@@ -3042,11 +3249,17 @@ function parseExpression(arr, n) {
3042
3249
  assertToken(arr, "i");
3043
3250
  assertToken(arr, "t");
3044
3251
  const count = isXcrit ? parseNumber(arr, n) : 1;
3045
- crit = new Dice();
3046
- for (let i = 0; i < count; i++) {
3047
- const max = finalResult.maxFace();
3048
- crit.setFace(max, finalResult.get(max));
3049
- finalResult = finalResult.deleteFace(max);
3252
+ const trackedCritSlice = finalResult.privateData?.natMaxCritSlice;
3253
+ if (count === 1 && trackedCritSlice) {
3254
+ crit = trackedCritSlice;
3255
+ finalResult = subtractCounts(finalResult, trackedCritSlice);
3256
+ } else {
3257
+ crit = new Dice();
3258
+ for (let i = 0; i < count; i++) {
3259
+ const max = finalResult.maxFace();
3260
+ crit.setFace(max, finalResult.get(max));
3261
+ finalResult = finalResult.deleteFace(max);
3262
+ }
3050
3263
  }
3051
3264
  critNorm = crit.total();
3052
3265
  crit = op.call(crit, parseBinaryArgument(arg, arr, n));
@@ -3097,7 +3310,9 @@ function parseExpression(arr, n) {
3097
3310
  missNorm = miss && missNorm ? miss.total() / missNorm : 1;
3098
3311
  }
3099
3312
  let norm = finalResult.total();
3100
- finalResult = op.call(finalResult, arg);
3313
+ if (!acAlreadyApplied) {
3314
+ finalResult = op.call(finalResult, arg);
3315
+ }
3101
3316
  norm = norm ? finalResult.total() / norm : 1;
3102
3317
  if (crit) {
3103
3318
  const result2 = combineDiceWithNormalization(
@@ -3288,6 +3503,7 @@ function parseDice(s, n) {
3288
3503
  if (rerollOne) {
3289
3504
  result = result.reroll(1);
3290
3505
  }
3506
+ result.privateData.checkDie = { sides, rerollOne };
3291
3507
  return result;
3292
3508
  }
3293
3509
  function peek(arr, expected) {
@@ -3575,6 +3791,6 @@ var Mixture = class _Mixture {
3575
3791
  }
3576
3792
  };
3577
3793
 
3578
- export { ALL_OUTCOME_TYPES, DiceParseError, DiceQuery, EPS, LRUCache, MISS_NONE_OUTCOME, Mixture, OUTCOME_DISPLAY_ORDER, PMF, calculateBounceOdds, clearParserCache, critProbability, getCachingEnabled, onAnyHit, onCritOnly, onHitOnly, onMissDamageOnly, onMissOnly, onPotentCantripOnly, onSaveFailOnly, onSaveHalfOnly, parse, pmfCache, setCachingEnabled, sortOutcomes, tryParse, withRollType };
3794
+ export { ALL_OUTCOME_TYPES, DiceParseError, DiceQuery, EPS, LRUCache, MISS_NONE_OUTCOME, Mixture, OUTCOME_DISPLAY_ORDER, PMF, calculateBounceOdds, clearParserCache, critProbability, diceSumDistribution, explodingPoolMatchProbability, faceWeights, getCachingEnabled, jointSumAndMatch, onAnyHit, onCritOnly, onHitOnly, onMissDamageOnly, onMissOnly, onPotentCantripOnly, onSaveFailOnly, onSaveHalfOnly, parse, pmfCache, setCachingEnabled, sortOutcomes, tryParse, withRollType };
3579
3795
  //# sourceMappingURL=index.js.map
3580
3796
  //# sourceMappingURL=index.js.map