js+canvas實現簡單掃雷小遊戲
掃雷小遊戲作為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,n,P) { let arr = initialize(row,n); for (let r = 0; r < row; r++) { for (let c = 0; c < col; c++) { drawRct(r * P,c * P,'rgb(102,102,102)',context);//該方法用於繪製整個畫面,下面會寫出宣告 } } return arr; } //初始化 function initialize(row,n) { let gameArr = zoneInitialize(row,col); //生成沒有標記雷的矩陣 let bomb = bombProduce(n,gameArr,row,col); gameArr = signArrNum(bomb[0],bomb[1],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,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,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,x + l,y + r,r); container.lineTo(x + l,y + l - r); container.arcTo(x + l,y + l,x + l - r,r); container.lineTo(x + r,y + l); container.arcTo(x,x,y + l - r,r); container.lineTo(x,y + r); container.arcTo(x,x + r,r); container.fillStyle = color; container.closePath(); container.fill(); container.stroke(); } //畫出方塊上對應的數字 function drawNum(x,alPha,color = 'rgb(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,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)'; else color = 'rgb(255,255,255)'; drawRct(r * P,context); drawNum(r * P,num); } } }
接下來寫出點選事件的處理,這裡對於點選後的向四個方向擴充套件,我採用了以下圖片所示的擴充套件
如上圖片,在點選時在點選位置往四周擴散,之後上下的按上下方向繼續擴散,左右的除本方向外還有往上下方向擴散,在遇到數字時停下。
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,P); } else if (statusArr[posX][posY] === 0) { this.style.cursor = "auto"; statusArr[posX][posY] = 1;//重置狀態 drawRct(posX * P,posY * P,'rgb(255,255)',context); drawNum(posX * P,gameArr[posX][posY]); outNum(gameArr,posY,posX,'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,L / 2,0)'); statusArr[posX][posY] = 2; signArr[signArr.length] = str; } else if (statusArr[posX][posY] === 2) { //取消標記 drawRct(posX * P,102)'); statusArr[posX][posY] = 0; signArr = signArr.filter(item => {//使用過濾器方法將當前位置的座標標記清除 if (item === str) return false; return true; }) } gameComplete(); return false; //阻止事件冒泡 } //自動跳出數字 function outNum(arr,status) {//arr為傳入的陣列,x,y為處理的位置,row,col為遊戲的行列,status用於儲存擴充套件的方向 if (status === 'middle') { outNumHandle(arr,x - 1,'left'); outNumHandle(arr,x + 1,'right'); outNumHandle(arr,y - 1,'top'); outNumHandle(arr,y + 1,'down'); } else if (status === 'left') { outNumHandle(arr,'down'); } else if (status === 'right') { outNumHandle(arr,'down'); } else if (status === 'top') { outNumHandle(arr,'top'); } else { outNumHandle(arr,'down'); } } //跳出數字具體操作 function outNumHandle(arr,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,context); drawNum(y * P,arr[y][x]); statusArr[y][x] = 1; } return; } drawRct(y * P,context); drawNum(y * P,arr[y][x]); statusArr[y][x] = 1; outNum(arr,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,P); return; } count++; } if (r < row - 1 && statusArr[r + 1][c] === 2) { if (!(bombArr.includes(strProduce(r + 1,P); return; } count++; } if (c > 0 && statusArr[r][c - 1] === 2) { if (!(bombArr.includes(strProduce(r,c - 1)))) { alert('error'); drawEnd(row,P); return; } count++; } if (c < col - 1 && statusArr[r][c + 1] === 2) { if (!(bombArr.includes(strProduce(r,c + 1)))) { alert('error'); drawEnd(row,P); return; } count++; } if (r > 0 && c > 0 && statusArr[r - 1][c - 1] === 2) { if (!(bombArr.includes(strProduce(r - 1,P); return; } count++; } if (r < row - 1 && c < col - 1 && statusArr[r + 1][c + 1] === 2) { if (!(bombArr.includes(strProduce(r + 1,P); return; } count++; } if (r > 0 && c < col - 1 && statusArr[r - 1][c + 1] === 2) { if (!(bombArr.includes(strProduce(r - 1,P); return; } count++; } if (r < row - 1 && c > 0 && statusArr[r + 1][c - 1] === 2) { if (!(bombArr.includes(strProduce(r + 1,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,context); drawNum((r - 1) * P,gameArr[r - 1][c]); statusArr[r - 1][c] = 1; } if (r < row - 1 && statusArr[r + 1][c] === 0) { drawRct((r + 1) * P,context); drawNum((r + 1) * P,gameArr[r + 1][c]); statusArr[r + 1][c] = 1; } if (c > 0 && statusArr[r][c - 1] === 0) { drawRct(r * P,(c - 1) * P,context); drawNum(r * P,gameArr[r][c - 1]); statusArr[r][c - 1] = 1; } if (c < col - 1 && statusArr[r][c + 1] === 0) { drawRct(r * P,(c + 1) * P,gameArr[r][c + 1]); statusArr[r][c + 1] = 1; } if (r > 0 && c > 0 && statusArr[r - 1][c - 1] === 0) { drawRct((r - 1) * P,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,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,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,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,800,'rgb(0,context);
一個簡單的掃雷遊戲就這樣實現了(說實話我覺得是簡陋不是簡單。。。。)
當然這個只是遊戲的初步實現,其實這個遊戲還可以增加難度設定,用圖片來表示雷,在點到雷的時候增加聲音等等,當然這些也並不難,如果大家有興趣的話可以嘗試優化這個遊戲。
希望這篇部落格能對大家有所幫助,也希望大神能指出我的不足。
附上一張醜爆的遊戲介面
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。