Press Start to watch a drunkard take random steps on a 2D grid. The blue dot is the current position; the faint trail shows the path. The chart below the grid tracks the distance from the origin over time.
<div class="controls">
<button id="startBtn" type="button">Start</button>
<button id="stepBtn" type="button">Step ×10</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
<span class="label">Dimension:</span>
<button id="dim2d" type="button" class="dim active">2D</button>
<button id="dim3d" type="button" class="dim">3D</button>
</div>
<div class="stats">
Steps: <strong id="stepCount">0</strong> |
Distance: <strong id="distVal">0.00</strong> |
Returns to origin: <strong id="returnCount">0</strong>
</div>
<canvas id="canvas" width="460" height="200"></canvas>
<div class="chart-label">Distance from origin over time</div>
<canvas id="chart" width="460" height="100"></canvas>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.controls { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; margin-bottom: .5rem; }
.label { font-size: .85rem; color: #555; margin-left: .3rem; }
.stats { font-size: .85rem; color: #444; margin-bottom: .4rem; }
button {
font: 600 13px system-ui; padding: .35rem .75rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 7px; cursor: pointer;
}
button.ghost { background: #fff; color: #1d3557; }
button.dim { background: #e8eef3; color: #1d3557; border-color: #cdd9e3; }
button.dim.active { background: #1d3557; color: #fff; }
canvas { display: block; border-radius: 8px; background: #f4f7fa; margin-bottom: .3rem; }
.chart-label { font-size: .75rem; color: #888; margin-bottom: .2rem; }
// Pólya's theorem: a symmetric random walk on Z^d is recurrent (returns to origin
// infinitely often, probability 1) for d=1,2 and transient for d≥3.
// This demo shows a 2D walk (recurrent) vs a 3D walk projected to 2D (transient).
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const chartCanvas = document.getElementById('chart');
const cctx = chartCanvas.getContext('2d');
let pos = { x: 0, y: 0, z: 0 };
let steps = 0, returns = 0;
let dim = 2;
let running = false;
let timer = null;
let history = []; // distance over time (downsampled)
const MAX_HISTORY = 460;
// Map lattice coords to canvas pixels (center = 230,100; scale = 8px per unit)
const CX = 230, CY = 100, SCALE = 8;
let trail = []; // screen coords
const MAX_TRAIL = 600;
function latticeToScreen(x, y) {
return { sx: CX + x * SCALE, sy: CY + y * SCALE };
}
function dist() {
if (dim === 2) return Math.sqrt(pos.x * pos.x + pos.y * pos.y);
return Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z);
}
function step() {
if (dim === 2) {
const d = Math.floor(Math.random() * 4);
if (d === 0) pos.x++;
else if (d === 1) pos.x--;
else if (d === 2) pos.y++;
else pos.y--;
} else {
const d = Math.floor(Math.random() * 6);
if (d === 0) pos.x++;
else if (d === 1) pos.x--;
else if (d === 2) pos.y++;
else if (d === 3) pos.y--;
else if (d === 4) pos.z++;
else pos.z--;
}
steps++;
if (pos.x === 0 && pos.y === 0 && pos.z === 0) returns++;
const { sx, sy } = latticeToScreen(pos.x, pos.y);
trail.push({ sx, sy });
if (trail.length > MAX_TRAIL) trail.shift();
if (history.length < MAX_HISTORY) history.push(dist());
else { history.shift(); history.push(dist()); }
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Grid lines (faint)
ctx.strokeStyle = '#dde3ea';
ctx.lineWidth = 0.5;
for (let x = CX % SCALE; x < canvas.width; x += SCALE) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
}
for (let y = CY % SCALE; y < canvas.height; y += SCALE) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
}
// Origin cross
ctx.strokeStyle = '#b0bec8'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(CX - 6, CY); ctx.lineTo(CX + 6, CY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(CX, CY - 6); ctx.lineTo(CX, CY + 6); ctx.stroke();
// Trail
if (trail.length > 1) {
ctx.beginPath();
ctx.moveTo(trail[0].sx, trail[0].sy);
for (let i = 1; i < trail.length; i++) ctx.lineTo(trail[i].sx, trail[i].sy);
ctx.strokeStyle = 'rgba(100,140,180,0.35)';
ctx.lineWidth = 1.2;
ctx.stroke();
}
// Current position dot
const { sx, sy } = latticeToScreen(pos.x, pos.y);
ctx.beginPath(); ctx.arc(sx, sy, 5, 0, Math.PI * 2);
ctx.fillStyle = returns > 0 ? '#0a7d33' : '#1d3557';
ctx.fill();
}
function drawChart() {
cctx.clearRect(0, 0, chartCanvas.width, chartCanvas.height);
if (history.length < 2) return;
const maxD = Math.max(...history, 1);
const w = chartCanvas.width, h = chartCanvas.height;
cctx.beginPath();
for (let i = 0; i < history.length; i++) {
const x = (i / (MAX_HISTORY - 1)) * w;
const y = h - (history[i] / maxD) * (h - 4) - 2;
if (i === 0) cctx.moveTo(x, y); else cctx.lineTo(x, y);
}
cctx.strokeStyle = '#1d7fd4';
cctx.lineWidth = 1.4;
cctx.stroke();
// Baseline
cctx.beginPath(); cctx.moveTo(0, h - 1); cctx.lineTo(w, h - 1);
cctx.strokeStyle = '#ccd5de'; cctx.lineWidth = 1; cctx.stroke();
}
function updateStats() {
document.getElementById('stepCount').textContent = steps;
document.getElementById('distVal').textContent = dist().toFixed(2);
document.getElementById('returnCount').textContent = returns;
}
function doSteps(n) {
for (let i = 0; i < n; i++) step();
draw(); drawChart(); updateStats();
}
function reset() {
pos = { x: 0, y: 0, z: 0 };
steps = 0; returns = 0;
trail = [{ sx: CX, sy: CY }];
history = [0];
running = false;
clearInterval(timer); timer = null;
document.getElementById('startBtn').textContent = 'Start';
draw(); drawChart(); updateStats();
}
document.getElementById('startBtn').addEventListener('click', () => {
running = !running;
document.getElementById('startBtn').textContent = running ? 'Pause' : 'Resume';
if (running) {
timer = setInterval(() => doSteps(5), 50);
} else {
clearInterval(timer); timer = null;
}
});
document.getElementById('stepBtn').addEventListener('click', () => {
if (running) { clearInterval(timer); timer = null; running = false; document.getElementById('startBtn').textContent = 'Resume'; }
doSteps(10);
});
document.getElementById('resetBtn').addEventListener('click', reset);
document.getElementById('dim2d').addEventListener('click', () => {
dim = 2;
document.getElementById('dim2d').classList.add('active');
document.getElementById('dim3d').classList.remove('active');
reset();
});
document.getElementById('dim3d').addEventListener('click', () => {
dim = 3;
document.getElementById('dim3d').classList.add('active');
document.getElementById('dim2d').classList.remove('active');
reset();
});
reset();
<div class="controls">
<button id="startBtn" type="button">Start</button>
<button id="stepBtn" type="button">Step ×10</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
<span class="label">Dimension:</span>
<button id="dim2d" type="button" class="dim active">2D</button>
<button id="dim3d" type="button" class="dim">3D</button>
</div>
<div class="stats">
Steps: <strong id="stepCount">0</strong> |
Distance: <strong id="distVal">0.00</strong> |
Returns to origin: <strong id="returnCount">0</strong>
</div>
<canvas id="canvas" width="460" height="200"></canvas>
<div class="chart-label">Distance from origin over time</div>
<canvas id="chart" width="460" height="100"></canvas>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.controls { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; margin-bottom: .5rem; }
.label { font-size: .85rem; color: #555; margin-left: .3rem; }
.stats { font-size: .85rem; color: #444; margin-bottom: .4rem; }
button {
font: 600 13px system-ui; padding: .35rem .75rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 7px; cursor: pointer;
}
button.ghost { background: #fff; color: #1d3557; }
button.dim { background: #e8eef3; color: #1d3557; border-color: #cdd9e3; }
button.dim.active { background: #1d3557; color: #fff; }
canvas { display: block; border-radius: 8px; background: #f4f7fa; margin-bottom: .3rem; }
.chart-label { font-size: .75rem; color: #888; margin-bottom: .2rem; }
// Pólya's theorem: a symmetric random walk on Z^d is recurrent (returns to origin
// infinitely often, probability 1) for d=1,2 and transient for d≥3.
// This demo shows a 2D walk (recurrent) vs a 3D walk projected to 2D (transient).
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const chartCanvas = document.getElementById('chart');
const cctx = chartCanvas.getContext('2d');
let pos = { x: 0, y: 0, z: 0 };
let steps = 0, returns = 0;
let dim = 2;
let running = false;
let timer = null;
let history = []; // distance over time (downsampled)
const MAX_HISTORY = 460;
// Map lattice coords to canvas pixels (center = 230,100; scale = 8px per unit)
const CX = 230, CY = 100, SCALE = 8;
let trail = []; // screen coords
const MAX_TRAIL = 600;
function latticeToScreen(x, y) {
return { sx: CX + x * SCALE, sy: CY + y * SCALE };
}
function dist() {
if (dim === 2) return Math.sqrt(pos.x * pos.x + pos.y * pos.y);
return Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z);
}
function step() {
if (dim === 2) {
const d = Math.floor(Math.random() * 4);
if (d === 0) pos.x++;
else if (d === 1) pos.x--;
else if (d === 2) pos.y++;
else pos.y--;
} else {
const d = Math.floor(Math.random() * 6);
if (d === 0) pos.x++;
else if (d === 1) pos.x--;
else if (d === 2) pos.y++;
else if (d === 3) pos.y--;
else if (d === 4) pos.z++;
else pos.z--;
}
steps++;
if (pos.x === 0 && pos.y === 0 && pos.z === 0) returns++;
const { sx, sy } = latticeToScreen(pos.x, pos.y);
trail.push({ sx, sy });
if (trail.length > MAX_TRAIL) trail.shift();
if (history.length < MAX_HISTORY) history.push(dist());
else { history.shift(); history.push(dist()); }
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Grid lines (faint)
ctx.strokeStyle = '#dde3ea';
ctx.lineWidth = 0.5;
for (let x = CX % SCALE; x < canvas.width; x += SCALE) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
}
for (let y = CY % SCALE; y < canvas.height; y += SCALE) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
}
// Origin cross
ctx.strokeStyle = '#b0bec8'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(CX - 6, CY); ctx.lineTo(CX + 6, CY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(CX, CY - 6); ctx.lineTo(CX, CY + 6); ctx.stroke();
// Trail
if (trail.length > 1) {
ctx.beginPath();
ctx.moveTo(trail[0].sx, trail[0].sy);
for (let i = 1; i < trail.length; i++) ctx.lineTo(trail[i].sx, trail[i].sy);
ctx.strokeStyle = 'rgba(100,140,180,0.35)';
ctx.lineWidth = 1.2;
ctx.stroke();
}
// Current position dot
const { sx, sy } = latticeToScreen(pos.x, pos.y);
ctx.beginPath(); ctx.arc(sx, sy, 5, 0, Math.PI * 2);
ctx.fillStyle = returns > 0 ? '#0a7d33' : '#1d3557';
ctx.fill();
}
function drawChart() {
cctx.clearRect(0, 0, chartCanvas.width, chartCanvas.height);
if (history.length < 2) return;
const maxD = Math.max(...history, 1);
const w = chartCanvas.width, h = chartCanvas.height;
cctx.beginPath();
for (let i = 0; i < history.length; i++) {
const x = (i / (MAX_HISTORY - 1)) * w;
const y = h - (history[i] / maxD) * (h - 4) - 2;
if (i === 0) cctx.moveTo(x, y); else cctx.lineTo(x, y);
}
cctx.strokeStyle = '#1d7fd4';
cctx.lineWidth = 1.4;
cctx.stroke();
// Baseline
cctx.beginPath(); cctx.moveTo(0, h - 1); cctx.lineTo(w, h - 1);
cctx.strokeStyle = '#ccd5de'; cctx.lineWidth = 1; cctx.stroke();
}
function updateStats() {
document.getElementById('stepCount').textContent = steps;
document.getElementById('distVal').textContent = dist().toFixed(2);
document.getElementById('returnCount').textContent = returns;
}
function doSteps(n) {
for (let i = 0; i < n; i++) step();
draw(); drawChart(); updateStats();
}
function reset() {
pos = { x: 0, y: 0, z: 0 };
steps = 0; returns = 0;
trail = [{ sx: CX, sy: CY }];
history = [0];
running = false;
clearInterval(timer); timer = null;
document.getElementById('startBtn').textContent = 'Start';
draw(); drawChart(); updateStats();
}
document.getElementById('startBtn').addEventListener('click', () => {
running = !running;
document.getElementById('startBtn').textContent = running ? 'Pause' : 'Resume';
if (running) {
timer = setInterval(() => doSteps(5), 50);
} else {
clearInterval(timer); timer = null;
}
});
document.getElementById('stepBtn').addEventListener('click', () => {
if (running) { clearInterval(timer); timer = null; running = false; document.getElementById('startBtn').textContent = 'Resume'; }
doSteps(10);
});
document.getElementById('resetBtn').addEventListener('click', reset);
document.getElementById('dim2d').addEventListener('click', () => {
dim = 2;
document.getElementById('dim2d').classList.add('active');
document.getElementById('dim3d').classList.remove('active');
reset();
});
document.getElementById('dim3d').addEventListener('click', () => {
dim = 3;
document.getElementById('dim3d').classList.add('active');
document.getElementById('dim2d').classList.remove('active');
reset();
});
reset();
Notice how the walk wanders far out, then doubles back and crosses the origin again. That crossing is guaranteed — given enough time, the 2D walk returns with certainty. Compare this to what Brownian motion looks like in physics: the same mathematics, scaled to atomic distances.
Comments
Loading comments...