js+canvas實(shí)現(xiàn)簡(jiǎn)單掃雷小游戲
掃雷小游戲作為windows自帶的一個(gè)小游戲,受到很多人的喜愛(ài),今天我們就來(lái)嘗試使用h5的canvas結(jié)合js來(lái)實(shí)現(xiàn)這個(gè)小游戲。
要寫(xiě)游戲,首先要明確游戲的規(guī)則,掃雷游戲是一個(gè)用鼠標(biāo)操作的游戲,通過(guò)點(diǎn)擊方塊,根據(jù)方塊的數(shù)字推算雷的位置,標(biāo)記出所有的雷,打開(kāi)所有的方塊,即游戲成功,若點(diǎn)錯(cuò)雷的位置或標(biāo)記雷錯(cuò)誤,則游戲失敗。
具體的游戲操作如下
1.可以通過(guò)鼠標(biāo)左鍵打開(kāi)隱藏的方塊,打開(kāi)后若不是雷,則會(huì)向四個(gè)方向擴(kuò)展
2.可以通過(guò)鼠標(biāo)右鍵點(diǎn)擊未打開(kāi)的方塊來(lái)標(biāo)記雷,第二次點(diǎn)擊取消標(biāo)記
3.可以通過(guò)鼠標(biāo)右鍵點(diǎn)擊已打開(kāi)且有數(shù)字的方塊來(lái)檢查當(dāng)前方塊四周的標(biāo)記是否正確
接下來(lái)開(kāi)始編寫(xiě)代碼
首先寫(xiě)好HTML的結(jié)構(gòu),這里我簡(jiǎn)單地使用一個(gè)canvas標(biāo)簽,其他內(nèi)容的擴(kuò)展在之后實(shí)現(xiàn)(游戲的規(guī)則,游戲的難度設(shè)置)
<!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>
接下來(lái)我們來(lái)初始化一些內(nèi)容。包括canvas畫(huà)布的寬高,游戲共有幾行幾列,幾個(gè)雷,每個(gè)格子的大小。
//獲取canvas畫(huà)布
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
canvas.width = 480;
canvas.height = 480;
//定義各屬性
let R = 3; //格子圓角半徑
let L = 15; //每個(gè)格子實(shí)際長(zhǎng)
let P = 16; //每個(gè)格子占長(zhǎng)
let row = 30; //行數(shù)
let col = 30; //列數(shù)
let N = 50; //雷數(shù)
為了后面的操作,我要用幾個(gè)數(shù)組來(lái)儲(chǔ)存一些位置,一個(gè)方塊是否為雷的數(shù)組,該數(shù)組用于描繪出整個(gè)畫(huà)面每個(gè)方塊對(duì)應(yīng)的內(nèi)容;一個(gè)數(shù)組用于描述方塊狀態(tài),即是否打開(kāi)或者被標(biāo)記;一個(gè)數(shù)組用來(lái)記載生成的雷的位置;一個(gè)數(shù)組用來(lái)記載標(biāo)記的位置。
var wholeArr = drawInitialize(row, col, N, R, L, P);
var gameArr = wholeArr[0] //位置數(shù)組
var bombArr = wholeArr[1] //雷的位置數(shù)組
var statusArr = zoneInitialize(row, col); //狀態(tài)數(shù)組 0為未打開(kāi)且未標(biāo)記 1為打開(kāi) 2為標(biāo)記
var signArr = []; //標(biāo)記數(shù)組
//畫(huà)出初始界面
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);//該方法用于繪制整個(gè)畫(huà)面,下面會(huì)寫(xiě)出聲明
}
}
return arr;
}
//初始化
function initialize(row, col, n) {
let gameArr = zoneInitialize(row, col); //生成沒(méi)有標(biāo)記雷的矩陣
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); //將行的每個(gè)位置用0填充
for (let i = 0; i < row; i++)
rArr[i] = [...cArr];
return rArr;
}
//隨機(jī)生成雷
function bombProduce(n, arr, row, col) { //隨機(jī)生成n個(gè)雷
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];
}
//標(biāo)記數(shù)字
function signArrNum(gArr, bArr, n, row, col) {
for (let i = 0; i < n; i++) { //為每個(gè)雷的四周的非雷的數(shù)字標(biāo)記加一
let r = parseInt(analyseStr(bArr[i]).row);
let c = parseInt(analyseStr(bArr[i]).col);
if (r > 0 && gArr[c][r - 1] != -1)//判斷該位置是否為雷,是則不進(jìn)行操作
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}`;
}
//解析雷數(shù)組字符串
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] };
}
接下來(lái)將繪制的方法寫(xiě)出來(lái),這里我使用紅色的方塊來(lái)代表雷
//畫(huà)出單個(gè)方塊
function drawRct(x, y, l, r, color, container = context) {//x,y為繪制的位置,l為方塊的邊長(zhǎng),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();
}
//畫(huà)出方塊上對(duì)應(yīng)的數(shù)字
function drawNum(x, y, l, r, alPha, color = 'rgb(0,0,0)', container = context) {//參數(shù)含義與上面的方法一樣,alPha為要寫(xiě)的數(shù)字
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();
}
//畫(huà)出游戲結(jié)束界面
function drawEnd(row, col, R, L, P) {
for (let r = 0; r < row; r++) {
for (let c = 0; c < col; c++) {//將整個(gè)界面繪制出來(lái)
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);
}
}
}
接下來(lái)寫(xiě)出點(diǎn)擊事件的處理,這里對(duì)于點(diǎn)擊后的向四個(gè)方向擴(kuò)展,我采用了以下圖片所示的擴(kuò)展

如上圖片,在點(diǎn)擊時(shí)在點(diǎn)擊位置往四周擴(kuò)散,之后上下的按上下方向繼續(xù)擴(kuò)散,左右的除本方向外還有往上下方向擴(kuò)散,在遇到數(shù)字時(shí)停下。
canvas.onclick = function(e) {
e = e || window.e;
let x = e.clientX - canvas.offsetLeft;
let y = e.clientY - canvas.offsetTop; //獲取鼠標(biāo)在canvas畫(huà)布上的坐標(biāo)
let posX = Math.floor(x / P);
let posY = Math.floor(y / P);//將坐標(biāo)轉(zhuǎn)化為數(shù)組下標(biāo)
if (gameArr[posX][posY] === -1 && statusArr[posX][posY] !== 2) { //點(diǎn)到雷
alert('error');
drawEnd(row, col, R, L, P);
} else if (statusArr[posX][posY] === 0) {
this.style.cursor = "auto";
statusArr[posX][posY] = 1;//重置狀態(tài)
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();//游戲成功,在下面代碼定義
}
//右鍵標(biāo)記雷,取消標(biāo)記,檢查四周
canvas.oncontextmenu = function(e) {
e = e || window.e;
let x = e.clientX - canvas.offsetLeft;
let y = e.clientY - canvas.offsetTop; //獲取鼠標(biāo)在canvas畫(huà)布上的坐標(biāo)
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) //檢查四周雷數(shù)
checkBomb(posX, posY);
if (statusArr[posX][posY] === 0) { //標(biāo)記雷
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) { //取消標(biāo)記
drawRct(posX * P, posY * P, L, R, 'rgb(102,102,102)');
statusArr[posX][posY] = 0;
signArr = signArr.filter(item => {//使用過(guò)濾器方法將當(dāng)前位置的坐標(biāo)標(biāo)記清除
if (item === str)
return false;
return true;
})
}
gameComplete();
return false; //阻止事件冒泡
}
//自動(dòng)跳出數(shù)字
function outNum(arr, x, y, row, col, status) {//arr為傳入的數(shù)組,x,y為處理的位置,row,col為游戲的行列,status用于儲(chǔ)存擴(kuò)展的方向
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');
}
}
//跳出數(shù)字具體操作
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);
}
//檢查數(shù)字四周的雷的標(biāo)記并操作
function checkBomb(r, c) {
//1.檢查四周是否有被標(biāo)記確定的位置
//2.記下標(biāo)記的位置數(shù)count
//3.若count為0,則return;若count大于0,檢查是否標(biāo)記正確
//4.如果標(biāo)記錯(cuò)誤,提示游戲失敗,若標(biāo)記正確但數(shù)量不夠,則return跳出,若標(biāo)記正確且數(shù)量正確,將其余位置顯示出來(lái)
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;
}
}
接著寫(xiě)出找到所有雷的情況,即游戲成功通關(guān)
//成功找出所有的雷
function gameComplete() {
var count = new Set(signArr).size;
if (count != bombArr.length) //雷的數(shù)量不對(duì)
{
return false;
}
for (let i of signArr) { //雷的位置不對(duì)
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;
}
最后調(diào)用方法畫(huà)出游戲界面,這個(gè)調(diào)用要放在數(shù)組聲明之前,因?yàn)閿?shù)組那里也有繪制的方法,這個(gè)方法會(huì)覆蓋繪制方塊的畫(huà)面。
drawRct(0, 0, 800, 0, 'rgb(0,0,0)', context);
一個(gè)簡(jiǎn)單的掃雷游戲就這樣實(shí)現(xiàn)了(說(shuō)實(shí)話(huà)我覺(jué)得是簡(jiǎn)陋不是簡(jiǎn)單。。。。)
當(dāng)然這個(gè)只是游戲的初步實(shí)現(xiàn),其實(shí)這個(gè)游戲還可以增加難度設(shè)置,用圖片來(lái)表示雷,在點(diǎn)到雷的時(shí)候增加聲音等等,當(dāng)然這些也并不難,如果大家有興趣的話(huà)可以嘗試優(yōu)化這個(gè)游戲。
希望這篇博客能對(duì)大家有所幫助,也希望大神能指出我的不足。
附上一張丑爆的游戲界面

更多關(guān)于Js游戲的精彩文章,請(qǐng)查看專(zhuān)題:《JavaScript經(jīng)典游戲 玩不?!?/a>
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
JS動(dòng)態(tài)插入并立即執(zhí)行回調(diào)函數(shù)的方法
這篇文章主要介紹了JS動(dòng)態(tài)插入并立即執(zhí)行回調(diào)函數(shù)的方法,實(shí)例分析了動(dòng)態(tài)插入js文件及執(zhí)行回調(diào)函數(shù)的相關(guān)技巧,需要的朋友可以參考下2016-04-04
js parentElement和offsetParent之間的區(qū)別
這里主要說(shuō)的是 offsetParent 屬性,這個(gè)屬性在 MSDN 的文檔中也沒(méi)有解釋清楚,這就讓人更難理解這個(gè)屬性。 這幾天在網(wǎng)上找了些資料看看,再加上自己的一些測(cè)試,對(duì)此屬性有了那么一點(diǎn)的了解,在這里總結(jié)一下。2010-03-03
event.keyCode鍵碼值表 附只能輸入特定的字符串代碼
非常不錯(cuò)的應(yīng)用,讓文本框里只能輸入money大家看下具體的實(shí)現(xiàn)代碼,真是只有想到,原理很簡(jiǎn)單。2009-05-05
JS獲取月的最后一天與JS得到一個(gè)月份最大天數(shù)的實(shí)例代碼
本篇文章主要是對(duì)JS獲取月的最后一天與JS得到一個(gè)月份最大天數(shù)的實(shí)例代碼進(jìn)行了介紹,需要的朋友可以過(guò)來(lái)參考下,希望對(duì)大家有所幫助2013-12-12
JavaScript 32位整型無(wú)符號(hào)操作示例
所有整數(shù)字變量默認(rèn)都是有符號(hào)整數(shù),JavaScript 進(jìn)行位操作時(shí),是采用32位有符號(hào)整型,這意味著其轉(zhuǎn)換的結(jié)果也是32位有符號(hào)整型2013-12-12
一道超經(jīng)典js面試題Foo.getName()的故事
Foo.getName算是一道比較老的面試題了,大致百度了一下在17年就有相關(guān)文章在介紹它,下面這篇文章主要給大家介紹了關(guān)于一道超經(jīng)典js面試題Foo.getName()的相關(guān)資料,需要的朋友可以參考下2022-03-03
JS數(shù)組reduce你不得不知道的25個(gè)高級(jí)用法
reduce作為ES5新增的常規(guī)數(shù)組方法之一,對(duì)比f(wàn)orEach 、filter和map,在實(shí)際使用上好像有些被忽略,下面這篇文章主要給大家介紹了關(guān)于JS數(shù)組reduce你不得不知道的25個(gè)高級(jí)用法,需要的朋友可以參考下2021-06-06

