Reactで神経衰弱アプリ作ってみた（バックエンド編）

Reactを使って神経衰弱アプリを作ろう！ 全6回のうち、今回はバックエンド編です！

　・決起編
　・Front環境構築編
　・フロントエンド構築編
　・コンポーネント作成編
　・バックエンド編 ★本記事
　・リファクタリング編

前回の記事は以下よりご覧いただけます。シリーズ通して読んでいただけたら嬉しいです。 

Reactで神経衰弱アプリ作ってみた（コンポーネント作成編） 

Reactを使って神経衰弱アプリを作ろう！今回はロジック部分となるバックエンド編です。

 

## 【ゲームのイメージ】

 - 12組24枚のカードを用いる
 - カードをシャッフルし、縦3x横8でランダムに配置する
 - 60秒以内にすべての組を揃えることがクリア条件
 - ゲームクリアするとゲームクリアモーダルが、失敗するとゲームオーバーモーダルが表示される
 - モーダルに表示される「もう一回」ボタンを押下すると再度ゲームが始まる

 

## 【実装】

バックエンド側で実装するロジックとしては大きく5 つあります。

①カードを生成する関数

②カードをシャッフルする関数

③ゲーム時間のカウントダウンを行う関数

④ゲーム中カードがクリックされた時の処理を行う関数

⑤モーダルを表示する関数

⑥再ゲームを行うための関数

 

それぞれ実装内容を見ていきたいと思います。

### ①カードを生成する関数

これがないと神経衰弱ができません。

カード情報を生成し、それを②で後述する関数でシャッフルし、結果を返す関数です。この関数を使用することで、トランプのようなペアカードが入った配列を生成できます。

まず、カードの値（A~L）を元に、各値のペアカードを作成します。各カードには一意のid とその対応する value を持つオブジェクトが生成されます。


```
const generateCards = (): CardData[] => {
  const values = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"];
  const cards = values.flatMap((value) => [
    { id: Math.random(), value },
    { id: Math.random(), value },
  ]);
  return shuffle(cards)
};
 ```

### ②カードをシャッフルする関数

続いて、配列の要素をランダムな順序に並べ替え、シャッフルした配列を返す関数を作成します。

この関数では、①で生成されたカード情報の配列を引数とし、ランダムにシャッフルした結果を返します。


```
const shuffle = (array:any) => {
  let currentIndex = array.length;
  let randomIndex;
  // 配列の要素をシャッフルする
  while (currentIndex !== 0) {
      randomIndex = Math.floor(Math.random() * currentIndex);
      currentIndex--;
      // 現在の要素とランダムな要素を交換
      [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
  }
  return array;
}
``` 

①と②を組み合わせて実行することで、以下のように24枚のシャッフルされたカードが生成されます。


```
[
  { id: 0.9345, value: "B" },
  { id: 0.1293, value: "J" },
  { id: 0.1943, value: "B" },
  { id: 0.4381, value: "E" },
  { id: 0.2398, value: "F" },
  // ...他のカード
]
 ```

### ③ゲーム時間のカウントダウンを行う関数

この関数では、React の useEffect フックと setInterval を使用して、カウントダウンタイマーを実装しています。渡された残り時間を1秒ごとに減少させ、タイマーが0になるとカウントダウンを停止します。

グローバルなsetIntervalメソッドとclearIntervalメソッドが、1000ミリ秒（1秒）ごとにカウントダウンしてくれる機能を作ってくれています。
countTimeが変わる（1秒経つ）毎にclearIntervalメソッドが走り、useCountDownTimer関数が実行され直し続けるので誤ってタイマーが重複してしまうことが防げています。


```
const useCountDownTimer = (
  countTime: number | null,
  setCountTime: (arg0: number) => void
) => {
  useEffect(() => {
    const countDownInterval = setInterval(() => {
      if (countTime === 0) {
        clearInterval(countDownInterval);
      }
      if (countTime && countTime > 0) {
        setCountTime(countTime - 1);
      }
    }, 1000);
    return () => {
      clearInterval(countDownInterval);
    };
  }, [countTime]);
};
タイマー（制限時間）を60秒にセットします。



  const TIMEOUT = 60;  
  const [countTime, setCountTime] = useState<number>(TIMEOUT);
  useCountDownTimer(countTime, setCountTime);
 ```

### ④ゲーム中カードがクリックされた時の処理を行う関数

続いて、ゲームのメインロジックとなるカードがクリックされた時の処理を行う関数を作成していきます。この関数では、すでにめくられているカードや揃ったカードがクリックされた場合、何もせずに処理を終了します。新規でカードをクリックされた場合は、クリックされたカードのIDを flippedCards に追加します。2枚目のカードがめくられた場合、それらのカードの値が一致するかを確認し、一致すれば matchedCards に追加します。2枚が揃わなかった場合、1秒後にめくったカードを裏返すために flippedCards をリセットします。


```
  const handleCardClick = (id: number) => {
    //揃い済みのカード、めくったカードをクリックしたら何も処理をせず返す。
    if (
      flippedCards.length === 2 ||
      matchedCards.includes(id) ||
      flippedCards.includes(id)
    ) {
      return;
    }
    //めくられたカードのidをFlippedCardsの要素に追加する。
    //1枚目、2枚目の要素がnewFlippedCardsに追加される。
    const newFlippedCards = [...flippedCards, id];
    setFlippedCards(newFlippedCards);
    //2枚めくられたら以下の判定処理を実施
    if (newFlippedCards.length === 2) {
      const [firstId, secondId] = newFlippedCards;
      //カードの配列のなかから、1枚目、2枚目のカードIDと一致するカード情報を検索
      const firstCard = cards.find((card) => card.id === firstId);
      const secondCard = cards.find((card) => card.id === secondId);
      //1枚目と2枚目のカードの内容が一致するか確認。？を使いnull対策。
      //一致したら、matchedCardsにfirstId,secondIDを追加
      if (firstCard?.value === secondCard?.value) {
        setMatchedCards([...matchedCards, firstId, secondId]);
      }
      //1秒後にsetFlippedCardsを実行し、flippedCards状態が空になり、すべてのカードが裏返しに戻る
      setTimeout(() => setFlippedCards([]), 1000);
    }
  };
``` 

### ⑤モーダルを表示する関数

揃えたカードが24枚に達したらゲームクリアモーダル、制限時間になっても24枚揃えることができなければゲームオーバーモーダルを表示することにしています。
countTimeが変わる（1秒経つ）毎に確かめているため、ゲームクリアモーダル表示までに若干ラグを感じる場合もありますが、そこはご愛嬌ということで…


```
  const NUMBEROFCARDS = 24; //カードの総枚数を明示的に定義
  const [flippedCards, setFlippedCards] = useState<number[]>([]);
  const [matchedCards, setMatchedCards] = useState<number[]>([]);
  const [gameOverModalIsOpen, setGameOverModalIsOpen] = useState(false);
  const [clearModalIsOpen, setClearModalIsOpen] = useState(false);
  useEffect(() => {
    if (matchedCards.length === NUMBEROFCARDS) {
      setClearModalIsOpen(true);
      setCountTime(0); //　タイマーを0にする
      return;
    }
    if (countTime === 0) {
      setGameOverModalIsOpen(true);
      return;
    }
    setClearModalIsOpen(false);
    setGameOverModalIsOpen(false);
  }, [countTime]);
 ```

### ⑥再ゲームを行うための関数

タイマー、ペア成立済のカード、カード情報等のすべての情報を初期化していきます。

これらを初期化することで、新しくシャッフルされたカードが生成され再度ゲームを行うことができます。


```
  const retryGames = () => {
    setCountTime(TIMEOUT);
    setMatchedCards([]);
    setCards(generateCards());
  };
 ```

今回は神経衰弱アプリのバックエンドが担っている処理について解説しました。ゲームロジック自体はとてもシンプルなので、プログラミング経験の浅い方でも実装できる内容かなと思います。遊び方に合わせてタイマーの時間を延長するなどカスタマイズしてみてください！