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 相关文章推荐
轻轻松松学习JavaScript
Feb 25 Javascript
Javascript 垃圾收集机制介绍理解
May 14 Javascript
jquery css 设置table的奇偶行背景色示例
Jun 03 Javascript
JS实现从网页顶部掉下弹出层效果的方法
Aug 06 Javascript
window.onerror()的用法与实例分析
Jan 27 Javascript
url中的特殊符号有什么含义(推荐)
Jun 17 Javascript
基于Layer+jQuery的自定义弹框
May 26 Javascript
移动端日期插件Mobiscroll.js使用详解
Dec 19 Javascript
利用angularjs1.4制作的简易滑动门效果
Feb 28 Javascript
node使用Mongoose类库实现简单的增删改查
Nov 08 Javascript
微信JS-SDK updateAppMessageShareData安卓不能自定义分享详解
Mar 29 Javascript
详解使用uni-app开发微信小程序之登录模块
May 09 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
一个SQL管理员的web接口
2006/10/09 PHP
关于php连接mssql:pdo odbc sql server
2011/07/20 PHP
php时间戳转换的示例
2014/03/31 PHP
php session的锁和并发
2016/01/22 PHP
PHP简单计算两个时间差的方法示例
2017/06/20 PHP
第一个JavaScript入门基础 document.write输出
2010/02/22 Javascript
浅析JavaScript Array和string的转换(推荐)
2016/05/20 Javascript
jQuery查找节点并获取节点属性的方法
2016/09/09 Javascript
Ajax跨域实现代码(后台jsp)
2017/01/21 Javascript
Vue.js实现在下拉列表区域外点击即可关闭下拉列表的功能(自定义下拉列表)
2017/05/30 Javascript
微信小程序三级联动地址选择器的实例代码
2017/07/12 Javascript
微信小程序之滚动视图容器的实现方法
2017/09/26 Javascript
NW.js 简介与使用方法
2018/02/01 Javascript
Webstorm2016使用技巧(SVN插件使用)
2018/10/29 Javascript
Vue数据双向绑定的深入探究
2018/11/27 Javascript
JS中call()和apply()的功能及用法实例分析
2019/06/28 Javascript
javascript移动端 电子书 翻页效果实现代码
2019/09/07 Javascript
python中使用sys模板和logging模块获取行号和函数名的方法
2014/04/15 Python
Python3读取文件常用方法实例分析
2015/05/22 Python
python将处理好的图像保存到指定目录下的方法
2019/01/10 Python
Python爬虫实战之12306抢票开源
2019/01/24 Python
用python3 返回鼠标位置的实现方法(带界面)
2019/07/05 Python
NumPy统计函数的实现方法
2020/01/21 Python
使用python实现飞机大战游戏
2020/03/23 Python
matplotlib quiver箭图绘制案例
2020/04/17 Python
python生成xml时规定dtd实例方法
2020/09/21 Python
Joe Fresh官网:加拿大时尚品牌和零售连锁店
2016/11/30 全球购物
Feelunique澳大利亚:欧洲的化妆品零售电商
2019/12/18 全球购物
海量信息软件测试笔试题
2015/08/08 面试题
怎样有效的进行自我评价
2013/10/06 职场文书
运动会邀请函范文
2014/01/31 职场文书
高三学生评语大全
2014/04/25 职场文书
王兆力在市委党的群众路线教育实践活动总结大会上的讲话稿
2014/10/25 职场文书
2014年行政助理工作总结
2014/11/19 职场文书
丧事答谢词
2015/01/05 职场文书
2019求职信:应届生求职信范文
2019/04/24 职场文书