@nekooftheabyss/fortuna
Version:
A Gacha-like system to roll random items with weights.
104 lines (99 loc) • 2.73 kB
text/typescript
// Copyright 2022-2024 NeTT. All rights reserved. MIT license.
/**
* Data fed to the constructor.
* The `result` property holds the result that will be returned after rolling.
* `chance` is the weight of the result.
*/
export interface GachaChoice<T> {
result: T;
chance: number;
}
/**
* Data transformed by the constructor, fed to the binary search function.
* The `result` property holds the result that will be returned after rolling.
* `cumulativeChance` is used to make it fit for binary search.
*/
export interface ComputedGachaData<T> {
result: T;
cumulativeChance: number;
}
/**
* Gacha system for rolling n distinct items
* from the weighted collection.
*/
export class LimitedGachaMachine<T> {
constructor(items: GachaChoice<T>[]) {
this.
this.
}
set items(items: GachaChoice<T>[]) {
this.
this.
}
/** Setup items for rolling. */
let i = 0;
let cumulativeChance = 0;
while (i < items.length) {
cumulativeChance += items[i].chance;
this.
result: items[i].result,
cumulativeChance: cumulativeChance,
};
i += 1;
}
}
/**
* Roll distinct items from the gacha machine.
* ```ts
* const machine = new GachaMachine(items);
* machine.get(11)
* ```
*
* However, rolling distinct items does not mutate the pool.
* The items rolled are only distinct within the `n` items.
*/
get(count: number): T[] {
if (count > this.
throw new RangeError(`count must be less than number of items in pool.`);
}
const result = new Array<T>(count);
let i = 0;
const data = this.
while (i < count) {
const res = rollWithBinarySearch(data);
result[i] = data[res].result;
data.splice(res, 1);
i += 1;
}
return result;
}
}
function rollWithBinarySearch<T>(
items: ComputedGachaData<T>[],
): number {
const totalChance = items[items.length - 1].cumulativeChance;
if (items.length === 1) return 0;
const rng = Math.random() * totalChance;
let lower = 0;
let max = items.length - 1;
let mid = (max + lower) >> 1;
while (
mid != 0 && lower <= max
) {
if (
(items[mid].cumulativeChance > rng &&
items[mid - 1].cumulativeChance < rng) ||
items[mid].cumulativeChance == rng
) return mid;
if (items[mid].cumulativeChance < rng) {
lower = mid + 1;
mid = (max + lower) >> 1;
} else {
max = mid - 1;
mid = (max + lower) >> 1;
}
}
return mid;
}