🎯

汎用ボードゲームエンジン「Dagaz」を作っている話 / (2) なぜ競合プロジェクトでは満足できなかったのか

2022/12/06に公開

はじめに

この記事は、「ボードゲーム・パズルプログラミング Advent Calendar 2022」第5日目の記事です。
UTCではまだ12月5日なので完全にセーフですね。
https://adventar.org/calendars/7522

同AdCの1日目の記事で、「Dagaz」というボードゲームの静的Webアプリ化を支援するフレームワークを作っている話を書きました。
https://zenn.dev/stepney141/articles/fb012e1120dc17
この記事では「Zillions of Games」という先行するソフトの重要性と問題点を紹介し、その問題点への解決策を提示するものとしてDagazを位置づけました。

ですが、同記事で紹介しなかったDagazの類似プロジェクトもいくつか存在します。
その中で最も代表的なのが「boardgame.io」というTypeScript製のゲーム実装フレームワークです。

Dagazのゲームエンジン部分をアップデートすることが決まった際、「Dagazの内部でboardgame.ioを直接使うことは可能か」を検討しました。
ですが結局、「boardgame.ioはDagazを作るのには向かないが、boardgame.ioの設計や実装の仕方は大いに参考になる」という結論に至りました。

この記事では、boardgame.ioの特徴や独自性を紹介すると同時に、なぜboardgame.ioがDagazに向かないと判断したかを解説します。
また、ソフトウェア開発全般において「既にある実装では本当にダメなのか」を考えることはとても大事だと思いますが、その意思決定の一例としても参考になれば幸いです。

ゲームを計算するとは何か

「ゲームを実装するフレームワーク」を考える前に、そもそも「ボードゲームをコンピュータに実装するとはどういうことか」について考えてみます。これによって「フレームワークが何を抽象化してどんな機能を提供すべきか」ということが明確になります。
コンピュータでゲームを『計算』するには、まずゲームという対象を何らかのデータ構造として表現し、次にその対象の変化を表現するための何らかのアルゴリズムが必要となります。

データ構造として決めるのは、「局面(position)」と「着手(move)」です。「局面」とは、「特定時刻におけるゲームの状態を過不足なく表現する情報の組」のことです。「着手」は、「特定時刻における局面と次時刻における局面の間の差分」と見なすことができます。
どんな情報を「局面」として管理すべきかはゲームによって様々です。盤面に駒や石がどう配置されているか・現在の手番などはどのゲームにも共通する基本的な局面情報ですが、将棋なら持ち駒の情報、囲碁ならアゲハマの情報など、ゲーム固有の局面情報も多数あります。
また、千日手やコウの判定のためにはゲーム開始からの局面の履歴も必要になることから分かるように、特定時刻の局面にまつわる情報だけでゲームの全てを説明できるとは限りません。

必要なアルゴリズムは、ざっくり言って以下のような手続きです。これらの手続きを組み合わせると「現在局面が必勝局面であることの判定」なども作れます。

  • 可能な着手の列挙:(現在局面) => (可能な着手の集合)
  • 着手が可能かの判定:(現在局面, 着手) => (合法 or 非合法)
  • 勝ち負けの判定:(現在局面) => (勝ち or 負け or 未決着)
  • 次局面の生成:(現在局面, 着手) => (次局面)
  • 前局面の生成:(現在局面, 着手) => (前局面)

これらのデータ構造とアルゴリズムを組み合わせれば、コンピュータ上にバーチャルなゲームを再現することも出来ますし、最も原始的なCPUプレイヤー(合法手の中から一様ランダムに選んで指すだけ)を作ることも容易です。

boardgame.ioの概要

boardgame.ioは、"an engine for creating turn-based games using JavaScript (手番制ゲームをJavaScriptで作るためのエンジン)"です。
https://boardgame.io/

boardgame.ioで最も大きな位置を占めているのは、「ゲームの進行状況をフロントエンドの状態管理の枠組みに当てはめ、内部的に immer.js と Redux を利用していい感じに面倒を見てくれる」という局面の状態管理の機能です。
優秀な点として、「手番」という単位で離散的に局面の状態が遷移するゲームならば、将棋だろうがポーカーだろうがパズルだろうが何でも扱えることがまず挙げられます(リアルタイムアクションゲームとかを実装するのは辛いです)。
また、不完全情報ゲーム(プレイヤーが知ることの出来る情報が限られているゲーム。カードゲーム類が代表例)やランダム要素のあるゲームも完璧にサポートされています。
他にも複数人でのオンライン同時対戦を簡単に行える仕組みを提供していたり、React / React Nativeに対応していたり、簡単なCPU対戦の思考ルーチンを組める仕組みがあったりと、かなり至れり尽くせりです。

その他にも色々とかゆいところに手の届く機能が充実していますが、詳しい機能紹介は公式ドキュメントを参照してください。
https://boardgame.io/documentation/#/

公式の説明では「ゲーム制作エンジン」として紹介されているboardgame.ioですが、実情としては「テーブルゲームのWebアプリ開発のためのライブラリ」という感じです。
現代的なフロントエンド開発に慣れている人であれば、普段通りのフロントエンド開発環境にboardgame.ioを追加で導入すれば、割とスムーズにテーブルゲームのWebアプリを実装できます。

boardgame.ioの特徴

局面の捉え方

boardgame.ioではゲームにまつわる状態を「G」と「ctx」に分け、フレームワークの責務を明確にしています。

{
  // 各手番におけるゲームの局面の状態(プログラマが管理する)
  G: {},

  // 各手番におけるゲームのメタ情報(フレームワークが管理する)
  ctx: {
    turn: 0,
    currentPlayer: '0', //現在の手番のプレイヤー
    numPlayers: 2, //プレイヤー数
    /* 後略 */
  }
}
  • G :各手番におけるゲームの局面の状態であり、プログラマ側が好きな形で与えて自分で管理するものです。
    • ただし JSON としてパースできる形式でなくてはならないという制限があります
    • 内部での型は any です
  • ctx :各手番におけるゲームのメタ情報であり、フレームワークが自動で管理してくれます。
    • こちらは G と対照的に、内部ではきっちりと型が決まっています

盤面状態をこのふたつに分けることによって、ユーザーが考えるべきことを削減していると同時に、フレームワークの管理する領分とユーザーの管理する領分の間に自然な境界を作っています。大変巧みな発想です。

指し手の捉え方

boardgame.ioでは、着手は (G, ctx) => G...つまり「現在局面から次局面への純粋関数」として定義されます。
内部的に immer.js を使っているため、新しい G の値を直接返さずとも G を直接書き換える形で着手を記述することも可能です。

この捉え方は近年のフロントエンド開発界隈における副作用排除の傾向にもうまく合致しており、フロントエンドのコードの中でboardgame.ioを使いやすくすることにも貢献しています。

手番の捉え方

boardgame.ioでは状態遷移のスパンとして、最もベーシックな「手番(turn)」の他、「phase」と「stage」という単位が用意されています。

  • turn:各プレイヤーは自分のturnで着手を行う
    • turnの順番がゲームの途中で動的に変わるようにも指定できる
  • phase:turnよりも大きな単位であり、ひとつのphaseの中に複数のturnを含めることができる
    • 例) 人狼:全員が自分のturnで発言する『昼』と、人狼だけが自分のturnで会話する『夜』という2つのフェーズを繰り返す
    • 例) カードゲーム類:「自分のturnで『山からカードを取る』という着手を行う」フェーズと「自分のturnで『手札からカードを1枚切る』という着手を行う」フェーズを繰り返す
  • stage:turnよりも小さな単位であり、ひとつのturnの中に複数のstageを含めることができる
    • ひとつのturnを複数のstageに分け、段階を踏んでひとつのturnを完遂させる
    • 例) あるプレイヤーの行動が、他の複数のプレイヤーに特定の行動を強制させるような場面:stageを使うと簡単に実装できる

boardgame.ioとDagazの違い

確かに「ボードゲームをWebアプリとして実装するためのフレームワーク」としては、boardgame.ioは非常に練られた設計をしています。
しかし、DagazやZillions of GamesのようなGGPソフトウェアとは、根本的な設計思想も含めた大きな差異がいくつもあり、Dagazとは目指しているものが大きく違っています。

boardgame.ioがやっているのは、「ターン制」という概念に焦点を当てたテーブルゲームの抽象化です。「ターン」「フェーズ」「ステージ」という『ゲームの構造』に則ってゲームの状態遷移を記述する枠組みを提供し、ゲームの詳細なルールの記述はプログラマに任せています。
ターン制という『ゲームの構造』に乗っかることによって、プログラマによるテーブルゲームの実装作業を支援しよう...というのがboardgame.ioの基本思想です。
boardgame.ioはあくまでも『フレームワーク』であり、プログラマの開発を支援するためのものなのです。

もっと言うと、boardgame.ioが提供しているのは、ゲームの全体的な流れを「ターン制の状態遷移という普遍的な骨組みの上に各ゲームのルール(ドメイン知識)を肉付けして得られるシステム」として記述する仕組みです。どのように肉付けするか...つまりルールの記述や合法性検証は完全にプログラマの自由に任されています。
フレームワーク自体はあくまでも「状態遷移」という骨組みに主眼を置いていることから、ゲームの表現力が「肉」の部分によって左右されることがないというメリットがあります。つまり、「石」「駒」「カード」など用いる道具に違いがあっても同じように実装でき、人狼のようなそもそも道具を使わないゲームにも対応できます。

ですが、Dagazにおいては「骨組みだけしか提供しない」のでは困るのです。
「ゲームを計算するとは何か」節で先述したように、ゲームをコンピュータ上に実装するためには「局面と着手」および「それを操作する手続き」の定義が必要です。boardgame.ioでは着手と操作手続きを記述する仕組みは用意されていますが、最も肝心な「局面(=G)」の記述についてはなんら支援をしてくれません。
この性質は、「様々なボードゲームを統一的に実装する」「ソフトウェア開発の専門家でなくても簡便に使えるようにする」というDagazの方針とは相性が悪いです。

「それなら、Dagazのルール形式をboardgame.ioに変換する仕組みを用意すれば良いのでは?」と思われるかもしれませんが、それはあまり現実的ではありません。
DagazがベースとしているZillions of Gamesなど、多くのGeneral Game Playingエンジンは、ゲームの全体的な流れを「各ゲームのルール(ドメイン知識)という流動的な要素の連鎖」として記述する思想を持ち、「ルールを記述する仕組み」をこそユーザーに提供します。
Zillions of Gamesは「ひとつひとつの駒の挙動」に注目してルールを記述させ、「ゲーム全体の構造」に焦点を当てていません。
boardgame.ioが注目していたターン制という「構造」は、Zillions of Gamesにおいては各ゲームの「石」「駒」「カード」といった要素の織り成すダイナミクスの中に捨象されます。
「Dagazのルールの要素を全部分解してboardgame.ioの提供するコンポーネントに沿うように再構成する」というのは、そもそも両者の抽象化のアプローチが異なっていることから、非常に厄介な仕事なのです。

boardgame.ioからDagazに活かせるもの

  • 構造的かつ宣言的なゲームのルール記述の仕方
  • オンライン対戦サーバ/クライアントの実装と抽象化
  • eventシステム
  • 動的な手番の変化
  • 不完全情報ゲームの実装方法
    • 「UI側で情報を覆い隠す」のではなく「フレームワークからプレイヤー側に送る内部データそのものを制限する」というアプローチは正直目からウロコだった
  • 非確定ゲームやランダム要素の実装方法
  • TypeScriptプロジェクトとしてのドキュメント整備、ディレクトリ構成、周辺スクリプト/ツール類の整備方法

Discussion