Boardgame.ioでマルチプレイオンラインボードゲームを作る①

こちらに投稿した記事の転記となります。

https://sirkus.co.jp/2023/06/16/%e3%82%aa%e3%83%b3%e3%83%a9%e3%82%a4%e3%83%b3%e3%83%9c%e3%83%bc%e3%83%89%e3%82%b2%e3%83%bc%e3%83%a0%e3%82%92boardgame-io%e3%81%a7%e4%bd%9c%e3%82%8b/

過去の記事
- [Boardgame.ioでマルチプレイオンラインボードゲームを作る①](https://sirkus.co.jp/2023/06/16/%e3%82%aa%e3%83%b3%e3%83%a9%e3%82%a4%e3%83%b3%e3%83%9c%e3%83%bc%e3%83%89%e3%82%b2%e3%83%bc%e3%83%a0%e3%82%92boardgame-io%e3%81%a7%e4%bd%9c%e3%82%8b/) boardgame.ioの立ち上げ、基本、マルチプレイサーバの作成(本記事)
- [Boardgame.ioでマルチプレイオンラインボードゲームを作る②](https://sirkus.co.jp/2023/06/26/boardgame-io%e3%81%a7%e3%83%9e%e3%83%ab%e3%83%81%e3%83%97%e3%83%ac%e3%82%a4%e3%82%aa%e3%83%b3%e3%83%a9%e3%82%a4%e3%83%b3%e3%83%9c%e3%83%bc%e3%83%89%e3%82%b2%e3%83%bc%e3%83%a0%e3%82%92%e4%bd%9c/) ターン、フェーズ、ステージの制御

boardgame.ioとはnode.jsベースのWebアプリフレームワークで
ステート管理、ゲームフェーズ機能やマルチプレイヤー、ロビー機能などを持っているボードゲーム向けのオープンソースとなります。
https://github.com/boardgameio/boardgame.io
以前はgithubのオーナーはgoogleだったのですが、今はboardgameioとなっています。

## チュートリアル

### セットアップ

https://boardgame.io/

今回はReactを使用した手順となっています。
* 前提条件：node.js/npm/npxのインストール

* プロジェクトの作成 & boardgame.ioのインストール
```
npx create-react-app bgio-tutorial
cd bgio-tutorial
npm install boardgame.io
```
### 簡単なゲームを作ってみる(3目並べ)

#### `G`の定義
`G`とはゲームの状態を定義するクラスとなり、src/Game.jsに定義していく形になります。
ここでは公式チュートリアルの3目並べを見ていきます。
```javascript:Game.js
export const TicTacToe = {
  // ゲームの初期設定。ここでは、9つのセルをnullで初期化した配列を作成しています。
  setup: () => ({ cells: Array(9).fill(null) }),

  moves: {
    // クリックしたセルに対する動作を定義。
    // ここでは、選択したセルにプレイヤーIDを設定しています。
    clickCell: ({ G, playerID }, id) => {
      G.cells[id] = playerID;
    },
  },
};
```
#### Clientの定義
```javascript:App.js
import { Client } from 'boardgame.io/react';
import { TicTacToe } from './Game';

const App = Client({ game: TicTacToe });

export default App;
```
まだ、ゲームの定義とクライアントの紐づけを行っただけで、UIは全くありませんがこの状態で`npm start`を実行すると、デバッグウィンドウがある状態のゲーム画面を立ち上げることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666388/07d3d139-5d14-ad8b-e0b9-3f01be1affa4.png)
デバッグウィンドウからGの状態を色々変えられたりします。
`clickcell(3)`などとしてEnterを押すとマス3に打ったこととなり`G`を確認するとプレイヤーIDが指定のセルに打ったことになっているかと思います、その後`endturn`を選択しEnterを押すとターンを渡すことができます。
このデバッグウィンドウはClientに`debug:false`を追加することで消せます。

#### ルール上打てない手を制御する
今の状態
```diff_javascript:Game.js
+ import { INVALID_MOVE } from 'boardgame.io/core';


export const TicTacToe = {
  // ゲームの初期設定。ここでは、9つのセルをnullで初期化した配列を作成しています。
  setup: () => ({ cells: Array(9).fill(null) }),

  moves: {
    // クリックしたセルに対する動作を定義。
    // ここでは、選択したセルにプレイヤーIDを設定しています。
    clickCell: ({ G, playerID }, id) => {
+      // 指定セルにすでに何か入っている場合は操作不可
+      if (G.cells[id] !== null) {
+        return INVALID_MOVE;
+      }
      G.cells[id] = playerID;
    }

  },
};
``` 

#### ターン終了の定義
先ほどはendturnを手動で支持しましたが、実際は一手打ったら自動でターンを渡したいです。
`G`にturnを定義することで、ターン終了の定義を行うことができます。
今回は1手打ったらターンを渡したいのでmin/maxに1と入力します。
```diff_javascript:Game.js
export const TicTacToe = {
  setup: () => { /* ... */ },

+  turn: {
+    minMoves: 1,
+    maxMoves: 1,
+  },

  moves: { /* ... */ },
}
```

#### ゲーム終了条件(勝利/引き分け)
ゲーム終了条件にはendifステートメントを追加し、
return { winner: プレイヤー番号 };
return { draw: true };
等を返却することで、ゲーム終了条件を定義できます。
```diff_javascript:Game.js
export const TicTacToe = {
  // 他ステートメント省略

+  endIf: ({ G, ctx }) => {
+    if (IsVictory(G.cells)) { // 勝利条件に合致したら
+      return { winner: ctx.currentPlayer }; // プレイヤー番号が勝利
+    }
+    if (IsDraw(G.cells)) {// 引き分け条件に合致したら
+      return { draw: true }; // 引き分け
+    }
  },
};

function IsVictory(cells) {
    // 一直線に並んだら
    const positions = [
        [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6],
        [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]
    ];

    const isRowComplete = row => {
        const symbols = row.map(i => cells[i]);
        return symbols.every(i => i !== null && i === symbols[0]);
    };

    return positions.map(isRowComplete).some(i => i === true);
}

function IsDraw(cells) {
    // 全部のセルが埋まったら
    return cells.filter(c => c === null).length === 0;
}
```

#### UIの定義
UIそのものは単なるReactのコードですのでここでは省略します。(コードは[こちら](https://boardgame.io/documentation/#/tutorial?id=building-a-board))
```javascript:Board.js
import React from 'react';

export function TicTacToeBoard({ ctx, G, moves }) {
  // onClickにGへのアクションを定義する
  const onClick = (id) => moves.clickCell(id);

  let winner = '';
  // ctxからゲーム終了条件が入っているかどうか確認
  if (ctx.gameover) {
    winner =
      ctx.gameover.winner !== undefined ? (
        <div id="winner">Winner: {ctx.gameover.winner}</div>
      ) : (
        <div id="winner">Draw!</div>
      );
  }

//...

  let tbody = [];
  for (let i = 0; i < 3; i++) {
    let cells = [];
    for (let j = 0; j < 3; j++) {
      const id = 3 * i + j;
      cells.push(
        <td key={id}>
          {G.cells[id] ? ( //Gからセル情報を取り出し
            <div style={cellStyle}>{G.cells[id]}</div>
          ) : (
            <button style={cellStyle} onClick={() => onClick(id)} /> //先ほどアクションを定義したonClickボタンを定義
          )}
        </td>
      );
    }
    tbody.push(<tr key={i}>{cells}</tr>);
  }
```
最後にApp.jsに`G`と盤面UIクラスをそれぞれ定義してゲーム完成です
```javascript:App.js
import { Client } from 'boardgame.io/react';
import { TicTacToe } from './Game';
import { TicTacToeBoard } from './Board';

const App = Client({
  game: TicTacToe,
  board: TicTacToeBoard,
});

export default App;
```

#### Bot
aiステートメントでBotを定義することができます。
Botに対してはその時打てる手の全パターンを教えてあげる必要があります。
```javascript:Game.js
export const TicTacToe = {
//他ステートメント省略

  ai: {
    enumerate: (G, ctx) => {
      let moves = [];
      for (let i = 0; i < 9; i++) {
        if (G.cells[i] === null) {
          moves.push({ move: 'clickCell', args: [i] });
        }
      }
      return moves;
    },
  },
};
```
この状態でデバッグウィンドウに「ai」というタブができているはずなので、そこで`play`ボタンを押すとAIが手を打ってくれます。
上記では単に空いているセルをmovesに格納して返却しているだけなのですが、可能な手の中からランダムに打つわけではなく最善手を打ってくれるように見えます。どうやらboardgame.io側のほうで勝利条件に従った最善手を行う機能が入っているようです。

#### マルチプレイヤー
マルチプレイヤーに入っていきます。
マルチプレイヤーにはClientにmultiplayerを追加しつつ、プレイヤーごとにIDをつけておきます。
プレイヤーごとにタグが分かれているのはプレイヤーごとに表示させるUIを意味しています。今は両方表示されてしまっている状態なので実際は動的に出し分けしてください。
```diff_javascript:App.js
import React from 'react';
import { Client } from 'boardgame.io/react';
+ import { SocketIO } from 'boardgame.io/multiplayer'
import { TicTacToe } from './Game';
import { TicTacToeBoard } from './Board';

const TicTacToeClient = Client({
  game: TicTacToe,
  board: TicTacToeBoard,
+  multiplayer: SocketIO({ server: 'localhost:8000' }),
});

const App = () => (
  <div>
+    <TicTacToeClient playerID="0" />
+    <TicTacToeClient playerID="1" />
  </div>
);

export default App;
```
multiplayerに渡すserverを定義します。
boardgame.io側で機能が用意されていますのでそれをそのまま使います。
```javascript:server.js
const { Server, Origins } = require('boardgame.io/server');
const { TicTacToe } = require('./Game');

const server = Server({
  games: [TicTacToe],
  origins: [Origins.LOCALHOST],
});

server.run(8000);
```

サーバを動かすために設定をします
`npm install esm`
を行った後package.jsonに以下を追加してください
```diff_json:package.json
{
  "scripts": {
...
+    "serve": "node -r esm src/server.js"
  }
}
```
これで`npm run serve`で制御用のサーバーを立ち上げたのち、`npm start`でアプリケーションサーバを立ち上げることでマルチプレイで行えます。
(今はお試しで1画面に両プレイヤーを出すようにしているので上部の画面がPlayer1、下部の画面がPlayer0になっています。)