Exact outcome mapping
Binary choices and coin flips
A single flip requests one unsigned byte and uses its least-significant bit. Even byte values map to the first configured label and odd byte values map to the second. Because a byte contains 128 even and 128 odd values, the mapping is balanced. Batch flips request packed bytes and read their bits least-significant first; every extracted bit is still an independent binary sample from the source.
Dice and bounded integers
Dice use an unsigned 32-bit little-endian value. When 232 is not divisible by the number of faces, values in the incomplete upper bucket are rejected and a fresh value is requested. An accepted value is reduced modulo the face count, then one is added for the displayed die result. This avoids the modulo bias produced by mapping every possible 32-bit value directly.
Weighted choices
The weighted coin is the only tool that is not an even split, and it is the only one that accepts a probability. It draws an unbiased integer from 0 through 99 with the same rejection sampling as dice, then reports the first label when that integer is below the configured percentage. Applying the threshold to an exact integer rather than to a floating-point value means the long-run rate is exactly the configured ratio. A 0% or 100% configuration is not offered, and certainty is resolved without drawing randomness at all so it cannot be merely near-certain.
Custom labels
Editing labels changes only the displayed names; it does not change either configured side's probability. Every other tool on the site is an even split.
Selection is separate from presentation
The browser selects the result before starting an animation timer. Reduced-motion mode, the visible animation toggle, CSS failure, rendering speed, and the moment a user clicks cannot alter that selected value. The result is also exposed as text and through an ARIA live status, so the coin face is not the only indicator.
What the tests cover
- Exact binary boundaries using deterministic bytes 0, 1, 254, and 255.
- Lower and upper accepted values for D4, D6, D8, D10, D12, and D20.
- Rejection of the incomplete upper modulo bucket before a value is accepted.
- The weighted threshold on both sides of the boundary, and certain configurations resolved without consuming randomness.
- An exhaustive deterministic byte fixture with exactly balanced extracted bits.
- A 10,000-result batch with length, binary range, and total checks.
- Series completion, cumulative proportions, deviation, and streak analysis.
- Explicit failure when Web Crypto is unavailable; no
Math.random()fallback.
Every check above runs in the release gate, and a failure blocks the deployment. The complete module those tests cover is published further down this page.
The complete selection module
This is the entire outcome source for every tool on the site, read straight from src/lib/random.ts when this page was built. Nothing else on the site produces a coin, dice, weighted, or experiment result, so the listing cannot drift from the deployed behaviour.
/**
* Stable identifier for result metadata and exported experiment data.
* Increment the version whenever the source or outcome mapping changes.
*/
export const RANDOM_METHOD_ID = 'web-crypto-getRandomValues-v1' as const;
export const RANDOM_METHOD_VERSION = 1 as const;
export type RandomBit = 0 | 1;
/** A replaceable byte source keeps boundary tests deterministic. */
export interface RandomByteSource {
fill(target: Uint8Array): void;
}
export class RandomSourceUnavailableError extends Error {
constructor() {
super(
'Cryptographically strong randomness is unavailable. This tool requires Web Crypto and does not fall back to Math.random().',
);
this.name = 'RandomSourceUnavailableError';
}
}
export function isWebCryptoAvailable(): boolean {
return Boolean(
globalThis.crypto &&
typeof globalThis.crypto.getRandomValues === 'function',
);
}
/**
* The production source is resolved lazily so server rendering can import this
* module without requiring a browser global.
*/
export const webCryptoByteSource: RandomByteSource = {
fill(target) {
const cryptoApi = globalThis.crypto;
if (!isWebCryptoAvailable() || !cryptoApi) {
throw new RandomSourceUnavailableError();
}
cryptoApi.getRandomValues(target);
},
};
const UINT32_RANGE = 0x1_0000_0000;
const WEB_CRYPTO_MAX_BYTES_PER_CALL = 65_536;
function fillBytes(target: Uint8Array, source: RandomByteSource): void {
for (
let offset = 0;
offset < target.length;
offset += WEB_CRYPTO_MAX_BYTES_PER_CALL
) {
source.fill(
target.subarray(
offset,
Math.min(offset + WEB_CRYPTO_MAX_BYTES_PER_CALL, target.length),
),
);
}
}
function readUint32LittleEndian(bytes: Uint8Array): number {
return (
bytes[0] +
bytes[1] * 0x100 +
bytes[2] * 0x1_0000 +
bytes[3] * 0x100_0000
);
}
/** Return an unbiased binary value. */
export function randomBit(
source: RandomByteSource = webCryptoByteSource,
): RandomBit {
const bytes = new Uint8Array(1);
fillBytes(bytes, source);
return (bytes[0] & 1) as RandomBit;
}
/**
* Return an integer in [0, maxExclusive) without modulo bias.
*
* Values in the incomplete upper portion of the uint32 range are rejected, so
* every accepted output has exactly the same number of source values.
*/
export function randomInt(
maxExclusive: number,
source: RandomByteSource = webCryptoByteSource,
): number {
if (
!Number.isSafeInteger(maxExclusive) ||
maxExclusive < 1 ||
maxExclusive > UINT32_RANGE
) {
throw new RangeError(
`maxExclusive must be an integer from 1 through ${UINT32_RANGE}.`,
);
}
const acceptanceLimit =
UINT32_RANGE - (UINT32_RANGE % maxExclusive);
const bytes = new Uint8Array(4);
while (true) {
fillBytes(bytes, source);
const value = readUint32LittleEndian(bytes);
if (value < acceptanceLimit) {
return value % maxExclusive;
}
}
}
/**
* Return 1 with probability `favourableOutcomes / totalOutcomes`, and 0
* otherwise, with no bias beyond that configured ratio.
*
* The ratio is exact because it is applied to an unbiased bounded integer
* rather than to a floating-point value: a threshold comparison on
* `randomInt(totalOutcomes)` cannot round a probability the way `< 0.3` on a
* float can.
*/
export function randomWeightedBit(
favourableOutcomes: number,
totalOutcomes: number,
source: RandomByteSource = webCryptoByteSource,
): RandomBit {
if (!Number.isSafeInteger(totalOutcomes) || totalOutcomes < 1) {
throw new RangeError('totalOutcomes must be a positive safe integer.');
}
if (
!Number.isSafeInteger(favourableOutcomes) ||
favourableOutcomes < 0 ||
favourableOutcomes > totalOutcomes
) {
throw new RangeError(
'favourableOutcomes must be an integer from 0 through totalOutcomes.',
);
}
// Certain outcomes must not consume randomness, so that a 0% or 100%
// configuration is exactly certain rather than merely overwhelmingly likely.
if (favourableOutcomes === 0) return 0;
if (favourableOutcomes === totalOutcomes) return 1;
return randomInt(totalOutcomes, source) < favourableOutcomes ? 1 : 0;
}
/**
* Sample many independent binary values with one packed byte allocation and
* the minimum practical number of Web Crypto calls.
*/
export function sampleMany(
count: number,
source: RandomByteSource = webCryptoByteSource,
): Uint8Array {
if (!Number.isSafeInteger(count) || count < 0) {
throw new RangeError('count must be a non-negative safe integer.');
}
if (count === 0) {
return new Uint8Array();
}
const packedBytes = new Uint8Array(Math.ceil(count / 8));
const samples = new Uint8Array(count);
fillBytes(packedBytes, source);
for (let index = 0; index < count; index += 1) {
samples[index] =
(packedBytes[index >> 3] >> (index & 7)) & 1;
}
return samples;
}Browser and security assumptions
The method relies on a current browser, a secure context, a working operating-system random source, and an uncompromised device and page. Web Crypto is designed for cryptographic use, but this site does not expose the browser's internal entropy source, produce a signed transcript, commit to a value before reveal, or let another party independently reproduce a particular result.
Important limitations
- “Cryptographically strong” does not mean physically or atmospherically random.
- A 50/50 configuration does not guarantee short-run balance or alternating outcomes.
- Client-side results can be changed by a compromised device, browser extension, or modified source.
- This tool is not independently audited evidence for disputes, gambling, legal drawings, prizes, or regulated use.
- Do not use random output instead of informed consent, safety judgment, or professional advice.
Corrections and security reports
This site is built and maintained by Wayde Lyle. Report a reproducible correctness problem or a suspected randomness defect through the maintainer's public profile. Include the browser, the exact steps, and what you observed. For a sensitive vulnerability, ask for a private channel first rather than publishing exploit details.