Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] A few small improvements #22

Merged
merged 8 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 65 additions & 73 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,125 +1,117 @@
# fortuna v2

Weighted gacha system.

## Usage

Create an item using `GachaMachine.createItem`

More weight = more common
More weight = more common. Think of it as in terms of probability.

```js

import { GachaMachine } from "https://deno.land/x/fortuna/mod.ts"
// or
import { GachaMachine } from "https://deno.land/x/fortuna/dist/fortuna.js"

const items = [
{ result: "SSR cool character", chance: 1 },
{ result: "Kinda rare character", chance: 3 },
{ result: "Mob character", chance: 5 },
{ result: "Mob character", chance: 5 },
{ result: "Mob character", chance: 5 },
]
{ result: "Mob character #1", chance: 5 },
{ result: "Mob character #2", chance: 5 },
{ result: "Mob character #3", chance: 5 },


const machine = new GachaMachine(items)

machine.get(10) // Rolls 10x
machine.get(10) // Rolls 10x

/*
My result:
[
"Kinda rare character",
"Mob character",
"Mob character",
"Mob character",
"Mob character",
"Mob character #1",
"Mob character #3",
"Mob character #3",
"Mob character #1",
"Kinda rare character",
"Mob character",
"Mob character",
"Mob character",
"Mob character"
"Mob character #2",
"Mob character #2" ,
"Mob character #1",
"Mob character #2"
]
*/
```

### Plain weighted random selection

You probably don't need all complicated stuff. Here's a quick way to just create a simple weight-based gacha system:
(Only works on v1.1.0 and above)
(Only works on v3.0.1 and above)

```ts
import { GachaMachine } from 'https://deno.land/x/fortuna/mod.ts' // wherever you are importing from.
import { roll } from "https://deno.land/x/fortuna/src/roll.ts"; // wherever you are importing from.

const items = [
{ result: "SSR cool character", chance: 1 },
{ result: "Kinda rare character", chance: 3 },
{ result: "Mob character", chance: 5 },
{ result: "Mob character", chance: 5 },
{ result: "Mob character", chance: 5 },
]
{ result: "Mob character #1", chance: 5 },
{ result: "Mob character #2", chance: 5 },
{ result: "Mob character #3", chance: 5 },
];

GachaMachine.rollWithLinearSearch(items) // Rolls one item from the list of items using linear search.
roll(items); // Rolls one item from the list of items using linear search.
```

`GachaMachine#get()` works using Binary Search by default. Using the Binary Search method explicitly requires a different structure of data for input.
You can also provide two individual arrays for choices and weights.

```ts
import { GachaMachine } from 'https://deno.land/x/fortuna/mod.ts' // wherever you are importing from.
import { roll } from "https://deno.land/x/fortuna/src/roll.ts"; // wherever you are importing from.

const items = [
{ result: "SSR cool character", cumulativeChance: 1 },
{ result: "Kinda rare character", cumulativeChance: 4 },
{ result: "Mob character", cumulativeChance: 9 },
{ result: "Mob character", cumulativeChance: 14 },
{ result: "Mob character", cumulativeChance: 19 },
]

GachaMachine.rollWithBinarySearch(items) // Rolls one item from the list of items using linear search.
"SSR cool character",
"Kinda rare character",
"Mob character #1",
"Mob character #2",
"Mob character #3",
];
const chances = [1, 3, 5, 5, 5];

roll(items, chances); // Rolls one item from the list of items using linear search.
```

## How it works

Fortuna has two algorithms, one being a static method in the `GachaMachine` class and one
being a standalone function.

### Algorithm 1

Fortuna provides a simple function `roll` that generates a pseudo random number
and performs linear search on the provided data in order to find the proper item.
Providing the total chance may result in faster rolls.

```ts
// JS PSEUDOCODE

// Each choice is an object with
// chance: number
// result: T (type parameter)
//
// total chance can be supplied
// manually or computed from data
function roll(choices, total) {
// runs a loop to compute total
if(!total) total = sum_by_chance(data)

// generate random number for choosing
let rng = random_number(0, total)

// run a loop to find the choice
let current = 0.0;
for (choice in data) {
current = current + choice.chance;
if(rng < current) return choice.result;
}
}
```
import { roll } from "https://deno.land/x/fortuna/src/roll.ts"; // wherever you are importing from.

Fortuna's default `GachaMachine.get()` uses a more complex, but faster approach.

- When the `GachaMachine` class is instantiated, data is transformed into a form
suitable for binary search.
const items = [
{ result: "SSR cool character", chance: 1 },
{ result: "Kinda rare character", chance: 3 },
{ result: "Mob character #1", chance: 5 },
{ result: "Mob character #2", chance: 5 },
{ result: "Mob character #3", chance: 5 },
];

- When `GachaMachine.get()` is run, Fortuna performs binary search on this transformed
data.
roll(items, 19); // Rolls one item from the list of items using linear search.
```

The `roll` function is more suitable when the weighted data is used only once.
```ts
import { roll } from "https://deno.land/x/fortuna/src/roll.ts"; // wherever you are importing from.

The `get` method is suitable when the weighted data is reused. It comes at extra cost
during initialization but compensates for it with better performance when sampling.
const items = [
"SSR cool character",
"Kinda rare character",
"Mob character #1",
"Mob character #2",
"Mob character #3",
];
const chances = [1, 3, 5, 5, 5];

roll(items, chances, 19); // Rolls one item from the list of items using linear search.
```

## Documentation

Documentation for the latest version can be found in [https://doc.deno.land/https://deno.land/x/fortuna/mod.ts](https://doc.deno.land/https://deno.land/x/fortuna/mod.ts)

A guide for usage can be found in [docs.nekooftheabyss.moe](https://docs.nekooftheabyss.moe/fortuna) (not updated for v3 yet).

70 changes: 0 additions & 70 deletions benches/100k_rolls.ts

This file was deleted.

51 changes: 0 additions & 51 deletions benches/1_roll.ts

This file was deleted.

33 changes: 21 additions & 12 deletions benches/comparison.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
* Total number of items in pool: 905
*/

import { GachaMachine } from "../history/fortuna_v2.ts";
import { GachaMachine as GM } from "../mod.ts";
import { GachaMachine, roll } from "../mod.ts";

import pokemon from "../testdata/pokemon.json" assert { type: "json" };

Expand All @@ -29,7 +28,7 @@ const itemsForPicker = items.map((x) => ({
}));
const itemsForRWC = items.map((x) => ({ id: x.result, weight: x.chance }));

console.log(items.reduce((acc, a) => acc + a.chance, 0), items.length);
const totalChance = items.reduce((acc, a) => acc + a.chance, 0);

/**
* No op
Expand Down Expand Up @@ -76,16 +75,26 @@ Deno.bench("Balastrong/wrand", { group: "gacha" }, () => {
* Initializing class is done in the bench too
* to avoid bias.
*/
Deno.bench("retraigo/fortuna v2", { group: "gacha" }, () => {
Deno.bench("retraigo/fortuna", { baseline: true, group: "gacha" }, () => {
const machine = new GachaMachine(items);
machine.get(20);
machine.get(1e4);
});

/**
* Initializing class is done in the bench too
* to avoid bias.
/**
* Plain rolls without requiring any setup
*/
Deno.bench("retraigo/fortuna v3", { baseline: true, group: "gacha" }, () => {
const machine = new GM(items);
machine.get(20);
});
Deno.bench("retraigo/fortuna/roll", { group: "gacha" }, () => {
for (let i = 0; i < 1e4; ++i) {
roll(items);
}
});

/**
* Plain rolls without requiring any setup
* with totalChance
*/
Deno.bench("retraigo/fortuna/roll (with totalChance)", { group: "gacha" }, () => {
for (let i = 0; i < 1e4; ++i) {
roll(items, totalChance);
}
});
19 changes: 19 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading