Skip to main content
Module

x/fortuna/src/limited_machine.ts

A TypeScript module for random events and gacha.
Latest
File
// 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> { #items: ComputedGachaData<T>[]; constructor(items: GachaChoice<T>[]) { this.#items = new Array(items.length); this.#configItems(items); } set items(items: GachaChoice<T>[]) { this.#items = new Array(items.length); this.#configItems(items); } /** Setup items for rolling. */ #configItems(items: GachaChoice<T>[]) { let i = 0; let cumulativeChance = 0; while (i < items.length) { cumulativeChance += items[i].chance; this.#items[i] = { 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.#items.length) { 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.#items.slice(0); 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;}