jsを使った神経衰弱の作り方

## はじめに
いよいよ本格的な冬になってきましたね。
外は寒くて家の中にいることも多くなったと思います。そこで今回は、カードゲームの定番の一つの神経衰弱をjsを使って書いていこうと思います。
### ソースコード一覧
HTML
```
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>神経衰弱 - CPU対戦</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>神経衰弱 - CPU対戦</h1>
  <p id="instructions">プレイヤー vs CPU！カードを2枚めくってペアを揃えましょう。</p>
  <div id="score-board">
    <span id="player-score">プレイヤー: 0</span> | 
    <span id="cpu-score">CPU: 0</span>
  </div>
  <div id="game-board" class="game-board"></div>
  <button id="restart-button">リスタート</button>
  <script src="script.js"></script>
</body>
</html>
```
javaScript
```
const symbols = ["🍎", "🍌", "🍇", "🍓", "🍍", "🍒"];
const cards = [...symbols, ...symbols]; // ペアを作成

const gameBoard = document.getElementById("game-board");
const playerScoreElement = document.getElementById("player-score");
const cpuScoreElement = document.getElementById("cpu-score");
const restartButton = document.getElementById("restart-button");

// 状態管理
let firstCard = null;
let secondCard = null;
let preventClick = false;
let matchedCount = 0;
let playerScore = 0;
let cpuScore = 0;
let isPlayerTurn = true; // プレイヤーのターン管理

// カードをシャッフルする関数
function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
}
shuffle(cards);

// カード生成
function createCards() {
  cards.forEach((symbol) => {
    const card = document.createElement("div");
    card.classList.add("card");
    card.dataset.symbol = symbol;

    card.addEventListener("click", () => {
      if (preventClick || !isPlayerTurn || card.classList.contains("flipped") || card.classList.contains("matched")) {
        return;
      }

      flipCard(card);

      if (!firstCard) {
        firstCard = card; // 最初のカード
      } else {
        secondCard = card; // 2枚目のカード
        checkMatch();
      }
    });

    gameBoard.appendChild(card);
  });
}

// カードを裏返す
function flipCard(card) {
  card.classList.add("flipped");
  card.textContent = card.dataset.symbol;
}

// カードを裏返す（元に戻す）
function unflipCards(card1, card2) {
  setTimeout(() => {
    card1.classList.remove("flipped");
    card2.classList.remove("flipped");
    card1.textContent = "";
    card2.textContent = "";
  }, 1000);
}

// ペア判定
function checkMatch() {
  if (firstCard.dataset.symbol === secondCard.dataset.symbol) {
    firstCard.classList.add("matched");
    secondCard.classList.add("matched");

    // スコア加算
    if (isPlayerTurn) {
      playerScore++;
      playerScoreElement.textContent = `プレイヤー: ${playerScore}`;
    } else {
      cpuScore++;
      cpuScoreElement.textContent = `CPU: ${cpuScore}`;
    }

    matchedCount += 1;

    // ゲーム終了判定
    if (matchedCount === cards.length / 2) {
      setTimeout(() => {
        const winner = playerScore > cpuScore ? "プレイヤーの勝ち！" : "CPUの勝ち！";
        alert(`${winner}`);
      }, 500);
    }

    // ペアが揃った場合はターンを維持
    resetTurn(false); // ターンを切り替えない
    if (!isPlayerTurn) {
      setTimeout(cpuTurn, 1000); // CPUがペアを当てたらもう一度カードをめくる
    }
  } else {
    preventClick = true;
    unflipCards(firstCard, secondCard);
    setTimeout(() => {
      resetTurn(true); // ターンを切り替える
    }, 1000);
  }
}

// ターンをリセット
function resetTurn(switchTurn) {
  firstCard = null;
  secondCard = null;
  preventClick = false;

  // ターンを切り替える場合のみフラグを変更
  if (switchTurn) {
    isPlayerTurn = !isPlayerTurn;

    // CPUのターンの場合は自動で動作
    if (!isPlayerTurn) {
      setTimeout(cpuTurn, 1000);
    }
  }
}

// CPUのターン
function cpuTurn() {
  const availableCards = Array.from(document.querySelectorAll(".card:not(.flipped):not(.matched)"));

  if (availableCards.length < 2) return;

  // ランダムで2枚のカードを選ぶ
  const firstChoice = availableCards[Math.floor(Math.random() * availableCards.length)];
  flipCard(firstChoice);

  let secondChoice;
  do {
    secondChoice = availableCards[Math.floor(Math.random() * availableCards.length)];
  } while (secondChoice === firstChoice);

  setTimeout(() => {
    flipCard(secondChoice);
    firstCard = firstChoice;
    secondCard = secondChoice;
    checkMatch();
  }, 1000);
}

// ゲームリセット
function resetGame() {
  gameBoard.innerHTML = "";
  firstCard = null;
  secondCard = null;
  preventClick = false;
  matchedCount = 0;
  playerScore = 0;
  cpuScore = 0;
  isPlayerTurn = true;

  playerScoreElement.textContent = "プレイヤー: 0";
  cpuScoreElement.textContent = "CPU: 0";

  shuffle(cards);
  createCards();
}

// 初期化
createCards();
restartButton.addEventListener("click", resetGame);

```
CSS
```
/* 基本スタイル */
body {
  font-family: 'Arial', sans-serif;
  text-align: center;
  margin: 0;
  padding: 0;
  background-color: #f4f4f4;
}

h1 {
  margin-top: 20px;
  color: #333;
}

#instructions {
  margin: 10px 0;
  font-size: 16px;
  color: #555;
}

/* ゲームボード */
.game-board {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
  gap: 10px;
  max-width: 600px;
  margin: 20px auto;
}

/* カードスタイル */
.card {
  width: 100px;
  height: 150px;
  background-color: #ccc;
  border: 2px solid #333;
  border-radius: 8px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 24px;
  color: transparent;
  cursor: pointer;
  transition: transform 0.3s, background-color 0.3s;
}

.card.flipped, .card.matched {
  background-color: #fff;
  color: #000;
  cursor: default;
  transform: rotateY(180deg);
}

/* リスタートボタン */
#restart-button {
  margin-top: 20px;
  padding: 10px 20px;
  font-size: 16px;
  background-color: #007BFF;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  transition: background-color 0.3s;
}

#restart-button:hover {
  background-color: #0056b3;
}
/* スコア表示 */
#score-board {
  margin-top: 10px;
  font-size: 18px;
  color: #333;
}
```
#### コードの解説
　カードの生成をしている関数では、配列 cards をループして、それぞれのシンボルに対応するカードを1枚ずつ作成します。最初のカードをクリックすると firstCard にセットし、2枚目のカードをクリックすると secondCard にセットし、ペア判定を行います。さらに、生成されたカードをすべて gameBoard に追加し、プレイ可能な状態にします。
　ペア判定をしている関数ではdataset.symbol を比較して、2枚のカードが同じシンボルであるかを判定しています。
　CPUの行動を設定している関数では、random関数を使用し一枚目のめくるカードを決定し、二枚目のめくるカードを決めるときはdo文を使い一枚目のカードと被らないようにしています。
 #### 実行結果
 ![スクリーンショット 2024-12-16 143713.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943867/b5d03bfc-a394-f6cd-2499-99b03a24a1e6.jpeg)
 スタート画面
 ![スクリーンショット 2024-12-16 144056.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943867/d33e9465-feb9-1b13-0260-d1dbafb8b0f0.jpeg
 )実行中
 ![スクリーンショット 2024-12-16 144331.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943867/8e726611-03bc-be90-42dd-a5f9082f8fbd.jpeg)
 ペアを当てた時
 ![スクリーンショット 2024-12-16 150554.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943867/88fdac67-d1ab-d7f2-cbd6-77695e8c31d8.jpeg)
 プレイヤー勝利時
 ![スクリーンショット 2024-12-16 150801.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943867/6062e821-856b-63fe-158e-d264ccddde6c.jpeg)
 CPU勝利時
 #### まとめ
 　神経衰弱を作ってみて思ったことはカードのペア判定やCPUのカードのめくり方を定義することがとても難しいと感じました。
  　また、改善点として、CPUのカードのめくり方をめくられたことのあるカードの配置を記憶し、そのデータをもとにめくるカードを決めるといった機能の追加ができればさらにjsについての理解を深めることができると思いました。