import { TRarity } from "warframe-public-export-plus"; export interface IRngResult { type: string; itemCount: number; probability: number; } export const getRandomElement = (arr: readonly T[]): T | undefined => { return arr[Math.floor(Math.random() * arr.length)]; }; // Returns a random integer between min (inclusive) and max (inclusive). // https://stackoverflow.com/a/1527820 export const getRandomInt = (min: number, max: number): number => { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; }; export const generateRewardSeed = (): bigint => { const hiDword = getRandomInt(0, 0x7fffffff); const loDword = getRandomInt(0, 0xffffffff); let seed = (BigInt(hiDword) << 32n) | BigInt(loDword); if (Math.random() < 0.5) { seed *= -1n; seed -= 1n; } return seed; }; export const getRewardAtPercentage = ( pool: T[], percentage: number ): T | undefined => { if (pool.length == 0) return; const totalChance = pool.reduce((accum, item) => accum + item.probability, 0); const randomValue = percentage * totalChance; let cumulativeChance = 0; for (const item of pool) { cumulativeChance += item.probability; if (randomValue <= cumulativeChance) { return item; } } return pool[pool.length - 1]; }; export const getRandomReward = (pool: T[]): T | undefined => { return getRewardAtPercentage(pool, Math.random()); }; export const getRandomWeightedReward = ( pool: T[], weights: Record ): (T & { probability: number }) | undefined => { const resultPool: (T & { probability: number })[] = []; const rarityCounts: Record = { COMMON: 0, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 }; for (const entry of pool) { ++rarityCounts[entry.rarity]; } for (const entry of pool) { resultPool.push({ ...entry, probability: weights[entry.rarity] / rarityCounts[entry.rarity] }); } return getRandomReward(resultPool); }; export const getRandomWeightedRewardUc = ( pool: T[], weights: Record ): (T & { probability: number }) | undefined => { const resultPool: (T & { probability: number })[] = []; const rarityCounts: Record = { COMMON: 0, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 }; for (const entry of pool) { ++rarityCounts[entry.Rarity]; } for (const entry of pool) { resultPool.push({ ...entry, probability: weights[entry.Rarity] / rarityCounts[entry.Rarity] }); } return getRandomReward(resultPool); }; // ChatGPT generated this. It seems to have a good enough distribution. export const mixSeeds = (seed1: number, seed2: number): number => { let seed = seed1 ^ seed2; seed ^= seed >>> 21; seed ^= seed << 35; seed ^= seed >>> 4; return seed >>> 0; }; // Seeded RNG with identical results to the game client. Based on work by Donald Knuth. export class SRng { state: bigint; constructor(seed: bigint | number) { this.state = BigInt(seed); } randomInt(min: number, max: number): number { const diff = max - min; if (diff != 0) { this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; min += (Number(this.state >> 32n) & 0x3fffffff) % (diff + 1); } return min; } randomElement(arr: readonly T[]): T | undefined { return arr[this.randomInt(0, arr.length - 1)]; } randomElementPop(arr: T[]): T | undefined { if (arr.length != 0) { const index = this.randomInt(0, arr.length - 1); const elm = arr[index]; arr.splice(index, 1); return elm; } return undefined; } randomFloat(): number { this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; return (Number(this.state >> 38n) & 0xffffff) * 0.000000059604645; } randomReward(pool: T[]): T | undefined { return getRewardAtPercentage(pool, this.randomFloat()); } churnSeed(its: number): void { while (its--) { this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; } } shuffleArray(arr: T[]): void { for (let lastIdx = arr.length - 1; lastIdx >= 1; --lastIdx) { const swapIdx = this.randomInt(0, lastIdx); const tmp = arr[swapIdx]; arr[swapIdx] = arr[lastIdx]; arr[lastIdx] = tmp; } } shuffledArray(inarr: readonly T[]): T[] { const arr = [...inarr]; this.shuffleArray(arr); return arr; } } export const sequentallyUniqueRandomElement = ( deck: readonly T[], idx: number, lookbehind: number, seed: number = 0 ): T | undefined => { // This algorithm may modify a shuffle up to index `lookbehind + 1`. It assumes that the last `lookbehind` cards are not adjusted. if (lookbehind + 1 >= deck.length - lookbehind) { throw new Error( `this algorithm cannot guarantee ${lookbehind} unique cards in a row with a deck of size ${deck.length}` ); } const iteration = Math.trunc(idx / deck.length); const card = idx % deck.length; const currentShuffle = new SRng(mixSeeds(new SRng(iteration).randomInt(0, 100_000), seed)).shuffledArray(deck); if (card < currentShuffle.length - lookbehind) { // We are indexing before the end of the deck, so adjustments may be needed to achieve uniqueness. const window: T[] = []; { const previousShuffle = new SRng( mixSeeds(new SRng(iteration - 1).randomInt(0, 100_000), seed) ).shuffledArray(deck); for (let i = previousShuffle.length - lookbehind; i != previousShuffle.length; ++i) { window.push(previousShuffle[i]); } } // From this point on, `window.length == lookbehind` should hold. for (let i = 0; i != lookbehind; ++i) { if (window.indexOf(currentShuffle[i]) != -1) { for (let j = i; ; ++j) { // `j < currentShuffle.length - lookbehind` should hold. if (window.indexOf(currentShuffle[j]) == -1) { const tmp = currentShuffle[j]; currentShuffle[j] = currentShuffle[i]; currentShuffle[i] = tmp; break; } } } window.splice(0, 1); window.push(currentShuffle[i]); } } return currentShuffle[card]; };