【ゲーム】JavaScript:09 三目並べ(まるぺけ)

 「❌⭕️ 三目並べ(まるぺけ)」は、3×3のマス目内で「〇」と「×」を交互に置き、縦・横・斜めいずれかに自分のマークを3つ並べたほうが勝ちとなる古典的なパズルゲームです。このWeb版ではプレイヤーが「〇」、コンピュータが「×」として、3段階の難易度から対戦できます。

遊び方と操作方法

  1. ページを開くと難易度プルダウンが表示されるので、コンピュータの強さ(簡単/普通/難しい)を選択。
  2. 盤面(3×3のグリッド)が空の状態で表示されるので、プレイヤー(「〇」)のターン時に任意のマスをクリックしてマークを置きます。
  3. プレイヤーが置くと自動的にコンピュータ(「×」)が思考ルーチンに基づいて1手を打ちます。
  4. いずれかが3つ並べるか、全マスが埋まって引き分けになるまで続きます。
  5. 「🔄 リセット」ボタンでいつでも初期状態に戻せます。

ルール

  • 空いているマスにのみマークを置けます。
  • 先に自分のマークを縦・横・斜めいずれかにつなげて3つ並べたプレイヤーが勝利。
  • 全マスが埋まっても勝敗がつかなければ「引き分け」。
  • 難易度「easy」…ランダム手、「medium」…自分または相手の勝ちを阻止する手を優先、「hard」…(現状mediumと同じ)最善手を狙います。

🎮ゲームプレイ

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

09 三目並べ(まるぺけ)

素材のダウンロード

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

tic_tac_toe_bg.png

ゲーム画面イメージ

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

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>❌⭕️ 三目並べ 🎉</title>
  <style>
    /* ── 全体設定 ── */
    body {
      margin: 0;
      padding: 0;
      font-family: 'Arial', sans-serif;
      background: url('tic_tac_toe_bg.png') no-repeat center center fixed;
      background-size: cover;
      display: flex;
      flex-direction: column;
      align-items: center;
      min-height: 100vh;
    }

    /* ── タイトル ── */
    h1 {
      margin-top: 20px;
      font-size: 3rem;
      color: #fff;
      /* 半透明の黒背景で視認性アップ */
      background-color: rgba(0, 0, 0, 0.6);
      padding: 12px 24px;
      border-radius: 8px;
      text-shadow: 2px 2px 6px #000;
    }

    /* ── 難易度選択 ── */
    #level {
      margin: 10px;
      background-color: rgba(255,255,255,0.9);
      padding: 8px 12px;
      border-radius: 5px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.3);
    }
    #level label {
      font-size: 1.2rem;
      color: #000;
      text-shadow: none;
    }
    #difficulty {
      margin-left: 10px;
      padding: 5px 8px;
      font-size: 1rem;
      border-radius: 5px;
      border: 1px solid #ccc;
      background-color: #fff;
      color: #000;
    }

    /* ── ゲーム盤 ── */
    #game-board {
      display: grid;
      grid-template-columns: repeat(3, 100px);
      grid-template-rows: repeat(3, 100px);
      gap: 8px;
      margin: 20px 0;
    }
    .cell {
      width: 100px;
      height: 100px;
      background: rgba(255,255,255,0.95);
      border: 3px solid #333;
      border-radius: 10px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 2.5rem;
      cursor: pointer;
      box-shadow: 0 4px 8px rgba(0,0,0,0.3);
      transition: transform 0.2s, background 0.3s;
    }
    .cell:hover {
      transform: scale(1.05);
      background: rgba(255,255,255,1);
    }
    .cell.taken {
      cursor: default;
      opacity: 0.7;
    }

    /* ── ステータス表示 ── */
    #status {
      margin: 10px;
      padding: 10px 20px;
      font-size: 1.5rem;
      color: #fff;
      background-color: rgba(0,0,0,0.5);
      border-radius: 8px;
      text-shadow: 1px 1px 4px #000;
    }

    /* ── リセットボタン ── */
    button {
      padding: 10px 20px;
      font-size: 1rem;
      margin-bottom: 20px;
      border: none;
      border-radius: 5px;
      background-color: #ff4081;
      color: #fff;
      cursor: pointer;
      box-shadow: 0 4px 6px rgba(0,0,0,0.3);
      transition: background 0.3s;
    }
    button:hover {
      background-color: #e91e63;
    }
  </style>
</head>
<body>
  <!-- タイトル -->
  <h1>❌⭕️ 三目並べ 🎉</h1>

  <!-- 難易度選択 -->
  <div id="level">
    <label for="difficulty">🛠️ コンピューターの強さ:</label>
    <select id="difficulty">
      <option value="easy">簡単</option>
      <option value="medium">普通</option>
      <option value="hard">難しい</option>
    </select>
  </div>

  <!-- ゲーム盤 -->
  <div id="game-board"></div>

  <!-- ステータス表示 -->
  <div id="status">🎮 プレイヤー 〇 のターン</div>

  <!-- リセットボタン -->
  <button onclick="resetGame()">🔄 リセット</button>

  <script>
    // ── 要素取得 ──
    const board      = document.getElementById('game-board');
    const status     = document.getElementById('status');
    const difficulty = document.getElementById('difficulty');

    // ── ゲーム状態 ──
    let currentPlayer = '〇';            // 人間は「〇」
    let gameActive    = true;            // ゲーム継続フラグ
    let gameState     = Array(9).fill(''); // 盤面配列

    // ── 勝利パターン ──
    const winningConditions = [
      [0,1,2], [3,4,5], [6,7,8],  // 横列
      [0,3,6], [1,4,7], [2,5,8],  // 縦列
      [0,4,8], [2,4,6]            // 斜め
    ];

    /**
     * セルクリック時のハンドラ
     */
    function handleCellClick(event) {
      const cell  = event.target;
      const index = parseInt(cell.getAttribute('data-index'));

      // 埋まっていたりゲーム終了中、人のターン以外は処理しない
      if (gameState[index] !== '' || !gameActive || currentPlayer !== '〇') {
        return;
      }

      // 人間の手を反映
      gameState[index] = currentPlayer;
      cell.textContent = currentPlayer;
      cell.classList.add('taken');

      // 勝利判定
      if (checkWinner()) {
        status.textContent = `🎉 プレイヤー ${currentPlayer} の勝利!`;
        gameActive = false;
        return;
      }

      // 引き分け判定
      if (gameState.every(cell => cell !== '')) {
        status.textContent = '😲 引き分けです!';
        gameActive = false;
        return;
      }

      // コンピューターのターンへ
      currentPlayer = '×';
      status.textContent = '💻 コンピューターのターン';
      setTimeout(computerMove, 800);
    }

    /**
     * コンピューターの手を決定・配置
     */
    function computerMove() {
      if (!gameActive) return;

      let moveIndex;
      // 難易度ごとの思考ルーチン
      if (difficulty.value === 'easy') {
        moveIndex = getRandomMove();
      } else if (difficulty.value === 'medium') {
        moveIndex = getWinningMove('×') || getWinningMove('〇') || getRandomMove();
      } else {
        moveIndex = getWinningMove('×') || getWinningMove('〇') || getRandomMove();
      }

      // コンピューターの手を反映
      gameState[moveIndex] = '×';
      const cell = document.querySelector(`.cell[data-index='${moveIndex}']`);
      cell.textContent = '×';
      cell.classList.add('taken');

      // 勝利判定
      if (checkWinner()) {
        status.textContent = '☠️ コンピューターの勝利!';
        gameActive = false;
        return;
      }

      // 引き分け判定
      if (gameState.every(cell => cell !== '')) {
        status.textContent = '😲 引き分けです!';
        gameActive = false;
        return;
      }

      // 人間のターンへ戻す
      currentPlayer = '〇';
      status.textContent = `🎮 プレイヤー ${currentPlayer} のターン`;
    }

    /**
     * 勝利できる手を探索
     * @param {string} player - '〇' か '×'
     */
    function getWinningMove(player) {
      for (let condition of winningConditions) {
        const [a, b, c] = condition;
        if (gameState[a] === player && gameState[b] === player && gameState[c] === '') return c;
        if (gameState[a] === player && gameState[b] === ''    && gameState[c] === player) return b;
        if (gameState[a] === ''    && gameState[b] === player && gameState[c] === player) return a;
      }
      return null;
    }

    /**
     * 空きセルからランダムに選択
     */
    function getRandomMove() {
      const available = gameState
        .map((val, idx) => val === '' ? idx : null)
        .filter(idx => idx !== null);
      return available[Math.floor(Math.random() * available.length)];
    }

    /**
     * 勝利条件を満たしているかどうかチェック
     */
    function checkWinner() {
      return winningConditions.some(condition => {
        const [a, b, c] = condition;
        return (
          gameState[a] === currentPlayer &&
          gameState[b] === currentPlayer &&
          gameState[c] === currentPlayer
        );
      });
    }

    /**
     * ゲームをリセット
     */
    function resetGame() {
      gameActive = true;
      currentPlayer = '〇';
      gameState = Array(9).fill('');
      status.textContent = `🎮 プレイヤー ${currentPlayer} のターン`;
      board.innerHTML = '';
      createBoard();
    }

    /**
     * 盤面のセルを作成
     */
    function createBoard() {
      for (let i = 0; i < 9; i++) {
        const cell = document.createElement('div');
        cell.classList.add('cell');
        cell.setAttribute('data-index', i);
        cell.addEventListener('click', handleCellClick);
        board.appendChild(cell);
      }
    }

    // 初期盤面を生成
    createBoard();
  </script>
</body>
</html>

アルゴリズムの流れ

ステップ処理内容
盤面初期化 (createBoard)9 マスの <div> を生成し、data-index とクリックイベントを設定
プレイヤー手執行 (handleCellClick)空きマスに「〇」を配置→勝利判定→引き分け判定→コンピュータ手番への切替
勝利判定 (checkWinner)winningConditions の各三角配列に当てはまるかチェック
コンピュータ手 (computerMove)難易度別に:• easy → getRandomMove()• medium/hard → getWinningMove('×') または getWinningMove('〇') で勝ち/阻止手候補、なければランダム
勝ち手探索 (getWinningMove)勝利/阻止可能な 3 連の空きマス位置を走査
ランダム手 (getRandomMove)空きマスのインデックスを列挙し、ランダムに1つ返却
リセット (resetGame)ゲーム状態初期化&盤面再生成

関数の詳細

関数名機能
createBoard()盤面 <div> 要素を 3×3 生成し、data-index とクリック ハンドラを登録
handleCellClick(event)プレイヤーの石を配置→勝敗/引き分け判定→次ターンに移行
computerMove()コンピュータの手を決定し配置→勝敗/引き分け判定→次ターンに移行
getWinningMove(player)指定プレイヤーが即勝利または相手阻止可能なマスを探索し、見つかればそのインデックスを返却
getRandomMove()空きマスからランダムなインデックスを返却
checkWinner()winningConditions 配列を走査し、現行プレイヤーが勝利状態にあるかを真偽で返却
resetGame()ゲーム状態・UI を初期化し、盤面を再生成

このゲームの改造のポイント

  • ミニマックスAI:medium/hard を汎用的なミニマックス法に置き換え、最適手を常に選択する強力 AI を実装。
  • 先攻後攻切替:初期の先攻プレイヤーをランダム化したり、毎回交代制にして戦略の幅を広げる。
  • リプレイ機能:ゲーム中の全手を記録して再生できるようにし、自分や他者の戦略を振り返る。
  • UI拡張:タッチ操作対応、アニメーション演出(勝利ラインのハイライト)、カスタムテーマ(色/フォント)選択。
  • ネット対戦:WebSocket を利用し、離れた友人同士でリアルタイム対戦できる機能。
  • 拡張ボード:3×3 以外のサイズ(4×4、5×5)や「五目並べ」にも対応できる汎用ロジックへの一般化。

これらを加えて、自分だけの「まるぺけ」をより奥深いゲームに発展させてみてください!