関連記事

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目並べを見ていきます。

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

  moves: {
    // クリックしたセルに対する動作を定義。
    // ここでは、選択したセルにプレイヤーIDを設定しています。
    clickCell: ({ G, playerID }, id) => {
      G.cells[id] = playerID;
    },
  },
};

Clientの定義

import { Client } from 'boardgame.io/react';
import { TicTacToe } from './Game';

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

export default App;

まだ、ゲームの定義とクライアントの紐づけを行っただけで、UIは全くありませんがこの状態でnpm startを実行すると、デバッグウィンドウがある状態のゲーム画面を立ち上げることができます。


デバッグウィンドウからGの状態を色々変えられたりします。
clickcell(3)などとしてEnterを押すとマス3に打ったこととなりGを確認するとプレイヤーIDが指定のセルに打ったことになっているかと思います、その後endturnを選択しEnterを押すとターンを渡すことができます。
このデバッグウィンドウはClientにdebug:falseを追加することで消せます。

ルール上打てない手を制御する

今の状態ですとすでに打ったマスにも再度打ててしまうことになります。実行不可能な手を定義します。

+ 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と入力します。

export const TicTacToe = {
  setup: () => { /* ... */ },

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

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

ゲーム終了条件(勝利/引き分け)

ゲーム終了条件にはendifステートメントを追加し、
return { winner: プレイヤー番号 };
return { draw: true };
等を返却することで、ゲーム終了条件を定義できます。

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のコードですのでここでは省略します。(コードはこちら)
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クラスをそれぞれ定義してゲーム完成です

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に対してはその時打てる手の全パターンを教えてあげる必要があります。

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を意味しています。今は両方表示されてしまっている状態なので実際は動的に出し分けしてください。

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側で機能が用意されていますのでそれをそのまま使います。

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に以下を追加してください

{
  "scripts": {
...
+    "serve": "node -r esm src/server.js"
  }
}

これでnpm run serveで制御用のサーバーを立ち上げたのち、npm startでアプリケーションサーバを立ち上げることでマルチプレイで行えます。
(今はお試しで1画面に両プレイヤーを出すようにしているので上部の画面がPlayer1、下部の画面がPlayer0になっています。)