You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
temptress-bot/src/util/weightedList.spec.ts

101 lines
4.1 KiB

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<ItemT extends WeightableItem> = readonly [string, ItemT | WeightedList<ItemT>, ItemT, ItemT]
// type RandomTestCase<ItemT extends WeightableItem> = readonly [string, ItemT | WeightedList<ItemT>, ItemT, number, ItemT]
describe('shuffleWeightedList', () => {
test.each<NonRandomTestCase<string>>([
['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)
}
})
})