The demo below runs the same simple hill-climber on two different one-dimensional landscapes. Pick a landscape, drop a starting point, and watch the algorithm move.
<!-- Landscape selector + canvas for the fitness landscape demo -->
<div class="controls">
<label class="lbl">Landscape:</label>
<div class="btn-group">
<button id="btn-smooth" class="mode-btn active" type="button">Smooth</button>
<button id="btn-rugged" class="mode-btn" type="button">Rugged</button>
</div>
</div>
<p class="hint" id="hint">Click anywhere on the curve to drop the hill-climber.</p>
<canvas id="cv" width="480" height="220"></canvas>
<div class="status" id="status"></div>
<button id="btn-reset" class="ghost-btn" type="button">Reset</button>
/* Styles for the fitness landscape demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; align-items: center; gap: .7rem; margin-bottom: .5rem; flex-wrap: wrap; }
.lbl { font-weight: 600; font-size: .9rem; }
.btn-group { display: flex; gap: .3rem; }
.mode-btn { font: 600 13px system-ui; padding: .35rem .75rem; border: 1.5px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 6px; cursor: pointer; transition: all .15s; }
.mode-btn.active { background: #1d3557; color: #fff; }
.hint { font-size: .85rem; color: #555; margin: 0 0 .4rem; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; cursor: crosshair;
max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .5rem 0; }
.status.ok { color: #0a7d33; }
.status.warn { color: #b84c00; }
.ghost-btn { font: 600 13px system-ui; padding: .35rem .75rem; border: 1.5px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 6px; cursor: pointer; }
/* Fitness landscape hill-climber demo — smooth vs rugged */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const W = 480, H = 220;
const PAD_L = 10, PAD_R = 10, PAD_T = 20, PAD_B = 30;
const PLOT_W = W - PAD_L - PAD_R;
const PLOT_H = H - PAD_T - PAD_B;
/* Two landscapes: smooth (unimodal) and rugged (multimodal) */
const LANDSCAPES = {
smooth: {
/* Single-peak sine wave, always reaches the global optimum */
fn: (x) => Math.sin(x * Math.PI) * 0.9 + 0.05,
peaks: 1
},
rugged: {
/* Superposition of frequencies — several local peaks */
fn: (x) => (
0.55 * Math.sin(x * Math.PI * 2 - 0.4) +
0.25 * Math.sin(x * Math.PI * 6 + 0.8) +
0.15 * Math.sin(x * Math.PI * 14 + 1.2) +
0.5
) / 1.45,
peaks: 5
}
};
let mode = 'smooth';
let climberX = null;
let animId = null;
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const statusEl = document.getElementById('status');
const hintEl = document.getElementById('hint');
/* Evaluate fitness at position x in [0,1] */
function fitness(landscapeKey, x) {
return Math.max(0, Math.min(1, LANDSCAPES[landscapeKey].fn(x)));
}
/* Draw the landscape curve and the climber position */
function drawLandscape(landscapeKey, highlightX, climberHistory) {
ctx.clearRect(0, 0, W, H);
/* Shaded area under the curve */
const grad = ctx.createLinearGradient(0, PAD_T, 0, PAD_T + PLOT_H);
grad.addColorStop(0, 'rgba(29,53,87,0.15)');
grad.addColorStop(1, 'rgba(29,53,87,0.02)');
ctx.beginPath();
for (let px = 0; px <= PLOT_W; px++) {
const x = px / PLOT_W;
const y = PAD_T + PLOT_H - fitness(landscapeKey, x) * PLOT_H;
if (px === 0) ctx.moveTo(PAD_L + px, y);
else ctx.lineTo(PAD_L + px, y);
}
ctx.lineTo(PAD_L + PLOT_W, PAD_T + PLOT_H);
ctx.lineTo(PAD_L, PAD_T + PLOT_H);
ctx.closePath();
ctx.fillStyle = grad;
ctx.fill();
/* Main landscape curve */
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2.2;
for (let px = 0; px <= PLOT_W; px++) {
const x = px / PLOT_W;
const y = PAD_T + PLOT_H - fitness(landscapeKey, x) * PLOT_H;
if (px === 0) ctx.moveTo(PAD_L + px, y);
else ctx.lineTo(PAD_L + px, y);
}
ctx.stroke();
/* X-axis baseline */
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD_L, PAD_T + PLOT_H);
ctx.lineTo(PAD_L + PLOT_W, PAD_T + PLOT_H);
ctx.stroke();
/* Axis labels */
ctx.fillStyle = '#666';
ctx.font = '11px system-ui';
ctx.textAlign = 'left';
ctx.fillText('Low', PAD_L, PAD_T + PLOT_H + 18);
ctx.textAlign = 'right';
ctx.fillText('High', PAD_L + PLOT_W, PAD_T + PLOT_H + 18);
ctx.textAlign = 'center';
ctx.fillText('Solution space', PAD_L + PLOT_W / 2, PAD_T + PLOT_H + 18);
/* Dashed trail showing the climber's path */
if (climberHistory && climberHistory.length > 1) {
ctx.strokeStyle = 'rgba(200,50,50,0.35)';
ctx.lineWidth = 1.5;
ctx.setLineDash([3, 3]);
ctx.beginPath();
climberHistory.forEach((hx, i) => {
const px = PAD_L + hx * PLOT_W;
const py = PAD_T + PLOT_H - fitness(landscapeKey, hx) * PLOT_H;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
});
ctx.stroke();
ctx.setLineDash([]);
}
/* Red dot for the current climber position */
if (highlightX !== null) {
const px = PAD_L + highlightX * PLOT_W;
const py = PAD_T + PLOT_H - fitness(landscapeKey, highlightX) * PLOT_H;
ctx.beginPath();
ctx.arc(px, py, 7, 0, Math.PI * 2);
ctx.fillStyle = '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
}
/* Return next x if a better neighbor exists, null if at a local peak */
function hillClimbStep(landscapeKey, x) {
const delta = 0.01;
const here = fitness(landscapeKey, x);
const left = x - delta >= 0 ? fitness(landscapeKey, x - delta) : -1;
const right = x + delta <= 1 ? fitness(landscapeKey, x + delta) : -1;
if (left > here && left >= right) return x - delta;
if (right > here) return x + delta;
return null; /* reached a local peak — stop */
}
let history = [];
/* Animate the hill-climber one step at a time */
function animate(landscapeKey, x, steps) {
const next = hillClimbStep(landscapeKey, x);
if (next === null || steps <= 0) {
/* Climber stopped — check if it found the global optimum */
const f = (fitness(landscapeKey, x) * 100).toFixed(1);
const isGlobal = isGlobalOptimum(landscapeKey, x);
if (isGlobal) {
statusEl.className = 'status ok';
statusEl.textContent = i18nFmt('Global optimum reached! Fitness: %f%%', { f });
} else {
statusEl.className = 'status warn';
statusEl.textContent = i18nFmt('Stuck at local optimum. Fitness: %f%% — a better peak exists.', { f });
}
drawLandscape(landscapeKey, x, history);
return;
}
history.push(next);
climberX = next;
drawLandscape(landscapeKey, next, history);
animId = setTimeout(() => animate(landscapeKey, next, steps - 1), 60);
}
/* Return true if no point scores more than 2% higher than x */
function isGlobalOptimum(landscapeKey, x) {
const myF = fitness(landscapeKey, x);
for (let px = 0; px <= 200; px++) {
if (fitness(landscapeKey, px / 200) > myF + 0.02) return false;
}
return true;
}
function startClimber(x) {
if (animId) clearTimeout(animId);
climberX = x;
history = [x];
statusEl.className = 'status';
statusEl.textContent = 'Climbing…';
drawLandscape(mode, x, history);
animate(mode, x, 300);
}
cv.addEventListener('click', (e) => {
const rect = cv.getBoundingClientRect();
const scaleX = W / rect.width;
const px = (e.clientX - rect.left) * scaleX;
const x = (px - PAD_L) / PLOT_W;
if (x < 0 || x > 1) return;
hintEl.textContent = 'Running hill-climber…';
startClimber(x);
});
document.getElementById('btn-smooth').addEventListener('click', () => {
mode = 'smooth';
document.getElementById('btn-smooth').classList.add('active');
document.getElementById('btn-rugged').classList.remove('active');
reset();
});
document.getElementById('btn-rugged').addEventListener('click', () => {
mode = 'rugged';
document.getElementById('btn-rugged').classList.add('active');
document.getElementById('btn-smooth').classList.remove('active');
reset();
});
document.getElementById('btn-reset').addEventListener('click', reset);
function reset() {
if (animId) clearTimeout(animId);
climberX = null;
history = [];
statusEl.textContent = '';
statusEl.className = 'status';
hintEl.textContent = 'Click anywhere on the curve to drop the hill-climber.';
drawLandscape(mode, null, []);
}
/* Initial draw on page load */
drawLandscape(mode, null, []);
<!-- Landscape selector + canvas for the fitness landscape demo -->
<div class="controls">
<label class="lbl">Landscape:</label>
<div class="btn-group">
<button id="btn-smooth" class="mode-btn active" type="button">Smooth</button>
<button id="btn-rugged" class="mode-btn" type="button">Rugged</button>
</div>
</div>
<p class="hint" id="hint">Click anywhere on the curve to drop the hill-climber.</p>
<canvas id="cv" width="480" height="220"></canvas>
<div class="status" id="status"></div>
<button id="btn-reset" class="ghost-btn" type="button">Reset</button>
/* Styles for the fitness landscape demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; align-items: center; gap: .7rem; margin-bottom: .5rem; flex-wrap: wrap; }
.lbl { font-weight: 600; font-size: .9rem; }
.btn-group { display: flex; gap: .3rem; }
.mode-btn { font: 600 13px system-ui; padding: .35rem .75rem; border: 1.5px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 6px; cursor: pointer; transition: all .15s; }
.mode-btn.active { background: #1d3557; color: #fff; }
.hint { font-size: .85rem; color: #555; margin: 0 0 .4rem; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; cursor: crosshair;
max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .5rem 0; }
.status.ok { color: #0a7d33; }
.status.warn { color: #b84c00; }
.ghost-btn { font: 600 13px system-ui; padding: .35rem .75rem; border: 1.5px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 6px; cursor: pointer; }
/* Fitness landscape hill-climber demo — smooth vs rugged */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const W = 480, H = 220;
const PAD_L = 10, PAD_R = 10, PAD_T = 20, PAD_B = 30;
const PLOT_W = W - PAD_L - PAD_R;
const PLOT_H = H - PAD_T - PAD_B;
/* Two landscapes: smooth (unimodal) and rugged (multimodal) */
const LANDSCAPES = {
smooth: {
/* Single-peak sine wave, always reaches the global optimum */
fn: (x) => Math.sin(x * Math.PI) * 0.9 + 0.05,
peaks: 1
},
rugged: {
/* Superposition of frequencies — several local peaks */
fn: (x) => (
0.55 * Math.sin(x * Math.PI * 2 - 0.4) +
0.25 * Math.sin(x * Math.PI * 6 + 0.8) +
0.15 * Math.sin(x * Math.PI * 14 + 1.2) +
0.5
) / 1.45,
peaks: 5
}
};
let mode = 'smooth';
let climberX = null;
let animId = null;
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const statusEl = document.getElementById('status');
const hintEl = document.getElementById('hint');
/* Evaluate fitness at position x in [0,1] */
function fitness(landscapeKey, x) {
return Math.max(0, Math.min(1, LANDSCAPES[landscapeKey].fn(x)));
}
/* Draw the landscape curve and the climber position */
function drawLandscape(landscapeKey, highlightX, climberHistory) {
ctx.clearRect(0, 0, W, H);
/* Shaded area under the curve */
const grad = ctx.createLinearGradient(0, PAD_T, 0, PAD_T + PLOT_H);
grad.addColorStop(0, 'rgba(29,53,87,0.15)');
grad.addColorStop(1, 'rgba(29,53,87,0.02)');
ctx.beginPath();
for (let px = 0; px <= PLOT_W; px++) {
const x = px / PLOT_W;
const y = PAD_T + PLOT_H - fitness(landscapeKey, x) * PLOT_H;
if (px === 0) ctx.moveTo(PAD_L + px, y);
else ctx.lineTo(PAD_L + px, y);
}
ctx.lineTo(PAD_L + PLOT_W, PAD_T + PLOT_H);
ctx.lineTo(PAD_L, PAD_T + PLOT_H);
ctx.closePath();
ctx.fillStyle = grad;
ctx.fill();
/* Main landscape curve */
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2.2;
for (let px = 0; px <= PLOT_W; px++) {
const x = px / PLOT_W;
const y = PAD_T + PLOT_H - fitness(landscapeKey, x) * PLOT_H;
if (px === 0) ctx.moveTo(PAD_L + px, y);
else ctx.lineTo(PAD_L + px, y);
}
ctx.stroke();
/* X-axis baseline */
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD_L, PAD_T + PLOT_H);
ctx.lineTo(PAD_L + PLOT_W, PAD_T + PLOT_H);
ctx.stroke();
/* Axis labels */
ctx.fillStyle = '#666';
ctx.font = '11px system-ui';
ctx.textAlign = 'left';
ctx.fillText('Low', PAD_L, PAD_T + PLOT_H + 18);
ctx.textAlign = 'right';
ctx.fillText('High', PAD_L + PLOT_W, PAD_T + PLOT_H + 18);
ctx.textAlign = 'center';
ctx.fillText('Solution space', PAD_L + PLOT_W / 2, PAD_T + PLOT_H + 18);
/* Dashed trail showing the climber's path */
if (climberHistory && climberHistory.length > 1) {
ctx.strokeStyle = 'rgba(200,50,50,0.35)';
ctx.lineWidth = 1.5;
ctx.setLineDash([3, 3]);
ctx.beginPath();
climberHistory.forEach((hx, i) => {
const px = PAD_L + hx * PLOT_W;
const py = PAD_T + PLOT_H - fitness(landscapeKey, hx) * PLOT_H;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
});
ctx.stroke();
ctx.setLineDash([]);
}
/* Red dot for the current climber position */
if (highlightX !== null) {
const px = PAD_L + highlightX * PLOT_W;
const py = PAD_T + PLOT_H - fitness(landscapeKey, highlightX) * PLOT_H;
ctx.beginPath();
ctx.arc(px, py, 7, 0, Math.PI * 2);
ctx.fillStyle = '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
}
/* Return next x if a better neighbor exists, null if at a local peak */
function hillClimbStep(landscapeKey, x) {
const delta = 0.01;
const here = fitness(landscapeKey, x);
const left = x - delta >= 0 ? fitness(landscapeKey, x - delta) : -1;
const right = x + delta <= 1 ? fitness(landscapeKey, x + delta) : -1;
if (left > here && left >= right) return x - delta;
if (right > here) return x + delta;
return null; /* reached a local peak — stop */
}
let history = [];
/* Animate the hill-climber one step at a time */
function animate(landscapeKey, x, steps) {
const next = hillClimbStep(landscapeKey, x);
if (next === null || steps <= 0) {
/* Climber stopped — check if it found the global optimum */
const f = (fitness(landscapeKey, x) * 100).toFixed(1);
const isGlobal = isGlobalOptimum(landscapeKey, x);
if (isGlobal) {
statusEl.className = 'status ok';
statusEl.textContent = i18nFmt('Global optimum reached! Fitness: %f%%', { f });
} else {
statusEl.className = 'status warn';
statusEl.textContent = i18nFmt('Stuck at local optimum. Fitness: %f%% — a better peak exists.', { f });
}
drawLandscape(landscapeKey, x, history);
return;
}
history.push(next);
climberX = next;
drawLandscape(landscapeKey, next, history);
animId = setTimeout(() => animate(landscapeKey, next, steps - 1), 60);
}
/* Return true if no point scores more than 2% higher than x */
function isGlobalOptimum(landscapeKey, x) {
const myF = fitness(landscapeKey, x);
for (let px = 0; px <= 200; px++) {
if (fitness(landscapeKey, px / 200) > myF + 0.02) return false;
}
return true;
}
function startClimber(x) {
if (animId) clearTimeout(animId);
climberX = x;
history = [x];
statusEl.className = 'status';
statusEl.textContent = 'Climbing…';
drawLandscape(mode, x, history);
animate(mode, x, 300);
}
cv.addEventListener('click', (e) => {
const rect = cv.getBoundingClientRect();
const scaleX = W / rect.width;
const px = (e.clientX - rect.left) * scaleX;
const x = (px - PAD_L) / PLOT_W;
if (x < 0 || x > 1) return;
hintEl.textContent = 'Running hill-climber…';
startClimber(x);
});
document.getElementById('btn-smooth').addEventListener('click', () => {
mode = 'smooth';
document.getElementById('btn-smooth').classList.add('active');
document.getElementById('btn-rugged').classList.remove('active');
reset();
});
document.getElementById('btn-rugged').addEventListener('click', () => {
mode = 'rugged';
document.getElementById('btn-rugged').classList.add('active');
document.getElementById('btn-smooth').classList.remove('active');
reset();
});
document.getElementById('btn-reset').addEventListener('click', reset);
function reset() {
if (animId) clearTimeout(animId);
climberX = null;
history = [];
statusEl.textContent = '';
statusEl.className = 'status';
hintEl.textContent = 'Click anywhere on the curve to drop the hill-climber.';
drawLandscape(mode, null, []);
}
/* Initial draw on page load */
drawLandscape(mode, null, []);
Comments
Loading comments...