@yipe/dice 0.7.0 → 0.8.1

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.
@@ -3452,6 +3452,10 @@ function d20PMF(rerollOne) {
3452
3452
  }
3453
3453
 
3454
3454
  // src/builder/roll.ts
3455
+ var rollPMFCache = new LRUCache(4e3);
3456
+ function clearRollCache() {
3457
+ rollPMFCache.clear();
3458
+ }
3455
3459
  var defaultConfig = {
3456
3460
  count: 1,
3457
3461
  sides: 0,
@@ -3501,6 +3505,17 @@ var RollBuilder = class _RollBuilder {
3501
3505
  getSubRollConfigs() {
3502
3506
  return this.subRollConfigs.map((c) => ({ ...c }));
3503
3507
  }
3508
+ /**
3509
+ * A cheap, stable string that FULLY identifies this builder's PMF — used by {@link AttackBuilder.toPMF}
3510
+ * to cache resolved attack PMFs across rebuilds without walking the AST via {@link toExpression}. A plain
3511
+ * roll is fully determined by its {@link RollConfig} array (count/sides/modifier/reroll/explode/minimum/
3512
+ * bestOf/keep/rollType/isSubtraction), so serializing that is sound. Subclasses whose PMF depends on
3513
+ * hidden state NOT captured by `subRollConfigs` (half/scale/max/parsed/pooled/composite transforms) return
3514
+ * `null` to opt OUT of caching — a conservative miss is always safe; a wrong key would corrupt DPR.
3515
+ */
3516
+ cacheKey() {
3517
+ return JSON.stringify(this.subRollConfigs);
3518
+ }
3504
3519
  // for testing
3505
3520
  static fromConfig(config) {
3506
3521
  return new _RollBuilder([{ ...defaultConfig, ...config }]);
@@ -3853,8 +3868,16 @@ var RollBuilder = class _RollBuilder {
3853
3868
  }
3854
3869
  return result.replace(/\+ -/g, "-");
3855
3870
  }
3871
+ // Main AST entry point. Cached by the cheap config key across identical rebuilds; see `rollPMFCache`.
3856
3872
  toPMF(eps = 0) {
3857
- return pmfFromRollBuilder(this, eps);
3873
+ const key = this.cacheKey();
3874
+ if (key === null) return pmfFromRollBuilder(this, eps);
3875
+ const fullKey = `${key}*e${eps}`;
3876
+ const cached = rollPMFCache.get(fullKey);
3877
+ if (cached) return cached;
3878
+ const pmf = pmfFromRollBuilder(this, eps);
3879
+ rollPMFCache.set(fullKey, pmf);
3880
+ return pmf;
3858
3881
  }
3859
3882
  get pmf() {
3860
3883
  return this.toPMF();
@@ -4009,6 +4032,9 @@ var HalfRollBuilder = class _HalfRollBuilder extends RollBuilder {
4009
4032
  hasHiddenState() {
4010
4033
  return this.innerRoll.hasHiddenState();
4011
4034
  }
4035
+ cacheKey() {
4036
+ return null;
4037
+ }
4012
4038
  // No need to override create if we don't expose RollBuilder methods that use it,
4013
4039
  // but HalfRollBuilder extends RollBuilder so it does.
4014
4040
  // However, HalfRollBuilder seems to just wrap another roll.
@@ -4053,6 +4079,9 @@ var ScaleRollBuilder = class _ScaleRollBuilder extends RollBuilder {
4053
4079
  hasHiddenState() {
4054
4080
  return this.innerRoll.hasHiddenState();
4055
4081
  }
4082
+ cacheKey() {
4083
+ return null;
4084
+ }
4056
4085
  get lastConfig() {
4057
4086
  return this.innerRoll.lastConfig;
4058
4087
  }
@@ -4097,6 +4126,9 @@ var MaxOfRollBuilder = class _MaxOfRollBuilder extends RollBuilder {
4097
4126
  hasHiddenState() {
4098
4127
  return this.innerRoll.hasHiddenState();
4099
4128
  }
4129
+ cacheKey() {
4130
+ return null;
4131
+ }
4100
4132
  get lastConfig() {
4101
4133
  return this.innerRoll.lastConfig;
4102
4134
  }
@@ -4174,6 +4206,10 @@ var AlwaysHitBuilder = class _AlwaysHitBuilder extends RollBuilder {
4174
4206
  get critThreshold() {
4175
4207
  return this.attackConfig.critThreshold;
4176
4208
  }
4209
+ cacheKey() {
4210
+ const base = super.cacheKey();
4211
+ return base === null ? null : `H|${this.attackConfig.critThreshold}|${base}`;
4212
+ }
4177
4213
  // TODO - move this to AC Builder… or if we create a DC builder that has critOn, throw an error?
4178
4214
  critOn(critThreshold) {
4179
4215
  const newConfig = { critThreshold };
@@ -4224,6 +4260,10 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
4224
4260
  get critThreshold() {
4225
4261
  return this.attackConfig.critThreshold;
4226
4262
  }
4263
+ cacheKey() {
4264
+ const base = super.cacheKey();
4265
+ return base === null ? null : `C|${this.fromAlwaysHit ? 1 : 0}|${this.attackConfig.critThreshold}|${this.attackConfig.ac ?? ""}|${base}`;
4266
+ }
4227
4267
  critOn(critThreshold) {
4228
4268
  const newConfig = { critThreshold, ac: this.attackConfig.ac };
4229
4269
  return new _AlwaysCritBuilder(this, newConfig, this.fromAlwaysHit);
@@ -4254,6 +4294,9 @@ var ParsedRollBuilder = class _ParsedRollBuilder extends RollBuilder {
4254
4294
  hasHiddenState() {
4255
4295
  return true;
4256
4296
  }
4297
+ cacheKey() {
4298
+ return null;
4299
+ }
4257
4300
  create(configs) {
4258
4301
  return new RollBuilder(configs);
4259
4302
  }
@@ -4289,6 +4332,9 @@ var PooledRollBuilder = class _PooledRollBuilder extends RollBuilder {
4289
4332
  hasHiddenState() {
4290
4333
  return true;
4291
4334
  }
4335
+ cacheKey() {
4336
+ return null;
4337
+ }
4292
4338
  d(_sides) {
4293
4339
  throw new Error("Cannot add dice to a pooled roll. The pool is finalized.");
4294
4340
  }
@@ -4392,6 +4438,9 @@ var CompositeSumRollBuilder = class _CompositeSumRollBuilder extends RollBuilder
4392
4438
  hasHiddenState() {
4393
4439
  return true;
4394
4440
  }
4441
+ cacheKey() {
4442
+ return null;
4443
+ }
4395
4444
  getSubRollConfigs() {
4396
4445
  return [];
4397
4446
  }
@@ -4944,6 +4993,10 @@ function getASTSignature(node) {
4944
4993
  }
4945
4994
 
4946
4995
  // src/builder/attack.ts
4996
+ var attackPMFCache = new LRUCache(4e3);
4997
+ function clearAttackCache() {
4998
+ attackPMFCache.clear();
4999
+ }
4947
5000
  var AttackBuilder = class _AttackBuilder {
4948
5001
  constructor(check, hitEffect, critEffect, missEffect) {
4949
5002
  this.check = check;
@@ -5122,9 +5175,46 @@ var AttackBuilder = class _AttackBuilder {
5122
5175
  weights: { hit: phit, crit: pcrit, miss: pmiss }
5123
5176
  };
5124
5177
  }
5125
- // By default, create PMF with no pruning
5178
+ /**
5179
+ * A cheap, complete key for this attack's resolved PMF, or `null` when it can't be cached soundly (an
5180
+ * effect whose PMF isn't captured by its {@link RollConfig}s — see {@link RollBuilder.cacheKey}). Composed
5181
+ * from the check + hit/crit/miss effect keys + `eps`. `critEffect === null` (noCrit) and `undefined`
5182
+ * (auto-double the hit dice) are distinct crit states, encoded separately.
5183
+ */
5184
+ cacheKey(eps) {
5185
+ const checkKey = this.check.cacheKey();
5186
+ if (checkKey === null) return null;
5187
+ let hitKey = "";
5188
+ if (this.hitEffect) {
5189
+ const k = this.hitEffect.cacheKey();
5190
+ if (k === null) return null;
5191
+ hitKey = k;
5192
+ }
5193
+ let critKey;
5194
+ if (this.critEffect === null) critKey = "n";
5195
+ else if (this.critEffect === void 0) critKey = "a";
5196
+ else {
5197
+ const k = this.critEffect.cacheKey();
5198
+ if (k === null) return null;
5199
+ critKey = k;
5200
+ }
5201
+ let missKey = "";
5202
+ if (this.missEffect) {
5203
+ const k = this.missEffect.cacheKey();
5204
+ if (k === null) return null;
5205
+ missKey = k;
5206
+ }
5207
+ return `${checkKey}*H${hitKey}*C${critKey}*M${missKey}*e${eps}`;
5208
+ }
5209
+ // By default, create PMF with no pruning. Cached by the cheap config key across identical rebuilds.
5126
5210
  toPMF(eps = 0) {
5127
- return this.resolve(eps).pmf;
5211
+ const key = this.cacheKey(eps);
5212
+ if (key === null) return this.resolve(eps).pmf;
5213
+ const cached = attackPMFCache.get(key);
5214
+ if (cached) return cached;
5215
+ const pmf = this.resolve(eps).pmf;
5216
+ attackPMFCache.set(key, pmf);
5217
+ return pmf;
5128
5218
  }
5129
5219
  get pmf() {
5130
5220
  return this.toPMF();
@@ -5152,6 +5242,10 @@ var ACBuilder = class _ACBuilder extends RollBuilder {
5152
5242
  get critThreshold() {
5153
5243
  return this.attackConfig.critThreshold;
5154
5244
  }
5245
+ cacheKey() {
5246
+ const base = super.cacheKey();
5247
+ return base === null ? null : `A|${this.attackConfig.ac}|${this.attackConfig.critThreshold}|${base}`;
5248
+ }
5155
5249
  // TODO - move this to AC Builder… or if we create a DC builder that has critOn, throw an error?
5156
5250
  critOn(threshold) {
5157
5251
  const newConfig = {
@@ -5212,6 +5306,10 @@ RollBuilder.prototype.ac = function(targetAC) {
5212
5306
  };
5213
5307
 
5214
5308
  // src/builder/save.ts
5309
+ var savePMFCache = new LRUCache(4e3);
5310
+ function clearSaveCache() {
5311
+ savePMFCache.clear();
5312
+ }
5215
5313
  var SaveBuilder = class _SaveBuilder {
5216
5314
  constructor(check, failureEffect, saveOutcome = "normal") {
5217
5315
  this.check = check;
@@ -5248,9 +5346,32 @@ var SaveBuilder = class _SaveBuilder {
5248
5346
  weights: { success: psuccess, fail: pfail }
5249
5347
  };
5250
5348
  }
5251
- // By default, create PMF with no pruning
5349
+ /**
5350
+ * A cheap, complete key for this save's resolved PMF, or `null` when it can't be cached soundly.
5351
+ * {@link resolve} reads exactly three things: the DC check (via `resolveProbabilities`), the failure
5352
+ * effect's PMF, and the save outcome — so composing their keys pins it. A `ParsedRollBuilder` failure
5353
+ * effect returns `null`, which correctly forces this uncached.
5354
+ */
5355
+ cacheKey(eps) {
5356
+ const checkKey = this.check.cacheKey();
5357
+ if (checkKey === null) return null;
5358
+ let failKey = "";
5359
+ if (this.failureEffect) {
5360
+ const k = this.failureEffect.cacheKey();
5361
+ if (k === null) return null;
5362
+ failKey = k;
5363
+ }
5364
+ return `${checkKey}*F${failKey}*O${this.saveOutcome}*e${eps}`;
5365
+ }
5366
+ // By default, create PMF with no pruning. Cached by the cheap config key across identical rebuilds.
5252
5367
  toPMF(eps = 0) {
5253
- return this.resolve(eps).pmf;
5368
+ const key = this.cacheKey(eps);
5369
+ if (key === null) return this.resolve(eps).pmf;
5370
+ const cached = savePMFCache.get(key);
5371
+ if (cached) return cached;
5372
+ const pmf = this.resolve(eps).pmf;
5373
+ savePMFCache.set(key, pmf);
5374
+ return pmf;
5254
5375
  }
5255
5376
  get pmf() {
5256
5377
  return this.toPMF();
@@ -5286,6 +5407,10 @@ function resolveProbabilities(check) {
5286
5407
  }
5287
5408
 
5288
5409
  // src/builder/dc.ts
5410
+ var dcPMFCache = new LRUCache(4e3);
5411
+ function clearDCCache() {
5412
+ dcPMFCache.clear();
5413
+ }
5289
5414
  var DCBuilder = class _DCBuilder extends RollBuilder {
5290
5415
  constructor(baseRoll, saveConfig) {
5291
5416
  super(baseRoll.getSubRollConfigs());
@@ -5326,7 +5451,22 @@ var DCBuilder = class _DCBuilder extends RollBuilder {
5326
5451
  const expression = new RollBuilder(allConfigs).toExpression();
5327
5452
  return `(${expression} DC ${this.saveConfig.dc})`;
5328
5453
  }
5454
+ /**
5455
+ * The DC check's PMF is fully determined by the save DC plus everything `toPMF` below reads off the roll
5456
+ * configs (`rollType`, `baseReroll`, `modifier`, bonus dice) — all of which `super.cacheKey()` already
5457
+ * serializes. So extend the base key with the DC, mirroring {@link AlwaysHitBuilder.cacheKey}.
5458
+ */
5459
+ cacheKey() {
5460
+ const base = super.cacheKey();
5461
+ return base === null ? null : `DC|${this.saveConfig.dc}|${base}`;
5462
+ }
5329
5463
  toPMF(eps = 0) {
5464
+ const key = this.cacheKey();
5465
+ const fullKey = key === null ? null : `${key}*e${eps}`;
5466
+ if (fullKey !== null) {
5467
+ const cached = dcPMFCache.get(fullKey);
5468
+ if (cached) return cached;
5469
+ }
5330
5470
  const saveDC = this.saveDC;
5331
5471
  const rollType = this.rollType;
5332
5472
  const rerollOne = this.baseReroll > 0;
@@ -5348,7 +5488,9 @@ var DCBuilder = class _DCBuilder extends RollBuilder {
5348
5488
  [0, psuccess > 0 ? psuccess : 0],
5349
5489
  [1, pfail > 0 ? pfail : 0]
5350
5490
  ]);
5351
- return PMF.fromMap(m, eps);
5491
+ const pmf = PMF.fromMap(m, eps);
5492
+ if (fullKey !== null) dcPMFCache.set(fullKey, pmf);
5493
+ return pmf;
5352
5494
  }
5353
5495
  };
5354
5496
  RollBuilder.prototype.dc = function(saveDC) {
@@ -5356,6 +5498,6 @@ RollBuilder.prototype.dc = function(saveDC) {
5356
5498
  return new DCBuilder(this).dc(saveDC);
5357
5499
  };
5358
5500
 
5359
- export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, DCBuilder, HalfRollBuilder, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, RollBuilder, SaveBuilder, ScaleRollBuilder, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll, sumRolls };
5501
+ export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, DCBuilder, HalfRollBuilder, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, RollBuilder, SaveBuilder, ScaleRollBuilder, builderPMFCache, clearAttackCache, clearDCCache, clearRollCache, clearSaveCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll, sumRolls };
5360
5502
  //# sourceMappingURL=index.js.map
5361
5503
  //# sourceMappingURL=index.js.map