Play the game yourself, or run thousands of automated rounds and watch the win rates converge. The simulation runs the exact Monty Hall setup: random car placement, host always reveals a goat from the unchosen doors, you either switch or stay.
<p class="hint">Pick a door, the host reveals a goat, then decide: <b>switch</b> or <b>stay</b>. Run thousands of rounds automatically to see the win rates converge.</p>
<div class="doors" id="doors"></div>
<div class="msg" id="msg">Choose a door to start.</div>
<div class="btns" id="btns">
<button id="switchBtn" type="button" disabled>Switch door</button>
<button id="stayBtn" type="button" disabled>Stay</button>
</div>
<div class="divider"></div>
<div class="stats">
<div class="stat-block">
<div class="bar-label">Switching</div>
<div class="bar-wrap"><div class="bar bar-switch" id="barSwitch"></div></div>
<div class="stat-nums" id="numsSwitch">0 wins / 0 rounds</div>
</div>
<div class="stat-block">
<div class="bar-label">Staying</div>
<div class="bar-wrap"><div class="bar bar-stay" id="barStay"></div></div>
<div class="stat-nums" id="numsStay">0 wins / 0 rounds</div>
</div>
</div>
<div class="btns auto-btns">
<button id="auto100" type="button" class="ghost">Run 100 rounds</button>
<button id="auto1000" type="button" class="ghost">Run 1,000 rounds</button>
<button id="resetAll" type="button" class="ghost danger">Reset all</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.doors { display: flex; gap: 12px; justify-content: center; margin: .5rem 0 .7rem; }
.door { width: 74px; height: 110px; border-radius: 10px; border: 2px solid #adb1b8;
background: #c9ccd1; display: flex; flex-direction: column; align-items: center;
justify-content: flex-end; padding-bottom: 10px; font-size: 1.1rem; font-weight: 700;
cursor: pointer; user-select: none; transition: all .15s; position: relative; }
.door:hover:not([disabled]) { background: #bcc0c6; }
.door[disabled] { cursor: default; }
.door .emoji { font-size: 2.2rem; position: absolute; top: 18px; opacity: 0; transition: opacity .3s; }
.door.open .emoji { opacity: 1; }
.door.chosen { border-color: #1d3557; background: #dbe8f5; }
.door.goat-reveal { border-color: #e8a838; background: #fdf3dc; cursor: default; }
.door.win { border-color: #0a7d33; background: #d4f0e0; }
.door.lose { border-color: #c92f3c; background: #fde8ea; }
.door-num { font-size: .85rem; color: #555; }
.msg { font-size: 1rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0 .5rem; text-align: center; }
.msg.ok { color: #0a7d33; }
.msg.bad { color: #c92f3c; }
.msg.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button.danger { border-color: #c92f3c; color: #c92f3c; }
button:disabled { opacity: .4; cursor: default; }
.divider { border-top: 1px solid #e0e4e8; margin: .9rem 0; }
.stats { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .7rem; }
.stat-block { display: flex; align-items: center; gap: .6rem; }
.bar-label { width: 62px; font-size: .85rem; font-weight: 600; color: #333; text-align: right; }
.bar-wrap { flex: 1; background: #e8eef3; border-radius: 6px; height: 22px; overflow: hidden; }
.bar { height: 100%; border-radius: 6px; transition: width .4s ease; width: 0%; }
.bar-switch { background: #1d6d3a; }
.bar-stay { background: #c92f3c; }
.stat-nums { font-size: .8rem; color: #555; width: 130px; }
.auto-btns { margin-top: .3rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Monty Hall simulation
// - Car placed randomly behind one of 3 doors (uniform 1/3 each)
// - Host always reveals a goat from the unchosen doors (never the car, never your pick)
// - Switch = move to the remaining closed unchosen door
// - Stay = keep original pick
// Expected: switch wins 2/3, stay wins 1/3
const DOORS = [0, 1, 2];
let state = null; // { car, chosen, revealed, final, won, strategy }
let switchWins = 0, switchTotal = 0, stayWins = 0, stayTotal = 0;
function randInt(n) { return Math.floor(Math.random() * n); }
function simulate(strategy) {
const car = randInt(3);
const chosen = randInt(3);
// Host reveals a goat: pick from doors that are not chosen and not the car
const revealable = DOORS.filter(d => d !== chosen && d !== car);
const revealed = revealable[randInt(revealable.length)];
// Remaining door (not chosen, not revealed)
const remaining = DOORS.find(d => d !== chosen && d !== revealed);
const finalDoor = strategy === 'switch' ? remaining : chosen;
const won = finalDoor === car;
return { car, chosen, revealed, final: finalDoor, won, strategy };
}
function renderDoors(s) {
const el = document.getElementById('doors');
el.innerHTML = '';
DOORS.forEach(i => {
const d = document.createElement('div');
d.className = 'door';
if (!s) {
d.addEventListener('click', () => onChoose(i));
} else {
if (i === s.revealed) {
d.className = 'door goat-reveal open';
} else if (s.final !== undefined) {
if (i === s.final) d.className = 'door ' + (s.won ? 'win open' : 'lose open');
else if (i === s.car && !s.won) d.className = 'door win open';
else d.className = 'door';
} else {
if (i === s.chosen) d.className = 'door chosen';
}
}
const emoji = document.createElement('span');
emoji.className = 'emoji';
if (!s) {
emoji.textContent = '';
} else if (i === s.revealed) {
emoji.textContent = '🐐';
} else if (s.final !== undefined) {
emoji.textContent = (i === s.car) ? '🚗' : '🐐';
}
const num = document.createElement('span');
num.className = 'door-num';
num.textContent = fmt('Door %i%%v%', { i: i, v: 1 });
d.appendChild(emoji);
d.appendChild(num);
el.appendChild(d);
});
}
function setMsg(t, cls) {
const m = document.getElementById('msg');
m.textContent = t;
m.className = 'msg ' + (cls || '');
}
function setBtns(switchEn, stayEn) {
document.getElementById('switchBtn').disabled = !switchEn;
document.getElementById('stayBtn').disabled = !stayEn;
}
function updateStats() {
const sw = switchTotal ? (switchWins / switchTotal) : 0;
const st = stayTotal ? (stayWins / stayTotal) : 0;
document.getElementById('barSwitch').style.width = (sw * 100).toFixed(1) + '%';
document.getElementById('barStay').style.width = (st * 100).toFixed(1) + '%';
document.getElementById('numsSwitch').textContent =
fmt('%switchWins% wins / %switchTotal% rounds (%v%%)', { switchWins: switchWins, switchTotal: switchTotal, v: switchTotal ? (sw * 100).toFixed(1) : '—' });
document.getElementById('numsStay').textContent =
fmt('%stayWins% wins / %stayTotal% rounds (%v%%)', { stayWins: stayWins, stayTotal: stayTotal, v: stayTotal ? (st * 100).toFixed(1) : '—' });
}
function onChoose(i) {
const car = randInt(3);
const chosen = i;
const revealable = DOORS.filter(d => d !== chosen && d !== car);
const revealed = revealable[randInt(revealable.length)];
state = { car, chosen, revealed, final: undefined };
renderDoors(state);
setMsg(fmt('Door %revealed%%v% is a goat! Switch or stay?', { revealed: revealed, v: 1 }), 'info');
setBtns(true, true);
}
function onDecide(strategy) {
const remaining = DOORS.find(d => d !== state.chosen && d !== state.revealed);
state.final = strategy === 'switch' ? remaining : state.chosen;
state.won = state.final === state.car;
state.strategy = strategy;
if (strategy === 'switch') { switchTotal++; if (state.won) switchWins++; }
else { stayTotal++; if (state.won) stayWins++; }
renderDoors(state);
setMsg(state.won ? '🎉 You won the car!' : fmt('🐐 Goat. The car was behind door %car%%v%.', { car: state.car, v: 1 }),
state.won ? 'ok' : 'bad');
setBtns(false, false);
updateStats();
}
function runAuto(n) {
for (let i = 0; i < n; i++) {
const s = simulate(i % 2 === 0 ? 'switch' : 'stay'); // alternate evenly
if (s.strategy === 'switch') { switchTotal++; if (s.won) switchWins++; }
else { stayTotal++; if (s.won) stayWins++; }
}
state = null;
renderDoors(null);
setMsg(fmt('Ran %n% rounds. Switch: %v%% | Stay: %v2%%', { n: n, v: (switchWins/switchTotal*100).toFixed(1), v2: (stayWins/stayTotal*100).toFixed(1) }), 'info');
setBtns(false, false);
updateStats();
}
document.getElementById('switchBtn').addEventListener('click', () => onDecide('switch'));
document.getElementById('stayBtn').addEventListener('click', () => onDecide('stay'));
document.getElementById('auto100').addEventListener('click', () => runAuto(100));
document.getElementById('auto1000').addEventListener('click', () => runAuto(1000));
document.getElementById('resetAll').addEventListener('click', () => {
switchWins = switchTotal = stayWins = stayTotal = 0;
state = null;
renderDoors(null);
setMsg('Choose a door to start.', '');
setBtns(false, false);
updateStats();
});
renderDoors(null);
updateStats();
// Verify the math: after many simulations, switch should win ~2/3 and stay ~1/3
// (logged to console for the node verification step)
let testSW = 0, testST = 0, testSWW = 0, testSTW = 0;
for (let i = 0; i < 90000; i++) {
const s = simulate('switch'); testSW++; if (s.won) testSWW++;
const t = simulate('stay'); testST++; if (t.won) testSTW++;
}
console.log('VERIFY switch win rate:', (testSWW/testSW).toFixed(4), '(expected ~0.6667)');
console.log('VERIFY stay win rate:', (testSTW/testST).toFixed(4), '(expected ~0.3333)');
<p class="hint">Pick a door, the host reveals a goat, then decide: <b>switch</b> or <b>stay</b>. Run thousands of rounds automatically to see the win rates converge.</p>
<div class="doors" id="doors"></div>
<div class="msg" id="msg">Choose a door to start.</div>
<div class="btns" id="btns">
<button id="switchBtn" type="button" disabled>Switch door</button>
<button id="stayBtn" type="button" disabled>Stay</button>
</div>
<div class="divider"></div>
<div class="stats">
<div class="stat-block">
<div class="bar-label">Switching</div>
<div class="bar-wrap"><div class="bar bar-switch" id="barSwitch"></div></div>
<div class="stat-nums" id="numsSwitch">0 wins / 0 rounds</div>
</div>
<div class="stat-block">
<div class="bar-label">Staying</div>
<div class="bar-wrap"><div class="bar bar-stay" id="barStay"></div></div>
<div class="stat-nums" id="numsStay">0 wins / 0 rounds</div>
</div>
</div>
<div class="btns auto-btns">
<button id="auto100" type="button" class="ghost">Run 100 rounds</button>
<button id="auto1000" type="button" class="ghost">Run 1,000 rounds</button>
<button id="resetAll" type="button" class="ghost danger">Reset all</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.doors { display: flex; gap: 12px; justify-content: center; margin: .5rem 0 .7rem; }
.door { width: 74px; height: 110px; border-radius: 10px; border: 2px solid #adb1b8;
background: #c9ccd1; display: flex; flex-direction: column; align-items: center;
justify-content: flex-end; padding-bottom: 10px; font-size: 1.1rem; font-weight: 700;
cursor: pointer; user-select: none; transition: all .15s; position: relative; }
.door:hover:not([disabled]) { background: #bcc0c6; }
.door[disabled] { cursor: default; }
.door .emoji { font-size: 2.2rem; position: absolute; top: 18px; opacity: 0; transition: opacity .3s; }
.door.open .emoji { opacity: 1; }
.door.chosen { border-color: #1d3557; background: #dbe8f5; }
.door.goat-reveal { border-color: #e8a838; background: #fdf3dc; cursor: default; }
.door.win { border-color: #0a7d33; background: #d4f0e0; }
.door.lose { border-color: #c92f3c; background: #fde8ea; }
.door-num { font-size: .85rem; color: #555; }
.msg { font-size: 1rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0 .5rem; text-align: center; }
.msg.ok { color: #0a7d33; }
.msg.bad { color: #c92f3c; }
.msg.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button.danger { border-color: #c92f3c; color: #c92f3c; }
button:disabled { opacity: .4; cursor: default; }
.divider { border-top: 1px solid #e0e4e8; margin: .9rem 0; }
.stats { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .7rem; }
.stat-block { display: flex; align-items: center; gap: .6rem; }
.bar-label { width: 62px; font-size: .85rem; font-weight: 600; color: #333; text-align: right; }
.bar-wrap { flex: 1; background: #e8eef3; border-radius: 6px; height: 22px; overflow: hidden; }
.bar { height: 100%; border-radius: 6px; transition: width .4s ease; width: 0%; }
.bar-switch { background: #1d6d3a; }
.bar-stay { background: #c92f3c; }
.stat-nums { font-size: .8rem; color: #555; width: 130px; }
.auto-btns { margin-top: .3rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Monty Hall simulation
// - Car placed randomly behind one of 3 doors (uniform 1/3 each)
// - Host always reveals a goat from the unchosen doors (never the car, never your pick)
// - Switch = move to the remaining closed unchosen door
// - Stay = keep original pick
// Expected: switch wins 2/3, stay wins 1/3
const DOORS = [0, 1, 2];
let state = null; // { car, chosen, revealed, final, won, strategy }
let switchWins = 0, switchTotal = 0, stayWins = 0, stayTotal = 0;
function randInt(n) { return Math.floor(Math.random() * n); }
function simulate(strategy) {
const car = randInt(3);
const chosen = randInt(3);
// Host reveals a goat: pick from doors that are not chosen and not the car
const revealable = DOORS.filter(d => d !== chosen && d !== car);
const revealed = revealable[randInt(revealable.length)];
// Remaining door (not chosen, not revealed)
const remaining = DOORS.find(d => d !== chosen && d !== revealed);
const finalDoor = strategy === 'switch' ? remaining : chosen;
const won = finalDoor === car;
return { car, chosen, revealed, final: finalDoor, won, strategy };
}
function renderDoors(s) {
const el = document.getElementById('doors');
el.innerHTML = '';
DOORS.forEach(i => {
const d = document.createElement('div');
d.className = 'door';
if (!s) {
d.addEventListener('click', () => onChoose(i));
} else {
if (i === s.revealed) {
d.className = 'door goat-reveal open';
} else if (s.final !== undefined) {
if (i === s.final) d.className = 'door ' + (s.won ? 'win open' : 'lose open');
else if (i === s.car && !s.won) d.className = 'door win open';
else d.className = 'door';
} else {
if (i === s.chosen) d.className = 'door chosen';
}
}
const emoji = document.createElement('span');
emoji.className = 'emoji';
if (!s) {
emoji.textContent = '';
} else if (i === s.revealed) {
emoji.textContent = '🐐';
} else if (s.final !== undefined) {
emoji.textContent = (i === s.car) ? '🚗' : '🐐';
}
const num = document.createElement('span');
num.className = 'door-num';
num.textContent = fmt('Door %i%%v%', { i: i, v: 1 });
d.appendChild(emoji);
d.appendChild(num);
el.appendChild(d);
});
}
function setMsg(t, cls) {
const m = document.getElementById('msg');
m.textContent = t;
m.className = 'msg ' + (cls || '');
}
function setBtns(switchEn, stayEn) {
document.getElementById('switchBtn').disabled = !switchEn;
document.getElementById('stayBtn').disabled = !stayEn;
}
function updateStats() {
const sw = switchTotal ? (switchWins / switchTotal) : 0;
const st = stayTotal ? (stayWins / stayTotal) : 0;
document.getElementById('barSwitch').style.width = (sw * 100).toFixed(1) + '%';
document.getElementById('barStay').style.width = (st * 100).toFixed(1) + '%';
document.getElementById('numsSwitch').textContent =
fmt('%switchWins% wins / %switchTotal% rounds (%v%%)', { switchWins: switchWins, switchTotal: switchTotal, v: switchTotal ? (sw * 100).toFixed(1) : '—' });
document.getElementById('numsStay').textContent =
fmt('%stayWins% wins / %stayTotal% rounds (%v%%)', { stayWins: stayWins, stayTotal: stayTotal, v: stayTotal ? (st * 100).toFixed(1) : '—' });
}
function onChoose(i) {
const car = randInt(3);
const chosen = i;
const revealable = DOORS.filter(d => d !== chosen && d !== car);
const revealed = revealable[randInt(revealable.length)];
state = { car, chosen, revealed, final: undefined };
renderDoors(state);
setMsg(fmt('Door %revealed%%v% is a goat! Switch or stay?', { revealed: revealed, v: 1 }), 'info');
setBtns(true, true);
}
function onDecide(strategy) {
const remaining = DOORS.find(d => d !== state.chosen && d !== state.revealed);
state.final = strategy === 'switch' ? remaining : state.chosen;
state.won = state.final === state.car;
state.strategy = strategy;
if (strategy === 'switch') { switchTotal++; if (state.won) switchWins++; }
else { stayTotal++; if (state.won) stayWins++; }
renderDoors(state);
setMsg(state.won ? '🎉 You won the car!' : fmt('🐐 Goat. The car was behind door %car%%v%.', { car: state.car, v: 1 }),
state.won ? 'ok' : 'bad');
setBtns(false, false);
updateStats();
}
function runAuto(n) {
for (let i = 0; i < n; i++) {
const s = simulate(i % 2 === 0 ? 'switch' : 'stay'); // alternate evenly
if (s.strategy === 'switch') { switchTotal++; if (s.won) switchWins++; }
else { stayTotal++; if (s.won) stayWins++; }
}
state = null;
renderDoors(null);
setMsg(fmt('Ran %n% rounds. Switch: %v%% | Stay: %v2%%', { n: n, v: (switchWins/switchTotal*100).toFixed(1), v2: (stayWins/stayTotal*100).toFixed(1) }), 'info');
setBtns(false, false);
updateStats();
}
document.getElementById('switchBtn').addEventListener('click', () => onDecide('switch'));
document.getElementById('stayBtn').addEventListener('click', () => onDecide('stay'));
document.getElementById('auto100').addEventListener('click', () => runAuto(100));
document.getElementById('auto1000').addEventListener('click', () => runAuto(1000));
document.getElementById('resetAll').addEventListener('click', () => {
switchWins = switchTotal = stayWins = stayTotal = 0;
state = null;
renderDoors(null);
setMsg('Choose a door to start.', '');
setBtns(false, false);
updateStats();
});
renderDoors(null);
updateStats();
// Verify the math: after many simulations, switch should win ~2/3 and stay ~1/3
// (logged to console for the node verification step)
let testSW = 0, testST = 0, testSWW = 0, testSTW = 0;
for (let i = 0; i < 90000; i++) {
const s = simulate('switch'); testSW++; if (s.won) testSWW++;
const t = simulate('stay'); testST++; if (t.won) testSTW++;
}
console.log('VERIFY switch win rate:', (testSWW/testSW).toFixed(4), '(expected ~0.6667)');
console.log('VERIFY stay win rate:', (testSTW/testST).toFixed(4), '(expected ~0.3333)');
Comments
Loading comments...