利用js+canvas实现扫雷游戏

 更新时间:2022年06月08日 11:43:44   作者:白.恩  
这篇文章主要为大家详细介绍了利用js+canvas实现扫雷游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

本文实例为大家分享了用js+canvas实现扫雷游戏的具体代码,供大家参考,具体内容如下

记录js学习后制作的第一关小游戏。

这里的代码还不够精简,许多地方偷懒没有封装,逻辑也有许多可以优化。

<body>
 
    胜利条件,找出所有地雷并标记
    <form action="javaScript:createContent()">
        <div id="message" style="color: red; display: none;">地雷数量必须小于地图大小xy的平方</div>
        <br /> 
        地图大小xy :<input id="xyNum" type="number" required="true" name="points" min="1" max="50"  /> 
        booNum:<input id="booNum" type="number" required="true" name="points" min="1" max="2500"/>
        <input type="submit" value="OK" : />
        <br /> 1. 输入宽度 <br />2. 输入地雷数(地雷数小于宽*宽) <br /> 3. 单击确定  <br />
        鼠标右键:<br />
        第一次:标记您的猜测<br />
        第二次: 取消标签<br />
    </form>
    <div id= 'game'>
 
    </div>
    <script src="./js/MarkObs.js"></script>
    <script src="./js/Isboo.js"></script>
    <script src="./js/lei.js"></script>
    <script>
    let xy = document.getElementById('xyNum');
    let boo = document.getElementById('booNum');
    let meg = document.getElementById("message");
    let div = document.getElementById('game');
 
    //获取输入的宽高和地雷数
    createContent = function (){
            // console.log(xy.value);
            // console.log(boo.value);
            let xyNum = xy.value; 
            let booNum = boo.value; 
            // console.log(Math.pow(xyNum,2));
            
            //判断输入是否合法
            if(Math.pow(xyNum,2)<boo.value){
                meg.style.display = 'block';
            }
            else {//绘制地图
                div.innerHTML = '';//清除上次div里的地图
                let game = new Game('game',xyNum,booNum);
            }
        }
    </script>
</body>

lei.js

/* 一个自定义原型数组方法  可以放到html里
二维数组查找
arr:要找数组第一第二项 找到返回下标,没有返回-1
PS:只要this数组和arr数组第一第二项的值相等,即为找到。
*/
Array.prototype.myindexOf = function(arr){
    for(let i=0;i<this.length;i++){
        
        if((this[i][0] == arr[0]) &&(this[i][1]==arr[1])){
            return i;
        }
    }
    return -1;
}
/*
初始化地雷图
id:传入绘制地图的容器id
xyNum:长||宽的格子数(地图固定正方形)
booNum:地雷数
*/ 
class Game {
    constructor(id,xyNum,booNum){
        this.xyNum = xyNum;
        this.booNum = booNum;
        this.id = id;
 
        this.booArrs = [];//保存雷的位置
        this.boox = -1;//地雷在x轴第几个块
        this.booy = -1;//地雷在x轴第几个块
 
        this.numArrs = [];//保存提醒数字的位置以及数字
        this.num = 0;//保存找到的提醒数字的个数
 
        this.markArrs = [];//保存标记位置的数组
 
        //单个块的宽高
        this.divw = 20;
 
        // 初始化画布
        this.initCanvas(xyNum);
        // 初始化地雷位置(地雷用-1代替,图片绘制麻烦)
        this.initBooxy(xyNum,booNum);
        // 初始化遮挡物
        this.initObs(xyNum);
 
        //判断是否胜利
        this.win();
    
 
    }
    /*初始化画布(包括网格)
    xyNum:传入需要绘制的宽格子数
    */ 
    initCanvas(xyNum){
        this.canvas = document.createElement('canvas');
        this.ctx = this.canvas.getContext('2d');
        
        //1为border
        this.canvas.width = (this.divw+1)*xyNum;
        this.canvas.height = (this.divw+1)*xyNum;
 
        // 绘制网格坐标
                // 获取canvas的宽高;
                let w = this.canvas.width;
                let h = this.canvas.height;
                // 绘制水平线
                for (let i = 1; i < h / 21; i++) {
                    this.ctx.beginPath();
                    this.ctx.lineTo(0,  21 * i) //起点
                    this.ctx.lineTo(w, 21 * i); //重点
                    this.ctx.stroke();
        
                }
                // h绘制垂直线
                for (let i = 1; i < w / 21; i++) {
                    this.ctx.beginPath();
                    this.ctx.lineTo(21 * i,0) //起点
                    this.ctx.lineTo(21 * i,h); //重点
                    this.ctx.stroke();
        
                }
                // ctx.stroke();
 
        // 放入容器
        this.div = document.getElementById(this.id);
        this.div.appendChild(this.canvas);
        
        // 绑定点击事件!!!
        this.canvas.addEventListener('mousedown',this.mouseDown.bind(this))//!!!!注意需要更改this指向,用bind
        
        // 清除鼠标右键的默认事件 “contextmenu“
        this.canvas.addEventListener("contextmenu",function(event){
            event.preventDefault()
        })
    }
 
    /*初始化地雷(包括提醒数字)
    xyNum:传入地图的宽的格子数
    booNum:传入地雷数
    */ 
    initBooxy (xyNum,booNum){
 
        // 随机地雷位置 并保存起来
        for(let i=0;i<booNum;i++){
 
            // x,y为地雷所在格子坐标,从0开始
            this.boox = parseInt(Math.random()*xyNum);
            this.booy = parseInt(Math.random()*xyNum);
 
            //避免雷的位置重复
            while(this.booArrs.myindexOf([this.boox,this.booy])!=-1){
                this.boox = parseInt(Math.random()*xyNum);
                this.booy = parseInt(Math.random()*xyNum);
            }
 
            this.booArrs.push([this.boox,this.booy])//!!!保存地雷的位置
            console.log(i,'x:'+this.boox,'y:'+this.booy);
 
            //绘制地雷
            this.ctx.beginPath();//不清楚可不可以删
            this.ctx.rect(this.boox*21,this.booy*21,20,20);
            this.ctx.fillStyle = 'red';
            this.ctx.fill();
        }
 
        // 绘制地雷位置周围提醒数字
            // 这里的逻辑可以优化,不提前绘制数字,在点击清除障碍物后再判断绘制。  
 
        /*
            想法一:在每个雷周围添加数字1,如果在多个雷交集处累加
            想法二:所有块依次判断周围是否有雷,有几个雷,就fillText()多少
            想法三:(一二结合)先找每个雷,该雷周围的8个块依次 判断周围有几个雷
        */ 
            // 这里为法二
       for(let i=0;i<xyNum;i++){
           for(let j=0;j<xyNum;j++){
               let num = 0;//提醒数字 ,每次重置为0
            
                if(this.booArrs.myindexOf([i-1,j-1]) !=-1){
                    num++;
                }
                if(this.booArrs.myindexOf([i-1,j]) !=-1){
                    num++;
                }
                if(this.booArrs.myindexOf([i-1,j+1]) !=-1){
                    num++;
                }
                if(this.booArrs.myindexOf([i,j-1]) !=-1){
                    num++;
                }
                if(this.booArrs.myindexOf([i,j+1]) !=-1){
                    num++;
                }
                if(this.booArrs.myindexOf([i+1,j-1]) !=-1){
                    num++;
                }
                if(this.booArrs.myindexOf([i+1,j]) !=-1){
                    num++;
                }
                if(this.booArrs.myindexOf([i+1,j+1]) !=-1){
                    num++;
                }
               
                //绘制提醒数字  
                if(num!=0 && (this.booArrs.myindexOf([i,j]) ==-1 )){//(this.booArrs.myindexOf([i,j]) ==-1)地雷不标注提示数字若。要标注需要+1(本身)
 
                this.ctx.font = '18px fasdg'
                this.ctx.fillStyle = '#000'
                this.ctx.fillText(num,i*(this.divw+1)+2,(j+1)*(this.divw+1)-2);//加1和j+1为测试结果,-+2是为了文本在格子里居中//y为文本中线坐标
                
                this.numArrs.push([i,j,num]);//i,j为提醒数字的块坐标,num为装数组里的值(myindexOf来判断)
                }
                // this.NUM = num;
           }
       }
        
 
        
    }
 
    /*初始化遮挡物
     xyNum:传入地图的宽的格子数
    */ 
   initObs(xyNum){
    for(let i=0;i<xyNum;i++){
        for(let j=0;j<xyNum;j++){
 
            this.ctx.beginPath();
            this.ctx.rect(i*21,j*21,20,20);
            // this.ctx.fillStyle = 'rgb(155,25,205,0.7)';//设置障碍物透明度可以方便查看雷的位置
            this.ctx.fillStyle = 'rgb(155,25,205,1)';//正常游戏时透明度为'1‘
            this.ctx.fill();
        }
    }
   }
 
 
/*点击事件在initCanvas中绑定*/  
   mouseDown(){
 
    //这里使用preventDefault,默认事件被没有消除,是因为触发鼠标右键的默认事件的事件类型不是mousedown,是contextmenu
    // event.preventDefault(); //ie9以下不兼容 
    
    this.clix = Math.floor(event.layerX/( this.divw+1));//this.divw为20是块的宽
    this.cliy = Math.floor(event.layerY/( this.divw+1)); 
 
    // 鼠标左键
    if(event.button==0){
        this.clearObs(this.clix,this.cliy);
 
 
    }
    
    // 鼠标右键
    else if(event.button==2){
        
        
        this.markObs(this.clix,this.cliy);
    }
   
   }
 
 
   /*扫雷*/  //这里的代码可以封装一下 为了方便此处没有封装
   clearObs(x,y){
    // console.log(x,y);点击坐标
 
    this.ctx.clearRect(x*21,y*21,20,20);//清除指定块
    
    // 点击到标记,点击到提醒数字,点击到地雷,点击到空白,
    if(this.markArrs.myindexOf([x,y])!=-1){  //点击到标记,重新覆盖
        this.ctx.rect(x*21,y*21,20,20);
        this.ctx.fillStyle = 'rgb(155,25,205,1)';
        this.ctx.fill();
        
        this.ctx.beginPath();
        this.ctx.fillStyle = 'red';
        this.ctx.fillText('?',x*(this.divw+1)+2,(y+1)*(this.divw+1)-2);
        this.ctx.fill();
 
    }
    else if(this.numArrs.myindexOf([x,y])!=-1){//点击到提醒数字
        let index = this.numArrs.myindexOf([x,y]);//下标
        let num = this.numArrs[index][2];//提醒数字
        this.ctx.fillText(num,x*(this.divw+1)+2,(y+1)*(this.divw+1)-2);//加1和j+1为测试结果,-+2是为了文本在格子里居中//y为文本中线坐标
        this.num++;
    }
    else if(this.booArrs.myindexOf([x,y])!=-1){//,点击到地雷,全部绘制
        console.log(this.booArrs.myindexOf([x,y]));
            //绘制全图
            // 绘制提醒数字
            for(let i=0;i<this.xyNum;i++){
                for(let j=0;j<this.xyNum;j++){
                    let num = 0;//提醒数字 ,每次重置为0
                     // if(booArrs.indexof([i-1,j-1]) != -1){//数组是对象这样永远-1
                     this.ctx.clearRect(i*21,j*21,20,20);
                     if(this.booArrs.myindexOf([i-1,j-1]) !=-1){
                         num++;
                     }
                     if(this.booArrs.myindexOf([i-1,j]) !=-1){
                         num++;
                     }
                     if(this.booArrs.myindexOf([i-1,j+1]) !=-1){
                         num++;
                     }
                     if(this.booArrs.myindexOf([i,j-1]) !=-1){
                         num++;
                     }
                     if(this.booArrs.myindexOf([i,j+1]) !=-1){
                         num++;
                     }
                     if(this.booArrs.myindexOf([i+1,j-1]) !=-1){
                         num++;
                     }
                     if(this.booArrs.myindexOf([i+1,j]) !=-1){
                         num++;
                     }
                     if(this.booArrs.myindexOf([i+1,j+1]) !=-1){
                         num++;
                     }
                     
     
                    
                     //绘制提醒数字
                     if(num!=0 && (this.booArrs.myindexOf([i,j]) ==-1 )){//(this.booArrs.myindexOf([i,j]) ==-1)地雷不标注提示数字若要标注需要+1(本身)
     
                     this.ctx.font = '18px fasdg'
                     this.ctx.fillStyle = '#000'
                     this.ctx.fillText(num,i*(this.divw+1)+2,(j+1)*(this.divw+1)-2);//加1和j+1为测试结果,-+2是为了文本在格子里居中//y为文本中线坐标
                     
                     this.numArrs.push([i,j,num]);//i,j为提醒数字的块坐标,num为装数组里的值(myindexOf来判断)
                     }
                     // this.NUM = num;
                }
            }
            // 绘制地雷
            for(let i=0;i<this.booArrs.length;i++){
                this.ctx.fillStyle = 'red';
                this.ctx.rect(this.booArrs[i][0]*21,this.booArrs[i][1]*21,20,20);
                this.ctx.fill(); 
            }
            this.ctx.clearRect((this.xyNum-1)*21,(this.xyNum-1)*21,20,20);//每次最后一个都会变红,不知道原因,此处专门删除。
           
            alert('你惊动了雷雷');
 
            
    }
 
    else {
 
        this.isboo(this.ctx,x,y,this.booArrs,this.numArrs,this.markArrs,this.xyNum);
 
 
    } 
   }
 
   win (){//标记数组==地雷数组
    this.tim = setInterval(()=>{
        if(this.booArrs.length ==this.markArrs.length){
            for(let i=0;i<this.booNum;i++){
                
                if( true == this.booArrs.some(()=>{
                    return this.markArrs.myindexOf(this.booArrs[i])!=-1;
                })){
                   this.booNum--;
                }
                if(this.booNum==0){
                    clearInterval(this.tim);
                    alert('you are win');
                    }
            }
        }
    },10)
 
   }
   isboo(ctx,x,y,booArrs,numArrs,markArrs,xyNum){
       new Isboo(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
   }
 
 
    /*标记 
    */
   markObs(x,y){  
       console.log(x,y);
    new MarkObs(this.ctx,x,y,this.booArrs,this.divw,this.markArrs);
    
 
   }
 
   
}

isboo.js

Array.prototype.myindexOf = function(arr){
    for(let i=0;i<this.length;i++){
        
        if((this[i][0] == arr[0]) &&(this[i][1]==arr[1])){
            return i;
        }
    }
    return -1;
}
/*
这里解决点击到空白格子时,把周围的空白格一起显示。此处的逻辑可以再优化.
ctx:布局
x,点击位置
y,点击位置
booArrs:炸弹的位置数组
numArrs:提示数的位置
markArrs:标记的位置
*/ 
class Isboo {
    constructor(ctx,x,y,booArrs,numArrs,markArrs,xyNum){
        this.x = x;
        this.y = y;
        
        // 判断有没有提醒数字
        this.isbool(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
        this.isboor(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
        this.isboot(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
        this.isboob(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
    }
    isbool(ctx,x,y,booArrs,numArrs,markArrs,xyNum){
        if((numArrs.myindexOf([x,y])==-1)&&(x<xyNum)&&(markArrs.myindexOf([x,y])==-1)){
            ctx.clearRect(x*21,y*21,20,20);
            x+=1;
            this.isbool(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
            // this.isboor(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
            this.isboot(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
            this.isboob(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
        }else {
            return ;
        }
    }
    isboor(ctx,x,y,booArrs,numArrs,markArrs,xyNum){
        if((numArrs.myindexOf([x,y])==-1)&&(x>=0)&&(markArrs.myindexOf([x,y])==-1)){
            ctx.clearRect(x*21,y*21,20,20);
            x-=1;
            // this.isbool(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
            this.isboor(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
            this.isboot(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
            this.isboob(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
        }else {
            return ;
        }
    }
    isboot(ctx,x,y,booArrs,numArrs,markArrs,xyNum){
        if((numArrs.myindexOf([x,y])==-1)&&(y<xyNum)&&(markArrs.myindexOf([x,y])==-1)){
            ctx.clearRect(x*21,y*21,20,20);
            y+=1;
            // this.isbool(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
            // this.isboor(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
            this.isboot(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
            // this.isboob(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
 
        }else {
            return ;
        }
    }
    isboob(ctx,x,y,booArrs,numArrs,markArrs,xyNum){
        if((numArrs.myindexOf([x,y])==-1)&&(y>=0)&&(markArrs.myindexOf([x,y])==-1)){
            ctx.clearRect(x*21,y*21,20,20);
            y-=1;
            // this.isbool(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
            // this.isboor(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
            // this.isboot(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
            this.isboob(ctx,x,y,booArrs,numArrs,markArrs,xyNum);
        }else {
            return ;
        }
    }
 
}

MarkObs.js

Array.prototype.myindexOf = function(arr){
    for(let i=0;i<this.length;i++){
        
        if((this[i][0] == arr[0]) &&(this[i][1]==arr[1])){
            return i;
        }
    }
    return -1;
}
/*
ctx:布局
x,点击位置
y,点击位置
booArrs:炸弹的位置数组
divw:各自宽度
markarrs:标记数组
*/ 
class MarkObs{
    constructor(ctx,x,y,booArrs,divw,markarrs){
        this.markObs(ctx,x,y,booArrs,divw,markarrs);
    }
 
    markObs(ctx,x,y,booArrs,divw,markarrs){
 
        if(markarrs.myindexOf([x,y])==-1){//如果标记数组里没有该地址,则标记,并添加进数组
        ctx.beginPath();
        ctx.fillStyle = 'red';
        ctx.fillText('?',x*(divw+1)+2,(y+1)*(divw+1)-2);
        markarrs.push([x,y]);
        }else {//如果标记数组里有该地址,则取消标记,并从数组中删除
            ctx.clearRect(x*(divw+1),y*(divw+1),divw,divw);
            ctx.beginPath();
            ctx.rect(x*21,y*21,20,20);
            ctx.fillStyle = 'rgb(155,25,205,1)';
            ctx.fill();
            markarrs.splice((markarrs.myindexOf([x,y])),1);
        }
    }
 
}

页面效果

初始化障碍物设置了透明度时

正常游戏时

这里点击右键标记后忘了把填充颜色设置回来。所以后面变红。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • JavaScript 2018 中即将迎来的新功能

    JavaScript 2018 中即将迎来的新功能

    JavaScript 2018 中即将迎来的新功能:异步生成器及更好的正则表达式。更多常规正则表达式功能大家跟随小编一起通过本文学习吧
    2018-09-09
  • JS数组的常用10种方法详解

    JS数组的常用10种方法详解

    这篇文章主要介绍了JS数组的常用10种方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-05-05
  • 详解小程序开发经验:多页面数据同步

    详解小程序开发经验:多页面数据同步

    这篇文章主要介绍了小程序开发经验:多页面数据同步,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-05-05
  • js获取多个tagname的节点数组

    js获取多个tagname的节点数组

    写了个获取多个tagname节点集合的小方法。类似于jQuery的$(‘iput,select,textarea’,'#form’)的效果,返回是按节点在原有文档流中的顺序返回的
    2013-09-09
  • javascript入门之window对象【新手必看】

    javascript入门之window对象【新手必看】

    本文系统介绍了javascript的window对象以及一些控制函数的用法,仅供大家参考
    2016-11-11
  • js原生瀑布流插件制作

    js原生瀑布流插件制作

    这篇文章主要为大家详细介绍了js原生瀑布流插件制作,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-10-10
  • bootstrap弹出层的多种触发方式

    bootstrap弹出层的多种触发方式

    这篇文章主要为大家详细介绍了bootstrap弹出层的多种触发方式,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-05-05
  • js两种拼接字符串的简单方法(必看)

    js两种拼接字符串的简单方法(必看)

    下面小编就为大家带来一篇js两种拼接字符串的简单方法(必看)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-09-09
  • Javascript remove 自定义数组删除方法

    Javascript remove 自定义数组删除方法

    Javascript自定义数组删除方法remove(),需要的朋友可以参考下。
    2009-10-10
  • SOSO地图API使用(一)在地图上画圆实现思路与代码

    SOSO地图API使用(一)在地图上画圆实现思路与代码

    最近在做SOSO地图相关开发,遇到相关画圆知识,特此简单记录下来,接下来讲解如何在在地图上画圆,感兴趣的朋友可以了解下
    2013-01-01

最新评论