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 (python3 ships 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, import throws SyntaxError: Cannot use import statement outside a module.
  • The demo and test scripts 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

  • weight is a probability from 0.0 to 1.0: 0.1 means true about 10% of the time.
  • Math.random() returns a float in [0, 1). The comparison is true whenever the draw falls at or below weight, and since draws are uniform across [0, 1), the fraction that satisfy it is weight itself.
  • The result is a real boolean (true/false), which reads as 1/0 when used numerically. Edge cases: weight = 1 is always true (every draw is < 1), and weight = 0 is 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

  • withRandom swaps Math.random for a stub returning a fixed draw, then restores it in a finally block so the stub cannot leak into a later test.
  • The boundary test confirms the comparison is <=: a draw exactly equal to the weight (0.1 at weight 0.1) counts as true, while a draw above it is false.
  • weight = 1 is asserted true across 1000 calls, since every draw in [0, 1) is at or below 1. The last test uses a real Set over 500 draws at weight 0.5 to 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.1 biases each flip toward false; changing it re-biases the whole run.
  • The reduce tallies true and false into a counts object that sums to LIMIT.

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/height attributes set the 300x300 drawing buffer; the CSS controls the on-screen size.
  • <script type="module"> loads app.js as an ES module so it can import the 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: absolute with all four offsets at 0 plus margin: auto centers the canvas both ways; the 400px display 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.1 drives both the odds and the expected number of lit cells (about 10 of 100). Raise it and more cells turn purple.
  • CELL_SIZE is SCREEN_SIZE / DIM (30 pixels); BORDER trims each square by a pixel per side so the cells read as a grid.
  • The ternary paints a true cell NEON_PURPLE and 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 := help makes a bare make print the help screen.
  • PORT ?= 8000 sets 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.

References