@yipe/dice 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,25 @@
1
1
  // src/common/bounce.ts
2
+ function faceWeights(faces, minimum = 0, reroll = 0) {
3
+ const f = Math.max(0, Math.floor(faces));
4
+ if (f <= 0) return [];
5
+ let weights = new Array(f).fill(1 / f);
6
+ const r = Math.max(0, Math.min(Math.floor(reroll), f));
7
+ if (r > 0) {
8
+ const rerollMass = r / f;
9
+ const uniformReroll = rerollMass / f;
10
+ weights = weights.map((_, i) => (i < r ? 0 : 1 / f) + uniformReroll);
11
+ }
12
+ const minV = Math.max(0, Math.floor(minimum));
13
+ if (minV > 1) {
14
+ const collapsed = new Array(f).fill(0);
15
+ for (let v = 1; v <= f; v++) {
16
+ const target = Math.min(f, Math.max(v, minV));
17
+ collapsed[target - 1] += weights[v - 1];
18
+ }
19
+ weights = collapsed;
20
+ }
21
+ return weights;
22
+ }
2
23
  function binom(n, k) {
3
24
  if (k < 0 || k > n) return 0;
4
25
  let result = 1;
@@ -47,6 +68,128 @@ function calculateBounceOdds(diceCount, dieFaces, options) {
47
68
  );
48
69
  return Math.min(1, pMatchFirst + pNoMatchFirst * pMatchAfterReroll);
49
70
  }
71
+ function diceSumDistribution(dice, weights) {
72
+ let dist = /* @__PURE__ */ new Map([[0, 1]]);
73
+ for (let die = 0; die < dice; die++) {
74
+ const next = /* @__PURE__ */ new Map();
75
+ for (const [sum, mass] of dist) {
76
+ for (let face = 1; face <= weights.length; face++) {
77
+ const w = weights[face - 1] ?? 0;
78
+ if (w <= 0) continue;
79
+ const s = sum + face;
80
+ next.set(s, (next.get(s) ?? 0) + mass * w);
81
+ }
82
+ }
83
+ dist = next;
84
+ }
85
+ return dist;
86
+ }
87
+ function sumAllDistinctDistribution(dice, weights) {
88
+ const faceCount = weights.length;
89
+ let dp = /* @__PURE__ */ new Map([[0, /* @__PURE__ */ new Map([[0, 1]])]]);
90
+ for (let face = 1; face <= faceCount; face++) {
91
+ const w = weights[face - 1] ?? 0;
92
+ const next = /* @__PURE__ */ new Map();
93
+ for (const [count, sumMap] of dp) next.set(count, new Map(sumMap));
94
+ if (w > 0) {
95
+ for (const [count, sumMap] of dp) {
96
+ const nextCount = count + 1;
97
+ if (nextCount > dice) continue;
98
+ const target = next.get(nextCount) ?? /* @__PURE__ */ new Map();
99
+ for (const [sum, mass] of sumMap) {
100
+ const s = sum + face;
101
+ target.set(s, (target.get(s) ?? 0) + mass * w);
102
+ }
103
+ next.set(nextCount, target);
104
+ }
105
+ }
106
+ dp = next;
107
+ }
108
+ let factorial = 1;
109
+ for (let i = 2; i <= dice; i++) factorial *= i;
110
+ const chosen = dp.get(dice) ?? /* @__PURE__ */ new Map();
111
+ const result = /* @__PURE__ */ new Map();
112
+ for (const [sum, mass] of chosen) result.set(sum, mass * factorial);
113
+ return result;
114
+ }
115
+ function jointSumAndMatch(dice, weights) {
116
+ if (dice <= 1) return /* @__PURE__ */ new Map();
117
+ const total = diceSumDistribution(dice, weights);
118
+ const distinct = sumAllDistinctDistribution(dice, weights);
119
+ const result = /* @__PURE__ */ new Map();
120
+ for (const [sum, mass] of total) {
121
+ const matchMass = Math.max(0, mass - (distinct.get(sum) ?? 0));
122
+ if (matchMass > 0) result.set(sum, matchMass);
123
+ }
124
+ return result;
125
+ }
126
+ function explodingPoolMkDistribution(pMax, count, budget) {
127
+ const binomial = (n, p) => {
128
+ const result = new Array(n + 1).fill(0);
129
+ result[0] = 1;
130
+ for (let trial = 0; trial < n; trial++) {
131
+ const next = new Array(n + 1).fill(0);
132
+ for (let successes = 0; successes <= trial; successes++) {
133
+ const mass = result[successes];
134
+ if (mass <= 0) continue;
135
+ next[successes] += mass * (1 - p);
136
+ next[successes + 1] += mass * p;
137
+ }
138
+ for (let i = 0; i <= n; i++) result[i] = next[i];
139
+ }
140
+ return result;
141
+ };
142
+ const memo = /* @__PURE__ */ new Map();
143
+ const f = (pending, remainingBudget) => {
144
+ if (pending === 0) return /* @__PURE__ */ new Map([["0,0", 1]]);
145
+ if (remainingBudget === 0) {
146
+ const binom2 = binomial(pending, pMax);
147
+ const result2 = /* @__PURE__ */ new Map();
148
+ for (let m = 0; m <= pending; m++) {
149
+ const mass = binom2[m];
150
+ if (mass > 0) result2.set(`${m},${pending - m}`, mass);
151
+ }
152
+ return result2;
153
+ }
154
+ const key = `${pending},${remainingBudget}`;
155
+ const cached = memo.get(key);
156
+ if (cached) return cached;
157
+ const result = /* @__PURE__ */ new Map();
158
+ const accumulate = (mk, mass) => {
159
+ result.set(mk, (result.get(mk) ?? 0) + mass);
160
+ };
161
+ for (const [mk, mass] of f(pending, remainingBudget - 1)) {
162
+ const [m, k] = mk.split(",").map(Number);
163
+ accumulate(`${m + 1},${k}`, mass * pMax);
164
+ }
165
+ for (const [mk, mass] of f(pending - 1, remainingBudget)) {
166
+ const [m, k] = mk.split(",").map(Number);
167
+ accumulate(`${m},${k + 1}`, mass * (1 - pMax));
168
+ }
169
+ memo.set(key, result);
170
+ return result;
171
+ };
172
+ return f(count, budget);
173
+ }
174
+ function explodingPoolMatchProbability(pMax, faces, count, budget) {
175
+ if (count <= 1) return 0;
176
+ const mkDistribution = explodingPoolMkDistribution(pMax, count, budget);
177
+ const nonMaxFaces = Math.max(1, faces - 1);
178
+ let pMatchTotal = 0;
179
+ for (const [mk, weight] of mkDistribution) {
180
+ const [m, k] = mk.split(",").map(Number);
181
+ if (m >= 2) {
182
+ pMatchTotal += weight;
183
+ continue;
184
+ }
185
+ let pAllDistinctAmongNonMax = 1;
186
+ for (let i = 0; i < k; i++) {
187
+ pAllDistinctAmongNonMax *= (nonMaxFaces - i) / nonMaxFaces;
188
+ }
189
+ pMatchTotal += weight * (1 - Math.max(0, pAllDistinctAmongNonMax));
190
+ }
191
+ return Math.min(1, Math.max(0, pMatchTotal));
192
+ }
50
193
 
51
194
  // src/common/errors.ts
52
195
  var DiceParseError = class _DiceParseError extends Error {
@@ -1878,6 +2021,30 @@ var _PMF = class _PMF {
1878
2021
  `freq(${this.identifier},${freq})`
1879
2022
  );
1880
2023
  }
2024
+ /**
2025
+ * Splits this PMF into two complementary PMFs by an arbitrary per-damage-value factor in
2026
+ * `[0, 1]` — bin `d`'s mass, `count`, and `attr` split `factor(d)` / `1 - factor(d)` between the
2027
+ * two results (via the same proportional scaling {@link applyHitFrequency} uses, `scaleBin`), so
2028
+ * `a.add(b)` recovers this PMF exactly and both halves stay chart-attributable. Unlike
2029
+ * {@link applyHitFrequency}, mass is NOT redistributed to a miss bin at 0 — each bin stays at its
2030
+ * own damage value in whichever half it lands in. `factor` outside `[0, 1]` is clamped.
2031
+ *
2032
+ * Built for `dice-match` trigger slicing: splitting a hit/crit sub-PMF into "matched" and
2033
+ * "did not match" halves by the exact per-damage-value match probability.
2034
+ */
2035
+ splitByFactor(factor) {
2036
+ const a = /* @__PURE__ */ new Map();
2037
+ const b = /* @__PURE__ */ new Map();
2038
+ for (const [damage, bin] of this.map) {
2039
+ const f = Math.min(1, Math.max(0, factor(damage)));
2040
+ if (f > 0) a.set(damage, _PMF.scaleBin(bin, f));
2041
+ if (f < 1) b.set(damage, _PMF.scaleBin(bin, 1 - f));
2042
+ }
2043
+ return [
2044
+ new _PMF(a, this.epsilon, false, `split+(${this.identifier})`),
2045
+ new _PMF(b, this.epsilon, false, `split-(${this.identifier})`)
2046
+ ];
2047
+ }
1881
2048
  scaleMass(factor) {
1882
2049
  if (factor === 1) return this;
1883
2050
  const scaledMap = /* @__PURE__ */ new Map();
@@ -3624,6 +3791,6 @@ var Mixture = class _Mixture {
3624
3791
  }
3625
3792
  };
3626
3793
 
3627
- export { ALL_OUTCOME_TYPES, DiceParseError, DiceQuery, EPS, LRUCache, MISS_NONE_OUTCOME, Mixture, OUTCOME_DISPLAY_ORDER, PMF, calculateBounceOdds, clearParserCache, critProbability, getCachingEnabled, onAnyHit, onCritOnly, onHitOnly, onMissDamageOnly, onMissOnly, onPotentCantripOnly, onSaveFailOnly, onSaveHalfOnly, parse, pmfCache, setCachingEnabled, sortOutcomes, tryParse, withRollType };
3794
+ export { ALL_OUTCOME_TYPES, DiceParseError, DiceQuery, EPS, LRUCache, MISS_NONE_OUTCOME, Mixture, OUTCOME_DISPLAY_ORDER, PMF, calculateBounceOdds, clearParserCache, critProbability, diceSumDistribution, explodingPoolMatchProbability, faceWeights, getCachingEnabled, jointSumAndMatch, onAnyHit, onCritOnly, onHitOnly, onMissDamageOnly, onMissOnly, onPotentCantripOnly, onSaveFailOnly, onSaveHalfOnly, parse, pmfCache, setCachingEnabled, sortOutcomes, tryParse, withRollType };
3628
3795
  //# sourceMappingURL=index.js.map
3629
3796
  //# sourceMappingURL=index.js.map