简介:这是一份基于纯前端技术实现的汉诺塔益智小游戏源码包,面向Web前端初学者与算法可视化教学场景,帮助理解递归思想、栈结构及交互式游戏开发逻辑。资源包含21个文件,涵盖2个核心HTML页面、2个JS脚本(含主逻辑hanoi.js与弹窗组件sweet-alert.min.js)、3个CSS样式文件、6张界面截图与操作演示MP4视频,以及字体、图标和说明文档等配套资源,整体压缩包大小为19.82MB。已有941人学习下载,适合用于课程设计、算法演示或自学实践。用户可直接运行index.html体验完整功能:支持3–8层圆盘自由配置,实时显示最小步数与当前移动次数,提供撤销操作、自动求解及响应式UI,所有逻辑均在浏览器端完成,无需后端依赖,代码结构清晰、注释完备,便于二次开发与教学拓展。
1. 用纯 HTML + CSS + JavaScript 实现可交互的汉诺塔小游戏,不依赖任何框架或构建工具
你点开一个网页,三根竖杆从左到右排开,上面叠着大小不一的圆盘——最小的在顶,最大的在底。你点击一个圆盘,它高亮;再点另一根杆,如果规则允许(只能小盘压大盘、每次只动一个),圆盘就“飞”过去。没有 npm install,没有 webpack,没有 React 绑定状态,只有<html>标签里塞进的 300 行以内原生代码。这就是「html实现汉诺塔小游戏源码」的真实含义:它不是教学 PPT 里的伪代码,而是能直接保存为.html文件、双击在浏览器里运行、支持鼠标拖拽/点击、带步数统计和胜利判定的完整可执行体。适合刚学完 DOM 操作的新手照着调试逻辑,也适合前端面试前快速复现经典递归可视化案例。它解决的不是“怎么解汉诺塔”,而是“怎么把抽象递归过程变成用户能摸得着的界面反馈”。所有状态管理、动画过渡、规则校验都压在script标签里,连 CSS 都用内联样式或<style>写死——这才是搜索“html 汉诺塔 源码”时,90% 用户真正想抄走就跑的最小可行版本。
2. 从 DOM 结构到游戏状态机:搭建汉诺塔的三层骨架
2.1 用语义化 HTML 定义三根柱子与可移动圆盘
汉诺塔的物理结构必须先在 HTML 中具象化。不能用 div 堆砌一堆无意义的盒子,而要让每个元素承载明确职责:柱子是容器,圆盘是可操作实体,状态栏是信息出口。以下结构是经过多次简化后保留的最小必要节点:
<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>汉诺塔小游戏</title> <style> /* 后续章节详述 */ </style> </head> <body> <div class="game-header"> <h1>汉诺塔(3层)</h1> <p>步数:<span id="step-count">0</span> | 状态:<span id="game-status">等待开始</span></p> </div> <!-- 三根柱子,用>* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: "Segoe UI", sans-serif; background: #f5f7fa; color: #333; } .game-header { text-align: center; padding: 16px; background: #2c3e50; color: white; } .game-header h1 { margin-bottom: 8px; font-weight: 600; } .game-header p { font-size: 14px; opacity: 0.9; } .tower-container { display: flex; justify-content: space-around; align-items: flex-end; min-height: 400px; padding: 20px 0; } .tower { position: relative; width: 80px; height: 300px; } .tower-base { position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 100px; height: 12px; background: #2c3e50; border-radius: 2px; } .tower::before { content: ""; position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 6px; height: 100%; background: #34495e; } .disk { position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); height: 32px; border-radius: 4px; background: linear-gradient(135deg, #3498db, #2980b9); color: white; font-weight: bold; display: flex; align-items: center; justify-content: center; user-select: none; cursor: grab; transition: all 0.2s ease; } /* 圆盘尺寸映射:size=1 → 宽120px, size=2 → 160px, size=3 → 200px */ .disk-1 { width: 120px; } .disk-2 { width: 160px; } .disk-3 { width: 200px; } /* 堆叠顺序:顶部圆盘 z-index 最高 */ .disk-1 { z-index: 10; } .disk-2 { z-index: 20; } .disk-3 { z-index: 30; } /* 拖拽悬停反馈:目标柱子高亮边框 */ .tower.drag-over { outline: 2px dashed #27ae60; } /* 拖拽中圆盘半透明 */ .disk:active, .disk[aria-grabbed="true"] { opacity: 0.7; transform: translateX(-50%) scale(0.95); }注意:
z-index值必须严格按size逆序设置(大圆盘在下,z-index 小),否则视觉堆叠会错乱;transition仅作用于opacity和transform,避免触发动画重排(reflow);user-select: none防止拖拽时文字被选中。
2.3 用 JavaScript 实现状态驱动的游戏引擎:规则校验与动作闭环
HTML 和 CSS 是静态骨架,JavaScript 才是让游戏“活起来”的引擎。这里不采用全局变量堆砌,而是封装成HanoiGame类,聚焦三个核心能力:状态快照(记录每根柱子上有哪些圆盘)、移动校验(是否符合“小盘不能压大盘”)、动作执行(DOM 更新 + 步数累加)。以下是精简后的核心逻辑:
class HanoiGame { constructor() { this.towers = { left: [3, 2, 1], // 初始状态:左柱有3个盘,从底到顶为3→2→1 mid: [], right: [] }; this.selectedDisk = null; this.selectedTower = null; this.stepCount = 0; this.isDragging = false; this.init(); } init() { // 绑定重置按钮 document.getElementById('reset-btn').addEventListener('click', () => this.reset()); // 绑定所有圆盘的拖拽事件 document.querySelectorAll('.disk').forEach(disk => { disk.addEventListener('dragstart', (e) => this.handleDragStart(e)); disk.addEventListener('dragend', () => this.handleDragEnd()); }); // 绑定柱子的放置事件 document.querySelectorAll('.tower').forEach(tower => { tower.addEventListener('dragover', (e) => this.handleDragOver(e)); tower.addEventListener('drop', (e) => this.handleDrop(e)); }); } handleDragStart(e) { const disk = e.target; const size = parseInt(disk.dataset.size); const towerId = disk.closest('.tower').dataset.id; // 记录被拖起的圆盘及其所在柱子 this.selectedDisk = { size, towerId }; this.isDragging = true; // 设置拖拽数据(仅传递 size,不传 DOM) e.dataTransfer.setData('text/plain', size.toString()); } handleDragOver(e) { e.preventDefault(); // 允许 drop const tower = e.currentTarget; if (!this.isDragging) return; // 添加悬停样式 tower.classList.add('drag-over'); } handleDrop(e) { e.preventDefault(); const tower = e.currentTarget; const targetId = tower.dataset.id; const size = parseInt(e.dataTransfer.getData('text/plain')); // 规则校验:不能放回原柱;目标柱空或顶部盘比当前盘大 const canMove = this.selectedDisk.towerId !== targetId && (this.towers[targetId].length === 0 || this.towers[targetId][this.towers[targetId].length - 1] > size); if (canMove) { // 执行移动:从原柱移除,向目标柱添加 const fromId = this.selectedDisk.towerId; this.towers[fromId] = this.towers[fromId].filter(s => s !== size); this.towers[targetId].push(size); // 更新 DOM:将圆盘元素移动到目标柱子内部 const diskElement = document.querySelector(`.disk[data-size="${size}"]`); tower.appendChild(diskElement); // 步数+1,更新状态栏 this.stepCount++; document.getElementById('step-count').textContent = this.stepCount; // 检查胜利条件:所有盘都在 right 柱上且顺序正确([1,2,3]) if (JSON.stringify(this.towers.right) === '[1,2,3]') { document.getElementById('game-status').textContent = '🎉 恭喜!通关成功'; document.getElementById('game-status').style.color = '#27ae60'; } else { document.getElementById('game-status').textContent = '继续努力'; document.getElementById('game-status').style.color = '#34495e'; } } else { document.getElementById('game-status').textContent = '❌ 违反规则:只能小盘压大盘'; document.getElementById('game-status').style.color = '#e74c3c'; setTimeout(() => { document.getElementById('game-status').textContent = '继续努力'; document.getElementById('game-status').style.color = '#34495e'; }, 2000); } // 清理状态 this.selectedDisk = null; this.isDragging = false; } handleDragEnd() { this.isDragging = false; } reset() { // 重置 towers 状态 this.towers = { left: [3, 2, 1], mid: [], right: [] }; this.stepCount = 0; this.selectedDisk = null; // 重置 DOM:将所有圆盘放回 left 柱,按 size 降序排列(视觉上从底到顶) const leftTower = document.querySelector('.tower[data-id="left"]'); leftTower.innerHTML = ` <div class="tower-base"></div> <div class="disk disk-3">// 在 HanoiGame.init() 中追加: document.querySelectorAll('.tower').forEach((tower, index) => { tower.tabIndex = 0; tower.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this.handleTowerClick(tower); } else if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { e.preventDefault(); const towers = Array.from(document.querySelectorAll('.tower')); const currentIndex = towers.indexOf(tower); const nextIndex = e.key === 'ArrowLeft' ? (currentIndex - 1 + towers.length) % towers.length : (currentIndex + 1) % towers.length; towers[nextIndex].focus(); } }); }); // 新增方法:处理柱子键盘点击 handleTowerClick(tower) { if (!this.selectedDisk) return; const targetId = tower.dataset.id; const size = this.selectedDisk.size; // 复用 handleDrop 中的校验逻辑 const canMove = this.selectedDisk.towerId !== targetId && (this.towers[targetId].length === 0 || this.towers[targetId][this.towers[targetId].length - 1] > size); if (canMove) { // 执行移动(同 handleDrop) const fromId = this.selectedDisk.towerId; this.towers[fromId] = this.towers[fromId].filter(s => s !== size); this.towers[targetId].push(size); // DOM 移动:找到对应 size 的圆盘并 append 到目标柱 const diskElement = document.querySelector(`.disk[data-size="${size}"]`); tower.appendChild(diskElement); this.stepCount++; document.getElementById('step-count').textContent = this.stepCount; // 胜利检查 if (JSON.stringify(this.towers.right) === '[1,2,3]') { document.getElementById('game-status').textContent = '🎉 恭喜!通关成功'; document.getElementById('game-status').style.color = '#27ae60'; } } }注意:
tabindex="0"让元素进入标准焦点流,ArrowLeft/ArrowRight实现环形切换(% towers.length防止越界),Enter触发移动而非聚焦——这符合“焦点在柱子上,按 Enter 执行操作”的直觉。
3.2 为圆盘添加 aria-label 与 role 属性提升屏幕阅读器体验
视障用户依赖屏幕阅读器理解界面。每个圆盘需声明其语义角色(role="img")和描述(aria-label),柱子需标注role="region"和aria-labelledby关联标题:
<!-- 修改 HTML 中的 tower 容器 --> <div class="tower">/* 隐藏但保留在读取流中 */ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }提示:
aria-label明确说明“第几层”和“尺寸”,比单纯说“圆盘3”更易理解;role="region"告诉阅读器这是一个独立内容区域;.sr-only是无障碍开发标准技巧,确保文本不显示但可被读取。
4. 优化性能与兼容性:让汉诺塔在旧版浏览器中稳定运行
4.1 替代 drag-and-drop API 的降级方案(IE11 及以下)
原生dragstart/dragover/drop在 IE11 中存在事件冒泡异常和dataTransfer不兼容问题。我们提供mousedown/mousemove/mouseup手动实现作为 fallback,通过getBoundingClientRect()计算鼠标相对于柱子的位置:
// 在 HanoiGame.init() 中检测 drag support if (!('draggable' in document.createElement('span'))) { // IE11 或更老浏览器,启用 mouse fallback this.enableMouseFallback(); } enableMouseFallback() { let isMouseDown = false; let draggedDisk = null; let offsetX = 0; document.addEventListener('mousedown', (e) => { if (e.target.classList.contains('disk')) { isMouseDown = true; draggedDisk = e.target; const rect = draggedDisk.getBoundingClientRect(); offsetX = e.clientX - rect.left; draggedDisk.style.position = 'absolute'; draggedDisk.style.zIndex = '1000'; draggedDisk.style.cursor = 'grabbing'; } }); document.addEventListener('mousemove', (e) => { if (isMouseDown && draggedDisk) { draggedDisk.style.left = (e.clientX - offsetX) + 'px'; draggedDisk.style.top = (e.clientY - 20) + 'px'; } }); document.addEventListener('mouseup', (e) => { if (isMouseDown && draggedDisk) { isMouseDown = false; // 检查鼠标释放时是否在某根柱子上 const towers = document.querySelectorAll('.tower'); let targetTower = null; for (const tower of towers) { const rect = tower.getBoundingClientRect(); if (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom) { targetTower = tower; break; } } if (targetTower) { const size = parseInt(draggedDisk.dataset.size); const targetId = targetTower.dataset.id; const fromId = draggedDisk.closest('.tower').dataset.id; const canMove = fromId !== targetId && (this.towers[targetId].length === 0 || this.towers[targetId][this.towers[targetId].length - 1] > size); if (canMove) { // 执行移动(同前) this.towers[fromId] = this.towers[fromId].filter(s => s !== size); this.towers[targetId].push(size); targetTower.appendChild(draggedDisk); this.stepCount++; document.getElementById('step-count').textContent = this.stepCount; } } // 重置样式 draggedDisk.style.position = ''; draggedDisk.style.zIndex = ''; draggedDisk.style.left = ''; draggedDisk.style.top = ''; draggedDisk.style.cursor = ''; draggedDisk = null; } }); }注意:此 fallback 仅在检测到不支持
draggable时激活;getBoundingClientRect()返回的是视口坐标,需减去offsetX保证拖拽跟随鼠标;zIndex: 1000确保拖拽中圆盘显示在最上层。
4.2 使用 CSS 自定义属性统一管理主题色与动画时长
为便于后续修改主题(如深色模式),将颜色和过渡时间抽离为 CSS 变量,避免硬编码:
:root { --primary-color: #3498db; --primary-dark: #2980b9; --success-color: #27ae60; --error-color: #e74c3c; --bg-color: #f5f7fa; --tower-color: #34495e; --disk-transition: all 0.2s ease; } .disk { background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); transition: var(--disk-transition); } .tower::before { background: var(--tower-color); } .tower.drag-over { outline: 2px dashed var(--success-color); }提示:
var(--disk-transition)可在 JS 中动态修改(如document.documentElement.style.setProperty('--disk-transition', 'all 0.1s ease')),实现运行时动画加速;--success-color同时用于胜利提示和悬停边框,保证视觉一致性。
5. 扩展玩法:从 3 层到 N 层的参数化重构与难度调节
5.1 将盘数抽象为配置项,支持动态生成 N 层汉诺塔
原始代码写死 3 层,但“html实现汉诺塔小游戏源码”常被用于教学演示不同规模问题。我们提取DISK_COUNT常量,用循环生成 DOM 和初始状态,使代码可扩展至 4、5、6 层(实际受屏幕宽度限制,建议 ≤6):
class HanoiGame { constructor(config = {}) { this.diskCount = config.diskCount || 3; // 默认3层 this.towers = { left: Array.from({ length: this.diskCount }, (_, i) => this.diskCount - i), // [3,2,1] mid: [], right: [] }; // ...其余初始化 } renderDisks() { const leftTower = document.querySelector('.tower[data-id="left"]'); leftTower.innerHTML = '<div class="tower-base"></div>'; // 从大到小生成圆盘(视觉上底部最大) for (let size = this.diskCount; size >= 1; size--) { const width = 100 + size * 20; // size=1→120px, size=2→140px... 保证递增 const disk = document.createElement('div'); disk.className = `disk disk-${size}`; disk.dataset.size = size; disk.draggable = true; disk.textContent = size; disk.style.width = `${width}px`; leftTower.appendChild(disk); } // 清空其他柱子 document.querySelectorAll('.tower[data-id="mid"], .tower[data-id="right"]') .forEach(t => t.innerHTML = '<div class="tower-base"></div>'); } // 在 init() 中调用 init() { this.renderDisks(); // ...绑定事件 } } // 使用方式:new HanoiGame({ diskCount: 4 });注意:
width = 100 + size * 20确保圆盘宽度严格随 size 增大,避免视觉错乱;Array.from({ length: n }, (_, i) => n - i)生成降序数组[n, n-1, ..., 1],符合汉诺塔初始堆叠逻辑。
5.2 添加难度选择下拉菜单与实时重置功能
用户需要直观入口切换难度。我们在.game-header中插入<select>,监听change事件触发重置:
<!-- 在 game-header 中追加 --> <div class="difficulty-selector"> <label for="disk-select">难度:</label> <select id="disk-select"> <option value="3">3层(入门)</option> <option value="4">4层(熟练)</option> <option value="5">5层(挑战)</option> </select> </div>// 在 HanoiGame.init() 中绑定 document.getElementById('disk-select').addEventListener('change', (e) => { const newCount = parseInt(e.target.value); this.diskCount = newCount; this.reset(); // reset 方法需适配新 diskCount }); // 修改 reset() 方法以支持新盘数 reset() { this.towers = { left: Array.from({ length: this.diskCount }, (_, i) => this.diskCount - i), mid: [], right: [] }; this.stepCount = 0; this.selectedDisk = null; this.renderDisks(); // 重新渲染 DOM document.getElementById('step-count').textContent = '0'; document.getElementById('game-status').textContent = '等待开始'; document.getElementById('game-status').style.color = '#34495e'; }提示:
<select>的value直接对应层数,无需额外映射;renderDisks()解耦了 DOM 生成逻辑,使reset()可复用;选项文案加入“入门/熟练/挑战”降低认知负荷。
5.3 胜利步数验证:对比理论最优解(2ⁿ−1)并给出评价
汉诺塔 n 层的最少移动步数是数学定理:2ⁿ−1。我们在胜利时计算当前步数与理论值的比值,给出鼓励性评价,增强游戏反馈:
// 在胜利判定分支中追加 const theoreticalMin = Math.pow(2, this.diskCount) - 1; const efficiency = (this.stepCount / theoreticalMin * 100).toFixed(1); let message = '🎉 恭喜!通关成功'; if (this.stepCount === theoreticalMin) { message += '(完美解!)'; } else if (efficiency <= 110) { message += `(接近最优:${efficiency}%)`; } else { message += `(当前步数:${this.stepCount},最优需${theoreticalMin})`; } document.getElementById('game-status').textContent = message; document.getElementById('game-status').style.color = '#27ae60';注意:
Math.pow(2, n) - 1是标准公式,toFixed(1)保留一位小数避免100.0%冗余;评价文案分三级(完美/接近/一般),避免挫败感;theoreticalMin在胜利时计算,不增加运行时开销。
本文还有配套的精品资源,点击获取