欢迎来到feenan的藏宝洞!. 这里是我的第一个宝贝哦.有眼光的宝子们看到就是赚到哈哈。

关于扫雷很多人都不会陌生,但是不一定大家都会玩。我在读六年级的时候,家里才有了电脑,我自然也就早早的接触到了作为装机游戏的扫雷,那个时候年纪小逻辑性也差,对于扫雷实则是开了九窍,一窍不通。但是当时非常痴迷另一款游戏:血战上海滩,哈哈感觉快跑题了。

去年学习了前端之后突然产生了我也要写一个扫雷游戏的冲动,由于当时没有学习react,vue等等框架,所以就直接使用了原生的JS。因此,前端新手绝对可以看懂此文哒!

接下来,让我为大家揭秘一下原生扫雷的核心逻辑吧!全部代码会在文末奉上,有需要的直接下拉到文末。

一.编程基础

实现基础:JavaScript,css,html。

简单介绍该游戏:运行代码后,会直接生成初级难度的游戏实例,此时页面上会有一个由10 * 10个小方格组成的一个大的正方格,其中每一个格子支持右键和左键,右键插旗表示用户认为该处有雷,左键则直接展示一个数字,该数字表示周围的雷数,也可能左键点击直接点击到雷那么游戏结束。
原生扫雷.png

二.网络扫雷游戏功能===本游戏实现功能

1)左键点击后显示数字(游戏继续)或者所有雷(游戏失败);

2)右键插旗;

3)遇0打通;

4)重新开始;

5)更改难度

三.主要的功能实现逻辑(也是遇到的难点)

1.雷与数字如何与处于行列中的Dom元素匹配?

一开始的思路:生成Dom元素,不管是雷(mine)还是数字(number)直接把类型绑定在Dom的身上,点击的时候再去照周围有几个雷。但是会有不方便的地方就是,当去判断周围有几个雷,或者是0的时候需要打通的情况都要去遍历周围8个格子的DOM对象,而且如果游戏失败或者成功就要遍历所有的DOM对象。

实际上使用的:DOM元素身上设置一个属性存储它再行列中的坐标,另设一个矩阵去按行列坐标存储这个dom对应的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//创建DOM映射矩阵
this.squares=[]; //存储所有单元格的信息,它是一个二维数组
for(var i=0;i<this.tr;i++){
this.squares[i]=[];
for(var j=0;j<this.td;j++){
//this.squares[i][j]=;
//n++;

//取一个方块在数组里的数据要使用行与列的形式去取。找方块周围的方块的时候要用坐标的形式去取。行与列的形式跟坐标的形式x,y是刚好相反的
if(rn.indexOf(n++)!=-1){
//如果这个条件成立,说明现在循环到的这个索引在雷的数组里找到了,那就表示这个索引对应的是个雷
this.squares[i][j]={type:'mine',x:j,y:i};

}else{
//value 一开始都是0;
this.squares[i][j]={type:'number',x:j,y:i,value:0};
}
}
}

2.如何保证雷的位置随机?

思路:随机生成雷的位置(tr*td范围内),然后在生成映射矩阵的时候把雷给到对应的位置。

1
2
3
4
5
6
7
8
9
//生成n个不重复的数字 === 雷的位置
function randomNum(){
var square=new Array(this.tr*this.td); //生成一个空数组,但是有长度,长度为格子的总数
for(var i=0;i<square.length;i++){
square[i]=i;
}
square.sort(function(){return 0.5-Math.random()});
return square.slice(0,this.mineNum);
}

3.如何做切换的?

通过点击事件里的js来切换类名以及innerHtml来控制的。

左键:对于单元格通过映射拿到type,如果是数字单元格使用value值切换类名;如果是雷,就通过两层遍历(外层tr,里层rd)通过对映射矩阵里的每一项进行判断进而显示所有的雷;如果是0,那就需要进一步的处理(走打通逻辑);

右键:当游戏还没有结束这个单元格无旗也没有显示数字或者为空的时候右键就插旗了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//当前点击的方块
var curSquare=this.squares[clickedDom.pos[0]][clickedDom.pos[1]];
var cl=['zero','one','two','three','four','five','six','seven','eigth'];

if(curSquare.type=='number'){
//用户点到的是数字
//console.log('你点到数字了!')
clickedDom.innerHTML=curSquare.value;
clickedDom.className=cl[curSquare.value];
}

if(curSquare.type=='mine'){
// 用户颠倒的是雷
for(var i=0;i<this.tr;i++){
for(var j=0;j<this.td;j++){
if(this.squares[i][j].type=='mine'){
this.tds[i][j].className='mine';
}
}
}
}

4.如何计算雷周围的数字,如何找到周围的八个格子?

进入思维误区:拿到每个格子,然后去找周围的八个格子看有多少雷,立足点放在了格子上;而格子比雷多,必然有很多空格子,每个格子与相邻的格子都有交叉的部分,这就致使很多不必要的循环。

解题思路:拿到每个雷,然后找格子更新数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function updateNum(){
for(var i=0;i<this.tr;i++){
for(var j=0;j<this.td;j++){
//只更新的是雷周围的数字。
if(this.squares[i][j].type=='number'){
continue;
}
var num=getAround(this.squares[i][j]); //获取到每一个雷周围格子的坐标(不一定是八个,存在边界情况)

for(var k=0;k<num.length;k++){
/* num[i] == [0, 1]
num[i][0] == 0
num[i][1] == 1 */

this.squares[num[k][0]][num[k][1]].value+=1;
}
}
}

function getAround(square){
var x=square.x;
var y=square.y;
var result=[]; //把找到的格子的坐标返回出去(二维数组)

/*
x-1,y-1 x,y-1 x+1,y-1
x-1,y x,y x+1,y
x-1,y+1 x,y+1 x+1,y+1

*/

//通过坐标去循环九宫格
for(var i=x-1;i<=x+1;i++){
for(var j=y-1;j<=y+1;j++){
if(
i<0 || //格子超出了左边的范围
j<0 || //格子超出了上边的范围
i>this.td-1 || //格子超出了右边的范围
j>this.tr-1 || //格子超出了下边的范围
(i==x && j==y) || //当前循环到的格子是自己
this.squares[j][i].type=='mine' //周围的格子是个雷
){
continue;
}

result.push([j,i]); //要以行与列的形式返回出去。因为到时候需要用它去取数组里的数据
}
}}}

5.如何打通?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
if(curSquare.value==0){
/*
用户点到了数字0
1、显示自己
2、找四周
1、显示四周(如果四周的值不为0,那就显示到这里,不需要再找了)
2、如果值为0
1、显示自己
2、找四周(如果四周的值不为0,那就显示到这里,不需要再找了)
1、显示自己
2、找四周(如果四周的值不为0,那就显示到这里,不需要再找了)
*/

obj.innerHTML=''; //如果数字为0的话,就不显示
getAllZero(curSquare);
}
function getAllZero(square){
var around=This.getAround(square); //找到了周围的n个格子

for(var i=0;i<around.length;i++){
//around[i]=[0,0]
var x=around[i][0]; //行
var y=around[i][1]; //列

This.tds[x][y].className=cl[This.squares[x][y].value];

if(This.squares[x][y].value==0){
//如果以某个格子为中心找到的格子值为0,那就需要接着调用函数(递归)
if(!This.tds[x][y].check){
//给对应的td添加一个属性,这条属性用于决定这个格子有没有被找过。如果找过的话,它的值就为true,下一次就不会再找了
This.tds[x][y].check=true;
getAllZero(This.squares[x][y]);
}
}else{
This.tds[x][y].innerHTML=This.squares[x][y].value;
}

}
}

四.总结

1.打游戏让人上头,写游戏也让人上头;

2.对于设计具有环环相扣的逻辑时一定要多思考,提前规划,否则就会因为某一环的缺陷导致后续地方都得重写。

3.思考环节:对于浏览器的工作原理的进一步了解对于某些特殊的bug能够提前规避,大家来猜一猜剩余雷数与的变更与弹框的出现究竟哪一个先哪一个后呢?下期揭晓答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(obj.className=='flag'){
this.mineNumDom.innerHTML=--this.surplusMine;
}else{
this.mineNumDom.innerHTML=++this.surplusMine;
}
if(this.surplusMine==0){
//剩余的雷的数量为0,表示用户已经标完小红旗了,这时候要判断游戏是成功还是结束
if(this.allRight){
//这个条件成立说明用户全部标对了
alert('恭喜你,游戏通过');
this.removeEvent();
}else{
alert('游戏失败');
this.gameOver();
}
}

More code info: feenan的第一个宝贝