@yipe/dice 0.7.0 → 0.8.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.
@@ -16,6 +16,7 @@ export declare class ACBuilder extends RollBuilder {
16
16
  onHit(count: number, die: RollBuilder, modifier: number): AttackBuilder;
17
17
  onHit(count: number, sides: number, modifier: number): AttackBuilder;
18
18
  get critThreshold(): number;
19
+ cacheKey(): string | null;
19
20
  critOn(threshold: number): ACBuilder;
20
21
  alwaysCrits(): AlwaysCritBuilder;
21
22
  toExpression(): string;
@@ -1 +1 @@
1
- {"version":3,"file":"ac.d.ts","sourceRoot":"","sources":["../../src/builder/ac.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAExD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;CACvB;AACD,qBAAa,SAAU,SAAQ,WAAW;IACxC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IAEpC,YAAY,QAAQ,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,EAQzE;IAMD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACvC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACtD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IACnD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IACxE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IAMrE,IAAI,aAAa,IAAI,MAAM,CAE1B;IAGD,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAMnC;IAED,WAAW,IAAI,iBAAiB,CAS/B;IAGQ,YAAY,IAAI,MAAM,CAM9B;IAEQ,KAAK,CAAC,GAAG,GAAE,MAAU,GAAG,GAAG,CA0BnC;IAEQ,IAAI,IAAI,SAAS,CAOzB;CACF"}
1
+ {"version":3,"file":"ac.d.ts","sourceRoot":"","sources":["../../src/builder/ac.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAExD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;CACvB;AACD,qBAAa,SAAU,SAAQ,WAAW;IACxC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IAEpC,YAAY,QAAQ,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,EAQzE;IAMD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACvC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACtD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IACnD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IACxE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IAMrE,IAAI,aAAa,IAAI,MAAM,CAE1B;IAEQ,QAAQ,IAAI,MAAM,GAAG,IAAI,CAGjC;IAGD,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAMnC;IAED,WAAW,IAAI,iBAAiB,CAS/B;IAGQ,YAAY,IAAI,MAAM,CAM9B;IAEQ,KAAK,CAAC,GAAG,GAAE,MAAU,GAAG,GAAG,CA0BnC;IAEQ,IAAI,IAAI,SAAS,CAOzB;CACF"}
@@ -4,6 +4,8 @@ import type { ACBuilder } from "./ac.js";
4
4
  import { AlwaysCritBuilder, AlwaysHitBuilder, RollBuilder } from "./roll.js";
5
5
  import type { AttackResolution, CheckBuilder } from "./types.js";
6
6
  type ActionEffect = RollBuilder;
7
+ /** Clears the resolved-attack PMF cache (test/bench seam; mirrors {@link clearParserCache}). */
8
+ export declare function clearAttackCache(): void;
7
9
  export declare class AttackBuilder implements CheckBuilder {
8
10
  readonly check: ACBuilder | AlwaysHitBuilder | AlwaysCritBuilder;
9
11
  private readonly hitEffect?;
@@ -33,6 +35,13 @@ export declare class AttackBuilder implements CheckBuilder {
33
35
  pMiss: number;
34
36
  };
35
37
  resolve(eps?: number): AttackResolution;
38
+ /**
39
+ * A cheap, complete key for this attack's resolved PMF, or `null` when it can't be cached soundly (an
40
+ * effect whose PMF isn't captured by its {@link RollConfig}s — see {@link RollBuilder.cacheKey}). Composed
41
+ * from the check + hit/crit/miss effect keys + `eps`. `critEffect === null` (noCrit) and `undefined`
42
+ * (auto-double the hit dice) are distinct crit states, encoded separately.
43
+ */
44
+ private cacheKey;
36
45
  toPMF(eps?: number): PMF;
37
46
  get pmf(): PMF;
38
47
  toQuery(eps?: number): DiceQuery;
@@ -1 +1 @@
1
- {"version":3,"file":"attack.d.ts","sourceRoot":"","sources":["../../src/builder/attack.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAGtC,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAEhB,WAAW,EACZ,MAAM,QAAQ,CAAC;AAChB,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE9D,KAAK,YAAY,GAAG,WAAW,CAAC;AAEhC,qBAAa,aAAc,YAAW,YAAY;IAE9C,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,gBAAgB,GAAG,iBAAiB;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAC3B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IAJ9B,YACW,KAAK,EAAE,SAAS,GAAG,gBAAgB,GAAG,iBAAiB,EAC/C,SAAS,CAAC,EAAE,YAAY,YAAA,EACxB,UAAU,CAAC,EAAE,YAAY,GAAG,IAAI,YAAA,EAChC,UAAU,CAAC,EAAE,YAAY,YAAA,EACxC;IAEJ,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACxC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACvD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IACpD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IACzE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IAWtE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACxC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACvD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IACpD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IACzE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IAWtE,MAAM,IAAI,aAAa,CAEtB;IAGD,YAAY,IAAI,MAAM,CA+CrB;IAED,oBAAoB,CAClB,KAAK,EAAE,SAAS,GAAG,gBAAgB,GAAG,iBAAiB,EACvD,GAAG,GAAE,MAAU,GACd;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAmGlE;IAED,OAAO,CAAC,GAAG,GAAE,MAAY,GAAG,gBAAgB,CA+D3C;IAGD,KAAK,CAAC,GAAG,GAAE,MAAU,GAAG,GAAG,CAE1B;IAED,IAAI,GAAG,QAEN;IAGD,OAAO,CAAC,GAAG,GAAE,MAAU,GAAG,SAAS,CAElC;CACF"}
1
+ {"version":3,"file":"attack.d.ts","sourceRoot":"","sources":["../../src/builder/attack.ts"],"names":[],"mappings":"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"}
@@ -3503,6 +3503,17 @@ var RollBuilder = class _RollBuilder {
3503
3503
  getSubRollConfigs() {
3504
3504
  return this.subRollConfigs.map((c) => ({ ...c }));
3505
3505
  }
3506
+ /**
3507
+ * A cheap, stable string that FULLY identifies this builder's PMF — used by {@link AttackBuilder.toPMF}
3508
+ * to cache resolved attack PMFs across rebuilds without walking the AST via {@link toExpression}. A plain
3509
+ * roll is fully determined by its {@link RollConfig} array (count/sides/modifier/reroll/explode/minimum/
3510
+ * bestOf/keep/rollType/isSubtraction), so serializing that is sound. Subclasses whose PMF depends on
3511
+ * hidden state NOT captured by `subRollConfigs` (half/scale/max/parsed/pooled/composite transforms) return
3512
+ * `null` to opt OUT of caching — a conservative miss is always safe; a wrong key would corrupt DPR.
3513
+ */
3514
+ cacheKey() {
3515
+ return JSON.stringify(this.subRollConfigs);
3516
+ }
3506
3517
  // for testing
3507
3518
  static fromConfig(config) {
3508
3519
  return new _RollBuilder([{ ...defaultConfig, ...config }]);
@@ -4011,6 +4022,9 @@ var HalfRollBuilder = class _HalfRollBuilder extends RollBuilder {
4011
4022
  hasHiddenState() {
4012
4023
  return this.innerRoll.hasHiddenState();
4013
4024
  }
4025
+ cacheKey() {
4026
+ return null;
4027
+ }
4014
4028
  // No need to override create if we don't expose RollBuilder methods that use it,
4015
4029
  // but HalfRollBuilder extends RollBuilder so it does.
4016
4030
  // However, HalfRollBuilder seems to just wrap another roll.
@@ -4055,6 +4069,9 @@ var ScaleRollBuilder = class _ScaleRollBuilder extends RollBuilder {
4055
4069
  hasHiddenState() {
4056
4070
  return this.innerRoll.hasHiddenState();
4057
4071
  }
4072
+ cacheKey() {
4073
+ return null;
4074
+ }
4058
4075
  get lastConfig() {
4059
4076
  return this.innerRoll.lastConfig;
4060
4077
  }
@@ -4099,6 +4116,9 @@ var MaxOfRollBuilder = class _MaxOfRollBuilder extends RollBuilder {
4099
4116
  hasHiddenState() {
4100
4117
  return this.innerRoll.hasHiddenState();
4101
4118
  }
4119
+ cacheKey() {
4120
+ return null;
4121
+ }
4102
4122
  get lastConfig() {
4103
4123
  return this.innerRoll.lastConfig;
4104
4124
  }
@@ -4176,6 +4196,10 @@ var AlwaysHitBuilder = class _AlwaysHitBuilder extends RollBuilder {
4176
4196
  get critThreshold() {
4177
4197
  return this.attackConfig.critThreshold;
4178
4198
  }
4199
+ cacheKey() {
4200
+ const base = super.cacheKey();
4201
+ return base === null ? null : `H|${this.attackConfig.critThreshold}|${base}`;
4202
+ }
4179
4203
  // TODO - move this to AC Builder… or if we create a DC builder that has critOn, throw an error?
4180
4204
  critOn(critThreshold) {
4181
4205
  const newConfig = { critThreshold };
@@ -4226,6 +4250,10 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
4226
4250
  get critThreshold() {
4227
4251
  return this.attackConfig.critThreshold;
4228
4252
  }
4253
+ cacheKey() {
4254
+ const base = super.cacheKey();
4255
+ return base === null ? null : `C|${this.fromAlwaysHit ? 1 : 0}|${this.attackConfig.critThreshold}|${this.attackConfig.ac ?? ""}|${base}`;
4256
+ }
4229
4257
  critOn(critThreshold) {
4230
4258
  const newConfig = { critThreshold, ac: this.attackConfig.ac };
4231
4259
  return new _AlwaysCritBuilder(this, newConfig, this.fromAlwaysHit);
@@ -4256,6 +4284,9 @@ var ParsedRollBuilder = class _ParsedRollBuilder extends RollBuilder {
4256
4284
  hasHiddenState() {
4257
4285
  return true;
4258
4286
  }
4287
+ cacheKey() {
4288
+ return null;
4289
+ }
4259
4290
  create(configs) {
4260
4291
  return new RollBuilder(configs);
4261
4292
  }
@@ -4291,6 +4322,9 @@ var PooledRollBuilder = class _PooledRollBuilder extends RollBuilder {
4291
4322
  hasHiddenState() {
4292
4323
  return true;
4293
4324
  }
4325
+ cacheKey() {
4326
+ return null;
4327
+ }
4294
4328
  d(_sides) {
4295
4329
  throw new Error("Cannot add dice to a pooled roll. The pool is finalized.");
4296
4330
  }
@@ -4394,6 +4428,9 @@ var CompositeSumRollBuilder = class _CompositeSumRollBuilder extends RollBuilder
4394
4428
  hasHiddenState() {
4395
4429
  return true;
4396
4430
  }
4431
+ cacheKey() {
4432
+ return null;
4433
+ }
4397
4434
  getSubRollConfigs() {
4398
4435
  return [];
4399
4436
  }
@@ -4946,6 +4983,10 @@ function getASTSignature(node) {
4946
4983
  }
4947
4984
 
4948
4985
  // src/builder/attack.ts
4986
+ var attackPMFCache = new LRUCache(4e3);
4987
+ function clearAttackCache() {
4988
+ attackPMFCache.clear();
4989
+ }
4949
4990
  var AttackBuilder = class _AttackBuilder {
4950
4991
  constructor(check, hitEffect, critEffect, missEffect) {
4951
4992
  this.check = check;
@@ -5124,9 +5165,46 @@ var AttackBuilder = class _AttackBuilder {
5124
5165
  weights: { hit: phit, crit: pcrit, miss: pmiss }
5125
5166
  };
5126
5167
  }
5127
- // By default, create PMF with no pruning
5168
+ /**
5169
+ * A cheap, complete key for this attack's resolved PMF, or `null` when it can't be cached soundly (an
5170
+ * effect whose PMF isn't captured by its {@link RollConfig}s — see {@link RollBuilder.cacheKey}). Composed
5171
+ * from the check + hit/crit/miss effect keys + `eps`. `critEffect === null` (noCrit) and `undefined`
5172
+ * (auto-double the hit dice) are distinct crit states, encoded separately.
5173
+ */
5174
+ cacheKey(eps) {
5175
+ const checkKey = this.check.cacheKey();
5176
+ if (checkKey === null) return null;
5177
+ let hitKey = "";
5178
+ if (this.hitEffect) {
5179
+ const k = this.hitEffect.cacheKey();
5180
+ if (k === null) return null;
5181
+ hitKey = k;
5182
+ }
5183
+ let critKey;
5184
+ if (this.critEffect === null) critKey = "n";
5185
+ else if (this.critEffect === void 0) critKey = "a";
5186
+ else {
5187
+ const k = this.critEffect.cacheKey();
5188
+ if (k === null) return null;
5189
+ critKey = k;
5190
+ }
5191
+ let missKey = "";
5192
+ if (this.missEffect) {
5193
+ const k = this.missEffect.cacheKey();
5194
+ if (k === null) return null;
5195
+ missKey = k;
5196
+ }
5197
+ return `${checkKey}*H${hitKey}*C${critKey}*M${missKey}*e${eps}`;
5198
+ }
5199
+ // By default, create PMF with no pruning. Cached by the cheap config key across identical rebuilds.
5128
5200
  toPMF(eps = 0) {
5129
- return this.resolve(eps).pmf;
5201
+ const key = this.cacheKey(eps);
5202
+ if (key === null) return this.resolve(eps).pmf;
5203
+ const cached = attackPMFCache.get(key);
5204
+ if (cached) return cached;
5205
+ const pmf = this.resolve(eps).pmf;
5206
+ attackPMFCache.set(key, pmf);
5207
+ return pmf;
5130
5208
  }
5131
5209
  get pmf() {
5132
5210
  return this.toPMF();
@@ -5154,6 +5232,10 @@ var ACBuilder = class _ACBuilder extends RollBuilder {
5154
5232
  get critThreshold() {
5155
5233
  return this.attackConfig.critThreshold;
5156
5234
  }
5235
+ cacheKey() {
5236
+ const base = super.cacheKey();
5237
+ return base === null ? null : `A|${this.attackConfig.ac}|${this.attackConfig.critThreshold}|${base}`;
5238
+ }
5157
5239
  // TODO - move this to AC Builder… or if we create a DC builder that has critOn, throw an error?
5158
5240
  critOn(threshold) {
5159
5241
  const newConfig = {
@@ -5371,6 +5453,7 @@ exports.RollBuilder = RollBuilder;
5371
5453
  exports.SaveBuilder = SaveBuilder;
5372
5454
  exports.ScaleRollBuilder = ScaleRollBuilder;
5373
5455
  exports.builderPMFCache = builderPMFCache;
5456
+ exports.clearAttackCache = clearAttackCache;
5374
5457
  exports.d = d;
5375
5458
  exports.d10 = d10;
5376
5459
  exports.d100 = d100;