A weighted random pick chooses one item from a list where each item's chance of being selected is proportional to its weight: an item with weight 3 is picked three times as often as one with weight 1. It is the machinery behind loot drops in games, spreading traffic across servers by capacity, and choosing an A/B test variant by rollout percentage. The standard technique — sometimes called roulette-wheel selection — turns the weights into cumulative sums, then draws a single random point and looks up which item owns it. You will implement it with an injectable random source so the picks are deterministic under test.
weightedRandom(
items: T[], // the values to choose from
weights: number[], // weights[i] is the relative chance of items[i]
rng?: () => number // returns a float in [0, 1); defaults to Math.random
): T // one element of items
// weights [3, 1, 2] give a, b, c the slices [0,3), [3,4), [4,6) of a 0..6 line.
weightedRandom(['a', 'b', 'c'], [3, 1, 2], () => 0); // 'a'
weightedRandom(['a', 'b', 'c'], [3, 1, 2], () => 0.62); // 'b' (point 3.72)
// A zero weight means "never pick me":
weightedRandom(['x', 'y', 'z'], [1, 0, 1], () => 0.5); // 'z', never 'y'
// Invalid inputs throw instead of guessing:
weightedRandom(['a'], [1, 2]); // lengths differ
weightedRandom(['a', 'b'], [0, 0]); // weights sum to zero
[1, 2] and [10, 20] give the same 1-to-2 odds. They do not need to sum to 1.NaN, or Infinity weight throws, and so does a set that sums to zero (there would be nothing to pick).items and weights must be arrays of the same, non-zero length.rng returning a float in [0, 1) to force a specific pick; it defaults to Math.random. You may assume rng stays in range, as Math.random does.