Click Run games to simulate rounds of the St. Petersburg game. Each round flips a fair coin until heads, then records the payoff 2n. Watch what the typical payout really looks like.
<div class="controls">
<label>Games per run:
<select id="numGames">
<option value="100">100</option>
<option value="500" selected>500</option>
<option value="2000">2,000</option>
<option value="10000">10,000</option>
</select>
</label>
<button id="runBtn" type="button">Run games</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div class="stats" id="stats"></div>
<div class="chart-wrap">
<canvas id="chart" width="540" height="220"></canvas>
</div>
<div class="legend">
<span class="dot dot-bar"></span> Fraction of games ending on flip n
<span class="dot dot-avg"></span> Running average payout
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .6rem; }
label { font-size: .88rem; color: #444; }
select { font-size: .88rem; padding: .25rem .4rem; border: 1px solid #aaa; border-radius: 6px; background: #fff; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stats { font-size: .88rem; min-height: 1.3em; margin-bottom: .4rem; color: #333; }
.stats b { color: #1d3557; }
.chart-wrap { width: 100%; overflow-x: auto; }
canvas { display: block; max-width: 100%; }
.legend { font-size: .78rem; color: #555; margin-top: .35rem; display: flex; align-items: center; gap: .3rem; flex-wrap: wrap; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 2px; vertical-align: middle; }
.dot-bar { background: #457b9d; }
.dot-avg { background: #e63946; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// St. Petersburg game: flip fair coin until heads; win 2^n where n = flip number.
// Demonstrates that almost all payouts are tiny (2 or 4) but the rare jackpot
// dominates the theoretical expectation, causing the running average to never settle.
function playGame() {
let n = 1;
// Each flip: 0.5 chance of heads (stop), 0.5 of tails (continue)
while (Math.random() >= 0.5) n++;
return { n, payout: Math.pow(2, n) };
}
let allPayouts = [];
let runningAvgs = [];
function runGames(count) {
for (let i = 0; i < count; i++) {
const { n, payout } = playGame();
allPayouts.push({ n, payout });
const prev = runningAvgs.length ? runningAvgs[runningAvgs.length - 1] : 0;
const total = allPayouts.length;
runningAvgs.push(prev + (payout - prev) / total);
}
}
function computeStats() {
if (!allPayouts.length) return null;
const payouts = allPayouts.map(g => g.payout);
const total = payouts.length;
const mean = runningAvgs[runningAvgs.length - 1];
const median = [...payouts].sort((a,b)=>a-b)[Math.floor(total/2)];
const max = Math.max(...payouts);
const maxN = allPayouts.reduce((a,g) => g.payout > a.payout ? g : a).n;
return { total, mean, median, max, maxN };
}
function buildFreqMap() {
const freq = {};
for (const { n } of allPayouts) freq[n] = (freq[n] || 0) + 1;
return freq;
}
function drawChart() {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.offsetWidth || 540;
canvas.width = W;
const H = 220;
canvas.height = H;
ctx.clearRect(0, 0, W, H);
const total = allPayouts.length;
if (!total) {
ctx.fillStyle = '#aaa';
ctx.font = '13px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Click "Run games" to start', W / 2, H / 2);
return;
}
const freq = buildFreqMap();
const maxFlip = Math.max(15, ...Object.keys(freq).map(Number));
const SHOW = Math.min(maxFlip, 20);
// Bar chart: fraction of games ending on each flip n=1..SHOW
const barPad = { l: 48, r: 16, t: 16, b: 36 };
const barH = H - barPad.t - barPad.b;
const barW = W - barPad.l - barPad.r;
const maxFrac = Math.max(...Array.from({length: SHOW}, (_, i) => (freq[i+1]||0)/total));
// Y axis label
ctx.fillStyle = '#555'; ctx.font = '10px system-ui'; ctx.textAlign = 'right';
for (let t = 0; t <= 4; t++) {
const frac = maxFrac * t / 4;
const y = barPad.t + barH - (barH * t / 4);
ctx.fillText((frac * 100).toFixed(1) + '%', barPad.l - 4, y + 3);
ctx.strokeStyle = '#e0e0e0'; ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.moveTo(barPad.l, y); ctx.lineTo(barPad.l + barW, y); ctx.stroke();
}
const bw = barW / SHOW;
for (let i = 0; i < SHOW; i++) {
const n = i + 1;
const frac = (freq[n] || 0) / total;
const bh = maxFrac ? (frac / maxFrac) * barH : 0;
const x = barPad.l + i * bw;
const y = barPad.t + barH - bh;
ctx.fillStyle = '#457b9d';
ctx.fillRect(x + 1, y, bw - 2, bh);
// X tick labels
if (n === 1 || n % 5 === 0) {
ctx.fillStyle = '#666'; ctx.font = '10px system-ui'; ctx.textAlign = 'center';
ctx.fillText('n=' + n, x + bw / 2, barPad.t + barH + 14);
}
}
// Running average line (right y-axis in log scale for visual)
// We draw the running average as a thin red line over the last SHOW*? points, normalized
if (runningAvgs.length > 1) {
const pts = runningAvgs;
const maxAvg = Math.max(...pts);
const minAvg = Math.min(...pts);
const range = maxAvg - minAvg || 1;
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 1.5;
ctx.beginPath();
for (let i = 0; i < pts.length; i++) {
const x = barPad.l + (i / (pts.length - 1)) * barW;
const y = barPad.t + barH - ((pts[i] - minAvg) / range) * barH;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
// Label current average
const lastAvg = pts[pts.length - 1];
ctx.fillStyle = '#e63946'; ctx.font = 'bold 10px system-ui'; ctx.textAlign = 'left';
ctx.fillText(fmt('avg $%lastAvg%', { lastAvg: lastAvg.toFixed(2) }), barPad.l + barW - 70, barPad.t + 12);
}
// Axes
ctx.strokeStyle = '#999'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(barPad.l, barPad.t); ctx.lineTo(barPad.l, barPad.t + barH); ctx.lineTo(barPad.l + barW, barPad.t + barH); ctx.stroke();
}
function updateStats() {
const s = computeStats();
const el = document.getElementById('stats');
if (!s) { el.textContent = ''; return; }
el.innerHTML =
fmt('<b>%total% games</b> — running avg: <b>$%mean%</b> | median: <b>$%median%</b> | best: <b>$%max%</b> (flip %maxN%)', { total: s.total.toLocaleString(), mean: s.mean.toFixed(2), median: s.median, max: s.max.toLocaleString(), maxN: s.maxN });
}
document.getElementById('runBtn').addEventListener('click', () => {
const count = parseInt(document.getElementById('numGames').value, 10);
runGames(count);
updateStats();
drawChart();
});
document.getElementById('resetBtn').addEventListener('click', () => {
allPayouts = []; runningAvgs = [];
document.getElementById('stats').textContent = '';
drawChart();
});
// Initial empty chart
drawChart();
// Verify core logic in a deterministic way (seeded-like assertions):
// playGame must return n>=1 and payout = 2^n
// (verified below — comment kept for auditability)
// let g = playGame(); console.assert(g.payout === Math.pow(2, g.n), 'payout=2^n');
<div class="controls">
<label>Games per run:
<select id="numGames">
<option value="100">100</option>
<option value="500" selected>500</option>
<option value="2000">2,000</option>
<option value="10000">10,000</option>
</select>
</label>
<button id="runBtn" type="button">Run games</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div class="stats" id="stats"></div>
<div class="chart-wrap">
<canvas id="chart" width="540" height="220"></canvas>
</div>
<div class="legend">
<span class="dot dot-bar"></span> Fraction of games ending on flip n
<span class="dot dot-avg"></span> Running average payout
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .6rem; }
label { font-size: .88rem; color: #444; }
select { font-size: .88rem; padding: .25rem .4rem; border: 1px solid #aaa; border-radius: 6px; background: #fff; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stats { font-size: .88rem; min-height: 1.3em; margin-bottom: .4rem; color: #333; }
.stats b { color: #1d3557; }
.chart-wrap { width: 100%; overflow-x: auto; }
canvas { display: block; max-width: 100%; }
.legend { font-size: .78rem; color: #555; margin-top: .35rem; display: flex; align-items: center; gap: .3rem; flex-wrap: wrap; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 2px; vertical-align: middle; }
.dot-bar { background: #457b9d; }
.dot-avg { background: #e63946; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// St. Petersburg game: flip fair coin until heads; win 2^n where n = flip number.
// Demonstrates that almost all payouts are tiny (2 or 4) but the rare jackpot
// dominates the theoretical expectation, causing the running average to never settle.
function playGame() {
let n = 1;
// Each flip: 0.5 chance of heads (stop), 0.5 of tails (continue)
while (Math.random() >= 0.5) n++;
return { n, payout: Math.pow(2, n) };
}
let allPayouts = [];
let runningAvgs = [];
function runGames(count) {
for (let i = 0; i < count; i++) {
const { n, payout } = playGame();
allPayouts.push({ n, payout });
const prev = runningAvgs.length ? runningAvgs[runningAvgs.length - 1] : 0;
const total = allPayouts.length;
runningAvgs.push(prev + (payout - prev) / total);
}
}
function computeStats() {
if (!allPayouts.length) return null;
const payouts = allPayouts.map(g => g.payout);
const total = payouts.length;
const mean = runningAvgs[runningAvgs.length - 1];
const median = [...payouts].sort((a,b)=>a-b)[Math.floor(total/2)];
const max = Math.max(...payouts);
const maxN = allPayouts.reduce((a,g) => g.payout > a.payout ? g : a).n;
return { total, mean, median, max, maxN };
}
function buildFreqMap() {
const freq = {};
for (const { n } of allPayouts) freq[n] = (freq[n] || 0) + 1;
return freq;
}
function drawChart() {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.offsetWidth || 540;
canvas.width = W;
const H = 220;
canvas.height = H;
ctx.clearRect(0, 0, W, H);
const total = allPayouts.length;
if (!total) {
ctx.fillStyle = '#aaa';
ctx.font = '13px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Click "Run games" to start', W / 2, H / 2);
return;
}
const freq = buildFreqMap();
const maxFlip = Math.max(15, ...Object.keys(freq).map(Number));
const SHOW = Math.min(maxFlip, 20);
// Bar chart: fraction of games ending on each flip n=1..SHOW
const barPad = { l: 48, r: 16, t: 16, b: 36 };
const barH = H - barPad.t - barPad.b;
const barW = W - barPad.l - barPad.r;
const maxFrac = Math.max(...Array.from({length: SHOW}, (_, i) => (freq[i+1]||0)/total));
// Y axis label
ctx.fillStyle = '#555'; ctx.font = '10px system-ui'; ctx.textAlign = 'right';
for (let t = 0; t <= 4; t++) {
const frac = maxFrac * t / 4;
const y = barPad.t + barH - (barH * t / 4);
ctx.fillText((frac * 100).toFixed(1) + '%', barPad.l - 4, y + 3);
ctx.strokeStyle = '#e0e0e0'; ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.moveTo(barPad.l, y); ctx.lineTo(barPad.l + barW, y); ctx.stroke();
}
const bw = barW / SHOW;
for (let i = 0; i < SHOW; i++) {
const n = i + 1;
const frac = (freq[n] || 0) / total;
const bh = maxFrac ? (frac / maxFrac) * barH : 0;
const x = barPad.l + i * bw;
const y = barPad.t + barH - bh;
ctx.fillStyle = '#457b9d';
ctx.fillRect(x + 1, y, bw - 2, bh);
// X tick labels
if (n === 1 || n % 5 === 0) {
ctx.fillStyle = '#666'; ctx.font = '10px system-ui'; ctx.textAlign = 'center';
ctx.fillText('n=' + n, x + bw / 2, barPad.t + barH + 14);
}
}
// Running average line (right y-axis in log scale for visual)
// We draw the running average as a thin red line over the last SHOW*? points, normalized
if (runningAvgs.length > 1) {
const pts = runningAvgs;
const maxAvg = Math.max(...pts);
const minAvg = Math.min(...pts);
const range = maxAvg - minAvg || 1;
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 1.5;
ctx.beginPath();
for (let i = 0; i < pts.length; i++) {
const x = barPad.l + (i / (pts.length - 1)) * barW;
const y = barPad.t + barH - ((pts[i] - minAvg) / range) * barH;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
// Label current average
const lastAvg = pts[pts.length - 1];
ctx.fillStyle = '#e63946'; ctx.font = 'bold 10px system-ui'; ctx.textAlign = 'left';
ctx.fillText(fmt('avg $%lastAvg%', { lastAvg: lastAvg.toFixed(2) }), barPad.l + barW - 70, barPad.t + 12);
}
// Axes
ctx.strokeStyle = '#999'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(barPad.l, barPad.t); ctx.lineTo(barPad.l, barPad.t + barH); ctx.lineTo(barPad.l + barW, barPad.t + barH); ctx.stroke();
}
function updateStats() {
const s = computeStats();
const el = document.getElementById('stats');
if (!s) { el.textContent = ''; return; }
el.innerHTML =
fmt('<b>%total% games</b> — running avg: <b>$%mean%</b> | median: <b>$%median%</b> | best: <b>$%max%</b> (flip %maxN%)', { total: s.total.toLocaleString(), mean: s.mean.toFixed(2), median: s.median, max: s.max.toLocaleString(), maxN: s.maxN });
}
document.getElementById('runBtn').addEventListener('click', () => {
const count = parseInt(document.getElementById('numGames').value, 10);
runGames(count);
updateStats();
drawChart();
});
document.getElementById('resetBtn').addEventListener('click', () => {
allPayouts = []; runningAvgs = [];
document.getElementById('stats').textContent = '';
drawChart();
});
// Initial empty chart
drawChart();
// Verify core logic in a deterministic way (seeded-like assertions):
// playGame must return n>=1 and payout = 2^n
// (verified below — comment kept for auditability)
// let g = playGame(); console.assert(g.payout === Math.pow(2, g.n), 'payout=2^n');
Notice the pattern: most games end quickly (heads on flip 1 or 2), paying just $2 or $4. The rare jackpot — 10 or 20 tails in a row — happens almost never in any finite run, even though it dominates the mathematical expectation. The running average can spike dramatically after a lucky game, then drift back down. Infinite expected value doesn't mean you'll ever get rich — it just means the average never settles.
Comments
Loading comments...