js扫雷代码
我花了三个晚上重写扫雷,扫雷不是代码因为闲,是扫雷因为市面上的教程代码都太“教科书”了。它们把逻辑拆成十几个函数,代码每个函数三行注释,扫雷读起来像在翻阅字典。代码可扫雷这东西,扫雷核心就两件事:数格子周围有几颗雷,代码以及点开空格时那一片连锁反应。扫雷其他全是代码包装。
先别急着写UI。扫雷你得在内存里建一个二维数组,代码比如 board[row][col],扫雷每个格子存一个对象,代码里面塞 mine: boolean、扫雷revealed: boolean、adjacentCount: number。雷是随机埋的,但第一次点击不能炸——这规则很多新手会漏。我见过有人用递归重新生成棋盘,结果递归太深,浏览器直接卡死。正确做法是:先随机埋雷,再在第一次点击后,如果踩中雷,就把这颗雷挪到另一个非雷格子上。O(1)搞定,别整花活。
数周围雷数这段,我写的时候差点摔键盘。八个方向,每个方向都要检查边界。你写 if (row > 0 && board[row-1][col].mine) count++,复制粘贴八遍,看着就想吐。后来我改成用两个数组存偏移量:
const dirs = [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]]; for (let [dr, dc] of dirs) { let nr = r + dr, nc = c + dc; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && board[nr][nc].mine) count++; } 清爽多了。代码这东西,有时候就是换个写法的事,跟算法没关系,纯属手感。
最让人头疼的是展开空白区域。你点一个空格,它周围如果也没雷,得继续往外扩,像水波一样。有人用递归,但棋盘大、空格多时,递归层数深了会爆栈。我试过用栈模拟:
function flood(row, col) { let stack = [[row, col]]; while (stack.length) { let [r, c] = stack.pop(); if (board[r][c].revealed) continue; board[r][c].revealed = true; if (board[r][c].adjacentCount === 0) { for (let [dr, dc] of dirs) { let nr = r + dr, nc = c + dc; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !board[nr][nc].revealed && !board[nr][nc].mine) { stack.push([nr, nc]); } } } } } 迭代比递归稳,而且debug的时候你能看到栈里还剩什么,心里有底。
右键插旗子这个交互,我一开始没做。后来发现,不插旗子你根本没法玩——你记不住哪些格子你猜过雷。旗子逻辑简单:right-click 事件里切换 flagged 状态,同时更新一个计数。但有个细节:旗子插错位置,游戏结束时要显示“打叉”的雷,这个状态得单独存,不然UI会乱。
UI渲染我建议用canvas而不是DOM。原因很实际:一个30×16的棋盘有480个格子,每个格子一个div,重绘时浏览器要处理480次布局计算,慢得明显。canvas画格子,一次 fillRect 搞定,重绘整盘也就几毫秒。代价是事件处理你得自己算坐标——click 事件里 e.offsetX / cellSize 取整就行,不复杂。
最后说个我踩过的坑:计时器。扫雷有计时功能,但用户可能在中途切换标签页,或者点开格子后立刻刷新。我用 setInterval 每秒加1,但没处理浏览器后台时定时器被节流的问题。后来改成记录开始时间 Date.now(),每次渲染时算差值,彻底解决。别依赖定时器累加,时间这种东西,永远拿当前时间减开始时间,最准。
代码写完那天晚上,我拿自己写的扫雷玩了半小时,赢了三次,输了一次——因为中间接了个电话,回来忘了之前推理到哪。这游戏最难的从来不是代码,是记忆和逻辑的配合。但代码写对了,至少你不会因为程序bug输掉。
