@yipe/dice 0.8.1 → 0.10.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/README.md +169 -19
- package/dist/builder/ac.d.ts.map +1 -1
- package/dist/builder/ast.d.ts +32 -2
- package/dist/builder/ast.d.ts.map +1 -1
- package/dist/builder/attack.d.ts.map +1 -1
- package/dist/builder/dc.d.ts.map +1 -1
- package/dist/builder/example.d.ts +4 -4
- package/dist/builder/example.d.ts.map +1 -1
- package/dist/builder/index.cjs +889 -90
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.d.ts +1 -0
- package/dist/builder/index.d.ts.map +1 -1
- package/dist/builder/index.js +886 -91
- package/dist/builder/index.js.map +1 -1
- package/dist/builder/roll.d.ts +5 -1
- package/dist/builder/roll.d.ts.map +1 -1
- package/dist/builder/save.d.ts.map +1 -1
- package/dist/index.cjs +178 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +177 -16
- package/dist/index.js.map +1 -1
- package/dist/parser/dice.d.ts +14 -0
- package/dist/parser/dice.d.ts.map +1 -1
- package/dist/parser/rollType.d.ts +58 -0
- package/dist/parser/rollType.d.ts.map +1 -0
- package/dist/pmf/pmf.d.ts +10 -4
- package/dist/pmf/pmf.d.ts.map +1 -1
- package/dist/pmf/query.d.ts +26 -0
- package/dist/pmf/query.d.ts.map +1 -1
- package/dist/turn/index.d.ts +3 -0
- package/dist/turn/index.d.ts.map +1 -0
- package/dist/turn/plan.d.ts +55 -0
- package/dist/turn/plan.d.ts.map +1 -0
- package/dist/turn/state.d.ts +23 -0
- package/dist/turn/state.d.ts.map +1 -0
- package/dist/turn/turn.d.ts +149 -0
- package/dist/turn/turn.d.ts.map +1 -0
- package/dist/turn/types.d.ts +99 -0
- package/dist/turn/types.d.ts.map +1 -0
- package/package.json +1 -1
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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)
|
|
@@ -1513,7 +1566,7 @@ var _PMF = class _PMF {
|
|
|
1513
1566
|
const id = this.identifier;
|
|
1514
1567
|
let key = `${id}`;
|
|
1515
1568
|
for (let i = 1; i < n; i++) key += `+${id}`;
|
|
1516
|
-
return `${key}@${eps}`;
|
|
1569
|
+
return `${key}@${eps}|${this.fingerprint()}`;
|
|
1517
1570
|
}
|
|
1518
1571
|
/**
|
|
1519
1572
|
* Efficiently computes this PMF convolved with itself `n` times.
|
|
@@ -1864,16 +1917,27 @@ var _PMF = class _PMF {
|
|
|
1864
1917
|
return `v4:${raw ? "RAW" : "N"}:${id1}+${id2}@${eps}|${p1.fingerprint()}|${p2.fingerprint()}`;
|
|
1865
1918
|
}
|
|
1866
1919
|
/**
|
|
1867
|
-
* A
|
|
1868
|
-
* cache keys change
|
|
1869
|
-
*
|
|
1870
|
-
*
|
|
1920
|
+
* A content fingerprint of every bin (probability, per-label `count`, per-label `attr`) plus
|
|
1921
|
+
* the `normalized` flag, so convolution/power cache keys change whenever the underlying
|
|
1922
|
+
* numbers do. Mass/bin-count/face-sum alone are not content-unique: `mapDamage` variants can
|
|
1923
|
+
* keep the same identifier, support, mass, and face sum while differing in per-bin
|
|
1924
|
+
* probabilities or in the `count`/`attr` channels `convolve()`/`power()` actually propagate --
|
|
1925
|
+
* that previously let `power()` return one PMF's cached result for a different PMF. Memoized
|
|
1926
|
+
* because a PMF is immutable once constructed -- this avoids re-deriving the key on every
|
|
1927
|
+
* convolve()/power() call (including cache hits). Bin order is sorted by damage value (and
|
|
1928
|
+
* label keys sorted within each bin) so two equal-content PMFs built via different code paths
|
|
1929
|
+
* fingerprint identically regardless of Map insertion order.
|
|
1871
1930
|
*/
|
|
1872
1931
|
fingerprint() {
|
|
1873
1932
|
if (this._fingerprint === void 0) {
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1933
|
+
const bins = [...this.map.entries()].sort((a, b) => a[0] - b[0]);
|
|
1934
|
+
const parts = [];
|
|
1935
|
+
for (const [damageValue, bin] of bins) {
|
|
1936
|
+
const countStr = Object.keys(bin.count).sort().map((k) => `${k}:${bin.count[k]}`).join(",");
|
|
1937
|
+
const attrStr = bin.attr ? Object.keys(bin.attr).sort().map((k) => `${k}:${bin.attr[k]}`).join(",") : "";
|
|
1938
|
+
parts.push(`${damageValue}:${bin.p}[${countStr}]{${attrStr}}`);
|
|
1939
|
+
}
|
|
1940
|
+
this._fingerprint = `${this.normalized ? 1 : 0}|${parts.join(";")}`;
|
|
1877
1941
|
}
|
|
1878
1942
|
return this._fingerprint;
|
|
1879
1943
|
}
|
|
@@ -2105,11 +2169,13 @@ var _PMF = class _PMF {
|
|
|
2105
2169
|
/** Quantile / inverse CDF for p in [0,1]. Returns smallest x with CDF ≥ p. */
|
|
2106
2170
|
quantile(p) {
|
|
2107
2171
|
if (this.map.size === 0) return 0;
|
|
2172
|
+
const totalMass = this.mass();
|
|
2173
|
+
if (totalMass <= 0) return 0;
|
|
2108
2174
|
const s = this.support().sort((a, b) => a - b);
|
|
2109
2175
|
let acc = 0;
|
|
2110
2176
|
for (const x of s) {
|
|
2111
2177
|
acc += this.pAt(x);
|
|
2112
|
-
if (acc >= p) return x;
|
|
2178
|
+
if (acc / totalMass >= p) return x;
|
|
2113
2179
|
}
|
|
2114
2180
|
return s[s.length - 1];
|
|
2115
2181
|
}
|
|
@@ -2970,6 +3036,12 @@ function combineDiceWithNormalization(dice, normValue, outcomeType, currentNorm,
|
|
|
2970
3036
|
finalResult = finalResult.combine(dice);
|
|
2971
3037
|
return { newNorm: currentNorm * normValue, updatedResult: finalResult };
|
|
2972
3038
|
}
|
|
3039
|
+
function subtractCounts(a, b) {
|
|
3040
|
+
const result = new Dice();
|
|
3041
|
+
for (const [key, value] of a.getFaceEntries()) result.increment(key, value);
|
|
3042
|
+
for (const [key, value] of b.getFaceEntries()) result.increment(key, -value);
|
|
3043
|
+
return result;
|
|
3044
|
+
}
|
|
2973
3045
|
function parseExpression(arr, n) {
|
|
2974
3046
|
const result = (() => {
|
|
2975
3047
|
const res = parseArgument(arr, n);
|
|
@@ -2977,8 +3049,29 @@ function parseExpression(arr, n) {
|
|
|
2977
3049
|
})();
|
|
2978
3050
|
let op = parseOperation(arr);
|
|
2979
3051
|
let finalResult = result;
|
|
3052
|
+
let baseDieMeta = result.privateData?.checkDie && !result.privateData.checkDie.rerollOne ? result.privateData.checkDie : void 0;
|
|
3053
|
+
let bonusOnly = Dice.scalar(0);
|
|
2980
3054
|
while (op != null) {
|
|
2981
3055
|
const arg = !op.unary ? parseArgument(arr, n) : finalResult;
|
|
3056
|
+
let acAlreadyApplied = false;
|
|
3057
|
+
if (baseDieMeta) {
|
|
3058
|
+
if (op === Dice.prototype.addNonZero) {
|
|
3059
|
+
bonusOnly = bonusOnly.add(arg);
|
|
3060
|
+
} else if (op === Dice.prototype.subtract) {
|
|
3061
|
+
bonusOnly = bonusOnly.subtract(arg);
|
|
3062
|
+
} else if (op === Dice.prototype.ac && typeof arg === "number") {
|
|
3063
|
+
const natMaxSlice = bonusOnly.add(baseDieMeta.sides);
|
|
3064
|
+
const restSlice = subtractCounts(finalResult, natMaxSlice);
|
|
3065
|
+
const gatedNatMaxSlice = natMaxSlice.ac(arg);
|
|
3066
|
+
finalResult = restSlice.ac(arg).combine(gatedNatMaxSlice);
|
|
3067
|
+
finalResult.privateData.checkDie = baseDieMeta;
|
|
3068
|
+
finalResult.privateData.natMaxCritSlice = gatedNatMaxSlice;
|
|
3069
|
+
acAlreadyApplied = true;
|
|
3070
|
+
baseDieMeta = void 0;
|
|
3071
|
+
} else {
|
|
3072
|
+
baseDieMeta = void 0;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
2982
3075
|
let crit;
|
|
2983
3076
|
let critNorm = 1;
|
|
2984
3077
|
if (arr[0] === "x" || arr[0] === "c") {
|
|
@@ -2989,11 +3082,17 @@ function parseExpression(arr, n) {
|
|
|
2989
3082
|
assertToken(arr, "i");
|
|
2990
3083
|
assertToken(arr, "t");
|
|
2991
3084
|
const count = isXcrit ? parseNumber(arr, n) : 1;
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
3085
|
+
const trackedCritSlice = finalResult.privateData?.natMaxCritSlice;
|
|
3086
|
+
if (count === 1 && trackedCritSlice) {
|
|
3087
|
+
crit = trackedCritSlice;
|
|
3088
|
+
finalResult = subtractCounts(finalResult, trackedCritSlice);
|
|
3089
|
+
} else {
|
|
3090
|
+
crit = new Dice();
|
|
3091
|
+
for (let i = 0; i < count; i++) {
|
|
3092
|
+
const max = finalResult.maxFace();
|
|
3093
|
+
crit.setFace(max, finalResult.get(max));
|
|
3094
|
+
finalResult = finalResult.deleteFace(max);
|
|
3095
|
+
}
|
|
2997
3096
|
}
|
|
2998
3097
|
critNorm = crit.total();
|
|
2999
3098
|
crit = op.call(crit, parseBinaryArgument(arg, arr, n));
|
|
@@ -3044,7 +3143,9 @@ function parseExpression(arr, n) {
|
|
|
3044
3143
|
missNorm = miss && missNorm ? miss.total() / missNorm : 1;
|
|
3045
3144
|
}
|
|
3046
3145
|
let norm = finalResult.total();
|
|
3047
|
-
|
|
3146
|
+
if (!acAlreadyApplied) {
|
|
3147
|
+
finalResult = op.call(finalResult, arg);
|
|
3148
|
+
}
|
|
3048
3149
|
norm = norm ? finalResult.total() / norm : 1;
|
|
3049
3150
|
if (crit) {
|
|
3050
3151
|
const result2 = combineDiceWithNormalization(
|
|
@@ -3235,6 +3336,7 @@ function parseDice(s, n) {
|
|
|
3235
3336
|
if (rerollOne) {
|
|
3236
3337
|
result = result.reroll(1);
|
|
3237
3338
|
}
|
|
3339
|
+
result.privateData.checkDie = { sides, rerollOne };
|
|
3238
3340
|
return result;
|
|
3239
3341
|
}
|
|
3240
3342
|
function peek(arr, expected) {
|
|
@@ -3345,6 +3447,65 @@ function parseOperation(s) {
|
|
|
3345
3447
|
return;
|
|
3346
3448
|
}
|
|
3347
3449
|
|
|
3450
|
+
// src/parser/rollType.ts
|
|
3451
|
+
var DECIMAL_INTEGER = /^\s*[+-]?\d+\s*$/;
|
|
3452
|
+
function tryParse(expression) {
|
|
3453
|
+
if (DECIMAL_INTEGER.test(expression)) {
|
|
3454
|
+
const value = Number(expression);
|
|
3455
|
+
return Number.isSafeInteger(value) ? PMF.delta(value) : PMF.empty();
|
|
3456
|
+
}
|
|
3457
|
+
try {
|
|
3458
|
+
return parse(expression);
|
|
3459
|
+
} catch {
|
|
3460
|
+
return PMF.empty();
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
var CHECK_TOKEN = /\b(AC|DC)\b/i;
|
|
3464
|
+
var D20_RUN = /\bh?d20(?:\s*[><]\s*h?d20)*/i;
|
|
3465
|
+
var RUN_FOR_ROLL_TYPE = {
|
|
3466
|
+
flat: "d20",
|
|
3467
|
+
advantage: "d20 > d20",
|
|
3468
|
+
disadvantage: "d20 < d20",
|
|
3469
|
+
"elven accuracy": "d20 > d20 > d20"
|
|
3470
|
+
};
|
|
3471
|
+
function withRollType(expression, rollType) {
|
|
3472
|
+
const scopes = enclosingScopes(expression);
|
|
3473
|
+
let rewritten = "";
|
|
3474
|
+
let copiedUpTo = 0;
|
|
3475
|
+
for (const run of expression.matchAll(new RegExp(D20_RUN, "gi"))) {
|
|
3476
|
+
const at = run.index;
|
|
3477
|
+
if (!isAttackRoll(expression, scopes, at)) continue;
|
|
3478
|
+
const replacement = run[0].toLowerCase().startsWith("h") ? `h${RUN_FOR_ROLL_TYPE[rollType]}` : RUN_FOR_ROLL_TYPE[rollType];
|
|
3479
|
+
rewritten += expression.slice(copiedUpTo, at) + replacement;
|
|
3480
|
+
copiedUpTo = at + run[0].length;
|
|
3481
|
+
}
|
|
3482
|
+
return rewritten + expression.slice(copiedUpTo);
|
|
3483
|
+
}
|
|
3484
|
+
function enclosingScopes(expression) {
|
|
3485
|
+
const open = [];
|
|
3486
|
+
const scopes = [];
|
|
3487
|
+
for (let i = 0; i < expression.length; i++) {
|
|
3488
|
+
if (expression[i] === "(") open.push(i);
|
|
3489
|
+
else if (expression[i] === ")") {
|
|
3490
|
+
const start = open.pop();
|
|
3491
|
+
if (start !== void 0) scopes.push({ start, end: i + 1 });
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
return scopes.sort((a, b) => a.end - a.start - (b.end - b.start));
|
|
3495
|
+
}
|
|
3496
|
+
function isAttackRoll(expression, scopes, at) {
|
|
3497
|
+
let scoped = false;
|
|
3498
|
+
for (const scope of scopes) {
|
|
3499
|
+
if (at < scope.start || at >= scope.end) continue;
|
|
3500
|
+
scoped = true;
|
|
3501
|
+
const check2 = CHECK_TOKEN.exec(expression.slice(scope.start, scope.end));
|
|
3502
|
+
if (check2) return check2[1].toUpperCase() === "AC";
|
|
3503
|
+
}
|
|
3504
|
+
if (scoped) return false;
|
|
3505
|
+
const check = CHECK_TOKEN.exec(expression);
|
|
3506
|
+
return check !== null && check[1].toUpperCase() === "AC";
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3348
3509
|
// src/pmf/mixture.ts
|
|
3349
3510
|
var Mixture = class _Mixture {
|
|
3350
3511
|
constructor(eps = EPS) {
|
|
@@ -3463,6 +3624,6 @@ var Mixture = class _Mixture {
|
|
|
3463
3624
|
}
|
|
3464
3625
|
};
|
|
3465
3626
|
|
|
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 };
|
|
3627
|
+
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
3628
|
//# sourceMappingURL=index.js.map
|
|
3468
3629
|
//# sourceMappingURL=index.js.map
|