原生 JS 實現掃雷 (分析+代碼實現)


閱讀這篇文章需要掌握的基礎知識: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 === 0) {

        //第一次打開
        if (isFirstOpen) {

          isFirstOpen = false;
          let count = 0; //當前地雷數

          //生成地雷
          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++;
                  }
                }
              }
            }
          }
        }

        //執行打開方格函數
        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) {
            //踩雷
            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("游戲結束");
          } 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);
                }
              }
            }
          } else {
            //打開計雷數不為0的方格
            block.innerHTML = block.count; //顯示計雷數
          }

        }
      }
      //鼠標右鍵標記方格
      else if (e.button === 2) {

        let block = grid[_i][_j];
        if (block.innerHTML !== '') {
          block.innerHTML = '';
        } else {
          block.innerHTML = '';
        }
      }

      //判斷游戲是否結束(所有的非雷方格已打開)
      for (let i = 0; i < row; i++) {
        for (let j = 0; j < col; j++) {
          if (!grid[i][j].isMine && !grid[i][j].isOpen) {
            return;
          }
        }
      }
      alert("游戲勝利");
    }
  </script>
</body>

</html>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM