javaScript开发扫雷游戏

前言

扫雷是windows自带的游戏,通过翻开小方块,来推理雷的位置

本文讲解如何通过javaScript制作扫雷游戏,并运用canvas画布绘制windows扫雷效果



一、技术拆分

  1. canvas画图
  2. 如何埋雷
  3. 左点击翻牌
  4. 右点击插旗
  5. 翻拍计算雷
  6. 翻牌自动翻周围的牌
  7. 判断胜利
通过技术拆分,清晰的知道项目有什么难点,会遇到什么问题

二、canvas画图

准备一个canvas,左点击事件和右点击事件,canvas的宽度和高度由雷地图决定
 <canvas @click.stop="leftClick" @contextmenu.prevent.stop="rightClick" id="canvasScanMine" :width="canvasWidth"
            :height="canvasHeight"
            :style="{'margin':'0 30px','position': 'relative','width': canvasWidth+'px','height': canvasHeight + 'px'}">
        您的浏览器不支持canvas,请更换浏览器重新尝试
    </canvas>
获取canvas对象
this.context = document.getElementById('canvasScanMine').getContext("2d");


画方块

主要由一个矩形,四条边组成,本来画矩形边框也可以,这里主要为了模拟明暗变化以突显3D凸起效果,每个方块有四个坐标
 drawBlock(i, j) {
     //填充矩形
     this.context.fillStyle = '#C0C0C0'
     this.context.fillRect(i * this.blockWidth, j * this.blockWidth, this.blockWidth, this.blockWidth);

     //画四条线
     //左
     this.drawLine(
         '#ececec',
         i * this.blockWidth + 1,
         j * this.blockWidth + 1,
         i * this.blockWidth + 1,
         j * this.blockWidth + 1 + this.blockWidth - 2)

     //上
     this.drawLine(
         '#ececec',
         i * this.blockWidth + 1,
         j * this.blockWidth + 1,
         i * this.blockWidth + 1 + this.blockWidth - 2,
         j * this.blockWidth + 1)

     //右
     this.drawLine(
         'rgb(128,128,128)',
         i * this.blockWidth + 1 + this.blockWidth - 2,
         j * this.blockWidth + 1,
         i * this.blockWidth + 1 + this.blockWidth - 2,
         j * this.blockWidth + 1 + this.blockWidth - 2)

     //下
     this.drawLine(
         'rgb(128,128,128)',
         i * this.blockWidth + 2,
         j * this.blockWidth + this.blockWidth - 1,
         i * this.blockWidth + 2 + this.blockWidth,
         j * this.blockWidth + this.blockWidth - 1)
 },
这里画线需要指定不同的颜色,所以需要beginPath(),否则四个线都是一个颜色和样式。
每条线都有两个坐标
   drawLine(color, beginX, beginY, endX, endY) {
            this.context.beginPath()  //开启新路径,用于四条线画不同的颜色
            this.context.strokeStyle = color
            this.context.moveTo(beginX, beginY)
            this.context.lineTo(endX, endY)
            this.context.lineWidth = 2;
            this.context.stroke()
        },

效果图
可以明细的看到由每个方块和四条边框线组成,每个方块的线有明暗变化以突显3D凸起效果

在这里插入图片描述



右键点击插旗效果

由两个三角形模拟,每个三角形都有三个坐标
this.context.beginPath()
this.context.fillStyle = "red"
this.context.moveTo(x * this.blockWidth + this.blockWidth / 2, y * this.blockWidth + 1)
this.context.lineTo(x * this.blockWidth + this.blockWidth / 2, y * this.blockWidth + this.blockWidth / 2 + 1)
this.context.lineTo(x * this.blockWidth + this.blockWidth / 4 - 1, y * this.blockWidth + this.blockWidth / 4)
this.context.fill()

this.context.beginPath()
this.context.fillStyle = "black"
this.context.moveTo(x * this.blockWidth + this.blockWidth / 2, y * this.blockWidth + this.blockWidth / 2)
this.context.lineTo(x * this.blockWidth + this.blockWidth / 4 - 3, y * this.blockWidth + this.blockWidth - 3)
this.context.lineTo(x * this.blockWidth + this.blockWidth / 4 * 3, y * this.blockWidth + this.blockWidth - 3)
this.context.fill()
效果还不错😂

在这里插入图片描述



如何画雷

四个参数,方块的x坐标,方块的y坐标,方块的半径,起始角度,结束角度,这里是画满圆
this.context.beginPath()
this.context.fillStyle = 'black'
this.context.arc(x1 * this.blockWidth + this.blockWidth / 2 , y1 * this.blockWidth + this.blockWidth / 2 , this.blockWidth / 2 - 5,  0, 2 * Math.PI)
this.context.fill()
this.context.stroke()

效果图
在这里插入图片描述



如何画雷的个数

通过fillText方法画文本,三个参数,文本值,x坐标,y坐标
this.context.beginPath()
this.context.fillStyle = this.getNumberColor(index)
this.context.font = "17px Microsoft Yahei"
this.context.fillText(“文本”, row * this.blockWidth + this.blockWidth / 2 - 5, column * this.blockWidth + this.blockWidth / 2 + 6)
至此,canvas的画图技术难点已经全部攻克,剩下的都是内部逻辑

三、如何生成雷

准备工作,初始化参数和地图,地图是一个二维对象数组
init() {
    this.gameStart = false
    this.gameEnd = false
    this.history = []
    this.scan = []
    for (let i = 0; i < this.row; i++) {
        this.scan[i] = []
        for (let j = 0; j < this.column; j++) {

            this.scan[i][j] = {
                isFlag: false,//被右键标记的方块
                isMine: false, //是否是雷
                isTurn: false,//被翻开的方块
            }

            this.drawBlock(i, j)

        }
    }
}
生成雷有个小技巧,就是在第一次点击的时候才生成雷,避免用户第一次就直接点击到雷

如何准确的知道点击的是哪个方块
通过获取点击的offset坐标,这是点击的元素的相对坐标,再除以方块宽度,取个整,就能准确的获得扫雷地图的二维坐标


如何生成雷
本项目采取的是将地图方块放到list中,排除点击的位置,再循环list生成雷,直到满足雷的个数

这里也有个缺点,如果总是在相同的位置生成雷,会一直循环下去

//获取点击坐标
let x = parseInt(e.offsetX / this.blockWidth);
let y = parseInt(e.offsetY / this.blockWidth);

let mine = this.scan[x][y]

//当第一次点击时才生成雷
if (!this.gameStart) {
this.gameStart = true
let list = [];

//将雷放到list
for (let y1 = 0; y1 < this.column; y1++) {
   for (let x1 = 0; x1 < this.row; x1++) {
       //排除点击的位置,防止刚点击就失败
       if (x1 !== x && y1 !== y) {
           list.push(this.scan[x1][y1]);
       }
   }
}

//再随机生成雷
	while (true) {
   		if (list.filter(v => v.isMine).length === this.mineTotal) {
       		break
   		}
	   list[this.random(0, list.length)].isMine = true;
	}
}


四、翻牌计算雷

这里比较死板,就是计算当前点击位置周围的八个坐标,统计是否有雷
getNumber(row, column) {
  let index = 0;
    //左
    if (row - 1 >= 0 && this.scan[row - 1][column].isMine) {
        index++;
    }
    //左斜下
    if (column + 1 < this.scan[row].length && row - 1 >= 0 && this.scan[row - 1][column + 1].isMine) {
        index++;
    }
    //下
    if (column + 1 < this.scan[row].length && this.scan[row][column + 1].isMine) {
        index++;
    }
    //右斜下
    if (column + 1 < this.scan[row].length && row + 1 < this.scan[column + 1].length && this.scan[row + 1][column + 1].isMine) {
        index++;
    }
    //右
    if (row + 1 < this.scan.length && this.scan[row + 1][column].isMine) {
        index++;
    }
    //右斜上
    if (column - 1 >= 0 && row + 1 < this.scan.length && this.scan[row + 1][column - 1].isMine) {
        index++;
    }
    //上
    if (column - 1 >= 0 && this.scan[row][column - 1].isMine) {
        index++;
    }
    //左斜上
    if (column - 1 >= 0 && row - 1 >= 0 && this.scan[row - 1][column - 1].isMine) {
        index++;
    }
    return index;
}


五、自动翻周围的牌

扫雷有个特点,就是周围有空白块,会自动翻,这里使用递归算法,达到自动翻牌的效果

在这里插入图片描述
主要有四个逻辑点

逻辑点一

点击事件,处理当前点击方块,如果没雷则自动翻牌
if (!this.scan[x][y].isTurn && this.handleBlock(x, y)) {
        this.history = [];
        //如果是空白,则扩展
        this.findBlank(x, y);
    }


逻辑点二

画出翻牌样式,统计雷的个数,标记当前位置已经被翻
handleBlock(row, column) {
 let _this = this;
 let flag = true;
 if (!this.scan[row][column].isMine) {
     this.scan[row][column].isTurn = true

     //改背景
     this.context.beginPath()
     this.context.fillStyle = "#C0C0C0"
     this.context.fillRect(row * this.blockWidth, column * this.blockWidth, this.blockWidth, this.blockWidth);

     //画线框
     this.context.beginPath()
     this.context.fillStyle = "#000000"
     this.context.lineWidth = 0.1
     this.context.strokeRect(row * this.blockWidth, column * this.blockWidth, this.blockWidth, this.blockWidth);

     let index = 0;
     if ((index = _this.getNumber(row, column)) > 0) {
         flag = false;
         //画文本(雷的个数)
         this.context.beginPath()
         this.context.fillStyle = this.getNumberColor(index)
         this.context.font = "17px Microsoft Yahei"
         this.context.fillText(index, row * this.blockWidth + this.blockWidth / 2 - 5, column * this.blockWidth + this.blockWidth / 2 + 6)
     }
 }
 return flag;
},


逻辑点三

获取当前方块周围的八个格子,并记录坐标

如果坐标在记录中不存在,则递归继续翻牌

 findBlank(x, y) {
     this.history.push(x + "," + y);
     //左
     if (x - 1 >= 0 && !this.isExists(x - 1, y)) {
         this.findBlankTemplate(x - 1, y);
     }
     //左斜上
     if (x - 1 >= 0 && y - 1 >= 0 && !this.isExists(x - 1, y - 1)) {
         this.findBlankTemplate(x - 1, y - 1);
     }
     //左斜下
     if (x - 1 >= 0 && y + 1 < this.scan[x].length && !this.isExists(x - 1, y + 1)) {
         this.findBlankTemplate(x - 1, y + 1);
     }
     //下
     if (y + 1 < this.scan[x].length && !this.isExists(x, y + 1)) {
         this.findBlankTemplate(x, y + 1);
     }
     //右斜下
     if (x + 1 < this.scan.length && y + 1 < this.scan[x].length && !this.isExists(x + 1, y + 1)) {
         this.findBlankTemplate(x + 1, y + 1);
     }
     //右
     if (x + 1 < this.scan.length && !this.isExists(x + 1, y)) {
         this.findBlankTemplate(x + 1, y);
     }
     //右斜上
     if (x + 1 < this.scan.length && y - 1 >= 0 && !this.isExists(x + 1, y - 1)) {
         this.findBlankTemplate(x + 1, y - 1);
     }
     //上
     if (y - 1 >= 0 && !this.isExists(x, y - 1)) {
         this.findBlankTemplate(x, y - 1);
     }
 },


逻辑点四

记录方块坐标,如果无雷,则继续翻牌
findBlankTemplate(x, y) {
     this.history.push(x + "," + y);
      if (!this.scan[x][y].isMine) {
          if (this.handleBlock(x, y)) {
              this.findBlank(x, y);
          }
      }
  },
整体逻辑就是找雷,如果无雷,就找周围的方块八块,并递归这个步骤
递归的结束条件 就是 方块如果在记录的坐标中,则不继续拓展

六、判断胜利

本项目判断胜利必须要求所有的雷都被插旗,并且所有的牌除雷都被翻开 需要统计三个数量:
  1. 插旗的个数
  2. 插旗插中雷的个数
  3. 翻牌的个数
并且插旗的个数 = 插旗插中雷的个数 = 雷的个数

方块数量 - 插旗个数 = 翻牌数量
表示赢得胜利

gameOver() {
        let realMineTotal = 0 //插旗插中雷的个数
        let flagTotal = 0 //插旗的个数
        let turnTotal = 0 //翻牌的个数

        for (let y1 = 0; y1 <this.column; y1++){
            for (let x1 = 0; x1 < this.row; x1++) {
                if (this.scan[x1][y1].isFlag){
                    flagTotal++;
                }
                if (this.scan[x1][y1].isFlag && this.scan[x1][y1].isMine){
                    realMineTotal++;
                }
                if (this.scan[x1][y1].isTurn){
                    turnTotal++;
                }
            }
        }

        //所有的牌除开雷都要翻开,并且旗子标记对才算胜利
        if (flagTotal === this.mineTotal && realMineTotal === this.mineTotal && turnTotal === this.row * this.column - realMineTotal){
            this.gameEnd = true
            if (window.confirm("胜利,是否重新开始?")) {
                this.init()
            }
        }
    },
},


七、体验和源码

扫雷体验可以去我的个人网站,源码可以查看我的文章分享

扫雷体验地址


Python系列:
读取文件 – 使用python读取 xls,xlsx,csv,doc,docx,pdf 格式的文件

阅读小工具 – 使用python开发无边框窗体阅读小工具

操作xlsx文件 – 使用openpyxl技术对xlsx的各种操作


前端系列:
扫雷游戏 – JavaScript 仿造 windows 编写 扫雷游戏

前端工具库 xlsx 处理表头合并 – 如何使用xlsx技术处理复杂的表头合并

CSS 布局技巧 – 对整体布局的心得体会

NVM Node 多版本控制教程 – Node版本控制神器 NVM


Spring系列:
Spring部署 – Spring的多种linux部署方式

Spring实现策略模式 – 通过Spring实现多种策略模式

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值