原生 JavaScript 實現掃雷 完整思路分析,代碼實現。 ...
閱讀這篇文章需要掌握的基礎知識:Html5、CSS、JavaScript
線上Demo:查看
掃雷規則
在寫掃雷之前,我們先瞭解下它的游戲規則
● 掃雷是一個矩陣,地雷隨機分佈在方格上。
● 方格上的數字代表著這個方格所在的九宮格內有多少個地雷。
● 方格上的旗幟為玩家所作標記。
● 踩到地雷,游戲失敗。
● 打開所有非雷方格,游戲勝利。
功能實現思路分析
矩陣的生成
- 矩陣的生成有多種方式可以實現,我們這裡使用<table>+<span>標簽。
- 通過 js 給定行數與列數在<table>的 innerHtml 寫入<span>標簽來動態生成矩陣。
方格的打開與標記
- 通過 onmousedown 事件,傳入點擊的方格的坐標及event,判斷event為左鍵還是右鍵。
- 左鍵打開方格,右鍵標記方格。
地雷的隨機分佈
- 由於第一次打開的方格不能為地雷所以我們把生成地雷的函數放在第一次點擊方格時。
- 我們通過迴圈用 Math.random() 函數來隨機生成地雷的二維坐標。
- 判斷坐標是否不為第一次點擊方格的坐標以及沒有雷存在。
- 是則將方格設置為地雷,當前地雷數+1,並且將九宮格內的方格的計雷數+1。
- 否則跳過進入下個迴圈,直到地雷的數量達到設定的最大雷數,結束迴圈。
踩到地雷游戲結束
- 打開方格為地雷時,提示游戲結束。
- 通過遍歷矩陣來打開所有地雷
連鎖打開方格
- 當打開的方格為計雷數為0的方格,自動打開九宮格內的非雷方格。
- 如果打開的非雷方格九宮格內仍有非雷方格,繼續打開九宮格內的非雷方格,直到沒有為止。
游戲勝利條件
- 當所有非雷方格被打開即為游戲勝利。
- 在每次打開方格函數中都遍歷一遍矩陣,當找到有未打開的非雷方格時則結束遍歷。
- 當遍歷完未找到未打開的非雷方格則提示游戲勝利。
剩餘地雷數與計時器
- 地雷的總數減去玩家標記的方格數即為剩餘地雷數
- 計時器可以用setInterval()函數實現
代碼實現
生成矩陣
我們先在<body>里寫一個<table>標簽,設定個 id='grid'
<table id='grid'></table>
然後在<script>里 定義兩個變數 row--行數 col--列數
通過兩個for迴圈把 (方格)<span> 寫入到 (矩陣)<table> 里,通過<td><tr>標簽控制行列。
var row = 10; //行數
var col = 10; //列數
//生成矩陣html <tr>--行標簽 <td>--列標簽
let gridHtml = '';
for (let i = 0; i < row; i++) {
gridHtml += '<tr>'
for (let j = 0; j < col; j++) {
gridHtml += '<td><span class="blocks"></span></td>';
}
gridHtml += '<tr>'
}
//寫入html
document.getElementById('grid').innerHTML = gridHtml;
寫一下矩陣和方格的CSS樣式。
#grid {
margin: auto; /* 讓矩陣居中顯示於頁面 */
}
.blocks {
width: 30px;
height: 30px;
line-height: 30px;
display: block; /* 讓span以block方式顯示 */
text-align: center;
border: solid 1px #000;
user-select: none; /* 設置不可拖拽選中 */
cursor: pointer; /* 設置滑鼠停留樣式 */
}
.blocks:hover {
background: #0af; /* 滑鼠停留時背景顏色變化 */
}
至此打開頁面,矩陣就初步顯示出來了。
把矩陣的方格放入二維數組中
我們先定義一個全局變數grid。
把剛纔寫的生成矩陣的代碼寫成一個函數 function init_grid()
document.getElementsByClassName('blocks') 返回的是一個一維數組,我們把它通過兩個for迴圈轉化為二維數組。
給每個方格定義一個屬性 count 計雷數 --- blocks[i].count = 0;
然後把返回值賦值給grid --- grid = init_grid();
var row = 10; //行數 var col = 10; //列數 var grid = init_grid(); //初始化矩陣 (row-行數 col-列數) function init_grid() { //生成矩陣html <tr>--行標簽 <td>--列標簽 let gridHtml = ''; for (let i = 0; i < row; i++) { gridHtml += '<tr>' for (let j = 0; j < col; j++) { gridHtml += '<td><span class="blocks"></span></td>'; } gridHtml += '<tr>' } //寫入html document.getElementById('grid').innerHTML = gridHtml; //返回矩陣二維數組 let blocks = document.getElementsByClassName('blocks'); let grid = new Array(); for (let i = 0; i < blocks.length; i++) { if (i % col === 0) { grid.push(new Array()); } //初始化計雷數 blocks[i].count = 0; grid[parseInt(i / col)].push(blocks[i]); } return grid; }
寫完了這段我們先寫一段代碼測試下grid有沒有賦值成功,遍歷grid把方格的值改為對應的坐標。
for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { grid[i][j].innerHTML = i + ',' + j; } }
可以看到 grid 已經賦值成功!沒成功的回去檢查下代碼。(Tip:測試完記得把測試代碼刪除)
方格的點擊事件
定義一個函數 function block_click( _i, _j, e) 的大致框架
e為傳入的滑鼠事件,e.button ( 0為左鍵,2為右鍵 )。
isOpen屬性為自定義屬性,用來判斷方格是否打開。
//方格點擊事件 _i:坐標i _j:坐標j e:滑鼠事件 function block_click(_i, _j, e) { //跳過已打開的方格 if (grid[_i][_j].isOpen) { return; } //滑鼠左鍵打開方格 if (e.button === 0) { } //滑鼠右鍵標記方格 else if (e.button === 2) { } }
然後修改下之前寫在 init_grid 函數里的<span>的屬性,綁定 onmousedown 事件,傳入 i,j 坐標,和滑鼠事件 event
gridHtml += '<td><span class="blocks" onmousedown="block_click(' + i + ',' + j + ',event)"></span></td>';
修改下body的屬性 加入防拖拽生成新頁面和屏蔽右鍵菜單。
<!-- ondragstart:防拖拽生成新頁面 oncontextmenu:屏蔽右鍵菜單--> <body ondragstart='return false' oncontextmenu='self.event.returnValue=false'>
我們在滑鼠左鍵事件裡面寫下測試代碼,當左鍵方格時顯示它的坐標。
//滑鼠左鍵打開方格 if (e.button === 0) { grid[_i][_j].innerHTML = _i + ',' + _j; }
效果如下,沒成功的回去檢查下代碼。(Tip:測試完記得把測試代碼刪除)
方格的標記
在滑鼠右鍵事件寫標記代碼,這裡用 ▲ 來作為標記。
右擊一次添加標記,再次右擊刪除標記。
//滑鼠右鍵標記方格 else if (e.button === 2) { let block = grid[_i][_j]; if (block.innerHTML !== '▲') { block.innerHTML = '▲'; } else { block.innerHTML = ''; } }
效果如下:
隨機生成地雷
由於第一次打開的方格不能為地雷所以我們把生成地雷的函數放在第一次點擊方格時。
先定義全局變數 maxCount --- 最大地雷數 isFirstOpen --- 是否第一次打開方格。
var row = 10; //行數 var col = 10; //列數 var grid = init_grid(); var maxCount = 10; //最大地雷數量 var isFirstOpen = true; //第一次打開方格
在滑鼠左鍵事件裡面寫第一次打開方格生成地雷的代碼的大致框架。
//滑鼠左鍵打開方格 if (e.button === 0) { //第一次打開 if (isFirstOpen) { isFirstOpen = false; let count = 0; //當前地雷數 //生成地雷 while (count < maxCount) { //........ } } }
完善生成地雷代碼:
生成隨機坐標 ri,rj,判斷該坐標不等於第一次點擊方格的坐標以及該坐標表方格不為地雷。
條件成立,將坐標對應方格的 isMine 設置為true,當前地雷數+1,並使九宮格內非雷方格的計雷數 count +1
自定義屬性isMine代表方格為地雷。
自定義屬性count為計雷數。
當地雷數大於最大地雷數,結束迴圈。
//生成地雷 while (count < maxCount) { //生成隨機坐標 let ri = Math.floor(Math.random() * row); let rj = Math.floor(Math.random() * col); //坐標不等於第一次點擊方格的坐標 && 非雷方格 if (!(ri === _i && rj === _j) && !grid[ri][rj].isMine) { grid[ri][rj].isMine = true; //自定義屬性isMine代表方格為地雷 count++; //當前地雷數+1 //更新九宮格內非雷方格的計雷數 for (let i = ri - 1; i < ri + 2; i++) { for (let j = rj - 1; j < rj + 2; j++) { //判斷坐標防越界 if (i > -1 && j > -1 && i < row && j < col) { //計雷數+1 grid[i][j].count++; } } } } }
寫個測試代碼在生成地雷後顯示所有方格的狀態。(Tip:測試完記得把測試代碼刪除)
for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { //判斷方格是否為雷 if (grid[i][j].isMine) { //顯示為雷 grid[i][j].innerHTML = '雷'; } else { //否則顯示計雷數 grid[i][j].innerHTML = grid[i][j].count; } } }
效果如下:可以看到已經隨機生成了雷,計雷數也正確顯示了。
方格的打開事件
在生成地雷的代碼下,加入方格打開代碼函數 block_open(_i,_j) 的大致框架。
定義 function op(block) 函數設定打開方格的狀態與樣式。
判定打開的方格的類型
block.isMine 為打開地雷方格 --> 游戲結束
block.count === 0 為打開計雷數為0的方格 --> 連鎖打開非雷方格
else 為打開計雷數大於0的方格 --> 顯示方格計雷數
//滑鼠左鍵打開方格 if (e.button === 0) { //第一次打開 if (isFirstOpen) { //....... } //執行打開方格函數 block_open(_i, _j); //打開方格函數 function block_open(_i, _j) { let block = grid[_i][_j]; op(block); //設定打開方格的狀態與樣式 function op(block) { block.isOpen = true; //isOpen為自定義屬性,設置為true代表已打開 block.style.background = '#ccc'; //將背景設置為灰色 block.style.cursor = 'default'; //將滑鼠停留樣式設置為預設 } if (block.isMine) { //踩雷 } else if (block.count === 0) { //打開計雷數為0的方格 } else { //打開計雷數不為0的方格 } } }
打開非雷方格顯示計雷數
我們先把最簡單的顯示方格計雷數搞定。
else { //打開計雷數不為0的方格 block.innerHTML = block.count; //顯示計雷數 }
效果如下:
踩雷游戲結束
接下來寫踩雷代碼,當打開的方格為雷時,將其顯示為'雷',並打開所有的地雷,提示游戲結束。
if (block.isMine) { //踩雷 block.innerHTML = '雷'; //顯示為 '雷' //遍歷矩陣打開所有的地雷方格 for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { //找到地雷 block = grid[i][j]; if (!block.isOpen && block.isMine) { op(block); //設置打開狀態和樣式 block.innerHTML = '雷'; //顯示為 '雷' } } } //提示游戲結束 alert("游戲結束"); }
效果如下:
連鎖打開方格
打開的方格為計雷數為0的方格,自動打開九宮格內的非雷方格,迴圈遞歸到沒有為止。
計雷數為0就沒必要讓innerHtml顯示0了,保持空白就行。
else if (block.count === 0) { //打開計雷數為0的方格 //遍歷九宮格內的方格 for (let i = _i - 1; i < _i + 2; i++) { for (let j = _j - 1; j < _j + 2; j++) { //判斷是否越界&&跳過已打開的方格&&非雷 if (i > -1 && j > -1 && i < row && j < col && !grid[i][j].isOpen && !grid[i][j].ismine) { //遞歸打開方格函數 block_open(i, j); } } } }
效果如下:
游戲勝利條件
掃雷大體框架已經出來了,我們現在做勝利條件的判定。
在方格點擊函數最後寫判斷代碼。
//方塊點擊事件 _i:坐標i _j:坐標j e:滑鼠事件 function block_click(_i, _j, e) { //跳過已打開的方塊 if (grid[_i][_j].isOpen) { //... } //滑鼠左鍵打開方塊 if (e.button === 0) { //... } //滑鼠右鍵標記方塊 else if (e.button === 2) { //... } //遍歷矩陣 let isWin = true; for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) {
let block = grid[i][j];
//判斷游戲勝利條件(所有的非雷方格已打開) if (!block.isMine && !block.isOpen) { //如果有未打開的非雷方塊 條件不成立 isWin = false; } } } if (isWin) { alert("游戲勝利"); } }
效果如下:(還專門玩了一遍^ ^)
游戲部分到這裡就完成了!
剩餘地雷數與計時器
最後,我們做一下剩餘地雷數和計時器的顯示。
我們寫個 <div> 在 <table> 的上面,放兩個 <span> 來做顯示框,<label> 用來給 js 計數。
<div id='bar'> <span class='bar'>剩餘雷數:<label id='count'>0</label></span> <span class='bar'>計時:<label id='time'>0</label>s</span> </div> <table id='grid'></table>
再寫下CSS樣式:
#bar { text-align: center; margin-bottom: 20px; } .bar { height: 25px; width: 150px; line-height: 25px; display: inline-block; border: solid 1px #000; margin-left: 20px; margin-right: 20px; }
效果如下:
在 js 中定義兩個全局變數拿到 <lable> count 和 time
然後讓地雷數量等於最大地雷數,設置個100ms定時器,每次+0.1s,保留一位小數。
var count = document.getElementById('count'); //剩餘地雷數 count.innerHTML = maxCount; //初始化剩餘雷數 var time = document.getElementById('time'); //計時器 var timer = setInterval(function () { let seconds = (parseFloat(time.innerHTML) + 0.1).toFixed(1); //保留一位小數 time.innerHTML = seconds; }, 100) //定時器 100ms執行一次
我們修改下方格點擊事件中遍歷矩陣的代碼,更新剩餘地雷數,勝利時結束計時。
//遍歷矩陣 let isWin = true; count.innerHTML = maxCount; //重置剩餘地雷數 for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { let block = grid[i][j]; //找到標記 if (block.innerHTML === '▲') { count.innerHTML = parseInt(count.innerHTML) - 1; //剩餘地雷數-1 } //判斷游戲勝利條件(所有的非雷方格已打開) if (!block.isMine && !block.isOpen) { //如果有未打開的非雷方塊 條件不成立 isWin = false; } } } if (isWin) { clearInterval(timer); //游戲勝利結束計時,清除定時器 alert("游戲勝利"); }
再修改踩雷的代碼,結束計時。
if (block.isMine) { //踩雷 block.innerHTML = '雷'; //顯示為 '雷' //遍歷矩陣打開所有的地雷方格 for (let i = 0; i < row; i++) { for (let j = 0; j < col; j++) { //找到地雷 block = grid[i][j]; if (!block.isOpen && block.isMine) { op(block); //設置打開狀態和樣式 block.innerHTML = '雷'; //顯示為 '雷' } } } clearInterval(timer); //游戲結束停止計時,清除定時器 //提示游戲結束 alert("游戲結束"); }
OK,大功告成!!!後續還可以加入選擇難度的功能,重新開始按鈕,動畫效果等等,這個就看你們發揮了!!
完整代碼
<!DOCTYPE html> <html> <head> <title>掃雷</title> <style> #bar { text-align: center; margin-bottom:20px; } .bar { height: 25px; width: 150px; line-height: 25px; display: inline-block; border: solid 1px #000; margin-left: 20px; margin-right: 20px; } #grid { margin: auto; } .blocks { width: 30px; height: 30px; line-height: 30px; display: block; text-align: center; border: solid 1px #000; user-select: none; cursor: pointer; } .blocks:hover { background: #0af; } </style> </head> <!-- ondragstart:防拖拽生成新頁面 oncontextmenu:屏蔽右鍵菜單--> <body ondragstart='return false' oncontextmenu='self.event.returnValue=false'> <div id='bar'> <span class='bar'>剩餘雷數:<label id='count'>0</label></span> <span class='bar'>計時:<label id='time'>0</label>s</span> </div> <table id='grid'></table> <script> var row = 10; //行數 var col = 10; //列數 var maxCount = 10; //最大地雷數量 var isFirstOpen = true; //第一次打開方格 var grid = init_grid(); //初始化 var count = document.getElementById('count'); //剩餘雷數 var time = document.getElementById('time'); //計時 //初始化矩陣 (row-行數 col-列數) function init_grid() { //生成矩陣html <tr>--行標簽 <td>--列標簽 let gridHtml = ''; for (let i = 0; i < row; i++) { gridHtml += '<tr>' for (let j = 0; j < col; j++) { gridHtml += '<td><span class="blocks" onmousedown="block_click(' + i + ',' + j + ',event)"></span></td>'; } gridHtml += '<tr>' } //寫入html document.getElementById('grid').innerHTML = gridHtml; //返回矩陣二維數組 let blocks = document.getElementsByClassName('blocks'); let grid = new Array(); for (let i = 0; i < blocks.length; i++) { if (i % col === 0) { grid.push(new Array()); } //初始化計雷數 blocks[i].count = 0; grid[parseInt(i / col)].push(blocks[i]); } return grid; } //方格點擊事件 _i:坐標i _j:坐標j e:滑鼠事件 function block_click(_i, _j, e) { //跳過已打開的方格 if (grid[_i][_j].isOpen) { return; } //滑鼠左鍵打開方格 if (e.button ===