@allemandi/gacha-engine
Version:
Practical, type-safe toolkit for simulating and understanding gacha rates and rate-ups.
317 lines (257 loc) โข 8.59 kB
Markdown
# ๐ @allemandi/gacha-engine
[](https://www.npmjs.com/package/@allemandi/gacha-engine)
[](https://github.com/allemandi/gacha-engine/blob/main/LICENSE)
> **Practical, type-safe toolkit for simulating and understanding gacha rates and rate-ups.**
> Supports `"weighted"` and `"flatRate"` modes for different gacha strategies.
> Works in Node.js and browsers โ supports ESM, CommonJS, and UMD.
---
<!-- omit from toc -->
## ๐ Table of Contents
- [โจ Features](#-features)
- [๐ ๏ธ Installation](#๏ธ-installation)
- [๐ Quick Usage Examples](#-quick-usage-examples)
- [๐ API](#-api)
- [Constructor](#constructor)
- [Methods](#methods)
- [๐งช Tests](#-tests)
- [๐ Related Projects](#-related-projects)
- [๐ค Contributing](#-contributing)
---
## โจ Features
- ๐ฒ **Roll simulation** โ Perform gacha rolls with weighted or flat-rate logic
- ๐ **Probability analysis** โ Drop rates, cumulative probabilities, target probabilities
- ๐ **Multi-rarity support** โ Flexible rarity-based or item-based probability distributions
- โก **Performance optimized** โ Efficient with cached calculations
- ๐ก๏ธ **Type-safe** โ Written in TypeScript with strict configuration validation
---
## ๐ ๏ธ Installation
```bash
# Yarn
yarn add @allemandi/gacha-engine
# NPM
npm install @allemandi/gacha-engine
```
## ๐ Quick Usage Examples
**ESM (Weighted Mode)**
```js
import { GachaEngine } from '@allemandi/gacha-engine';
const pools = [
{
rarity: 'SSR',
items: [
{ name: 'Super Hobo', weight: 1.0, rateUp: true },
{ name: 'Broke King', weight: 0.5 },
{ name: 'Cardboard Hero', weight: 0.5 }
]
},
{
rarity: 'SR',
items: [
{ name: 'Cold Salaryman', weight: 2.5, rateUp: true },
{ name: 'Numb Artist', weight: 1.25 },
{ name: 'Crying Cook', weight: 1.25 }
]
},
{
rarity: 'R',
items: [
{ name: 'Regular Joe', weight: 5.0 },
{ name: 'Normal Person', weight: 5.0 }
]
}
];
const rarityRates = {
SSR: 0.01,
SR: 0.05,
R: 0.94
};
const engine = new GachaEngine({ mode: 'weighted', pools, rarityRates });
console.log('10 rolls:', engine.roll(10).join(', '));
const rate = engine.getItemDropRate('Super Hobo');
console.log('Drop rate for Super Hobo:', (rate * 100) + '%');
// (1.0 / 2.0) * 0.01 = 0.005 โ 0.5%
const cumulative = engine.getCumulativeProbabilityForItem('Super Hobo', 300);
console.log('Probability in 300 rolls:', (cumulative * 100).toFixed(1) + '%');
// ~77.8%
console.log('Rolls for 50% chance:', engine.getRollsForTargetProbability('Super Hobo', 0.5));
// 139
console.log('Rate-up items:', engine.getRateUpItems().join(', '));
// Super Hobo, Cold Salaryman
```
**CommonJS (Flat Rate Mode)**
```js
const { GachaEngine } = require('@allemandi/gacha-engine');
const pools = [
{
rarity: 'SSR',
items: [
{ name: 'Superior Rat', weight: 0.008, rateUp: true },
{ name: 'Dumpster King', weight: 0.002 }
]
},
{
rarity: 'SR',
items: [
{ name: 'Sleepy Chef', weight: 0.04 }
]
},
{
rarity: 'R',
items: [
{ name: 'Unknown Student', weight: 0.95 }
]
}
];
const engine = new GachaEngine({ mode: 'flatRate', pools });
console.log('Roll x5:', engine.roll(5).join(', '));
const dropRate = engine.getItemDropRate('Superior Rat');
console.log('Drop rate for Superior Rat:', (dropRate * 100) + '%');
// 0.8%
const cumulative = engine.getCumulativeProbabilityForItem('Superior Rat', 200);
console.log('Chance after 200 rolls:', (cumulative * 100).toFixed(1) + '%');
// 79.9%
const rollsFor50 = engine.getRollsForTargetProbability('Superior Rat', 0.5);
console.log('Rolls for 50% chance:', rollsFor50);
// 87
console.log('Rate-up items:', engine.getRateUpItems().join(', '));
// Superior Rat
```
**UMD (Browser, Weighted Mode)**
```html
<script src="https://unpkg.com/@allemandi/gacha-engine"></script>
<script>
const { GachaEngine } = AllemandiGachaEngine;
const engine = new GachaEngine({
mode: 'weighted',
rarityRates: {
SSR: 0.02,
SR: 0.08,
R: 0.90
},
pools: [
{
rarity: 'SSR',
items: [
{ name: 'Trash Wizard', weight: 0.5 },
{ name: 'Park Master', weight: 1.5, rateUp: true }
]
},
{
rarity: 'SR',
items: [
{ name: 'Street Sweeper', weight: 1.0 },
{ name: 'Bench Philosopher', weight: 3.0, rateUp: true }
]
},
{
rarity: 'R',
items: [
{ name: 'Bus Stop Ghost', weight: 5.0 }
]
}
]
});
const rate = engine.getItemDropRate('Park Master');
const rolls = engine.getRollsForTargetProbability('Park Master', 0.75);
const cumulative = engine.getCumulativeProbabilityForItem('Park Master', 200);
console.log('1x Roll:', engine.roll());
console.log('Drop rate for Park Master:', (rate * 100).toFixed(2) + '%');
// 1.5 / 2.0 * 0.02 = 0.015 โ 1.50%
console.log('Cumulative 200 rolls:', (cumulative * 100).toFixed(1) + '%');
// "95.1%"
console.log('Rolls for 75% chance:', rolls);
// 92
console.log('Rate-up items:', engine.getRateUpItems().join(', '));
// "Park Master, Bench Philosopher"
console.log('All items:', engine.getAllItemDropRates().map(i => i.name));
// ["Trash Wizard", "Park Master", "Street Sweeper", "Bench Philosopher", "Bus Stop Ghost"]
</script>
```
## ๐ API
### Constructor
`new GachaEngine(config: GachaEngineConfig)`
Creates a new GachaEngine instance with validation.
**Config Options:**
- Weighted Mode
```ts
{
mode: 'weighted'; // (default)
rarityRates: Record<string, number>; // Required: must sum to 1.0
pools: Array<{
rarity: string; // Must match a key in `rarityRates`
items: Array<{
name: string;
weight: number;
rateUp?: boolean;
}>
}>
}
```
- Flat Rate Mode
```ts
{
mode: 'flatRate';
pools: Array<{
rarity: string; // Used only for categorization
items: Array<{
name: string;
weight: number; // Interpreted as direct probability (must sum to 1.0 across all items)
rateUp?: boolean;
}>
}>
}
```
### Methods
#### Rolling
`roll(count?: number): string[]`
- Simulate gacha rolls and returns item names
- `count`: Number of rolls to perform (default: 1)
- Returns array of item names
#### Analysis
`getItemDropRate(name: string): number`
- Returns the effective drop rate for a specific item
- In weighted mode:
- Computed as `dropRate = (item.weight / totalPoolWeight) ร rarityBaseRate`
- In flat rate mode:
- `Returns the item's defined probability.
- Throws if the item does not exist.
`getRarityProbability(rarity: string): number`
- Returns the base probability for a given rarity tier
- Only in "weighted" mode.
- Throws in flatRate mode.
`getCumulativeProbabilityForItem(name: string, rolls: number): number`
- Calculates probability of getting the item at least once in N rolls
- Uses formula: `1 - (1 - dropRate)^rolls`
`getRollsForTargetProbability(name: string, targetProbability: number): number`
- Calculates the minimum number of rolls needed to reach a specific probability of pulling a given item.
- Returns `Infinity` if item has zero drop rate
- Returns 1 if target probability โฅ 1.0
#### Utility
`getRateUpItems(): string[]`
- Returns names of all items marked with `rateUp: true`
`getAllItemDropRates(): Array<{name: string, dropRate: number, rarity: string}>`
- Returns a list of all items with:
- name: Item name
- dropRate: Calculated drop probability
- rarity: Associated rarity (or "flatRate" in flat mode)
## ๐งช Tests
> Available in the GitHub repo only.
```bash
# Run the test suite with Vitest
yarn test
# or
npm test
```
## ๐ Related Projects
Check out these related projects that might interest you:
**[@allemandi/embed-utils](https://github.com/allemandi/embed-utils)**
- Fast, type-safe utilities for vector embedding comparison and search.
**[Embed Classify CLI](https://github.com/allemandi/embed-classify-cli)**
- Node.js CLI tool for local text classification using word embeddings.
## ๐ค Contributing
If you have ideas, improvements, or new features:
1. Fork the project
2. Create your feature branch (git checkout -b feature/amazing-feature)
3. Commit your changes (git commit -m 'Add some amazing feature')
4. Push to the branch (git push origin feature/amazing-feature)
5. Open a Pull Request