We Blog Weblog

〇✕ゲーム【CPU戦】

Web

2024年12月25日

みなさんメリークリスマスです。
ケミストのWeb担当みやのです。

「Web」記事では、これまでに得たWebに関する知識を記録として残していきたいと思います。

今回は「〇✕ゲーム」の「CPU戦」を作ってみました!クリスマスに何をやっているんだろう

難易度は「ふつう」です。さあレッツトライ!

  • あなたの番です
  • 相手の番です
  • 勝利ッ!
  • 敗北した...
  • 引き分け...
もう一回遊ぶ

つくってみよう

とりあえずプレイヤーは〇で先攻、CPUは✕で後攻で固定です。

HTMLは前回とほぼ同じです。

<div class="wrapper">
  <div class="game-container">
    <div class="message-container">
      <ul class="message-list">
        <li id="maru-turn" class="js-hidden">あなたの番です</li>
        <li id="batsu-turn" class="js-hidden">相手の番です</li>
        <li id="maru-win" class="js-hidden">勝利ッ!</li>
        <li id="batsu-win" class="js-hidden">敗北した...</li>
        <li id="draw" class="js-hidden">引き分け...</li>
      </ul>
    </div>
    <div class="squares-container">
      <div class="squares-box">
        <div id="1-1" class="square"></div>
        <div id="1-2" class="square"></div>
        <div id="1-3" class="square"></div>
        <div id="2-1" class="square"></div>
        <div id="2-2" class="square"></div>
        <div id="2-3" class="square"></div>
        <div id="3-1" class="square"></div>
        <div id="3-2" class="square"></div>
        <div id="3-3" class="square"></div>
      </div>
    </div>
    <div class="btn-container">
      <div class="js-hidden" id="reset-btn">
        <span class="btn btn-reset">もう一回遊ぶ</span>
      </div>
    </div>
  </div>
</div>

「〇の番」を「あなたの番です」に変えてみました あな番


CSSは変更なしです。


スクリプトは(ChatGPTが)頑張りました。

document.addEventListener('DOMContentLoaded', () => {
  const squares = document.querySelectorAll('.square');
  const messageList = document.querySelector('.message-list');
  const maruTurnMessage = document.getElementById('maru-turn');
  const batsuTurnMessage = document.getElementById('batsu-turn');
  const maruWinMessage = document.getElementById('maru-win');
  const batsuWinMessage = document.getElementById('batsu-win');
  const drawMessage = document.getElementById('draw');
  const resetBtn = document.getElementById('reset-btn');
  let currentTurn = 'maru';
  let gameActive = true;
  let winningLine = [];
  const gameState = [
    ['', '', ''],
    ['', '', ''],
    ['', '', '']
  ];
  const winningPatterns = [
    [[0, 0], [0, 1], [0, 2]],
    [[1, 0], [1, 1], [1, 2]],
    [[2, 0], [2, 1], [2, 2]],
    [[0, 0], [1, 0], [2, 0]],
    [[0, 1], [1, 1], [2, 1]],
    [[0, 2], [1, 2], [2, 2]],
    [[0, 0], [1, 1], [2, 2]],
    [[0, 2], [1, 1], [2, 0]]
  ];
  function checkWinner() {
    for (const pattern of winningPatterns) {
      const [a, b, c] = pattern;
      if (
        gameState[a[0]][a[1]] !== '' &&
        gameState[a[0]][a[1]] === gameState[b[0]][b[1]] &&
        gameState[a[0]][a[1]] === gameState[c[0]][c[1]]
      ) {
        winningLine = [a, b, c];
        return gameState[a[0]][a[1]];
      }
    }
    return '';
  }
  function checkDraw() {
    return gameState.flat().every(cell => cell !== '');
  }
  function handleSquareClick(e) {
    const square = e.target;
    const row = square.id.split('-')[0] - 1;
    const col = square.id.split('-')[1] - 1;
    if (gameState[row][col] !== '' || !gameActive) {
      return;
    }
    if (currentTurn === 'maru') {
      square.classList.add('js-maru-checked');
      gameState[row][col] = 'maru';
    }
    const winner = checkWinner();
    if (winner !== '') {
      endGame(winner === 'maru' ? '〇の勝ちです!' : '✕の勝ちです!');
      if (winner === 'maru') {
        fireworks();
      }
      drawWinningLine();
      return;
    }
    if (checkDraw()) {
      endGame('引き分けです!');
      return;
    }
    currentTurn = 'batsu';
    updateTurnMessage();
    if (currentTurn === 'batsu') {
      setTimeout(cpuTurn, 500);
    }
  }
  function updateTurnMessage() {
    maruTurnMessage.classList.add('js-hidden');
    batsuTurnMessage.classList.add('js-hidden');
    if (currentTurn === 'maru') {
      maruTurnMessage.classList.remove('js-hidden');
    } else {
      batsuTurnMessage.classList.remove('js-hidden');
    }
  }
  function cpuTurn() {
    if (!gameActive) return;
    let square;
    const availableSquares = Array.from(squares).filter(sq => !sq.classList.contains('js-maru-checked') && !sq.classList.contains('js-batsu-checked'));
    for (const sq of availableSquares) {
      const row = sq.id.split('-')[0] - 1;
      const col = sq.id.split('-')[1] - 1;
      gameState[row][col] = 'batsu';
      if (checkWinner() === 'batsu') {
        square = sq;
        break;
      }
      gameState[row][col] = '';
    }
    if (!square) {
      for (const sq of availableSquares) {
        const row = sq.id.split('-')[0] - 1;
        const col = sq.id.split('-')[1] - 1;
        gameState[row][col] = 'maru';
        if (checkWinner() === 'maru') {
          square = sq;
          gameState[row][col] = '';
          break;
        }
        gameState[row][col] = '';
      }
    }
    if (!square) {
      square = availableSquares[Math.floor(Math.random() * availableSquares.length)];
    }
    square.classList.add('js-batsu-checked');
    const row = square.id.split('-')[0] - 1;
    const col = square.id.split('-')[1] - 1;
    gameState[row][col] = 'batsu';
    const winner = checkWinner();
    if (winner !== '') {
      endGame(winner === 'maru' ? '〇の勝ちです!' : '✕の勝ちです!');
      drawWinningLine();
      return;
    }
    if (checkDraw()) {
      endGame('引き分けです!');
      return;
    }
    currentTurn = 'maru';
    updateTurnMessage();
  }
  function endGame(message) {
    gameActive = false;
    maruTurnMessage.classList.add('js-hidden');
    batsuTurnMessage.classList.add('js-hidden');
    if (message.includes('勝ち')) {
      if (message.includes('〇')) {
        maruWinMessage.classList.remove('js-hidden');
      } else {
        batsuWinMessage.classList.remove('js-hidden');
      }
    } else {
      drawMessage.classList.remove('js-hidden');
    }
    resetBtn.classList.remove('js-hidden');
    squares.forEach(square => square.classList.add('js-unclickable'));
    }
    const drawWinningLine = () => {
      const container = document.querySelector('.squares-container');
      const lineContainer = document.createElement('div');
      lineContainer.classList.add('line-container');
      container.appendChild(lineContainer);
      const [a, b, c] = winningLine;
      if (a[0] === 0 && b[0] === 0 && c[0] === 0) {
        const line = document.createElement('div');
        line.classList.add('line-horizontal');
        line.style.top = `calc(33.33% / 2 - 4px)`;
        lineContainer.appendChild(line);
      } else if (a[0] === 1 && b[0] === 1 && c[0] === 1) {
        const line = document.createElement('div');
        line.classList.add('line-horizontal');
        line.style.top = `calc(33.33% + 33.33% / 2 - 4px)`;
        lineContainer.appendChild(line);
      } else if (a[0] === 2 && b[0] === 2 && c[0] === 2) {
        const line = document.createElement('div');
        line.classList.add('line-horizontal');
        line.style.top = `calc(66.66% + 33.33% / 2 - 4px)`;
        lineContainer.appendChild(line);
      } else if (a[1] === 0 && b[1] === 0 && c[1] === 0) {
        const line = document.createElement('div');
        line.classList.add('line-vertical');
        line.style.left = `calc(33.33% / 2 - 4px)`;
        lineContainer.appendChild(line);
      } else if (a[1] === 1 && b[1] === 1 && c[1] === 1) {
        const line = document.createElement('div');
        line.classList.add('line-vertical');
        line.style.left = `calc(33.33% + 33.33% / 2 - 4px)`;
        lineContainer.appendChild(line);
      } else if (a[1] === 2 && b[1] === 2 && c[1] === 2) {
        const line = document.createElement('div');
        line.classList.add('line-vertical');
        line.style.left = `calc(66.66% + 33.33% / 2 - 4px)`;
        lineContainer.appendChild(line);
      } else if (a[0] === 0 && a[1] === 0 && b[0] === 1 && b[1] === 1 && c[0] === 2 && c[1] === 2) {
        const line = document.createElement('div');
        line.classList.add('line-diagonal');
        lineContainer.appendChild(line);
      } else if (a[0] === 0 && a[1] === 2 && b[0] === 1 && b[1] === 1 && c[0] === 2 && c[1] === 0) {
        const line = document.createElement('div');
        line.classList.add('line-diagonal-reverse');
        lineContainer.appendChild(line);
      }
    };
    resetBtn.addEventListener('click', () => {
      stopFireworks();
      gameActive = true;
      currentTurn = 'maru';
      gameState.forEach(row => row.fill(''));
      squares.forEach(square => {
        square.textContent = '';
        square.classList.remove('js-maru-checked', 'js-batsu-checked', 'js-unclickable');
      });
      maruTurnMessage.classList.remove('js-hidden');
      batsuTurnMessage.classList.add('js-hidden');
      maruWinMessage.classList.add('js-hidden');
      batsuWinMessage.classList.add('js-hidden');
      drawMessage.classList.add('js-hidden');
      resetBtn.classList.add('js-hidden');
      const lineContainer = document.querySelector('.line-container');
        if (lineContainer) {
          lineContainer.remove();
        }
    });
    squares.forEach(square => {
      square.addEventListener('click', handleSquareClick);
    });
  updateTurnMessage();
});

CPUの行動パターンを考えるのが難しかったです。

とりあえず「〇が3つ揃いそうなら邪魔してくる」「✕が3つ揃いそうなら勝ちに行く」という感じにしてみました。

fireworksはこちらが勝利した場合のみ発動するようにしました。

枠線を「井」にしてアニメーションさせる

〇✕ゲームっぽく、枠線を漢字の「井」のようにして、さらに1本ずつ「横横縦縦」とアニメーション表示させてみようと思います。

まずCSSに追記します。

.squares-box {
  border: solid 2px transparent;
}
.square {
  border: solid 2px transparent;
}
.line-horizontal2, .line-vertical2 {
  position: absolute;
  background-color: #333;
  z-index: 1;
}
.line-horizontal2 {
  height: 4px;
  width: 100%;
  left: 0;
  transform: scaleX(0);
  transform-origin: left;
}
.line-vertical2 {
  width: 4px;
  height: 100%;
  top: 0;
  transform: scaleY(0);
  transform-origin: top;
}
@keyframes drawLine2 {
  to {
    transform: scale(1);
  }
}
.line-horizontal2 {
  animation: drawLine2 0.3s ease forwards;
}
.line-vertical2 {
  animation: drawLine2 0.3s ease forwards;
}

.squares-boxと.squareの枠線を透明にしておきます。


スクリプトに追記します。

function draw井Animation() {
  const container = document.querySelector('.squares-container');
  const existingLines = container.querySelectorAll('.line-horizontal2, .line-vertical2');
  existingLines.forEach(line => line.remove());
  const line1 = document.createElement('div');
  line1.classList.add('line-horizontal2');
  line1.style.top = 'calc(33.33% - 2px)';
  container.appendChild(line1);
  const line2 = document.createElement('div');
  line2.classList.add('line-horizontal2');
  line2.style.top = 'calc(66.66% - 2px)';
  const line3 = document.createElement('div');
  line3.classList.add('line-vertical2');
  line3.style.left = 'calc(33.33% - 2px)';
  const line4 = document.createElement('div');
  line4.classList.add('line-vertical2');
  line4.style.left = 'calc(66.66% - 2px)';
  setTimeout(() => {
    container.appendChild(line2);
  }, 300);
  setTimeout(() => {
    container.appendChild(line3);
  }, 600);
  setTimeout(() => {
    container.appendChild(line4);
  }, 900);
}
window.addEventListener('load', draw井Animation);
document.querySelector('#reset-btn').addEventListener('click', () => {
  const container = document.querySelector('.squares-container');
  const existingLines = container.querySelectorAll('.line-horizontal2, .line-vertical2');
  existingLines.forEach(line => {
    line.remove();
  });
  setTimeout(() => {
    draw井Animation();
  }, 300);
});

今後の課題

・難易度を「かんたん」「ふつう」「むずかしい」から選べるようにする

・先攻、後攻を選べるようにする

・「現在〇連勝中」とか「〇勝〇敗」みたいに戦績を表示

気が向いたらやります!

 

2024年もお世話になりました。

2025年もよろしくお願いします。

この記事を書いた人
みやの
Web・DTP担当

Contact Us

ご意見、ご相談、料金のお見積もりなど、お気軽にお問い合わせください。

お問い合わせはこちら

TOP