资讯动态

原生JS实现可交互汉诺塔游戏:状态机+拖拽+自动求解

发布时间:2026/9/14 14:58:10 来源:尧图企业网站定制
简介这是一份基于纯前端技术实现的汉诺塔益智小游戏源码包面向Web开发初学者与HTML/CSS/JavaScript入门学习者帮助理解递归思想、DOM操作与交互逻辑设计。资源共21个文件包含2个核心HTML页面、2个JS脚本含主游戏逻辑hanoi.js与弹窗组件sweet-alert.min.js、3个CSS样式文件含重置、默认及提示样式、6张界面截图PNG、1段演示MP4视频以及字体文件、图标和说明文档等完整呈现可运行项目的结构规范与资源组织方式。压缩包大小为19.82MB开箱即用无需后端支持。目前已有941人学习下载。读者可直接部署运行体验动态计步、最小步数提示、手动回退、AI自动求解等功能配套演示视频与多张界面截图直观展示交互效果readme.txt提供配置说明便于快速上手与二次开发。1. 用纯 HTMLCSSJS 实现可交互汉诺塔不是静态演示而是真能玩的逻辑闭环你点开一个汉诺塔网页拖着圆盘在三根柱子间来回试错系统实时校验“小盘不能压大盘”倒计时式显示当前步数与理论最优解的差距还能一键回退上一步、甚至让 JS 自动推演完成——这不是教学动画而是一个完整状态机驱动的交互式益智游戏。它不依赖任何框架仅靠原生 DOM 操作与递归算法支撑全部逻辑所有状态盘片位置、移动历史、步数统计、胜负判定都由 JavaScript 精确维护CSS 负责响应式布局与过渡动画HTML 提供语义化结构。适合前端初学者拆解状态管理范式也适合算法学习者观察递归解法如何映射到 UI 层38 层盘片自由配置支持移动端触摸拖拽与桌面端鼠标拖放双模式所有交互反馈即时可见。源码包里已预置sweet-alert.min.js实现轻量弹窗提示normalize.css统一基础样式default.css定制游戏视觉层整个项目可直接双击index.html运行无需构建工具或本地服务器。2. 汉诺塔状态机设计从数学递归到 DOM 元素映射的三层建模汉诺塔本质是状态空间搜索问题其核心约束只有两条每次只能移动一个盘片、大盘不能叠在小盘之上。但要将这个抽象规则落地为可交互 UI必须建立三层映射关系数学状态 → 内存数据结构 → DOM 元素位置。本项目采用栈Stack模型模拟每根柱子每个栈存储盘片编号1 为最小n 为最大初始状态为[ [n, n-1, ..., 1], [], [] ]目标状态为[ [], [], [n, n-1, ..., 1] ]。关键在于避免用数组索引硬编码柱子 ID而是通过>function initDisks(n) { const columns document.querySelectorAll(.column); // 清空所有柱子 columns.forEach(col col.innerHTML ); // 为第 0 根柱子左生成 n 个盘片从大到小叠放 for (let i n; i 1; i--) { const disk document.createElement(div); disk.className disk; disk.dataset.size i; // 记录盘片大小用于碰撞检测 disk.textContent i; disk.style.width ${80 i * 12}px; // 尺寸随编号增大视觉可辨 disk.style.zIndex i; // 大盘在下小盘在上z-index 反向设置 columns[0].appendChild(disk); } }注意dataset.size是校验移动合法性的唯一依据而非 DOM 顺序或 CSS 类名zIndex设置为i非n-i1是因为浏览器渲染时 z-index 数值越大越靠前而我们需要视觉上大盘在底层所以大盘i 值大z-index 更高但被后续 append 的小盘覆盖——这依赖于 DOM 插入顺序与 z-index 共同作用实际效果是后 append 的小盘自然浮在先 append 的大盘上方符合物理直觉。2.2 拖拽事件流与状态同步机制拖拽分三阶段mousedown记录起始柱子与被拖盘片mousemove实时计算光标相对柱子容器的位置mouseup触发落点判定。关键不在视觉拖动效果而在落点合法性校验let draggedDisk null; let sourceColumn null; document.addEventListener(mousedown, e { if (e.target.classList.contains(disk)) { draggedDisk e.target; sourceColumn parseInt(e.target.closest(.column).dataset.column); e.target.style.opacity 0.6; } }); document.addEventListener(mouseup, e { if (!draggedDisk) return; const targetColumnEl e.target.closest(.column); if (!targetColumnEl) { resetDragState(); return; } const targetColumn parseInt(targetColumnEl.dataset.column); if (targetColumn sourceColumn) { resetDragState(); return; } // 校验目标柱子为空或顶部盘片比 draggedDisk 大 const targetStack stacks[targetColumn]; const topDiskSize targetStack.length 0 ? parseInt(document.querySelector(.disk[data-size${targetStack[targetStack.length-1]}]).dataset.size) : Infinity; const draggedSize parseInt(draggedDisk.dataset.size); if (draggedSize topDiskSize) { // 合法移动更新内存栈 DOM 位置 stacks[sourceColumn].pop(); stacks[targetColumn].push(draggedSize); moveDiskToColumn(draggedDisk, targetColumnEl); updateMoveCount(); checkWinCondition(); } else { // 非法移动播放错误提示 swal(违规操作, 小盘不能放在大盘上, error); } resetDragState(); });提示stacks是全局二维数组[[...], [...], [...]]存储每根柱子上的盘片 size 值moveDiskToColumn()函数负责将draggedDisk元素从原柱子removeChild()并appendChild()到目标柱子同时触发 CSS transition 动画updateMoveCount()不仅更新页面显示的步数还实时比对Math.pow(2, n) - 1计算出的理论最小步数当currentSteps minSteps时触发胜利逻辑。2.3 递归求解器与自动演示实现原理用户点击“电脑完成”按钮时并非调用黑盒 API而是执行本地 JavaScript 递归函数将每一步移动指令转化为 DOM 操作队列function solveHanoi(n, from, to, aux, moves []) { if (n 1) { moves.push({ disk: n, from, to }); } else { solveHanoi(n - 1, from, aux, to, moves); moves.push({ disk: n, from, to }); solveHanoi(n - 1, aux, to, from, moves); } return moves; } // 执行自动演示 function autoSolve() { const n parseInt(document.getElementById(disk-count).value); const moves solveHanoi(n, 0, 2, 1); // 从柱0→柱2辅助柱1 let stepIndex 0; const interval setInterval(() { if (stepIndex moves.length) { clearInterval(interval); return; } const { disk, from, to } moves[stepIndex]; // 找到柱 from 上最顶层的 disk 元素size disk const diskEl document.querySelector(.column[data-column${from}] .disk[data-size${disk}]); if (diskEl) { moveDiskToColumn(diskEl, document.querySelector(.column[data-column${to}])); stacks[from] stacks[from].filter(s s ! disk); stacks[to].push(disk); updateMoveCount(); stepIndex; } }, 600); // 每步间隔 600ms可调 }关键点递归函数返回的是纯数据指令数组不包含任何 DOM 操作moveDiskToColumn()是复用的手动拖拽移动函数保证自动与手动路径完全一致stacks数组在自动执行中同步更新确保状态一致性。这种分离使调试变得简单打印moves数组即可验证算法正确性无需介入 UI 层。3. CSS 布局与交互动效用 Flexbox 构建响应式柱体与 transition 实现盘片滑动汉诺塔 UI 的视觉合理性高度依赖布局稳定性与动效自然度。本项目放弃绝对定位与 JavaScript 计算 left/top转而采用 Flexbox 布局 CSS transition 实现柱体自适应与盘片平滑位移既降低维护成本又保障移动端兼容性。3.1 三柱布局的 Flexbox 实现与断点适配.game-area容器设为display: flex三根.column子元素均设flex: 1在桌面端等宽分布当屏幕宽度 768px 时切换为flex-direction: column柱体垂直堆叠避免水平溢出.game-area { display: flex; justify-content: space-around; align-items: flex-end; min-height: 400px; margin: 20px 0; } .column { width: 80px; background: #2c3e50; border-radius: 4px; position: relative; flex: 1; min-height: 300px; display: flex; flex-direction: column-reverse; /* 关键让新 append 的盘片从底部向上堆 */ align-items: center; padding-bottom: 20px; } media (max-width: 768px) { .game-area { flex-direction: column; align-items: center; } .column { width: 90%; max-width: 200px; } }注意flex-direction: column-reverse是实现“盘片自然堆叠”的核心技巧。DOM 中盘片按从大到小顺序 append而column-reverse使它们在视觉上从小到大自下而上排列无需计算每个盘片的top值align-items: center保证所有盘片水平居中padding-bottom: 20px为底部留出基座空间。3.2 盘片尺寸、颜色与 transition 动画控制每个盘片的宽度、背景色、文字大小均通过>.disk { height: 24px; border-radius: 4px; color: white; font-weight: bold; text-align: center; line-height: 24px; cursor: grab; user-select: none; transition: all 0.3s ease-out; /* 所有属性平滑过渡 */ box-shadow: 0 2px 4px rgba(0,0,0,0.2); } .disk[data-size1] { background: #e74c3c; width: 104px; font-size: 12px; } .disk[data-size2] { background: #e67e22; width: 116px; font-size: 13px; } .disk[data-size3] { background: #f1c40f; width: 128px; font-size: 14px; } .disk[data-size4] { background: #2ecc71; width: 140px; font-size: 15px; } .disk[data-size5] { background: #3498db; width: 152px; font-size: 16px; } .disk[data-size6] { background: #9b59b6; width: 164px; font-size: 17px; } .disk[data-size7] { background: #1abc9c; width: 176px; font-size: 18px; } .disk[data-size8] { background: #e84393; width: 188px; font-size: 19px; }提示transition: all 0.3s ease-out使盘片在appendChild()到新柱子时自动滑入目标位置无需 JS 控制left/topcursor: grab提供拖拽意图反馈user-select: none防止文字被误选。所有颜色采用 Material Design 调色板确保对比度达标WCAG AA。3.3 移动端触摸支持与事件降级处理为兼容 iOS/Android需监听touchstart/touchmove/touchend事件并与鼠标事件共存// 合并鼠标与触摸事件 const startEvent ontouchstart in window ? touchstart : mousedown; const moveEvent ontouchstart in window ? touchmove : mousemove; const endEvent ontouchstart in window ? touchend : mouseup; document.addEventListener(startEvent, handleStart, false); document.addEventListener(endEvent, handleEnd, false); function handleStart(e) { const touch e.type touchstart ? e.touches[0] : e; // ... 同 mousedown 逻辑但使用 touch.clientX/clientY }关键细节touchstart事件中需阻止默认行为e.preventDefault()防止页面滚动干扰handleStart中获取坐标统一用touch.clientX/clientY避免e.pageX/pageY在移动端的兼容性问题moveDiskToColumn()函数内部不区分事件类型只接收 DOM 元素与目标容器实现逻辑复用。4. 参数配置与边界校验盘片数量控制、步数统计与胜利判定逻辑用户可自由调节盘片数量38该配置直接影响初始状态、理论最小步数、UI 布局密度及自动求解耗时。参数变更必须触发全量重置且所有校验需在 DOM 层与内存层同步生效避免状态撕裂。4.1 盘片数量选择器与动态重置流程HTML 中input typerange iddisk-count min3 max8 value5绑定change事件触发完整重置div classcontrol-group label fordisk-count盘片数量span iddisk-value5/span/label input typerange iddisk-count min3 max8 value5 /divdocument.getElementById(disk-count).addEventListener(change, function() { const n parseInt(this.value); document.getElementById(disk-value).textContent n; // 重置所有状态 stacks [[], [], []]; // 清空内存栈 moveCount 0; document.getElementById(move-count).textContent moveCount; document.getElementById(min-steps).textContent Math.pow(2, n) - 1; initDisks(n); // 重建 DOM hideWinModal(); // 隐藏胜利弹窗若存在 });注意initDisks(n)必须在stacks重置后调用否则新盘片会错误地 push 到旧栈中document.getElementById(min-steps)实时更新理论值让用户感知难度增长——3 层需 7 步8 层需 255 步形成明确挑战梯度。4.2 步数统计与非法操作拦截机制移动步数moveCount仅在合法移动后递增且需防重复计数如快速连续点击同一盘片function updateMoveCount() { moveCount; document.getElementById(move-count).textContent moveCount; const n parseInt(document.getElementById(disk-count).value); const minSteps Math.pow(2, n) - 1; const stepsElement document.getElementById(move-count); if (moveCount minSteps) { stepsElement.style.color #27ae60; } else if (moveCount minSteps) { stepsElement.style.color #e74c3c; } }提示颜色变化提供即时反馈——绿色表示达成最优解红色表示已超步stepsElement.style.color直接操作内联样式避免 CSS class 切换带来的额外复杂度此逻辑与checkWinCondition()分离因胜利判定需额外检查盘片是否全部到达目标柱而步数颜色仅反映效率。4.3 胜利判定与多条件复合校验胜利条件不仅是步数达标更是所有盘片按大小顺序完整出现在第三根柱子上。需同时校验stacks[2]数组内容与长度function checkWinCondition() { const n parseInt(document.getElementById(disk-count).value); const targetStack stacks[2]; if (targetStack.length ! n) return; // 数量不足 // 检查是否严格递减视觉上从下到上大→小数组中从左到右大→小 for (let i 0; i n; i) { if (targetStack[i] ! n - i) { return; } } // 全部匹配触发胜利 showWinModal(); } function showWinModal() { swal({ title: 恭喜通关, text: 你用了 ${moveCount} 步达到理论最优解, icon: success, buttons: { confirm: { text: 再玩一次, value: true, visible: true } } }).then((willRestart) { if (willRestart) { const n parseInt(document.getElementById(disk-count).value); document.getElementById(disk-count).value n; document.getElementById(disk-value).textContent n; // 触发重置 document.getElementById(disk-count).dispatchEvent(new Event(change)); } }); }关键点targetStack[i] ! n - i校验数组第 i 位是否等于n-i即最大盘在索引 0次大盘在索引 1…这与stacks[2]的 push 顺序一致swal弹窗使用icon: success与自定义按钮文本提升完成仪式感dispatchEvent(new Event(change))主动触发 range 输入框的 change 事件确保重置逻辑被调用而非仅修改 value 属性。5. 回退功能实现与历史状态管理用数组栈保存每一步快照“回退”是提升游戏体验的关键功能它要求系统能撤销任意步操作而非仅最后一步。本项目采用操作历史栈History Stack存储每一步的完整状态快照包括三根柱子的盘片数组、当前步数、被移动盘片信息回退时直接恢复上一快照。5.1 历史记录数据结构与快照捕获时机每次合法移动后将当前stacks数组的深拷贝推入historyStack并记录移动详情const historyStack []; function recordStep(fromCol, toCol, diskSize) { const snapshot [ [...stacks[0]], // 浅拷贝数组但内部元素为数字可直接复制 [...stacks[1]], [...stacks[2]] ]; historyStack.push({ stacks: snapshot, moveCount: moveCount, from: fromCol, to: toCol, disk: diskSize }); } // 在合法移动后调用 recordStep(sourceColumn, targetColumn, draggedSize);注意[...stacks[0]]是 ES6 展开语法对一维数字数组有效若盘片对象含复杂属性需改用JSON.parse(JSON.stringify(stacks))或structuredClone()现代浏览器。此处因stacks仅存数字展开语法高效安全。5.2 回退操作与 DOM 状态同步还原点击“回退”按钮时弹出栈顶快照覆盖当前stacks并重绘所有柱子document.getElementById(undo-btn).addEventListener(click, function() { if (historyStack.length 0) return; const lastState historyStack.pop(); stacks lastState.stacks; moveCount lastState.moveCount; document.getElementById(move-count).textContent moveCount; // 重绘所有柱子 renderColumns(); // 恢复颜色 document.getElementById(move-count).style.color moveCount Math.pow(2, parseInt(document.getElementById(disk-count).value)) - 1 ? #27ae60 : #34495e; }); function renderColumns() { const columns document.querySelectorAll(.column); columns.forEach((col, idx) { col.innerHTML ; const stack stacks[idx]; stack.forEach(size { const disk document.createElement(div); disk.className disk; disk.dataset.size size; disk.textContent size; disk.style.width ${80 size * 12}px; disk.style.zIndex size; col.appendChild(disk); }); }); }提示renderColumns()完全重建 DOM而非尝试 diff 更新因盘片数量少≤8、操作频率低性能无压力historyStack.pop()同时移除并返回最后一个快照天然支持多级回退按钮禁用逻辑未实现但可通过this.disabled historyStack.length 0动态控制增强 UX。5.3 回退与自动求解的冲突规避策略当用户执行“电脑完成”后历史栈应清空防止回退到自动步骤中间状态function autoSolve() { // ... 自动求解逻辑前 historyStack.length 0; // 清空历史栈 // ... 后续执行 }关键设计自动求解被视为一次性原子操作不产生中间历史用户若在自动过程中手动干预则新操作重新开始记录历史。这种设计避免了“回退到自动步骤一半”的逻辑混乱符合用户心智模型——自动演示是旁观手动操作才是参与。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价