@yipe/dice 0.8.1 → 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.
Files changed (42) hide show
  1. package/README.md +169 -19
  2. package/dist/builder/ac.d.ts.map +1 -1
  3. package/dist/builder/ast.d.ts +32 -2
  4. package/dist/builder/ast.d.ts.map +1 -1
  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 +889 -90
  10. package/dist/builder/index.cjs.map +1 -1
  11. package/dist/builder/index.d.ts +1 -0
  12. package/dist/builder/index.d.ts.map +1 -1
  13. package/dist/builder/index.js +886 -91
  14. package/dist/builder/index.js.map +1 -1
  15. package/dist/builder/roll.d.ts +5 -1
  16. package/dist/builder/roll.d.ts.map +1 -1
  17. package/dist/builder/save.d.ts.map +1 -1
  18. package/dist/index.cjs +178 -15
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +177 -16
  23. package/dist/index.js.map +1 -1
  24. package/dist/parser/dice.d.ts +14 -0
  25. package/dist/parser/dice.d.ts.map +1 -1
  26. package/dist/parser/rollType.d.ts +58 -0
  27. package/dist/parser/rollType.d.ts.map +1 -0
  28. package/dist/pmf/pmf.d.ts +10 -4
  29. package/dist/pmf/pmf.d.ts.map +1 -1
  30. package/dist/pmf/query.d.ts +26 -0
  31. package/dist/pmf/query.d.ts.map +1 -1
  32. package/dist/turn/index.d.ts +3 -0
  33. package/dist/turn/index.d.ts.map +1 -0
  34. package/dist/turn/plan.d.ts +55 -0
  35. package/dist/turn/plan.d.ts.map +1 -0
  36. package/dist/turn/state.d.ts +23 -0
  37. package/dist/turn/state.d.ts.map +1 -0
  38. package/dist/turn/turn.d.ts +149 -0
  39. package/dist/turn/turn.d.ts.map +1 -0
  40. package/dist/turn/types.d.ts +99 -0
  41. package/dist/turn/types.d.ts.map +1 -0
  42. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./common/errors.js";
3
3
  export * from "./common/lru-cache.js";
4
4
  export * from "./common/types.js";
5
5
  export * from "./parser/parser.js";
6
+ export * from "./parser/rollType.js";
6
7
  export * from "./pmf/mixture.js";
7
8
  export * from "./pmf/pmf.js";
8
9
  export * from "./pmf/query.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -207,6 +207,10 @@ var _DiceQuery = class _DiceQuery {
207
207
  if (this._combinedWithAttr) {
208
208
  return this._combinedWithAttr;
209
209
  }
210
+ if (this._combinedProvided) {
211
+ this._combinedWithAttr = this.combined.withAttribution();
212
+ return this._combinedWithAttr;
213
+ }
210
214
  if (this.singles.every((pmf) => pmf.hasAttribution())) {
211
215
  this._combinedWithAttr = this.combined;
212
216
  return this._combinedWithAttr;
@@ -977,6 +981,55 @@ var _DiceQuery = class _DiceQuery {
977
981
  }
978
982
  return out;
979
983
  }
984
+ /**
985
+ * Per-outcome probabilities and damage ranges, aggregated over the individual
986
+ * singles rather than read off the combined distribution.
987
+ *
988
+ * `damageRange` is the sum, over every single that can produce the outcome, of
989
+ * that single's own conditional damage range: "what this outcome contributes
990
+ * across the whole turn when every attack that can produce it does". Linear in
991
+ * the number of attacks by construction.
992
+ *
993
+ * Prefer this over {@link DiceQuery.snapshot} for a multi-attack query.
994
+ * `snapshot` reads `damageRange` off the combined PMF's `count`, which the
995
+ * convolution accumulates as an expected count, so its `avg` is size-biased
996
+ * for N≥2 (its own doc comment says so). The two agree for a single attack.
997
+ *
998
+ * Only outcomes that actually occur appear in the result.
999
+ *
1000
+ * Like every `singles`-based helper on this class, it describes the singles
1001
+ * and not an explicitly provided `combined`. `Turn.toQuery()` supplies one whose
1002
+ * distribution also contains rider attacks that are absent from `singles`
1003
+ * (an `otherwise([unarmed, unarmed])` flurry, say), so those attacks do not
1004
+ * appear here. For rider-inclusive figures read the combined distribution
1005
+ * directly: {@link DiceQuery.outcomeTotals}, {@link DiceQuery.outcomeDamageRanges}.
1006
+ *
1007
+ * @param outcomes Which outcomes to consider; defaults to every canonical one.
1008
+ */
1009
+ outcomeStats(outcomes = ALL_OUTCOME_TYPES) {
1010
+ const stats = /* @__PURE__ */ new Map();
1011
+ const perSingle = this.singles.map((pmf) => new _DiceQuery([pmf], void 0, this._eps));
1012
+ for (const outcome of outcomes) {
1013
+ const atLeastOneProbability = this.probAtLeastOne(outcome);
1014
+ if (atLeastOneProbability <= 0) continue;
1015
+ const damageRange = { min: 0, avg: 0, max: 0 };
1016
+ let contributors = 0;
1017
+ for (const single of perSingle) {
1018
+ if (single.probAtLeastOne(outcome) <= 0) continue;
1019
+ contributors++;
1020
+ const stat = single.damageStatsFrom(outcome);
1021
+ damageRange.min += stat.min;
1022
+ damageRange.avg += stat.avg;
1023
+ damageRange.max += stat.max;
1024
+ }
1025
+ stats.set(outcome, {
1026
+ atLeastOneProbability,
1027
+ allProbability: contributors > 0 ? this.probExactlyK(outcome, contributors) : 0,
1028
+ damageRange
1029
+ });
1030
+ }
1031
+ return stats;
1032
+ }
980
1033
  /**
981
1034
  * Snapshot of the distribution in the exact shape the UI consumes.
982
1035
  * - outcome probabilities are "at least one" (and equal to "all" for a single PMF)
@@ -1513,7 +1566,7 @@ var _PMF = class _PMF {
1513
1566
  const id = this.identifier;
1514
1567
  let key = `${id}`;
1515
1568
  for (let i = 1; i < n; i++) key += `+${id}`;
1516
- return `${key}@${eps}`;
1569
+ return `${key}@${eps}|${this.fingerprint()}`;
1517
1570
  }
1518
1571
  /**
1519
1572
  * Efficiently computes this PMF convolved with itself `n` times.
@@ -1864,16 +1917,27 @@ var _PMF = class _PMF {
1864
1917
  return `v4:${raw ? "RAW" : "N"}:${id1}+${id2}@${eps}|${p1.fingerprint()}|${p2.fingerprint()}`;
1865
1918
  }
1866
1919
  /**
1867
- * A small content fingerprint (mass + bin count + face sum) so convolution
1868
- * cache keys change if the underlying numbers do. Memoized because a PMF is
1869
- * immutable once constructed — this avoids re-summing every key on each
1870
- * 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.
1871
1930
  */
1872
1931
  fingerprint() {
1873
1932
  if (this._fingerprint === void 0) {
1874
- let faceSum = 0;
1875
- for (const k of this.map.keys()) faceSum += k;
1876
- 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(";")}`;
1877
1941
  }
1878
1942
  return this._fingerprint;
1879
1943
  }
@@ -2105,11 +2169,13 @@ var _PMF = class _PMF {
2105
2169
  /** Quantile / inverse CDF for p in [0,1]. Returns smallest x with CDF ≥ p. */
2106
2170
  quantile(p) {
2107
2171
  if (this.map.size === 0) return 0;
2172
+ const totalMass = this.mass();
2173
+ if (totalMass <= 0) return 0;
2108
2174
  const s = this.support().sort((a, b) => a - b);
2109
2175
  let acc = 0;
2110
2176
  for (const x of s) {
2111
2177
  acc += this.pAt(x);
2112
- if (acc >= p) return x;
2178
+ if (acc / totalMass >= p) return x;
2113
2179
  }
2114
2180
  return s[s.length - 1];
2115
2181
  }
@@ -2970,6 +3036,12 @@ function combineDiceWithNormalization(dice, normValue, outcomeType, currentNorm,
2970
3036
  finalResult = finalResult.combine(dice);
2971
3037
  return { newNorm: currentNorm * normValue, updatedResult: finalResult };
2972
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
+ }
2973
3045
  function parseExpression(arr, n) {
2974
3046
  const result = (() => {
2975
3047
  const res = parseArgument(arr, n);
@@ -2977,8 +3049,29 @@ function parseExpression(arr, n) {
2977
3049
  })();
2978
3050
  let op = parseOperation(arr);
2979
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);
2980
3054
  while (op != null) {
2981
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
+ }
2982
3075
  let crit;
2983
3076
  let critNorm = 1;
2984
3077
  if (arr[0] === "x" || arr[0] === "c") {
@@ -2989,11 +3082,17 @@ function parseExpression(arr, n) {
2989
3082
  assertToken(arr, "i");
2990
3083
  assertToken(arr, "t");
2991
3084
  const count = isXcrit ? parseNumber(arr, n) : 1;
2992
- crit = new Dice();
2993
- for (let i = 0; i < count; i++) {
2994
- const max = finalResult.maxFace();
2995
- crit.setFace(max, finalResult.get(max));
2996
- 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
+ }
2997
3096
  }
2998
3097
  critNorm = crit.total();
2999
3098
  crit = op.call(crit, parseBinaryArgument(arg, arr, n));
@@ -3044,7 +3143,9 @@ function parseExpression(arr, n) {
3044
3143
  missNorm = miss && missNorm ? miss.total() / missNorm : 1;
3045
3144
  }
3046
3145
  let norm = finalResult.total();
3047
- finalResult = op.call(finalResult, arg);
3146
+ if (!acAlreadyApplied) {
3147
+ finalResult = op.call(finalResult, arg);
3148
+ }
3048
3149
  norm = norm ? finalResult.total() / norm : 1;
3049
3150
  if (crit) {
3050
3151
  const result2 = combineDiceWithNormalization(
@@ -3235,6 +3336,7 @@ function parseDice(s, n) {
3235
3336
  if (rerollOne) {
3236
3337
  result = result.reroll(1);
3237
3338
  }
3339
+ result.privateData.checkDie = { sides, rerollOne };
3238
3340
  return result;
3239
3341
  }
3240
3342
  function peek(arr, expected) {
@@ -3345,6 +3447,65 @@ function parseOperation(s) {
3345
3447
  return;
3346
3448
  }
3347
3449
 
3450
+ // src/parser/rollType.ts
3451
+ var DECIMAL_INTEGER = /^\s*[+-]?\d+\s*$/;
3452
+ function tryParse(expression) {
3453
+ if (DECIMAL_INTEGER.test(expression)) {
3454
+ const value = Number(expression);
3455
+ return Number.isSafeInteger(value) ? PMF.delta(value) : PMF.empty();
3456
+ }
3457
+ try {
3458
+ return parse(expression);
3459
+ } catch {
3460
+ return PMF.empty();
3461
+ }
3462
+ }
3463
+ var CHECK_TOKEN = /\b(AC|DC)\b/i;
3464
+ var D20_RUN = /\bh?d20(?:\s*[><]\s*h?d20)*/i;
3465
+ var RUN_FOR_ROLL_TYPE = {
3466
+ flat: "d20",
3467
+ advantage: "d20 > d20",
3468
+ disadvantage: "d20 < d20",
3469
+ "elven accuracy": "d20 > d20 > d20"
3470
+ };
3471
+ function withRollType(expression, rollType) {
3472
+ const scopes = enclosingScopes(expression);
3473
+ let rewritten = "";
3474
+ let copiedUpTo = 0;
3475
+ for (const run of expression.matchAll(new RegExp(D20_RUN, "gi"))) {
3476
+ const at = run.index;
3477
+ if (!isAttackRoll(expression, scopes, at)) continue;
3478
+ const replacement = run[0].toLowerCase().startsWith("h") ? `h${RUN_FOR_ROLL_TYPE[rollType]}` : RUN_FOR_ROLL_TYPE[rollType];
3479
+ rewritten += expression.slice(copiedUpTo, at) + replacement;
3480
+ copiedUpTo = at + run[0].length;
3481
+ }
3482
+ return rewritten + expression.slice(copiedUpTo);
3483
+ }
3484
+ function enclosingScopes(expression) {
3485
+ const open = [];
3486
+ const scopes = [];
3487
+ for (let i = 0; i < expression.length; i++) {
3488
+ if (expression[i] === "(") open.push(i);
3489
+ else if (expression[i] === ")") {
3490
+ const start = open.pop();
3491
+ if (start !== void 0) scopes.push({ start, end: i + 1 });
3492
+ }
3493
+ }
3494
+ return scopes.sort((a, b) => a.end - a.start - (b.end - b.start));
3495
+ }
3496
+ function isAttackRoll(expression, scopes, at) {
3497
+ let scoped = false;
3498
+ for (const scope of scopes) {
3499
+ if (at < scope.start || at >= scope.end) continue;
3500
+ scoped = true;
3501
+ const check2 = CHECK_TOKEN.exec(expression.slice(scope.start, scope.end));
3502
+ if (check2) return check2[1].toUpperCase() === "AC";
3503
+ }
3504
+ if (scoped) return false;
3505
+ const check = CHECK_TOKEN.exec(expression);
3506
+ return check !== null && check[1].toUpperCase() === "AC";
3507
+ }
3508
+
3348
3509
  // src/pmf/mixture.ts
3349
3510
  var Mixture = class _Mixture {
3350
3511
  constructor(eps = EPS) {
@@ -3463,6 +3624,6 @@ var Mixture = class _Mixture {
3463
3624
  }
3464
3625
  };
3465
3626
 
3466
- 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 };
3627
+ 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 };
3467
3628
  //# sourceMappingURL=index.js.map
3468
3629
  //# sourceMappingURL=index.js.map