A stateful random generator that maintains a target probability distribution over a sequence of draws, providing a more stable user experience (UX) compared to pure random (less likely to get "unreasonable" streaks).
Pure random (Math.random()) can produce long streaks that feel "unfair" to users — like getting 10 heads in a row in a 50/50 coin flip, or seeing the same item appear repeatedly in a card draw.
balanced-random tracks the historical distribution and gently adjusts the probability of each outcome to prevent extreme streaks, while still maintaining the target probability over the long run.
- Built-in Typescript support
- Isomorphic package: works in Node.js and browsers
- Configurable balance factor (0 = pure random, 1 = round-robin)
- Works with any number of choices, with custom weights
npm install balanced-randomYou can also install balanced-random with pnpm, yarn, or slnpm
import { createRandomBoolean } from 'balanced-random'
// 50/50 by default
const coin = createRandomBoolean()
// Bias with probability of true (0-1); false gets the remainder
const mostlyTrue = createRandomBoolean({ true_weight: 0.8 })
// Or give both sides explicit weights (any non-negative ratio)
const loaded = createRandomBoolean({ true_weight: 8, false_weight: 2 })
for (let i = 0; i < 100; i++) {
console.log(coin.next()) // true or false
}import { createRandom } from 'balanced-random'
const loot = createRandom({
elements: [
{ value: 'common', weight: 80 },
{ value: 'rare', weight: 15 },
{ value: 'epic', weight: 4 },
{ value: 'legendary', weight: 1 },
],
})
for (let i = 0; i < 100; i++) {
console.log(loot.next()) // weighted random with balance
}import { createRandom } from 'balanced-random'
import seedrandom from 'seedrandom'
const rng = createRandom({
elements: [
{ value: 'A', weight: 1 },
{ value: 'B', weight: 1 },
],
random_generator: seedrandom('my-seed'),
})const balanced = createRandom({
elements: [
{ value: 'heads', weight: 1 },
{ value: 'tails', weight: 1 },
],
balance_factor: 0.5, // default
// 0 = pure random (no balance)
// 1 = aggressive balance (round-robin for under-represented outcomes)
})Creates a balanced random boolean generator.
Options:
| Option | Type | Default | Description |
|---|---|---|---|
true_weight |
number |
0.5 |
Weight for true. Alone, must be 0-1 (probability); with false_weight, any non-negative ratio |
false_weight |
number |
0.5 |
Weight for false. If omitted and true_weight is set, defaults to 1 - true_weight |
random_generator |
() => number |
Math.random |
Custom random number generator (returns 0-1) |
Creates a balanced random generator for any number of outcomes.
Options:
| Option | Type | Default | Description |
|---|---|---|---|
elements |
{ value: T, weight: number }[] |
required | Array of possible outcomes with their target weights |
random_generator |
() => number |
Math.random |
Custom random number generator (returns 0-1) |
balance_factor |
number (0-1) |
0.5 |
How aggressively to balance. 0 = pure random, 1 = round-robin |
Instance Properties:
| Property | Type | Description |
|---|---|---|
next() |
T |
Returns the next random value |
draw_count |
number |
Total number of draws so far |
elements |
Element[] |
Array of elements with current state |
Element Properties:
| Property | Type | Description |
|---|---|---|
value |
T |
The outcome value |
target_weight |
number |
Normalized target probability |
acc_count |
number |
How many times this element has been drawn |
draw_weight |
number |
Adjusted probability for next draw |
The algorithm tracks "owed draws" — how many times each outcome is under-represented relative to its target probability. Each call to next() runs one cycle:
- Select an outcome using the current
draw_weightvalues - Increment the selected element's
acc_count - Calculate
owe_count = target_count - acc_countfor each element (target_count = target_weight × total draws) - Blend the target probability with the owed ratio for the next draw:
draw_weight = target_weight * (1 - balance_factor) + (owe_count / total_owe) * balance_factor
- Normalize so
draw_weightsums to 1
This creates a distribution that:
- Maintains the target probability over time
- Prevents extreme streaks by favoring under-represented outcomes
- Feels more "fairly random" to users without being deterministic
This project is licensed with BSD-2-Clause
This is free, libre, and open-source software. It comes down to four essential freedoms [ref]:
- The freedom to run the program as you wish, for any purpose
- The freedom to study how the program works, and change it so it does your computing as you wish
- The freedom to redistribute copies so you can help others
- The freedom to distribute copies of your modified versions to others