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 相关文章推荐
使用JQuery进行跨域请求
Jan 25 Javascript
几种延迟加载JS代码的方法加快网页的访问速度
Oct 12 Javascript
jquery实现上下左右滑动的方法
Feb 09 Javascript
基于JQuery实现图片轮播效果(焦点图)
Feb 02 Javascript
JS实现根据文件字节数返回文件大小的方法
Aug 02 Javascript
AngularJS入门教程之数据绑定用法示例
Nov 01 Javascript
jquery dataview数据视图插件使用方法
Dec 23 Javascript
微信小程序分页加载的实例代码
Jul 11 Javascript
JS原生轮播图的简单实现(推荐)
Jul 22 Javascript
解决vue页面DOM操作不生效的问题
Mar 17 Javascript
Vue二次封装axios为插件使用详解
May 21 Javascript
vue+element UI实现树形表格带复选框的示例代码
Apr 16 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 用数组降低程序的时间复杂度
2009/12/04 PHP
ThinkPHP5&amp;5.1框架关联模型分页操作示例
2019/08/03 PHP
PHP+MySQL实现在线测试答题实例
2020/01/02 PHP
PHP中echo与print区别点整理
2021/03/09 PHP
jquery实现div阴影效果示例代码
2013/09/16 Javascript
js面向对象编程之如何实现方法重载
2014/07/02 Javascript
jQuery给元素添加样式的方法详解
2015/12/30 Javascript
jQuery实现伪分页的方法分享
2016/02/17 Javascript
JavaScript中Array的实用操作技巧分享
2016/09/11 Javascript
js实现目录链接,内容跟着目录滚动显示的简单实例
2016/10/15 Javascript
微信小程序页面生命周期详解
2018/01/31 Javascript
vue.js select下拉框绑定和取值方法
2018/03/03 Javascript
JQuery属性操作与循环用法示例
2019/05/15 jQuery
vue项目中全局引入1个.scss文件的问题解决
2019/08/01 Javascript
vue 实现强制类型转换 数字类型转为字符串
2019/11/07 Javascript
Javascript实现简易天数计算器
2020/05/18 Javascript
python ElementTree 基本读操作示例
2009/04/09 Python
Python中文编码那些事
2014/06/25 Python
Python socket.error: [Errno 98] Address already in use的原因和解决方法
2014/08/25 Python
编写同时兼容Python2.x与Python3.x版本的代码的几个示例
2015/03/30 Python
python 链接和操作 memcache方法
2017/03/04 Python
pandas数据预处理之dataframe的groupby操作方法
2018/04/13 Python
利用Python查看微信共同好友功能的实现代码
2019/04/24 Python
Python实现堡垒机模式下远程命令执行操作示例
2019/05/09 Python
PyQt5 窗口切换与自定义对话框的实例
2019/06/20 Python
Python 获取指定文件夹下的目录和文件的实现
2019/08/30 Python
PIL.Image.open和cv2.imread的比较与相互转换的方法
2020/06/03 Python
Python绘图之柱形图绘制详解
2020/07/28 Python
PyCharm 2020.2.2 x64 下载并安装的详细教程
2020/10/15 Python
小学英语教学反思案例
2014/02/04 职场文书
《果园机器人》教学反思
2014/04/13 职场文书
公司领导班子对照材料
2014/08/18 职场文书
产品陈列协议书(标准版)
2014/09/17 职场文书
二十年同学聚会致辞
2015/07/28 职场文书
通知怎么写?
2019/04/17 职场文书
关于 Python json中load和loads区别
2021/11/07 Python