import { describe, expect, test } from '@jest/globals' import { shuffleWeightedList, WeightableItem, WeightedList } from './weightedList' import { forbiddenRNG } from './random' import { default as seedrandom } from 'seedrandom' type NonRandomTestCase = readonly [string, ItemT | WeightedList, ItemT, ItemT] // type RandomTestCase = readonly [string, ItemT | WeightedList, ItemT, number, ItemT] describe('shuffleWeightedList', () => { test.each>([ ['returns empty value for empty list', [], 'empty', 'empty'], ['returns item value for lone item', 'text', 'empty', 'text'], ['returns single item for singleton list', ['wrapped'], 'empty', 'wrapped'], ['returns single weighted item for singleton list', [{ wt: 10, v: 'found' }], 'empty', 'found'], [ 'returns lone item with nonzero weight', [ { wt: 0, v: 'zero' }, { wt: 2, v: 'goal' }, { wt: 0, v: 'none' }, ], 'empty', 'goal', ], [ 'returns lone item without zero weight', [{ wt: 0, v: 'zero' }, 'where am i', { wt: 0, v: 'none' }], 'empty', 'where am i', ], [ 'returns empty for list with all zero weights', [ { wt: 0, v: 'zero' }, { wt: 0, v: 'goal??' }, { wt: 0, v: 'none' }, ], 'its basically empty', 'its basically empty', ], [ 'returns nested singleton for list with all zero weights save nested list', [ { wt: 0, v: 'zero' }, { wt: 1, v: [{ wt: 1, v: ['goal!!!'] }] }, { wt: 0, v: 'none' }, ], 'its basically empty', 'goal!!!', ], [ 'returns nested singleton for list with all zero weights save nested list without weight', [{ wt: 0, v: 'zero' }, [{ wt: 1, v: [{ wt: 1, v: 'goal!?!' }] }], { wt: 0, v: 'none' }], 'hell', 'goal!?!', ], ])(' %s (%j with default %j -> %j)', (caseName, input, empty, result) => { expect(shuffleWeightedList(input, empty, forbiddenRNG)).toEqual(result) }) test(' chooses a value roughly evenly at random from the options for an unweighted list', () => { const input: WeightedList<'a' | 'b' | 'c' | 'd' | ''> = ['a', 'b', 'c', 'd'] const random = seedrandom('poppy') const counts = { a: 0, b: 0, c: 0, d: 0, '': 0 } const totalRounds = 10_000 for (let roundsLeft = totalRounds; roundsLeft > 0; roundsLeft -= 1) { const result = shuffleWeightedList(input, '', random) counts[result] = (counts[result] ?? 0) + 1 } expect(counts['']).toBe(0) for (const key of ['a', 'b', 'c', 'd'] as ('a' | 'b' | 'c' | 'd')[]) { // Confirm everything is between 24-26% after 10,000 rounds expect(Math.abs(25 - (100 * counts[key]) / totalRounds)).toBeLessThan(1) } }) test(' chooses a value roughly evenly at random from the options for a weighted list', () => { const input: WeightedList<'a' | 'b' | 'c' | 'd'> = [ { wt: 2, v: 'a' }, { wt: 5, v: 'b' }, { wt: 0, v: 'c' }, 'd', ] const random = seedrandom('sesame') const counts = { a: 0, b: 0, c: 0, d: 0 } const totalRounds = 10_000 for (let roundsLeft = totalRounds; roundsLeft > 0; roundsLeft -= 1) { const result = shuffleWeightedList(input, 'c', random) counts[result] = (counts[result] ?? 0) + 1 } expect(counts['c']).toBe(0) for (const [expected, key] of [ [25, 'a'], [62.5, 'b'], [12.5, 'd'], ] as [number, 'a' | 'b' | 'd'][]) { // Confirm everything is within 1% of its expected weight after 10,000 rounds expect(Math.abs(expected - (100 * counts[key]) / totalRounds)).toBeLessThan(1) } }) })