js+canvas实现简单扫雷小游戏


Posted in Javascript onJanuary 22, 2021

扫雷小游戏作为windows自带的一个小游戏,受到很多人的喜爱,今天我们就来尝试使用h5的canvas结合js来实现这个小游戏。

要写游戏,首先要明确游戏的规则,扫雷游戏是一个用鼠标操作的游戏,通过点击方块,根据方块的数字推算雷的位置,标记出所有的雷,打开所有的方块,即游戏成功,若点错雷的位置或标记雷错误,则游戏失败。

具体的游戏操作如下

1.可以通过鼠标左键打开隐藏的方块,打开后若不是雷,则会向四个方向扩展

2.可以通过鼠标右键点击未打开的方块来标记雷,第二次点击取消标记

3.可以通过鼠标右键点击已打开且有数字的方块来检查当前方块四周的标记是否正确

接下来开始编写代码

首先写好HTML的结构,这里我简单地使用一个canvas标签,其他内容的扩展在之后实现(游戏的规则,游戏的难度设置)

<!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">
 <title>Document</title>
 <style>
 #canvas {
 display: block;
 margin: 0 auto;
 }
 </style>
</head>
 
<body>
 <div id="play">
 <canvas id="canvas"></canvas>
 </div>
 <script src="js/game.js"></script>
</body>
 
</html>

接下来我们来初始化一些内容。包括canvas画布的宽高,游戏共有几行几列,几个雷,每个格子的大小。

//获取canvas画布
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
canvas.width = 480;
canvas.height = 480;
 
//定义各属性
let R = 3; //格子圆角半径
let L = 15; //每个格子实际长
let P = 16; //每个格子占长
let row = 30; //行数
let col = 30; //列数
let N = 50; //雷数

为了后面的操作,我要用几个数组来储存一些位置,一个方块是否为雷的数组,该数组用于描绘出整个画面每个方块对应的内容;一个数组用于描述方块状态,即是否打开或者被标记;一个数组用来记载生成的雷的位置;一个数组用来记载标记的位置。

var wholeArr = drawInitialize(row, col, N, R, L, P);
var gameArr = wholeArr[0] //位置数组
var bombArr = wholeArr[1] //雷的位置数组
var statusArr = zoneInitialize(row, col); //状态数组 0为未打开且未标记 1为打开 2为标记
var signArr = []; //标记数组
 
//画出初始界面
function drawInitialize(row, col, n, R, L, P) {
 let arr = initialize(row, col, n);
 for (let r = 0; r < row; r++) {
 for (let c = 0; c < col; c++) {
 drawRct(r * P, c * P, L, R, 'rgb(102,102,102)', context);//该方法用于绘制整个画面,下面会写出声明
 }
 }
 return arr;
}
 
//初始化
function initialize(row, col, n) {
 let gameArr = zoneInitialize(row, col); //生成没有标记雷的矩阵
 let bomb = bombProduce(n, gameArr, row, col);
 gameArr = signArrNum(bomb[0], bomb[1], n, row, col);
 return [gameArr, bomb[1]];
}
 
//界面矩阵初始化
function zoneInitialize(row, col) { //生成row行col列的矩阵
 let cArr = new Array(col);
 let rArr = new Array(row);
 cArr = cArr.fill(0); //将行的每个位置用0填充
 for (let i = 0; i < row; i++)
 rArr[i] = [...cArr];
 return rArr;
}
 
//随机生成雷
function bombProduce(n, arr, row, col) { //随机生成n个雷
 let count = 0;
 let bombArr = [];
 
 while (true) {
 if (count === n)
 break;
 let r = Math.floor(Math.random() * row);
 let c = Math.floor(Math.random() * col);
 if (arr[c][r] === 0) {
 arr[c][r] = -1;
 bombArr[count] = strProduce(c, r);
 count++;
 }
 }
 return [arr, bombArr];
}
 
//标记数字
function signArrNum(gArr, bArr, n, row, col) {
 for (let i = 0; i < n; i++) { //为每个雷的四周的非雷的数字标记加一
 let r = parseInt(analyseStr(bArr[i]).row);
 let c = parseInt(analyseStr(bArr[i]).col);
 if (r > 0 && gArr[c][r - 1] != -1)//判断该位置是否为雷,是则不进行操作
 gArr[c][r - 1]++;
 if (r < row - 1 && gArr[c][r + 1] !== -1)
 gArr[c][r + 1]++;
 if (c > 0 && gArr[c - 1][r] !== -1)
 gArr[c - 1][r]++;
 if (c < col - 1 && gArr[c + 1][r] !== -1)
 gArr[c + 1][r]++;
 if (r > 0 && c > 0 && gArr[c - 1][r - 1] != -1)
 gArr[c - 1][r - 1]++;
 if (r < row - 1 && c < col - 1 && gArr[c + 1][r + 1] != -1)
 gArr[c + 1][r + 1]++;
 if (r > 0 && c < col - 1 && gArr[c + 1][r - 1] != -1)
 gArr[c + 1][r - 1]++;
 if (r < row - 1 && c > 0 && gArr[c - 1][r + 1] != -1)
 gArr[c - 1][r + 1]++;
 }
 return gArr;
}
 
//生成字符串
function strProduce(r, c) {
 return `row:${c}|col:${r}`;
}
 
//解析雷数组字符串
function analyseStr(str) {
 str = str.split('|');
 str[0] = str[0].split(':');
 str[1] = str[1].split(':');
 return { row: str[0][1], col: str[1][1] };
}

接下来将绘制的方法写出来,这里我使用红色的方块来代表雷

//画出单个方块
function drawRct(x, y, l, r, color, container = context) {//x,y为绘制的位置,l为方块的边长,r为方块圆角半径,color为方块的填充颜色
 container.beginPath();
 container.moveTo(x + r, y);
 container.lineTo(x + l - r, y);
 container.arcTo(x + l, y, x + l, y + r, r);
 container.lineTo(x + l, y + l - r);
 container.arcTo(x + l, y + l, x + l - r, y + l, r);
 container.lineTo(x + r, y + l);
 container.arcTo(x, y + l, x, y + l - r, r);
 container.lineTo(x, y + r);
 container.arcTo(x, y, x + r, y, r);
 container.fillStyle = color;
 container.closePath();
 container.fill();
 container.stroke();
}
 
//画出方块上对应的数字
function drawNum(x, y, l, r, alPha, color = 'rgb(0,0,0)', container = context) {//参数含义与上面的方法一样,alPha为要写的数字
 if (alPha === 0)
 alPha = "";
 container.beginPath();
 container.fillStyle = color;
 container.textAlign = 'center';
 container.textBaseline = 'middle';
 container.font = '8Px Adobe Ming Std';
 container.fillText(alPha, x + l / 2, y + l / 2);
 container.closePath();
}
 
//画出游戏结束界面
function drawEnd(row, col, R, L, P) {
 for (let r = 0; r < row; r++) {
 for (let c = 0; c < col; c++) {//将整个界面绘制出来
 let num = gameArr[r][c];
 let color;
 if (num === -1)
 color = 'rgb(255,0,0)';
 else
 color = 'rgb(255,255,255)';
 drawRct(r * P, c * P, L, R, color, context);
 drawNum(r * P, c * P, L, R, num);
 }
 }
}

接下来写出点击事件的处理,这里对于点击后的向四个方向扩展,我采用了以下图片所示的扩展

js+canvas实现简单扫雷小游戏

如上图片,在点击时在点击位置往四周扩散,之后上下的按上下方向继续扩散,左右的除本方向外还有往上下方向扩散,在遇到数字时停下。

canvas.onclick = function(e) {
 e = e || window.e;
 let x = e.clientX - canvas.offsetLeft;
 let y = e.clientY - canvas.offsetTop; //获取鼠标在canvas画布上的坐标
 let posX = Math.floor(x / P);
 let posY = Math.floor(y / P);//将坐标转化为数组下标
 if (gameArr[posX][posY] === -1 && statusArr[posX][posY] !== 2) { //点到雷
 alert('error');
 drawEnd(row, col, R, L, P);
 } else if (statusArr[posX][posY] === 0) {
 this.style.cursor = "auto";
 statusArr[posX][posY] = 1;//重置状态
 drawRct(posX * P, posY * P, L, R, 'rgb(255,255,255)', context);
 drawNum(posX * P, posY * P, L, R, gameArr[posX][posY]);
 outNum(gameArr, posY, posX, row, col, 'middle');
 }
 gameComplete();//游戏成功,在下面代码定义
}
 
//右键标记雷,取消标记,检查四周
canvas.oncontextmenu = function(e) {
 e = e || window.e;
 let x = e.clientX - canvas.offsetLeft;
 let y = e.clientY - canvas.offsetTop; //获取鼠标在canvas画布上的坐标
 let posX = Math.floor(x / P);
 let posY = Math.floor(y / P);
 let str = strProduce(posX, posY);
 if (gameArr[posX][posY] > 0 && statusArr[posX][posY] === 1) //检查四周雷数
 checkBomb(posX, posY);
 if (statusArr[posX][posY] === 0) { //标记雷
 drawRct(posX * P, posY * P, L, L / 2, 'rgb(255,0,0)');
 statusArr[posX][posY] = 2;
 signArr[signArr.length] = str;
 } else if (statusArr[posX][posY] === 2) { //取消标记
 drawRct(posX * P, posY * P, L, R, 'rgb(102,102,102)');
 statusArr[posX][posY] = 0;
 signArr = signArr.filter(item => {//使用过滤器方法将当前位置的坐标标记清除
 if (item === str)
 return false;
 return true;
 })
 }
 gameComplete();
 return false; //阻止事件冒泡
}
 
//自动跳出数字
function outNum(arr, x, y, row, col, status) {//arr为传入的数组,x,y为处理的位置,row,col为游戏的行列,status用于储存扩展的方向
 if (status === 'middle') {
 outNumHandle(arr, x - 1, y, row, col, 'left');
 outNumHandle(arr, x + 1, y, row, col, 'right');
 outNumHandle(arr, x, y - 1, row, col, 'top');
 outNumHandle(arr, x, y + 1, row, col, 'down');
 } else if (status === 'left') {
 outNumHandle(arr, x - 1, y, row, col, 'left');
 outNumHandle(arr, x, y - 1, row, col, 'top');
 outNumHandle(arr, x, y + 1, row, col, 'down');
 } else if (status === 'right') {
 outNumHandle(arr, x + 1, y, row, col, 'right');
 outNumHandle(arr, x, y - 1, row, col, 'top');
 outNumHandle(arr, x, y + 1, row, col, 'down');
 } else if (status === 'top') {
 outNumHandle(arr, x, y - 1, row, col, 'top');
 } else {
 outNumHandle(arr, x, y + 1, row, col, 'down');
 }
}
 
//跳出数字具体操作
function outNumHandle(arr, x, y, row, col, status) {
 if (x < 0 || x > row - 1 || y < 0 || y > col - 1) //超出边界的情况
 return;
 if (arr[y][x] !== 0) {
 if (arr[y][x] !== -1) {
 drawRct(y * P, x * P, L, R, 'rgb(255,255,255)', context);
 drawNum(y * P, x * P, L, R, arr[y][x]);
 statusArr[y][x] = 1;
 }
 return;
 }
 drawRct(y * P, x * P, L, R, 'rgb(255,255,255)', context);
 drawNum(y * P, x * P, L, R, arr[y][x]);
 statusArr[y][x] = 1;
 outNum(arr, x, y, row, col, status);
}
 
//检查数字四周的雷的标记并操作
function checkBomb(r, c) {
 //1.检查四周是否有被标记确定的位置
 //2.记下标记的位置数count
 //3.若count为0,则return;若count大于0,检查是否标记正确
 //4.如果标记错误,提示游戏失败,若标记正确但数量不够,则return跳出,若标记正确且数量正确,将其余位置显示出来
 let bombNum = gameArr[r][c];
 let count = 0;
 if (r > 0 && statusArr[r - 1][c] === 2) {
 if (!(bombArr.includes(strProduce(r - 1, c)))) {
 alert('error');
 drawEnd(row, col, R, L, P);
 return;
 }
 count++;
 }
 if (r < row - 1 && statusArr[r + 1][c] === 2) {
 if (!(bombArr.includes(strProduce(r + 1, c)))) {
 alert('error');
 drawEnd(row, col, R, L, P);
 return;
 }
 count++;
 }
 if (c > 0 && statusArr[r][c - 1] === 2) {
 if (!(bombArr.includes(strProduce(r, c - 1)))) {
 alert('error');
 drawEnd(row, col, R, L, P);
 return;
 }
 count++;
 }
 if (c < col - 1 && statusArr[r][c + 1] === 2) {
 if (!(bombArr.includes(strProduce(r, c + 1)))) {
 alert('error');
 drawEnd(row, col, R, L, P);
 return;
 }
 count++;
 }
 if (r > 0 && c > 0 && statusArr[r - 1][c - 1] === 2) {
 if (!(bombArr.includes(strProduce(r - 1, c - 1)))) {
 alert('error');
 drawEnd(row, col, R, L, P);
 return;
 }
 count++;
 }
 if (r < row - 1 && c < col - 1 && statusArr[r + 1][c + 1] === 2) {
 if (!(bombArr.includes(strProduce(r + 1, c + 1)))) {
 alert('error');
 drawEnd(row, col, R, L, P);
 return;
 }
 count++;
 }
 if (r > 0 && c < col - 1 && statusArr[r - 1][c + 1] === 2) {
 if (!(bombArr.includes(strProduce(r - 1, c + 1)))) {
 alert('error');
 drawEnd(row, col, R, L, P);
 return;
 }
 count++;
 }
 if (r < row - 1 && c > 0 && statusArr[r + 1][c - 1] === 2) {
 if (!(bombArr.includes(strProduce(r + 1, c - 1)))) {
 alert('error');
 drawEnd(row, col, R, L, P);
 return;
 }
 count++;
 }
 
 if (count !== bombNum)
 return;
 else {
 
 outNotBomb(c, r);
 }
}
 
 
//跳出四周非雷的方块
function outNotBomb(c, r) {
 if (r > 0 && statusArr[r - 1][c] === 0) {
 drawRct((r - 1) * P, c * P, L, R, 'rgb(255,255,255)', context);
 drawNum((r - 1) * P, c * P, L, R, gameArr[r - 1][c]);
 statusArr[r - 1][c] = 1;
 }
 if (r < row - 1 && statusArr[r + 1][c] === 0) {
 drawRct((r + 1) * P, c * P, L, R, 'rgb(255,255,255)', context);
 drawNum((r + 1) * P, c * P, L, R, gameArr[r + 1][c]);
 statusArr[r + 1][c] = 1;
 }
 if (c > 0 && statusArr[r][c - 1] === 0) {
 drawRct(r * P, (c - 1) * P, L, R, 'rgb(255,255,255)', context);
 drawNum(r * P, (c - 1) * P, L, R, gameArr[r][c - 1]);
 statusArr[r][c - 1] = 1;
 }
 if (c < col - 1 && statusArr[r][c + 1] === 0) {
 drawRct(r * P, (c + 1) * P, L, R, 'rgb(255,255,255)', context);
 drawNum(r * P, (c + 1) * P, L, R, gameArr[r][c + 1]);
 statusArr[r][c + 1] = 1;
 }
 if (r > 0 && c > 0 && statusArr[r - 1][c - 1] === 0) {
 drawRct((r - 1) * P, (c - 1) * P, L, R, 'rgb(255,255,255)', context);
 drawNum((r - 1) * P, (c - 1) * P, L, R, gameArr[r - 1][c - 1]);
 statusArr[r - 1][c - 1] = 1;
 }
 if (r < row - 1 && c < col - 1 && statusArr[r + 1][c + 1] === 0) {
 drawRct((r + 1) * P, (c + 1) * P, L, R, 'rgb(255,255,255)', context);
 drawNum((r + 1) * P, (c + 1) * P, L, R, gameArr[r + 1][c + 1]);
 statusArr[r + 1][c + 1] = 1;
 }
 if (r > 0 && c < col - 1 && statusArr[r - 1][c + 1] === 0) {
 drawRct((r - 1) * P, (c + 1) * P, L, R, 'rgb(255,255,255)', context);
 drawNum((r - 1) * P, (c + 1) * P, L, R, gameArr[r - 1][c + 1]);
 statusArr[r - 1][c + 1] = 1;
 }
 if (r < row - 1 && c > 0 && statusArr[r + 1][c - 1] === 0) {
 drawRct((r + 1) * P, (c - 1) * P, L, R, 'rgb(255,255,255)', context);
 drawNum((r + 1) * P, (c - 1) * P, L, R, gameArr[r + 1][c - 1]);
 statusArr[r + 1][c - 1] = 1;
 }
}

接着写出找到所有雷的情况,即游戏成功通关

//成功找出所有的雷
function gameComplete() {
 var count = new Set(signArr).size;
 if (count != bombArr.length) //雷的数量不对
 {
 return false;
 }
 for (let i of signArr) { //雷的位置不对
 if (!(bombArr.includes(i))) {
 return false;
 }
 }
 for (let i of statusArr) {
 if (i.includes(0)) {
 return false;
 }
 }
 alert('恭喜你成功了');
 canvas.onclick = null;
 canvas.onmouseover = null;
 canvas.oncontextmenu = null;
}

最后调用方法画出游戏界面,这个调用要放在数组声明之前,因为数组那里也有绘制的方法,这个方法会覆盖绘制方块的画面。

drawRct(0, 0, 800, 0, 'rgb(0,0,0)', context);

一个简单的扫雷游戏就这样实现了(说实话我觉得是简陋不是简单。。。。) 

当然这个只是游戏的初步实现,其实这个游戏还可以增加难度设置,用图片来表示雷,在点到雷的时候增加声音等等,当然这些也并不难,如果大家有兴趣的话可以尝试优化这个游戏。

希望这篇博客能对大家有所帮助,也希望大神能指出我的不足。

附上一张丑爆的游戏界面

js+canvas实现简单扫雷小游戏

更多关于Js游戏的精彩文章,请查看专题:《JavaScript经典游戏 玩不停》

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

Javascript 相关文章推荐
Aptana调试javascript图解教程
Nov 30 Javascript
jQuery Selector选择器小结
May 06 Javascript
jQuery实现的类flash菜单效果代码
May 17 Javascript
前台js改变Session的值(用ajax实现)
Dec 28 Javascript
JavaScript中this关键词的使用技巧、工作原理以及注意事项
May 20 Javascript
js类定义函数时用prototype与不用的区别示例介绍
Jun 10 Javascript
js中遍历对象的属性和值的方法
Jul 27 Javascript
BootStrap便签页的简单应用
Jan 06 Javascript
js代码延迟一定时间后执行一个函数的实例
Feb 15 Javascript
AngularJS实现的输入框字数限制提醒功能示例
Oct 26 Javascript
JavaScript程序设计高级算法之动态规划实例分析
Nov 24 Javascript
angularjs实现table表格td单元格单击变输入框/可编辑状态示例
Feb 21 Javascript
Vue常用传值方式、父传子、子传父及非父子实例分析
Feb 24 #Javascript
es6中let和const的使用方法详解
Feb 24 #Javascript
jquery制作的移动端购物车效果完整示例
Feb 24 #jQuery
jquery实现的放大镜效果示例
Feb 24 #jQuery
JS数组扁平化、去重、排序操作实例详解
Feb 24 #Javascript
JS前端面试必备——基本排序算法原理与实现方法详解【插入/选择/归并/冒泡/快速排序】
Feb 24 #Javascript
Vue快速实现通用表单验证的方法
Feb 24 #Javascript
You might like
不错的PHP学习之php4与php5之间会穿梭一点点感悟
2007/05/03 PHP
浅谈discuz密码加密的方式
2014/05/22 PHP
关于PHP中字符串与多进制转换函数的实例代码
2016/11/03 PHP
Yii2表单事件之Ajax提交实现方法
2017/05/04 PHP
张孝祥JavaScript学习阶段性总结(2)--(X)HTML学习
2007/02/03 Javascript
js的with语句使用方法
2007/09/21 Javascript
ie下动态加态js文件的方法
2011/09/13 Javascript
Web Inspector:关于在 Sublime Text 中调试Js的介绍
2013/04/18 Javascript
用jquery统计子菜单的条数示例代码
2013/10/18 Javascript
JavaScript利用构造函数和原型的方式模拟C#类的功能
2014/03/06 Javascript
Javascript基础教程之while语句
2015/01/18 Javascript
在JavaScript中操作时间之getMonth()方法的使用
2015/06/10 Javascript
实现非常简单的js双向数据绑定
2015/11/06 Javascript
基于JS实现密码框(password)中显示文字提示功能代码
2016/05/27 Javascript
plupload+artdialog实现多平台上传文件
2016/07/19 Javascript
详解vue-Resource(与后端数据交互)
2017/01/16 Javascript
JS实现的添加弹出层并完成锁屏操作示例
2017/04/07 Javascript
基于JS实现前端压缩上传图片的实例代码
2019/05/14 Javascript
jQuery操作cookie的示例代码
2019/06/05 jQuery
微信小程序 button样式设置为图片的方法
2020/06/19 Javascript
[01:21:36]CHAOS vs Alliacne 2019国际邀请赛小组赛 BO2 第一场 8.15
2019/08/16 DOTA
Django中传递参数到URLconf的视图函数中的方法
2015/07/18 Python
python解决汉字编码问题:Unicode Decode Error
2017/01/19 Python
python实现二级登陆菜单及安装过程
2019/06/21 Python
python中利用numpy.array()实现俩个数值列表的对应相加方法
2019/08/26 Python
如何在Canvas中添加事件的方法示例
2019/05/21 HTML / CSS
高一化学教学反思
2014/02/05 职场文书
银行爱岗敬业演讲稿
2014/05/05 职场文书
投资建议书模板
2014/05/12 职场文书
优秀的应届生自荐信
2014/05/23 职场文书
常务副县长“四风”个人对照检查材料思想汇报
2014/10/02 职场文书
纪检监察立案决定书
2015/06/24 职场文书
母亲节主题班会
2015/08/14 职场文书
八年级数学教学反思
2016/02/17 职场文书
学生早退检讨书(范文)
2019/08/19 职场文书
Mysql中调试存储过程最简单的方法
2021/06/30 MySQL