Reactによるブラウザベースのマルチプレイヤー三目並べゲーム
Published: 2024-03-10 22:36:26

**この記事では、ReactでTic Tac Toeゲームを作成する手順について説明します。** 特に、この記事では[React SDKの](https://www.pubnub.com/docs/sdks/react)バージョン1を使用していますが、以下の手順と原則はすべて現在でも有効です。

三目並べは、子供の頃の[ゲームの](https://www.pubnub.com/solutions/gaming/)典型です。必要なのは、書くものと書くものだけです。しかし、別の場所にいる人と遊びたい場合はどうすればいいのだろう？この場合、あなたと他のプレイヤーをゲームにつなぐアプリケーションを使う必要があります。

そのアプリケーションは、あなたの一挙手一投足が瞬時に相手に伝わり、またその逆もしかりとなるようなリアルタイム体験を提供する必要がある。もしアプリケーションがこのような体験を提供しないのであれば、あなただけでなく多くの人はもうそのアプリケーションを使うことはないでしょう。

では、どうすれば開発者は、プレイヤーが世界中どこにいても、チックタック・トゥやどんなゲームでもプレイできるような[コネクテッド体験を](https://www.pubnub.com/blog/connected-shared-experiences-a-developers-guide/)提供できるのでしょうか？

リアルタイム・マルチプレイヤー・ゲームのコンセプト
-------------------------

マルチプレイヤーゲームのリアルタイムインフラを提供するには、いくつかの方法がある。[Socket.IO](https://www.pubnub.com/learn/glossary/what-is-socketio/)、[SignalR](https://pubnub.com/guides/what-is-signalr/)、[WebSocketなどの](https://www.pubnub.com/learn/glossary/what-is-websocket/)技術やオープンソースのプロトコルを使用して、独自のインフラを一から構築するルートがあります。

これは一見魅力的なルートに見えるかもしれないが、いくつかの問題が発生する。100人のユーザーを扱うのは難しくないが、10万人以上のユーザーをどう扱うのか？インフラの問題に加えて、ゲームのメンテナンスも心配しなければならない。

結局のところ、重要なのはゲームのプレイヤーに素晴らしい体験を提供することだけです。しかし、インフラの問題はどのように解決するのでしょうか？そこでPubNubの登場です。

PubNubは、グローバルな[データストリームネットワークを通じて](https://pubnub.com/products/pubnub-platform/)、あらゆるアプリケーションに電力を供給するリアルタイムインフラを提供します。 最も一般的なプログラミング言語を含む[70以上のSDKを](https://pubnub.com/docs/)備えたPubNubは、**100ミリ秒未満で**あらゆるデバイスへのメッセージの送受信を簡素化します。セキュアでスケーラブル、かつ信頼性が高いため、独自のインフラストラクチャの作成や保守を心配する必要はありません。

PubNubを使用してマルチプレイヤーゲームを開発することがいかに簡単であるかを示すために、[PubNub React SDKを](https://pubnub.com/docs/sdks/javascript/react)使用して簡単なReactチックタックトゥゲームを構築します。このゲームでは、2人のプレイヤーがユニークなゲームチャンネルに接続して対戦します。プレイヤーの一挙手一投足がチャンネルに公開され、相手のボードをリアルタイムで更新します。

アプリの概要
------

完成後のアプリはこんな感じになります。

![Screen shot of the React Tic Tac Toe Game](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jpy/5d2874d8b7c577a9ce9a5acc6ade755f/React-tic-tac-toe-overview.png "Screen shot of the React Tic Tac Toe Game")プレイヤーはまずロビーに参加し、そこでチャンネルを作成するか、チャンネルに参加します。チャンネルを作成したプレイヤーは_ルームIDを_取得し、他のプレイヤーと共有します。チャンネルを作成したプレイヤーは_プレイヤーXと_なり、ゲーム開始時に先手を取ります。

[![Create a room channel](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jpz/d6a1b08ddc0924624f35e779c0c0059a/create-room.gif "Create a room channel")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jpz/d6a1b08ddc0924624f35e779c0c0059a/create-room.gif)

与えられた_ルームID_でチャンネルに参加したプレイヤーは_プレイヤーOと_なります。複数人がいる場合、そのチャンネルではゲームが進行中であり、プレイヤーは参加できません.チャンネルにプレーヤーが2人いるとゲームが開始されます。

[![Join the room channel](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq0/96a0ccd6b75f19b211747f353b27bd39/join-channel.gif "Join the room channel")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq0/96a0ccd6b75f19b211747f353b27bd39/join-channel.gif)

ゲーム終了時に勝者の得点が1点加算されます。ゲームが引き分けに終わった場合、どちらのプレイヤーにも得点は与えられません。_プレイヤーXに_新しいラウンドを始めるかゲームを終了するかのモーダルが表示される。プレイヤー_Xが_ゲームを続行する場合、ボードは新しいラウンドのためにリセットされます。そうでない場合、ゲームは終了し、両プレイヤーはロビーに戻ります。

[![Exit to lobby](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq1/f475cc1aeed8a4184348569634ed968d/exit-to-lobby-1.gif " Exit to lobby")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq1/f475cc1aeed8a4184348569634ed968d/exit-to-lobby-1.gif)

ロビーのセットアップ
----------

ロビーをセットアップする前に、無料のPubNubアカウントにサインアップして[PubNubの管理ダッシュボードから](https://admin.pubnub.com)無料のPub/Sub APIキーを取得してください。

キーを取得したら**App.jsの**コンストラクタに挿入する。

```js
// App.js
import React, { Component } from 'react';
import Game from './Game';
import Board from './Board';
import PubNubReact from 'pubnub-react';
import Swal from "sweetalert2";
import shortid  from 'shortid';
import './Game.css';

class App extends Component {
  constructor(props) {
    super(props);
    // REPLACE with your keys
    this.pubnub = new PubNubReact({
      publishKey: "YOUR_PUBLISH_KEY_HERE",
      subscribeKey: "YOUR_SUBSCRIBE_KEY_HERE"
    });

    this.state = {
      piece: '', // X or O
      isPlaying: false, // Set to true when 2 players are in a channel
      isRoomCreator: false,
      isDisabled: false,
      myTurn: false,
    };

    this.lobbyChannel = null; // Lobby channel
    this.gameChannel = null; // Game channel
    this.roomId = null; // Unique id when player creates a room
    this.pubnub.init(this); // Initialize PubNub
  }

  render() {
    return ();
    }
  }

  export default App;
```

また、コンストラクタの中で状態オブジェクトと変数を初期化する。オブジェクトと変数についてはこのファイルを通して説明する。最後にコンストラクタの最後でPubNubを初期化する。

**render**メソッドの中と**return**文の中にLobbyコンポーネントのマークアップを追加する。

```js
return (
    <div>
      <div className="title">
        <p> React Tic Tac Toe </p>
      </div>

      {
        !this.state.isPlaying &&
        <div className="game">
          <div className="board">
            <Board
                squares={0}
                onClick={index => null}
              />

            <div className="button-container">
              <button
                className="create-button "
                disabled={this.state.isDisabled}
                onClick={(e) => this.onPressCreate()}
                > Create
              </button>
              <button
                className="join-button"
                onClick={(e) => this.onPressJoin()}
                > Join
              </button>
            </div>

          </div>
        </div>
      }

      {
        this.state.isPlaying &&
        <Game
          pubnub={this.pubnub}
          gameChannel={this.gameChannel}
          piece={this.state.piece}
          isRoomCreator={this.state.isRoomCreator}
          myTurn={this.state.myTurn}
          xUsername={this.state.xUsername}
          oUsername={this.state.oUsername}
          endGame={this.endGame}
        />
      }
    </div>
);
```

Lobbyコンポーネントはタイトル、空のチックタック・トゥ・ボード（プレイヤーがマスを押しても何も起こらない）、「Create_」と_「Join_」_ボタンで構成される。このコンポーネントは、ステート値_isPlayingが_falseの場合のみ表示されます。もしisPlayingがtrueに設定されていれば、ゲームは開始され、コンポーネントはGameコンポーネントに変更されます。

BoardコンポーネントはLobbyコンポーネントの一部でもある。[Board](https://github.com/PubNubDevelopers/react-tictactoe/blob/master/src/Board.js)コンポーネントの中に[Square](https://github.com/PubNubDevelopers/react-tictactoe/blob/master/src/Square.js)コンポーネントがあります。ロビーコンポーネントとゲームコンポーネントに集中するために、この2つのコンポーネントの詳細には触れません。

プレイヤーが'Create'ボタンを押すと、ボタンは無効化され、複数のチャンネルを作ることはできません。Join'ボタンは無効化されませんが、これはプレイヤーがチャンネルに参加する場合に備えています。Create」ボタンが押されると、**onPressCreate()**メソッドが呼び出されます。

### チャンネルの作成

**onPressCreate()** で最初にすることは、5文字に切り詰めたランダムな文字列IDを生成することです。これは[**shortid()**](https://www.npmjs.com/package/shortid)を使って行う。この文字列を'_tictactoelobby-'_に追加することで、プレイヤーが購読するユニークなロビーチャンネルとなります。

```js
// Create a room channel
onPressCreate = (e) => {
  // Create a random name for the channel
  this.roomId = shortid.generate().substring(0,5);
  this.lobbyChannel = 'tictactoelobby--' + this.roomId; // Lobby channel name

  this.pubnub.subscribe({
    channels: [this.lobbyChannel],
    withPresence: true // Checks the number of people in the channel
  });
}
```

つのチャンネルに2人以上のプレイヤーが参加できないようにするために、[Presenceを](https://pubnub.com/docs/sdks/javascript/react)使用します。後ほど、チャンネルの占有率をチェックするロジックについて説明します。

プレイヤーがロビーチャンネルに登録すると、ルーム ID がモーダルで表示され、他のプレイヤーがそのチャンネルに参加できるようになります。

[![Share the room id](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq2/2e171d056766c2d974cb5b9f3001b5a3/share-roomid-1.png "Share the room id")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq2/2e171d056766c2d974cb5b9f3001b5a3/share-roomid-1.png)

このモーダルと、このアプリで使用されているすべてのモーダルは、JavaScript のデフォルトの alert() ポップアップ ボックスを置き換えるために、[**SweetAlert2**](https://sweetalert2.github.io/#download)によって作成されています。

```js
// Inside of onPressCreate()
// Modal
Swal.fire({
  position: 'top',
  allowOutsideClick: false,
  title: 'Share this room ID with your friend',
  text: this.roomId,
  width: 275,
  padding: '0.7em',
  // Custom CSS to change the size of the modal
  customClass: {
      heightAuto: false,
      title: 'title-class',
      popup: 'popup-class',
      confirmButton: 'button-class'
  }
})
```

**onPressCreate()**の最後で、アプリの新しい状態を反映するために状態の値を変更します。

```js
this.setState({
  piece: 'X',
  isRoomCreator: true,
  isDisabled: true, // Disable the 'Create' button
  myTurn: true, // Player X makes the 1st move
});
```

プレイヤーがルームを作成したら、他のプレイヤーがそのルームに参加するのを待つ必要があります。ルームに参加するためのロジックを見てみましょう。

### チャンネルに参加する

プレイヤーが'Join'ボタンを押すと、**onPressJoin()**が呼び出されます。入力フィールドに_ルームIDを_入力するよう、プレイヤーにモーダルが表示されます。

[![Enter the room id](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq3/97e46c029a2583ac018ab14f44ce8572/enter-roomid.png "Enter the room id")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq3/97e46c029a2583ac018ab14f44ce8572/enter-roomid.png)

プレイヤーが_ルーム_ _IDを_入力して「OK」ボタンを押すと、**joinRoom(value)** **が**呼び出されます。このメソッドは入力フィールドが空の場合や、プレイヤーが「キャンセル」ボタンを押した場合には呼び出されません。

```js
// The 'Join' button was pressed
onPressJoin = (e) => {
  Swal.fire({
    position: 'top',
    input: 'text',
    allowOutsideClick: false,
    inputPlaceholder: 'Enter the room id',
    showCancelButton: true,
    confirmButtonColor: 'rgb(208,33,41)',
    confirmButtonText: 'OK',
    width: 275,
    padding: '0.7em',
    customClass: {
      heightAuto: false,
      popup: 'popup-class',
      confirmButton: 'join-button-class',
      cancelButton: 'join-button-class'
    }
  }).then((result) => {
    // Check if the user typed a value in the input field
    if(result.value){
      this.joinRoom(result.value);
    }
  })
}
```

**joinRoom()**で最初に行うことは、**onPressCreate()**で行ったことと同様に、**valueを**'_tictactoelobby-'_に追加することです。

```js
// Join a room channel
joinRoom = (value) => {
  this.roomId = value;
  this.lobbyChannel = 'tictactoelobby--' + this.roomId;
}
```

プレイヤーがロビーチャンネルに登録する前に、[hereNow()](https://pubnub.com/docs/sdks/javascript/react)を使用してチャンネルの総占有率をチェックする必要があります。 総占有率が2未満であれば、プレイヤーはロビーチャンネルに登録することができます。

```js
// Check the number of people in the channel
this.pubnub.hereNow({
  channels: [this.lobbyChannel],
}).then((response) => {
    if(response.totalOccupancy < 2){
      this.pubnub.subscribe({
        channels: [this.lobbyChannel],
        withPresence: true
      });

      this.setState({
        piece: 'O', // Player O
      });

      this.pubnub.publish({
        message: {
          notRoomCreator: true,
        },
        channel: this.lobbyChannel
      });
    }
}).catch((error) => {
  console.log(error);
});
```

プレイヤーがロビーチャンネルに登録すると、_pieceの_状態が'O'に変更され、そのロビーチャンネルにメッセージが発行される。このメッセージは、他のプレイヤーがチャンネルに参加したことを_プレイヤーXに_通知します。メッセージリスナーは**componentDidUpdate()**で設定します。

総占有率が2より大きい場合、ゲームが進行中であり、チャンネルに参加しようとしたプレイヤーはアクセスを拒否されます。以下のコードは**hereNow()**のif文の下にあります。

```js
// Below the if statement in hereNow()
else{
  // Game in progress
  Swal.fire({
    position: 'top',
    allowOutsideClick: false,
    title: 'Error',
    text: 'Game in progress. Try another room.',
    width: 275,
    padding: '0.7em',
    customClass: {
        heightAuto: false,
        title: 'title-class',
        popup: 'popup-class',
        confirmButton: 'button-class'
    }
  })
}
```

次に、**componentDidUpdate()**を見てみましょう。

### ゲームの開始

**componentDidUpdate()**では、プレイヤーがチャンネルに接続しているかどうか、つまり_this.lobbyChannelが_ **null**でないかどうかをチェックします。もし**null**でなければ、チャンネルに届くすべてのメッセージをリスニングするリスナーをセットアップします。

```js
componentDidUpdate() {
  // Check that the player is connected to a channel
  if(this.lobbyChannel != null){
    this.pubnub.getMessage(this.lobbyChannel, (msg) => {
      // Start the game once an opponent joins the channel
      if(msg.message.notRoomCreator){
        // Create a different channel for the game
        this.gameChannel = 'tictactoegame--' + this.roomId;

        this.pubnub.subscribe({
          channels: [this.gameChannel]
        });
      }
    });
  }
}
```

到着したメッセージが_msg.message.notRoomCreator_であるかどうかをチェックします。もしそうなら、_ルームIDを_文字列に追加した新しいチャンネル「_tictactoegame-」を_作成します。ゲームチャンネルは、プレーヤーが行ったすべての手を公開するために使用されます。

最後に、ゲームチャンネルを購読した後、_isPlayingの_状態をtrueに設定します。そうすることで、ロビーコンポーネントがゲームコンポーネントに置き換わります。

```js
 this.setState({
   isPlaying: true
 });

 // Close the modals if they are opened
 Swal.close();
}
```

ゲーム・コンポーネントが表示されたら、_Swal.close()_を実行して、ロビー・コンポーネントからすべてのモーダルを閉じます。

これで、2人のプレイヤーがユニークなゲームチャンネルに接続され、チックタックトーを始めることができます！次のセクションでは、ゲームコンポーネントのUIとロジックを実装します。

ゲーム機能のビルド
---------

**Game.jsで**最初にすることは、**ベースコンストラクタを**設定することです：

```js
// Game.js
import React from 'react';
import Board from './Board';
import Swal from "sweetalert2";

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(''), // 3x3 board
      xScore: 0,
      oScore: 0,
      whosTurn: this.props.myTurn // Player X goes first
    };

    this.turn = 'X';
    this.gameOver = false;
    this.counter = 0; // Game ends in a tie when counter is 9
  }

  render() {
    return ();
  }
 }
export default Game;
```

ステート・オブジェクトのために、配列の_squares_プロパティを初期化する。これについては後ほど説明する。また、プレイヤーのスコアを0に設定し、_whosTurnの_値を_myTurnに_設定する。これは、_プレイヤーXの_場合は**true**、_プレイヤーOの_場合は**falseに**初期化される。

変数_turnと_ _counterの_値は、ゲームの進行を通じて変化する。ゲームが終了すると、_gameOverが_ **trueに**設定される。

### UIの追加

次に、Gameコンポーネントのマークアップを**render**メソッドの中で設定する。

```js
render() {
  let status;
  // Change to current player's turn
  status = `${this.state.whosTurn ? "Your turn" : "Opponent's turn"}`;

  return (
    <div className="game">
      <div className="board">
        <Board
            squares={this.state.squares}
            onClick={index => this.onMakeMove(index)}
          />
          <p className="status-info">{status}</p>
      </div>

      <div className="scores-container">
        <div>
          <p>Player X: {this.state.xScore} </p>
        </div>

        <div>
          <p>Player O: {this.state.oScore} </p>
        </div>
      </div>
    </div>
  );
}
```

プレイヤーに自分の番なのか、相手の番なのかを知らせるために、_ステータスの_値をUIに表示します。_whosTurn_ステートのブール値は、移動が行われるたびに更新されます。残りのUIは、ボードコンポーネントとプレイヤーのスコアで構成されています。

### ロジックの追加

プレイヤーがボード上で手を打つとき、**onMakeMove(** **index****)の**呼び出しが行われます。ボードは3行3列なので、全部で9マスあります。各マスには固有の**インデックス**値があり、0から始まり8で終わります。

```js
onMakeMove = (index) =>{
  const squares = this.state.squares;

  // Check if the square is empty and if it's the player's turn to make a move
  if(!squares[index] && (this.turn === this.props.piece)){
    squares[index] = this.props.piece;

    this.setState({
      squares: squares,
      whosTurn: !this.state.whosTurn
    });

    // Other player's turn to make a move
    this.turn = (this.turn === 'X') ? 'O' : 'X';

    // Publish move to the channel
    this.props.pubnub.publish({
      message: {
        index: index,
        piece: this.props.piece,
        turn: this.turn
      },
      channel: this.props.gameChannel
    });

    // Check if there is a winner
    this.checkForWinner(squares)
  }
}
```

配列された_マスの_状態を取得した後、条件文を使って、プレイヤーがタッチしたマスが空かどうか、そして自分の手番かどうかをチェックする。どちらかの条件が満たされなければ、そのマスに駒は置かれない。そうでない場合、そのプレイヤーの駒は、その駒が置かれたインデックスの配列_マスに_追加されます。

例えば、_プレーヤーXが_0行2列目に着手し、条件文が真であれば、_squares\[2\]_ は "X "となります。[![Example with the squares array](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq4/a8487d17f078fae4ccfa874baac16013/squares-example.png "Example with the squares array")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq4/a8487d17f078fae4ccfa874baac16013/squares-example.png)次に、ゲームの新しい状態を反映するように状態が変更され、_手番が_更新され、他のプレーヤーが手を打つことができるようになります。相手の碁盤が現在のデータで更新されるように、ゲームチャンネルにデータを公開します。この処理はすべてリアルタイムで行われるため、有効な手が打たれるとすぐに両プレイヤーは自分の碁盤が更新されるのを見ることができる。このメソッドで最後に行うことは、**checkForWinner(squares)**を呼び出して勝者がいるかどうかをチェックすることです。

その前に、**componentDidMount****()** を見てみよう。ここでは、ゲームチャンネルに届いた新しいメッセージのリスナーをセットアップする。

```js
componentDidMount(){
  this.props.pubnub.getMessage(this.props.gameChannel, (msg) => {
    // Update other player's board
    if(msg.message.turn === this.props.piece){
      this.publishMove(msg.message.index, msg.message.piece);
    }
  });
}
```

両プレイヤーは同じゲームチャンネルに接続しているので、どちらもこのメッセージを受信します。メソッド**publishMove(index, piece)**が呼ばれます。**indexは**駒が置かれた位置、**pieceは**移動を行ったプレイヤーの駒です。このメソッドは、現在の手でボードを更新し、勝者がいるかどうかをチェックします。現在の手を打ったプレーヤーがこの処理をもう一度やり直す必要がないように、_if_文はプレーヤーの駒が_turnの_値と一致するかどうかをチェックする。もし一致すれば、そのプレーヤーのボードが更新される。

```js
// Opponent's move is published to the board
publishMove = (index, piece) => {
  const squares = this.state.squares;

  squares[index] = piece;
  this.turn = (squares[index] === 'X')? 'O' : 'X';

  this.setState({
    squares: squares,
    whosTurn: !this.state.whosTurn
  });

  this.checkForWinner(squares)
}
```

盤面を更新するロジックは**onMakeMove()**と同じです。それでは、**checkForWinner()**について説明します。

```js
checkForWinner = (squares) => {
  // Possible winning combinations
  const possibleCombinations = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];

  // Iterate every combination to see if there is a match
  for (let i = 0; i < possibleCombinations.length; i += 1) {
    const [a, b, c] = possibleCombinations[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      this.announceWinner(squares[a]);
      return;
    }
  }
}
```

すべての勝ちの組み合わせは二重配列の_possibleCombinationsに_あり、すべての配列がゲームに勝つ可能性のある組み合わせです。_possibleCombinationsの_すべての配列は、配列_squaresと_照合されます。もし一致すれば勝ちとなる。これをより明確にするために、例に従って説明しよう。

プレイヤーXが2行目0列目に勝ち筋を作ったとする。そのポジションの_インデックスは_6である：

[![Example of a winning move](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq5/77dcc8ab49b7e7b4e41ac6f1365a2df9/winning-move-example-1.png "Example of a winning move")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq5/77dcc8ab49b7e7b4e41ac6f1365a2df9/winning-move-example-1.png)_プレイヤーXの_勝ちの組み合わせは\[2,4,6\]である。配列の_正方形は_次のように更新される：配列は次のように更新される： \["o", "", "x", "o", "x", "", "x", "", ""\]。

_for_ ループの中で、_\[a,b,c\]_が\[2,4,6\]の値を持つとき、\[2,4,6\]は _すべて_ 同じ_Xの_値を持つので、_for_ループの中の_if_文は真になります。勝者のスコアを更新する必要があるので、勝者に賞を与えるために**announceWinner()**が呼ばれます。

ゲームが引き分けで終わった場合、そのラウンドの勝者はいない。引き分けかどうかをチェックするために、碁盤上で手が打たれるたびに1ずつ増加するカウンタを使用します。

```js
// Below the for loop in checkForWinner()
// Check if the game ends in a draw
this.counter++;
// The board is filled up and there is no winner
if(this.counter === 9){
  this.gameOver = true;
  this.newRound(null);
}
```

もしカウンタが9に達したら、そのプレイヤーは碁盤の最後のマスで勝利の手を打たなかったので、ゲームは引き分けに終わります。この場合、勝者はいないので、**newRound()**メソッドは**null**引数で呼ばれます。

このメソッドに行く前に、**announceWinner()**に戻りましょう。

```js
// Update score for the winner
announceWinner = (winner) => {
  let pieces = {
    'X': this.state.xScore,
    'O': this.state.oScore
  }

  if(winner === 'X'){
    pieces['X'] += 1;
    this.setState({
      xScore: pieces['X']
    });
  }
  else{
    pieces['O'] += 1;
    this.setState({
      oScore: pieces['O']
    });
  }
  // End the game once there is a winner
  this.gameOver = true;
  this.newRound(winner);
}
```

このメソッドのパラメータは_勝者_です。勝者が'X'か'O'かをチェックし、勝者の得点を1点増やします。ゲームが終了したので、変数_gameOverに_trueがセットされ、メソッド**newRound()**が呼び出されます。

### 新しいラウンドの開始

_プレイヤーXは_別のラウンドをプレイするか、ゲームを終了してロビーに戻るかを選択できます。

[![Endgame modal for Player X](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq6/f7b9623c4b0b628d51d8271f249747ba/playerx-endgame-modal.png "Endgame modal for Player X")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq6/f7b9623c4b0b628d51d8271f249747ba/playerx-endgame-modal.png)

もう一方のプレイヤーは、プレイヤー_Xが_どうするか決めるまで待つように言われています。

[![Endgame modal for Player O](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq7/d96413b49ec1b85838aca013cfba174b/playero-endgame-modal.png "Endgame modal for Player O")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq7/d96413b49ec1b85838aca013cfba174b/playero-endgame-modal.png)

プレイヤー_Xが_どうするか決めると、ゲームチャンネルにメッセージが公開され、相手プレイヤーに知らせる。その後、UIが更新される。

```js
newRound = (winner) => {
  // Announce the winner or announce a tie game
  let title = (winner === null) ? 'Tie game!' : `Player ${winner} won!`;
  // Show this to Player O
  if((this.props.isRoomCreator === false) && this.gameOver){
    Swal.fire({
      position: 'top',
      allowOutsideClick: false,
      title: title,
      text: 'Waiting for a new round...',
      confirmButtonColor: 'rgb(208,33,41)',
      width: 275,
      customClass: {
          heightAuto: false,
          title: 'title-class',
          popup: 'popup-class',
          confirmButton: 'button-class',
      } ,
    });
    this.turn = 'X'; // Set turn to X so Player O can't make a move
  }

  // Show this to Player X
  else if(this.props.isRoomCreator && this.gameOver){
    Swal.fire({
      position: 'top',
      allowOutsideClick: false,
      title: title,
      text: 'Continue Playing?',
      showCancelButton: true,
      confirmButtonColor: 'rgb(208,33,41)',
      cancelButtonColor: '#aaa',
      cancelButtonText: 'Nope',
      confirmButtonText: 'Yea!',
      width: 275,
      customClass: {
          heightAuto: false,
          title: 'title-class',
          popup: 'popup-class',
          confirmButton: 'button-class',
          cancelButton: 'button-class'
      } ,
    }).then((result) => {
      // Start a new round
      if (result.value) {
        this.props.pubnub.publish({
          message: {
            reset: true
          },
          channel: this.props.gameChannel
        });
      }

      else{
        // End the game
        this.props.pubnub.publish({
          message: {
            endGame: true
          },
          channel: this.props.gameChannel
        });
      }
    })
  }
 }
```

メッセージが_リセットさ_れた場合、プレイヤーのスコア以外のすべてのステート値と変数は初期値にリセットされます。まだ開いているモダルはすべて閉じられ、両プレイヤーにとって新しいラウンドが始まります。

_endGameという_メッセージでは、すべてのモダルが閉じられ、**endGame()**というメソッドが呼ばれます。このメソッドは**App.jsに**あります。

```js
// Reset everything
endGame = () => {
  this.setState({
    piece: '',
    isPlaying: false,
    isRoomCreator: false,
    isDisabled: false,
    myTurn: false,
  });

  this.lobbyChannel = null;
  this.gameChannel = null;
  this.roomId = null;

  this.pubnub.unsubscribe({
    channels : [this.lobbyChannel, this.gameChannel]
  });
}
```

ステートの値と変数はすべて初期値にリセットされます。チャンネル名はnullにリセットされます。プレイヤーが部屋を作成するたびに新しい名前が生成されるからです。チャンネル名はもう役に立たないので、プレイヤーはロビーとゲームチャンネルの両方から退会します。_isPlayingの_値はfalseにリセットされるので、ゲームコンポーネントはロビーコンポーネントに置き換えられます。

**App.jsに**含める最後のメソッドは**componentWillUnmount()**で、両方のチャンネルからプレイヤーの登録を解除します。

```js
componentWillUnmount() {
  this.pubnub.unsubscribe({
    channels : [this.lobbyChannel, this.gameChannel]
  });
}
```

ゲームが動作するために必要なことはこれだけです！ゲームのCSSファイルは[レポで](https://github.com/PubNubDevelopers/react-tictactoe/blob/master/src/Game.css)入手できます。では、ゲームを起動して動かしてみよう。

ゲームを動かす
-------

ゲームを実行する前に、いくつか小さなステップを踏む必要がある。まず、[Presence機能を](https://pubnub.com/docs/sdks/javascript/react)有効にする必要があります。[Presence](https://pubnub.com/docs/sdks/javascript/react)機能はチャンネルに参加している人数を取得するために使います（ロビーチャンネルを購読するときに_withPresenceを_使いました）。[PubNub管理者ダッシュボードに](https://admin.pubnub.com/#/login)行き、自分のアプリケーションをクリックします。**Keysetを**クリックし、**Application add-onsまで**スクロールダウンします。**Presence** スイッチを**オンに**切り替えます。デフォルト値はそのままにしてください。

[![Enable presence in PubNub Admin Dashboard](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq8/fb24ea6026b76234f79ee65d2426ac71/enable-presence-1.png "Enable presence in PubNub Admin Dashboard")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq8/fb24ea6026b76234f79ee65d2426ac71/enable-presence-1.png)

アプリで使用される3つの依存関係をインストールし、アプリを実行するには、アプリのルート・ディレクトリにある**dependencies.sh**スクリプトを実行します。

```js
# dependencies.sh
npm install --save pubnub pubnub-react
npm install --save shortid
npm install --save sweetalert2

npm start
```

ターミナルでアプリのルート・ディレクトリに移動し、以下のコマンドを入力してスクリプトを実行可能にしてください：

```js
chmod +x dependencies.sh
```

このコマンドでスクリプトを実行する：

```js
./dependencies.sh
```

![Run the React app locally](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq9/fb4f1a520b9d68e1c283a61d5fa04653/run-the-app.png "Run the React app locally")別のタブ、またはできればウィンドウを開き、http://localhost:3000 をコピー＆ペーストしてください。1つのウィンドウで'Create'ボタンをクリックしてチャンネルを作成します。_ルームIDを_表示するモーダルがポップアップします。そのIDをコピー＆ペーストしてください。もう一つのウィンドウで「Join」ボタンをクリックします。モーダルがポップアップしたら、入力フィールドに_ルームIDを_ 入力し、'OK'ボタンを押してください。

[![Create and join the channel](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqa/01343a31a36c16277b86d8a1ed92570b/create-join-lobby.png "Create and join the channel")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqa/01343a31a36c16277b86d8a1ed92570b/create-join-lobby.png)

プレイヤーが接続されると、ゲームが始まります。チャンネル作成に使用したウインドウが先手を取ります.盤上の任意のマスを押すと、両方のウィンドウの駒**Xが** リアルタイムで盤上に表示されます。同じ盤面の別のマスを押しても、もうあなたの手番ではないので何も起こりません。もう一方のウインドウでは、盤面のどのマスを押しても、駒**Oが**そのマスに置か **_れます_** 。

[![Place the piece on the board](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqb/454174588fc9acf9c2f086c3e9c53a19/place-piece-on-board.png "Place the piece on the board")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqb/454174588fc9acf9c2f086c3e9c53a19/place-piece-on-board.png)

勝敗が決するか引き分けになるまで続けます。モーダルが表示され、そのラウンドの勝者、または引き分けに終わったことが告げられます。同じモーダルで、_プレイヤーXは_ ゲームを続けるか終了するかを決めなければならない。_プレイヤー O_ のモーダルは、新しいラウンドを待つように指示する。

[![End of game modals](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqc/71b832403fecb058be460282895e3f10/end-of-game.png "End of game modals")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqc/71b832403fecb058be460282895e3f10/end-of-game.png)

プレイヤー_Xが_ ゲームを続行した場合、スコア以外のすべてがリセットされる。そうでない場合、両プレイヤーはロビーに戻り、そこで新しいチャンネルを作成したり、参加したりすることができます。ゲームデモは以下からご覧いただけます：

**この記事の内容に関して提案や質問がありますか？[devrel@pubnub.com](mailto:devrel@pubnub.com) までご連絡ください。**

#### コンテンツ

[リアルタイム・マルチプレイ・ゲームのコンセプトアプリの](#h-0)[概要](#h-1)[ロビーの](#h-2)[セットアップ](#h-1)[チャンネルの](#h-3)作成[チャンネルへの](#h-4)参加[ゲームの](#h-5)[開始](#h-4)[ゲーム機能の](#h-6)[構築](#h-5)[UIAの](#h-7)追加[ロジックの](#h-8)追加[新しいラウンドの](#h-9)[開始](#h-8)[ゲームの](#h-10)実行

PubNubはどのようにお役に立ちますか？
=====================

この記事は[PubNub.comに](https://www.pubnub.com/blog/browser-based-multiplayer-tic-tac-toe-game-in-react/)掲載されたものです。

PubNubのプラットフォームは、開発者がウェブアプリ、モバイルアプリ、IoTデバイス向けにリアルタイムのインタラクティブ機能を構築、提供、管理できるよう支援します。

私たちのプラットフォームの基盤は、業界最大かつ最もスケーラブルなリアルタイムエッジメッセージングネットワークです。世界15か所以上で8億人の月間アクティブユーザーをサポートし、99.999%の信頼性を誇るため、停電や同時実行数の制限、トラフィックの急増による遅延の問題を心配する必要はありません。

PubNubを体験
---------

[ライブツアーを](https://www.pubnub.com/tour/introduction/)チェックして、5分以内にすべてのPubNub搭載アプリの背後にある本質的な概念を理解する

セットアップ
------

[PubNubアカウントに](https://admin.pubnub.com/signup/)サインアップすると、PubNubキーに無料ですぐにアクセスできます。

始める
---

[PubNubのドキュメントは](https://www.pubnub.com/docs)、ユースケースや[SDKに](https://www.pubnub.com/docs)関係なく、あなたを立ち上げ、実行することができます。