原生 JS 实现扫雷 (分析+代码实现)
阅读这篇文章需要掌握的基础知识:html5、css、javascript
在线demo:查看
扫雷规则
在写扫雷之前,我们先了解下它的游戏规则
● 扫雷是一个矩阵,地雷随机分布在方格上。
● 方格上的数字代表着这个方格所在的九宫格内有多少个地雷。
● 方格上的旗帜为玩家所作标记。
● 踩到地雷,游戏失败。
● 打开所有非雷方格,游戏胜利。
功能实现思路分析
矩阵的生成
- 矩阵的生成有多种方式可以实现,我们这里使用<table>+<span>标签。
- 通过 js 给定行数与列数在<table>的 innerhtml 写入<span>标签来动态生成矩阵。
方格的打开与标记
- 通过 onmousedown 事件,传入点击的方格的坐标及event,判断event为左键还是右键。
- 左键打开方格,右键标记方格。
地雷的随机分布
- 由于第一次打开的方格不能为地雷所以我们把生成地雷的函数放在第一次点击方格时。
- 我们通过循环用 math.random() 函数来随机生成地雷的二维坐标。
- 判断坐标是否不为第一次点击方格的坐标以及没有雷存在。
- 是则将方格设置为地雷,当前地雷数+1,并且将九宫格内的方格的计雷数+1。
- 否则跳过进入下个循环,直到地雷的数量达到设定的最大雷数,结束循环。
踩到地雷游戏结束
- 打开方格为地雷时,提示游戏结束。
- 通过遍历矩阵来打开所有地雷
连锁打开方格
- 当打开的方格为计雷数为0的方格,自动打开九宫格内的非雷方格。
- 如果打开的非雷方格九宫格内仍有非雷方格,继续打开九宫格内的非雷方格,直到没有为止。
游戏胜利条件
- 当所有非雷方格被打开即为游戏胜利。
- 在每次打开方格函数中都遍历一遍矩阵,当找到有未打开的非雷方格时则结束遍历。
- 当遍历完未找到未打开的非雷方格则提示游戏胜利。
剩余地雷数与计时器
- 地雷的总数减去玩家标记的方格数即为剩余地雷数
- 计时器可以用setinterval()函数实现
代码实现
生成矩阵
我们先在<body>里写一个<table>标签,设定个 id='grid'
<table id='grid'></table>
然后在<script>里 定义两个变量 row--行数 col--列数
通过两个for循环把 (方格)<span> 写入到 (矩阵)<table> 里,通过<td><tr>标签控制行列。
var row = 10; //行数 var col = 10; //列数 //生成矩阵html <tr>--行标签 <td>--列标签 let gridhtml = ''; for (let i = 0; i < row; i++) { gridhtml += '<tr>' for (let j = 0; j < col; j++) { gridhtml += '<td><span class="blocks"></span></td>'; } gridhtml += '<tr>' } //写入html document.getelementbyid('grid').innerhtml = gridhtml;
写一下矩阵和方格的css样式。
#grid { margin: auto; /* 让矩阵居中显示于页面 */ } .blocks { width: 30px; height: 30px; line-height: 30px; display: block; /* 让span以block方式显示 */ text-align: center; border: solid 1px #000; user-select: none; /* 设置不可拖拽选中 */ cursor: pointer; /* 设置鼠标停留样式 */ } .blocks:hover { background: #0af; /* 鼠标停留时背景颜色变化 */ }
至此打开页面,矩阵就初步显示出来了。
把矩阵的方格放入二维数组中
我们先定义一个全局变量grid。
把刚才写的生成矩阵的代码写成一个函数 function init_grid()
document.getelementsbyclassname('blocks') 返回的是一个一维数组,我们把它通过两个for循环转化为二维数组。
给每个方格定义一个属性 count 计雷数 --- blocks[i].count = 0;
然后把返回值赋值给grid --- grid = init_grid();
var row = 10; //行数 var col = 10; //列数 var grid = init_grid(); //初始化矩阵 (row-行数 col-列数) function init_grid() { //生成矩阵html <tr>--行标签 <td>--列标签 let gridhtml = ''; for (let i = 0; i < row; i++) { gridhtml += '<tr>' for (let j = 0; j < col; j++) { gridhtml += '<td><span class="blocks"></span></td>'; } gridhtml += '<tr>' } //写入html document.getelementbyid('grid').innerhtml = gridhtml; //返回矩阵二维数组 let blocks = document.getelementsbyclassname('blocks'); let grid = new array(); for (let i = 0; i < blocks.length; i++) { if (i % col === 0) { grid.push(new array()); } //初始化计雷数 blocks[i].count = 0; grid[parseint(i / col)].push(blocks[i]); } return grid; }
写完了这段我们先写一段代码测试下grid有没有赋值成功,遍历grid把方格的值改为对应的坐标。
for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { grid[i][j].innerhtml = i + ',' + j; } }
可以看到 grid 已经赋值成功!没成功的回去检查下代码。(tip:测试完记得把测试代码删除)
方格的点击事件
定义一个函数 function block_click( _i, _j, e) 的大致框架
e为传入的鼠标事件,e.button ( 0为左键,2为右键 )。
isopen属性为自定义属性,用来判断方格是否打开。
//方格点击事件 _i:坐标i _j:坐标j e:鼠标事件 function block_click(_i, _j, e) { //跳过已打开的方格 if (grid[_i][_j].isopen) { return; } //鼠标左键打开方格 if (e.button === 0) { } //鼠标右键标记方格 else if (e.button === 2) { } }
然后修改下之前写在 init_grid 函数里的<span>的属性,绑定 onmousedown 事件,传入 i,j 坐标,和鼠标事件 event
gridhtml += '<td><span class="blocks" onmousedown="block_click(' + i + ',' + j + ',event)"></span></td>';
修改下body的属性 加入防拖拽生成新页面和屏蔽右键菜单。
<!-- ondragstart:防拖拽生成新页面 oncontextmenu:屏蔽右键菜单--> <body ondragstart='return false' oncontextmenu='self.event.returnvalue=false'>
我们在鼠标左键事件里面写下测试代码,当左键方格时显示它的坐标。
//鼠标左键打开方格 if (e.button === 0) { grid[_i][_j].innerhtml = _i + ',' + _j; }
效果如下,没成功的回去检查下代码。(tip:测试完记得把测试代码删除)
方格的标记
在鼠标右键事件写标记代码,这里用 ▲ 来作为标记。
右击一次添加标记,再次右击删除标记。
//鼠标右键标记方格 else if (e.button === 2) { let block = grid[_i][_j]; if (block.innerhtml !== '▲') { block.innerhtml = '▲'; } else { block.innerhtml = ''; } }
效果如下:
随机生成地雷
由于第一次打开的方格不能为地雷所以我们把生成地雷的函数放在第一次点击方格时。
先定义全局变量 maxcount --- 最大地雷数 isfirstopen --- 是否第一次打开方格。
var row = 10; //行数 var col = 10; //列数 var grid = init_grid(); var maxcount = 10; //最大地雷数量 var isfirstopen = true; //第一次打开方格
在鼠标左键事件里面写第一次打开方格生成地雷的代码的大致框架。
//鼠标左键打开方格 if (e.button === 0) { //第一次打开 if (isfirstopen) { isfirstopen = false; let count = 0; //当前地雷数 //生成地雷 while (count < maxcount) { //........ } } }
完善生成地雷代码:
生成随机坐标 ri,rj,判断该坐标不等于第一次点击方格的坐标以及该坐标表方格不为地雷。
条件成立,将坐标对应方格的 ismine 设置为true,当前地雷数+1,并使九宫格内非雷方格的计雷数 count +1
自定义属性ismine代表方格为地雷。
自定义属性count为计雷数。
当地雷数大于最大地雷数,结束循环。
//生成地雷 while (count < maxcount) { //生成随机坐标 let ri = math.floor(math.random() * row); let rj = math.floor(math.random() * col); //坐标不等于第一次点击方格的坐标 && 非雷方格 if (!(ri === _i && rj === _j) && !grid[ri][rj].ismine) { grid[ri][rj].ismine = true; //自定义属性ismine代表方格为地雷 count++; //当前地雷数+1 //更新九宫格内非雷方格的计雷数 for (let i = ri - 1; i < ri + 2; i++) { for (let j = rj - 1; j < rj + 2; j++) { //判断坐标防越界 if (i > -1 && j > -1 && i < row && j < col) { //计雷数+1 grid[i][j].count++; } } } } }
写个测试代码在生成地雷后显示所有方格的状态。(tip:测试完记得把测试代码删除)
for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { //判断方格是否为雷 if (grid[i][j].ismine) { //显示为雷 grid[i][j].innerhtml = '雷'; } else { //否则显示计雷数 grid[i][j].innerhtml = grid[i][j].count; } } }
效果如下:可以看到已经随机生成了雷,计雷数也正确显示了。
方格的打开事件
在生成地雷的代码下,加入方格打开代码函数 block_open(_i,_j) 的大致框架。
定义 function op(block) 函数设定打开方格的状态与样式。
判定打开的方格的类型
block.ismine 为打开地雷方格 --> 游戏结束
block.count === 0 为打开计雷数为0的方格 --> 连锁打开非雷方格
else 为打开计雷数大于0的方格 --> 显示方格计雷数
//鼠标左键打开方格 if (e.button === 0) { //第一次打开 if (isfirstopen) { //....... } //执行打开方格函数 block_open(_i, _j); //打开方格函数 function block_open(_i, _j) { let block = grid[_i][_j]; op(block); //设定打开方格的状态与样式 function op(block) { block.isopen = true; //isopen为自定义属性,设置为true代表已打开 block.style.background = '#ccc'; //将背景设置为灰色 block.style.cursor = 'default'; //将鼠标停留样式设置为默认 } if (block.ismine) { //踩雷 } else if (block.count === 0) { //打开计雷数为0的方格 } else { //打开计雷数不为0的方格 } } }
打开非雷方格显示计雷数
我们先把最简单的显示方格计雷数搞定。
else { //打开计雷数不为0的方格 block.innerhtml = block.count; //显示计雷数 }
效果如下:
踩雷游戏结束
接下来写踩雷代码,当打开的方格为雷时,将其显示为'雷',并打开所有的地雷,提示游戏结束。
if (block.ismine) { //踩雷 block.innerhtml = '雷'; //显示为 '雷' //遍历矩阵打开所有的地雷方格 for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { //找到地雷 block = grid[i][j]; if (!block.isopen && block.ismine) { op(block); //设置打开状态和样式 block.innerhtml = '雷'; //显示为 '雷' } } } //提示游戏结束 alert("游戏结束"); }
效果如下:
连锁打开方格
打开的方格为计雷数为0的方格,自动打开九宫格内的非雷方格,循环递归到没有为止。
计雷数为0就没必要让innerhtml显示0了,保持空白就行。
else if (block.count === 0) { //打开计雷数为0的方格 //遍历九宫格内的方格 for (let i = _i - 1; i < _i + 2; i++) { for (let j = _j - 1; j < _j + 2; j++) { //判断是否越界&&跳过已打开的方格&&非雷 if (i > -1 && j > -1 && i < row && j < col && !grid[i][j].isopen && !grid[i][j].ismine) { //递归打开方格函数 block_open(i, j); } } } }
效果如下:
游戏胜利条件
扫雷大体框架已经出来了,我们现在做胜利条件的判定。
在方格点击函数最后写判断代码。
//方块点击事件 _i:坐标i _j:坐标j e:鼠标事件 function block_click(_i, _j, e) { //跳过已打开的方块 if (grid[_i][_j].isopen) { //... } //鼠标左键打开方块 if (e.button === 0) { //... } //鼠标右键标记方块 else if (e.button === 2) { //... } //遍历矩阵 let iswin = true; for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) {
let block = grid[i][j];
//判断游戏胜利条件(所有的非雷方格已打开) if (!block.ismine && !block.isopen) { //如果有未打开的非雷方块 条件不成立 iswin = false; } } } if (iswin) { alert("游戏胜利"); } }
效果如下:(还专门玩了一遍^ ^)
游戏部分到这里就完成了!
剩余地雷数与计时器
最后,我们做一下剩余地雷数和计时器的显示。
我们写个 <div> 在 <table> 的上面,放两个 <span> 来做显示框,<label> 用来给 js 计数。
<div id='bar'> <span class='bar'>剩余雷数:<label id='count'>0</label></span> <span class='bar'>计时:<label id='time'>0</label>s</span> </div> <table id='grid'></table>
再写下css样式:
#bar { text-align: center; margin-bottom: 20px; } .bar { height: 25px; width: 150px; line-height: 25px; display: inline-block; border: solid 1px #000; margin-left: 20px; margin-right: 20px; }
效果如下:
在 js 中定义两个全局变量拿到 <lable> count 和 time
然后让地雷数量等于最大地雷数,设置个100ms定时器,每次+0.1s,保留一位小数。
var count = document.getelementbyid('count'); //剩余地雷数 count.innerhtml = maxcount; //初始化剩余雷数 var time = document.getelementbyid('time'); //计时器 var timer = setinterval(function () { let seconds = (parsefloat(time.innerhtml) + 0.1).tofixed(1); //保留一位小数 time.innerhtml = seconds; }, 100) //定时器 100ms执行一次
我们修改下方格点击事件中遍历矩阵的代码,更新剩余地雷数,胜利时结束计时。
//遍历矩阵 let iswin = true; count.innerhtml = maxcount; //重置剩余地雷数 for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { let block = grid[i][j]; //找到标记 if (block.innerhtml === '▲') { count.innerhtml = parseint(count.innerhtml) - 1; //剩余地雷数-1 } //判断游戏胜利条件(所有的非雷方格已打开) if (!block.ismine && !block.isopen) { //如果有未打开的非雷方块 条件不成立 iswin = false; } } } if (iswin) { clearinterval(timer); //游戏胜利结束计时,清除定时器 alert("游戏胜利"); }
再修改踩雷的代码,结束计时。
if (block.ismine) { //踩雷 block.innerhtml = '雷'; //显示为 '雷' //遍历矩阵打开所有的地雷方格 for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { //找到地雷 block = grid[i][j]; if (!block.isopen && block.ismine) { op(block); //设置打开状态和样式 block.innerhtml = '雷'; //显示为 '雷' } } } clearinterval(timer); //游戏结束停止计时,清除定时器 //提示游戏结束 alert("游戏结束"); }
ok,大功告成!!!后续还可以加入选择难度的功能,重新开始按钮,动画效果等等,这个就看你们发挥了!!
完整代码
<!doctype html> <html> <head> <title>扫雷</title> <style> #bar { text-align: center; margin-bottom:20px; } .bar { height: 25px; width: 150px; line-height: 25px; display: inline-block; border: solid 1px #000; margin-left: 20px; margin-right: 20px; } #grid { margin: auto; } .blocks { width: 30px; height: 30px; line-height: 30px; display: block; text-align: center; border: solid 1px #000; user-select: none; cursor: pointer; } .blocks:hover { background: #0af; } </style> </head> <!-- ondragstart:防拖拽生成新页面 oncontextmenu:屏蔽右键菜单--> <body ondragstart='return false' oncontextmenu='self.event.returnvalue=false'> <div id='bar'> <span class='bar'>剩余雷数:<label id='count'>0</label></span> <span class='bar'>计时:<label id='time'>0</label>s</span> </div> <table id='grid'></table> <script> var row = 10; //行数 var col = 10; //列数 var maxcount = 10; //最大地雷数量 var isfirstopen = true; //第一次打开方格 var grid = init_grid(); //初始化 var count = document.getelementbyid('count'); //剩余雷数 var time = document.getelementbyid('time'); //计时 //初始化矩阵 (row-行数 col-列数) function init_grid() { //生成矩阵html <tr>--行标签 <td>--列标签 let gridhtml = ''; for (let i = 0; i < row; i++) { gridhtml += '<tr>' for (let j = 0; j < col; j++) { gridhtml += '<td><span class="blocks" onmousedown="block_click(' + i + ',' + j + ',event)"></span></td>'; } gridhtml += '<tr>' } //写入html document.getelementbyid('grid').innerhtml = gridhtml; //返回矩阵二维数组 let blocks = document.getelementsbyclassname('blocks'); let grid = new array(); for (let i = 0; i < blocks.length; i++) { if (i % col === 0) { grid.push(new array()); } //初始化计雷数 blocks[i].count = 0; grid[parseint(i / col)].push(blocks[i]); } return grid; } //方格点击事件 _i:坐标i _j:坐标j e:鼠标事件 function block_click(_i, _j, e) { //跳过已打开的方格 if (grid[_i][_j].isopen) { return; } //鼠标左键打开方格 if (e.button === 0) { //第一次打开 if (isfirstopen) { isfirstopen = false; let count = 0; //当前地雷数 //生成地雷 while (count < maxcount) { //生成随机坐标 let ri = math.floor(math.random() * row); let rj = math.floor(math.random() * col); //坐标不等于第一次点击方格的坐标 && 非雷方格 if (!(ri === _i && rj === _j) && !grid[ri][rj].ismine) { grid[ri][rj].ismine = true; //自定义属性ismine代表方格为地雷 count++; //当前地雷数+1 //更新九宫格内非雷方格的计雷数 for (let i = ri - 1; i < ri + 2; i++) { for (let j = rj - 1; j < rj + 2; j++) { //判断坐标防越界 if (i > -1 && j > -1 && i < row && j < col) { //计雷数+1 grid[i][j].count++; } } } } } } //执行打开方格函数 block_open(_i, _j); //打开方格函数 function block_open(_i, _j) { let block = grid[_i][_j]; op(block); //设定打开方格的状态与样式 function op(block) { block.isopen = true; //isopen为自定义属性,设置为true代表已打开 block.style.background = '#ccc'; //将背景设置为灰色 block.style.cursor = 'default'; //将鼠标停留样式设置为默认 } if (block.ismine) { //踩雷 block.innerhtml = '雷'; //显示为 '雷' //遍历矩阵打开所有的地雷方格 for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { //找到地雷 block = grid[i][j]; if (!block.isopen && block.ismine) { op(block); //设置打开状态和样式 block.innerhtml = '雷'; //显示为 '雷' } } } //提示游戏结束 alert("游戏结束"); } else if (block.count === 0) { //打开计雷数为0的方格 //遍历九宫格内的方格 for (let i = _i - 1; i < _i + 2; i++) { for (let j = _j - 1; j < _j + 2; j++) { //判断是否越界&&跳过已打开的方格&&非雷 if (i > -1 && j > -1 && i < row && j < col && !grid[i][j].isopen && !grid[i][j].ismine) { //递归打开方格函数 block_open(i, j); } } } } else { //打开计雷数不为0的方格 block.innerhtml = block.count; //显示计雷数 } } } //鼠标右键标记方格 else if (e.button === 2) { let block = grid[_i][_j]; if (block.innerhtml !== '▲') { block.innerhtml = '▲'; } else { block.innerhtml = ''; } } //判断游戏是否结束(所有的非雷方格已打开) for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { if (!grid[i][j].ismine && !grid[i][j].isopen) { return; } } } alert("游戏胜利"); } </script> </body> </html>