The bumpy curve below is your landscape — the horizontal axis is a solution, the vertical axis is its quality (higher = better). The red dot is the current position of the climber.
<div class="controls">
<button id="btnStep" type="button">Step</button>
<button id="btnRun" type="button">Run</button>
<button id="btnRestart" type="button">Random Restart</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="560" height="280"></canvas>
<div class="info" id="info">Press Step or Run to start climbing.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; background: #fff; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; width: 100%; max-width: 560px; height: auto;
border: 1px solid #cdd9e3; border-radius: 8px; }
.info { margin-top: .55rem; font-size: .92rem; font-weight: 600; min-height: 1.4em; color: #1d3557; }
.info.stuck { color: #c92f3c; }
.info.global { color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Hill climbing on a 1-D bumpy landscape.
// The landscape is a fixed sum-of-sines with several local maxima.
// The climber moves in discrete steps of size STEP across [0,1].
// "Random restart" picks a new random start and climbs again.
const W = 560, H = 280;
const MARGIN = { top: 20, right: 20, bottom: 30, left: 44 };
const PLOT_W = W - MARGIN.left - MARGIN.right;
const PLOT_H = H - MARGIN.top - MARGIN.bottom;
const STEP = 0.01; // neighborhood step size (±1 step)
const POINTS = 300; // landscape resolution
// Landscape: sum of sines — bumpy but deterministic.
// Designed so the global max is near x≈0.46, with clear local maxima
// near x≈0.08, x≈0.17, x≈0.37, x≈0.65, x≈0.77 so any start on the
// left half (e.g. 0.05) gets visibly trapped well below the global peak.
function landscape(x) {
return (
0.45 * Math.sin(Math.PI * x) +
0.35 * Math.sin(5.5 * Math.PI * x + 0.3) +
0.20 * Math.sin(13 * Math.PI * x + 1.1) +
0.10 * Math.cos(21 * Math.PI * x + 0.7)
);
}
// Pre-compute y values and global max.
const xs = Array.from({ length: POINTS }, (_, i) => i / (POINTS - 1));
const ys = xs.map(landscape);
const yMin = Math.min(...ys);
const yMax = Math.max(...ys);
const globalMaxX = xs[ys.indexOf(yMax)];
// Map data coords -> canvas coords.
function cx(x) { return MARGIN.left + x * PLOT_W; }
function cy(y) { return MARGIN.top + PLOT_H - ((y - yMin) / (yMax - yMin)) * PLOT_H; }
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Climber state.
let pos = 0.1; // x in [0,1]
let stuck = false;
let bestX = pos;
let bestY = landscape(pos);
let restarts = 0;
let runTimer = null;
function drawLandscape() {
ctx.clearRect(0, 0, W, H);
// Axes.
ctx.strokeStyle = '#adb1b8';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(MARGIN.left, MARGIN.top);
ctx.lineTo(MARGIN.left, MARGIN.top + PLOT_H);
ctx.lineTo(MARGIN.left + PLOT_W, MARGIN.top + PLOT_H);
ctx.stroke();
// Y tick labels.
ctx.fillStyle = '#666';
ctx.font = '11px system-ui';
ctx.textAlign = 'right';
for (let t = 0; t <= 4; t++) {
const v = yMin + (t / 4) * (yMax - yMin);
const py = cy(v);
ctx.fillText(v.toFixed(1), MARGIN.left - 4, py + 3.5);
ctx.strokeStyle = '#e8eef3';
ctx.lineWidth = 0.8;
ctx.beginPath(); ctx.moveTo(MARGIN.left, py); ctx.lineTo(MARGIN.left + PLOT_W, py); ctx.stroke();
}
// Landscape curve.
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2.2;
xs.forEach((x, i) => {
const px = cx(x), py = cy(ys[i]);
i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
});
ctx.stroke();
// Global max marker (dashed vertical).
ctx.setLineDash([4, 3]);
ctx.strokeStyle = '#0a7d33';
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(cx(globalMaxX), MARGIN.top);
ctx.lineTo(cx(globalMaxX), MARGIN.top + PLOT_H);
ctx.stroke();
ctx.setLineDash([]);
// Global max label.
ctx.fillStyle = '#0a7d33';
ctx.font = '11px system-ui';
ctx.textAlign = 'left';
ctx.fillText('global max', cx(globalMaxX) + 4, MARGIN.top + 14);
// Best-found dot (outline).
const bpx = cx(bestX), bpy = cy(bestY);
ctx.beginPath();
ctx.arc(bpx, bpy, 7, 0, 2 * Math.PI);
ctx.fillStyle = 'rgba(10,125,51,0.20)';
ctx.fill();
// Current climber dot.
const ppx = cx(pos), ppy = cy(landscape(pos));
ctx.beginPath();
ctx.arc(ppx, ppy, 7, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946';
ctx.fill();
}
function setInfo(msg, cls) {
const el = document.getElementById('info');
el.textContent = msg;
el.className = 'info' + (cls ? ' ' + cls : '');
}
function clamp(x) { return Math.max(0, Math.min(1, x)); }
function step() {
if (stuck) return false;
const cur = landscape(pos);
const left = landscape(clamp(pos - STEP));
const right = landscape(clamp(pos + STEP));
const best = Math.max(left, right);
if (best <= cur) {
stuck = true;
const y = landscape(pos);
if (y > bestY) { bestY = y; bestX = pos; }
const isGlobal = Math.abs(pos - globalMaxX) < 0.05;
drawLandscape();
if (isGlobal) {
setInfo(fmt('Reached the global maximum! x = %pos%, y = %y%', { pos: pos.toFixed(2), y: y.toFixed(3) }), 'global');
} else {
setInfo(fmt('Stuck on a local maximum. x = %pos%, y = %y%. Try a Random Restart.', { pos: pos.toFixed(2), y: y.toFixed(3) }), 'stuck');
}
return false;
}
if (right >= left) pos = clamp(pos + STEP);
else pos = clamp(pos - STEP);
const y = landscape(pos);
if (y > bestY) { bestY = y; bestX = pos; }
drawLandscape();
setInfo(fmt('Climbing… x = %pos%, y = %y%', { pos: pos.toFixed(2), y: y.toFixed(3) }));
return true;
}
function stopRun() {
if (runTimer !== null) { clearInterval(runTimer); runTimer = null; }
}
document.getElementById('btnStep').addEventListener('click', () => {
stopRun();
step();
});
document.getElementById('btnRun').addEventListener('click', () => {
stopRun();
if (stuck) return;
runTimer = setInterval(() => { if (!step()) stopRun(); }, 60);
});
document.getElementById('btnRestart').addEventListener('click', () => {
stopRun();
pos = Math.random();
stuck = false;
restarts++;
drawLandscape();
setInfo(fmt('New random start at x = %pos%. Press Run.', { pos: pos.toFixed(2) }));
});
document.getElementById('btnReset').addEventListener('click', () => {
stopRun();
pos = 0.1;
stuck = false;
restarts = 0;
bestX = pos;
bestY = landscape(pos);
drawLandscape();
setInfo('Press Step or Run to start climbing.');
});
// Initial draw.
drawLandscape();
setInfo('Press Step or Run to start climbing.');
<div class="controls">
<button id="btnStep" type="button">Step</button>
<button id="btnRun" type="button">Run</button>
<button id="btnRestart" type="button">Random Restart</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="560" height="280"></canvas>
<div class="info" id="info">Press Step or Run to start climbing.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; background: #fff; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; width: 100%; max-width: 560px; height: auto;
border: 1px solid #cdd9e3; border-radius: 8px; }
.info { margin-top: .55rem; font-size: .92rem; font-weight: 600; min-height: 1.4em; color: #1d3557; }
.info.stuck { color: #c92f3c; }
.info.global { color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Hill climbing on a 1-D bumpy landscape.
// The landscape is a fixed sum-of-sines with several local maxima.
// The climber moves in discrete steps of size STEP across [0,1].
// "Random restart" picks a new random start and climbs again.
const W = 560, H = 280;
const MARGIN = { top: 20, right: 20, bottom: 30, left: 44 };
const PLOT_W = W - MARGIN.left - MARGIN.right;
const PLOT_H = H - MARGIN.top - MARGIN.bottom;
const STEP = 0.01; // neighborhood step size (±1 step)
const POINTS = 300; // landscape resolution
// Landscape: sum of sines — bumpy but deterministic.
// Designed so the global max is near x≈0.46, with clear local maxima
// near x≈0.08, x≈0.17, x≈0.37, x≈0.65, x≈0.77 so any start on the
// left half (e.g. 0.05) gets visibly trapped well below the global peak.
function landscape(x) {
return (
0.45 * Math.sin(Math.PI * x) +
0.35 * Math.sin(5.5 * Math.PI * x + 0.3) +
0.20 * Math.sin(13 * Math.PI * x + 1.1) +
0.10 * Math.cos(21 * Math.PI * x + 0.7)
);
}
// Pre-compute y values and global max.
const xs = Array.from({ length: POINTS }, (_, i) => i / (POINTS - 1));
const ys = xs.map(landscape);
const yMin = Math.min(...ys);
const yMax = Math.max(...ys);
const globalMaxX = xs[ys.indexOf(yMax)];
// Map data coords -> canvas coords.
function cx(x) { return MARGIN.left + x * PLOT_W; }
function cy(y) { return MARGIN.top + PLOT_H - ((y - yMin) / (yMax - yMin)) * PLOT_H; }
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Climber state.
let pos = 0.1; // x in [0,1]
let stuck = false;
let bestX = pos;
let bestY = landscape(pos);
let restarts = 0;
let runTimer = null;
function drawLandscape() {
ctx.clearRect(0, 0, W, H);
// Axes.
ctx.strokeStyle = '#adb1b8';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(MARGIN.left, MARGIN.top);
ctx.lineTo(MARGIN.left, MARGIN.top + PLOT_H);
ctx.lineTo(MARGIN.left + PLOT_W, MARGIN.top + PLOT_H);
ctx.stroke();
// Y tick labels.
ctx.fillStyle = '#666';
ctx.font = '11px system-ui';
ctx.textAlign = 'right';
for (let t = 0; t <= 4; t++) {
const v = yMin + (t / 4) * (yMax - yMin);
const py = cy(v);
ctx.fillText(v.toFixed(1), MARGIN.left - 4, py + 3.5);
ctx.strokeStyle = '#e8eef3';
ctx.lineWidth = 0.8;
ctx.beginPath(); ctx.moveTo(MARGIN.left, py); ctx.lineTo(MARGIN.left + PLOT_W, py); ctx.stroke();
}
// Landscape curve.
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2.2;
xs.forEach((x, i) => {
const px = cx(x), py = cy(ys[i]);
i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
});
ctx.stroke();
// Global max marker (dashed vertical).
ctx.setLineDash([4, 3]);
ctx.strokeStyle = '#0a7d33';
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(cx(globalMaxX), MARGIN.top);
ctx.lineTo(cx(globalMaxX), MARGIN.top + PLOT_H);
ctx.stroke();
ctx.setLineDash([]);
// Global max label.
ctx.fillStyle = '#0a7d33';
ctx.font = '11px system-ui';
ctx.textAlign = 'left';
ctx.fillText('global max', cx(globalMaxX) + 4, MARGIN.top + 14);
// Best-found dot (outline).
const bpx = cx(bestX), bpy = cy(bestY);
ctx.beginPath();
ctx.arc(bpx, bpy, 7, 0, 2 * Math.PI);
ctx.fillStyle = 'rgba(10,125,51,0.20)';
ctx.fill();
// Current climber dot.
const ppx = cx(pos), ppy = cy(landscape(pos));
ctx.beginPath();
ctx.arc(ppx, ppy, 7, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946';
ctx.fill();
}
function setInfo(msg, cls) {
const el = document.getElementById('info');
el.textContent = msg;
el.className = 'info' + (cls ? ' ' + cls : '');
}
function clamp(x) { return Math.max(0, Math.min(1, x)); }
function step() {
if (stuck) return false;
const cur = landscape(pos);
const left = landscape(clamp(pos - STEP));
const right = landscape(clamp(pos + STEP));
const best = Math.max(left, right);
if (best <= cur) {
stuck = true;
const y = landscape(pos);
if (y > bestY) { bestY = y; bestX = pos; }
const isGlobal = Math.abs(pos - globalMaxX) < 0.05;
drawLandscape();
if (isGlobal) {
setInfo(fmt('Reached the global maximum! x = %pos%, y = %y%', { pos: pos.toFixed(2), y: y.toFixed(3) }), 'global');
} else {
setInfo(fmt('Stuck on a local maximum. x = %pos%, y = %y%. Try a Random Restart.', { pos: pos.toFixed(2), y: y.toFixed(3) }), 'stuck');
}
return false;
}
if (right >= left) pos = clamp(pos + STEP);
else pos = clamp(pos - STEP);
const y = landscape(pos);
if (y > bestY) { bestY = y; bestX = pos; }
drawLandscape();
setInfo(fmt('Climbing… x = %pos%, y = %y%', { pos: pos.toFixed(2), y: y.toFixed(3) }));
return true;
}
function stopRun() {
if (runTimer !== null) { clearInterval(runTimer); runTimer = null; }
}
document.getElementById('btnStep').addEventListener('click', () => {
stopRun();
step();
});
document.getElementById('btnRun').addEventListener('click', () => {
stopRun();
if (stuck) return;
runTimer = setInterval(() => { if (!step()) stopRun(); }, 60);
});
document.getElementById('btnRestart').addEventListener('click', () => {
stopRun();
pos = Math.random();
stuck = false;
restarts++;
drawLandscape();
setInfo(fmt('New random start at x = %pos%. Press Run.', { pos: pos.toFixed(2) }));
});
document.getElementById('btnReset').addEventListener('click', () => {
stopRun();
pos = 0.1;
stuck = false;
restarts = 0;
bestX = pos;
bestY = landscape(pos);
drawLandscape();
setInfo('Press Step or Run to start climbing.');
});
// Initial draw.
drawLandscape();
setInfo('Press Step or Run to start climbing.');
Comments
Loading comments...