「じゃんけん脳トレアプリ」を作りながらReactとTypeScriptを学べるチュートリアル


こんにちは、とまだです。

みなさん、頭の回転は早い方ですか？

私は 30 代前半ですが、**数秒前にやろうとしていたことを忘れる**ことがあり、さすがに不安になってきました。

そんなときには、脳トレが効果的らしいですね！！！！

そこで今回は、React と TypeScript を使って「じゃんけん脳トレアプリ」を作るチュートリアルをご紹介します。

## 作るアプリのイメージ

簡単にいうと「指示通りの手を出すじゃんけん」です。

https://janken-brain-training-ckqy.vercel.app/

このアプリでは指示に従って**勝つ**、**負ける**、**引き分ける**の 3 つのパターンがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/25d11c8e-e522-b069-cbcf-21e8a9dccd06.png)

普通、じゃんけんと言えば常に**勝つ**ことを目指しますが、その常識・ルールを覆すような指示が出されるので、脳トレになるという寸法です。
（よくありますよね）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364501/eb928484-b517-444c-9428-bbfc742b97ad.png)

また、正解数と不正解数が表示されるので、自分の脳トレの成果を確認できます。

では、早速作り方を見ていきましょう！

## 対象となる読者

- React と TypeScript を使ってアプリを作ってみたい初心者
- React に興味がある方
- TypeScript に興味がある方

## 1. プロジェクトの作成

まずは、新しい React プロジェクトを作成しましょう。今回は、高速な開発環境を提供する「Vite」というツールを使います。

ターミナル（コマンドプロンプトや PowerShell）を開いて、以下のコマンドを実行してください。

```bash
npm create vite@latest janken-brain-training -- --template react-ts
```

このコマンドは、「janken-brain-training」という名前のプロジェクトを作成します。また、React と TypeScript を使用するテンプレートを選択しています。

コマンドを実行すると、新しいフォルダが作成されます。そのフォルダに移動しましょう。

```bash
cd janken-brain-training
```

## 2. 依存関係のインストール

プロジェクトのフォルダに移動したら、必要なパッケージをインストールします。以下のコマンドを実行してください。

```bash
npm install
```

次に、スタイリングを簡単に行える Tailwind CSS をプロジェクトに追加します。

```bash
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
```

## 3. Tailwind CSS の設定

Tailwind CSS を使用するための設定ファイルを作成します。以下のコマンドを実行してください。

```bash
npx tailwindcss init -p
```

次に、`tailwind.config.js`ファイルを開いて、以下のように編集します。

```javascript:tailwind.config.js
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

最後に、`src/index.css`ファイルを開いて、以下の内容で置き換えます。

```css:src/index.css
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
```

これで、Tailwind CSS の設定が完了しました！

## 4. 型定義の作成

TypeScript の魅力の 1 つは、型定義によってコードの安全性を高められることです。じゃんけん脳トレアプリで使用する型を定義しましょう。

`src/types.ts`ファイルを作成し、以下の内容を記述します。

```typescript:src/types.ts
// じゃんけんの手を表す型
export type Hand = 'rock' | 'paper' | 'scissors';

// プレイヤーへの指示を表す型
export type Instruction = 'win' | 'lose' | 'draw';

// ゲームの結果を表す型
export type Result = 'correct' | 'incorrect';

// ゲームの状態を表すインターフェース
export interface GameState {
  playerHand: Hand | null;  // プレイヤーの選んだ手
  computerHand: Hand | null;  // コンピューターの手
  instruction: Instruction | null;  // 現在の指示
  result: Result | null;  // ゲームの結果
  score: {  // スコア
    correct: number;  // 正解数
    incorrect: number;  // 不正解数
  };
}
```

<details>
<summary>型定義の詳細説明</summary>

- `Hand` 型：じゃんけんの手（グー、チョキ、パー）を表します。
- `Instruction` 型：プレイヤーへの指示（勝つ、負ける、引き分ける）を表します。
- `Result` 型：ゲームの結果（正解、不正解）を表します。
- `GameState` インターフェース：ゲームの現在の状態を表します。プレイヤーの手、コンピューターの手、指示、結果、スコアが含まれます。

これらの型定義を使用することで、コード内で間違った値を使用するミスを防ぎ、開発効率を向上させることができます。

</details>

## 5. コンポーネントの作成

次に、アプリの主要な部分となるコンポーネントを作成します。`src/components`ディレクトリを作成し、その中に以下の 2 つのファイルを作成してください。

### ScoreBoard.tsx

これは、現在のスコアを表示するコンポーネントです。

```tsx:src/components/ScoreBoard.tsx
import React from 'react';
import { GameState } from '../types';

interface ScoreBoardProps {
  score: GameState['score'];
}

const ScoreBoard: React.FC<ScoreBoardProps> = ({ score }) => {
  return (
    <div className="mt-4 text-xl">
      <p>正解: {score.correct}</p>
      <p>不正解: {score.incorrect}</p>
    </div>
  );
};

export default ScoreBoard;
```

<details>
<summary>ScoreBoard コンポーネントの詳細説明</summary>

このコンポーネントは、現在の正解数と不正解数を表示します。

- `ScoreBoardProps` インターフェース：このコンポーネントが受け取るプロパティの型を定義しています。
- `score` プロパティ：`GameState` の `score` オブジェクトを受け取ります。
- コンポーネントの中身：受け取った `score` オブジェクトから正解数と不正解数を表示します。

シンプルに、現在のスコアを表示するだけのコンポーネントです。

</details>

### GameLogic.tsx

これは、ゲームのメインロジックを処理するコンポーネントです。

```tsx:src/components/GameLogic.tsx
import React, { useState, useCallback, useEffect } from 'react';
import { Hand, Instruction, Result, GameState } from '../types';

const hands: Hand[] = ['rock', 'paper', 'scissors'];
const instructions: Instruction[] = ['win', 'lose', 'draw'];

const getComputerHand = (): Hand => {
  const randomIndex = Math.floor(Math.random() * hands.length);
  return hands[randomIndex];
};

const getInstruction = (): Instruction => {
  const randomIndex = Math.floor(Math.random() * instructions.length);
  return instructions[randomIndex];
};

const getResult = (playerHand: Hand, computerHand: Hand, instruction: Instruction): Result => {
  const actualResult =
    playerHand === computerHand ? 'draw' :
    (playerHand === 'rock' && computerHand === 'scissors') ||
    (playerHand === 'paper' && computerHand === 'rock') ||
    (playerHand === 'scissors' && computerHand === 'paper') ? 'win' : 'lose';

  return actualResult === instruction ? 'correct' : 'incorrect';
};

interface GameLogicProps {
  onStateChange: (newState: GameState) => void;
}

const GameLogic: React.FC<GameLogicProps> = ({ onStateChange }) => {
  const [gameState, setGameState] = useState<GameState>({
    playerHand: null,
    computerHand: getComputerHand(),
    instruction: getInstruction(),
    result: null,
    score: { correct: 0, incorrect: 0 },
  });

  const startNewRound = useCallback(() => {
    setGameState((prevState) => ({
      ...prevState,
      playerHand: null,
      computerHand: getComputerHand(),
      instruction: getInstruction(),
      result: null,
    }));
  }, []);

  const playHand = useCallback((hand: Hand) => {
    if (!gameState.instruction || gameState.result) return;

    const result = getResult(hand, gameState.computerHand!, gameState.instruction);

    setGameState((prevState) => ({
      ...prevState,
      playerHand: hand,
      result,
      score: {
        correct: prevState.score.correct + (result === 'correct' ? 1 : 0),
        incorrect: prevState.score.incorrect + (result === 'incorrect' ? 1 : 0),
      },
    }));

    setTimeout(startNewRound, 2000);
  }, [gameState.instruction, gameState.computerHand, gameState.result, startNewRound]);

  useEffect(() => {
    startNewRound();
  }, [startNewRound]);

  useEffect(() => {
    onStateChange(gameState);
  }, [gameState, onStateChange]);

  return (
    <div className="flex justify-center">
      {hands.map((hand) => (
        <button
          key={hand}
          onClick={() => playHand(hand)}
          className="px-4 py-2 m-2 text-2xl bg-blue-500 text-white rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
        >
          {hand === 'rock' ? '✊' : hand === 'paper' ? '✋' : '✌️'} {hand === 'rock' ? 'グー' : hand === 'paper' ? 'パー' : 'チョキ'}
        </button>
      ))}
    </div>
  );
};

export default GameLogic;
```

<details>
<summary>GameLogic コンポーネントの詳細説明</summary>

このコンポーネントは、ゲームの核となるロジックを処理します。

1. ゲームの状態管理：

   - `useState` フックを使用して、ゲームの状態（`gameState`）を管理します。
   - 初期状態では、コンピューターの手と指示をランダムに設定します。

2. 新しいラウンドの開始：

   - `startNewRound` 関数は、新しいラウンドを開始するためにゲームの状態をリセットします。
   - `useCallback` フックを使用して、この関数をメモ化し、不必要な再レンダリングを防ぎます。

3. プレイヤーの手の処理：

   - `playHand` 関数は、プレイヤーが手を選んだときに呼び出されます。
   - 結果を判定し、スコアを更新し、2 秒後に新しいラウンドを開始します。

4. 副作用の処理：

   - 最初の `useEffect` フックは、コンポーネントがマウントされたときに最初のラウンドを開始します。
   - 2 番目の `useEffect` フックは、ゲームの状態が変更されるたびに親コンポーネントに通知します。

5. UI のレンダリング：
   - 3 つの手（グー、チョキ、パー）のボタンをレンダリングします。
   - 各ボタンをクリックすると、対応する手が選択されます。

役割としては、ユーザーの入力を受け付け、ゲームの進行を制御する部分を担当しています。

</details>

## 6. App.tsx の更新

最後に、アプリケーションのメインコンポーネントである `App.tsx` を更新します。このファイルでは、先ほど作成したコンポーネントを組み合わせてアプリケーション全体を構築します。

`src/App.tsx` ファイルを以下の内容で置き換えてください。

```tsx:src/App.tsx
import React, { useState } from 'react';
import GameLogic from './components/GameLogic';
import ScoreBoard from './components/ScoreBoard';
import { GameState } from './types';

const App: React.FC = () => {
  const [gameState, setGameState] = useState<GameState>({
    playerHand: null,
    computerHand: null,
    instruction: null,
    result: null,
    score: { correct: 0, incorrect: 0 },
  });

  return (
    <div className="container mx-auto p-4 max-w-md">
      <h1 className="text-3xl font-bold text-center mb-6">じゃんけん脳トレ</h1>
      {gameState.instruction && (
        <p className="text-xl text-center mb-4">
          指示: {gameState.instruction === 'win' ? '勝ってください！' :
                 gameState.instruction === 'lose' ? '負けてください！' :
                 '引き分けてください！'}
        </p>
      )}
      {gameState.computerHand && (
        <p className="text-xl text-center mb-4">
          コンピューター: {gameState.computerHand === 'rock' ? '✊' : gameState.computerHand === 'paper' ? '✋' : '✌️'}
        </p>
      )}
      <GameLogic onStateChange={setGameState} />
      <div className="h-16 flex items-center justify-center">
        {gameState.result && (
          <p className="text-2xl font-bold text-center">
            {gameState.result === 'correct' ? '正解！' : '不正解...'}
          </p>
        )}
      </div>
      <ScoreBoard score={gameState.score} />
    </div>
  );
};

export default App;
```

<details>
<summary>App コンポーネントの詳細説明</summary>

このコンポーネントは、アプリケーション全体の構造を定義します。

1. 状態管理：

   - `useState` フックを使用して、ゲームの全体的な状態（`gameState`）を管理します。

2. UI のレンダリング：
   - アプリケーションのタイトルを表示します。
   - 現在の指示（勝つ、負ける、引き分ける）を表示します。
   - コンピューターの選んだ手を表示します。
   - `GameLogic` コンポーネントを配置し、ゲームの状態変更をハンドリングします。
   - 結果（正解または不正解）を表示する固定高さの領域を設けています。これにより、結果表示時にレイアウトが変動するのを防ぎます。
   - `ScoreBoard` コンポーネントを使用して、現在のスコアを表示します。

全体として、ゲームの流れを視覚的に表現し、プレイヤーがゲームの進行状況を把握しやすくなるようにしています。

</details>

## 7. アプリケーションの実行

さあ、いよいよアプリケーションを起動してみましょう！以下のコマンドを実行してください。

```bash
npm run dev
```

このコマンドを実行すると、開発サーバーが起動します。ターミナルに表示される URL をブラウザで開いてください（通常は `http://localhost:5173` です）。

ブラウザでアプリケーションが表示されたら、以下の操作を試してみてください。

1. 画面に表示されるコンピューターの手と指示を確認します。
2. 指示に従って、適切な手のボタンをクリックします。
3. 結果（正解または不正解）が表示されることを確認します。
4. スコアが更新されることを確認します。
5. 2 秒後に新しいラウンドが始まることを確認します。

## まとめ

いかがでしたでしょうか？

インタラクティブ、かつ脳トレ効果のある「じゃんけん脳トレアプリ」を作成することができました。

もう少し発展させるならば、以下のような機能を追加してみるのも面白いかもしれません。

- ゲームの難易度を調整する機能
- ゲームの時間制限を設定する機能
- プレイヤー名を入力してスコアを保存する機能