マルチプレイヤーアーキテクチャ
En Parlant~ のマルチプレイヤーは、WebSocketリレーサーバーを使用して2人のプレイヤーをリアルタイムに接続します。ピアツーピアは使用せず、すべての通信はリレーを経由します。これによりネットワーキングがシンプルに保たれ、ファイアウォールやNATを越えても確実に動作します。
アーキテクチャ概要
Section titled “アーキテクチャ概要”Player A (host) Relay Server Player B (joiner) | | | |--- create_game(name) ----------->| | |<-- game_created(code) -----------| | | |<--- join_game(code, name) -----| |<-- game_joined(name) ------------|--- game_joined(name) -------->| | | | |--- game_move(uci, times) ------->|--- game_move(uci, times) --->| |<-- game_move(uci, times) --------|<-- game_move(uci, times) ----| | | | |--- heartbeat ------------------>|--- peer_heartbeat ----------->|リレーはシンプルな転送レイヤーです。ルームをメモリ上に保持し、各ルーム内の2人のプレイヤー間でイベントをルーティングし、クリーンアップを処理します。サーバー側にはゲームロジックや手の検証はなく、クライアントが権限を持ちます。
技術スタック
Section titled “技術スタック”- フロントエンド(クライアント): socket.io-client — 標準の Socket.IO JavaScript クライアント
- バックエンド(リレー): socketioxide — Axum 上に構築された Rust 製 Socket.IO サーバー
- プロトコル: WebSocket 上の Socket.IO(必要に応じて HTTP ロングポーリングへの自動フォールバック付き)
Socket.IO が生の WebSocket よりも選ばれた理由は、自動再接続、ルーム/ネームスペース管理、構造化されたイベントハンドリングをすぐに利用できるためです。
ゲームフロー
Section titled “ゲームフロー”1. ゲームの作成
Section titled “1. ゲームの作成”ホストが Multiplayer をクリックし、表示名を入力します。クライアントは名前を含む create_game イベントを送信します。サーバーは以下を行います:
- 一意の6文字のルームコードを生成
- ルームを作成し、ホストを最初のプレイヤーとして追加
game_created(code)で応答し、ホストがコードを共有できるようにする
2. ゲームへの参加
Section titled “2. ゲームへの参加”参加者がルームコードと表示名を入力します。クライアントは join_game(code, name) を送信します。サーバーは以下を行います:
- コードでルームを検索
- 参加者を2人目のプレイヤーとして追加
- 両方のプレイヤーに
game_joined(name)を送信 — それぞれが相手の表示名を受け取る
両方のプレイヤーがルームに入ると:
- ホストは常に白、参加者は常に黒でプレイします。 これは交渉ではなく、参加順で決定されます。
- 手は UCI 形式(例:
e2e4)で、両プレイヤーの持ち時間とともに送信されます。 - リレーは各
game_moveイベントを対戦相手に転送します。両プレイヤーは確認として自分の手も受け取ります。
4. ゲーム終了イベント
Section titled “4. ゲーム終了イベント”ゲーム終了のシナリオを処理するイベントがいくつかあります:
- 投了 — 対戦相手に転送され、投了ダイアログがトリガーされる
- 引き分け提案 — 対戦相手に転送され、受諾または無視できる
- 引き分け受諾 — 両プレイヤーに転送され、引き分けとしてゲームが終了する
5. リマッチ
Section titled “5. リマッチ”ゲーム終了後、いずれかのプレイヤーが ready イベントを送信してリマッチの意思を示すことができます。両プレイヤーが ready を送信すると、クライアントは盤面をリセットし、色を交換します(またはそのまま維持 — クライアント側で決定)。
ルームコード
Section titled “ルームコード”ルームコードは6文字で、口頭で共有しやすいよう XX-XX-XX の形式にフォーマットされています。文字セットは視覚的に紛らわしい文字を除外しています:
0とOなし(ゼロと文字の O)1、I、Lなし(数字の1と大文字の I と大文字の L)
これにより、ボイスチャットでコードを読み上げたり、友人のメッセージからコードを入力したりする際の「それはゼロ?それとも O?」という問題を回避できます。
ハートビートシステム
Section titled “ハートビートシステム”マルチプレイヤー接続では、プレイヤーが切断されたことを検出する必要があります — ネットワーク切断、アプリの終了、ノートパソコンのスリープなど原因はさまざまです。En Parlant~ はこのためにハートビートシステムを使用しています:
- 各クライアントが 5秒 ごとにサーバーへ
heartbeatイベントを送信 - サーバーはハートビートを確認し、対戦相手に
peer_heartbeatとして転送 - クライアントは対戦相手から最後に
peer_heartbeatを受信した時刻を追跡 isPeerAlive(timeoutMs)関数が、対戦相手の最後のハートビートが許容閾値内かどうかを確認
これにより UI の接続状態インジケーターが駆動されます。ハートビートが届かなくなると、プレイヤーは対戦相手が切断された可能性があることを確認でき、待機するかゲームを離れるかを選択できます。
ルームのクリーンアップ
Section titled “ルームのクリーンアップ”リレーサーバーはメモリリークを防ぐため、アイドル状態のルームを自動的に削除します:
- ルームは 30分間 アクティビティがないとアイドルとみなされる
- クリーンアップタスクが 60秒 ごとに実行され、アイドル閾値を超えたルームをすべて削除
- ルームがクリーンアップされると、残っている接続はすべて切断される
永続ストレージはありません。サーバーが再起動すると、すべてのルームが消えます。これは意図的な設計です — リレーはステートレスかつエフェメラルです。進行中のゲームは再開が必要になりますが、実際にはこのケースはまれです。
デフォルトのリレーは Fly.io 上で実行され、自動 TLS による低遅延の WebSocket 接続を提供します。独自のリレーを実行する手順については、マルチプレイヤーサーバーセットアップガイドを参照してください。
ローカルでのテスト
Section titled “ローカルでのテスト”開発中にマルチプレイヤーをテストするには:
-
リレーをクローンして実行します:
Terminal window git clone https://github.com/DarrellThomas/en-parlant-relay.gitcd en-parlant-relaycargo runサーバーはポート 3210 で起動します。
-
En Parlant~ で、リレーサーバーの URL を
ws://localhost:3210に変更します。 -
アプリを2つのインスタンスで開いて(またはアプリ1つと開発モード1つで)、両プレイヤーをシミュレートします。
手、ハートビート、およびすべてのイベントは本番リレーとまったく同じように動作します — 違いは接続 URL のみです。