# War3AI / OpenWar3 完全版ドキュメント > 出典 https://war3ai.com/ja 。AI Agent のための Warcraft III 1.27 オープン API。Bot を書くときは、末尾の「API カタログ」に載っている Game メソッドだけを使ってください。 --- # ドキュメント概要 > OpenWar3 とは何か、何ができるか。クイックスタート、最初の Bot、LLM で Bot を書く、API・プロトコル、ゲートウェイ・MCP への入口。 **OpenWar3** は War3AI のオープンインターフェース層です。Warcraft III 1.27 に注入するランタイムと、Python SDK で構成されています。 - ランタイムは **50 ms** ごとに、マップ全体の完全な状態を共有メモリへプッシュします。全プレイヤーの資源と人口、全ユニットの HP・マナ、オーダー、攻撃中の相手、スキルのクールダウン、バフ、インベントリ、地面のアイテム、樹木、生産キュー、昼夜。さらに**イベントストリーム**もあります。ユニットの出現と死亡、1 回ごとのダメージ、生産完了…… - 外部プログラムは**約 1 フレーム**の遅延で**セマンティックコマンド**を発行できます。移動、攻撃、採集、建設、訓練、スペル使用、スキル習得、蘇生、アイテム使用、購入……すべてのコマンドに**レシート**が返り、エンジンが受理したかどうかと、受理されなかった場合の理由コードがわかります。 - 伝えるのは「何をするか」だけです。ユニットは 4 文字コード、スキルはオーダー名で指定し、ゲーム内の呼び方と一致します。「どう実現するか」はランタイムが担当します。 そのため LLM には低レベルの知識も、画面を見ることも必要ありません。ドキュメントを読めば、内政も戦闘もこなす Bot を書けます。対戦に出したあとは、レシートとイベントをもとに自分で修正していきます。 対戦だけではありません。[キャンバス](https://war3ai.com/ja/docs/canvas/) でゲーム画面に自分のパネルや注釈を描き、[UI と入力](https://war3ai.com/ja/docs/ui-input/) で描いたボタンをクリック可能にしたりホットキーに反応させたりでき、[JASS チャネル](https://war3ai.com/ja/docs/jass/) で外部からゲーム内の 1291 個の関数を呼び出せます。RPG マップでは [AI コンパニオン](https://war3ai.com/ja/docs/companion/) を連れて行くこともできます。作った AI は [スキーム](https://war3ai.com/ja/docs/schemes/) にまとめて、ワンクリックで切り替えたり、エクスポートして共有したりできます。新しい遊び方を丸ごと [ゲームプレイ MOD](https://war3ai.com/ja/docs/mods/) として書くこともできます。 Python を書かなくても接続できます。[ゲートウェイ](https://war3ai.com/ja/docs/gateway/) を使えば、どんな言語やブラウザのページからでも WebSocket / JSON で同じ API を呼べます。[MCP サーバー](https://war3ai.com/ja/docs/mcp/) を使えば、Claude Code のような Agent がツールを直接呼び出して、局面を見たりコマンドを出したりできます。 - [クイックスタート](https://war3ai.com/ja/docs/quickstart/): 環境を整え、1 コマンドでゲームを起動し、サンプル Bot が操作を引き継ぐのを確認します。 - [LLM で Bot を書く](https://war3ai.com/ja/docs/ai-bot/): プログラミング不要。プロンプトをコピーし、戦術を説明して Agent に任せます。 - [メンタルモデル](https://war3ai.com/ja/docs/concepts/): スナップショット、コマンド、レシート、イベント、ティック。Bot を書く前に 5 分で目を通してください。 - [API カタログ](https://war3ai.com/ja/api/): すべての API に、実測ステータス、レイテンシ区分、内部メカニズムを記載しています。 ## あなたに合った読み方 | あなたは | まず読む | 次に | |---|---|---| | Warcraft はプレイするが、プログラミングはしない | [クイックスタート](https://war3ai.com/ja/docs/quickstart/) → [LLM で Bot を書く](https://war3ai.com/ja/docs/ai-bot/) | 困ったら [よくある質問](https://war3ai.com/ja/docs/faq/) | | Python が書ける | [最初の Bot](https://war3ai.com/ja/docs/first-bot/) → [メンタルモデル](https://war3ai.com/ja/docs/concepts/) → [15 のルール](https://war3ai.com/ja/docs/rules/) | [プロ戦術クックブック](https://war3ai.com/ja/docs/cookbook/)、[サンプル Bot](https://war3ai.com/ja/docs/examples/) | | Coding Agent や自動化を作っている | [Agent による自律反復](https://war3ai.com/ja/docs/agent-loop/) | [レシートと理由コード](https://war3ai.com/ja/docs/reason-codes/)、[`llms-full.txt`](https://war3ai.com/ja/llms-full.txt) | | LLM にゲーム中の判断をさせたい | [LLM をアドバイザーに](https://war3ai.com/ja/docs/llm-coach/) | [頭上の吹き出しとローカルモデル](https://war3ai.com/ja/docs/speech/) | | Agent に直接操作させたい(Claude Code など) | [LLM がツールを直接呼び出す(MCP)](https://war3ai.com/ja/docs/mcp/) | [UI と入力](https://war3ai.com/ja/docs/ui-input/) | | 他の言語を使う(JS、C#、Go、Rust……) | [ゲートウェイ](https://war3ai.com/ja/docs/gateway/) | より低レベル:[W3P プロトコル](https://war3ai.com/ja/docs/protocol/) | | 異なる人の AI 同士を対戦させたい | [フェアモード](https://war3ai.com/ja/docs/fair-mode/) | [アリーナ](https://war3ai.com/ja/arena/) | | RPG / カスタムマップで自分だけの遊びを作りたい | [ゲームプレイ MOD](https://war3ai.com/ja/docs/mods/) | [UI と入力](https://war3ai.com/ja/docs/ui-input/)、[キャンバス](https://war3ai.com/ja/docs/canvas/)、[JASS チャネル](https://war3ai.com/ja/docs/jass/)、[RPG コンパニオン](https://war3ai.com/ja/docs/companion/) | | 自分の AI を他の人と共有したい | [AI スキーム](https://war3ai.com/ja/docs/schemes/) | [Farsight コンソール](https://war3ai.com/ja/docs/console/) | ## リポジトリの構成 ```text start.bat 唯一の入口:ゼロからのデプロイ + Farsight を開く。stop.bat ですべてを完全に停止 sdk/python/ インターフェース層。openwar3/ が公開ファサード(Game + Bot)。ここから始めます brains/ 意思決定層 examples/ hello_bot(経済)→ rush_bot(出兵)→ macro_bot(内政)→ micro_bot(マイクロ + クリーピング)、buddy(RPG コンパニオン)、 mod_hero_roguelike / mod_endless_defense(ゲームプレイ MOD) xwar3/ リファレンスブレイン:戦略層(秒単位)+ リフレックス層(4 プロセス)+ 勝率モデル console/ Farsight Web コンソール(FastAPI + React) gateway/ ゲートウェイ(WebSocket / JSON)+ JS クライアント + ブラウザ用デモページ director/ 自動カメラワーク、頭上の HP バー speech/ 頭上のチャット吹き出し + ローカル LLM runtime/ マルチインスタンス管理(設定に従ってゲームごとに再起動) data/ order-ids.txt。自分のゲームからデータを抽出するツール schemes/ あなたの AI スキーム(mine/)と他の人が共有したスキーム(installed/)。リポジトリには含めない tools/ play.py(1 コマンドでゲーム起動)、run_scheme.py(スキームランナー)、war3_mcp.py(MCP サーバー)、run_tests.py、実機検証スクリプト docs/ API カタログ api.json(コードから生成)、プロトコル、マニュアル ``` ランタイムとあなたのコードの間にあるのは、バージョン付きの [W3P プロトコル](https://war3ai.com/ja/docs/protocol/) だけです。Python SDK を使うのが一番手軽ですが、他の言語でもプロトコルに沿って接続できます。 ## API の「実測ステータス」とは API カタログでは、各 API に次の 3 つのステータスのいずれかが付いています。 - **実機検証済み**:内部の経路(アクション番号、引数の形、読み戻した効果)を実際の対戦で検証済みで、検証スクリプトで守られています。 - **実験的**:新しく追加された API で、テストインスタンスでは動作を確認済み、項目ごとの実機検証を継続中です。使うことはできますが、API の細部は今後変わる可能性があります。 - **推定 / 未完全検証**:内部メカニズムはエンジン自身のやり方(JASS の同等関数など)をそのまま踏襲していますが、対戦の中での項目ごとの確認はまだです。使う前にレシートを確認してください。 > **補足** > > 現在サポートしているのは **Warcraft III 1.27**(The Frozen Throne)のみです。1.24 〜 1.28 は同じエンジン構造で、複数バージョン対応は [ロードマップ](https://war3ai.com/ja/roadmap/) の P4 フェーズで行います。1.29 以降と Reforged は別のエンジンのため、対応の対象外です。 --- # クイックスタート > start.bat をダブルクリックすればすべて自動でインストールされます。Farsight でゲームのディレクトリを設定し、試合を始めてサンプル Bot に操作を任せます。所要時間は約 15 分です。 ## 必要なもの | | 要件 | 説明 | |---|---|---| | OS | Windows 10 / 11、64 ビット | 現在は Windows のみ対応 | | ゲーム | Warcraft III **1.27a**(The Frozen Throne、`Game.dll` 1.27.0.52240) | あなたが正規に所有しているクライアント。ディスク上のゲームファイルは一切変更しません | **ほかに事前にインストールするものはありません。** `start.bat` がダウンロードするのは Python 3.13(公式のポータブル版、約 14 MB)だけで、リポジトリの `bin\env\` に配置します。管理者権限は不要で、システムの PATH も変更しません。中国国内のネットワークでは自動でミラーに切り替えます。使える Python がすでにマシンに入っていれば、それをそのまま使います。PowerShell は Windows 標準のものを使い、Farsight の Web ページはビルド済みのものがリポジトリに同梱されているため、Node.js は不要です。 ## インストール 1. **コードを入手する** ```bash git clone https://github.com/OPENXXAI/OpenWar3AI.git ``` または [ZIP をダウンロード](https://github.com/OPENXXAI/OpenWar3AI/archive/refs/heads/main.zip)して展開します。ランタイム(注入 DLL とランチャー)はリポジトリに同梱されているので、別途ダウンロードする必要はありません。 2. **`start.bat` をダブルクリックする** 初回は次のことを自動で行います。 - Python 3.13 をダウンロード - Python パッケージをインストールし、ランタイムのファイルを照合してから配置 - AMAI をダウンロードして、リファレンスブレインが使う戦略データを生成(AMAI は独自ライセンスのため、生成物は git に含めません。失敗してもリファレンスブレインにしか影響しません) - Farsight のトップページ「コントロールセンター」 `http://127.0.0.1:8866` を開く 各ステップの結果を表示し、うまくいかなかったステップでは補う方法を教えてくれます。2 回目以降のダブルクリックでは 1〜2 秒のチェックだけを行って Farsight を開きます。 黒いウィンドウは数秒で自動的に閉じます。Farsight はバックグラウンドで動き続け、ブラウザを閉じても止まりません。 3. **コントロールセンターでゲームのディレクトリを設定する** コントロールセンターの一番上で「自動検出」するか、「参照…」から Warcraft III のディレクトリを自分で選びます。Farsight はゲームのバージョンを確認し、**あなた自身のゲームからデータを抽出**します(ユニット表、アビリティ、アイテム、相性表……Blizzard のファイルはコードと一緒には配布されません)。 バージョンが 1.27a でない場合は知らせてくれます。マップと次の試合の設定は、どちらもこのディレクトリが基準です(`<ゲームディレクトリ>\Maps` 以下のすべてのフォルダのマップを選べます)。あとでディレクトリを変えるときは「設定」ページで行います。 4. **試合を始めて、サンプル Bot に任せる** いちばん手軽なのは、Farsight の「インスタンスと開始設定」ページでインスタンス番号にチェックを入れ、AI スキームを選んで「テスト開始」をクリックする方法です。コマンドラインでも実行できます。 ```bash python tools/play.py --bot brains/examples/hello_bot.py ``` このコマンドは、ゲームインスタンスを起動し、ランタイムを注入し、自動で試合を開始してから Bot を立ち上げます。**農民が採掘に向かい、本拠地が農民の生産を始めれば成功です。** コマンドの `python` には `openwar3.json` に記録されているものを使います。`start.bat` が自分でインストールしたものは `bin\env\python\python.exe` にあります。 ## start.bat と stop.bat ```bash start.bat # デプロイのチェック + Farsight を開く start.bat setup # 完全チェック:Python パッケージの再インストール、AMAI の再試行 start.bat restart # Farsight のバックエンドだけを再起動(ゲームと各サービスには影響なし) start.bat node # あわせて Node.js もインストール(公式サイトをプレビューするときにしか使わず、普段は不要) start.bat 5 6 # あわせて 5 番、6 番インスタンスのテストを開始(ゲーム + リファレンスブレイン) stop.bat # すべてを完全に停止。stop.bat --keep-llm ならローカルモデルを VRAM に残す ``` ゲートウェイ、頭上の吹き出し、ローカル LLM も Farsight の「コントロールセンター」で起動・停止でき、別のスクリプトを探す必要はありません。**完全に停止する**には、`stop.bat` をダブルクリックするか、コントロールセンター右上の「すべて停止」をクリックします。ゲームインスタンス、AI、ゲートウェイ、吹き出し、このシステムが使うローカルモデル、Farsight のバックエンドが順にすべて停止します。MCP サーバーは Claude などのクライアントが管理しているため、停止されません。 > **設定ファイル** > > `openwar3.json` は `start.bat` と Farsight が自動で書き込みます。このマシン固有のパスだけを保存し、git には含めません。ポート、ローカル LLM のアドレスやモデル名を変えたいときは、`openwar3.example.json` を参考に、それと異なる項目だけを書きます。 ## play.py のパラメーター ```bash python tools/play.py --bot my_bot.py --inst 9 --race 2 --enemy-race 1 --difficulty 3 --speed 200 python tools/play.py --bot my_bot.py --inst 9 --attach # ゲームはすでに起動済み。Bot だけを接続する python tools/play.py --bot my_bot.py --fair # フェアモード:視界内のものだけが見える ``` | パラメーター | デフォルト | 説明 | |---|---|---| | `--bot` | 必須 | Bot ファイルのパス(ファイル内に `Bot` のサブクラスが 1 つ必要) | | `--inst` | `9` | インスタンス番号。実行中のインスタンスと番号がぶつからないようにしてください(Farsight の「インスタンスと開始設定」ページで使用中の番号を確認できます) | | `--race` | `1` | 自軍の種族:1 ヒューマン、2 オーク、3 アンデッド、4 ナイトエルフ | | `--enemy-race` | `0` | 相手の種族 | | `--difficulty` | `2` | コンピューターの難易度:2 Easy、3 Normal、4 Insane | | `--speed` | `100` | ゲーム速度(パーセント、200 = 2 倍速) | | `--map` | 設定の `default_map` | マップ | | `--attach` | | ゲームを起動せず、すでに実行中のインスタンスに接続するだけ | | `--hz` | `5` | 1 秒あたりの `on_tick` の呼び出し回数 | | `--minutes` | `60` | 最大実行時間(実時間の分) | | `--fair` | | [フェアモード](https://war3ai.com/ja/docs/fair-mode/) | | `--player` | | 何番のプレイヤーとして指揮するか(AI 対 AI のときに使用) | > **注意** > > `--minimize` でゲームを起動しないでください。**ウィンドウを最小化するとゲームのシミュレーションは止まり**(クロックが進まない)、Bot はいつまでも試合開始を待ち続けます。 `play.py` を使わずに、SDK のコマンドラインから実行中のインスタンスに直接接続することもできます。 ```bash python -m openwar3 run brains/examples/hello_bot.py --inst 5 # Bot を実行 python -m openwar3 status --inst 5 # 接続して、スナップショット / ファストレーンの状態を表示 python -m openwar3 catalog # API カタログを表示 ``` ## 動いたら次へ - [最初の Bot を書く](https://war3ai.com/ja/docs/first-bot/): 10 行の最小 Bot から始めて、兵の生産と出撃を一歩ずつ加えます。 - [LLM に書いてもらう](https://war3ai.com/ja/docs/ai-bot/): プロンプトテンプレートをコピーし、戦い方を普段の言葉で説明します。 ## セルフチェック ```bash python tools/run_tests.py # SDK / リファレンスブレイン / リフレックス層 / コンソール / 吹き出し / サンプル。それぞれ 1 つのサブプロセスで実行 ``` オフラインテストはゲームを起動しなくても実行できます。Farsight の「コントロールセンター」にも環境チェックがあり、各部分がインストールできているかを確認できます。 --- # 最初の Bot > 10 行の最小 Bot から始めて、農民の生産、人口の確保、兵の生産、ヒーロー、出撃を加え、最後にレシートの読み方を理解します。 Bot とは `openwar3.Bot` を継承したクラスのことです。必要なフックだけをオーバーライドすれば、「見る」と「やる」は `g`(`Game`)が担当します。 ## 最小の Bot ```python title="my_bot.py" from openwar3 import Bot class MyBot(Bot): def on_start(self, g): # 試合に入った後に 1 回呼ばれる g.message("参戦します") def on_tick(self, g): # 毎秒約 5 回 for w in g.idle_workers(): g.gather(w, g.nearest(g.gold_mines(), w)) ``` ```bash python tools/play.py --bot my_bot.py ``` 手の空いた農民が最寄りの金鉱へ向かいます。フックは 4 つです。 | フック | 呼ばれるタイミング | |---|---| | `on_start(g)` | 試合に入った後、最初のティックの前に 1 回 | | `on_tick(g)` | 毎ティック(デフォルトで毎秒 5 回)。1 ティックが時間を超えると自動的に後ろにずれ、どんどん溜まっていくことはありません | | `on_event(g, ev)` | 毎ティックの `on_tick` の前に、前回のティック以降のイベントを 1 件ずつ渡します | | `on_end(g, reason)` | 試合が終わったとき(ゲームプロセスがなくなった / 自軍のユニットがいなくなった / 手動で停止した)に 1 回 | > **ヒント** > > `on_tick` で例外が発生しても試合全体は中断されません。ランナーがスタックトレースを表示し、次のティックから続行します。**20 ティック連続でエラー**になったときだけ停止します。 ## 経済を加える:農民を作り、人口を確保する ```python from openwar3 import Bot class Economy(Bot): def on_tick(self, g): res = g.resources() # 読み取れないときは 0 ではなく None halls = g.my_buildings({"htow", "hkee", "hcas"}) if res is None or not halls: return home = halls[0] # 1. 手の空いた農民はゴールドを採掘 for w in g.idle_workers(): mine = g.nearest(g.gold_mines(), w) if mine: g.gather(w, mine) # 2. 農民を作る:キューには 1 つだけ入れる(いっぱいにするとゴールドがキューにロックされる) if len(g.my_workers()) < 15 and not g.queue(home): g.train(home, "hpea") # 3. 人口がもうすぐ上限:建設中でない農民を見つけ、本拠地の近くに Farm を建てる if res["food_cap"] - res["food_used"] <= 6: builder = next((w for w in g.my_workers() if not g.is_constructing(w)), None) if builder: g.build_near(builder, "hhou", home.x, home.y) ``` 注目すべき書き方が 3 つあります。 - **`g.queue(home)` が空のときだけ訓練する。** 毎ティック訓練命令を出すと 7 スロットのキューが埋まり、ゴールドがロックされます(実測:本拠地のキューに農民が 4 人入り、300 ゴールドがロックされて、序盤が大きく遅れました)。 - **座標を決め打ちせずに `build_near` を使う。** 近いところから順に置ける場所を探し、ティックをまたいで結果を追跡します。ゴールドが足りないときは何もしません。決め打ちの座標は、ちょうど木立の中かもしれません。 - **建設中の農民は選ばない。** ヒューマンの Farm は建設に 35 秒かかり、途中でワーカーを別の場所へ送ると基礎の工事が止まります。 4 種族すべてで動く完全版は `brains/examples/hello_bot.py` です:1 鉱山 5 人、鉱山がいっぱいなら伐採、工事が止まった基礎の建設再開まで行います。 ## Barracks、ヒーロー、出撃を加える ```python from openwar3 import Bot WAVE = 8 class Rush(Bot): def on_start(self, g): self.attacking = False def on_tick(self, g): halls = g.my_buildings({"htow", "hkee", "hcas"}) if not halls: return home = halls[0] # ヒーロー:祭壇があってヒーローがいない -> まず復活を試み、復活できなければ訓練する(ヒーローは唯一なので、死んだ後に訓練し直そうとすると拒否される) altars = g.my_buildings({"halt"}) if altars and not g.my_heroes(): if not g.revive(altars[0]): g.train(altars[0], "Hpal") for h in g.my_heroes(): info = g.hero_info(h) if info and info["skill_points"]: g.learn(h, "AHhb") # Holy Light # Barracks から Footman を出し続ける(キューには 1 つだけ) for b in g.my_buildings({"hbar"}): if not g.queue(b): g.train(b, "hfoo") # 1 波分たまったら出撃し、壊滅したら本拠地に戻る army = g.my_army() if len(army) >= WAVE: self.attacking = True elif len(army) < WAVE // 2: self.attacking = False if self.attacking: target = g.nearest([e for e in g.enemies() if g.is_building(e)], home) if target: idle = [u for u in army if not g.order_of(u)] # 手の空いたユニットにだけ命令する g.attack_move(idle, target.x, target.y) ``` 完全版は `brains/examples/rush_bot.py` を参照してください(`hello_bot` を継承し、Barracks / 祭壇がなければ建てます)。 ## レシートを読む すべてのコマンドはレシートを返します。`if r:` が「エンジンが受理した」という意味で、受理されなかった場合は `r.reason` に理由が入っています。 ```python r = g.train(barracks, "hfoo") if not r: print(r.reason) # rejected(人口不够)(= 人口不足) print(r.verdict) # 3 ``` よく見る理由コード:`3` 人口不足、`8` ゴールド不足、`9` 木材不足、`32` キューがいっぱい、`183` 前提条件が足りない、`221` その項目がない / 建設中 / ヒーローがすでにいる、`1001` ターゲットが見えない。全一覧は [レシートと理由コード](https://war3ai.com/ja/docs/reason-codes/) を参照してください。 > **受理 ≠ 成功** > > レシートが示すのは「エンジンがこのコマンドを受け付けた」ということだけです。木立の中の建設地点もエンジンはその場で受理し、ワーカーが到着してから失敗します。スキルは中断されることもあります。結果はスナップショットとイベントで確認してください:建物は `build_near` で建て(基礎が現れたかを追跡してくれます)、スキルは `g.cooldown()` がクールダウンに入ったかを見ます。 ## 次のステップ - [メンタルモデル](https://war3ai.com/ja/docs/concepts/): スナップショット、コマンド、イベント、ティック、バッチ —— なぜこう設計されているのか。 - [プロの戦術レシピ集](https://war3ai.com/ja/docs/cookbook/): 21 のレシピ:採掘の飽和、人口で詰まらない、集中攻撃、瀕死ユニットを下げる、夜のクリープ狩り…… --- # LLM で Bot を書く > プログラミングができなくても作れます。あなたはどう戦ってほしいかをはっきり伝え、コードは LLM が書きます。プロンプトテンプレートをコピーして戦い方を説明し、動かして、また直してもらいましょう。 Warcraft は遊べるけれどプログラミングはできない人にも、時間を節約したい開発者にも向いています。全体の流れは 1 つの対話です:**あなたが戦い方を説明する → モデルがコードを書く → あなたが 1 試合動かす → 見た現象をモデルに伝える → モデルが直す**。 > **ヒント** > > まず [クイックスタート](https://war3ai.com/ja/docs/quickstart/) に従って環境を整え、`hello_bot` を動かしておきましょう(農民が採掘に向かうのを確認します)。そうしておけば、問題が起きたときに環境の問題なのか Bot の問題なのかを切り分けられます。 ## 1. モデルに資料を用意する モデルが上手に書けるかどうかは、8 割がた正しい資料を読めているかで決まります。使っているツールに合わせて 1 つ選んでください。 | 使っているもの | 資料の渡し方 | |---|---| | **リポジトリを読める Coding Agent**(Claude Code、Cursor、Codex など) | リポジトリのディレクトリで起動し、まず `docs/BOT_HANDBOOK_ZH.md`、`docs/api.json` とサンプル 1 つ(運営なら `brains/examples/macro_bot.py`、戦闘なら `micro_bot.py`)を読ませます | | **Web にアクセスできるチャットモデル** | まず [`https://war3ai.com/llms-full.txt`](https://war3ai.com/ja/llms-full.txt) を読ませます。サイト全体のドキュメントがこの 1 ファイルにまとまっています | | **Web チャットでインターネットに接続できない** | ハンドブック、[`api.json`](https://war3ai.com/ja/api.json)、サンプルファイル 1 つをプロンプトの後ろに貼り付けます | | **ローカルモデル**(LM Studio、Ollama) | 同上。コンテキストウィンドウは 32K トークン以上を推奨します。そうでないとハンドブックと API カタログが収まりません | 特定のプロの戦術を入れたい場合は、[プロの戦術レシピ集](https://war3ai.com/ja/docs/cookbook/) から該当するレシピも貼り付けます。 ## 2. このプロンプトをコピーする 最後の「欲しい戦い方」をあなた自身の言葉に置き換えてください。具体的であるほど良くなります。 ```text あなたは Warcraft III 1.27 用の AI(Python)を書きます。使ってよいのは api.json に載っている Game のメソッドだけで、 存在しないメソッドをでっち上げないでください。書き方は rush_bot.py に倣い、openwar3.Bot を継承して on_start(g) と on_tick(g) を実装します。 ルール: - on_tick は毎秒およそ 5 回呼ばれるので、速く保つこと(中で sleep しない)。 - 読み取れない値は 0 ではなく None なので、使う前にチェックすること。 - コマンドはレシート(Receipt)を返す。`if r:` が「エンジンが受理した」という意味。受理されなかった場合は r.reason に理由が入っている (人口不足、ゴールド不足、ターゲットが見えない、そのヒーローはすでにいる……)ので、次のティックで再試行するか別のやり方に変えること。 - 特定の敵を攻撃するには g.attack(兵, 敵) を使う。敵は視界内にいる必要があり、見えない敵への命令は拒否される。 - ヒーローが死んだら g.revive(祭壇) で復活させる。もう一度訓練することはできない。 - 建物は g.build_near(ワーカー, 建物コード, x, y) で建てる。置ける場所を自分で探して結果を追跡し、ゴールドが足りないときは何もしない。 - 「今何が起きたか」(誰が死んだか、誰がダメージを受けたか、ヒーローのレベルアップ、アイテムのドロップ)を知りたければ on_event(g, ev) を実装する。 - 同じユニットに毎ティック同じコマンドを出し直さないこと(今やっていることが中断される)。命令は「手が空いている」ユニットに出す。 - 採集は idle_workers() のワーカーにだけ割り当てる。1 つの金鉱には最大 5 人まで。 - 訓練キューには 1 つだけ入れる(g.queue(建物) が空になってから次を入れる)。人口で詰まっているかは g.production(建物).blocked で確認する。 - 1 ティックで多くのコマンドを出すときは with g.batch(): で囲む(ゲームスレッドを待つのが 1 回で済む)。 - 誰を攻撃するかは g.time_to_kill(自軍の集団, 敵) で選ぶ(相性とアーマーを考慮済み)。どこへ行くかは g.path_distance で選ぶ(到達できなければ None)。 - フェアモードでは視界内のものしか見えない。以前見た敵は g.last_seen() で取得する。 - ユニットは 4 文字コードで表す(ヒューマンの Peasant は hpea、Footman は hfoo、Barracks は hbar……)。スキルはオーダー文字列で表す(thunderbolt = Storm Bolt、 blizzard = Blizzard、holybolt = Holy Light……、全一覧は data/order-ids.txt)。スキルの習得には 4 文字コードを使う(AHtb、AHbz……)。 欲しい戦い方: <ここに普段の言葉で書く。例: 「ヒューマン。序盤は農民 5 人でゴールドを掘り、1 人で伐採。最初のヒーローは Archmage。Barracks 2 つで Footman と Rifleman を出す。 兵が 12 体たまったらヒーローを連れて相手の拡張を攻める。ヒーローの HP が 30% を切ったら本拠地へ撤退。 クリープ狩りでは本拠地に近いキャンプを優先する。」> ``` ### 戦い方をはっきり伝えるには モデルが一番苦手なのはあいまいな要求です。「もっと攻撃的に」より、次のような情報のほうが役に立ちます。 - **種族とヒーロー**:どのヒーローを先に出すか、スキルの習得順(例:Archmage なら Water Elemental、Blizzard、Water Elemental……)。 - **建設順序**:何人目の農民で Barracks を建てるか、いつティアアップするか、Barracks をいくつ建てるか。 - **ユニット構成**:Footman + Rifleman? 何体そろったら出撃するか? - **攻撃と撤退の条件**:何体たまったら出撃するか、ヒーローの HP がどこまで下がったら撤退するか、壊滅したら本拠地に戻って立て直すか。 - **クリープ狩り**:するかしないか、いつするか(夜になってから?)、勝てるキャンプだけを狙うか? - **フェアかどうか**:将来アリーナに出るなら「視界内に見えている敵だけを使う」と伝えます。 ## 3. 動かす モデルが書いたコードを `brains/my_bot.py` として保存し、次を実行します。 ```bash python tools/play.py --bot brains/my_bot.py --race 1 --difficulty 2 ``` 早く結果を見たいなら `--speed 200`(2 倍速)を付けます。 ## 4. 直してもらう - **エラーが出た**:**エラー全文**をそのままモデルに貼り付け、「直して」と伝えます。 - **うまく戦えない**:推測した原因ではなく、**ゲーム内で何が見えたか**を説明します。たとえば「ヒーローがずっと本拠地に立ったまま動かない」「兵が 1 体ずつ突っ込んでいく」「農民が 1 つの鉱山に群がっている」など。 - **新しい戦術を追加したい**:一度に 1 つだけ追加し、1 試合動かして壊れていないことを確認してから次を追加します。 > **補足** > > 自分でコマンドを実行できる Coding Agent なら、ステップ 3 と 4 も任せられます:1 試合動かし、ログとレシートを読み、コードを直して、また動かします。十分な情報を Agent に見せる方法は [Agent による自律反復](https://war3ai.com/ja/docs/agent-loop/) を参照してください。 ## 5. よくある問題 | 現象 | たいていの原因 | |---|---| | 何も動かない | インスタンス番号が違う(`--inst`)、またはゲームがまだ試合に入っていない | | 農民が採掘しない | 作業中の農民に命令している。`idle_workers()` にだけ割り当てる | | いつまでも建物が建たない | `build_near` を使い、座標を決め打ちしない。レシートの `reason` がゴールド不足になっていないか確認する | | ヒーローが出てこない | `train` のレシートを確認:人口不足? それともヒーローが死んでいる(`revive` が必要)? | | ヒーローがスキルを使わない | 習得していない(`learn`)か、マナがない。使った後に `cooldown()` がクールダウンに入ったか確認する | | 兵がティックごとにガクガクする | 毎ティック命令を出し直している。手が空いているユニットにだけ命令する | | 兵が出てこず、ゴールドが増え続ける | 人口で詰まっている:`g.production(barracks).blocked` を確認する | | モデルが存在しないメソッドを使う | プロンプトで「api.json のメソッドだけを使う」ともう一度強調し、api.json を全部貼り付ける | ## さらに進むには - すべての API と、各 API の内部の仕組み:[API カタログ](https://war3ai.com/ja/api/)。 - リファレンスブレイン(`brains/xwar3/strategy`)は、拡張・クリープ狩り・出撃までこなす完全な AI です。モデルにその考え方を読ませることはできますが、より低レベルなインターフェースを使っているため、そのまま真似するのはおすすめしません。 - 将来 [アリーナ](https://war3ai.com/ja/arena/) に出るときは視界内の敵しか見えません —— 今のうちから `--fair` を付けて自分を縛っておけば、後で書き直す必要がありません。 --- # Agent による自律反復 > Coding Agent に自分で試合を回し、結果を読み、コードを直し、また回させます。そのために必要なのは、無人で実行できるコマンド、構造化された対局レポート、そして明確な目標です。 [LLM で Bot を書く](https://war3ai.com/ja/docs/ai-bot/) では、「1 試合動かす → 現象を見る → モデルに伝える」というステップをあなたが担当していました。コマンドを実行できる Coding Agent(Claude Code、Codex、Cursor の Agent モードなど)なら、このステップも引き受けてループを閉じることができます。 ```text コードを直す ──► 1 試合回す(無人)──► 対局レポートを読む ──► 結果に最も効く 1 か所を探す ──┐ ▲ │ └─────────────────────────────────────────────────────────────────────────────────────────┘ ``` このループを実際に収束させるには、Agent に 3 つのものが必要です。 ## 1. 無人で実行できるコマンド ```bash python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair ``` - `--minutes` で試合が必ず終わるようにします(実時間の分)。Agent が 1 試合に張り付いたままになりません。 - `--speed 200` で 2 倍速にして時間を節約します —— ただし Bot 内では**ゲームクロックで待ち**(`g.clock()`)、実時間での `sleep` は使わないでください。 - `--fair` で最初からアリーナのルールに沿って書かせます。視界内のものしか見えません。 - 実行が終わると、ターミナルに終了理由が表示されます。例:`我方没有单位了`(自軍のユニットがいなくなった)、`到时间了`(時間切れ)。Bot 自身が `print` した内容もターミナルに出ます。 > **注意** > > ウィンドウを最小化するとゲームのシミュレーションは止まります。Agent にはデフォルトのウィンドウモードでゲームを起動させ、あなたが使っているインスタンスと番号がぶつからないようにしてください(`--inst`)。 ## 2. 構造化された対局レポート ターミナル出力は人間向けです。Agent に見せるべきなのは、何が起きたか、何ができなかったか、それはなぜかをまとめた JSON です。SDK はすでに材料をそろえています —— レシートには理由コードが、イベントストリームには生産完了と損害が含まれています。それらを集めるだけです。 ```python title="recorder.py" import collections, json, time from openwar3 import Bot class Recorder(Bot): """Bot に対局レポートを追加する。これを継承し、自分の on_start / on_event の中で super() を呼ぶ。""" def on_start(self, g): self.rejects = collections.Counter() # "train hfoo: rejected(人口不够)" -> 回数(人口不够 = 人口不足) self.timeline = [] # [ゲーム秒, 種別, 4 文字コード]:訓練 / 研究 / 建設 / アップグレードの完了 self.lost = collections.Counter() # 自軍が失ったもの self.killed = collections.Counter() # 自軍が倒したもの def check(self, r, what): """コマンドを包んで拒否理由を記録する:self.check(g.train(b, "hfoo"), "train hfoo")""" if r is not None and not r: self.rejects[f"{what}: {r.reason}"] += 1 return r def on_event(self, g, ev): me = g.me() if ev.kind == "production.done" and ev.owner == me: self.timeline.append([round(ev.clock), ev.done_kind, ev.done_code]) elif ev.kind == "unit.died": (self.lost if ev.owner == me else self.killed)[ev.type] += 1 def on_end(self, g, reason): report = {"reason": reason, "timeline": self.timeline, "lost": self.lost, "killed": self.killed, "rejects": self.rejects.most_common(10)} try: # ゲームがすでに終了しているかもしれない。読めなければ諦める report |= {"clock": g.clock(), "resources": g.resources(), "army": len(g.my_army()), "workers": len(g.my_workers())} except Exception: pass with open(f"run_{int(time.time())}.json", "w", encoding="utf-8") as f: json.dump(report, f, ensure_ascii=False, indent=1) ``` このレポートで答えられる問い: | シグナル | 出どころ | 分かること | |---|---|---| | 最も多い拒否理由 | レシートの `reason` / `verdict` | 人口でずっと詰まっている(3)、資源が足りないのに命令し続けている(8 / 9)、霧の中のターゲットを攻撃しようとしている(1001)、ヒーローが死んでいるのに訓練しようとしている(221) | | 生産タイムライン | `production.done` イベント(かかったゲーム秒数付き) | 何秒で最初のヒーローが出たか、何秒でティアアップしたか、Barracks が兵を出し続けているか。プロの序盤と比較できる | | 双方の損害 | `unit.died` イベント | 兵を献上し続けていないか、ヒーローが何回死んだか、クリープ狩りで得をしたか | | 終了理由 | `on_end(g, reason)` | `我方没有单位了` = 負け。`到时间了` = まだ勝敗がついていない | | 最終的な兵力と資源 | `on_end` 時にスナップショットを 1 回読む | ゴールドを使わずに貯めている = 生産が追いついていない。ワーカーが少なすぎる = 経済が立ち上がっていない | > **補足** > > プログラムによる勝敗判定は [アリーナ](https://war3ai.com/ja/arena/) の基盤実験の 1 つで、まだロードマップ上にあります。現時点では `我方没有单位了` で負けを判定し、「見えている敵の建物が全滅した」ことで勝ちを近似的に判定できます。 ## 3. 明確な目標といくつかの制約 以下を Agent に渡し、目標に合わせて書き換えてください。 ```text 目標:brains/my_bot.py が Echo Isles で「Easy」難易度のコンピューター(ヒューマン対ランダム種族)に安定して勝てるようにする。 各ラウンド: 1. python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair を実行する 2. ターミナル出力と最新の run_*.json を読む:終了理由、生産タイムライン、最も多い拒否理由、双方の損害 3. 結果に最も影響している問題を「1 つ」見つけ、その 1 か所だけを直す。変更理由と根拠にしたデータをコードのコメントに書く 4. ステップ 1 に戻る。3 試合続けて改善がなければ止まり、レポートとあなたの判断を私に伝える 制約: - docs/api.json にあるメソッドだけを使い、API をでっち上げない - 同じユニットに毎ティック同じコマンドを出し直さない。命令は手が空いているユニットにだけ出す - --fair を維持する(視界内に見えている敵だけを使う) - コードを変更する前に python tools/run_tests.py を実行し、サンプルを壊していないことを確認する ``` ## ループを早く収束させるための習慣 - **一度に直すのは 1 か所だけ。** 3 か所同時に直すと、勝ってもどれが効いたのか、負けてもどれが原因なのか分かりません。 - **比較には十分な試合数を。** 同じ局面でもランダム性は大きく、2 試合で分かるのは大きな差だけです。「改善したか」の判断には、少なくとも数試合の傾向を見てください。 - **戦略の調整より先に「拒否」を直す。** レシートで最も多い拒否理由が、たいてい Bot の最大のバグです。 - **判断をコメントに書く。** 次のラウンドの Agent(あるいは次の会話)がコメントからなぜそう書いたかを知ることができ、直した箇所を元に戻さずに済みます。 - **オフラインテストで下支えする。** 重要なロジックには、ゲームを起動せずに動く単体テストを書きます(サンプル Bot のテストは `brains/examples/tests/` にあります)。Agent には変更のたびにまずそれを実行させます。 --- # LLM を戦略コーチにする > 「何を貯めるか、どこに人を回すか、この 1 分は攻めるか待つか」を LLM に任せ、ルール層は実行と拒否だけを担当します。リファレンスブレインはすでにこの方式で動いています。このページではそのパターンと落とし穴を説明します。 Bot を書き進めていくと、運営層のルールが 1 枚ずつ貼り足されていくことに気づきます。伐採人数のルール、1 鉱山 5 人のルール、木材が余ったら半分にするルール、ゴールドが多く木材が少なければ伐採に多めに回すルール……。どのルールも単独では正しいのに、組み合わさると「鉱山は人手不足なのに、農民が全員木を切っている」といった、**どのルールも責任を持たない**状況が生まれます。 このような「全体を見て優先順位をつける」判断は、もともと `if / else` で書くのに向いていませんが、LLM はまさにこれが得意です。リファレンスブレイン(`brains/xwar3/strategy/brain/coach.py`)は、以下の階層構成を採用しています。 ## 階層構成 ```text LLM(アドバイザー) 20 ゲーム秒に 1 回、非同期。どのティックもブロックしない 入力:局面スナップショット 1 ページ分(資源、人口、農民の配置、鉱山、兵種、技術、ヒーロー、敵情報、直近の出来事) 出力:厳密な JSON —— 一言の診断 + ワーカー配分 + 優先して作るもの + この 1 分の方針 + やってはいけないこと │ ▼ ホワイトリスト + 上下限クランプ + 拒否 ルール層(Bot、毎ティック) 助言を既存機能への「バイアス」に変換:ワーカー配分、建設 / 訓練の優先度、攻撃方針 │ ▼ 実行層(SDK / リフレックス層) 命令を出す、レシートを読む、マイクロ操作 ``` ## 出力契約 モデルには固定フィールドの JSON だけを出力させ、フィールドの追加・削除は許しません。 ```json { "diagnosis": "一文で:局面で最大の問題。入力データの中に根拠がなければならない", "workers": { "gold": 10, "lumber": 6 }, "priority": ["hpea", "hhou", "hbar"], "posture": "creep", "avoid": ["木材が足りないうちに Iron Plating を先に研究しない"] } ``` | フィールド | ルール層での使い方 | リファレンスブレインのクランプ | |---|---|---| | `workers` | ゴールド採掘・伐採の目標人数 | 採掘 2 ~ 25、伐採 1 ~ 20。合計は農民の総数を超えない | | `priority` | 訓練 / 建設 / 研究の優先順 | 最大 4 個。「選択可能コード」表に出てくる 4 文字コードのみ受け付ける | | `posture` | この 1 分の方針 | `attack` `defend` `creep` `expand` `recover` `hold` のいずれか | | `avoid` | この 1 分にやってはいけないこと | 最大 2 件 | | `diagnosis` | ログとコンソール表示にのみ使用 | — | プロンプトは種族ごとに 1 つずつ用意し、その種族特有のトレードオフだけを書きます(ヒューマンの共同建設と Militia、オークの Burrow、アンデッドの Haunted Gold Mine、ナイトエルフの Entangled Gold Mine……)。共通ルールは共通部分にまとめ、4 回コピーしないでください。 ## 4 つの厳格な制約 この 4 つは、いずれもリファレンスブレインが実際に痛い目を見て学んだものです。 1. **アドバイザーは決してユニットに直接命令しません。** 150 ms 単位の現場は見えませんし、幻覚も起こします。変えるのは目標と優先度だけで、具体的に誰がどこへ行き、誰を攻撃するかは引き続きルール層とリフレックス層が決めます —— 指揮権の持ち主は 1 人だけです。 2. **非同期。** アドバイザーへの問い合わせは 1 回約 1 秒かかります。バックグラウンドスレッドで実行して最新の結果を採用し、**どのティックもブロックしません**。モデルが起動していない、タイムアウトした、でたらめな回答を返した、という場合はこの層がないものとして扱い、純粋なルールの挙動に戻ります。古すぎる助言(3 間隔を超えたもの)も使いません。 3. **ホワイトリスト + クランプ。** 各フィールドは既存の機能に対応付けられなければならず、数値は妥当な範囲にクランプします。認識できない内容は、黙って無視するのではなく**カウントしてから破棄**します。 4. **すべてをカウントする。** 何回問い合わせ、何回成功し、何回タイムアウトし、何回クランプされ、各フィールドが何回採用されたかを、最後にモデルへ送った入力と合わせて公開します。そうしないと「この層は本当に役に立っているのか」という問いに答えられません。 > **安全に劣化できるものほど、気づかないうちに劣化する** > > アドバイザーは「失敗 = この層がない」ように設計されているため、モデルサービスが起動していないと、Bot の挙動は純粋なルールとまったく同じになり、外からはまったく見分けがつきません。リファレンスブレインでは、6 インスタンスすべてのアドバイザーが丸一日接続できていなかったのに、誰も気づかなかったことがあります。「最後に成功した時刻」と「最後に失敗した理由」は必ず公開してください —— [Farsight コンソール](https://war3ai.com/ja/docs/console/) の「戦略コーチ」ページはまさにそのためのものです。 ## 自分の Bot に実装する 以下は最小限のスケルトンです。OpenAI 互換の API であれば何でも使え(LM Studio、Ollama、クラウド API など)、標準ライブラリだけで動きます。 ```python title="coached_bot.py" import collections, json, threading, urllib.request from openwar3 import Bot BASE = "http://127.0.0.1:1234/v1" # LM Studio / Ollama / 任意の OpenAI 互換サービス MODEL = "your-model" POSTURES = {"attack", "defend", "creep", "expand", "recover", "hold"} SYSTEM = """あなたは Warcraft III の運営コーチです。担当は運営と戦略だけで、マイクロ操作は扱いません。 JSON だけを出力し、フィールドは固定です:{"diagnosis": 一文, "workers": {"gold": 整数, "lumber": 整数}, "priority": [4 文字コード, 最大 4 個, allowed に含まれるもののみ], "posture": 6 つから 1 つ, "avoid": [最大 2 件]} 与えられた局面データだけに基づいて答え、データにないことは作らないでください。""" def ask(state: dict) -> dict: body = {"model": MODEL, "temperature": 0.3, "max_tokens": 260, "messages": [{"role": "system", "content": SYSTEM}, {"role": "user", "content": json.dumps(state, ensure_ascii=False)}]} req = urllib.request.Request(f"{BASE}/chat/completions", json.dumps(body).encode(), {"Content-Type": "application/json"}) with urllib.request.urlopen(req, timeout=8) as r: text = json.load(r)["choices"][0]["message"]["content"] return json.loads(text[text.index("{"): text.rindex("}") + 1]) class CoachedBot(Bot): EVERY = 20.0 # ゲーム秒:運営判断の時間スケールは分単位なので、毎ティック聞く必要はない allowed = {"hpea", "hfoo", "hrif", "hkni", "hhou", "hbar", "hbla", "Rhme", "Rhar"} def on_start(self, g): self.plan, self.plan_at, self.asked_at, self.busy = {}, -1e9, -1e9, False self.stats = collections.Counter() def summary(self, g) -> dict: # スナップショットはメインスレッドで読んでおき、バックグラウンドスレッドは g に触らない res = g.resources() or {} return {"clock": round(g.clock() or 0), "gold": res.get("gold"), "lumber": res.get("lumber"), "food": [res.get("food_used"), res.get("food_cap")], "workers": len(g.my_workers()), "idle_workers": len(g.idle_workers()), "army": collections.Counter(u.type for u in g.my_army()), "enemy_seen": collections.Counter(u.type for u, _t, _age in g.last_seen(max_age=90)), "night": g.is_night(), "allowed": sorted(self.allowed)} def consult(self, state, now): try: p = ask(state) self.stats["ok"] += 1 posture = p.get("posture") if posture not in POSTURES: self.stats["bad_posture"] += 1 # カウントしてから破棄。黙って捨てない posture = "hold" self.plan = {"gold": min(25, max(2, int(p["workers"]["gold"]))), # クランプ "lumber": min(20, max(1, int(p["workers"]["lumber"]))), "priority": [c for c in p.get("priority", []) if c in self.allowed][:4], "posture": posture} self.plan_at = now except Exception as e: # タイムアウト / でたらめな回答:この層はないものとして扱う self.stats[f"error:{type(e).__name__}"] += 1 finally: self.busy = False def on_tick(self, g): now = g.clock() or 0.0 if not self.busy and now - self.asked_at >= self.EVERY: self.busy, self.asked_at = True, now threading.Thread(target=self.consult, args=(self.summary(g), now), daemon=True).start() plan = self.plan if now - self.plan_at <= 3 * self.EVERY else {} # 古すぎる助言は使わない # ↓ ルール層:plan が空ならデフォルトのルールで動く。plan があれば配分・優先度・方針だけを調整し、具体的な命令は引き続きルールが決める ... ``` ## モデルの選び方 | 状況 | おすすめ | |---|---| | ローカルで速さ重視 | MoE モデル(1 回にごく一部のパラメータしか使わない)は、同サイズの密なモデルよりずっと速く動きます。リファレンスブレインは Qwen3.6-35B-A3B(LM Studio、Q4)を使っており、中央値 **1.09 秒**、最遅 1.45 秒、5/5 の出力がそのまま `json.loads` できます | | ローカルの「思考」するモデル | **思考部分を必ずオフにしてください**。そうしないとトークンがすべて思考に使われ、JSON が 1 つも出てきません。LM Studio は `/no_think` を無視するため、リファレンスブレインは `/v1/completions` に切り替え、ChatML を自前で組み立てて、空の `` と `{` を 1 つあらかじめ埋めています | | クラウドモデル | 通常はレイテンシが高めですが、この階層構成はもともと非同期です。運営判断は分単位なので、数秒の遅延は許容できます | > **補足** > > 同じモデルでユニットに声を当てることもできます。[頭上の吹き出しとローカルモデル](https://war3ai.com/ja/docs/speech/) を参照してください。モデルに毎ティック直接命令させたい(参謀としてではなく)場合は、[アリーナ](https://war3ai.com/ja/arena/) の JSON ゲートウェイを待ってください。 --- # LLM がツールを直接呼び出す(MCP) > tools/war3_mcp.py は MCP サーバーです。Claude Code、Claude Desktop、または MCP に対応した任意のクライアントに登録すれば、LLM がコードを書かずに、局面を見る、コマンドを出す、画面上でプレイヤーに話しかける、カードを出してプレイヤーに尋ねる、スクリーンショットで画面を見る、といったことを直接行えます。 `tools/war3_mcp.py` は **MCP サーバー**(stdio)です。Claude Code、Claude Desktop、ローカルモデル用の Agent フレームワーク —— MCP に対応した任意のクライアントに登録すれば、LLM がコードを書かずに**直接**、局面を見る、コマンドを出す、ゲーム画面上でプレイヤーに話しかける、プレイヤーに尋ねる、スクリーンショットで画面を見る、といったことができます。 Bot を書く、アドバイザーを務める、ユニットにしゃべらせる、に続くもう 1 つの接続方法です:**LLM 自身がツールの使い手になります**。 ## 登録する ```bash claude mcp add war3 -- python <リポジトリ>\tools\war3_mcp.py --inst 9 # Claude Code。<リポジトリ> は自分の openwar3 フォルダに置き換える ``` 他のクライアントでは、次の形式で設定を書きます。 ```json {"mcpServers": {"war3": {"command": "python", "args": ["<リポジトリ>\\tools\\war3_mcp.py", "--inst", "9"]}}} ``` ゲームに接続するのは最初にツールが呼ばれたときなので、ゲームは後から起動しても構いません。ゲームを閉じて起動し直した場合も、次の呼び出しで自動的に再接続します。`--role` を付けると、LLM にできることを制限できます。 | ロール | 使えるもの | |---|---| | `dev`(デフォルト) | すべてのツール。`war3_jass` を含む | | `player --player N` | N 番プレイヤーのユニットだけを指揮でき、その視界内のものだけが見えます(フェアモード)。JASS はなし | | `observer` | 読み取り専用。画面への描画も、ユニットにしゃべらせることもできない。下したコマンドはランタイムがそのまま拒否 | `player` ロールの制限は[ゲートウェイ](https://war3ai.com/ja/docs/gateway/)と同じです:ゲームの終了、速度変更、一時停止は使えず、他プレイヤーの手の内が見える API も使えません。プレイヤー番号を取るクエリは自分の分しか照会できません。 いくつかの上限:1 つのツール結果は最大 20 万文字で、超えた分は切り詰められ、範囲の絞り方が示されます。`war3_ask_player` の待ち時間は最大 120 秒です。スクリーンショットの `scale` は 0.1 から 1 の間です。 ## ツール | ツール | 内容 | |---|---| | `war3_overview` | 局面の要約:時刻、資源、人口、自軍の兵種ごとの数、ヒーロー(HP、マナ、レベル、クールダウン)、見えている敵の兵種、生産。**最初にこれを呼ぶ** | | `war3_units` | ユニット一覧(`owner` は me / enemy / creep / all、`types` で絞り込み)。コマンドを出すときは `addr` を使う | | `war3_events` | 前回の呼び出し以降に起きたこと:死亡、レベルアップ、スキル使用、生産完了、チャット、プレイヤーがボタンをクリック……(デフォルトでは大量に流れる数種類を除外) | | `war3_call` | 任意の公開 API を呼ぶ(`move`、`attack_move`、`train`、`build`、`cast`、`learn`、`ui.button`、`canvas.text`……)。ユニットは `{"unit": addr}` で指定 | | `war3_api` | API を調べる:キーワードで名前と説明を検索 | | `war3_toast` / `war3_say` | 画面上部に 1 行のテキスト / ユニットの頭上にひと言 | | `war3_ask_player` | 画面中央にプレイヤー向けの選択カードを数枚出し、クリックを待って、どれが選ばれたかを返す(ゲームを一時停止できる) | | `war3_screenshot` | ゲーム画面のスクリーンショット(PNG。ウィンドウが隠れていても撮れて、フォーカスも奪わない) | | `war3_jass` | JASS を実行(dev 専用。ワールドを変更できるのはシングルプレイのみ) | こんな使い方ができます: - **一緒に遊ぶ / コーチ**:`war3_overview` で局面を見て、`war3_toast` で画面にアドバイスを出す。 - **プレイしながらプレイヤーに尋ねる**:`war3_ask_player` でカードを 3 枚出し、プレイヤーが選んだとおりに進める。 - **実況**:`war3_events` で何が起きたかを読み、`war3_say` でユニット自身にしゃべらせる。 - **1 つの部隊を直接指揮する**:`player` ロール + `war3_call` で、自分のユニットだけを動かす。 - **画面を見て UI を調整する**:`war3_screenshot` で 1 枚撮り、自分で描いたボタンの配置が正しいかを確認する。 ## 会話はたとえばこんな感じ ```text あなた:今の局面を見て、それから画面で聞いて:次は拡張、兵の量産、ティアアップのどれにする? → war3_overview {} ← 局面の要約:ゲーム時間、ゴールド 500、人口 10/12、自軍 htow 1 · hpea 5 · Hpal 1、敵は見えず、生産中のものなし → war3_ask_player {"question": "次はどうする?", "options": ["兵の量産", "拡張", "ティアアップ"], "pause": true} ← {"picked": 1, "option": "拡張"} モデル:拡張を選びましたね。まず war3_units で手の空いている農民を探し、最寄りの金鉱がどこかを確認します…… ``` ## 実測 2026-09-25: - 自作の MCP クライアントを実際の試合につないで 7/7:ハンドシェイク → ツール一覧(10 個)→ `war3_overview`(`htow` 1、`hpea` 5)→ `war3_units` → `war3_toast` → `war3_screenshot`(PNG 約 20 万バイト)→ `war3_ask_player`(カード 3 枚。2 枚目のクリックをシミュレート → `{"picked": 1, "option": "拡張"}`)。 - Claude Code 2.1 に実際に登録:Claude Code が自分でサーバーを起動してハンドシェイクし、ステータスは `connected`。10 個のツールがすべて `mcp__war3__*` としてツール一覧に現れました。 ## 実装 - 改行区切りの JSON-RPC 2.0(`initialize` / `tools/list` / `tools/call` / `ping`)。プロトコルバージョンは 2025-06-18 で、2025-03-26 と 2024-11-05 にも対応します。 - ツールのエラーは MCP の規約どおり結果の中に入れ(`isError: true`)、接続は切りません。 - [ゲートウェイ](https://war3ai.com/ja/docs/gateway/) と、同じロールのホワイトリスト、ユニット引数の形式、「局面の要約」を共有しています。 - ログは stderr に出し、stdout にはプロトコルだけを流します。 --- # メンタルモデル > スナップショット、コマンド、レシート、イベント、ティック、バッチ。この 6 つの概念を理解すれば、API がなぜこの形なのか、どう書けば速くなるのかが分かります。 ## スナップショット:読み取り、待ち時間ゼロ ランタイムは **50 ms** ごとにゲームスレッド上でワールド全体を収集し、共有メモリに書き込みます。`g.snapshot()` で得られるのは、**完全で一貫した**ワールドです。 - 16 のプレイヤースロット:ゴールド、木材、人口、上限、累計採集量、種族。 - 最大 1024 ユニット:型、所有者、座標、HP / マナ(上限を含む)、現在のオーダーとオーダーのターゲット、**実際に攻撃している相手**(タスクターゲット)、ヒーローのレベル / 経験値 / スキルポイント、各プレイヤーからの可視性。 - 最大 256 件のユニット詳細:12 個のアビリティ(レベル、残りクールダウン)、8 個の buff、6 スロットのインベントリ。 - 地面のアイテム、木(2 秒ごとに更新)、生産テーブル(訓練 / 研究 / 建設 / アップグレードの進捗)、ゲームクロック、ゲーム内時刻。 1 回の読み取りは約 **0.4 ms**(Python でのパース)で、ゲームスレッドを待ちません。だから、**好きなだけ読んでください**。`g.units()`、`g.my_army()`、`g.cooldown()`、`g.inventory()` などの API はすべて同じスナップショットから取り出すので、1 ティックで何回呼んでもほとんどコストはかかりません。 > **ヒント** > > 発行周期は `g.set_publish_period(ms)` で 16 ~ 1000 ミリ秒の範囲で調整できます。1 回の収集はゲームスレッド上で約 0.5 ~ 0.9 ms なので、33 ms でも問題ありません。値はマシン全体で共有され、最後に書き込んだものが有効になります。 ## コマンド:書き込み、約 1 フレーム `g.move / attack / gather / build / train / cast …` はゲームスレッドに渡されて実行されます。ランタイムはゲームスレッドの**イベントディスパッチ**の中で、クライアントが送信したコマンドをまとめて実行します。そのため、コマンド 1 件の待ち時間は約 **1 フレーム**です(イベントがまとまって処理されるタイミングに当たれば約 0.1 ms、そうでなければ次のディスパッチまで待ちます)。 - コマンドには**ユニット 1 体またはリスト**を渡せます。リスト内のユニットには同じフレームでまとめて命令が出ます。 - `queue='after'` を付けると Shift キューになり、今の作業を終えてから実行します。 - コマンドのユニットにはスナップショットから取得したオブジェクトをそのまま使えます。SDK は**ハンドルペア**で同一性を確認します(アドレスは新しいユニットに再利用されますが、ハンドルは再利用されません)。 ## レシート:すべてのコマンドに付く ```python r = g.build(worker, "hbar", x, y) if r: # エンジンが受理した ... else: r.reason # 'rejected(金不够)'(= ゴールド不足) r.verdict # 8 r.exec_us # このコマンドがゲームスレッド上で実行に要したマイクロ秒数 ``` レシートは**同じフレーム**の中で読み戻されます:命令前後のユニットのオーダー、エンジン関数の戻り値、実行可否チェックの理由コード。レシートが答えるのは「エンジンがこのコマンドを受理したか、しなかったならなぜか」であり、「最終的に成功したか」には**答えません** —— 成功したかどうかはスナップショットとイベントで確認します。 すべてのステータスコードと理由コードは [レシートと理由コード](https://war3ai.com/ja/docs/reason-codes/) を参照してください。 ## イベント:何が起きたか `on_event(g, ev)` は毎ティックの `on_tick` の前に、前回のティック以降のイベントを 1 件ずつ渡します。 | イベント | 意味 | |---|---| | `unit.appeared` / `unit.died` / `unit.removed` | ユニットの出現、死亡、消滅(金鉱に入る、変換される、死体が朽ちるのも消滅に含まれ、死亡と同じではありません) | | `unit.damaged` / `order.changed` / `owner.changed` | HP の減少、オーダーの変更、所有者の変更 | | `hero.levelup` | ヒーローのレベルアップ | | `item.appeared` / `item.removed` | 地面のアイテムの出現、拾われた・使われた | | `damage` | エンジンレベル:**1 発ごと**のダメージ。ダメージ元のユニット、攻撃タイプ、ダメージタイプ、実際に減った HP、アーマー適用前のダメージ | | `killed` | エンジンレベル:この 1 発でとどめを刺した。倒したユニット付き | | `production.done` | 訓練 / 研究 / 建設 / アップグレードの完了。4 文字コードとかかったゲーム秒数付き。相手のものも含まれる | | `spell.cast` | ユニットがスキルを使った:スキルの 4 文字コード、レベル、クールダウン秒数、詠唱地点 | | `message` | 画面のメッセージ枠に 1 件表示された:ゲームのヒント(「Farm がもっと必要です」)、チャット(`.chat` に発言者と内容)、システムメッセージ | | `selection.changed` / `player.left` | ローカルプレイヤーの選択が変わった / プレイヤーが退出した、または敗北判定で除外された | | `game.started` / `game.ended` | 新しい試合の開始 / 試合から退出 | キャンバスのボタンのクリック、ホットキー、地面のクリックといった入力イベントについては [UI と入力](https://war3ai.com/ja/docs/ui-input/) を参照してください。 > **注意** > > イベントストリームは**グローバル**です。相手の生産完了やクリープの死亡も含まれます。`ev.owner` やユニットのハンドルでフィルタリングしてください。 ## ティック:Bot のリズム `on_tick` はデフォルトで毎秒 5 回(実時間)呼ばれます。1 ティックの所要時間はほぼあなた自身の計算時間だけです:スナップショットは待ち時間ゼロ、コマンドは約 1 フレーム。1 ティックが周期を超えると自動的に後ろにずれ、どんどん溜まっていくことはありません。 - **2 倍速では実時間で待たないでください。** 3 ゲーム秒待ちたいなら、`g.clock()` が 3 進んだかを見ます。`sleep(1.5)` は使いません。 - **`on_tick` の中で `sleep` しないでください。** 「少し後でやる」必要があるなら、現在のゲーム時刻を記録しておき、次のティックで判定します。 ## バッチ:数十件のコマンドでも待つのは 1 回 1 ティックで多くのコマンドを出すときは、`with g.batch():` で囲みます。 ```python with g.batch(): g.attack(melee, target_a) g.attack(ranged, target_b) g.move(wounded, home.x, home.y) g.cast(hero, "thunderclap") # ブロック終了時にバッチ全体を送信:同じフレームで実行され、ゲームスレッドを待つのは 1 回だけ ``` - ブロック内のコマンドは `Pending` を返し、ブロック終了後にレシートになります。ブロック終了前に読むと例外が発生します。 - ブロック内で例外が発生すると、**バッチ全体が破棄されます**(中途半端なコマンドは、送らないより危険です)。 - 実測(移動 8 件):1 件ずつなら 68 ~ 99 ms、バッチなら **6.5 ~ 10 ms**。 同じ考え方はクエリにも使えます:`g.can_do_many([(u, code), ...])`、`g.tech_many([...])` で一度にまとめて問い合わせます。 ## 自分が書いたばかりのものを読む 同じティック内では、スナップショットにはまだ直前に出したコマンドが反映されていません(次の発行で追いつきます)。そのため、2 つのロジックが同じワーカーを取り合うことがあります。片方が Farm を建てに行かせたばかりなのに、もう片方はスナップショットを見て、まだ手が空いていると判断してしまうのです。 `g.order_of(u)` がこの問題を解決します。スナップショットが追いつくまでは、レシートにある新しいオーダーを優先します。**「手が空いているか」の判定には `u.order` ではなく `g.order_of(u)` を使ってください。** `g.idle_workers()` は「このティックで仕事を割り当てられたばかり」のワーカーをすでに除外しています。 ## レイテンシの段階 | 段階 | 経路 | レイテンシ | 用途 | |---|---|---|---| | 0 | プッシュスナップショット + イベントストリーム | 1 回の読み取り約 0.4 ms。データは 50 ms ごとに更新 | すべての「見る」API | | 1 | ファストレーン | 約 1 フレーム。6 プロセス同時実行時の中央値 0.06 ms | すべてのコマンドとクエリ(SDK のデフォルト) | | 2 | コントロールチャネル | 20 ~ 40 ms | フォールバック、一部の UI 系操作(ゲーム速度、吹き出し、メッセージ) | | 3 | [ゲートウェイ](https://war3ai.com/ja/docs/gateway/)(WebSocket / JSON) | 段階 1 + 約 1 ms | 任意の言語、ブラウザ、LLM、別のマシン上のプログラム | [API カタログ](https://war3ai.com/ja/api/) では、各 API がどの段階を使うかを示しています。 --- # 15 のルール > どれも実際の対戦で痛い目を見て得たものです。Bot を書くときに照らし合わせれば、原因調査の手間の大半を省けます。 > **ヒント** > > このページを [`api.json`](https://war3ai.com/ja/api.json) と一緒に LLM に渡すと、書かれる Bot の回り道がずっと少なくなります。 ## 状態を読む ### 1. 読めないときは `None`、0 ではない `resources()`、`time_of_day()`、`production()`、`cooldown()` はいずれも `None` を返すことがあります(ロード中、ユニットに詳細がない、建物が生産していない……)。使う前に判定してください。 ```python res = g.resources() if res is None: return ``` ### 2. ユニットはハンドルで識別し、アドレスは使わない アドレスは新しいユニットに再利用されます。古いアドレスが新しく生まれたユニットを指すこともあります。ティックをまたいであるユニットを覚えておくには、`u.handle` を保存し、`g.unit(handle)` で取り戻します。 ### 3. イベントストリームはグローバル `production.done` や `unit.died` には、対戦相手やクリープのものも含まれます。`ev.owner`(または建物のハンドル)でフィルタリングしてください。 ```python if ev.kind == "production.done" and ev.owner == g.me(): ... ``` ### 4. 金鉱に入ったワーカーはスナップショットにいない ワーカーは金鉱に入った瞬間にスナップショットから消えます(`unit.removed` で、死亡ではありません)。鉱山ごとの人数を数えるなら**自分で記録**し、スナップショットに合わせて記録を減らさないでください。さもないと満員の鉱山にさらに人を送ってしまいます。 ## コマンドを出す ### 5. レシートの「受理」≠ 成功 森の中の建設地点でもエンジンはその場で受理し、ワーカーが到着してから失敗します。スキルは中断されることもあります。効果はスナップショットとイベントで確認します。建物の建設には `build_near`(建設が始まったかを追跡します)を使い、スキルは `g.cooldown()` がクールダウンに入ったかで確認します。 ### 6. 見えない目標は攻撃できない 霧の中の敵に目標指定のコマンドを出すと拒否され、理由コードは **1001** です。霧の中の敵を追うには、最後に見えた位置に `attack_move` します。 ### 7. 命令は暇なユニットにだけ出す 毎ティック同じユニットに同じコマンドを出し直すと、動作が中断されます。兵はその場で痙攣したようになり、ワーカーの採集サイクルはリセットされます。「暇かどうか」の判定には `g.order_of(u)`(このティックで出したばかりの命令も含む)を使い、スナップショットの `u.order` は使わないでください(スナップショットはまだ追いついていません)。 ### 8. Shift は「現在の命令の直後に挿入」しかない エンジンには「末尾に追加」がありません。`queue='after'` で B、C を続けて送ると、A、C、B の順になります。複数の地点を順番に巡るには `g.path(units, 地点リスト)`、1 体のワーカーで複数の建物を連続建設するには `g.build_queue(worker, 計画)` を使ってください。これらは逆順に挿入して、順序をうまく処理してくれます。 ### 9. 1 ティックのコマンドは 1 バッチで送る 数十のコマンドを 1 件ずつ送ると、ゲームスレッドを数十回待つことになります。`with g.batch():` で包めば待つのは 1 回だけです。 ## 経済と生産 ### 10. 1 鉱山のワーカーは最大 5 人 それ以上増やしても収入は増えません。ワーカーの目標数は鉱山の数に合わせます。各鉱山に採金 5 人、加えて伐採を数人。 ### 11. 訓練キューは 1 体だけ 7 枠を埋めると資金がキューにロックされます(実測では、タウンホールにピーザント 4 体をキューに入れて 300 ゴールドがロックされ、序盤が大きく遅れました)。`g.queue(b)` が空になってから次を追加してください。 ### 12. 人口の詰まりは生産表で確認 `g.production(b).blocked` = キューに入っているのに始まっていない状態で、ほとんどの場合は人口不足です。「人口が上限に近づいたら建てる」より一歩早く気づけます。戦闘で兵をまとめて失い、補充しようとしてキューが詰まった時点でわかります。 ### 13. ヒーローは唯一、タウンホールのキューが空でないとティアアップできない - ヒーローが死んだら `g.revive(祭壇)` しかありません。再び訓練しようとすると拒否されます(221)。蘇生にも人口が必要です(ヒーローは 5)。 - タウンホールのキューに何か残っているときは、タウンホールをアップグレードできません(理由コード 185、「建物が使用中」)。 ## 時間と空間 ### 14. 2 倍速では実時間で待たない 3 ゲーム秒待ちたいなら、`g.clock()` が 3 進んだかを見ます。`sleep(1.5)` ではありません。倍速ではエンジンの時計が実時間より速く進みます。 ### 15. 島マップや森の多いマップでは直線距離を使わない クリープキャンプや拡張地点の選択には `g.path_distance(a, b)`(地上 A*。森、崖、建物を迂回)を使います。到達できない場合は `None` を返します。直線で最も近い地点は、海の向こう側かもしれません。 ## もう 1 つ:フェアモードで書く `--fair` では視界内のユニット、アイテム、生産、イベントしか見えません。アリーナのルールもこれと同じです。今からフェアモードで書いておけば、将来 [アリーナ](https://war3ai.com/ja/arena/) に出るときに書き換える必要はありません。詳しくは [フェアモード](https://war3ai.com/ja/docs/fair-mode/) を参照してください。 --- # フェアモード > ゲームに注入したクライアントはマップ全体を読めます。フェアモードでは、Bot に見えるのは視界内のものだけになります。人間のプレイヤーと同じで、アリーナのルールとも一致します。 このプロジェクトの観測能力は「クライアントが全プレイヤーの状態を保持している」ことに由来します。スナップショットには、霧の中の敵を含め、マップ全体のすべてのユニットが入っています。デバッグには便利ですが、対戦では公平ではありません。 **フェアモード**では、SDK があなたの視界に基づいてフィルタリングします。 ```bash python tools/play.py --bot my_bot.py --fair python -m openwar3 run my_bot.py --inst 5 --fair ``` ```python from openwar3 import Game, run g = Game(inst=5, fair=True) # Game を直接使う run(MyBot, inst=5, fair=True) # またはランナーに任せる ``` ## フィルタリングされるもの | 内容 | フェアモードでは | |---|---| | ユニット | 自軍のすべて + 現在自軍から見えている敵と中立ユニット | | 地面のアイテム | 自軍ユニットの視界内のものだけ(昼 / 夜の視界はデータ表に従ってそれぞれ計算) | | 生産表 | 見えている建物のものだけ(相手が何を訓練しているかは見えない) | | イベント | 自分のもの。見えている(または 1 秒以内に見えていた)もの。自軍が与えたダメージ | ## 視界の出どころ - スナップショットの各ユニットには**可視性マスク**があります。第 p ビット = プレイヤー p が今そのユニットを見ている(フィールドにユニットがいる 0 〜 11 番のみ対象。自分のユニットは自分から常に見える)。`u.visible_to(g.me())` はこれを直接読むので、待ち時間はゼロです。 - 任意の地点:`g.visible(x, y)` でエンジンに問い合わせます(見える / 霧 / ブラックマスク)。ファストレーン経由で、1 回あたり約 1 フレームです。1 ティックで多くのユニットを判定するときは、`g.visible()` を 1 件ずつ呼ばずに、スナップショットの `u.visible_to()` を使ってください。 ## 敵の記憶:`last_seen` 人間のプレイヤーは「さっきあそこでレイダーの一団を見た」と覚えています。SDK も代わりに記憶します。スナップショットを更新するたびに、その時点で自軍から見えている敵とクリープのユニットを記録します(最後の位置、HP、時刻)。死ぬのを見たら削除し、ゲームが変わればクリアします。 ```python for u, t, age in g.last_seen(max_age=60): # 60 ゲーム秒以内に見た敵 print(u.type, u.x, u.y, f"{age:.0f}s 前") heroes = [r for r in g.last_seen() if r[0].is_hero] # 相手ヒーローを最後に見た場所 camps = g.last_seen(owner="creep") # 見たことのあるクリープ ``` フェアモードでは、これが唯一の「相手の情報源」です。人間のプレイヤーと同じです。通常モードでも視界に基づいて記録するので、同じコードをそのまま使えます。 ## 何番プレイヤーとして指揮するか ```bash python tools/play.py --bot my_bot.py --player 1 --attach ``` `--player N`(または `Game(player=N)`)を指定すると、Bot は N 番プレイヤーとして指揮し、N 番プレイヤーのユニットだけを操作できます。2 つの AI を対戦させるには、同じゲームでこのチャネルを 2 本開きます。 > **ローカルモードでの公平性は取り決めであり、セキュリティ境界ではありません** > > 自分のマシン上では、プログラムがマップ全体を読むのを防ぐ手段はありません。`--fair` はあなた自身への制約です。本当の対戦での公平性は [アリーナ](https://war3ai.com/ja/arena/) のレフェリープロセスが保証します。Bot は共有メモリに一切触れられず、レフェリーが視界でフィルタリングした観測だけを受け取り、アクションを提出することしかできません。各アクションは先にユニットの所有権がチェックされます。 ## 今すぐ有効にすべき理由 - 将来アリーナに出るときのルールはこれです。今からフェアモードで書いておけば、そのときに 1 行も変更する必要はありません。 - マップ全体の情報を手放して初めて、Bot の本当の実力がわかります(リファレンスブレインは現在、コンピューターの隊長の目標地点など、マップ全体の情報に大きく依存しています。これはちょうどよい試金石になります)。 - フェアモードで書いた偵察、記憶、判断こそが、本当に価値のある AI の能力です。 --- # プロの戦術レシピ集 > 上級者の強さの大半は、数十個の「小さな習慣」から生まれます。このページでは、よくあるプロの立ち回りを 1 つずつ SDK のコードに落とし込みます。どれもそのまま on_tick に貼り付けられます。 前提:`g` は `Game`、`home` は自軍の本拠地(`g.my_buildings({"htow", "hkee", "hcas"})[0]`)、`now = g.clock()` とします。API の詳細は [API カタログ](https://war3ai.com/ja/api/)、そのまま動く完全な例は [サンプル Bot](https://war3ai.com/ja/docs/examples/) にあります。 > **ヒント** > > LLM にある戦術を追加させるときは、「もっとプロっぽく戦って」と説明するより、該当するレシピをコードごと貼り付けるほうがずっと効果的です。 ## 一、運営 ### 1. 農民を遊ばせない、1 鉱山 5 人 ```python for w in g.idle_workers(): # 手の空いたワーカーだけに指示(作業中のワーカーに出し直すと採集が中断される) mine = g.nearest([m for m in g.gold_mines() if crew[m.addr] < 5], w) g.gather(w, mine) if mine else g.gather(w, g.trees(w.x, w.y, limit=1)[0]) ``` 各鉱山に何人送ったか(`crew`)は自分で記録します。金鉱に入っているワーカーはスナップショットに載らないためです。完全な例は `hello_bot.py` を参照してください。 ### 2. キューは 1 つだけ、ゴールドを寝かせない ```python for b in g.my_buildings({"hbar"}): if not g.queue(b): # 空になってから次を入れる g.train(b, "hfoo") ``` ### 3. 人口で詰まらない ```python stuck = any(p.blocked for _b, p in g.all_production("me")) # キューにあるのに始まらない = 人口不足 res = g.resources() if stuck or res["food_cap"] - res["food_used"] <= 6: g.build_near(builder, "hhou", home.x, home.y) ``` `blocked` は「そろそろ上限」より一歩早く分かります。戦闘で兵をまとめて失い、補充しようとしてキューが詰まった瞬間に気づけます。 ### 4. 建設順序 + 建て終わったら自分で鉱山に戻る(Shift で戻す) ```python spot = g.build_near(w, "hbar", home.x, home.y) if spot: g.gather(w, mine, queue="after") # 建て終わったら採掘に戻る。次のティックで探し直す必要がない ``` 1 人の農民に続けて何棟も建てさせるには:`g.build_queue(w, [("hhou", x1, y1), ("hhou", x2, y2)])`。ゴールドは着工時に引かれます。 ### 5. ティアアップのタイミング、攻防アップグレード ```python if not g.queue(hall) and g.can_do(hall, "hkee") in (0, 220): # 本拠地のキューが空でないとアップグレードできない(空でなければ 185) g.upgrade(hall, "hkee") p = g.production(hall) # ティアアップの進捗 if p and p.kind == "upgrade": print(f"Keep 完成まであと {p.remaining:.0f} 秒") for sm in g.my_buildings({"hbla"}): if not g.queue(sm): ok = [u for u, v in zip(UPS, g.can_do_many([(sm, u) for u in UPS])) if v in (0, 220)] if ok: g.research(sm, ok[0]) ``` ### 6. 拡張:歩行距離で最も近い鉱山を選ぶ ```python mines = [m for m in g.gold_mines() if g.dist(m, home) > 1500 and not taken(m)] best = min(mines, key=lambda m: g.path_distance(home, m) or 1e9) # 島の鉱山は None を返す -> 最後に回る ``` ## 二、偵察と情報 ### 7. 相手が何をしているかを見る ```python for b, p in g.all_production("enemy"): # 見えている相手の建物が何を訓練 / 研究 / アップグレードしているか print(b.type, p.kind, p.queue, f"{p.progress:.0%}" if p.progress is not None else "停滞中") ``` イベントと組み合わせる:`ev.kind == "production.done" and ev.owner != g.me()` —— 相手がたった今何を完成させたか。 ### 8. 見たものを覚えておく(戦場の霧) ```python for u, t, age in g.last_seen(max_age=60): # 60 ゲーム秒以内に見た敵(最後の位置と HP) ... hero_seen = [r for r in g.last_seen() if r[0].is_hero] # 相手のヒーローを最後に見た場所 ``` フェアモードでは、これが相手の情報を得る唯一の手段です。人間のプレイヤーと同じです。 ### 9. コンピューターの相手がどこを攻めるか(コンピューター AI にのみ有効) ```python plan = g.enemy_ai_plan(some_enemy_soldier) # コンピューターのキャプテンが向かう先 ``` コンピューターは出撃前に目標地点を決めています —— 先回りしてそこへ兵を戻しておきましょう。 ## 三、クリープ狩り ### 10. 夜にクリープ狩り ```python if g.is_night(): # 18 時 ~ 6 時:クリープは眠っている(先手を取れて囲まれない)、全員の視界が短くなる ... wait = g.seconds_until(18) # 日没まであと何ゲーム秒か(1 日 480 秒) ``` ### 11. 勝てるキャンプだけを狙う ```python from openwar3 import combat mine = [g.stats(u) for u in army] def ttk(target): return combat.time_to_kill(mine, g.stats(target), target_hp=target.hp) or 1e9 camp = [c for c in g.creeps() if g.dist(c, center) < 600] ours = max(ttk(c) for c in camp) # このキャンプを全滅させるのにかかる時間(概算) theirs = g.time_to_kill(camp, weakest_of_mine) or 1e9 # 相手が自軍で最も弱いユニットを倒すのにかかる時間 if ours < theirs and g.reachable(center, camp[0]): g.attack_move(army, camp[0].x, camp[0].y) ``` 完全な例:`micro_bot.py` の `_maybe_creep`。 ## 四、マイクロ操作 ### 12. 集中攻撃:一番近い敵ではなく「最も早く倒せる敵」を狙う ```python target = min(visible_enemies, key=lambda e: g.time_to_kill(fighters, e) or 1e9) g.attack([u for u in fighters if (g.current_target(u) or target).handle != target.handle], target) ``` 「それを攻撃していない」ユニットにだけ命令し(`current_target`)、すでに攻撃中のユニットを邪魔しないようにします。 ### 13. 瀕死のユニットを下げる ```python for u in army: if u.hp < u.hp_max * 0.35: g.move(u, *toward(home, u, 500)) # 本拠地の方向へ 500 下がる。3 秒以内は繰り返し下げない ``` 集中攻撃されているかの判定:`damage` イベントで、同じユニットが短時間に複数のダメージ元から攻撃されている = 囲まれている。 ### 14. ヒーローを守る、経験値を献上しない ```python for h in g.my_heroes(): if h.hp < h.hp_max * 0.4: g.move(h, home.x, home.y) g.use_item(h, slot_of(h, "phea")) # 回復ポーション:inventory(h) でスロット番号を探す ``` ### 15. 相性:適切なユニットに適切な相手を攻撃させる ```python s = g.stats(u) best = max(enemies, key=lambda e: s.dps_vs(g.stats(e))) # Rifleman は Gryphon Rider を撃つ(Pierce 対 Small ×2)、Gryphon Rider は Footman を撃つ(Magic 対 Large ×2) ``` 相性表はゲームデータから来ています:`combat.damage_multiplier("pierce", "small") == 2.0`。 ### 16. 挟み撃ちと経路:タワーを迂回する ```python route = g.walk_path(army_center, target) # 地上の最短経路の折れ点 g.path(army, route, attack=True) # 各地点を順番にアタックムーブで通過 ``` タワーを避けるには、経路探索グリッド上でタワーの周囲を通行不可にしてから計算します。 ```python grid = g.grid().copy() for t in towers: grid.block_area(t.x, t.y, 800) # タワーの射程 700 + 余裕 route = grid.path((army_x, army_y), (target.x, target.y)) ``` ### 17. 1 ティック分のコマンドは 1 バッチで送る ```python with g.batch(): g.attack(melee, target_a) g.attack(ranged, target_b) g.move(wounded, *home_xy) g.cast(hero, "thunderclap") ``` 数十件のコマンドでも、ゲームスレッドを待つのは 1 回だけです(実測:8 件の移動が 68 ms → 6.5 ms)。 ### 18. 攻城:砲で地面を撃つ ```python g.attack_ground(mortars, tower.x, tower.y) # Mortar Team / Demolisher で一帯を砲撃(木立の裏、透明ユニット) ``` ## 五、ヒーロー ### 19. スキル習得表 ```python SKILLS = {"Hamg": ["AHwe", "AHbz", "AHwe", "AHbz", "AHwe", "AHmt"]} # Water Elemental、Blizzard……レベル 6 の究極技は Mass Teleport info = g.hero_info(h) if info and info["skill_points"]: g.learn(h, SKILLS[h.type][learned_count]) # 拒否された(レベル不足で究極技を習得できない)ら次のレベルまで待つ ``` ### 20. スキルが本当に発動したか ```python r = g.cast(h, "thunderbolt", target=enemy_hero) # 次のティック: if g.cooldown(h, "AHtb"): # クールダウンに入った = 本当に発動した。受理 ≠ 発動 ... ``` ### 21. ポーションを買う、帰還する ```python g.buy(shop, "phea") # ヒーローはショップの隣に立っていること g.use_item(hero, slot, x=home.x, y=home.y) # Scroll of Town Portal(地点指定でアイテムを使う) ``` ## 六、振り返り - 毎ティックの判断をログに書き出し(`print` は実行ウィンドウに出力されます)、`g.say(unit, "撤退")` と組み合わせてゲーム内で確認します。 - `production.done` イベントには「何秒かかったか」が含まれます —— 自分の建設タイムライン(何秒で最初のヒーローが出たか、何秒でティアアップしたか)を集計し、上級者と比較しましょう。 - Agent 自身に振り返らせる:[Agent による自律反復](https://war3ai.com/ja/docs/agent-loop/) の対局レポートを参照してください。 --- # サンプル Bot > シンプルなものから複雑なものまで 4 つのサンプル。どれもそのまま動き、各ロジックが SDK の機能に対応しています。完全なリファレンスブレインもあります。 サンプルはすべて `brains/examples/` にあります。後のサンプルは前のサンプルを継承し、新しい部分だけを追加しています。順番に読むことをおすすめします。 | サンプル | 学べること | 実行方法 | |---|---|---| | `hello_bot.py` | 採集(1 鉱山に 5 人、満員なら伐採へ)、ワーカーの生産(キューは 1 体だけ)、人口建物の建設、建設が止まった建物の再開。4 種族すべてで動きます | `python tools/play.py --bot brains/examples/hello_bot.py` | | `rush_bot.py` | 兵舎と祭壇(なければ `build_near` で建てる)、ヒーロー優先(死んだら蘇生)、スキルポイントがあれば習得、1 波分たまったらアタックムーブ | `… --bot brains/examples/rush_bot.py` | | `macro_bot.py` | ビルドオーダー + 建て終えたら自分で鉱山に戻る(Shift)、人口で詰まったら即座に補充、兵舎のキューは 1 体、攻撃/防御アップグレード、ティアアップと上位ユニット、**地上の実際の移動距離**で目標を選んで経路をたどる | `… --bot brains/examples/macro_bot.py --speed 200` | | `micro_bot.py` | 内政の上に戦闘の制御を追加:最も早く倒せる敵に集中攻撃、瀕死のユニットを後退、ヒーローの生存、夜は勝てるクリープキャンプを選ぶ、敵が本拠地まで来たら防衛に戻る。1 ティックのコマンドは 1 バッチで送信 | `… --bot brains/examples/micro_bot.py --fair` | > **補足** > > `hello_bot` と `rush_bot` のコメントには、実機で踏んだ落とし穴が記録されています。たとえば「毎回最初のワーカーを選んで家を建てさせたら、ファーム 3 つがすべて建設途中のまま放置された」「ハードコードした兵舎の座標がちょうど森の中で、3 分経っても 1 つも建たなかった」など。コードよりもコメントを読むほうが得るものは多いはずです。 ## hello_bot:経済 ```python # 種族 -> (ワーカー, タウンホール群, 人口建物) RACES = { "h": ("hpea", {"htow", "hkee", "hcas"}, "hhou"), "o": ("opeo", {"ogre", "ostr", "ofrt"}, "otrb"), "u": ("uaco", {"unpl", "unp1", "unp2"}, "uzig"), "e": ("ewsp", {"etol", "etoa", "etoe"}, "emow"), } MINE_CAP = 5 # 1 鉱山あたり最大 5 人(それ以上は収入が増えない) LUMBER_CREW = 5 # 伐採の人数:各鉱山の採金 5 人 + この人数 = ワーカーの目標数 ``` やることは 3 つです。暇なワーカーに金を採掘させる(鉱山ごとの人数は自分で記録し、満員なら伐採へ)。ワーカーが足りなければ生産する(キューには 1 体だけ)。人口が上限に近づいたら、建設中でないワーカーを選んでタウンホールの近くに人口建物を建てる(ヒューマンとオークは、建設が止まった建物にワーカーを送って建設を再開させます)。 ## rush_bot:出兵と出撃 `hello_bot` に 3 つを追加しています。兵舎と祭壇がなければ建てる。祭壇でヒーローを出し(**死んだらまず蘇生**。ヒーローは唯一)、スキルポイントがあれば習得する。兵が 8 体たまったら全員で敵のタウンホールへアタックムーブし、大きく削られたら帰還して再びためる。ティックごとに戦闘を中断しないよう、命令は暇な兵にだけ出します。 ## macro_bot:内政の基本 ```python TECH = { "h": dict(order=["halt", "hbar", "hbla", "hlum"], altar="halt", hero="Hamg", skills=["AHwe", "AHbz", "AHab"], barracks="hbar", soldiers=["hfoo", "hrif", "hkni"], smith="hbla", upgrades=["Rhme", "Rhar", "Rhra", "Rhla"], tiers=["hkee", "hcas"]), ... } ``` プロプレイヤーが毎試合やっていることで、それぞれが SDK の機能に対応しています。ビルドオーダー表 + `gather(..., queue="after")` で建設後に鉱山へ戻る。`production().blocked` で人口の詰まりを検出する。`g.queue` で兵舎のキューを 1 体に保つ。`can_do` で次のレベルの攻撃/防御アップグレードを研究できるかエンジンに問い合わせる。ティアアップと上位ユニット(実機での教訓:ずっとティア 1 のままでいたら、23 分でティア 3 のナイトとグリフォンライダーに押し切られた)。`path_distance` で目標を選び、`path()` で経由点をたどる。 ## micro_bot:戦闘が始まったら ```python def _fight(self, g, army, foes, home, now): ... visible = [e for e in foes if e.visible_to(me)] # 見えない目標は拒否される(1001) atk = [s for s in (g.stats(u) for u in fighters) if s] target = min(visible, key=lambda e: _ttk(g, atk, e)) # 最も近い敵ではなく、最も早く倒せる敵 idle_or_other = [u for u in fighters if g.current_target(u) is None or g.current_target(u).handle != target.handle] if idle_or_other: g.attack(idle_or_other, target) ``` 実機:5 分間で 1497 ティック、3023 コマンド、エラー 0。 ## リファレンスブレイン:完全な AI `brains/xwar3/` は拡張、クリーピング、出撃をこなす完全な AI で、3 層で構成されています。 | 層 | 場所 | 周期 | 役割 | |---|---|---|---| | 戦略層 | `strategy/` | 秒単位 | AMAI 式の複数戦略の選択と切り替え、ビルド表、カウンターユニット、ヒーロー選択。オプションで[LLM 戦略アドバイザー](https://war3ai.com/ja/docs/llm-coach/) | | リフレックス層 | `reflex/`(4 つの独立プロセス) | 100 ms 単位 | 生存、スペル使用、集中攻撃、アイテム拾い | | 勝率モデル | `worldmodel/` | — | 勝てるかどうか(推論サブセット) | 複数のプロセスは**クレームテーブル**でユニットを共有し、優先度によって誰が制御するかを決めます。手動操作 95 > 生存 90 > スキル回避 85 > スペル使用 80 > アイテム拾い 70 > … > 戦略 50 > ワーカー割り当て 45。あなたの Bot はテーブル上では `bot` として扱われ、デフォルトの優先度は 50 です。 > **注意** > > リファレンスブレインは SDK の低レベル層(`w3cmd` / `act`)を直接使い、マップ全体の情報に大きく依存しています。「考え方」の参考には向いていますが、LLM にそのまま真似させることはおすすめしません。AMAI のデータが必要です。`start.bat` が初回のデプロイ時に AMAI の公開リポジトリから取得して生成します(AMAI は独自ライセンスのため、生成物は git に含めません。うまくいかなかった場合は `start.bat setup` で再試行します)。 リファレンスブレインを起動する最も簡単な方法は [Farsight コンソール](https://war3ai.com/ja/docs/console/) です。「インスタンスと開始設定」ページでインスタンス番号にチェックを入れ、「テスト開始」をクリックします。 --- # デバッグとパフォーマンス > ティックが遅い、コマンドが効かない、ゲームが動かない。現象から原因を絞り込み、付属の実機検証スクリプトで確認します。 ## レシートを見る 各コマンドのレシートが一次情報です。 ```python r = g.cast(hero, "blizzard", x=tx, y=ty) if not r: print(r.reason, r.verdict) # rejected(…)と理由コード print(r.exec_us, r.engine_us) # このコマンドがゲームスレッドで実行された時間(µs)/ そのうちエンジンの命令関数自体にかかった時間 ``` 通常、1 つのコマンドがゲームスレッドで使う時間は数マイクロ秒から数百マイクロ秒です。バッチブロックの終了後、`g.last_receipts` にそのバッチの各コマンドのレシートが入っています。 ## ゲーム内で見る ```python g.say(unit, "撤退") # ユニットの頭上にチャット吹き出しを表示(ゲームには影響しない) g.message("クリーピング開始") # 左下のメッセージ欄に 1 行表示(ローカルでのみ見える) ``` `print` の内容は Bot を実行しているターミナルに表示されます。ティックごとの主要な判断を出力し、頭上の吹き出しと組み合わせると、コードを読むよりずっと早く状況を把握できます。 ## ティックが遅い まず次のケースに当てはまらないか確認してください。 | 原因 | 対処 | |---|---| | コマンドを 1 件ずつ送り、毎回 1 フレーム待っている | `with g.batch():` で包めば、数十件でも待つのは 1 回だけ | | `g.visible()` / `g.can_do()` を 1 件ずつ呼んでいる(毎回ファストレーン経由で 1 フレーム待つ) | 可視性はスナップショットの `u.visible_to()` を使い、実行可否は `g.can_do_many([...])` でまとめて問い合わせる | | `on_tick` の中で `sleep` や待機をしている | ゲーム時間を記録し、次のティックで判定する | | 重い処理(経路探索、全マップ走査)を毎ティック再計算している | 結果をキャッシュし、数ティックおきに再計算する。`g.grid()` には 2 秒のキャッシュがあり、`g.stats()` のテクノロジーレベルは 5 秒ごとにキャッシュされる | ## ゲームが動かない / Bot がゲーム開始を待ち続ける | 現象 | 主な原因 | |---|---| | ずっと「ゲーム開始待ち」のまま | インスタンス番号が違う。またはゲームウィンドウが**最小化**されている。最小化中はゲームのシミュレーションが止まっている(時計が進まない) | | ゲームは動いているのに、Bot の命令に反応がない | 他人のユニットに命令している(レシートが `not_owner`)。または Bot が observer として接続している(`forbidden`) | | コマンドが `held` になる | このユニットがより優先度の高い層(リファレンスブレインのリフレックス層、コンソールからの手動命令)に確保されていて、送信されなかった | | 一時停止中もコマンドを出せる | 正常です。一時停止中はエンジンの時計が止まりますが、イベントディスパッチは動き続け、コマンドも通常どおり実行されます | ## 接続して状態を見る ```bash python -m openwar3 status --inst 5 ``` 接続状態を出力します。ゲームの pid、ワールドの発行周期と 1 回の収集時間、ファストレーンのカウンター、ゲーム中かどうか、ユニット数、ゲーム時計。 ## 実機検証スクリプト テストインスタンスを 1 つ起動し、SDK の各機能があなたのマシンで正常に動くか 1 項目ずつ確認します。 ```bash python tools/sdk_live_check.py --inst 20 # すべて python tools/sdk_live_check.py --inst 20 --only prod # 1 セクションだけ確認 ``` セクション:バッチ、時間、生産、キュー付き命令、戦闘ステータス、経路探索、フェアモード。各セクションは実際の対戦でコマンドを出して効果を読み戻し、合格数を出力します。 オフラインテストはゲームを起動しなくても実行できます。 ```bash python tools/run_tests.py ``` ## 「バグに見える」よくあるケース - **建設のレシートは受理されたのに、いつまでも建設が始まらない**:森の中の地点でもエンジンはその場で受理し、ワーカーが到着してから失敗します。`build_near` を使えば、建設を追跡し、失敗した地点をしばらくブラックリストに入れてくれます。 - **スキルのレシートは受理されたのに、発動しない**:中断されたか、マナ不足です。使用後の次のティックで `g.cooldown()` がクールダウンに入ったか確認してください。 - **攻撃命令は受理されたのに、兵が別の相手を攻撃する**:特定の目標を攻撃するには `g.attack(兵, 敵)`(右クリックのセマンティクス)を使います。生の攻撃オーダーは目標に対してオーダーを切り替えるだけで目標を記録しないため、近くの別の相手を攻撃しに行きます。 - **ワーカーの数が合わない**:金鉱に入ったワーカーはスナップショットにいません。 - **死んだヒーローを訓練できない**:ヒーローは唯一なので `g.revive(祭壇)` が必要です。蘇生には人口が必要で、死亡後約 3 ゲーム秒経たないと蘇生できません。 --- # RPG コンパニオン > RPG / カスタムマップで、プレイヤーに AI の相棒をつけます。あなたについて歩き、敵との戦いを手伝い、HP が減れば回復し、話し相手にもなります。4 つのモードがあり、クラスを 1 つ継承していくつかの属性を変えるだけで、あなただけのコンパニオンになります。 対戦だけではありません。RPG やカスタムマップでは、自分に **AI の相棒**をつけられます。あなたについて歩き、敵との戦いを手伝い、HP が減れば回復してくれて、手が空いているときは話しかけてきます —— セリフはローカル LLM につなぐこともできます。 **どう使うかはあなた次第です。** この機能は 3 層の API に分かれていて、下の層から上の層まで、どれも直接使えます。 | 層 | 内容 | 向いている用途 | |---|---|---| | **JASS チャネル** `g.jass` | マップ作者が使える 1291 個の JASS 関数を、名前で直接呼び出します(ユニット作成、同盟設定、アイテム付与、名前変更、テキスト表示、ヒーロー蘇生……) | 自分で遊び方を作りたい | | **便利 API** | `g.spawn`、`g.set_alliance`、`g.player_slots`、`g.show_text`、`g.map_data`:よく使う処理をまとめてあります | 自分用の補助スクリプトを書く | | **コンパニオンフレームワーク** | `openwar3.companion.Companion` + `openwar3.talk.Talk`:継承していくつかの属性を変えるだけで、追従、援護、回復、会話をこなす相棒になります | 相棒がほしい | > **注意** > > **ローカル、または LAN で自分で立てた**ゲーム専用です。ユニット作成や同盟設定のような操作は、自分のマシンが一方的にワールドを変更するものです。シングルプレイ(コンピューターとの対戦)なら問題ありませんが、マルチプレイでは他のプレイヤーと同期がずれます。そのためマルチプレイでは、JASS チャネルは読み取り専用の関数だけを許可し、コンパニオンは自動的に「話すだけ」に切り替わります。 ## 最速で始める:Farsight でワンクリック起動 1. **マップを選ぶ**:Farsight の「インスタンス」ページ →「次のゲームの設定」→ マップで、RPG マップを 1 つ選びます(ゲームディレクトリの `Maps` 以下にある `Scenario`、`Download` 内のマップがすべて一覧に出ます。例:`(4)WarChasers`)。 2. **スキームを選ぶ**:インスタンスカードの「AI スキーム」ドロップダウンで **コンパニオンのサンプル(buddy)** を選び →「選択」。 3. **テストを開始する**:ゲームが起動したら、**あなた自身がゲームウィンドウでプレイします**。コンパニオン —— 「ヒカリ」という名前のパラディン —— があなたのそばに現れます。 コマンドラインからも起動できます。 ```bash python tools/play.py --bot brains/examples/buddy.py --inst 20 --rpg --map "<ゲームディレクトリ>\Maps\Scenario\(4)WarChasers.w3m" ``` `--rpg`(スキームのマニフェストでは `"judge": false`)は、対戦ルールで勝敗を判定しないという意味です。RPG ではヒーローが死んでも蘇生でき、「建物が全滅したら負け」というルールもありません。多くの RPG マップは読み込み後に「任意のキーを押して続行」の画面で止まります。SDK は「ゲーム中なのに、ゲーム時計がずっと 0 のまま」であることに気づくと、自分でスペースキーを 1 回押します(`g.press_to_continue()`。ゲームウィンドウにキー入力のメッセージを送るだけで、フォーカスは奪いません)。 ## 自分のコンパニオンを書く ```python from openwar3.companion import Companion from openwar3.talk import Talk class MyBuddy(Companion): mode = "ally" # モード。下の表を参照 unit = "Hpal" # 作るユニット:任意の 4 文字コード。マップ独自のものも可 nickname = "ヒカリ" heal = ("holybolt", "AHhb", 0.55) # (スペルのオーダー名, 習得するスキル, 主人の HP がこの割合を下回ったら回復);None = 回復しない follow_distance = 350 talk = Talk(persona="元気な小さなパラディン。主人を応援するのが大好き") ``` ### 4 つのモード | mode | コンパニオンの正体 | 説明 | |---|---|---| | `ally`(デフォルト) | 空いているプレイヤースロットを 1 つ使い、あなたの**同盟者**になる | 自分の色と名前を持ちます(スコアボードや同盟パネルに `nickname` が表示されます)。あなたからは操作できず、自分で戦います。フレームワークが自動で同盟 + 視界共有に設定します | | `own` | **あなたの所有ユニット**として作る | いつでも手動で指揮できます。あなたが操作していないときは、AI が代わりに動かします | | `adopt` | マップに**すでにある**ユニットを引き継ぐ | `adopt(g)` をオーバーライドしてそのユニットを返します(マップがくれたペットやお供など) | | `voice` | ユニットは作らず、**話すだけ** | 話し相手、リマインダー。ワールドを変更しないので、マルチプレイでも使えます | 空きスロットがなければ、`ally` は自動で `own` に切り替わります。マルチプレイの場合やユニットを作れない場合は、自動で `voice` に切り替わります。 > **補足** > > `ally` モードのコンパニオンが追うのは「そのスロットにいま存在する最適なユニット」(ヒーロー優先)で、特定の 1 体に固定されるわけではありません。実測では、コンパニオンを本物のプレイヤーとして扱い、パラディンを削除してマップ独自のヒーローを支給したマップがありました —— コンパニオンはそのヒーローをそのまま引き継ぎ、マップがそのヒーローに設定したスキルも習得します。ヒーローが死んだら、その場での蘇生を優先します。マップ側が蘇生した場合は、そのまま使い続けます。 ### ティックごとに何をするか 順番にチェックし、条件が成立した行動を取ります。 | 順番 | 行動 | 条件 | 調整項目 | |---|---|---|---| | 1 | 撤退 | 自分の HP が 25% 未満で、近くに敵がいる:主人の後ろまで下がる | `retreat_at` | | 2 | 回復 | 主人の HP が設定値を下回り、スキルのクールダウンが明けていて、距離が 900 以内 | `heal`(None で無効) | | 3 | 援護 | 主人の周りに敵がいる:**主人を攻撃している敵 > 主人が攻撃している敵 > 最も近い敵** | `assist_radius`、または `pick_target` をオーバーライド | | 4 | 追従 | 主人から離れすぎたら追いかける。ある程度以上離れたら、戦闘をやめてまっすぐ戻る | `follow_distance`、`leash` | | 5 | 雑談 | 敵がいないとき、1 〜 2.5 分ごとに一言話す | セリフ表 | 「敵」はゲーム内の同盟関係で判断します(20 秒ごとに更新)。RPG マップには同盟勢力がいくつもあることが多く、「自分以外のプレイヤー」をすべて敵とみなすことはできません。 オーバーライドできるフック:`find_master`(誰が主人か。デフォルトはローカルプレイヤーのレベルが最も高いヒーロー)、`adopt`、`pick_target`、`on_poke`(主人がコンパニオンを右クリックした)、そして Bot の `on_start` / `on_tick` / `on_event` / `on_end`。回復、援護、撃破、追従、撤退、発言、蘇生の回数はすべて `self.stats` に記録され、終了時に出力されます。 ### 呼びかけ方 - **チャットコマンド**:チャット欄に `-follow`(ついてきて)、`-stay`(その場で待機)、`-heal`(すぐに回復)、`-hi`(あいさつ)と入力します。コマンド表は `commands` で変更し、反応は `on_command` をオーバーライドして変えます。 - **コンパニオンを右クリック**:`on_poke` が呼ばれます。サンプルでの反応は、主人の HP が満タンでなければ回復を 1 回かけ、満タンなら一言話す、というものです。 - **ポートレート会話**:あいさつ、主人が倒れたとき、主人のレベルアップ、コンパニオンの復帰。この数種類のセリフは、ゲーム自身のポートレート会話で話します(下部のポートレートがコンパニオンに切り替わり、画面に字幕が出ます)。それ以外は頭上の吹き出しで表示します。 - **ステータスパネル**:画面左側のパネルに、コンパニオンの HP バー、いま何をしているか、気分(ご機嫌 / わくわく / 緊張 / 怖い / 悲しい)、撃破数と回復回数を表示します。[キャンバス](https://war3ai.com/ja/docs/canvas/) で描いているので、マルチプレイでも安全です。 ### 会話とローカル LLM `Talk` はイベントに応じてセリフを選び、頭上に吹き出しを出します。`voice` モードのときや吹き出しを出せないときは、画面の左下に表示します。各セリフはスキームのログにも書き込まれるので、何を話したかを後から確認できます。 | イベント | タイミング | イベント | タイミング | |---|---|---|---| | `hello` | 登場したとき | `master_low` | 主人の HP が少ない | | `poke` | 主人に右クリックされた | `master_levelup` | 主人がレベルアップした | | `fight` | 戦闘開始 | `master_died` / `master_back` | 主人が倒れた / 復活した | | `kill` | 敵を 1 体倒した(倒した相手の名前を口にする) | `buddy_low` / `buddy_died` / `buddy_back` | コンパニオン自身の HP が少ない / 倒れた / 戻ってきた | | `healed` | 主人を回復した | `idle` / `item` | 雑談 / アイテムを拾った | セリフには `{master}`、`{me}`、`{map}`、`{enemy}`、`{level}`、`{item}` といったプレースホルダーを使えます。セリフを変えるなら `talk.lines` を直接編集し、クールダウンは `talk.cooldown` で設定します。 **ローカル LLM につなぐ**:`Talk(llm=LocalLLM(url, model))`。OpenAI 互換の API ならどれでも使えます(LM Studio、Ollama……)。モデルはバックグラウンドスレッドで回答し、返答が届いてから話します。モデルが起動していない、タイムアウトした、エラーになった場合は固定のセリフを話すので、ゲームが止まることはありません。リクエストはあなたが指定したローカルのアドレスにだけ送られ、その内容はゲーム内で起きたこと(主人の名前、倒した敵)です。 ## カスタムマップのユニット名 RPG マップのユニット、アイテム、ヒーローの多くはマップ独自に作られたもので(4 文字コードは `HC07`、`I00A` のような形)、組み込みの名前表では見つかりません。`g.map_data` は、いまのゲームのマップファイルを直接読みます。 ```python md = g.map_data md.name_of("HC07") # 'Optimus Primo' —— マップで変更された名前を優先 md.hero_names("HC07") # 称号のリスト md.hero_skills("OC10") # マップがこのヒーローに設定したスキル md.tooltip("I00A") # 説明文 ``` 保護や最適化がかかったマップ(人気 RPG の多く)には標準のオブジェクトデータファイルが含まれていないため、名前はマップ内のテキストデータから読み取ります。実測では、手元の RPG / カスタムマップ 38 個すべての解析に成功し、そのうち 37 個でユニット名を取得できました。 ## スキームにして共有する コンパニオンは `openwar3.Bot` のサブクラスにすぎないので、[AI スキーム](https://war3ai.com/ja/docs/schemes/) にして他の人と共有できます。マニフェストに 2 項目を追加します。 ```json {"id": "my-buddy", "name": "わたしのコンパニオン", "entry": "my_buddy.py", "fair": false, "judge": false} ``` `"fair": false`:JASS チャネル(ユニット作成、同盟設定)を使うため。`"judge": false`:対戦ルールで勝敗を判定しないため。 ## 実測記録 2026-09-24、テストインスタンス、WarChasers マップ、2 倍速: - JASS チャネルの 18 項目のチェックがすべて合格:プレイヤースロット、ユニットとハンドルの相互変換、実数の戻り値、文字列引数、空きスロットへのユニット作成、同盟設定、名前変更、ユニット削除。プレイヤーレーンからの呼び出しや引数の個数違いは、正しく拒否されました。 - コンパニオン:「任意のキーを押して続行」を自分で押す → 主人のそばに現れてあいさつ → 主人についてヒーロー選択用のサークルに入り、マップからヒーローを支給されて引き継ぐ → 追従(主人から 200 〜 400)→ 敵と戦い、1 体倒して「お見事!」と言う → HP が減って撤退 → 戦死後にマップによって蘇生され、再び追従。 ## まだできていないこと 1. **プレイヤーが入力した任意のチャット文は読み取れません**。決まったチャットコマンドはすでに使えます。コンパニオンと本当に自由に会話するには、入力されたテキストそのものを取得する必要があります。 2. **コンパニオンは個々のマップの遊び方(クエスト、ショップ、ストーリー)を理解していません**。できるのは汎用的な追従、援護、回復です。特定のマップを理解させたいなら、サブクラスでそのマップ向けに書きます —— `g.map_data` で名前を調べられ、`g.jass` で任意の関数を呼べます。ここはまさに、あなた自身が決めるために残してある部分です。 --- # キャンバス > ゲーム画面にテキストボックス、パネル、プログレスバー、画像、地面に沿った円、矢印付きのルートを描きます。ランタイムが毎フレーム自前で描画し、ゲームの状態は変えないので、マルチプレイでも安全です。Python、HTTP、共有メモリへの直接書き込みのどれでも使えます。 外部プログラムから、ゲーム画面に**テキストボックス、パネル、プログレスバー、画像、地面の円、矢印付きの地面のルート**を描けます。描画はランタイムが毎フレーム自前で行います。独自の HUD、補助線、ヒント、チュートリアルの注釈、配信用の情報ボードなどに向いています。 ## キャンバスと JASS の画面系関数、どちらを使うか | | キャンバス(このページ) | [JASS の画面系関数](https://war3ai.com/ja/docs/jass/) | |---|---|---| | 描くのは誰か | ランタイムが自前で描画 | ゲーム自身(フローティングテキスト、エフェクト、パネル、ポートレート会話……) | | マルチプレイ | **安全**:自分のマシンの画面に描くだけで、ゲームオブジェクトを作らず、ゲームの状態も変えない | シングルプレイのみ | | 見た目 | 自由:中国語フォント、角丸、半透明、枠線、任意の色、ローカルの画像 | ゲーム本来のスタイル | | 対象への追従 | ユニット、ワールド座標、画面位置に追従。地面の円は地形の起伏に沿う | 関数による | | コスト | 実測で 1 フレームあたり 0.2 〜 0.35 ms(要素 9 個) | 1 回の呼び出しで約 13 ms | 2 つの方法は併用できます。ゲーム本来のスタイルの演出は JASS で、独自のパネル、補助線、ヒントはキャンバスで描きます。 ## Python ```python c = g.canvas # 初回使用時にランタイムが描画フックを入れる(約 0.1 秒) c.text("title", "こんにちは、キャンバスです", screen=(40, 110), color=(255, 220, 80), bg=(0, 0, 0, 170), border="#C49C40", size=22, bold=True) c.panel("status", "コンパニオン · ヒカリ", ["気分:ご機嫌", "撃破:12"], screen=(16, 330)) c.bar("hp", 0.62, unit=hero, lift=260, width=100, height=12, color=(80, 220, 80), text="62%") # ユニットに追従 c.text("tag", "ボスが大技を撃つぞ!", unit=boss, lift=320, color=(255, 80, 80), size=22, bold=True) c.circle("danger", (x, y), 300, color=(255, 60, 60), fill=(255, 60, 60, 60), width=3) # 地面の危険エリア c.circle("aura", hero, 450, color=(80, 200, 255, 220)) # ユニットに追従する円 c.path("route", [(x1, y1), (x2, y2), (x3, y3)], color=(255, 220, 0), width=5, arrow=True) c.image("icon", "icon.png", screen=(40, 170), width=64, height=64) c.remove("danger"); c.hide("tag"); c.clear() # clear は自分が描いたものだけを消す c.expire("tag", 5) # 5 秒後にひとりでに消える with c.batch(): ... # 多数の変更をまとめ、共有メモリへの書き込みは 1 回だけ c.stats() # drawnFrames が増えている = 実際に描画されている ``` 各要素は `key` で識別します。同じ key でもう一度描くと、更新になります。 **クリック可能**:テキストボックスとパネルに `clickable=True` を付けると(ホバー時の色は `hover=` で指定)、クリックされたときにイベントストリームに `ui.click` が 1 件届き、`ev.key` がその key になります。その要素の上でのクリックはゲームには届きません。既成のボタン、選択カード、ホットキー、地面のクリックについては [UI と入力](https://war3ai.com/ja/docs/ui-input/) を参照してください。 **位置**(各要素に 1 つ指定): - `screen=(x, y)`:画面のピクセル。負の値は右端 / 下端から数えます。`center=True` で中心揃えになります。 - `frac=(0.5, 0.1)`:画面に対する比率。 - `world=(x, y)`:ワールド座標。 - `unit=ユニット`:ユニットに追従します。ワールドとユニット上のテキストやプログレスバーは、下辺の中点をその点に合わせ、`lift` で上に持ち上げます。 ワールドとユニット上の要素は、デフォルトで下部の操作パネルと上部の昼夜の時計を避けます(`over_ui=True` にすると上に重ねます)。**色**は `(r, g, b)`、`(r, g, b, a)`、`"#RRGGBB"`、`"#RRGGBBAA"` のいずれかで書けます。 | メソッド | 描くもの | よく使う引数 | |---|---|---| | `text(key, テキスト, ...)` | テキストボックス。複数行は `\n` | `color`、`bg` 背景色(省略すると透明)、`border`、`size`、`bold`、`shadow`、`width`(この幅で折り返す)、`radius` 角丸 | | `panel(key, タイトル, [行...], ...)` | パネル(暗い半透明の背景、金色の枠) | `text` と同じ | | `bar(key, 0..1, ...)` | プログレスバー:HP、クールダウン、詠唱ゲージ | `width`、`height`、`color`、`bg`、`border`、`text` | | `image(key, パス, ...)` | ローカルの画像(png / jpg / bmp / gif) | `width`、`height`(省略すると元のサイズ) | | `circle(key, ユニットまたは地点, 半径, ...)` | 地面の円。地形に沿う | `color` 線の色、`fill` 塗り(透明度付き)、`width` 線幅 | | `path(key, [点...], ...)` | 地面の折れ線 | `color`、`width`、`arrow` 終端の矢印。点は座標でもユニットでもよい | ## HTTP(任意の言語) Farsight のバックエンド(ローカルでのみ待ち受け): ```http POST /api/instances/20/canvas {"set": [ {"key": "banner", "kind": "text", "text": "HTTP から描いたキャンバス", "frac": [0.5, 0.12], "center": true, "color": "#FFDC50", "bg": [0, 0, 0, 180]}, {"key": "hp", "kind": "bar", "value": 0.8, "unit": 596125988, "lift": 260, "text": "80%"}, {"key": "zone", "kind": "circle", "center": 596125988, "radius": 600, "color": [255, 200, 0], "width": 4}, {"key": "route", "kind": "path", "points": [[-4587, -9092], [-5387, -8792]], "color": "#50C8FF"} ], "remove": ["old"], "clear": false} GET /api/instances/20/canvas いま描いている要素 + 描画したフレーム数 ``` `kind` は Python のメソッド名と同じで、引数名も同じです。ユニットはスナップショットのアドレス `addr` で指定します。 ## 共有メモリに直接書き込む Python や Farsight を経由しなくても使えます。まずセマンティックコマンド `canvas_enable`(W3P オペコード 73)を 1 回送ると、ランタイムが共有メモリブロック `Local\War3Canvas_` を作成します。構成は、先頭 64 バイト + 256 エントリ × 112 バイト + 64 KB のテキスト / 点プールです。書き込みは seqlock で行います(シーケンス番号を奇数にする → エントリとプールを書く → シーケンス番号を偶数にする)。ランタイムは毎フレーム 1 回読み、書きかけのデータを読んだ場合は前のフレームの内容を使い続けます。また、描画済みフレーム数、要素数、異常カウントを書き戻します。Python のリファレンス実装は `sdk/python/w3canvas.py` で、構造体の定義はプロトコルのヘッダーファイルにあります。[W3P プロトコル](https://war3ai.com/ja/docs/protocol/) を参照してください。 ## 複数のプログラムが同時に描く MOD、Farsight、MCP、ゲートウェイが同じ試合に同時に描くことがありますが、キャンバスは 1 つしかありません。ルールは、**各プログラムは自分の要素だけを扱う**ことです。 - 書き込む前に名前付きロックを取り、既存の要素を読み出して、他のプログラムの分は残し、自分の分を差し替えてから書き戻します。 - 各要素には描いたのが誰かが記録されます(プロセス ID + プロセス内の連番)。描いたプログラムが終了すると、次に誰かが書き込むときについでに消されます。そのボタンもクリックを横取りしなくなります。 - 要素の番号は共有カウンターから割り当てるので、重複しません。 Python SDK はすでにこのとおりに動作し、`clear()` も自分の分だけを消します。共有メモリに直接書き込む場合もこのルールに従ってください。そうしないと、他のプログラムの要素を消してしまいます。レイアウトの詳細は [W3P プロトコル](https://war3ai.com/ja/docs/protocol/) を参照してください。 ## 実測と注意点 - 2026-09-25 の実測(1920×1080、2 倍速):要素 9 個で 1 フレームあたり 0.27 〜 0.34 ms、約 63 フレーム / 秒、異常 0 回。9 件の書き込みに 6 ms。ヒーローが移動しても、ユニットに追従する円、テキスト、HP バーはきちんとついてきました。内容が変わったときだけテクスチャを描き直し、位置が動くだけなら描き直しません。 - 描画はゲームの UI の後、マウスカーソルの前に行われます。そのため、ゲーム自身の HP バー、ユニット、UI の上に重なり、マウスカーソルはさらにその上に描かれます。下部の操作パネルと上部の昼夜の時計は避けますが、**マップ独自のパネル(右上のリーダーボード、カウントダウン)は避けません** —— 自分のパネルは右上に置かないでください。 - 試合中でないとき(メインメニュー、リザルト画面)は、ワールド座標やユニット上に置いた要素は描かれません。画面位置に置いた要素は通常どおり描かれます。 - 地面の円は、円周上の 64 点をそれぞれ地面に投影したものです。地形に高低差があると形も起伏します —— これは正しい動作で、実際の地面の上に描いているからです。 - 初回はフックの導入とフォントのウォームアップに約 1 秒かかり、その間テキスト系の要素はまだ描かれません。円と線は描かれます。 - 描画中に異常が 1 回でも起きると、そのセッションでは以後描画しません(頭上の吹き出しと同じ保護機構)。`stats()` の `faults` が 1 になります。 - テキスト、画像パス、点の合計は 64 KB まで、要素は最大 256 個です。画像パスは、ゲームプロセスから読めるローカルのパスである必要があります。 [AI コンパニオン](https://war3ai.com/ja/docs/companion/) のステータスパネルもキャンバスで描いています:HP バー、いま何をしているか、気分、撃破数と回復回数。 --- # UI と入力 > キャンバス上のボタンや選択カードをクリック可能にし、ホバーで自動的にハイライトします。ホットキーの登録、地面のクリックによる位置指定、マウスが指している場所の取得、ローカルプレイヤーが何を選択しているかの把握ができます。クリック、ホットキー、スキル使用、チャット全文、プレイヤーの退出は、すべてイベントストリームに入ります。 [キャンバス](https://war3ai.com/ja/docs/canvas/) で描いたものが、**クリックできる**ようになりました。ランタイムがゲームウィンドウの入力を横取りし、外部プログラムは次のことができます。 | 機能 | 概要 | ゲームに届くか | |---|---|---| | **クリック可能なキャンバス要素** | ボタン、選択カード、パネル:クリックで `ui.click` を発行し、ホバーで自動ハイライト | ボタン上のクリックは**届かない** | | **ホットキー** | `F5` や `ctrl+shift+Q` のような組み合わせを登録し、押すと `hotkey` を発行 | 握りつぶすかどうかを選べる(そのキーが生む文字も含めて) | | **地面のクリック** | ワールド上をクリックすると、地面の座標付きで `mouse.world` を発行 | 握りつぶすかどうかを選べる(「位置をクリックして塔を置く」など) | | **マウス位置** | 毎フレーム更新:画面のピクセル、指している地面の地点、ホバー中のキャンバス要素 | —— | | **選択** | ローカルプレイヤーが何を選択しているか。変わるたびに `selection.changed` を発行 | —— | すべて**ローカルの入力 + ローカルの描画**です。コマンドストリームには入らないので、マルチプレイでも安全です。ただし、コールバックの中でワールドを変更する(ユニットを出す、属性を変える)なら、やはりシングルプレイ専用になります。 ## Python:g.ui ```python ui = g.ui # 初回使用時にランタイムがウィンドウの入力を横取りする ui.button("shop", "ポーションを買う(50 ゴールド)", screen=(40, 300), on_click=lambda g, ev: buy(g)) c = ui.choice("レベルアップ!報酬を 1 つ選ぼう", [("筋力 +5", "打たれ強くなる"), ("攻撃速度 +20%", "もっと戦える"), ("オオカミを召喚", "仲間が 1 体増える")], pause=True, on_pick=lambda g, i: give(g, i)) # 画面中央にカードを 1 列表示。pause=True なら選ぶ間ゲームを一時停止 i = c.wait(timeout=30) # ブロックして待つこともできる(待つ間もイベントは処理され、取りこぼさない) ui.hotkey("F5", lambda g, ev: g.say(hero, "了解!")) # デフォルトで握りつぶす ui.hotkey("ctrl+shift+Q", on_press=..., swallow=False) ui.mouse(on_click, capture=True, buttons=("left", "right")) # 地面のクリックを捕捉:左右どちらのボタンも報告し、握りつぶす xy = ui.pick_point("地面をクリック:塔をどこに建てる?") # ブロッキング版:次の地面への左クリック -> (x, y)、Esc またはタイムアウト -> None ui.cursor() # {'screen': (x, y), 'world': (x, y, z) または None, 'hover': 'shop'} ui.toast("第 3 ウェーブが来た!", seconds=3) ui.close() # 自分のコントロールとホットキーを片付ける。ほかに入力を使うプログラムがなければウィンドウの入力を返す g.close() # または接続ごと切る(with Game(...) as g: と書いてもよい) ``` コールバックの引数は `(g, ev)` で、あなたが `g.events()` を呼んだときに発火します —— Bot と [ゲームプレイ MOD](https://war3ai.com/ja/docs/mods/) のランナーは毎ティック呼んでいます。コールバックを指定していないクリックは `ui.clicks` に入ります。コールバック内で例外が起きてもログに記録されるだけで、ほかのコールバックやイベントには影響しません。 キャンバスの低レベル API も直接使えます:`g.canvas.text(..., clickable=True, hover=色)` とし、クリックはイベントストリームから受け取ります。`ev.key` は描いたときに指定した key です。クリック可能な要素を描くと入力は自動的に有効になるので、先に `g.ui` に触れる必要はありません。 **ホットキーの書き方**:`F1` ~ `F24`、`A` ~ `Z`、`0` ~ `9`、`numpad0` ~ `numpad9`、`space enter esc tab backspace insert delete home end pageup pagedown left up right down`。前に `ctrl+`、`shift+`、`alt+` を付けられます。 > **注意** > > 修飾キーなしの英字や数字は、チャット入力やゲームのショートカットキーとぶつかります。F5 ~ F8 のようにゲームが使っていないキーか、組み合わせキーを優先してください。 ## 新しいイベント `g.events()` に次のものが加わりました(全フィールドは [W3P プロトコル](https://war3ai.com/ja/docs/protocol/) を参照)。 | kind | 発生するとき | 便利フィールド | |---|---|---| | `ui.click` | インタラクティブなキャンバス要素がクリックされた | `.key` キャンバスの key、`.button`(`'left'` / `'right'`)、`.mods` 修飾キー | | `ui.hover` | マウスがキャンバス要素に入った / 出た | `.key`(出たときは `None`) | | `hotkey` | 登録したホットキーが押された | `.key` ホットキーの書き方、`.mods` | | `mouse.world` | 地面のクリックを有効にしているとき、ワールド上をクリックした | `.x .y` 地面の座標、`.button`、`.value`(1 = 握りつぶした) | | `selection.changed` | ローカルプレイヤーの選択が変わった | ユニットは `g.selection()` で取得 | | `spell.cast` | ユニットがスキルを使った(スキルのクールダウン開始) | `.spell` 4 文字コード、`.b` レベル、`.value` クールダウン秒数、`.x .y` 詠唱地点 | | `message` | 画面のメッセージ枠に 1 件表示された | `.text` 全文、`.frame` どの枠か、`.chat`(チャットの場合) | | `player.left` | プレイヤーが退出した、または敗北判定で除外された | `.player` | | `game.ended` | 試合から退出した | —— | ## チャットと画面メッセージ プレイヤーがチャット欄に打った文字は、`message` イベントの `.chat` から直接読めます。 ```python for ev in g.events(): if ev.kind == "message" and ev.chat and ev.chat["text"] == "-follow": ... # ev.chat = {'channel': '所有人', 'sender': 'プレイヤー名', 'text': '-follow'} ``` `g.messages()` は別のカーソルを持つもう 1 つの読み口で、ゲームのヒント(「Farm がもっと必要です」「そこには建設できません」)もここに入ります。Bot を書くときにこれを使えば、コマンドがなぜ実行されなかったのかがわかります。 ## 他の言語から使う - **ゲートウェイ**:`ui.button`、`ui.choice`、`ui.hotkey`、`ui.mouse`、`ui.cursor` などのメソッドは、[ゲートウェイ](https://war3ai.com/ja/docs/gateway/) でも同じ名前で使えます。リモートからはコールバック関数を渡せないので、クリックとホットキーはイベントのプッシュから受け取ります(`ui.click` イベントには `key` が付きます)。 - **共有メモリに直接書き込む**:まずセマンティックコマンド `input_enable`(W3P オペコード 74)を送ると、ランタイムが入力の横取りを始めます。入力ブロック `Local\War3Input_` に、あなたはホットキー表とマウスのオン・オフを書き込み、ランタイムはマウス位置、指している地面の地点、ホバー中の要素を書き戻します。キャンバス要素のフラグビット `0x40` は「インタラクティブ」を表します。レイアウトは [W3P プロトコル](https://war3ai.com/ja/docs/protocol/) を参照してください。 ## 複数のプログラムで同時に使う MOD、Farsight、MCP、ゲートウェイの各セッションが、1 つの試合に同時にボタンを置いたりホットキーを登録したりしても、互いに干渉しません。 - 各プログラムは自分のホットキーと地面クリックのオン・オフを登録し、SDK が全員の分を 1 つの表にまとめてランタイムに渡します。同じキーは 1 件だけ残し、イベントは全員に送られ、各プログラムはキーで自分のホットキーを見分けます。 - `ui.close()` が取り下げるのは自分の分だけで、最後のプログラムがいなくなったときにウィンドウの入力を返します。 - プログラムが強制終了され、後片付けが間に合わなかった場合:ランタイムは 2 秒ごとに確認し、登録したプログラムがすべて終了していれば、残されたホットキーと地面クリックの横取りを消します。それらが描いたボタンもクリックを横取りしなくなります。 ## 実測 2026-09-25、テストインスタンスでの実機検証 16/16: - ボタンをクリック → `ui.click` + コールバック。ランタイムの横取りカウンタが +1(ゲームにはこのクリックが届いていない)。ボタンの外をクリックしても発火しない。 - F6 → `hotkey`。地面をクリック → `mouse.world`(握りつぶされる)。 - パラディンを 1 体出して選択 → `selection.changed`、`g.selection()` と一致。Divine Shield を使う → `spell.cast('AHds', 1, 35.0)`。 - マップのテキスト → `message`。チャット → `message`、`.chat` から発言者と内容を解析。 - コンピューターを敗北判定 → `player.left`。試合を終了 → `game.ended`。 人がマウスでボタンをクリックしたときや、ホバー時のハイライトも 1 つずつ目で確認しました。 ## 制約と注意点 - **位置は実際のマウスで決まる**:ゲーム自身がシステムカーソルから位置を読むので、ホバーや `cursor()` は実際のマウスを反映します。横取りするのはボタンやキーの入力だけです。 - **マウスカーソルの下に描かれる**:Warcraft はカーソルを画面の一部として毎フレーム描き込みます。キャンバスも頭上の吹き出しも、ゲームがカーソルを描く手順の直前に描かれます。そのためゲームの UI の上に重なり、カーソルがさらにその上に重なります。そのフレームでカーソルが描かれない(非表示やカットシーン中の)ときだけ、最後の手順で描く方式に戻ります。 - **システムのスケーリング**:自分でテストを書き、ウィンドウメッセージでクリックを送る場合、DPI を認識しないプロセスが送った座標はシステムによって拡大されます(150% スケーリングで実測 ×1.5)。テストプログラムでは先に DPI 対応を宣言してください。人による実際のクリックには影響しません。 - **初回はフォントのウォームアップが必要**で、約 1 秒かかります。その間はボタンがまだ描かれていないので、クリックできません。 - **試合中でないときは地面のクリックを報告しない**:メインメニューとリザルト画面では、`mouse.world` は発行せず、握りつぶしもしません。 - 押した時点で握りつぶした 1 回は、離す前にほかのプログラムに切り替えたり、マウスをウィンドウの外へドラッグしたりした場合もリセットされます。次にボタンを離す操作まで握りつぶしてしまうことはありません。 - 1.27 にはゲームの UI フレームを新しく作る関数がありません(1.31 から)。ここでのボタンやカードはすべてランタイムが描いているので、見た目は自由ですが、ゲーム自身のメニュー階層には現れません。 --- # JASS チャネル > マップ作者が使える 1291 個の JASS 関数を、ゲームの外から名前で直接呼び出せるようになりました。ユニット作成、属性変更、エフェクト、パネル、ダイアログ、サウンド、カメラ、霧……Farsight コンソール、コマンドライン、HTTP、Python の 4 通りの使い方があります。 マップ作者がマップスクリプトで使える **1291 個の JASS native** を、すべてゲームの外から名前で直接呼び出せるようになりました。ユニット作成、属性変更、エフェクト描画、パネルやダイアログの表示、サウンド再生、カメラ操作、霧の変更……ゲームをさらにカスタマイズするのに使えます —— RPG の補助、[AI コンパニオン](https://war3ai.com/ja/docs/companion/)、自作のミニゲーム、デバッグツールなど。 | 使い方 | 向いている用途 | 入口 | |---|---|---| | **Farsight の「JASS コンソール」ページ** | 手で試す、結果を見ながら直す | 左サイドバーの「システム → JASS コンソール」:スクリプトを書いて実行をクリック。右側で分類ごとに関数を調べ、クリックするとスクリプトに挿入 | | **コマンドライン** | 手で試す、またはスクリプトファイルにして繰り返し実行する | `python -m openwar3 jass --inst 20`(対話モード)、`-e "コード"`、`my_script.j`、`--list キーワード` | | **HTTP** | 任意の言語の外部プログラム | `POST /api/instances/{n}/jass` など(下記参照)。Farsight のバックエンドはローカルでのみ待ち受け | | **Python** | スキーム、コンパニオン、ツールを書く | `g.jass.任意の関数(...)`。よく使う画面演出とインタラクションは `openwar3.visual` にまとめてあります | > **注意** > > 境界は 3 つあり、どれも仕組み上の制約です。 > > - ワールドを変更できるのは**シングルプレイ**(ローカルのコンピューターとの対戦)だけです。自分のマシンが一方的にオブジェクトを作ったりユニットを変更したりすると、マルチプレイでは他のプレイヤーと同期がずれます —— マルチプレイでは読み取り専用の関数(`Get*`、`Is*`、`Count*`……)だけを許可します。 > - 自分のマシン上のツール専用です。プレイヤーとして接続した場合(`Game(player=N)`)やフェアモードでの呼び出しは拒否されます。 > - ローカル、または LAN で自分で立てたゲーム専用です。 > > マルチプレイで画面に何かを追加したいなら、[キャンバス](https://war3ai.com/ja/docs/canvas/) を使ってください。ランタイムが自前で描くもので、ゲームの状態は変えません。 ## スクリプトの書き方 コンソール、コマンドライン、HTTP は同じスクリプトを使います。1 行に 1 文で、**JASS をそのまま貼り付けられます**(`call` / `set` / `local`、`true` / `false` / `null`、`'Hpal'` の 4 文字コード、`//` コメント)。Python 風に書くこともできます。 ```text set h = hero() // 組み込み:自軍のメインヒーロー local texttag t = CreateTextTag() call SetTextTagText(t, "|cffffcc00+128 クリティカル!|r", 0.024) call SetTextTagPosUnit(t, h, 60) call SetTextTagVelocity(t, 0, 0.03) call SetTextTagPermanent(t, false) call SetTextTagLifespan(t, 4) call SetTextTagVisibility(t, true) call PingMinimapEx(h.x, h.y + 300, 3, 255, 0, 0, false) set u = CreateUnit(Player(0), 'hfoo', h.x + 200, h.y, 270) print("作成した", u, "ヒーローレベル", GetHeroLevel(h)) ``` - **変数は保持されます**:同じインスタンスの同じゲームの中なら、ある実行で `set` した変数を次の実行でもそのまま使えます。ゲームが変わると自動でクリアされ、手動でクリアすることもできます。 - **組み込み関数**:`hero()` 自軍のメインヒーロー、`me()` ローカルプレイヤー、`unit('hfoo')` ユニットを 1 体探す、`unit_at(x, y)`、`wait(秒)`、`print(...)`。ユニットからは `.x`、`.y`、`.hp`、`.hp_max`、`.mana`、`.type`、`.owner`、`.level` を読め、四則演算と比較ができます。 - `if`、`loop`、`function` は**使えません** —— ロジックを書くなら Python の `g.jass`(普通の関数呼び出しです)を使うか、[スキーム](https://war3ai.com/ja/docs/schemes/) にしてください。 - エラーが起きると、何行目で、なぜ失敗したか(その関数がない、引数の個数が違う、変数が未定義……)を教えてくれます。エラーより前の文はすでに実行されています。 引数と戻り値: | シグネチャ上の型 | 渡すもの | 説明 | |---|---|---| | 整数 | 数値。`'Hpal'` の 4 文字コードは自動で変換 | | | 実数 | 数値 | ランタイムがエンジンの要求する形式に変換 | | 真偽値 | `true` / `false` | | | 文字列 | `"..."` | 中国語やゲームのカラーコードに対応。ゲームが保持し続けるもの(フローティングテキスト、パネル、ボタン、チャットコマンド)は、その場でコピーを取るので安全 | | ハンドル | 変数に入ったハンドル、またはユニット(`hero()` などは自動でハンドルに変換) | | | 関数(code) | `null` のみ | 外から JASS 関数は渡せません。`TimerStart(t, 60, false, null)` のような使い方は可能 | | 文字列の戻り値 | —— | エンジンが返すのは文字列テーブルの番号なので、テキストは読み戻せません。ユニット名には `g.map_data.name_of` を使います | ## 分類 関数は名前で分類されていて、コンソールの右側と `--list` はどちらもこの分類を使います。 | 分類 | 個数 | 例 | |---|---|---| | 画面効果 | 80 | フローティングテキスト、ライトニングの線、エフェクト、地面の画像、地面のマーク、ユニットの色 / 拡大縮小 / アニメーション再生 | | UI パネル | 146 | 複数行パネル、リーダーボード、カウントダウンウィンドウ、ダイアログ、クエスト、画面テキスト、ミニマップのピン、ポートレート会話、全画面フィルター | | カメラ | 44 | カメラフィールド、パン、カメラの揺れ | | サウンド・音楽 | 50 | サウンドの作成と再生、音楽の再生 | | 霧・視界 | 25 | 可視領域、霧のオン / オフ | | アイテム / ヒーロー / ユニット | 63 / 32 / 161 | アイテム作成、ヒーローのレベル設定、所有者の変更、スキル追加 | | プレイヤー / 同盟 / 資源 | 71 | 同盟の設定、ゴールドと木材の変更 | | トリガー / イベント / タイマー | 62 | トリガー作成、イベント登録、タイマー | | 地形 / 天候 / 破壊可能オブジェクト | 45 | 天候エフェクト、地形の変更、破壊可能オブジェクトの作成 | | ゲーム進行 | 57 | ゲーム速度、一時停止、時刻(昼夜) | | その他 | …… | ユニットグループと領域、ストレージ、コンピューター AI スクリプト、型変換と数学、イベントレスポンス…… | 2026-09-24 に 1 つずつ実機で呼び出し、効果を目で確認したのは **94 個**です。残りも同じ経路を通りますが、1 つずつ効果を確認してはいません。 > **補足** > > 「イベントレスポンス」系の関数(`GetTriggerUnit`、`GetClickedButton`……)は、トリガーが実行されているその瞬間にしか値を持たず、外から呼ぶと 0 か空になります。「起きたかどうか」を知りたいなら、後述のイベントカウントを使ってください。 ## HTTP Farsight のバックエンド(デフォルトは `127.0.0.1:8866`、ローカルでのみ待ち受け): ```http GET /api/jass/natives?q=TextTag&cat=visual POST /api/instances/20/jass {"code": "set h = hero()\ncall PingMinimapEx(h.x, h.y, 3, 255, 0, 0, false)"} -> {"ok": true, "rows": [...], "printed": [...], "vars": {...}} -> エラー時:{"ok": false, "error": "第 2 行:...", "line": 2} POST /api/instances/20/jass/call {"name": "SetUnitScale", "args": [{"unit": 599669636}, 1.4, 1.4, 1.4]} POST /api/instances/20/jass/reset 記憶した変数をクリア ``` ユニット引数は `{"unit": アドレス}` と書きます。アドレスは、スナップショット内のユニットの `addr` です。実測で 1 リクエストあたり 60 〜 90 ms。 ## Python:g.jass と openwar3.visual ```python j = g.jass t = j.CreateTextTag() j.SetTextTagText(t, "こんにちは", 0.024) # 引数のルールはスクリプトと同じ。スナップショットのユニットやアイテムのオブジェクトをそのまま渡せる j.signature("CreateImage") # シグネチャを調べる ``` `openwar3.visual.Visual(g)` は、実測済みのよく使う画面演出を、1 つにつき 1 行で呼べるようにまとめたものです(ティックごとに `v.tick()` を 1 回呼ぶと、期限切れのものを削除し、ユニットに追従する線や円を移動させます。`v.clear()` ですべて削除)。 | メソッド | 効果 | |---|---| | `float_text(テキスト, ユニットまたは地点, ...)` | フローティングテキスト:ダメージ数値、頭上のヒント。中国語も色も使えます | | `link(a, b, kind)` | 2 体のユニットの間に線を引き、ユニットに追従させる:牽引 / スピリットリンク / ライフドレイン / ヒーリングウェーブ | | `effect(モデル, ユニットまたは地点, ...)` | エフェクトモデル:頭上、足元、または 1 回だけ再生(爆発、光の柱) | | `ring(ユニットまたは地点, 半径, color)` | 地面の範囲円:スキル範囲、危険エリア、集合地点。ユニットに追従させることも可能 | | `ping(地点, color)` | ミニマップのピン | | `board(タイトル, 行...)` | 右上の複数行パネル(アイコン付き)。セルごとに変更可能 | | `countdown(タイトル, 秒)` | 右上のカウントダウンウィンドウ。秒読みはゲーム自身が行う | | `scene(名前, セリフ, portrait)` | ポートレート会話:下部のポートレートが話すユニットに変わり、画面に「名前:セリフ」の字幕が出る | | `screen_tint(color, alpha)` | 全画面フィルター(デフォルトは周囲が赤くなる:HP 低下の警告) | | `sound(パス)` / `reveal(地点, 半径, 秒)` / `look(ユニット, ...)` | サウンド再生 / 霧を一部晴らす / ユニットの色変更、拡大、アニメーション再生、点滅 | ## インタラクション:JASS 関数を書かずに、プレイヤーの行動を知る JASS でプレイヤーに反応するにはトリガー関数を書く必要がありますが、外から関数は渡せません。そこで、**条件もアクションもない空のトリガーを作ってイベントだけを登録し、それが何回実行されたかを数えます。** 実測では、空のトリガーでもきちんとカウントされます。 | メソッド | 用途 | |---|---| | `chat_commands(["-follow", "-stay"])` → `.poll()` | プレイヤーがチャット欄に入力したコマンド(完全一致、または前方一致) | | `menu(タイトル, [ボタン...])` → `.clicked()` | 画面中央のボタンメニュー。どれがクリックされたか | | `hotkeys(("left", "right", "up", "down", "esc"))` → `.poll()` | 矢印キーと Esc が何回押されたか | | `on("TriggerRegister...Event", 引数...)` → `.poll()` | 任意の JASS イベントが何回起きたか:ユニットの死亡、領域への進入、被ダメージ、タイマー…… | 制約は、「何回起きたか」しか分からず、「誰が、何を入力したか」は分からないことです。誰なのかを区別したいなら、対象ごとにカウンターを 1 つずつ作ります。[AI コンパニオン](https://war3ai.com/ja/docs/companion/) のチャットコマンドも、この方法でつないでいます。 ## 注意点 - **作ったものは自分で削除する**:フローティングテキスト、線、画像、パネル、トリガー……削除しなければずっと残ります(`Visual.clear()` は自分が作ったものを削除します)。ゲーム内で同時に存在できるフローティングテキストは、最大で約 100 個です。 - **BJ 関数は native ではありません**:`CreateTextTagUnitBJ` のような関数は、マップスクリプト内で native を組み合わせて作られたもので、ここにはありません —— その実装に倣って native を呼んでください。 - **定数によっては先に変換が必要**:たとえば `ConvertPlayerColor(1)`、`ConvertFogState(4)`(値は common.j を参照)。 - 1 回の呼び出しは約 13 ms(ハンドル変換を含む)。プロトコル層は W3P オペコード 70 〜 72 です。[W3P プロトコル](https://war3ai.com/ja/docs/protocol/) を参照してください。 --- # ゲームプレイ MOD > スキームは、あなたの代わりに戦う AI だけではありません。ルール一式にもなれます。あなた自身がゲームウィンドウでプレイし、MOD が序盤の配置、敵の出現、報酬、画面上のボタンや選択カード、勝敗判定を担当します。openwar3.Mod を継承すれば、ファイル 1 つで遊び方が 1 つできあがります。 [AI スキーム](https://war3ai.com/ja/docs/schemes/) には 2 種類あります。`kind: bot` は AI で、あなたの代わりに戦います。`kind: mod` は**ルール一式**です —— あなた自身がゲームウィンドウでプレイし、MOD がお題を出します:序盤をどう配置するか、時間やイベントに応じてどう敵を出すか、どんな報酬を与えるか、画面にどんなボタンや選択カードを出すか、いつ勝ちとするか。 MOD が使うのは、すべて既存の機能です:[UI と入力](https://war3ai.com/ja/docs/ui-input/)(クリックできるボタン、カード、ホットキー、地面のクリック)、[キャンバス](https://war3ai.com/ja/docs/canvas/)(パネル、プログレスバー、ルート)、[JASS チャネル](https://war3ai.com/ja/docs/jass/)(ユニットの生成、属性の変更、アイテムの付与)、イベントストリーム(死亡、レベルアップ、スキル使用、チャット)。 ## 2 つのサンプル Farsight の「AI スキーム」→「組み込み」から選べます。 | MOD | 遊び方 | 使っている機能 | |---|---|---| | **ヒーローローグライク** `builtin/hero-roguelike` | あなたの手元にはパラディンが 1 体だけ。敵が四方からウェーブごとに押し寄せます。レベルが上がるたびに、画面中央の 3 択から強化を 1 つ選びます(選ぶ間ゲームは一時停止)。10 ウェーブ耐え抜けば勝ち、ヒーローが死んだら負け | `g.ui.choice`(クリックできるカード + 一時停止)、`hero.levelup` / `killed` / `spell.cast` イベント、チャットの `-help`、JASS によるヒーローの属性変更とアイテム付与 | | **エンドレスディフェンス** `builtin/endless-defense` | 敵は反対側のスタート地点から、地面に描かれた赤い線に沿って本拠地へ突撃してきます。1 ウェーブ守り切るごとにゴールドがもらえます。画面のボタンをクリックするか F7 を押すと次のウェーブを前倒しで呼べて、報酬は ×1.5。F8 を押してから地面を左クリックすると、無料の Guard Tower を 1 基置けます(右クリックでキャンセル) | `g.ui.button`、`g.ui.hotkey`、`g.ui.mouse`(地面のクリックを捕捉)、キャンバスのパネル / プログレスバー / ルート、JASS による敵の出現とゴールドの追加 | どちらのサンプルも 150 行ほどで、コードは `brains/examples/mod_hero_roguelike.py` と `brains/examples/mod_endless_defense.py` にあります。 ```bash python tools/play.py --bot brains/examples/mod_hero_roguelike.py --inst 9 # ゲームを起動し、MOD が進行を引き受ける。あなたはゲームウィンドウでプレイ ``` ## MOD を書く ```python from openwar3 import Mod class Survive(Mod): name = "survive" def on_start(self, g): super().on_start(g) # シングルプレイのチェック + コンピューター対戦相手を抑え込む self.foe = self.wave_player(g) # 空きスロットのプレイヤーを「ウェーブ側」にする:誰とも同盟せず、コンピューター AI もなし self.every(30, self.wave) # 30 ゲーム秒ごとに 1 ウェーブ(一時停止中は進まない) g.ui.hotkey("F7", lambda g, ev: self.wave(g)) def wave(self, g): self.spawn_ring(g, self.foe, "ugho", 6, self.home(g), 1400, attack_to=self.home(g)) def on_event(self, g, ev): if ev.kind == "unit.died" and ev.type == "htow": self.finish("loss", "本拠地が落とされた") ``` `Mod` は `Bot` に次のものを加えたものです。 | メソッド / 属性 | 説明 | |---|---| | `on_start / on_tick / on_event / on_end` | Bot と同じです。`on_start` / `on_tick` をオーバーライドするときは、先に `super()` を呼ぶのを忘れないでください | | `every(秒, fn, first=)` / `after(秒, fn)` | **ゲーム時間**で進むタイマー。コールバックは `fn(g)` | | `finish(result, reason)` | この試合を終わらせます(`'win'` / `'loss'` / `'unknown'`)。ランナーは次のティックで停止し、画面中央に結果パネルを描き、スキームの戦績はこれに従って記録されます | | `wave_player(g)` | 最初の空きスロットのプレイヤー。ウェーブ側として使います | | `spawn_ring(g, プレイヤー, ユニット, 数, 中心, 半径, attack_to=)` | 円周上にユニットを出します。1 ウェーブ数十体でも重くなりません。JASS ハンドルを返します | | `alive_of(g, プレイヤー)` / `attack_move_all(g, プレイヤー, 地点)` | あるプレイヤーの生存ユニット / 全員をアタックムーブで向かわせる(数秒おきに呼べば、敵が追いかけ続けます) | | `home(g)` / `hud(g, タイトル, 行)` | 自軍の本拠地の位置 / 右上の情報パネル | | `neutralize_ai = True` | 開始時にコンピューター対戦相手を抑え込みます:そのユニットを 5 秒ごとに一時停止させ、ゴールドと木材を 0 にします。対戦マップには必ずコンピューターが 1 人いるので、MOD が独自のルールを決めるときに邪魔をさせないためです | | `single_player_only = True` | 他に人間のプレイヤーがいれば実行を拒否します(ワールドを変える JASS は他の人との同期ずれを起こします) | | `linger_s = 6` | 勝敗が決まった後、結果画面で何秒止めてから終了するか | `finish()` は `Bot` でも使えます。通常の Bot も自分で終了を宣言できます。 ## スキームにして共有する `scheme.json` に `"kind": "mod"` と書き、エントリファイルで `Mod` のサブクラスを定義します。 ```json {"id": "survive", "name": "10 ウェーブ耐え抜け", "kind": "mod", "entry": "survive.py", "class": "Survive"} ``` MOD は常に**フェアモードを使わず**(お題を出すレフェリーなので、マップ全体を見てワールドを変える必要があります)、**対戦ルールでの勝敗判定もしません**(勝敗は `finish` で報告します)。マニフェストに `fair` / `judge` を書いても効果はありません。zip のエクスポート、インポート、信頼、戦績は Bot のスキームとまったく同じです。[AI スキーム](https://war3ai.com/ja/docs/schemes/) を参照してください。MOD もコードなので、他の人の MOD も初回実行前に同じく信頼の確認が必要です。 ## 実測 2026-09-25、テストインスタンスにて: - **ヒーローローグライク**:最初のウェーブが出現し、右上のパネルが動いている。ヒーローを 3 レベルに上げる → 画面中央にカードが出て、ゲーム時計が止まる。カードを 2 回クリック → 2 回の強化が反映され(筋力 22 → 27)、時計が再び動き出す。 - **エンドレスディフェンス**:パネル、地面のルート、ボタンがすべて表示されている。F8 + 地面をクリック → 本拠地の横に防御塔が 1 基増えた。ウェーブを片付けきる前にボタンをクリック → 「このウェーブはまだ片付いていません」と表示。 ## 制約 - **シングルプレイ専用**:ユニットの生成や属性の変更は JASS チャネルを通るため、マルチプレイでは同期がずれます。これはロックステップモデルによるもので、マルチプレイでの遊び方は同期チャネルを待つ必要があります([ロードマップ](https://war3ai.com/ja/roadmap/) を参照)。 - MOD はマップ全体を見ます —— お題を出す側であって、プレイヤーではないからです。 - 対戦マップのコンピューター対戦相手は「抑え込まれて」いるだけで、取り除かれてはいません(取り除くと対戦ルールの勝利判定が発動してしまいます)。 --- # Farsight コンソール > ローカルの Web コンソールで、唯一の入口でもあります。ゲームディレクトリの設定、各サービスの起動と停止、ゲームインスタンスの起動と停止、次のゲームの設定、AI の思考の確認、手動命令、ディレクター、対戦記録。 Farsight はあなたのマシン上で動く Web コンソールで、**127.0.0.1 でのみ待ち受けます**。システム全体の唯一の入口でもあり、試合の開始、AI の切り替え、ゲートウェイ、頭上の吹き出し、ローカル LLM はすべてここから操作できます。別のスクリプトを探す必要はありません。 ```bash start.bat # デプロイのチェック後、Farsight http://127.0.0.1:8866 を開く start.bat 5 6 # あわせて 5 番、6 番インスタンスのテストを開始(ゲーム + リファレンスブレイン) start.bat restart # Farsight のバックエンドだけを再起動(サーバー側のコードを変更したあとに使用。ゲームと各サービスには影響なし) stop.bat # すべてを完全に停止 ``` ポートは `openwar3.json` の `ports.console` で変更できます(デフォルトは 8866)。 ## コントロールセンター Farsight のトップページです。 - **ゲームディレクトリ**:自動検出するか自分で選びます。ゲームのバージョンを確認し、あなたのゲームからデータを抽出します。 - **ローカルサービス**:[ゲートウェイ](https://war3ai.com/ja/docs/gateway/)、[頭上のチャット吹き出し](https://war3ai.com/ja/docs/speech/)、ローカル LLM(LM Studio)、公式サイトのローカルプレビュー。どのカードでも起動、停止、再起動、ログの確認ができます。さらに、[MCP](https://war3ai.com/ja/docs/mcp/) サーバーがクライアントに登録されているかどうかも表示します。 - **環境チェック**:Python、ランタイムのファイル、ゲームデータ、AMAI データなど、各部分がインストールできているかどうか。 - **すべて停止**(右上):ゲームインスタンス、AI、ゲートウェイ、吹き出し、このシステムが使うローカルモデル、Farsight のバックエンドを順にすべて停止します。`stop.bat` をダブルクリックするのと同じです。MCP サーバーは Claude などのクライアントが管理しているため停止されません。LM Studio のアプリ本体も終了しません。 ## ページ | グループ | ページ | 役割 | |---|---|---| | ハブ | コントロールセンター | 前の節を参照 | | 対戦 | 概要 | 現在のインスタンスの対戦状況 | | | 戦場指揮 | マップビュー。手動で命令を出せます(手動命令はクレームテーブルで最も高い優先度:95) | | | ユニットデータ | 各ユニットのオーダー、タスク目標、マナ、ヒーローのレベルと経験値、スキルのクールダウン、インベントリ | | | AI の判断 / 戦闘の判断 | リファレンスブレインがこのティックで何を考えているか、各戦闘判断の詳細 | | | 戦略アドバイザー | [LLM アドバイザー](https://war3ai.com/ja/docs/llm-coach/) の状態:モデルサーバーが起動しているか、各インスタンスが接続されているか、直近の提案とその入力 | | | ディレクター | 自動カメラワーク、頭上の HP バー | | | 頭上の吹き出し | ユニットに話させる、ローカルモデルとの会話、農民の井戸端会議、カメラ会話、戦況トリガー、モデル設定。[頭上の吹き出し](https://war3ai.com/ja/docs/speech/) を参照 | | | 命令速度 | APM とコマンドのスループット | | | イベントと入力 | このゲームで起きたこと(スキル使用、チャットと画面メッセージ、ボタンのクリック、ホットキー、地面のクリック、選択、プレイヤーの退出)をカテゴリ別に絞り込んで表示。横にはマウス位置、ホバー中の項目、ローカルプレイヤーの選択。[UI と入力](https://war3ai.com/ja/docs/ui-input/) を参照 | | 記録 | ログ / 対戦記録 | 各インスタンスのログソース。各ゲームの結果、時間、兵力のピーク | | | 問題メモ | ゲーム中に Pause/Break で一時停止して時点を記録し、あとでここに説明を追記 | | システム | インスタンスと開始設定 | インスタンスの起動と停止。**次のゲーム**のマップ(対戦マップのほか、RPG / カスタムマップも選べます)、両陣営の種族、難易度、ゲーム速度を設定。インスタンスごとに AI スキームを 1 つ選択。「テスト開始」でゲーム + AI をワンクリックで起動 | | | AI スキーム | スキームのインポート、エクスポート、コピー、信頼、削除。インスタンスのスキーム切り替え(対戦中のゲームもその場で別の AI に引き継げます)、スキームごとの戦績の確認。[AI スキーム](https://war3ai.com/ja/docs/schemes/) を参照 | | | JASS コンソール | JASS スクリプトを書いて実行。右側で 1291 個の関数をカテゴリ別に検索し、クリックでスクリプトに挿入。変数は同じゲームの間ずっと保持されます。[JASS チャネル](https://war3ai.com/ja/docs/jass/) を参照 | | | 接続と拡張 | ゲートウェイの状態とワンクリック起動。開発 / プレイヤー / 観戦者の各ロール向けに生成した接続 URL、MCP の登録コマンドと設定。JS、Python のサンプル。[ゲートウェイ](https://war3ai.com/ja/docs/gateway/)、[MCP](https://war3ai.com/ja/docs/mcp/) を参照 | | | データとディスク | 録画、対戦記録、ログなどの実行データがそれぞれディスクをどれだけ使っているか、直近 1 日でどれだけ増えたか、どれを削除できるか(Farsight は何も自動では削除しません) | | | 設定 | ゲームディレクトリ、表示言語、外観(ダーク / ライト、モダン / Warcraft 風)など | | | フィードバックと提案 | 問題や提案をそのまま私たちに送信できます。診断情報はチェックを入れたときだけ添付され、送信前にプレビューできます | Ctrl + K でコマンドパレットを開きます。ページ移動、インスタンス切り替え、現在のゲームの終了、新しいブレインの起動ができます。 サイドバー下部の「最近の更新」には、Farsight と基盤に最近追加された内容が並びます。Farsight は起動時(その後は 6 時間ごと)に War3AI.com へ新しいバージョンがあるかを問い合わせ、あればお知らせします。Farsight が更新されるとページ上部にバナーが表示されます。入力中の内容を保存してから「再読み込み」をクリックしてください。 ## マルチインスタンス `runtime/farm.py` が複数起動を担当します(Farsight がインスタンスを起動・停止するときに代わりに呼び出します)。オリジナルの `War3.exe` ランチャーを `War3-.exe` という名前でコピーし(ゲームファイルは一切変更しません)、インスタンスごとに番号とディレクトリ(`bin/inst/`)を割り当てます。ゲームが終わると、`next_game.json`(コンソールの「インスタンスと開始設定」ページで変更しているのがこのファイル)に従って次のゲームを自動で開始します。 > **ヒント** > > Bot は `--inst N` で指定したインスタンスに接続します。コンソールの「インスタンスと開始設定」ページで使用中の番号を確認し、リファレンスブレインと番号が重ならないようにしてください。 ## ライブページ `http://127.0.0.1:8866/live` は OBS の「ブラウザソース」に入れるのに適したスクロールログのページで、AI の判断と戦況を表示します。 ## API コンソールのサーバーは、ローカルの REST + WebSocket API の集まりです(インスタンスの状態、次のゲームの設定、ユニットの詳細、手動命令、ログ、対戦記録、ディレクター、AI スキーム、JASS 呼び出し、キャンバス……)。Web ページはそのクライアントの 1 つにすぎず、どの言語のプログラムからでも直接呼び出せます。API の一覧は `console/server/app.py` のファイル先頭に書かれています。スキーム、JASS、キャンバスの 3 つの使い方は、それぞれ [AI スキーム](https://war3ai.com/ja/docs/schemes/)、[JASS チャネル](https://war3ai.com/ja/docs/jass/)、[キャンバス](https://war3ai.com/ja/docs/canvas/) を参照してください。 --- # AI スキーム > 1 つのスキームは 1 つの完全な AI です。Farsight でワンクリックで切り替えられ、対戦中のゲームでもすぐに別の AI が引き継げます。zip でエクスポートして共有し、他の人のスキームをインポートしてテストでき、スキームごとの戦績は自動で集計されます。 1 つの**スキーム** = 1 つの完全な AI:フォルダ 1 つ + マニフェスト `scheme.json` + コード。ゲームインスタンスごとにスキームを 1 つ選びます。Farsight でワンクリックで切り替えられ、**対戦中のゲームでもすぐに別の AI が引き継げます**。 他の人が共有したスキームをインポートすると**別枠**に入り、あなた自身のスキームとは互いに影響しません。変更したいときは「マイにコピー」します。 ```text schemes/ mine// マイスキーム:自分で書いたもの、または他のスキームからコピーして改造したもの(自由に変更でき、次のゲームから反映) installed// インストール済み:他の人が共有した zip をここに展開(初回実行前に信頼の確認が必要) brains/xwar3/ 組み込み:リファレンスブレイン(完全な AI) brains/examples/ 組み込み:4 つの学習用サンプル hello / rush / macro / micro、コンパニオンのサンプル buddy、2 つのゲームプレイ MOD(ヒーローローグライク、エンドレスディフェンス) ``` スキームは、あなたの代わりに戦う AI とは限りません。`kind: mod` のスキームは**ゲームのルール一式**で、あなた自身がプレイし、スキームがお題を出します。[ゲームプレイ MOD](https://war3ai.com/ja/docs/mods/) を参照してください。 ## Farsight で使う 「AI スキーム」ページ(左サイドバーの「システム → AI スキーム」): | 操作 | 内容 | |---|---| | スキームをインポート(zip) | `installed/` にインストールします。同じ id がすでにインストール済みなら、置き換えるかどうかを確認します(置き換えたら信頼の確認をやり直す必要があります) | | インスタンスに適用… | インスタンスを選び、「すぐに反映」(現在の AI を停止し、新しいスキームがこのゲームを引き継ぐ)または「次のテスト開始から反映」を選びます | | マイにコピー | `mine/` にコピーします。作者は「自分」、バージョンは 0.1.0 として記録し、どのスキームのどのバージョンからコピーしたかも記録します | | zip をエクスポート | `-<バージョン>.zip` にまとめます。これを人に送れば共有になります | | フォルダを開く | エクスプローラーでスキームのディレクトリを開き、コードを直接編集します | | 信頼 | 他の人のスキームは、初回実行前に必ずクリックする必要があります(後述の「信頼とセキュリティ」を参照) | | 最近の戦績 | このスキームの各ゲームの勝敗、時間、終了理由 | | 削除 | 削除できるのは「マイ」と「インストール済み」だけです。インスタンスが使用中のものは削除できません | インスタンスカードにも「AI スキーム」の行が加わりました:ドロップダウンでスキームを選ぶ →「切り替え(すぐに反映)」。インスタンスが実行されていないときはボタンが「選択」になり、次に「テスト開始」したときにそのスキームで AI を起動します。 ## マニフェスト scheme.json ```json { "format": 1, "id": "fast-rush", "name": "3 分ラッシュ", "version": "1.2.0", "author": "山田", "description": "この AI がどんな戦い方をするかを一言で", "entry": "rush_bot.py", "class": "RushBot", "fair": true, "hz": 5, "races": ["human", "orc"], "license": "MIT" } ``` | フィールド | 必須 | 説明 | |---|---|---| | `id` | ✔ | 小文字の英字、数字、`-`、`_`、2 〜 41 文字 | | `entry` | ✔ | スキームディレクトリ内の `.py` ファイル 1 つ(絶対パスと `..` は不可) | | `kind` | | デフォルトは `bot`(`openwar3.Bot` のサブクラスで、あなたの代わりに戦う)。`mod` = [ゲームプレイ MOD](https://war3ai.com/ja/docs/mods/)(`openwar3.Mod` のサブクラス。常にフェアモードを使わず、対戦ルールでの勝敗判定もしない) | | `class` | | エントリファイル内の Bot(または Mod)サブクラス名。省略すると、エントリファイル内の最後の `openwar3.Bot` サブクラスを使います | | `fair` | | デフォルトは `true`:視界内のものしか見えず、アリーナと同じルールです。`false` = マップ全体が見え、[JASS チャネル](https://war3ai.com/ja/docs/jass/) も使えるようになります(コンパニオンに必要) | | `judge` | | デフォルトは `true`:対戦ルールで勝敗を判定します。RPG / コンパニオンのスキームでは `false` にします | | `hz` | | `on_tick` を 1 秒に何回呼ぶか。デフォルトは 5 | | `format` | | マニフェストの形式バージョン。現在は 1。ローカルの OpenWar3 より新しいものは拒否され、更新を促されます | | その他 | | `name`、`version`、`author`、`description`、`races`、`license`、`homepage`、`forked_from` は表示専用 | スキームのディレクトリは Python のモジュール検索パスに追加されるので、エントリファイルから同じディレクトリにある他のファイルを `import` できます。サードパーティのパッケージ(numpy、torch……)は自動ではインストールされません —— 必要なものは `description` に明記してください。 **最小のスキームは、ファイル 2 つで十分です**: ```python # my_bot.py from openwar3 import Bot class MyBot(Bot): def on_tick(self, g): for w in g.idle_workers(): mine = g.nearest(g.gold_mines(), w) if mine: g.gather(w, mine) ``` ```json {"id": "my-first", "name": "はじめての AI", "entry": "my_bot.py"} ``` `schemes/mine/my-first/` に置き、Farsight を再読み込みすれば表示されます。もっと手軽な出発点:「組み込み」からサンプルを 1 つ選び、「マイにコピー」をクリックします。 ## 実行方法と戦績 スキームは**スキームランナー**が実行します(Farsight の「テスト開始 / 切り替え」で起動されるのがこれです)。 ```bash python tools/run_scheme.py --inst 20 --scheme builtin/micro --hours 6 ``` - インスタンスごとに常駐の監視プロセスが 1 つあり、**ゲームごとに子プロセスを 1 つ起動して**スキームを実行します。スキームのコードがクラッシュしても、監視プロセスは巻き込まれません。「マイスキーム」のコードを変更すると、次のゲームから自動で新しいコードが使われます。 - ゲームが終わるたびに戦績を 1 行記録します:スキーム、バージョン、作者、勝敗、理由、ゲーム時間、エラー回数。Farsight の勝率はここから集計されます。 勝敗の判定方法: | 状況 | 記録 | |---|---| | 相手の建物が全滅 | 勝ち | | 自軍の建物が全滅(兵が生き残っていても同じ —— 対戦ではこれで負けと判定されます) | 負け | | 自軍のユニットが全滅 | 負け | | Farsight で手動で終了 / 停止 | 未定 | | 切り替えた時点で、そのゲームがすでに 60 ゲーム秒以上経過していた(途中からの引き継ぎ) | 別途カウントし、**勝率には含めない** | | ゲーム時計が長時間進まない | 未定 | 勝敗がつくと、ランナーはリザルト画面を閉じ、「次のゲームの設定」に従って次のゲームを開始し、スキームが引き続き引き継ぎます —— 一晩中放置して戦績を貯めることができます。一時停止は終了とはみなしません。一時停止中も Bot は通常どおり動き、止まるのはゲーム時計だけです。 ## 信頼とセキュリティ **スキームはコードであり、実行時にはあなた本人と同じ権限を持ちます**(ファイルの読み書きもネットワーク接続もできます)。そのため: - `installed/` のスキームはデフォルトで**信頼されておらず**、あなたが「信頼」をクリックするまで、Farsight もランナーも実行を拒否します。 - 同じ id のスキームを置き換えてインストールすると、**信頼がリセットされます**(新しいバージョン = 新しいコード)。 - インポート時のチェック:zip は 50 MB 以下、ファイル数は 2000 以下。絶対パスと `..` は不可(スキームのディレクトリの外に書き込まれるのを防ぐため)。マニフェストが不正、またはエントリファイルが存在しない場合はその場で拒否します。 > **注意** > > 信頼する前に、「フォルダを開く」でコードに一通り目を通してください。スキームは信頼できる人からだけ受け取ってください。 ## API(スクリプト向け) | API | 説明 | |---|---| | `GET /api/schemes` | スキーム一覧 + 戦績 + 各インスタンスで選択中・実行中のスキーム | | `GET /api/schemes/results?ref=` | 1 つのスキームの直近 30 ゲーム | | `POST /api/schemes/import` | zip をインポート | | `GET /api/schemes/export?ref=` | zip をダウンロード | | `POST /api/schemes/fork` | マイにコピー | | `POST /api/schemes/trust` | 信頼 | | `DELETE /api/schemes?ref=` | 削除(インスタンスが使用中なら拒否) | | `POST /api/instances/{n}/scheme` | インスタンスのスキームを変更:このゲームをすぐに引き継ぐか、次のテスト開始から反映 | Python ではライブラリを直接使えます:`from openwar3 import schemes`(`list_schemes`、`install_zip`、`export_zip`、`fork`、`trust`、`stats`……)。 ## 今後:スキームサイト エクスポートした zip がそのまま共有の単位なので、サイトは外側に 1 層加えるだけで済みます。Farsight からワンクリックでアップロードし、サイトからダウンロードしたものは「スキームをインポート」とまったく同じチェックを通り、同じく信頼の確認が必要です。戦績の報告を選ぶこともでき、サイトはバージョンごとに勝率を集計します。Farsight には「スキームサイトで共有」ボタンの場所がすでに用意されています。進捗は [ロードマップ](https://war3ai.com/ja/roadmap/) を参照してください。 --- # 頭上の吹き出しとローカルモデル > ゲーム内の任意のユニットの頭上に、任意の役柄で吹き出しを表示します。ローカル LLM につなげば、1 文を入力すると 1 文の返答がユニットの頭上に表示されます。 吹き出しは観賞用のレイヤーです。勝敗には影響せず、配信、実況、デバッグに向いています。 - 任意のユニットが任意の役柄で話せます。複数のユニットが同時に話すこともできます。 - 吹き出しごとに、フォントサイズ、色、幅、しっぽ、透明度、タイピング速度を個別にカスタマイズできます。 - ローカル LLM(LM Studio)に直接接続でき、ストリーミング出力に対応します。生成しながら吹き出しを更新します。 ## Bot から使う 最も簡単なのは SDK 組み込みの `say` です。 ```python g.say(hero, "俺に続け!", seconds=4) ``` ## 起動と UI **いちばん手軽なのは Farsight のトップページ「コントロールセンター」です**。まず「ローカル LLM → 起動してモデルを読み込む」(LM Studio のローカルサーバー + 設定済みのモデルを VRAM に読み込む)をクリックし、次に「頭上のチャット吹き出し → 起動」をクリックします。カードではログの確認、停止、再起動ができます。 UI は Farsight 左側の「頭上の吹き出し」ページにあります。ユニットに話させる(ユニットを選ぶ、テキストを書く、スタイルを調整する、モデルと会話する)、農民の井戸端会議、カメラ会話、戦況トリガー、モデル設定があり、上部バーで選んだインスタンスに対して操作します。 コマンドラインでも起動できます。 ```bash python speech/speak_launch.py # ローカルモデルサーバーを起動 + モデルを読み込んでウォームアップ + 吹き出し API を起動 python speech/speak_launch.py --restart # コードを変更したあとに API を再起動 python speech/speak_launch.py --stop # API を停止し、モデルを VRAM からアンロード ``` 各ステップは「すでにあればスキップ」するので、何度実行しても副作用はありません。 ## HTTP API デフォルトは `http://127.0.0.1:8872/`(ポートは `openwar3.json` の `ports.speech`)で、どのプログラムからでも呼び出せます。 ### ユニットに話させる `POST /api/say` ```json { "inst": 16, "bubbles": [ { "unit": "0x14A12614", "name": "マウンテンキング", "text": "俺に続け!" }, { "unit": "0x14A12924", "name": "アークメイジ", "text": "ブリザードは任せろ。", "style": { "font_px": 26, "text_color": "#FFE080", "bg_color": "#C0102040" } }, { "screen": [960, 110], "key": 1, "name": "ナレーション", "text": "オークの第 1 波到着まであと 30 秒。", "style": { "tail": false, "type_ms": 0 } }, { "world": [-4684, 2644], "key": 2, "text": "集合地点", "style": { "font_px": 16 } } ] } ``` | フィールド | 説明 | |---|---| | `unit` / `world` / `screen` | いずれか 1 つ:ユニットに追従(HP バーがあればその真上に表示)/ マップ座標 / 画面ピクセル(ナレーション用) | | `name` | 1 行目に表示する話者名。自由に書けて、そのユニットである必要はありません | | `text` | 本文。自動で折り返します | | `duration_ms` | 表示時間。0 = 自動で 3 〜 5 秒 | | `key` | ワールド / 画面の吹き出しの番号。同じ key の新しいメッセージが古いものを置き換えます | | `update` | 同じ吹き出しがすでにある場合、テキストだけを差し替え、タイマーはリセットしません(ストリーミング出力用) | | `style` | `font_px`、`max_width_px`、`text_color`、`bg_color`、`border_color`、`tail`、`side_px`、`opacity`、`type_ms`、`font`…… | 同時に表示できる吹き出しは最大 32 個です。1 フレームあたりのコストは平均約 0.1 〜 0.2 ms です。 ### ローカルモデルと会話する `POST /api/chat` ```json { "inst": 16, "unit": "0x14A12614", "name": "マウンテンキング", "persona": "あなたは Warcraft のマウンテンキング、ムラディンを演じます。豪快で酒好き。口語で 1〜2 文、40 文字以内。", "message": "この先にオーガの群れがいる。突っ込むか?", "stream": true } ``` 返り値は `{"reply": "...", "first_token_ms": 283, "total_ms": 342}` で、この返答はすでにそのユニットの頭上に表示されています。同じユニットは直近 6 往復の会話を記憶します。 ### その他 | エンドポイント | 説明 | |---|---| | `GET /api/instances` | 実行中のゲーム | | `GET /api/units?inst=16&mine=true&heroes=true` | ユニット一覧(中国語名、座標、HP 付き) | | `POST /api/clear` | 吹き出しを 1 つ、またはすべて消去 | | `GET /api/llm`、`POST /api/llm` | モデル設定の確認 / 変更(`base_url`、`model`、`max_tokens`、`temperature`) | | `POST /api/banter` | 農民の井戸端会議:拠点のワーカーがキャラ設定に沿って順番にぼやき、開始時に口上を述べます(戦況はすべて実データ) | | `POST /api/camtalk` | カメラ会話:画面に映っているヒーローと従者が役柄に応じて会話します | | `POST /api/events` | 戦況トリガー:開戦、戦闘終了、ヒーロー戦死、ティアアップ、建物破壊……何かが起きたときだけ話します | ## ローカルモデルの選び方 RTX 5090 1 枚での実測(ゲームのセリフ 5 本): | モデル | VRAM | 速度 | 1 回の返答 | 結論 | |---|---|---|---|---| | **Qwen3.6-35B-A3B**(MoE、毎回 3B のみアクティブ)、Q4、思考オフ | 20.6 GB | 約 142 token/s | **約 0.3 秒**(最初のトークンまで約 0.27 秒) | 推奨:速く、中国語のロールプレイが自然 | | gpt-oss-20b(MXFP4)、推論 low | 11.3 GB | 約 280 token/s | 0.3 〜 0.8 秒 | VRAM が厳しいとき向け。中国語はやや平板 | | Qwen3.6-27B(dense)、Q4 | 17.2 GB | 約 39 token/s | 5.5 秒経ってもまだ思考中 | リアルタイム会話には不向き | - **速度は「毎回アクティブになるパラメータ数」で決まり、総パラメータ数ではありません**:35B の MoE は 3B しかアクティブにならず、27B の dense モデルより 3 〜 4 倍速くなります。 - **「思考」は必ずオフにしてください**:オフにしないと、トークンがすべて思考に使われ、1 文字も返答しません。 - 吹き出しは 1 秒あたり約 22 文字の速さで 1 文字ずつ表示されるため、生成速度はもはやボトルネックではありません。体験を本当に左右するのは**最初のトークンまでの遅延**です。 > **セリフを「本物らしく」する** > > モデルに渡す戦況は必ず実データ(試合数、勝敗、兵力、備蓄)にし、「これらの事実だけを使うこと」と明示してください。実測では、この制約がないとモデルは起きていない戦闘をでっち上げました。 --- # ゲートウェイ > WebSocket / JSON ゲートウェイ:Python SDK で呼べる公開 API を、JS、C#、Go、Rust、ブラウザのページ、別のマシン上のプログラムからも呼べます。3 つのロールがあり、JS クライアントとブラウザ用デモページが付属します。レイテンシはファストレーンに約 1 ms 加わるだけです。 ゲートウェイは、ファストレーンとプッシュされる状態を **WebSocket / JSON** で包みます。[API カタログ](https://war3ai.com/ja/api/) にある、Python SDK で呼べる公開 API は、JS、C#、Go、Rust、ブラウザのページ、別のマシン上のプログラム、LLM からも呼べます。メソッド名も引数も同じです。レイテンシはファストレーンに約 1 ms 加わるだけです。 **いちばん手軽なのは Farsight のトップページ「コントロールセンター」→ ゲートウェイ → 起動です**(停止、再起動、ログの確認、デモページを開く操作も同じカードにあります)。コマンドラインでは次のとおりです。 ```bash python gateway/server.py # ws://127.0.0.1:8870/ws(ポートは openwar3.json の ports.gateway) python gateway/server.py --open # 同上。ポートが待ち受けを始めたらデモページ http://127.0.0.1:8870/demo を開く python gateway/server.py --host 0.0.0.0 # LAN 向け:自動的にトークンを要求(bin/gateway/token.txt) python gateway/server.py --allow-origin http://localhost:5173 # 自作の Web ページからも接続できるようにする ``` ## 接続とロール 接続先:`ws://127.0.0.1:8870/ws?inst=9&role=dev`(`inst=` の代わりに `pid=` も使えます。トークンが必要なときは `&token=` を付けます)。 | ロール | 呼べるもの | 向いている用途 | |---|---|---| | `dev` | すべて:観測、コマンド、ゲーム制御、サンドボックス(JASS でワールドを変更)、UI の描画 | ローカルツール、[ゲームプレイ MOD](https://war3ai.com/ja/docs/mods/)、コンパニオン | | `player`(`&player=N` を付ける) | 観測、N 番プレイヤーのユニットの指揮、UI の描画。**デフォルトでフェアモード**で、N 番プレイヤーの視界内のものしか見えません(`&fair=0` で無効化) | あるプレイヤーの代わりに戦う Bot や LLM | | `observer` | 読み取り専用(下したコマンドはランタイムがそのまま拒否) | 観戦、実況、データ収集 | `player` が使えないもの:ゲームの終了、速度変更、一時停止といったゲーム制御。他プレイヤーの手の内が見える `players`、`enemy_ai_plan`。ゲームプロセスにローカルファイルを開かせる `canvas.image`。そして JASS。`resources`、`tech`、`stats` のようにプレイヤー番号を取るクエリは、自分の分しか照会できません。 1 つの接続が 1 つのセッションで、ファストレーンを 1 本占有します(ランタイム全体で 16 本)。ゲートウェイの同時セッションは最大 12 個で、Bot、MOD、Farsight のために何本か残しておきます。切断時に片付けるのは、そのセッション自身が描いたものとホットキーだけで、ほかのプログラムが描いたものには触れません。 ## メッセージ 接続すると、まず `hello` が届きます:プロトコルのバージョン、ロール、ゲームのプロセス ID、そのロールで呼べるメソッドの一覧。以降、各リクエストに `id` を付けると、応答にも同じ `id` が付きます。 ```json → {"id": 1, "op": "call", "method": "units", "args": ["me"]} ← {"id": 1, "ok": true, "result": [{"addr": 123456, "type": "hpea", "owner": 0, "x": -4480, "y": 2120, "hp": 220, "hp_max": 220}]} → {"id": 2, "op": "call", "method": "move", "args": [[{"unit": 123456}], 100, 200]} → {"id": 3, "op": "call", "method": "ui.button", "args": ["shop", "ポーションを買う"], "kwargs": {"screen": [40, 300]}} → {"id": 4, "op": "subscribe", "state": {"hz": 4, "units": "mine"}, "events": true} ← {"type": "state", ...} {"type": "events", ...} 以降、継続的にプッシュ → {"id": 5, "op": "overview"} 局面の要約:資源、兵種ごとの数、ヒーロー、見えている敵、生産 → {"id": 6, "op": "jass", "code": "call PingMinimapEx(0, 0, 3, 255, 0, 0, false)"} dev 専用 → {"id": 7, "op": "api"} メソッド一覧(ほかに ping / unsubscribe) ``` - **ユニット引数**は `{"unit": アドレス}` と書きます。アドレスはユニットの JSON にある `addr` です。`"handle": [lo, hi]` を付ければ、そのアドレスが別のユニットに再利用されていないかを照合できます。 - **メソッド名**は Game の公開メソッドそのもので、それに加えて `ui.*`(button / choice / toast / hotkey / mouse / cursor…)、`canvas.*`(text / panel / bar / image / circle / path / remove…)、`jass.<関数名>`(dev 専用)があります。 - リモートからはコールバック関数を渡せません。クリックやホットキーはイベントのプッシュから受け取ります。`ui.click` イベントには `key` が付きます。[UI と入力](https://war3ai.com/ja/docs/ui-input/) を参照してください。 - 1 件の呼び出しでエラーが起きても、返るのはその 1 件のエラーだけで(`ok: false` と `error`)、接続は切れません。送られてきたものが JSON でない場合も同じです。 - イベント JSON のフィールドは [W3P プロトコル](https://war3ai.com/ja/docs/protocol/) と同じで、さらに便利フィールド(`spell`、`key`、`text`、`chat`、`button`、`player`、`mods`)が付きます。 HTTP でも使えます。単発の呼び出しや curl に向いています:`GET /api?role=player` でメソッド一覧を取得し、`POST /call` に `inst`、`role`、`method`、`args`、`kwargs` を付けて 1 回呼び出します。`/call` はセッションを再利用します:ゲームを再起動してプロセスが変われば自動的に新しいセッションに切り替え、10 分間アイドルのセッションは片付けます。 ## クライアント **JS**(ブラウザまたは Node 22+、依存なし):`gateway/clients/js/openwar3.mjs` ```js import { OpenWar3, unit } from "./openwar3.mjs"; const ow = new OpenWar3("ws://127.0.0.1:8870/ws", { inst: 9, role: "dev" }); await ow.connect(); const mine = await ow.api.units("me"); await ow.api.move(mine.slice(0, 3).map(unit), 100, 200); await ow.api.ui.button("hi", "押してね", { screen: [40, 300] }); // 最後の普通のオブジェクト = キーワード引数 ow.on("event:ui.click", (e) => console.log("クリックされた", e.key)); await ow.subscribe({ state: { hz: 2, units: "mine" }, events: true }); ``` Node 20 / 21 では `--experimental-websocket` が必要です。完全な例は `gateway/clients/js/example.mjs` にあります。 **ブラウザ用デモページ** `http://127.0.0.1:8870/demo`:局面、自軍ユニットの表、ゲームにボタンを 1 つ置く操作、イベントストリームを 1 ページで確認できます。 **その他の言語**:任意の WebSocket ライブラリ + 上の JSON だけで十分です。共有メモリに触れる必要はありません。 **LLM**:[MCP サーバー](https://war3ai.com/ja/docs/mcp/) をそのまま使ってください。よく使う操作が既成のツールになっています。 ## 実測 2026-09-25、実際の試合につないで 1 項目ずつ検証し 16/16(ゲートウェイ 9 項目 + MCP 7 項目):ハンドシェイク(dev ロールで 121 個のメソッド)、`units('me')`、局面の要約、画面へのヒント表示、ボタンの配置。購読した状態でゲーム内のそのボタンをクリック → `ui.click` がクライアントにプッシュされる。JASS。不正なユニットを渡すとその 1 件だけがエラーになる。HTTP `/call`(observer ロール)。 JS クライアント(Node)とブラウザ用デモページも動作を確認しました:Web ページから置いたボタンをゲーム内でクリックすると、Web ページのイベントログに `ui.click` が届きます。 ## セキュリティ - デフォルトではローカルの `127.0.0.1` だけで待ち受け、トークンは不要です(Farsight と同じ)。`--host` にローカル以外のアドレスを指定すると、自動的にトークンを要求します。`--auth` を付けるとローカルでもトークンが必要になります。 - **ブラウザ上のほかのサイトからは接続できません**:ブラウザが開く接続には必ずオリジン(`Origin`)が付きます。ゲートウェイが受け付けるのは自身のデモページと `--allow-origin` で指定した URL だけです。Python、Node、curl のようなプログラムはオリジンを送らないので、通常どおり接続できます。ローカルだけで待ち受けているときは `Host` も照合し、外部のドメイン名をローカルに解決させる攻撃を防ぎます。 - ロールは接続時にクライアントが自己申告します。ローカルモードではこれは取り決めであって、セキュリティ境界ではありません。アリーナでは、誰にどのロールを与えるかをレフェリープロセスが決めます。[アリーナ](https://war3ai.com/ja/arena/) を参照してください。 --- # W3P プロトコル > ランタイムと外部プログラムの間の契約のすべて。8 つの共有メモリ、ワールド状態の読み取り、イベントの読み取り、コマンドの送信、レシート、レーンのロール、キャンバス、UI と入力を扱います。Python 以外の言語から接続する場合はこのページを読んでください。 ランタイムと外部プログラムは**共有メモリだけ**でデータをやり取りします。以下がそのすべてです。 - リファレンス実装は Python の `sdk/python/w3world.py`(読み取り)と `sdk/python/w3fast.py`(書き込み)です。各構造体のサイズとオフセットはここに定義されており、テストで固定されています。 - **プロトコルが記述するのは意味だけで、ゲームのバージョンとは無関係です。** ゲームのバージョンが変わってもランタイム側が対応し、プロトコルは変わりません。新しいフィールドはブロックの末尾にのみ追加されるため、古いクライアントもそのまま動きます。 > **補足** > > ほとんどの人はこのページを読む必要はありません —— Python SDK を使えば十分です。C++ / C# / Rust / Go などの言語から直接接続したい場合や、SDK の下で何が起きているかを知りたい場合にだけ必要になります。 ## 1. 8 つの共有メモリ `` はゲームのプロセス ID です。 | 名前 | 方向 | 内容 | 同期方式 | |---|---|---|---| | `Local\War3World_` | ランタイム → あなた | ワールド状態:ヘッダー + 16 プレイヤー + 最大 1024 ユニット + 256 件のユニット詳細 + 256 個の地面のアイテム + 拡張領域 + 生産テーブル | seqlock | | `Local\War3Trees_` | ランタイム → あなた | 最大 4096 個の破壊可能オブジェクト(木など)、2 秒ごとに更新 | seqlock | | `Local\War3Events_` | ランタイム → あなた | イベントリング、8192 件 | 各エントリが自身のシーケンス番号を持つ | | `Local\War3Map_` | ランタイム → あなた | マップ:地形グリッド(1 マス 128、最大 256×256)+ プレイ可能領域の境界 + スタート地点。試合開始後、数秒かけて分割して計算 | seqlock(計算完了後は変化しない) | | `Local\War3Fast_` | 双方向 | コマンドレーン:16 レーン × 16 スロット。各スロットにコマンド 1 件 + レシート。各レーンはロールを持つ | スロットごとに単一ライター・単一リーダー | | `Local\War3Canvas_` | あなた → ランタイム | [キャンバス](https://war3ai.com/ja/docs/canvas/):ヘッダー 64 バイト + 256 要素 × 112 バイト + 64 KB のテキスト / 点プール。`canvas_enable` を 1 回送ってから作成されます | seqlock(あなたが書き、ランタイムが毎フレーム読む) | | `Local\War3Msgs_` | ランタイム → あなた | 画面メッセージのリング:ゲームのヒント、チャット、システムメッセージの全文。128 件 × 256 バイト | 各エントリが自身のシーケンス番号を持つ | | `Local\War3Input_` | 双方向 | [UI と入力](https://war3ai.com/ja/docs/ui-input/):ランタイムがマウス位置、カーソルが指している地面の地点、ホバー中の要素を書き戻し、あなたはホットキー表とマウスのオン・オフを書き込みます。`input_enable` を 1 回送ってから、ランタイムが入力の横取りを始めます | ホットキー表は seqlock | **複数のクライアントが同時にキャンバスと入力を使う場合**:この 2 つのブロックはどちらも 1 つしかなく、各自がばらばらに書くと互いに上書きしてしまいます。取り決めは次のとおりで、自作のクライアントもこれに従ってください。 - **キャンバス**:名前付きミューテックス `Local\War3CanvasMutex_` を保持して読み取り - 変更 - 書き込みを行い、差し替えるのは自分の要素だけで、他のクライアントの要素はそのまま残します(プールのオフセットは詰め直します)。所有プロセスが終了済みの要素と、所有者のない要素は消します。要素の `reserved[1]` = 所有プロセスの ID、`reserved[2]` = プロセス内の連番。要素の番号はブロックヘッダーのオフセット 60 にあるカウンターから割り当てます(`0x10000` から)。 - **入力**:各クライアントは自分のホットキーとマウスのオン・オフを `Local\War3InputClients_`(ヘッダー 16 バイト + 16 クライアント × 528 バイト)に登録します。`Local\War3InputMutex_` を保持して自分のエントリを更新し、生きているクライアントの分をまとめて入力ブロックに書き込みます:ホットキーは「キーコード + 修飾キー」で重複を除き、マウスのオン・オフは和集合を取ります。イベントはすべてのクライアントに送られ、各自が「キーコード + 修飾キー」で自分のホットキーを見分けます。登録表にほかの生きているクライアントがいる間は、`input_enable 0` を送らないでください。 - **ランタイム**:所有プロセスが終了済みのクリック可能な要素は、クリックを横取りしなくなります。2 秒ごとに登録表を確認し、登録したクライアントがすべて終了していれば、入力ブロックのホットキー表とマウスのオン・オフをゼロに戻します。 ## 2. ワールド状態の読み取り(seqlock) ```text loop: s1 = block.seq (オフセット 8、int32) if s1 が奇数: リトライ (ランタイムが書き込み中) ヘッダー + players + units[unitCount] + details[detailCount] + items[itemCount] をコピー if block.seq != s1: リトライ ``` - **ヘッダー**:発行カウンタ(増えない = 発行が止まっている)、エンジンのゲームクロック、試合ごとに +1 される epoch、自分のプレイヤー番号、試合中かどうか、ゲーム速度、発行周期、この 1 回分をゲームスレッド上で収集するのにかかったマイクロ秒数、イベントのシーケンス番号、区間ごとの所要時間。クライアントは `requestedPeriodMs` に書き込んで発行周期(16 ~ 1000 ms)をリクエストできます。 - **ユニット**(112 バイト):ハンドルペア(**ユニットはハンドルペアで識別します**。アドレスは再利用されます)、型の 4 文字コード、所有者、フラグ、座標、HP / マナ(上限を含む)、現在のオーダー + オーダーのターゲット、タスクターゲット(実際に攻撃している相手)、ヒーローのレベル / 経験値 / スキルポイント、詳細のインデックス、`visibleTo`(ビット p = プレイヤー p が今それを見えている)。 - **詳細**(288 バイト、ヒーロー > プレイヤーのユニット > クリープの順に割り当て、最大 256 件):12 個のアビリティ(コード / レベル / フラグ / 残りクールダウン秒)、8 個の buff コード、6 スロットのインベントリ。 - **ブロック末尾の拡張**(追加のみで、前方のオフセットは動かさないため、古いクライアントもそのまま動きます):拡張領域 `EXT1`(ゲーム内時刻、昼夜の進行速度、生産テーブルの件数)と生産テーブル `prods[128]`(訓練 / 研究 / 建設 / アップグレード中の建物、キュー、総所要時間、経過時間、停止しているかどうか)。**magic が一致した場合にのみ使います。** ## 3. イベントの読み取り ```text head = ring.writeSeq (オフセット 8) for seq in (cursor, head]: e = ring.events[(seq - 1) % 8192] if e.seq > seq: 1 件取りこぼした(読むのが遅くて上書きされた) elif e.seq != seq: まだ書き込み途中。次回読む else: e を処理 ``` イベント構造体は 64 バイトです:`seq kind clockMs addr handleLo handleHi typeId owner a b x y value extra`。 - 連続する 2 回の発行を比較して得られるもの(精度 = 発行周期):`unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started`。 - エンジンレベル(ランタイムがゲームスレッド上でその場で記録するため、**1 発ごと**に発生):`damage`(ダメージ元、ダメージタイプ、攻撃タイプ、位置、実際に減った HP、アーマー適用前のダメージ)、`killed`(倒したユニット)。 - 生産テーブルの追跡から得られるもの:`production.done`(完了した 4 文字コード、種別、かかったゲーム秒数。相手のものも発行されます)。 - ランタイムが発行のたびにあわせてチェックするもの:`spell.cast`(スキルのクールダウン開始:`a` スキルの 4 文字コード、`b` レベル、`value` クールダウン秒数、`x/y` 詠唱地点)、`player.left`(`a` プレイヤー番号、`b` 新しいスロット状態)、`selection.changed`(ローカルプレイヤーの選択。全リストはワールドブロックの拡張領域にある)、`game.ended`(試合から退出)。 - 画面メッセージ:`message`(`a` = メッセージのシーケンス番号。全文は共有メモリ `Local\War3Msgs_` で引く:128 件 × 256 バイトで、ゲームのヒント、チャット、システムメッセージがすべて入っている。`b` = メッセージ枠の番号)。 - UI と入力(`input_enable` を有効にした後):`ui.click`(`a` キャンバス要素の id、`b` 1 左クリック / 2 右クリック)、`ui.hover`、`hotkey`(`a` ホットキーの id、`b` 仮想キーコード)、`mouse.world`(`x/y` 地面の座標、`value` = 1 はゲームに渡さず握りつぶしたことを示す)。修飾キーはいずれも `extra` に入る。 ## 4. コマンドの送信 1. **クライアントオブジェクト 1 つがレーン 1 本を占有します**:`Local\War3FastMutex_` を保持した状態で、空いている(または所有プロセスが死んでいる)レーンを探し、ロール、プレイヤー番号、自分の pid を書き込みます。同じプロセスで 2 種類のロールが必要なら 2 本開きます。 2. スロットを埋めます:セマンティックコマンドのフラグ、オペコード、`args[11]`、期限 `deadlineMs`。 3. すべてのスロットを書き終えたらコミット済みにし、レーンの `submitSeq` を 1 増やします。 4. `Local\War3FastDone__` イベントを待ち(またはポーリングし)、レシートを読んで、スロットを返却します。 ランタイムはゲームスレッドのイベントディスパッチ内でまとめて実行します。1 回の処理の時間予算は **4 ms**(実時間の高精度タイマー)で、超えた分は次のディスパッチに持ち越されます。**期限を過ぎたスロットは実行されません** —— 「一時停止から再開したら古いコマンドがもう一度実行された」ということは起きません。 `args` のインデックス:`0..2` ユニット(アドレス、ハンドル lo、ハンドル hi)、`3` オーダー ID または 4 文字コード、`4..6` ターゲット、`7/8` x / y(float のビット列)、`9` extra(プレイヤー番号 / スロット番号 / オン・オフ / キュー位置)、`10` mode(0 ターゲットなし / 1 地点指定 / 2 ターゲット指定)。 ### オペコード | オペコード | 名前 | 説明 | |---|---|---| | 1 | `point` | ユニットに地点指定の命令を出す(移動 / アタックムーブ / パトロール / 地面攻撃 / 地点指定の詠唱)。extra bit0 = キュー(現在のオーダーの後ろに差し込む) | | 2 | `target` | ユニットにターゲット指定の命令を出す(右クリック攻撃 / 採集 / 修理 / ターゲット指定の詠唱 / アイテムを拾う)。ターゲットは見えている必要があります | | 3 | `immediate` | ターゲットなしのコマンド(停止 / その場で待機 / 訓練 / 研究 / アップグレード / ターゲットなしの詠唱) | | 4 | `build` | ワーカーが建物を建てる(座標は 32 単位にスナップ) | | 5 | `learn` | ヒーローがアビリティを習得する | | 6 | `use_item` | インベントリの extra 番目のスロットを使う | | 7 | `revive` | 祭壇でヒーローを復活させる | | 8 | `rally` | 集結地点(地点指定 / ターゲット指定) | | 9 | `buy` | ショップが隣にいるヒーローにアイテムを売る | | 10 | `item_drop` | アイテムを手放す:味方に渡す、ショップに売る(`code` = 受け取るユニット)、または地面に置く | | 20 ~ 25 | クエリ | `q_tech` 技術カウント、`q_feasible` 実行可否、`q_visible` 可視性、`q_mine_gold` 金鉱の残量、`q_captain` コンピューターのキャプテン、`q_dead_heroes` 死亡ヒーロー一覧 | | 30 | `pause` | 一時停止 / 再開 | | 40 ~ 50 | カメラ | カメラ状態の読み取り、フィールドの設定、地点を見る、追従、リセット、回転、境界、スムージング、UI の表示切替、クリーンな画面、霧 | | 60 ~ 63 | HUD | クエストボタンの文字、クエストパネルのタイトルと説明、更新、パネルが開かれているかの読み取り | | 70 | `jass` | JASS native を名前で呼び出す(1291 個):名前と文字列引数はスロットの付加領域に、その他の引数はシグネチャに従って `args` に入れます。戻り値は `value[0]`。ローカルツールのレーン専用で、関数引数を取るものやスクリプトスレッドを中断させるものはすべて拒否されます。[JASS チャネル](https://war3ai.com/ja/docs/jass/) を参照 | | 71 / 72 | `jass_handle_of` / `jass_unit_of` | スナップショットのユニット ↔ JASS ハンドルの相互変換(スナップショットのハンドルペアは JASS ハンドルではありません) | | 73 | `canvas_enable` | キャンバスの共有メモリを作成し、描画フックを取り付けます。どのレーンからでも送れます(キャンバスは自分の画面にだけ描かれます)。初回はフックの取り付けがあるため、タイムアウトは 2 秒以上にしてください | | 74 | `input_enable` | `extra` = 1 でゲームウィンドウの入力(キャンバス要素のクリック / ホバー、ホットキー、地面のクリック)を横取りし、0 で返却します。入力ブロック `Local\War3Input_`:ヘッダー 128 バイト + ホットキー 32 件 × 16 バイト。あなたはホットキー表とマウスのオン・オフを書き込み、ランタイムはマウス位置、カーソルが指している地面の地点、ホバー中の要素を書き戻します。どのレーンからでも送れます(影響するのはローカルの入力だけです)。[UI と入力](https://war3ai.com/ja/docs/ui-input/) を参照 | ## 5. レシート レシートは 52 バイト(+ 所要時間 8 バイト)です:`status`、`engineReturn`、`verdict`(拒否の理由コード)、`orderBefore / orderAfter`(同じフレームで読み戻したユニットのオーダー)、`value[8]`(クエリ結果)、`execUs`(このコマンドがゲームスレッド上で実行に要したマイクロ秒数)、`engineUs`(そのうちエンジンの命令関数そのものにかかった時間)。 すべてのステータスコードと理由コードは [レシートと理由コード](https://war3ai.com/ja/docs/reason-codes/) を参照してください。 ## 6. レーンのロール | ロール | できること | |---|---| | `dev` | ローカルツール:セマンティックコマンド(ローカルプレイヤーのユニットを指揮)+ JASS チャネル | | `player` | セマンティックコマンドのみ。レーンが属するプレイヤーのユニットだけを指揮できます(他人のユニット = `not_owner`) | | `observer` | クエリ、カメラ、HUD パネル状態の読み取り、キャンバスとローカル入力の有効化のみ。それ以外はすべて `forbidden` | AI 同士の対戦 = 同じ試合で `player` レーンを 2 本開きます(player 0 / player 1)。 > **注意** > > ローカルモードでは、ロールはクライアントが自己申告します(取り決めであり、セキュリティ境界ではありません)。[アリーナ](https://war3ai.com/ja/arena/) ではレフェリープロセスがレーンを作成し、選手には `player` レーンだけを渡します。 ## 7. 実測済みのセマンティクス - 敵への右クリック(smart)= **その 1 体**を攻撃します(オーダーのターゲットもタスクターゲットもそのユニット)。生の攻撃オーダーをターゲットコマンドで送ると、攻撃オーダーに切り替わるだけでターゲットは記憶されず、近くの別の敵を攻撃しに行きます。 - エンジンは見えないユニットへのターゲットコマンドを許可しません:夜になって遠くのキャンプが戦場の霧に入ると、右クリックはすべて拒否されます(1001)。 - 建設が「受理」されたというのは、ワーカーが命令を受けたというだけです:木立の中の地点でもその場で受理され、ワーカーが到着してから失敗します。明らかに埋まっている地点はその場で拒否されます。 - ヒーローは死亡後約 3 ゲーム秒経たないと復活できません。人口が足りない場合も拒否されます(ヒーローも人口を使います)。 - インベントリ内のアイテムは地面のアイテムに含まれません。拾うと `item.removed` が発行されます。 - 一時停止中はエンジンクロックが止まりますが、コマンドは通常どおり送れます。 - 最小化した状態で起動したゲームはシミュレーションが止まっています(クロックが進みません)。 --- # レシートと理由コード > 各コマンドのレシートにはステータスコードと理由コードが含まれます。これは Bot と Agent が自己修正するための根拠で、「なぜうまくいかなかったか」を機械可読な数値に変えます。 ```python r = g.train(barracks, "hfoo") bool(r) # False r.status # 1 -> rejected r.verdict # 3 -> 人口不足 r.reason # 'rejected(人口不够)' r.exec_us # このコマンドがゲームスレッドで実行された時間(マイクロ秒) ``` `if r:` は `r.status == 0`(エンジンが受理した)と同じ意味です。 ## ステータスコード `status` | コード | 名前 | 意味 | よくある原因 | |---|---|---|---| | 0 | `accepted` | エンジンが受理した | —(ただし受理 ≠ 成功。後述) | | 1 | `rejected` | エンジンに拒否された | `verdict` を確認 | | 2 | `bad_unit` | ユニットが存在しない、またはハンドルが一致しない | ユニットがすでに死んでいる。古いユニットオブジェクトを使った | | 3 | `not_owner` | 自分のユニットではない | `player` として他人のユニットに命令した | | 4 | `fault` | 実行時の例外(ランタイムが捕捉済みで、ゲームは落ちない) | 再現手順を添えて報告してください | | 5 | `bad_args` | 引数エラー | 座標、グリッド番号、4 文字コードの誤り | | 6 | `unsupported` | 未対応 | このバージョンのランタイムにはこの機能がない | | 7 | `bad_target` | 無効な目標 | 目標がすでに消えている。目標の種類が違う | | 8 | `forbidden` | レーンのロールで許可されていない | `observer` として命令した | | 97 | `cancelled` | バッチブロック内で例外が発生し、バッチ全体が送信されなかった | `with g.batch():` ブロック内のコードでエラー | | 98 | `held` | ユニットがより優先度の高い層に確保されていて、送信されなかった | リファレンスブレインのリフレックス層や、コンソールからの手動命令がこのユニットを確保している | | 99 | `timeout` | タイムアウト | ゲームの一時停止やカクつきで期限を過ぎた(期限切れのコマンドは実行されない) | ## 理由コード `verdict` 拒否されたとき、ランタイムはエンジン自身の実行可否チェックを使って理由を示します。命令を出さずに先に問い合わせることもできます。`g.can_do(ユニット, 4文字コード)` は同じコードを返します。 | コード | 意味 | 対処 | |---|---|---| | 0 / 220 | 可能 | — | | 3 | 人口不足 | 人口建物を建てる。`g.production(b).blocked` で早めに検出 | | 8 | ゴールド不足 | 資金を待つ。命令前に `g.can_afford(code)` | | 9 | 木材不足 | 伐採の人数を増やす | | 32 | 訓練キューが満杯(7 枠) | キューは 1 体だけ:`g.queue(b)` が空になってから追加 | | 183 | 前提テクノロジー / 建物が不足 | 先に前提の建物を建てる、ティアアップする | | 185 | 建物が使用中 | 祭壇がヒーローを蘇生中。タウンホールのキューが空でないとアップグレードできない | | 221 | 該当項目なし / 建設中 / アップグレード中 / すでに存在 | ヒーローがすでにいる(死んだら `revive`)。このショップでは売っていない | | 89 | ショップに未入荷 | 開始時はアイテム表の入荷時間になるまで在庫がない。新しく建てたショップは完成した時点からカウントが始まる | | 1001 | 目標が見えない | 目標が戦場の霧かブラックマスクの中にいる。その位置に `attack_move` | ## 受理 ≠ 成功 レシートが示すのは「エンジンがこのコマンドを受け取った」ことだけで、同じフレーム内で読み戻したものです。その後に起こることは関知しません。 | コマンド | レシートで受理されても失敗しうるケース | 確認方法 | |---|---|---| | 建設 | 森の中の地点でもその場では受理され、ワーカーが到着してから失敗する | `build_near` を使う(建設が始まったかを追跡する)か、`production.done` を待つ | | スペル使用 | 中断された、マナ不足 | 次のティックで `g.cooldown(u, スキル)` がクールダウンに入ったか確認 | | 訓練 | キューには入ったが、人口不足でいつまでも始まらない | `g.production(b).blocked` | | 移動 / 攻撃 | 別のロジック(またはより優先度の高い層)に上書きされた | `g.current_target(u)`、`g.order_of(u)` | ## クエリ API 以下の API は命令を出さず、エンジンに問い合わせるだけです。結果もレシートの `value` に入ります(SDK は値を直接返します)。 | API | 戻り値 | |---|---| | `g.can_do(u, code)` / `g.can_do_many([(u, code), ...])` | 上表の理由コード | | `g.tech(code, player=None)` / `g.tech_many([...])` | 研究レベル / 完成した建物数(アップグレード系列を含む) | | `g.visible(x, y)` | この地点が自軍から見えているか | | `g.gold_left(mine)` | 金鉱の残りゴールド | | `g.enemy_ai_plan(敵ユニット)` | コンピューター対戦相手の隊長が兵をどこへ向かわせるか(コンピューター AI にのみ有効) | --- # データの出どころ > データの種類ごとの取得元と精度。「何かおかしい」と思ったら、まずこのページを確認してください。 | データ | 取得元 | 精度 | |---|---|---| | ユニット、資源、オーダー、スキル、バフ、インベントリ | ランタイムが 50 ms ごとにプッシュするワールドブロック | 発行周期(16 ms まで調整可能) | | ダメージ、キルイベント | ランタイムがゲームスレッド上でその場で記録。1 回ごと | 即時 | | その他のイベント(出現、死亡、オーダー変更、レベルアップ……) | 連続する 2 回の発行を比較 | 発行周期 | | 生産表(訓練 / 研究 / 建設 / アップグレード) | エンジンの生産アビリティのタイマーフィールド + ランタイムによる経過時間の累積 | 約 ±0.2 ゲーム秒 | | 戦闘ステータス、相性表 | ゲーム付属のデータ表(あなたのマシンのゲームから抽出) | アイテム、オーラ、バフによる補正は含まない | | 経路探索 | エンジンの地形の通行可能性(1 マス 128)+ 樹木 + 建物の占有範囲、SDK 側の A* | 1 マス。1 マスより狭い隙間は通れないと判定 | | ゲーム内時刻 | JASS `GetFloatGameState(GAME_STATE_TIME_OF_DAY)` | 発行周期 | | 可視性 | ランタイムがユニットごと・プレイヤーごとに計算した可視性マスク | 発行周期 | | テクノロジー数、実行可否、金鉱の残量 | ファストレーンのクエリで、エンジンに直接問い合わせ | 即時 | ## ゲームデータはコードと一緒に配布しない ユニット表、スキル、アイテム、ヒーロー、バフ、ダメージ相性表などは Blizzard のゲームファイルに由来するため、**リポジトリには含めません**。Farsight の「コントロールセンター」でゲームディレクトリを設定すると、あなた自身のゲームから自動で抽出されます。手動で実行することもできます。 ```bash python data/tools/extract_game_data.py ``` 抽出結果は `data/game/`(git には含めない)に置かれます。元の `.slk` / `.txt` と、整形済みの `units.json`、`names.json`、`skills.json`、`items.json`、`heroes.json`、`buffs.json` です。 ## 具体的な数値 | 項目 | 値 | |---|---| | 1 日 | 480 ゲーム秒(昼と夜がそれぞれ 240 秒)、1 時間 = 20 ゲーム秒。開始時は朝 8 時 | | 昼 | 6:00 〜 18:00 | | アーマー係数 | 0.06(ゲームのデータ表より) | | ワールドブロックの容量 | プレイヤー 16、ユニット 1024、ユニット詳細 256、地面のアイテム 256、生産 128 | | 樹木 | 破壊可能オブジェクト最大 4096、2 秒ごとに更新 | | イベントリング | 8192 件。読み取りが遅すぎると取りこぼす(SDK が検出可能) | | マップのマス | 1 マス 128 ゲーム単位、最大 256 × 256 | ## 実測で校正した例 - 生産時間:ピーザント 14.9、ファーム 34.9、Iron Forged Swords 59.9 ゲーム秒。ランタイムがプッシュした値と一致(誤差 0.2 秒未満)。 - 戦闘ステータスをゲーム内のパネルと照合:パラディン HP 650、マナ 255、アーマー 3.9、攻撃力 24 〜 34。攻撃アップグレード 1 段階のフットマン 13 〜 15。 - エンジンのダメージイベントにあるアーマー適用前のダメージ(14 / 15 / 15)は、`stats()` が計算した範囲に収まっている。 - 経路探索:Echo Isles 116 × 88 マス、相手のタウンホールまでの地上距離 10642(直線 9856)、グリッド構築 18 ms、A* 1 回約 1 ms。 --- # よくある質問 > これはチートツールですか?どのバージョンに対応していますか?AI は何を見て、何ができますか?プログラミングができなくても使えますか?…… ## これはチートツールですか? いいえ。AI の研究と娯楽のための開発用インターフェースで、**あなた自身が正規に所有するクライアント**でのみ使用します。ローカル、LAN、または自分で立てたゲームで、コンピューターや他の AI と対戦するためのものです。**Battle.net やアンチチートを備えたサーバーでは使用できません**。人間同士の対戦を対象とした機能も一切提供しません。詳しくは [利用範囲](https://war3ai.com/ja/docs/legal/) を参照してください。 ## どのゲームバージョンに対応していますか? 現在は **Warcraft III 1.27**(The Frozen Throne)のみに対応しています。1.24 〜 1.28 は同じエンジン構造で、複数バージョン対応(バージョンごとのシンボル表の選択、シグネチャによるフォールバック、起動時セルフチェックによる機能一覧の生成)は [ロードマップ](https://war3ai.com/ja/roadmap/) の P4 フェーズで行います。1.29 以降のバージョンと Reforged は別のエンジンのため個別の対応が必要で、現時点では対応を約束していません。 ## ゲームファイルを変更しますか? いいえ。ランタイムはゲームの実行中に注入され、**ディスク上の Game.dll** やその他のゲームファイルは変更しません。複数起動する場合も、オリジナルの `War3.exe` ランチャーをそのままコピーしてリネームするだけです。ゲームデータ(ユニット表など)はあなた自身のゲームから抽出し、コードと一緒には配布しません。 ## AI には何が見えますか? プロプレイヤーが知りたいことはほぼすべてで、50 ms ごとに更新されます。 - 全プレイヤーのゴールド、木材、人口。全ユニットの位置、HP・マナ、現在のオーダー、**攻撃中の相手**、レベルと経験値。 - ヒーローとユニットのスキルレベルと残りクールダウン、かかっているバフ、インベントリ。 - 各建物が訓練 / 研究 / 建設 / アップグレードしている内容と進捗、人口不足で止まっているかどうか。 - 地面のアイテム、樹木、マップの通行可能 / 建設可能グリッド、スタート地点、ゲーム内時刻(昼夜)。 - イベントストリーム:ユニットの出現と死亡、**1 回ごとのダメージ**(攻撃者、攻撃タイプ、アーマー適用前のダメージ)、キル、生産完了、ヒーローのレベルアップ…… - エンジンに直接問い合わせることもできます。ある操作がいま可能か、不可能ならその理由。あるテクノロジーのレベル。ある地点が見えているか。金鉱の残量。コンピューター対戦相手がどこへ兵を向けようとしているか。 その上で SDK は、戦闘ステータス(相性、アーマー、攻撃/防御アップグレード)、「倒すまでに何秒かかるか」、地上の経路探索も計算して提供します。すべての API は [API カタログ](https://war3ai.com/ja/api/) を参照してください。 ## AI は何ができますか? プレイヤーができる操作はほぼすべて可能です。移動、アタックムーブ、指定目標への攻撃、停止、ホールドポジション、パトロール、地面攻撃、採集、修理、建設(配置場所の自動探索あり)、訓練 / 研究 / アップグレード、キャンセル、スキル習得、スペル使用(ユニット対象 / 地点対象 / 対象なし)、ラリーポイント、ヒーローの蘇生、アイテムを拾う / 使う / 捨てる / 渡す / 売る、購入、Call to Arms。Shift キュー、ウェイポイントに沿った行軍、1 体のワーカーによる連続建設。さらにゲーム速度、一時停止、頭上の吹き出しも使えます。すべてのコマンドにレシートがあります。 プレイヤーの操作に加えて、ゲーム画面に自分のパネルや注釈を描くこと([キャンバス](https://war3ai.com/ja/docs/canvas/))や、シングルプレイでマップ作者が使える 1291 個の JASS 関数を呼び出すこと([JASS チャネル](https://war3ai.com/ja/docs/jass/))もできます。 ## RPG / カスタムマップでも使えますか? 使えます。RPG マップを選び、インスタンスに「コンパニオンのサンプル」スキームを設定すれば、ゲーム開始後はあなた自身がプレイし、そばには一緒に戦い、回復してくれ、話し相手にもなる AI の仲間がついて来ます。[RPG コンパニオン](https://war3ai.com/ja/docs/companion/) を参照してください。`g.map_data` でマップのカスタムユニットの名前を読み取れます。[JASS チャネル](https://war3ai.com/ja/docs/jass/) ではユニットの生成、同盟の設定、パネルの表示などができます……どう遊ぶかはあなた次第です。ワールドを変える操作はシングルプレイでのみ使えます(マルチプレイでは同期ずれが起きます)が、キャンバスはマルチプレイでも安全です。 ## プログラミングができなくても使えますか? 使えます。[クイックスタート](https://war3ai.com/ja/docs/quickstart/) に従って環境を整え、[LLM で Bot を書く](https://war3ai.com/ja/docs/ai-bot/) を読んでください。あなたは普段の言葉で戦術を説明し、LLM がコードを書きます。動かして問題があれば、エラーメッセージやゲーム内で見た現象を伝えて修正させます。 ## Python しか使えませんか? SDK は Python です。ランタイムと外部プログラムの間にあるのは共有メモリのプロトコル([W3P](https://war3ai.com/ja/docs/protocol/))だけなので、Windows の共有メモリを読み書きできる言語なら何でも接続できます。もっと手軽なのは [ゲートウェイ](https://war3ai.com/ja/docs/gateway/)(WebSocket / JSON)です。JS、C#、Go、Rust、ブラウザのページ、別のマシン上のプログラムから同じ API を呼べます。LLM の Agent は [MCP](https://war3ai.com/ja/docs/mcp/) に直接つなげます。 ## どの LLM が一番いいですか? コードを書ける主要なモデルならどれでも使えます。大切なのはモデルではなく、**正しい資料を渡すこと**(マニュアル + `api.json` + サンプル 1 つ)と、API カタログに存在するメソッドだけを使うよう指示することです。ゲーム中のリアルタイムな判断(アドバイザー、セリフ)はレイテンシに敏感で、ローカルの MoE モデルが良い結果を出しています。[LLM をアドバイザーに](https://war3ai.com/ja/docs/llm-coach/) と [頭上の吹き出しとローカルモデル](https://war3ai.com/ja/docs/speech/) を参照してください。 ## ゲームが重くなりませんか? ワールドステートの 1 回の収集は、ゲームスレッド上で中央値 0.5 〜 0.9 ms(ユニット 100 〜 120 体)で、50 ms ごとに 1 回です。コマンドはゲームスレッド上で 1 件あたり数マイクロ秒、キューの 1 回の処理には 4 ms の時間予算があり、終わらなかった分は次回に回すので、ゲームを止めることはありません。ゲームへの呼び出しはすべて例外保護されており、Bot がクラッシュしてもその陣営が止まるだけで、ゲームを巻き込んで落とすことはありません。 ## 複数のゲームを同時に起動できますか? できます。`runtime/farm.py` がマルチインスタンスを管理し、インスタンスごとに番号が付きます。起動と停止は [Farsight コンソール](https://war3ai.com/ja/docs/console/) で行います。Bot は `--inst N` で指定したインスタンスに接続します。 ## 2 つの AI を対戦させられますか? 同じゲームで `player` チャネルを 2 本開けば(`--player 0` / `--player 1`)AI 対 AI になります。ローカルモードでの公平性は取り決めに頼ることになります。レフェリー、視界フィルタリング、所有権チェックを備えた正式な対戦は [アリーナ](https://war3ai.com/ja/arena/)(P6 フェーズ)で行います。 ## Mac / Linux に対応していますか? 現在は Windows 10 / 11 のみに対応しています。 ## ライセンスは? ライセンスは正式版と同時に公開します。サードパーティのコンポーネントはそれぞれのライセンスを保持します(例:MinHook は BSD-2)。AMAI は独自ライセンスのため、その派生データはプロジェクトと一緒には配布せず、インストール時に AMAI の公開リポジトリから取得して生成します。 ## 問題はどこに報告すればよいですか? 正式版のリリース後に問題報告の窓口を開設します。報告の際は、インスタンス番号、`python -m openwar3 status` の出力、再現手順を添えてください。まずは [デバッグとパフォーマンス](https://war3ai.com/ja/docs/debugging/) で解決できないか確認してみてください。 --- # 利用範囲 > できること、できないこと、本サイトのアクセス解析、そして商標とサードパーティのライセンスについて。本プロジェクトを使用することで、これらの範囲を守ることに同意したものとみなされます。 ## できること - **あなた自身が正規に所有する** Warcraft III 1.27 クライアントで使用すること。 - ローカル、オフライン、LAN、または自分で立てたゲームで、AI をコンピューター対戦相手や他の AI と対戦させること。 - 自分の AI の対戦を研究、教育、娯楽、配信に使うこと。 - SDK、リファレンスブレイン、サンプル、ツールをもとに、それぞれのライセンスに従って二次開発すること。 ## できないこと - **Battle.net、またはアンチチートを備えたあらゆるサーバーやプラットフォームで使用してはいけません**。アンチチートが有効なセッションと同時に使用することもできません。 - 人間同士の対戦で不正な優位を得るために使用してはいけません。 - Blizzard のゲームファイルや、そこから抽出したデータを配布してはいけません(本プロジェクトも配布しません。ゲームデータは利用者が自分のゲームから抽出します)。 - ランタイムの利用許諾に従ってください。 ## 技術面での約束 - ディスク上の `Game.dll` やその他のゲームファイルは変更しません。すべての変更は実行時に行われます。 - 複数起動は、オリジナルの `War3.exe` ランチャーをそのままコピーしてリネームするだけです。 - プロジェクトには Blizzard のコードやゲームファイルは一切含まれていません。 ## あなたの責任 リバースエンジニアリングやゲームの改変に関する法律は地域によって異なります。**お住まいの地域で本プロジェクトを使用することが合法かどうかは利用者自身が確認し、使用の結果については利用者自身が責任を負うものとします。** 本プロジェクトは「現状のまま」提供され、明示または黙示を問わず、いかなる保証も伴いません。 ## 本サイトのアクセス解析 本サイト(war3ai.com)では、Microsoft Clarity でアクセス状況を計測しています。どのページを見たか、どこから来たか、どれくらい滞在したか、どこをクリックしどこまでスクロールしたか、そして匿名の閲覧リプレイとヒートマップです。これはドキュメントとページの改善にのみ使います。 - 登録は不要で、氏名やメールアドレスのような個人を特定する情報は収集しません。入力欄の文字はデフォルトで隠され、記録されません。 - Clarity はブラウザに Cookie を保存し、同じ訪問者の複数回の訪問を区別します。データは Microsoft が処理します。[Microsoft のプライバシーに関する声明](https://privacy.microsoft.com/privacystatement) を参照してください。 - 計測されたくない場合は、本サイトの任意の URL の末尾に `?stats=off` を付けて一度開いてください。そのブラウザでは以後計測されません(`?stats=on` で元に戻ります)。ブラウザのトラッキング防止機能で `clarity.ms` をブロックしてもかまいません。サイトは通常どおり使えます。 ローカルの Farsight、SDK、ランタイムには、このような計測は含まれていません。Farsight が war3ai.com に接続するのは次の 2 つの場合だけです。1 つは、起動時とその後 6 時間ごとにバージョン一覧を読み込み、新しいバージョンがあるかを確認するとき。もう 1 つは、「フィードバックと提案」ページで送信をクリックしたときで、あなたが書いたフィードバックと 1 つのマシン識別子(システムの識別番号にソルトを加えてハッシュ化したもので、元の値は逆算できません。連投の防止に使います)を送信します。診断情報はチェックを入れたときだけ添付され、送信前にプレビューできます。 これら 2 種類のリクエストが war3ai.com に届くと、サーバーは IP アドレス、Cloudflare が判定した国または地域、クライアントのバージョン(User-Agent)を記録します。これは不正利用の防止と、稼働している Farsight の台数の統計のために使います。バージョン確認の記録は 90 日後に自動的に削除されます。フィードバックはこれらの情報とともに、メンテナーが対応して削除するまで保存されます。これらのデータはプロジェクトのメンテナーだけが管理画面で確認でき、他者に提供されることはありません。 ## 商標 Warcraft® は Blizzard Entertainment, Inc. の商標または登録商標です。War3AI / OpenWar3 は独立したコミュニティプロジェクトであり、Blizzard Entertainment とは関係がなく、その承認や後援も受けていません。本文中に登場するその他の製品名(Claude、GPT、Gemini、Qwen など)はそれぞれの所有者に帰属し、互換性の説明のためにのみ使用しています。 ## サードパーティのコンポーネントとデータ | コンポーネント / データ | ライセンス | 取り扱い | |---|---|---| | MinHook | BSD-2-Clause | ランタイムとともに使用し、ライセンス表記を保持 | | AMAI | 独自ライセンス | プロジェクトと一緒には配布しない。`start.bat` がデプロイ時に AMAI の公開リポジトリから取得し、ツールでリファレンスブレイン用のデータを生成 | | ゲームデータ(ユニット、スキル、アイテムなど) | Blizzard | プロジェクトと一緒には配布しない。利用者が自分のゲームから抽出 | | 公開試合のリプレイから抽出した事実データ(建物の配置、序盤のビルド順) | — | 事実データのみ。リファレンスブレインで使用 | --- # API カタログ(api.json) ステータス:verified = 内部経路を実機検証済み、experimental = 新しい API(動作は確認済み、項目ごとの実機検証を継続中)、inferred = 推定 / 未完全検証。レイテンシ:プッシュ型スナップショット(共有メモリを読むだけで、ゲームスレッドを待たない(約 0.05 ms)); ファストレーン(約 1 フレーム:ゲームスレッドでバッチ実行); コントロールチャネル(20~40 ms(UI 系操作の旧経路)); 直接書き込み(ゲームスレッドのキューを通さない:共有メモリへの書き込み(キャンバス)、またはゲームウィンドウへのメッセージ送信); ローカル計算(純粋な計算またはファイルの読み取りで、ゲームに触れない) ## 観測 状態を読むだけで、ゲームは変更しません。ほとんどはプッシュ型スナップショットを直接読むため、待ち時間ゼロです。 - `snapshot(max_age: 'float' = 0.05)` [verified] [プッシュ型スナップショット] マップ全体の完全な状態(WorldState):.units .players .items .clock .me。max_age 秒以内の再呼び出しは同じものを返します。 ⚠ 金鉱に入っているワーカーは一覧に含まれません。デフォルトではマップ全体が見えます(ロックステップモデルではローカルにすべてのデータがあるため)。Game(fair=True) のときだけ視界でフィルタリングされます。 (内部: W3P ワールドブロック Local\War3World_(ランタイムが 50 ms ごとにプッシュ、seqlock)) - `last_seen(owner: 'str | int' = 'enemy', max_age: 'float | None' = None) -> 'list'` [verified] [プッシュ型スナップショット] 最後に見た敵(または 'creep' クリープ、あるいは特定のプレイヤー番号)のユニット:[(そのときのユニットの状態, そのときのゲームクロック, 経過秒数)]。新しいものが先頭です。 死んだのを見たら一覧から削除されます。フェアモードでも通常モードでも「自軍が今見えているもの」を基準に記録します —— これがプレイヤーの頭の中にある地図です: 偵察した兵力、相手のヒーローを最後に見た場所、相手がいつ拡張したか。max_age を指定すると、その秒数(ゲーム秒)以内のものだけを返します。 (内部: プッシュスナップショットの visibleTo(スナップショットを更新するたびに、見えている敵 / クリープのユニットを記録)) - `map()` [verified] [プッシュ型スナップショット] この試合の地形表 MapInfo:.walkable(x,y) .buildable(x,y) .at(x,y) .bounds(プレイ可能領域).starts(スタート地点).cells(bit0 通行不可、bit1 建設不可)。 試合開始後、計算完了まで数秒かかり、完了前は None を返します。木は含まれません(trees() を使います)。 (内部: W3P マップブロック Local\War3Map_(試合開始後にランタイムが分割して計算、IsTerrainPathable で通行 / 建設を判定)) - `me() -> 'int | None'` [verified] [プッシュ型スナップショット] 自分のプレイヤー番号(0~11)。 (内部: ワールドブロックのヘッダー) - `resources(player: 'int | None' = None) -> 'dict | None'` [verified] [プッシュ型スナップショット] {'gold','lumber','food_used','food_cap','gold_gathered','lumber_gathered'}。player のデフォルトは自軍で、どのプレイヤーのものも読めます。 読み取れない場合は None を返します。0 とみなさないでください。 (内部: ワールドブロック players[16]) - `players() -> 'list'` [verified] [プッシュ型スナップショット] 全 16 プレイヤースロット:Player(id, gold, lumber, food_used, food_cap, gold_gathered, lumber_gathered, race, known)。 (内部: ワールドブロック players[16]) - `units(owner: 'str | int' = 'all', types=None, alive: 'bool' = True) -> 'list'` [verified] [プッシュ型スナップショット] ユニットを所有者 / 型で絞り込みます。owner:'me' / 'enemy' / 'creep' / 'all' / プレイヤー番号。types:4 文字コードの集合。 (内部: ワールドブロック units[]) - `unit(handle) -> 'object | None'` [verified] [プッシュ型スナップショット] ハンドルペア (lo, hi) でユニットを探します(オーダーのターゲット、タスクターゲット、イベントが返すのはいずれもハンドルペアです)。 (内部: ワールドブロック by_handle) - `is_building(u) -> 'bool'` [verified] [プッシュ型スナップショット] 建物かどうか(タワーを含む)。ユニット表の移動速度 0 で判定します。アンデッドの本拠地は占有面積が 0 なので、占有面積では判定しないでください。 (内部: スナップショット + units.json(spd==0 = 建物)) - `my_workers() -> 'list'` [verified] [プッシュ型スナップショット] 自軍のワーカー(Peasant / Peon / Acolyte / Wisp)。 (内部: プッシュスナップショット) - `idle_workers() -> 'list'` [verified] [プッシュ型スナップショット] 仕事のないワーカー:オーダーもタスクもないもの(このティックで仕事を割り当てたばかりのものは除く)。 ⚠ タスクのあるワーカーに採集命令を出し直すと採集サイクルが中断されます(収入がゼロになります)。 (内部: プッシュスナップショット(オーダースロット + タスクスロット)) - `my_heroes() -> 'list'` [verified] [プッシュ型スナップショット] 自軍の生きているヒーロー(死んだヒーローは祭壇の復活リストにあります。revive を参照)。 (内部: プッシュスナップショット) - `my_army() -> 'list'` [verified] [プッシュ型スナップショット] 自軍の戦闘ユニット:ワーカーでも建物でもないもの。 (内部: プッシュスナップショット + units.json) - `my_buildings(types=None) -> 'list'` [verified] [プッシュ型スナップショット] 自軍の建物(タワー、建設中の基礎を含む)。types で特定の種類だけに絞れます(例:{'hbar'})。 (内部: プッシュスナップショット) - `is_constructing(worker) -> 'bool'` [verified] [プッシュ型スナップショット] このワーカーが建設中かどうか(建設に向かっている途中 / 修理を手伝っている場合、このティックで割り当てたばかりの場合も含む)。建設担当を選ぶときはこのワーカーを除外してください。そうしないと前の基礎の工事が止まります。 (内部: プッシュスナップショット(オーダー = 建物の 4 文字コード、または建設命令 / 修理命令)) - `under_construction(building) -> 'bool'` [verified] [プッシュ型スナップショット] この建物がまだ完成していない(HP が満タンでない)。⚠ ダメージを受けた建物も満タンではありません —— 序盤の判定には十分ですが、戦闘が始まった後は時間と合わせて判断してください。 (内部: プッシュスナップショット(基礎の HP はごく低い値から満タンまで増えていく)) - `gold_mines() -> 'list'` [verified] [プッシュ型スナップショット] マップ上の金鉱。⚠ ナイトエルフの Entangled Gold Mine と中立の金鉱は同じ座標にそれぞれ 1 ユニットずつあるので、採集には自分の方を割り当ててください。 (内部: プッシュスナップショット(ngol/egol/ugol)) - `enemies(fighters_only: 'bool' = False) -> 'list'` [verified] [プッシュ型スナップショット] 敵プレイヤーのユニット(クリープを除く)。fighters_only:ワーカーと建物を除外します。 (内部: プッシュスナップショット) - `creeps() -> 'list'` [verified] [プッシュ型スナップショット] クリープ(中立敵対)。⚠ 夜は視界が短くなり、遠くのキャンプが戦場の霧に入ると、それへのターゲットコマンドは拒否されます(理由コード 1001)。 (内部: プッシュスナップショット(owner 12 = 中立敵対)) - `life_mana(u) -> 'dict | None'` [verified] [プッシュ型スナップショット] {'hp','hp_max','mana','mana_max'}(浮動小数点、エンジンの生の値)。u にはスナップショットで取得したユニットをそのまま渡せます(最新のものに置き換えられます)。 (内部: ワールドブロックのユニット hp/hpMax/mana/manaMax) - `hero_info(hero) -> 'dict | None'` [verified] [プッシュ型スナップショット] {'level','xp','skill_points'}。 (内部: ワールドブロックのユニット level/xp/skillPoints) - `abilities(u) -> 'list'` [verified] [プッシュ型スナップショット] [{code, level, cooldown, flags}]。buff は buffs(u) にあります。「詳細」を持つユニットだけが対象です(ヒーロー > プレイヤーのユニット > クリープの順、最大 256 体)。 (内部: ワールドブロックの詳細:アビリティ(コード / レベル / フラグ / 残りクールダウン)) - `buffs(u) -> 'list'` [verified] [プッシュ型スナップショット] ユニットにかかっている buff コード(例:'BHds' Divine Shield、'Bslo' Slow)。各コードの効果は data/game/buffs.json を参照してください。 (内部: ワールドブロックの詳細:B で始まるアビリティオブジェクト) - `cooldown(u, ability: 'str') -> 'float | None'` [verified] [プッシュ型スナップショット] このアビリティのクールダウンが残り何秒か(ゲーム秒)。0 = 使用可能。そのアビリティがない(またはこのユニットに詳細がない)場合は None を返します。 (内部: ワールドブロックの詳細:アビリティの残りクールダウン(アビリティのタイマー)) - `inventory(hero) -> 'list | None'` [verified] [プッシュ型スナップショット] 6 スロットのアイテムの 4 文字コード(空きスロットは None)。インベントリがなければ None を返します。 (内部: ワールドブロックの詳細:インベントリ 6 スロット) - `current_order(u) -> 'dict | None'` [verified] [プッシュ型スナップショット] {'order','target','x','y'}:ユニットが今持っているオーダー(order は 0x000D00xx または建物の 4 文字コード、0 = アイドル)。 target はハンドルペアで、g.unit(target) でユニットに変換します。 (内部: ワールドブロックのユニット order / オーダーのターゲット / オーダーのターゲット地点) - `current_target(u)` [verified] [プッシュ型スナップショット] ユニットが**実際に攻撃 / 追跡している**ユニット(なければ None)。 ⚠ 攻撃命令を出すとオーダースロットはすぐに空になり、攻撃はタスク側に残ります —— 「誰を攻撃しているか」の判定には current_order ではなくこちらを使ってください。 (内部: ワールドブロックのユニットのタスクターゲット) - `clock() -> 'float | None'` [verified] [プッシュ型スナップショット] エンジンのゲームクロック(ゲーム秒、ロード中は 0)。ゲーム速度を上げると実時間より速く進みます。 (内部: ワールドブロックのヘッダー clockMs(エンジンのゲームクロック)) - `production(building)` [verified] [プッシュ型スナップショット] この建物が今何をしているか:Production(kind, queue, duration, elapsed, blocked, progress, remaining…)。何もしていなければ None を返します。 kind は 'queue'(訓練 / 研究 / ヒーロー。queue は最大 7 スロットで、[0] が実行中のもの)/ 'construction'(建設中)/ 'upgrade'(本拠地 / タワーのアップグレード)。 blocked = キューにあるのに始まっていない(たいていは人口不足 —— Farm を建てる時です)。progress は 0..1。 相手の建物も見られます(フェアモードでは見えている建物のみ)。 (内部: ワールドブロックの生産テーブル(Aque/ABnP/AUnP アビリティオブジェクト + ランタイムが追跡する経過時間。実測誤差 < 0.2 ゲーム秒)) - `queue(building) -> 'list'` [verified] [プッシュ型スナップショット] 訓練 / 研究キュー内の 4 文字コード([0] が実行中)。アイドルまたは生産建物でない場合 = []。 (内部: ワールドブロックの生産テーブル) - `all_production(owner: 'str | int' = 'me') -> 'list'` [verified] [プッシュ型スナップショット] 進行中のすべての生産 [(建物, Production)]。owner は units() と同じです:'me' / 'enemy' / プレイヤー番号 / 'all'。 プロの使い方:相手が何の兵を訓練しているか、何の技術を研究しているか、いつティアアップするかを見る(建物を偵察できたとき)。 (内部: ワールドブロックの生産テーブル) - `path_distance(a, b) -> 'float | None'` [verified] [プッシュ型スナップショット] 地上ユニットが a から b まで歩く距離(a、b はユニットまたは (x,y))。到達できなければ None。島マップで「このクリープキャンプ / 拡張地点に地上から行けるか」を判定するのに使います。 直線距離より信頼できます(木立、崖、建物を迂回)。精度は 1 マス 128 で、1 マスより狭い隙間は通れないと判定します。 (内部: マップブロック(エンジンの IsTerrainPathable)+ 木ブロック + 建物の占有範囲、SDK 側で A*(1 マス 128)) - `reachable(a, b) -> 'bool | None'` [verified] [プッシュ型スナップショット] 地上から到達できるか(マップブロックの計算が終わっていない = None)。 (内部: 同上) - `walk_path(a, b) -> 'list | None'` [verified] [プッシュ型スナップショット] 経路の折れ点 [(x,y)...](最後の点が b)。path(units, 地点リスト) と組み合わせて、部隊をこの経路に沿って移動させます(タワーを避ける、裏道を通る)。 (内部: 同上) - `upkeep(player: 'int | None' = None) -> 'dict | None'` [inferred] [プッシュ型スナップショット] 維持費の段階:{'level': 'none'/'low'/'high', 'income': 1.0/0.7/0.4, 'next_at': 次の段階になる人口(なければ None)}。 プロの常識:Tier 3 へのアップグレードや攻防アップグレードの間は人口 50 で止め、決戦の直前にだけ 80 まで上げます。 (内部: 1.27 の固定ルール:人口 0~50 は徴収なし、51~80 は収入 ×0.7、81~100 は ×0.4) - `xp_to_next(hero) -> 'int | None'` [verified] [プッシュ型スナップショット] ヒーローが次のレベルまでに必要な経験値(レベル 10 = 0)。 (内部: ワールドブロック level/xp + MiscGame の NeedHeroXP 式) - `creep_camps(link: 'float' = 600.0) -> 'list'` [verified] [プッシュ型スナップショット] フィールド上の(見えている)クリープをキャンプにまとめます:[{'x','y','units','level','hp','max_level'}]。自軍の本拠地から近い順です。 level = キャンプの合計レベル(クリープ狩りの難易度によく使われる指標)、hp = 合計 HP。time_to_kill / path_distance と組み合わせて狙うキャンプを選びます。 (内部: プッシュスナップショット(距離 600 以内のクリープを 1 グループにまとめる)+ units.json のレベル) - `buff_info(code: 'str') -> 'dict | None'` [verified] [ローカル計算] buff コードが何か:{'ability','effect','dur','hero_dur','targets'}(例:'Bslo' -> Slow)。1 つのコードに複数行ある場合は最初の行を返します。 (内部: data/game/buffs.json(AbilityData.slk の BuffID -> アビリティ / 効果 / 持続時間)) - `stats(u, player: 'int | None' = None)` [verified] [プッシュ型スナップショット] ユニットの戦闘属性 combat.UnitStats:HP / マナ上限、アーマー(攻防アップグレード、ヒーローの敏捷性を含む)、アーマータイプ、移動速度、昼 / 夜の視界、 武器(攻撃できる対象、射程、攻撃間隔、ダメージ範囲、攻撃タイプ、スプラッシュ)。u にはユニット(所有者の技術とヒーローレベルを自動で使用)または 4 文字コード(player のデフォルトは自軍)を渡します。 さらに .dps_vs(相手) / .hits_to_kill(相手) / combat.time_to_kill(集団, 相手) と組み合わせます。⚠ アイテム、オーラ、buff は含みません。 (内部: データテーブル(UnitBalance/UnitWeapons/UpgradeData/MiscGame)+ リアルタイムの技術レベル + ヒーローレベル) - `time_to_kill(attackers, target) -> 'float | None'` [verified] [プッシュ型スナップショット] このユニット群が一緒に target を攻撃して倒すまでのゲーム秒数(target の現在の HP を使用。相性、アーマー、攻防アップグレードを考慮し、移動、スプラッシュ、回復は考慮しない)。 プロの使い方:集中攻撃は一番近い敵ではなく、「最も早く倒せる」敵(time_to_kill が最小のもの)から狙います。攻撃できない = None。 (内部: stats() + リアルタイムの HP) - `time_of_day() -> 'float | None'` [verified] [プッシュ型スナップショット] ゲーム内時刻(時、0~24)。試合開始時は朝 8 時です。1 日 = 480 ゲーム秒(昼と夜がそれぞれ 240 秒で、昼夜の進行速度に応じて伸縮)。 読み取れない(古いランタイム / 試合中でない)場合は None を返します。 (内部: ワールドブロックの拡張領域:GetFloatGameState(GAME_STATE_TIME_OF_DAY)) - `is_night() -> 'bool | None'` [verified] [プッシュ型スナップショット] 今が夜かどうか(18:00~6:00)。プロの戦術:夜はクリープが眠っている(先手でクリープ狩りをしても囲まれない)、全ユニットの視界が短くなる(奇襲の好機)、 ナイトエルフの Sentinel / ユニットは夜に木のそばで姿を隠す。読み取れない場合は None を返します。 (内部: ワールドブロックの拡張領域(6~18 時が昼)) - `seconds_until(hour: 'float') -> 'float | None'` [verified] [プッシュ型スナップショット] ゲーム内時刻が hour 時になるまであと何ゲーム秒か(例:seconds_until(18) = 日没までの時間。夜のクリープ狩りの計画に使います)。 (内部: ワールドブロックの拡張領域 + 1 日 480 秒(実測 20 ゲーム秒 / 時)) - `items_on_ground() -> 'list'` [verified] [プッシュ型スナップショット] 地面のアイテム [Item(addr, handle_lo, handle_hi, type, x, y, life)]。拾われる / 使われると item.removed イベントが発行されます。 (内部: ワールドブロック items[](地面のもののみ:所有者のハンドルがすべて FF)) - `trees(x: 'float | None' = None, y: 'float | None' = None, limit: 'int' = 60) -> 'list'` [verified] [プッシュ型スナップショット] 生きている木(DestructableData で targType に tree を含むもの)。(x,y) を渡すと近い順に並べ、最大 limit 本を返します。 各要素は Tree(addr, handle_lo, handle_hi, type, x, y, life) で、そのまま gather に渡して伐採させられます。 (内部: 木ブロック Local\War3Trees_(2 秒ごとに更新)) - `events() -> 'list'` [verified] [プッシュ型スナップショット] 前回の呼び出し以降に起きたこと:unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started(これらは発行の比較から得られ、精度 = 発行周期 50 ms)、 さらにエンジンレベルの damage / killed(ランタイムがゲームスレッド上でその場で記録するため、**1 発ごと**に発生): damage:handle = 攻撃を受けた側、.source_addr = 攻撃した側(snapshot().unit_by_addr でユニットに変換)、.value = 実際に減った HP、 .raw_damage = アーマー適用前のダメージ、.attack_type(normal/pierce/siege/magic/chaos/hero/spell)、.damage_type killed:この 1 発でとどめを刺した、.source_addr = 倒した側 さらにランタイムが生産テーブルを追跡して得る production.done(精度 = 発行周期):ユニット = 建物、.done_code = 完了した 4 文字コード、 .done_kind = 'training'(兵 / ヒーロー / 復活)/ 'research' / 'construction'(建物の完成)/ 'upgrade'(ティアアップ / タワーのアップグレード)、.value = かかったゲーム秒数 09-25 の補完: spell.cast:ユニット = 詠唱者、.spell スキルの 4 文字コード、b レベル、value クールダウン秒数、x,y 詠唱地点(スキルのクールダウン開始時に検出、精度 = 発行周期) player.left:.player 退出した / 敗北判定で除外されたプレイヤー番号。game.ended:試合から退出 selection.changed:ローカルプレイヤーの選択が変わった(ユニットは g.selection() で取得) message:画面のメッセージ枠に出た 1 件(ゲームのヒント、チャット、システム):.text 全文、.frame メッセージ枠の番号、 .chat = {'channel', 'sender', 'text'}(チャットの場合。プレイヤーがチャット欄に打った文字はここから読む) ui.click / ui.hover / hotkey / mouse.world:UI と入力(g.ui)。.key はキャンバスの key / ホットキーの書き方 各要素は Event(seq, kind, clock, addr, handle, type, owner, a, b, x, y, value, extra)。 フェアモード(fair=True)で渡されるのは、自分のユニットのイベント、今見えている(または 1 秒以内まで見えていた)ユニットのイベント、自軍が受けた / 自軍が与えたダメージ、 そしてローカルの UI / メッセージ / 試合関連のイベントだけです。 (内部: イベントリング Local\War3Events_(発行の比較 + ランタイムが捕捉したダメージイベント)) - `selection() -> 'list'` [verified] [プッシュ型スナップショット] ローカルプレイヤーが今選択しているユニット(メインのユニットが先頭。最大 12 体)。選択が変わると selection.changed イベントが発行されます。 (内部: W3P ワールドブロックの拡張領域 selAddrs(ランタイムが発行のたびにローカルプレイヤーの選択を含める)) - `messages() -> 'list'` [verified] [プッシュ型スナップショット] 前回の呼び出し以降に画面のメッセージ枠へ新しく出たメッセージ:[{'text', 'frame', 'repeat', 'seq', 'game_ms'}]。 ゲームのヒント(「Farm がもっと必要です」「そこには建設できません」)、チャット、システムメッセージがすべてここに入ります。frame でどのメッセージ枠かを区別します。 イベントストリームの message イベントと同じものです(カーソルはそれぞれ別)。 (内部: 共有メモリ Local\War3Msgs_(ランタイムが捕捉した画面メッセージ)) - `tech(code: 'str', player: 'int | None' = None) -> 'int | None'` [verified] [ファストレーン] 研究レベル / 完成した建物の数(アップグレード系列も含む:Castle も htow として数える)。player のデフォルトは自軍で、どのプレイヤーでも問い合わせられます。 (内部: W3P クエリ q_tech(エンジンのプレイヤー技術カウント)) - `can_do(u, code: 'str') -> 'int | None'` [verified] [ファストレーン] エンジンの実行可否判定:0/220 なら実行可能。3 人口不足、8 ゴールド不足、9 木材不足、32 キューがいっぱい、183 前提条件不足、185 祭壇で復活中、221 その項目がない / 建設中。 ⚠ ワーカーの建物建設に対しては常に 221 になるため、設置場所の判定には使えません(build_near を使います)。 (内部: W3P クエリ q_feasible(エンジンの実行可否チェック)) - `can_do_many(pairs) -> 'list'` [verified] [ファストレーン] can_do をまとめて問い合わせます:pairs = [(ユニット, 4 文字コード), ...]。同じ順序の判定コードのリストを返します(問い合わせできなかったものは None)。 1 ティックで何を建てる / 訓練するかを計画するときは、先にまとめて問い合わせると、can_do を 1 つずつ呼ぶより N 倍速くなります(リファレンスブレイン 09-23:建設計画 76 -> 25 ms)。 (内部: W3P クエリ q_feasible × N、1 バッチで送信) - `tech_many(codes, player: 'int | None' = None) -> 'dict'` [verified] [ファストレーン] 多数の技術 / 建物のカウントを一度に問い合わせます:{4 文字コード: 数または None}。 (内部: W3P クエリ q_tech × N、1 バッチで送信) - `visible(x: 'float', y: 'float') -> 'bool | None'` [verified] [ファストレーン] この地点が今自軍から見えているか(戦場の霧 / ブラックマスクの中でないか)。フェアモードの bot は見えている敵だけを使うべきです。 (内部: W3P クエリ q_visible(見えている / 戦場の霧 / ブラックマスク)) - `gold_left(mine) -> 'int | None'` [inferred] [ファストレーン] 金鉱の残りゴールド。 (内部: W3P クエリ q_mine_gold(エンジンの金鉱残量)) - `enemy_ai_plan(enemy_unit) -> 'dict | None'` [verified] [ファストレーン] コンピューター AI のキャプテン:兵を連れてどこへ向かうか(出撃前に、あなたの基地のどこを攻めるかが分かります)。コンピューターの相手にのみ有効です。キャプテンについていないユニットなら None を返します。 (内部: W3P クエリ q_captain(敵兵が従っているコンピューターのキャプテン)) - `order_of(u) -> 'int | None'` [verified] [プッシュ型スナップショット] ユニットの現在のオーダー。**このティックで出したばかりのものを含みます**(スナップショットがまだ追いついていないときはレシートの新しいオーダーを使います)。 ⚠ 09-23 の実戦:hello_bot が農民を Farm の建設に送った直後、同じティックで rush_bot がスナップショット上でその農民を「アイドル」と判断して Barracks の建設に送り、Farm が何度も途中で放棄されました。 「手が空いている / 建設中でない」ユニットを選ぶときは、u.order ではなくこれを使ってください。 (内部: スナップショットのオーダー + このプロセスで受理されたばかりのコマンド(レシート)) - `can_afford(code: 'str') -> 'bool'` [verified] [プッシュ型スナップショット] 今のゴールド / 木材で code(ユニット、建物)を買えるか(units.json の価格で判定)。価格表にないものはすべて買えるとみなします。 ⚠ ティアアップの 4 文字コードは表では累計価格なので、ここでの判定はやや保守的になります。最終的にはエンジンのレシートが正です。 (内部: プッシュスナップショットの自軍資源 + units.json の価格) - `map_data()` [verified] [ローカル計算] プレイ中のマップのデータ(openwar3.mapdata.MapData):name_of('HC07') でカスタムユニット / アイテム / アビリティの名前、hero_names、tooltip。 RPG マップのユニットはほとんどがマップ独自のもので、組み込みの名前表にはありません。ランチャーから起動したゲームでない場合(マップファイルが見つからない)は None を返します。 (内部: マップファイル(ランチャーの --map のパス):w3u/w3t/w3a + wts。保護されたマップはマップ内の TXT を読む) ## コマンド ユニットに何かをさせます。約 1 フレームで反映し、すべてにレシートがあります。 - `batch() -> 'Batch'` [verified] [ファストレーン] 1 ティック分のコマンドを 1 つのバッチにまとめます: with g.batch() as b: g.attack(archers, target) # Pending を返し、ブロック終了後にレシートになる g.move(wounded, *home) g.cast(hero, "thunderclap") print(b.sent, b.wait_ms, [r.reason for r in b.receipts]) コマンドを 1 件ずつ送るとそのたびにゲームスレッドの処理を 1 回待ちます(約 10 ms)。バッチなら待つのは 1 回だけです —— リファレンスブレインは 09-23 にこれで 1 ラウンドを 48 -> 26 ms に短縮しました。 * クレームテーブルによる調停はこれまでどおり 1 件ずつ行われます(押さえられているユニットはその場で held のレシートを受け取り、バッチには入りません)。 * ブロック内のコマンドは Pending を返します:ブロック終了前に .ok を読むと例外が発生し(レシートがまだ存在しないため)、ブロック終了後は Receipt と同じように使えます。 * ブロック内で例外が発生した = バッチ全体を破棄(status 97 cancelled)し、押さえていたユニットは解放されます。 * クエリ(can_do / tech / visible …)と build_near、buy はバッチに入らず、これまでどおりその場で問い合わせます —— 結果をすぐに使うためです。 一度に多数を問い合わせるなら can_do_many / tech_many を使います。 * ネストした with g.batch() は一番外側のバッチに統合されます。16 件を超えるとランタイムが自動で複数の区間に分けます(区間ごとに 1 回待機)。 (内部: ブロック内のコマンドを 1 バッチにため、ブロック終了時に一括送信(同じフレームで実行し、ゲームスレッドを待つのは 1 回だけ)) - `move(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [ファストレーン] (x,y) まで移動し、途中で攻撃しません(撤退にはこれを使います)。ユニット 1 体またはリストを渡せます(同じフレームでまとめて命令)。 queue='after':今の作業を終えてから向かいます(現在のオーダーの後ろに差し込む)。レシートの values[0] = 命令後にこのユニットがキューに持っているオーダー数(実行中のものを含む)。 (内部: W3P point:move(extra ビット = キュー方式)) - `attack_move(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [ファストレーン] アタックムーブ(A で地面を指定):途中で敵に遭遇すれば攻撃します。queue は move と同じです。 (内部: W3P point:attack を地点指定) - `attack(units, target, force: 'bool' = False, queue: 'str | None' = None)` [verified] [ファストレーン] target を攻撃します。デフォルトでは右クリックを使います(敵に対して = その 1 体を攻撃。09-23 の実測でオーダーのターゲットもタスクターゲットもそのユニット)。 ⚠ ターゲットは視界内にいる必要があり、見えないものは拒否されます(理由コード 1001)。 force=True で攻撃オーダー 0x0F を使います(味方や中立の小動物を攻撃するときに必要)—— 実測では、攻撃オーダーに切り替わるだけでターゲットを記憶せず、 近くの別の敵を攻撃しに行きます。特定のターゲットを攻撃する用途には使わないでください。 (内部: W3P target:ターゲットコマンド(右クリック smart)) - `stop(units)` [verified] [ファストレーン] 手元のすべての作業を止めます(オーダー ID 0x000D0004)。キューにあるオーダーも消去されます。 (内部: W3P immediate:stop) - `hold(units, queue: 'str | None' = None)` [verified] [ファストレーン] その場で待機します(追いかけず、射程内の敵だけを攻撃)。 (内部: W3P immediate:holdposition) - `patrol(units, x: 'float', y: 'float', queue: 'str | None' = None)` [inferred] [ファストレーン] 現在位置と (x,y) の間をパトロールします。 (内部: W3P point:patrol) - `attack_ground(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [ファストレーン] 地面攻撃:砲で一帯を砲撃します(透明ユニットや木立の裏の敵を撃つ、通路を封鎖する)。地面攻撃ができるユニットだけが受け付けます。 (内部: W3P point:attackground(攻城ユニット / Mortar Team / Demolisher)) - `cancel(building)` [verified] [ファストレーン] キャンセル:訓練 / 研究キューの最後のスロット(返金)、建設中の建物(75% 返金)、アップグレード中の本拠地。 (内部: W3P immediate:cancel) - `path(units, points, attack: 'bool' = False)` [verified] [ファストレーン] 一連の地点を順番に通過します(Shift で連続指定:ウェイポイント、タワーの迂回、偵察ルート)。attack=True なら各区間がアタックムーブになります。 一括で送信し、レシートは地点ごとに 1 つ(points の順)返ります。 (内部: 1 バッチ:最初の区間は即時実行、残りは逆順に queue='after' で差し込む(エンジンには「現在のオーダーの後ろに差し込む」しかないため)) - `gather(workers, target, queue: 'str | None' = None)` [verified] [ファストレーン] ゴールドの採掘 / 伐採(target は金鉱または trees() の木)。⚠ 手の空いたワーカー(idle_workers)にだけ割り当ててください:タスクのあるワーカーに出し直すと採集サイクルが中断されます。 プロの使い方:建て終わったら採掘に戻る = build(...) の後に gather(worker, mine, queue='after')。 (内部: W3P target:harvest(金鉱または木)) - `repair(workers, building, queue: 'str | None' = None)` [verified] [ファストレーン] 修理 / 建設の手伝い(ヒューマンとオークの建設現場は、建てる人がいないと工事が止まります)。 (内部: W3P target:repair) - `build(worker, code: 'str', x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [ファストレーン] ワーカーに (x,y) で code を建てさせます(座標は 32 単位にスナップ)。レシートが受理 = ワーカーのオーダーがすでにこの建物(または着工命令)になっている。 queue='after' のとき = ワーカーのオーダーキューに入った(レシートの values[0] がキュー数)。 ⚠ 受理 ≠ 建てられる:木立の中の地点もエンジンはその場で受理し、ワーカーが到着してから失敗します(09-23 実測)。ゴールドが別のところで使われても基礎は現れません。 どこに置けるか分からないなら build_near を使います(結果を追跡し、失敗した地点をブラックリストに入れます)。続けて何棟も建てるなら build_queue を使います。 (内部: W3P build:建設オーダー。同じフレームでワーカーのオーダーを読み戻して確認) - `build_queue(worker, plan)` [verified] [ファストレーン] 1 人のワーカーに順番に何棟も建てさせます(Shift で連続建設):plan = [(4 文字コード, x, y), ...]。一括で送信し、レシートは plan の順です。 ⚠ ゴールドは着工時に引かれます(キューに入れた時点では引かれません)—— 3 棟入れてもゴールドが 1 棟分しかなければ、残りの 2 棟はワーカーが到着した時点で失敗します。 (内部: 1 バッチ:最初の 1 棟は即時、残りは逆順に queue='after') - `build_near(worker, code: 'str', x: 'float', y: 'float', min_r: 'float' = 450, max_r: 'float' = 1500, max_tries: 'int' = 24)` [verified] [ファストレーン] (x,y) の周囲で近いところから順に置ける場所を探して code を建てます。**ブロックしない**ので、毎ティック呼んでも構いません: * この種類の建物の建設が進行中(ワーカーが移動中)-> その地点を返し、命令を出し直さない。 * 前回成功した(基礎が現れた)-> 今回は必要に応じて新しい地点を探す。 * 前回失敗した(ワーカーが到着してから置けないと分かり、エンジンがオーダーを取り消し、基礎もない)-> その地点を 45 秒間ブラックリストに入れ、次の地点を試す。 * ゴールドが足りない -> そのまま None を返す(試さない、ブラックリストにも入れない)。すべて試し終えたら None を返す。 ⚠ なぜ追跡が必要か:09-23 の実戦で、木立の中の地点をエンジンは**その場で受理**し、ワーカーが到着してから失敗しました(同じフレームのレシートでは判定できない)。 また、エンジンの設置可否チェックはワーカーの建物建設に対して常に 221 を返すため、先に「確認」してから建てることもできません。明らかに埋まっている地点(本拠地のど真ん中)だけはその場で拒否されます。 (内部: 地点ごとに build + 追跡(基礎が現れた = 成功。ワーカーがオーダーを放棄し、基礎もない = その地点をブラックリストに入れる)) - `train(building, code: 'str')` [verified] [ファストレーン] ユニットの訓練 / 技術の研究 / 本拠地のアップグレード(ティアアップ = 本拠地自体にアップグレード先の本拠地の 4 文字コードを指定。例:'hkee')。 拒否されると、レシートの reason に理由が入ります(人口不足、ゴールド不足、木材不足、キューがいっぱい、前提条件不足……)。 (内部: W3P immediate:4 文字コード。拒否時は実行可否の理由コード付き) - `learn(hero, ability: 'str')` [verified] [ファストレーン] ヒーローがアビリティを習得します(4 文字コード。例:'AHbz' Blizzard)。 (内部: W3P learn:スキルポイントが減った場合にのみ習得とみなす) - `cast(u, spell, target=None, x: 'float | None' = None, y: 'float | None' = None)` [verified] [ファストレーン] スキルを使います。spell はオーダー文字列('thunderbolt' Storm Bolt、'blizzard'、'holybolt' Holy Light…、data/order-ids.txt を参照)またはオーダー ID。 target を渡す = ユニット対象、x,y を渡す = 地面対象、どちらも渡さない = ターゲットなし(Thunder Clap、Divine Shield、Summon Water Elemental)。 レシートの受理はエンジンが受け付けたことを示すだけです。実際に発動したかは、cooldown() がクールダウンに入ったか、buffs() に現れたかで確認します。 (内部: W3P target / point / immediate(引数に応じて選択)) - `rally(building, x: 'float | None' = None, y: 'float | None' = None, target=None)` [verified] [ファストレーン] 集結地点を設定します(地点指定、またはユニット / 金鉱を指定)。 (内部: W3P rally) - `revive(altar, hero=None)` [verified] [ファストレーン] 祭壇で死んだヒーローを復活させます(hero を渡さなければリストの最初のヒーロー)。 拒否されるよくある理由(レシートの reason に書かれます):人口不足(ヒーローも人口を使う)、ゴールド不足、死んでから間もない(死後約 3 ゲーム秒経たないと復活できない)、 すでに復活が進行中(受理された時点でエンジンがそのスロットをその場で消去する)。 (内部: W3P revive:死亡ヒーロー一覧 -> 祭壇が死んだヒーローに復活を実行) - `pick_up(hero, item)` [verified] [ファストレーン] ヒーローが地面のアイテムを拾いに行きます(item は items_on_ground から取得)。拾うとインベントリに現れ、地面側では item.removed イベントが発行されます。 (内部: W3P target:アイテムを右クリック) - `use_item(hero, slot: 'int', target=None, x: 'float | None' = None, y: 'float | None' = None)` [verified] [ファストレーン] インベントリの slot 番目(0~5)のアイテムを使います。ターゲットユニットまたはターゲット地点を指定できます。 ⚠ 地点指定でアイテムを使う場合(例:Ivory Tower)、エンジンは成功しても 0 を返すため、レシートは常に受理扱いになります —— インベントリのそのスロットが空いたかで確認してください。 (内部: W3P use_item(スロット番号で指定)) - `drop_item(hero, slot: 'int', x: 'float', y: 'float')` [verified] [ファストレーン] インベントリの slot 番目のアイテムを (x,y) に落とします(ヒーローが歩いて行って置きます)。 (内部: W3P item_drop(JASS の UnitDropItemPoint を踏襲:dropitem 0xD0021 を地点指定 + アイテムを即時ターゲット)) - `give_item(hero, slot: 'int', to)` [verified] [ファストレーン] インベントリの slot 番目のアイテムを to(別のヒーロー / ユニット。歩いて行って手渡す)に渡します。ショップに渡す = 売却(sell_item を参照)。 (内部: W3P item_drop(JASS の UnitDropItemTarget を踏襲:dropitem をユニット指定)) - `sell_item(hero, slot: 'int', shop)` [verified] [ファストレーン] インベントリの slot 番目のアイテムをショップに売ります(ヒーローがショップの隣まで行く必要があります。売却可能なアイテムのみ受け付け、価格の半分が返ります)。 (内部: give_item と同じで、ターゲットがショップ(実測:Staff of Sanctuary が 125 ゴールドで売れた)) - `move_item(hero, slot: 'int', to_slot: 'int')` [verified] [ファストレーン] インベントリ内でスロットを移動します(slot 番目を to_slot 番目へ。両方にアイテムがあれば入れ替え)。ショートカットキーの配置を整えるのに使います。 (内部: W3P target:オーダー 0xD0022+スロット番号、ターゲット = アイテム(JASS の UnitDropItemSlot を踏襲)) - `buy(shop, item_code: 'str')` [inferred] [ファストレーン] ショップでアイテムを買います(ショップの隣に立っているヒーローに渡されます)。技術の前提条件が足りないとエンジンは 0 を返し、ゴールドは引かれません。 (内部: W3P buy:ショップが隣にいるヒーローに売る) - `call_to_arms(hall, on: 'bool' = True)` [verified] [ファストレーン] ヒューマンの Call to Arms:農民が Militia になります(Tier 1 の Town Hall にはこのアビリティがなく、Keep / Castle でのみ有効)。 (内部: W3P immediate:townbellon/off) ## ゲーム制御 ゲーム速度、一時停止、発行周期、頭上の吹き出し、キャンバス、UI と入力、メッセージ。 - `ui()` [verified] [直接書き込み] UI と入力(openwar3.ui.UI):クリックできるボタンと選択カード、ホットキー、地面のクリックによる位置指定、マウスが指している場所。 ボタン上のクリックはゲームに届きません。ローカルの入力 + ローカルの描画だけなので、マルチプレイでも安全です。 (内部: W3P 74 input_enable + 共有メモリ Local\War3Input_(ランタイムがウィンドウ入力を受け取る)) - `set_speed(percent: 'int') -> 'bool'` [verified] [コントロールチャネル] ゲーム速度(100 = 通常速度)。 (内部: アクション 47(25~800%)) - `pause(on: 'bool' = True)` [verified] [ファストレーン] ゲームの一時停止 / 再開。一時停止中はエンジンクロックが止まりますが、ファストレーンからは通常どおり命令できます(イベントディスパッチは動き続けています)。 (内部: W3P pause) - `set_publish_period(ms: 'int') -> 'None'` [verified] [プッシュ型スナップショット] ワールド状態の発行周期(16~1000 ミリ秒、デフォルト 50)。1 回の収集は約 0.5 ms なので、33 ms でも問題ありません。値はマシン全体で共有され、最後に書き込んだものが有効になります。 (内部: ワールドブロック requestedPeriodMs) - `say(u, text: 'str', seconds: 'float' = 4.0) -> 'bool'` [verified] [コントロールチャネル] ユニットの頭上にチャットの吹き出しを出します(配信 / デバッグ用。ゲームには影響しません)。出せなかったときは False を返し、理由は g.last_say_error に入ります。 (内部: アクション 56) - `message(text: 'str') -> 'bool'` [inferred] [コントロールチャネル] ゲーム画面左下のメッセージ欄に 1 行表示します(このマシンでのみ表示)。ゲーム自身が先に一度メッセージを表示している必要があります(DLL はそのときにメッセージ枠を捕捉します)。 (内部: アクション 45) - `end_game() -> 'bool'` [verified] [コントロールチャネル] このゲームプロセスを終了します(farm.py --keep を使っていれば、next_game.json に従って次の試合が自動で始まります)。 (内部: アクション 22) - `canvas()` [verified] [直接書き込み] キャンバス:ゲーム画面にテキストボックス、パネル、プログレスバー、画像、地面の円やルートを描きます(openwar3.canvas.Canvas)。 ランタイムが自前で描画し、ゲームのハンドルを作らず、ゲームの状態も変えません —— マルチプレイでも安全です。スタイルは自由(CJK 文字、角丸、半透明)。 (内部: W3P 73 canvas_enable + 共有メモリ Local\War3Canvas_(ランタイムが毎フレーム、ゲームがカーソルを描く直前に描画。カーソルがその上に重なる)) - `press_to_continue() -> 'bool'` [verified] [直接書き込み] 「任意のキーを押して続行」のロード画面でスペースキーを 1 回押します。多くの RPG / ストーリーマップはロード後にキーを押さないと始まりません(09-24 WarChasers で実測: 押さないとロード画面のまま止まり、ゲームクロックは 0、ファストレーンのキューも処理されない)。openwar3.run などはゲームに入る際に自動で押すので、通常は手動で呼ぶ必要はありません。 (内部: PostMessage WM_KEYDOWN/UP でスペースをゲームウィンドウに送信(フォーカスは奪わない)) ## サンドボックス JASS チャネル:ユニット生成、同盟設定、名前変更、テキスト表示……RPG の補助ツールやコンパニオン向け。ワールドを変更できるのはシングルプレイかつローカルツールの中だけです。 - `jass()` [verified] [ファストレーン] 任意の JASS native を名前で呼び出します:g.jass.CreateUnit(g.jass.Player(1), "Hpal", x, y, 270.0)。 引数 I/R/B/S/H は自動で変換されます(ユニット / アイテムのオブジェクトはそのまま渡せます)。マルチプレイでは読み取り専用のものしか呼べません。詳しくは openwar3/jass.py と docs/COMPANION_ZH.md を参照してください。 (内部: W3P 70 jass(ランタイムが名前で native テーブルを引く。1291 個)) - `player_slots() -> 'list[dict]'` [verified] [ファストレーン] 16 個のプレイヤースロット:controller(user 人間 / computer / neutral…)、state(empty / playing / left)、human、me、ally(自分と同盟かどうか)。 RPG マップでコンパニオンを置く空きスロットを探したり、シングルプレイかどうかを判定したりするのに使います。 (内部: JASS GetPlayerController / GetPlayerSlotState / IsPlayerAlly) - `spawn(code: 'str', x: 'float', y: 'float', player: 'int | None' = None, facing: 'float' = 270.0)` [verified] [ファストレーン] (x,y) にユニットを 1 体生成し(player のデフォルトはローカルプレイヤー)、スナップショット内のユニットを返します(次のワールド発行を待つので約 50 ms)。生成できなければ None を返します。 返されるユニットには jass_handle 属性が追加されます。⚠ シングルプレイでのみ使用できます(マルチプレイでは同期ずれを起こします)。 (内部: JASS CreateUnit + W3P 72 ハンドル -> ユニット) - `set_alliance(a: 'int', b: 'int', allied: 'bool' = True, vision: 'bool' = True, control: 'bool' = False, xp: 'bool' = False, both: 'bool' = True) -> 'None'` [verified] [ファストレーン] プレイヤー a から b への同盟関係を設定します:allied = 互いに攻撃しない + 互いに救援要請、vision = 視界の共有、control = ユニット制御の共有(b が a のユニットを指揮できる)、 xp = 経験値の共有。both=True なら両方向を同時に設定します(control は a -> b のみ)。 (内部: JASS SetPlayerAlliance) - `set_player_name(player: 'int', name: 'str') -> 'None'` [verified] [ファストレーン] プレイヤー名を変更します(スコアボード、チャット、同盟パネルに表示される名前)。コンパニオンに名前を付けるのに使います。 (内部: JASS SetPlayerName) - `show_text(text: 'str', seconds: 'float' = 6.0, player: 'int | None' = None) -> 'None'` [verified] [ファストレーン] ゲーム画面の左下に 1 行のテキストを表示します(マップのトリガーが使うあのテキスト)。デフォルトではローカルプレイヤーに表示します。|cffRRGGBB カラーコードに対応。 (内部: JASS DisplayTimedTextToPlayer) ## 接続とユーティリティ 接続状態と純粋な計算ユーティリティ。 - `status() -> 'dict'` [verified] [ローカル計算] 接続状態:pid、ワールドの発行(周期、収集にかかった時間)、ファストレーンのカウンタ。 (内部: ワールドブロック + ファストレーン + クレームテーブル) - `nearest(candidates, to)` [verified] [ローカル計算] to(ユニットまたは (x,y))に最も近いもの 1 つ。候補がなければ None を返します。 (内部: 純粋な計算)