Pick how many disks you want, then let the optimal algorithm play it out move by move. The solver never makes a mistake — but watch the move counter.
<p class="hint">Pick the number of disks, then press <b>Solve</b>. The optimal algorithm plays it out one move at a time — it never makes a mistake. Watch the <b>move counter</b>: each extra disk <b>doubles</b> the total (always 2ⁿ - 1).</p>
<div class="controls">
<label>Disks: <b id="ndisks">3</b></label>
<input id="slider" type="range" min="1" max="8" value="3">
</div>
<div id="pegs" class="pegs"></div>
<div class="readout">
<span>Move <b id="cur">0</b> of <b id="tot">7</b></span>
<span class="formula">2<sup id="exp">3</sup> - 1 = <b id="opt">7</b></span>
</div>
<div class="btns">
<button id="solve" type="button">Solve</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .8rem; margin: .3rem 0 .6rem; font-size: .95rem; }
.controls input { flex: 1; }
.pegs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; height: 150px;
align-items: end; padding: 0 4px; }
.peg { position: relative; height: 100%; display: flex; flex-direction: column;
justify-content: flex-end; align-items: center; }
.peg::before { content: ""; position: absolute; bottom: 0; width: 100%; height: 6px;
background: #1d3557; border-radius: 3px; }
.peg::after { content: ""; position: absolute; bottom: 6px; width: 6px; height: 90%;
background: #cdd9e3; border-radius: 3px; }
.disk { height: 16px; border-radius: 8px; margin-top: 3px; z-index: 1;
background: #457b9d; box-shadow: inset 0 -2px 0 rgba(0,0,0,.15); }
.readout { display: flex; justify-content: space-between; align-items: center;
margin: .7rem 0 .5rem; font-size: 1rem; font-weight: 600; }
.formula { color: #1d3557; font: 700 1rem ui-monospace, monospace; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .5; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Towers of Hanoi. The whole algorithm is the classic three-line recursion:
// hanoi(n, from, to, via): hanoi(n-1, from, via, to); move(from,to); hanoi(n-1, via, to, from)
// It is provably optimal — the shortest solution has exactly 2^n - 1 moves.
// The "thinking" is trivial; the difficulty lives only in the LENGTH of the answer.
let N = 3;
let pegs = [[], [], []]; // pegs[i] = list of disk sizes, bottom..top
let moves = []; // queued [from,to] moves
let step = 0, timer = null;
function buildMoves(n, from, to, via, out) {
if (n === 0) return;
buildMoves(n - 1, from, via, to, out);
out.push([from, to]);
buildMoves(n - 1, via, to, from, out);
}
function resetState() {
if (timer) { clearTimeout(timer); timer = null; }
pegs = [[], [], []];
for (let s = N; s >= 1; s--) pegs[0].push(s); // largest at bottom
moves = []; buildMoves(N, 0, 2, 1, moves);
step = 0;
document.getElementById('cur').textContent = '0';
document.getElementById('tot').textContent = moves.length;
document.getElementById('exp').textContent = N;
document.getElementById('opt').textContent = moves.length;
document.getElementById('solve').disabled = false;
render();
}
function render() {
const host = document.getElementById('pegs'); host.innerHTML = '';
const maxW = 92, minW = 30;
for (let p = 0; p < 3; p++) {
const col = document.createElement('div'); col.className = 'peg';
// render top-first so flex-end stacks bottom disk lowest
for (let i = pegs[p].length - 1; i >= 0; i--) {
const size = pegs[p][i];
const d = document.createElement('div'); d.className = 'disk';
const w = N === 1 ? maxW : minW + (size - 1) * (maxW - minW) / (N - 1);
d.style.width = w + '%';
col.appendChild(d);
}
host.appendChild(col);
}
}
function play() {
if (step >= moves.length) { document.getElementById('solve').disabled = false; return; }
const [f, t] = moves[step];
pegs[t].push(pegs[f].pop());
step++;
document.getElementById('cur').textContent = step;
render();
timer = setTimeout(play, Math.max(120, 700 / Math.sqrt(moves.length)));
}
document.getElementById('slider').addEventListener('input', e => {
N = +e.target.value;
document.getElementById('ndisks').textContent = N;
resetState();
});
document.getElementById('solve').addEventListener('click', () => {
if (step >= moves.length) resetState();
document.getElementById('solve').disabled = true;
console.log(fmt('Solving %N% disks: optimal = 2^%N% - 1 = %length% moves', { N: N, length: moves.length }));
play();
});
document.getElementById('reset').addEventListener('click', resetState);
resetState();
<p class="hint">Pick the number of disks, then press <b>Solve</b>. The optimal algorithm plays it out one move at a time — it never makes a mistake. Watch the <b>move counter</b>: each extra disk <b>doubles</b> the total (always 2ⁿ - 1).</p>
<div class="controls">
<label>Disks: <b id="ndisks">3</b></label>
<input id="slider" type="range" min="1" max="8" value="3">
</div>
<div id="pegs" class="pegs"></div>
<div class="readout">
<span>Move <b id="cur">0</b> of <b id="tot">7</b></span>
<span class="formula">2<sup id="exp">3</sup> - 1 = <b id="opt">7</b></span>
</div>
<div class="btns">
<button id="solve" type="button">Solve</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .8rem; margin: .3rem 0 .6rem; font-size: .95rem; }
.controls input { flex: 1; }
.pegs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; height: 150px;
align-items: end; padding: 0 4px; }
.peg { position: relative; height: 100%; display: flex; flex-direction: column;
justify-content: flex-end; align-items: center; }
.peg::before { content: ""; position: absolute; bottom: 0; width: 100%; height: 6px;
background: #1d3557; border-radius: 3px; }
.peg::after { content: ""; position: absolute; bottom: 6px; width: 6px; height: 90%;
background: #cdd9e3; border-radius: 3px; }
.disk { height: 16px; border-radius: 8px; margin-top: 3px; z-index: 1;
background: #457b9d; box-shadow: inset 0 -2px 0 rgba(0,0,0,.15); }
.readout { display: flex; justify-content: space-between; align-items: center;
margin: .7rem 0 .5rem; font-size: 1rem; font-weight: 600; }
.formula { color: #1d3557; font: 700 1rem ui-monospace, monospace; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .5; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Towers of Hanoi. The whole algorithm is the classic three-line recursion:
// hanoi(n, from, to, via): hanoi(n-1, from, via, to); move(from,to); hanoi(n-1, via, to, from)
// It is provably optimal — the shortest solution has exactly 2^n - 1 moves.
// The "thinking" is trivial; the difficulty lives only in the LENGTH of the answer.
let N = 3;
let pegs = [[], [], []]; // pegs[i] = list of disk sizes, bottom..top
let moves = []; // queued [from,to] moves
let step = 0, timer = null;
function buildMoves(n, from, to, via, out) {
if (n === 0) return;
buildMoves(n - 1, from, via, to, out);
out.push([from, to]);
buildMoves(n - 1, via, to, from, out);
}
function resetState() {
if (timer) { clearTimeout(timer); timer = null; }
pegs = [[], [], []];
for (let s = N; s >= 1; s--) pegs[0].push(s); // largest at bottom
moves = []; buildMoves(N, 0, 2, 1, moves);
step = 0;
document.getElementById('cur').textContent = '0';
document.getElementById('tot').textContent = moves.length;
document.getElementById('exp').textContent = N;
document.getElementById('opt').textContent = moves.length;
document.getElementById('solve').disabled = false;
render();
}
function render() {
const host = document.getElementById('pegs'); host.innerHTML = '';
const maxW = 92, minW = 30;
for (let p = 0; p < 3; p++) {
const col = document.createElement('div'); col.className = 'peg';
// render top-first so flex-end stacks bottom disk lowest
for (let i = pegs[p].length - 1; i >= 0; i--) {
const size = pegs[p][i];
const d = document.createElement('div'); d.className = 'disk';
const w = N === 1 ? maxW : minW + (size - 1) * (maxW - minW) / (N - 1);
d.style.width = w + '%';
col.appendChild(d);
}
host.appendChild(col);
}
}
function play() {
if (step >= moves.length) { document.getElementById('solve').disabled = false; return; }
const [f, t] = moves[step];
pegs[t].push(pegs[f].pop());
step++;
document.getElementById('cur').textContent = step;
render();
timer = setTimeout(play, Math.max(120, 700 / Math.sqrt(moves.length)));
}
document.getElementById('slider').addEventListener('input', e => {
N = +e.target.value;
document.getElementById('ndisks').textContent = N;
resetState();
});
document.getElementById('solve').addEventListener('click', () => {
if (step >= moves.length) resetState();
document.getElementById('solve').disabled = true;
console.log(fmt('Solving %N% disks: optimal = 2^%N% - 1 = %length% moves', { N: N, length: moves.length }));
play();
});
document.getElementById('reset').addEventListener('click', resetState);
resetState();
Add a disk and the number of moves doesn't grow a little — it doubles. One disk needs 1 move, two need 3, three need 7, then 15, 31, 63... always exactly 2n - 1. The algorithm that produces this is trivial and provably optimal; there is simply no shorter sequence. The difficulty was never in the reasoning — it lives entirely in the length of the answer.
Comments
Loading comments...