4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-10-24

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

完成品↓

See the Pen Untitled by かっきー (@arakaki621) on CodePen.

必須仕様

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

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

追加仕様

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

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

参考にしたものについて

まず前提知識の習得として、以下の教材を使用しました。

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

まず、React公式のチュートリアルに三目並べがあったのでそれを参考にしようとしましたが、思ったよりコードが読めない?

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

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

必須仕様の実装

マス目、題名等の見た目を作る

何はともあれ、まずは見た目を作っていきましょう。

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;
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関数`に入ってすぐの場所に書いておきます。そもそも既に入力されているマスがクリックされた時に、画面情報や状態を更新する必要がないですし。

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

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

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


















4
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?