引言:

扫雷是系统自带的经典小游戏,以前上学那会上机的时候就经常玩这个,趁着五一假期的最后一天,用canvas编写了这个小游戏,看着还行,不知道会不会有什么我没发现的bug,难度目前是设置成简易的,如果要改难度,代码稍做修改即可。

效果图

实现思路

1.创建9行9列的二维数组。

2.设置雷:随机行数 i 和列数 j,根据随机到 i、j 从二维数组中取出对应的元素,将取到的元素设置一个属性type等于1,表示当前元素已经是雷,并且递增雷计数器,然后递归调用;如果取到的元素已经是雷了,则跳过继续执行,雷计数器达到设定的最大值就跳出递归。

3.计算每个元素周围的雷数量(周围指的是 左上、上、右上、右、右下、下、左下、左 这8个位置),当前位置显示对应的数字(待会内容里面细说)

4.同样根据这个二维数组来创建遮罩的小方块,正好盖住之前创建的图形。

5.点击这个遮罩的小方块则触发揭开,揭开后根据对应的数字或者雷做不同的操作。

代码实现

创建背景及相关元素



//绘制背景及相关默认元素

Saolei.prototype.drawBG=function(){

var image,img,sx=0,sy=0,sWidth=141,sHeight=54,dx=20,dy=340,dWidth=141,dHeight=54;

//计时

image = this.imgObj['common'][15];

img = new _.ImageDraw({image:image,sx:sx,sy:sy,sWidth:sWidth,sHeight:sHeight, dx:dx, dy:dy ,dWidth:dWidth,dHeight:dHeight});

this.renderArr.push(img);



sx=0,sy=0,sWidth=141,sHeight=52,dx=180,dy=340,dWidth=141,dHeight=52;

//计雷

image = this.imgObj['common'][14];

img = new _.ImageDraw({image:image,sx:sx,sy:sy,sWidth:sWidth,sHeight:sHeight, dx:dx, dy:dy ,dWidth:dWidth,dHeight:dHeight});

this.renderArr.push(img);

//创建一个方形区域

var rect = new _.Rect({

x:24,

y:44,

width:289,

height:289,

stroke:true

})

this.renderArr.push(rect);



sx=0,sy=0,sWidth=100,sHeight=40,dx=120,dy=2,dWidth=100,dHeight=40;

//重新开始按钮

image = this.imgObj['common'][21];

img = new _.ImageDraw({image:image,sx:sx,sy:sy,sWidth:sWidth,sHeight:sHeight, dx:dx, dy:dy ,dWidth:dWidth,dHeight:dHeight});

this.renderArr.push(img);

this.reStartObj=img;

}
 

创建雷和显示对应的图片

1.随机row 和 col,并从二维数组中获取到这个对象;
2.判断他是否是雷,如果是则跳过当前;
3.如果当前不是雷,则标记当前对象为雷对象,并且更改图片;
4.递归,当达到设定的数量时跳出。



//创建被遮盖

Saolei.prototype.createUnder=function(){

var image,img,sx=0,sy=0,sWidth=79,sHeight=79,dx=0,dy=0,dWidth=32,dHeight=32;

var rows = this.rows;//行

var cols = this.cols;//列

image = this.imgObj['common'][9];

//二维网格数组

var gridArr=[];

var arr = this.gridArr,cell;

for(var i=0;i<rows;i++){//行

dy = 45+i*dHeight;

gridArr[i]=[];

for(var j=0;j<cols;j++){//列

dx = 25+j*dWidth;

img = new _.ImageDraw({image:image,sx:sx,sy:sy,sWidth:sWidth,sHeight:sHeight, dx:dx, dy:dy ,dWidth:dWidth,dHeight:dHeight});

img.type=0;

this.renderArr.push(img);



gridArr[i][j]=img;

}

}



this.gridArr=gridArr;



//创建雷

this.createLei();



}



//创建雷

Saolei.prototype.createLei=function(){

//当达到设定的数量时跳出

if(this.leiMaxCount<=0) {

return ;

}

var arr = this.gridArr;

/*

1.随机row 和 col,并从二维数组中获取到这个对象

2.判断他是否是雷,如果是则跳过当前

3.如果当前不是雷,则标记当前对象为雷对象,并且更改图片

4.递归,当达到设定的数量时跳出

*/

var row = _.getRandom(0,this.rows);

var col = _.getRandom(0,this.cols);

var cell = arr[row][col];

if(cell.type==0){

//标记为雷

cell.type=1;

cell.image = this.imgObj['common'][18];

this.leiMaxCount--;

console.log(row,col);

}

//递归

this.createLei();

}
 

计算周围雷的数量并显示

1.循环之前定义的二维数组

2.如果当前元素的下标是(i,j),则左上为(i-1,j-1),上为(i-1,j ),右上为(i-1,j+1),以此类推,如下图所示:

3.分别取出这些元素,并判断他们是不是雷,如果是则计数累加,最后将计数对应到相应的图片,然后显示出来。



//计算周边雷的数量并更改对象的相关参数

Saolei.prototype.computedLei=function(){

var arr = this.gridArr,cell;

for(var i=0;i<arr.length;i++){//行

for(var j=0;j<arr[i].length;j++){//列

cell = arr[i][j];

if(cell.type==1){//当前是雷则直接跳过

continue;

}

var count=0;



//左上

var ci = i-1,cj = j-1,ccell;

if(ci>=0 && cj>=0){

ccell = arr[ci][cj];

if(ccell.type==1){

count++;

}

}

//上

ci = i-1,cj = j,ccell;

if(ci>=0 && cj>=0){

ccell = arr[ci][cj];

if(ccell.type==1){

count++;

}

}

//右上

ci = i-1,cj = j+1,ccell;

if(ci>=0 && cj<this.cols){

ccell = arr[ci][cj];

if(ccell.type==1){

count++;

}

}

//右

ci = i,cj = j+1,ccell;

if(cj<this.cols){

ccell = arr[ci][cj];

if(ccell.type==1){

count++;

}

}

//右下

ci = i+1,cj = j+1,ccell;

if(ci<this.rows && cj<this.cols){

ccell = arr[ci][cj];

if(ccell.type==1){

count++;

}

}

//下

ci = i+1,cj = j,ccell;

if(ci<this.rows){

ccell = arr[ci][cj];

if(ccell.type==1){

count++;

}

}

//左下

ci = i+1,cj = j-1,ccell;

if(ci<this.rows && cj >=0){

ccell = arr[ci][cj];

if(ccell.type==1){

count++;

}

}

//左

ci = i,cj = j-1,ccell;

if(cj >= 0){

ccell = arr[ci][cj];

if(ccell.type==1){

count++;

}

}

//设定周围雷的数量

cell.count=count;



if(count==0){//因为0那张图片下标用的9

count=9;

}

//更换图片

cell.image = this.imgObj['common'][count];

}

}

}
 

创建遮罩



//创建遮盖

Saolei.prototype.createOver=function(){

var image,img,sx=0,sy=0,sWidth=79,sHeight=79,dx=0,dy=0,dWidth=32,dHeight=32;

image = this.imgObj['common'][10];

var arr = this.gridArr;

for(var i=0;i<arr.length;i++){//行

this.overArr[i]=[];

for(var j=0;j<arr[i].length;j++){//列

dy = 45+i*dHeight;

dx = 25+j*dWidth;

img = new _.ImageDraw({image:image,sx:sx,sy:sy,sWidth:sWidth,sHeight:sHeight, dx:dx, dy:dy ,dWidth:dWidth,dHeight:dHeight});

img.i=i,img.j=j;

this.renderArr.push(img);

this.overArr[i][j]=img;

}

}

}
 

创建计时和计数器



//创建计数和计时器

Saolei.prototype.createCount=function(){

//计时器

var x=115,y=382,content=0;

var text = new _.Text({

x:x,

y:y,

text:content,

font:'26px ans-serif',

textAlign:'center',

fill:true,

fillStyle:'white'

});

this.renderArr.push(text);

this.timeCountObj=text;



x=222,y=382,content=this.leiCount;

var text = new _.Text({

x:x,

y:y,

text:content,

font:'26px ans-serif',

textAlign:'center',

fill:true,

fillStyle:'white'

});

this.renderArr.push(text);

this.leiCountObj=text;

}
 

加入鼠标移动事件



//鼠标移动事件

Saolei.prototype.mouseMove=function(e){

if(this.endAnimate)return ;



var pos = _.getOffset(e);//获取鼠标位置

var isCatch=false;

if(this.reStartObj.isPoint(pos)){

this.el.style.cursor = 'pointer';//改为手状形态

}else{

this.el.style.cursor = '';//改为普通形态

}



if(this.end)return ;//结束了已经

if(!isCatch){

//循环遮罩数组

var arr = this.overArr,cell;

for(var i=0;i<arr.length;i++){//行

for(var j=0;j<arr[i].length;j++){//列

cell = arr[i][j];

if(cell.isPoint(pos)&& !cell.open){//鼠标捕捉,被打开的同样不捕获

if(!cell.state){//打上标记的不做处理

cell.image= this.imgObj['common'][11];

}

}else{

if(!cell.state){//打上标记的不做处理

cell.image= this.imgObj['common'][10];

}

}

}

}

this.render();

}

}
 

加入鼠标点击事件



//鼠标点击事件

Saolei.prototype.mouseClick=function(e){

if(this.endAnimate)return ;//结束动画的时候不许点击



var pos = _.getOffset(e);//获取鼠标位置

if(this.reStartObj.isPoint(pos)){//重新开始被点击

this.restart();

return ;

}

if(this.end)return ;//结束了已经



//循环遮罩数组

var arr = this.overArr,cell,cellArr=this.gridArr;

for(var i=0;i<arr.length;i++){//行

for(var j=0;j<arr[i].length;j++){//列

cell = arr[i][j];

if(cell.isPoint(pos) && !cell.open){//鼠标捕捉,被打开的同样不捕获

if(!this.start){

doStart.call(this);

}

//移出当前对象

cell.open=true;//被打开

this.clearAssign(this.renderArr,cell);

//获取对应的格子对象

var item = cellArr[i][j];

if(item.type==1){//如果是雷,则显示爆炸动画并提示失败

this.boom(item);

}else{//如不是雷

if(item.count==0){//判断周围雷数量,如果是0则打开周围,并依次递归

this.openOver(cell.i,cell.j);

}

//判断是否达到胜利的条件

this.successOrNot();

}

}

}

}



this.render();



function doStart(){

this.start=true;

this.timmer = setInterval(function(){

this.timmerCount++;

this.timeCountObj.text=this.timmerCount;

this.render();

}.bind(this),1000);

}

}
 

成功判定1

未打开的数量与雷的数量相同



//判断是否成功

Saolei.prototype.successOrNot=function(cell){

var arr = this.overArr,cell,count=0;

for(var i=0;i<arr.length;i++){//行

for(var j=0;j<arr[i].length;j++){//列

cell = arr[i][j];

if(!cell.open){

count++;

}

}

}

if(count==this.leiSucCount){//未打开的数量和雷的数量一样,表示成功

this.end=true;

clearInterval(this.timmer);//清除计时器的定时任务

//打开所有的雷

this.openAllLei();

//显示成功表情(延时)

setTimeout(this.endShow.bind(this,'suc'),100);

}

}
 

成功判定2

标记为雷(插旗)的数量与类总数相同



//判断是否成功 -根据插红旗

Saolei.prototype.successOrNot2=function(cell){

var arr = this.overArr,cell,count=0,gridArr = this.gridArr,item;

var count=0;

for(var i=0;i<arr.length;i++){//行

for(var j=0;j<arr[i].length;j++){//列

cell = arr[i][j];

if(cell.state==1){//红旗

item=gridArr[i][j];

if(item.type==1){//正好又是雷

count++;

}

}

}

}

if(count==this.leiSucCount){//未打开的数量和雷的数量一样,表示成功

this.end=true;

clearInterval(this.timmer);//清除计时器的定时任务

//打开所有的雷

this.openAllLei();

//显示成功表情(延时)

setTimeout(this.endShow.bind(this,'suc'),100);

}

}
 

触雷效果

鼠标点击雷后,会触发雷爆炸的一个动画,这是通过图片的切换来实现的



//爆炸效果

Saolei.prototype.boom=function(cell){

this.end=true;

this.endAnimate=true;

clearInterval(this.timmer);//清除计时器的定时任务

//开启爆炸的动画

this.timmer = setInterval(this.boomAnimate.bind(this,cell),100)

}

//爆炸动画

Saolei.prototype.boomAnimate=function(cell){

//切换图片

cell.index = (cell.index || 0)+1;

if(cell.index>this.boomCount){

//结束动画

clearInterval(this.timmer);

//因为图片有些不一样,需要修正一下

cell.sWidth=79;

cell.sHeight=79;

cell.image= this.imgObj['common'][18];

//打开所有的雷

this.openAllLei();

//显示失败表情(延时)

setTimeout(this.endShow.bind(this),100);

this.endAnimate=false;

this.render();

return ;

}

//因为图片有些不一样,需要修正一下

cell.sWidth=61;

cell.sHeight=53;

cell.image= this.imgObj['boom'][cell.index];

this.render();

}
 

加入鼠标右键事件

此事件是做插旗或者标记为未知等操作的。



//右键事件

Saolei.prototype.contextMenu=function(e){

if(this.end)return ;//结束了已经

var e = e||window.event;

//取消右键默认事件

e.preventDefault && e.preventDefault();



var pos = _.getOffset(e);//获取鼠标位置

//循环遮罩数组

var arr = this.overArr,cell,cellArr=this.gridArr;

for(var i=0;i<arr.length;i++){//行

for(var j=0;j<arr[i].length;j++){//列

cell = arr[i][j];

if(cell.isPoint(pos) && !cell.open){//鼠标捕捉,被打开的同样不捕获

//右键切换

if(!cell.state){//如果是没有状态的,则标记为雷,小旗

cell.state=1;

cell.image= this.imgObj['common'][12];

this.leiCount--;

this.leiCountObj.text=this.leiCount;

//判断如果小旗数量和数据都对上了,也判断为成功

this.successOrNot2(cell);

}else if(cell.state==1){//如果状态为雷的,标记为未知,问号

cell.state=2;

cell.image= this.imgObj['common'][13];

this.leiCount++;

this.leiCountObj.text=this.leiCount;

}else if(cell.state==2){//如果状态为未知的,则现在原来的

cell.state=0;

cell.image= this.imgObj['common'][10];

}

}

}

}

this.render();

}
 

最后加入重新开始事件,胜利和失败图片显示就完成了。

智一面web前端开发工程师(vue)