前言
扫雷是windows自带的游戏,通过翻开小方块,来推理雷的位置本文讲解如何通过javaScript制作扫雷游戏,并运用canvas画布绘制windows扫雷效果
一、技术拆分
- canvas画图
- 如何埋雷
- 左点击翻牌
- 右点击插旗
- 翻拍计算雷
- 翻牌自动翻周围的牌
- 判断胜利
二、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);
}
}
},
整体逻辑就是找雷,如果无雷,就找周围的方块八块,并递归这个步骤
递归的结束条件 就是 方块如果在记录的坐标中,则不继续拓展
六、判断胜利
本项目判断胜利必须要求所有的雷都被插旗,并且所有的牌除雷都被翻开 需要统计三个数量:- 插旗的个数
- 插旗插中雷的个数
- 翻牌的个数
方块数量 - 插旗个数 = 翻牌数量
表示赢得胜利
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实现多种策略模式