<div class="controls">
<label>Your bankroll: <input id="k" type="number" value="10" min="1" max="99"></label>
<label>Casino bankroll: <input id="opp" type="number" value="90" min="1" max="999"></label>
</div>
<canvas id="chart" width="520" height="220"></canvas>
<div class="stats" id="stats"></div>
<div class="btns">
<button id="run" type="button">Run simulation</button>
<button id="run10" type="button">Run 100 times</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="results" id="results"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .7rem; }
label { font-size: .9rem; display: flex; align-items: center; gap: .4rem; }
input[type=number] { width: 70px; padding: .3rem .4rem; border: 1px solid #adb1b8;
border-radius: 6px; font-size: .95rem; }
canvas { display: block; width: 100%; max-width: 520px; border: 1px solid #cdd9e3;
border-radius: 8px; background: #f7f9fb; margin-bottom: .5rem; }
.stats { font-size: .9rem; color: #444; min-height: 1.4em; margin-bottom: .5rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
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; }
.results { font-size: .9rem; line-height: 1.6; color: #333; }
.ruin { color: #c92f3c; font-weight: 600; }
.win { color: #0a7d33; font-weight: 600; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Gambler’s Ruin: bankroll random walk between 0 and N = k + opp.
// Each step: +1 with prob 0.5, -1 with prob 0.5.
// The walk stops when it hits 0 (ruin) or N (win).
// Exact ruin probability = (N - k) / N (fair game).
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { t: 16, r: 10, b: 24, l: 36 };
let ruinCount = 0, winCount = 0, totalRuns = 0;
function getParams() {
const k = Math.max(1, parseInt(document.getElementById('k').value) || 10);
const opp = Math.max(1, parseInt(document.getElementById('opp').value) || 90);
return { k, N: k + opp };
}
// Run one walk; return { path, ruined }
function runWalk(k, N) {
const path = [k];
let pos = k;
while (pos > 0 && pos < N) {
pos += Math.random() < 0.5 ? 1 : -1;
path.push(pos);
if (path.length > 100000) break; // safety cap
}
return { path, ruined: pos === 0 };
}
function drawWalk(path, N, ruined) {
ctx.clearRect(0, 0, W, H);
const gw = W - PAD.l - PAD.r;
const gh = H - PAD.t - PAD.b;
// grid lines
ctx.strokeStyle = '#dde3ea';
ctx.lineWidth = 1;
[0, 0.25, 0.5, 0.75, 1].forEach(frac => {
const y = PAD.t + gh * (1 - frac);
ctx.beginPath(); ctx.moveTo(PAD.l, y); ctx.lineTo(W - PAD.r, y); ctx.stroke();
ctx.fillStyle = '#888'; ctx.font = '10px system-ui';
ctx.textAlign = 'right';
ctx.fillText(Math.round(frac * N), PAD.l - 4, y + 3);
});
// walk path (subsample if very long)
const step = Math.max(1, Math.floor(path.length / gw));
const pts = path.filter((_, i) => i % step === 0 || i === path.length - 1);
ctx.beginPath();
pts.forEach((v, i) => {
const x = PAD.l + (i / (pts.length - 1 || 1)) * gw;
const y = PAD.t + gh * (1 - v / N);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.strokeStyle = ruined ? '#c92f3c' : '#0a7d33';
ctx.lineWidth = 1.8;
ctx.stroke();
// ruin / win line
ctx.setLineDash([4, 3]);
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.l, PAD.t + gh); ctx.lineTo(W - PAD.r, PAD.t + gh);
ctx.stroke();
ctx.setLineDash([]);
}
function updateStats(k, N, path) {
const exactRuin = ((N - k) / N * 100).toFixed(1);
const expTime = k * (N - k);
const cls = totalRuns === 0 ? '' : '';
let html = fmt('Exact ruin probability: <b>%exactRuin% %</b> | Expected steps: <b>%expTime%</b> | Last walk: <b>%v%</b> steps', { exactRuin: exactRuin, expTime: expTime.toLocaleString(), v: path.length - 1 });
if (totalRuns > 0) {
const obsPct = (ruinCount / totalRuns * 100).toFixed(1);
html += " " + fmt('| Simulated ruin rate: <b>%ruinCount%/%totalRuns% = %obsPct% %</b>', { ruinCount: ruinCount, totalRuns: totalRuns, obsPct: obsPct });
}
document.getElementById('stats').innerHTML = html;
}
function doRun(times) {
const { k, N } = getParams();
let lastPath, lastRuined;
for (let i = 0; i < times; i++) {
const { path, ruined } = runWalk(k, N);
totalRuns++; if (ruined) ruinCount++; else winCount++;
lastPath = path; lastRuined = ruined;
}
drawWalk(lastPath, N, lastRuined);
updateStats(k, N, lastPath);
const outcome = lastRuined
? fmt('<span class="ruin">Ruined</span> after %v% steps.', { v: (lastPath.length - 1).toLocaleString() })
: fmt('<span class="win">Won!</span> Reached %N% after %v% steps.', { N: N, v: (lastPath.length - 1).toLocaleString() });
document.getElementById('results').innerHTML = outcome;
}
function doReset() {
ruinCount = 0; winCount = 0; totalRuns = 0;
ctx.clearRect(0, 0, W, H);
document.getElementById('stats').textContent = '';
document.getElementById('results').textContent = '';
}
document.getElementById('run').addEventListener('click', () => doRun(1));
document.getElementById('run10').addEventListener('click', () => doRun(100));
document.getElementById('reset').addEventListener('click', doReset);
// Draw initial empty chart with axes
(function initChart() {
const { k, N } = getParams();
drawWalk([k], N, false);
updateStats(k, N, [k]);
})();
// Verify: exact ruin formula
console.assert(
Math.abs((100 - 10) / 100 - 0.9) < 1e-10,
'Ruin formula: k=10, N=100 should give 0.9'
);
console.assert(
Math.abs((1000 - 10) / 1000 - 0.99) < 1e-10,
'Ruin formula: k=10, N=1000 should give 0.99'
);
console.assert(
10 * (100 - 10) === 900,
'Expected steps: k=10, N=100 should be 900'
);
console.log('Gambler’s ruin formulas verified.');
<div class="controls">
<label>Your bankroll: <input id="k" type="number" value="10" min="1" max="99"></label>
<label>Casino bankroll: <input id="opp" type="number" value="90" min="1" max="999"></label>
</div>
<canvas id="chart" width="520" height="220"></canvas>
<div class="stats" id="stats"></div>
<div class="btns">
<button id="run" type="button">Run simulation</button>
<button id="run10" type="button">Run 100 times</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="results" id="results"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .7rem; }
label { font-size: .9rem; display: flex; align-items: center; gap: .4rem; }
input[type=number] { width: 70px; padding: .3rem .4rem; border: 1px solid #adb1b8;
border-radius: 6px; font-size: .95rem; }
canvas { display: block; width: 100%; max-width: 520px; border: 1px solid #cdd9e3;
border-radius: 8px; background: #f7f9fb; margin-bottom: .5rem; }
.stats { font-size: .9rem; color: #444; min-height: 1.4em; margin-bottom: .5rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
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; }
.results { font-size: .9rem; line-height: 1.6; color: #333; }
.ruin { color: #c92f3c; font-weight: 600; }
.win { color: #0a7d33; font-weight: 600; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Gambler’s Ruin: bankroll random walk between 0 and N = k + opp.
// Each step: +1 with prob 0.5, -1 with prob 0.5.
// The walk stops when it hits 0 (ruin) or N (win).
// Exact ruin probability = (N - k) / N (fair game).
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { t: 16, r: 10, b: 24, l: 36 };
let ruinCount = 0, winCount = 0, totalRuns = 0;
function getParams() {
const k = Math.max(1, parseInt(document.getElementById('k').value) || 10);
const opp = Math.max(1, parseInt(document.getElementById('opp').value) || 90);
return { k, N: k + opp };
}
// Run one walk; return { path, ruined }
function runWalk(k, N) {
const path = [k];
let pos = k;
while (pos > 0 && pos < N) {
pos += Math.random() < 0.5 ? 1 : -1;
path.push(pos);
if (path.length > 100000) break; // safety cap
}
return { path, ruined: pos === 0 };
}
function drawWalk(path, N, ruined) {
ctx.clearRect(0, 0, W, H);
const gw = W - PAD.l - PAD.r;
const gh = H - PAD.t - PAD.b;
// grid lines
ctx.strokeStyle = '#dde3ea';
ctx.lineWidth = 1;
[0, 0.25, 0.5, 0.75, 1].forEach(frac => {
const y = PAD.t + gh * (1 - frac);
ctx.beginPath(); ctx.moveTo(PAD.l, y); ctx.lineTo(W - PAD.r, y); ctx.stroke();
ctx.fillStyle = '#888'; ctx.font = '10px system-ui';
ctx.textAlign = 'right';
ctx.fillText(Math.round(frac * N), PAD.l - 4, y + 3);
});
// walk path (subsample if very long)
const step = Math.max(1, Math.floor(path.length / gw));
const pts = path.filter((_, i) => i % step === 0 || i === path.length - 1);
ctx.beginPath();
pts.forEach((v, i) => {
const x = PAD.l + (i / (pts.length - 1 || 1)) * gw;
const y = PAD.t + gh * (1 - v / N);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.strokeStyle = ruined ? '#c92f3c' : '#0a7d33';
ctx.lineWidth = 1.8;
ctx.stroke();
// ruin / win line
ctx.setLineDash([4, 3]);
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.l, PAD.t + gh); ctx.lineTo(W - PAD.r, PAD.t + gh);
ctx.stroke();
ctx.setLineDash([]);
}
function updateStats(k, N, path) {
const exactRuin = ((N - k) / N * 100).toFixed(1);
const expTime = k * (N - k);
const cls = totalRuns === 0 ? '' : '';
let html = fmt('Exact ruin probability: <b>%exactRuin% %</b> | Expected steps: <b>%expTime%</b> | Last walk: <b>%v%</b> steps', { exactRuin: exactRuin, expTime: expTime.toLocaleString(), v: path.length - 1 });
if (totalRuns > 0) {
const obsPct = (ruinCount / totalRuns * 100).toFixed(1);
html += " " + fmt('| Simulated ruin rate: <b>%ruinCount%/%totalRuns% = %obsPct% %</b>', { ruinCount: ruinCount, totalRuns: totalRuns, obsPct: obsPct });
}
document.getElementById('stats').innerHTML = html;
}
function doRun(times) {
const { k, N } = getParams();
let lastPath, lastRuined;
for (let i = 0; i < times; i++) {
const { path, ruined } = runWalk(k, N);
totalRuns++; if (ruined) ruinCount++; else winCount++;
lastPath = path; lastRuined = ruined;
}
drawWalk(lastPath, N, lastRuined);
updateStats(k, N, lastPath);
const outcome = lastRuined
? fmt('<span class="ruin">Ruined</span> after %v% steps.', { v: (lastPath.length - 1).toLocaleString() })
: fmt('<span class="win">Won!</span> Reached %N% after %v% steps.', { N: N, v: (lastPath.length - 1).toLocaleString() });
document.getElementById('results').innerHTML = outcome;
}
function doReset() {
ruinCount = 0; winCount = 0; totalRuns = 0;
ctx.clearRect(0, 0, W, H);
document.getElementById('stats').textContent = '';
document.getElementById('results').textContent = '';
}
document.getElementById('run').addEventListener('click', () => doRun(1));
document.getElementById('run10').addEventListener('click', () => doRun(100));
document.getElementById('reset').addEventListener('click', doReset);
// Draw initial empty chart with axes
(function initChart() {
const { k, N } = getParams();
drawWalk([k], N, false);
updateStats(k, N, [k]);
})();
// Verify: exact ruin formula
console.assert(
Math.abs((100 - 10) / 100 - 0.9) < 1e-10,
'Ruin formula: k=10, N=100 should give 0.9'
);
console.assert(
Math.abs((1000 - 10) / 1000 - 0.99) < 1e-10,
'Ruin formula: k=10, N=1000 should give 0.99'
);
console.assert(
10 * (100 - 10) === 900,
'Expected steps: k=10, N=100 should be 900'
);
console.log('Gambler’s ruin formulas verified.');
Notice how the path wanders, sometimes climbing hopefully, before eventually crashing to zero. Run it many times: the fraction of runs that end in your ruin converges to the exact formula k / N, where k is your starting bankroll and N is the total wealth. With $10 vs $990 that is just 1 %.
Comments
Loading comments...