Skip to main content
pcloadletter

The Monty Hall Problem, a side-by-side simulation

I saw a cool version of the Monty Hall game here recently: https://monty.donk.systems.

This is really cool! But I had an itch I wanted to scratch: rather than manually test out the probabilities, I wanted to run two games side-by-side: one where a switch happened and one where a switch didn't happen. Then, running those over and over, we should see the overall win percentages converge to 66.7% for switching and 33.3% for not switching.

So I coded up a simulation here to scratch my itch!

Controls #

To control the simulation, you can start or stop here. You can also adjust the speed.

x3

Score #

Cumulative stats as the games go on.

Scenario Plays Wins Win Percent
Switch 0 0 -
No switch 0 0 -

Switch simulation #

If we switch cards after the first goat reveal.

No switch simulation #

If we don't switch.

Code #

If interested, the code for this simulation can be viewed here:

HTML #

<section class="board" id="switch-board">
	<div class="doors">
		<div class="door">
			<div class="guess"></div>
			<div class="card"></div>
		</div>
		<div class="door">
			<div class="guess"></div>
			<div class="card"></div>
		</div>
		<div class="door">
			<div class="guess"></div>
			<div class="card"></div>
		</div>
	</div>
	<div class="status"></div>
</section>

<section class="board" id="switch-board">
	<div class="doors">
		<div class="door">
			<div class="guess"></div>
			<div class="card"></div>
		</div>
		<div class="door">
			<div class="guess"></div>
			<div class="card"></div>
		</div>
		<div class="door">
			<div class="guess"></div>
			<div class="card"></div>
		</div>
	</div>
	<div class="status"></div>
</section>

JavaScript #

let running = false;
const playBtn = document.querySelector("#play-btn");
const speedControl = document.querySelector("#speed-control");
const speedDisplay = document.querySelector("#speed-display");

let speed = parseInt(speedControl.value);
const randi = (n) => Math.floor(Math.random() * n);
const sleep = (s) =>
	new Promise((res) => setTimeout(res, (s * 1000) / Math.max(1, speed)));

let playsSwitch = 0;
let winsSwitch = 0;
let playsNoSwitch = 0;
let winsNoSwitch = 0;

const playsEl = document.querySelector("#plays");
const winsEl = document.querySelector("#wins");
const winPctEl = document.querySelector("#win-pct");
const nsPlaysEl = document.querySelector("#ns-plays");
const nsWinsEl = document.querySelector("#ns-wins");
const nsWinPctEl = document.querySelector("#ns-win-pct");

const pct = (w, p) => (p ? ((100 * w) / p).toFixed(1) + "%" : "-");
const updateScoreboard = () => {
	playsEl.textContent = playsSwitch;
	winsEl.textContent = winsSwitch;
	winPctEl.textContent = pct(winsSwitch, playsSwitch);
	nsPlaysEl.textContent = playsNoSwitch;
	nsWinsEl.textContent = winsNoSwitch;
	nsWinPctEl.textContent = pct(winsNoSwitch, playsNoSwitch);
};

class MontyBoard {
	constructor(rootId, isSwitch) {
		this.root = document.getElementById(rootId);
		this.isSwitch = isSwitch;
		this.guessEls = Array.from(this.root.querySelectorAll(".guess"));
		this.cardEls = Array.from(this.root.querySelectorAll(".card"));
		this.statusEl = this.root.querySelector(".status");
		this._looping = false;
	}
	clear() {
		this.guessEls.forEach((el) => (el.textContent = ""));
		this.cardEls.forEach((el) => (el.textContent = ""));
	}
	status(t) {
		this.statusEl.textContent = t;
	}
	async playOnce() {
		if (!running) return;
		this.status("\u00A0");
		this.clear();
		const doors = ["G", "G", "G"];
		const winIndex = randi(3);
		doors[winIndex] = "C";
		await sleep(0.5);
		this.status("Initial guess");
		await sleep(0.5);
		let guessIndex = randi(3);
		this.guessEls[guessIndex].textContent = "✔";
		await sleep(0.5);
		this.status("Revealing a goat");
		await sleep(0.5);
		const revealChoices = [0, 1, 2].filter(
			(i) => i !== guessIndex && i !== winIndex
		);
		const doorToReveal = revealChoices[randi(revealChoices.length)];
		this.cardEls[doorToReveal].textContent = "G";
		await sleep(0.5);
		this.status(this.isSwitch ? "Switching choice" : "Not switching choice");
		await sleep(0.5);
		if (this.isSwitch) {
			this.guessEls[guessIndex].textContent = "";
			guessIndex = [0, 1, 2].find(
				(i) => i !== guessIndex && i !== doorToReveal
			);
			this.guessEls[guessIndex].textContent = "✔";
		}
		await sleep(0.5);
		this.status("Reveal");
		await sleep(0.5);
		this.cardEls.forEach((el, i) => (el.textContent = doors[i]));
		const win = guessIndex === winIndex;
		if (this.isSwitch) {
			playsSwitch++;
			if (win) winsSwitch++;
		} else {
			playsNoSwitch++;
			if (win) winsNoSwitch++;
		}
		this.status(win ? "Reveal (WIN!)" : "Reveal (LOSE!)");
		updateScoreboard();
		await sleep(2);
	}
	start() {
		if (this._looping) return;
		this._looping = true;
		const loop = async () => {
			while (running) {
				await this.playOnce();
			}
			this._looping = false;
		};
		loop();
	}
}

const boards = [
	new MontyBoard("switch-board", true),
	new MontyBoard("no-switch-board", false),
];

playBtn.addEventListener("click", () => {
	running = !running;
	playBtn.textContent = running ? "Stop simulation" : "Start simulation";
	if (running) boards.forEach((b) => b.start());
});

speedControl.addEventListener("input", (e) => {
	speed = parseInt(e.target.value) || 1;
	speedDisplay.textContent = "x" + speed;
});

updateScoreboard();
Enjoy this post? Please subscribe to my RSS feed on Feedly or add my RSS XML file to another reader!