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