@allemandi/gacha-engine 0.2.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -62
- package/dist/gacha-engine.d.ts +3 -18
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/types.d.ts +13 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@allemandi/gacha-engine)
|
|
4
4
|
[](https://github.com/allemandi/gacha-engine/blob/main/LICENSE)
|
|
5
5
|
|
|
6
|
-
> **Practical, type-safe toolkit for simulating and understanding gacha rates and rate-ups.**
|
|
7
|
-
>
|
|
8
|
-
> Works in Node.js
|
|
6
|
+
> **Practical, type-safe toolkit for simulating and understanding gacha rates and rate-ups.**
|
|
7
|
+
> Supports `"weighted"` and `"flatRate"` modes for different gacha strategies.
|
|
8
|
+
> Works in Node.js and browsers – supports ESM, CommonJS, and UMD.
|
|
9
|
+
|
|
10
|
+
---
|
|
9
11
|
|
|
10
12
|
<!-- omit from toc -->
|
|
11
13
|
## 🔖 Table of Contents
|
|
14
|
+
|
|
12
15
|
- [✨ Features](#-features)
|
|
13
16
|
- [🛠️ Installation](#️-installation)
|
|
14
17
|
- [🚀 Quick Usage Examples](#-quick-usage-examples)
|
|
@@ -17,13 +20,17 @@
|
|
|
17
20
|
- [🔗 Related Projects](#-related-projects)
|
|
18
21
|
- [🤝 Contributing](#-contributing)
|
|
19
22
|
|
|
23
|
+
---
|
|
20
24
|
|
|
21
25
|
## ✨ Features
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
26
|
+
|
|
27
|
+
- 🎲 **Roll simulation** – Perform gacha rolls with weighted or flat-rate logic
|
|
28
|
+
- 🔍 **Probability analysis** – Drop rates, cumulative probabilities, target probabilities
|
|
29
|
+
- 📐 **Multi-rarity support** – Flexible rarity-based or item-based probability distributions
|
|
30
|
+
- ⚡ **Performance optimized** – Efficient with cached calculations
|
|
31
|
+
- 🛡️ **Type-safe** – Written in TypeScript with strict configuration validation
|
|
32
|
+
|
|
33
|
+
---
|
|
27
34
|
|
|
28
35
|
## 🛠️ Installation
|
|
29
36
|
```bash
|
|
@@ -37,7 +44,7 @@ npm install @allemandi/gacha-engine
|
|
|
37
44
|
|
|
38
45
|
## 🚀 Quick Usage Examples
|
|
39
46
|
|
|
40
|
-
**ESM**
|
|
47
|
+
**ESM (Weighted Mode)**
|
|
41
48
|
```js
|
|
42
49
|
import { GachaEngine } from '@allemandi/gacha-engine';
|
|
43
50
|
|
|
@@ -47,17 +54,15 @@ const pools = [
|
|
|
47
54
|
items: [
|
|
48
55
|
{ name: 'Super Hobo', weight: 0.8, rateUp: true },
|
|
49
56
|
{ name: 'Broke King', weight: 0.4 },
|
|
50
|
-
{ name: 'Cardboard Hero', weight: 0.4 }
|
|
51
|
-
{ name: 'Newspaper Warmer', weight: 0.4 }
|
|
57
|
+
{ name: 'Cardboard Hero', weight: 0.4 }
|
|
52
58
|
]
|
|
53
59
|
},
|
|
54
60
|
{
|
|
55
|
-
rarity: 'SR',
|
|
61
|
+
rarity: 'SR',
|
|
56
62
|
items: [
|
|
57
63
|
{ name: 'Cold Salaryman', weight: 1.5, rateUp: true },
|
|
58
64
|
{ name: 'Numb Artist', weight: 1.8 },
|
|
59
|
-
{ name: 'Crying Cook', weight: 1.8 }
|
|
60
|
-
{ name: 'Lonely Cat', weight: 1.8 }
|
|
65
|
+
{ name: 'Crying Cook', weight: 1.8 }
|
|
61
66
|
]
|
|
62
67
|
},
|
|
63
68
|
{
|
|
@@ -70,80 +75,135 @@ const pools = [
|
|
|
70
75
|
];
|
|
71
76
|
|
|
72
77
|
const rarityRates = {
|
|
73
|
-
SSR: 0.01,
|
|
74
|
-
SR: 0.05,
|
|
75
|
-
R: 0.94
|
|
78
|
+
SSR: 0.01,
|
|
79
|
+
SR: 0.05,
|
|
80
|
+
R: 0.94
|
|
76
81
|
};
|
|
77
82
|
|
|
78
|
-
const engine = new GachaEngine({ pools, rarityRates });
|
|
83
|
+
const engine = new GachaEngine({ mode: 'weighted', pools, rarityRates });
|
|
79
84
|
|
|
80
|
-
|
|
81
|
-
const results = engine.roll(10);
|
|
82
|
-
console.log(`10 rolls result: ${results.join(', ')}`);
|
|
85
|
+
console.log('10 rolls:', engine.roll(10).join(', '));
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Output: ~0.4% (0.8/2.0 * 0.01)
|
|
87
|
+
const rate = engine.getItemDropRate('Super Hobo');
|
|
88
|
+
console.log('Drop rate for Super Hobo:', (rate * 100) + '%');
|
|
89
|
+
// ~0.4% → (0.8 / 1.6) * 0.01 = 0.005 → 0.5%
|
|
88
90
|
|
|
89
|
-
const
|
|
90
|
-
console.log(
|
|
91
|
+
const cumulative = engine.getCumulativeProbabilityForItem('Super Hobo', 300);
|
|
92
|
+
console.log('Probability in 300 rolls:', (cumulative * 100) + '%');
|
|
93
|
+
// ~77.7%
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
console.log('Rolls for 50% chance:', engine.getRollsForTargetProbability('Super Hobo', 0.5));
|
|
96
|
+
// 139
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// Output: "Super Hobo, Cold Salaryman"
|
|
98
|
+
console.log('Rate-up items:', engine.getRateUpItems().join(', '));
|
|
99
|
+
// Super Hobo, Cold Salaryman
|
|
98
100
|
```
|
|
99
101
|
|
|
100
|
-
**CommonJS**
|
|
102
|
+
**CommonJS (Flat Rate Mode)**
|
|
101
103
|
```js
|
|
102
104
|
const { GachaEngine } = require('@allemandi/gacha-engine');
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
const pools = [
|
|
107
|
+
{
|
|
108
|
+
rarity: 'SSR',
|
|
109
|
+
items: [
|
|
110
|
+
{ name: 'God-Tier Rat', weight: 0.003, rateUp: true },
|
|
111
|
+
{ name: 'Dumpster King', weight: 0.002 }
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
rarity: 'SR',
|
|
116
|
+
items: [
|
|
117
|
+
{ name: 'Sleepy Chef', weight: 0.015 }
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
rarity: 'R',
|
|
122
|
+
items: [
|
|
123
|
+
{ name: 'Unknown Student', weight: 0.1 }
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const engine = new GachaEngine({ mode: 'flatRate', pools });
|
|
129
|
+
|
|
130
|
+
console.log('Roll x5:', engine.roll(5).join(', '));
|
|
131
|
+
|
|
132
|
+
const dropRate = engine.getItemDropRate('God-Tier Rat');
|
|
133
|
+
console.log('Drop rate for God-Tier Rat:', (dropRate * 100) + '%');
|
|
134
|
+
// 0.3%
|
|
135
|
+
|
|
136
|
+
const cumulative = engine.getCumulativeProbabilityForItem('God-Tier Rat', 500);
|
|
137
|
+
console.log('Chance after 500 rolls:', (cumulative * 100).toFixed(1) + '%');
|
|
138
|
+
// ~78.5%
|
|
139
|
+
|
|
140
|
+
const rollsFor50 = engine.getRollsForTargetProbability('God-Tier Rat', 0.5);
|
|
141
|
+
console.log('Rolls for 50% chance:', rollsFor50);
|
|
142
|
+
// ~231
|
|
143
|
+
|
|
144
|
+
console.log('Rate-up items:', engine.getRateUpItems().join(', '));
|
|
145
|
+
// God-Tier Rat
|
|
146
|
+
|
|
147
|
+
console.log('All items:', engine.getAllItems().join(', '));
|
|
148
|
+
// God-Tier Rat, Dumpster King, Sleepy Chef, Unknown Student
|
|
107
149
|
```
|
|
108
150
|
|
|
109
|
-
**UMD (Browser)**
|
|
151
|
+
**UMD (Browser, Weighted Mode)**
|
|
110
152
|
```html
|
|
111
153
|
<script src="https://unpkg.com/@allemandi/gacha-engine"></script>
|
|
112
154
|
<script>
|
|
113
|
-
// Access the GachaEngine class
|
|
114
155
|
const { GachaEngine } = window.AllemandiGachaEngine;
|
|
115
|
-
|
|
156
|
+
|
|
116
157
|
const engine = new GachaEngine({
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
158
|
+
mode: 'weighted',
|
|
159
|
+
rarityRates: {
|
|
160
|
+
SSR: 0.02,
|
|
161
|
+
SR: 0.08,
|
|
162
|
+
R: 0.90
|
|
121
163
|
},
|
|
122
164
|
pools: [
|
|
123
165
|
{
|
|
124
166
|
rarity: 'SSR',
|
|
125
167
|
items: [
|
|
126
|
-
{ name: '
|
|
127
|
-
{ name: '
|
|
168
|
+
{ name: 'Trash Wizard', weight: 1.0 },
|
|
169
|
+
{ name: 'Park Master', weight: 1.0, rateUp: true }
|
|
128
170
|
]
|
|
129
171
|
},
|
|
130
172
|
{
|
|
131
173
|
rarity: 'SR',
|
|
132
174
|
items: [
|
|
133
|
-
{ name: 'Street Sweeper', weight:
|
|
175
|
+
{ name: 'Street Sweeper', weight: 2.0 },
|
|
176
|
+
{ name: 'Bench Philosopher', weight: 1.0 }
|
|
134
177
|
]
|
|
135
178
|
},
|
|
136
179
|
{
|
|
137
180
|
rarity: 'R',
|
|
138
181
|
items: [
|
|
139
|
-
{ name: '
|
|
182
|
+
{ name: 'Bus Stop Ghost', weight: 5.0 }
|
|
140
183
|
]
|
|
141
184
|
}
|
|
142
185
|
]
|
|
143
186
|
});
|
|
144
187
|
|
|
145
|
-
|
|
146
|
-
|
|
188
|
+
const rate = engine.getItemDropRate('Park Master');
|
|
189
|
+
const rolls = engine.getRollsForTargetProbability('Park Master', 0.75);
|
|
190
|
+
const cumulative = engine.getCumulativeProbabilityForItem('Park Master', 200);
|
|
191
|
+
|
|
192
|
+
console.log('1x Roll:', engine.roll());
|
|
193
|
+
console.log('Drop rate for Park Master:', (rate * 100).toFixed(2) + '%');
|
|
194
|
+
// 1.0 / 2.0 * 0.02 = 0.01 → 1.00%
|
|
195
|
+
|
|
196
|
+
console.log('Cumulative 200 rolls:', (cumulative * 100).toFixed(1) + '%');
|
|
197
|
+
// ~86.6%
|
|
198
|
+
|
|
199
|
+
console.log('Rolls for 75% chance:', rolls);
|
|
200
|
+
// ~138
|
|
201
|
+
|
|
202
|
+
console.log('Rate-up items:', engine.getRateUpItems().join(', '));
|
|
203
|
+
// Park Master
|
|
204
|
+
|
|
205
|
+
console.log('All items:', engine.getAllItemDropRates().map(i => i.name));
|
|
206
|
+
// ["Trash Wizard", "Park Master", "Street Sweeper", "Bench Philosopher", "Bus Stop Ghost"]
|
|
147
207
|
</script>
|
|
148
208
|
```
|
|
149
209
|
|
|
@@ -154,14 +214,38 @@ console.log('Single roll:', engine.roll());
|
|
|
154
214
|
|
|
155
215
|
Creates a new GachaEngine instance with validation.
|
|
156
216
|
|
|
157
|
-
**Config
|
|
158
|
-
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
217
|
+
**Config Options:**
|
|
218
|
+
|
|
219
|
+
- Weighted Mode
|
|
220
|
+
```ts
|
|
221
|
+
{
|
|
222
|
+
mode: 'weighted'; // (default)
|
|
223
|
+
rarityRates: Record<string, number>; // Required: must sum to 1.0
|
|
224
|
+
pools: Array<{
|
|
225
|
+
rarity: string; // Must match a key in `rarityRates`
|
|
226
|
+
items: Array<{
|
|
227
|
+
name: string;
|
|
228
|
+
weight: number;
|
|
229
|
+
rateUp?: boolean;
|
|
230
|
+
}>
|
|
231
|
+
}>
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
- Flat Rate Mode
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
{
|
|
238
|
+
mode: 'flatRate';
|
|
239
|
+
pools: Array<{
|
|
240
|
+
rarity: string; // Used only for categorization
|
|
241
|
+
items: Array<{
|
|
242
|
+
name: string;
|
|
243
|
+
weight: number; // Interpreted as direct probability (must sum to 1.0 across all items)
|
|
244
|
+
rateUp?: boolean;
|
|
245
|
+
}>
|
|
246
|
+
}>
|
|
247
|
+
}
|
|
248
|
+
```
|
|
165
249
|
|
|
166
250
|
### Methods
|
|
167
251
|
|
|
@@ -173,26 +257,36 @@ Creates a new GachaEngine instance with validation.
|
|
|
173
257
|
|
|
174
258
|
#### Analysis
|
|
175
259
|
`getItemDropRate(name: string): number`
|
|
176
|
-
- Returns the effective drop rate
|
|
177
|
-
-
|
|
260
|
+
- Returns the effective drop rate for a specific item
|
|
261
|
+
- In weighted mode:
|
|
262
|
+
- Computed as `dropRate = (item.weight / totalPoolWeight) × rarityBaseRate`
|
|
263
|
+
- In flat rate mode:
|
|
264
|
+
- `Returns the item's defined probability.
|
|
265
|
+
- Throws if the item does not exist.
|
|
178
266
|
|
|
179
267
|
`getRarityProbability(rarity: string): number`
|
|
180
|
-
- Returns the base probability for a rarity tier
|
|
268
|
+
- Returns the base probability for a given rarity tier
|
|
269
|
+
- Only in "weighted" mode.
|
|
270
|
+
- Throws in flatRate mode.
|
|
181
271
|
|
|
182
272
|
`getCumulativeProbabilityForItem(name: string, rolls: number): number`
|
|
183
273
|
- Calculates probability of getting the item at least once in N rolls
|
|
184
274
|
- Uses formula: `1 - (1 - dropRate)^rolls`
|
|
185
275
|
|
|
186
276
|
`getRollsForTargetProbability(name: string, targetProbability: number): number`
|
|
187
|
-
-
|
|
277
|
+
- Calculates the minimum number of rolls needed to reach a specific probability of pulling a given item.
|
|
188
278
|
- Returns `Infinity` if item has zero drop rate
|
|
279
|
+
- Returns 1 if target probability ≥ 1.0
|
|
189
280
|
|
|
190
281
|
#### Utility
|
|
191
282
|
`getRateUpItems(): string[]`
|
|
192
283
|
- Returns names of all items marked with `rateUp: true`
|
|
193
284
|
|
|
194
285
|
`getAllItemDropRates(): Array<{name: string, dropRate: number, rarity: string}>`
|
|
195
|
-
- Returns
|
|
286
|
+
- Returns a list of all items with:
|
|
287
|
+
- name: Item name
|
|
288
|
+
- dropRate: Calculated drop probability
|
|
289
|
+
- rarity: Associated rarity (or "flatRate" in flat mode)
|
|
196
290
|
|
|
197
291
|
## 🧪 Tests
|
|
198
292
|
|
package/dist/gacha-engine.d.ts
CHANGED
|
@@ -2,25 +2,17 @@ import { GachaEngineConfig } from './types';
|
|
|
2
2
|
export declare class GachaEngine {
|
|
3
3
|
private static readonly SCALE;
|
|
4
4
|
private static readonly MAX_SAFE_SCALE;
|
|
5
|
+
private mode;
|
|
5
6
|
private pools;
|
|
6
7
|
private rarityRatesScaled;
|
|
8
|
+
private flatRateMap;
|
|
7
9
|
private dropRateCacheScaled;
|
|
8
|
-
constructor(
|
|
9
|
-
/**
|
|
10
|
-
* Convert floating point rates to scaled integers
|
|
11
|
-
*/
|
|
10
|
+
constructor(config: GachaEngineConfig);
|
|
12
11
|
private scaleRarityRates;
|
|
13
|
-
/**
|
|
14
|
-
* Convert probability to scaled integer
|
|
15
|
-
*/
|
|
16
12
|
private toScaled;
|
|
17
|
-
/**
|
|
18
|
-
* Convert scaled integer back to probability
|
|
19
|
-
*/
|
|
20
13
|
private fromScaled;
|
|
21
14
|
private validateConfig;
|
|
22
15
|
getItemDropRate(name: string): number;
|
|
23
|
-
getRarityProbability(rarity: string): number;
|
|
24
16
|
getCumulativeProbabilityForItem(name: string, rolls: number): number;
|
|
25
17
|
getRollsForTargetProbability(name: string, targetProbability: number): number;
|
|
26
18
|
getRateUpItems(): string[];
|
|
@@ -29,16 +21,9 @@ export declare class GachaEngine {
|
|
|
29
21
|
dropRate: number;
|
|
30
22
|
rarity: string;
|
|
31
23
|
}[];
|
|
32
|
-
/**
|
|
33
|
-
* Get scaled drop rate for internal calculations
|
|
34
|
-
*/
|
|
35
|
-
private getItemDropRateScaled;
|
|
36
24
|
roll(count?: number): string[];
|
|
37
25
|
private selectRarity;
|
|
38
26
|
private selectItemFromPool;
|
|
39
|
-
/**
|
|
40
|
-
* Debug method to inspect scaled values
|
|
41
|
-
*/
|
|
42
27
|
getDebugInfo(): {
|
|
43
28
|
scale: number;
|
|
44
29
|
rarityRatesScaled: Record<string, number>;
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function t(t,
|
|
1
|
+
function t(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,a=Array(e);r<e;r++)a[r]=t[r];return a}function e(e,r){var a="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(a)return(a=a.call(e)).next.bind(a);if(Array.isArray(e)||(a=function(e,r){if(e){if("string"==typeof e)return t(e,r);var a={}.toString.call(e).slice(8,-1);return"Object"===a&&e.constructor&&(a=e.constructor.name),"Map"===a||"Set"===a?Array.from(e):"Arguments"===a||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(a)?t(e,r):void 0}}(e))||r&&e&&"number"==typeof e.length){a&&(e=a);var n=0;return function(){return n>=e.length?{done:!0}:{done:!1,value:e[n++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function r(){return r=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var r=arguments[e];for(var a in r)({}).hasOwnProperty.call(r,a)&&(t[a]=r[a])}return t},r.apply(null,arguments)}var a,n=/*#__PURE__*/function(){function t(t){if(this.mode=void 0,this.pools=[],this.rarityRatesScaled={},this.flatRateMap=new Map,this.dropRateCacheScaled=new Map,this.mode=t.mode,"weighted"===t.mode){var r=t;this.pools=r.pools,this.rarityRatesScaled=this.scaleRarityRates(r.rarityRates),this.validateConfig(r.rarityRates)}else{if("flatRate"!==t.mode)throw new Error("Unknown gacha mode: "+this.mode);for(var a,n=e(t.pools);!(a=n()).done;)for(var i,o=e(a.value.items);!(i=o()).done;){var s=i.value;if(s.weight<0)throw new Error('FlatRate item "'+s.name+'" must have non-negative weight');this.flatRateMap.set(s.name,s.weight)}var l=Array.from(this.flatRateMap.values()).reduce(function(t,e){return t+e},0);if(Math.abs(l-1)>1e-6)throw new Error("FlatRate item rates must sum to 1.0, but got "+l)}}var a=t.prototype;return a.scaleRarityRates=function(t){for(var e={},r=0,a=Object.entries(t);r<a.length;r++){var n=a[r],i=n[0],o=n[1];if(o<0||o>1)throw new Error('Rarity rate for "'+i+'" must be between 0 and 1, got '+o);e[i]=this.toScaled(o)}return e},a.toScaled=function(e){if(e>t.MAX_SAFE_SCALE/t.SCALE)throw new Error("Probability "+e+" too large for safe integer arithmetic");return Math.round(e*t.SCALE)},a.fromScaled=function(e){return e/t.SCALE},a.validateConfig=function(t){var r=new Set(Object.keys(this.rarityRatesScaled)),a=new Set(this.pools.map(function(t){return t.rarity})),n=Array.from(a).filter(function(t){return!r.has(t)});if(n.length>0)throw new Error("Missing rarity rates for: "+n.join(", "));var i=Object.values(t).reduce(function(t,e){return t+e},0);if(Math.abs(i-1)>1e-10)throw new Error("Rarity rates must sum to 1.0, got "+i);for(var o,s=e(this.pools);!(o=s()).done;){var l=o.value;if(0===l.items.length)throw new Error('Rarity "'+l.rarity+'" has no items');if(l.items.reduce(function(t,e){return t+e.weight},0)<=0)throw new Error('Rarity "'+l.rarity+'" has zero total weight');for(var u,h=e(l.items);!(u=h()).done;){var f=u.value;if(f.weight<0)throw new Error('Item "'+f.name+'" weight must be non-negative, got '+f.weight)}if(!l.items.some(function(t){return t.weight>0}))throw new Error('Rarity "'+l.rarity+'" must have at least one item with positive weight')}},a.getItemDropRate=function(r){if("flatRate"===this.mode)return this.flatRateMap.get(r)||0;if(this.dropRateCacheScaled.has(r))return this.fromScaled(this.dropRateCacheScaled.get(r));for(var a,n=e(this.pools);!(a=n()).done;){var i=a.value,o=i.items.find(function(t){return t.name===r});if(o){if(0===o.weight)return this.dropRateCacheScaled.set(r,0),0;var s=i.items.reduce(function(t,e){return t+e.weight},0),l=this.rarityRatesScaled[i.rarity],u=this.toScaled(o.weight),h=this.toScaled(s),f=Math.round(u*l/t.SCALE),c=Math.round(f*t.SCALE/h);return this.dropRateCacheScaled.set(r,c),this.fromScaled(c)}}throw new Error('Item "'+r+'" not found')},a.getCumulativeProbabilityForItem=function(t,e){var r=this.getItemDropRate(t);return 0===r?0:r>=1?1:1-Math.pow(1-r,e)},a.getRollsForTargetProbability=function(t,e){if(e<=0)return 0;if(e>=1)return 1;var r=this.getItemDropRate(t);return r<=0?Infinity:Math.ceil(Math.log(1-e)/Math.log(1-r))},a.getRateUpItems=function(){return"weighted"===this.mode||this.pools.length>0?this.pools.flatMap(function(t){return t.items.filter(function(t){return t.rateUp}).map(function(t){return t.name})}):[]},a.getAllItemDropRates=function(){var t=this;return"flatRate"===this.mode?Array.from(this.flatRateMap.entries()).map(function(t){return{name:t[0],dropRate:t[1],rarity:"flatRate"}}):this.pools.flatMap(function(e){return e.items.map(function(r){return{name:r.name,dropRate:t.getItemDropRate(r.name),rarity:e.rarity}})})},a.roll=function(t){var r=this;void 0===t&&(t=1);for(var a=[],n=function(){if("flatRate"===r.mode)for(var t,n=Math.random(),i=0,o=e(r.flatRateMap.entries());!(t=o()).done;){var s=t.value;if(n<(i+=s[1])){a.push(s[0]);break}}else{var l=r.selectRarity(),u=r.pools.find(function(t){return t.rarity===l}),h=r.selectItemFromPool(u);a.push(h.name)}},i=0;i<t;i++)n();return a},a.selectRarity=function(){for(var e=Math.floor(Math.random()*t.SCALE),r=0,a=0,n=Object.entries(this.rarityRatesScaled);a<n.length;a++){var i=n[a];if(e<(r+=i[1]))return i[0]}return Object.keys(this.rarityRatesScaled)[0]},a.selectItemFromPool=function(t){for(var a,n=this,i=t.items.filter(function(t){return t.weight>0}),o=i.map(function(t){return r({},t,{scaledWeight:n.toScaled(t.weight)})}),s=o.reduce(function(t,e){return t+e.scaledWeight},0),l=Math.floor(Math.random()*s),u=0,h=e(o);!(a=h()).done;){var f=a.value;if(l<(u+=f.scaledWeight))return{name:f.name,weight:f.weight}}return i[0]},a.getDebugInfo=function(){for(var e={},a=0,n=Object.entries(this.rarityRatesScaled);a<n.length;a++){var i=n[a];e[i[0]]=this.fromScaled(i[1])}return{scale:t.SCALE,rarityRatesScaled:r({},this.rarityRatesScaled),rarityRatesFloat:e}},t}();a=n,n.SCALE=1e6,n.MAX_SAFE_SCALE=Math.floor(Number.MAX_SAFE_INTEGER/a.SCALE),exports.GachaEngine=n;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../src/gacha-engine.ts"],"sourcesContent":["import { RarityInput, GachaEngineConfig } from './types';\n\nexport class GachaEngine {\n // Scale factor for fixed-point arithmetic (1,000,000 = 6 decimal places)\n private static readonly SCALE = 1000000;\n private static readonly MAX_SAFE_SCALE = Math.floor(Number.MAX_SAFE_INTEGER / this.SCALE);\n\n private pools: RarityInput[];\n private rarityRatesScaled: Record<string, number>; // Scaled to integers\n private dropRateCacheScaled = new Map<string, number>(); // Cache scaled rates\n\n constructor({ rarityRates, pools }: GachaEngineConfig) {\n this.pools = pools;\n this.rarityRatesScaled = this.scaleRarityRates(rarityRates);\n this.validateConfig(rarityRates);\n }\n\n /**\n * Convert floating point rates to scaled integers\n */\n private scaleRarityRates(rarityRates: Record<string, number>): Record<string, number> {\n const scaled: Record<string, number> = {};\n for (const [rarity, rate] of Object.entries(rarityRates)) {\n if (rate < 0 || rate > 1) {\n throw new Error(`Rarity rate for \"${rarity}\" must be between 0 and 1, got ${rate}`);\n }\n scaled[rarity] = this.toScaled(rate);\n }\n return scaled;\n }\n\n /**\n * Convert probability to scaled integer\n */\n private toScaled(probability: number): number {\n if (probability > GachaEngine.MAX_SAFE_SCALE / GachaEngine.SCALE) {\n throw new Error(`Probability ${probability} too large for safe integer arithmetic`);\n }\n return Math.round(probability * GachaEngine.SCALE);\n }\n\n /**\n * Convert scaled integer back to probability\n */\n private fromScaled(scaledInt: number): number {\n return scaledInt / GachaEngine.SCALE;\n }\n\n private validateConfig(originalRates: Record<string, number>): void {\n const configuredRarities = new Set(Object.keys(this.rarityRatesScaled));\n const usedRarities = new Set(this.pools.map(p => p.rarity));\n const missingArray = Array.from(usedRarities).filter(r => !configuredRarities.has(r));\n \n if (missingArray.length > 0) {\n throw new Error(`Missing rarity rates for: ${missingArray.join(', ')}`);\n }\n\n // Validate that rates sum to exactly 1.0 (within floating point precision)\n const totalRate = Object.values(originalRates).reduce((sum, rate) => sum + rate, 0);\n const totalScaled = Object.values(this.rarityRatesScaled).reduce((sum, rate) => sum + rate, 0);\n \n if (Math.abs(totalRate - 1.0) > 1e-10) {\n throw new Error(`Rarity rates must sum to 1.0, got ${totalRate}`);\n }\n\n // Ensure scaled rates sum to SCALE (accounting for rounding)\n if (Math.abs(totalScaled - GachaEngine.SCALE) > Object.keys(this.rarityRatesScaled).length) {\n console.warn(`Scaled rates sum to ${totalScaled}, expected ${GachaEngine.SCALE}. This is likely due to rounding.`);\n }\n\n for (const pool of this.pools) {\n if (pool.items.length === 0) {\n throw new Error(`Rarity \"${pool.rarity}\" has no items`);\n }\n \n const totalWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n if (totalWeight <= 0) {\n throw new Error(`Rarity \"${pool.rarity}\" has zero total weight`);\n }\n\n // Validate that all weights are non-negative\n for (const item of pool.items) {\n if (item.weight < 0) {\n throw new Error(`Item \"${item.name}\" weight must be non-negative, got ${item.weight}`);\n }\n }\n\n // Ensure at least one item has positive weight\n const hasPositiveWeight = pool.items.some(item => item.weight > 0);\n if (!hasPositiveWeight) {\n throw new Error(`Rarity \"${pool.rarity}\" must have at least one item with positive weight`);\n }\n }\n }\n\n getItemDropRate(name: string): number {\n if (this.dropRateCacheScaled.has(name)) {\n return this.fromScaled(this.dropRateCacheScaled.get(name)!);\n }\n\n for (const pool of this.pools) {\n const item = pool.items.find(i => i.name === name);\n if (item) {\n // Handle zero weight items (never drop)\n if (item.weight === 0) {\n this.dropRateCacheScaled.set(name, 0);\n return 0;\n }\n\n const totalPoolWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n const baseRarityRateScaled = this.rarityRatesScaled[pool.rarity];\n \n // Convert weights to scaled integers for perfect precision\n const itemWeightScaled = this.toScaled(item.weight);\n const totalWeightScaled = this.toScaled(totalPoolWeight);\n \n // Scaled arithmetic: (itemWeight * baseRate) / totalWeight\n const numeratorScaled = Math.round((itemWeightScaled * baseRarityRateScaled) / GachaEngine.SCALE);\n const rateScaled = Math.round((numeratorScaled * GachaEngine.SCALE) / totalWeightScaled);\n \n this.dropRateCacheScaled.set(name, rateScaled);\n return this.fromScaled(rateScaled);\n }\n }\n throw new Error(`Item \"${name}\" not found`);\n }\n\n getRarityProbability(rarity: string): number {\n if (!this.rarityRatesScaled[rarity]) {\n throw new Error(`Rarity \"${rarity}\" not found`);\n }\n return this.fromScaled(this.rarityRatesScaled[rarity]);\n }\n\n getCumulativeProbabilityForItem(name: string, rolls: number): number {\n const rateScaled = this.getItemDropRateScaled(name);\n \n if (rateScaled === 0) return 0;\n if (rateScaled >= GachaEngine.SCALE) return 1;\n \n // Calculate (1 - rate)^rolls using scaled arithmetic\n const failRateScaled = GachaEngine.SCALE - rateScaled;\n const failRate = this.fromScaled(failRateScaled);\n \n // For large rolls, we need to be careful with precision\n const cumulativeFailProbability = Math.pow(failRate, rolls);\n const cumulativeProbability = 1 - cumulativeFailProbability;\n \n return Math.min(1, Math.max(0, cumulativeProbability));\n }\n\n getRollsForTargetProbability(name: string, targetProbability: number): number {\n if (targetProbability <= 0) return 0;\n if (targetProbability >= 1) return 1;\n \n const rate = this.getItemDropRate(name);\n if (rate <= 0) return Infinity;\n if (rate >= 1) return 1;\n \n return Math.ceil(Math.log(1 - targetProbability) / Math.log(1 - rate));\n }\n\n getRateUpItems(): string[] {\n return this.pools.flatMap(p =>\n p.items.filter(i => i.rateUp).map(i => i.name)\n );\n }\n\n getAllItemDropRates(): { name: string; dropRate: number; rarity: string }[] {\n return this.pools.flatMap(p =>\n p.items.map(i => ({\n name: i.name,\n dropRate: this.getItemDropRate(i.name),\n rarity: p.rarity\n }))\n );\n }\n\n /**\n * Get scaled drop rate for internal calculations\n */\n private getItemDropRateScaled(name: string): number {\n if (this.dropRateCacheScaled.has(name)) {\n return this.dropRateCacheScaled.get(name)!;\n }\n \n // Trigger calculation and caching\n this.getItemDropRate(name);\n return this.dropRateCacheScaled.get(name)!;\n }\n\n roll(count: number = 1): string[] {\n const results: string[] = [];\n for (let i = 0; i < count; i++) {\n const rarity = this.selectRarity();\n const pool = this.pools.find(p => p.rarity === rarity)!;\n const item = this.selectItemFromPool(pool);\n results.push(item.name);\n }\n return results;\n }\n\n private selectRarity(): string {\n const rand = Math.floor(Math.random() * GachaEngine.SCALE);\n let cumulativeScaled = 0;\n \n for (const [rarity, rateScaled] of Object.entries(this.rarityRatesScaled)) {\n cumulativeScaled += rateScaled;\n if (rand < cumulativeScaled) return rarity;\n }\n \n // Fallback (should never happen with proper validation)\n return Object.keys(this.rarityRatesScaled)[0];\n }\n\n private selectItemFromPool(pool: RarityInput): { name: string; weight: number } {\n // Filter out zero-weight items (they can never be selected)\n const selectableItems = pool.items.filter(item => item.weight > 0);\n \n if (selectableItems.length === 0) {\n throw new Error(`No selectable items in pool for rarity \"${pool.rarity}\"`);\n }\n\n // Convert all weights to scaled integers for perfect precision\n const scaledItems = selectableItems.map(item => ({\n ...item,\n scaledWeight: this.toScaled(item.weight)\n }));\n \n const totalScaledWeight = scaledItems.reduce((sum, item) => sum + item.scaledWeight, 0);\n const rand = Math.floor(Math.random() * totalScaledWeight);\n let cumulative = 0;\n \n for (const item of scaledItems) {\n cumulative += item.scaledWeight;\n if (rand < cumulative) {\n return { name: item.name, weight: item.weight };\n }\n }\n \n // Fallback (should never happen)\n return selectableItems[0];\n }\n\n /**\n * Debug method to inspect scaled values\n */\n getDebugInfo(): {\n scale: number;\n rarityRatesScaled: Record<string, number>;\n rarityRatesFloat: Record<string, number>;\n } {\n const rarityRatesFloat: Record<string, number> = {};\n for (const [rarity, scaledRate] of Object.entries(this.rarityRatesScaled)) {\n rarityRatesFloat[rarity] = this.fromScaled(scaledRate);\n }\n \n return {\n scale: GachaEngine.SCALE,\n rarityRatesScaled: { ...this.rarityRatesScaled },\n rarityRatesFloat\n };\n }\n}"],"names":["GachaEngine","_ref","rarityRates","pools","rarityRatesScaled","dropRateCacheScaled","Map","this","scaleRarityRates","validateConfig","_proto","prototype","scaled","_i","_Object$entries","Object","entries","length","_Object$entries$_i","rarity","rate","Error","toScaled","probability","MAX_SAFE_SCALE","SCALE","Math","round","fromScaled","scaledInt","originalRates","configuredRarities","Set","keys","usedRarities","map","p","missingArray","Array","from","filter","r","has","join","totalRate","values","reduce","sum","totalScaled","abs","console","warn","_iterator","_step","_createForOfIteratorHelperLoose","done","pool","value","items","i","weight","_step2","_iterator2","item","name","hasPositiveWeight","some","getItemDropRate","get","_step3","_iterator3","find","set","totalPoolWeight","baseRarityRateScaled","itemWeightScaled","totalWeightScaled","numeratorScaled","rateScaled","getRarityProbability","getCumulativeProbabilityForItem","rolls","getItemDropRateScaled","failRate","cumulativeFailProbability","pow","min","max","getRollsForTargetProbability","targetProbability","Infinity","ceil","log","getRateUpItems","flatMap","rateUp","getAllItemDropRates","_this","dropRate","roll","count","_this2","results","_loop","selectRarity","selectItemFromPool","push","rand","floor","random","cumulativeScaled","_i2","_Object$entries2","_Object$entries2$_i","_this3","selectableItems","_step4","scaledItems","_extends","scaledWeight","totalScaledWeight","cumulative","_iterator4","getDebugInfo","rarityRatesFloat","_i3","_Object$entries3","_Object$entries3$_i","scale","Number","MAX_SAFE_INTEGER","_GachaEngine"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/gacha-engine.ts"],"sourcesContent":["import {\n RarityInput,\n GachaEngineConfig,\n WeightedGachaEngineConfig,\n FlatRateGachaEngineConfig,\n} from './types';\n\nexport class GachaEngine {\n private static readonly SCALE = 1_000_000;\n private static readonly MAX_SAFE_SCALE = Math.floor(Number.MAX_SAFE_INTEGER / GachaEngine.SCALE);\n\n private mode: 'weighted' | 'flatRate';\n private pools: RarityInput[] = [];\n private rarityRatesScaled: Record<string, number> = {};\n private flatRateMap: Map<string, number> = new Map();\n private dropRateCacheScaled = new Map<string, number>();\n\n constructor(config: GachaEngineConfig) {\n this.mode = config.mode;\n\n if (config.mode === 'weighted') {\n const weightedConfig = config as WeightedGachaEngineConfig;\n this.pools = weightedConfig.pools;\n this.rarityRatesScaled = this.scaleRarityRates(weightedConfig.rarityRates);\n this.validateConfig(weightedConfig.rarityRates);\n } else if (config.mode === 'flatRate') {\n const flatConfig = config as FlatRateGachaEngineConfig;\n for (const pool of flatConfig.pools) {\n for (const item of pool.items) {\n if (item.weight < 0) {\n throw new Error(`FlatRate item \"${item.name}\" must have non-negative weight`);\n }\n this.flatRateMap.set(item.name, item.weight); // Here, interpreted as direct probability\n }\n }\n const total = Array.from(this.flatRateMap.values()).reduce((sum, v) => sum + v, 0);\n if (Math.abs(total - 1.0) > 1e-6) {\n throw new Error(`FlatRate item rates must sum to 1.0, but got ${total}`);\n }\n } else {\n throw new Error(`Unknown gacha mode: ${this.mode}`);\n }\n }\n\n private scaleRarityRates(rarityRates: Record<string, number>): Record<string, number> {\n const scaled: Record<string, number> = {};\n for (const [rarity, rate] of Object.entries(rarityRates)) {\n if (rate < 0 || rate > 1) {\n throw new Error(`Rarity rate for \"${rarity}\" must be between 0 and 1, got ${rate}`);\n }\n scaled[rarity] = this.toScaled(rate);\n }\n return scaled;\n }\n\n private toScaled(probability: number): number {\n if (probability > GachaEngine.MAX_SAFE_SCALE / GachaEngine.SCALE) {\n throw new Error(`Probability ${probability} too large for safe integer arithmetic`);\n }\n return Math.round(probability * GachaEngine.SCALE);\n }\n\n private fromScaled(scaledInt: number): number {\n return scaledInt / GachaEngine.SCALE;\n }\n\n private validateConfig(originalRates: Record<string, number>): void {\n const configuredRarities = new Set(Object.keys(this.rarityRatesScaled));\n const usedRarities = new Set(this.pools.map(p => p.rarity));\n const missing = Array.from(usedRarities).filter(r => !configuredRarities.has(r));\n\n if (missing.length > 0) {\n throw new Error(`Missing rarity rates for: ${missing.join(', ')}`);\n }\n\n const totalRate = Object.values(originalRates).reduce((sum, rate) => sum + rate, 0);\n if (Math.abs(totalRate - 1.0) > 1e-10) {\n throw new Error(`Rarity rates must sum to 1.0, got ${totalRate}`);\n }\n\n for (const pool of this.pools) {\n if (pool.items.length === 0) {\n throw new Error(`Rarity \"${pool.rarity}\" has no items`);\n }\n\n const totalWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n if (totalWeight <= 0) {\n throw new Error(`Rarity \"${pool.rarity}\" has zero total weight`);\n }\n\n for (const item of pool.items) {\n if (item.weight < 0) {\n throw new Error(`Item \"${item.name}\" weight must be non-negative, got ${item.weight}`);\n }\n }\n\n if (!pool.items.some(i => i.weight > 0)) {\n throw new Error(`Rarity \"${pool.rarity}\" must have at least one item with positive weight`);\n }\n }\n }\n\n getItemDropRate(name: string): number {\n if (this.mode === 'flatRate') {\n return this.flatRateMap.get(name) || 0;\n }\n\n if (this.dropRateCacheScaled.has(name)) {\n return this.fromScaled(this.dropRateCacheScaled.get(name)!);\n }\n\n for (const pool of this.pools) {\n const item = pool.items.find(i => i.name === name);\n if (item) {\n if (item.weight === 0) {\n this.dropRateCacheScaled.set(name, 0);\n return 0;\n }\n\n const totalPoolWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n const baseRarityRateScaled = this.rarityRatesScaled[pool.rarity];\n const itemWeightScaled = this.toScaled(item.weight);\n const totalWeightScaled = this.toScaled(totalPoolWeight);\n const numeratorScaled = Math.round((itemWeightScaled * baseRarityRateScaled) / GachaEngine.SCALE);\n const rateScaled = Math.round((numeratorScaled * GachaEngine.SCALE) / totalWeightScaled);\n\n this.dropRateCacheScaled.set(name, rateScaled);\n return this.fromScaled(rateScaled);\n }\n }\n\n throw new Error(`Item \"${name}\" not found`);\n }\n\n getCumulativeProbabilityForItem(name: string, rolls: number): number {\n const rate = this.getItemDropRate(name);\n if (rate === 0) return 0;\n if (rate >= 1) return 1;\n\n const cumulativeFailProbability = Math.pow(1 - rate, rolls);\n return 1 - cumulativeFailProbability;\n }\n\n getRollsForTargetProbability(name: string, targetProbability: number): number {\n if (targetProbability <= 0) return 0;\n if (targetProbability >= 1) return 1;\n\n const rate = this.getItemDropRate(name);\n if (rate <= 0) return Infinity;\n return Math.ceil(Math.log(1 - targetProbability) / Math.log(1 - rate));\n }\n\n getRateUpItems(): string[] {\n if (this.mode === 'weighted') {\n return this.pools.flatMap(p => p.items.filter(i => i.rateUp).map(i => i.name));\n } else {\n if (this.pools.length > 0) {\n return this.pools.flatMap(p => p.items.filter(i => i.rateUp).map(i => i.name));\n }\n return [];\n }\n}\n\n\n getAllItemDropRates(): { name: string; dropRate: number; rarity: string }[] {\n if (this.mode === 'flatRate') {\n return Array.from(this.flatRateMap.entries()).map(([name, dropRate]) => ({\n name,\n dropRate,\n rarity: 'flatRate',\n }));\n }\n\n return this.pools.flatMap(p =>\n p.items.map(i => ({\n name: i.name,\n dropRate: this.getItemDropRate(i.name),\n rarity: p.rarity,\n }))\n );\n }\n\n roll(count: number = 1): string[] {\n const results: string[] = [];\n for (let i = 0; i < count; i++) {\n if (this.mode === 'flatRate') {\n const rand = Math.random();\n let cumulative = 0;\n for (const [name, rate] of this.flatRateMap.entries()) {\n cumulative += rate;\n if (rand < cumulative) {\n results.push(name);\n break;\n }\n }\n } else {\n const rarity = this.selectRarity();\n const pool = this.pools.find(p => p.rarity === rarity)!;\n const item = this.selectItemFromPool(pool);\n results.push(item.name);\n }\n }\n return results;\n }\n\n private selectRarity(): string {\n const rand = Math.floor(Math.random() * GachaEngine.SCALE);\n let cumulative = 0;\n\n for (const [rarity, scaledRate] of Object.entries(this.rarityRatesScaled)) {\n cumulative += scaledRate;\n if (rand < cumulative) return rarity;\n }\n\n return Object.keys(this.rarityRatesScaled)[0];\n }\n\n private selectItemFromPool(pool: RarityInput): { name: string; weight: number } {\n const items = pool.items.filter(i => i.weight > 0);\n const scaledItems = items.map(i => ({\n ...i,\n scaledWeight: this.toScaled(i.weight),\n }));\n\n const totalScaledWeight = scaledItems.reduce((sum, i) => sum + i.scaledWeight, 0);\n const rand = Math.floor(Math.random() * totalScaledWeight);\n let cumulative = 0;\n\n for (const item of scaledItems) {\n cumulative += item.scaledWeight;\n if (rand < cumulative) {\n return { name: item.name, weight: item.weight };\n }\n }\n\n return items[0]; // Fallback\n }\n\n getDebugInfo(): {\n scale: number;\n rarityRatesScaled: Record<string, number>;\n rarityRatesFloat: Record<string, number>;\n } {\n const rarityRatesFloat: Record<string, number> = {};\n for (const [rarity, scaledRate] of Object.entries(this.rarityRatesScaled)) {\n rarityRatesFloat[rarity] = this.fromScaled(scaledRate);\n }\n\n return {\n scale: GachaEngine.SCALE,\n rarityRatesScaled: { ...this.rarityRatesScaled },\n rarityRatesFloat,\n };\n }\n}\n"],"names":["GachaEngine","config","this","mode","pools","rarityRatesScaled","flatRateMap","Map","dropRateCacheScaled","weightedConfig","scaleRarityRates","rarityRates","validateConfig","Error","_step","_iterator","_createForOfIteratorHelperLoose","done","_step2","_iterator2","value","items","item","weight","name","set","total","Array","from","values","reduce","sum","v","Math","abs","_proto","prototype","scaled","_i","_Object$entries","Object","entries","length","_Object$entries$_i","rarity","rate","toScaled","probability","MAX_SAFE_SCALE","SCALE","round","fromScaled","scaledInt","originalRates","configuredRarities","Set","keys","usedRarities","map","p","missing","filter","r","has","join","totalRate","_iterator3","_step3","pool","i","_iterator4","_step4","some","getItemDropRate","get","_iterator5","_step5","find","totalPoolWeight","baseRarityRateScaled","itemWeightScaled","totalWeightScaled","numeratorScaled","rateScaled","getCumulativeProbabilityForItem","rolls","pow","getRollsForTargetProbability","targetProbability","Infinity","ceil","log","getRateUpItems","flatMap","rateUp","getAllItemDropRates","_this","_ref","dropRate","roll","count","_this2","results","_loop","_step6","rand","random","cumulative","_iterator6","_step6$value","push","selectRarity","selectItemFromPool","floor","_i2","_Object$entries2","_Object$entries2$_i","_this3","_step7","scaledItems","_extends","scaledWeight","totalScaledWeight","_iterator7","getDebugInfo","rarityRatesFloat","_i3","_Object$entries3","_Object$entries3$_i","scale","Number","MAX_SAFE_INTEGER"],"mappings":"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"}
|
package/dist/index.module.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function t(t,
|
|
1
|
+
function t(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,a=Array(e);r<e;r++)a[r]=t[r];return a}function e(e,r){var a="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(a)return(a=a.call(e)).next.bind(a);if(Array.isArray(e)||(a=function(e,r){if(e){if("string"==typeof e)return t(e,r);var a={}.toString.call(e).slice(8,-1);return"Object"===a&&e.constructor&&(a=e.constructor.name),"Map"===a||"Set"===a?Array.from(e):"Arguments"===a||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(a)?t(e,r):void 0}}(e))||r&&e&&"number"==typeof e.length){a&&(e=a);var n=0;return function(){return n>=e.length?{done:!0}:{done:!1,value:e[n++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function r(){return r=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var r=arguments[e];for(var a in r)({}).hasOwnProperty.call(r,a)&&(t[a]=r[a])}return t},r.apply(null,arguments)}var a,n=/*#__PURE__*/function(){function t(t){if(this.mode=void 0,this.pools=[],this.rarityRatesScaled={},this.flatRateMap=new Map,this.dropRateCacheScaled=new Map,this.mode=t.mode,"weighted"===t.mode){var r=t;this.pools=r.pools,this.rarityRatesScaled=this.scaleRarityRates(r.rarityRates),this.validateConfig(r.rarityRates)}else{if("flatRate"!==t.mode)throw new Error("Unknown gacha mode: "+this.mode);for(var a,n=e(t.pools);!(a=n()).done;)for(var i,o=e(a.value.items);!(i=o()).done;){var s=i.value;if(s.weight<0)throw new Error('FlatRate item "'+s.name+'" must have non-negative weight');this.flatRateMap.set(s.name,s.weight)}var l=Array.from(this.flatRateMap.values()).reduce(function(t,e){return t+e},0);if(Math.abs(l-1)>1e-6)throw new Error("FlatRate item rates must sum to 1.0, but got "+l)}}var a=t.prototype;return a.scaleRarityRates=function(t){for(var e={},r=0,a=Object.entries(t);r<a.length;r++){var n=a[r],i=n[0],o=n[1];if(o<0||o>1)throw new Error('Rarity rate for "'+i+'" must be between 0 and 1, got '+o);e[i]=this.toScaled(o)}return e},a.toScaled=function(e){if(e>t.MAX_SAFE_SCALE/t.SCALE)throw new Error("Probability "+e+" too large for safe integer arithmetic");return Math.round(e*t.SCALE)},a.fromScaled=function(e){return e/t.SCALE},a.validateConfig=function(t){var r=new Set(Object.keys(this.rarityRatesScaled)),a=new Set(this.pools.map(function(t){return t.rarity})),n=Array.from(a).filter(function(t){return!r.has(t)});if(n.length>0)throw new Error("Missing rarity rates for: "+n.join(", "));var i=Object.values(t).reduce(function(t,e){return t+e},0);if(Math.abs(i-1)>1e-10)throw new Error("Rarity rates must sum to 1.0, got "+i);for(var o,s=e(this.pools);!(o=s()).done;){var l=o.value;if(0===l.items.length)throw new Error('Rarity "'+l.rarity+'" has no items');if(l.items.reduce(function(t,e){return t+e.weight},0)<=0)throw new Error('Rarity "'+l.rarity+'" has zero total weight');for(var u,h=e(l.items);!(u=h()).done;){var f=u.value;if(f.weight<0)throw new Error('Item "'+f.name+'" weight must be non-negative, got '+f.weight)}if(!l.items.some(function(t){return t.weight>0}))throw new Error('Rarity "'+l.rarity+'" must have at least one item with positive weight')}},a.getItemDropRate=function(r){if("flatRate"===this.mode)return this.flatRateMap.get(r)||0;if(this.dropRateCacheScaled.has(r))return this.fromScaled(this.dropRateCacheScaled.get(r));for(var a,n=e(this.pools);!(a=n()).done;){var i=a.value,o=i.items.find(function(t){return t.name===r});if(o){if(0===o.weight)return this.dropRateCacheScaled.set(r,0),0;var s=i.items.reduce(function(t,e){return t+e.weight},0),l=this.rarityRatesScaled[i.rarity],u=this.toScaled(o.weight),h=this.toScaled(s),f=Math.round(u*l/t.SCALE),c=Math.round(f*t.SCALE/h);return this.dropRateCacheScaled.set(r,c),this.fromScaled(c)}}throw new Error('Item "'+r+'" not found')},a.getCumulativeProbabilityForItem=function(t,e){var r=this.getItemDropRate(t);return 0===r?0:r>=1?1:1-Math.pow(1-r,e)},a.getRollsForTargetProbability=function(t,e){if(e<=0)return 0;if(e>=1)return 1;var r=this.getItemDropRate(t);return r<=0?Infinity:Math.ceil(Math.log(1-e)/Math.log(1-r))},a.getRateUpItems=function(){return"weighted"===this.mode||this.pools.length>0?this.pools.flatMap(function(t){return t.items.filter(function(t){return t.rateUp}).map(function(t){return t.name})}):[]},a.getAllItemDropRates=function(){var t=this;return"flatRate"===this.mode?Array.from(this.flatRateMap.entries()).map(function(t){return{name:t[0],dropRate:t[1],rarity:"flatRate"}}):this.pools.flatMap(function(e){return e.items.map(function(r){return{name:r.name,dropRate:t.getItemDropRate(r.name),rarity:e.rarity}})})},a.roll=function(t){var r=this;void 0===t&&(t=1);for(var a=[],n=function(){if("flatRate"===r.mode)for(var t,n=Math.random(),i=0,o=e(r.flatRateMap.entries());!(t=o()).done;){var s=t.value;if(n<(i+=s[1])){a.push(s[0]);break}}else{var l=r.selectRarity(),u=r.pools.find(function(t){return t.rarity===l}),h=r.selectItemFromPool(u);a.push(h.name)}},i=0;i<t;i++)n();return a},a.selectRarity=function(){for(var e=Math.floor(Math.random()*t.SCALE),r=0,a=0,n=Object.entries(this.rarityRatesScaled);a<n.length;a++){var i=n[a];if(e<(r+=i[1]))return i[0]}return Object.keys(this.rarityRatesScaled)[0]},a.selectItemFromPool=function(t){for(var a,n=this,i=t.items.filter(function(t){return t.weight>0}),o=i.map(function(t){return r({},t,{scaledWeight:n.toScaled(t.weight)})}),s=o.reduce(function(t,e){return t+e.scaledWeight},0),l=Math.floor(Math.random()*s),u=0,h=e(o);!(a=h()).done;){var f=a.value;if(l<(u+=f.scaledWeight))return{name:f.name,weight:f.weight}}return i[0]},a.getDebugInfo=function(){for(var e={},a=0,n=Object.entries(this.rarityRatesScaled);a<n.length;a++){var i=n[a];e[i[0]]=this.fromScaled(i[1])}return{scale:t.SCALE,rarityRatesScaled:r({},this.rarityRatesScaled),rarityRatesFloat:e}},t}();a=n,n.SCALE=1e6,n.MAX_SAFE_SCALE=Math.floor(Number.MAX_SAFE_INTEGER/a.SCALE);export{n as GachaEngine};
|
|
2
2
|
//# sourceMappingURL=index.module.js.map
|
package/dist/index.module.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.module.js","sources":["../src/gacha-engine.ts"],"sourcesContent":["import { RarityInput, GachaEngineConfig } from './types';\n\nexport class GachaEngine {\n // Scale factor for fixed-point arithmetic (1,000,000 = 6 decimal places)\n private static readonly SCALE = 1000000;\n private static readonly MAX_SAFE_SCALE = Math.floor(Number.MAX_SAFE_INTEGER / this.SCALE);\n\n private pools: RarityInput[];\n private rarityRatesScaled: Record<string, number>; // Scaled to integers\n private dropRateCacheScaled = new Map<string, number>(); // Cache scaled rates\n\n constructor({ rarityRates, pools }: GachaEngineConfig) {\n this.pools = pools;\n this.rarityRatesScaled = this.scaleRarityRates(rarityRates);\n this.validateConfig(rarityRates);\n }\n\n /**\n * Convert floating point rates to scaled integers\n */\n private scaleRarityRates(rarityRates: Record<string, number>): Record<string, number> {\n const scaled: Record<string, number> = {};\n for (const [rarity, rate] of Object.entries(rarityRates)) {\n if (rate < 0 || rate > 1) {\n throw new Error(`Rarity rate for \"${rarity}\" must be between 0 and 1, got ${rate}`);\n }\n scaled[rarity] = this.toScaled(rate);\n }\n return scaled;\n }\n\n /**\n * Convert probability to scaled integer\n */\n private toScaled(probability: number): number {\n if (probability > GachaEngine.MAX_SAFE_SCALE / GachaEngine.SCALE) {\n throw new Error(`Probability ${probability} too large for safe integer arithmetic`);\n }\n return Math.round(probability * GachaEngine.SCALE);\n }\n\n /**\n * Convert scaled integer back to probability\n */\n private fromScaled(scaledInt: number): number {\n return scaledInt / GachaEngine.SCALE;\n }\n\n private validateConfig(originalRates: Record<string, number>): void {\n const configuredRarities = new Set(Object.keys(this.rarityRatesScaled));\n const usedRarities = new Set(this.pools.map(p => p.rarity));\n const missingArray = Array.from(usedRarities).filter(r => !configuredRarities.has(r));\n \n if (missingArray.length > 0) {\n throw new Error(`Missing rarity rates for: ${missingArray.join(', ')}`);\n }\n\n // Validate that rates sum to exactly 1.0 (within floating point precision)\n const totalRate = Object.values(originalRates).reduce((sum, rate) => sum + rate, 0);\n const totalScaled = Object.values(this.rarityRatesScaled).reduce((sum, rate) => sum + rate, 0);\n \n if (Math.abs(totalRate - 1.0) > 1e-10) {\n throw new Error(`Rarity rates must sum to 1.0, got ${totalRate}`);\n }\n\n // Ensure scaled rates sum to SCALE (accounting for rounding)\n if (Math.abs(totalScaled - GachaEngine.SCALE) > Object.keys(this.rarityRatesScaled).length) {\n console.warn(`Scaled rates sum to ${totalScaled}, expected ${GachaEngine.SCALE}. This is likely due to rounding.`);\n }\n\n for (const pool of this.pools) {\n if (pool.items.length === 0) {\n throw new Error(`Rarity \"${pool.rarity}\" has no items`);\n }\n \n const totalWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n if (totalWeight <= 0) {\n throw new Error(`Rarity \"${pool.rarity}\" has zero total weight`);\n }\n\n // Validate that all weights are non-negative\n for (const item of pool.items) {\n if (item.weight < 0) {\n throw new Error(`Item \"${item.name}\" weight must be non-negative, got ${item.weight}`);\n }\n }\n\n // Ensure at least one item has positive weight\n const hasPositiveWeight = pool.items.some(item => item.weight > 0);\n if (!hasPositiveWeight) {\n throw new Error(`Rarity \"${pool.rarity}\" must have at least one item with positive weight`);\n }\n }\n }\n\n getItemDropRate(name: string): number {\n if (this.dropRateCacheScaled.has(name)) {\n return this.fromScaled(this.dropRateCacheScaled.get(name)!);\n }\n\n for (const pool of this.pools) {\n const item = pool.items.find(i => i.name === name);\n if (item) {\n // Handle zero weight items (never drop)\n if (item.weight === 0) {\n this.dropRateCacheScaled.set(name, 0);\n return 0;\n }\n\n const totalPoolWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n const baseRarityRateScaled = this.rarityRatesScaled[pool.rarity];\n \n // Convert weights to scaled integers for perfect precision\n const itemWeightScaled = this.toScaled(item.weight);\n const totalWeightScaled = this.toScaled(totalPoolWeight);\n \n // Scaled arithmetic: (itemWeight * baseRate) / totalWeight\n const numeratorScaled = Math.round((itemWeightScaled * baseRarityRateScaled) / GachaEngine.SCALE);\n const rateScaled = Math.round((numeratorScaled * GachaEngine.SCALE) / totalWeightScaled);\n \n this.dropRateCacheScaled.set(name, rateScaled);\n return this.fromScaled(rateScaled);\n }\n }\n throw new Error(`Item \"${name}\" not found`);\n }\n\n getRarityProbability(rarity: string): number {\n if (!this.rarityRatesScaled[rarity]) {\n throw new Error(`Rarity \"${rarity}\" not found`);\n }\n return this.fromScaled(this.rarityRatesScaled[rarity]);\n }\n\n getCumulativeProbabilityForItem(name: string, rolls: number): number {\n const rateScaled = this.getItemDropRateScaled(name);\n \n if (rateScaled === 0) return 0;\n if (rateScaled >= GachaEngine.SCALE) return 1;\n \n // Calculate (1 - rate)^rolls using scaled arithmetic\n const failRateScaled = GachaEngine.SCALE - rateScaled;\n const failRate = this.fromScaled(failRateScaled);\n \n // For large rolls, we need to be careful with precision\n const cumulativeFailProbability = Math.pow(failRate, rolls);\n const cumulativeProbability = 1 - cumulativeFailProbability;\n \n return Math.min(1, Math.max(0, cumulativeProbability));\n }\n\n getRollsForTargetProbability(name: string, targetProbability: number): number {\n if (targetProbability <= 0) return 0;\n if (targetProbability >= 1) return 1;\n \n const rate = this.getItemDropRate(name);\n if (rate <= 0) return Infinity;\n if (rate >= 1) return 1;\n \n return Math.ceil(Math.log(1 - targetProbability) / Math.log(1 - rate));\n }\n\n getRateUpItems(): string[] {\n return this.pools.flatMap(p =>\n p.items.filter(i => i.rateUp).map(i => i.name)\n );\n }\n\n getAllItemDropRates(): { name: string; dropRate: number; rarity: string }[] {\n return this.pools.flatMap(p =>\n p.items.map(i => ({\n name: i.name,\n dropRate: this.getItemDropRate(i.name),\n rarity: p.rarity\n }))\n );\n }\n\n /**\n * Get scaled drop rate for internal calculations\n */\n private getItemDropRateScaled(name: string): number {\n if (this.dropRateCacheScaled.has(name)) {\n return this.dropRateCacheScaled.get(name)!;\n }\n \n // Trigger calculation and caching\n this.getItemDropRate(name);\n return this.dropRateCacheScaled.get(name)!;\n }\n\n roll(count: number = 1): string[] {\n const results: string[] = [];\n for (let i = 0; i < count; i++) {\n const rarity = this.selectRarity();\n const pool = this.pools.find(p => p.rarity === rarity)!;\n const item = this.selectItemFromPool(pool);\n results.push(item.name);\n }\n return results;\n }\n\n private selectRarity(): string {\n const rand = Math.floor(Math.random() * GachaEngine.SCALE);\n let cumulativeScaled = 0;\n \n for (const [rarity, rateScaled] of Object.entries(this.rarityRatesScaled)) {\n cumulativeScaled += rateScaled;\n if (rand < cumulativeScaled) return rarity;\n }\n \n // Fallback (should never happen with proper validation)\n return Object.keys(this.rarityRatesScaled)[0];\n }\n\n private selectItemFromPool(pool: RarityInput): { name: string; weight: number } {\n // Filter out zero-weight items (they can never be selected)\n const selectableItems = pool.items.filter(item => item.weight > 0);\n \n if (selectableItems.length === 0) {\n throw new Error(`No selectable items in pool for rarity \"${pool.rarity}\"`);\n }\n\n // Convert all weights to scaled integers for perfect precision\n const scaledItems = selectableItems.map(item => ({\n ...item,\n scaledWeight: this.toScaled(item.weight)\n }));\n \n const totalScaledWeight = scaledItems.reduce((sum, item) => sum + item.scaledWeight, 0);\n const rand = Math.floor(Math.random() * totalScaledWeight);\n let cumulative = 0;\n \n for (const item of scaledItems) {\n cumulative += item.scaledWeight;\n if (rand < cumulative) {\n return { name: item.name, weight: item.weight };\n }\n }\n \n // Fallback (should never happen)\n return selectableItems[0];\n }\n\n /**\n * Debug method to inspect scaled values\n */\n getDebugInfo(): {\n scale: number;\n rarityRatesScaled: Record<string, number>;\n rarityRatesFloat: Record<string, number>;\n } {\n const rarityRatesFloat: Record<string, number> = {};\n for (const [rarity, scaledRate] of Object.entries(this.rarityRatesScaled)) {\n rarityRatesFloat[rarity] = this.fromScaled(scaledRate);\n }\n \n return {\n scale: GachaEngine.SCALE,\n rarityRatesScaled: { ...this.rarityRatesScaled },\n rarityRatesFloat\n };\n }\n}"],"names":["GachaEngine","_ref","rarityRates","pools","rarityRatesScaled","dropRateCacheScaled","Map","this","scaleRarityRates","validateConfig","_proto","prototype","scaled","_i","_Object$entries","Object","entries","length","_Object$entries$_i","rarity","rate","Error","toScaled","probability","MAX_SAFE_SCALE","SCALE","Math","round","fromScaled","scaledInt","originalRates","configuredRarities","Set","keys","usedRarities","map","p","missingArray","Array","from","filter","r","has","join","totalRate","values","reduce","sum","totalScaled","abs","console","warn","_iterator","_step","_createForOfIteratorHelperLoose","done","pool","value","items","i","weight","_step2","_iterator2","item","name","hasPositiveWeight","some","getItemDropRate","get","_step3","_iterator3","find","set","totalPoolWeight","baseRarityRateScaled","itemWeightScaled","totalWeightScaled","numeratorScaled","rateScaled","getRarityProbability","getCumulativeProbabilityForItem","rolls","getItemDropRateScaled","failRate","cumulativeFailProbability","pow","min","max","getRollsForTargetProbability","targetProbability","Infinity","ceil","log","getRateUpItems","flatMap","rateUp","getAllItemDropRates","_this","dropRate","roll","count","_this2","results","_loop","selectRarity","selectItemFromPool","push","rand","floor","random","cumulativeScaled","_i2","_Object$entries2","_Object$entries2$_i","_this3","selectableItems","_step4","scaledItems","_extends","scaledWeight","totalScaledWeight","cumulative","_iterator4","getDebugInfo","rarityRatesFloat","_i3","_Object$entries3","_Object$entries3$_i","scale","Number","MAX_SAFE_INTEGER","_GachaEngine"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.module.js","sources":["../src/gacha-engine.ts"],"sourcesContent":["import {\n RarityInput,\n GachaEngineConfig,\n WeightedGachaEngineConfig,\n FlatRateGachaEngineConfig,\n} from './types';\n\nexport class GachaEngine {\n private static readonly SCALE = 1_000_000;\n private static readonly MAX_SAFE_SCALE = Math.floor(Number.MAX_SAFE_INTEGER / GachaEngine.SCALE);\n\n private mode: 'weighted' | 'flatRate';\n private pools: RarityInput[] = [];\n private rarityRatesScaled: Record<string, number> = {};\n private flatRateMap: Map<string, number> = new Map();\n private dropRateCacheScaled = new Map<string, number>();\n\n constructor(config: GachaEngineConfig) {\n this.mode = config.mode;\n\n if (config.mode === 'weighted') {\n const weightedConfig = config as WeightedGachaEngineConfig;\n this.pools = weightedConfig.pools;\n this.rarityRatesScaled = this.scaleRarityRates(weightedConfig.rarityRates);\n this.validateConfig(weightedConfig.rarityRates);\n } else if (config.mode === 'flatRate') {\n const flatConfig = config as FlatRateGachaEngineConfig;\n for (const pool of flatConfig.pools) {\n for (const item of pool.items) {\n if (item.weight < 0) {\n throw new Error(`FlatRate item \"${item.name}\" must have non-negative weight`);\n }\n this.flatRateMap.set(item.name, item.weight); // Here, interpreted as direct probability\n }\n }\n const total = Array.from(this.flatRateMap.values()).reduce((sum, v) => sum + v, 0);\n if (Math.abs(total - 1.0) > 1e-6) {\n throw new Error(`FlatRate item rates must sum to 1.0, but got ${total}`);\n }\n } else {\n throw new Error(`Unknown gacha mode: ${this.mode}`);\n }\n }\n\n private scaleRarityRates(rarityRates: Record<string, number>): Record<string, number> {\n const scaled: Record<string, number> = {};\n for (const [rarity, rate] of Object.entries(rarityRates)) {\n if (rate < 0 || rate > 1) {\n throw new Error(`Rarity rate for \"${rarity}\" must be between 0 and 1, got ${rate}`);\n }\n scaled[rarity] = this.toScaled(rate);\n }\n return scaled;\n }\n\n private toScaled(probability: number): number {\n if (probability > GachaEngine.MAX_SAFE_SCALE / GachaEngine.SCALE) {\n throw new Error(`Probability ${probability} too large for safe integer arithmetic`);\n }\n return Math.round(probability * GachaEngine.SCALE);\n }\n\n private fromScaled(scaledInt: number): number {\n return scaledInt / GachaEngine.SCALE;\n }\n\n private validateConfig(originalRates: Record<string, number>): void {\n const configuredRarities = new Set(Object.keys(this.rarityRatesScaled));\n const usedRarities = new Set(this.pools.map(p => p.rarity));\n const missing = Array.from(usedRarities).filter(r => !configuredRarities.has(r));\n\n if (missing.length > 0) {\n throw new Error(`Missing rarity rates for: ${missing.join(', ')}`);\n }\n\n const totalRate = Object.values(originalRates).reduce((sum, rate) => sum + rate, 0);\n if (Math.abs(totalRate - 1.0) > 1e-10) {\n throw new Error(`Rarity rates must sum to 1.0, got ${totalRate}`);\n }\n\n for (const pool of this.pools) {\n if (pool.items.length === 0) {\n throw new Error(`Rarity \"${pool.rarity}\" has no items`);\n }\n\n const totalWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n if (totalWeight <= 0) {\n throw new Error(`Rarity \"${pool.rarity}\" has zero total weight`);\n }\n\n for (const item of pool.items) {\n if (item.weight < 0) {\n throw new Error(`Item \"${item.name}\" weight must be non-negative, got ${item.weight}`);\n }\n }\n\n if (!pool.items.some(i => i.weight > 0)) {\n throw new Error(`Rarity \"${pool.rarity}\" must have at least one item with positive weight`);\n }\n }\n }\n\n getItemDropRate(name: string): number {\n if (this.mode === 'flatRate') {\n return this.flatRateMap.get(name) || 0;\n }\n\n if (this.dropRateCacheScaled.has(name)) {\n return this.fromScaled(this.dropRateCacheScaled.get(name)!);\n }\n\n for (const pool of this.pools) {\n const item = pool.items.find(i => i.name === name);\n if (item) {\n if (item.weight === 0) {\n this.dropRateCacheScaled.set(name, 0);\n return 0;\n }\n\n const totalPoolWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n const baseRarityRateScaled = this.rarityRatesScaled[pool.rarity];\n const itemWeightScaled = this.toScaled(item.weight);\n const totalWeightScaled = this.toScaled(totalPoolWeight);\n const numeratorScaled = Math.round((itemWeightScaled * baseRarityRateScaled) / GachaEngine.SCALE);\n const rateScaled = Math.round((numeratorScaled * GachaEngine.SCALE) / totalWeightScaled);\n\n this.dropRateCacheScaled.set(name, rateScaled);\n return this.fromScaled(rateScaled);\n }\n }\n\n throw new Error(`Item \"${name}\" not found`);\n }\n\n getCumulativeProbabilityForItem(name: string, rolls: number): number {\n const rate = this.getItemDropRate(name);\n if (rate === 0) return 0;\n if (rate >= 1) return 1;\n\n const cumulativeFailProbability = Math.pow(1 - rate, rolls);\n return 1 - cumulativeFailProbability;\n }\n\n getRollsForTargetProbability(name: string, targetProbability: number): number {\n if (targetProbability <= 0) return 0;\n if (targetProbability >= 1) return 1;\n\n const rate = this.getItemDropRate(name);\n if (rate <= 0) return Infinity;\n return Math.ceil(Math.log(1 - targetProbability) / Math.log(1 - rate));\n }\n\n getRateUpItems(): string[] {\n if (this.mode === 'weighted') {\n return this.pools.flatMap(p => p.items.filter(i => i.rateUp).map(i => i.name));\n } else {\n if (this.pools.length > 0) {\n return this.pools.flatMap(p => p.items.filter(i => i.rateUp).map(i => i.name));\n }\n return [];\n }\n}\n\n\n getAllItemDropRates(): { name: string; dropRate: number; rarity: string }[] {\n if (this.mode === 'flatRate') {\n return Array.from(this.flatRateMap.entries()).map(([name, dropRate]) => ({\n name,\n dropRate,\n rarity: 'flatRate',\n }));\n }\n\n return this.pools.flatMap(p =>\n p.items.map(i => ({\n name: i.name,\n dropRate: this.getItemDropRate(i.name),\n rarity: p.rarity,\n }))\n );\n }\n\n roll(count: number = 1): string[] {\n const results: string[] = [];\n for (let i = 0; i < count; i++) {\n if (this.mode === 'flatRate') {\n const rand = Math.random();\n let cumulative = 0;\n for (const [name, rate] of this.flatRateMap.entries()) {\n cumulative += rate;\n if (rand < cumulative) {\n results.push(name);\n break;\n }\n }\n } else {\n const rarity = this.selectRarity();\n const pool = this.pools.find(p => p.rarity === rarity)!;\n const item = this.selectItemFromPool(pool);\n results.push(item.name);\n }\n }\n return results;\n }\n\n private selectRarity(): string {\n const rand = Math.floor(Math.random() * GachaEngine.SCALE);\n let cumulative = 0;\n\n for (const [rarity, scaledRate] of Object.entries(this.rarityRatesScaled)) {\n cumulative += scaledRate;\n if (rand < cumulative) return rarity;\n }\n\n return Object.keys(this.rarityRatesScaled)[0];\n }\n\n private selectItemFromPool(pool: RarityInput): { name: string; weight: number } {\n const items = pool.items.filter(i => i.weight > 0);\n const scaledItems = items.map(i => ({\n ...i,\n scaledWeight: this.toScaled(i.weight),\n }));\n\n const totalScaledWeight = scaledItems.reduce((sum, i) => sum + i.scaledWeight, 0);\n const rand = Math.floor(Math.random() * totalScaledWeight);\n let cumulative = 0;\n\n for (const item of scaledItems) {\n cumulative += item.scaledWeight;\n if (rand < cumulative) {\n return { name: item.name, weight: item.weight };\n }\n }\n\n return items[0]; // Fallback\n }\n\n getDebugInfo(): {\n scale: number;\n rarityRatesScaled: Record<string, number>;\n rarityRatesFloat: Record<string, number>;\n } {\n const rarityRatesFloat: Record<string, number> = {};\n for (const [rarity, scaledRate] of Object.entries(this.rarityRatesScaled)) {\n rarityRatesFloat[rarity] = this.fromScaled(scaledRate);\n }\n\n return {\n scale: GachaEngine.SCALE,\n rarityRatesScaled: { ...this.rarityRatesScaled },\n rarityRatesFloat,\n };\n }\n}\n"],"names":["GachaEngine","config","this","mode","pools","rarityRatesScaled","flatRateMap","Map","dropRateCacheScaled","weightedConfig","scaleRarityRates","rarityRates","validateConfig","Error","_step","_iterator","_createForOfIteratorHelperLoose","done","_step2","_iterator2","value","items","item","weight","name","set","total","Array","from","values","reduce","sum","v","Math","abs","_proto","prototype","scaled","_i","_Object$entries","Object","entries","length","_Object$entries$_i","rarity","rate","toScaled","probability","MAX_SAFE_SCALE","SCALE","round","fromScaled","scaledInt","originalRates","configuredRarities","Set","keys","usedRarities","map","p","missing","filter","r","has","join","totalRate","_iterator3","_step3","pool","i","_iterator4","_step4","some","getItemDropRate","get","_iterator5","_step5","find","totalPoolWeight","baseRarityRateScaled","itemWeightScaled","totalWeightScaled","numeratorScaled","rateScaled","getCumulativeProbabilityForItem","rolls","pow","getRollsForTargetProbability","targetProbability","Infinity","ceil","log","getRateUpItems","flatMap","rateUp","getAllItemDropRates","_this","_ref","dropRate","roll","count","_this2","results","_loop","_step6","rand","random","cumulative","_iterator6","_step6$value","push","selectRarity","selectItemFromPool","floor","_i2","_Object$entries2","_Object$entries2$_i","_this3","_step7","scaledItems","_extends","scaledWeight","totalScaledWeight","_iterator7","getDebugInfo","rarityRatesFloat","_i3","_Object$entries3","_Object$entries3$_i","scale","Number","MAX_SAFE_INTEGER"],"mappings":"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"}
|
package/dist/index.umd.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t||self).AllemandiGachaEngine={})}(this,function(t){function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,a=Array(e);r<e;r++)a[r]=t[r];return a}function r(t,r){var a="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(a)return(a=a.call(t)).next.bind(a);if(Array.isArray(t)||(a=function(t,r){if(t){if("string"==typeof t)return e(t,r);var a={}.toString.call(t).slice(8,-1);return"Object"===a&&t.constructor&&(a=t.constructor.name),"Map"===a||"Set"===a?Array.from(t):"Arguments"===a||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(a)?e(t,r):void 0}}(t))||r&&t&&"number"==typeof t.length){a&&(t=a);var n=0;return function(){return n>=t.length?{done:!0}:{done:!1,value:t[n++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function a(){return a=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var r=arguments[e];for(var a in r)({}).hasOwnProperty.call(r,a)&&(t[a]=r[a])}return t},a.apply(null,arguments)}var n,i=/*#__PURE__*/function(){function t(t){
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t||self).AllemandiGachaEngine={})}(this,function(t){function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,a=Array(e);r<e;r++)a[r]=t[r];return a}function r(t,r){var a="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(a)return(a=a.call(t)).next.bind(a);if(Array.isArray(t)||(a=function(t,r){if(t){if("string"==typeof t)return e(t,r);var a={}.toString.call(t).slice(8,-1);return"Object"===a&&t.constructor&&(a=t.constructor.name),"Map"===a||"Set"===a?Array.from(t):"Arguments"===a||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(a)?e(t,r):void 0}}(t))||r&&t&&"number"==typeof t.length){a&&(t=a);var n=0;return function(){return n>=t.length?{done:!0}:{done:!1,value:t[n++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function a(){return a=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var r=arguments[e];for(var a in r)({}).hasOwnProperty.call(r,a)&&(t[a]=r[a])}return t},a.apply(null,arguments)}var n,i=/*#__PURE__*/function(){function t(t){if(this.mode=void 0,this.pools=[],this.rarityRatesScaled={},this.flatRateMap=new Map,this.dropRateCacheScaled=new Map,this.mode=t.mode,"weighted"===t.mode){var e=t;this.pools=e.pools,this.rarityRatesScaled=this.scaleRarityRates(e.rarityRates),this.validateConfig(e.rarityRates)}else{if("flatRate"!==t.mode)throw new Error("Unknown gacha mode: "+this.mode);for(var a,n=r(t.pools);!(a=n()).done;)for(var i,o=r(a.value.items);!(i=o()).done;){var s=i.value;if(s.weight<0)throw new Error('FlatRate item "'+s.name+'" must have non-negative weight');this.flatRateMap.set(s.name,s.weight)}var l=Array.from(this.flatRateMap.values()).reduce(function(t,e){return t+e},0);if(Math.abs(l-1)>1e-6)throw new Error("FlatRate item rates must sum to 1.0, but got "+l)}}var e=t.prototype;return e.scaleRarityRates=function(t){for(var e={},r=0,a=Object.entries(t);r<a.length;r++){var n=a[r],i=n[0],o=n[1];if(o<0||o>1)throw new Error('Rarity rate for "'+i+'" must be between 0 and 1, got '+o);e[i]=this.toScaled(o)}return e},e.toScaled=function(e){if(e>t.MAX_SAFE_SCALE/t.SCALE)throw new Error("Probability "+e+" too large for safe integer arithmetic");return Math.round(e*t.SCALE)},e.fromScaled=function(e){return e/t.SCALE},e.validateConfig=function(t){var e=new Set(Object.keys(this.rarityRatesScaled)),a=new Set(this.pools.map(function(t){return t.rarity})),n=Array.from(a).filter(function(t){return!e.has(t)});if(n.length>0)throw new Error("Missing rarity rates for: "+n.join(", "));var i=Object.values(t).reduce(function(t,e){return t+e},0);if(Math.abs(i-1)>1e-10)throw new Error("Rarity rates must sum to 1.0, got "+i);for(var o,s=r(this.pools);!(o=s()).done;){var l=o.value;if(0===l.items.length)throw new Error('Rarity "'+l.rarity+'" has no items');if(l.items.reduce(function(t,e){return t+e.weight},0)<=0)throw new Error('Rarity "'+l.rarity+'" has zero total weight');for(var f,u=r(l.items);!(f=u()).done;){var h=f.value;if(h.weight<0)throw new Error('Item "'+h.name+'" weight must be non-negative, got '+h.weight)}if(!l.items.some(function(t){return t.weight>0}))throw new Error('Rarity "'+l.rarity+'" must have at least one item with positive weight')}},e.getItemDropRate=function(e){if("flatRate"===this.mode)return this.flatRateMap.get(e)||0;if(this.dropRateCacheScaled.has(e))return this.fromScaled(this.dropRateCacheScaled.get(e));for(var a,n=r(this.pools);!(a=n()).done;){var i=a.value,o=i.items.find(function(t){return t.name===e});if(o){if(0===o.weight)return this.dropRateCacheScaled.set(e,0),0;var s=i.items.reduce(function(t,e){return t+e.weight},0),l=this.rarityRatesScaled[i.rarity],f=this.toScaled(o.weight),u=this.toScaled(s),h=Math.round(f*l/t.SCALE),c=Math.round(h*t.SCALE/u);return this.dropRateCacheScaled.set(e,c),this.fromScaled(c)}}throw new Error('Item "'+e+'" not found')},e.getCumulativeProbabilityForItem=function(t,e){var r=this.getItemDropRate(t);return 0===r?0:r>=1?1:1-Math.pow(1-r,e)},e.getRollsForTargetProbability=function(t,e){if(e<=0)return 0;if(e>=1)return 1;var r=this.getItemDropRate(t);return r<=0?Infinity:Math.ceil(Math.log(1-e)/Math.log(1-r))},e.getRateUpItems=function(){return"weighted"===this.mode||this.pools.length>0?this.pools.flatMap(function(t){return t.items.filter(function(t){return t.rateUp}).map(function(t){return t.name})}):[]},e.getAllItemDropRates=function(){var t=this;return"flatRate"===this.mode?Array.from(this.flatRateMap.entries()).map(function(t){return{name:t[0],dropRate:t[1],rarity:"flatRate"}}):this.pools.flatMap(function(e){return e.items.map(function(r){return{name:r.name,dropRate:t.getItemDropRate(r.name),rarity:e.rarity}})})},e.roll=function(t){var e=this;void 0===t&&(t=1);for(var a=[],n=function(){if("flatRate"===e.mode)for(var t,n=Math.random(),i=0,o=r(e.flatRateMap.entries());!(t=o()).done;){var s=t.value;if(n<(i+=s[1])){a.push(s[0]);break}}else{var l=e.selectRarity(),f=e.pools.find(function(t){return t.rarity===l}),u=e.selectItemFromPool(f);a.push(u.name)}},i=0;i<t;i++)n();return a},e.selectRarity=function(){for(var e=Math.floor(Math.random()*t.SCALE),r=0,a=0,n=Object.entries(this.rarityRatesScaled);a<n.length;a++){var i=n[a];if(e<(r+=i[1]))return i[0]}return Object.keys(this.rarityRatesScaled)[0]},e.selectItemFromPool=function(t){for(var e,n=this,i=t.items.filter(function(t){return t.weight>0}),o=i.map(function(t){return a({},t,{scaledWeight:n.toScaled(t.weight)})}),s=o.reduce(function(t,e){return t+e.scaledWeight},0),l=Math.floor(Math.random()*s),f=0,u=r(o);!(e=u()).done;){var h=e.value;if(l<(f+=h.scaledWeight))return{name:h.name,weight:h.weight}}return i[0]},e.getDebugInfo=function(){for(var e={},r=0,n=Object.entries(this.rarityRatesScaled);r<n.length;r++){var i=n[r];e[i[0]]=this.fromScaled(i[1])}return{scale:t.SCALE,rarityRatesScaled:a({},this.rarityRatesScaled),rarityRatesFloat:e}},t}();n=i,i.SCALE=1e6,i.MAX_SAFE_SCALE=Math.floor(Number.MAX_SAFE_INTEGER/n.SCALE),t.GachaEngine=i});
|
|
2
2
|
//# sourceMappingURL=index.umd.js.map
|
package/dist/index.umd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.umd.js","sources":["../src/gacha-engine.ts"],"sourcesContent":["import { RarityInput, GachaEngineConfig } from './types';\n\nexport class GachaEngine {\n // Scale factor for fixed-point arithmetic (1,000,000 = 6 decimal places)\n private static readonly SCALE = 1000000;\n private static readonly MAX_SAFE_SCALE = Math.floor(Number.MAX_SAFE_INTEGER / this.SCALE);\n\n private pools: RarityInput[];\n private rarityRatesScaled: Record<string, number>; // Scaled to integers\n private dropRateCacheScaled = new Map<string, number>(); // Cache scaled rates\n\n constructor({ rarityRates, pools }: GachaEngineConfig) {\n this.pools = pools;\n this.rarityRatesScaled = this.scaleRarityRates(rarityRates);\n this.validateConfig(rarityRates);\n }\n\n /**\n * Convert floating point rates to scaled integers\n */\n private scaleRarityRates(rarityRates: Record<string, number>): Record<string, number> {\n const scaled: Record<string, number> = {};\n for (const [rarity, rate] of Object.entries(rarityRates)) {\n if (rate < 0 || rate > 1) {\n throw new Error(`Rarity rate for \"${rarity}\" must be between 0 and 1, got ${rate}`);\n }\n scaled[rarity] = this.toScaled(rate);\n }\n return scaled;\n }\n\n /**\n * Convert probability to scaled integer\n */\n private toScaled(probability: number): number {\n if (probability > GachaEngine.MAX_SAFE_SCALE / GachaEngine.SCALE) {\n throw new Error(`Probability ${probability} too large for safe integer arithmetic`);\n }\n return Math.round(probability * GachaEngine.SCALE);\n }\n\n /**\n * Convert scaled integer back to probability\n */\n private fromScaled(scaledInt: number): number {\n return scaledInt / GachaEngine.SCALE;\n }\n\n private validateConfig(originalRates: Record<string, number>): void {\n const configuredRarities = new Set(Object.keys(this.rarityRatesScaled));\n const usedRarities = new Set(this.pools.map(p => p.rarity));\n const missingArray = Array.from(usedRarities).filter(r => !configuredRarities.has(r));\n \n if (missingArray.length > 0) {\n throw new Error(`Missing rarity rates for: ${missingArray.join(', ')}`);\n }\n\n // Validate that rates sum to exactly 1.0 (within floating point precision)\n const totalRate = Object.values(originalRates).reduce((sum, rate) => sum + rate, 0);\n const totalScaled = Object.values(this.rarityRatesScaled).reduce((sum, rate) => sum + rate, 0);\n \n if (Math.abs(totalRate - 1.0) > 1e-10) {\n throw new Error(`Rarity rates must sum to 1.0, got ${totalRate}`);\n }\n\n // Ensure scaled rates sum to SCALE (accounting for rounding)\n if (Math.abs(totalScaled - GachaEngine.SCALE) > Object.keys(this.rarityRatesScaled).length) {\n console.warn(`Scaled rates sum to ${totalScaled}, expected ${GachaEngine.SCALE}. This is likely due to rounding.`);\n }\n\n for (const pool of this.pools) {\n if (pool.items.length === 0) {\n throw new Error(`Rarity \"${pool.rarity}\" has no items`);\n }\n \n const totalWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n if (totalWeight <= 0) {\n throw new Error(`Rarity \"${pool.rarity}\" has zero total weight`);\n }\n\n // Validate that all weights are non-negative\n for (const item of pool.items) {\n if (item.weight < 0) {\n throw new Error(`Item \"${item.name}\" weight must be non-negative, got ${item.weight}`);\n }\n }\n\n // Ensure at least one item has positive weight\n const hasPositiveWeight = pool.items.some(item => item.weight > 0);\n if (!hasPositiveWeight) {\n throw new Error(`Rarity \"${pool.rarity}\" must have at least one item with positive weight`);\n }\n }\n }\n\n getItemDropRate(name: string): number {\n if (this.dropRateCacheScaled.has(name)) {\n return this.fromScaled(this.dropRateCacheScaled.get(name)!);\n }\n\n for (const pool of this.pools) {\n const item = pool.items.find(i => i.name === name);\n if (item) {\n // Handle zero weight items (never drop)\n if (item.weight === 0) {\n this.dropRateCacheScaled.set(name, 0);\n return 0;\n }\n\n const totalPoolWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n const baseRarityRateScaled = this.rarityRatesScaled[pool.rarity];\n \n // Convert weights to scaled integers for perfect precision\n const itemWeightScaled = this.toScaled(item.weight);\n const totalWeightScaled = this.toScaled(totalPoolWeight);\n \n // Scaled arithmetic: (itemWeight * baseRate) / totalWeight\n const numeratorScaled = Math.round((itemWeightScaled * baseRarityRateScaled) / GachaEngine.SCALE);\n const rateScaled = Math.round((numeratorScaled * GachaEngine.SCALE) / totalWeightScaled);\n \n this.dropRateCacheScaled.set(name, rateScaled);\n return this.fromScaled(rateScaled);\n }\n }\n throw new Error(`Item \"${name}\" not found`);\n }\n\n getRarityProbability(rarity: string): number {\n if (!this.rarityRatesScaled[rarity]) {\n throw new Error(`Rarity \"${rarity}\" not found`);\n }\n return this.fromScaled(this.rarityRatesScaled[rarity]);\n }\n\n getCumulativeProbabilityForItem(name: string, rolls: number): number {\n const rateScaled = this.getItemDropRateScaled(name);\n \n if (rateScaled === 0) return 0;\n if (rateScaled >= GachaEngine.SCALE) return 1;\n \n // Calculate (1 - rate)^rolls using scaled arithmetic\n const failRateScaled = GachaEngine.SCALE - rateScaled;\n const failRate = this.fromScaled(failRateScaled);\n \n // For large rolls, we need to be careful with precision\n const cumulativeFailProbability = Math.pow(failRate, rolls);\n const cumulativeProbability = 1 - cumulativeFailProbability;\n \n return Math.min(1, Math.max(0, cumulativeProbability));\n }\n\n getRollsForTargetProbability(name: string, targetProbability: number): number {\n if (targetProbability <= 0) return 0;\n if (targetProbability >= 1) return 1;\n \n const rate = this.getItemDropRate(name);\n if (rate <= 0) return Infinity;\n if (rate >= 1) return 1;\n \n return Math.ceil(Math.log(1 - targetProbability) / Math.log(1 - rate));\n }\n\n getRateUpItems(): string[] {\n return this.pools.flatMap(p =>\n p.items.filter(i => i.rateUp).map(i => i.name)\n );\n }\n\n getAllItemDropRates(): { name: string; dropRate: number; rarity: string }[] {\n return this.pools.flatMap(p =>\n p.items.map(i => ({\n name: i.name,\n dropRate: this.getItemDropRate(i.name),\n rarity: p.rarity\n }))\n );\n }\n\n /**\n * Get scaled drop rate for internal calculations\n */\n private getItemDropRateScaled(name: string): number {\n if (this.dropRateCacheScaled.has(name)) {\n return this.dropRateCacheScaled.get(name)!;\n }\n \n // Trigger calculation and caching\n this.getItemDropRate(name);\n return this.dropRateCacheScaled.get(name)!;\n }\n\n roll(count: number = 1): string[] {\n const results: string[] = [];\n for (let i = 0; i < count; i++) {\n const rarity = this.selectRarity();\n const pool = this.pools.find(p => p.rarity === rarity)!;\n const item = this.selectItemFromPool(pool);\n results.push(item.name);\n }\n return results;\n }\n\n private selectRarity(): string {\n const rand = Math.floor(Math.random() * GachaEngine.SCALE);\n let cumulativeScaled = 0;\n \n for (const [rarity, rateScaled] of Object.entries(this.rarityRatesScaled)) {\n cumulativeScaled += rateScaled;\n if (rand < cumulativeScaled) return rarity;\n }\n \n // Fallback (should never happen with proper validation)\n return Object.keys(this.rarityRatesScaled)[0];\n }\n\n private selectItemFromPool(pool: RarityInput): { name: string; weight: number } {\n // Filter out zero-weight items (they can never be selected)\n const selectableItems = pool.items.filter(item => item.weight > 0);\n \n if (selectableItems.length === 0) {\n throw new Error(`No selectable items in pool for rarity \"${pool.rarity}\"`);\n }\n\n // Convert all weights to scaled integers for perfect precision\n const scaledItems = selectableItems.map(item => ({\n ...item,\n scaledWeight: this.toScaled(item.weight)\n }));\n \n const totalScaledWeight = scaledItems.reduce((sum, item) => sum + item.scaledWeight, 0);\n const rand = Math.floor(Math.random() * totalScaledWeight);\n let cumulative = 0;\n \n for (const item of scaledItems) {\n cumulative += item.scaledWeight;\n if (rand < cumulative) {\n return { name: item.name, weight: item.weight };\n }\n }\n \n // Fallback (should never happen)\n return selectableItems[0];\n }\n\n /**\n * Debug method to inspect scaled values\n */\n getDebugInfo(): {\n scale: number;\n rarityRatesScaled: Record<string, number>;\n rarityRatesFloat: Record<string, number>;\n } {\n const rarityRatesFloat: Record<string, number> = {};\n for (const [rarity, scaledRate] of Object.entries(this.rarityRatesScaled)) {\n rarityRatesFloat[rarity] = this.fromScaled(scaledRate);\n }\n \n return {\n scale: GachaEngine.SCALE,\n rarityRatesScaled: { ...this.rarityRatesScaled },\n rarityRatesFloat\n };\n }\n}"],"names":["GachaEngine","_ref","rarityRates","pools","rarityRatesScaled","dropRateCacheScaled","Map","this","scaleRarityRates","validateConfig","_proto","prototype","scaled","_i","_Object$entries","Object","entries","length","_Object$entries$_i","rarity","rate","Error","toScaled","probability","MAX_SAFE_SCALE","SCALE","Math","round","fromScaled","scaledInt","originalRates","configuredRarities","Set","keys","usedRarities","map","p","missingArray","Array","from","filter","r","has","join","totalRate","values","reduce","sum","totalScaled","abs","console","warn","_iterator","_step","_createForOfIteratorHelperLoose","done","pool","value","items","i","weight","_step2","_iterator2","item","name","hasPositiveWeight","some","getItemDropRate","get","_step3","_iterator3","find","set","totalPoolWeight","baseRarityRateScaled","itemWeightScaled","totalWeightScaled","numeratorScaled","rateScaled","getRarityProbability","getCumulativeProbabilityForItem","rolls","getItemDropRateScaled","failRate","cumulativeFailProbability","pow","min","max","getRollsForTargetProbability","targetProbability","Infinity","ceil","log","getRateUpItems","flatMap","rateUp","getAllItemDropRates","_this","dropRate","roll","count","_this2","results","_loop","selectRarity","selectItemFromPool","push","rand","floor","random","cumulativeScaled","_i2","_Object$entries2","_Object$entries2$_i","_this3","selectableItems","_step4","scaledItems","_extends","scaledWeight","totalScaledWeight","cumulative","_iterator4","getDebugInfo","rarityRatesFloat","_i3","_Object$entries3","_Object$entries3$_i","scale","Number","MAX_SAFE_INTEGER","_GachaEngine"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.umd.js","sources":["../src/gacha-engine.ts"],"sourcesContent":["import {\n RarityInput,\n GachaEngineConfig,\n WeightedGachaEngineConfig,\n FlatRateGachaEngineConfig,\n} from './types';\n\nexport class GachaEngine {\n private static readonly SCALE = 1_000_000;\n private static readonly MAX_SAFE_SCALE = Math.floor(Number.MAX_SAFE_INTEGER / GachaEngine.SCALE);\n\n private mode: 'weighted' | 'flatRate';\n private pools: RarityInput[] = [];\n private rarityRatesScaled: Record<string, number> = {};\n private flatRateMap: Map<string, number> = new Map();\n private dropRateCacheScaled = new Map<string, number>();\n\n constructor(config: GachaEngineConfig) {\n this.mode = config.mode;\n\n if (config.mode === 'weighted') {\n const weightedConfig = config as WeightedGachaEngineConfig;\n this.pools = weightedConfig.pools;\n this.rarityRatesScaled = this.scaleRarityRates(weightedConfig.rarityRates);\n this.validateConfig(weightedConfig.rarityRates);\n } else if (config.mode === 'flatRate') {\n const flatConfig = config as FlatRateGachaEngineConfig;\n for (const pool of flatConfig.pools) {\n for (const item of pool.items) {\n if (item.weight < 0) {\n throw new Error(`FlatRate item \"${item.name}\" must have non-negative weight`);\n }\n this.flatRateMap.set(item.name, item.weight); // Here, interpreted as direct probability\n }\n }\n const total = Array.from(this.flatRateMap.values()).reduce((sum, v) => sum + v, 0);\n if (Math.abs(total - 1.0) > 1e-6) {\n throw new Error(`FlatRate item rates must sum to 1.0, but got ${total}`);\n }\n } else {\n throw new Error(`Unknown gacha mode: ${this.mode}`);\n }\n }\n\n private scaleRarityRates(rarityRates: Record<string, number>): Record<string, number> {\n const scaled: Record<string, number> = {};\n for (const [rarity, rate] of Object.entries(rarityRates)) {\n if (rate < 0 || rate > 1) {\n throw new Error(`Rarity rate for \"${rarity}\" must be between 0 and 1, got ${rate}`);\n }\n scaled[rarity] = this.toScaled(rate);\n }\n return scaled;\n }\n\n private toScaled(probability: number): number {\n if (probability > GachaEngine.MAX_SAFE_SCALE / GachaEngine.SCALE) {\n throw new Error(`Probability ${probability} too large for safe integer arithmetic`);\n }\n return Math.round(probability * GachaEngine.SCALE);\n }\n\n private fromScaled(scaledInt: number): number {\n return scaledInt / GachaEngine.SCALE;\n }\n\n private validateConfig(originalRates: Record<string, number>): void {\n const configuredRarities = new Set(Object.keys(this.rarityRatesScaled));\n const usedRarities = new Set(this.pools.map(p => p.rarity));\n const missing = Array.from(usedRarities).filter(r => !configuredRarities.has(r));\n\n if (missing.length > 0) {\n throw new Error(`Missing rarity rates for: ${missing.join(', ')}`);\n }\n\n const totalRate = Object.values(originalRates).reduce((sum, rate) => sum + rate, 0);\n if (Math.abs(totalRate - 1.0) > 1e-10) {\n throw new Error(`Rarity rates must sum to 1.0, got ${totalRate}`);\n }\n\n for (const pool of this.pools) {\n if (pool.items.length === 0) {\n throw new Error(`Rarity \"${pool.rarity}\" has no items`);\n }\n\n const totalWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n if (totalWeight <= 0) {\n throw new Error(`Rarity \"${pool.rarity}\" has zero total weight`);\n }\n\n for (const item of pool.items) {\n if (item.weight < 0) {\n throw new Error(`Item \"${item.name}\" weight must be non-negative, got ${item.weight}`);\n }\n }\n\n if (!pool.items.some(i => i.weight > 0)) {\n throw new Error(`Rarity \"${pool.rarity}\" must have at least one item with positive weight`);\n }\n }\n }\n\n getItemDropRate(name: string): number {\n if (this.mode === 'flatRate') {\n return this.flatRateMap.get(name) || 0;\n }\n\n if (this.dropRateCacheScaled.has(name)) {\n return this.fromScaled(this.dropRateCacheScaled.get(name)!);\n }\n\n for (const pool of this.pools) {\n const item = pool.items.find(i => i.name === name);\n if (item) {\n if (item.weight === 0) {\n this.dropRateCacheScaled.set(name, 0);\n return 0;\n }\n\n const totalPoolWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n const baseRarityRateScaled = this.rarityRatesScaled[pool.rarity];\n const itemWeightScaled = this.toScaled(item.weight);\n const totalWeightScaled = this.toScaled(totalPoolWeight);\n const numeratorScaled = Math.round((itemWeightScaled * baseRarityRateScaled) / GachaEngine.SCALE);\n const rateScaled = Math.round((numeratorScaled * GachaEngine.SCALE) / totalWeightScaled);\n\n this.dropRateCacheScaled.set(name, rateScaled);\n return this.fromScaled(rateScaled);\n }\n }\n\n throw new Error(`Item \"${name}\" not found`);\n }\n\n getCumulativeProbabilityForItem(name: string, rolls: number): number {\n const rate = this.getItemDropRate(name);\n if (rate === 0) return 0;\n if (rate >= 1) return 1;\n\n const cumulativeFailProbability = Math.pow(1 - rate, rolls);\n return 1 - cumulativeFailProbability;\n }\n\n getRollsForTargetProbability(name: string, targetProbability: number): number {\n if (targetProbability <= 0) return 0;\n if (targetProbability >= 1) return 1;\n\n const rate = this.getItemDropRate(name);\n if (rate <= 0) return Infinity;\n return Math.ceil(Math.log(1 - targetProbability) / Math.log(1 - rate));\n }\n\n getRateUpItems(): string[] {\n if (this.mode === 'weighted') {\n return this.pools.flatMap(p => p.items.filter(i => i.rateUp).map(i => i.name));\n } else {\n if (this.pools.length > 0) {\n return this.pools.flatMap(p => p.items.filter(i => i.rateUp).map(i => i.name));\n }\n return [];\n }\n}\n\n\n getAllItemDropRates(): { name: string; dropRate: number; rarity: string }[] {\n if (this.mode === 'flatRate') {\n return Array.from(this.flatRateMap.entries()).map(([name, dropRate]) => ({\n name,\n dropRate,\n rarity: 'flatRate',\n }));\n }\n\n return this.pools.flatMap(p =>\n p.items.map(i => ({\n name: i.name,\n dropRate: this.getItemDropRate(i.name),\n rarity: p.rarity,\n }))\n );\n }\n\n roll(count: number = 1): string[] {\n const results: string[] = [];\n for (let i = 0; i < count; i++) {\n if (this.mode === 'flatRate') {\n const rand = Math.random();\n let cumulative = 0;\n for (const [name, rate] of this.flatRateMap.entries()) {\n cumulative += rate;\n if (rand < cumulative) {\n results.push(name);\n break;\n }\n }\n } else {\n const rarity = this.selectRarity();\n const pool = this.pools.find(p => p.rarity === rarity)!;\n const item = this.selectItemFromPool(pool);\n results.push(item.name);\n }\n }\n return results;\n }\n\n private selectRarity(): string {\n const rand = Math.floor(Math.random() * GachaEngine.SCALE);\n let cumulative = 0;\n\n for (const [rarity, scaledRate] of Object.entries(this.rarityRatesScaled)) {\n cumulative += scaledRate;\n if (rand < cumulative) return rarity;\n }\n\n return Object.keys(this.rarityRatesScaled)[0];\n }\n\n private selectItemFromPool(pool: RarityInput): { name: string; weight: number } {\n const items = pool.items.filter(i => i.weight > 0);\n const scaledItems = items.map(i => ({\n ...i,\n scaledWeight: this.toScaled(i.weight),\n }));\n\n const totalScaledWeight = scaledItems.reduce((sum, i) => sum + i.scaledWeight, 0);\n const rand = Math.floor(Math.random() * totalScaledWeight);\n let cumulative = 0;\n\n for (const item of scaledItems) {\n cumulative += item.scaledWeight;\n if (rand < cumulative) {\n return { name: item.name, weight: item.weight };\n }\n }\n\n return items[0]; // Fallback\n }\n\n getDebugInfo(): {\n scale: number;\n rarityRatesScaled: Record<string, number>;\n rarityRatesFloat: Record<string, number>;\n } {\n const rarityRatesFloat: Record<string, number> = {};\n for (const [rarity, scaledRate] of Object.entries(this.rarityRatesScaled)) {\n rarityRatesFloat[rarity] = this.fromScaled(scaledRate);\n }\n\n return {\n scale: GachaEngine.SCALE,\n rarityRatesScaled: { ...this.rarityRatesScaled },\n rarityRatesFloat,\n };\n }\n}\n"],"names":["GachaEngine","config","this","mode","pools","rarityRatesScaled","flatRateMap","Map","dropRateCacheScaled","weightedConfig","scaleRarityRates","rarityRates","validateConfig","Error","_step","_iterator","_createForOfIteratorHelperLoose","done","_step2","_iterator2","value","items","item","weight","name","set","total","Array","from","values","reduce","sum","v","Math","abs","_proto","prototype","scaled","_i","_Object$entries","Object","entries","length","_Object$entries$_i","rarity","rate","toScaled","probability","MAX_SAFE_SCALE","SCALE","round","fromScaled","scaledInt","originalRates","configuredRarities","Set","keys","usedRarities","map","p","missing","filter","r","has","join","totalRate","_iterator3","_step3","pool","i","_iterator4","_step4","some","getItemDropRate","get","_iterator5","_step5","find","totalPoolWeight","baseRarityRateScaled","itemWeightScaled","totalWeightScaled","numeratorScaled","rateScaled","getCumulativeProbabilityForItem","rolls","pow","getRollsForTargetProbability","targetProbability","Infinity","ceil","log","getRateUpItems","flatMap","rateUp","getAllItemDropRates","_this","_ref","dropRate","roll","count","_this2","results","_loop","_step6","rand","random","cumulative","_iterator6","_step6$value","push","selectRarity","selectItemFromPool","floor","_i2","_Object$entries2","_Object$entries2$_i","_this3","_step7","scaledItems","_extends","scaledWeight","totalScaledWeight","_iterator7","getDebugInfo","rarityRatesFloat","_i3","_Object$entries3","_Object$entries3$_i","scale","Number","MAX_SAFE_INTEGER"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
export interface RarityInput {
|
|
2
|
-
rarity: string;
|
|
3
|
-
items: GachaItem[];
|
|
4
|
-
}
|
|
5
1
|
export interface GachaItem {
|
|
6
2
|
name: string;
|
|
7
|
-
/**
|
|
8
|
-
* Weight determines relative probability within the rarity tier.
|
|
9
|
-
* Can be fractional (e.g., 0.5, 1.2) - will be converted to scaled integers internally.
|
|
10
|
-
* Use weight: 0 for items that should never drop (useful for placeholders or disabled items).
|
|
11
|
-
* Higher weights = higher probability within the rarity tier.
|
|
12
|
-
*/
|
|
13
3
|
weight: number;
|
|
14
4
|
rateUp?: boolean;
|
|
15
5
|
}
|
|
16
|
-
export interface
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
6
|
+
export interface RarityInput {
|
|
7
|
+
rarity: string;
|
|
8
|
+
items: GachaItem[];
|
|
9
|
+
}
|
|
10
|
+
/** Weighted mode requires explicit rarityRates */
|
|
11
|
+
export interface WeightedGachaEngineConfig {
|
|
12
|
+
mode: 'weighted';
|
|
22
13
|
rarityRates: Record<string, number>;
|
|
23
|
-
/** Array of rarity pools containing items */
|
|
24
14
|
pools: RarityInput[];
|
|
25
15
|
}
|
|
16
|
+
/** Flat rate mode does NOT use rarityRates */
|
|
17
|
+
export interface FlatRateGachaEngineConfig {
|
|
18
|
+
mode: 'flatRate';
|
|
19
|
+
pools: RarityInput[];
|
|
20
|
+
}
|
|
21
|
+
export type GachaEngineConfig = WeightedGachaEngineConfig | FlatRateGachaEngineConfig;
|
package/package.json
CHANGED