尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

纯HTML/CSS/JS实现可交互汉诺塔小游戏

纯HTML/CSS/JS实现可交互汉诺塔小游戏 简介这是一份基于纯前端技术实现的汉诺塔益智小游戏源码包面向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 langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title汉诺塔小游戏/title style /* 后续章节详述 */ /style /head body div classgame-header h1汉诺塔3层/h1 p步数span idstep-count0/span | 状态span idgame-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; } /* 圆盘尺寸映射size1 → 宽120px, size2 → 160px, size3 → 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-grabbedtrue] { opacity: 0.7; transform: translateX(-50%) scale(0.95); }注意z-index值必须严格按size逆序设置大圆盘在下z-index 小否则视觉堆叠会错乱transition仅作用于opacity和transform避免触发动画重排reflowuser-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-idleft]); leftTower.innerHTML div classtower-base/div div classdisk 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; } } }注意tabindex0让元素进入标准焦点流ArrowLeft/ArrowRight实现环形切换% towers.length防止越界Enter触发移动而非聚焦——这符合“焦点在柱子上按 Enter 执行操作”的直觉。3.2 为圆盘添加 aria-label 与 role 属性提升屏幕阅读器体验视障用户依赖屏幕阅读器理解界面。每个圆盘需声明其语义角色roleimg和描述aria-label柱子需标注roleregion和aria-labelledby关联标题!-- 修改 HTML 中的 tower 容器 -- div classtower>/* 隐藏但保留在读取流中 */ .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”更易理解roleregion告诉阅读器这是一个独立内容区域.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 层实际受屏幕宽度限制建议 ≤6class 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-idleft]); leftTower.innerHTML div classtower-base/div; // 从大到小生成圆盘视觉上底部最大 for (let size this.diskCount; size 1; size--) { const width 100 size * 20; // size1→120px, size2→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-idmid], .tower[data-idright]) .forEach(t t.innerHTML div classtower-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 classdifficulty-selector label fordisk-select难度/label select iddisk-select option value33层入门/option option value44层熟练/option option value55层挑战/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在胜利时计算不增加运行时开销。本文还有配套的精品资源点击获取
返回列表