@yipe/dice 0.2.1 → 0.2.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.
@@ -1385,40 +1385,68 @@ var _PMF = class _PMF {
1385
1385
  * - A PMF representing its outcome (e.g., damage dice).
1386
1386
  * - A weight representing its probability of being selected.
1387
1387
  *
1388
- * This is useful for modeling situations like:
1389
- * - Sneak Attack: 6d6 if first hit is a crit, 3d6 if first hit is non-crit, 0 otherwise.
1390
- * - Multiple exclusive spells: Fireball vs Cone of Cold vs nothing.
1391
- *
1392
1388
  * Notes:
1393
- * - If the total weight is less than 1, a zero PMF will automatically be added to
1394
- * make up the remaining probability.
1395
- * - Throws an error if the total weight is greater than 1 (invalid probability sum).
1396
- *
1397
- * @param options Array of `{ pmf, weight }` objects, each representing an outcome and its probability.
1398
- * @param label Optional name for debugging or charting.
1399
- * @param eps Optional tolerance for floating point rounding
1400
- * @returns A single PMF representing the exclusive mixture of all options.
1389
+ * - If total weight < 1 (within eps), leftover mass is assumed to be PMF.zero()
1390
+ *
1391
+ * @param options Array of `{ pmf, weight }` or `[PMF, number]`.
1392
+ * @param eps Optional tolerance for floating point rounding.
1401
1393
  */
1402
1394
  static exclusive(options, eps = EPS) {
1403
1395
  const items = options.map(
1404
1396
  (o) => Array.isArray(o) ? { pmf: o[0], weight: o[1] } : o
1405
1397
  );
1406
- const totalWeight = items.reduce((s, { weight }) => s + weight, 0);
1407
- if (totalWeight - 1 > eps) {
1398
+ for (const { weight } of items) {
1399
+ if (!Number.isFinite(weight) || weight < -eps) {
1400
+ throw new Error(`PMF.exclusive: invalid weight ${weight}.`);
1401
+ }
1402
+ }
1403
+ let totalWeight = items.reduce((s, { weight }) => s + weight, 0);
1404
+ if (Math.abs(totalWeight) <= eps) totalWeight = 0;
1405
+ if (Math.abs(1 - totalWeight) <= eps) totalWeight = 1;
1406
+ if (totalWeight > 1 + eps) {
1408
1407
  throw new Error(
1409
1408
  `PMF.exclusive: total weight ${totalWeight.toFixed(6)} exceeds 1.`
1410
1409
  );
1411
1410
  }
1412
- let out = items.reduce(
1413
- (acc, { pmf, weight }) => weight > 0 ? acc.addScaled(pmf, weight) : acc,
1414
- _PMF.empty(eps)
1415
- );
1416
- const leftover = 1 - totalWeight;
1411
+ let out = _PMF.empty(eps);
1412
+ for (const { pmf, weight } of items) {
1413
+ if (weight > eps) out = out.addScaled(pmf, weight);
1414
+ }
1415
+ const leftover = Math.max(0, 1 - totalWeight);
1417
1416
  if (leftover > eps) {
1418
1417
  out = out.addScaled(_PMF.zero(), leftover);
1419
1418
  }
1420
1419
  return out;
1421
1420
  }
1421
+ /**
1422
+ * PMF.mix()
1423
+ *
1424
+ * Builds a PMF as a linear combination of input PMFs with the given weights.
1425
+ * Unlike `exclusive`, this does NOT:
1426
+ * - enforce that weights sum to 1
1427
+ * - add leftover probability to δ0 (PMF.zero())
1428
+ *
1429
+ * Use when outcomes are not mutually exclusive, or for interpolation/blending.
1430
+ *
1431
+ * @param options Array of `{ pmf, weight }` or `[PMF, number]`.
1432
+ * @param eps Optional tolerance for skipping tiny weights.
1433
+ */
1434
+ static mix(options, eps = EPS) {
1435
+ const items = options.map(
1436
+ (o) => Array.isArray(o) ? { pmf: o[0], weight: o[1] } : o
1437
+ );
1438
+ for (const { weight } of items) {
1439
+ if (!Number.isFinite(weight)) {
1440
+ throw new Error(`PMF.mix: invalid weight ${weight}.`);
1441
+ }
1442
+ }
1443
+ let out = _PMF.empty(eps);
1444
+ for (const { pmf, weight } of items) {
1445
+ if (Math.abs(weight) <= eps) continue;
1446
+ out = out.addScaled(pmf, weight);
1447
+ }
1448
+ return out;
1449
+ }
1422
1450
  /**
1423
1451
  * General-purpose N-way mixture.
1424
1452
  * weights: Array of [weight, PMF].
@@ -2387,386 +2415,113 @@ function d20PMF(rerollOne) {
2387
2415
  return result;
2388
2416
  }
2389
2417
 
2390
- // src/builder/ast.ts
2391
- var defaultEps = 0;
2392
- var singleDiePMFCache = new LRUCache(1e3);
2393
- function astFromRollConfigs(configs) {
2394
- if (!configs || configs.length === 0) return void 0;
2395
- const children = [];
2396
- let constantSum = 0;
2397
- for (const cfg of configs) {
2398
- const sign = cfg.isSubtraction || cfg.count < 0 ? -1 : 1;
2399
- const count = Math.abs(cfg.count || 0);
2400
- constantSum += cfg.modifier || 0;
2401
- if ((cfg.sides || 0) <= 0) continue;
2402
- const die = {
2403
- type: "die",
2404
- sides: cfg.sides,
2405
- reroll: cfg.reroll > 0 ? cfg.reroll : void 0,
2406
- minimum: cfg.minimum > 0 ? cfg.minimum : void 0,
2407
- explode: cfg.explode && Number.isFinite(cfg.explode) && cfg.explode > 0 ? cfg.explode : void 0
2408
- };
2409
- let node = die;
2410
- let appliedRollType = false;
2411
- if (cfg.rollType && cfg.rollType !== "flat") {
2412
- if (cfg.sides === 20) {
2413
- node = {
2414
- type: "d20Roll",
2415
- rollType: cfg.rollType,
2416
- child: node
2417
- };
2418
- } else {
2419
- const n = cfg.rollType === "elven accuracy" ? 3 : 2;
2420
- const mode = cfg.rollType === "disadvantage" ? "lowest" : "highest";
2421
- const base = { type: "sum", count: n, child: node };
2422
- node = { type: "keep", mode, count: 1, child: base };
2423
- }
2424
- appliedRollType = true;
2425
- }
2426
- if (cfg.rollType === "flat" && cfg.keep && cfg.keep.total > 0) {
2427
- const base = { type: "sum", count: cfg.keep.total, child: node };
2428
- node = {
2429
- type: "keep",
2430
- mode: cfg.keep.mode,
2431
- count: cfg.keep.count,
2432
- child: base
2433
- };
2418
+ // src/builder/roll.ts
2419
+ var defaultConfig = {
2420
+ count: 1,
2421
+ sides: 0,
2422
+ modifier: 0,
2423
+ reroll: 0,
2424
+ explode: 0,
2425
+ minimum: 0,
2426
+ bestOf: 0,
2427
+ keep: void 0,
2428
+ rollType: "flat"
2429
+ };
2430
+ var RollBuilder = class _RollBuilder {
2431
+ constructor(countOrConfigs = 1) {
2432
+ __publicField(this, "subRollConfigs");
2433
+ // --- Dice Shortcut Methods ---
2434
+ __publicField(this, "d4", () => this.d(4));
2435
+ __publicField(this, "d6", () => this.d(6));
2436
+ __publicField(this, "d8", () => this.d(8));
2437
+ __publicField(this, "d10", () => this.d(10));
2438
+ __publicField(this, "d12", () => this.d(12));
2439
+ __publicField(this, "d20", () => this.d(20));
2440
+ __publicField(this, "d100", () => this.d(100));
2441
+ if (typeof countOrConfigs === "number") {
2442
+ const count = countOrConfigs;
2443
+ if (isNaN(count)) throw new Error("Invalid NaN value for count");
2444
+ this.subRollConfigs = [
2445
+ { ...defaultConfig, count, isSubtraction: count < 0 }
2446
+ ];
2434
2447
  } else {
2435
- const c = appliedRollType ? 1 : Math.max(1, count || 1);
2436
- node = { type: "sum", count: c, child: node };
2448
+ this.subRollConfigs = countOrConfigs.map((c) => ({ ...c }));
2437
2449
  }
2438
- children.push({ node, sign });
2439
2450
  }
2440
- if (children.length === 0) {
2441
- return { type: "constant", value: constantSum };
2451
+ get lastConfig() {
2452
+ return this.subRollConfigs[this.subRollConfigs.length - 1];
2442
2453
  }
2443
- const add = { type: "add", children };
2444
- if (constantSum !== 0)
2445
- add.children.push({
2446
- node: { type: "constant", value: constantSum },
2447
- sign: 1
2448
- });
2449
- return add;
2450
- }
2451
- function resolve(node, eps = defaultEps) {
2452
- const signature = getASTSignature(node);
2453
- const cacheKey = `${signature}_${eps}`;
2454
- const cached = builderPMFCache.get(cacheKey);
2455
- if (cached) return cached;
2456
- const result = (() => {
2457
- switch (node.type) {
2458
- case "constant":
2459
- return PMF.delta(node.value, eps);
2460
- case "die": {
2461
- return resolveSingleDie(node, eps);
2462
- }
2463
- case "sum": {
2464
- const base = resolve(node.child, eps);
2465
- const n = Math.max(0, Math.floor(node.count));
2466
- if (n === 0) return PMF.delta(0, eps);
2467
- if (n === 1) return base;
2468
- return base.power(n, eps);
2454
+ getSubRollConfigs() {
2455
+ return this.subRollConfigs.map((c) => ({ ...c }));
2456
+ }
2457
+ // for testing
2458
+ static fromConfig(config) {
2459
+ return new _RollBuilder([{ ...defaultConfig, ...config }]);
2460
+ }
2461
+ static fromConfigs(configs) {
2462
+ return new _RollBuilder(
2463
+ configs.map((config) => ({ ...defaultConfig, ...config }))
2464
+ );
2465
+ }
2466
+ static fromArgs(...args) {
2467
+ if (args.length === 1) {
2468
+ const arg = args[0];
2469
+ if (typeof arg === "number") {
2470
+ if (isNaN(arg)) throw new Error("Invalid NaN value for argument");
2471
+ return new _RollBuilder(0).plus(arg);
2469
2472
  }
2470
- case "add": {
2471
- let shift = 0;
2472
- const parts = [];
2473
- for (const c of node.children) {
2474
- if (c.node.type === "constant") {
2475
- shift += c.sign * c.node.value;
2476
- } else {
2477
- const p = resolve(c.node, eps);
2478
- parts.push(c.sign === 1 ? p : p.mapDamage((v) => -v));
2479
- }
2480
- }
2481
- if (parts.length === 0) return PMF.delta(shift, eps);
2482
- let res = parts.length === 1 ? parts[0] : PMF.convolveMany(parts, eps);
2483
- if (shift !== 0) res = res.mapDamage((v) => v + shift);
2484
- return res;
2473
+ if (arg instanceof _RollBuilder) {
2474
+ return arg;
2485
2475
  }
2486
- case "keep": {
2487
- const total = getTotalCount(node);
2488
- const die = findDie(node.child.child);
2489
- const perDie = die ? resolveSingleDie(die, eps) : PMF.delta(0, eps);
2490
- const keepCount = Math.max(0, Math.min(node.count, total));
2491
- if (keepCount === 0 || total === 0) return PMF.delta(0, eps);
2492
- return keepSumPMF(
2493
- perDie,
2494
- total,
2495
- keepCount,
2496
- node.mode === "highest",
2497
- eps
2498
- );
2476
+ }
2477
+ if (args.length === 2 || args.length === 3) {
2478
+ const [count, sidesOrDie, modifier] = args;
2479
+ if (typeof count !== "number") {
2480
+ throw new Error("First argument must be a number for multi-arg call");
2499
2481
  }
2500
- case "d20Roll": {
2501
- const childDie = findDie(node.child);
2502
- const rerollOne = !!childDie && (childDie.reroll || 0) >= 1;
2503
- return d20RollPMF(node.rollType, rerollOne);
2482
+ if (isNaN(count)) throw new Error("Invalid NaN value for count argument");
2483
+ if (sidesOrDie instanceof _RollBuilder) {
2484
+ const subRollConfigs = sidesOrDie.getSubRollConfigs();
2485
+ if (subRollConfigs.length === 0) {
2486
+ const result = new _RollBuilder(0);
2487
+ return modifier !== void 0 ? result.plus(modifier) : result;
2488
+ }
2489
+ const absCount = Math.abs(count);
2490
+ const newConfigs = subRollConfigs.map((config) => ({
2491
+ ...config,
2492
+ count: config.count * absCount,
2493
+ modifier: config.modifier * absCount
2494
+ }));
2495
+ let resultBuilder = new _RollBuilder(newConfigs);
2496
+ if (count < 0) {
2497
+ const negatedConfigs = resultBuilder.getSubRollConfigs().map((c) => ({ ...c, isSubtraction: !c.isSubtraction }));
2498
+ resultBuilder = new _RollBuilder(negatedConfigs);
2499
+ }
2500
+ return modifier !== void 0 ? resultBuilder.plus(modifier) : resultBuilder;
2501
+ } else if (typeof sidesOrDie === "number" || sidesOrDie === void 0) {
2502
+ if (typeof sidesOrDie === "number" && isNaN(sidesOrDie))
2503
+ throw new Error("Invalid NaN value for sides argument");
2504
+ let builder = new _RollBuilder(count);
2505
+ if (sidesOrDie && sidesOrDie > 0) {
2506
+ builder = builder.d(sidesOrDie);
2507
+ }
2508
+ return modifier !== void 0 ? builder.plus(modifier) : builder;
2504
2509
  }
2505
2510
  }
2506
- })();
2507
- builderPMFCache.set(cacheKey, result);
2508
- return result;
2509
- }
2510
- function pmfFromRollBuilder(rb, eps = defaultEps) {
2511
- const configs = rb.getSubRollConfigs?.() ?? [];
2512
- const ast = astFromRollConfigs(configs) || { type: "constant", value: 0 };
2513
- return resolve(ast, eps);
2514
- }
2515
- function resolveSingleDie(die, eps = defaultEps) {
2516
- const signature = getASTSignature(die);
2517
- const cacheKey = `${signature}_${eps}`;
2518
- const cached = singleDiePMFCache.get(cacheKey);
2519
- if (cached) return cached;
2520
- const s = Math.max(0, Math.floor(die.sides));
2521
- if (s <= 0) return PMF.delta(0, eps);
2522
- let probs = /* @__PURE__ */ new Map();
2523
- for (let v = 1; v <= s; v++) probs.set(v, 1 / s);
2524
- const r = Math.max(0, Math.floor(die.reroll || 0));
2525
- if (r > 0) {
2526
- const k = Math.min(r, s);
2527
- const rerollMass = k / s;
2528
- const uniformReroll = rerollMass / s;
2529
- const next = /* @__PURE__ */ new Map();
2530
- for (let v = 1; v <= s; v++) {
2531
- const keep = v <= k ? 0 : 1 / s;
2532
- next.set(v, keep + uniformReroll);
2511
+ throw new Error(`Invalid arguments passed: ${args.join(", ")}`);
2512
+ }
2513
+ // --- Core Dice Methods ---
2514
+ d(sides) {
2515
+ if (sides !== void 0 && isNaN(sides))
2516
+ throw new Error("Invalid NaN value for sides");
2517
+ if (sides === void 0) return this;
2518
+ if (this.lastConfig.sides && this.lastConfig.sides > 0) {
2519
+ throw new Error("Cannot add a die after adding a die");
2533
2520
  }
2534
- probs = next;
2535
- }
2536
- let pmf = PMF.fromMap(new Map(probs), eps);
2537
- const minV = Math.max(0, Math.floor(die.minimum || 0));
2538
- if (minV > 0) pmf = pmf.mapDamage((v) => Math.max(v, minV));
2539
- const explode = die.explode;
2540
- if (explode && Number.isFinite(explode) && explode > 0) {
2541
- const times = Math.floor(explode);
2542
- const maxFace = s;
2543
- const nonMax = /* @__PURE__ */ new Map();
2544
- const pMax = pmf.pAt(maxFace);
2545
- for (const v of pmf.support()) {
2546
- if (v !== maxFace) nonMax.set(v, pmf.pAt(v));
2547
- }
2548
- let nonMaxPMF = PMF.fromMap(nonMax, eps);
2549
- if (Math.abs(nonMaxPMF.mass() - (1 - pMax)) > eps) {
2550
- nonMaxPMF = nonMaxPMF.scaleMass(1 - pMax);
2551
- }
2552
- let tail = PMF.delta(0, eps);
2553
- const addOnce = pmf;
2554
- for (let t = 1; t <= times; t++) {
2555
- tail = tail.convolve(addOnce, eps);
2556
- }
2557
- const exploded = PMF.branch(
2558
- tail.mapDamage((v) => v + maxFace),
2559
- nonMaxPMF,
2560
- pMax
2561
- );
2562
- pmf = exploded;
2563
- }
2564
- singleDiePMFCache.set(cacheKey, pmf);
2565
- return pmf;
2566
- }
2567
- function findDie(node) {
2568
- switch (node.type) {
2569
- case "die":
2570
- return node;
2571
- case "constant":
2572
- return void 0;
2573
- case "sum":
2574
- case "d20Roll":
2575
- return findDie(node.child);
2576
- case "keep":
2577
- return findDie(node.child.child);
2578
- case "add":
2579
- for (const c of node.children) {
2580
- const d2 = findDie(c.node);
2581
- if (d2) return d2;
2582
- }
2583
- return void 0;
2584
- }
2585
- }
2586
- function getTotalCount(node) {
2587
- let cur = node.child;
2588
- while (cur.type === "keep") cur = cur.child;
2589
- return cur.type === "sum" ? Math.max(0, Math.floor(cur.count)) : 0;
2590
- }
2591
- function keepSumPMF(single, total, keep, highest, eps = defaultEps) {
2592
- if (keep >= total) return single.power(total, eps);
2593
- if (keep <= 0) return PMF.delta(0, eps);
2594
- const sortedSupport = [...single.support()].sort((a, b) => a - b);
2595
- const pmfSig = sortedSupport.map((val) => `${val}:${single.pAt(val).toPrecision(6)}`).join(",");
2596
- const cacheKey = `keep|${pmfSig}|t:${total}|k:${keep}|h:${highest ? 1 : 0}|e:${eps}`;
2597
- const cached = builderPMFCache.get(cacheKey);
2598
- if (cached) return cached;
2599
- const support = single.support();
2600
- const probs = support.map((v) => ({ v, p: single.pAt(v) }));
2601
- const out = /* @__PURE__ */ new Map();
2602
- function dfs(i, accValues, weight) {
2603
- if (i === total) {
2604
- const sorted = [...accValues].sort((a, b) => highest ? b - a : a - b);
2605
- let sum = 0;
2606
- for (let j = 0; j < keep; j++) sum += sorted[j];
2607
- out.set(sum, (out.get(sum) || 0) + weight);
2608
- return;
2609
- }
2610
- for (const { v, p } of probs) {
2611
- if (p <= 0) continue;
2612
- dfs(i + 1, [...accValues, v], weight * p);
2613
- }
2614
- }
2615
- dfs(0, [], 1);
2616
- const result = PMF.fromMap(out, eps);
2617
- builderPMFCache.set(cacheKey, result);
2618
- return result;
2619
- }
2620
- function getASTSignature(node) {
2621
- switch (node.type) {
2622
- case "constant":
2623
- return `c:${node.value}`;
2624
- case "die": {
2625
- const parts = [];
2626
- parts.push(`s:${node.sides}`);
2627
- if (node.reroll) parts.push(`r:${node.reroll}`);
2628
- if (node.minimum) parts.push(`m:${node.minimum}`);
2629
- if (node.explode) parts.push(`e:${node.explode}`);
2630
- return `d{${parts.join(",")}}`;
2631
- }
2632
- case "sum":
2633
- return `sum{c:${node.count},ch:${getASTSignature(node.child)}}`;
2634
- case "d20Roll":
2635
- return `d20{t:${node.rollType},ch:${getASTSignature(node.child)}}`;
2636
- case "keep":
2637
- return `keep{c:${node.count},m:${node.mode},ch:${getASTSignature(
2638
- node.child
2639
- )}}`;
2640
- case "add": {
2641
- let constantValue = 0;
2642
- const otherChildrenSigs = [];
2643
- for (const c of node.children) {
2644
- if (c.node.type === "constant") {
2645
- constantValue += c.sign * c.node.value;
2646
- } else {
2647
- otherChildrenSigs.push(
2648
- `${c.sign === -1 ? "-" : "+"}${getASTSignature(c.node)}`
2649
- );
2650
- }
2651
- }
2652
- if (constantValue !== 0) {
2653
- otherChildrenSigs.push(
2654
- constantValue > 0 ? `+c:${constantValue}` : `-c:${-constantValue}`
2655
- );
2656
- }
2657
- otherChildrenSigs.sort();
2658
- return `add[${otherChildrenSigs.join("")}]`;
2659
- }
2660
- }
2661
- }
2662
-
2663
- // src/builder/roll.ts
2664
- var defaultConfig = {
2665
- count: 1,
2666
- sides: 0,
2667
- modifier: 0,
2668
- reroll: 0,
2669
- explode: 0,
2670
- minimum: 0,
2671
- bestOf: 0,
2672
- keep: void 0,
2673
- rollType: "flat"
2674
- };
2675
- var RollBuilder = class _RollBuilder {
2676
- constructor(countOrConfigs = 1) {
2677
- __publicField(this, "subRollConfigs");
2678
- // --- Dice Shortcut Methods ---
2679
- __publicField(this, "d4", () => this.d(4));
2680
- __publicField(this, "d6", () => this.d(6));
2681
- __publicField(this, "d8", () => this.d(8));
2682
- __publicField(this, "d10", () => this.d(10));
2683
- __publicField(this, "d12", () => this.d(12));
2684
- __publicField(this, "d20", () => this.d(20));
2685
- __publicField(this, "d100", () => this.d(100));
2686
- if (typeof countOrConfigs === "number") {
2687
- const count = countOrConfigs;
2688
- if (isNaN(count)) throw new Error("Invalid NaN value for count");
2689
- this.subRollConfigs = [
2690
- { ...defaultConfig, count, isSubtraction: count < 0 }
2691
- ];
2692
- } else {
2693
- this.subRollConfigs = countOrConfigs.map((c) => ({ ...c }));
2694
- }
2695
- }
2696
- get lastConfig() {
2697
- return this.subRollConfigs[this.subRollConfigs.length - 1];
2698
- }
2699
- getSubRollConfigs() {
2700
- return this.subRollConfigs.map((c) => ({ ...c }));
2701
- }
2702
- // for testing
2703
- static fromConfig(config) {
2704
- return new _RollBuilder([{ ...defaultConfig, ...config }]);
2705
- }
2706
- static fromConfigs(configs) {
2707
- return new _RollBuilder(
2708
- configs.map((config) => ({ ...defaultConfig, ...config }))
2709
- );
2710
- }
2711
- static fromArgs(...args) {
2712
- if (args.length === 1) {
2713
- const arg = args[0];
2714
- if (typeof arg === "number") {
2715
- if (isNaN(arg)) throw new Error("Invalid NaN value for argument");
2716
- return new _RollBuilder(0).plus(arg);
2717
- }
2718
- if (arg instanceof _RollBuilder) {
2719
- return arg;
2720
- }
2721
- }
2722
- if (args.length === 2 || args.length === 3) {
2723
- const [count, sidesOrDie, modifier] = args;
2724
- if (typeof count !== "number") {
2725
- throw new Error("First argument must be a number for multi-arg call");
2726
- }
2727
- if (isNaN(count)) throw new Error("Invalid NaN value for count argument");
2728
- if (sidesOrDie instanceof _RollBuilder) {
2729
- const subRollConfigs = sidesOrDie.getSubRollConfigs();
2730
- if (subRollConfigs.length === 0) {
2731
- const result = new _RollBuilder(0);
2732
- return modifier !== void 0 ? result.plus(modifier) : result;
2733
- }
2734
- const absCount = Math.abs(count);
2735
- const newConfigs = subRollConfigs.map((config) => ({
2736
- ...config,
2737
- count: config.count * absCount,
2738
- modifier: config.modifier * absCount
2739
- }));
2740
- let resultBuilder = new _RollBuilder(newConfigs);
2741
- if (count < 0) {
2742
- const negatedConfigs = resultBuilder.getSubRollConfigs().map((c) => ({ ...c, isSubtraction: !c.isSubtraction }));
2743
- resultBuilder = new _RollBuilder(negatedConfigs);
2744
- }
2745
- return modifier !== void 0 ? resultBuilder.plus(modifier) : resultBuilder;
2746
- } else if (typeof sidesOrDie === "number" || sidesOrDie === void 0) {
2747
- if (typeof sidesOrDie === "number" && isNaN(sidesOrDie))
2748
- throw new Error("Invalid NaN value for sides argument");
2749
- let builder = new _RollBuilder(count);
2750
- if (sidesOrDie && sidesOrDie > 0) {
2751
- builder = builder.d(sidesOrDie);
2752
- }
2753
- return modifier !== void 0 ? builder.plus(modifier) : builder;
2754
- }
2755
- }
2756
- throw new Error(`Invalid arguments passed: ${args.join(", ")}`);
2757
- }
2758
- // --- Core Dice Methods ---
2759
- d(sides) {
2760
- if (sides !== void 0 && isNaN(sides))
2761
- throw new Error("Invalid NaN value for sides");
2762
- if (sides === void 0) return this;
2763
- if (this.lastConfig.sides && this.lastConfig.sides > 0) {
2764
- throw new Error("Cannot add a die after adding a die");
2765
- }
2766
- if (sides === 0) return this;
2767
- const newConfigs = this.getSubRollConfigs();
2768
- newConfigs[newConfigs.length - 1].sides = sides;
2769
- return new this.constructor(newConfigs);
2521
+ if (sides === 0) return this;
2522
+ const newConfigs = this.getSubRollConfigs();
2523
+ newConfigs[newConfigs.length - 1].sides = sides;
2524
+ return new this.constructor(newConfigs);
2770
2525
  }
2771
2526
  plus(modOrRoll, die) {
2772
2527
  if (typeof modOrRoll === "number" && isNaN(modOrRoll))
@@ -2901,19 +2656,11 @@ var RollBuilder = class _RollBuilder {
2901
2656
  copy() {
2902
2657
  return new this.constructor(this.getSubRollConfigs());
2903
2658
  }
2904
- ac(targetAC) {
2905
- if (isNaN(targetAC)) throw new Error("Invalid NaN value for targetAC");
2906
- return new ACBuilder(this, targetAC);
2907
- }
2908
2659
  withElvenAccuracy() {
2909
2660
  const newConfigs = this.getSubRollConfigs();
2910
2661
  newConfigs[newConfigs.length - 1].rollType = "elven accuracy";
2911
2662
  return new this.constructor(newConfigs);
2912
2663
  }
2913
- dc(saveDC) {
2914
- if (isNaN(saveDC)) throw new Error("Invalid NaN value for saveDC");
2915
- return new DCBuilder(this).dc(saveDC);
2916
- }
2917
2664
  toExpression() {
2918
2665
  const diceConfigs = this.subRollConfigs.filter(
2919
2666
  (config) => config.sides && config.sides > 0
@@ -2941,118 +2688,444 @@ var RollBuilder = class _RollBuilder {
2941
2688
  result += operator + expression;
2942
2689
  }
2943
2690
  }
2944
- if (totalModifier > 0) result += ` + ${totalModifier}`;
2945
- else if (totalModifier < 0) result += ` - ${Math.abs(totalModifier)}`;
2946
- return result.replace(/\+ -/g, "-");
2947
- }
2948
- toPMF(eps = 0) {
2949
- return pmfFromRollBuilder(this, eps);
2950
- }
2951
- toQuery(eps = 0) {
2952
- return this.toPMF(eps).query();
2953
- }
2954
- toAST() {
2955
- const configs = this.getSubRollConfigs();
2956
- return astFromRollConfigs(configs) || { type: "constant", value: 0 };
2691
+ if (totalModifier > 0) result += ` + ${totalModifier}`;
2692
+ else if (totalModifier < 0) result += ` - ${Math.abs(totalModifier)}`;
2693
+ return result.replace(/\+ -/g, "-");
2694
+ }
2695
+ toPMF(eps = 0) {
2696
+ return pmfFromRollBuilder(this, eps);
2697
+ }
2698
+ get pmf() {
2699
+ return this.toPMF();
2700
+ }
2701
+ toQuery(eps = 0) {
2702
+ return this.toPMF(eps).query();
2703
+ }
2704
+ toAST() {
2705
+ const configs = this.getSubRollConfigs();
2706
+ return astFromRollConfigs(configs) || { type: "constant", value: 0 };
2707
+ }
2708
+ configToSingleExpressionWithoutModifier(config, isRootDie) {
2709
+ if (!config.sides || config.sides <= 0) return "";
2710
+ let baseDie = `d${config.sides}`;
2711
+ if (config.reroll > 0) {
2712
+ if (config.minimum > 0 && config.explode > 0) ; else if (config.minimum > 0) {
2713
+ for (let i = config.reroll; i >= 1; i--) baseDie += ` reroll ${i}`;
2714
+ } else {
2715
+ for (let i = 1; i <= config.reroll; i++) baseDie += ` reroll ${i}`;
2716
+ }
2717
+ }
2718
+ if (config.minimum > 0) {
2719
+ baseDie = `${config.minimum}>${baseDie}`;
2720
+ if (config.reroll > 0 && config.explode > 0) {
2721
+ for (let i = 1; i <= config.reroll; i++) {
2722
+ baseDie += ` reroll ${i}`;
2723
+ }
2724
+ }
2725
+ }
2726
+ if (baseDie === "d20 reroll 1" && config.minimum <= 1) baseDie = "hd20";
2727
+ let mainExpression = "";
2728
+ switch (config.rollType) {
2729
+ case "advantage":
2730
+ mainExpression = `${baseDie} > ${baseDie}`;
2731
+ break;
2732
+ case "disadvantage":
2733
+ mainExpression = `${baseDie} < ${baseDie}`;
2734
+ break;
2735
+ case "elven accuracy":
2736
+ mainExpression = `${baseDie} > ${baseDie} > ${baseDie}`;
2737
+ break;
2738
+ case "flat":
2739
+ if (config.keep) {
2740
+ const mode = config.keep.mode === "highest" ? "kh" : "kl";
2741
+ mainExpression = `${config.keep.total}${mode}${config.keep.count}(${baseDie})`;
2742
+ } else {
2743
+ const isComplex = baseDie.length > `d${config.sides}`.length;
2744
+ const isHalflingShorthand = baseDie === "hd20";
2745
+ const isD20Shorthand = baseDie === "d20" && isRootDie;
2746
+ const hasMinimum = baseDie.startsWith("(") && baseDie.includes(">");
2747
+ const hasReroll = baseDie.includes("reroll");
2748
+ const effectiveCount = config.isSubtraction ? Math.abs(config.count) : config.count < 0 ? 1 : Math.abs(config.count);
2749
+ if (effectiveCount > 1) {
2750
+ const shouldAddParentheses = isComplex && (// Complex single roll: explode + reroll
2751
+ !hasMinimum && !hasReroll || // Simple complex case
2752
+ hasReroll && !hasMinimum);
2753
+ mainExpression = shouldAddParentheses ? `${effectiveCount}(${baseDie})` : `${effectiveCount}${baseDie}`;
2754
+ } else if (effectiveCount === 1) {
2755
+ if (config.isSubtraction) {
2756
+ mainExpression = `1${baseDie}`;
2757
+ } else if (isComplex || isHalflingShorthand || isD20Shorthand || config.count < 0) {
2758
+ mainExpression = baseDie;
2759
+ } else {
2760
+ mainExpression = `1${baseDie}`;
2761
+ }
2762
+ } else {
2763
+ mainExpression = baseDie;
2764
+ }
2765
+ }
2766
+ if (config.bestOf && config.count && config.bestOf < config.count) {
2767
+ mainExpression += `kh${config.bestOf}`;
2768
+ }
2769
+ break;
2770
+ }
2771
+ return mainExpression;
2772
+ }
2773
+ getRootDieConfig() {
2774
+ const configs = this.subRollConfigs;
2775
+ return configs.find((config) => config.sides > 0) || configs[0];
2776
+ }
2777
+ getAllDieConfigs() {
2778
+ return this.getSubRollConfigs();
2779
+ }
2780
+ getBonusDiceConfigs() {
2781
+ const allConfigs = this.subRollConfigs;
2782
+ const rootConfig = allConfigs.find((config) => config.sides > 0) || allConfigs[0];
2783
+ if (!rootConfig) return [];
2784
+ return allConfigs.filter((config) => config.sides > 0).filter((config) => config !== rootConfig);
2785
+ }
2786
+ getBonusDicePMFs(check, eps = 0) {
2787
+ return check.getBonusDiceConfigs().map(
2788
+ (config) => pmfFromRollBuilder(_RollBuilder.fromConfigs([config]), eps)
2789
+ );
2790
+ }
2791
+ get modifier() {
2792
+ return this.subRollConfigs.reduce(
2793
+ (sum, config) => sum + config.modifier,
2794
+ 0
2795
+ );
2796
+ }
2797
+ get rollType() {
2798
+ const rootConfig = this.getRootDieConfig();
2799
+ return rootConfig?.rollType || "flat";
2800
+ }
2801
+ get baseReroll() {
2802
+ const rootConfig = this.getRootDieConfig();
2803
+ return rootConfig?.reroll || 0;
2804
+ }
2805
+ };
2806
+
2807
+ // src/builder/factory.ts
2808
+ var rollFn = (count, sidesOrDie, modifier) => {
2809
+ if (sidesOrDie instanceof RollBuilder) {
2810
+ const subRollConfigs = sidesOrDie.getSubRollConfigs();
2811
+ if (subRollConfigs.length === 0) return new RollBuilder(0).plus(modifier);
2812
+ const absCount = Math.abs(count);
2813
+ const newConfigs = subRollConfigs.map((config) => ({
2814
+ ...config,
2815
+ count: config.count * absCount,
2816
+ modifier: config.modifier * absCount
2817
+ }));
2818
+ let resultBuilder = new RollBuilder(newConfigs);
2819
+ if (count < 0) {
2820
+ const negatedConfigs = resultBuilder.getSubRollConfigs().map((c) => ({ ...c, isSubtraction: !c.isSubtraction }));
2821
+ resultBuilder = new RollBuilder(negatedConfigs);
2822
+ }
2823
+ return resultBuilder.plus(modifier);
2824
+ } else {
2825
+ let builder = new RollBuilder(count);
2826
+ if (sidesOrDie && sidesOrDie > 0) {
2827
+ builder = builder.d(sidesOrDie);
2828
+ }
2829
+ return builder.plus(modifier);
2830
+ }
2831
+ };
2832
+ rollFn.d = (sides) => new RollBuilder(1).d(sides);
2833
+ rollFn.hd20 = () => new RollBuilder(1).d20().reroll(1);
2834
+ rollFn.d4 = () => new RollBuilder(1).d4();
2835
+ rollFn.d6 = () => new RollBuilder(1).d6();
2836
+ rollFn.d8 = () => new RollBuilder(1).d8();
2837
+ rollFn.d10 = () => new RollBuilder(1).d10();
2838
+ rollFn.d12 = () => new RollBuilder(1).d12();
2839
+ rollFn.d20 = () => new RollBuilder(1).d20();
2840
+ rollFn.d100 = () => new RollBuilder(1).d100();
2841
+ rollFn.flat = (n) => new RollBuilder(0).plus(n);
2842
+ function d(sides) {
2843
+ return new RollBuilder(1).d(sides);
2844
+ }
2845
+ var d4 = new RollBuilder(1).d4();
2846
+ var d6 = new RollBuilder(1).d6();
2847
+ var d8 = new RollBuilder(1).d8();
2848
+ var d10 = new RollBuilder(1).d10();
2849
+ var d12 = new RollBuilder(1).d12();
2850
+ var d20 = new RollBuilder(1).d20();
2851
+ var hd20 = new RollBuilder(1).d20().reroll(1);
2852
+ var d100 = new RollBuilder(1).d100();
2853
+ var flat = (n) => new RollBuilder(0).plus(n);
2854
+ var roll = rollFn;
2855
+ var builderPMFCache = new LRUCache(1e3);
2856
+
2857
+ // src/builder/ast.ts
2858
+ var defaultEps = 0;
2859
+ var singleDiePMFCache = new LRUCache(1e3);
2860
+ function astFromRollConfigs(configs) {
2861
+ if (!configs || configs.length === 0) return void 0;
2862
+ const children = [];
2863
+ let constantSum = 0;
2864
+ for (const cfg of configs) {
2865
+ const sign = cfg.isSubtraction || cfg.count < 0 ? -1 : 1;
2866
+ const count = Math.abs(cfg.count || 0);
2867
+ constantSum += cfg.modifier || 0;
2868
+ if ((cfg.sides || 0) <= 0) continue;
2869
+ const die = {
2870
+ type: "die",
2871
+ sides: cfg.sides,
2872
+ reroll: cfg.reroll > 0 ? cfg.reroll : void 0,
2873
+ minimum: cfg.minimum > 0 ? cfg.minimum : void 0,
2874
+ explode: cfg.explode && Number.isFinite(cfg.explode) && cfg.explode > 0 ? cfg.explode : void 0
2875
+ };
2876
+ let node = die;
2877
+ let appliedRollType = false;
2878
+ if (cfg.rollType && cfg.rollType !== "flat") {
2879
+ if (cfg.sides === 20) {
2880
+ node = {
2881
+ type: "d20Roll",
2882
+ rollType: cfg.rollType,
2883
+ child: node
2884
+ };
2885
+ } else {
2886
+ const n = cfg.rollType === "elven accuracy" ? 3 : 2;
2887
+ const mode = cfg.rollType === "disadvantage" ? "lowest" : "highest";
2888
+ const base = { type: "sum", count: n, child: node };
2889
+ node = { type: "keep", mode, count: 1, child: base };
2890
+ }
2891
+ appliedRollType = true;
2892
+ }
2893
+ if (cfg.rollType === "flat" && cfg.keep && cfg.keep.total > 0) {
2894
+ const base = { type: "sum", count: cfg.keep.total, child: node };
2895
+ node = {
2896
+ type: "keep",
2897
+ mode: cfg.keep.mode,
2898
+ count: cfg.keep.count,
2899
+ child: base
2900
+ };
2901
+ } else {
2902
+ const c = appliedRollType ? 1 : Math.max(1, count || 1);
2903
+ node = { type: "sum", count: c, child: node };
2904
+ }
2905
+ children.push({ node, sign });
2906
+ }
2907
+ if (children.length === 0) {
2908
+ return { type: "constant", value: constantSum };
2909
+ }
2910
+ const add = { type: "add", children };
2911
+ if (constantSum !== 0)
2912
+ add.children.push({
2913
+ node: { type: "constant", value: constantSum },
2914
+ sign: 1
2915
+ });
2916
+ return add;
2917
+ }
2918
+ function resolve(node, eps = defaultEps) {
2919
+ const signature = getASTSignature(node);
2920
+ const cacheKey = `${signature}_${eps}`;
2921
+ const cached = builderPMFCache.get(cacheKey);
2922
+ if (cached) return cached;
2923
+ const result = (() => {
2924
+ switch (node.type) {
2925
+ case "constant":
2926
+ return PMF.delta(node.value, eps);
2927
+ case "die": {
2928
+ return resolveSingleDie(node, eps);
2929
+ }
2930
+ case "sum": {
2931
+ const base = resolve(node.child, eps);
2932
+ const n = Math.max(0, Math.floor(node.count));
2933
+ if (n === 0) return PMF.delta(0, eps);
2934
+ if (n === 1) return base;
2935
+ return base.power(n, eps);
2936
+ }
2937
+ case "add": {
2938
+ let shift = 0;
2939
+ const parts = [];
2940
+ for (const c of node.children) {
2941
+ if (c.node.type === "constant") {
2942
+ shift += c.sign * c.node.value;
2943
+ } else {
2944
+ const p = resolve(c.node, eps);
2945
+ parts.push(c.sign === 1 ? p : p.mapDamage((v) => -v));
2946
+ }
2947
+ }
2948
+ if (parts.length === 0) return PMF.delta(shift, eps);
2949
+ let res = parts.length === 1 ? parts[0] : PMF.convolveMany(parts, eps);
2950
+ if (shift !== 0) res = res.mapDamage((v) => v + shift);
2951
+ return res;
2952
+ }
2953
+ case "keep": {
2954
+ const total = getTotalCount(node);
2955
+ const die = findDie(node.child.child);
2956
+ const perDie = die ? resolveSingleDie(die, eps) : PMF.delta(0, eps);
2957
+ const keepCount = Math.max(0, Math.min(node.count, total));
2958
+ if (keepCount === 0 || total === 0) return PMF.delta(0, eps);
2959
+ return keepSumPMF(
2960
+ perDie,
2961
+ total,
2962
+ keepCount,
2963
+ node.mode === "highest",
2964
+ eps
2965
+ );
2966
+ }
2967
+ case "d20Roll": {
2968
+ const childDie = findDie(node.child);
2969
+ const rerollOne = !!childDie && (childDie.reroll || 0) >= 1;
2970
+ return d20RollPMF(node.rollType, rerollOne);
2971
+ }
2972
+ }
2973
+ })();
2974
+ builderPMFCache.set(cacheKey, result);
2975
+ return result;
2976
+ }
2977
+ function pmfFromRollBuilder(rb, eps = defaultEps) {
2978
+ const configs = rb.getSubRollConfigs?.() ?? [];
2979
+ const ast = astFromRollConfigs(configs) || { type: "constant", value: 0 };
2980
+ return resolve(ast, eps);
2981
+ }
2982
+ function resolveSingleDie(die, eps = defaultEps) {
2983
+ const signature = getASTSignature(die);
2984
+ const cacheKey = `${signature}_${eps}`;
2985
+ const cached = singleDiePMFCache.get(cacheKey);
2986
+ if (cached) return cached;
2987
+ const s = Math.max(0, Math.floor(die.sides));
2988
+ if (s <= 0) return PMF.delta(0, eps);
2989
+ let probs = /* @__PURE__ */ new Map();
2990
+ for (let v = 1; v <= s; v++) probs.set(v, 1 / s);
2991
+ const r = Math.max(0, Math.floor(die.reroll || 0));
2992
+ if (r > 0) {
2993
+ const k = Math.min(r, s);
2994
+ const rerollMass = k / s;
2995
+ const uniformReroll = rerollMass / s;
2996
+ const next = /* @__PURE__ */ new Map();
2997
+ for (let v = 1; v <= s; v++) {
2998
+ const keep = v <= k ? 0 : 1 / s;
2999
+ next.set(v, keep + uniformReroll);
3000
+ }
3001
+ probs = next;
3002
+ }
3003
+ let pmf = PMF.fromMap(new Map(probs), eps);
3004
+ const minV = Math.max(0, Math.floor(die.minimum || 0));
3005
+ if (minV > 0) pmf = pmf.mapDamage((v) => Math.max(v, minV));
3006
+ const explode = die.explode;
3007
+ if (explode && Number.isFinite(explode) && explode > 0) {
3008
+ const times = Math.floor(explode);
3009
+ const maxFace = s;
3010
+ const nonMax = /* @__PURE__ */ new Map();
3011
+ const pMax = pmf.pAt(maxFace);
3012
+ for (const v of pmf.support()) {
3013
+ if (v !== maxFace) nonMax.set(v, pmf.pAt(v));
3014
+ }
3015
+ let nonMaxPMF = PMF.fromMap(nonMax, eps);
3016
+ if (Math.abs(nonMaxPMF.mass() - (1 - pMax)) > eps) {
3017
+ nonMaxPMF = nonMaxPMF.scaleMass(1 - pMax);
3018
+ }
3019
+ let tail = PMF.delta(0, eps);
3020
+ const addOnce = pmf;
3021
+ for (let t = 1; t <= times; t++) {
3022
+ tail = tail.convolve(addOnce, eps);
3023
+ }
3024
+ const exploded = PMF.branch(
3025
+ tail.mapDamage((v) => v + maxFace),
3026
+ nonMaxPMF,
3027
+ pMax
3028
+ );
3029
+ pmf = exploded;
2957
3030
  }
2958
- configToSingleExpressionWithoutModifier(config, isRootDie) {
2959
- if (!config.sides || config.sides <= 0) return "";
2960
- let baseDie = `d${config.sides}`;
2961
- if (config.reroll > 0) {
2962
- if (config.minimum > 0 && config.explode > 0) ; else if (config.minimum > 0) {
2963
- for (let i = config.reroll; i >= 1; i--) baseDie += ` reroll ${i}`;
2964
- } else {
2965
- for (let i = 1; i <= config.reroll; i++) baseDie += ` reroll ${i}`;
3031
+ singleDiePMFCache.set(cacheKey, pmf);
3032
+ return pmf;
3033
+ }
3034
+ function findDie(node) {
3035
+ switch (node.type) {
3036
+ case "die":
3037
+ return node;
3038
+ case "constant":
3039
+ return void 0;
3040
+ case "sum":
3041
+ case "d20Roll":
3042
+ return findDie(node.child);
3043
+ case "keep":
3044
+ return findDie(node.child.child);
3045
+ case "add":
3046
+ for (const c of node.children) {
3047
+ const d2 = findDie(c.node);
3048
+ if (d2) return d2;
2966
3049
  }
3050
+ return void 0;
3051
+ }
3052
+ }
3053
+ function getTotalCount(node) {
3054
+ let cur = node.child;
3055
+ while (cur.type === "keep") cur = cur.child;
3056
+ return cur.type === "sum" ? Math.max(0, Math.floor(cur.count)) : 0;
3057
+ }
3058
+ function keepSumPMF(single, total, keep, highest, eps = defaultEps) {
3059
+ if (keep >= total) return single.power(total, eps);
3060
+ if (keep <= 0) return PMF.delta(0, eps);
3061
+ const sortedSupport = [...single.support()].sort((a, b) => a - b);
3062
+ const pmfSig = sortedSupport.map((val) => `${val}:${single.pAt(val).toPrecision(6)}`).join(",");
3063
+ const cacheKey = `keep|${pmfSig}|t:${total}|k:${keep}|h:${highest ? 1 : 0}|e:${eps}`;
3064
+ const cached = builderPMFCache.get(cacheKey);
3065
+ if (cached) return cached;
3066
+ const support = single.support();
3067
+ const probs = support.map((v) => ({ v, p: single.pAt(v) }));
3068
+ const out = /* @__PURE__ */ new Map();
3069
+ function dfs(i, accValues, weight) {
3070
+ if (i === total) {
3071
+ const sorted = [...accValues].sort((a, b) => highest ? b - a : a - b);
3072
+ let sum = 0;
3073
+ for (let j = 0; j < keep; j++) sum += sorted[j];
3074
+ out.set(sum, (out.get(sum) || 0) + weight);
3075
+ return;
2967
3076
  }
2968
- if (config.minimum > 0) {
2969
- baseDie = `${config.minimum}>${baseDie}`;
2970
- if (config.reroll > 0 && config.explode > 0) {
2971
- for (let i = 1; i <= config.reroll; i++) {
2972
- baseDie += ` reroll ${i}`;
2973
- }
2974
- }
3077
+ for (const { v, p } of probs) {
3078
+ if (p <= 0) continue;
3079
+ dfs(i + 1, [...accValues, v], weight * p);
2975
3080
  }
2976
- if (baseDie === "d20 reroll 1" && config.minimum <= 1) baseDie = "hd20";
2977
- let mainExpression = "";
2978
- switch (config.rollType) {
2979
- case "advantage":
2980
- mainExpression = `${baseDie} > ${baseDie}`;
2981
- break;
2982
- case "disadvantage":
2983
- mainExpression = `${baseDie} < ${baseDie}`;
2984
- break;
2985
- case "elven accuracy":
2986
- mainExpression = `${baseDie} > ${baseDie} > ${baseDie}`;
2987
- break;
2988
- case "flat":
2989
- if (config.keep) {
2990
- const mode = config.keep.mode === "highest" ? "kh" : "kl";
2991
- mainExpression = `${config.keep.total}${mode}${config.keep.count}(${baseDie})`;
3081
+ }
3082
+ dfs(0, [], 1);
3083
+ const result = PMF.fromMap(out, eps);
3084
+ builderPMFCache.set(cacheKey, result);
3085
+ return result;
3086
+ }
3087
+ function getASTSignature(node) {
3088
+ switch (node.type) {
3089
+ case "constant":
3090
+ return `c:${node.value}`;
3091
+ case "die": {
3092
+ const parts = [];
3093
+ parts.push(`s:${node.sides}`);
3094
+ if (node.reroll) parts.push(`r:${node.reroll}`);
3095
+ if (node.minimum) parts.push(`m:${node.minimum}`);
3096
+ if (node.explode) parts.push(`e:${node.explode}`);
3097
+ return `d{${parts.join(",")}}`;
3098
+ }
3099
+ case "sum":
3100
+ return `sum{c:${node.count},ch:${getASTSignature(node.child)}}`;
3101
+ case "d20Roll":
3102
+ return `d20{t:${node.rollType},ch:${getASTSignature(node.child)}}`;
3103
+ case "keep":
3104
+ return `keep{c:${node.count},m:${node.mode},ch:${getASTSignature(
3105
+ node.child
3106
+ )}}`;
3107
+ case "add": {
3108
+ let constantValue = 0;
3109
+ const otherChildrenSigs = [];
3110
+ for (const c of node.children) {
3111
+ if (c.node.type === "constant") {
3112
+ constantValue += c.sign * c.node.value;
2992
3113
  } else {
2993
- const isComplex = baseDie.length > `d${config.sides}`.length;
2994
- const isHalflingShorthand = baseDie === "hd20";
2995
- const isD20Shorthand = baseDie === "d20" && isRootDie;
2996
- const hasMinimum = baseDie.startsWith("(") && baseDie.includes(">");
2997
- const hasReroll = baseDie.includes("reroll");
2998
- const effectiveCount = config.isSubtraction ? Math.abs(config.count) : config.count < 0 ? 1 : Math.abs(config.count);
2999
- if (effectiveCount > 1) {
3000
- const shouldAddParentheses = isComplex && (// Complex single roll: explode + reroll
3001
- !hasMinimum && !hasReroll || // Simple complex case
3002
- hasReroll && !hasMinimum);
3003
- mainExpression = shouldAddParentheses ? `${effectiveCount}(${baseDie})` : `${effectiveCount}${baseDie}`;
3004
- } else if (effectiveCount === 1) {
3005
- if (config.isSubtraction) {
3006
- mainExpression = `1${baseDie}`;
3007
- } else if (isComplex || isHalflingShorthand || isD20Shorthand || config.count < 0) {
3008
- mainExpression = baseDie;
3009
- } else {
3010
- mainExpression = `1${baseDie}`;
3011
- }
3012
- } else {
3013
- mainExpression = baseDie;
3014
- }
3015
- }
3016
- if (config.bestOf && config.count && config.bestOf < config.count) {
3017
- mainExpression += `kh${config.bestOf}`;
3114
+ otherChildrenSigs.push(
3115
+ `${c.sign === -1 ? "-" : "+"}${getASTSignature(c.node)}`
3116
+ );
3018
3117
  }
3019
- break;
3118
+ }
3119
+ if (constantValue !== 0) {
3120
+ otherChildrenSigs.push(
3121
+ constantValue > 0 ? `+c:${constantValue}` : `-c:${-constantValue}`
3122
+ );
3123
+ }
3124
+ otherChildrenSigs.sort();
3125
+ return `add[${otherChildrenSigs.join("")}]`;
3020
3126
  }
3021
- return mainExpression;
3022
- }
3023
- getRootDieConfig() {
3024
- const configs = this.subRollConfigs;
3025
- return configs.find((config) => config.sides > 0) || configs[0];
3026
- }
3027
- getAllDieConfigs() {
3028
- return this.getSubRollConfigs();
3029
- }
3030
- getBonusDiceConfigs() {
3031
- const allConfigs = this.subRollConfigs;
3032
- const rootConfig = allConfigs.find((config) => config.sides > 0) || allConfigs[0];
3033
- if (!rootConfig) return [];
3034
- return allConfigs.filter((config) => config.sides > 0).filter((config) => config !== rootConfig);
3035
- }
3036
- getBonusDicePMFs(check, eps = 0) {
3037
- return check.getBonusDiceConfigs().map(
3038
- (config) => pmfFromRollBuilder(_RollBuilder.fromConfigs([config]), eps)
3039
- );
3040
- }
3041
- get modifier() {
3042
- return this.subRollConfigs.reduce(
3043
- (sum, config) => sum + config.modifier,
3044
- 0
3045
- );
3046
- }
3047
- get rollType() {
3048
- const rootConfig = this.getRootDieConfig();
3049
- return rootConfig?.rollType || "flat";
3050
- }
3051
- get baseReroll() {
3052
- const rootConfig = this.getRootDieConfig();
3053
- return rootConfig?.reroll || 0;
3054
3127
  }
3055
- };
3128
+ }
3056
3129
 
3057
3130
  // src/builder/attack.ts
3058
3131
  var AttackBuilder = class _AttackBuilder {
@@ -3089,9 +3162,8 @@ var AttackBuilder = class _AttackBuilder {
3089
3162
  let effectPart = "";
3090
3163
  if (this.hitEffect) {
3091
3164
  effectPart = `(${this.hitEffect.toExpression()})`;
3092
- let crit = void 0;
3093
3165
  if (this.critEffect !== null) {
3094
- crit = this.critEffect ?? this.hitEffect.copy().doubleDice();
3166
+ const crit = this.critEffect ?? this.hitEffect.copy().doubleDice();
3095
3167
  const critThreshold = this.check.critThreshold;
3096
3168
  if (critThreshold < 15 || critThreshold > 20) {
3097
3169
  throw new Error(
@@ -3179,6 +3251,9 @@ var AttackBuilder = class _AttackBuilder {
3179
3251
  toPMF(eps = 0) {
3180
3252
  return this.resolve(eps).pmf;
3181
3253
  }
3254
+ get pmf() {
3255
+ return this.toPMF();
3256
+ }
3182
3257
  // By default, create query on PMF with no pruning
3183
3258
  toQuery(eps = 0) {
3184
3259
  return this.toPMF(eps).query();
@@ -3252,6 +3327,10 @@ var ACBuilder = class _ACBuilder extends RollBuilder {
3252
3327
  return new _ACBuilder(baseCopy, newConfig.ac, newConfig);
3253
3328
  }
3254
3329
  };
3330
+ RollBuilder.prototype.ac = function(targetAC) {
3331
+ if (isNaN(targetAC)) throw new Error("Invalid NaN value for targetAC");
3332
+ return new ACBuilder(this, targetAC);
3333
+ };
3255
3334
 
3256
3335
  // src/builder/save.ts
3257
3336
  var SaveBuilder = class _SaveBuilder {
@@ -3294,6 +3373,9 @@ var SaveBuilder = class _SaveBuilder {
3294
3373
  toPMF(eps = 0) {
3295
3374
  return this.resolve(eps).pmf;
3296
3375
  }
3376
+ get pmf() {
3377
+ return this.toPMF();
3378
+ }
3297
3379
  // By default, create query on PMF with no pruning
3298
3380
  toQuery(eps = 0) {
3299
3381
  return this.toPMF(eps).query();
@@ -3391,56 +3473,10 @@ var DCBuilder = class _DCBuilder extends RollBuilder {
3391
3473
  return PMF.fromMap(m, eps);
3392
3474
  }
3393
3475
  };
3394
-
3395
- // src/builder/factory.ts
3396
- var rollFn = (count, sidesOrDie, modifier) => {
3397
- if (sidesOrDie instanceof RollBuilder) {
3398
- const subRollConfigs = sidesOrDie.getSubRollConfigs();
3399
- if (subRollConfigs.length === 0) return new RollBuilder(0).plus(modifier);
3400
- const absCount = Math.abs(count);
3401
- const newConfigs = subRollConfigs.map((config) => ({
3402
- ...config,
3403
- count: config.count * absCount,
3404
- modifier: config.modifier * absCount
3405
- }));
3406
- let resultBuilder = new RollBuilder(newConfigs);
3407
- if (count < 0) {
3408
- const negatedConfigs = resultBuilder.getSubRollConfigs().map((c) => ({ ...c, isSubtraction: !c.isSubtraction }));
3409
- resultBuilder = new RollBuilder(negatedConfigs);
3410
- }
3411
- return resultBuilder.plus(modifier);
3412
- } else {
3413
- let builder = new RollBuilder(count);
3414
- if (sidesOrDie && sidesOrDie > 0) {
3415
- builder = builder.d(sidesOrDie);
3416
- }
3417
- return builder.plus(modifier);
3418
- }
3476
+ RollBuilder.prototype.dc = function(saveDC) {
3477
+ if (isNaN(saveDC)) throw new Error("Invalid NaN value for saveDC");
3478
+ return new DCBuilder(this).dc(saveDC);
3419
3479
  };
3420
- rollFn.d = (sides) => new RollBuilder(1).d(sides);
3421
- rollFn.hd20 = () => new RollBuilder(1).d20().reroll(1);
3422
- rollFn.d4 = () => new RollBuilder(1).d4();
3423
- rollFn.d6 = () => new RollBuilder(1).d6();
3424
- rollFn.d8 = () => new RollBuilder(1).d8();
3425
- rollFn.d10 = () => new RollBuilder(1).d10();
3426
- rollFn.d12 = () => new RollBuilder(1).d12();
3427
- rollFn.d20 = () => new RollBuilder(1).d20();
3428
- rollFn.d100 = () => new RollBuilder(1).d100();
3429
- rollFn.flat = (n) => new RollBuilder(0).plus(n);
3430
- function d(sides) {
3431
- return new RollBuilder(1).d(sides);
3432
- }
3433
- var d4 = new RollBuilder(1).d4();
3434
- var d6 = new RollBuilder(1).d6();
3435
- var d8 = new RollBuilder(1).d8();
3436
- var d10 = new RollBuilder(1).d10();
3437
- var d12 = new RollBuilder(1).d12();
3438
- var d20 = new RollBuilder(1).d20();
3439
- var hd20 = new RollBuilder(1).d20().reroll(1);
3440
- var d100 = new RollBuilder(1).d100();
3441
- var flat = (n) => new RollBuilder(0).plus(n);
3442
- var roll = rollFn;
3443
- var builderPMFCache = new LRUCache(1e3);
3444
3480
 
3445
3481
  export { ACBuilder, DCBuilder, RollBuilder, SaveBuilder, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll };
3446
3482
  //# sourceMappingURL=index.js.map