【ゲーム】JavaScript:33 マインスイーパー

 「💣 マインスイーパー 💣」は、9×9 のマス目に隠れた 10 個の地雷を推理で避けながら全マスを開いていくクラシックパズルゲームです。地雷の場所は開くまで分からず、地雷を踏むとゲームオーバー、一度も踏まずにすべて開ければクリアとなります。

遊び方・操作方法

  1. 左クリック:マスを開きます
  2. 右クリック:旗(🚩)を立て/外して地雷マークにします
  3. 地雷を避けながら、空きマスをすべて開くとクリア!

ルール

  • 初期画面で「スタート ▶️」をクリックするとゲーム盤面が生成されます
  • クリックで開いたマスには、その周囲8マスにある地雷の数が表示されます(0ならさらに自動で周囲が開放)
  • 周囲地雷数をヒントに、危険そうなマスには旗を立ててマーク
  • 全81マス中、71 マス(81−10)を安全に開けばクリア
  • 地雷を開いてしまうと即ゲームオーバー

🎮ゲームプレイ

以下のリンク先から実際にプレイできます。

33 マインスイーパー

素材のダウンロード

以下のリンクから使用する素材をダウンロードできます。

mine_sweeper_title.pngmine_sweeper_bg.png

ゲーム画面イメージ

プログラム全文(mine_sweeper.html)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>💣 マインスイーパー 💣</title>
  <style>
    /* ================= 全体背景 ================= */
    body {
      margin: 0;
      padding: 0;
      font-family: 'Arial', sans-serif;
      background: url('mine_sweeper_bg.png') no-repeat center center fixed;
      background-size: cover;
      user-select: none;
    }

    /* ================ オーバーレイ共通 ================ */
    .overlay {
      position: absolute;
      top: 50%; left: 50%;
      transform: translate(-50%, -50%);
      background-color: rgba(255,255,255,0.9);
      padding: 20px;
      border-radius: 10px;
      width: 90%; max-width: 600px;
      text-align: center;
      z-index: 10;               /* 終了画面を手前に */
    }
    .hidden { display: none; }

    /* ================ タイトル画像 ================ */
    .title-image {
      display: block;
      margin: 0 auto 10px;
      max-width: 80%;
      height: auto;
    }

    /* ================ タイトル画面の見出し ================ */
    #title-screen h1 {
      margin: 10px 0;
      font-size: 2em;
    }

    /* ================ ボタン共通 ================ */
    .btn {
      padding: 10px 20px;
      margin: 10px;
      font-size: 16px;
      font-weight: bold;
      color: #fff;
      background-color: #007bff;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      box-shadow: 0 2px 4px rgba(0,0,0,0.2);
      transition: background-color 0.2s, transform 0.1s;
    }
    .btn:hover { background-color: #0056b3; transform: translateY(-2px); }
    .btn:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(0,0,0,0.2); }

    /* ================ グリッドコンテナ ================ */
    .grid-container {
      display: grid;
      grid-template-columns: repeat(9, 40px);
      grid-template-rows: repeat(9, 40px);
      gap: 2px;
      justify-content: center;
      margin: 20px auto;
      position: relative;
      z-index: 1;                /* 背景として表示 */
    }

    /* ================ 各セル ================ */
    .cell {
      width: 40px; height: 40px;
      line-height: 40px;
      text-align: center;
      background-color: #ccc;
      border: 1px solid #999;
      font-size: 1.2em;
      cursor: pointer;
      user-select: none;
    }
    .cell.revealed {
      background-color: #eee;
      cursor: default;
    }

    /* ================ メッセージ表示 ================ */
    #game-message, #end-message {
      margin-top: 10px;
      padding: 8px;
      background-color: rgba(0,0,0,0.7);
      color: #fff;
      border-radius: 5px;
      min-height: 1.5em;
      font-size: 1em;
      position: relative;
      z-index: 1;
    }

    /* —— 修正追加 —— */
    /* ゲーム画面オーバーレイは盤面サイズに合わせる */
    #game-screen {
      width: auto !important;
      max-width: none !important;
      background-color: transparent !important;
      padding: 0 !important;
    }
    /* 終了画面オーバーレイの背景を透明にして盤面を隠さない */
    #end-screen {
      background-color: transparent !important;
      width: auto !important;
      max-width: none !important;
    }
  </style>
</head>
<body>
  <!-- タイトル画面 -->
  <div id="title-screen" class="overlay">
    <img src="mine_sweeper_title.png" alt="マインスイーパー タイトル" class="title-image">
    <h1>💣 マインスイーパー 💣</h1>
    <div class="instructions">
      <p>📖 遊び方・ルール</p>
      <p>・左クリックでマスを開きます</p>
      <p>・右クリックで旗🚩を立てます</p>
      <p>・すべての地雷を避けて開けたらクリア!</p>
    </div>
    <button id="start-btn" class="btn">スタート ▶️</button>
  </div>

  <!-- ゲーム画面 -->
  <div id="game-screen" class="overlay hidden">
    <div class="grid-container" id="grid"></div>
    <div id="game-message">🚀 ゲームスタート! 地雷を避けてね!</div>
  </div>

  <!-- 終了画面 -->
  <div id="end-screen" class="overlay hidden">
    <h2 id="end-heading">🎉 クリア! 🎉</h2>
    <div id="end-message">おめでとうございます!</div>
    <button id="restart-btn" class="btn">タイトルに戻る 🔄</button>
  </div>

  <script>
    // 定数設定
    const ROWS  = 9, COLS = 9, MINES = 10;

    // ゲーム状態
    let board = [], revealedCount = 0, isGameOver = false;

    // DOM取得
    const titleScreen = document.getElementById('title-screen');
    const gameScreen  = document.getElementById('game-screen');
    const endScreen   = document.getElementById('end-screen');
    const startBtn    = document.getElementById('start-btn');
    const restartBtn  = document.getElementById('restart-btn');
    const gridDiv     = document.getElementById('grid');
    const gameMsg     = document.getElementById('game-message');
    const endHeading  = document.getElementById('end-heading');
    const endMsg      = document.getElementById('end-message');

    /** タイトル画面表示 */
    function showTitle() {
      titleScreen.classList.remove('hidden');
      gameScreen.classList.add('hidden');
      endScreen.classList.add('hidden');
    }

    /** ゲーム開始 */
    function startGame() {
      isGameOver = false;
      revealedCount = 0;
      board = [];
      gridDiv.innerHTML = '';
      gameMsg.textContent = '🚀 ゲームスタート! 地雷を避けてね!';

      titleScreen.classList.add('hidden');
      endScreen.classList.add('hidden');
      gameScreen.classList.remove('hidden');

      initBoard();
      placeMines();
      calculateAdjacents();
      renderBoard();
    }

    /** ボード初期化 */
    function initBoard() {
      for (let r = 0; r < ROWS; r++) {
        board[r] = [];
        for (let c = 0; c < COLS; c++) {
          board[r][c] = { mine:false, adjacent:0, revealed:false, flagged:false };
        }
      }
    }

    /** 地雷配置 */
    function placeMines() {
      let placed = 0;
      while (placed < MINES) {
        const r = Math.floor(Math.random()*ROWS);
        const c = Math.floor(Math.random()*COLS);
        if (!board[r][c].mine) {
          board[r][c].mine = true;
          placed++;
        }
      }
    }

    /** 周囲地雷数計算 */
    function calculateAdjacents() {
      for (let r=0; r<ROWS; r++){
        for (let c=0; c<COLS; c++){
          if (board[r][c].mine) continue;
          let count = 0;
          for (let dr=-1; dr<=1; dr++){
            for (let dc=-1; dc<=1; dc++){
              const nr = r + dr, nc = c + dc;
              if (nr>=0 && nr<ROWS && nc>=0 && nc<COLS){
                if (board[nr][nc].mine) count++;
              }
            }
          }
          board[r][c].adjacent = count;
        }
      }
    }

    /** 盤面描画 */
    function renderBoard() {
      for (let r=0; r<ROWS; r++){
        for (let c=0; c<COLS; c++){
          const cell = document.createElement('div');
          cell.classList.add('cell');
          cell.dataset.row = r;
          cell.dataset.col = c;
          cell.addEventListener('click', ()=>openCell(r,c));
          cell.addEventListener('contextmenu', e=>{
            e.preventDefault();
            toggleFlag(r,c);
          });
          gridDiv.appendChild(cell);
        }
      }
    }

    /** マスを開く */
    function openCell(r,c){
      if (isGameOver) return;
      const info = board[r][c];
      if (info.revealed || info.flagged) return;
      const div = getCellDiv(r,c);
      info.revealed = true;
      div.classList.add('revealed');

      if (info.mine){
        div.textContent = '💣';
        gameOver(false);
      } else {
        revealedCount++;
        if (info.adjacent > 0) {
          div.textContent = info.adjacent;
        } else {
          // 周囲の空セルも自動開放
          for (let dr=-1; dr<=1; dr++){
            for (let dc=-1; dc<=1; dc++){
              const nr = r+dr, nc = c+dc;
              if (nr>=0 && nr<ROWS && nc>=0 && nc<COLS){
                if (!board[nr][nc].revealed) openCell(nr,nc);
              }
            }
          }
        }
        if (revealedCount === ROWS*COLS - MINES){
          gameOver(true);
        }
      }
    }

    /** 旗トグル */
    function toggleFlag(r,c){
      if (isGameOver) return;
      const info = board[r][c];
      if (info.revealed) return;
      const div = getCellDiv(r,c);
      info.flagged = !info.flagged;
      div.textContent = info.flagged ? '🚩' : '';
    }

    /**
     * ゲームオーバー/クリア
     * ・盤面は隠さずそのままにし、
     * ・end-screen overlay を表示
     */
    function gameOver(won){
      isGameOver = true;
      // すべてのセルを表示(地雷も数字も空白も)
      for (let r=0; r<ROWS; r++){
        for (let c=0; c<COLS; c++){
          const info = board[r][c];
          info.revealed = true;                  // 内部状態も明示的に開示
          const div = getCellDiv(r,c);
          div.classList.add('revealed');
          if (info.mine){
            div.textContent = '💣';
          } else if (info.adjacent > 0){
            div.textContent = info.adjacent;
          } else {
            div.textContent = '';
          }
        }
      }
      // 終了画面テキスト設定
      endHeading.textContent = won ? '🎉 クリア! 🎉' : '💥 ゲームオーバー 💥';
      endMsg.textContent     = won
        ? `おめでとう! ${MINES} 個の地雷を避けました!`
        : '残念… 地雷を踏んでしまいました。';
      // 終了画面を表示(ゲーム画面はそのまま背後に)
      endScreen.classList.remove('hidden');
    }

    /** セルDIV取得 */
    function getCellDiv(r,c){
      return gridDiv.querySelector(`.cell[data-row='${r}'][data-col='${c}']`);
    }

    // イベント登録
    startBtn.addEventListener('click', startGame);
    restartBtn.addEventListener('click', showTitle);
    document.addEventListener('DOMContentLoaded', showTitle);
  </script>
</body>
</html>

アルゴリズムの流れ

ステップ内容関数/命令
1. タイトル表示タイトル画面のみを表示showTitle()
2. ゲーム開始盤面データ初期化・地雷配置・隣接数計算・描画startGame()initBoard()placeMines()calculateAdjacents()renderBoard()
3. マスクリック左クリックでセル開放、右クリックで旗トグルopenCell(r,c) / toggleFlag(r,c)
4. 隣接地雷数表示セル開放時、周囲地雷数を表示し、0 なら周囲を再帰的に開放calculateAdjacents() / 再帰呼び出し
5. クリア/ゲームオーバー全安全セル開放でクリア、地雷踏破でゲームオーバーgameOver(won)
6. 終了画面表示盤面をそのまま残しつつ終了メッセージオーバーレイを表示endScreen.classList.remove('hidden')

関数の詳細

関数名説明
showTitle()タイトル画面を表示し、他の画面を隠す
startGame()ゲーム初期化 → 盤面準備 → 描画 → 開始メッセージ
initBoard()2D 配列 board[r][c] の初期オブジェクト生成
placeMines()ランダムに MINES 個の地雷を配置
calculateAdjacents()各セルの周囲 8 マスにある地雷数を計算
renderBoard()<div class="cell"> をグリッドに配置
openCell(r,c)セルを開放 → 地雷判定 → 0 周囲なら再帰開放
toggleFlag(r,c)フラグの ON/OFF 切り替え
gameOver(won)全セルを開放して結果メッセージを表示
getCellDiv(r,c)行・列をキーに対応するセル <div> を取得

改造のポイント

  • 盤面サイズ/地雷数の調整
    定数 ROWSCOLSMINES を変えるだけで難易度を自由に変更できます。
  • タイマー機能の追加
    開始からクリア/敗北までの時間を計測し、スコアに反映すると競争性がアップします。
  • 難易度別設定
    初級(9×9・10地雷)、中級(16×16・40地雷)、上級(30×16・99地雷)などを切り替えられる UI を追加。
  • スマホ最適化
    タップと長押しで旗を立てられるようにし、モバイルでの操作性を向上させましょう。
  • ビジュアル強化
    CSS アニメーションでセル開放エフェクトや、地雷発見時の爆発アニメを追加すると演出が楽しくなります。

アドバイス
マインスイーパーは瞬時の判断と手順管理が要。自動的に周囲を一括開放する再帰処理は強力ですが、処理対象のマスが多いと重くなることも。大きな盤面を追加する場合は、セットタイムアウトで少しずつ開放するなど、UI が固まらない工夫をしてみてください。