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

[fix] more fixes #15

Merged
merged 5 commits into from
Sep 18, 2022
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
64 changes: 23 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# fortuna
# fortuna v2
Weighted gacha system.

## Usage
Expand All @@ -8,12 +8,11 @@ Create an item using `GachaMachine.createItem`
More weight = more common
```js
const items = [
GachaMachine.createItem("SSR cool character", 1),
GachaMachine.createItem("Kinda rare character", 3),
GachaMachine.createItem("Mob character", 5),
GachaMachine.createItem("Mob character", 5),
GachaMachine.createItem("Mob character", 5),
}
{ 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 },
]

const machine = new GachaMachine(items)
Expand Down Expand Up @@ -42,23 +41,7 @@ You probably don't need all complicated stuff. Here's a quick way to just create
(Only works on v1.1.0 and above)

```ts
import { GachaMachine } from 'https://deno.land/x/fortuna@v1.2.0/mod.ts' // wherever you are importing from.

const items = [
GachaMachine.createRollChoice("SSR cool character", 1),
GachaMachine.createRollChoice("Kinda rare character", 3),
GachaMachine.createRollChoice("Mob character", 5),
GachaMachine.createRollChoice("Mob character", 5),
GachaMachine.createRollChoice("Mob character", 5),
]

GachaMachine._roll(items) // Rolls one item from the list of items
```

### Alternatively...
`GachaMachine.createRollChoice` just returns an object with `result` and `chance`. In otherwords, it's useless code. Just supply your own object ez.
```ts
import { GachaMachine } from 'https://deno.land/x/fortuna@v1.2.0/mod.ts' // wherever you are importing from.
import { GachaMachine } from 'https://deno.land/x/fortuna/mod.ts' // wherever you are importing from.

const items = [
{ result: "SSR cool character", chance: 1 },
Expand All @@ -68,29 +51,28 @@ const items = [
{ result: "Mob character", chance: 5 },
]

GachaMachine.roll(items) // Rolls one item from the list of items
GachaMachine.rollWithLinearSearch(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.

## 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)


## What I don't like about fortuna atm
I initially made fortuna for a very specific purpose. When I later decided to make it an open-source, general-purpose gacha system, I had to make a lot of changes which ended up making a large part of the code look niche. More like, it is niche. The only thing a person would need from fortuna is the `_roll` method which I have no idea why I prefixed with an underscore.

~~Especially the `tier` and `pool` system. Have they ever been of use in any place? If any, those features only make the rest of the code worse.~~ As of v1.2.0, fortuna's earlier algorithm was replaced with a much simpler one (I realized that I was using a bunch of worthless stuff). Older algorithms can be accessed via the `failures` directory.
```ts
import { GachaMachine } from 'https://deno.land/x/fortuna/mod.ts' // wherever you are importing from.

### How to test a failure?
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 },
]

```ts
import { roll } from "https://deno.land/x/fortuna@v1.2.0/failures/roll1.ts"
GachaMachine.rollWithBinarySearch(items) // Rolls one item from the list of items using linear search.
```

You can pass an array of items of the form `{ result: ItemType, chance: number}` where `result` is the value and `chance` is the weight of the value. `ItemType` is to be passed as a type parameter to `roll`.

## 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 v2 yet).

~~So I'll be redoing a large part of the code, mainly reworking the two features I mentioned. Hence, v2 will be coming soon with breaking changes for world peace... Hopefully.~~
v1.2.0 is out with better typings and a better algorithm already. `_roll` is now `roll`.
4 changes: 2 additions & 2 deletions benches/100k_rolls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { GachaMachine as M1 } from "../history/v1.ts";

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

const items = pokemon.slice(0, 151).map((x) => ({
const items = pokemon.slice().map((x) => ({
result: x.id,
tier: x.tier === "legendary" ? 1 : x.tier === "mythic" ? 2 : 3,
chance: x.tier === "legendary" ? 11 : x.tier === "mythic" ? 1 : 25,
Expand Down Expand Up @@ -51,7 +51,7 @@ Deno.bench("Algorithm V4 _ Sub", () => {
*/
Deno.bench("Algorithm V4", () => {
for (let i = 0; i < 1e6; ++i) {
roll4(items);
roll4(items, 3595);
}
});
/*
Expand Down
11 changes: 7 additions & 4 deletions history/algorithms/v4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ import type { GachaChoice } from "../../mod.ts";
*/
export function roll<ItemType>(
choices: GachaChoice<ItemType>[],
totalChance = 0,
): GachaChoice<ItemType> {
let total = 0;
let total = totalChance;
let i = 0;
while (i < choices.length) {
total += choices[i].chance;
i += 1;
if (totalChance === 0) {
while (i < choices.length) {
total += choices[i].chance;
i += 1;
}
}
const result = Math.random() * total;
let going = 0.0;
Expand Down
74 changes: 62 additions & 12 deletions mod.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,44 +47,94 @@ class GachaMachine {
#configItems(items1) {
let i1 = 0;
let cumulativeChance = 0;
const cumulativeChanceInTier = new Uint8Array(this.maxTier + 1);
while(i1 < items1.length){
cumulativeChance += items1[i1].chance;
this.items.push({
result: items1[i1].result,
chance: items1[i1].chance,
cumulativeChance: cumulativeChance,
cumulativeChanceInTier: Atomics.add(cumulativeChanceInTier, items1[i1].tier || 1, items1[i1].chance),
tier: items1[i1].tier || 1
});
cumulativeChance += items1[i1].chance;
i1 += 1;
}
this.totalChance = cumulativeChance;
}
get(count = 1) {
const result = [];
if (count === 1) {
return [
GachaMachine.rollWithBinarySearch(this.items, this.totalChance),
];
}
const result = new Array(count);
let i = 0;
while(i < count){
result[i] = GachaMachine.rollWithBinarySearch(this.items, this.totalChance);
i += 1;
}
return result;
}
getFromTier(tiers, count = 1) {
const toRoll = [];
let i = 0;
let cumulativeChance = 0;
while(i < this.items.length){
if (tiers.includes(this.items[i].tier)) {
cumulativeChance += this.items[i].chance;
toRoll.push({
...this.items[i],
cumulativeChance
});
}
i += 1;
}
if (toRoll.length === 0) return [];
const result = [];
i = 0;
while(i < count){
result.push(this.#roll());
result.push(GachaMachine.rollWithBinarySearch(toRoll, cumulativeChance));
i += 1;
}
return result;
}
#roll() {
if (this.items.length === 1) return this.items[0].result;
const rng = Math.random() * this.totalChance;
static rollWithBinarySearch(items, totalChance) {
if (!totalChance) totalChance = items[items.length - 1].cumulativeChance;
if (items.length === 1) return items[0].result;
const rng = Math.random() * totalChance;
let lower = 0;
let max = this.items.length - 1;
let max = items.length - 1;
let mid = Math.floor((max + lower) / 2);
while(!(this.items[mid].cumulativeChance > rng && this.items[mid - 1].cumulativeChance < rng) && this.items[mid].cumulativeChance !== rng && mid != 0 && lower <= max){
if (this.items[mid].cumulativeChance < rng) {
while(mid != 0 && lower <= max){
if (items[mid].cumulativeChance > rng && items[mid - 1].cumulativeChance < rng || items[mid].cumulativeChance == rng) return items[mid].result;
if (items[mid].cumulativeChance < rng) {
lower = mid + 1;
mid = Math.floor((max + lower) / 2);
} else {
max = mid - 1;
mid = Math.floor((max + lower) / 2);
}
}
return this.items[mid].result;
return items[mid].result;
}
static rollWithLinearSearch(choices, totalChance = 0) {
let total = totalChance;
let i = 0;
if (totalChance === 0) {
while(i < choices.length){
total += choices[i].chance;
i += 1;
}
}
const result = Math.random() * total;
let going = 0.0;
i = 0;
while(i < choices.length){
going += choices[i].chance;
if (result < going) {
return choices[i].result;
}
i += 1;
}
return choices[Math.floor(Math.random() * choices.length)].result;
}
}
export { GachaMachine as GachaMachine };
Loading