@yipe/dice 0.9.0 → 0.10.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/dist/index.js CHANGED
@@ -1566,7 +1566,7 @@ var _PMF = class _PMF {
1566
1566
  const id = this.identifier;
1567
1567
  let key = `${id}`;
1568
1568
  for (let i = 1; i < n; i++) key += `+${id}`;
1569
- return `${key}@${eps}`;
1569
+ return `${key}@${eps}|${this.fingerprint()}`;
1570
1570
  }
1571
1571
  /**
1572
1572
  * Efficiently computes this PMF convolved with itself `n` times.
@@ -1917,16 +1917,27 @@ var _PMF = class _PMF {
1917
1917
  return `v4:${raw ? "RAW" : "N"}:${id1}+${id2}@${eps}|${p1.fingerprint()}|${p2.fingerprint()}`;
1918
1918
  }
1919
1919
  /**
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).
1920
+ * A content fingerprint of every bin (probability, per-label `count`, per-label `attr`) plus
1921
+ * the `normalized` flag, so convolution/power cache keys change whenever the underlying
1922
+ * numbers do. Mass/bin-count/face-sum alone are not content-unique: `mapDamage` variants can
1923
+ * keep the same identifier, support, mass, and face sum while differing in per-bin
1924
+ * probabilities or in the `count`/`attr` channels `convolve()`/`power()` actually propagate --
1925
+ * that previously let `power()` return one PMF's cached result for a different PMF. Memoized
1926
+ * because a PMF is immutable once constructed -- this avoids re-deriving the key on every
1927
+ * convolve()/power() call (including cache hits). Bin order is sorted by damage value (and
1928
+ * label keys sorted within each bin) so two equal-content PMFs built via different code paths
1929
+ * fingerprint identically regardless of Map insertion order.
1924
1930
  */
1925
1931
  fingerprint() {
1926
1932
  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}`;
1933
+ const bins = [...this.map.entries()].sort((a, b) => a[0] - b[0]);
1934
+ const parts = [];
1935
+ for (const [damageValue, bin] of bins) {
1936
+ const countStr = Object.keys(bin.count).sort().map((k) => `${k}:${bin.count[k]}`).join(",");
1937
+ const attrStr = bin.attr ? Object.keys(bin.attr).sort().map((k) => `${k}:${bin.attr[k]}`).join(",") : "";
1938
+ parts.push(`${damageValue}:${bin.p}[${countStr}]{${attrStr}}`);
1939
+ }
1940
+ this._fingerprint = `${this.normalized ? 1 : 0}|${parts.join(";")}`;
1930
1941
  }
1931
1942
  return this._fingerprint;
1932
1943
  }
@@ -2158,11 +2169,13 @@ var _PMF = class _PMF {
2158
2169
  /** Quantile / inverse CDF for p in [0,1]. Returns smallest x with CDF ≥ p. */
2159
2170
  quantile(p) {
2160
2171
  if (this.map.size === 0) return 0;
2172
+ const totalMass = this.mass();
2173
+ if (totalMass <= 0) return 0;
2161
2174
  const s = this.support().sort((a, b) => a - b);
2162
2175
  let acc = 0;
2163
2176
  for (const x of s) {
2164
2177
  acc += this.pAt(x);
2165
- if (acc >= p) return x;
2178
+ if (acc / totalMass >= p) return x;
2166
2179
  }
2167
2180
  return s[s.length - 1];
2168
2181
  }
@@ -3023,6 +3036,12 @@ function combineDiceWithNormalization(dice, normValue, outcomeType, currentNorm,
3023
3036
  finalResult = finalResult.combine(dice);
3024
3037
  return { newNorm: currentNorm * normValue, updatedResult: finalResult };
3025
3038
  }
3039
+ function subtractCounts(a, b) {
3040
+ const result = new Dice();
3041
+ for (const [key, value] of a.getFaceEntries()) result.increment(key, value);
3042
+ for (const [key, value] of b.getFaceEntries()) result.increment(key, -value);
3043
+ return result;
3044
+ }
3026
3045
  function parseExpression(arr, n) {
3027
3046
  const result = (() => {
3028
3047
  const res = parseArgument(arr, n);
@@ -3030,8 +3049,29 @@ function parseExpression(arr, n) {
3030
3049
  })();
3031
3050
  let op = parseOperation(arr);
3032
3051
  let finalResult = result;
3052
+ let baseDieMeta = result.privateData?.checkDie && !result.privateData.checkDie.rerollOne ? result.privateData.checkDie : void 0;
3053
+ let bonusOnly = Dice.scalar(0);
3033
3054
  while (op != null) {
3034
3055
  const arg = !op.unary ? parseArgument(arr, n) : finalResult;
3056
+ let acAlreadyApplied = false;
3057
+ if (baseDieMeta) {
3058
+ if (op === Dice.prototype.addNonZero) {
3059
+ bonusOnly = bonusOnly.add(arg);
3060
+ } else if (op === Dice.prototype.subtract) {
3061
+ bonusOnly = bonusOnly.subtract(arg);
3062
+ } else if (op === Dice.prototype.ac && typeof arg === "number") {
3063
+ const natMaxSlice = bonusOnly.add(baseDieMeta.sides);
3064
+ const restSlice = subtractCounts(finalResult, natMaxSlice);
3065
+ const gatedNatMaxSlice = natMaxSlice.ac(arg);
3066
+ finalResult = restSlice.ac(arg).combine(gatedNatMaxSlice);
3067
+ finalResult.privateData.checkDie = baseDieMeta;
3068
+ finalResult.privateData.natMaxCritSlice = gatedNatMaxSlice;
3069
+ acAlreadyApplied = true;
3070
+ baseDieMeta = void 0;
3071
+ } else {
3072
+ baseDieMeta = void 0;
3073
+ }
3074
+ }
3035
3075
  let crit;
3036
3076
  let critNorm = 1;
3037
3077
  if (arr[0] === "x" || arr[0] === "c") {
@@ -3042,11 +3082,17 @@ function parseExpression(arr, n) {
3042
3082
  assertToken(arr, "i");
3043
3083
  assertToken(arr, "t");
3044
3084
  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);
3085
+ const trackedCritSlice = finalResult.privateData?.natMaxCritSlice;
3086
+ if (count === 1 && trackedCritSlice) {
3087
+ crit = trackedCritSlice;
3088
+ finalResult = subtractCounts(finalResult, trackedCritSlice);
3089
+ } else {
3090
+ crit = new Dice();
3091
+ for (let i = 0; i < count; i++) {
3092
+ const max = finalResult.maxFace();
3093
+ crit.setFace(max, finalResult.get(max));
3094
+ finalResult = finalResult.deleteFace(max);
3095
+ }
3050
3096
  }
3051
3097
  critNorm = crit.total();
3052
3098
  crit = op.call(crit, parseBinaryArgument(arg, arr, n));
@@ -3097,7 +3143,9 @@ function parseExpression(arr, n) {
3097
3143
  missNorm = miss && missNorm ? miss.total() / missNorm : 1;
3098
3144
  }
3099
3145
  let norm = finalResult.total();
3100
- finalResult = op.call(finalResult, arg);
3146
+ if (!acAlreadyApplied) {
3147
+ finalResult = op.call(finalResult, arg);
3148
+ }
3101
3149
  norm = norm ? finalResult.total() / norm : 1;
3102
3150
  if (crit) {
3103
3151
  const result2 = combineDiceWithNormalization(
@@ -3288,6 +3336,7 @@ function parseDice(s, n) {
3288
3336
  if (rerollOne) {
3289
3337
  result = result.reroll(1);
3290
3338
  }
3339
+ result.privateData.checkDie = { sides, rerollOne };
3291
3340
  return result;
3292
3341
  }
3293
3342
  function peek(arr, expected) {