@yipe/dice 0.2.11 → 0.2.13
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/builder/index.cjs +280 -30
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.d.cts +22 -2
- package/dist/builder/index.d.ts +22 -2
- package/dist/builder/index.js +280 -31
- package/dist/builder/index.js.map +1 -1
- package/package.json +1 -1
package/dist/builder/index.cjs
CHANGED
|
@@ -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,50 @@ function astFromRollConfigs(configs) {
|
|
|
2995
3079
|
appliedRollType = true;
|
|
2996
3080
|
}
|
|
2997
3081
|
if (cfg.rollType === "flat" && cfg.keep && cfg.keep.total > 0) {
|
|
2998
|
-
const
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
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 (k === 1 && cfg.keep.mode === "highest") {
|
|
3086
|
+
const perTrial = {
|
|
3087
|
+
type: "sum",
|
|
3088
|
+
count: baseCount,
|
|
3089
|
+
child: node
|
|
3090
|
+
};
|
|
3091
|
+
if (trials === 1) {
|
|
3092
|
+
node = perTrial;
|
|
3093
|
+
} else {
|
|
3094
|
+
node = { type: "maxOf", count: trials, child: perTrial };
|
|
3095
|
+
}
|
|
3096
|
+
} else if (trials === baseCount) {
|
|
3097
|
+
const base = { type: "sum", count: trials, child: node };
|
|
3098
|
+
node = {
|
|
3099
|
+
type: "keep",
|
|
3100
|
+
mode: cfg.keep.mode,
|
|
3101
|
+
count: k,
|
|
3102
|
+
child: base
|
|
3103
|
+
};
|
|
3104
|
+
} else {
|
|
3105
|
+
const perTrial = {
|
|
3106
|
+
type: "sum",
|
|
3107
|
+
count: baseCount,
|
|
3108
|
+
child: node
|
|
3109
|
+
};
|
|
3110
|
+
if (trials === 1) {
|
|
3111
|
+
node = perTrial;
|
|
3112
|
+
} else {
|
|
3113
|
+
const trialPool = {
|
|
3114
|
+
type: "sum",
|
|
3115
|
+
count: trials,
|
|
3116
|
+
child: perTrial
|
|
3117
|
+
};
|
|
3118
|
+
node = {
|
|
3119
|
+
type: "keep",
|
|
3120
|
+
mode: cfg.keep.mode,
|
|
3121
|
+
count: k,
|
|
3122
|
+
child: trialPool
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3005
3126
|
} else {
|
|
3006
3127
|
const c = appliedRollType ? 1 : Math.max(1, count || 1);
|
|
3007
3128
|
node = { type: "sum", count: c, child: node };
|
|
@@ -3055,14 +3176,14 @@ function resolve(node, eps = defaultEps) {
|
|
|
3055
3176
|
return res;
|
|
3056
3177
|
}
|
|
3057
3178
|
case "keep": {
|
|
3058
|
-
const
|
|
3059
|
-
const
|
|
3060
|
-
|
|
3061
|
-
const
|
|
3062
|
-
|
|
3179
|
+
const totalTrials = getTotalCount(node);
|
|
3180
|
+
const keepCount = Math.max(0, Math.min(node.count, totalTrials));
|
|
3181
|
+
if (keepCount === 0 || totalTrials === 0) return PMF.delta(0, eps);
|
|
3182
|
+
const perTrialNode = node.child.child;
|
|
3183
|
+
const perTrialPMF = resolve(perTrialNode, eps);
|
|
3063
3184
|
return keepSumPMF(
|
|
3064
|
-
|
|
3065
|
-
|
|
3185
|
+
perTrialPMF,
|
|
3186
|
+
totalTrials,
|
|
3066
3187
|
keepCount,
|
|
3067
3188
|
node.mode === "highest",
|
|
3068
3189
|
eps
|
|
@@ -3077,6 +3198,12 @@ function resolve(node, eps = defaultEps) {
|
|
|
3077
3198
|
const childPMF = resolve(node.child, eps);
|
|
3078
3199
|
return childPMF.scaleDamage(0.5, "floor");
|
|
3079
3200
|
}
|
|
3201
|
+
case "maxOf": {
|
|
3202
|
+
const childPMF = resolve(node.child, eps);
|
|
3203
|
+
const count = Math.max(1, Math.floor(node.count));
|
|
3204
|
+
if (count === 1) return childPMF;
|
|
3205
|
+
return computeMaxOfPMF(childPMF, count, eps);
|
|
3206
|
+
}
|
|
3080
3207
|
}
|
|
3081
3208
|
})();
|
|
3082
3209
|
builderPMFCache.set(cacheKey, result);
|
|
@@ -3147,6 +3274,7 @@ function findDie(node) {
|
|
|
3147
3274
|
case "sum":
|
|
3148
3275
|
case "d20Roll":
|
|
3149
3276
|
case "half":
|
|
3277
|
+
case "maxOf":
|
|
3150
3278
|
return findDie(node.child);
|
|
3151
3279
|
case "keep":
|
|
3152
3280
|
return findDie(node.child.child);
|
|
@@ -3163,6 +3291,38 @@ function getTotalCount(node) {
|
|
|
3163
3291
|
while (cur.type === "keep") cur = cur.child;
|
|
3164
3292
|
return cur.type === "sum" ? Math.max(0, Math.floor(cur.count)) : 0;
|
|
3165
3293
|
}
|
|
3294
|
+
function computeMaxOfPMF(pmf, count, eps = defaultEps) {
|
|
3295
|
+
if (count <= 1) return pmf;
|
|
3296
|
+
const support = pmf.support();
|
|
3297
|
+
const out = /* @__PURE__ */ new Map();
|
|
3298
|
+
if (count <= 6 && support.length <= 20) {
|
|
3299
|
+
let dfs = function(rollsLeft, currentMax, probability) {
|
|
3300
|
+
if (rollsLeft === 0) {
|
|
3301
|
+
out.set(currentMax, (out.get(currentMax) || 0) + probability);
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
for (const value of support) {
|
|
3305
|
+
const p = pmf.pAt(value);
|
|
3306
|
+
if (p > 0) {
|
|
3307
|
+
const newMax = Math.max(currentMax, value);
|
|
3308
|
+
dfs(rollsLeft - 1, newMax, probability * p);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
};
|
|
3312
|
+
dfs(count, -Infinity, 1);
|
|
3313
|
+
} else {
|
|
3314
|
+
const sortedSupport = [...support].sort((a, b) => a - b);
|
|
3315
|
+
for (const value of sortedSupport) {
|
|
3316
|
+
const cdfAtValue = pmf.cdfAt(value);
|
|
3317
|
+
const cdfAtValueMinus1 = value > sortedSupport[0] ? pmf.cdfAt(value - 1) : 0;
|
|
3318
|
+
const probMax = Math.pow(cdfAtValue, count) - Math.pow(cdfAtValueMinus1, count);
|
|
3319
|
+
if (probMax > eps) {
|
|
3320
|
+
out.set(value, probMax);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
return PMF.fromMap(out, eps);
|
|
3325
|
+
}
|
|
3166
3326
|
function keepSumPMF(single, total, keep, highest, eps = defaultEps) {
|
|
3167
3327
|
if (keep >= total) return single.power(total, eps);
|
|
3168
3328
|
if (keep <= 0) return PMF.delta(0, eps);
|
|
@@ -3171,24 +3331,106 @@ function keepSumPMF(single, total, keep, highest, eps = defaultEps) {
|
|
|
3171
3331
|
const cacheKey = `keep|${pmfSig}|t:${total}|k:${keep}|h:${highest ? 1 : 0}|e:${eps}`;
|
|
3172
3332
|
const cached = builderPMFCache.get(cacheKey);
|
|
3173
3333
|
if (cached) return cached;
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
const
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3334
|
+
if (keep === 1) {
|
|
3335
|
+
if (highest) {
|
|
3336
|
+
return computeMaxOfPMF(single, total, eps);
|
|
3337
|
+
} else {
|
|
3338
|
+
const neg = single.mapDamage((v) => -v);
|
|
3339
|
+
const minPMF = computeMaxOfPMF(neg, total, eps).mapDamage((v) => -v);
|
|
3340
|
+
builderPMFCache.set(cacheKey, minPMF);
|
|
3341
|
+
return minPMF;
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
let state = /* @__PURE__ */ new Map();
|
|
3345
|
+
const keyOf = (used, r) => `${used}|${r}`;
|
|
3346
|
+
state.set(keyOf(0, total), /* @__PURE__ */ new Map([[0, 1]]));
|
|
3347
|
+
const valuesDesc = highest ? [...sortedSupport].sort((a, b) => b - a) : [...sortedSupport].sort((a, b) => a - b);
|
|
3348
|
+
const binomPMF = (r, p) => {
|
|
3349
|
+
if (r <= 0) return [1];
|
|
3350
|
+
if (p <= eps) {
|
|
3351
|
+
const arr2 = new Array(r + 1).fill(0);
|
|
3352
|
+
arr2[0] = 1;
|
|
3353
|
+
return arr2;
|
|
3354
|
+
}
|
|
3355
|
+
if (1 - p <= eps) {
|
|
3356
|
+
const arr2 = new Array(r + 1).fill(0);
|
|
3357
|
+
arr2[r] = 1;
|
|
3358
|
+
return arr2;
|
|
3184
3359
|
}
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3360
|
+
const q = 1 - p;
|
|
3361
|
+
const arr = new Array(r + 1).fill(0);
|
|
3362
|
+
arr[0] = Math.pow(q, r);
|
|
3363
|
+
const ratio = p / q;
|
|
3364
|
+
for (let x = 1; x <= r; x++)
|
|
3365
|
+
arr[x] = arr[x - 1] * (r - x + 1) / x * ratio;
|
|
3366
|
+
let s = 0;
|
|
3367
|
+
for (let x = 0; x <= r; x++) s += arr[x];
|
|
3368
|
+
if (Math.abs(1 - s) > 1e-12) for (let x = 0; x <= r; x++) arr[x] /= s;
|
|
3369
|
+
return arr;
|
|
3370
|
+
};
|
|
3371
|
+
const pruneMap = (m, threshold) => {
|
|
3372
|
+
if (threshold <= 0) return m;
|
|
3373
|
+
const out = /* @__PURE__ */ new Map();
|
|
3374
|
+
for (const [sum, pr] of m) if (pr >= threshold) out.set(sum, pr);
|
|
3375
|
+
return out.size === m.size ? m : out;
|
|
3376
|
+
};
|
|
3377
|
+
const pruneState = (st, threshold) => {
|
|
3378
|
+
if (threshold <= 0) return st;
|
|
3379
|
+
const out = /* @__PURE__ */ new Map();
|
|
3380
|
+
for (const [k, m] of st) {
|
|
3381
|
+
const mm = pruneMap(m, threshold);
|
|
3382
|
+
if (mm.size > 0) out.set(k, mm);
|
|
3188
3383
|
}
|
|
3384
|
+
return out;
|
|
3385
|
+
};
|
|
3386
|
+
let processedMass = 0;
|
|
3387
|
+
for (const v of valuesDesc) {
|
|
3388
|
+
const p = single.pAt(v);
|
|
3389
|
+
if (p <= 0) continue;
|
|
3390
|
+
const q = Math.max(eps, 1 - processedMass);
|
|
3391
|
+
const pCond = Math.min(1, p / q);
|
|
3392
|
+
const next = /* @__PURE__ */ new Map();
|
|
3393
|
+
for (const [k, m] of state) {
|
|
3394
|
+
const [usedStr, rStr] = k.split("|");
|
|
3395
|
+
const used = parseInt(usedStr, 10);
|
|
3396
|
+
const r = parseInt(rStr, 10);
|
|
3397
|
+
if (r === 0) {
|
|
3398
|
+
const destKey = keyOf(used, 0);
|
|
3399
|
+
const dest = next.get(destKey) ?? /* @__PURE__ */ new Map();
|
|
3400
|
+
for (const [sum, pr] of m) dest.set(sum, (dest.get(sum) || 0) + pr);
|
|
3401
|
+
next.set(destKey, dest);
|
|
3402
|
+
continue;
|
|
3403
|
+
}
|
|
3404
|
+
const bin = binomPMF(r, pCond);
|
|
3405
|
+
const remainingCapacity = keep - used;
|
|
3406
|
+
for (let x = 0; x <= r; x++) {
|
|
3407
|
+
const px = bin[x];
|
|
3408
|
+
if (px <= eps) continue;
|
|
3409
|
+
const t = Math.min(x, remainingCapacity);
|
|
3410
|
+
const used2 = used + t;
|
|
3411
|
+
const r2 = r - x;
|
|
3412
|
+
const add = t * v;
|
|
3413
|
+
const destKey = keyOf(used2, r2);
|
|
3414
|
+
const dest = next.get(destKey) ?? /* @__PURE__ */ new Map();
|
|
3415
|
+
for (const [sum, pr] of m) {
|
|
3416
|
+
const s2 = sum + add;
|
|
3417
|
+
const prob = pr * px;
|
|
3418
|
+
const cur = dest.get(s2) || 0;
|
|
3419
|
+
const nv = cur + prob;
|
|
3420
|
+
if (nv >= eps) dest.set(s2, nv);
|
|
3421
|
+
}
|
|
3422
|
+
if (dest.size > 0) next.set(destKey, dest);
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
state = pruneState(next, eps * 1e-6);
|
|
3426
|
+
processedMass += p;
|
|
3427
|
+
}
|
|
3428
|
+
const finalKey = keyOf(keep, 0);
|
|
3429
|
+
const dist = state.get(finalKey) ?? /* @__PURE__ */ new Map();
|
|
3430
|
+
if (dist.size === 0) {
|
|
3431
|
+
return PMF.emptyMass();
|
|
3189
3432
|
}
|
|
3190
|
-
|
|
3191
|
-
const result = PMF.fromMap(out, eps);
|
|
3433
|
+
const result = PMF.fromMap(dist, eps);
|
|
3192
3434
|
builderPMFCache.set(cacheKey, result);
|
|
3193
3435
|
return result;
|
|
3194
3436
|
}
|
|
@@ -3214,6 +3456,8 @@ function getASTSignature(node) {
|
|
|
3214
3456
|
)}}`;
|
|
3215
3457
|
case "half":
|
|
3216
3458
|
return `half{ch:${getASTSignature(node.child)}}`;
|
|
3459
|
+
case "maxOf":
|
|
3460
|
+
return `maxOf{c:${node.count},ch:${getASTSignature(node.child)}}`;
|
|
3217
3461
|
case "add": {
|
|
3218
3462
|
let constantValue = 0;
|
|
3219
3463
|
const otherChildrenSigs = [];
|
|
@@ -3273,7 +3517,12 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
3273
3517
|
if (this.hitEffect) {
|
|
3274
3518
|
effectPart = `(${this.hitEffect.toExpression()})`;
|
|
3275
3519
|
if (this.critEffect !== null) {
|
|
3276
|
-
|
|
3520
|
+
let crit;
|
|
3521
|
+
if (this.critEffect) {
|
|
3522
|
+
crit = this.critEffect;
|
|
3523
|
+
} else {
|
|
3524
|
+
crit = this.hitEffect?.copy().doubleDice() ?? RollBuilder.fromArgs(0);
|
|
3525
|
+
}
|
|
3277
3526
|
const critThreshold = this.check.critThreshold;
|
|
3278
3527
|
if (critThreshold < 15 || critThreshold > 20) {
|
|
3279
3528
|
throw new Error(
|
|
@@ -3586,6 +3835,7 @@ RollBuilder.prototype.dc = function(saveDC) {
|
|
|
3586
3835
|
exports.ACBuilder = ACBuilder;
|
|
3587
3836
|
exports.DCBuilder = DCBuilder;
|
|
3588
3837
|
exports.HalfRollBuilder = HalfRollBuilder;
|
|
3838
|
+
exports.MaxOfRollBuilder = MaxOfRollBuilder;
|
|
3589
3839
|
exports.RollBuilder = RollBuilder;
|
|
3590
3840
|
exports.SaveBuilder = SaveBuilder;
|
|
3591
3841
|
exports.builderPMFCache = builderPMFCache;
|