AI駆動のゲームデザイン:プロトコル仕様からリーダーボードまで一発で
Leverage OJの書き換えは動作するプラットフォームで終わった:バックエンド、フロントエンド、ジャッジエンジン、ELOシステム、リアルタイムの人間対ボットマッチ。自然な次の質問は、AIエージェントがそれを自律的に使用できるか——APIに対してコードを実行するだけでなく、ゼロからゲーム全体を設計できるかだった。
答えはイエスで、1つの重要な要素がある:機械可読プロトコルドキュメントとMCPサーバー。
AI + 構造化APIの問題
LLMはREST APIを呼び出せる。難しいのは、典型的なAPIには微妙な相互依存を持つ数十のエンドポイント、実行時にのみ現れる検証ルール、OpenAPIスキーマからは明らかでないドメイン固有のプロトコル(ジャッジのstdin/stdout契約など)があることだ。
これをプロンプトで回避することはできるが、スケールしない。より良く機能するのは、AIに単一のプレーンテキストドキュメント——密度が高く、構造化されており、機械向けに書かれている——を与え、そこからナビゲートさせることだ。
バックエンドにGET /aiを追加した:完全なプラットフォームコンテキストをプレーンテキストで返すパブリックな認証不要のエンドポイント。
# Leverage OJ — AIコンテキスト
Leverageはジャッジ(ゲームルール)とボット(AIプレイヤー)を書く
競技プログラミングプラットフォームです。このドキュメントを任意のAIエージェントと
共有して、自律的にゲームを設計・提出できるようにします。
## ジャッジプロトコル
...
## ボットプロトコル
...
## REST APIクイックリファレンス
...
## MCPツール
...ドキュメントは約3KB。ジャッジ/ボットのstdin/stdoutプロトコル、利用可能な言語、認証要件付きのAPIエンドポイント、MCPサーバー用のClaude Desktop設定テンプレートが含まれている。任意のAIクライアントのコンテキストに貼り付けると、必要なすべてが揃う。
MCPサーバー
プラットフォームは13ツールのMCP(Model Context Protocol)サーバーを同梱している:
LEVERAGE_TOKEN=<jwt> pnpm run mcp| ツール | 機能 |
|---|---|
list_games | 既存のゲームを閲覧 |
test_judge | ジャッジ + ボットを実行、ラウンドごとの完全な結果を取得 |
test_bot | 既存の対戦相手に対してボットをテスト |
get_leaderboard | ゲームのELOランキング |
list_gamers | ゲームに登録されたボットをリスト |
get_match_result | ラウンド、スコア、デバッグ付きの完全なマッチ結果 |
submit_judge | ゲームにジャッジプログラムをアップロード |
submit_bot | リーダーボードに新しいボットを登録 |
submit_renderer | HTMLレンダラーをアップロード |
get_judge | 現在のジャッジソースを取得 |
list_matches | gameId/gamerId/statusでマッチを検索 |
get_gamer | ボットのソースとメタデータを読む |
analyze_match | 効率的なデバッグのためにマッチをdebugHighlightsに前処理 |
最後の2つはAIデバッグのために特別に追加された:list_matchesはエージェントが失敗したマッチを見つけることを可能にし、analyze_matchはラウンド全体で空でないデバッグエントリを抽出する——エージェントが30ラウンドのJSONブロブから間違った1行を探すためにスキャンする代わりに。
ワークフロー
このMCPサーバーに接続されたAIエージェントは、完全なゲームデザインサイクルを自律的に実行できる:
- 仕様を読む —
GET /aiが完全なプロトコルを与える - コンテキストを閲覧 —
list_games()で参照用の既存ゲームを見る - ジャッジを書く — ステップ1のプロトコルを使用
- テストボットを書く — ジャッジロジックを検証するのに十分シンプルで、勝つほど賢くない
- テスト —
test_judge(gameId, judgerCode, bot0Code, bot1Code) - デバッグ —
analyze_match(matchId)はdebugHighlightsを返す:何か興味深いことが起こったラウンドのみ - イテレート — ジャッジを修正、再テスト、
verdict=finishでスコアが正しく見えるまで繰り返す - 出荷 —
submit_judge、次に各ボットにsubmit_bot
ステップ6のキーインサイト:30ラウンドのゲームにはデバッグ出力があるラウンドが3つだけかもしれない。analyze_matchはそれらにフィルタリングし、エージェントはJSONの90%を要約せずにスキップできる。
エンドツーエンド:1セッションで4つのゲーム
このパイプラインをCodexで使用して4つの完全なゲームを生成した:
囚人のジレンマ — 2人、15ラウンド。ジャッジは協力/裏切りの履歴を追跡し、標準ペイオフマトリックス(T=5、R=3、P=1、S=0)を実装。ボット:AlwaysCooperate、AlwaysDefect、TitForTat(Python + JS)。
ブラックジャック — 4人、ディーラーがジャッジ。ジャッジがカードを配り、ヒット/スタンドを管理し、ディーラーハンドを計算し、支払いを行う。ボット:Conservative(≥15でスタンド)、Aggressive(≤17でヒット)、BasicStrategy、Stand17+(JS)。
ライアーズダイス — 4人。ジャッジがダイスロール、ビッド検証、ライアーコール、ライフ追跡を管理。ボット:RandomBot、Conservative、Bluffer(Python + JS)。
ナンバーオークション — 4人のメカニズムデザインゲーム。ジャッジは各ラウンドで数字カードを公開、ボットは匿名で入札、最高のユニークビッドが勝つ。ボット:Proportional、Random、Aggressive(Python + JS)。
各ゲームには以下が含まれる:
- Pythonジャッジ(約150-300行)
- Pythonで3-4ボット + JavaScriptで1つ
- ゲーム固有の可視化を持つHTMLレンダラー
- ルールと戦略ノート付きREADME
パイプライン全体——プロンプト、生成、テスト、デバッグ、DBへの注入——がエンドツーエンドで実行された。唯一の人間の介入は/aiエンドポイントURLをコンテキストにコピー&ペーストすることだった。
実装ノート
ジャッジプロトコルの実践
ジャッジは各ラウンドでボットの応答を受信しコマンドを出力する:
# ラウンド1:ジャッジが全ボットに初期ゲーム状態を送信
round_data = json.loads(sys.stdin.readline())
# round_data = {"round": 1, "responses": {}}
# ターンベースゲームでは、非アクティブプレイヤーはnullコマンドを取得
commands = {str(i): None for i in range(player_count)}
commands[str(active_player)] = build_command(state, active_player)
print(json.dumps({
"commands": commands,
"display": build_display(state),
"verdict": "continue"
}))重要なのは、commandsの値は非アクティブプレイヤーに対してnullにできること。botzone-neoはnullエントリをフィルタリングし、そのラウンドでそれらのボットを呼び出さない——現在のプレイヤーのみが行動するブラックジャックのようなターンベースゲームに不可欠。
JavaScriptの言語ギャップ
テスト中に、javascriptがbotzone-neoのコンパイルサービスに登録された言語でないことを発見した。Pythonボットは成功する;JSボットはコンパイルエラーで静かに失敗する。修正は構文検証にnode --checkを使用し実行にnodeを使用するJavaScriptLanguageクラスだった——ギャップが見つかれば簡単だが、混合言語テストスイートがある場合にのみ現れるほど微妙だった。
レンダラープロトコル
各ゲームのビジュアルリプレイはサンドボックス化されたiframeにロードされるHTMLファイルだ。通信はpostMessage経由で行われる:
// ホスト → iframe(ラウンドナビゲーション時)
iframe.contentWindow.postMessage({
type: 'gameLog',
gameLog: { rounds: [...], finalResult: {...} },
round: currentRoundIndex // 0-indexed
}, '*')
// レンダラーはそのラウンドのビジュアル状態のためにround.displayを読む(トップレベルフィールド)
window.addEventListener('message', (event) => {
if (event.data.type !== 'gameLog') return
const display = event.data.gameLog.rounds[event.data.round]?.display
render(display)
})displayフィールドはjudgeCmd内ではなく各ラウンドオブジェクトのトップレベルにある。最初のレンダラーがround.judgeCmd.displayを読んでいた微妙なポイントだった(それはボットごとのコマンドdictであり、表示データではない)。
マルチプレイヤーサポート
ジャッジプロトコルは設計上Nプレイヤーだ——commandsとresponsesはプレイヤーインデックスでキー付けされたdictだ。マルチプレイヤーのメイン作業はオートマッチスケジューラ内にある:
function combinations<T>(arr: T[], k: number): T[][] {
if (k === 1) return arr.map(x => [x])
return arr.flatMap((x, i) =>
combinations(arr.slice(i + 1), k - 1).map(rest => [x, ...rest])
)
}
// トップNからELOでkボットをサンプリング、C(n,k)マッチ組み合わせを生成
// キューバーストを避けるためティックあたり20マッチに上限Nプレイヤーゲームの ELOはペア比較を使用——最終スコアでプレイヤーをランク付けし、各ペアに標準ELO調整を適用。これは近似(ゲーム理論的に最適ではない)だが、テストした4プレイヤーゲームでは実際にうまく機能する。
驚いたこと
プロトコルドキュメントはAPIより重要だ。 RESTエンドポイントは発見可能;ジャッジ/ボットのstdin/stdout契約はそうではない。私たちが見たすべてのAIハルシネーションはAPIではなくジャッジプロトコルについてだった。/aiドキュメントがこれを修正した。
analyze_matchはすぐに元を取る。 それなしでは、失敗した30ラウンドのゲームをデバッグするには30のJSONオブジェクトを読む必要があった。それがあれば、エージェントは空でないデバッグ出力を持つ3つのハイライトされたラウンドを取得する。修正までの時間が顕著に短くなった。
混合言語テストスイートは静かな失敗を捕捉する。 ジャッジの純粋なPythonテストは通過する。混合Python + JSテストはサンドボックスのコンパイル時ギャップを明らかにする。常にすべての言語バリアントでテストする。
レンダラーはJSエッジで脆弱だ。 ??(nullish coalescing)演算子は一部のJSパーサーコンテキストで括弧なしで||と混合できない。a ?? b || nullを使用するレンダラーは静かに失敗する;(a ?? b) || nullは機能する。
プラットフォームは今や新しいゲームの設計が本当に午後のプロジェクトになる地点にある:ルールを書き、AIエージェントでジャッジ + ボット + レンダラーを生成し、MCP経由でエンドツーエンドでテストし、プラットフォームにプッシュする。インフラストラクチャが残りを処理する——サンドボックス化された実行、ELO追跡、マッチリプレイ、オートマッチスケジューリング、マルチプレイヤー組み合わせ論。
ここからの興味深い問題は運用的だ:本番デプロイ、実際のトラフィック、そして最終的には研究用途(RL環境、LLMベンチマーク、メカニズムデザイン実験)。
