6
4

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 1 year has passed since last update.

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

Last updated at Posted at 2023-06-16

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

過去の記事

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

チュートリアル

セットアップ

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

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

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

Clientの定義

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

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

今の状態

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

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

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

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

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

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

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のコードですのでここでは省略します。(コードはこちら)

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クラスをそれぞれ定義してゲーム完成です

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

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

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

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

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

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

6
4
0

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
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?