Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions project/classic-games/20260705-0415-towers-of-hanoi.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mini Towers of Hanoi</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-zinc-950 text-zinc-100">
<main class="mx-auto flex min-h-screen max-w-5xl flex-col gap-4 px-4 py-5">
<header class="flex flex-wrap items-center justify-between gap-3">
<div>
<h1 class="text-2xl font-semibold tracking-normal">Mini Towers of Hanoi</h1>
<p class="text-sm text-zinc-400">点击柱子选择顶部圆盘,再点击目标柱移动;大盘不能压在小盘上。</p>
</div>
<button id="resetBtn" class="rounded bg-cyan-400 px-4 py-2 text-sm font-semibold text-zinc-950 hover:bg-cyan-300">重开</button>
</header>

<section class="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div class="rounded border border-zinc-700 bg-zinc-900 p-3">
<div class="text-xs uppercase text-zinc-500">步数</div>
<div id="moveCount" class="text-2xl font-semibold">0</div>
</div>
<div class="rounded border border-zinc-700 bg-zinc-900 p-3">
<div class="text-xs uppercase text-zinc-500">最优</div>
<div id="bestCount" class="text-2xl font-semibold">31</div>
</div>
<div class="rounded border border-zinc-700 bg-zinc-900 p-3">
<div class="text-xs uppercase text-zinc-500">圆盘</div>
<div class="text-2xl font-semibold">5</div>
</div>
<div class="rounded border border-zinc-700 bg-zinc-900 p-3">
<div class="text-xs uppercase text-zinc-500">选中</div>
<div id="selectedLabel" class="text-2xl font-semibold">无</div>
</div>
</section>

<section class="rounded border border-zinc-700 bg-zinc-900 p-4">
<div class="mb-4 flex items-center justify-between gap-3">
<h2 class="text-lg font-semibold">棋盘</h2>
<span id="message" class="text-sm text-cyan-300">从左侧柱子开始。</span>
</div>
<div id="board" class="grid min-h-80 grid-cols-3 gap-3"></div>
</section>
</main>

<script>
const boardEl = document.getElementById("board");
const moveCountEl = document.getElementById("moveCount");
const bestCountEl = document.getElementById("bestCount");
const selectedLabelEl = document.getElementById("selectedLabel");
const messageEl = document.getElementById("message");
const resetBtn = document.getElementById("resetBtn");

const diskCount = 5;
const pegNames = ["A", "B", "C"];
let pegs = [];
let selectedPeg = null;
let moves = 0;
let gameOver = false;

function resetGame() {
pegs = [[], [], []];
for (let size = diskCount; size >= 1; size -= 1) {
pegs[0].push(size);
}
selectedPeg = null;
moves = 0;
gameOver = false;
setMessage("从左侧柱子开始。");
render();
}

function setMessage(text) {
messageEl.textContent = text;
}

function topDisk(pegIndex) {
const peg = pegs[pegIndex];
return peg[peg.length - 1];
}

function selectPeg(pegIndex) {
if (gameOver) return;
if (selectedPeg === null) {
if (!topDisk(pegIndex)) {
setMessage("空柱不能选。");
return;
}
selectedPeg = pegIndex;
setMessage(`已选 ${pegNames[pegIndex]} 柱顶部圆盘。`);
render();
return;
}
if (selectedPeg === pegIndex) {
selectedPeg = null;
setMessage("已取消选择。");
render();
return;
}
moveDisk(selectedPeg, pegIndex);
}

function canMove(from, to) {
const disk = topDisk(from);
const target = topDisk(to);
return Boolean(disk) && (!target || disk < target);
}

function moveDisk(from, to) {
if (!canMove(from, to)) {
setMessage("这一步不合法。");
selectedPeg = null;
render();
return;
}
pegs[to].push(pegs[from].pop());
selectedPeg = null;
moves += 1;
if (checkWin()) {
gameOver = true;
setMessage(`完成。共 ${moves} 步。`);
} else {
setMessage("继续移动圆盘。");
}
render();
}

function checkWin() {
return pegs[2].length === diskCount && pegs[2].every((disk, index) => diskCount - index === disk);
}

function createDisk(size) {
const disk = document.createElement("div");
const width = 34 + size * 11;
disk.className = "mx-auto h-8 rounded border border-cyan-200 bg-cyan-400 text-center text-sm font-semibold leading-8 text-zinc-950 shadow";
disk.style.width = `${width}%`;
disk.textContent = size;
return disk;
}

function renderPeg(pegIndex) {
const button = document.createElement("button");
button.type = "button";
button.className = "relative flex min-h-72 flex-col-reverse items-center gap-1 rounded border border-zinc-700 bg-zinc-800 px-2 pb-4 pt-8 hover:border-cyan-300";
if (selectedPeg === pegIndex) button.classList.add("ring-2", "ring-cyan-300");
button.addEventListener("click", () => selectPeg(pegIndex));

const pole = document.createElement("div");
pole.className = "absolute bottom-4 top-10 w-2 rounded bg-zinc-600";
button.appendChild(pole);

pegs[pegIndex].forEach((disk) => {
button.appendChild(createDisk(disk));
});

const label = document.createElement("div");
label.className = "absolute left-2 top-2 text-xs font-semibold text-zinc-500";
label.textContent = pegNames[pegIndex];
button.appendChild(label);
return button;
}

function render() {
boardEl.innerHTML = "";
pegs.forEach((_, index) => {
boardEl.appendChild(renderPeg(index));
});
moveCountEl.textContent = moves;
bestCountEl.textContent = 2 ** diskCount - 1;
selectedLabelEl.textContent = selectedPeg === null ? "无" : pegNames[selectedPeg];
}

resetBtn.addEventListener("click", resetGame);
resetGame();
</script>
</body>
</html>