A fair coin flip returns true half the time. A weighted coin flip returns true a chosen fraction of the time — 10%, 25%, whatever the simulation or generative-art piece needs. The function is one line built on Math.random(); the useful part is confirming the true rate tracks the weight and that the boundaries behave. This tutorial writes it as an ES module, pins the behavior with a Node test suite, prints a distribution at 10%, and paints the results on a browser canvas.
Prerequisites
- Node.js 18 or later (validated with Node 26.4.0)
- Python 3 for the local static server (
python3ships with the Xcode Command Line Tools on macOS) make(pre-installed on macOS and most Linux distributions)- A Unix-like terminal (macOS, Linux, or WSL on Windows)
No third-party npm packages are used. For the even-odds version this generalizes, see How to Create a Random Boolean Function in JavaScript (Node and Browser).
Step 1: Create the project and its package file
The "type": "module" field lets Node treat .js files as ES modules, so import/export work with no build step.
Create the file
mkdir -p js-weighted-coinflip-101
cd js-weighted-coinflip-101
touch package.json
Add the code: js-weighted-coinflip-101/package.json
{
"name": "js-weighted-coinflip-101",
"version": "1.0.0",
"description": "A weighted random boolean function for JavaScript (Node and browser).",
"type": "module",
"scripts": {
"demo": "node demo.js",
"test": "node --test"
},
"license": "MIT"
}
Detailed breakdown
"type": "module"switches the package to ES modules; without it,importthrowsSyntaxError: Cannot use import statement outside a module.- The
demoandtestscripts alias the two commands used later; there are no runtime dependencies.
Step 2: Write the weightedCoinFlip function
Create the file
touch weighted-coinflip.js
Add the code: js-weighted-coinflip-101/weighted-coinflip.js
// File: weighted-coinflip.js
// Return true with probability `weight` (0.0 to 1.0), otherwise false.
export const weightedCoinFlip = (weight) => Math.random() <= weight;
Detailed breakdown
weightis a probability from0.0to1.0:0.1means true about 10% of the time.Math.random()returns a float in[0, 1). The comparison is true whenever the draw falls at or belowweight, and since draws are uniform across[0, 1), the fraction that satisfy it isweightitself.- The result is a real boolean (
true/false), which reads as1/0when used numerically. Edge cases:weight = 1is always true (every draw is< 1), andweight = 0is effectively always false. Step 3’s tests pin these down.
Step 3: Pin the behavior with tests
Node’s built-in test runner (node --test, stable since Node 20) covers the parts that must always hold, with no framework to install.
Create the file
mkdir -p test
touch test/weighted-coinflip.test.js
Add the code: js-weighted-coinflip-101/test/weighted-coinflip.test.js
// File: test/weighted-coinflip.test.js
// Runs with the built-in Node test runner: `node --test`.
import { test } from "node:test";
import assert from "node:assert/strict";
import { weightedCoinFlip } from "../weighted-coinflip.js";
// Run `fn` with Math.random pinned to `value`, then restore it.
function withRandom(value, fn) {
const original = Math.random;
Math.random = () => value;
try {
return fn();
} finally {
Math.random = original;
}
}
test("returns a boolean", () => {
assert.equal(typeof weightedCoinFlip(0.3), "boolean");
});
test("a draw at or below the weight is true", () => {
assert.equal(withRandom(0.05, () => weightedCoinFlip(0.1)), true);
assert.equal(withRandom(0.1, () => weightedCoinFlip(0.1)), true); // boundary: <=
});
test("a draw above the weight is false", () => {
assert.equal(withRandom(0.5, () => weightedCoinFlip(0.1)), false);
});
test("weight 1.0 is always true", () => {
for (let i = 0; i < 1000; i++) {
assert.equal(weightedCoinFlip(1), true);
}
});
test("both outcomes appear at weight 0.5 over many flips", () => {
const seen = new Set(Array.from({ length: 500 }, () => weightedCoinFlip(0.5)));
assert.deepEqual([...seen].sort(), [false, true]);
});
Detailed breakdown
withRandomswapsMath.randomfor a stub returning a fixed draw, then restores it in afinallyblock so the stub cannot leak into a later test.- The boundary test confirms the comparison is
<=: a draw exactly equal to the weight (0.1at weight0.1) counts as true, while a draw above it is false. weight = 1is asserted true across 1000 calls, since every draw in[0, 1)is at or below1. The last test uses a realSetover 500 draws at weight0.5to confirm both outcomes occur.
Run the suite:
node --test
✔ returns a boolean
✔ a draw at or below the weight is true
✔ a draw above the weight is false
✔ weight 1.0 is always true
✔ both outcomes appear at weight 0.5 over many flips
ℹ tests 5
ℹ pass 5
ℹ fail 0
Step 4: Print the distribution at 10%
Run many weighted flips and count the outcomes. At weight 0.1, roughly a tenth should come back true.
Create the file
touch demo.js
Add the code: js-weighted-coinflip-101/demo.js
// File: demo.js
// Flip a weighted coin 100 times at 10% and count true vs false.
import { weightedCoinFlip } from "./weighted-coinflip.js";
const LIMIT = 100;
const WEIGHT = 0.1;
const flips = Array.from({ length: LIMIT }, () => weightedCoinFlip(WEIGHT));
console.log(flips);
const occurrences = flips.reduce((acc, value) => {
acc[value] = (acc[value] || 0) + 1;
return acc;
}, {});
console.log(occurrences);
Detailed breakdown
WEIGHT = 0.1biases each flip toward false; changing it re-biases the whole run.- The
reducetalliestrueandfalseinto a counts object that sums toLIMIT.
Run it:
node demo.js
The output varies on every run. One run produced:
[
false, true, false, false, false, true, false, false,
false, false, false, false, false, false, false, true,
false, true, true, false, false, false, false, false,
false, false, false, true, false, false, false, false,
false, false, false, false, false, false, false, false,
false, false, false, false, true, false, false, true,
false, false, false, false, false, false, false, true,
false, false, false, false, false, false, false, false,
false, false, false, false, true, true, false, false,
false, false, false, true, false, false, false, false,
false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, true,
false, false, true, false
]
{ false: 86, true: 14 }
The true count hovers near 10 out of 100 but rarely hits it exactly; a spread from roughly 5 to 20 is normal at this sample size.
Step 5: Visualize the weighting in the browser
A grid makes the ratio visible. This version fills a 10x10 grid at weight 0.1 and paints each true cell purple against a dark background, so about ten of the hundred cells should light up.
Start with the page. Create index.html:
Create the file
touch index.html
Add the code: js-weighted-coinflip-101/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>js-weighted-coinflip-101</title>
<link rel="stylesheet" href="./app.css" />
</head>
<body>
<canvas id="canvas" width="300" height="300"></canvas>
<script type="module" src="./app.js"></script>
</body>
</html>
Detailed breakdown
- The
width/heightattributes set the 300x300 drawing buffer; the CSS controls the on-screen size. <script type="module">loadsapp.jsas an ES module so it canimportthe weighted function.<canvas>needs an explicit closing tag; it is not a void element.
Add the stylesheet. Create app.css:
Create the file
touch app.css
Add the code: js-weighted-coinflip-101/app.css
/* File: app.css
* Center the canvas in the middle of the viewport.
*/
canvas {
padding: 0;
margin: auto;
display: block;
width: 400px;
height: 400px;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
Detailed breakdown
position: absolutewith all four offsets at0plusmargin: autocenters the canvas both ways; the400pxdisplay size scales up the 300x300 buffer.
Now the drawing code. Create app.js:
Create the file
touch app.js
Add the code: js-weighted-coinflip-101/app.js
// File: app.js
// Fill a 10x10 grid with weighted flips at 10%: purple for true, black for false.
import { weightedCoinFlip } from "./weighted-coinflip.js";
const canvas = document.getElementById("canvas");
const SCREEN_SIZE = 300; // must match the canvas width/height attributes
const DIM = 10; // grid is DIM x DIM cells
const CELL_SIZE = SCREEN_SIZE / DIM;
const WEIGHT = 0.1; // ~10% of cells should be true
const BORDER = 1;
const NEON_PURPLE = "#B026FF";
// One weighted flip per grid cell.
const cells = Array.from({ length: DIM * DIM }, () => weightedCoinFlip(WEIGHT));
console.log(cells);
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.clearRect(0, 0, SCREEN_SIZE, SCREEN_SIZE);
ctx.fillStyle = "#444444";
ctx.fillRect(0, 0, SCREEN_SIZE, SCREEN_SIZE);
let cursor = 0;
for (let i = 0; i < DIM; i++) {
for (let j = 0; j < DIM; j++) {
ctx.fillStyle = cells[cursor++] ? NEON_PURPLE : "black";
ctx.fillRect(
i * CELL_SIZE + BORDER,
j * CELL_SIZE + BORDER,
CELL_SIZE - BORDER * 2,
CELL_SIZE - BORDER * 2,
);
}
}
}
Detailed breakdown
WEIGHT = 0.1drives both the odds and the expected number of lit cells (about 10 of 100). Raise it and more cells turn purple.CELL_SIZEisSCREEN_SIZE / DIM(30 pixels);BORDERtrims each square by a pixel per side so the cells read as a grid.- The ternary paints a true cell
NEON_PURPLEand a false cell black against the dark gray background.
Step 6: Serve the page and drive it with a Makefile
Browsers block module imports over file://, so serve the folder over HTTP. A short Makefile wraps the demo, tests, and server.
Create the file
touch Makefile
Add the code: js-weighted-coinflip-101/Makefile
.DEFAULT_GOAL := help
SHELL := /bin/bash
PORT ?= 8000
.PHONY: help demo test serve
help: ## Show this help screen
@echo "Targets:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| sort \
| awk 'BEGIN {FS = ":.*?## "}; {printf " %-8s %s\n", $$1, $$2}'
demo: ## Flip the weighted coin 100 times at 10% and print the distribution
@node demo.js
test: ## Run the Node test suite (node --test)
@node --test
serve: ## Serve the browser visualization at http://localhost:$(PORT)/
@echo "Serving on http://localhost:$(PORT)/ (Ctrl-C to stop)"
@python3 -m http.server $(PORT)
Detailed breakdown
.DEFAULT_GOAL := helpmakes a baremakeprint the help screen.PORT ?= 8000sets a default port a caller can override, e.g.make serve PORT=9000.
Confirm the help screen, then serve:
make
Targets:
demo Flip the weighted coin 100 times at 10% and print the distribution
help Show this help screen
serve Serve the browser visualization at http://localhost:$(PORT)/
test Run the Node test suite (node --test)
Run make serve, open http://localhost:8000/ in a browser, and reload a few times. Each load repaints the grid, and about a tenth of the cells glow purple. Raise WEIGHT in app.js to see more cells light up. Press Ctrl-C to stop the server.
Recap
The function is Math.random() <= weight. The work around it is what makes it trustworthy: "type": "module" for clean imports, a node --test suite that stubs Math.random to pin the <= boundary and the weight = 1 extreme, a distribution demo at 10%, and a canvas grid to see the ratio. The same weight parameter drives simulations and generative art wherever you need a tunable true/false rate.
Related articles
- How to Create a Random Boolean Function in JavaScript (Node and Browser)
- How to Select a Random Item from a JavaScript Array (Node and Browser)
- How to Create a JavaScript Module (Node and Browser)