@yipe/dice 0.8.1 → 0.9.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.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)
@@ -3345,6 +3398,65 @@ function parseOperation(s) {
3345
3398
  return;
3346
3399
  }
3347
3400
 
3401
+ // src/parser/rollType.ts
3402
+ var DECIMAL_INTEGER = /^\s*[+-]?\d+\s*$/;
3403
+ function tryParse(expression) {
3404
+ if (DECIMAL_INTEGER.test(expression)) {
3405
+ const value = Number(expression);
3406
+ return Number.isSafeInteger(value) ? PMF.delta(value) : PMF.empty();
3407
+ }
3408
+ try {
3409
+ return parse(expression);
3410
+ } catch {
3411
+ return PMF.empty();
3412
+ }
3413
+ }
3414
+ var CHECK_TOKEN = /\b(AC|DC)\b/i;
3415
+ var D20_RUN = /\bh?d20(?:\s*[><]\s*h?d20)*/i;
3416
+ var RUN_FOR_ROLL_TYPE = {
3417
+ flat: "d20",
3418
+ advantage: "d20 > d20",
3419
+ disadvantage: "d20 < d20",
3420
+ "elven accuracy": "d20 > d20 > d20"
3421
+ };
3422
+ function withRollType(expression, rollType) {
3423
+ const scopes = enclosingScopes(expression);
3424
+ let rewritten = "";
3425
+ let copiedUpTo = 0;
3426
+ for (const run of expression.matchAll(new RegExp(D20_RUN, "gi"))) {
3427
+ const at = run.index;
3428
+ if (!isAttackRoll(expression, scopes, at)) continue;
3429
+ const replacement = run[0].toLowerCase().startsWith("h") ? `h${RUN_FOR_ROLL_TYPE[rollType]}` : RUN_FOR_ROLL_TYPE[rollType];
3430
+ rewritten += expression.slice(copiedUpTo, at) + replacement;
3431
+ copiedUpTo = at + run[0].length;
3432
+ }
3433
+ return rewritten + expression.slice(copiedUpTo);
3434
+ }
3435
+ function enclosingScopes(expression) {
3436
+ const open = [];
3437
+ const scopes = [];
3438
+ for (let i = 0; i < expression.length; i++) {
3439
+ if (expression[i] === "(") open.push(i);
3440
+ else if (expression[i] === ")") {
3441
+ const start = open.pop();
3442
+ if (start !== void 0) scopes.push({ start, end: i + 1 });
3443
+ }
3444
+ }
3445
+ return scopes.sort((a, b) => a.end - a.start - (b.end - b.start));
3446
+ }
3447
+ function isAttackRoll(expression, scopes, at) {
3448
+ let scoped = false;
3449
+ for (const scope of scopes) {
3450
+ if (at < scope.start || at >= scope.end) continue;
3451
+ scoped = true;
3452
+ const check2 = CHECK_TOKEN.exec(expression.slice(scope.start, scope.end));
3453
+ if (check2) return check2[1].toUpperCase() === "AC";
3454
+ }
3455
+ if (scoped) return false;
3456
+ const check = CHECK_TOKEN.exec(expression);
3457
+ return check !== null && check[1].toUpperCase() === "AC";
3458
+ }
3459
+
3348
3460
  // src/pmf/mixture.ts
3349
3461
  var Mixture = class _Mixture {
3350
3462
  constructor(eps = EPS) {
@@ -3463,6 +3575,6 @@ var Mixture = class _Mixture {
3463
3575
  }
3464
3576
  };
3465
3577
 
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 };
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 };
3467
3579
  //# sourceMappingURL=index.js.map
3468
3580
  //# sourceMappingURL=index.js.map