@yipe/dice 0.1.0 → 0.1.2
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 +18 -11
- package/dist/index.cjs +48 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -25
- package/dist/index.d.ts +24 -25
- package/dist/index.js +47 -42
- package/dist/index.js.map +1 -1
- package/package.json +19 -13
package/README.md
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
[](https://www.npmjs.com/package/@yipe/dice)
|
|
2
|
+
[](LICENSE)
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
4
6
|
|
|
5
|
-
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
[](https://github.com/yipe/dice/issues)
|
|
10
|
+
[](https://github.com/yipe/dice/actions)
|
|
11
|
+
[](https://github.com/yipe/dice/actions/workflows/ci.yml)
|
|
12
|
+
|
|
13
|
+
# 🎲 @yipe/dice
|
|
6
14
|
|
|
7
15
|
A TypeScript library for **D&D 5e damage-per-round (DPR) calculations**, designed for players, Dungeon Masters, and developers who want to analyze combat mathematically.
|
|
8
16
|
|
|
9
17
|
This library powers [dprcalc.com](https://dprcalc.com) and provides a precise, composable way to model dice rolls, attacks, and outcomes with probability mass functions (PMFs) — not just averages. This allows for rich charting and statistics with full outcome attribution.
|
|
10
18
|
|
|
11
19
|
```ts
|
|
12
|
-
import { parse } from "@
|
|
20
|
+
import { parse } from "@yipe/dice";
|
|
13
21
|
|
|
14
22
|
const attack = parse("(d20 + 8 AC 16) * (1d8 + 4) crit (2d8 + 4)");
|
|
15
23
|
console.log("DPR:", attack.mean());
|
|
@@ -29,9 +37,9 @@ console.log("DPR:", attack.mean());
|
|
|
29
37
|
|
|
30
38
|
```bash
|
|
31
39
|
# Install with npm or yarn
|
|
32
|
-
npm install @
|
|
40
|
+
npm install @yipe/dice
|
|
33
41
|
# or
|
|
34
|
-
yarn add @
|
|
42
|
+
yarn add @yipe/dice
|
|
35
43
|
```
|
|
36
44
|
|
|
37
45
|
## 📦 Core Concepts
|
|
@@ -48,7 +56,7 @@ yarn add @dpr/dice
|
|
|
48
56
|
Here's a simple example of calculating damage for a basic attack:
|
|
49
57
|
|
|
50
58
|
```ts
|
|
51
|
-
import { parse, DiceQuery } from "@
|
|
59
|
+
import { parse, DiceQuery } from "@yipe/dice";
|
|
52
60
|
|
|
53
61
|
const query = parse("(d20 + 8 AC 16) * (1d4 + 4) crit (2d4 + 4)").toQuery();
|
|
54
62
|
|
|
@@ -70,7 +78,7 @@ DPR: 4.35
|
|
|
70
78
|
Conditional damage ("once-per-turn damage riders") like Sneak Attack can be modeled easily:
|
|
71
79
|
|
|
72
80
|
```ts
|
|
73
|
-
import { parse, DiceQuery } from "@
|
|
81
|
+
import { parse, DiceQuery } from "@yipe/dice";
|
|
74
82
|
|
|
75
83
|
function damageRiderExample() {
|
|
76
84
|
const attack = parse("(d20 + 8 AC 16) * (1d4 + 4) crit (2d4 + 4)");
|
|
@@ -107,7 +115,7 @@ function damageRiderExample() {
|
|
|
107
115
|
You can generate full statistical distributions for visualization or reporting.
|
|
108
116
|
|
|
109
117
|
```ts
|
|
110
|
-
import { parse, DiceQuery } from "@
|
|
118
|
+
import { parse, DiceQuery } from "@yipe/dice";
|
|
111
119
|
|
|
112
120
|
const query2 = parse("(d20 + 8 AC 16) * (1d4 + 4) crit (2d4 + 4)").toQuery();
|
|
113
121
|
console.table(query2.toChartSeries());
|
|
@@ -227,7 +235,6 @@ Here is the basic example:
|
|
|
227
235
|
|
|
228
236
|
## 🧱 Roadmap
|
|
229
237
|
|
|
230
|
-
- [ ] **Publish** to npm as `@dpr/dice`
|
|
231
238
|
- [ ] Create a **web playground** with live examples
|
|
232
239
|
- [ ] Consider creating higher-level APIs: `Turn`, `Attack`, `DamageRider`
|
|
233
240
|
- [ ] Simplify and improve PMF and Query interface ergonomics
|
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
5
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
6
|
+
|
|
3
7
|
// src/lru-cache.ts
|
|
4
8
|
var LRUCache = class {
|
|
5
9
|
constructor(maxSize = 1e3) {
|
|
6
10
|
this.maxSize = maxSize;
|
|
7
|
-
this
|
|
11
|
+
__publicField(this, "cache", /* @__PURE__ */ new Map());
|
|
8
12
|
}
|
|
9
13
|
get(key) {
|
|
10
14
|
const value = this.cache.get(key);
|
|
@@ -43,12 +47,13 @@ var LRUCache = class {
|
|
|
43
47
|
};
|
|
44
48
|
|
|
45
49
|
// src/types.ts
|
|
46
|
-
var
|
|
47
|
-
var TEST_EPS = 1e-10;
|
|
50
|
+
var EPS = 1e-12;
|
|
48
51
|
|
|
49
52
|
// src/query.ts
|
|
50
53
|
var _DiceQuery = class _DiceQuery {
|
|
51
54
|
constructor(singles, combined) {
|
|
55
|
+
__publicField(this, "singles");
|
|
56
|
+
__publicField(this, "combined");
|
|
52
57
|
this.singles = Array.isArray(singles) ? singles : [singles];
|
|
53
58
|
const c = combined ?? PMF.convolveMany(this.singles);
|
|
54
59
|
this.combined = Math.abs(c.mass() - 1) <= 1e-12 ? c : c.normalize();
|
|
@@ -995,7 +1000,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
995
1000
|
outcomeTotals(outcomes = this.outcomeKeys()) {
|
|
996
1001
|
const totals = /* @__PURE__ */ new Map();
|
|
997
1002
|
outcomes.forEach((o) => totals.set(o, 0));
|
|
998
|
-
for (const [
|
|
1003
|
+
for (const [, row] of this.combined.map) {
|
|
999
1004
|
for (const o of outcomes) {
|
|
1000
1005
|
const p = row.count[o] || 0;
|
|
1001
1006
|
totals.set(o, (totals.get(o) || 0) + p);
|
|
@@ -1210,7 +1215,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1210
1215
|
* const bonusAttack = parse("(d20 + 3 AC 15) * (1d6 + 1)");
|
|
1211
1216
|
* const bothAttacks = mainAttack.convolve(bonusAttack);
|
|
1212
1217
|
*/
|
|
1213
|
-
convolve(other
|
|
1218
|
+
convolve(other) {
|
|
1214
1219
|
const singles = [...this.singles, ...other.singles];
|
|
1215
1220
|
return new _DiceQuery(singles);
|
|
1216
1221
|
}
|
|
@@ -1223,7 +1228,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1223
1228
|
*
|
|
1224
1229
|
* Returns tuple: [pFirstNonSubset, pFirstSubset, pAnySuccess, pNone]
|
|
1225
1230
|
*/
|
|
1226
|
-
firstSuccessSplit(successOutcome, subsetOutcome, eps =
|
|
1231
|
+
firstSuccessSplit(successOutcome, subsetOutcome, eps = EPS) {
|
|
1227
1232
|
const pmfs = this.singles;
|
|
1228
1233
|
if (!pmfs.length) {
|
|
1229
1234
|
throw new Error("firstSuccessSplitFromPMFs: pmfs must be non-empty");
|
|
@@ -1266,33 +1271,40 @@ var _DiceQuery = class _DiceQuery {
|
|
|
1266
1271
|
return [a, b, any, none];
|
|
1267
1272
|
}
|
|
1268
1273
|
};
|
|
1269
|
-
_DiceQuery
|
|
1274
|
+
__publicField(_DiceQuery, "DEFAULT_OUTCOMES", [
|
|
1270
1275
|
"hit",
|
|
1271
1276
|
"crit",
|
|
1272
1277
|
"missNone"
|
|
1273
|
-
];
|
|
1278
|
+
]);
|
|
1274
1279
|
var DiceQuery = _DiceQuery;
|
|
1275
|
-
var EPSILON = COMPUTATIONAL_EPS;
|
|
1276
1280
|
var pmfCache = new LRUCache(1e3);
|
|
1277
1281
|
var _PMF = class _PMF {
|
|
1278
|
-
constructor(map = /* @__PURE__ */ new Map(), epsilon =
|
|
1282
|
+
constructor(map = /* @__PURE__ */ new Map(), epsilon = EPS, normalized = false, identifier = `anon#${_PMF.__anonIdCounter++}`, _preservedProvidence = true) {
|
|
1279
1283
|
this.map = map;
|
|
1280
1284
|
this.epsilon = epsilon;
|
|
1281
1285
|
this.normalized = normalized;
|
|
1282
1286
|
this.identifier = identifier;
|
|
1283
1287
|
this._preservedProvidence = _preservedProvidence;
|
|
1284
|
-
|
|
1285
|
-
|
|
1288
|
+
// Cached computed values
|
|
1289
|
+
__publicField(this, "_support");
|
|
1290
|
+
__publicField(this, "_min");
|
|
1291
|
+
__publicField(this, "_max");
|
|
1292
|
+
__publicField(this, "_totalMass");
|
|
1293
|
+
__publicField(this, "_mean");
|
|
1294
|
+
__publicField(this, "_variance");
|
|
1295
|
+
__publicField(this, "_stdev");
|
|
1296
|
+
}
|
|
1297
|
+
static empty(epsilon = EPS, identifier = "empty") {
|
|
1286
1298
|
return new _PMF(/* @__PURE__ */ new Map(), epsilon, false, identifier);
|
|
1287
1299
|
}
|
|
1288
1300
|
// This has a single bin at value 0, mass of 1
|
|
1289
|
-
static zero(epsilon =
|
|
1301
|
+
static zero(epsilon = EPS) {
|
|
1290
1302
|
const m = /* @__PURE__ */ new Map();
|
|
1291
1303
|
m.set(0, { p: 1, count: { miss: 1 }, attr: {} });
|
|
1292
1304
|
return new _PMF(m, epsilon, false, "zero");
|
|
1293
1305
|
}
|
|
1294
1306
|
// This creates a single bin at value 0, but with weight 0.
|
|
1295
|
-
static emptyMass(
|
|
1307
|
+
static emptyMass() {
|
|
1296
1308
|
return _PMF.zero().scaleMass(0);
|
|
1297
1309
|
}
|
|
1298
1310
|
// Makes PMF iterable over [damage, bin] pairs.
|
|
@@ -1458,6 +1470,12 @@ var _PMF = class _PMF {
|
|
|
1458
1470
|
preservedProvidence() {
|
|
1459
1471
|
return this._preservedProvidence;
|
|
1460
1472
|
}
|
|
1473
|
+
getPowerCacheKey(n, eps) {
|
|
1474
|
+
const id = this.identifier;
|
|
1475
|
+
let key = `${id}`;
|
|
1476
|
+
for (let i = 1; i < n; i++) key += `+${id}`;
|
|
1477
|
+
return `${key}@${eps}`;
|
|
1478
|
+
}
|
|
1461
1479
|
/**
|
|
1462
1480
|
* Efficiently computes this PMF convolved with itself `n` times.
|
|
1463
1481
|
* Uses exponentiation by squaring to reduce total convolutions.
|
|
@@ -1474,7 +1492,7 @@ var _PMF = class _PMF {
|
|
|
1474
1492
|
}
|
|
1475
1493
|
if (n === 1) return this;
|
|
1476
1494
|
const epsilon = eps ?? this.epsilon;
|
|
1477
|
-
const key = getPowerCacheKey(
|
|
1495
|
+
const key = this.getPowerCacheKey(n, epsilon);
|
|
1478
1496
|
{
|
|
1479
1497
|
const cached = pmfCache?.get(key);
|
|
1480
1498
|
if (cached) return cached;
|
|
@@ -1500,7 +1518,7 @@ var _PMF = class _PMF {
|
|
|
1500
1518
|
/*
|
|
1501
1519
|
* Helper for chaining multiple identical attacks
|
|
1502
1520
|
*/
|
|
1503
|
-
replicate(n
|
|
1521
|
+
replicate(n) {
|
|
1504
1522
|
if (!Number.isInteger(n) || n <= 0) {
|
|
1505
1523
|
throw new Error("combineN(n): n must be a positive integer");
|
|
1506
1524
|
}
|
|
@@ -1873,7 +1891,7 @@ var _PMF = class _PMF {
|
|
|
1873
1891
|
* - Similar PMF lists share common prefixes (A+B, (A+B)+C, etc.)
|
|
1874
1892
|
* - Order-independent cache keys work better with consistent build patterns
|
|
1875
1893
|
*/
|
|
1876
|
-
static convolveMany(pmfList, eps =
|
|
1894
|
+
static convolveMany(pmfList, eps = EPS) {
|
|
1877
1895
|
if (pmfList.length === 0) return _PMF.empty(eps);
|
|
1878
1896
|
if (pmfList.length === 1) return pmfList[0];
|
|
1879
1897
|
return _PMF.reduceConvolveLeft(pmfList, eps);
|
|
@@ -1888,7 +1906,7 @@ var _PMF = class _PMF {
|
|
|
1888
1906
|
static fromJSON(jsonData) {
|
|
1889
1907
|
return new _PMF(
|
|
1890
1908
|
new Map(jsonData.bins),
|
|
1891
|
-
|
|
1909
|
+
EPS,
|
|
1892
1910
|
!!jsonData.normalized,
|
|
1893
1911
|
jsonData.identifier || "fromJSON"
|
|
1894
1912
|
);
|
|
@@ -2098,22 +2116,17 @@ var _PMF = class _PMF {
|
|
|
2098
2116
|
}
|
|
2099
2117
|
};
|
|
2100
2118
|
// Unique ID generator for anonymous PMFs to avoid cache key collisions
|
|
2101
|
-
_PMF
|
|
2119
|
+
__publicField(_PMF, "__anonIdCounter", 1);
|
|
2102
2120
|
var PMF = _PMF;
|
|
2103
|
-
function getPowerCacheKey(base, n, eps) {
|
|
2104
|
-
const id = base.identifier;
|
|
2105
|
-
let key = `${id}`;
|
|
2106
|
-
for (let i = 1; i < n; i++) key += `+${id}`;
|
|
2107
|
-
return `${key}@${eps}`;
|
|
2108
|
-
}
|
|
2109
2121
|
|
|
2110
2122
|
// src/dice.ts
|
|
2111
2123
|
var Dice = class _Dice {
|
|
2112
2124
|
constructor(x = 0) {
|
|
2113
|
-
this
|
|
2114
|
-
this
|
|
2115
|
-
this
|
|
2116
|
-
this
|
|
2125
|
+
__publicField(this, "faces", {});
|
|
2126
|
+
__publicField(this, "privateData", {});
|
|
2127
|
+
__publicField(this, "outcomeData", {});
|
|
2128
|
+
__publicField(this, "hasHitDistributionCalculated", false);
|
|
2129
|
+
__publicField(this, "identifier");
|
|
2117
2130
|
if (x <= 0) return;
|
|
2118
2131
|
for (let i = 1; i <= x; i++) {
|
|
2119
2132
|
this.faces[i] = 1;
|
|
@@ -2420,7 +2433,7 @@ var Dice = class _Dice {
|
|
|
2420
2433
|
* Convert dice to PMF using OutcomeType labels directly from damage distribution.
|
|
2421
2434
|
* This is much cleaner than the original complex distribution conversion.
|
|
2422
2435
|
*/
|
|
2423
|
-
toPMF(numEpsilon =
|
|
2436
|
+
toPMF(numEpsilon = EPS) {
|
|
2424
2437
|
const total = this.total();
|
|
2425
2438
|
if (total === 0) return PMF.empty(numEpsilon);
|
|
2426
2439
|
this.ensureHitDistribution();
|
|
@@ -2782,20 +2795,12 @@ function parseArgumentInternal(s, n) {
|
|
|
2782
2795
|
case "k":
|
|
2783
2796
|
assertToken(s, "k");
|
|
2784
2797
|
return parseKeep(s, n);
|
|
2785
|
-
case "0":
|
|
2786
|
-
case "1":
|
|
2787
|
-
case "2":
|
|
2788
|
-
case "3":
|
|
2789
|
-
case "4":
|
|
2790
|
-
case "5":
|
|
2791
|
-
case "6":
|
|
2792
|
-
case "7":
|
|
2793
|
-
case "8":
|
|
2794
|
-
case "9":
|
|
2795
2798
|
case "n":
|
|
2796
2799
|
return parseNumber(s, n);
|
|
2800
|
+
default:
|
|
2801
|
+
if (isDigit(c)) return parseNumber(s, n);
|
|
2802
|
+
return;
|
|
2797
2803
|
}
|
|
2798
|
-
return;
|
|
2799
2804
|
}
|
|
2800
2805
|
function parseBinaryArgument(arg, arr, n) {
|
|
2801
2806
|
if (arr.length >= 4 && arr[0] === "h" && peek(arr, "half")) {
|
|
@@ -2941,16 +2946,15 @@ function parseOperation(s) {
|
|
|
2941
2946
|
return;
|
|
2942
2947
|
}
|
|
2943
2948
|
|
|
2944
|
-
exports.COMPUTATIONAL_EPS = COMPUTATIONAL_EPS;
|
|
2945
2949
|
exports.Dice = Dice;
|
|
2946
2950
|
exports.DiceQuery = DiceQuery;
|
|
2951
|
+
exports.EPS = EPS;
|
|
2947
2952
|
exports.LRUCache = LRUCache;
|
|
2948
2953
|
exports.PMF = PMF;
|
|
2949
|
-
exports.TEST_EPS = TEST_EPS;
|
|
2950
2954
|
exports.clearParserCache = clearParserCache;
|
|
2951
2955
|
exports.getCachingEnabled = getCachingEnabled;
|
|
2952
|
-
exports.getPowerCacheKey = getPowerCacheKey;
|
|
2953
2956
|
exports.parse = parse;
|
|
2957
|
+
exports.pmfCache = pmfCache;
|
|
2954
2958
|
exports.setCachingEnabled = setCachingEnabled;
|
|
2955
2959
|
//# sourceMappingURL=index.cjs.map
|
|
2956
2960
|
//# sourceMappingURL=index.cjs.map
|