@allemandi/gacha-engine 0.1.0 → 0.2.1
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 +78 -43
- package/dist/gacha-engine.d.ts +21 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.module.js +2 -0
- package/dist/index.module.js.map +1 -0
- package/dist/index.umd.js +2 -0
- package/dist/index.umd.js.map +1 -0
- package/{src/types.ts → dist/types.d.ts} +2 -5
- package/package.json +16 -1
- package/.github/workflows/ci.yml +0 -42
- package/.github/workflows/publish.yml +0 -26
- package/eslint.config.mjs +0 -34
- package/src/gacha-engine.ts +0 -58
- package/src/index.ts +0 -1
- package/test/index.test.ts +0 -111
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -8
package/README.md
CHANGED
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
## ✨ Features
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
22
|
+
- 🎲 **Roll simulation**: Perform actual gacha rolls with weighted probabilities
|
|
23
|
+
- 🔍 **Probability analysis**: Calculate drop rates, cumulative probabilities, and rolls needed
|
|
24
|
+
- 📐 **Multi-rarity support**: Handle complex gacha systems with multiple rarity tiers
|
|
25
|
+
- ⚡ **Performance optimized**: Cached calculations and efficient algorithms
|
|
26
|
+
- 🛡️ **Type-safe**: Full TypeScript support with comprehensive validation
|
|
25
27
|
|
|
26
28
|
## 🛠️ Installation
|
|
27
29
|
```bash
|
|
@@ -43,38 +45,48 @@ const pools = [
|
|
|
43
45
|
{
|
|
44
46
|
rarity: 'SSR',
|
|
45
47
|
items: [
|
|
46
|
-
{ name: '
|
|
47
|
-
{ name: '
|
|
48
|
-
{ name: '
|
|
48
|
+
{ name: 'Super Hobo', weight: 0.8, rateUp: true }, // Rate-up: 0.8% of SSR pool
|
|
49
|
+
{ name: 'Broke King', weight: 0.4 }, // Standard: 0.4% of SSR pool
|
|
50
|
+
{ name: 'Cardboard Hero', weight: 0.4 },
|
|
51
|
+
{ name: 'Newspaper Warmer', weight: 0.4 }
|
|
49
52
|
]
|
|
50
53
|
},
|
|
51
54
|
{
|
|
52
|
-
rarity: 'SR',
|
|
55
|
+
rarity: 'SR',
|
|
53
56
|
items: [
|
|
54
|
-
{ name: '
|
|
55
|
-
{ name: '
|
|
57
|
+
{ name: 'Cold Salaryman', weight: 1.5, rateUp: true }, // Rate-up: 1.5% of SR pool
|
|
58
|
+
{ name: 'Numb Artist', weight: 1.8 }, // Standard 4-star rates
|
|
59
|
+
{ name: 'Crying Cook', weight: 1.8 },
|
|
60
|
+
{ name: 'Lonely Cat', weight: 1.8 }
|
|
56
61
|
]
|
|
57
62
|
}
|
|
58
63
|
];
|
|
59
64
|
|
|
60
65
|
const rarityRates = {
|
|
61
|
-
SSR: 0.
|
|
62
|
-
SR: 0.3
|
|
66
|
+
SSR: 0.01, // 1% chance for SSR (5-star)
|
|
67
|
+
SR: 0.03, // 3% chance for SR (4-star)
|
|
63
68
|
};
|
|
64
69
|
|
|
65
70
|
const engine = new GachaEngine({ pools, rarityRates });
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
// Perform actual rolls
|
|
73
|
+
const results = engine.roll(10);
|
|
74
|
+
console.log(`10 rolls result: ${results.join(', ')}`);
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
// Analyze probabilities
|
|
77
|
+
const dropRate = engine.getItemDropRate('Super Hobo');
|
|
78
|
+
console.log(`Drop rate for Super Hobo: ${(dropRate * 100).toFixed(3)}%`);
|
|
79
|
+
// Output: ~0.4% (0.8/2.0 * 0.01)
|
|
72
80
|
|
|
73
|
-
const
|
|
74
|
-
console.log(`
|
|
81
|
+
const cumulativeProb = engine.getCumulativeProbabilityForItem('Super Hobo', 300);
|
|
82
|
+
console.log(`Probability of getting Super Hobo in 300 rolls: ${(cumulativeProb * 100).toFixed(1)}%`);
|
|
83
|
+
|
|
84
|
+
const rollsNeeded = engine.getRollsForTargetProbability('Super Hobo', 0.5);
|
|
85
|
+
console.log(`Rolls needed for 50% chance: ${rollsNeeded}`);
|
|
75
86
|
|
|
76
87
|
const rateUpItems = engine.getRateUpItems();
|
|
77
88
|
console.log(`Current rate-up items: ${rateUpItems.join(', ')}`);
|
|
89
|
+
// Output: "Super Hobo, Cold Salaryman"
|
|
78
90
|
```
|
|
79
91
|
|
|
80
92
|
**CommonJS**
|
|
@@ -84,56 +96,79 @@ const { GachaEngine } = require('@allemandi/gacha-engine');
|
|
|
84
96
|
|
|
85
97
|
**UMD**
|
|
86
98
|
```html
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
<script src="https://unpkg.com/@allemandi/gacha-engine"></script>
|
|
100
|
+
<script>
|
|
101
|
+
const engine = new AllemandiGachaEngine.GachaEngine({
|
|
102
|
+
rarityRates: { 'SSR': 0.01 },
|
|
103
|
+
pools: [
|
|
104
|
+
{
|
|
105
|
+
rarity: 'SSR',
|
|
106
|
+
items: [
|
|
107
|
+
{ name: 'Park Master', weight: 0.7, rateUp: true },
|
|
108
|
+
{ name: 'Trash Titan', weight: 0.3 }
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
console.log('Single roll:', engine.roll());
|
|
115
|
+
// Single roll: ['Park Master'] or ['Trash Titan']
|
|
116
|
+
</script>
|
|
101
117
|
```
|
|
102
118
|
|
|
103
119
|
## 📘 API
|
|
104
|
-
`new GachaEngine(config: GachaEngineConfig)`
|
|
105
120
|
|
|
106
|
-
|
|
121
|
+
### Constructor
|
|
122
|
+
`new GachaEngine(config: GachaEngineConfig)`
|
|
107
123
|
|
|
108
|
-
|
|
124
|
+
Creates a new GachaEngine instance with validation.
|
|
109
125
|
|
|
110
|
-
|
|
126
|
+
**Config Properties:**
|
|
127
|
+
- `rarityRates` **(required)**: Object mapping rarity names to their base probabilities (must sum to ≤ 1.0)
|
|
128
|
+
- `pools` **(required)**: Array of rarity pools, each containing:
|
|
129
|
+
- `rarity`: String identifier matching a key in `rarityRates`
|
|
130
|
+
- `items`: Array of items with:
|
|
131
|
+
- `name`: Unique item identifier
|
|
132
|
+
- `weight`: Relative weight within the rarity pool (higher = more likely)
|
|
133
|
+
- `rateUp?`: Optional boolean flag for rate-up items
|
|
111
134
|
|
|
112
135
|
### Methods
|
|
136
|
+
|
|
137
|
+
#### Rolling
|
|
138
|
+
`roll(count?: number): string[]`
|
|
139
|
+
- Simulate gacha rolls and returns item names
|
|
140
|
+
- `count`: Number of rolls to perform (default: 1)
|
|
141
|
+
- Returns array of item names
|
|
142
|
+
|
|
143
|
+
#### Analysis
|
|
113
144
|
`getItemDropRate(name: string): number`
|
|
114
|
-
- Returns the effective drop rate
|
|
145
|
+
- Returns the effective drop rate (0-1) for a specific item
|
|
146
|
+
- Calculated as: `(item.weight / pool.totalWeight) × rarity.baseRate`
|
|
115
147
|
|
|
116
148
|
`getRarityProbability(rarity: string): number`
|
|
117
|
-
- Returns the base probability
|
|
149
|
+
- Returns the base probability for a rarity tier
|
|
118
150
|
|
|
119
151
|
`getCumulativeProbabilityForItem(name: string, rolls: number): number`
|
|
120
|
-
-
|
|
152
|
+
- Calculates probability of getting the item at least once in N rolls
|
|
153
|
+
- Uses formula: `1 - (1 - dropRate)^rolls`
|
|
121
154
|
|
|
122
155
|
`getRollsForTargetProbability(name: string, targetProbability: number): number`
|
|
123
|
-
- Returns
|
|
156
|
+
- Returns minimum rolls needed to reach target probability for an item
|
|
157
|
+
- Returns `Infinity` if item has zero drop rate
|
|
124
158
|
|
|
159
|
+
#### Utility
|
|
125
160
|
`getRateUpItems(): string[]`
|
|
126
|
-
- Returns
|
|
161
|
+
- Returns names of all items marked with `rateUp: true`
|
|
127
162
|
|
|
128
|
-
`getAllItemDropRates(): {
|
|
129
|
-
- Returns
|
|
163
|
+
`getAllItemDropRates(): Array<{name: string, dropRate: number, rarity: string}>`
|
|
164
|
+
- Returns complete list of all items with their calculated drop rates and rarities
|
|
130
165
|
|
|
131
166
|
## 🧪 Tests
|
|
132
167
|
|
|
133
168
|
> Available in the GitHub repo only.
|
|
134
169
|
|
|
135
170
|
```bash
|
|
136
|
-
# Run the test suite with
|
|
171
|
+
# Run the test suite with Vitest
|
|
137
172
|
yarn test
|
|
138
173
|
# or
|
|
139
174
|
npm test
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { GachaEngineConfig } from './types';
|
|
2
|
+
export declare class GachaEngine {
|
|
3
|
+
private pools;
|
|
4
|
+
private rarityRates;
|
|
5
|
+
private dropRateCache;
|
|
6
|
+
constructor({ rarityRates, pools }: GachaEngineConfig);
|
|
7
|
+
private validateConfig;
|
|
8
|
+
getItemDropRate(name: string): number;
|
|
9
|
+
getRarityProbability(rarity: string): number;
|
|
10
|
+
getCumulativeProbabilityForItem(name: string, rolls: number): number;
|
|
11
|
+
getRollsForTargetProbability(name: string, targetProbability: number): number;
|
|
12
|
+
getRateUpItems(): string[];
|
|
13
|
+
getAllItemDropRates(): {
|
|
14
|
+
name: string;
|
|
15
|
+
dropRate: number;
|
|
16
|
+
rarity: string;
|
|
17
|
+
}[];
|
|
18
|
+
roll(count?: number): string[];
|
|
19
|
+
private selectRarity;
|
|
20
|
+
private selectItemFromPool;
|
|
21
|
+
}
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function t(t,r){(null==r||r>t.length)&&(r=t.length);for(var e=0,n=Array(r);e<r;e++)n[e]=t[e];return n}
|
|
1
|
+
function t(t,r){(null==r||r>t.length)&&(r=t.length);for(var e=0,n=Array(r);e<r;e++)n[e]=t[e];return n}function r(r,e){var n="undefined"!=typeof Symbol&&r[Symbol.iterator]||r["@@iterator"];if(n)return(n=n.call(r)).next.bind(n);if(Array.isArray(r)||(n=function(r,e){if(r){if("string"==typeof r)return t(r,e);var n={}.toString.call(r).slice(8,-1);return"Object"===n&&r.constructor&&(n=r.constructor.name),"Map"===n||"Set"===n?Array.from(r):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?t(r,e):void 0}}(r))||e&&r&&"number"==typeof r.length){n&&(r=n);var i=0;return function(){return i>=r.length?{done:!0}:{done:!1,value:r[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}exports.GachaEngine=/*#__PURE__*/function(){function t(t){var r=t.rarityRates,e=t.pools;this.pools=void 0,this.rarityRates=void 0,this.dropRateCache=new Map,this.pools=e,this.rarityRates=r,this.validateConfig()}var e=t.prototype;return e.validateConfig=function(){var t=new Set(Object.keys(this.rarityRates)),e=new Set(this.pools.map(function(t){return t.rarity})),n=[].concat(e).filter(function(r){return!t.has(r)});if(n.length>0)throw new Error("Missing rarity rates for: "+n.join(", "));for(var i,o=r(this.pools);!(i=o()).done;){var a=i.value;if(0===a.items.length)throw new Error('Rarity "'+a.rarity+'" has no items');if(a.items.reduce(function(t,r){return t+r.weight},0)<=0)throw new Error('Rarity "'+a.rarity+'" has zero total weight')}},e.getItemDropRate=function(t){if(this.dropRateCache.has(t))return this.dropRateCache.get(t);for(var e,n=r(this.pools);!(e=n()).done;){var i=e.value,o=i.items.find(function(r){return r.name===t});if(o){var a=i.items.reduce(function(t,r){return t+r.weight},0),s=o.weight/a*this.rarityRates[i.rarity];return this.dropRateCache.set(t,s),s}}throw new Error('Item "'+t+'" not found')},e.getRarityProbability=function(t){if(!this.rarityRates[t])throw new Error('Rarity "'+t+'" not found');return this.rarityRates[t]},e.getCumulativeProbabilityForItem=function(t,r){var e=this.getItemDropRate(t);return 1-Math.pow(1-e,r)},e.getRollsForTargetProbability=function(t,r){var e=this.getItemDropRate(t);return e<=0?Infinity:Math.ceil(Math.log(1-r)/Math.log(1-e))},e.getRateUpItems=function(){return 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 this.pools.flatMap(function(r){return r.items.map(function(e){return{name:e.name,dropRate:t.getItemDropRate(e.name),rarity:r.rarity}})})},e.roll=function(t){var r=this;void 0===t&&(t=1);for(var e=[],n=function(){var t=r.selectRarity(),n=r.pools.find(function(r){return r.rarity===t}),i=r.selectItemFromPool(n);e.push(i.name)},i=0;i<t;i++)n();return e},e.selectRarity=function(){for(var t=Math.random(),r=0,e=0,n=Object.entries(this.rarityRates);e<n.length;e++){var i=n[e];if(t<=(r+=i[1]))return i[0]}return Object.keys(this.rarityRates)[0]},e.selectItemFromPool=function(t){for(var e,n=t.items.reduce(function(t,r){return t+r.weight},0),i=Math.random()*n,o=0,a=r(t.items);!(e=a()).done;){var s=e.value;if(i<=(o+=s.weight))return s}return t.items[0]},t}();
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/gacha-engine.ts"],"sourcesContent":["import { RarityInput, GachaEngineConfig } from './types';\n\nexport class GachaEngine {\n private pools: RarityInput[];\n private rarityRates: Record<string, number>;\n private dropRateCache = new Map<string, number>();\n\n constructor({ rarityRates, pools }: GachaEngineConfig) {\n this.pools = pools;\n this.rarityRates = rarityRates;\n this.validateConfig();\n }\n\n private validateConfig(): void {\n const configuredRarities = new Set(Object.keys(this.rarityRates));\n const usedRarities = new Set(this.pools.map(p => p.rarity));\n const missing = [...usedRarities].filter(r => !configuredRarities.has(r));\n if (missing.length > 0) {\n throw new Error(`Missing rarity rates for: ${missing.join(', ')}`);\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 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 }\n\n getItemDropRate(name: string): number {\n if (this.dropRateCache.has(name)) {\n return this.dropRateCache.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 const totalPoolWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n const baseRarityRate = this.rarityRates[pool.rarity];\n const rate = (item.weight / totalPoolWeight) * baseRarityRate;\n this.dropRateCache.set(name, rate);\n return rate;\n }\n }\n throw new Error(`Item \"${name}\" not found`);\n }\n\n getRarityProbability(rarity: string): number {\n if (!this.rarityRates[rarity]) {\n throw new Error(`Rarity \"${rarity}\" not found`);\n }\n return this.rarityRates[rarity];\n }\n\n getCumulativeProbabilityForItem(name: string, rolls: number): number {\n const rate = this.getItemDropRate(name);\n return 1 - Math.pow(1 - rate, rolls);\n }\n\n getRollsForTargetProbability(name: string, targetProbability: number): number {\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 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 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.random();\n let cumulative = 0;\n for (const [rarity, rate] of Object.entries(this.rarityRates)) {\n cumulative += rate;\n if (rand <= cumulative) return rarity;\n }\n return Object.keys(this.rarityRates)[0];\n }\n\n private selectItemFromPool(pool: RarityInput): { name: string; weight: number } {\n const totalWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n const rand = Math.random() * totalWeight;\n let cumulative = 0;\n for (const item of pool.items) {\n cumulative += item.weight;\n if (rand <= cumulative) return item;\n }\n return pool.items[0];\n }\n}"],"names":["GachaEngine","_ref","rarityRates","pools","this","dropRateCache","Map","validateConfig","_proto","prototype","configuredRarities","Set","Object","keys","usedRarities","map","p","rarity","missing","concat","filter","r","has","length","Error","join","_iterator","_step","_createForOfIteratorHelperLoose","done","pool","value","items","reduce","sum","i","weight","getItemDropRate","name","get","_iterator2","_step2","item","find","totalPoolWeight","rate","set","getRarityProbability","getCumulativeProbabilityForItem","rolls","Math","pow","getRollsForTargetProbability","targetProbability","Infinity","ceil","log","getRateUpItems","flatMap","rateUp","getAllItemDropRates","_this","dropRate","roll","count","_this2","results","_loop","selectRarity","selectItemFromPool","push","rand","random","cumulative","_i","_Object$entries","entries","_Object$entries$_i","_step3","totalWeight","_iterator3"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function t(t,r){(null==r||r>t.length)&&(r=t.length);for(var e=0,n=Array(r);e<r;e++)n[e]=t[e];return n}function r(r,e){var n="undefined"!=typeof Symbol&&r[Symbol.iterator]||r["@@iterator"];if(n)return(n=n.call(r)).next.bind(n);if(Array.isArray(r)||(n=function(r,e){if(r){if("string"==typeof r)return t(r,e);var n={}.toString.call(r).slice(8,-1);return"Object"===n&&r.constructor&&(n=r.constructor.name),"Map"===n||"Set"===n?Array.from(r):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?t(r,e):void 0}}(r))||e&&r&&"number"==typeof r.length){n&&(r=n);var i=0;return function(){return i>=r.length?{done:!0}:{done:!1,value:r[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var e=/*#__PURE__*/function(){function t(t){var r=t.rarityRates,e=t.pools;this.pools=void 0,this.rarityRates=void 0,this.dropRateCache=new Map,this.pools=e,this.rarityRates=r,this.validateConfig()}var e=t.prototype;return e.validateConfig=function(){var t=new Set(Object.keys(this.rarityRates)),e=new Set(this.pools.map(function(t){return t.rarity})),n=[].concat(e).filter(function(r){return!t.has(r)});if(n.length>0)throw new Error("Missing rarity rates for: "+n.join(", "));for(var i,o=r(this.pools);!(i=o()).done;){var a=i.value;if(0===a.items.length)throw new Error('Rarity "'+a.rarity+'" has no items');if(a.items.reduce(function(t,r){return t+r.weight},0)<=0)throw new Error('Rarity "'+a.rarity+'" has zero total weight')}},e.getItemDropRate=function(t){if(this.dropRateCache.has(t))return this.dropRateCache.get(t);for(var e,n=r(this.pools);!(e=n()).done;){var i=e.value,o=i.items.find(function(r){return r.name===t});if(o){var a=i.items.reduce(function(t,r){return t+r.weight},0),s=o.weight/a*this.rarityRates[i.rarity];return this.dropRateCache.set(t,s),s}}throw new Error('Item "'+t+'" not found')},e.getRarityProbability=function(t){if(!this.rarityRates[t])throw new Error('Rarity "'+t+'" not found');return this.rarityRates[t]},e.getCumulativeProbabilityForItem=function(t,r){var e=this.getItemDropRate(t);return 1-Math.pow(1-e,r)},e.getRollsForTargetProbability=function(t,r){var e=this.getItemDropRate(t);return e<=0?Infinity:Math.ceil(Math.log(1-r)/Math.log(1-e))},e.getRateUpItems=function(){return 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 this.pools.flatMap(function(r){return r.items.map(function(e){return{name:e.name,dropRate:t.getItemDropRate(e.name),rarity:r.rarity}})})},e.roll=function(t){var r=this;void 0===t&&(t=1);for(var e=[],n=function(){var t=r.selectRarity(),n=r.pools.find(function(r){return r.rarity===t}),i=r.selectItemFromPool(n);e.push(i.name)},i=0;i<t;i++)n();return e},e.selectRarity=function(){for(var t=Math.random(),r=0,e=0,n=Object.entries(this.rarityRates);e<n.length;e++){var i=n[e];if(t<=(r+=i[1]))return i[0]}return Object.keys(this.rarityRates)[0]},e.selectItemFromPool=function(t){for(var e,n=t.items.reduce(function(t,r){return t+r.weight},0),i=Math.random()*n,o=0,a=r(t.items);!(e=a()).done;){var s=e.value;if(i<=(o+=s.weight))return s}return t.items[0]},t}();export{e as GachaEngine};
|
|
2
|
+
//# sourceMappingURL=index.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.module.js","sources":["../src/gacha-engine.ts"],"sourcesContent":["import { RarityInput, GachaEngineConfig } from './types';\n\nexport class GachaEngine {\n private pools: RarityInput[];\n private rarityRates: Record<string, number>;\n private dropRateCache = new Map<string, number>();\n\n constructor({ rarityRates, pools }: GachaEngineConfig) {\n this.pools = pools;\n this.rarityRates = rarityRates;\n this.validateConfig();\n }\n\n private validateConfig(): void {\n const configuredRarities = new Set(Object.keys(this.rarityRates));\n const usedRarities = new Set(this.pools.map(p => p.rarity));\n const missing = [...usedRarities].filter(r => !configuredRarities.has(r));\n if (missing.length > 0) {\n throw new Error(`Missing rarity rates for: ${missing.join(', ')}`);\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 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 }\n\n getItemDropRate(name: string): number {\n if (this.dropRateCache.has(name)) {\n return this.dropRateCache.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 const totalPoolWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n const baseRarityRate = this.rarityRates[pool.rarity];\n const rate = (item.weight / totalPoolWeight) * baseRarityRate;\n this.dropRateCache.set(name, rate);\n return rate;\n }\n }\n throw new Error(`Item \"${name}\" not found`);\n }\n\n getRarityProbability(rarity: string): number {\n if (!this.rarityRates[rarity]) {\n throw new Error(`Rarity \"${rarity}\" not found`);\n }\n return this.rarityRates[rarity];\n }\n\n getCumulativeProbabilityForItem(name: string, rolls: number): number {\n const rate = this.getItemDropRate(name);\n return 1 - Math.pow(1 - rate, rolls);\n }\n\n getRollsForTargetProbability(name: string, targetProbability: number): number {\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 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 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.random();\n let cumulative = 0;\n for (const [rarity, rate] of Object.entries(this.rarityRates)) {\n cumulative += rate;\n if (rand <= cumulative) return rarity;\n }\n return Object.keys(this.rarityRates)[0];\n }\n\n private selectItemFromPool(pool: RarityInput): { name: string; weight: number } {\n const totalWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n const rand = Math.random() * totalWeight;\n let cumulative = 0;\n for (const item of pool.items) {\n cumulative += item.weight;\n if (rand <= cumulative) return item;\n }\n return pool.items[0];\n }\n}"],"names":["GachaEngine","_ref","rarityRates","pools","this","dropRateCache","Map","validateConfig","_proto","prototype","configuredRarities","Set","Object","keys","usedRarities","map","p","rarity","missing","concat","filter","r","has","length","Error","join","_iterator","_step","_createForOfIteratorHelperLoose","done","pool","value","items","reduce","sum","i","weight","getItemDropRate","name","get","_iterator2","_step2","item","find","totalPoolWeight","rate","set","getRarityProbability","getCumulativeProbabilityForItem","rolls","Math","pow","getRollsForTargetProbability","targetProbability","Infinity","ceil","log","getRateUpItems","flatMap","rateUp","getAllItemDropRates","_this","dropRate","roll","count","_this2","results","_loop","selectRarity","selectItemFromPool","push","rand","random","cumulative","_i","_Object$entries","entries","_Object$entries$_i","_step3","totalWeight","_iterator3"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
!function(t,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports):"function"==typeof define&&define.amd?define(["exports"],r):r((t||self).AllemandiGachaEngine={})}(this,function(t){function r(t,r){(null==r||r>t.length)&&(r=t.length);for(var e=0,n=Array(r);e<r;e++)n[e]=t[e];return n}function e(t,e){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(n)return(n=n.call(t)).next.bind(n);if(Array.isArray(t)||(n=function(t,e){if(t){if("string"==typeof t)return r(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?r(t,e):void 0}}(t))||e&&t&&"number"==typeof t.length){n&&(t=n);var i=0;return function(){return i>=t.length?{done:!0}:{done:!1,value:t[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}t.GachaEngine=/*#__PURE__*/function(){function t(t){var r=t.rarityRates,e=t.pools;this.pools=void 0,this.rarityRates=void 0,this.dropRateCache=new Map,this.pools=e,this.rarityRates=r,this.validateConfig()}var r=t.prototype;return r.validateConfig=function(){var t=new Set(Object.keys(this.rarityRates)),r=new Set(this.pools.map(function(t){return t.rarity})),n=[].concat(r).filter(function(r){return!t.has(r)});if(n.length>0)throw new Error("Missing rarity rates for: "+n.join(", "));for(var i,o=e(this.pools);!(i=o()).done;){var a=i.value;if(0===a.items.length)throw new Error('Rarity "'+a.rarity+'" has no items');if(a.items.reduce(function(t,r){return t+r.weight},0)<=0)throw new Error('Rarity "'+a.rarity+'" has zero total weight')}},r.getItemDropRate=function(t){if(this.dropRateCache.has(t))return this.dropRateCache.get(t);for(var r,n=e(this.pools);!(r=n()).done;){var i=r.value,o=i.items.find(function(r){return r.name===t});if(o){var a=i.items.reduce(function(t,r){return t+r.weight},0),s=o.weight/a*this.rarityRates[i.rarity];return this.dropRateCache.set(t,s),s}}throw new Error('Item "'+t+'" not found')},r.getRarityProbability=function(t){if(!this.rarityRates[t])throw new Error('Rarity "'+t+'" not found');return this.rarityRates[t]},r.getCumulativeProbabilityForItem=function(t,r){var e=this.getItemDropRate(t);return 1-Math.pow(1-e,r)},r.getRollsForTargetProbability=function(t,r){var e=this.getItemDropRate(t);return e<=0?Infinity:Math.ceil(Math.log(1-r)/Math.log(1-e))},r.getRateUpItems=function(){return this.pools.flatMap(function(t){return t.items.filter(function(t){return t.rateUp}).map(function(t){return t.name})})},r.getAllItemDropRates=function(){var t=this;return this.pools.flatMap(function(r){return r.items.map(function(e){return{name:e.name,dropRate:t.getItemDropRate(e.name),rarity:r.rarity}})})},r.roll=function(t){var r=this;void 0===t&&(t=1);for(var e=[],n=function(){var t=r.selectRarity(),n=r.pools.find(function(r){return r.rarity===t}),i=r.selectItemFromPool(n);e.push(i.name)},i=0;i<t;i++)n();return e},r.selectRarity=function(){for(var t=Math.random(),r=0,e=0,n=Object.entries(this.rarityRates);e<n.length;e++){var i=n[e];if(t<=(r+=i[1]))return i[0]}return Object.keys(this.rarityRates)[0]},r.selectItemFromPool=function(t){for(var r,n=t.items.reduce(function(t,r){return t+r.weight},0),i=Math.random()*n,o=0,a=e(t.items);!(r=a()).done;){var s=r.value;if(i<=(o+=s.weight))return s}return t.items[0]},t}()});
|
|
2
|
+
//# sourceMappingURL=index.umd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.umd.js","sources":["../src/gacha-engine.ts"],"sourcesContent":["import { RarityInput, GachaEngineConfig } from './types';\n\nexport class GachaEngine {\n private pools: RarityInput[];\n private rarityRates: Record<string, number>;\n private dropRateCache = new Map<string, number>();\n\n constructor({ rarityRates, pools }: GachaEngineConfig) {\n this.pools = pools;\n this.rarityRates = rarityRates;\n this.validateConfig();\n }\n\n private validateConfig(): void {\n const configuredRarities = new Set(Object.keys(this.rarityRates));\n const usedRarities = new Set(this.pools.map(p => p.rarity));\n const missing = [...usedRarities].filter(r => !configuredRarities.has(r));\n if (missing.length > 0) {\n throw new Error(`Missing rarity rates for: ${missing.join(', ')}`);\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 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 }\n\n getItemDropRate(name: string): number {\n if (this.dropRateCache.has(name)) {\n return this.dropRateCache.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 const totalPoolWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n const baseRarityRate = this.rarityRates[pool.rarity];\n const rate = (item.weight / totalPoolWeight) * baseRarityRate;\n this.dropRateCache.set(name, rate);\n return rate;\n }\n }\n throw new Error(`Item \"${name}\" not found`);\n }\n\n getRarityProbability(rarity: string): number {\n if (!this.rarityRates[rarity]) {\n throw new Error(`Rarity \"${rarity}\" not found`);\n }\n return this.rarityRates[rarity];\n }\n\n getCumulativeProbabilityForItem(name: string, rolls: number): number {\n const rate = this.getItemDropRate(name);\n return 1 - Math.pow(1 - rate, rolls);\n }\n\n getRollsForTargetProbability(name: string, targetProbability: number): number {\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 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 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.random();\n let cumulative = 0;\n for (const [rarity, rate] of Object.entries(this.rarityRates)) {\n cumulative += rate;\n if (rand <= cumulative) return rarity;\n }\n return Object.keys(this.rarityRates)[0];\n }\n\n private selectItemFromPool(pool: RarityInput): { name: string; weight: number } {\n const totalWeight = pool.items.reduce((sum, i) => sum + i.weight, 0);\n const rand = Math.random() * totalWeight;\n let cumulative = 0;\n for (const item of pool.items) {\n cumulative += item.weight;\n if (rand <= cumulative) return item;\n }\n return pool.items[0];\n }\n}"],"names":["GachaEngine","_ref","rarityRates","pools","this","dropRateCache","Map","validateConfig","_proto","prototype","configuredRarities","Set","Object","keys","usedRarities","map","p","rarity","missing","concat","filter","r","has","length","Error","join","_iterator","_step","_createForOfIteratorHelperLoose","done","pool","value","items","reduce","sum","i","weight","getItemDropRate","name","get","_iterator2","_step2","item","find","totalPoolWeight","rate","set","getRarityProbability","getCumulativeProbabilityForItem","rolls","Math","pow","getRollsForTargetProbability","targetProbability","Infinity","ceil","log","getRateUpItems","flatMap","rateUp","getAllItemDropRates","_this","dropRate","roll","count","_this2","results","_loop","selectRarity","selectItemFromPool","push","rand","random","cumulative","_i","_Object$entries","entries","_Object$entries$_i","_step3","totalWeight","_iterator3"],"mappings":"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"}
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
|
|
2
1
|
export interface RarityInput {
|
|
3
2
|
rarity: string;
|
|
4
3
|
items: GachaItem[];
|
|
5
4
|
}
|
|
6
|
-
|
|
7
5
|
export interface GachaItem {
|
|
8
6
|
name: string;
|
|
9
|
-
|
|
7
|
+
weight: number;
|
|
10
8
|
rateUp?: boolean;
|
|
11
9
|
}
|
|
12
|
-
|
|
13
10
|
export interface GachaEngineConfig {
|
|
14
|
-
rarityRates
|
|
11
|
+
rarityRates: Record<string, number>;
|
|
15
12
|
pools: RarityInput[];
|
|
16
13
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allemandi/gacha-engine",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Practical, type-safe toolkit for simulating and understanding gacha rates and rate-ups.",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.module.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"unpkg": "dist/index.umd.js",
|
|
9
9
|
"source": "src/index.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.module.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
10
25
|
"repository": "https://github.com/allemandi/gacha-engine.git",
|
|
11
26
|
"author": "allemandi <69766017+allemandi@users.noreply.github.com>",
|
|
12
27
|
"license": "MIT",
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
lint:
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
steps:
|
|
12
|
-
- uses: actions/checkout@v3
|
|
13
|
-
- uses: actions/setup-node@v3
|
|
14
|
-
with:
|
|
15
|
-
node-version: 18
|
|
16
|
-
cache: 'yarn'
|
|
17
|
-
- run: yarn install --frozen-lockfile
|
|
18
|
-
- run: yarn lint
|
|
19
|
-
|
|
20
|
-
test:
|
|
21
|
-
needs: lint
|
|
22
|
-
runs-on: ubuntu-latest
|
|
23
|
-
steps:
|
|
24
|
-
- uses: actions/checkout@v3
|
|
25
|
-
- uses: actions/setup-node@v3
|
|
26
|
-
with:
|
|
27
|
-
node-version: 18
|
|
28
|
-
cache: 'yarn'
|
|
29
|
-
- run: yarn install --frozen-lockfile
|
|
30
|
-
- run: yarn test
|
|
31
|
-
|
|
32
|
-
build:
|
|
33
|
-
needs: test
|
|
34
|
-
runs-on: ubuntu-latest
|
|
35
|
-
steps:
|
|
36
|
-
- uses: actions/checkout@v3
|
|
37
|
-
- uses: actions/setup-node@v3
|
|
38
|
-
with:
|
|
39
|
-
node-version: 18
|
|
40
|
-
cache: 'yarn'
|
|
41
|
-
- run: yarn install --frozen-lockfile
|
|
42
|
-
- run: yarn build
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
name: Publish to NPM
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- 'v*.*.*'
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
publish:
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
|
|
12
|
-
steps:
|
|
13
|
-
- uses: actions/checkout@v4
|
|
14
|
-
|
|
15
|
-
- uses: actions/setup-node@v4
|
|
16
|
-
with:
|
|
17
|
-
node-version: '18'
|
|
18
|
-
registry-url: 'https://registry.npmjs.org/'
|
|
19
|
-
cache: 'yarn'
|
|
20
|
-
|
|
21
|
-
- run: yarn install --frozen-lockfile
|
|
22
|
-
- run: yarn build
|
|
23
|
-
|
|
24
|
-
- run: yarn publish --non-interactive --access public
|
|
25
|
-
env:
|
|
26
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/eslint.config.mjs
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "eslint/config";
|
|
2
|
-
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
|
3
|
-
import tsParser from "@typescript-eslint/parser";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import js from "@eslint/js";
|
|
7
|
-
import { FlatCompat } from "@eslint/eslintrc";
|
|
8
|
-
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
-
const __dirname = path.dirname(__filename);
|
|
11
|
-
const compat = new FlatCompat({
|
|
12
|
-
baseDirectory: __dirname,
|
|
13
|
-
recommendedConfig: js.configs.recommended,
|
|
14
|
-
allConfig: js.configs.all
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
export default defineConfig([{
|
|
18
|
-
|
|
19
|
-
ignores: ["dist/**"],
|
|
20
|
-
|
|
21
|
-
extends: compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"),
|
|
22
|
-
|
|
23
|
-
plugins: {
|
|
24
|
-
"@typescript-eslint": typescriptEslint,
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
languageOptions: {
|
|
28
|
-
parser: tsParser,
|
|
29
|
-
ecmaVersion: 2020,
|
|
30
|
-
sourceType: "module",
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
rules: {},
|
|
34
|
-
}]);
|
package/src/gacha-engine.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { RarityInput, GachaEngineConfig } from './types';
|
|
2
|
-
export class GachaEngine {
|
|
3
|
-
private pools: RarityInput[];
|
|
4
|
-
private rarityRates: Record<string, number>;
|
|
5
|
-
|
|
6
|
-
constructor({ rarityRates = {}, pools }: GachaEngineConfig) {
|
|
7
|
-
this.pools = pools;
|
|
8
|
-
this.rarityRates = rarityRates;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
getItemDropRate(name: string): number {
|
|
12
|
-
for (const pool of this.pools) {
|
|
13
|
-
const item = pool.items.find(i => i.name === name);
|
|
14
|
-
if (item) {
|
|
15
|
-
const totalPoolProb = pool.items.reduce((sum, i) => sum + i.probability, 0);
|
|
16
|
-
const baseRarityRate = this.rarityRates[pool.rarity] ?? totalPoolProb;
|
|
17
|
-
return (item.probability / totalPoolProb) * baseRarityRate;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
throw new Error(`Item "${name}" not found`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
getRarityProbability(rarity: string): number {
|
|
24
|
-
const pool = this.pools.find(p => p.rarity === rarity);
|
|
25
|
-
if (!pool) throw new Error(`Rarity "${rarity}" not found`);
|
|
26
|
-
|
|
27
|
-
const totalProb = pool.items.reduce((sum, i) => sum + i.probability, 0);
|
|
28
|
-
const baseRate = this.rarityRates[rarity] ?? totalProb;
|
|
29
|
-
return baseRate;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
getCumulativeProbabilityForItem(name: string, rolls: number): number {
|
|
33
|
-
const rate = this.getItemDropRate(name);
|
|
34
|
-
return 1 - Math.pow(1 - rate, rolls);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
getRollsForTargetProbability(name: string, targetProbability: number): number {
|
|
38
|
-
const rate = this.getItemDropRate(name);
|
|
39
|
-
if (rate <= 0) return Infinity;
|
|
40
|
-
return Math.ceil(Math.log(1 - targetProbability) / Math.log(1 - rate));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
getRateUpItems(): string[] {
|
|
44
|
-
return this.pools.flatMap(p =>
|
|
45
|
-
p.items.filter(i => i.rateUp).map(i => i.name)
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
getAllItemDropRates(): { name: string; dropRate: number; rarity: string }[] {
|
|
50
|
-
return this.pools.flatMap(p =>
|
|
51
|
-
p.items.map(i => ({
|
|
52
|
-
name: i.name,
|
|
53
|
-
dropRate: this.getItemDropRate(i.name),
|
|
54
|
-
rarity: p.rarity
|
|
55
|
-
}))
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { GachaEngine } from "./gacha-engine";
|
package/test/index.test.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { GachaEngine } from '../src/gacha-engine';
|
|
3
|
-
import type { RarityInput, GachaEngineConfig } from '../src/types';
|
|
4
|
-
|
|
5
|
-
const mockPools: RarityInput[] = [
|
|
6
|
-
{
|
|
7
|
-
rarity: 'common',
|
|
8
|
-
items: [
|
|
9
|
-
{ name: 'ItemA', probability: 0.5 },
|
|
10
|
-
{ name: 'ItemB', probability: 0.5 },
|
|
11
|
-
],
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
rarity: 'rare',
|
|
15
|
-
items: [
|
|
16
|
-
{ name: 'ItemC', probability: 0.7 },
|
|
17
|
-
{ name: 'ItemD', probability: 0.3, rateUp: true },
|
|
18
|
-
],
|
|
19
|
-
},
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
const config: GachaEngineConfig = {
|
|
23
|
-
rarityRates: {
|
|
24
|
-
common: 0.8,
|
|
25
|
-
rare: 0.2,
|
|
26
|
-
},
|
|
27
|
-
pools: mockPools,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
describe('GachaEngine', () => {
|
|
31
|
-
const engine = new GachaEngine(config);
|
|
32
|
-
|
|
33
|
-
describe('getItemDropRate', () => {
|
|
34
|
-
it('should return correct drop rate using rarityRates', () => {
|
|
35
|
-
expect(engine.getItemDropRate('ItemA')).toBeCloseTo(0.4); // 0.5 * 0.8
|
|
36
|
-
expect(engine.getItemDropRate('ItemD')).toBeCloseTo(0.06); // 0.3 * 0.2
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('throws if item does not exist', () => {
|
|
40
|
-
expect(() => engine.getItemDropRate('Unknown')).toThrow('Item "Unknown" not found');
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('getRarityProbability', () => {
|
|
45
|
-
it('should return correct base rarity rate', () => {
|
|
46
|
-
expect(engine.getRarityProbability('common')).toBe(0.8);
|
|
47
|
-
expect(engine.getRarityProbability('rare')).toBe(0.2);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('throws if rarity not found', () => {
|
|
51
|
-
expect(() => engine.getRarityProbability('epic')).toThrow('Rarity "epic" not found');
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe('getCumulativeProbabilityForItem', () => {
|
|
56
|
-
it('should calculate cumulative probability correctly', () => {
|
|
57
|
-
const dropRate = engine.getItemDropRate('ItemA'); // 0.4
|
|
58
|
-
const rolls = 3;
|
|
59
|
-
const expected = 1 - Math.pow(1 - dropRate, rolls);
|
|
60
|
-
expect(engine.getCumulativeProbabilityForItem('ItemA', rolls)).toBeCloseTo(expected);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe('getRollsForTargetProbability', () => {
|
|
65
|
-
it('should calculate rolls to reach target probability', () => {
|
|
66
|
-
const target = 0.9;
|
|
67
|
-
const rate = engine.getItemDropRate('ItemA'); // 0.4
|
|
68
|
-
const expected = Math.ceil(Math.log(1 - target) / Math.log(1 - rate));
|
|
69
|
-
expect(engine.getRollsForTargetProbability('ItemA', target)).toBe(expected);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('returns Infinity if drop rate is zero', () => {
|
|
73
|
-
const zeroRateEngine = new GachaEngine({
|
|
74
|
-
pools: [{
|
|
75
|
-
rarity: 'none',
|
|
76
|
-
items: [
|
|
77
|
-
{ name: 'NeverDrops', probability: 0 },
|
|
78
|
-
{ name: 'Other', probability: 1 },
|
|
79
|
-
],
|
|
80
|
-
}],
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
expect(zeroRateEngine.getRollsForTargetProbability('NeverDrops', 0.5)).toBe(Infinity);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe('getRateUpItems', () => {
|
|
88
|
-
it('returns only rate-up item names', () => {
|
|
89
|
-
expect(engine.getRateUpItems()).toEqual(['ItemD']);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('returns empty array if no rate-up items', () => {
|
|
93
|
-
const noRateUpEngine = new GachaEngine({
|
|
94
|
-
pools: [{
|
|
95
|
-
rarity: 'common',
|
|
96
|
-
items: [{ name: 'NoRateUp', probability: 1 }],
|
|
97
|
-
}],
|
|
98
|
-
});
|
|
99
|
-
expect(noRateUpEngine.getRateUpItems()).toEqual([]);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe('getAllItemDropRates', () => {
|
|
104
|
-
it('returns all items with correct drop rates and rarities', () => {
|
|
105
|
-
const results = engine.getAllItemDropRates();
|
|
106
|
-
expect(results).toContainEqual({ name: 'ItemA', dropRate: 0.4, rarity: 'common' });
|
|
107
|
-
expect(results).toContainEqual({ name: 'ItemD', dropRate: 0.06, rarity: 'rare' });
|
|
108
|
-
expect(results).toHaveLength(4);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "Node",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"outDir": "dist",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true
|
|
12
|
-
},
|
|
13
|
-
"include": ["src"],
|
|
14
|
-
"exclude": ["node_modules", "dist", "test"]
|
|
15
|
-
}
|