@yipe/dice 0.2.11 → 0.2.12

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.
@@ -2659,6 +2659,14 @@ var RollBuilder = class _RollBuilder {
2659
2659
  const scaleInt = Math.floor(scale);
2660
2660
  if (scaleInt !== scale) throw new Error("Scale must be an integer");
2661
2661
  if (scaleInt <= 0) throw new Error("Scale must be > 0");
2662
+ if (scaleInt > 1) {
2663
+ const configs = this.getSubRollConfigs();
2664
+ if (configs.length === 1 && configs[0].keep && configs[0].count === 1) {
2665
+ const config = configs[0];
2666
+ const newConfig = { ...config, count: scaleInt };
2667
+ return new _RollBuilder([newConfig]);
2668
+ }
2669
+ }
2662
2670
  const newConfigs = this.getSubRollConfigs().map((config) => {
2663
2671
  if (!config.sides || config.sides <= 0) return config;
2664
2672
  return { ...config, count: config.count * scaleInt };
@@ -2878,6 +2886,10 @@ var RollBuilder = class _RollBuilder {
2878
2886
  half() {
2879
2887
  return new HalfRollBuilder(this);
2880
2888
  }
2889
+ // Create a "max of N rolls" version of this roll for crit damage with keep operations
2890
+ maxOf(count) {
2891
+ return new MaxOfRollBuilder(this, count);
2892
+ }
2881
2893
  };
2882
2894
  var HalfRollBuilder = class _HalfRollBuilder extends RollBuilder {
2883
2895
  constructor(innerRoll) {
@@ -2907,6 +2919,78 @@ var HalfRollBuilder = class _HalfRollBuilder extends RollBuilder {
2907
2919
  return new _HalfRollBuilder(this.innerRoll.copy());
2908
2920
  }
2909
2921
  };
2922
+ var MaxOfRollBuilder = class _MaxOfRollBuilder extends RollBuilder {
2923
+ constructor(innerRoll, count, diceCount, diceSides) {
2924
+ super(0);
2925
+ this.innerRoll = innerRoll;
2926
+ this.count = count;
2927
+ this.diceCount = diceCount;
2928
+ this.diceSides = diceSides;
2929
+ }
2930
+ get lastConfig() {
2931
+ return this.innerRoll.lastConfig;
2932
+ }
2933
+ getSubRollConfigs() {
2934
+ return this.innerRoll.getSubRollConfigs();
2935
+ }
2936
+ toExpression() {
2937
+ if (this.diceCount && this.diceSides) {
2938
+ return `max${this.count}(${this.diceCount}d${this.diceSides})`;
2939
+ }
2940
+ return `max${this.count}(?d?)`;
2941
+ }
2942
+ findDieInAST(node) {
2943
+ if (node.type === "die") return node;
2944
+ if (node.child) return this.findDieInAST(node.child);
2945
+ if (node.children) {
2946
+ for (const child of node.children) {
2947
+ const result = this.findDieInAST(child.node || child);
2948
+ if (result) return result;
2949
+ }
2950
+ }
2951
+ return null;
2952
+ }
2953
+ toAST() {
2954
+ if (this.diceCount && this.diceSides) {
2955
+ const sumChild = {
2956
+ type: "sum",
2957
+ count: this.diceCount,
2958
+ child: { type: "die", sides: this.diceSides }
2959
+ };
2960
+ return {
2961
+ type: "maxOf",
2962
+ count: this.count,
2963
+ child: sumChild
2964
+ };
2965
+ }
2966
+ try {
2967
+ const configs = this.innerRoll.getSubRollConfigs();
2968
+ if (configs.length === 1 && configs[0].sides) {
2969
+ const config = configs[0];
2970
+ const sumChild = {
2971
+ type: "sum",
2972
+ count: config.count,
2973
+ child: { type: "die", sides: config.sides }
2974
+ };
2975
+ return {
2976
+ type: "maxOf",
2977
+ count: this.count,
2978
+ child: sumChild
2979
+ };
2980
+ }
2981
+ } catch (e) {
2982
+ }
2983
+ throw new Error(
2984
+ `MaxOfRollBuilder.toAST(): Unsupported innerRoll configuration`
2985
+ );
2986
+ }
2987
+ toPMF(eps = 0) {
2988
+ return pmfFromRollBuilder(this, eps);
2989
+ }
2990
+ copy() {
2991
+ return new _MaxOfRollBuilder(this.innerRoll.copy(), this.count);
2992
+ }
2993
+ };
2910
2994
 
2911
2995
  // src/builder/factory.ts
2912
2996
  var rollFn = (count, sidesOrDie, modifier) => {
@@ -2995,13 +3079,41 @@ function astFromRollConfigs(configs) {
2995
3079
  appliedRollType = true;
2996
3080
  }
2997
3081
  if (cfg.rollType === "flat" && cfg.keep && cfg.keep.total > 0) {
2998
- const base = { type: "sum", count: cfg.keep.total, child: node };
2999
- node = {
3000
- type: "keep",
3001
- mode: cfg.keep.mode,
3002
- count: cfg.keep.count,
3003
- child: base
3004
- };
3082
+ const baseCount = Math.max(1, Math.floor(Math.abs(count || 1)));
3083
+ const trials = Math.max(1, Math.floor(cfg.keep.total));
3084
+ const k = Math.max(0, Math.floor(cfg.keep.count));
3085
+ if (trials === baseCount) {
3086
+ const base = { type: "sum", count: trials, child: node };
3087
+ node = {
3088
+ type: "keep",
3089
+ mode: cfg.keep.mode,
3090
+ count: k,
3091
+ child: base
3092
+ };
3093
+ } else {
3094
+ const perTrial = {
3095
+ type: "sum",
3096
+ count: baseCount,
3097
+ child: node
3098
+ };
3099
+ if (trials === 1) {
3100
+ node = perTrial;
3101
+ } else if (k === 1 && cfg.keep.mode === "highest") {
3102
+ node = { type: "maxOf", count: trials, child: perTrial };
3103
+ } else {
3104
+ const trialPool = {
3105
+ type: "sum",
3106
+ count: trials,
3107
+ child: perTrial
3108
+ };
3109
+ node = {
3110
+ type: "keep",
3111
+ mode: cfg.keep.mode,
3112
+ count: k,
3113
+ child: trialPool
3114
+ };
3115
+ }
3116
+ }
3005
3117
  } else {
3006
3118
  const c = appliedRollType ? 1 : Math.max(1, count || 1);
3007
3119
  node = { type: "sum", count: c, child: node };
@@ -3055,14 +3167,14 @@ function resolve(node, eps = defaultEps) {
3055
3167
  return res;
3056
3168
  }
3057
3169
  case "keep": {
3058
- const total = getTotalCount(node);
3059
- const die = findDie(node.child.child);
3060
- const perDie = die ? resolveSingleDie(die, eps) : PMF.delta(0, eps);
3061
- const keepCount = Math.max(0, Math.min(node.count, total));
3062
- if (keepCount === 0 || total === 0) return PMF.delta(0, eps);
3170
+ const totalTrials = getTotalCount(node);
3171
+ const keepCount = Math.max(0, Math.min(node.count, totalTrials));
3172
+ if (keepCount === 0 || totalTrials === 0) return PMF.delta(0, eps);
3173
+ const perTrialNode = node.child.child;
3174
+ const perTrialPMF = resolve(perTrialNode, eps);
3063
3175
  return keepSumPMF(
3064
- perDie,
3065
- total,
3176
+ perTrialPMF,
3177
+ totalTrials,
3066
3178
  keepCount,
3067
3179
  node.mode === "highest",
3068
3180
  eps
@@ -3077,6 +3189,12 @@ function resolve(node, eps = defaultEps) {
3077
3189
  const childPMF = resolve(node.child, eps);
3078
3190
  return childPMF.scaleDamage(0.5, "floor");
3079
3191
  }
3192
+ case "maxOf": {
3193
+ const childPMF = resolve(node.child, eps);
3194
+ const count = Math.max(1, Math.floor(node.count));
3195
+ if (count === 1) return childPMF;
3196
+ return computeMaxOfPMF(childPMF, count, eps);
3197
+ }
3080
3198
  }
3081
3199
  })();
3082
3200
  builderPMFCache.set(cacheKey, result);
@@ -3147,6 +3265,7 @@ function findDie(node) {
3147
3265
  case "sum":
3148
3266
  case "d20Roll":
3149
3267
  case "half":
3268
+ case "maxOf":
3150
3269
  return findDie(node.child);
3151
3270
  case "keep":
3152
3271
  return findDie(node.child.child);
@@ -3163,6 +3282,38 @@ function getTotalCount(node) {
3163
3282
  while (cur.type === "keep") cur = cur.child;
3164
3283
  return cur.type === "sum" ? Math.max(0, Math.floor(cur.count)) : 0;
3165
3284
  }
3285
+ function computeMaxOfPMF(pmf, count, eps = defaultEps) {
3286
+ if (count <= 1) return pmf;
3287
+ const support = pmf.support();
3288
+ const out = /* @__PURE__ */ new Map();
3289
+ if (count <= 6 && support.length <= 20) {
3290
+ let dfs = function(rollsLeft, currentMax, probability) {
3291
+ if (rollsLeft === 0) {
3292
+ out.set(currentMax, (out.get(currentMax) || 0) + probability);
3293
+ return;
3294
+ }
3295
+ for (const value of support) {
3296
+ const p = pmf.pAt(value);
3297
+ if (p > 0) {
3298
+ const newMax = Math.max(currentMax, value);
3299
+ dfs(rollsLeft - 1, newMax, probability * p);
3300
+ }
3301
+ }
3302
+ };
3303
+ dfs(count, -Infinity, 1);
3304
+ } else {
3305
+ const sortedSupport = [...support].sort((a, b) => a - b);
3306
+ for (const value of sortedSupport) {
3307
+ const cdfAtValue = pmf.cdfAt(value);
3308
+ const cdfAtValueMinus1 = value > sortedSupport[0] ? pmf.cdfAt(value - 1) : 0;
3309
+ const probMax = Math.pow(cdfAtValue, count) - Math.pow(cdfAtValueMinus1, count);
3310
+ if (probMax > eps) {
3311
+ out.set(value, probMax);
3312
+ }
3313
+ }
3314
+ }
3315
+ return PMF.fromMap(out, eps);
3316
+ }
3166
3317
  function keepSumPMF(single, total, keep, highest, eps = defaultEps) {
3167
3318
  if (keep >= total) return single.power(total, eps);
3168
3319
  if (keep <= 0) return PMF.delta(0, eps);
@@ -3171,24 +3322,106 @@ function keepSumPMF(single, total, keep, highest, eps = defaultEps) {
3171
3322
  const cacheKey = `keep|${pmfSig}|t:${total}|k:${keep}|h:${highest ? 1 : 0}|e:${eps}`;
3172
3323
  const cached = builderPMFCache.get(cacheKey);
3173
3324
  if (cached) return cached;
3174
- const support = single.support();
3175
- const probs = support.map((v) => ({ v, p: single.pAt(v) }));
3176
- const out = /* @__PURE__ */ new Map();
3177
- function dfs(i, accValues, weight) {
3178
- if (i === total) {
3179
- const sorted = [...accValues].sort((a, b) => highest ? b - a : a - b);
3180
- let sum = 0;
3181
- for (let j = 0; j < keep; j++) sum += sorted[j];
3182
- out.set(sum, (out.get(sum) || 0) + weight);
3183
- return;
3325
+ if (keep === 1) {
3326
+ if (highest) {
3327
+ return computeMaxOfPMF(single, total, eps);
3328
+ } else {
3329
+ const neg = single.mapDamage((v) => -v);
3330
+ const minPMF = computeMaxOfPMF(neg, total, eps).mapDamage((v) => -v);
3331
+ builderPMFCache.set(cacheKey, minPMF);
3332
+ return minPMF;
3333
+ }
3334
+ }
3335
+ let state = /* @__PURE__ */ new Map();
3336
+ const keyOf = (used, r) => `${used}|${r}`;
3337
+ state.set(keyOf(0, total), /* @__PURE__ */ new Map([[0, 1]]));
3338
+ const valuesDesc = highest ? [...sortedSupport].sort((a, b) => b - a) : [...sortedSupport].sort((a, b) => a - b);
3339
+ const binomPMF = (r, p) => {
3340
+ if (r <= 0) return [1];
3341
+ if (p <= eps) {
3342
+ const arr2 = new Array(r + 1).fill(0);
3343
+ arr2[0] = 1;
3344
+ return arr2;
3345
+ }
3346
+ if (1 - p <= eps) {
3347
+ const arr2 = new Array(r + 1).fill(0);
3348
+ arr2[r] = 1;
3349
+ return arr2;
3184
3350
  }
3185
- for (const { v, p } of probs) {
3186
- if (p <= 0) continue;
3187
- dfs(i + 1, [...accValues, v], weight * p);
3351
+ const q = 1 - p;
3352
+ const arr = new Array(r + 1).fill(0);
3353
+ arr[0] = Math.pow(q, r);
3354
+ const ratio = p / q;
3355
+ for (let x = 1; x <= r; x++)
3356
+ arr[x] = arr[x - 1] * (r - x + 1) / x * ratio;
3357
+ let s = 0;
3358
+ for (let x = 0; x <= r; x++) s += arr[x];
3359
+ if (Math.abs(1 - s) > 1e-12) for (let x = 0; x <= r; x++) arr[x] /= s;
3360
+ return arr;
3361
+ };
3362
+ const pruneMap = (m, threshold) => {
3363
+ if (threshold <= 0) return m;
3364
+ const out = /* @__PURE__ */ new Map();
3365
+ for (const [sum, pr] of m) if (pr >= threshold) out.set(sum, pr);
3366
+ return out.size === m.size ? m : out;
3367
+ };
3368
+ const pruneState = (st, threshold) => {
3369
+ if (threshold <= 0) return st;
3370
+ const out = /* @__PURE__ */ new Map();
3371
+ for (const [k, m] of st) {
3372
+ const mm = pruneMap(m, threshold);
3373
+ if (mm.size > 0) out.set(k, mm);
3374
+ }
3375
+ return out;
3376
+ };
3377
+ let processedMass = 0;
3378
+ for (const v of valuesDesc) {
3379
+ const p = single.pAt(v);
3380
+ if (p <= 0) continue;
3381
+ const q = Math.max(eps, 1 - processedMass);
3382
+ const pCond = Math.min(1, p / q);
3383
+ const next = /* @__PURE__ */ new Map();
3384
+ for (const [k, m] of state) {
3385
+ const [usedStr, rStr] = k.split("|");
3386
+ const used = parseInt(usedStr, 10);
3387
+ const r = parseInt(rStr, 10);
3388
+ if (r === 0) {
3389
+ const destKey = keyOf(used, 0);
3390
+ const dest = next.get(destKey) ?? /* @__PURE__ */ new Map();
3391
+ for (const [sum, pr] of m) dest.set(sum, (dest.get(sum) || 0) + pr);
3392
+ next.set(destKey, dest);
3393
+ continue;
3394
+ }
3395
+ const bin = binomPMF(r, pCond);
3396
+ const remainingCapacity = keep - used;
3397
+ for (let x = 0; x <= r; x++) {
3398
+ const px = bin[x];
3399
+ if (px <= eps) continue;
3400
+ const t = Math.min(x, remainingCapacity);
3401
+ const used2 = used + t;
3402
+ const r2 = r - x;
3403
+ const add = t * v;
3404
+ const destKey = keyOf(used2, r2);
3405
+ const dest = next.get(destKey) ?? /* @__PURE__ */ new Map();
3406
+ for (const [sum, pr] of m) {
3407
+ const s2 = sum + add;
3408
+ const prob = pr * px;
3409
+ const cur = dest.get(s2) || 0;
3410
+ const nv = cur + prob;
3411
+ if (nv >= eps) dest.set(s2, nv);
3412
+ }
3413
+ if (dest.size > 0) next.set(destKey, dest);
3414
+ }
3188
3415
  }
3416
+ state = pruneState(next, eps * 1e-6);
3417
+ processedMass += p;
3418
+ }
3419
+ const finalKey = keyOf(keep, 0);
3420
+ const dist = state.get(finalKey) ?? /* @__PURE__ */ new Map();
3421
+ if (dist.size === 0) {
3422
+ return PMF.emptyMass();
3189
3423
  }
3190
- dfs(0, [], 1);
3191
- const result = PMF.fromMap(out, eps);
3424
+ const result = PMF.fromMap(dist, eps);
3192
3425
  builderPMFCache.set(cacheKey, result);
3193
3426
  return result;
3194
3427
  }
@@ -3214,6 +3447,8 @@ function getASTSignature(node) {
3214
3447
  )}}`;
3215
3448
  case "half":
3216
3449
  return `half{ch:${getASTSignature(node.child)}}`;
3450
+ case "maxOf":
3451
+ return `maxOf{c:${node.count},ch:${getASTSignature(node.child)}}`;
3217
3452
  case "add": {
3218
3453
  let constantValue = 0;
3219
3454
  const otherChildrenSigs = [];
@@ -3273,7 +3508,12 @@ var AttackBuilder = class _AttackBuilder {
3273
3508
  if (this.hitEffect) {
3274
3509
  effectPart = `(${this.hitEffect.toExpression()})`;
3275
3510
  if (this.critEffect !== null) {
3276
- const crit = this.critEffect ?? this.hitEffect.copy().doubleDice();
3511
+ let crit;
3512
+ if (this.critEffect) {
3513
+ crit = this.critEffect;
3514
+ } else {
3515
+ crit = this.hitEffect?.copy().doubleDice() ?? RollBuilder.fromArgs(0);
3516
+ }
3277
3517
  const critThreshold = this.check.critThreshold;
3278
3518
  if (critThreshold < 15 || critThreshold > 20) {
3279
3519
  throw new Error(
@@ -3586,6 +3826,7 @@ RollBuilder.prototype.dc = function(saveDC) {
3586
3826
  exports.ACBuilder = ACBuilder;
3587
3827
  exports.DCBuilder = DCBuilder;
3588
3828
  exports.HalfRollBuilder = HalfRollBuilder;
3829
+ exports.MaxOfRollBuilder = MaxOfRollBuilder;
3589
3830
  exports.RollBuilder = RollBuilder;
3590
3831
  exports.SaveBuilder = SaveBuilder;
3591
3832
  exports.builderPMFCache = builderPMFCache;