文章目录
前言
本文是一篇关于如何用Javascript实现扫雷的博客,文章末尾有作者自己编写的扫雷程序以及下载链接,代码可能有缺陷,图片是本人自己绘制的,可能不太美观,还请高手指教。
一、扫雷是什么?
扫雷是一款大众类的益智小游戏。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。(抄百度的)
二、游戏流程
1.初始界面

上图是游戏运行时的初始界面,其组成分为两个部分,第一个是背景,第二个是按钮,两个部分。背景其实就是一张图片,而按钮是多个对象,分别为“开始游戏”、“无尽模式”、“游戏设置”、“游戏教程”四个对象,每个对象的属性包括:图片的资源路径,图片横纵坐标,图片的长和宽。与此同时,每个对象也有它自己的方法,而方法是用户点击了按钮后要执行的代码。例如“开始游戏”按钮就需要把游戏状态修改成“GAMEING”状态,生成一组游戏数据,并开始计时。而我是选择把这些对象存在一个数组中,然后专门定义两个函数分别用于绘制按钮和响应按钮点击。
2.游戏过程

上图是游戏中的截图,要实现上方所展示的功能,需要很多步骤,主要包括:绘制背景色,绘制底部方块,绘制数字,绘制遮挡方块,绘制标记,绘制计时以及地雷数,六个步骤。
(1)绘制背景色
绘制背景色的步骤比较简单,可以通过绘图软件,画出一个长和宽都为1px的图片,然后在绘制背景色时,把这个图片的坐标定在(0,0)然后再把图片拉宽拉长以填充整个画面。
(2)绘制底部方块

上图所示为底部方块,在游戏中主要是为了划分每一个方格,让游戏更加人性化,绘制方式是在一个循环中,根据生成的游戏数据,把每一个方格的坐标,大小计算出来,然后再开始绘制,嵌套两层循环即可。
(3)绘制数字

上图所示为数字,程序中需要先计算每个方格应该显示什么,在点击了开始游戏的按钮后生成了游戏数据,其中包括的就有每一个底部方块绘制的位置,每个方格应该显示什么,并存储在一个二维数组中。而决定流程如下:
我在我的游戏中用0表示什么都不绘制,而’B’(Bomb)表示炸弹,相应的数字代表周边的地雷数,存储方式如下图:

在数据计算完毕后,我们需要通过循环来绘制数字,在遍历二维数组的时候不要直接绘制,要判断一下这个方块是否被遮挡了,如果被遮挡方块遮挡了,就不需要绘制了。
(4)绘制遮挡方块

上图是游戏中的遮挡方块,它是这个游戏中必不可少的一个部分,其绘制方式是和数字的绘制方法一样的,但是需要注意的是,需要定义第二个二维数组,用来存储是否需要绘制遮挡方块,并在绘制时添加一个判断语句:是否被遮挡。
(5)绘制标记

上图为标记图标,程序中只需要在遮挡方块的上方再绘制一个标记就好了,而关于标记的数据是存储在第二个二维数组中的。我在设计程序是用’D’(Dug)表示已经被挖开的方块,用’U’(unknown)表示未被挖掘的遮挡方块,用’S’(Signed)表示被标记的方块,具体存储方式如下图所示:

(6)绘制计时以及地雷数
绘制计时以及地雷数前,需要知道绘制什么,也就是用户现在已经开始游戏了多少秒了,游戏中还剩多少个地雷没有被标记?而具体的处理方法是:
- 对于计时,我们需要在游戏开始前先定义一个变量,用于存储当前游戏开始的时间,而初始值为0,然后在用户开始游戏的时候开始一个定时器,时间定为1秒,每一秒就这个变量的数值自增1。
- 对于地雷数是在开始游戏前生成游戏数据的时候,会有预定需要放置多少个地雷的一个具体数值,而在开始游戏后,把这个数据备份一个,然后程序只需要在用户每一次标记地雷的时候,把这个备份出来的数字自减1,取消标记的时候自增1。
处理完上述的数据后,就是绘制上的问题了。我们所需要做的是把提前绘制好的“用时:”和“剩余:”两个图片的对象通过计算,得出在屏幕上的合适位置绘制上,同时空出所需绘制数字的空间,再后来把数字绘制在恰当的位置。
3.游戏结束
(1)游戏胜利

上图是游戏胜利后的图片,需要显示“用时”,“游戏板得分”,“总分”。因此,在游戏中需要有分数统计,例如打开一个方块就加分,标记了一个地雷,就加分。而且不同的难度对得分要有影响,比如,简单模式下,分数一般不超过1000,普通模式下,分数在5000附近徘徊……还有就是用时也要对分数有影响。而游戏板得分指的是不计算时间影响下的分数。
在有了分数之后,需要把胜利后的图片绘制在画面的中央,然后再把具体的文字信息写在相应的位置。
(2)游戏失败

上图是游戏引爆地雷后的图片,也需要显示“用时”,“游戏板得分”,“总分”,具体处理方法与成功相似,区别是背景图是失败的背景图,以及在引爆炸弹时的特效。
(3)结束后的处理
结束后我们只需要相应任意一个鼠标点击,只要用户点击了画布,我们就把游戏状态调回初始界面。
4.无尽模式
在无尽模式中,我们需要添加一个新的方块,是楼梯,每当用户挖开了楼梯,并且再次点击了这个楼梯,用户就可以下楼,到达新的一层,但是游戏中新生成的长和宽都会增加,难度会不断的上升。
5.游戏设置
在游戏设置中用户可以选择调整游戏的难度,分为“简单”,“普通”,“困难”三个难度,当用户点击了这个按钮之后,它会变亮,而其具体的实现方式就是调用在初始界面绘制和响应按钮点击的程序,再添加一点点的改动即可。
6.游戏教程
游戏教程功能我还没有开发,但是整体思路是在用户点击了游戏教程的按钮后,从下往上以一定的加速度落下一个板子,上面写着游戏的教程,然后当用户点击了右上角的“X”按钮之后就返回初始界面。
7.游戏细节
(1)炸弹爆炸
在用户不小心引爆炸弹之后,会在炸弹的相应位置播放一个爆炸的特效。要实现特效,就要知道实现播放视频的具体方法,一个视频对象的组成,也就是视频这个类的具体内容如下:
// 生成视频对象
obj = {
video: video, // 存储视频图片序列
doPlay: false, // 定义是否需要绘制
x: 0, // 定义绘制坐标
y: 0, // 定义绘制坐标
frameNum: 0, // 定义当前视频播放到第几帧
intervalTime: intervalTime, // 定义视频FPS
side: 100, // 定义视频缩放
lastTime: new Date().getTime(), // 获得当前时间戳
stayLastFrame: stayLastFrame, // 视频是否需要停止在最后一帧
loopPlayback: loopPlayback // 视频是否需要循环播放
}
接下来,我们要知道如何播放一个视频对象。我们需要知道当前的时间,然后比对上一帧是多少秒之前播放的,然后如果当前的时间已经和上一帧播放的时间有一定的时间的时候,我们就要绘制下一帧,也就是把目前播放的帧数自增1,具体代码如下:
currentTime = new Date().getTime()
if (currentTime - obj.lastTime >= obj.intervalTime) {
// 如果图片的下标等于数组的长度,则重置图片下标为0
if (obj.frameNum < obj.video.length) {
ctx.drawImage(obj.video[obj.frameNum], obj.x, obj.y, obj.side, obj.side)
// 加 1 得到下一张图片的下标
obj.frameNum++
} else if (obj.stayLastFrame == true) {
ctx.drawImage(obj.video[obj.video.length - 1], obj.x, obj.y, obj.side, obj.side)
} else if (obj.loopPlayback == true) {
obj.frameNum = 0
}
}
知道了视频对象的组成,播放的原理之后,我们就可以设计一个开始播放的函数了,只需要把这个视频对象调整为“开始播放”状态,然后把需要绘制的位置计算好,缩放的大小确定好就可以了,具体代码如下:
// 调整为开始播放
obj.doPlay = true
// 计算好绘制坐标
obj.x = x * coefficient + xAddNum
obj.y = y * coefficient + yAddNum
// 存入缩放大小
obj.side = side * sideMultiple
// 调整为从头播放
obj.frameNum = 0
(2)自动开零
在用户点击了一个空的方格时,程序需要帮助用户把与这一个0相连的所有的0及其周围一圈的方格都点开,而步骤分两步,第一步,打开所有相连的0,第二步,打开所有0周围的方格。第一步需要通过递归算法来实现,具体代码如下:
if (blockInfoAry[y][x] == 0) {
// 将该方块变为已打开并加分
openBlock(y, x)
// 递归算法找出所有未打开的连续的方格
if (0 < y - 1 && blockisDigAry[y - 1][x] != 'D') {
if (0 < x - 1 && blockisDigAry[y - 1][x - 1] != 'D') {
open0(x - 1, y - 1)
}
open0(x, y - 1)
if (x + 1 < blockWidthNumOutter + 1 && blockisDigAry[y - 1][x + 1] != 'D') {
open0(x + 1, y - 1)
}
}
if (0 < x - 1 && blockisDigAry[y][x - 1] != 'D') {
open0(x - 1, y)
}
if (x + 1 < blockWidthNumOutter + 1 && blockisDigAry[y][x + 1] != 'D') {
open0(x + 1, y)
}
if (y + 1 < blockHeightNumOutter + 1 && blockisDigAry[y + 1][x] != 'D') {
if (0 < x - 1 && blockisDigAry[y + 1][x - 1] != 'D') {
open0(x - 1, y + 1)
}
open0(x, y + 1)
if (x + 1 < blockWidthNumOutter + 1 && blockisDigAry[y + 1][x + 1] != 'D') {
open0(x + 1, y + 1)
}
}
}
第二步就比较简单了,只需要根据目前打开的这个方格,然后把周围的方格在第二个二维数组中标记为’D’即可,具体实现方法如下:
function openBeside() {
for (var i = 1; i < blockHeightNumOutter + 1; i++) {
for (var j = 1; j < blockWidthNumOutter + 1; j++) {
// 将所有已打开的0格周边的方格打开
if (blockInfoAry[i][j] == 0 && blockisDigAry[i][j] == 'D') {
// 加分
boardScore = boardScore + 1
blockisDigAry[i - 1][j - 1] = 'D'
blockisDigAry[i - 1][j] = 'D'
blockisDigAry[i - 1][j + 1] = 'D'
blockisDigAry[i][j - 1] = 'D'
blockisDigAry[i][j + 1] = 'D'
blockisDigAry[i + 1][j - 1] = 'D'
blockisDigAry[i + 1][j] = 'D'
blockisDigAry[i + 1][j + 1] = 'D'
}
}
}
}
总结
以上就是今天要讲的内容,本文仅仅简单介绍了如何用Javascript实现扫雷,也希望大家能够指正我的不足,让我们一起进步!
下载方式
链接:https://pan.baidu.com/s/15xTr8U28TmJ3KR4bSVM2lw
提取码:woww
代码内容
<!DOCTYPE html>
<html lang="ch">
<!--
COPYRIGHT(C): Jerry Chen
File NAME: 1.0.0.20210530_alpha.html
Author: Jerry Chen
Version: 1.0.0.20210530_alpha
Date: 1.0.0.20210530
DESCRIPTION: real-time display of timer and number of remaining bombs
Function List:
1. gameHandler: Draw the game interface.
2. clickResponse: Respond to the user clicking the canvas.
3. drawItem: Operate on the blocks of the game according to the relevant data.
4. responseButton: Respond to the button on the start screen.
5. drawButton: Draws the button on the start screen.
6. drawBlock: Draw the bottom box.
7. drawBomb: Draw bombs or numbers in the game.
8. drawBaffle: Draw a baffle to hide numbers or bombs.
9. open0: Determine if the current square is a "0" square, and if so, open all successive "0" squares around it.
10. openBeside: Open all the squares around the open "0" square.
11. generateBasicInfo: Calculate the relevant parameters of drawing, lay mines randomly, draw numbers according to mines, and open non-mine blocks randomly.
12. isWin: Judge the game.
13. drawFloatWindow: Paint the floating window after victory.
14. importVideo: Import the image of each frame of the video.
15. startPlay: Adjust the state of the video object to start playing.
16. playVideo: Play a video according to the properties of the video object and its methods.
17. countPoint: award bonus points for the number of bombs marked.
18. CalTotalScore: Calculate the total score.
19. timer: Timers and remaining bombs displayed in the game.
-->
<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>扫雷</title>
<style>
body {
text-align: center
}
canvas {
margin: 1 auto;
border: 1px solid black
}
</style>
</head>
<body>
<canvas id="main" width="1400" height="800"></canvas>
<script>
// 初始化
if (true) {
// 获取画布和画笔对象
var canvas = document.getElementById('main')
var ctx = canvas.getContext('2d')
// 加载游戏背景图片
var backgroundColorImg = new Image()
backgroundColorImg.src = 'images/backgroundColor.png'
// 修改添加游戏背景图片对象的属性
var backgroundColor = {
img: backgroundColorImg,
width: 1400,
height: 800
}
// 加载背景图片
var coverImg = new Image()
coverImg.src = 'images/cover.png'
// 修改添加背景图片对象的属性
var cover = {
img: coverImg,
width: 1400,
height: 800
}
// 加载背景图片
var blockImg = new Image()
blockImg.src = 'images/block.png'
// 修改添加背景图片对象的属性
var block = {
img: blockImg,
width: 100,
height: 100
}
// 加载炸弹图片
var bombImg = new Image()
bombImg.src = 'images/bomb.png'
// 修改添加炸弹图片对象的属性
var bomb = {
img: bombImg,
width: 100,
height: 100
}
// 加载未挖掘图片
var undigBlockImg = new Image()
undigBlockImg.src = 'images/undigBlock.png'
// 修改添加未挖掘图片对象的属性
var undigBlock = {
img: undigBlockImg,
width: 100,
height: 100
}
// 加载标记图片
var signedBlockImg = new Image()
signedBlockImg.src = 'images/signedBlock.png'
// 修改添加标记图片对象的属性
var signedBlock = {
img: signedBlockImg,
width: 100,
height: 100
}
// 加载数字0图片
var num0Img = new Image()
num0Img.src = 'images/num0.png'
// 修改添加数字0图片对象的属性
var num0 = {
img: num0Img,
width: 100,
height: 100
}
// 加载数字1图片
var num1Img = new Image()
num1Img.src = 'images/num1.png'
// 修改添加数字1图片对象的属性
var num1 = {
img: num1Img,
width: 100,
height: 100
}
// 加载数字2图片
var num2Img = new Image()
num2Img.src = 'images/num2.png'
// 修改添加数字2图片对象的属性
var num2 = {
img: num2Img,
width: 100,
height: 100
}
// 加载数字3图片
var num3Img = new Image()
num3Img.src = 'images/num3.png'
// 修改添加数字3图片对象的属性
var num3 = {
img: num3Img,
width: 100,
height: 100
}
// 加载数字4图片
var num4Img = new Image()
num4Img.src = 'images/num4.png'
// 修改添加数字4图片对象的属性
var num4 = {
img: num4Img,
width: 100,
height: 100
}
// 加载数字5图片
var num5Img = new Image()
num5Img.src = 'images/num5.png'
// 修改添加数字5图片对象的属性
var num5 = {
img: num5Img,
width: 100,
height: 100
}
// 加载数字6图片
var num6Img = new Image()
num6Img.src = 'images/num6.png'
// 修改添加数字6图片对象的属性
var num6 = {
img: num6Img,
width: 100,
height: 100
}
// 加载数字7图片
var num7Img = new Image()
num7Img.src = 'images/num7.png'
// 修改添加数字7图片对象的属性
var num7 = {
img: num7Img,
width: 100,
height: 100
}
// 加载数字8图片
var num8Img = new Image()
num8Img.src = 'images/num8.png'
// 修改添加数字8图片对象的属性
var num8 = {
img: num8Img,
width: 100,
height: 100
}
// 加载数字9图片
var num9Img = new Image()
num9Img.src = 'images/num9.png'
// 修改添加数字9图片对象的属性
var num9 = {
img: num9Img,
width: 100,
height: 100
}
// 定义数字图片的数组
numAry = [num0, num1, num2, num3, num4, num5, num6, num7, num8, num9]
// 加载用时图片
var usedTimeImg = new Image()
usedTimeImg.src = 'images/usedTime.png'
// 修改添加用时图片对象的属性
var usedTime = {
img: usedTimeImg,
width: 300,
height: 100
}
// 加载剩余图片
var remainBombImg = new Image()
remainBombImg.src = 'images/remainBomb.png'
// 修改添加剩余图片对象的属性
var remainBomb = {
img: remainBombImg,
width: 300,
height: 100
}
var remainBombNum = 0
// 加载开始游戏图片
var startGameImg = new Image()
startGameImg.src = 'images/startGame.png'
// 修改添加开始游戏图片对象的属性
var startGame = {
img: startGameImg,
width: 233,
height: 93,
func: function () {
gameState = 'GAMEING'
// 生成新游戏
generateBasicInfo(blockWidthNumOutter, blockHeightNumOutter, bombNum)
// 开始计时器
gameTime = setInterval(function () {
startTime++
}, 1000)
}
}
// 加载无尽模式图片
var endlessModeImg = new Image()
endlessModeImg.src = 'images/endlessMode.png'
// 修改添加无尽模式图片对象的属性
var endlessMode = {
img: endlessModeImg,
width: 233,
height: 93,
func: function () {
gameState = 'ENDLESS'
// 生成新游戏
generateBasicInfo(blockWidthNumOutter, blockHeightNumOutter, bombNum, mode = 1)
gameTime = setInterval(function () {
startTime++
}, 1000)
}
}
// 加载游戏设置图片
var optionImg = new Image()
optionImg.src = 'images/option.png'
// 修改添加游戏设置图片对象的属性
var option = {
img: optionImg,
width: 233,
height: 93,
func: function () {
gameState = 'OPTION'
}
}
// 加载游戏教程图片
var gameTutorialImg = new Image()
gameTutorialImg.src = 'images/gameTutorial.png'
// 修改添加游戏教程图片对象的属性
var gameTutorial = {
img: gameTutorialImg,
width: 233,
height: 93,
func: function () {
alert('未开发功能')
}
}
buttonAry = [startGame, endlessMode, option, gameTutorial]
// 加载简单图片
var easyImg = new Image()
easyImg.src = 'images/easy.png'
// 修改添加简单图片对象的属性
var easy = {
img: easyImg,
width: 233,
height: 93,
isSelected: false,
func: function () {
// 重新定义游戏宽度及高度
blockWidthNumOutter = 10
blockHeightNumOutter = 5
difficultyNum = 0.1
// 重新定义炸弹占比
bombNum = parseInt(blockWidthNumOutter * blockHeightNumOutter * difficultyNum)
// 重新定义是否需要生成预览图
doGenerateExample = false
// 定义分数系数
pointCoefficient = (blockWidthNumOutter / 10) * (blockHeightNumOutter / 5) * (difficultyNum / 0.1) * 3
// 将其它选项调整为未选中
for (var i = 0; i < difficulty.length; i++) {
difficulty[i].isSelected = false
}
// 将本选项调整为已经选中
easy.isSelected = true
}
}
// 加载普通图片
var normalImg = new Image()
normalImg.src = 'images/normal.png'
// 修改添加普通图片对象的属性
var normal = {
img: normalImg,
width: 233,
height: 93,
isSelected: true,
func: function () {
// 重新定义游戏宽度及高度
blockWidthNumOutter = 20
blockHeightNumOutter = 10
difficultyNum = 0.2
// 重新定义炸弹占比
bombNum = parseInt(blockWidthNumOutter * blockHeightNumOutter * difficultyNum)
// 重新定义是否需要生成预览图
doGenerateExample = true
// 定义分数系数
pointCoefficient = (blockWidthNumOutter / 10) * (blockHeightNumOutter / 5) * (difficultyNum / 0.1) * 3
// 将其它选项调整为未选中
for (var i = 0; i < difficulty.length; i++) {
difficulty[i].isSelected = false
}
normal.isSelected = true
}
}
// 加载困难图片
var difficultImg = new Image()
difficultImg.src = 'images/difficult.png'
// 修改添加困难图片对象的属性
var difficult = {
img: difficultImg,
width: 233,
height: 93,
isSelected: false,
func: function () {
// 重新定义游戏宽度及高度
blockWidthNumOutter = 30
blockHeightNumOutter = 15
difficultyNum = 0.25
// 重新定义炸弹占比
bombNum = parseInt(blockWidthNumOutter * blockHeightNumOutter * difficultyNum)
// 重新定义是否需要生成预览图
doGenerateExample = false
// 定义分数系数
pointCoefficient = (blockWidthNumOutter / 10) * (blockHeightNumOutter / 5) * (difficultyNum / 0.1) * 3
// 将其它选项调整为未选中
for (var i = 0; i < difficulty.length; i++) {
difficulty[i].isSelected = false
}
difficult.isSelected = true
}
}
// 加载返回图片
var backImg = new Image()
backImg.src = 'images/back.png'
// 修改添加困难图片对象的属性
var back = {
img: backImg,
width: 233,
height: 93,
func: function () {
gameState = 'COVER'
}
}
// 加载已选择图片
var selectedImg = new Image()
selectedImg.src = 'images/selected.png'
// 修改添加已选择图片对象的属性
var selected = {
img: selectedImg,
width: 233,
height: 93
}
var difficulty = [easy, normal, difficult, back]
// 加载恭喜图片
var floatingWindowWinImg = new Image()
floatingWindowWinImg.src = 'images/floatingWindowWin.png'
// 修改添加恭喜图片对象的属性
var floatingWindowWin = {
img: floatingWindowWinImg,
width: 402,
height: 402
}
// 加载再接再厉图片
var floatingWindowLoseImg = new Image()
floatingWindowLoseImg.src = 'images/floatingWindowLose.png'
// 修改添加再接再厉图片对象的属性
var floatingWindowLose = {
img: floatingWindowLoseImg,
width: 402,
height: 402
}
// 加载楼梯图片
var ladderImg = new Image()
ladderImg.src = 'images/ladder.png'
// 修改添加楼梯图片对象的属性
var ladder = {
img: ladderImg,
width: 100,
height: 100
}
// 导入视频
var bombExplode = importVideo('bombExplode', 17, 4, intervalTime = 1000 / 60)
var electric = importVideo('electric_', 18, 5)
// 初始化需要绘制动画的炸弹列表
var bombAry = []
// 定义数一圈的坐标增量
var aroundAry = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]
// 画布宽高
var canvasWidth = 1400
var canvasHeight = 800
// 定义扫雷游戏难度 默认为普通
normal.func()
// 定义用于存储地图的二维列表
var blockInfoAry
// 定义用于存储方格是否挖开的二维列表
var blockisDigAry
// 定义系数以及增值
var coefficient
var xAddNum
var yAddNum
var side
// 定义游戏状态
var gameState = 'COVER'
// 定义游戏时间并初始化为0
var startTime = 0
// 定义是否要生成预览图的变量
var doGenerateExample = false
// 定义游戏板分数及其系数
var boardScore = 0
var pointCoefficient
window.onload = function () {
// 定义鼠标点击事件
canvas.onclick = clickResponse
// 定义屏幕刷新率
setInterval(gameHandler, 1000 / 30)
}
}
// 定时器执行的函数
function gameHandler() {
// 清空画布
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
if (gameState == 'COVER') {
// 绘制背景颜色
ctx.drawImage(cover.img, 0, 0, canvasWidth, canvasHeight)
drawButton(800, 100, buttonAry)
} else if (gameState == 'GAMEING') {
// 绘制背景颜色
ctx.drawImage(backgroundColor.img, 0, 0, canvasWidth, canvasHeight)
// 绘制方块
drawBlock(blockWidthNumOutter, blockHeightNumOutter)
// 绘制炸弹
drawBomb(blockWidthNumOutter, blockHeightNumOutter)
// 绘制遮挡方块
drawBaffle(blockWidthNumOutter, blockHeightNumOutter)
// 绘制计时器
timer()
} else if (gameState == 'OPTION') {
// 绘制背景颜色
ctx.drawImage(backgroundColor.img, 0, 0, canvasWidth, canvasHeight)
// 绘制难度按钮
drawButton(200, 150, difficulty, mode = 'DIFFICULTY', 50)
} else if (gameState.substr(0, 8) == 'GAMEOVER') {
// 截取游戏结束是胜利还是失败
var overType = gameState.substr(9)
// 绘制背景颜色
ctx.drawImage(backgroundColor.img, 0, 0, canvasWidth, canvasHeight)
// 绘制方块
drawBlock(blockWidthNumOutter, blockHeightNumOutter)
// 绘制炸弹
drawBomb(blockWidthNumOutter, blockHeightNumOutter)
// 绘制遮挡方块
drawBaffle(blockWidthNumOutter, blockHeightNumOutter)
// 绘制计时器
timer()
// 绘制爆炸后的闪电
playVideo(electric, stayLastFrame = false, loopPlayback = false)
// 绘制结束浮窗
drawFloatWindow()
} else if (gameState == 'ENDLESS') {
// 绘制背景颜色
ctx.drawImage(backgroundColor.img, 0, 0, canvasWidth, canvasHeight)
// 绘制方块
drawBlock(blockWidthNumOutter, blockHeightNumOutter)
// 绘制炸弹
drawBomb(blockWidthNumOutter, blockHeightNumOutter)
// 绘制遮挡方块
drawBaffle(blockWidthNumOutter, blockHeightNumOutter)
// 绘制计时器
timer()
}
}
// 响应鼠标右键
document.oncontextmenu = function (e) {
//点击右键后要执行的代码
// 计算当前在画布上点击的位置
var clickXY = windowToCanvas(event.clientX, event.clientY)
var clickX = clickXY.x
var clickY = clickXY.y
// 计算存储的坐标
var aryPosX = parseInt((clickX - xAddNum) / coefficient)
var aryPosY = parseInt((clickY - yAddNum) / coefficient)
if (blockisDigAry[aryPosY + 1][aryPosX + 1] != 'D') {
if (blockisDigAry[aryPosY + 1][aryPosX + 1] == 'U' && remainBombNum > 0) {
blockisDigAry[aryPosY + 1][aryPosX + 1] = 'S'
remainBombNum = remainBombNum - 1
} else if (blockisDigAry[aryPosY + 1][aryPosX + 1] == 'S') {
blockisDigAry[aryPosY + 1][aryPosX + 1] = 'U'
remainBombNum = remainBombNum + 1
}
}
return false;//阻止浏览器的默认弹窗行为
}
// 响应鼠标点击的函数
function clickResponse() {
// 计算当前在画布上点击的位置
var clickXY = windowToCanvas(event.clientX, event.clientY)
var clickX = clickXY.x
var clickY = clickXY.y
if (gameState == 'COVER') {
responseButton(800, 100, clickX, clickY, buttonAry)
} else if (gameState == 'GAMEING') {
// 计算存储的坐标
var aryPosX = parseInt((clickX - xAddNum) / coefficient)
var aryPosY = parseInt((clickY - yAddNum) / coefficient)
// 当用户点开了炸弹
if (blockInfoAry[aryPosY + 1][aryPosX + 1] == 'B' && blockisDigAry[aryPosY + 1][aryPosX + 1] != 'S') {
bombAry.push(startPlay(aryPosX, aryPosY, bombExplode, 1))
var radius = 5
bombAry.push(startPlay(aryPosX - radius, aryPosY - radius, electric, radius * 2 + 1))
// 游戏状态变为失败
gameState = 'GAMEOVER LOSE'
// 将点击的方块变为已打开的方块并加分
openBlock(aryPosY + 1, aryPosX + 1)
} else if (blockisDigAry[aryPosY + 1][aryPosX + 1] == 'U') {
// 将点击的方块变为已打开的方块并加分
openBlock(aryPosY + 1, aryPosX + 1)
// 如果点开的是0, 自动点开周边的方格
open0(aryPosX + 1, aryPosY + 1)
openBeside()
} else if (blockisDigAry[aryPosY + 1][aryPosX + 1] == 'D') {
openAround(aryPosX + 1, aryPosY + 1)
}
// 判断当前用户是否已经胜利
isWin()
} else if (gameState == 'OPTION') {
// 绘制难度按钮
responseButton(200, 200, clickX, clickY, difficulty, 50)
} else if (gameState.substr(0, 8) == 'GAMEOVER') {
// 重置分数
boardScore = 0
// 重置游戏时间
startTime = 0
// 初始化需要绘制动画的炸弹列表
bombAry = []
// 游戏状态调整为封面
gameState = 'COVER'
} else if (gameState == 'ENDLESS') {
// 计算存储的坐标
var aryPosX = parseInt((clickX - xAddNum) / coefficient)
var aryPosY = parseInt((clickY - yAddNum) / coefficient)
// 当用户点开了炸弹
if (blockInfoAry[aryPosY + 1][aryPosX + 1] == 'B' && blockisDigAry[aryPosY + 1][aryPosX + 1] != 'S') {
// 播放炸弹旋转的图片
bombAry.push(startPlay(aryPosX, aryPosY, bombExplode, 1))
// 定义绘制闪电爆炸的范围
var radius = 5
bombAry.push(startPlay(aryPosX - radius, aryPosY - radius, electric, radius * 2 + 1))
// 游戏状态变为失败
gameState = 'GAMEOVER LOSE'
// 清空计时器
clearInterval(gameTime)
// 将点击的方块变为已打开的方块并加分
openBlock(aryPosY + 1, aryPosX + 1)
} else if (blockInfoAry[aryPosY + 1][aryPosX + 1] == 'S' && blockisDigAry[aryPosY + 1][aryPosX + 1] != 'U') { // 下楼
// 对游戏进行难度提高
blockWidthNumOutter += 2
blockHeightNumOutter += 1
difficultyNum += 0.03
// 下楼加分
boardScore += 30 * coefficient
bombNum = blockWidthNumOutter * blockHeightNumOutter * difficultyNum
// 将点击的方块变为已打开的方块并加分
openBlock(aryPosY + 1, aryPosX + 1)
// 生成新游戏
generateBasicInfo(blockWidthNumOutter, blockHeightNumOutter, bombNum, mode = 1)
} else if (blockisDigAry[aryPosY + 1][aryPosX + 1] == 'U') {
// 将点击的方块变为已打开的方块并加分
openBlock(aryPosY + 1, aryPosX + 1)
} else if (blockisDigAry[aryPosY + 1][aryPosX + 1] == 'D') {openAround(aryPosX + 1, aryPosY + 1)}
// 如果点开的是0, 自动点开周边的方格
open0(aryPosX + 1, aryPosY + 1)
openBeside()
// 判断当前用户是否已经胜利
isWin(mode = 1)
}
}
function timer() {
// 定义绘制方块靠近边缘最大限度
var rowEdge = 50
var columnEdge = 50
// 绘制“用时:”
ctx.drawImage(usedTime.img, rowEdge, 0, columnEdge * 3, columnEdge)
// 绘制用时
for (var i = 0; i < String(startTime).length; i++) {
ctx.drawImage(numAry[String(startTime)[i]].img, rowEdge + columnEdge * (i + 3), 0, columnEdge, columnEdge)
}
// 绘制“剩余:”
ctx.drawImage(remainBomb.img, rowEdge + columnEdge * (i + 4), 0, columnEdge * 3, columnEdge)
// 重新定义开始绘制横坐标
rowEdge = rowEdge + columnEdge * (i + 4)
// 绘制剩余炸弹个数
for (var i = 0; i < String(remainBombNum).length; i++) {
ctx.drawImage(numAry[String(remainBombNum)[i]].img, rowEdge + columnEdge * (i + 3), 0, columnEdge, columnEdge)
}
}
function countPoint() {
// 截取游戏结束是胜利还是失败
var overType = gameState.substr(9)
var remainBombNum = 0
for (var i = 1; i < blockHeightNumOutter + 1; i++) {
for (var j = 1; j < blockWidthNumOutter + 1; j++) {
if (overType == 'LOSE') {
if (blockInfoAry[i][j] == 'B' && blockisDigAry[i][j] == 'S') {
// 标记了炸弹加分
boardScore += 20 * pointCoefficient
}
} else if (overType == 'WIN')
if (blockInfoAry[i][j] == 'B') {
// 标记了炸弹加分
boardScore += 20 * pointCoefficient
}
}
}
}
function CalTotalScore() {
pointCoefficient = (blockWidthNumOutter / 10) * (blockHeightNumOutter / 5) * (difficultyNum / 0.1) * 3
totalScore = Math.round((boardScore * pointCoefficient * 0.2) / (startTime * 0.01 + 1) / 3)
return totalScore
}
function importVideo(fileTypeName, frameNum, digtal, intervalTime = 1000 / 30, stayLastFrame = true, loopPlayback = false) {
// 定义存储视频图片的数组
var video = []
// 定义存储待加入的数字
var num
for (var i = 1; i <= frameNum; i++) {
// 计算前方补0的文件名
num = ''
for (var j = 0; j < digtal - String(i).length; j++) {num += '0'}
num += String(i)
// 根据文件名导入文件
var img = new Image()
img.src = 'videos/' + fileTypeName + '/' + fileTypeName + num + '.png'
// 将文件对象存入列表中
video.push(img)
}
// 生成视频对象
obj = {
video: video, // 存储视频图片序列
doPlay: false, // 定义是否需要绘制
x: 0, // 定义绘制坐标
y: 0, // 定义绘制坐标
frameNum: 0, // 定义当前视频播放到第几帧
intervalTime: intervalTime, // 定义视频FPS
side: 100, // 定义视频缩放
lastTime: new Date().getTime(), // 获得当前时间戳
stayLastFrame: stayLastFrame, // 视频是否需要停止在最后一帧
loopPlayback: loopPlayback // 视频是否需要循环播放
}
// 返回列表
return obj
}
function startPlay(x, y, obj, sideMultiple = 1) {
// 调整为开始播放
obj.doPlay = true
// 计算好绘制坐标
obj.x = x * coefficient + xAddNum
obj.y = y * coefficient + yAddNum
// 存入缩放大小
obj.side = side * sideMultiple
// 调整为从头播放
obj.frameNum = 0
return obj
}
function playVideo(obj) {
currentTime = new Date().getTime()
if (currentTime - obj.lastTime >= obj.intervalTime) {
// 如果图片的下标等于数组的长度,则重置图片下标为0
if (obj.frameNum < obj.video.length) {
ctx.drawImage(obj.video[obj.frameNum], obj.x, obj.y, obj.side, obj.side)
// 加 1 得到下一张图片的下标
obj.frameNum++
} else if (obj.stayLastFrame == true) {
ctx.drawImage(obj.video[obj.video.length - 1], obj.x, obj.y, obj.side, obj.side)
} else if (obj.loopPlayback == true) {
obj.frameNum = 0
}
}
}
function drawFloatWindow() {
// 根据结束状态绘制不同类型的浮窗
if (gameState.substr(9) == 'WIN') {
obj = floatingWindowWin
} else if (gameState.substr(9) == 'LOSE') {
obj = floatingWindowLose
}
// 计算绘制的坐标
drawX = canvasWidth / 2 - obj.width / 2
drawY = canvasHeight / 2 - obj.height / 2
// 绘制浮窗图片
ctx.drawImage(obj.img, drawX, drawY)
var textList = ['用时: ' + startTime + '秒', '游戏板得分: ' + Math.round(String(boardScore)), '总分: ' + CalTotalScore()]
for (var i = 0; i < 3; i++) {
ctx.font = "30px Arial"
ctx.fillStyle = "#000000"
ctx.fillText(textList[i], drawX + 60, drawY + 202 + 60 * i)
}
}
function openBlock(blockX, blockY) {
// 将点击的方块变为已打开的方块
blockisDigAry[blockX][blockY] = 'D'
// 随机加分
boardScore = boardScore + (Math.random() * (5 - 2) + 2)
}
function isWin(mode = 0) {
var remainBombNum = 0
for (var i = 1; i < blockHeightNumOutter + 1; i++) {
for (var j = 1; j < blockWidthNumOutter + 1; j++) {
if (blockInfoAry[i][j] == 'B' && blockisDigAry[i][j] == 'D') {
if (mode == 0)
// 清空计时器
clearInterval(gameTime)
// 修改游戏状态
gameState = 'GAMEOVER LOSE'
return 0
}
// 将所有已打开的0格周边的方格打开
if (blockInfoAry[i][j] != 'B' && blockisDigAry[i][j] == 'U') {
remainBombNum++
}
}
}
if (remainBombNum == 0 && mode == 0) {
// 清空计时器
clearInterval(gameTime)
// 修改游戏状态
gameState = 'GAMEOVER WIN'
// 胜利加分
boardScore += 100 * (blockWidthNumOutter / 10) * (blockHeightNumOutter / 5) * (difficultyNum / 0.1) * 3
}
}
function drawItem(x, y, obj) {
// 计算绘制的坐标
drawX = x * coefficient + xAddNum
drawY = y * coefficient + yAddNum
// 绘制一个方块
ctx.drawImage(obj.img, drawX, drawY, side, side)
}
function responseButton(x, y, _clickX, _clickY, _buttonAry, gape = 20) {
// 循环判断点击的按钮并执行其功能
for (var i = 0; i < _buttonAry.length; i++) {
// 判断是否点击在按钮上
if (_clickX >= x && _clickX <= x + _buttonAry[i].width && _clickY >= i * (_buttonAry[i].height + gape) + y && _clickY <= i * (_buttonAry[i].height + gape) + y + _buttonAry[i].height) {
// 执行其功能
_buttonAry[i].func()
}
}
}
function drawButton(x, y, _buttonAry, mode, gape = 20) {
// 循环绘制按钮
for (var i = 0; i < _buttonAry.length; i++) {
if (mode == 'DIFFICULTY' && _buttonAry[i].isSelected == true) {
ctx.drawImage(selected.img, x, i * (_buttonAry[i].height + gape) + y)
}
ctx.drawImage(_buttonAry[i].img, x, i * (_buttonAry[i].height + gape) + y)
}
}
function drawBlock(widthNum, heightNum) {
// 绘制底部方块
for (var i = 0; i < heightNum; i++) {
for (var j = 0; j < widthNum; j++) {
drawItem(j, i, block)
}
}
}
function drawBomb(widthNum, heightNum) {
// 根据参数绘制场上的炸弹
for (var i = 1; i < heightNum + 1; i++) {
for (var j = 1; j < widthNum + 1; j++) {
if (blockisDigAry[i][j] == 'D') {
if (blockInfoAry[i][j] == 'B' && bombExplode.doPlay == true) {
for (var k = 0; k < bombAry.length; k++) {
playVideo(bombAry[k])
}
} else if (blockInfoAry[i][j] == 'S') {// 如果为楼梯则绘制楼梯
drawItem(j - 1, i - 1, ladder)
} else if (blockInfoAry[i][j] != 0) {// 如果为数字则绘制相应数字
// console.log(blockInfoAry[i][j])
drawItem(j - 1, i - 1, numAry[blockInfoAry[i][j]])
}
}
}
}
}
function drawBaffle(widthNum, heightNum) {
// 绘制上方的挡板
for (var i = 1; i < heightNum + 1; i++) {
for (var j = 1; j < widthNum + 1; j++) {
// 如果为未打开的方块则绘制挡板
if (blockisDigAry[i][j] == 'U') {
drawItem(j - 1, i - 1, undigBlock)
}
else if (blockisDigAry[i][j] == 'S') {
drawItem(j - 1, i - 1, undigBlock)
drawItem(j - 1, i - 1, signedBlock)
}
}
}
}
function open0(x, y) {
if (blockInfoAry[y][x] == 0) {
// 将该方块变为已打开并加分
openBlock(y, x)
// 递归算法找出所有未打开的连续的方格
if (0 < y - 1 && blockisDigAry[y - 1][x] != 'D') {
if (0 < x - 1 && blockisDigAry[y - 1][x - 1] != 'D') {
open0(x - 1, y - 1)
}
open0(x, y - 1)
if (x + 1 < blockWidthNumOutter + 1 && blockisDigAry[y - 1][x + 1] != 'D') {
open0(x + 1, y - 1)
}
}
if (0 < x - 1 && blockisDigAry[y][x - 1] != 'D') {
open0(x - 1, y)
}
if (x + 1 < blockWidthNumOutter + 1 && blockisDigAry[y][x + 1] != 'D') {
open0(x + 1, y)
}
if (y + 1 < blockHeightNumOutter + 1 && blockisDigAry[y + 1][x] != 'D') {
if (0 < x - 1 && blockisDigAry[y + 1][x - 1] != 'D') {
open0(x - 1, y + 1)
}
open0(x, y + 1)
if (x + 1 < blockWidthNumOutter + 1 && blockisDigAry[y + 1][x + 1] != 'D') {
open0(x + 1, y + 1)
}
}
}
}
function openBeside() {
for (var i = 1; i < blockHeightNumOutter + 1; i++) {
for (var j = 1; j < blockWidthNumOutter + 1; j++) {
// 将所有已打开的0格周边的方格打开
if (blockInfoAry[i][j] == 0 && blockisDigAry[i][j] == 'D') {
// 加分
boardScore = boardScore + 1
blockisDigAry[i - 1][j - 1] = 'D'
blockisDigAry[i - 1][j] = 'D'
blockisDigAry[i - 1][j + 1] = 'D'
blockisDigAry[i][j - 1] = 'D'
blockisDigAry[i][j + 1] = 'D'
blockisDigAry[i + 1][j - 1] = 'D'
blockisDigAry[i + 1][j] = 'D'
blockisDigAry[i + 1][j + 1] = 'D'
}
}
}
}
function openAround(x, y) {
// 将计数器重置为0
cnt = 0
// 数周边一圈的数字
for (var i = 0; i < aroundAry.length; i++) {
if (blockisDigAry[y + aroundAry[i][1]][x + aroundAry[i][0]] == 'S') {
cnt++
}
}
if (blockInfoAry[y][x] == cnt && cnt != 0) {
// 遍历周边一圈的数字
for (var i = 0; i < aroundAry.length; i++) {
var tmpX = x + aroundAry[i][0]
var tmpY = y + aroundAry[i][1]
if (blockInfoAry[tmpY][tmpX] == 'B' && blockisDigAry[tmpY][tmpX] != 'S') {
console.log((tmpX + tmpY + bombExplode + ' '))
// 播放炸弹旋转的图片
bombAry.push(startPlay(tmpX - 1, tmpY - 1, bombExplode, 1))
// 定义绘制闪电爆炸的范围
var radius = 5
bombAry.push(startPlay(tmpX - radius - 1, tmpY - radius - 1, electric, radius * 2 + 1))
// 游戏状态变为失败
gameState = 'GAMEOVER LOSE'
// 清空计时器
clearInterval(gameTime)
// 标记为已打开
blockisDigAry[tmpY][tmpX] = 'D'
// 随机加分
boardScore = boardScore + (Math.random() * (5 - 2) + 2)
} else if (blockisDigAry[tmpY][tmpX] != 'S') {
// 标记为已打开
blockisDigAry[tmpY][tmpX] = 'D'
// 随机加分
boardScore = boardScore + (Math.random() * (5 - 2) + 2)
open0(tmpX, tmpY)
openBeside(tmpX, tmpY)
}
}
}
}
function generateBasicInfo(widthNum, heightNum, num, mode = 0, gape = 2) {
// gape为每个方块之间的距离
// 根据参数生成游戏中的界面绘制的参数
// 定义绘制方块靠近边缘最大限度
var rowEdge = 50
var columnEdge = 50
// 定义绘制区域的高度和宽度
var drawAreaWidth = canvasWidth - rowEdge * 2
var drawAreaHeight = canvasHeight - columnEdge * 2
// 定义绘制方块的宽度和长度
var drawWidth = (drawAreaWidth / widthNum) - gape
var drawHeight = (drawAreaHeight / heightNum) - gape
// 为保证绘画的方格是正方形并不越界, 绘制边长最短的情况
side = Math.min(drawWidth, drawHeight)
// 定义绘制的坐标
var drawX
var drawY
// 为使目标在画布中央, 定义绘制坐标增值
var startX = (drawAreaWidth / 2) - ((side + gape) * widthNum) / 2
var startY = (drawAreaHeight / 2) - ((side + gape) * heightNum) / 2
// 计算系数以及增值
coefficient = side + gape
xAddNum = rowEdge + startX + (gape / 2)
yAddNum = columnEdge + startY + (gape / 2)
// 初始化保存方格的二维列表
blockInfoAry = []
var temp;
for (var i = 0; i < heightNum + 2; i++) {
temp = []
for (var j = 0; j < widthNum + 2; j++) {
if (i == 0 || j == 0 || i == heightNum + 1 || j == widthNum + 1) {
temp.push('E')
} else {
temp.push(0)
}
}
blockInfoAry.push(temp)
}
// 初始化保存挡板的二维列表
blockisDigAry = []
var temp;
for (var i = 0; i < heightNum + 2; i++) {
temp = []
for (var j = 0; j < widthNum + 2; j++) {temp.push('U')}
blockisDigAry.push(temp)
}
// 剩余炸弹数量为炸弹数量
remainBombNum = parseInt(num)
// 随机生成炸弹
for (var i = 0; i < num; i++) {
// 随机生成坐标
x = parseInt(Math.random() * (widthNum) + 1)
y = parseInt(Math.random() * (heightNum) + 1)
if (blockInfoAry[y][x] != 'B') {
blockInfoAry[y][x] = 'B'
} else {
i--
}
}
// 根据炸弹生成数字
var cnt // 用于记录周边一圈的数字
for (var i = 1; i < heightNum + 1; i++) {
for (var j = 1; j < widthNum + 1; j++) {
if (blockInfoAry[i][j] != 'B') {
// 将计数器重置为0
cnt = 0
// 数周边一圈的数字
for (var k = 0; k < aroundAry.length; k++) {
if (blockInfoAry[i + aroundAry[k][1]][j + aroundAry[k][0]] == 'B') {
cnt++
}
}
blockInfoAry[i][j] = cnt
}
}
}
// 如果是无尽模式需生成楼梯就随机生成楼梯
while (mode == 1) {
// 随机生成坐标
x = parseInt(Math.random() * (widthNum) + 1)
y = parseInt(Math.random() * (heightNum) + 1)
// 如果不是炸弹就绘制楼梯
if (blockInfoAry[y][x] != 'B') {
blockInfoAry[y][x] = 'S'
break
}
}
// 随机点开非炸弹方格
for (var i = 0; i < blockWidthNumOutter * blockHeightNumOutter * pointCoefficient * 0.002; i++) {
// 随机生成坐标
x = parseInt(Math.random() * (blockWidthNumOutter) + 1)
y = parseInt(Math.random() * (blockHeightNumOutter) + 1)
if (blockInfoAry[y][x] != 'B') {
blockisDigAry[y][x] = 'D'
// 如果点开的是0, 自动点开周边的方格
open0(x, y)
openBeside()
} else {
i--
}
}
}
function windowToCanvas(x, y) {
// 获取用户点击坐标
var cvsbox = canvas.getBoundingClientRect()
return { x: Math.round(x - cvsbox.left), y: Math.round(y - cvsbox.top) }
}
</script>
</body>
</html>

2173

被折叠的 条评论
为什么被折叠?



