js+canvas實(shí)現(xiàn)簡(jiǎn)單掃雷小游戲
掃雷小游戲作為windows自帶的一個(gè)小游戲,受到很多人的喜愛,今天我們就來嘗試使用h5的canvas結(jié)合js來實(shí)現(xiàn)這個(gè)小游戲。
要寫游戲,首先要明確游戲的規(guī)則,掃雷游戲是一個(gè)用鼠標(biāo)操作的游戲,通過點(diǎn)擊方塊,根據(jù)方塊的數(shù)字推算雷的位置,標(biāo)記出所有的雷,打開所有的方塊,即游戲成功,若點(diǎn)錯(cuò)雷的位置或標(biāo)記雷錯(cuò)誤,則游戲失敗。
具體的游戲操作如下
1.可以通過鼠標(biāo)左鍵打開隱藏的方塊,打開后若不是雷,則會(huì)向四個(gè)方向擴(kuò)展
2.可以通過鼠標(biāo)右鍵點(diǎn)擊未打開的方塊來標(biāo)記雷,第二次點(diǎn)擊取消標(biāo)記
3.可以通過鼠標(biāo)右鍵點(diǎn)擊已打開且有數(shù)字的方塊來檢查當(dāng)前方塊四周的標(biāo)記是否正確
接下來開始編寫代碼
首先寫好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>
接下來我們來初始化一些內(nèi)容。包括canvas畫布的寬高,游戲共有幾行幾列,幾個(gè)雷,每個(gè)格子的大小。
//獲取canvas畫布 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ù)組來儲(chǔ)存一些位置,一個(gè)方塊是否為雷的數(shù)組,該數(shù)組用于描繪出整個(gè)畫面每個(gè)方塊對(duì)應(yīng)的內(nèi)容;一個(gè)數(shù)組用于描述方塊狀態(tài),即是否打開或者被標(biāo)記;一個(gè)數(shù)組用來記載生成的雷的位置;一個(gè)數(shù)組用來記載標(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為未打開且未標(biāo)記 1為打開 2為標(biāo)記 var signArr = []; //標(biāo)記數(shù)組 //畫出初始界面 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ì)寫出聲明 } } return arr; } //初始化 function initialize(row, col, n) { let gameArr = zoneInitialize(row, col); //生成沒有標(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] }; }
接下來將繪制的方法寫出來,這里我使用紅色的方塊來代表雷
//畫出單個(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(); } //畫出方塊上對(duì)應(yīng)的數(shù)字 function drawNum(x, y, l, r, alPha, color = 'rgb(0,0,0)', container = context) {//參數(shù)含義與上面的方法一樣,alPha為要寫的數(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(); } //畫出游戲結(jié)束界面 function drawEnd(row, col, R, L, P) { for (let r = 0; r < row; r++) { for (let c = 0; c < col; c++) {//將整個(gè)界面繪制出來 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); } } }
接下來寫出點(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畫布上的坐標(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畫布上的坐標(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 => {//使用過濾器方法將當(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ù)量正確,將其余位置顯示出來 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; } }
接著寫出找到所有雷的情況,即游戲成功通關(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)用方法畫出游戲界面,這個(gè)調(diào)用要放在數(shù)組聲明之前,因?yàn)閿?shù)組那里也有繪制的方法,這個(gè)方法會(huì)覆蓋繪制方塊的畫面。
drawRct(0, 0, 800, 0, 'rgb(0,0,0)', context);
一個(gè)簡(jiǎn)單的掃雷游戲就這樣實(shí)現(xiàn)了(說實(shí)話我覺得是簡(jiǎn)陋不是簡(jiǎn)單。。。。)
當(dāng)然這個(gè)只是游戲的初步實(shí)現(xiàn),其實(shí)這個(gè)游戲還可以增加難度設(shè)置,用圖片來表示雷,在點(diǎn)到雷的時(shí)候增加聲音等等,當(dāng)然這些也并不難,如果大家有興趣的話可以嘗試優(yōu)化這個(gè)游戲。
希望這篇博客能對(duì)大家有所幫助,也希望大神能指出我的不足。
附上一張丑爆的游戲界面
更多關(guān)于Js游戲的精彩文章,請(qǐng)查看專題:《JavaScript經(jīng)典游戲 玩不?!?/a>
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
ES6 迭代器(Iterator)和 for.of循環(huán)使用方法學(xué)習(xí)(總結(jié))
這篇文章主要介紹了ES6 迭代器(Iterator)和 for.of循環(huán)使用方法學(xué)習(xí)總結(jié),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02JS中通過url動(dòng)態(tài)獲取圖片大小的方法小結(jié)(兩種方法)
這篇文章主要介紹了JS中通過url動(dòng)態(tài)獲取圖片大小的方法小結(jié),本文給大家列舉了兩種方法,大家可以嘗試下看哪種方法好用,感興趣的朋友跟隨小編一起看看吧2018-10-10用JS實(shí)現(xiàn)簡(jiǎn)單的屏幕錄像機(jī)功能
這篇文章主要給大家介紹了如何用JS實(shí)現(xiàn)簡(jiǎn)單的屏幕錄像機(jī),文中通過代碼示例給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-12-12js實(shí)現(xiàn)的訂閱發(fā)布者模式簡(jiǎn)單示例
這篇文章主要介紹了js實(shí)現(xiàn)的訂閱發(fā)布者模式,結(jié)合完整示例形式分析了js訂閱發(fā)布者模式相關(guān)實(shí)現(xiàn)與使用方法,需要的朋友可以參考下2020-03-03微信小程序?qū)崿F(xiàn)云開發(fā)上傳文件、圖片功能
在使用小程序的過程中,在編輯個(gè)人資料時(shí),通常會(huì)面臨上傳頭像、上傳背景圖片的情況,而這個(gè)開發(fā)過程需要怎樣實(shí)現(xiàn)呢?這篇文章主要給大家介紹了關(guān)于微信小程序?qū)崿F(xiàn)云開發(fā)上傳文件、圖片功能的相關(guān)資料,需要的朋友可以參考下2022-12-12JS實(shí)現(xiàn)的合并多個(gè)數(shù)組去重算法示例
這篇文章主要介紹了JS實(shí)現(xiàn)的合并多個(gè)數(shù)組去重算法,涉及javascript數(shù)組遍歷、判斷、運(yùn)算、排序等相關(guān)操作技巧,需要的朋友可以參考下2018-04-04玩轉(zhuǎn)JavaScript函數(shù):apply/call/bind技巧
歡迎來到這篇關(guān)于JavaScript中apply、call、bind函數(shù)的指南,這里充滿了實(shí)用技巧和深入理解,讓你的編程之旅更加游刃有余,趕快翻開這個(gè)神秘的“魔法書”,讓我們一起探索吧!2024-01-01為JavaScript提供睡眠功能(sleep) 自編譯JS引擎
如何在js中讓函數(shù)睡眠多少秒? 經(jīng)常會(huì)有Javascript初學(xué)者提出這樣的問題,自從js出現(xiàn)以來.2010-08-08