React + JavaScriptでまるばつゲーム(三目並べ)を作ってみた。

業務で念願のReact案件に携わることになったのですが、
Reactの知識ゼロのため一週間言語学習の時間に充てていました。
その中で、実際に何かを作ってみようということになり、まるばつゲームを作ったので復習も兼ねてまとめます。

完成品↓
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="oNeaqmy" data-user="arakaki621" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/arakaki621/pen/oNeaqmy">
  Untitled</a> by かっきー (<a href="https://codepen.io/arakaki621">@arakaki621</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# 必須仕様

- ３×３のマス目を表示させ、クリックしたマス目上に◯もしくは×を表示させる。
- "◯の番です。"と"×の番です。"を交互に表示させる。
- リセットボタンを押下すると、始めからスタートできるようにする。

一旦ここまでの実装で、という話になりました。

# 追加仕様
- 勝敗判定をする。
- 勝敗判定後、マス目を押せないようにする。
- 既に◯か×が表示されている箇所を2回以上クリックしても変更できないようにする。

上記2つが自分で実装したいと思った機能です。  

# 参考にしたものについて
まず前提知識の習得として、以下の教材を使用しました。

- [モダンJavaScriptの基礎から始める挫折しないためのReact入門](https://www.udemy.com/course/modern_javascipt_react_beginner/)
- [モダンJavaScriptの基本から始める React実践の教科書](https://www.amazon.co.jp/%E3%83%A2%E3%83%80%E3%83%B3JavaScript%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%81%8B%E3%82%89%E5%A7%8B%E3%82%81%E3%82%8B-React%E5%AE%9F%E8%B7%B5%E3%81%AE%E6%95%99%E7%A7%91%E6%9B%B8-%E6%9C%80%E6%96%B0ReactHooks%E5%AF%BE%E5%BF%9C-Informatics-IDEA/dp/481561072X)

Udemyで実際にコードを書きながら進めて雰囲気掴み、実際にコーディングしている時は、書籍を読みながら進めていました。

まず、[React公式のチュートリアル](https://ja.reactjs.org/tutorial/tutorial.html)に三目並べがあったのでそれを参考にしようとしましたが、思ったよりコードが読めない？

会社の先輩に相談したら、(2021年10月時点では)React公式はクラスコンポーネントという概念を使っており、上記参考にしたものには載ってないかもとのこと。

なので上記参考にしたものをベースに、調べたり質問したりしながら進めることにしました。

# 必須仕様の実装
## マス目、題名等の見た目を作る
何はともあれ、まずは見た目を作っていきましょう。

```javascript:App.js
import React from 'react';
import './App.css';

const App = () => {
  return (
    <>
      <div className="title">
        <p>◯×ゲーム</p>
      </div>
      <div className="turn">
        <p>◯の番だよ〜</p>
      </div>
      <div className="grid_squares">
        <button className="squares"></button>
        <button className="squares"></button>
        <button className="squares"></button>
        <button className="squares"></button>
        <button className="squares"></button>
        <button className="squares"></button>
        <button className="squares"></button>
        <button className="squares"></button>
        <button className="squares"></button>
      </div>
      <div className="wrapper_reset">
        <button className="reset"> リセット
        </button>
      </div>
    </>
  );
}

export default App;
```

``````javascript:App.css
// 題名表示
.title {
  text-align: center;
  font-size: 30px;
}

// 順番表示
.turn {
  text-align: center;
  font-size: 20px;
}

// マス目
.squares {
  font-size: 30px;
  width: 50px;
  height: 50px;
  background-color: white;
  border: 1px solid black;
}

.grid_squares {
  display: grid;
  justify-content: center;
  grid-gap: 0px;
  grid-template-columns: 50px 50px 50px;
  grid-template-rows: 50px 50px 50px;
}

// リセットボタン
.reset {
  width: 100px;
  height: 50px;
  margin: 30px;
  border-radius: 10px;
}

.wrapper_reset {
  text-align: center;
}
```

</div></details>

下記画像のようになりましたか？
![23845905-0DDA-4334-9BAE-925EABE1AEA2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1833388/428f5570-9504-d4dd-48b0-fded8634a51e.jpeg)

それでは見た目が出来上がったところで、ロジック部分を実装していきましょう！

## ボタン9個をスッキリさせる
まず全く同じことを9回も書いているボタン部分を変更します。
◯か×が入るマス目9個を配列で扱えるようにしていきましょう。

``` javascript:App.js
import React, { useState } from 'react';
import './App.css';

const App = () => {
  const [gameBoard, setGameBoard] = useState(new Array(9).fill(""));

  return (
    <>
      <div className="title">
        <p>◯×ゲーム</p>
      </div>
      <div className="turn">
        <p>◯の番だよ〜</p>
      </div>
      <div className="grid_squares">
        {gameBoard.map((board, index) => {
          return (
          <button
           key={index}
           className="squares">{board}</button>)
        })}
      </div>
      <div className="wrapper_reset">
        <button className="reset"> リセット
        </button>
      </div>
    </>
  );
}

export default App;
```

</div></details>

だいぶスッキリしました！
マス目がクリックされる度に描画更新をしたいので、useStateを使用し、宣言と初期化。

先ほど9回も書いていたボタン部分はmap関数を使用し、配列の数(9個)分ループして表示するようにしています。

上記画像と同じように表示されているはずです！

## クリックしたマス目上に◯もしくは×を交互に表示

```javascript
import React, { useState } from 'react';
import './App.css';

const App = () => {
  // マス目
  const [gameBoard, setGameBoard] = useState(new Array(9).fill(""));
  // ゲームの順番
  const [gameTurn, setGameTurn] = useState(true);
  // 表示させる文字列
  const [displayStr, setDisplayStr] = useState("〇の番だよ〜")

  const onClickButton = (index) => {
    // 現在の画面情報を一旦コピー
    const newGameBoard = [...gameBoard];
    // どちらの順番かを判定、〇か×をセット
    newGameBoard[index] = (gameTurn === true ? "〇" : "×");
    // 各情報を更新
    setGameTurn(!gameTurn);
    setGameBoard(newGameBoard);
    if (gameTurn === true) {
      setDisplayStr("×の番だよ〜");
    } else {
      setDisplayStr("〇の番だよ〜");
    }
  }

  return (
    <>
      <div className="title">
        <p>◯×ゲーム</p>
      </div>
      <div className="turn">
        <p>{displayStr}</p>
      </div>
      <div className="grid_squares">
        {gameBoard.map((board, index) => {
          return (
          <button
          key={index}
          className="squares"
          onClick={() => {
            onClickButton(index);
          }}>{board}</button>)
        })}
      </div>
      <div className="wrapper_reset">
        <button className="reset"> リセット
        </button>
      </div>
    </>
  );
}

export default App;
```

</div></details>

急に難しそうになりましたが、変数や関数の役割1つずつ確認していけば大丈夫です。

- `gameTurn`　○の番か、×の番かを管理
- `displayStr`　プレイヤーが○の番か、×の番かを分かるように表示させる文字列を管理（後々、勝敗が決着した場合もこちらのdisplayStrを使用して○の勝ち！と表示させるようにします。）
- `onClickButton`　ロジックの肝となる関数。マス目がクリックされる度に呼ばれます。やっていることは単純で、クリックされたら今どちらの番かを判定し、◯か×をマス目に反映させます。その後は順番、マス目全体、表示させる文字列を更新します。

描画に関するreturn以降の部分ですが、この中でややこしいのはmap関数の中の処理かなと思います。
変数定義の際に、上記で”配列の数(9個)分ループして表示するようにしています。”とさらっと書きましたが、もうちょっと細かく説明すると、「レンダリングされる度に、◯か×か空白が入っている配列の中身を1個表示させている。のを9回ループしている。」という感じです。

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1833388/91da33f1-2ae0-4cbf-2d21-62cbd9e16e5b.gif)

上記のように、クリックしたら順番に応じて◯か×かがマス目と文字列に反映されるようになりました！

## リセットできるようにする
こちらの実装は簡単ですね。リセットボタン押下時に、全ての状態を初期化するだけです。

```javascript:App.js
const onClickReset = () => {
    // 初期化
    setGameBoard(new Array(9).fill(""));
    setGameTurn(true);
    setDisplayStr("〇の番だよ〜");
  }
```

```javascript:App.js
<div className="wrapper_reset">
<button className="reset" onClick={onClickReset}>リセット</button>
</div>
```

</div></details>

これで必須仕様完成しました〜〜〜！！！
ここから先は追加仕様として実装していきます。

# 追加仕様の実装
## 勝敗判定

先に言っておくと、特に勝敗判定に関しては自分でもいいコードを書けていなくて；；
私はこう組みました、っていうのを以下に貼っておきます；；

```javascript:App.js
 // 勝ちパターン
  const complete_patterns = [
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];

const onClickButton = (index) => {
    // 現在の画面情報を一旦コピー
    const newGameBoard = [...gameBoard];
    // どちらの順番かを判定、〇か×をセット
    newGameBoard[index] = (gameTurn === true ? "〇" : "×");
    // 各情報を更新
    setGameTurn(!gameTurn);
    setGameBoard(newGameBoard);
    if (gameTurn === true) {
      setDisplayStr("×の番だよ〜");
    } else {
      setDisplayStr("〇の番だよ〜");
    }

    // 勝敗判定
    for (let i = 0; i < complete_patterns.length; i++) {
      if (
        newGameBoard[complete_patterns[i][0]] ===
        newGameBoard[complete_patterns[i][1]] &&
        newGameBoard[complete_patterns[i][0]] ===
        newGameBoard[complete_patterns[i][2]]
      ) {
        if (newGameBoard[complete_patterns[i][0]] !== "") {
          setDisplayStr(`${gameTurn === true ? "〇" : "×"}の勝ち！`);
          return;
        }
      }
    }
  }
```

</div></details>

ロジックとしては、まず`complete_patterns`にて、勝ちパターンを全て定義しておきます。
ボタンがクリックされる度に、全ての勝ち手パターンと盤面を比べて、3つのマス目が同じであるかを判定する、という内容です。

上記内容は、勝敗判定コードでいうfor文とif分で判定しています。
（A＝B、A＝CということはB＝Cという三段論法（？）的な感じで）

2つ目のif文の""判定はあまりいい案が思い浮かばず、、、妥協案のようになっています。
(ゲーム開始時、◯も×も押されておらずマス目は全て空白なので、1つ目のif文を通ってしまい勝利条件に判定されてしまいます。空白の時だけ勝利判定しない、というようにしています。)

![1569FD7D-C0B2-4457-859F-0C88728E1364.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1833388/6f6f7f82-edf0-4b4f-d39e-0af5b5d78375.jpeg)

一応これで勝利判定はできるようになりました！

## 勝敗判定後、クリック禁止
```javascript:App.js
// ゲーム続行判定
  const [isGameEnd, setIsGameEnd] = useState(false);

```
勝負の決着がついたかを管理する変数を追加します。
falseの場合はゲーム中、trueの場合はゲーム終了(勝利判定済み)です。

```javascript:App.js
// 勝敗判定
    for (let i = 0; i < complete_patterns.length; i++) {
      if (
        newGameBoard[complete_patterns[i][0]] ===
        newGameBoard[complete_patterns[i][1]] &&
        newGameBoard[complete_patterns[i][0]] ===
        newGameBoard[complete_patterns[i][2]]
      ) {
        if (newGameBoard[complete_patterns[i][0]] !== "") {
          setDisplayStr(`${gameTurn === true ? "〇" : "×"}の勝ち！`);
          setIsGameEnd(true);
          return;
        }
      }
    }
```
決着がついた時は`setIsGameEnd`でtrueに状態更新。

```javascript:App.js
return (
    <>
      <div className="title">
        <p>◯×ゲーム</p>
      </div>
      <div className="turn">
        <p>{displayStr}</p>
      </div>
      <div className="grid_squares">
        {gameBoard.map((board, index) => {
          return (
          <button
          key={index}
          className="squares"
          onClick={() => {
            onClickButton(index);
          }}
          disabled={isGameEnd}>{board}</button>)
        })}
        
      </div>
      <div className="wrapper_reset">
      <button className="reset" onClick={onClickReset}>リセット</button>
      </div>
    </>
  );
```

ボタンの表示/非表示は`disabled`を切り替えます。
`disabled`がtrueの場合は非表示、falseの場合は表示されるので、`isGameEnd`を使用します。

これで勝利判定した後でも、ゲームが続くことはなくなりました！

## 2回以上クリック禁止
ボタンクリックされた際に、既に○か×が表示されていたらアラートを出すようにします。

```javascript:App.js
if (gameBoard[index] !== "") {
      // クリック済
      alert("押せないよ！");
      return;
    }
```

上記判定でOKです！
書く場所についてですが、`onClickButton関数`に入ってすぐの場所に書いておきます。そもそも既に入力されているマスがクリックされた時に、画面情報や状態を更新する必要がないですし。

# 終わりに
ちょっと自分的にあまりいけてないコードだなあとは思いつつも、自分の考えをコードにすることは大事だと思うので、理解の整理も含めて記事を書いてみました。
もしこのコードを参考にする方がいたら、ロジックやデザイン等、ぜひ色々自由に変えてみてください！

ちなみに、あえて引き分け判定は書きませんでした。
私の思いつく限りでは後は引き分け判定を実装すればまるばつゲームとしてはいけてると思うので追加で実装してみてください。

後は私への課題としてはコンポーネント化したいなあと考えています。
もしかしたらコンポーネント化する記事も書く、、、かも、、、？