<!-- Knight’s Tour demo — interactive Warnsdorff heuristic on an 8x8 board -->
<div class="controls">
<button id="btn-run" type="button">Run tour</button>
<button id="btn-step" type="button">Step</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="move-counter" id="move-counter"></span>
</div>
<div class="hint" id="hint">Click any square to set the start, then press Run tour or Step.</div>
<div id="board" class="board"></div>
<div class="status" id="status"></div>
/* Board styles — light/dark squares, visited (blue), knight (red) */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
.move-counter { font: 600 13px ui-monospace, monospace; color: #555; margin-left: .3rem; }
.hint { font-size: .85rem; color: #555; margin-bottom: .4rem; min-height: 1.2em; }
.board { display: grid; grid-template-columns: repeat(8, 1fr); gap: 2px;
max-width: 368px; aspect-ratio: 1; user-select: none; }
.cell { display: flex; align-items: center; justify-content: center;
font: 700 11px ui-monospace, monospace; border-radius: 3px;
cursor: pointer; transition: background .15s; }
.cell.light { background: #f0d9b5; }
.cell.dark { background: #b58863; }
.cell.visited { background: #7eadd4; color: #fff; }
.cell.visited.dark { background: #5e90b8; }
.cell.knight { background: #e63946; color: #fff; border-radius: 50%; }
.cell.knight.dark { background: #c92f3c; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-top: .4rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
// Knight’s Tour via Warnsdorff’s rule — always move to the unvisited square with the fewest onward moves
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const MOVES = [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]];
const N = 8;
let visited = []; // boolean array — true if the square has been visited
let path = []; // ordered list of [row, col] visited squares
let running = false;
let timer = null;
let startR = 0, startC = 0;
// Warnsdorff degree = number of unvisited neighbors from (r,c) excluding already-visited squares
function degree(r, c, vis) {
let d = 0;
for (const [dr, dc] of MOVES) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < N && nc >= 0 && nc < N && !vis[nr * N + nc]) d++;
}
return d;
}
// Tie-break by distance from board center — prefer squares farther from center (edges and corners first)
function distCenter(r, c) {
return (r - 3.5) * (r - 3.5) + (c - 3.5) * (c - 3.5);
}
// Pick the unvisited neighbor with the minimum Warnsdorff degree; break ties by preferring peripheral squares
function nextMove(r, c, vis) {
let best = null, bestDeg = 9, bestDist = -1;
for (const [dr, dc] of MOVES) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < N && nc >= 0 && nc < N && !vis[nr * N + nc]) {
const d = degree(nr, nc, vis);
const dist = distCenter(nr, nc);
if (d < bestDeg || (d === bestDeg && dist > bestDist)) {
bestDeg = d; bestDist = dist; best = [nr, nc];
}
}
}
return best;
}
function cellClass(r, c) {
return (r + c) % 2 === 0 ? 'light' : 'dark';
}
function render() {
const board = document.getElementById('board');
board.innerHTML = '';
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
const cell = document.createElement('div');
const idx = r * N + c;
const isKnight = path.length > 0 && path[path.length - 1][0] === r && path[path.length - 1][1] === c;
let cls = 'cell ' + cellClass(r, c);
if (isKnight) cls += ' knight';
else if (visited[idx]) cls += ' visited';
cell.className = cls;
if (visited[idx] && !isKnight) cell.textContent = path.findIndex(([pr, pc]) => pr === r && pc === c) + 1;
if (!running && path.length === 0) {
cell.title = 'Click to start here';
cell.addEventListener('click', () => setStart(r, c));
}
board.appendChild(cell);
}
}
const mc = document.getElementById('move-counter');
mc.textContent = path.length > 0 ? i18nFmt('Move %n% / 64', { n: path.length }) : '';
}
function setStart(r, c) {
startR = r; startC = c;
reset();
visited[r * N + c] = true;
path = [[r, c]];
render();
document.getElementById('hint').textContent = 'Start square set. Press Run tour or Step.';
}
function reset() {
clearInterval(timer); timer = null; running = false;
visited = new Array(N * N).fill(false);
path = [];
setStatus('', '');
document.getElementById('btn-run').disabled = false;
document.getElementById('btn-step').disabled = false;
document.getElementById('hint').textContent = 'Click any square to set the start, then press Run tour or Step.';
render();
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + cls;
}
function step() {
if (path.length === 0) {
// If no move has been made yet, place the knight on the start square first
visited[startR * N + startC] = true;
path = [[startR, startC]];
}
const [r, c] = path[path.length - 1];
const nxt = nextMove(r, c, visited);
if (nxt) {
const [nr, nc] = nxt;
visited[nr * N + nc] = true;
path.push([nr, nc]);
render();
if (path.length === N * N) {
setStatus('Tour complete! The knight visited all 64 squares.', 'ok');
stopRun();
}
} else {
if (path.length < N * N) {
setStatus(i18nFmt('Stuck at move %n% — no unvisited neighbor available.', { n: path.length }), 'bad');
}
stopRun();
}
}
function stopRun() {
clearInterval(timer); timer = null; running = false;
document.getElementById('btn-run').disabled = true;
document.getElementById('btn-step').disabled = true;
}
document.getElementById('btn-run').addEventListener('click', () => {
if (running) return;
running = true;
document.getElementById('hint').textContent = 'Running Warnsdorff’s rule…';
timer = setInterval(() => step(), 60);
});
document.getElementById('btn-step').addEventListener('click', () => {
if (running) return;
if (path.length === 0) {
document.getElementById('hint').textContent = 'Running Warnsdorff’s rule…';
}
step();
});
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
<!-- Knight’s Tour demo — interactive Warnsdorff heuristic on an 8x8 board -->
<div class="controls">
<button id="btn-run" type="button">Run tour</button>
<button id="btn-step" type="button">Step</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="move-counter" id="move-counter"></span>
</div>
<div class="hint" id="hint">Click any square to set the start, then press Run tour or Step.</div>
<div id="board" class="board"></div>
<div class="status" id="status"></div>
/* Board styles — light/dark squares, visited (blue), knight (red) */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
.move-counter { font: 600 13px ui-monospace, monospace; color: #555; margin-left: .3rem; }
.hint { font-size: .85rem; color: #555; margin-bottom: .4rem; min-height: 1.2em; }
.board { display: grid; grid-template-columns: repeat(8, 1fr); gap: 2px;
max-width: 368px; aspect-ratio: 1; user-select: none; }
.cell { display: flex; align-items: center; justify-content: center;
font: 700 11px ui-monospace, monospace; border-radius: 3px;
cursor: pointer; transition: background .15s; }
.cell.light { background: #f0d9b5; }
.cell.dark { background: #b58863; }
.cell.visited { background: #7eadd4; color: #fff; }
.cell.visited.dark { background: #5e90b8; }
.cell.knight { background: #e63946; color: #fff; border-radius: 50%; }
.cell.knight.dark { background: #c92f3c; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-top: .4rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
// Knight’s Tour via Warnsdorff’s rule — always move to the unvisited square with the fewest onward moves
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const MOVES = [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]];
const N = 8;
let visited = []; // boolean array — true if the square has been visited
let path = []; // ordered list of [row, col] visited squares
let running = false;
let timer = null;
let startR = 0, startC = 0;
// Warnsdorff degree = number of unvisited neighbors from (r,c) excluding already-visited squares
function degree(r, c, vis) {
let d = 0;
for (const [dr, dc] of MOVES) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < N && nc >= 0 && nc < N && !vis[nr * N + nc]) d++;
}
return d;
}
// Tie-break by distance from board center — prefer squares farther from center (edges and corners first)
function distCenter(r, c) {
return (r - 3.5) * (r - 3.5) + (c - 3.5) * (c - 3.5);
}
// Pick the unvisited neighbor with the minimum Warnsdorff degree; break ties by preferring peripheral squares
function nextMove(r, c, vis) {
let best = null, bestDeg = 9, bestDist = -1;
for (const [dr, dc] of MOVES) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < N && nc >= 0 && nc < N && !vis[nr * N + nc]) {
const d = degree(nr, nc, vis);
const dist = distCenter(nr, nc);
if (d < bestDeg || (d === bestDeg && dist > bestDist)) {
bestDeg = d; bestDist = dist; best = [nr, nc];
}
}
}
return best;
}
function cellClass(r, c) {
return (r + c) % 2 === 0 ? 'light' : 'dark';
}
function render() {
const board = document.getElementById('board');
board.innerHTML = '';
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
const cell = document.createElement('div');
const idx = r * N + c;
const isKnight = path.length > 0 && path[path.length - 1][0] === r && path[path.length - 1][1] === c;
let cls = 'cell ' + cellClass(r, c);
if (isKnight) cls += ' knight';
else if (visited[idx]) cls += ' visited';
cell.className = cls;
if (visited[idx] && !isKnight) cell.textContent = path.findIndex(([pr, pc]) => pr === r && pc === c) + 1;
if (!running && path.length === 0) {
cell.title = 'Click to start here';
cell.addEventListener('click', () => setStart(r, c));
}
board.appendChild(cell);
}
}
const mc = document.getElementById('move-counter');
mc.textContent = path.length > 0 ? i18nFmt('Move %n% / 64', { n: path.length }) : '';
}
function setStart(r, c) {
startR = r; startC = c;
reset();
visited[r * N + c] = true;
path = [[r, c]];
render();
document.getElementById('hint').textContent = 'Start square set. Press Run tour or Step.';
}
function reset() {
clearInterval(timer); timer = null; running = false;
visited = new Array(N * N).fill(false);
path = [];
setStatus('', '');
document.getElementById('btn-run').disabled = false;
document.getElementById('btn-step').disabled = false;
document.getElementById('hint').textContent = 'Click any square to set the start, then press Run tour or Step.';
render();
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + cls;
}
function step() {
if (path.length === 0) {
// If no move has been made yet, place the knight on the start square first
visited[startR * N + startC] = true;
path = [[startR, startC]];
}
const [r, c] = path[path.length - 1];
const nxt = nextMove(r, c, visited);
if (nxt) {
const [nr, nc] = nxt;
visited[nr * N + nc] = true;
path.push([nr, nc]);
render();
if (path.length === N * N) {
setStatus('Tour complete! The knight visited all 64 squares.', 'ok');
stopRun();
}
} else {
if (path.length < N * N) {
setStatus(i18nFmt('Stuck at move %n% — no unvisited neighbor available.', { n: path.length }), 'bad');
}
stopRun();
}
}
function stopRun() {
clearInterval(timer); timer = null; running = false;
document.getElementById('btn-run').disabled = true;
document.getElementById('btn-step').disabled = true;
}
document.getElementById('btn-run').addEventListener('click', () => {
if (running) return;
running = true;
document.getElementById('hint').textContent = 'Running Warnsdorff’s rule…';
timer = setInterval(() => step(), 60);
});
document.getElementById('btn-step').addEventListener('click', () => {
if (running) return;
if (path.length === 0) {
document.getElementById('hint').textContent = 'Running Warnsdorff’s rule…';
}
step();
});
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
Notice how the knight tends to hug the edges and corners early, leaving the more connected interior squares for later — exactly what the heuristic intends. In the rare case of a tie the result can vary, but Warnsdorff's rule succeeds on nearly every starting square of the standard board.
Comments
Loading comments...