Thousand Parsec

百の世界を支配し、千パーセクにも及ぶ広大な銀河帝国。銀河の他の部分とは違い、そこにいるのは柄の悪い輩ではない。文化的で学術的な伝統を持った知的な人々がそこには暮らしている。偉大な科学技術大学が次々に作られる彼らの輝かしい惑星は、現在の平和と繁栄の象徴である。宇宙域の遥か彼方から宇宙船がやってきては、一流の研究者を遠くから連れてくる。彼らは、衆生が試みた中で最も高い目標を持つプロジェクトに自身の技能を捧げに来たのである。そのプロジェクトとは、全ての言語、文化、法体系をまたいだ銀河全体を結ぶ分散コンピューターネットワークの開発である...。

Thousand Parsec は単なるビデオゲームではありません。Thousand Parsec はフレームワークであり、マルチプレイヤーのターンベース宇宙帝国戦略ゲームを作るための完全なツールキットが付属します。ゲームプロトコルは一般的であり、様々な種類のゲームはもちろん、クライアント、サーバー、AI ソフトウェアさえ作ることができます。そのサイズゆえ計画と実行が難しくなってはしまうものの、Thousand Parsec ではコントリビューターが垂直開発と水平開発の薄い垣根を超えることになります。そのためこのソフトウェアはオープンソースプロジェクトのアーキテクチャを議論する上で興味深い対象と言えるでしょう。

Thousand Parsec が属するゲームジャンルを、ゲーム評論家は “4X” と呼びます ――“explore (探検), expand (拡張), exploit (開発), and exterminate (根絶)” という、帝国を指揮するプレイヤーの常套手段の略です1。4X のゲームでは、プレイヤーは斥候してマップを埋めていき (探索)、帝国の領土と影響力を広め (拡張)、支配するエリア内の資源を収集・利用し (開発)、ライバルプレイヤーを攻撃・征服します (根絶) 。経済、技術開発、マイクロマネジメント、およびその他の事項の何を優先するかによって、ストラテジージャンルの中でも特別な深さと複雑さを持ったゲームプレイが楽しめます。

プレイヤーの視点で見ると、Thousand Parsec のゲームには三つの主要なコンポーネントが登場します。一つ目はクライアントであり、これはプレイヤーが宇宙と対話を行うアプリケーションです。クライアントがネットワーク越しに繋がるのがサーバーであり、他のプレイヤー (または AI) のクライアントも同じサーバーに接続します。この接続ではプロトコルが非常に重要になります。サーバーはゲーム全体の状態を保持し、各ターンの開始時にクライアントを更新します。プレイヤーは更新された状態に基づいて様々な行動を行い、それをサーバーに送り返し、サーバーが次のターンの状態を計算します。プレイヤーが行える行動はルールセットによって制限されます。このルールセットがプレイするゲームを定義し、サーバーサイドで実装・実行され、任意のクライアントがプレイヤーに実際のゲームとして示します。

可能なゲームは多様であり、その多様性をサポートするには複雑なアーキテクチャが必要なので、Thousand Parsec はゲーマーと開発者の両方にとってワクワクするプロジェクトとなります。もしあなたがゲームのフレームワークの解析に少しも興味がない真面目なコーダーであったとしても、Thousand Parsec を支えるクライアント-サーバー通信、動的な設定、メタデータの処理、そしてレイヤー化された実装に興味を持ってもらえると思います。こういった要素は全て、典型的なオープンソースのスタイルで優れた設計へと有機的に成長してきました。

Thousand Parsec のコアは、ゲームプロトコルとそれに関連する機能の標準仕様の集合です。本章では基本的にこの視点から見たフレームワークについて議論しますが、実際の実装を見た方がずっと理解しやすいこともあります。そのため、私たちは主要コンポーネントのそれぞれについて「フラッグシップ」の実装を選んで具体的な議論を行うことにしました。

クライアントのモデルケースは tpclient-pywx です。これは比較的成熟した wxPython ベースのクライアントであり、最新バージョンのゲームプロトコルのサポートが一番充実しています。このクライアントを助けるのが、キャッシュなどの機能を提供するヘルパーライブラリ libtpclient-py と、最新バージョンの Thousand Parsec プロトコルを実装する Python ライブラリ libtpproto-py です。サーバーは tpserver-cpp を取り上げます。これは最新バージョンの機能とプロトコルを実装する C++ サーバーです。tpserver-cpp は様々なルールセットをサポートしますが、その中でも Missile and Torpedo Wars というマイルストーンルールセットは、様々な機能を使った「伝統的な」宇宙 4X ゲームの例として適しています。

Star Empire の解説

Thousand Parsec の世界を支えるパーツを正確に紹介するには、最初にゲームについての大まかな説明が必要でしょう。ここでは Missile and Torpedo Wars というルールセットを解説します。これは Thousand Parsec による二番目のマイルストーンルールセットであり、Thousand Parsec プロトコルの最新メインラインバージョンが持つ主要な機能のほとんどを利用しています。この節の残りでは、Thousand Parsec で使われる馴染みの薄い用語の意味を説明します。

Missile and Torpedo Wars は Thousand Parsec で利用可能なメソッドを全て実装しており、その意味で最もきめの細かいルールセットです。そうしているのは執筆時点でこのルールセットだけであり、楽しさの詰まった完全なゲームへとすぐに拡張できます。

Thousand Parsec サーバーへの接続を確立するときに、クライアントはゲームエンティティのリストをサーバーに問い合わせ、全体のカタログをダウンロードします。このカタログにはオブジェクト、ボード、メッセージ、カテゴリ、デザイン、コンポーネント、プロパティ、プレイヤー、資源といったゲームの状態を定義する全てのものが含まれます (全てこの節で説明します)。ゲームの開始時 (そして各ターンの終了時) にクライアントがダウンロードする要約データとしてはずいぶん多いと思うかもしれませんが、こういった情報はゲームにとって絶対不可欠です。このデータのダウンロードは数秒で終了し、クライアントはゲーム世界のあらゆる情報をプロットするための準備が整います。

クライアントがサーバーに初めて接続すると、ランダムな惑星が生成されて新しいプレイヤーの「母星」として割り当てられ、そこに二つの艦隊が自動的に生成されます。艦隊は Scout デザインであり、Alpha Missile Tube を装備した Scout Hull を持っています。最初は Explosive コンポーネントが存在しないので、このデフォルトの艦隊は戦艦対戦艦あるいは線艦対惑星の戦闘を行うことができません。言ってしまえば、敵にとってこの艦隊は格好の標的です。

ゲーム開始時には、艦隊に武器を装備させることがプレイヤーにとって重要になります。これは Build Weapon 命令を使って武器デザインを作成し、出来上がった武器を Load Armament 命令を使って艦隊に搭載することで行えます。Build Weapon 命令は惑星の資源を武器に変換します (資源は各惑星にランダムな量と割合で割り当てられます)。完成した爆発弾頭は惑星の地表に置かれるので、Load Armanent 命令を使ってこれを艦隊に移送します。

惑星表面で簡単に入手できる資源を使い切った後は、マイニングを通して資源を入手することが重要になります。資源は minable と inaccessible という二つの状態のいずれかにあり、惑星で Mine 命令を使うと minable な資源を少しずつ表面資源に変換できます。表面資源は建設に利用可能です。

オブジェクト

Thousand Parsec の世界において物理的なモノは全てオブジェクトであり、実は世界自体もオブジェクトです。この設計によってゲームに登場できる要素を事実上無限となり、さらにオブジェクトを少ししか使わないルールセットも単純に表せます。新しいオブジェクトを追加するのに加えて、各オブジェクトは独自の情報を保持してそれを Thousand Parsec プロトコルで送ることができます。五つの基本的な組み込み型が現在デフォルトで提供されます: Universe, Galaxy, Star System, Planet, Fleet です。

Universe は Thousand Parsec のトップレベルオブジェクトであり、全てのプレイヤーから常にアクセス可能です。Universe オブジェクト自身はゲームの管理を行いませんが、ゲームに関する非常に重要な情報を保持します: 現在のターン数です。 Thousand Parsec で “年” と呼ばれるターン数は、ターンの終わりごとに一つ進みます。この数は符号無し 32 ビット整数として保存されるので、ゲームは 4,294,967,295 年まで進行できます。理論上は可能であるものの、この年まで進んだゲームを私たちは今まで見たことがありません。

Galaxy は近接する Star System, Planet, Fleet といったオブジェクトを保持するコンテナであり、それ以外の情報は持ちません。ゲーム中には多数の Galaxy が存在可能であり、それぞれが Universe の一部分をホストします。

これまでの二つのオブジェクトと同じように、Star System も低レベルオブジェクトのコンテナです。ただし Star System はクライアントに対して視覚的に表示されるオブジェクトを含む一番上位のオブジェクトです。Star System は Planet や Fleet を (少なくとも一時的には) 含みます。

Planet は大きな天体であり、入植したり、資源採掘施設・生産設備・地上兵器などを配備したりできます。Planet はプレイヤーが所有できる一番上位のオブジェクトです。Planet の所有権はプレイヤーにとって非常に重要であり、ほとんどのルールセットでは所有する Planet がなくなったプレイヤーは敗北となります。Planet オブジェクトは比較的多くデータを保持します。例えば次のデータです:

ここまでに説明した組み込みオブジェクトを使えば、昔ながらの宇宙 4X ゲームのルールセットの基本的な部分を作っていくことができます。もちろん、ソフトウェアエンジニアリングの原則に沿うように、ルールセットからのオブジェクトクラスの拡張も可能になっています。ルールセットの設計者は必要に応じて新しいオブジェクトタイプを作成し、既存のオブジェクトタイプに新たな情報を加えることが可能です。このためゲームで利用可能なオブジェクトは事実上無限に拡張できます。

命令

命令 (order) はルールセットで定義され、Fleet と Planet オブジェクトに対して指示されます。コアサーバーにデフォルトの命令タイプは存在しませんが、命令はゲームの一番基礎的な部分で重要な役割を果たします。ルールセットに応じて様々なタスクを達成するのに命令は使われます。4X ジャンルであれば、たいていのルールセットで実装されている標準的な命令が存在します: Move, Intercept, Build, Colonize, Attack 命令です。

4X の最初の X (explore) を達成するには、プレイヤーが宇宙の中を動き回る必要があります。これを行うのが Fleet オブジェクトに対する Move 命令です。Thousand Parsec フレームワークの柔軟で拡張性の高い精神に則って、Move 命令はルールセットごとに異なる実装が可能となっています。MinisecMissile and Torpedo Wars では、Move 命令が三次元座標をパラメータとして受け取ります。するとサーバーサイドで到着予定時刻が計算され、所要ターン数がクライアントに返されます。チームワークが実装されていないルールセットでは、Move 命令は疑似的な Attack 命令のように動作します。例えば MinisecMissile of Torpedo Wars において敵艦隊がいる場所に Move すると、その後しばらく激しい戦闘が起こります。Move 命令が三次元座標でないパラメータを受け取るルールセットもあります。例えば Risk というルールセットでは、「ワームホール」で直接つながっている惑星に一ターンで移動する Move しか許されていません。

Fleet オブジェクトに送られることが多い Intercept 命令は、空間内の他のオブジェクト (たいていは敵艦隊) との接触を行います。Move で事足りるだろうと思うかもしれませんが、二つのオブジェクトがターンの実行時に異なる方向へ移動する可能性があるので、Move を使って三次元座標に移動した場合には他の艦隊の場所に直接移動できません。そのため Intercept のような命令が必要となります。敵艦隊を宇宙から消し去ったり、危機的な状況で攻撃を避けるために Intercept が使われます。

Build 命令は 4X の二つの X (expand と exploit) を行います。帝国を拡張 (expand) する分かりやすい方法は、たくさんの艦隊と戦艦を作って遠くへ移動させることです。Build 命令は通常 Planet オブジェクトに送られ、その惑星が持つ資源の量 (およびその開発状況) による制限を受けます。資源を多く持つ星が母星になったラッキーなプレイヤーは、Build 命令を通してゲーム序盤を有利に進めることができます。

Colonize 命令も Build 命令と同様に expand と exploit を行います。この命令はほとんど常に Fleet オブジェクトに送られ、Colonize 命令を使ったプレイヤーは所有者のいない惑星を自分のものにできます。宇宙に散らばる惑星の支配を広げるために使用します。

Mine 命令は exploit を行います。この命令は普通 Planet オブジェクトや他の天体に送られ、天体表面からは利用できない資源の採掘を可能にします。Mine 命令を使うと資源が表面から利用可能になり、この資源で Build 命令を行えばプレイヤーの支配を広める助けになります。

いくつかのルールセットで実装される Attack 命令は、敵 Fleet あるいは Planet に対する戦闘を明示的に開始して 4X の最後の X (exterminate) を行う命令です。チームベースのルールセットでは、同士討ちを防ぎ連携攻撃を行うために (Move や Intercept による攻撃とは別の) Attack 命令が必要になります。

Thousand Parsec フレームワークではルールセットの開発者が自分で命令タイプを定義するようになっているので、思い付くがままに独自の命令を作成できます (さらに言えば作成が推奨されます)。任意のオブジェクトに好きなデータを格納できる機能のおかげで、開発者は独自の命令タイプを使った様々な興味深いことを行えます。

資源

資源 (resource) はゲーム内の Object に追加で格納されるデータのことです。よく使われる資源 (中でも Planet がよく使う資源) に注目すると、ルールセットの拡張を簡単に作成できます。Thousand Parsec の他の部分における設計判断と同様に、資源が導入されるときに重要視されたのは拡張可能性でした。

多くの資源はルールセット設計者によって実装されますが、フレームワークを通じて同じ扱いを受ける資源が一つあります: プレイヤーの母星を表す Home Planet です。

Thousand Parsec のベストプラクティスでは、資源は何らかのオブジェクトに変換できるものを表すべきだとされます。例えば Minisec には Ship Parts という資源があり、宇宙中の Planet にランダムに割り当てられます。Ship Parts を持つ惑星を Colonize すると、この資源を Build 命令で Fleet に変換できるようになります。

Missile and Torpedo Wars はこれまでで一番徹底的に資源を活用しているルールセットでしょう。武器に動的な特性を持たせ、惑星から艦隊に積み込んだり、逆に艦隊から惑星に下ろしたりできるようにした初めてのルールセットです。ルールセットの実装ではこれを行うために、ゲーム中に作成されるゲーム武器のそれぞれについて資源タイプを作成し、戦艦の武器タイプを資源で管理することで自由な移動を可能にしています。Missile and Torpedo Wars は他にも、Factories という資源を惑星に結び付けて工場 (惑星の生産能力) を管理します。

デザイン

Thousand Parsec では、武器と戦艦の両方が複数のコンポーネントを持つ場合があります。コンポーネントが組み合わさるとデザイン (design) の基礎となります。デザインとはゲーム内で作ったり使ったりするものの骨組みです。ルールセットを作るとき、設計者はすぐに次の問題にぶつかります: 「武器や戦艦のデザインを動的に作成できるべきだろうか、それとも決められたデザインを使うようにするべきだろうか?」 一方では、決まったデザインを使ったゲームは開発とバランス調整が簡単になります。しかしもう一方では、デザインを動的に作成できればゲームは複雑でやりごたえのある楽しいものとなります。

ユーザーがデザインを作成できると、ゲームがずっと高度なものになります。ユーザーは戦艦とその武装を自分で考えた戦略に基づいて設計するので、ゲームごとに違いが顕著に出ます。これは運 (母星の配置など) やゲームの他の戦略から生じるを格差を薄らげる助けになるでしょう。こういったデザインは Thousand Parsec Component Language (TPCL, 後述) で記述されるコンポーネントのルールによって制御され、ルールセットごとに異なります。そのため武器や戦艦のデザインを実装する開発者であっても、追加の機能をプログラムする必要はありません。ルールセットで利用する各コンポーネントについてシンプルなルールを設定すればそれで済みます。

慎重に計画を立ててバランスを上手く取らなければ、独自デザインの利点もゲームをつまらなくする要因になります。ゲームの終盤では新しい武器と戦艦の設計にとてつもない時間がかかるので、クライアントがデザインを作るときのユーザーエクスペリエンスをどのように高めるかが問題になります。デザインの作成は必須ではあるもののゲームの他の部分と全く関係ない要素なので、デザインウィンドウをクライアントに統合すると非常に邪魔になります。Thousand Parsec の最も完全なクライアント tpclient-pywx では、このウィンドウのランチャーをメニューバーのサブメニューという目立たない位置に配置しています (この場所はゲームにほとんど使われません)。

デザインの機能はルールセットの開発者が簡単にアクセスでき、かつゲームが事実上無限の拡張性を持つように設計されています。今あるルールセットの多くは事前に設定されたデザインを使っていますが、Missile and Torpedo Wars では武器と戦艦のデザインの全てを様々なコンポーネントを使って作成できます。

Thousand Parsec プロトコル

Thousand Parsec プロトコルこそがこのプロジェクトの基礎である、と言えるかもしれません。ルールセットの開発者が利用可能な機能、サーバーの動作、クライアントの処理を定義するのがこのプロトコルであるからです。さらに最も重要なこととして、このプロトコルのおかげで (まるで星間通信の規格のように) 様々なソフトウェアのコンポーネントがお互いを理解できるようになります。

サーバーはルールセットで提供される指示に従ってゲームの現在状態を管理します。各ターンでプレイヤーのクライアントはゲームの状態に関する情報をいくらか受け取ります。例えばオブジェクト、オブジェクトの所有者と現在の状態、実行中の命令、資源の貯蔵量、技術の進捗、メッセージ、そしてそのプレイヤーに見えているもの全てに関する情報です。プレイヤーは与えられた現在状態から命令の発行やデザインの作成といった操作を行い、これをサーバーに送り返します。サーバーはこれを受けて次のターンの状態を計算します。こういった通信は全て Thousand Parsec プロトコルという枠組みで行われます。このアーキテクチャから生まれる興味深い帰結として、AI クライアント (サーバーやルールセットとは個別に存在する、コンピュータープレイヤーを作成する唯一の方法) がクライアントの人間プレイヤーと同じ制限を受けます。つまり、AI クライアントは隠された情報にアクセスしたりルールに従わない行動をするような「チート」を行うことができません。

プロトコルの仕様はフレームをいくつか定義します。フレームは階層的であり、(Header 以外の) 各フレームはベースフレームに独自のデータを追加します。フレームの型の中には抽象的に存在するだけで実際に使われることはなく、具象化されたフレームを説明するために存在するフレームもあります。またフレームに方向を指定して、サーバーとクライアントの片方からの送信だけをサポートすれば十分であると示すことも可能です。

Thousand Parsec プロトコルは TCP/IP 越しにスタンドアローンで動作しますが、HTTP といった別のプロトコルでトンネルすることもできます。また SSL 暗号化もサポートします。

基本

Thousand Parsec プロトコルにはクライアントとサーバーの通信でお決まりのフレームがいくつかあります。前述の Header フレームは他の全てのフレームの基礎であり、Request あるいは Response がその直接の子孫となります。Request は (いずれかの方向の) 通信を開始するためのフレームの基礎であり、Response はその通信の返事を表すフレームの基礎です。OKFail というフレームはどちらも Response であり、文字通りの真偽値を表すのに使われます。Sequence フレームも Response であり、リクエストに対する返答が複数のフレームからなることを示します。

Thousand Parsec は数値 ID を使って物を表すので、ID を使ってデータをやり取りするフレームが色々あります。例えば Get With ID フレームは ID を使う一番簡単なリクエストです。ID を持つ親の「スロット」にある物 (オブジェクトに対する命令など) を取得する Get With ID and Slot というフレームもあります。その他にも、クライアントの初期状態を設定する場合など、複数の ID を取得する必要が生じることがよくありますが、これは Get ID Sequence 型のリクエストと ID Sequence 型のレスポンスで処理されます。複数のアイテムのリクエストするときには、まず Get ID Sequence リクエストおよび ID Sequence レスポンスを行い、その後にそれぞれ Get With ID リクエストあるいはアイテムを記述した適切なレスポンスを行うという方法が使われます。

プレイヤーとゲーム

クライアントはゲームを始める前にいくつかするべきことがあります。最初クライアントは Connect フレームをサーバーに送り、OK または Fail を受け取ります ――Connect にはクライアントのプロトコルバージョンが含まれるので、バージョンが合わないときには失敗して Fail が返ります。あるいはサーバーが移動した場合やサーバープールを使っている場合には Redirect フレームが返ります。その後クライアントは Login フレームを送り、プレイヤーの識別と (必要ならば) 認証を行います。サーバーに初めて接続するプレイヤーは (サーバーが許可するなら) Create Account フレームを最初に送ることもできます。

Thousand Parsec では様々なことが行えるので、クライアントはまずサーバーがサポートするプロトコルの機能を確かめる必要があります。これを行うのが Get Features リクエストと Features レスポンスです。サーバーから返る機能の例を示します:

同様に Get Games リクエストと Game の列からなるレスポンスによってサーバーで進行中のゲームに関する情報がクライアントに伝えられます。一つの Game フレームにはゲームに関する次のような情報が含まれます:

もちろんここで重要となるのが、プレイヤーが誰と対決するのか (あるいはチームを組むのか) です。このためのフレームが存在し、Get Player IDs リクエストと List of Player IDs レスポンス、そして複数の Get Player Data リクエストと Player Data レスポンスが使われます。Player Data フレームにはプレイヤーの名前や種族といった情報が含まれます。

ゲームにおけるターンもプロトコルで制御されます。自身の操作を終えたプレイヤーは Finished Turn リクエストを送信して次のターンの準備ができたことをサーバーに伝え、全てのプレイヤーがこのリクエストを送信すると次のターンが計算されます。またサーバーが設定する時間制限もあり、操作の遅いプレイヤーや反応しなくなったプレイヤーがゲームの進行を止めてしまわないようになっています。クライアントは Get Time Remaining リクエストを送信して、サーバーからの Time Remaining レスポンスに応じた値にローカルのタイマーを設定します。

最後に、Thousand Parsec は様々な形態のメッセージをサポートします。全プレイヤーに向けたブロードキャスト、一人のプレイヤーに向けた通知、プレイヤー同士のやり取りなどです。メッセージの順序や可視性の管理には “board” コンテナが使われます。アイテムのときと同じやり方で、Get Board IDs リクエスト、List of Board IDs レスポンス、そして複数の Get Board リクエストと Board レスポンスからなるやり取りが行われます。

クライアントがメッセージボードの準備を完了すると、Get Message リクエストでスロットごとにメッセージをボードに取得します (このため Get MessageGet With ID and slot をベースフレームとして使います)。サーバーが返す Message フレームには、メッセージの件名・本文、メッセージが作成されたターン数、メッセージが参照するエンティティへの参照が含まれます。Thousand Parsec に登場する通常のアイテム (プレイヤーやオブジェクトなど) に加えて、メッセージの優先度、プレイヤーの操作、命令状態などへの参照もあります。クライアントは Post Message フレーム ――Message を送信するフレーム―― でメッセージを加えたり、Remove Message フレームで削除したりできます (このフレームは GetMessage がベースです)。

オブジェクト・命令・資源

ゲーム宇宙との対話の大部分は、オブジェクト・命令・資源に対する機能を表すフレームを通じて行われます。

宇宙の物理的な状態 (少なくともプレイヤーから観測あるいは制御可能な部分) は接続時にクライアントが取得し、その後もターンごとに更新する必要があります。クライアントは Get Object IDs リクエスト (Get ID Sequence) を発行し、サーバーは List of Object レスポンスを返します。その後クライアントは各オブジェクトについて Get Object by ID リクエストで詳細情報を問い合わせ、サーバーはその情報を詰めた Object フレームを返します。ここで取得する情報は、オブジェクトのタイプ、名前、サイズ、位置、速度、行える命令、現在の命令といった情報であり、プレイヤーから何が見えるかが考慮されます。プロトコルには Get Object IDs by Position というリクエストもあり、これを使うと指定した球内にある全てのオブジェクトをクライアントに送ることができます。

クライアントが可能な命令を取得するときのも似た方法を使います。つまり Get Order Description IDs リクエストを最初に発行し、その後 List of Order Description IDs レスポンスの各 ID ついて Get Order Description を発行し、Order Description レスポンスを受け取るというものです。命令と命令キューの実装はプロトコルの歴史と共に大きく変わりました。最初のバージョンでは各オブジェクトが一つの命令キューを持っていました。クライアントは Order リクエストを (命令タイプやターゲットオブジェクトといった情報と共に) 発行し、予想される命令の結果の詳細を Outcome レスポンスで受け取り、その後に実際の結果を Result フレームで受け取るというものです。

次のバージョンでは、Order フレームが Outcome フレームの内容を取り込み (これが可能なのは OutCome が命令の詳細だけで決まるので、サーバーからの入力を必要としないためです)、加えて Result フレームは削除されました。プロトコルの最新バージョンでは命令キューがオブジェクトと切り離され、Get Order Queue IDs, List of Order Queue IDs, Get Order Queue, Order Queue というフレームが新しく作られました。これらの機能はメッセージやボードと同様です。Get Order フレームと Insert Order フレーム (両方とも GetWithIDSlot リクエスト) を使うと、クライアントからキューにある命令へのアクセスおよび命令の削除ができます。Insert Order フレームは命令の伝達手段として機能します。このフレームが用意されたのは、クライアントが Probe Order という別のフレームを使ってローカルで使うための命令の情報を入手できるようにするためです。

資源に関する情報の取得もアイテムのときと同様に行います。つまり Get Resource Description IDs リクエストと List of Resource Description IDs レスポンス、そして複数の Get Resource Description リクエストと Resource Description レスポンスが使われます。

デザインの操作

Thousand Parsec プロトコルにおけるデザインの扱いは、四つのサブカテゴリの操作に分解できます。そのサブカテゴリとは、カテゴリ、コンポーネント、プロパティ、デザインの四つです。

カテゴリはデザインタイプを区別します。最もよく使わる二つのデザインタイプは戦艦 (ship) と武器 (weapon) です。カテゴリは名前と説明だけからなるので、作るのは簡単です。Category フレームにはこの二つの文字列しか含まれません。ルールセットは各カテゴリを表す Category フレームを Add Category リクエストを使って Design Store に保存し、以降カテゴリの管理はアイテムと同様に Get Category IDs リクエストと List of Category IDs レスポンスを使って行われます。

コンポーネントはデザインを構成する異なる部分やモジュールからなります。船体やミサイル、あるいはミサイルの発射台まで様々なものがコンポーネントとなります。コンポーネントはカテゴリよりも込み入っており、Component フレームには次の情報が含まれます:

特筆に値するのがコンポーネントに関連付けられる Requirements 関数です。コンポーネントは戦艦や武器といった建築可能なオブジェクトを構成する部品なので、デザインに追加するときには矛盾が起きないことを確認する必要があります。Requirements 関数は、デザインに追加されるコンポーネントが既存のコンポーネントと衝突しないかを確認します。例えば Missile and Torpedo Wars では、アルファミサイル発射台がない戦艦はアルファミサイルを積むことができません。この確認はクライアントとサーバーの両方で行われるので、関数全体が簡潔な言語 (TPCL については後述) で書かれプロトコルフレームに含まれます。

デザインのプロパティは Property フレームでやり取りされます。ルールセットはゲームで使うプロパティを公開し、そこには戦艦に設置できるミサイル発射台の数や船体タイプそれぞれに取り付けられるアーマーの量などが含まれます。Component フレームと同様に Property フレームも TPCL を利用します。Property フレームが含む情報は次の通りです:

プロパティのランクは依存関係の階層を区別するために使われます。プロパティに関する TPCL 関数では、そのプロパティ以下のランクを持つプロパティに依存する関数は書けないことになっています。例えば Armor プロパティがランク 1 で Invisibility プロパティがランク 0 なら、Invisibility は Armor に依存してはいけません。ランクは依存関係が循環してしまうのを避けるために実装されました。Calculate 関数はプロパティの表示方法を定義するのに使われ、この関数を使って測定方法の変更に対応します。Missile and Torpedo Wars は XML を使ってゲームデータファイルからプロパティをインポートします。図 21.2 にゲームデータから抽出されたプロパティの例を示します。

<prop>
<CategoryIDName>Ships</CategoryIDName>
<rank value="0"/>
<name>Colonise</name>
<displayName>Can Colonise Planets</displayName>
<description>Can the ship colonise planets</description>
<tpclDisplayFunction> (lambda (design bits) (let ((n (apply + bits))) (cons n (if (= n 1) "Yes" "No")) ) )
</tpclDisplayFunction>
<tpclRequirementsFunction> (lambda (design) (cons #t ""))
</tpclRequirementsFunction>
</prop>
図 21.2 プロパティの例

これは Ships が持つランク 0 の プロパティを表します。Colonise と呼ばれるこのプロパティは、戦艦が惑星に入植できるかどうかを表します。tpclDisplayFunction という名前で表される TPCL 関数 Calculate は、与えられた戦艦がこのプロパティの表す機能を持つかどうかを “Yes” または “No” で返します。機能の追加をこのように行えば、ルールセットの設計者はゲームのメトリクスと細かく制御し、その影響を比較してプレイヤーに分かりやすいフォーマットで出力できます。

戦艦や武器などの実際のデザインは Design フレームを使って生成・操作されます。現在のルールセットの全てで、戦艦と武器を作るときには既存のコンポーネントとプロパティからなるデザインが使われます。設計者は TPCL の Requirements 関数を使ってプロパティとコンポーネントのルールを設計するので、デザインの作成はいくぶん単純です。Design フレームには次の情報が含まれます:

このフレームは他のフレームと少し異なります。最も重要な違いは、デザインがプレイヤーに所有されるアイテムであるために、デザインごとに所有者が設定される点です。加えてデザインはインスタンスの数をカウンターを使って追跡します。

サーバーの管理

サーバーの管理を行うためのプロトコルの拡張機能が用意されており、これを使えばサーバーをリモートから制御できます。この機能がよく使われるのは、管理クライアント (シェル風のコマンドインターフェースあるいは GUI の設定パネル) からゲーム設定の変更といったメンテナンス処理を行うときです。ただ他にもシングルプレイヤーゲームの管理といった特殊な処理も行えます。

前節で説明したゲームプロトコルと同様に、管理クライアントはまず Connect リクエストと Login リクエストを使って接続の確立と認証を行います (ゲームを遊ぶときとは異なるポートを使います)。接続が完了すると、クライアントはログメッセージをサーバーから読んだりコマンドを送ったりできるようになります。

ログメッセージは Log Message フレームを使ってクライアントにプッシュされます。メッセージには重要度レベルとテキストが含まれ、クライアントは受け取ったメッセージのうちどれを表示するか (あるいは一切表示しないか) を選択できます。

サーバーからは Command Update フレームが送られることもあり、このフレームはクライアントがローカルに持つコマンドリストの更新を指示します。サーバーがサポートするコマンドが書かれたこのリストは Get Command Description IDs フレームへのレスポンスとしてサーバーから入手できます。その後クライアントは個別のコマンドの説明を入手するために Get Command Description フレームを発行し、サーバーから Command Description フレームをレスポンスとして手に入れます。

この情報の受け渡しはメインのゲームプロトコルの処理と非常に似ています (実際、最初は同じ基盤を持っていました)。コマンドをユーザーに伝えておけば、それを使って何らかの準備をしてネットワークの使用を抑えられます。管理プロトコルは当時ゲームプロトコルが成熟した証だとみなされました。必要とされる機能の多くがゲームプロトコルにあったので、管理プロトコルの開発者は最初からコードを書くことなく既存のライブラリにコードを足すだけで済んだのです。

様々な機能のサポート

サーバーの永続化

他のターンベースストラテジーのゲームと同じように、Thousand Parsec の一ゲームは非常に長くなる可能性があります。サーバーはプレイヤーの 24 時間単位の生活よりもはるかに長いライフサイクルを持ちますが、様々な理由により途中で実行を中断することがあります。プレイヤーがゲームを離れた時点からゲームを再開できるように、Thousand Parsec サーバーには (複数の) ゲーム宇宙全体を永続的にデータベースに書き出す機能があります。この節の後半で説明されるシングルプレイヤーのゲームでもこの機能が使われます。

フラッグシップのサーバー tpserver-cpp は、抽象的な永続化インターフェースとモジュラーなプラグインシステムを使って多数のデータベースバックエンドをサポートします。執筆時点の tpserver-cpp には MySQL と SQLite 用のモジュールが付属します。

Persistence という抽象クラスが (20.1 節 で説明した) ゲーム要素をサーバーで保存、更新、取得する機能を提供します。サーバーのデータベースはゲームが進行し状態が変化するたびに更新されるので、突然サーバーが終了・クラッシュしたとしても、再起動すれば保存したデータを回復できます。

Thousand Parsec Component Language

Thousand Parsec Comment Language (TPCL) を使うと、クライアントはサーバーと対話せずにデザインを作成でき、デザインのプロパティ・部品・妥当性についてのフィードバックをすぐに得られます。プレイヤーは戦艦の構造・推進力・装備・装甲・武器を利用可能な技術に応じてカスタマイズし、インタラクティブに戦艦の新しいクラスを作成できます。

TPCL は Scheme のサブセットです。一部を変更しているものの大部分は Scheme R5RS 規格に沿っており、互換性のあるインタープリタであればどんなものでも利用可能です。Scheme が選ばれた理由をいくつかあげると、言語が単純なこと、埋め込み言語として豊富な利用例があること、たくさんの言語で実装されたインタープリタがあること、そしてオープンソースプロジェクトとして一番重要な、言語の利用とインタープリタの開発に関するドキュメントが充実していることがあります。

次に示すのは TPCL で書かれたコンポーネントとプロパティが使う Requirements 関数の例です。この関数はサーバーサイドのルールセットにインクルードされ、ゲームプロトコルを通してクライアントに渡されます。

(lambda (design)
  (if (> (designType.MaxSize design) (designType.Size design))
      (if (= (designType.num-hulls design) 1)
          (cons #t "")
          (cons #f "Ship can only have one hull")
      )
      (cons #f "This many components can't fit into this Hull")
  )
)

Scheme さえ知っていれば、このコードは簡単に読めるでしょう。ゲーム (クライアントとサーバーの両方) がこれを実行すると、この関数はもう一方のコンポーネントのプロパティ (MaxSize, Size, Num-Hulls) を調べ、このコンポーネントをデザインに追加できるかを確認します。つまりまずコンポーネントの Size がデザインの最大サイズ以下であることを確認し、それからデザインに船体が一つだけであることを確かめます (二番目の条件文からこの関数は戦艦の船体の Requirements 関数だと分かります)。

BattleXML

戦争においては全ての戦闘が重要です。宇宙の果てで軽装の斥候部隊が起こす小競り合いも、大都市の上空で大艦隊が繰り広げる最終決戦も、その重要性は変わりません。Thousand Parsec フレームワークではルールセットが戦闘処理を管理し、クライアントサイドが戦闘に関して行えることは一切ありません。具体的には、プレイヤーは戦闘の開始とその結果をメッセージとして受け取り、その結果に基づいてオブジェクトの状態を変更する (例えば破壊された戦艦を消去するなど) だけです。通常プレイヤーが目にするのは高レベルな情報だけですが、ルールセット内部には複雑な戦闘メカニクスが存在します。戦闘について少し知っておけば、戦闘を有利に進められるでしょう (少なくとも好奇心は満たされるはずです)。

ここで登場するのが BattleXML です。戦闘のデータは二つの部分に分かれます: グラフィックを指定する「メディアの」定義と、戦闘で何が起こるかを指定する「戦闘の」定義です。二つの定義は戦闘ビューアで読むことができ、Thousand Parsec には 2D と 3D のビューアがあります。バトルの進行はルールセットが完全に支配するので、BattleXML を作るのはルールセットの仕事です。

メディアの定義はビューアの見た目と結び付いていて、XML データおよびそれが参照するグラフィックファイルとモデルファイルが含まれた辞書あるいはアーカイブとして保存されます。XML データは各戦艦 (などのオブジェクト) を表す画像、武器発射時や戦艦破壊時に再生するアニメーションやアクションなどを指定します。XML ファイルが参照するファイルの場所は XML ファイルからの相対パスであり、親ディレクトリを参照することはできません。

戦闘の定義はビューアやメディアと独立です。戦闘開始時にまず両サイドのエンティティがユニークな識別子で表され、名前・説明・タイプといった情報も与えられます。それから戦闘がラウンドごとに記述されます。つまり、オブジェクトの移動、武器の使用 (どれが、どれに使用したか)、オブジェクトへのダメージ、オブジェクトの破壊、あるいはログメッセージが出力されるということです。戦闘の各ラウンドをどれだけ細かく説明するかはルールセットによって決まります。

メタサーバー

Thousand Parsec がプレイできるパブリックなサーバーを探すというのは、宇宙奥深くを独り彷徨うステルススカウトを見つけるようなものです。探索すべき場所を知っていなければ、見つかる可能性はほとんどありません。幸いにも、パブリックなサーバーは自身をメタサーバーに登録でき、このメタサーバーの場所はプレイヤーがみな知っていることになっています。

現在の実装は metaserver-lite という PHP スクリプトであり、Thousand Parsec のウェブサイトといった中央の場所に置かれています。Thousand Parsec サーバーは HTTP リクエストをメタサーバーに送信し、更新アクション、場所 (プロトコル・ホスト・ポート)、プレイヤー数、オブジェクトカウント、管理者といった情報を伝えます。サーバーのリストは指定された時間 (デフォルトでは十分) が経過すると期限切れとなるので、サーバーは定期的にメタサーバーにある情報を更新することが求められます。

スクリプトはその後 (アクションが指定されていなければ) サーバーのリストをウェブサイトに変換し、クリック可能な (通常は tp:// スキームの) URL として表示します。あるいはバッジアクションを使って、サーバーリストをコンパクトな「バッジ」形式で表示することもできます。

クライアントは get を使ってメタサーバーにリクエストを発行することで利用可能なサーバーのリストを取得できます。こうするとメタサーバーはサーバーにつき一つ以上の Game フレームをクライアントに返します。tpclient-pywx では、こうして返されるリストが最初の接続ウィンドウに表示されるようになっています。

シングルプレイヤーモード

Thousand Parsec はネットワーク越しのマルチプレイヤーをサポートするように最初から設計されています。しかし、プレイヤーがローカルサーバーを作ってそこに AI クライアントを何台か追加して、そのシングルプレイヤー用宇宙の征服を始めるのを妨げるものは何もありません。Thousand Parsec プロジェクトはこの処理を簡略化するための標準的なメタデータと機能をいくつか定義しており、設定は GUI ウィザードを実行したりシナリオファイルをダブルクリックするだけで済みます。

この機能のコアにあるのは、各コンポーネント (サーバー、AI クライアント、ルールセットなど) の機能とプロパティに関するメタデータに対するフォーマットを定める XML DTD です。コンポーネントパッケージには一つ以上のこういった XML ファイルが付属し、全てのメタデータは「サーバー」と「AI クライアント」という二つの部分に集められます。サーバーのメタデータには通常ルールセットに関する (一つ以上の) メタデータがあります。こういったデータがサーバーのメタデータにあるのは、一つのルールセットが複数のサーバーで実装されることはあり得るものの細かい設定が異なる可能性があるので、実装ごとに個別のメタデータが必要になるためです。メタデータに含まれるコンポーネントには次の情報が含まれます:

必須パラメータはプレイヤーから設定できず、通常はローカルのシングルプレイヤーにおいてコンポーネントの動作を調整するオプションとして動作します。プレイヤーが指定するパラメータには独自のフォーマットが存在し、名前、データ型、デフォルト、値の範囲、メインのコマンド文字列に付け足されるフォーマット文字列などが指定できます。

パッケージ化された設定 (ルールセット独自のクライアントに対するゲーム設定プリセットなど) を使用する場合には、互換性のあるコンポーネント中からいずれかを選択してシングルプレイヤーゲームを構築することになります。プレイヤーはゲームをプレイするためにクライアントを起動するはずなので、クライアントの選択は事実上存在しません (クライアントは以降の選択を簡単にするワークフローを提供するべきです)。次の選択はルールセットであり、プレイヤーにはサポートしているルールセットのリストが表示されるでしょう (この時点ではサーバーの詳細は関係ありません)。選択されたルールセットが複数のインストール済みサーバーで実装されている (稀な) 場合にはプレイヤーがその中から一つをさらに選択し、一つしかない場合には自動的に選ばれます。するとメタデータからルールセットとサーバーの設定のデフォルト値が読み込まれ、さらなる設定のためにプレイヤーに表示されます。最後に、互換性のある AI クライアントがインストールされている場合にはそれを追加できます。

こうしてゲームが設定されると、クライアントはメタデータに含まれるコマンド文字列の情報を使って、ルールセット、ルールセットのパラメータ、サーバーなどのパラメータを適切に設定したローカルサーバーを起動します。前述の管理プロトコル拡張などでサーバーが起動し接続を受け付けているのが確認されると、指定された設定の AI がクライアントと同じ方法で起動してゲームに接続し、そのことが確認されます。以上の処理が完了すると最後にクライアントが (オンラインゲームに接続するのと同じ処理で) サーバーに接続し、プレイヤーが探索・交易・征服を行う準備が整います。

シングルプレイヤーのもう一つのとても重要な機能が、ゲームのセーブとロード (あるいは保存済みシナリオのロード) です。この場合には (一つ以上のファイルからなる) セーブデータにはシングルプレイヤーゲームの設定、およびゲームに関する永続データが保存されます。プレイヤーのシステムに適切なコンポーネントの互換性のあるバージョンがインストールされていれば、セーブデータの読み込みは完全に自動的に行われます。そのため特にシナリオであれば、クリック一回で起動するアイコンを作成できます。現在の Thousand Parsec には専用のシナリオエディタや編集モード付きのクライアントが存在しません。この理由は、セーブデータという機能が目指したのが、ルールセットの通常の動作とは別に永続データを作成する方法を提供し、その一貫性と互換性を保証することだったためです。

ここまでシングルプレイヤーの機能を抽象的に説明してきました。具体的に見ていくと、Python のクライアントヘルパーライブラリ libtpclient-py が Thousand Parsec プロジェクトのシングルプレイヤーメカニクスを完全に実装した唯一のライブラリです。このライブラリが提供する SinglePlayerGame クラスをインスタンス化すると、システム上のシングルプレイヤーメタデータが自動的に収集されます (メタデータを表す XML ファイルの保存場所に関するガイドラインがプラットフォームごとに存在します)。クライアントは利用可能なコンポーネント、例えばサーバー、ルールセット、AI クライアント、(Python の連想配列で辞書として保存される) パラメータなどを SinglePlayerGame に問い合わせることができます。前述のゲーム構築の一般的なプロセスに従って、クライアントは次に示すような処理を行います:

  1. 利用可能なルールセットのリストを SinglePlayerGame.ruleSet で取得し、SinglePlayerGame.rname を選択されたルールセットに設定する。

  2. ルールセットを実装するサーバーのリストを SinglePlayerGame.list_servers_with_ruleset で取得し、必要であればユーザーにその中から選択させ、SinglePlayerGame.sname を選択された (あるいは唯一の) サーバーに設定する。

  3. サーバーとルールセットに関するパラメータをそれぞれ SinglePlayerGame.list_rparamsSinglePlayerGame.list_sparams で取得し、プレイヤーに設定させる。

  4. 現在のルールセットをサポートする AI クライアントを SinglePlayerGame.list_aiclients_with_ruleset で検索し、SinglePlayerGame.list_aiparams で取得したパラメータを使って一つ以上の AI をプレイヤーに追加させる。

  5. SinglePlayerGame.start を使ってゲームを起動する。成功すると、接続すべき TCP/IP ポートが返る。

  6. 最後に SinglePlayerGame.stop でゲームを終了する (そしてサーバーと AI クライアントのプロセスを全て kill する)。

フラッグシップの Thousand Parsec クライアント tpclient-pywx では、こういった処理を行うユーザーフレンドリーなウィザードが用意されており、起動時にはロードするセーブデータやシナリオファイルを選ぶだけです。このユーザー中心的なウィザードのために開発されたワークフローは、このプロジェクトのオープンソース開発プロセスから生まれた優れたデザインの例です。開発者は最初に提案したのは内部処理に合わせた今とは大きく異なるやり方だったのですが、コミュニティによる議論と協調的な開発により、プレイヤーにとって格段に使いやすいウィザードが生み出されました。

最後に、セーブされたゲームとシナリオは現在 tpserver-cpp で実装され、libtpclient-py のサポート機能や tpclient-pywx のインターフェースも利用しています。これを可能にしているのは永続ストレージモジュール SQLite です。これはパブリックドメインのオープンソース RDBMS であり、外部プロセスを必要とせずデータベースを単一ファイルに保存するのが特徴です。SQLite がインストールされているなら、サーバーでそれを使うよう必須パラメータを通して設定できます。通常通りゲーム中に (一時フォルダに保存される) データベースは何度も更新され、プレイヤーがゲームをセーブをしたときにデータベースも指定された場所にコピーされます。そのときコピーされたデータベースにはシングルプレイヤーの設定データを表す特別なテーブルが付きます。このデータベースがその後どのように読み込まれるかは明らかでしょう。

教訓

高機能なフレームワーク Thousand Parsec の誕生と成長を見ていけば、これまでに行われた設計判断をじっくりと振り返って評価できます。オリジナルのコア開発者 (Tim Ansell と Lee Begg) はフレームワークをゼロから作成し、同様のプロジェクトを始めるにあたってのアイデアを私たちに与えてくれました。

上手く行ったこと

Thousand Parsec の開発で重要だったのが、最初にフレームワークのサブセットを定義・構築し、それから実装行ったことです。この反復的でインクリメンタルな設計プロセスによりフレームワークは有機的に成長し、新しい機能をシームレスに取り込めるようになりました。またこの考え方に基づいて Thousand Parsec プロトコルにはバージョンが付くようになり、このプロトコルはフレームワークが成功を収めた理由としてあげられるまでになりました。プロトコルのバージョン付けによってレームワークの成長が可能になり、新しいメソッドやゲームプレイへの道が開かれました。

拡張なフレームワークを開発するときに重要なのは、ごく短期間で目標にアプローチすること、そしてそれを反復することです。数週間ごとのマイナーリリースが行える程度の短いイテレーションを採用すれば、プロジェクトを素早く前進させ、そうしながらフィードバックをすぐに得られるようになります。実装においてもう一つ上手く行ったのが、クライアント-サーバーモデルを使ってクライアントをゲームロジックと独立に開発できるようにしたことです。ゲームロジックとクライアントソフトウェアの分離は Thousand Parsec の成功に大きく寄与しました。

上手く行かなかったこと

Thousand Parsec フレームワークの大きな失敗は、バイナリプロトコルを使ったことです。想像が付くと思いますが、バイナリプロトコルのデバッグは楽しいものではなく、長い時間がかかります。どうか同じ轍を踏まないでください。それから、プロトコルの柔軟性が高すぎるのも問題です。プロトコルを作るときには、必要な機能だけを実装すべきでしょう。

イテレーションがときに長くなりすぎることがありました。大きなフレームワークをオープンソースで開発スケジュールと共に開発するときには、追加される機能を小分けにしてイテレーションを行い、開発の流れを止めないことが重要です。

最後に

軌道上の建設所にある超巨大戦艦プロトタイプのスケルトン船体を建設用シップで見学するかのように、Thousand Parsec のアーキテクチャを様々な視点から見てきました。柔軟性と拡張性という設計基準は最初から開発者の頭にあったものの、フレームワークの歴史を見れば、新鮮な視点とアイデアに満ちたオープンソースのエコシステムだけが、大きな可能性と高機能で一貫した動作を両立できたというのは明らかです。Thousand Parsec は他に類を見ない野心的なプロジェクトであり、オープンソースという地平に立つ多くのプロジェクトと同じように、やるべきことは多く残されています。これから Thousand Parsec が進化と拡張を続け、さらに複雑なゲームが開発されることを願っています。何と言っても、千パーセクの旅も一歩から始まるのです。


  1. Thousand Parsec が影響を受けた素晴らしい商用ゲームをいくつかあげると、VGA Planets, Stars!, Master of Orion, Galactic Civilizations, Space Empires シリーズがあります。こういったゲームに馴染みのない読者のために言っておくと、同じゲームプレイスタイルを持つ有名なゲームに Civilization シリーズがあります (設定は異なりますが)。リアルタイムの 4X ゲームもあり、例えば Imperium GalacticaSins of a Solar Empire がそうです。[return]

広告