TypeScriptとReactで三目並べ作成

## 1. はじめに
三目並べ、または別名「ティックタックトー」として知られるゲームは、多くの世代を魅了し続けているシンプルながらも奥深いゲームのひとつです。今回の記事では、最新のフロントエンド技術であるTypeScriptとReactを使用して、三目並べゲームをゼロからデザインし、実装する手順を詳しく解説します。プログラミング初心者から経験者まで、TypeScriptやReactの基本的な知識があれば、自分自身でこのゲームを再現することができます。

## 2. 前提条件と準備
三目並べゲームの実装を進めるために、以下の前提条件を満たしていることが必要です。

### 必要なツール:
1. **Node.js**: クロスプラットフォームに対応したオープンソースのJavaScript実行環境。
2. **npm (Node Package Manager)**: Node.jsで使用するパッケージを管理・インストールするためのツール。

### 準備手順:
1 . まず、Node.jsが既にインストールされているかどうかを確認します。コマンドプロンプトやターミナルを開き、以下のコマンドを入力します。
```bash
node -v
```
このコマンドを実行すると、インストールされているNode.jsのバージョンが表示されます。

2 . 次に、npmがインストールされているかを確認します。
```bash
npm -v
```
もし上記のコマンドでバージョン情報が表示されなかった場合、またはまだNode.jsやnpmをインストールしていない場合は、[Node.js公式サイト](https://nodejs.org/)から最新のバージョンをダウンロードしてインストールしてください。npmはNode.jsに同梱されているため、Node.jsをインストールすることで自動的にnpmも利用可能となります。

これらのツールが整ったら、三目並べの実装をスムーズに進めることができます。準備が整ったら、次のステップに進んでください。

## 3. 新しいReactアプリケーションのセットアップ

Reactのプロジェクトを開始するには、`create-react-app`という便利なツールを活用します。このツールは、Reactアプリケーションを迅速に構築するための初期設定やテンプレートを提供してくれます。今回はTypeScriptを使用してゲームを構築するため、TypeScriptテンプレートを指定してアプリケーションを作成します。

### アプリケーションの作成手順:

1 **プロジェクトの生成**  
以下のコマンドを実行して、`tic-tac-toe`という名前で新しいReactアプリケーションを生成します。

```bash
npx create-react-app tic-tac-toe --template typescript
```

2 **プロジェクトディレクトリへの移動**  
生成されたアプリケーションディレクトリに移動します。

```bash
cd tic-tac-toe
```

3 **アプリケーションの起動**  
以下のコマンドを実行して、アプリケーションをローカルサーバー上で動作させます。

```bash
npm run start
```

ブラウザが自動的に開かれ、http://localhost:3000 にアクセスすると、回転するReactのロゴを見ることができます。これにより、アプリケーションが正常にセットアップされ、実行されていることが確認できます。

これで、Reactアプリケーションの基本的なフレームワークが整いました。次のステップでは、三目並べのゲームロジックとインターフェースを実装していきましょう。


## 4. ゲームボードデザイン
三目並べのボードを整えるための手順を以下に詳述します。

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

`Board.tsx`という新しいコンポーネントを導入するための下準備として、まず適切なディレクトリ構成を整えます。。

```bash
mkdir src/components
touch src/components/Board.tsx
```

### 2.  `Board.tsx`の詳細構築

`Board.tsx`内でのゲームボードの基盤となる構造を以下のように構築します。

```Board.tsx
import React from "react";

type Props = {
  board: Array<Array<string | null>>;
  onClickCell: (row: number, col: number) => void;
};

const Board: React.FC<Props> = ({ board, onClickCell }) => {
  return (
    <div className="board">
      {board.map((rowValues, rowIndex) => (
        <div key={rowIndex} className="board-row">
          {rowValues.map((cellValue, colIndex) => (
            <button
              key={colIndex}
              className="cell"
              data-value={cellValue}
              onClick={() => onClickCell(rowIndex, colIndex)}
            >
              {cellValue}
            </button>
          ))}
        </div>
      ))}
    </div>
  );
};

export default Board;
```

### 3. スタイリングを追加

次に、ゲームボードとセルに基本的なスタイルを追加します。ここではCSSを使っていますが、好みに応じてSCSSやstyled-componentsなどの方法を選択しても構いません。

`App.css`に以下のスタイルを上書きして保存してください。

```App.css
.App {
  text-align: center;
}

.board {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  gap: 5px;
  margin: 0 auto;
  padding: 5px;
  border-radius: 10px;
  box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.3);
  background-color: white;
}

.cell {
  width: 100px;
  height: 100px;
  font-size: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #fff;
  transition: 0.3s all;
  border: 1px solid #333;
  color: #333;
}
```


### 4. `App.tsx`に`Board`コンポーネントを組み込む

`App.tsx`に`Board`コンポーネントを組み込むことで、全体の連携を確立します。

```App.tsx
import React, { useState } from "react";
import "./App.css";
import Board from "./components/Board";

function App() {
  type CellValue = "X" | "O" | null;

  const [board, setBoard] = useState<CellValue[][]>([
    [null, null, null],
    [null, null, null],
    [null, null, null],
  ]);

  const handleCellClick = (row: number, col: number) => {
    // ここでセルのクリックイベントを処理します
  };

  return (
    <div className="App">
      <Board board={board} onClickCell={handleCellClick} />
    </div>
  );
}

export default App;
```

これで、ゲームボードの基本的なデザインと機能が完成しました。セルをクリックしたときの動作は、次のステップでゲームのステート管理を行う際に実装します。



## 5. ゲームのステート管理
ゲームのステート管理を実装し、セルをクリックするとOかXが表示する処理を付け加えて、プレイヤーがセルをクリックするごとに入れ変わるようにしました。

```App.tsx
import React, { useState, useEffect } from "react";
import "./App.css";
import Board from "./components/Board";

function App() {
  type CellValue = "X" | "O" | null;

  const [board, setBoard] = useState<CellValue[][]>([
    [null, null, null],
    [null, null, null],
    [null, null, null],
  ]);

  const [currentPlayer, setCurrentPlayer] = useState<"X" | "O">("X");
  const [winner, setWinner] = useState<CellValue | "Draw">(null);

  const handleCellClick = (row: number, col: number) => {
    // 既にセルが埋まっている、またはゲームが終了している場合は何もしない
    if (board[row][col] || winner) return;

    const newBoard = board.map((row) => [...row]);
    newBoard[row][col] = currentPlayer;

    setBoard(newBoard);

    // 現在のプレイヤーを切り替えます。
    setCurrentPlayer(currentPlayer === "X" ? "O" : "X");
  };

  useEffect(() => {
    const checkWinner = (board: CellValue[][]): CellValue | "Draw" => {
      //ここに勝利条件を確認する処理を書く
      return null;
    };

    const isBoardFull = (board: CellValue[][]): boolean => {
      //ここにボードが埋まってるか判断する処理を書く
      return true;
    };
  }, [board]);

  return (
    <div className="App">
      <Board board={board} onClickCell={handleCellClick} />
    </div>
  );
}

export default App;
```

勝利条件を判定をする仕組みは、次のステップで説明します。


## 6. 勝利条件の確認とリセット機能
勝利条件を判定をする仕組みと、ゲームの勝敗が決定したら、盤面をリセットする機能を追加しました。

```App.tsx
import React, { useEffect, useState } from "react";
import "./App.css";
import Board from "./components/Board";

function App() {
  type CellValue = "X" | "O" | null;

  const [board, setBoard] = useState<CellValue[][]>([
    [null, null, null],
    [null, null, null],
    [null, null, null],
  ]);

  const [currentPlayer, setCurrentPlayer] = useState<"X" | "O">("X");
  const [winner, setWinner] = useState<CellValue | "Draw">(null);

  const handleCellClick = (row: number, col: number) => {
    // 既にセルが埋まっている、またはゲームが終了している場合は何もしない
    if (board[row][col] || winner) return;

    const newBoard = board.map((row) => [...row]);
    newBoard[row][col] = currentPlayer;

    setBoard(newBoard);

    // 現在のプレイヤーを切り替えます。
    setCurrentPlayer(currentPlayer === "X" ? "O" : "X");
  };

  const resetGame = () => {
    setBoard([
      [null, null, null],
      [null, null, null],
      [null, null, null],
    ]);
    setCurrentPlayer("X");
    setWinner(null);
  };

  useEffect(() => {
    const checkWinner = (board: CellValue[][]): CellValue | "Draw" => {
      // 横、縦、斜めの勝利条件をチェック
      for (let i = 0; i < 3; i++) {
        // 横の勝利条件
        if (
          board[i][0] &&
          board[i][0] === board[i][1] &&
          board[i][0] === board[i][2]
        ) {
          return board[i][0];
        }
        // 縦の勝利条件
        if (
          board[0][i] &&
          board[0][i] === board[1][i] &&
          board[0][i] === board[2][i]
        ) {
          return board[0][i];
        }
      }
      // 左上から右下への斜めの勝利条件
      if (
        board[0][0] &&
        board[0][0] === board[1][1] &&
        board[0][0] === board[2][2]
      ) {
        return board[0][0];
      }
      // 右上から左下への斜めの勝利条件
      if (
        board[0][2] &&
        board[0][2] === board[1][1] &&
        board[0][2] === board[2][0]
      ) {
        return board[0][2];
      }

      // 引き分けの条件
      if (isBoardFull(board)) {
        return "Draw";
      }

      // 勝利者がいない場合はnullを返す
      return null;
    };

    const isBoardFull = (board: CellValue[][]): boolean => {
      for (let i = 0; i < 3; i++) {
        for (let j = 0; j < 3; j++) {
          if (!board[i][j]) {
            return false; // セルが空の場合、ボードはまだ埋まっていない
          }
        }
      }
      return true; // 全てのセルが埋まっている
    };

    const result = checkWinner(board);
    if (result === "X" || result === "O" || result === "Draw") {
      setWinner(result);
    }
  }, [board]);

  return (
    <div className="App">
      <Board board={board} onClickCell={handleCellClick} />
      {winner ? (
        <>
          <div className="result">
            {winner === "Draw" ? "Draw" : `Winner: ${winner}`}
          </div>
          <button onClick={resetGame} className="reset-button">
            Game Reset
          </button>
        </>
      ) : null}
    </div>
  );
}

export default App;
```

これで基本的な三目並べの仕組みの実装は完了です。次のステップでスタイリングやUIを改善します。
## 7. スタイリングとUIの改善
App.cssを書き換えれば完成です。

```App.css
.App {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  text-align: center;
  background-color: #f4f4f4;
}

.result {
  font-size: 32px; /* フォントサイズを大きく */
  color: #2c3e50; /* 色をダークブルーに */
  margin-top: 20px; /* 上のマージンを追加 */
}

.reset-button {
  margin-top: 20px; /* 上のマージンを追加 */
  padding: 10px 15px; /* パディングを追加 */
  background-color: #3498db; /* 背景色をブルーに */
  color: white; /* テキストカラーを白に */
  border: none; /* ボーダーを削除 */
  cursor: pointer; /* カーソルをポインターに */
  border-radius: 5px; /* 角を丸く */
  font-size: 16px; /* フォントサイズを変更 */
  transition: background-color 0.3s; /* 背景色の変更を滑らかに */
}

.reset-button:hover {
  background-color: #2980b9; /* ホバー時の背景色を変更 */
}

.board {
  display: grid;
  grid-template-columns: repeat(3, 100px); /* セルのサイズを統一 */
  gap: 5px;
  margin: 0 auto;
  border: 3px solid #333;
  padding: 5px;
  border-radius: 10px;
  box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.3);
  background-color: white;
}

.cell {
  width: 100px;
  height: 100px;
  font-size: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #fff;
  transition: 0.3s all;
  border: 1px solid #333;

  /* セルの文字色を指定する */
  color: #333;
}

/* 〇を表現するためのスタイル */
.cell[data-value="O"] {
  color: #3498db; /* 〇の色を青に */
}

/* ×を表現するためのスタイル */
.cell[data-value="X"] {
  color: #e74c3c; /* ×の色を赤に */
}

.cell:hover {
  background-color: #ddd;
}

button {
  font-family: inherit;
  font-size: inherit;
  color: inherit;
  background-color: transparent;
  border: none;
  cursor: pointer;
}

/* ボタンにフォーカスされた時のスタイルを削除 */
button:focus {
  outline: none;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
    monospace;
}

```

## 8. まとめ

このチュートリアルを通じて、TypeScriptとReactを用いた三目並べのゲームを開発する手順を学びました。このプロジェクトにおいて、以下の要点やテクニックを中心に取り組みました。

1. **環境設定**: Node.jsとnpmのインストールから始め、`create-react-app`を使って新規ReactアプリをTypeScriptテンプレートで生成しました。これにより、TypeScriptの型安全性を取り入れた開発環境をすぐにセットアップできました。

2. **コンポーネントベースの設計**: `Board`コンポーネントの作成を中心に、コンポーネント化を活用し、再利用可能で読みやすいコードの構築を目指しました。

3. **ステート管理**: Reactの`useState`と`useEffect`フックを使用してゲームの状態を管理しました。これにより、セルのクリックイベントや現在のプレイヤー、勝利条件などのゲームのダイナミクスをうまくハンドリングすることができました。

4. **スタイリング**: ゲームボードやセルに基本的なスタイルを適用することで、ユーザーフレンドリーなUIを実現しました。

5. **TypeScriptの利点**: 型安全性を保つことで、バグを早期に検出し、コードの品質を高めました。また、TypeScriptを使用することで、コンポーネントや関数の引数の型を明確にすることができ、後からコードを読み返した際や他の開発者がコードを見た時に、その動作が直感的に理解しやすくなりました。

最後に、三目並べはシンプルなゲームですが、ReactとTypeScriptを使用することで、より効率的かつ綺麗にゲームのロジックを組むことができました。今回の知識は、他のゲームやアプリケーションを作成する際の基盤としても応用可能です。