<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>汉诺塔 - 自由层数版</title>
<style>
:root {
--primary: #2196F3;
--bg: #f5f7fa;
--card: #ffffff;
--pole: #795548;
--base: #5d4037;
--text: #333;
}
* { box-sizing: border-box; touch-action: manipulation; -webkit-tap-highlight-color: transparent; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
margin: 0;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
h1 { margin: 10px 0; font-size: 1.4rem; text-align: center; }
.panel {
background: var(--card);
padding: 15px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
width: 100%;
max-width: 600px;
margin-bottom: 15px;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
margin-bottom: 15px;
}
.field { display: flex; flex-direction: column; gap: 4px; }
.field label { font-size: 0.8rem; color: #666; font-weight: 600; }
input, select {
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 16px;
width: 100%;
background: #fff;
}
.btn-row { display: flex; gap: 10px; }
button {
flex: 1;
padding: 12px;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
color: white;
cursor: pointer;
transition: filter 0.2s;
}
button:active { filter: brightness(0.9); }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-start { background: var(--primary); }
.btn-step { background: #4CAF50; }
.btn-reset { background: #f44336; }
.game-box {
width: 100%;
max-width: 600px;
background: var(--card);
border-radius: 12px;
padding: 10px;
margin-bottom: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.towers {
display: flex;
justify-content: space-around;
align-items: flex-end;
height: 300px;
border-bottom: 6px solid var(--base);
position: relative;
overflow: hidden;
}
.tower {
position: relative;
width: 32%;
height: 100%;
display: flex;
flex-direction: column-reverse;
align-items: center;
}
.pole {
position: absolute;
bottom: 0;
width: 6px;
height: 90%;
background: var(--pole);
border-radius: 3px 3px 0 0;
z-index: 0;
}
.disk {
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.1);
z-index: 1;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.label {
position: absolute;
bottom: -22px;
font-size: 0.8rem;
color: #888;
font-weight: bold;
}
.log-box {
width: 100%;
max-width: 600px;
height: 140px;
background: var(--card);
border-radius: 12px;
padding: 10px;
overflow-y: auto;
font-family: monospace;
font-size: 0.85rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.log-item {
padding: 3px 0;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
}
.step-num { color: var(--primary); font-weight: bold; margin-right: 8px; white-space: nowrap; }
.step-msg { text-align: right; }
@media (max-width: 600px) {
.towers { height: 45vw; min-height: 180px; }
.settings-grid { grid-template-columns: 1fr 1fr; }
.field:nth-child(3) { grid-column: span 2; }
h1 { font-size: 1.2rem; }
button { padding: 10px; font-size: 0.9rem; }
}
</style>
</head>
<body>
<h1>汉诺塔演示</h1>
<div class="panel">
<div class="settings-grid">
<div class="field">
<label>层数 (≥2)</label>
<input type="number" id="inpCount" value="3" min="2">
</div>
<div class="field">
<label>速度 (ms/步)</label>
<input type="number" id="inpSpeed" value="500" min="1" step="50">
</div>
<div class="field">
<label>模式</label>
<select id="inpMode">
<option value="auto">自动播放</option>
<option value="manual">手动单步</option>
</select>
</div>
</div>
<div class="btn-row">
<button class="btn-start" id="btnStart">开始</button>
<button class="btn-step" id="btnStep" disabled>下一步</button>
<button class="btn-reset" id="btnReset">重置</button>
</div>
</div>
<div class="game-box">
<div class="towers" id="gameArea">
<div class="tower" id="t0"><div class="pole"></div><div class="label">A</div></div>
<div class="tower" id="t1"><div class="pole"></div><div class="label">B</div></div>
<div class="tower" id="t2"><div class="pole"></div><div class="label">C</div></div>
</div>
</div>
<div class="log-box" id="logBox"></div>
<script>
const state = {
towers: [[], [], []],
pos: [], // 每个盘子当前所在柱子索引
idx: 0n, // 当前步数(BigInt支持超大步数)
total: 0n, // 总步数
timer: null,
running: false,
n: 3
};
const $ = id => document.getElementById(id);
const dom = {
t: [$('t0'), $('t1'), $('t2')],
log: $('logBox'),
area: $('gameArea'),
start: $('btnStart'),
step: $('btnStep'),
reset: $('btnReset'),
count: $('inpCount'),
speed: $('inpSpeed'),
mode: $('inpMode')
};
// ---------- 初始化 ----------
function init() {
stop();
let n = parseInt(dom.count.value);
if (isNaN(n) || n < 2) n = 3;
dom.count.value = n;
state.n = n;
state.towers = [[], [], []];
state.pos = new Array(n + 1).fill(0);
state.idx = 0n;
state.total = (1n << BigInt(n)) - 1n;
for (let i = n; i >= 1; i--) state.towers[0].push(i);
render();
clearLog();
addLog('Info', `已就绪 (${n}层, 共 ${state.total.toString()} 步)`);
updateBtns();
}
// ---------- 实时计算下一步 ----------
function getDisk(k) {
let d = 1;
let t = k;
while ((t & 1n) === 0n) {
t >>= 1n;
d++;
}
return d;
}
function doStep() {
if (state.idx >= state.total) {
finish();
return false;
}
const k = state.idx + 1n;
const disk = getDisk(k);
const from = state.pos[disk];
const diff = BigInt(state.n - disk);
const clockwise = (diff & 1n) === 1n;
const to = clockwise ? (from + 1) % 3 : (from + 2) % 3;
state.towers[from].pop();
state.towers[to].push(disk);
state.pos[disk] = to;
state.idx = k;
render();
const names = ['A', 'B', 'C'];
addLog(`#${k.toString()}`, `${names[from]} → ${names[to]}`);
return true;
}
// ---------- 自动播放 ----------
function loop() {
if (!state.running) return;
const cont = doStep();
if (!cont) {
stop();
return;
}
const speed = Math.max(1, parseInt(dom.speed.value) || 500);
state.timer = setTimeout(loop, speed);
}
function startAuto() {
if (state.running) return;
if (state.idx >= state.total) {
alert('已完成,请点击重置');
return;
}
state.running = true;
updateBtns();
loop();
}
function stop() {
state.running = false;
clearTimeout(state.timer);
updateBtns();
}
function finish() {
stop();
addLog('Done', '🎉 移动完成!');
setTimeout(() => alert(`恭喜!完成 ${state.total.toString()} 步`), 50);
}
// ---------- 渲染 ----------
function render() {
const h = dom.area.clientHeight;
const maxH = Math.floor((h - 10) / state.n);
const diskH = Math.max(1, Math.min(25, maxH));
dom.t.forEach((el, i) => {
const pole = el.querySelector('.pole');
const lbl = el.querySelector('.label');
el.innerHTML = '';
el.appendChild(pole);
el.appendChild(lbl);
state.towers[i].forEach(size => {
const d = document.createElement('div');
d.className = 'disk';
const w = 20 + (size / state.n) * 70;
d.style.width = `${w}%`;
d.style.height = `${diskH}px`;
const hueSpan = state.n > 10 ? 120 : 200;
const hue = 200 + (size / state.n) * hueSpan;
d.style.backgroundColor = `hsl(${hue}, 70%, 60%)`;
el.appendChild(d);
});
});
}
// ---------- UI ----------
function updateBtns() {
const isManual = dom.mode.value === 'manual';
const done = state.idx >= state.total;
dom.start.disabled = state.running || done;
dom.step.disabled = state.running || !isManual || done;
dom.count.disabled = state.running;
dom.start.textContent = state.running ? '运行中...' : '开始';
}
function addLog(tag, msg) {
const div = document.createElement('div');
div.className = 'log-item';
div.innerHTML = `<span class="step-num">${tag}</span><span class="step-msg">${msg}</span>`;
dom.log.prepend(div);
}
function clearLog() { dom.log.innerHTML = ''; }
// ---------- 事件 ----------
dom.start.onclick = () => dom.mode.value === 'auto' ? startAuto() : doStep();
dom.step.onclick = doStep;
dom.reset.onclick = init; // 重置随时可用,无确认弹窗
dom.mode.onchange = () => { stop(); updateBtns(); };
// 层数改变立即生效(防抖500ms)
let debounce;
dom.count.addEventListener('input', () => {
clearTimeout(debounce);
debounce = setTimeout(init, 500);
});
dom.count.addEventListener('change', init);
let rTimer;
window.addEventListener('resize', () => {
clearTimeout(rTimer);
rTimer = setTimeout(render, 200);
});
init();
</script>
</body>
</html>
index.html
index.html