js实现扫雷
学习扫雷笔记,学习资料:渡一教育实现扫雷视频,基本上所有都是原生js的,部分bug不知为何就是用js就实现不了,然后无奈之下尝试了jQuery结果可以,如果解决了bug再改,用了jq的地方会标注即写上js写法,大致实现效果如下
先说不足,由于自己本身也没有解决,在实现点击雷后会将全部雷显示出来,并且alert一个lose,但是只有alert先确定结束后全部雷才会显示出来,之后会继续尝试改进。
HTML部分,因为是学习+练习就只写了基础功能,界面样式什么的后期会有所修改
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="stylesheet" href="css/index.css"> <script src="js/jquery.js"></script> <title>Document</title> </head> <body> <div id="content"> <!-- 顶端按钮 --> <div id="level"> <button class="active">初级</button> <button>中级</button> <button>高级</button> <button class="big">重新开始</button> </div> <!-- 棋盘 --> <div id="gameBox"> <!-- js动态添加的 --> </div> <!-- 底部文本 --> <div id="bottomText"> <div class="text">剩余雷数:<span class="mineNum"></sapn></div> </div> </div> <script src="js/index.js"></script> </body> </html>
三个部分,上部按钮,中部扫雷界面(‘棋盘’),下部剩余雷数text,中部‘棋盘’是用添加上去的
CSS部分,前面部分是对基础界面的样式修改,后面td.one-td.eight部分是给实现扫雷点击后出现的数字改颜色用的
注意 td的border-color,是用来实现‘棋盘’上宛如砖块一样的立体效果,设置上方和左侧为白色,其他地方为想要的颜色即可,
#content{ height: 900px; margin: 0 auto; width: 800px; } #level{ height: 25px; width: 280px; margin: 0 auto; } #level button{ height: 25px; line-height: 25px; width: 60px; text-align: center; background: rgb(73, 139, 141);; color: white; padding: 0px; margin: 0px; border: 0px; border-radius: 5px; outline: none; cursor: pointer; } #level button.active{ background: #0bb9b6; } #level button.big{ width: 80px; } #gameBox{ padding-top: 20px; } #bottomText{ height: 20px; width: 200px; margin: 0 auto; line-height: 20px; } /* .mineNum{ height: 20px; width: 50px; } */ table{ border-spacing: 1px; background-color: #929196; margin: 0 auto; } td{ height: 20px; width: 20px; background-color: #ccc; padding: 0; border: 1px solid; border-color: #fff #a1a1a1 #a1a1a1 #fff; text-align: center; line-height: 20px; font-weight: bold; } .mine{ background:#c9c9c9 url(../images/mine.png) no-repeat center; background-size: cover; } .flag{ background:#ccc url(../images/flag.png) no-repeat center; background-size: cover; } td.zero{ background-color: #d9d9d9; border-color: #d9d9d9; } td.one{ color: red; background-color: #d9d9d9; border-color: #d9d9d9; } td.two{ color: green; background-color: #d9d9d9; border-color: #d9d9d9; } td.three{ color: yellow; background-color: #d9d9d9; border-color: #d9d9d9; } td.four{ color: blue; background-color: #d9d9d9; border-color: #d9d9d9; } td.five{ color: blueviolet; background-color: #d9d9d9; border-color: #d9d9d9; } td.six{ color: brown; background-color: #d9d9d9; border-color: #d9d9d9; } td.seven{ color: cadetblue; background-color: #d9d9d9; border-color: #d9d9d9; } td.eight{ color: coral; background-color: #d9d9d9; border-color: #d9d9d9; }
JS部分!
首先使用的是面向对象思想,在部分地方会对写好的函数进行调用,函数是写在原型上的。
第一步先写了构造函数,写上了后期所需要用到的各种变量,用在后期生成‘棋盘’的时候。
//面向对象思想 function Mine(tr,td,mineNum){ this.tr=tr;//行数 this.td=td;//列数 this.mineNum=mineNum;//雷的总数 this.squares=[];//存储所有方块的信息,二维数组,以行列的信息存储 this.tds=[];//存储所有单元格的DOM对象(二维数组) this.surplusMine=mineNum;//剩余雷的数量 this.allRight=false;//玩家标注的小红旗是否全是雷,判断是否游戏成功 this.parent=document.getElementById('gameBox'); }
然后是createDom(),生成‘棋盘’,用的是table,td,tr,但是ul和li也是能够实现的
生成基础‘棋盘’后,写格子的点击事件调用了play函数,后面会讲到这个函数
Mine.prototype.createDom=function(){ var This=this; var table=document.createElement('table'); for(var i=0;i<this.tr;i++){ var domTr=document.createElement('tr');//通过构造函数传入的行数来生成tr this.tds[i]=[]; for(var j=0;j<this.td;j++){//列 var domTd=document.createElement('td');//同上 domTd.pos=[i,j];//存储对应的位置,行与列(后面会用到坐标,坐标和行与列的关系是x y恰好相反) domTd.onmousedown=function(){//当对应的格子被点击时 This.play(event,this);//This 实例对象 this 被点击的Td } this.tds[i][j]=domTd; domTr.appendChild(domTd); } table.appendChild(domTr); } this.parent.innerHTML=""; this.parent.appendChild(table); }
然后init函数
其中使用了一个random函数,用于随机生成雷的位置,用td*tr得到大于需要雷的数量的有序数,用排序+random得到随机数,通过mineNum的大小来确定随机数的数量,最后返回一个数组。
Mine.prototype.randomNum=function(){ var square=new Array(this.td*this.tr); 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); }
Mine.prototype.init=function(){ var rn=this.randomNum();//雷的位置 var n=0;//作为访问随机数字的位置的索引,即rn的索引 for(var i=0;i<this.tr;i++){ this.squares[i]=[]; for(var j=0;j<this.td;j++){ n++;//取方块在数组里的数组用行与列的方式去取,取方块周围对应的数字用坐标 if(rn.indexOf(n)!=-1){ this.squares[i][j]={type:'mine',x:j,y:i}; }else{ this.squares[i][j]={type:'number',x:j,y:i,value:0}; } } } this.update(); this.createDom(); this.parent.oncontextmenu=function(){ return false; } // this.mineNumDom=document.getElementsByClassName('mineNum'); // this.mineNumDom.innerHTML=this.surplusMine; $('.mineNum').html(this.surplusMine);//出bug,用原生无法显示,未解决,原生实现应该就是上面两行,但是也不知道哪里出的问题 }
上方init函数
定义一个n用于一会比较用,两个for循环用rn.indexOf(n)来判断rn即随机数有哪些,若存在随机数数组中,则未squares里对应的坐标位赋值type:'mine',否则则为number。
update()晚点再说,大致就是刷新。
this.parent.oncontextmenu是用于取消鼠标右键的点击事件,为一会给右键点击添加旗子🚩做准备。
最后一行用了jQuery,是往最底下的span里放对应的雷数。
然后是getAround()函数,由于在坐标上,一个雷的左上角是x-1,右下角是x+1,y同理,故两个for循环,调用这个函数可获得在某个雷附近的 除掉‘棋盘’四个角以外导致的误差值+自身+九宫格除掉自身以外八个格子中是雷的格子。
//找某个格子周围的八个格子 Mine.prototype.getAround=function(square){ var x=square.x; var y=square.y; var result=[];//把找到的格子(雷以外的)的坐标返回【二维数组】 //x坐标的值从x-1到x+1,y类似 //坐标循环九宫格(坐标) 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]);//返回行与列的形式 } } return result; }
然后在update中调用了getAround
//更新所有数字 Mine.prototype.update=function(){ 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=this.getAround(this.squares[i][j]);//获取雷周围的数组 for(var k=0;k<num.length;k++){ this.squares[num[k][0]][num[k][1]].value+=1; } } } }
先用了两个for循环,将不是雷的位置排除出去(跳出循环),然后再调用getAround函数,得到对应的这个雷旁边的坐标的数组,由于是二维数组,用num[k][0]来获取x坐标,y坐标同理,然后令对应位置上的value加1
然后是最开始的createDom里面调用过的play函数,由于这个函数比较长,将其分成两部分来说明(还是连在一起的)
play()第一部分
Mine.prototype.play=function(ev,obj){ var This=this; if(ev.which==1 && obj.className!='flag'){ //且在立了小红旗之后不能再点击左键,只能右键取消了之后才可再次点击左键 var curSquare=this.squares[obj.pos[0]][obj.pos[1]]; var cl=['zero','one','two','three','four','five','six','seven','eight']; if(curSquare.type=='number'){ obj.innerHTML=curSquare.value; obj.className=cl[curSquare.value]; //处理零的情况 if(curSquare.value==0){ obj.innerHTML=''; function getAllZero(square){ var around=This.getAround(square); for(var i=0;i<around.length;i++){ var x=around[i][0];//行 var y=around[i][1];//列 This.tds[x][y].className=cl[This.squares[x][y].value]; //递归 1.显示自身 2.找四周 然后重复 if(This.squares[x][y].value==0){ //给遍历过的格子加属性降低资源浪费 if(!This.tds[x][y].check){ This.tds[x][y].check=true; getAllZero(This.squares[x][y]); } }else{ //某个格子为中心查找到的值不为零,则显示 This.tds[x][y].innerHTML=This.squares[x][y].value; } } } getAllZero(curSquare); } }else{ this.gameOver(obj); }}
最开始先将函数作用域里的this提出来
第一个if,两个判断:1.鼠标左键被点击 2.被点击的这个格子没有小红旗(这个属性可有可无,根据个人需要来加,加上了就是在有小红旗的时候必须先右键取消掉小红旗才能再次左键点击有效)
写个cl给添加css属性用
在判断这个格子是数字不是雷后,就将它的文本设置为value值,加上对应的css属性,倘若是零的话,用一个函数来处理,getAllZero()中先获取值为零的格子附近的八个格子,将对应的css属性加上后,判断value值是不是零,若是用递归,原因:查找周围的规律如下
找到了则显示自身,然后继续找四周还有没有,然后找到了然后显示自身,然后继续找...一直重复,直到找到的格子value不是零,才将它的innerHTML值显示出来
其中有一个小问题就是,对于x这个格子来说,x-1周围的格子里有它,x+1的格子里也有它,就会造成重复查询,资源浪费,所以就给查询过的格子加属性,降低资源浪费。
然后调用getAllZero()
最后else点击到的如果不是数字(即雷)就游戏结束调用gameOver函数(一会说)
play()第二部分
if(ev.which==3){ //如果右击的是数字,忽略 if(obj.className&&obj.className!='flag'){ return; } obj.className=obj.className=='flag'?'':'flag';//切换小旗 if(this.squares[obj.pos[0]][obj.pos[1]].type=='mine'){ this.allRight=true; }else{ this.allRight=false; } //小旗的添加和减少 if(obj.className=='flag'){ $('.mineNum').html(--this.surplusMine); }else{ $('.mineNum').html(++this.surplusMine); } if(this.surplusMine==0){ if(this.allRight){ alert('win'); }else{ alert('lose'); this.gameOver; } } } }
这一部分处理的是鼠标右键时的情况,如果右击的时已经左击点出来了的数字,就忽略掉。
然后用三目运算符,对不是数字的格子进行有无小旗的切换。
在最开始构造函数的地方定义了一个allRight属性,然后右键处理的地方,若是插上小旗,且所有在规定数量以内插上小旗的地方type都是mine则allRight为true,否则为false,用来在游戏最后判断小旗是否推断出了全部雷的地方是准确的。
然后if-else控制小旗数量在span里的显示和surplusMine属性
最后用surplusMine来判断是否游戏即将结束,全对弹出win,否则调用gameOver函数。
//定义游戏失败 Mine.prototype.gameOver=function(clickTd){ //1.显示所有的雷 //2.取消所有格子点击事件 //3.给点中的格子标红 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'; } this.tds[i][j].onmousedown=null; } } if(clickTd){ clickTd.style.backgroundColor="white"; } alert('lose'); }
在判断游戏是结束时需做上方代码注释里的三件事。
最后就是处理调节游戏难度,动态生成不同的‘棋盘’
//button功能实现 var btns=document.querySelectorAll('#level button'); var mine=null;//存储生成的实例 var ln=0;//处理当前选中的状态 var arr=[[9,9,10],[16,16,40],[28,28,99]];//行数列数雷数的二维数组 for(let i=0;i<btns.length-1;i++){ btns[i].onclick=function(){ btns[ln].className=""; this.className="active"; mine=new Mine(...arr[i]);//扩展运算符... 直接放arr[i]取不出来。要写成arr[i][0],arr[i][1] mine.init(); ln=i; } } btns[0].onclick();//初始化 btns[3].onclick=function(){ mine.init(); }
将构造函数需要的数据写成二维数组,在点击对应按钮时生成‘棋盘’,并取消掉上一个‘棋盘’的显示(classname)
两个注意点:1. ...是拓展运算符
2. for循环里用的是let不是var,为了ln=i是onclick里的i
令最开始打开的时候不是空的,写上代码点击第一个按钮
然后最后的onclick是处理重新开始按钮的
大致就是这样,仍有许多细节需要修改,若其中存在问题,还请大佬多多指教