<div class="hh-wrap">
<div class="panel-left">
<div class="landscape-label">Fitness Landscape</div>
<canvas id="landscape" width="320" height="160"></canvas>
<div class="info-row">
<span>Step: <b id="step">0</b></span>
<span>Position: <b id="pos">โ</b></span>
<span>Score: <b id="score">โ</b></span>
</div>
<div class="heuristic-row">
Active heuristic: <span id="active-h" class="h-badge">โ</span>
</div>
<div class="btns">
<button id="btn-run">Run</button>
<button id="btn-step" class="ghost">Step</button>
<button id="btn-reset" class="ghost">Reset</button>
</div>
</div>
<div class="panel-right">
<div class="log-label">Heuristic Switch Log</div>
<div id="log" class="log"></div>
<div class="score-bars">
<div class="bar-row"><span class="h-label hc">Hill-climb</span><div class="bar-bg"><div id="bar-hc" class="bar hc" style="width:33%"></div></div><span id="sc-hc" class="bar-val">0</span></div>
<div class="bar-row"><span class="h-label rw">Random walk</span><div class="bar-bg"><div id="bar-rw" class="bar rw" style="width:33%"></div></div><span id="sc-rw" class="bar-val">0</span></div>
<div class="bar-row"><span class="h-label gr">Greedy restart</span><div class="bar-bg"><div id="bar-gr" class="bar gr" style="width:33%"></div></div><span id="sc-gr" class="bar-val">0</span></div>
</div>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; font-size: 13px; color: #222; margin: 0; background: #f5f7fa; }
.hh-wrap { display: flex; gap: 12px; padding: 10px; min-height: 300px; }
.panel-left { flex: 0 0 340px; display: flex; flex-direction: column; gap: 8px; }
.panel-right { flex: 1; display: flex; flex-direction: column; gap: 8px; }
.landscape-label, .log-label { font-weight: 700; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #556; }
canvas { border-radius: 8px; border: 1px solid #dde; background: #fff; display: block; }
.info-row { display: flex; gap: 12px; font-size: 12px; color: #444; }
.heuristic-row { font-size: 12px; }
.h-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-weight: 700; font-size: 11px; color: #fff; background: #888; }
.h-badge.hc { background: #2563eb; }
.h-badge.rw { background: #d97706; }
.h-badge.gr { background: #16a34a; }
.btns { display: flex; gap: 6px; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.log { flex: 1; overflow-y: auto; max-height: 160px; border: 1px solid #dde; border-radius: 8px; background: #fff; padding: 6px 8px; font-size: 11px; color: #333; line-height: 1.7; }
.log .entry { border-bottom: 1px solid #f0f0f0; padding: 1px 0; }
.score-bars { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
.bar-row { display: flex; align-items: center; gap: 6px; }
.h-label { font-size: 11px; font-weight: 600; width: 88px; flex-shrink: 0; }
.h-label.hc { color: #2563eb; }
.h-label.rw { color: #d97706; }
.h-label.gr { color: #16a34a; }
.bar-bg { flex: 1; background: #e9ecef; border-radius: 6px; height: 10px; overflow: hidden; }
.bar { height: 10px; border-radius: 6px; transition: width .35s; }
.bar.hc { background: #2563eb; }
.bar.rw { background: #d97706; }
.bar.gr { background: #16a34a; }
.bar-val { font-size: 11px; font-weight: 600; width: 28px; text-align: right; color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Fitness landscape โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 20 positions, fixed quality scores (0-100). The landscape has two peaks and
// some valleys to stress-test the hyperheuristic's selection mechanism.
const QUALITY = [30,45,60,72,85,78,62,40,25,15,20,35,55,70,90,82,65,45,28,12];
const N = QUALITY.length;
// โโ Low-level heuristics โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// hill_climb: move to the neighbor with the highest quality (left or right).
// random_walk: jump to a uniformly random position.
// greedy_restart: reset to the highest-quality position not recently visited.
function hillClimb(pos, visited) {
const left = pos > 0 ? QUALITY[pos-1] : -1;
const right = pos < N-1 ? QUALITY[pos+1] : -1;
if (left >= right && left > QUALITY[pos]) return pos - 1;
if (right > left && right > QUALITY[pos]) return pos + 1;
return pos; // stuck at local peak
}
function randomWalk(pos) {
let next; do { next = Math.floor(Math.random() * N); } while (next === pos);
return next;
}
function greedyRestart(pos, visited) {
// find best unvisited position, fallback to global best
let best = -1, bestQ = -1;
for (let i = 0; i < N; i++) {
if (!visited.has(i) && QUALITY[i] > bestQ) { bestQ = QUALITY[i]; best = i; }
}
return best >= 0 ? best : QUALITY.indexOf(Math.max(...QUALITY));
}
// โโ Hyperheuristic: simple credit-based selection โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Each heuristic accumulates credit = sum of quality improvements it produced
// in the last WINDOW steps. The selector always picks the highest-credit heuristic.
// Ties are broken randomly.
const HEURISTICS = ['hc','rw','gr'];
const H_LABEL = { hc:'Hill-climb', rw:'Random walk', gr:'Greedy restart' };
const WINDOW = 4;
let state = null;
function initState() {
return {
pos: Math.floor(N / 2),
step: 0,
running: false,
visited: new Set(),
history: [], // { heuristic, gainedQ }
credit: { hc: 0, rw: 0, gr: 0 },
uses: { hc: 0, rw: 0, gr: 0 },
currentH: 'hc',
lastH: null,
timer: null
};
}
function selectHeuristic(s) {
// recompute credits from last WINDOW entries
const wins = { hc: 0, rw: 0, gr: 0 };
const recent = s.history.slice(-WINDOW);
for (const e of recent) if (e.gainedQ > 0) wins[e.heuristic] += e.gainedQ;
// pick best; tie-break randomly
let best = -Infinity, chosen = [];
for (const h of HEURISTICS) {
if (wins[h] > best) { best = wins[h]; chosen = [h]; }
else if (wins[h] === best) chosen.push(h);
}
s.credit = wins;
return chosen[Math.floor(Math.random() * chosen.length)];
}
function doStep(s) {
s.step++;
s.visited.add(s.pos);
const prevQ = QUALITY[s.pos];
const h = s.currentH;
let next;
if (h === 'hc') next = hillClimb(s.pos, s.visited);
else if (h === 'rw') next = randomWalk(s.pos);
else next = greedyRestart(s.pos, s.visited);
const gained = QUALITY[next] - prevQ;
s.history.push({ heuristic: h, gainedQ: gained });
s.uses[h]++;
s.pos = next;
const newH = selectHeuristic(s);
if (newH !== s.currentH) {
log(fmt('Step %step%: switched %v% โ %v2% (pos %next%, ฮq=%v3%%gained%)', { step: s.step, v: H_LABEL[s.currentH], v2: H_LABEL[newH], next: next, v3: gained>=0?'+':'', gained: gained }));
}
s.currentH = newH;
render(s);
}
// โโ Canvas drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const C = document.getElementById('landscape');
const ctx = C.getContext('2d');
const W = C.width, H = C.height;
const PAD = { t: 10, b: 24, l: 16, r: 10 };
const COLORS = { hc: '#2563eb', rw: '#d97706', gr: '#16a34a' };
function xOf(i) { return PAD.l + (i / (N-1)) * (W - PAD.l - PAD.r); }
function yOf(q) { return PAD.t + (1 - q/100) * (H - PAD.t - PAD.b); }
function drawLandscape(s) {
ctx.clearRect(0, 0, W, H);
// fill under curve
ctx.beginPath();
ctx.moveTo(xOf(0), yOf(QUALITY[0]));
for (let i = 1; i < N; i++) ctx.lineTo(xOf(i), yOf(QUALITY[i]));
ctx.lineTo(xOf(N-1), H - PAD.b);
ctx.lineTo(xOf(0), H - PAD.b);
ctx.closePath();
ctx.fillStyle = 'rgba(99,131,180,.13)';
ctx.fill();
// curve
ctx.beginPath();
ctx.moveTo(xOf(0), yOf(QUALITY[0]));
for (let i = 1; i < N; i++) ctx.lineTo(xOf(i), yOf(QUALITY[i]));
ctx.strokeStyle = '#8fa8c8';
ctx.lineWidth = 1.5;
ctx.stroke();
// x-axis labels
ctx.fillStyle = '#999';
ctx.font = '9px system-ui';
ctx.textAlign = 'center';
for (let i = 0; i < N; i += 4) {
ctx.fillText(i, xOf(i), H - PAD.b + 12);
}
// visited trail
if (s) {
const trail = s.history.slice(-12);
trail.forEach((e, ti) => {
// estimate position from history (rough)
});
// draw current agent
const px = xOf(s.pos), py = yOf(QUALITY[s.pos]);
ctx.beginPath();
ctx.arc(px, py, 7, 0, Math.PI*2);
ctx.fillStyle = COLORS[s.currentH] || '#555';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
// quality label
ctx.fillStyle = COLORS[s.currentH] || '#555';
ctx.font = 'bold 10px system-ui';
ctx.textAlign = 'center';
ctx.fillText('q=' + QUALITY[s.pos], px, py - 11);
}
}
function updateBars(s) {
const total = s.uses.hc + s.uses.rw + s.uses.gr || 1;
['hc','rw','gr'].forEach(h => {
const pct = Math.round(s.uses[h] / total * 100);
document.getElementById('bar-'+h).style.width = pct + '%';
document.getElementById('sc-'+h).textContent = s.uses[h];
});
}
function render(s) {
drawLandscape(s);
document.getElementById('step').textContent = s.step;
document.getElementById('pos').textContent = s.pos;
document.getElementById('score').textContent = QUALITY[s.pos];
const badge = document.getElementById('active-h');
badge.textContent = H_LABEL[s.currentH];
badge.className = 'h-badge ' + s.currentH;
updateBars(s);
}
function log(msg) {
const el = document.getElementById('log');
const d = document.createElement('div');
d.className = 'entry';
d.textContent = msg;
el.appendChild(d);
el.scrollTop = el.scrollHeight;
}
// โโ Controls โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
state = initState();
drawLandscape(state);
document.getElementById('btn-run').addEventListener('click', function() {
if (!state.running) {
state.running = true;
this.textContent = 'Pause';
state.timer = setInterval(() => {
if (state.step >= 60) { clearInterval(state.timer); state.running = false; document.getElementById('btn-run').textContent = 'Run'; return; }
doStep(state);
}, 280);
} else {
state.running = false;
this.textContent = 'Run';
clearInterval(state.timer);
}
});
document.getElementById('btn-step').addEventListener('click', function() {
if (state.step >= 60) return;
doStep(state);
});
document.getElementById('btn-reset').addEventListener('click', function() {
clearInterval(state && state.timer);
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('log').innerHTML = '';
state = initState();
drawLandscape(state);
document.getElementById('step').textContent = '0';
document.getElementById('pos').textContent = 'โ';
document.getElementById('score').textContent = 'โ';
document.getElementById('active-h').textContent = 'โ';
document.getElementById('active-h').className = 'h-badge';
['hc','rw','gr'].forEach(h => {
document.getElementById('bar-'+h).style.width = '33%';
document.getElementById('sc-'+h).textContent = '0';
});
});
// verify core logic (console assertions)
(function verify() {
// Hill-climb always moves to a strictly better neighbor or stays
for (let pos = 0; pos < N; pos++) {
const next = hillClimb(pos, new Set());
console.assert(QUALITY[next] >= QUALITY[pos],
'hillClimb: moved to worse position from', pos);
}
// Greedy restart returns a valid index
const gr = greedyRestart(5, new Set([14]));
console.assert(gr >= 0 && gr < N, 'greedyRestart: out-of-bounds index');
console.assert(gr !== 14, 'greedyRestart: picked visited peak');
// Credit selection: the heuristic with the highest recent gain wins
const fakeState = initState();
fakeState.history = [
{ heuristic: 'hc', gainedQ: 10 },
{ heuristic: 'rw', gainedQ: 2 },
{ heuristic: 'gr', gainedQ: 1 }
];
const sel = selectHeuristic(fakeState);
console.assert(sel === 'hc', 'selectHeuristic: expected hc to win with highest credit');
console.log('hyperheuristics-demo: all assertions passed');
})();
<div class="hh-wrap">
<div class="panel-left">
<div class="landscape-label">Fitness Landscape</div>
<canvas id="landscape" width="320" height="160"></canvas>
<div class="info-row">
<span>Step: <b id="step">0</b></span>
<span>Position: <b id="pos">โ</b></span>
<span>Score: <b id="score">โ</b></span>
</div>
<div class="heuristic-row">
Active heuristic: <span id="active-h" class="h-badge">โ</span>
</div>
<div class="btns">
<button id="btn-run">Run</button>
<button id="btn-step" class="ghost">Step</button>
<button id="btn-reset" class="ghost">Reset</button>
</div>
</div>
<div class="panel-right">
<div class="log-label">Heuristic Switch Log</div>
<div id="log" class="log"></div>
<div class="score-bars">
<div class="bar-row"><span class="h-label hc">Hill-climb</span><div class="bar-bg"><div id="bar-hc" class="bar hc" style="width:33%"></div></div><span id="sc-hc" class="bar-val">0</span></div>
<div class="bar-row"><span class="h-label rw">Random walk</span><div class="bar-bg"><div id="bar-rw" class="bar rw" style="width:33%"></div></div><span id="sc-rw" class="bar-val">0</span></div>
<div class="bar-row"><span class="h-label gr">Greedy restart</span><div class="bar-bg"><div id="bar-gr" class="bar gr" style="width:33%"></div></div><span id="sc-gr" class="bar-val">0</span></div>
</div>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; font-size: 13px; color: #222; margin: 0; background: #f5f7fa; }
.hh-wrap { display: flex; gap: 12px; padding: 10px; min-height: 300px; }
.panel-left { flex: 0 0 340px; display: flex; flex-direction: column; gap: 8px; }
.panel-right { flex: 1; display: flex; flex-direction: column; gap: 8px; }
.landscape-label, .log-label { font-weight: 700; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #556; }
canvas { border-radius: 8px; border: 1px solid #dde; background: #fff; display: block; }
.info-row { display: flex; gap: 12px; font-size: 12px; color: #444; }
.heuristic-row { font-size: 12px; }
.h-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-weight: 700; font-size: 11px; color: #fff; background: #888; }
.h-badge.hc { background: #2563eb; }
.h-badge.rw { background: #d97706; }
.h-badge.gr { background: #16a34a; }
.btns { display: flex; gap: 6px; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.log { flex: 1; overflow-y: auto; max-height: 160px; border: 1px solid #dde; border-radius: 8px; background: #fff; padding: 6px 8px; font-size: 11px; color: #333; line-height: 1.7; }
.log .entry { border-bottom: 1px solid #f0f0f0; padding: 1px 0; }
.score-bars { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
.bar-row { display: flex; align-items: center; gap: 6px; }
.h-label { font-size: 11px; font-weight: 600; width: 88px; flex-shrink: 0; }
.h-label.hc { color: #2563eb; }
.h-label.rw { color: #d97706; }
.h-label.gr { color: #16a34a; }
.bar-bg { flex: 1; background: #e9ecef; border-radius: 6px; height: 10px; overflow: hidden; }
.bar { height: 10px; border-radius: 6px; transition: width .35s; }
.bar.hc { background: #2563eb; }
.bar.rw { background: #d97706; }
.bar.gr { background: #16a34a; }
.bar-val { font-size: 11px; font-weight: 600; width: 28px; text-align: right; color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Fitness landscape โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 20 positions, fixed quality scores (0-100). The landscape has two peaks and
// some valleys to stress-test the hyperheuristic's selection mechanism.
const QUALITY = [30,45,60,72,85,78,62,40,25,15,20,35,55,70,90,82,65,45,28,12];
const N = QUALITY.length;
// โโ Low-level heuristics โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// hill_climb: move to the neighbor with the highest quality (left or right).
// random_walk: jump to a uniformly random position.
// greedy_restart: reset to the highest-quality position not recently visited.
function hillClimb(pos, visited) {
const left = pos > 0 ? QUALITY[pos-1] : -1;
const right = pos < N-1 ? QUALITY[pos+1] : -1;
if (left >= right && left > QUALITY[pos]) return pos - 1;
if (right > left && right > QUALITY[pos]) return pos + 1;
return pos; // stuck at local peak
}
function randomWalk(pos) {
let next; do { next = Math.floor(Math.random() * N); } while (next === pos);
return next;
}
function greedyRestart(pos, visited) {
// find best unvisited position, fallback to global best
let best = -1, bestQ = -1;
for (let i = 0; i < N; i++) {
if (!visited.has(i) && QUALITY[i] > bestQ) { bestQ = QUALITY[i]; best = i; }
}
return best >= 0 ? best : QUALITY.indexOf(Math.max(...QUALITY));
}
// โโ Hyperheuristic: simple credit-based selection โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Each heuristic accumulates credit = sum of quality improvements it produced
// in the last WINDOW steps. The selector always picks the highest-credit heuristic.
// Ties are broken randomly.
const HEURISTICS = ['hc','rw','gr'];
const H_LABEL = { hc:'Hill-climb', rw:'Random walk', gr:'Greedy restart' };
const WINDOW = 4;
let state = null;
function initState() {
return {
pos: Math.floor(N / 2),
step: 0,
running: false,
visited: new Set(),
history: [], // { heuristic, gainedQ }
credit: { hc: 0, rw: 0, gr: 0 },
uses: { hc: 0, rw: 0, gr: 0 },
currentH: 'hc',
lastH: null,
timer: null
};
}
function selectHeuristic(s) {
// recompute credits from last WINDOW entries
const wins = { hc: 0, rw: 0, gr: 0 };
const recent = s.history.slice(-WINDOW);
for (const e of recent) if (e.gainedQ > 0) wins[e.heuristic] += e.gainedQ;
// pick best; tie-break randomly
let best = -Infinity, chosen = [];
for (const h of HEURISTICS) {
if (wins[h] > best) { best = wins[h]; chosen = [h]; }
else if (wins[h] === best) chosen.push(h);
}
s.credit = wins;
return chosen[Math.floor(Math.random() * chosen.length)];
}
function doStep(s) {
s.step++;
s.visited.add(s.pos);
const prevQ = QUALITY[s.pos];
const h = s.currentH;
let next;
if (h === 'hc') next = hillClimb(s.pos, s.visited);
else if (h === 'rw') next = randomWalk(s.pos);
else next = greedyRestart(s.pos, s.visited);
const gained = QUALITY[next] - prevQ;
s.history.push({ heuristic: h, gainedQ: gained });
s.uses[h]++;
s.pos = next;
const newH = selectHeuristic(s);
if (newH !== s.currentH) {
log(fmt('Step %step%: switched %v% โ %v2% (pos %next%, ฮq=%v3%%gained%)', { step: s.step, v: H_LABEL[s.currentH], v2: H_LABEL[newH], next: next, v3: gained>=0?'+':'', gained: gained }));
}
s.currentH = newH;
render(s);
}
// โโ Canvas drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const C = document.getElementById('landscape');
const ctx = C.getContext('2d');
const W = C.width, H = C.height;
const PAD = { t: 10, b: 24, l: 16, r: 10 };
const COLORS = { hc: '#2563eb', rw: '#d97706', gr: '#16a34a' };
function xOf(i) { return PAD.l + (i / (N-1)) * (W - PAD.l - PAD.r); }
function yOf(q) { return PAD.t + (1 - q/100) * (H - PAD.t - PAD.b); }
function drawLandscape(s) {
ctx.clearRect(0, 0, W, H);
// fill under curve
ctx.beginPath();
ctx.moveTo(xOf(0), yOf(QUALITY[0]));
for (let i = 1; i < N; i++) ctx.lineTo(xOf(i), yOf(QUALITY[i]));
ctx.lineTo(xOf(N-1), H - PAD.b);
ctx.lineTo(xOf(0), H - PAD.b);
ctx.closePath();
ctx.fillStyle = 'rgba(99,131,180,.13)';
ctx.fill();
// curve
ctx.beginPath();
ctx.moveTo(xOf(0), yOf(QUALITY[0]));
for (let i = 1; i < N; i++) ctx.lineTo(xOf(i), yOf(QUALITY[i]));
ctx.strokeStyle = '#8fa8c8';
ctx.lineWidth = 1.5;
ctx.stroke();
// x-axis labels
ctx.fillStyle = '#999';
ctx.font = '9px system-ui';
ctx.textAlign = 'center';
for (let i = 0; i < N; i += 4) {
ctx.fillText(i, xOf(i), H - PAD.b + 12);
}
// visited trail
if (s) {
const trail = s.history.slice(-12);
trail.forEach((e, ti) => {
// estimate position from history (rough)
});
// draw current agent
const px = xOf(s.pos), py = yOf(QUALITY[s.pos]);
ctx.beginPath();
ctx.arc(px, py, 7, 0, Math.PI*2);
ctx.fillStyle = COLORS[s.currentH] || '#555';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
// quality label
ctx.fillStyle = COLORS[s.currentH] || '#555';
ctx.font = 'bold 10px system-ui';
ctx.textAlign = 'center';
ctx.fillText('q=' + QUALITY[s.pos], px, py - 11);
}
}
function updateBars(s) {
const total = s.uses.hc + s.uses.rw + s.uses.gr || 1;
['hc','rw','gr'].forEach(h => {
const pct = Math.round(s.uses[h] / total * 100);
document.getElementById('bar-'+h).style.width = pct + '%';
document.getElementById('sc-'+h).textContent = s.uses[h];
});
}
function render(s) {
drawLandscape(s);
document.getElementById('step').textContent = s.step;
document.getElementById('pos').textContent = s.pos;
document.getElementById('score').textContent = QUALITY[s.pos];
const badge = document.getElementById('active-h');
badge.textContent = H_LABEL[s.currentH];
badge.className = 'h-badge ' + s.currentH;
updateBars(s);
}
function log(msg) {
const el = document.getElementById('log');
const d = document.createElement('div');
d.className = 'entry';
d.textContent = msg;
el.appendChild(d);
el.scrollTop = el.scrollHeight;
}
// โโ Controls โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
state = initState();
drawLandscape(state);
document.getElementById('btn-run').addEventListener('click', function() {
if (!state.running) {
state.running = true;
this.textContent = 'Pause';
state.timer = setInterval(() => {
if (state.step >= 60) { clearInterval(state.timer); state.running = false; document.getElementById('btn-run').textContent = 'Run'; return; }
doStep(state);
}, 280);
} else {
state.running = false;
this.textContent = 'Run';
clearInterval(state.timer);
}
});
document.getElementById('btn-step').addEventListener('click', function() {
if (state.step >= 60) return;
doStep(state);
});
document.getElementById('btn-reset').addEventListener('click', function() {
clearInterval(state && state.timer);
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('log').innerHTML = '';
state = initState();
drawLandscape(state);
document.getElementById('step').textContent = '0';
document.getElementById('pos').textContent = 'โ';
document.getElementById('score').textContent = 'โ';
document.getElementById('active-h').textContent = 'โ';
document.getElementById('active-h').className = 'h-badge';
['hc','rw','gr'].forEach(h => {
document.getElementById('bar-'+h).style.width = '33%';
document.getElementById('sc-'+h).textContent = '0';
});
});
// verify core logic (console assertions)
(function verify() {
// Hill-climb always moves to a strictly better neighbor or stays
for (let pos = 0; pos < N; pos++) {
const next = hillClimb(pos, new Set());
console.assert(QUALITY[next] >= QUALITY[pos],
'hillClimb: moved to worse position from', pos);
}
// Greedy restart returns a valid index
const gr = greedyRestart(5, new Set([14]));
console.assert(gr >= 0 && gr < N, 'greedyRestart: out-of-bounds index');
console.assert(gr !== 14, 'greedyRestart: picked visited peak');
// Credit selection: the heuristic with the highest recent gain wins
const fakeState = initState();
fakeState.history = [
{ heuristic: 'hc', gainedQ: 10 },
{ heuristic: 'rw', gainedQ: 2 },
{ heuristic: 'gr', gainedQ: 1 }
];
const sel = selectHeuristic(fakeState);
console.assert(sel === 'hc', 'selectHeuristic: expected hc to win with highest credit');
console.log('hyperheuristics-demo: all assertions passed');
})();
Comments
Loading comments...