基于JavaScript的扫雷游戏:从核心算法到课设答辩实战
简介这是一份基于JavaScript的扫雷游戏完整源码项目面向计算机、电子信息工程、数学等专业的学生可作为课程设计、期末大作业或毕业设计的参考。项目采用原生JavaScript开发未使用第三方库通过实现随机布雷、点击操作、右键标记、游戏重置等功能帮助学习者掌握JS语法、事件驱动、DOM操作及基础算法与数据结构。压缩包共9个文件包含3个JS脚本、1个HTML页面、1个CSS样式、2张PNG图片、1张JPG图片和1个MD文档整体大小仅34KB其中JS脚本负责游戏核心逻辑HTML与CSS构建交互界面图片资源用于地雷与数字图标MD文档可辅助理解代码结构。已有55人学习下载。源码包含完整可运行的扫雷游戏逻辑采用模块化封装可直接运行体验也方便二次修改与扩展是理工科学生从理论学习走向实践操作的高质量参考资料尤其适合入门级JavaScript学习者循序渐进地研读。1. 拿到了基于javascript的扫雷游戏.zip先确认这三件事拿到一个名为「毕设课设基于javascript的扫雷游戏.zip」的压缩包多数人第一件事是解压后双击 index.html。这个动作没问题但如果你准备拿它去答辩或者想从零复刻出来最好先确认三件事棋盘渲染是不是完全用原生 HTML/CSS/JavaScript 完成的核心算法是不是写在独立 js 文件里以及文档里写没写运行环境。扫雷在课设里出现频率高不是因为算法多难而是它能同时考察二维数组、事件监听、递归和 DOM 更新正好覆盖前端基础课的几个知识点。我按一个前端工程师收到这个 zip 后的处理顺序来讲先看压缩包结构再复现雷区算法然后用 ES6 把代码重构到能讲清楚的状态最后在打包成 zip 前把容易翻车的地方全部扫一遍。2. 拆开 zip 先看结构扫雷游戏的 HTML、CSS、JS 分工与运行方式别人交上来的 zip 不一定跟你想的一样常见做法是先看目录再决定用 HBuilder 还是命令行起服务。下面这套顺序可以复用到绝大多数类似的前端课设包。2.1 用 HBuilder 配置 html、css、javascript 的最小启动步骤先把 zip 解压到全英文路径。Windows 下直接右键解压macOS 下注意别漏掉隐藏目录。我习惯用命令行解压因为能看到解压过程中是否有文件名乱码mkdir -p ~/workspace/minesweeper cd ~/workspace/minesweeper unzip -O gbk ../基于javascript的扫雷游戏.zip -d . find . -type f | sort-O gbk让 unzip 按 GBK 解码中文文件名如果解压出来全是乱码去掉这个参数再试。find . -type f | sort能列出项目文件清单先检查index.html、css、js是否齐全。接下来在 HBuilder 里配置最小运行环境。用 HBuilder 打开项目操作是 文件 打开目录选中解压后的文件夹然后在index.html上右键选 运行 浏览器运行。HBuilder 内置服务器会给出一个http://localhost地址比直接双击 index.html 更接近真实部署环境。不建议用file://打开因为部分浏览器对本地模块和 JSON 请求有跨域限制答辩时地址栏带文件路径也不好看。标准的扫雷课设包至少要有这几个文件路径作用index.html页面骨架棋盘容器、难度按钮、剩余雷数、计时器css/style.css格子尺寸与颜色、数字配色、插旗状态js/game.js雷区生成、揭开、递归、胜负判断不操作 DOMjs/ui.jsDOM 查询、事件绑定、计时器刷新、渲染棋盘如果项目只有一个 index.htmlCSS 和 JS 全部堆在里面也不是不能跑但老师让你改面板尺寸时改起来会非常吃力。拆分文件的目的是把计算和渲染分成两层后面加难度切换时不用重写逻辑。2.2 扫雷游戏的三个核心模块界面渲染、状态矩阵、计时与计数扫雷的核心状态是一张二维矩阵每个格子是一个 javascript 对象字段至少要有mine是否有雷、revealed是否揭开、flagged是否插旗、adjacentMines周边雷数。界面渲染只负责把这个矩阵画到页面上const createBoard (rows, cols) Array.from({ length: rows }, () Array.from({ length: cols }, () ({ mine: false, revealed: false, flagged: false, adjacentMines: 0 })) );这里用两层Array.from第一层生成行第二层生成列。新手常犯的错误是用Array(cols).fill({})这样每行每列都引用同一个 javascript 对象改一个格子所有格子一起变排错时非常难发现。ES6 的Array.from配箭头函数能保证每个格子都是独立的新对象。很多同学喜欢在渲染函数里直接读 DOM class 来判断格子是否被揭开比如cell.classList.contains(revealed)。这样做小项目能跑但状态散落在 DOM 里计时器、判赢、递归都要去读 DOM改起来牵一发动全身。建议把所有状态收在board矩阵里DOM 只是显示层。2.3 计时与剩余雷数用 javascript 函数控制 setInterval课设评分表里通常有一条“计时功能”很多人用setInterval(() document.getElementById(timer).textContent seconds, 1000)草草实现。问题在于点第二次开始按钮时旧定时器没有清秒数会加速页面关不掉定时器。let timerId null; let seconds 0; function startTimer() { if (timerId ! null) return; timerId setInterval(() { seconds 1; document.getElementById(timer).textContent seconds; if (seconds 999) stopTimer(); }, 1000); } function stopTimer() { clearInterval(timerId); timerId null; }clearInterval接受的是setInterval返回的 id所以timerId必须保存在函数外面。上方的if (timerId ! null) return是去重防止重复计时。剩余雷数同理用一个flagCount与总雷数做差值插旗时加一取消插旗时减一。注意把计时函数放进Minesweeper类时setInterval回调里的this会指向 window。回调要么用箭头函数要么用const self this保存实例否则会报Cannot read properties of undefined一类的错误。3. 扫雷核心算法随机布雷、数字计算与递归展开的实现界面的皮好画真正会被老师追问的是这三段布雷、算数字、展开。下面给出能直接抄进课设项目的写法每段都带参数说明。3.1 用随机洗牌代替双重随机避免布雷函数取到重复坐标最直观的布雷写法是 while 循环里用Math.random生成行列再判断是否已有雷。这种做法在小棋盘上勉强能用但中级、高级难度下越到后面碰撞越多循环次数不可控。常见做法是一次性生成所有合法坐标用 Fisher-Yates 洗牌取前mines个function initMines(board, rows, cols, mines, firstRow, firstCol) { const cells []; for (let r 0; r rows; r) { for (let c 0; c cols; c) { if (r firstRow c firstCol) continue; // 首格安全 cells.push([r, c]); } } for (let i cells.length - 1; i 0; i--) { const j Math.floor(Math.random() * (i 1)); [cells[i], cells[j]] [cells[j], cells[i]]; // 解构赋值交换 } for (let k 0; k mines; k) { const [r, c] cells[k]; board[r][c].mine true; } }firstRow和firstCol是玩家第一次点击的坐标生成雷之前先把它排除这是扫雷的默认规则首格永远不是雷。解构赋值交换数组元素是 ES6 写法答辩时能说出这一步通常会被认为基础扎实。3.2 计算周边雷数边界数组与 javascript 对象字段的配合布雷之后要遍历每个非雷格统计周围 8 个方向里有几颗雷。边界判断最省的写法是定义DIRS数组用两重循环统一处理而不是写八个 ifconst DIRS [-1, 0, 1]; function calcAdjacentMines(board, rows, cols) { for (let r 0; r rows; r) { for (let c 0; c cols; c) { if (board[r][c].mine) continue; let count 0; for (const dr of DIRS) { for (const dc of DIRS) { if (dr 0 dc 0) continue; const nr r dr; const nc c dc; if (nr 0 nr rows nc 0 nc cols board[nr][nc].mine) { count; } } } board[r][c].adjacentMines count; } } }这里依赖的是每个格子都是独立 javascript 对象adjacentMines写到对象上UI 层随时可以读取。最容易写错的是把DIRS写成[0, 1, -1]然后循环里漏掉跳过(0,0)导致把格子自己也算进去。3.3 左键揭开与右键插旗javascript 监听事件绑定的完整写法事件绑定放到 UI 层游戏逻辑放在 game 对象里。左键用click右键用contextmenu必须在回调里调用preventDefault()否则浏览器会弹出右键菜单function bindCellClick(cell, row, col, game) { cell.addEventListener(click, () game.reveal(row, col)); cell.addEventListener(contextmenu, (e) { e.preventDefault(); game.toggleFlag(row, col); }); }cell是渲染后的格子 DOMgame是游戏实例。箭头函数里的this不绑定到当前元素所以直接访问game。揭开操作的核心判断顺序是已揭开、已插旗、游戏结束三个条件先短路再判断是不是雷reveal(row, col) { const cell this.board[row][col]; if (cell.revealed || cell.flagged || this.gameOver) return; cell.revealed true; this.revealedCount; if (cell.mine) { this.gameOver true; this.ui.showLose(); return; } if (cell.adjacentMines 0) { for (const dr of DIRS) { for (const dc of DIRS) { const nr row dr; const nc col dc; if (nr 0 nr this.rows nc 0 nc this.cols) { this.reveal(nr, nc); } } } } this.ui.renderCell(row, col); this.checkWin(); }递归展开的终止条件不是“遇到数字就停”而是靠最前面的cell.revealed判断。一个格子被揭开后再次进入函数会被直接 return不会出现两个格子互相递归的无限循环。adjacentMines 0只对空白格扩散数字格揭开后显示数字即可。3.4 插旗方法toggleFlag 与剩余雷数刷新插旗只需要改flagged字段然后刷新格子的样式和页面上方的剩余雷数toggleFlag(row, col) { const cell this.board[row][col]; if (cell.revealed || this.gameOver) return; cell.flagged !cell.flagged; this.flagCount cell.flagged ? 1 : -1; this.ui.renderCell(row, col); this.ui.updateMineCounter(this.mines - this.flagCount); }这里用绝对值更新计数而不是每次都重新遍历棋盘。如果老师要求连续点击插旗和取消插旗这个写法不会丢计数。4. 课设答辩要过关判赢、计时停止、难度切换与 ES6 重构游戏已经能玩下面处理答辩时一定会被问的三件事怎么判赢、为什么代码没有一堆全局函数、难度参数怎么来的。4.1 用 ES6 的 class 把 javascript 基础语法组织成可答辩的代码前面的函数都能跑但把board、gameOver、revealedCount全放在全局作用域刷新页面时很容易残留状态。常见做法是封装成一个Minesweeper类构造函数接收行、列、雷数class Minesweeper { constructor(rows, cols, mines) { this.rows rows; this.cols cols; this.mines mines; this.board createBoard(rows, cols); this.gameOver false; this.revealedCount 0; this.flagCount 0; this.timerId null; } }类方法之间互相调用要用this.reveal、this.toggleFlag这些方法在类里已经收敛。外部只需要new Minesweeper(9, 9, 10)和game.reveal(row, col)两个入口。用 class 组织代码不代表一定高级但能把散落各处的状态收拢答辩时解释对象生命周期更清楚。给事件回调绑方法时要注意一点game.reveal直接传给addEventListener方法里的this会变成 DOM 元素。前面 3.3 的bindCellClick已经用箭头函数包了一层所以没有踩这个坑。如果以后想省掉绑定函数可以直接在类里写this.reveal this.reveal.bind(this);这也是课程设计里常见的“加分写法”。4.2 判赢条件揭开数量 插旗数量谁说了算很多人用flagCount mines判赢这是错的。玩家可以把旗子插在错误位置插满 10 面旗仍然没有揭开所有安全格。正确判据是已揭开的安全格数量等于总格数减雷数。checkWin() { const safeCells this.rows * this.cols - this.mines; if (this.revealedCount ! safeCells) return false; this.gameOver true; this.stopTimer(); this.ui.showWin(); return true; }revealedCount只在reveal里增加不会因取消插旗而变化。在reveal方法的踩雷分支里先执行gameOver true再调showLose保证踩雷的优先级高于判赢因为一次点击只能触发一个分支。4.3 三级难度参数表初级、中级、高级的经典配置课设通常要求做难度切换经典参数沿用 Windows 扫雷的配置难度行数 rows列数 cols雷数 mines总格数初级 easy991081中级 medium161640256高级 hard163099480注意中级和高级的行列顺序16 行 30 列表示横向长条和扫雷传统布局一致。如果在代码里写反格子会变成窄高条答辩时容易被指出来。const DIFFICULTY { easy: { rows: 9, cols: 9, mines: 10 }, medium: { rows: 16, cols: 16, mines: 40 }, hard: { rows: 16, cols: 30, mines: 99 } }; function resetGame(level) { const s DIFFICULTY[level]; game new Minesweeper(s.rows, s.cols, s.mines); ui.resetBoard(s.rows, s.cols); }切换难度时先停掉旧定时器再把面板 DOM 清空重建避免老格子的事件监听残留。5. 打包成 zip 前的检查清单运行时报错、资源路径与压缩包完整性代码改完最后一步是把整个项目打成「毕设课设基于javascript的扫雷游戏.zip」。这一步看起来简单实际翻车率不低。5.1 在浏览器控制台定位 javascript 运行时报错HBuilder 运行后按 F12 打开开发者工具Console 面板会直接显示 javascript 运行时报错。最常见的两条Cannot read properties of null说明脚本执行时找不到对应 id 的 DOM 元素game is not defined说明脚本加载顺序不对。要控制顺序在 HTML 里给 script 标签加deferscript srcjs/game.js defer/script script srcjs/ui.js defer/scriptdefer让脚本在 DOM 解析完之后执行比把 script 放在 body 底部更稳。如果报错信息带行列号点击右侧链接就能跳到对应 js 文件不用在代码里乱加 console.log。5.2 检查相对路径、中文文件名与 zip 压缩包完整性检查所有src和href是否以相对路径开始不要写/js/game.js这种开头带斜杠的绝对路径。某些本地服务器和在线课堂平台会把项目挂到子目录绝对路径会直接 404。另外压缩包解压时如果报invalid zip archive: could not find eocd通常是下载过程不完整或者文件被文本编辑器打开过先把源项目重新压一份再用unzip -t校验。5.3 一个有用的收尾技巧压缩前删除隐藏文件夹macOS 上的 zip 会带上__MACOSX隐藏目录和.DS_StoreWindows 有时也会多出Thumbs.db。这些文件不影响浏览器运行但传到课设系统后可能让老师看到一堆无关文件。压缩前执行一次清理find . -name __MACOSX -type d -prune -exec rm -rf {} find . -name .DS_Store -delete zip -r ../基于javascript的扫雷游戏.zip index.html css js -x *.DS_Store这段命令先把隐藏目录删掉再只把index.html、css、js三个资源放进 zip报告文件单独放 zip 外层目录。最后用unzip -t ../基于javascript的扫雷游戏.zip验证压缩包里的文件数量和结构确认无误再提交。本文还有配套的精品资源点击获取