# War3AI / OpenWar3 完整文件
> 來源 https://war3ai.com/zh-tw 。為 AI Agent 打造的《魔獸爭霸 III》1.27 開放介面。寫 Bot 時只能使用文末「API 目錄」中列出的 Game 方法。
---
# 文件總覽
> OpenWar3 文件:是什麼、能做什麼;快速開始、寫第一個 Bot、讓 LLM 寫 AI、API 和協定、閘道與 MCP,依你的情況該從哪一頁讀起。
**OpenWar3** 是 War3AI 的開放介面層:一個注入《魔獸爭霸 III》1.27 的執行環境,加上一套 Python SDK。
- 執行環境每 **50 ms** 把整張地圖的完整狀態推送到共用記憶體:所有玩家的資源與人口,所有單位的血量魔力、指令、正在攻擊誰、技能冷卻、buff、物品欄,地上的物品、樹木、生產佇列、晝夜。另外還有一條**事件流**:單位出現與死亡、每一下傷害、生產完成……
- 外部程式以**約一幀**的延遲下達**語意命令**:移動、攻擊、採集、建造、訓練、施法、學技能、復活、使用物品、購買物品……每條命令都有**回執**,寫明引擎有沒有接下,沒接下時附上原因碼。
- 你只需要說「做什麼」:單位用四字碼、技能用指令名,和遊戲裡的叫法一致;「怎麼做到」交給執行環境。
因此 LLM 不需要任何底層知識,也不需要看畫面。讀完文件,它就能寫出一個會運營、會打仗的 Bot,上場之後再依據回執和事件自行修正。
不只是對戰:[畫板](https://war3ai.com/zh-tw/docs/canvas/)能在遊戲畫面上畫出自己的面板和標註,[介面與輸入](https://war3ai.com/zh-tw/docs/ui-input/)讓畫出來的按鈕能點、熱鍵按了有反應,[JASS 通道](https://war3ai.com/zh-tw/docs/jass/)能從外部呼叫遊戲裡的 1291 個函式,在 RPG 地圖裡還能幫自己配一個[AI 玩伴](https://war3ai.com/zh-tw/docs/companion/)。寫好的 AI 可以做成[方案](https://war3ai.com/zh-tw/docs/schemes/),一鍵切換、匯出分享;一整套新玩法可以寫成[玩法模組](https://war3ai.com/zh-tw/docs/mods/)。
不寫 Python 也能接入:[閘道](https://war3ai.com/zh-tw/docs/gateway/)讓任何語言、瀏覽器頁面透過 WebSocket / JSON 呼叫同樣的 API,[MCP 伺服器](https://war3ai.com/zh-tw/docs/mcp/)讓 Claude Code 這類 Agent 直接呼叫工具看局面、下命令。
- [快速開始](https://war3ai.com/zh-tw/docs/quickstart/): 裝好環境,一條命令開一局,看範例 Bot 接手。
- [用 LLM 寫一個 Bot](https://war3ai.com/zh-tw/docs/ai-bot/): 不會寫程式也沒關係:複製提示詞、描述打法,交給 Agent。
- [心智模型](https://war3ai.com/zh-tw/docs/concepts/): 快照、命令、回執、事件、一拍。寫 Bot 前花五分鐘讀一遍。
- [API 目錄](https://war3ai.com/zh-tw/api/): 所有 API,每一個都標註了實測狀態、延遲等級和底層機制。
## 依你的情況選一條路
| 你是 | 從這裡讀 | 接著 |
|---|---|---|
| 會玩魔獸,不會寫程式 | [快速開始](https://war3ai.com/zh-tw/docs/quickstart/) → [用 LLM 寫一個 Bot](https://war3ai.com/zh-tw/docs/ai-bot/) | 遇到問題看 [常見問題](https://war3ai.com/zh-tw/docs/faq/) |
| 會 Python | [第一個 Bot](https://war3ai.com/zh-tw/docs/first-bot/) → [心智模型](https://war3ai.com/zh-tw/docs/concepts/) → [十五條規矩](https://war3ai.com/zh-tw/docs/rules/) | [職業打法食譜](https://war3ai.com/zh-tw/docs/cookbook/)、[範例 Bot](https://war3ai.com/zh-tw/docs/examples/) |
| 在做 Coding Agent/自動化 | [Agent 自主迭代](https://war3ai.com/zh-tw/docs/agent-loop/) | [回執與原因碼](https://war3ai.com/zh-tw/docs/reason-codes/)、[`llms-full.txt`](https://war3ai.com/zh-tw/llms-full.txt) |
| 想讓 LLM 在局內做決策 | [LLM 當參謀](https://war3ai.com/zh-tw/docs/llm-coach/) | [頭頂氣泡與本機模型](https://war3ai.com/zh-tw/docs/speech/) |
| 想讓 Agent 直接上手操作(Claude Code 等) | [LLM 直接呼叫工具(MCP)](https://war3ai.com/zh-tw/docs/mcp/) | [介面與輸入](https://war3ai.com/zh-tw/docs/ui-input/) |
| 使用其他語言(JS、C#、Go、Rust……) | [閘道](https://war3ai.com/zh-tw/docs/gateway/) | 更底層:[W3P 協定](https://war3ai.com/zh-tw/docs/protocol/) |
| 想讓不同人的 AI 互相對戰 | [公平模式](https://war3ai.com/zh-tw/docs/fair-mode/) | [對戰平台](https://war3ai.com/zh-tw/arena/) |
| 想在 RPG/自訂地圖裡打造自己的玩法 | [玩法模組](https://war3ai.com/zh-tw/docs/mods/) | [介面與輸入](https://war3ai.com/zh-tw/docs/ui-input/)、[畫板](https://war3ai.com/zh-tw/docs/canvas/)、[JASS 通道](https://war3ai.com/zh-tw/docs/jass/)、[RPG 玩伴](https://war3ai.com/zh-tw/docs/companion/) |
| 想把自己的 AI 分享給別人 | [AI 方案](https://war3ai.com/zh-tw/docs/schemes/) | [遠見指揮台](https://war3ai.com/zh-tw/docs/console/) |
## 儲存庫裡有什麼
```text
start.bat 唯一的入口:從零部署 + 開啟遠見;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(玩法模組)
xwar3/ 參考大腦:策略層(秒級)+ 毫秒層(4 個行程)+ 勝率模型
console/ 遠見網頁指揮台(FastAPI + React)
gateway/ 閘道(WebSocket / JSON)+ JS 用戶端 + 瀏覽器示範頁
director/ 自動運鏡、頭頂血條
speech/ 頭頂聊天氣泡 + 本機 LLM
runtime/ 多實例編排(每局依設定重開)
data/ order-ids.txt;從你自己的遊戲擷取資料的工具
schemes/ 你的 AI 方案(mine/)和別人分享的方案(installed/),不納入儲存庫
tools/ play.py(一條命令開局)、run_scheme.py(方案執行器)、war3_mcp.py(MCP 伺服器)、run_tests.py、實機核對腳本
docs/ API 目錄 api.json(由程式碼產生)、協定、手冊
```
執行環境和你的程式碼之間只隔著一份帶版本號的 [W3P 協定](https://war3ai.com/zh-tw/docs/protocol/):用 Python SDK 最省事,用其他語言照著協定接入也可以。
## API 的「實測狀態」是什麼意思
API 目錄裡的每個 API 都標註了以下三種狀態之一:
- **實機驗證**:底層那條路徑(動作編號、參數形式、讀回的效果)已在真實對局中驗證,並有核對腳本把關。
- **實驗**:新加入的 API,已在測試實例上跑通,還在逐項實機驗證。可以使用,API 細節可能還會調整。
- **推斷/未完整實測**:底層機制照搬引擎自己的做法(例如 JASS 的等價函式),但還沒在對局裡逐項核對。使用前先看回執。
> **說明**
>
> 目前只支援**《魔獸爭霸 III》1.27**(寒冰霸權)。1.24 ~ 1.28 是同一套引擎結構,多版本相容列在[路線圖](https://war3ai.com/zh-tw/roadmap/)的 P4 階段;1.29 之後及重製版是另一套引擎,不在承諾範圍內。
---
# 快速開始
> 按兩下 start.bat 自動裝好一切,在遠見裡設定好遊戲目錄,開一局看範例 Bot 接管。大約 15 分鐘。
## 你需要
| | 需求 | 說明 |
|---|---|---|
| 系統 | Windows 10 / 11,64 位元 | 目前只支援 Windows |
| 遊戲 | 魔獸爭霸 III **1.27a**(寒冰霸權,`Game.dll` 1.27.0.52240) | 你自己合法擁有的用戶端;不修改磁碟上的任何遊戲檔案 |
**其他都不用先裝。** `start.bat` 只下載一樣東西:Python 3.13(官方可攜式套件,約 14 MB),放在儲存庫的 `bin\env\` 裡,不需要系統管理員權限、不修改系統 PATH,在中國大陸網路下會自動改用鏡像站;電腦上已經有能用的 Python 就直接使用。PowerShell 用 Windows 內建的;遠見的網頁隨儲存庫提供已建置好的版本,不需要 Node.js。
## 安裝
1. **取得程式碼**
```bash
git clone https://github.com/OPENXXAI/OpenWar3AI.git
```
或者[下載壓縮檔](https://github.com/OPENXXAI/OpenWar3AI/archive/refs/heads/main.zip)後解壓縮。執行環境(注入 DLL 和啟動器)隨儲存庫一起提供,不用另外下載。
2. **按兩下 `start.bat`**
第一次執行時它會自己:
- 下載 Python 3.13;
- 安裝 Python 套件,核對執行環境檔案後裝好;
- 下載 AMAI,產生參考大腦要用的戰略資料(AMAI 採自訂授權,產生的檔案不進 git;失敗只影響參考大腦);
- 開啟遠見的首頁「控制中心」 `http://127.0.0.1:8866`。
每一步都會印出結果,沒成功的那一步會告訴你怎麼補上。之後每次按兩下只做一兩秒的檢查,就會開啟遠見。
黑色視窗幾秒後會自己關閉:遠見在背景繼續執行,關掉瀏覽器也不會停止。
3. **在控制中心設定遊戲目錄**
控制中心最上方可以「自動查找」,也可以「瀏覽…」自己選擇魔獸爭霸 III 的目錄。遠見會檢查遊戲版本,並**從你自己的遊戲裡擷取資料**(單位表、技能、物品、克制表……暴雪的檔案不隨程式碼散布)。
版本不是 1.27a 時會提示你。地圖和下一局的設定都以這個目錄為準(`<遊戲目錄>\Maps` 下所有資料夾的地圖都能選);之後要換目錄,請到「設定」頁。
4. **開一局,讓範例 Bot 接管**
最省事的方法是在遠見「實例與開局」頁勾選一個實例編號,選好 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 # 部署檢查 + 開啟遠見
start.bat setup # 完整檢查:重新安裝 Python 套件、重試 AMAI
start.bat restart # 只重新啟動遠見後台(遊戲和各項服務不受影響)
start.bat node # 順便安裝一份 Node.js(只有預覽官網時才需要,平常用不到)
start.bat 5 6 # 順便開始 5、6 號實例的測試(遊戲 + 參考大腦)
stop.bat # 徹底停止全部;stop.bat --keep-llm 把本機模型留在顯示記憶體裡
```
閘道、頭頂氣泡、本機 LLM 也都在遠見「控制中心」裡啟動和停止,不必再找別的指令碼。**徹底停止**:按兩下 `stop.bat`,或者在控制中心右上角點「全部停止」——遊戲實例、AI、閘道、氣泡、本系統使用的本機模型、遠見後台依序全部停止。MCP 伺服器由 Claude 等用戶端管理,不會被停止。
> **設定檔**
>
> `openwar3.json` 由 `start.bat` 和遠見自動寫入,只存本機路徑,不進 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` 的子類別) |
| `--inst` | `9` | 實例編號。別和正在跑的實例撞號(遠見的「實例與開局」頁能看到哪些編號在使用中) |
| `--race` | `1` | 我方種族:1 人類、2 獸人、3 不死族、4 夜精靈 |
| `--enemy-race` | `0` | 對手種族 |
| `--difficulty` | `2` | 電腦對手難度:2 簡單、3 普通、4 瘋狂 |
| `--speed` | `100` | 遊戲倍速(百分比,200 = 2 倍速) |
| `--map` | 設定檔裡的 `default_map` | 地圖 |
| `--attach` | | 不啟動遊戲,只接上已經在跑的實例 |
| `--hz` | `5` | 每秒呼叫 `on_tick` 幾次 |
| `--minutes` | `60` | 最多跑幾分鐘(實際時間) |
| `--fair` | | [公平模式](https://war3ai.com/zh-tw/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/zh-tw/docs/first-bot/): 從 10 行的最小 Bot 開始,一步步加上出兵和出擊。
- [讓 LLM 幫你寫](https://war3ai.com/zh-tw/docs/ai-bot/): 複製提示詞範本,用白話描述打法。
## 自我檢查
```bash
python tools/run_tests.py # SDK / 參考大腦 / 毫秒層 / 指揮台 / 氣泡 / 範例,每套一個子行程
```
離線測試不需要開遊戲。遠見「控制中心」裡也有環境自檢,能看到各部分有沒有裝好。
---
# 第一個 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): # 進入對局後呼叫一次
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
```
閒著的農民會去最近的金礦。四個掛鉤:
| 掛鉤 | 什麼時候呼叫 |
|---|---|
| `on_start(g)` | 進入對局後、第一拍之前呼叫一次 |
| `on_tick(g)` | 每一拍(預設每秒 5 次)。一拍逾時會自動順延,不會越積越多 |
| `on_event(g, ev)` | 每拍的 `on_tick` 之前,把上一拍以來的事件逐一交給你 |
| `on_end(g, reason)` | 這一局結束(遊戲行程不見了 / 我方沒有單位了 / 手動停止)時呼叫一次 |
> **提示**
>
> `on_tick` 裡拋出例外不會中斷整局:執行器會印出堆疊、下一拍繼續;**連續出錯 20 拍**才會停下。
## 加上經濟:生產農民、蓋農場補人口
```python
from openwar3 import Bot
class Economy(Bot):
def on_tick(self, g):
res = g.resources() # 讀不到時是 None,不是 0
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. 人口快滿:找一個沒在蓋房子的農民,在大廳附近蓋農場
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)
```
三個值得注意的寫法:
- **`g.queue(home)` 為空才訓練。** 每拍都下訓練命令會把 7 格佇列排滿、把錢鎖住(實測大廳排了 4 個農民、300 金鎖在佇列裡,開局慢了一大截)。
- **用 `build_near`,而不是寫死座標。** 它會自己由近到遠找放得下的位置、跨拍追蹤結果;錢不夠時什麼也不做。寫死的座標很可能剛好是樹林。
- **不挑正在蓋房子的農民。** 人類的農場要蓋 35 秒,中途把工人派走,地基就停工了。
完整、四個種族都能跑的版本是 `brains/examples/hello_bot.py`:一座礦 5 人、礦滿了就去伐木、續建停工的地基。
## 加上兵營、英雄和出擊
```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") # 聖光術
# 兵營持續生產步兵(佇列只排 1 個)
for b in g.my_buildings({"hbar"}):
if not g.queue(b):
g.train(b, "hfoo")
# 存夠一波就出擊;打殘了就回家
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` 為基礎繼承,沒有兵營 / 祭壇就蓋)。
## 看懂回執
每條命令都會回傳一張回執。`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/zh-tw/docs/reason-codes/)。
> **接下 ≠ 做成**
>
> 回執只說明「引擎接下了這條命令」。樹林裡的建造點引擎也會當場接下,工人走到才失敗;技能可能被打斷。效果要看快照和事件:蓋房子用 `build_near`(它會追蹤地基是否出現),放技能看 `g.cooldown()` 有沒有進入冷卻。
## 下一步
- [心智模型](https://war3ai.com/zh-tw/docs/concepts/): 快照、命令、事件、一拍、批次 —— 為什麼這樣設計。
- [職業打法食譜](https://war3ai.com/zh-tw/docs/cookbook/): 21 招:飽和採集、人口不卡、集火、拉殘血、夜裡打野……
---
# 用 LLM 寫一個 Bot
> 不會寫程式也能做:你負責說清楚想要它怎麼打,LLM 負責寫程式碼。複製提示詞範本,描述打法,跑起來,再讓它修改。
適合會玩魔獸、不會寫程式的人,也適合想省時間的開發者。整個過程就是一段對話:**你描述打法 → 模型寫程式碼 → 你跑一局 → 把看到的現象告訴模型 → 它修改**。
> **提示**
>
> 先依照 [快速開始](https://war3ai.com/zh-tw/docs/quickstart/) 把環境裝好,並跑通 `hello_bot`(看到農民去採礦)。這樣出問題時,你才分得清是環境的問題還是 Bot 的問題。
## 1. 幫模型準備資料
模型寫得好不好,八成取決於它有沒有讀到正確的資料。依你用的工具選一種:
| 你用的是 | 怎麼提供資料 |
|---|---|
| **能讀取儲存庫的 Coding Agent**(Claude Code、Cursor、Codex 等) | 在儲存庫目錄裡開啟它,讓它先讀 `docs/BOT_HANDBOOK_ZH.md`、`docs/api.json` 和一個範例(運營看 `brains/examples/macro_bot.py`,戰鬥看 `micro_bot.py`) |
| **能上網的對話模型** | 讓它先讀 [`https://war3ai.com/llms-full.txt`](https://war3ai.com/zh-tw/llms-full.txt),全站文件都在這一個檔案裡 |
| **網頁對話、不能上網** | 把手冊、[`api.json`](https://war3ai.com/zh-tw/api.json) 和一個範例檔案貼在提示詞後面 |
| **本機模型**(LM Studio、Ollama) | 同上。上下文視窗建議 32K token 以上,否則放不下手冊和 API 目錄 |
想要某種職業打法,再把 [職業打法食譜](https://war3ai.com/zh-tw/docs/cookbook/) 裡對應的那一招貼上。
## 2. 複製這段提示詞
把最後的「我想要的打法」換成你自己的話,越具體越好:
```text
你要為《魔獸爭霸 III》1.27 寫一個 AI(Python)。只能使用 api.json 裡列出的 Game 方法,
不要編造不存在的方法。寫法參照 rush_bot.py:繼承 openwar3.Bot,實作 on_start(g) 和 on_tick(g)。
規則:
- on_tick 每秒大約呼叫 5 次,要快(別在裡面 sleep)。
- 讀不到的值是 None,不是 0,先判斷再使用。
- 命令會回傳一張回執(Receipt),`if r:` 就代表「引擎接下了」;沒接下時 `r.reason` 寫著原因
(人口不夠、金不夠、目標看不見、這個英雄已經有了……),下一拍再試,或者換個做法。
- 攻擊一個具體的敵人用 g.attack(兵, 敵人);敵人必須在視野內,看不見的會被拒。
- 英雄陣亡要用 g.revive(祭壇) 復活,不能再訓練一個。
- 蓋房子用 g.build_near(工人, 建築代碼, x, y):它會自己找放得下的位置、追蹤結果,錢不夠時什麼也不做。
- 想知道「剛才發生了什麼」(誰死了、誰掉血、英雄升級、物品掉落)就實作 on_event(g, ev)。
- 不要每一拍都對同一個單位重複下同一條命令(會打斷它正在做的事);只對「閒著的」下令。
- 採集只派 idle_workers() 裡的工人。一座金礦最多 5 個工人。
- 訓練佇列只排 1 個(g.queue(建築) 空了再排);人口卡住看 g.production(建築).blocked。
- 一拍要下很多條命令時,包在 with g.batch(): 裡(只等一次遊戲執行緒)。
- 選打誰用 g.time_to_kill(我方一群, 敵人)(已算入克制和護甲);選去哪裡用 g.path_distance(走不到會回傳 None)。
- 公平模式下只看得見視野內的東西;以前看到過的敵人用 g.last_seen()。
- 單位用四字碼表示(人類農民 hpea、步兵 hfoo、兵營 hbar……),技能用訂單名(thunderbolt 風暴之錘、
blizzard 暴風雪、holybolt 聖光術……,完整列表在 data/order-ids.txt),學習技能用四字碼(AHtb、AHbz……)。
我想要的打法:
<在這裡用白話寫,例如:
「人類,開局 5 個農民採金、1 個伐木;先出大魔導師;兩座兵營出步兵和火槍兵;
存到 12 個兵帶著英雄去打對面分礦;英雄血量低於 30% 就撤回家;
打野時優先打離家近的營地。」>
```
### 怎麼把打法說清楚
模型最怕含糊的要求。比起「打得兇一點」,下面這些資訊更有用:
- **種族和英雄**:先出哪個英雄、技能加點順序(例如大魔導師:水元素、暴風雪、水元素……)。
- **建造順序**:第幾個農民時蓋兵營、什麼時候升級主城、要幾座兵營。
- **出兵組合**:步兵 + 火槍兵?到多少數量出門?
- **進退條件**:存到多少兵出擊、英雄血量低於多少撤退、打殘了回家重新集結。
- **打野**:打不打、什麼時候打(天黑後?)、只打打得過的點?
- **是否公平**:將來要上擂台就說「只用視野內看得見的敵人」。
## 3. 跑起來
把模型給的程式碼存成 `brains/my_bot.py`,然後:
```bash
python tools/play.py --bot brains/my_bot.py --race 1 --difficulty 2
```
想快點看到結果,加上 `--speed 200`(2 倍速)。
## 4. 讓它修改
- **出錯了**:把**完整的錯誤訊息**原封不動貼回給模型,說「修一下」。
- **打得不好**:描述你**在遊戲裡看到了什麼**,而不是你猜的原因。例如「英雄一直站在家裡不動」「兵一個一個送上去」「農民擠在同一座礦上」。
- **想加新打法**:一次只加一件事,跑一局確認沒壞,再加下一件。
> **說明**
>
> 能自己執行命令的 Coding Agent 可以把第 3、4 步也接手:跑一局、讀日誌和回執、改程式碼、再跑。怎麼讓它看到足夠的資訊,見 [Agent 自主迭代](https://war3ai.com/zh-tw/docs/agent-loop/)。
## 5. 常見問題
| 現象 | 多半是 |
|---|---|
| 什麼都不動 | 實例編號不對(`--inst`),或者遊戲還沒進入對局 |
| 農民不採礦 | 對正在工作的農民下了令;只派 `idle_workers()` 裡的 |
| 一直蓋不出房子 | 用 `build_near`,別把座標寫死;看回執的 `reason` 是不是錢不夠 |
| 英雄出不來 | 看 `train` 的回執:人口不夠?還是英雄陣亡了(要 `revive`)? |
| 英雄不放技能 | 沒學(`learn`)或者沒魔力;放完看 `cooldown()` 有沒有進入冷卻 |
| 兵一拍一拍地抽搐 | 每拍都在重新下命令;只對閒著的單位下令 |
| 兵出不來、錢一直漲 | 人口卡住了:看 `g.production(兵營).blocked` |
| 模型用了不存在的方法 | 在提示詞裡再強調一遍「只能用 api.json 裡的方法」,並把 api.json 完整貼上 |
## 進階
- 全部 API 和每個 API 的底層機制:[API 目錄](https://war3ai.com/zh-tw/api/);
- 參考大腦(`brains/xwar3/strategy`)是一個完整的、會開礦打野出擊的 AI,可以讓模型讀它的思路,但它用的是更底層的 API,不建議直接照抄;
- 將來上 [對戰平台](https://war3ai.com/zh-tw/arena/) 時只能看見視野內的敵人 —— 現在就加上 `--fair` 自我約束,將來就不用改。
---
# Agent 自主迭代
> 讓 Coding Agent 自己跑對局、讀結果、改程式碼、再跑:它需要一條能無人值守執行的命令、一份結構化的對局報告,以及一個明確的目標。
在 [用 LLM 寫一個 Bot](https://war3ai.com/zh-tw/docs/ai-bot/) 裡,「跑一局 → 看現象 → 告訴模型」這一步由你來做。能執行命令的 Coding Agent(Claude Code、Codex、Cursor 的 Agent 模式等)可以把這一步也接手,形成閉環:
```text
改程式碼 ──► 跑一局(無人值守)──► 讀對局報告 ──► 找出最影響結果的一處 ──┐
▲ │
└─────────────────────────────────────────────────────────────────────┘
```
要讓這個閉環真正收斂,Agent 需要三樣東西。
## 1. 一條無人值守的命令
```bash
python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair
```
- `--minutes` 讓這一局一定會結束(以實際時間的分鐘計),Agent 不會卡在一局裡;
- `--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 = [] # [遊戲秒, 類別, 四字碼]:訓練 / 研究 / 建造 / 升級完成
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` 事件(帶有花了多少遊戲秒) | 第幾秒出第一個英雄、第幾秒升級主城、兵營有沒有一直在出兵;可以和職業選手的開局比較 |
| 雙方傷亡 | `unit.died` 事件 | 是不是一直在送兵、英雄陣亡幾次、打野有沒有賺 |
| 結束原因 | `on_end(g, reason)` | `我方没有单位了`(我方沒有單位了)= 輸了;`到时间了`(時間到了)= 還沒分出勝負 |
| 最終兵力和資源 | `on_end` 時讀一次快照 | 錢存著沒花 = 生產跟不上;工人太少 = 經濟沒起來 |
> **說明**
>
> 以程式判定勝負是 [對戰平台](https://war3ai.com/zh-tw/arena/) 的地基實驗之一,還在路線圖上。目前可以用 `我方没有单位了`(我方沒有單位了)判定輸,用「看得見的敵方建築全滅」近似判定贏。
## 3. 一個明確的目標和幾條約束
把下面這段交給 Agent,依你的目標修改:
```text
目標:讓 brains/my_bot.py 在 Echo Isles 上穩定打贏「簡單」難度的電腦(人類對隨機種族)。
每一輪:
1. 執行 python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair
2. 讀終端機輸出和最新的 run_*.json:結束原因、生產時間軸、被拒最多的原因、雙方傷亡
3. 找出最影響結果的「一個」問題,只改這一處;在程式碼註解裡寫下修改理由和依據的資料
4. 回到第 1 步。連續 3 局沒有進步就停下來,把報告和你的判斷告訴我
約束:
- 只能用 docs/api.json 裡的方法,不要編造 API
- 不要每拍對同一個單位重複下同一條命令;只對閒著的單位下令
- 保持 --fair(只用視野內看得見的敵人)
- 改程式碼前先執行 python tools/run_tests.py,確認沒有把範例改壞
```
## 讓閉環更快收斂的幾個習慣
- **一次只改一處。** 同時改三處,贏了不知道是哪一處起作用,輸了也不知道是哪一處改壞的。
- **比較時要打夠局數。** 同一個局面的隨機性很大;兩局只看得出很大的差距。判斷「是否進步」至少要看幾局的趨勢。
- **先修「被拒」再調策略。** 回執裡被拒最多的原因,往往就是 Bot 最大的 bug。
- **把判斷寫進註解。** 下一輪的 Agent(或下一次對話)能從註解裡知道為什麼這樣寫,不會把修好的地方改回去。
- **用離線測試兜底。** 為關鍵邏輯寫不需要開遊戲的單元測試(範例 Bot 的測試在 `brains/examples/tests/`),Agent 每次改完先跑一遍。
---
# LLM 當參謀
> 把「該存什麼、該往哪裡派人、這一分鐘該打還是該緩」交給 LLM,規則層只負責執行和否決。參考大腦已經這樣做了,這一頁把模式和陷阱講清楚。
Bot 寫到一定程度,你會發現運營層的規則是一條一條疊上去的:伐木人數一條、每座礦 5 人一條、木材多了減半一條、金多木少就多派人一條……每條單獨看都對,合起來卻會出現「礦上缺人,而所有農民都在砍樹」這種**沒有任何一條規則負責**的局面。
這類「看全局、排優先順序」的判斷本來就不適合寫成 `if / else`,卻正好是 LLM 擅長的。參考大腦(`brains/xwar3/strategy/brain/coach.py`)用的就是下面這套分層。
## 分層
```text
LLM(顧問) 每 20 遊戲秒一次,非同步,不阻塞任何一拍
輸入:一頁局面快照(資源、人口、農民分布、礦、兵種、科技、英雄、敵情、最近發生的事)
輸出:嚴格 JSON —— 一句診斷 + 工人配比 + 優先生產什麼 + 這一分鐘的姿態 + 不要做的事
│
▼ 白名單 + 上下限鉗制 + 否決
規則層(Bot 每拍) 把建議轉成既有能力的「偏置」:工人配比、建造 / 訓練優先順序、進攻姿態
│
▼
執行層(SDK / 毫秒層) 下令、讀回執、微操
```
## 輸出契約
讓模型只輸出固定欄位的 JSON,不准增刪:
```json
{
"diagnosis": "一句話:局面裡最大的問題,必須能在輸入資料裡找到依據",
"workers": { "gold": 10, "lumber": 6 },
"priority": ["hpea", "hhou", "hbar"],
"posture": "creep",
"avoid": ["木材不夠時別先研究鐵甲"]
}
```
| 欄位 | 規則層怎麼用 | 參考大腦的鉗制 |
|---|---|---|
| `workers` | 採金、伐木的目標人數 | 採金 2 ~ 25,伐木 1 ~ 20;兩者相加不超過農民總數 |
| `priority` | 訓練 / 建造 / 研究的優先順序 | 最多 4 個;只接受「可選代碼」表裡出現過的四字碼 |
| `posture` | 這一分鐘的姿態 | 只能是 `attack` `defend` `creep` `expand` `recover` `hold` 其中之一 |
| `avoid` | 這一分鐘不要做的事 | 最多 2 條 |
| `diagnosis` | 只用於日誌和指揮台顯示 | — |
每個種族各一份提示詞,只寫這一族特有的取捨(人類的協力建造和民兵、獸人的地洞、不死族的鬧鬼金礦、夜精靈的纏繞金礦……),通用規則放在共用部分,不要抄四遍。
## 四條硬性約束
這四條都是參考大腦實際繳過學費的:
1. **顧問永遠不直接對單位下令。** 它看不到 150 ms 的現場,也會產生幻覺。它只改目標和優先順序,具體誰去哪、打誰,仍由規則層和毫秒層決定 —— 指揮權只能有一個主人。
2. **非同步。** 顧問一次大約 1 秒,在背景執行緒跑,以最新結果為準,**永遠不阻塞一拍**。模型沒啟動、逾時、亂答,就當沒有這一層,行為回到純規則。建議太舊(超過 3 個間隔)也不用。
3. **白名單 + 鉗制。** 每個欄位都要能對應到既有能力,數值鉗制在合理區間。不認得的內容**計數後丟棄**,而不是默默忽略。
4. **全程計數。** 問過幾次、成功幾次、逾時幾次、被鉗制幾次、各欄位被採納幾次,連同最後一次送給模型的輸入一起發布出來。否則「這一層到底有沒有用」是個無法回答的問題。
> **能安全降級的東西,最容易悄悄降級**
>
> 顧問設計成「失敗 = 當沒有這一層」,所以模型服務沒啟動時,Bot 的行為和純規則一模一樣,從外面完全看不出來。參考大腦就發生過整整一天 6 個實例的顧問全部連不上、卻沒人發現的事。一定要把「最近一次成功時間」和「最近一次失敗原因」發布出來 —— [遠見指揮台](https://war3ai.com/zh-tw/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 = """你是《魔獸爭霸 III》的運營教練,只管運營和戰略,不管微操。
只輸出 JSON,欄位固定:{"diagnosis": 一句話, "workers": {"gold": 整數, "lumber": 整數},
"priority": [四字碼, 最多 4 個, 只能用 allowed 裡的], "posture": 六選一, "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 模型(每次只啟用少量參數)比同尺寸的稠密模型快得多。參考大腦用 Qwen3.6-35B-A3B(LM Studio,Q4),中位數 **1.09 秒**,最慢 1.45 秒,5/5 的輸出能直接 `json.loads` |
| 本機、會「思考」的模型 | **必須關掉思考段落**,否則 token 全花在思考上、一個 JSON 都產生不出來。LM Studio 不理會 `/no_think`;參考大腦改走 `/v1/completions`,自己組 ChatML、預先填入空的 `` 和一個 `{` |
| 雲端模型 | 延遲通常更高,但這套分層本來就是非同步的;運營決策以分鐘計,幾秒的延遲可以接受 |
> **說明**
>
> 同一個模型還能幫單位配音:見 [頭頂氣泡與本機模型](https://war3ai.com/zh-tw/docs/speech/)。想讓模型直接逐拍下令(而不是當參謀),請等 [對戰平台](https://war3ai.com/zh-tw/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、當參謀、讓單位說話,這是另一種接入方式:**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/zh-tw/docs/gateway/)的限制一樣:拿不到結束遊戲、調整速度、暫停,拿不到看得見別人底牌的 API,帶玩家編號的查詢只能查自己。
幾個上限:一個工具結果最多 20 萬個字元,超出的部分會截斷並提示怎麼縮小範圍;`war3_ask_player` 最多等 120 秒;截圖的 `scale` 在 0.1 到 1 之間。
## 工具
| 工具 | 做什麼 |
|---|---|
| `war3_overview` | 一頁局面:時間、資源、人口、我方各兵種數量、英雄(血量、魔力、等級、冷卻)、看得見的敵方兵種、生產。**先呼叫它** |
| `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` | 螢幕上方一行字 / 單位頭頂一句話 |
| `war3_ask_player` | 在螢幕中間給玩家幾張選項卡片,等玩家點選,回傳選了哪一個(可以暫停遊戲) |
| `war3_screenshot` | 遊戲畫面截圖(PNG;視窗被擋住也能截,不搶焦點) |
| `war3_jass` | 執行一段 JASS(只開放給 dev;改動世界只限單人局) |
能玩出來的:
- **陪玩 / 教練**:`war3_overview` 看局面,`war3_toast` 在螢幕上給建議;
- **邊打邊問玩家**:`war3_ask_player` 跳出三張卡片,玩家點哪張就照哪張來;
- **解說**:`war3_events` 讀取發生了什麼,`war3_say` 讓單位自己說出來;
- **直接指揮一支部隊**:`player` 角色 + `war3_call`,只能動自己的單位;
- **看圖調整介面**:`war3_screenshot` 截一張,看自己畫的按鈕擺得對不對。
## 一次對話大概是這樣
```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`(三張卡片,模擬點第二張 → `{"picked": 1, "option": "開分礦"}`)。
- Claude Code 2.1 實際掛上:它自己啟動伺服器、交握,狀態 `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/zh-tw/docs/gateway/) 共用同一套角色白名單、單位參數格式和「一頁局面」。
- 日誌走 stderr,stdout 只有協定內容。
---
# 心智模型
> 快照、命令、回執、事件、一拍、批次。理解這六個概念,就理解了 API 為什麼長這樣,以及怎麼寫才快。
## 快照:讀取,零等待
執行環境每 **50 ms** 在遊戲執行緒上把整個世界採集一遍,寫進共享記憶體。`g.snapshot()` 拿到的是一份**完整、自洽**的世界:
- 16 個玩家欄位:金、木、人口、人口上限、累計採集量、種族;
- 最多 1024 個單位:類型、擁有者、座標、生命 / 魔力(含上限)、目前訂單和訂單目標、**實際在打誰**(任務目標)、英雄等級 / 經驗 / 技能點、每個玩家對它的可見性;
- 最多 256 份單位細節:12 個技能(等級、剩餘冷卻)、8 個 buff、6 格物品欄;
- 地上物品、樹(每 2 秒更新)、生產表(訓練 / 研究 / 建造 / 升級的進度)、遊戲時鐘、遊戲內時間。
讀一份約 **0.4 ms**(Python 解析),不必等遊戲執行緒。所以:**儘管讀**。`g.units()`、`g.my_army()`、`g.cooldown()`、`g.inventory()` 這類 API 都是從同一份快照裡取值,一拍裡呼叫多少次都不貴。
> **提示**
>
> 發布週期可以調整:`g.set_publish_period(ms)`,16 ~ 1000 毫秒。一次採集在遊戲執行緒上約 0.5 ~ 0.9 ms,設成 33 ms 也沒問題。整台機器共用一個值,以最後寫入的為準。
## 命令:寫入,約一幀
`g.move / attack / gather / build / train / cast …` 交給遊戲執行緒執行。執行環境在遊戲執行緒的**事件分派**裡批次執行用戶端提交的命令,所以一條命令大約等**一幀**(落在事件叢集裡時約 0.1 ms,否則等到下一次分派)。
- 命令可以傳入**一個單位或一個清單**,清單裡的單位在同一幀一起下令;
- 加上 `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/zh-tw/docs/reason-codes/)。
## 事件:發生了什麼
`on_event(g, ev)` 會在每拍的 `on_tick` 之前,把上一拍以來的事件逐一交給你:
| 事件 | 含義 |
|---|---|
| `unit.appeared` / `unit.died` / `unit.removed` | 單位出現、死亡、消失(進入金礦、被轉化、屍體腐爛也算消失,不等於死亡) |
| `unit.damaged` / `order.changed` / `owner.changed` | 掉血、換訂單、換擁有者 |
| `hero.levelup` | 英雄升級 |
| `item.appeared` / `item.removed` | 地上的物品出現、被撿走或用掉 |
| `damage` | 引擎層級:**每一下**傷害。來源單位、攻擊類型、傷害類型、實際扣血、護甲前傷害 |
| `killed` | 引擎層級:這一下把它打死了,帶有兇手 |
| `production.done` | 訓練 / 研究 / 建造 / 升級完成,帶有四字碼和花了多少遊戲秒。對手的也有 |
| `spell.cast` | 單位放了技能:技能四字碼、等級、冷卻秒數、施法點 |
| `message` | 螢幕訊息框裡出現一條:遊戲提示(「需要更多的農場」)、聊天(`.chat` 裡有發言者和內容)、系統訊息 |
| `selection.changed` / `player.left` | 本機玩家的選取變了 / 有玩家離開或被判定落敗移除 |
| `game.started` / `game.ended` | 新的一局開始 / 離開對局 |
畫板按鈕被點擊、熱鍵、點地面這些輸入事件,請見 [介面與輸入](https://war3ai.com/zh-tw/docs/ui-input/)。
> **注意**
>
> 事件流是**全域**的:對手的生產完成、野怪的死亡都在裡面。請用 `ev.owner` 或單位控制代碼過濾。
## 一拍:Bot 的節奏
`on_tick` 預設每秒呼叫 5 次(依實際時間)。一拍的耗時基本上就是你自己的運算:快照零等待,命令約一幀。一拍超過週期會自動順延,不會越積越多。
- **2 倍速下別依實際時間等待。** 想等 3 遊戲秒,就看 `g.clock()` 增加了 3,不要 `sleep(1.5)`。
- **別在 `on_tick` 裡 `sleep`。** 需要「過一會兒再做」,就記下目前的遊戲時間,下一拍再判斷。
## 批次:幾十條命令只等一次
一拍要下很多條命令時,包在 `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")
# 區塊結束時整批提交:同一幀執行,只等一次遊戲執行緒
```
- 區塊裡的命令回傳 `Pending`,區塊結束後變成回執;區塊結束前讀取它會拋出錯誤;
- 區塊裡拋出例外時,**整批作廢**(只送出一半的命令比不送更危險);
- 實測 8 條移動:逐條 68 ~ 99 ms,一批 **6.5 ~ 10 ms**。
同樣的思路也適用於查詢:`g.can_do_many([(u, code), ...])`、`g.tech_many([...])` 一次查詢很多個。
## 讀取自己剛寫入的
同一拍裡,快照還看不到你剛下的命令(下一次發布才會跟上)。兩段邏輯可能會搶同一個工人:一段剛派它去蓋農場,另一段看快照以為它還閒著。
`g.order_of(u)` 解決這個問題:快照跟上之前,它以回執裡的新訂單為準。**判斷「是否閒置」用 `g.order_of(u)`,不要用 `u.order`。** `g.idle_workers()` 已經把「這一拍剛被派了工作的」排除掉了。
## 延遲檔位
| 檔位 | 通道 | 延遲 | 用於 |
|---|---|---|---|
| 0 | 推送快照 + 事件流 | 讀一份約 0.4 ms;資料每 50 ms 更新一份 | 所有「看」的 API |
| 1 | 快車道 | 約 1 幀;6 個行程並行時中位數 0.06 ms | 所有命令和查詢(SDK 預設) |
| 2 | 控制通道 | 20 ~ 40 ms | 備援、少數介面類操作(倍速、氣泡、訊息) |
| 3 | [閘道](https://war3ai.com/zh-tw/docs/gateway/)(WebSocket / JSON) | 檔位 1 + 約 1 ms | 任何語言、瀏覽器、LLM、另一台電腦上的程式 |
[API 目錄](https://war3ai.com/zh-tw/api/) 裡每個 API 都標明了它走哪一個檔位。
---
# 十五條規矩
> 每一條都是在真實對局裡踩坑踩出來的。寫 Bot 時對照一遍,能省下大部分除錯時間。
> **提示**
>
> 把這一頁和 [`api.json`](https://war3ai.com/zh-tw/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, 點清單)`;要讓一個工人連續建造多座,用 `g.build_queue(worker, 計畫)` —— 它們會倒序插入,幫你處理好。
### 9. 一拍的命令一批送出
幾十條命令逐條送出,要等幾十次遊戲執行緒;包進 `with g.batch():` 只需要等一次。
## 經濟與生產
### 10. 一座礦最多 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`。直線距離最近的那個點,可能在海的另一邊。
## 還有一條:照公平模式寫
`--fair` 下只看得見視野內的單位、物品、生產和事件,擂台用的就是這套規則。現在就照公平模式寫,將來上[對戰平台](https://war3ai.com/zh-tw/arena/)不用修改。詳見[公平模式](https://war3ai.com/zh-tw/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)` 詢問引擎(看得見/戰爭迷霧/黑色遮罩),走快車道,每次約一幀。一拍裡要判斷很多單位時,請用快照裡的 `u.visible_to()`,別逐一呼叫 `g.visible()`。
## 敵人的記憶:`last_seen`
人類玩家會記得「剛才在那邊看到一隊狼騎兵」。SDK 也幫你記:每次刷新快照時,記下我方此刻看得見的敵方與野怪單位(最後位置、血量、時間);看到它陣亡就刪除,換局時清空。
```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 號玩家的單位。兩個 AI 對戰,就是在同一局裡開兩條這樣的通道。
> **本機模式下,公平是約定,不是安全邊界**
>
> 在你自己的電腦上,沒有任何辦法阻止一個程式讀取全圖。`--fair` 是你對自己的約束;真正的比賽由[對戰平台](https://war3ai.com/zh-tw/arena/)的裁判行程保證:Bot 永遠碰不到共用記憶體,只能拿到裁判依視野過濾後的觀察,只能提交動作,而且每個動作都會先校驗單位歸屬。
## 為什麼現在就該開啟
- 將來上擂台時規則就是這樣,現在照公平模式寫,到時候一行都不用改;
- 少了全圖資訊,才知道你的 Bot 真正的實力如何(參考大腦目前大量依賴全圖資訊,例如電腦隊長的目標點 —— 這正好是一次檢驗);
- 在公平模式下寫出來的偵察、記憶與判斷,才是真正有價值的 AI 能力。
---
# 職業打法食譜
> 高手的優勢大多來自幾十個「小習慣」。這一頁把常見的職業打法逐條落實到 SDK 程式碼上,每段都能直接貼進 on_tick。
約定:`g` 是 `Game`,`home` 是我方主基地(`g.my_buildings({"htow", "hkee", "hcas"})[0]`),`now = g.clock()`。API 細節見 [API 目錄](https://war3ai.com/zh-tw/api/),完整可執行的範例在 [範例 Bot](https://war3ai.com/zh-tw/docs/examples/)。
> **提示**
>
> 要讓 LLM 加入某種打法時,把對應的那一招連同程式碼一起貼給它,比描述「打得職業一點」有效得多。
## 一、運營
### 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") # 蓋完回去採礦,不用下一拍再找它
```
一個農民連蓋幾座:`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"主城還要 {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 遊戲秒內看到過的敵人(最後的位置和血量)
...
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) # 離天黑還有幾遊戲秒(一天 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))) # 火槍兵打獅鷲(穿刺打小甲 ×2)、獅鷲打步兵(魔法打大甲 ×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. 一拍的命令一批送出
```python
with g.batch():
g.attack(melee, target_a)
g.attack(ranged, target_b)
g.move(wounded, *home_xy)
g.cast(hero, "thunderclap")
```
幾十條命令只等一次遊戲執行緒(實測 8 條移動 68 ms → 6.5 ms)。
### 18. 攻城:用砲攻擊地面
```python
g.attack_ground(mortars, tower.x, tower.y) # 迫擊砲 / 投石車對一塊地方開火(樹林後面、隱形單位)
```
## 五、英雄
### 19. 技能加點表
```python
SKILLS = {"Hamg": ["AHwe", "AHbz", "AHwe", "AHbz", "AHwe", "AHmt"]} # 水元素、暴風雪……6 級終極技能群體傳送
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) # 回城卷軸(對點使用物品)
```
## 六、復盤
- 每拍把決策寫進日誌(`print` 會輸出到執行視窗),搭配 `g.say(單位, "撤")` 在遊戲裡查看;
- `production.done` 事件帶有「花了多少秒」—— 統計自己的建造時間軸(第幾秒出第一個英雄、第幾秒升級主城),和高手的比較;
- 讓 Agent 自己復盤:見 [Agent 自主迭代](https://war3ai.com/zh-tw/docs/agent-loop/) 裡的對局報告。
---
# 範例 Bot
> 四個由淺入深的範例,每個都能直接執行,每一段邏輯都對應一項 SDK 能力。另附一個完整的參考大腦。
範例都在 `brains/examples/`,後一個繼承前一個,只加入新東西。建議依序閱讀:
| 範例 | 學什麼 | 執行方式 |
|---|---|---|
| `hello_bot.py` | 採集(一座礦 5 人、礦滿了就去伐木)、生產農民(佇列只排 1 個)、蓋人口建築、續建停工的地基;四個種族都能跑 | `python tools/play.py --bot brains/examples/hello_bot.py` |
| `rush_bot.py` | 兵營和祭壇(沒有就用 `build_near` 蓋)、先出英雄(陣亡就復活)、有技能點就學、湊滿一波就攻擊移動 | `… --bot brains/examples/rush_bot.py` |
| `macro_bot.py` | 建造順序 + 蓋完自動回礦(Shift)、人口卡住立刻補、兵營佇列 1 個、攻防升級、升級主堡和高階兵種、依**地面實際路程**選目標並沿路徑前進 | `… --bot brains/examples/macro_bot.py --speed 200` |
| `micro_bot.py` | 在運營之上接手戰鬥:集火最快能擊殺的目標、拉回殘血單位、英雄保命、夜裡挑打得贏的野怪營地、敵人摸到家門口就回防;一拍的命令一批送出 | `… --bot brains/examples/micro_bot.py --fair` |
> **說明**
>
> `hello_bot` 和 `rush_bot` 的註解裡記錄了實機踩到的坑,例如「每次都挑第一個工人去蓋房子,結果 3 座農場全是蓋到一半的地基」「寫死的兵營座標剛好是樹林,3 分鐘一座都沒蓋出來」。讀註解比讀程式碼更有收穫。
## 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 # 一座礦最多 5 個農民(再多收入也不會增加)
LUMBER_CREW = 5 # 伐木人數:每座礦 5 個採金 + 這麼多伐木 = 工人目標數
```
做三件事:閒置工人去採金(自己記錄每座礦有幾個人,礦滿了就去伐木);工人不夠就生產(佇列裡只排 1 個);人口快滿時,找一個沒在蓋房子的工人,在大廳旁邊蓋人口建築(人類、獸人還會派人去續建停工的地基)。
## rush_bot:出兵與出擊
在 `hello_bot` 的基礎上加三件事:兵營和祭壇沒有就蓋;祭壇出英雄(**陣亡了先復活**,英雄是唯一的)、有技能點就學;湊到 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` 詢問引擎能不能研究下一級攻防;升級主堡和高階兵種(實機教訓:一直停在 T1,23 分鐘被 T3 的騎士、獅鷲騎士推平);依 `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,分為三層:
| 層 | 位置 | 節奏 | 做什麼 |
|---|---|---|---|
| 策略層 | `strategy/` | 秒級 | AMAI 式的多戰略選擇與切換、建造表、反制兵種、選英雄;可選的 [LLM 運營顧問](https://war3ai.com/zh-tw/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` 重試)。
啟動參考大腦最簡單的方法是用[遠見指揮台](https://war3ai.com/zh-tw/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) # 這條在遊戲執行緒上執行了多少微秒/其中引擎下令函式本身花了多少
```
正常情況下,一條命令在遊戲執行緒上花費幾微秒到幾百微秒。批次區塊結束後,`g.last_receipts` 就是這一批每條命令的回執。
## 在遊戲裡看
```python
g.say(unit, "撤") # 單位頭頂冒出一個聊天氣泡(不影響遊戲)
g.message("開始練功") # 在左下角訊息區印出一行字(只有本機看得見)
```
`print` 的內容會出現在執行 Bot 的終端機裡。把每拍的關鍵決策印出來,再搭配頭頂氣泡,比看程式碼快得多。
## 一拍很慢
先看看是不是以下幾種情況:
| 原因 | 改法 |
|---|---|
| 一條一條送出命令,每條都等一幀 | 包進 `with g.batch():`,幾十條只等一次 |
| 逐一呼叫 `g.visible()` / `g.can_do()`(每次都走快車道等一幀) | 可見性用快照裡的 `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、世界發布週期與每次擷取耗時、快車道計數、是否在對局中、單位數、遊戲時鐘。
## 實機核對腳本
開一個測試實例,逐項核對 SDK 能力在你的電腦上是否正常:
```bash
python tools/sdk_live_check.py --inst 20 # 全部
python tools/sdk_live_check.py --inst 20 --only prod # 只核對一節
```
分節:批次、時間、生產、排程下令、戰鬥屬性、尋路、公平模式。每一節都會在真實對局裡下命令、讀回效果,並印出通過數。
離線測試不需要開遊戲:
```bash
python tools/run_tests.py
```
## 常見的「看起來像 bug」
- **蓋房子的回執接下了,卻一直沒有地基**:樹林裡的點引擎也會當場接下,工人走到了才失敗。改用 `build_near`,它會追蹤結果,並把失敗的點暫時列入黑名單。
- **技能的回執接下了,卻沒放出來**:被打斷了,或者魔力不足。放完後下一拍看 `g.cooldown()` 有沒有進入冷卻。
- **攻擊命令接下了,兵卻去打別人**:要攻擊特定目標,請用 `g.attack(兵, 敵人)`(右鍵語意)。原始的攻擊指令對目標只會更換指令、不會記住目標,於是會去打附近的其他單位。
- **工人數對不上**:進入金礦的工人不在快照裡。
- **陣亡的英雄訓練不出來**:英雄是唯一的,要用 `g.revive(祭壇)`;復活需要人口,陣亡後約 3 遊戲秒才能復活。
---
# RPG 玩伴
> 在 RPG / 自訂地圖裡幫玩家配一個 AI 夥伴:跟著你、幫你打怪、殘血時幫你補血、陪你聊天。四種形態,繼承一個類別、改幾個屬性,就是你自己的玩伴。
不只是對戰。在 RPG、自訂地圖裡,你可以幫自己配一個 **AI 夥伴**:它跟著你走、幫你打怪,你殘血時幫你補血,沒事的時候陪你聊兩句 —— 台詞還能接本機 LLM。
**怎麼用由你自己決定。** 這件事拆成三層 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`:繼承一下、改幾個屬性,就是一個會跟隨、助戰、補血、聊天的夥伴 | 想要一個夥伴 |
> **注意**
>
> 只用於**單機、區域網路自建**的遊戲。造單位、設盟友這類操作是本機單方面修改世界:單人局(和電腦打)沒問題;多人局會讓其他玩家不同步,所以多人局裡 JASS 通道只放行唯讀的函式,玩伴自動退回「只說話」。
## 最快上手:在遠見裡一鍵開啟
1. **選地圖**:遠見「實例」頁 →「下一局設定」→ 地圖,選一張 RPG 地圖(遊戲目錄 `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」時會自己按一下空白鍵(`g.press_to_continue()`,只往遊戲視窗送按鍵訊息,不搶焦點)。
## 寫一個自己的玩伴
```python
from openwar3.companion import Companion
from openwar3.talk import Talk
class MyBuddy(Companion):
mode = "ally" # 形態,見下表
unit = "Hpal" # 造什麼:任何四字碼,地圖自訂的也行
nickname = "小聖"
heal = ("holybolt", "AHhb", 0.55) # (施法訂單名, 要學的技能, 主人血量低於多少就補血);None = 不補血
follow_distance = 350
talk = Talk(persona="活潑的小聖騎士,愛幫主人加油")
```
### 四種形態
| mode | 玩伴是誰 | 說明 |
|---|---|---|
| `ally`(預設) | 占一個空著的玩家槽位,當你的**盟友** | 有自己的顏色和名字(計分板、盟友面板顯示 `nickname`);你點不到它,它自己打。框架自動設成同盟 + 共享視野 |
| `own` | 造在**你名下** | 你隨時能手動指揮它;你不管的時候,AI 替你操作 |
| `adopt` | 接管地圖裡**已有**的單位 | 覆寫 `adopt(g)`,回傳那個單位(地圖給你的寵物、隨從) |
| `voice` | 不造單位,**只說話** | 陪聊、提醒;不修改世界,多人局也能用 |
沒有空槽位時 `ally` 自動退回 `own`;多人局或造不出來時自動退回 `voice`。
> **說明**
>
> `ally` 形態的玩伴認的是「那個槽位現在最好的單位」(英雄優先),而不是死守一個單位。實測時有地圖把玩伴當成真人玩家,刪掉聖騎士、發了一個地圖英雄 —— 玩伴會直接接管這個英雄,也會學地圖為它設定的技能。英雄死了優先原地復活;地圖自己復活了就接著用。
### 每一拍做什麼
依序檢查,哪條成立就做哪條:
| 順序 | 行為 | 條件 | 可調 |
|---|---|---|---|
| 1 | 撤退 | 自己血量低於 25% 且附近有敵人:退到主人身後 | `retreat_at` |
| 2 | 補血 | 主人血量低於設定值、技能冷卻好了、距離在 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`。範例裡的反應是:主人沒滿血就幫主人補一口血,否則說句話。
- **頭像對白**:打招呼、主人倒下、主人升級、玩伴回來,這幾句會用遊戲自己的頭像對白來說(底部頭像換成玩伴,畫面上出現字幕),其餘的用頭頂氣泡顯示。
- **狀態面板**:畫面左側一塊面板,顯示玩伴的血條、正在做什麼、心情(開心 / 興奮 / 緊張 / 害怕 / 難過)、擊殺和補血次數。它用 [畫板](https://war3ai.com/zh-tw/docs/canvas/) 繪製,多人局也安全。
### 說話,以及本機 LLM
`Talk` 依事件挑選台詞,頭頂冒出氣泡;`voice` 形態或冒不出氣泡時,顯示在畫面左下角。每句話也會寫進方案日誌,事後能查它說過什麼。
| 事件 | 什麼時候 | 事件 | 什麼時候 |
|---|---|---|---|
| `hello` | 剛來 | `master_low` | 主人殘血 |
| `poke` | 主人右鍵點它 | `master_levelup` | 主人升級 |
| `fight` | 開打 | `master_died` / `master_back` | 主人倒下 / 復活 |
| `kill` | 打死一隻怪(會說出怪的名字) | `buddy_low` / `buddy_died` / `buddy_back` | 玩伴自己殘血 / 倒下 / 回來 |
| `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 地圖的單位、物品、英雄大多是地圖自己新建的(四字碼像 `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)不帶標準的物件資料檔,名稱會從地圖裡的文字資料讀取。實測本機 38 張 RPG / 自訂地圖全部解析成功,其中 37 張取得了單位名稱。
## 做成方案分享
玩伴就是一個 `openwar3.Bot` 子類別,可以做成 [AI 方案](https://war3ai.com/zh-tw/docs/schemes/) 分享給別人。說明書裡多寫兩項:
```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. **讀不到玩家打的任意聊天文字**。固定的聊天命令已經能用;要讓玩伴真的陪你自由聊天,還需要拿到文字本身。
2. **玩伴不懂具體地圖的玩法**(任務、商店、劇情)。它做的是通用的跟隨、助戰、補血;要懂某一張地圖,就在子類別裡針對那張地圖寫 —— `g.map_data` 能查名稱,`g.jass` 能呼叫任何函式。這正是留給你自己決定的部分。
---
# 畫板
> 在遊戲畫面上畫文字框、面板、進度條、圖片、貼著地面的圈和帶箭頭的路線。執行環境每幀自己繪製,不修改遊戲狀態,多人局也安全;Python、HTTP、直接寫共享記憶體都行。
外部程式可以在遊戲畫面上畫**文字框、面板、進度條、圖片、地上的圈、地上的路線(帶箭頭)**,由執行環境每幀自己繪製。拿來做自己的 HUD、輔助線、提示、教學標註、直播資訊板,都很合適。
## 畫板和 JASS 畫面函式,怎麼選
| | 畫板(本頁) | [JASS 畫面函式](https://war3ai.com/zh-tw/docs/jass/) |
|---|---|---|
| 誰來畫 | 執行環境自繪 | 遊戲自己(浮動文字、特效、面板、頭像對白……) |
| 多人局 | **安全**:只畫在本機畫面上,不建立遊戲物件、不修改遊戲狀態 | 只能單人局 |
| 樣式 | 隨意:中文字型、圓角、半透明、邊框、任意顏色、本機圖片 | 遊戲原生風格 |
| 跟著東西走 | 跟單位、世界座標、螢幕位置;地上的圈貼著地形起伏 | 看具體函式 |
| 開銷 | 實測每幀 0.2 ~ 0.35 ms(9 個元素) | 每次呼叫約 13 ms |
兩條路可以一起用:原生風格的效果用 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", "Boss 要放大招了!", 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(): ... # 一次改很多條,只寫一次共享記憶體
c.stats() # drawnFrames 在增加 = 真的在畫
```
每個元素用一個 `key` 識別:同一個 key 再畫一次就是更新。
**能點**:文字框和面板加上 `clickable=True`(滑鼠停在上面時的顏色用 `hover=` 設定),點中時事件流裡會來一條 `ui.click`,`ev.key` 就是這個 key,點在它上面的那一下遊戲收不到。現成的按鈕、選項卡片、熱鍵、點地面請見 [介面與輸入](https://war3ai.com/zh-tw/docs/ui-input/)。
**位置**(每個元素給一個):
- `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, ...)` | 進度條:血量、冷卻、讀條 | `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(任何語言)
遠見後端(只監聽本機):
```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 和遠見也可以:先送一次語意命令 `canvas_enable`(W3P 操作碼 73),執行環境會建好共享記憶體區塊 `Local\War3Canvas_`:標頭 64 位元組 + 256 條 × 112 位元組 + 64 KB 文字 / 點池。依 seqlock 寫入(序號變奇數 → 寫條目和池 → 序號變偶數),執行環境每幀讀一次,讀到寫了一半的就沿用上一幀,並回寫已繪製幀數、元素數和異常計數。Python 參考實作是 `sdk/python/w3canvas.py`,結構定義在協定標頭檔裡,見 [W3P 協定](https://war3ai.com/zh-tw/docs/protocol/)。
## 好幾個程式同時畫
模組、遠見、MCP、閘道可能同時在同一局裡畫東西,畫板只有一塊。規矩是:**每個程式只動自己的元素**。
- 寫入之前先取得一把具名鎖,讀出現有的元素,保留別人的,換上自己的,再寫回去;
- 每個元素記著是誰畫的(行程 ID + 行程內序號),畫它的程式結束了,下一次有人寫入時順手清掉;它的按鈕也不再攔截點擊;
- 元素編號從共用的計數器分配,不會撞號。
Python SDK 已經這麼做了,`clear()` 也只清自己的。自己直接寫共享記憶體的話照這個來,否則會把別人的東西沖掉。配置細節見 [W3P 協定](https://war3ai.com/zh-tw/docs/protocol/)。
## 實測與注意事項
- 2026-09-25 實測(1920×1080,2 倍速):9 個元素每幀 0.27 ~ 0.34 ms,約 63 幀 / 秒,0 次異常;寫入 9 條用時 6 ms;英雄走動時,跟著單位的圈、文字、血條都跟得上。內容變了才重畫貼圖,只挪動位置不重畫。
- 畫在遊戲介面之後、滑鼠游標之前:蓋在遊戲自己的血條、單位和介面上面,滑鼠游標則蓋在它上面。它會避開底部操作面板和頂部晝夜球,但**不會避讓地圖自己的面板**(右上角的排行榜、倒數計時)—— 自己的面板別放在右上角。
- 不在對局裡(主選單、結算畫面)時,放在世界座標和單位上的元素不畫,螢幕位置的照畫。
- 地上的圈是把圓周上的 64 個點各自投影到地面上,地形有高低時形狀會跟著起伏 —— 這是對的:它畫在真實的地面上。
- 第一次開啟要裝掛鉤、預熱字型,約 1 秒,期間文字類元素先不畫,圈和線照畫。
- 繪製時出現一次異常,本次工作階段就不再畫(和頭頂氣泡同一套保護),`stats()` 裡的 `faults` 會變成 1。
- 文字、圖片路徑和點一共 64 KB,最多 256 個元素;圖片路徑必須是遊戲行程讀得到的本機路徑。
[AI 玩伴](https://war3ai.com/zh-tw/docs/companion/) 的狀態面板就是用畫板畫的:血條、正在做什麼、心情、擊殺和補血次數。
---
# 介面與輸入
> 畫板上的按鈕、選項卡片能點,滑鼠停在上面會自動醒目顯示;登記熱鍵、點地面選位置、讀取滑鼠指著哪裡、知道本機玩家選取了誰。點擊、熱鍵、放技能、聊天全文、玩家離開,都會進入事件流。
[畫板](https://war3ai.com/zh-tw/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("升級了!選一個獎勵", [("力量 +5", "更耐打"), ("攻速 +20%", "輸出更高"), ("召喚狼", "多一個幫手")],
pause=True, on_pick=lambda g, i: give(g, i)) # 螢幕中間一排卡片;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 和[玩法模組](https://war3ai.com/zh-tw/docs/mods/)的執行器每拍都會呼叫。沒有給回呼的點擊會進入 `ui.clicks`。回呼裡拋出例外只會記入日誌,不影響其他回呼和事件。
也可以直接使用畫板底層:`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/zh-tw/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` 四字碼、`.b` 等級、`.value` 冷卻秒數、`.x .y` 施法點 |
| `message` | 螢幕訊息框裡出現一條 | `.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()` 另有一份獨立的游標:遊戲提示(「需要更多的農場」「不能在那裡建造」)也在裡面。寫 Bot 時用它,就知道一條命令為什麼沒做成。
## 從其他語言使用
- **閘道**:`ui.button`、`ui.choice`、`ui.hotkey`、`ui.mouse`、`ui.cursor` 這些方法在 [閘道](https://war3ai.com/zh-tw/docs/gateway/) 上以同名提供。遠端無法傳入回呼函式,點擊和熱鍵從事件推送裡接收(`ui.click` 事件帶有 `key`)。
- **直接寫入共用記憶體**:先送出語意命令 `input_enable`(W3P 操作碼 74),執行環境開始接管輸入;在輸入區塊 `Local\War3Input_` 裡,你寫入熱鍵表和滑鼠開關,它回寫滑鼠位置、指著的地面點和懸停項目。畫板項目的旗標位元 `0x40` 表示「可互動」。配置見 [W3P 協定](https://war3ai.com/zh-tw/docs/protocol/)。
## 好幾個程式同時使用
模組、遠見、MCP、閘道的每個工作階段可能同時在一局遊戲裡放按鈕、登記熱鍵,彼此互不干擾:
- 每個程式登記自己的熱鍵和地面點擊開關,SDK 把大家的合併成一張表交給執行環境。同一個鍵只留一條,事件發給所有人,各自依鍵認出自己的熱鍵;
- `ui.close()` 只撤掉自己的,最後一個程式離開了才交還視窗輸入;
- 程式被強制結束、來不及收尾:執行環境每 2 秒檢查一次,登記過的程式全都結束了,就清掉它們留下的熱鍵和地面點擊攔截,它們畫的按鈕也不再攔截點擊。
## 實測
2026-09-25,測試實例上的實機核對 16/16:
- 點按鈕 → `ui.click` + 回呼,執行環境的攔截計數 +1(遊戲沒收到這一下);點在按鈕外面不觸發;
- F6 → `hotkey`;點地面 → `mouse.world`(被吞掉);
- 刷出一個聖騎士、選取它 → `selection.changed`,`g.selection()` 對得上;放神聖護甲 → `spell.cast('AHds', 1, 35.0)`;
- 地圖文字 → `message`;聊天 → `message`,`.chat` 解析出發言者和內容;
- 判定電腦落敗 → `player.left`;結束對局 → `game.ended`。
真人用滑鼠點按鈕、懸停醒目顯示也逐一看過。
## 邊界與注意事項
- **位置用的是真實滑鼠**:遊戲本身依系統游標讀取位置,所以懸停、`cursor()` 反映的是真實滑鼠。攔截只管按鍵。
- **畫在滑鼠游標下面**:魔獸把游標當作畫面的一部分,每幀畫進去。畫板和頭頂氣泡都在遊戲畫游標那一步之前繪製:蓋住遊戲介面,被游標蓋住。只有這一幀沒畫游標(隱藏或過場動畫)時,才退回到最後一步繪製。
- **系統縮放**:自己寫測試、用視窗訊息投遞點擊時,未宣告 DPI 感知的行程送過去的座標會被系統放大(150% 縮放時實測 ×1.5)。測試程式要先宣告 DPI 感知。真人點擊不受影響。
- **第一次要預熱字型**,約 1 秒。這段期間按鈕還沒畫出來,點不中。
- **不在對局裡時不回報地面點擊**:主選單和結算畫面上 `mouse.world` 不發出,也不吞掉。
- 按下時被吞掉的那一下,放開前切換到別的程式或滑鼠拖出了視窗,也會復位,不會連下一次放開也吞掉。
- 1.27 沒有建立新遊戲介面框架的函式(1.31 才有):這裡的按鈕、卡片都是執行環境畫的,樣式隨你定,但不會出現在遊戲本身的選單層級裡。
---
# JASS 通道
> 地圖作者能用的 1291 個 JASS 函式,現在可以從遊戲外面依名稱直接呼叫:造單位、改屬性、特效、面板、對話框、聲音、鏡頭、迷霧……遠見指揮台、命令列、HTTP、Python 四種用法。
地圖作者在地圖腳本裡能用的 **1291 個 JASS native**,現在都能從遊戲外面依名稱直接呼叫:造單位、改屬性、畫特效、彈出面板和對話框、播放聲音、移動鏡頭、改迷霧……拿來為遊戲做進一步的自訂 —— RPG 輔助、[AI 玩伴](https://war3ai.com/zh-tw/docs/companion/)、自製小玩法、偵錯工具。
| 用法 | 適合 | 入口 |
|---|---|---|
| **遠見「JASS 控制台」頁** | 手動試、邊看邊改 | 左側欄「系統 → JASS 控制台」:寫好腳本點執行,右邊依分類查函式,點一下就插入腳本 |
| **命令列** | 手動試,或寫成腳本檔案反覆執行 | `python -m openwar3 jass --inst 20`(互動)、`-e "程式碼"`、`my_script.j`、`--list 關鍵字` |
| **HTTP** | 任何語言的外部程式 | `POST /api/instances/{n}/jass` 等(見下文),遠見後端只監聽本機 |
| **Python** | 寫方案、寫玩伴、寫工具 | `g.jass.任意函式(...)`;常用的畫面和互動封裝在 `openwar3.visual` |
> **注意**
>
> 三條邊界,都是機制決定的:
>
> - 只有**單人局**(和本機的電腦打)能修改世界。本機單方面建立物件、修改單位會讓多人局裡其他玩家不同步 —— 多人局只放行唯讀的函式(`Get*`、`Is*`、`Count*`……)。
> - 只給本機自己的工具使用;以玩家身分連線(`Game(player=N)`)或在公平模式下呼叫會被拒絕。
> - 只用於單機、區域網路自建的遊戲。
>
> 要在多人局裡往畫面上加東西,用 [畫板](https://war3ai.com/zh-tw/docs/canvas/):它是執行環境自己畫的,不修改遊戲狀態。
## 腳本寫法
控制台、命令列、HTTP 用的是同一套腳本。一行一句,**可以直接貼上 JASS**(`call` / `set` / `local`、`true` / `false` / `null`、`'Hpal'` 四字碼、`//` 註解),也可以寫成 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')` 找一個單位、`unit_at(x, y)`、`wait(秒)`、`print(...)`。單位能讀 `.x`、`.y`、`.hp`、`.hp_max`、`.mana`、`.type`、`.owner`、`.level`,支援四則運算和比較。
- **不支援** `if`、`loop`、`function` —— 要寫邏輯,就用 Python 的 `g.jass`(它就是普通的函式呼叫),或者寫成 [方案](https://war3ai.com/zh-tw/docs/schemes/)。
- 出錯時會告訴你第幾行、為什麼(沒有這個函式、參數個數不對、變數未定義……);出錯之前的語句已經生效。
參數和回傳值:
| 簽章裡 | 傳什麼 | 說明 |
|---|---|---|
| 整數 | 數字;`'Hpal'` 四字碼自動轉換 | |
| 實數 | 數字 | 執行環境轉成引擎要的格式 |
| 布林 | `true` / `false` | |
| 字串 | `"..."` | 支援中文和遊戲的顏色碼;會被遊戲存起來的那些(浮動文字、面板、按鈕、聊天命令)都是當場複製一份,安全 |
| 控制代碼 | 變數裡的控制代碼,或者單位(`hero()` 這種會自動換成控制代碼) | |
| 函式(code) | 只能 `null` | 從外面給不出 JASS 函式;`TimerStart(t, 60, false, null)` 這種可以 |
| 回傳字串 | —— | 引擎回傳的是字串表編號,讀不回文字。單位名稱用 `g.map_data.name_of` |
## 分類
函式依名稱分了類,控制台右邊和 `--list` 都依這個分:
| 分類 | 個數 | 例子 |
|---|---|---|
| 畫面效果 | 80 | 浮動文字、閃電連線、特效、地面貼圖、地面印記、單位變色 / 縮放 / 播放動作 |
| 介面面板 | 146 | 多行面板、排行榜、倒數計時視窗、對話框、任務、螢幕文字、小地圖閃點、頭像對白、全螢幕濾鏡 |
| 鏡頭 | 44 | 鏡頭欄位、平移、鏡頭震動 |
| 聲音音樂 | 50 | 建立和播放聲音、播放音樂 |
| 迷霧視野 | 25 | 可見區域、開關迷霧 |
| 物品 / 英雄 / 單位 | 63 / 32 / 161 | 造物品、設英雄等級、換擁有者、加技能 |
| 玩家 / 同盟 / 資源 | 71 | 設同盟、改金錢木材 |
| 觸發器 / 事件 / 計時器 | 62 | 建立觸發器、註冊事件、計時器 |
| 地形 / 天氣 / 可破壞物 | 45 | 天氣效果、改地形、造可破壞物 |
| 遊戲流程 | 57 | 遊戲速度、暫停、晝夜時間 |
| 其他 | …… | 單位組與區域、儲存、電腦 AI 腳本、型別轉換與數學、事件回應…… |
2026-09-24 逐一實機呼叫、親眼看過效果的有 **94 個**;其餘走的是同一條路,只是沒有逐一看過效果。
> **說明**
>
> 「事件回應」類函式(`GetTriggerUnit`、`GetClickedButton`……)只在觸發器執行的那一刻有值,從外面呼叫拿到的是 0 或空值。想知道「發生了沒有」,用下文的事件計數。
## HTTP
遠見後端(預設 `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`。實測一次請求 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)` 把實測過的常用畫面效果封裝成一句話一個(每拍呼叫一次 `v.tick()`:到期的刪掉、跟著單位的線和圈移過去;`v.clear()` 全部刪除):
| 方法 | 效果 |
|---|---|
| `float_text(文字, 單位或點, ...)` | 浮動文字:傷害數字、頭頂提示,中文和顏色都行 |
| `link(a, b, kind)` | 兩個單位之間一條線,跟著單位走:牽引 / 靈魂鏈 / 吸血 / 治療波 |
| `effect(模型, 單位或點, ...)` | 特效模型:頭頂、腳下,或者播放一次(爆炸、光柱) |
| `ring(單位或點, 半徑, color)` | 地上的範圍圈:技能範圍、危險區、集合點,可以跟隨單位 |
| `ping(點, color)` | 小地圖閃點 |
| `board(標題, 行...)` | 右上角多行面板(帶圖示),可以逐格修改 |
| `countdown(標題, 秒)` | 右上角倒數計時視窗,遊戲自己走秒 |
| `scene(名字, 話, portrait)` | 頭像對白:底部頭像換成會說話的單位,畫面上出現「名字:話」字幕 |
| `screen_tint(color, alpha)` | 全螢幕濾鏡(預設四周泛紅:殘血警告) |
| `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 事件發生了幾次:單位死亡、進入區域、受傷、計時器…… |
局限是只知道「發生了幾次」,不知道「是誰、打了什麼字」。要分清是誰,就為每個物件各建一個計數器。[AI 玩伴](https://war3ai.com/zh-tw/docs/companion/) 的聊天命令就是這樣接上的。
## 注意事項
- **建立出來的東西要自己刪**:浮動文字、連線、貼圖、面板、觸發器……不刪就一直在(`Visual.clear()` 會刪它自己建立的)。遊戲裡同時最多約 100 個浮動文字。
- **BJ 函式不是 native**:`CreateTextTagUnitBJ` 這類是地圖腳本裡用 native 組出來的,這裡沒有 —— 照著它的實作去呼叫 native。
- **有些常數要先轉換**:例如 `ConvertPlayerColor(1)`、`ConvertFogState(4)`(取值見 common.j)。
- 一次呼叫約 13 ms(含控制代碼換算);協定層是 W3P 操作碼 70 ~ 72,見 [W3P 協定](https://war3ai.com/zh-tw/docs/protocol/)。
---
# 玩法模組
> 方案不只是一個替你打的 AI,也可以是一套規則:你自己在遊戲視窗裡玩,模組負責布置開局、刷怪、給獎勵、在螢幕上提供按鈕和選項卡片、判定勝負。繼承 openwar3.Mod,一個檔案就是一套玩法。
[AI 方案](https://war3ai.com/zh-tw/docs/schemes/) 有兩種:`kind: bot` 是一個 AI,替你打;`kind: mod` 是**一套規則** —— 你自己在遊戲視窗裡玩,模組出題:開局怎麼布置、依時間或事件刷怪、給什麼獎勵、螢幕上提供哪些按鈕和選項卡片、什麼時候算贏。
模組用到的全是現成的能力:[介面與輸入](https://war3ai.com/zh-tw/docs/ui-input/)(能點的按鈕、卡片、熱鍵、點地面)、[畫板](https://war3ai.com/zh-tw/docs/canvas/)(面板、進度條、路線)、[JASS 通道](https://war3ai.com/zh-tw/docs/jass/)(刷單位、改屬性、給物品)、事件流(死亡、升級、放技能、聊天)。
## 兩個範例
在遠見的「AI 方案」→「內建」裡就能選:
| 模組 | 玩法 | 用到的能力 |
|---|---|---|
| **英雄 Roguelike** `builtin/hero-roguelike` | 你只有一個聖騎士,怪物一波波從四面包圍上來;每升一級,螢幕中間三選一強化(選擇時遊戲暫停);撐過 10 波就贏,英雄死了就輸 | `g.ui.choice`(可點擊卡片 + 暫停)、`hero.levelup` / `killed` / `spell.cast` 事件、聊天 `-help`、JASS 改英雄屬性、給物品 |
| **無盡守城** `builtin/endless-defense` | 怪物從對面出生點沿著地上的紅線衝向主城;每守住一波就給金幣;點螢幕按鈕或按 F7 提前叫下一波,獎勵 ×1.5;按 F8 再用左鍵點地面,放一座免費箭塔(右鍵取消) | `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 # 開一局,模組接手,你在遊戲視窗裡玩
```
## 寫一個模組
```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 遊戲秒一波(暫停時不計時)
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=)` | 在一圈上刷單位,一波幾十個也不卡;回傳 JASS 控制代碼 |
| `alive_of(g, 玩家)` / `attack_move_all(g, 玩家, 點)` | 某個玩家還活著的單位 / 全部攻擊移動過去(每隔幾秒呼叫一次,怪物就會追著走) |
| `home(g)` / `hud(g, 標題, 行)` | 我方主城的位置 / 右上角的資訊面板 |
| `neutralize_ai = True` | 開局把電腦對手壓住:它的單位每 5 秒暫停一次,金幣和木材歸零。對戰地圖裡總有一個電腦,模組自訂規則時不要讓它攪局 |
| `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"}
```
模組固定**不走公平模式**(它是出題的裁判,要看全圖、改動世界),也**不依對戰規則判定勝負**(勝負由 `finish` 回報);說明書裡寫了 `fair` / `judge` 也不會生效。匯出 zip、匯入、信任、戰績都和 Bot 方案完全一樣,見 [AI 方案](https://war3ai.com/zh-tw/docs/schemes/)。模組也是程式碼,別人的模組第一次執行前同樣要確認信任。
## 實測
2026-09-25,測試實例上:
- **英雄 Roguelike**:第一波刷出來,右上角面板在更新;把英雄提升到 3 級 → 螢幕中間跳出卡片、遊戲時鐘停住;點兩次卡片 → 兩次強化生效(力量 22 → 27),時鐘恢復。
- **無盡守城**:面板、地上的路線、按鈕都在;F8 + 點地面 → 主城旁邊多了一座防禦塔;這一波還沒清完就點按鈕 → 提示「這一波還沒清完」。
## 邊界
- **只能用於單人局**:刷單位、改屬性走的是 JASS 通道,在多人局裡會不同步。這是鎖步模型決定的,多人玩法要等同步通道(見 [路線圖](https://war3ai.com/zh-tw/roadmap/))。
- 模組看得到全圖 —— 它是出題的,不是玩家。
- 對戰地圖裡的電腦對手只是被「壓住」,並沒有被移除(移除會觸發對戰規則的勝利判定)。
---
# 遠見指揮台
> 本機網頁控制台,也是唯一的入口:設定遊戲目錄、啟動/停止各項服務、啟動/停止遊戲實例、設定下一局、查看 AI 在想什麼、手動下令、導播、對局紀錄。
遠見(Farsight)是在你本機執行的網頁指揮台,**只監聽 127.0.0.1**。它也是整套系統唯一的入口:開局、換 AI、閘道、頭頂氣泡、本機 LLM 都在這裡操作,不必再找別的指令碼。
```bash
start.bat # 部署檢查,然後開啟遠見 http://127.0.0.1:8866
start.bat 5 6 # 順便開始 5、6 號實例的測試(遊戲 + 參考大腦)
start.bat restart # 只重新啟動遠見後台(修改伺服器端程式碼後使用;遊戲和各項服務不受影響)
stop.bat # 徹底停止全部
```
連接埠在 `openwar3.json` 的 `ports.console` 中修改(預設 8866)。
## 控制中心
遠見的首頁。
- **遊戲目錄**:自動尋找或自行選擇,檢查遊戲版本,從你的遊戲中擷取資料。
- **本機服務**:[閘道](https://war3ai.com/zh-tw/docs/gateway/)、[頭頂聊天氣泡](https://war3ai.com/zh-tw/docs/speech/)、本地大模型(LM Studio)、官網本地預覽,每張卡片上都能啟動、停止、重新啟動、看日誌;另外顯示 [MCP](https://war3ai.com/zh-tw/docs/mcp/) 伺服器有沒有被用戶端掛上。
- **環境自檢**:Python、執行環境檔案、遊戲資料、AMAI 資料等各部分是否都已裝好。
- **全部停止**(右上角):遊戲實例、AI、閘道、氣泡、本系統使用的本機模型、遠見後台依序全部停止;和按兩下 `stop.bat` 一樣。MCP 伺服器由 Claude 等用戶端管理,不會被停止;LM Studio 程式本身也不會被關閉。
## 頁面
| 分組 | 頁面 | 做什麼 |
|---|---|---|
| 總控 | 控制中心 | 見上一節 |
| 對局 | 總覽 | 目前實例的對局概況 |
| | 戰場指揮 | 地圖檢視;可以手動下令(手動命令在仲裁表裡優先順序最高:95) |
| | 單位資料 | 每個單位的指令、任務目標(實際在攻擊誰)、魔力、英雄等級經驗、技能冷卻、物品欄 |
| | AI 決策/戰鬥決策 | 參考大腦這一拍在想什麼、每一次戰鬥決策的明細 |
| | 運營顧問 | [LLM 參謀](https://war3ai.com/zh-tw/docs/llm-coach/)的狀態:模型服務是否在線、每個實例有沒有連上、最近一條建議以及它看到的輸入 |
| | 導播台 | 自動運鏡、頭頂血條 |
| | 頭頂氣泡 | 讓單位說話、和本機模型對話、農民茶話會、鏡頭對白、戰況觸發、模型設定。請見 [頭頂氣泡](https://war3ai.com/zh-tw/docs/speech/) |
| | 指令速度 | APM 與命令吞吐量 |
| | 事件與輸入 | 這一局發生了什麼:放技能、聊天和螢幕訊息、點按鈕、熱鍵、點地面、選取、玩家離開,可依類別篩選;旁邊是滑鼠位置、懸停項目和本機選取。請見 [介面與輸入](https://war3ai.com/zh-tw/docs/ui-input/) |
| 紀錄 | 日誌/對局紀錄 | 每個實例的日誌來源;每一局的結果、時長、兵力峰值 |
| | 問題備忘 | 在遊戲中按 Pause/Break 暫停並記下時間點,之後再回來這裡補上描述 |
| 系統 | 實例與開局 | 啟動/停止實例;設定**下一局**的地圖(對戰地圖,也可以選 RPG/自訂地圖)、雙方種族、難度、遊戲速度;每個實例選一個 AI 方案;「開始測試」一鍵啟動遊戲 + AI |
| | AI 方案 | 匯入、匯出、複製、信任、刪除方案,為實例切換方案(正在進行的這一局也能立即換人接手),查看每個方案的戰績。請見 [AI 方案](https://war3ai.com/zh-tw/docs/schemes/) |
| | JASS 控制台 | 撰寫 JASS 腳本後點選執行,右側可依分類查詢 1291 個函式、點一下即可插入腳本;變數在同一局裡會一直保留。請見 [JASS 通道](https://war3ai.com/zh-tw/docs/jass/) |
| | 串接與擴充 | 閘道狀態和一鍵啟動;依開發/選手/觀眾角色產生的連線網址、MCP 掛載命令和設定;JS、Python 範例。請見 [閘道](https://war3ai.com/zh-tw/docs/gateway/)、[MCP](https://war3ai.com/zh-tw/docs/mcp/) |
| | 數據與磁盤 | 錄製、對局紀錄、日誌等執行資料各占用多少磁碟空間、最近一天新增多少,哪些可以刪除(遠見不會自動刪除任何東西) |
| | 設定 | 遊戲目錄、介面語言、外觀(深色/淺色、現代/魔獸風格)等 |
| | 反饋與建議 | 遇到問題、有建議,直接提交給我們;診斷資訊只在你勾選時附上,送出前可以預覽 |
按 Ctrl + K 開啟命令面板:跳頁、切換實例、結束目前這一局、啟動新大腦。
側邊欄底部的「最近更新」列出遠見和底層平台最近新增了什麼。遠見啟動時(之後每 6 小時)會詢問 War3AI.com 有沒有新版本,有的話會提醒你。遠見更新後,頁面頂端會出現一條橫幅,先儲存好手上的輸入,再點「重新整理」。
## 多實例
`runtime/farm.py` 負責多開(遠見啟動/停止實例時會替你呼叫它):把原版 `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 呼叫、畫板……),網頁只是其中一個用戶端,任何語言的程式都能直接呼叫。API 清單寫在 `console/server/app.py` 的檔案開頭;方案、JASS、畫板這三組的用法分別請見 [AI 方案](https://war3ai.com/zh-tw/docs/schemes/)、[JASS 通道](https://war3ai.com/zh-tw/docs/jass/)、[畫板](https://war3ai.com/zh-tw/docs/canvas/)。
---
# AI 方案
> 一個方案就是一套完整的 AI。在遠見裡一鍵切換,正在打的這一局也能立刻換人接管;匯出 zip 分享給別人,匯入別人的方案來測試,每個方案的戰績自動統計。
一個**方案** = 一套完整的 AI:一個資料夾 + 一份說明書 `scheme.json` + 程式碼。每個遊戲實例選一個方案;在遠見裡一鍵切換,**正在打的這一局也能立刻換人接管**。
別人分享的方案匯入進來是**獨立的一塊**,和你自己的方案互不影響;想改就「複製到我的」。
```text
schemes/
mine// 我的方案:自己寫的,或從別的方案複製來改的(隨便改,下一局生效)
installed// 已安裝:別人分享的 zip 解壓縮在這裡(第一次執行前要確認信任)
brains/xwar3/ 內建:參考大腦(完整 AI)
brains/examples/ 內建:四個教學範例 hello / rush / macro / micro,玩伴範例 buddy,兩個玩法模組(英雄 Roguelike、無盡守城)
```
方案不一定是替你打的 AI:`kind: mod` 的方案是一套**玩法規則**,你自己玩,它出題,見 [玩法模組](https://war3ai.com/zh-tw/docs/mods/)。
## 在遠見裡使用
「AI 方案」頁(左側欄「系統 → AI 方案」):
| 操作 | 做什麼 |
|---|---|
| 匯入方案(zip) | 裝進 `installed/`;同一個 id 已經裝過時會詢問是否取代(取代後要重新確認信任) |
| 套用到實例… | 選實例 +「立刻生效」(停掉目前的 AI,新方案接管這一局)或「下次開始測試時生效」 |
| 複製到我的 | 複製一份到 `mine/`,作者記為「我」、版本 0.1.0,並記下複製自哪個方案的哪個版本 |
| 匯出 zip | 打包成 `-<版本>.zip`,發給別人就是分享 |
| 開啟資料夾 | 在檔案總管裡開啟方案目錄,直接改程式碼 |
| 信任 | 別人的方案第一次執行前必須點(見下文「信任和安全」) |
| 最近戰績 | 這個方案每一局的勝負、時長、結束原因 |
| 刪除 | 只能刪「我的」和「已安裝」;有實例正在使用的不能刪 |
實例卡片上也多了一行「AI 方案」:下拉選方案 →「切換(立刻生效)」。實例沒在執行時按鈕叫「選定」,下次「開始測試」就用它啟動 AI。
## 說明書 scheme.json
```json
{
"format": 1,
"id": "fast-rush",
"name": "三分鐘速攻",
"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` 檔案(不允許絕對路徑,不允許 `..`) |
| `kind` | | 預設 `bot`(`openwar3.Bot` 的子類別,替你打);`mod` = [玩法模組](https://war3ai.com/zh-tw/docs/mods/)(`openwar3.Mod` 的子類別,固定不走公平模式、不依對戰規則判定勝負) |
| `class` | | 入口檔案裡的 Bot(或 Mod)子類別名稱;不寫就取入口檔案裡最後一個 `openwar3.Bot` 子類別 |
| `fair` | | 預設 `true`:只看得見視野內的東西,和對戰平台同一套規則。`false` = 全圖可見,也才能用 [JASS 通道](https://war3ai.com/zh-tw/docs/jass/)(玩伴需要) |
| `judge` | | 預設 `true`:依對戰規則判定勝負。RPG / 玩伴方案寫 `false` |
| `hz` | | `on_tick` 每秒呼叫幾次,預設 5 |
| `format` | | 說明書格式版本,目前是 1;比本機 OpenWar3 新的會被拒絕並提示更新 |
| 其餘 | | `name`、`version`、`author`、`description`、`races`、`license`、`homepage`、`forked_from` 只用於顯示 |
方案目錄會加進 Python 的模組搜尋路徑,入口檔案可以 `import` 同目錄下的其他檔案。第三方套件(numpy、torch……)不會自動安裝 —— 請在 `description` 裡寫清楚需要什麼。
**最小的方案,兩個檔案就夠**:
```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/`,遠見重新整理一下就能看見。更省事的起點:在「內建」裡挑一個範例,點「複製到我的」。
## 執行方式和戰績
方案由**方案執行器**執行(遠見的「開始測試 / 切換」啟動的就是它):
```bash
python tools/run_scheme.py --inst 20 --scheme builtin/micro --hours 6
```
- 每個實例一個常駐的監督行程,**每一局啟動一個子行程**執行方案:方案程式碼當掉不會連累監督行程;「我的方案」改了程式碼,下一局自動用新的。
- 每局結束記一行戰績:方案、版本、作者、勝負、原因、遊戲時長、出錯次數。遠見裡的勝率就從這裡統計。
勝負怎麼判:
| 情況 | 記為 |
|---|---|
| 對面建築全沒了 | 勝 |
| 我方建築全沒了(兵還活著也算 —— 對戰就是這樣判負的) | 負 |
| 我方單位全沒了 | 負 |
| 在遠見裡手動結束 / 停止 | 未定 |
| 切換時這局已經打了 60 遊戲秒以上(半路接手) | 另外計數,**不計入勝率** |
| 遊戲時鐘長時間不走 | 未定 |
分出勝負後執行器會關掉結算畫面,依「下一局設定」開下一局,方案接著接管 —— 可以掛一整夜累積戰績。暫停不算結束:暫停期間 Bot 照常執行,只有遊戲時鐘停住。
## 信任和安全
**方案就是程式碼,執行時擁有和你本人一樣的權限**(能讀寫檔案、能連網)。所以:
- `installed/` 裡的方案預設**不被信任**,遠見和執行器都拒絕執行,直到你點「信任」;
- 取代安裝同一個 id 的方案會**重設信任**(新版本等於新程式碼);
- 匯入時會檢查:zip 不超過 50 MB、不超過 2000 個檔案;不允許絕對路徑和 `..`(防止寫到方案目錄外面);說明書不合法或入口檔案不存在就直接拒絕。
> **注意**
>
> 信任之前先「開啟資料夾」把程式碼讀一遍。只從你信得過的人那裡取得方案。
## API(給腳本用)
| API | 說明 |
|---|---|
| `GET /api/schemes` | 方案清單 + 戰績 + 各實例選定的、正在執行的方案 |
| `GET /api/schemes/results?ref=` | 一個方案最近 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 就是分享的單位,網站只需要在外面加一層:在遠見裡一鍵上傳;在網站上下載,走和「匯入方案」完全相同的檢查、同樣要確認信任;可以選擇回報戰績,網站依版本彙總勝率。遠見裡「分享到方案網站」的按鈕已經預留了位置。進度見 [路線圖](https://war3ai.com/zh-tw/roadmap/)。
---
# 頭頂氣泡與本機模型
> 讓遊戲裡任意單位以任意身分在頭頂彈出對話氣泡;接上本機 LLM,一句話輸入,一句回覆就出現在單位頭上。
氣泡屬於觀賞層:不影響勝負,適合直播、賽事解說和偵錯。
- 任意單位、任意身分都能說話;多個單位可以同時說;
- 每個氣泡的字級、顏色、寬度、尾巴、透明度、打字速度都能個別自訂;
- 能直接接上本機 LLM(LM Studio),支援串流輸出:一邊生成一邊更新氣泡。
## 從 Bot 裡使用
最簡單的方式是 SDK 內建的 `say`:
```python
g.say(hero, "跟我衝!", seconds=4)
```
## 啟動與介面
**最省事:遠見首頁「控制中心」**——先點「本地大模型 → 啟動並載入模型」(LM Studio 本機服務 + 把設定好的模型載入顯示記憶體),再點「頭頂聊天氣泡 → 啟動」。卡片上能看日誌、停止、重新啟動。
介面在遠見左側的「頭頂氣泡」頁:讓單位說話(選單位、輸入文字、調整樣式、和模型對話)、農民茶話會、鏡頭對白、戰況觸發、模型設定,都針對頂端列選取的實例操作。
也可以用命令列:
```bash
python speech/speak_launch.py # 啟動本機模型服務 + 載入模型並預熱 + 啟動氣泡 API
python speech/speak_launch.py --restart # 修改程式碼後重新啟動 API
python speech/speak_launch.py --stop # 停止 API,並把模型從顯示記憶體卸載
```
每一步都是「已存在就略過」,重複執行沒有副作用。
## 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": "第一波獸人還有 30 秒抵達。",
"style": { "tail": false, "type_ms": 0 } },
{ "world": [-4684, 2644], "key": 2, "text": "集合點", "style": { "font_px": 16 } }
]
}
```
| 欄位 | 說明 |
|---|---|
| `unit` / `world` / `screen` | 三選一:跟著單位移動(有血條時貼在血條正上方)/地圖座標/螢幕像素(旁白用) |
| `name` | 第一行顯示的說話者,隨意填寫,不必是這個單位 |
| `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 個氣泡;每幀開銷平均約 0.1 ~ 0.2 ms。
### 和本機模型對話 `POST /api/chat`
```json
{
"inst": 16, "unit": "0x14A12614", "name": "山丘之王",
"persona": "你扮演魔獸爭霸裡的山丘之王穆拉丁,豪爽、愛喝酒。一兩句口語,不超過40個字。",
"message": "前面有一群食人魔,我們衝不衝?",
"stream": true
}
```
回傳 `{"reply": "...", "first_token_ms": 283, "total_ms": 342}`,同時這句回覆已經出現在那個單位頭上。同一個單位會記住最近 6 輪對話。
### 其他
| API | 說明 |
|---|---|
| `GET /api/instances` | 執行中的遊戲 |
| `GET /api/units?inst=16&mine=true&heroes=true` | 單位清單(含中文名稱、座標、血量) |
| `POST /api/clear` | 清除一個或全部氣泡 |
| `GET /api/llm`、`POST /api/llm` | 查看/修改模型設定(`base_url`、`model`、`max_tokens`、`temperature`) |
| `POST /api/banter` | 農民茶話會:家裡的工人依人設輪流吐槽,開局報幕(戰況全是真實資料) |
| `POST /api/camtalk` | 鏡頭對白:鏡頭裡的英雄和隨從依身分對話 |
| `POST /api/events` | 戰況觸發:開戰、打完、英雄陣亡、升級主堡、被拆……有事發生才說話 |
## 本機模型怎麼選
在一張 RTX 5090 上實測(5 句遊戲台詞):
| 模型 | 顯示記憶體 | 速度 | 一句回覆 | 結論 |
|---|---|---|---|---|
| **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 秒 | 顯示記憶體吃緊時使用,中文略顯平淡 |
| Qwen3.6-27B(稠密),Q4 | 17.2 GB | 約 39 token/s | 5.5 秒後仍在思考 | 不適合即時對話 |
- **速度看「每次啟用的參數量」,不看總參數量**:35B 的 MoE 只啟用 3B,比 27B 稠密模型快 3 ~ 4 倍。
- **一定要關閉「思考」**:不關的話 token 全花在思考上,一個字都沒回。
- 氣泡逐字打出的速度約每秒 22 個字,生成速度已經不是瓶頸,真正影響體驗的是**首字延遲**。
> **讓台詞「像真的」**
>
> 給模型的戰況一律使用真實資料(場次、勝負、兵力、庫存),並明確要求「只能使用這些事實」。實測發現,不加這條限制時,模型會編造出沒發生過的戰鬥。
---
# 閘道
> WebSocket / JSON 閘道:Python SDK 能呼叫的公開 API,JS、C#、Go、Rust、瀏覽器頁面、另一台電腦上的程式都能呼叫。三種角色,附 JS 用戶端和瀏覽器示範頁;延遲是快車道再加約 1 ms。
閘道把快車道和推送狀態包裝成 **WebSocket / JSON**。[API 目錄](https://war3ai.com/zh-tw/api/) 裡 Python SDK 能呼叫的公開 API,JS、C#、Go、Rust、瀏覽器頁面、另一台電腦上的程式、LLM 都能呼叫,方法名稱和參數都一樣。延遲是快車道再加約 1 ms。
**最省事:遠見首頁「控制中心」→ 網關 → 啟動**(停止、重新啟動、看日誌、開啟示範頁也在那張卡片上)。命令列:
```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 # 給區域網路使用:自動要求權杖(bin/gateway/token.txt)
python gateway/server.py --allow-origin http://localhost:5173 # 讓你自己的網頁也能連
```
## 連線和角色
連線位址:`ws://127.0.0.1:8870/ws?inst=9&role=dev`(也可以用 `pid=` 代替 `inst=`;需要權杖時加上 `&token=`)。
| 角色 | 能呼叫 | 適合 |
|---|---|---|
| `dev` | 全部:觀察、命令、遊戲控制、沙盒(JASS 改動世界)、畫介面 | 本機工具、[玩法模組](https://war3ai.com/zh-tw/docs/mods/)、玩伴 |
| `player`(加上 `&player=N`) | 觀察、指揮 N 號玩家的單位、畫介面;**預設為公平模式**,只看得見 N 號玩家視野內的東西(`&fair=0` 關閉) | 替某個玩家上場的 Bot 或 LLM |
| `observer` | 唯讀(執行環境直接拒絕它下的命令) | 觀戰、解說、資料蒐集 |
`player` 拿不到:結束遊戲、調整速度、暫停這類遊戲控制,看得見別人底牌的 `players`、`enemy_ai_plan`,會讓遊戲行程開啟本機檔案的 `canvas.image`,還有 JASS。`resources`、`tech`、`stats` 這類帶玩家編號的查詢只能查自己。
一個連線就是一個工作階段,占用一條快車道(執行環境總共 16 條)。閘道同時最多 12 個工作階段,給 Bot、模組和遠見留幾條。中斷連線時只收掉這個工作階段自己畫的東西和熱鍵,別的程式畫的不動。
## 訊息
連上之後會先收到 `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`。見 [介面與輸入](https://war3ai.com/zh-tw/docs/ui-input/)。
- 一個呼叫出錯只會回報這一個(`ok: false` 加上 `error`),連線不會中斷;送來的不是 JSON 也一樣。
- 事件 JSON 的欄位和 [W3P 協定](https://war3ai.com/zh-tw/docs/protocol/) 一致,另外帶有便捷欄位(`spell`、`key`、`text`、`chat`、`button`、`player`、`mods`)。
也可以用 HTTP,適合一次性的呼叫和 curl:`GET /api?role=player` 列出方法目錄,`POST /call` 帶上 `inst`、`role`、`method`、`args`、`kwargs` 呼叫一次。`/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`:局面、我方單位表、在遊戲裡放一個按鈕、事件流,一頁看完。
**其他語言**:任何 WebSocket 函式庫 + 上面的 JSON 就夠了,不必碰共用記憶體。
**LLM**:直接用 [MCP 伺服器](https://war3ai.com/zh-tw/docs/mcp/),它把常用的事情做成了現成的工具。
## 實測
2026-09-25,連上一局真實對局逐項核對 16/16(閘道 9 項 + MCP 7 項):交握(dev 角色 121 個方法)、`units('me')`、一頁局面、螢幕提示、放按鈕;訂閱之後在遊戲裡點那個按鈕 → `ui.click` 推送到用戶端;JASS;傳入一個無效單位只回報這一個錯誤;HTTP `/call`(observer 角色)。
JS 用戶端(Node)和瀏覽器示範頁也跑過:網頁上放的按鈕在遊戲裡被點擊,網頁的事件日誌收到 `ui.click`。
## 安全
- 預設只監聽本機 `127.0.0.1`,不需要權杖(和遠見一樣)。`--host` 不是本機位址時自動要求權杖;`--auth` 讓本機也需要權杖。
- **瀏覽器裡別的網站連不上**:瀏覽器發起的連線都帶有來源(`Origin`),閘道只認自己的示範頁和 `--allow-origin` 指定的網址;Python、Node、curl 這類程式不帶來源,照常連線。只監聽本機時還會核對 `Host`,擋住把外部網域解析到本機的攻擊。
- 角色是連線時自行宣告的:在本機模式下它是約定,不是安全邊界。對戰平台要由裁判行程決定誰拿到什麼角色,見 [對戰平台](https://war3ai.com/zh-tw/arena/)。
---
# W3P 協定
> 執行環境和外部程式之間的全部契約:八塊共享記憶體、讀取世界狀態、讀取事件、下命令、回執、車道角色、畫板、介面與輸入。要用 Python 以外的語言接入,請看這一頁。
執行環境和外部程式之間**只透過共享記憶體**交換資料,下面這些就是全部。
- 參考實作是 Python 的 `sdk/python/w3world.py`(讀)和 `sdk/python/w3fast.py`(寫),每個結構的大小和位移都寫在裡面,有測試釘住;
- **協定只描述語意,和遊戲版本無關。** 換遊戲版本時由執行環境自行適配,協定不變;新欄位只追加在區塊尾端,舊用戶端照常能用。
> **說明**
>
> 大多數人不需要讀這一頁 —— 用 Python SDK 就好。只有當你想用 C++ / C# / Rust / Go 等語言直接接入,或想知道 SDK 底下發生了什麼時,才需要它。
## 1. 八塊共享記憶體
`` 是遊戲的行程 ID。
| 名稱 | 方向 | 內容 | 同步方式 |
|---|---|---|---|
| `Local\War3World_` | 執行環境 → 你 | 世界狀態:標頭 + 16 個玩家 + 最多 1024 個單位 + 256 份單位細節 + 256 個地上物品 + 擴充區 + 生產表 | seqlock |
| `Local\War3Trees_` | 執行環境 → 你 | 最多 4096 個可破壞物(樹等),每 2 秒更新 | seqlock |
| `Local\War3Events_` | 執行環境 → 你 | 事件環,8192 筆 | 每筆自帶序號 |
| `Local\War3Map_` | 執行環境 → 你 | 地圖:地形格(128 一格,最多 256×256)+ 可玩區邊界 + 出生點;開局後幾秒內分批算完 | seqlock(算好後不再變動) |
| `Local\War3Fast_` | 雙向 | 命令車道:16 條 × 16 槽;每槽一條命令 + 回執;每條車道帶有角色 | 每槽單一寫入者、單一讀取者 |
| `Local\War3Canvas_` | 你 → 執行環境 | [畫板](https://war3ai.com/zh-tw/docs/canvas/):標頭 64 位元組 + 256 個元素 × 112 位元組 + 64 KB 文字 / 點池;送出一次 `canvas_enable` 後才建立 | seqlock(你寫入,執行環境每幀讀取) |
| `Local\War3Msgs_` | 執行環境 → 你 | 螢幕訊息環:遊戲提示、聊天、系統訊息的全文,128 筆 × 256 位元組 | 每筆自帶序號 |
| `Local\War3Input_` | 雙向 | [介面與輸入](https://war3ai.com/zh-tw/docs/ui-input/):執行環境回寫滑鼠位置、指著的地面點、懸停項目;你寫入熱鍵表和滑鼠開關;送出一次 `input_enable` 後,執行環境才開始接管輸入 | 熱鍵表 seqlock |
**好幾個用戶端同時使用畫板和輸入**:這兩塊都只有一份,各寫各的會互相覆蓋。約定如下,自己寫用戶端也要照做:
- **畫板**:持有具名互斥鎖 `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、本方玩家編號、是否在局內、倍速、發布週期、這一份在遊戲執行緒上採集花費的微秒數、事件序號、分段耗時。用戶端可以寫入 `requestedPeriodMs` 請求發布週期(16 ~ 1000 ms)。
- **單位**(112 位元組):控制代碼對(**用控制代碼對辨識單位**,位址會被重複使用)、類型四字碼、擁有者、旗標、座標、生命 / 魔力(含上限)、目前訂單 + 訂單目標、任務目標(實際在打誰)、英雄等級 / 經驗 / 技能點、細節索引、`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: 漏了一筆(讀太慢被覆寫)
elif e.seq != seq: 還沒寫完,下次再讀
else: 處理 e
```
事件結構 64 位元組:`seq kind clockMs addr handleLo handleHi typeId owner a b x y value extra`。
- 比對相鄰兩次發布得出(精度 = 發布週期):`unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started`;
- 引擎層級(執行環境在遊戲執行緒上當場記下,**每一下**都有):`damage`(來源、傷害類型、攻擊類型、位置、實際扣血、護甲前傷害)、`killed`(兇手);
- 追蹤生產表得出:`production.done`(完成的四字碼、類別、花了多少遊戲秒;對手的也會發);
- 執行環境每次發布時順帶檢查:`spell.cast`(技能開始冷卻:`a` 技能四字碼、`b` 等級、`value` 冷卻秒數、`x/y` 施法點)、`player.left`(`a` 玩家編號、`b` 新的槽位狀態)、`selection.changed`(本機玩家的選取,完整清單在世界區塊擴充區)、`game.ended`(離開對局);
- 螢幕訊息:`message`(`a` = 訊息序號,全文到共享記憶體 `Local\War3Msgs_` 裡查:128 筆 × 256 位元組,遊戲提示、聊天、系統訊息都在;`b` = 訊息框編號);
- 介面與輸入(開啟 `input_enable` 之後):`ui.click`(`a` 畫板項目 id、`b` 1 左鍵 / 2 右鍵)、`ui.hover`、`hotkey`(`a` 熱鍵 id、`b` 虛擬鍵碼)、`mouse.world`(`x/y` 地面座標、`value` = 1 表示被吞掉了),修飾鍵都在 `extra`。
## 4. 下命令
1. **一個用戶端物件占用一條車道**:持有 `Local\War3FastMutex_` 找一條閒置(或擁有者行程已結束)的車道,寫入角色、玩家編號、自己的 pid。同一行程需要兩種角色就開兩條;
2. 填槽:語意命令旗標、操作碼、`args[11]`、截止時間 `deadlineMs`;
3. 所有槽寫完後標記提交,把車道的 `submitSeq` 加 1;
4. 等待 `Local\War3FastDone__` 事件(或輪詢),讀取回執,歸還槽。
執行環境在遊戲執行緒的事件分派中批次執行:一次清空的時間預算為 **4 ms**(真實的高精度計時),超過就把剩下的留到下一次分派。**過了截止時間的槽不會再執行** —— 不會出現「暫停恢復後舊命令又執行一遍」的情況。
`args` 索引:`0..2` 單位(位址、handle lo、handle hi)、`3` 訂單編號或四字碼、`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 | 鏡頭 | 讀取鏡頭狀態、設定欄位、看向一點、跟隨、重設、旋轉、邊界、平滑、介面顯示 / 隱藏、乾淨畫面、迷霧 |
| 60 ~ 63 | HUD | 任務按鈕文字、任務面板標題與描述、重新整理、讀取面板是否已開啟 |
| 70 | `jass` | 依名稱呼叫 JASS native(1291 個):名稱和字串參數放在槽的附加區,其餘參數依簽章放進 `args`;回傳值在 `value[0]`。只開放給本機工具車道;帶函式參數或會暫停腳本執行緒的一律拒絕。見 [JASS 通道](https://war3ai.com/zh-tw/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 位元組,你寫入熱鍵表和滑鼠開關,執行環境回寫滑鼠位置、指著的地面點、懸停項目。任何車道都能送出(只影響本機輸入)。見 [介面與輸入](https://war3ai.com/zh-tw/docs/ui-input/) |
## 5. 回執
回執 52 位元組(+8 位元組耗時):`status`、`engineReturn`、`verdict`(被拒原因碼)、`orderBefore / orderAfter`(同一幀讀回的單位訂單)、`value[8]`(查詢結果)、`execUs`(這一條在遊戲執行緒上執行了多少微秒)、`engineUs`(其中引擎下令函式本身)。
全部狀態碼和原因碼見 [回執與原因碼](https://war3ai.com/zh-tw/docs/reason-codes/)。
## 6. 車道角色
| 角色 | 能做什麼 |
|---|---|
| `dev` | 本機工具:語意命令(指揮本機玩家的單位)+ JASS 通道 |
| `player` | 只能下語意命令,只能指揮車道所屬玩家的單位(別人的 = `not_owner`) |
| `observer` | 只能查詢、操作鏡頭、讀取 HUD 面板狀態、開啟畫板和本機輸入;其他一律 `forbidden` |
兩個 AI 對打 = 同一局裡開兩條 `player` 車道(player 0 / player 1)。
> **注意**
>
> 本機模式下,角色由用戶端自行宣告(這是約定,不是安全邊界)。[對戰平台](https://war3ai.com/zh-tw/arena/) 由裁判行程建立車道,只把 `player` 車道交給選手。
## 7. 實測過的語意
- 右鍵(smart)對著敵人 = 攻擊**這一個**(訂單目標、任務目標都是它);原始攻擊令走目標命令只會換上攻擊令、不記錄目標,會去打附近別的單位;
- 引擎不允許對看不見的單位下目標命令:天黑後遠處營地進入迷霧,右鍵一律被拒(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` | 參數錯誤 | 座標、格號、四字碼寫錯 |
| 6 | `unsupported` | 不支援 | 這個版本的執行環境沒有這項能力 |
| 7 | `bad_target` | 目標無效 | 目標已經不在了;目標類型不對 |
| 8 | `forbidden` | 車道角色不允許 | 以 `observer` 身分下令 |
| 97 | `cancelled` | 批次區塊內拋出例外,整批都沒送出 | `with g.batch():` 區塊裡的程式碼出錯 |
| 98 | `held` | 單位被更高優先順序的層佔用,沒有送出 | 參考大腦的毫秒層、指揮台的手動下令正佔用這個單位 |
| 99 | `timeout` | 逾時 | 遊戲暫停或卡頓時超過了截止時間(過期的命令不會再執行) |
## 原因碼 `verdict`
被拒時,執行環境會用引擎自己的可行性檢查說明原因。也可以先不下令、先詢問:`g.can_do(單位, 四字碼)` 會回傳同樣的碼。
| 碼 | 含義 | 怎麼辦 |
|---|---|---|
| 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 有效) |
---
# 資料從哪裡來
> 每一類資料的來源與精度。遇到「看起來不對」的怪事時,先查這一頁。
| 資料 | 來源 | 精度 |
|---|---|---|
| 單位、資源、指令、技能、buff、物品欄 | 執行環境每 50 ms 推送的世界區塊 | 發布週期(可調到 16 ms) |
| 傷害、擊殺事件 | 執行環境在遊戲執行緒上當場記下,每一下都有 | 即時 |
| 其他事件(出現、死亡、更換指令、升級……) | 比對相鄰兩次發布 | 發布週期 |
| 生產表(訓練/研究/建造/升級) | 引擎生產技能的計時欄位 + 執行環境累加的已進行時間 | 約 ±0.2 遊戲秒 |
| 戰鬥屬性、克制表 | 遊戲內建的資料表(從你本機的遊戲擷取) | 不含物品、光環、buff 的加成修正 |
| 尋路 | 引擎的地形可通行性(128 一格)+ 樹木 + 建築佔地,在 SDK 端跑 A* | 一格;窄於一格的縫隙判定為不通 |
| 遊戲內時間 | JASS `GetFloatGameState(GAME_STATE_TIME_OF_DAY)` | 發布週期 |
| 可見性 | 執行環境為每個單位依玩家計算的可見性遮罩 | 發布週期 |
| 科技計數、可行性、金礦剩餘量 | 快車道查詢,直接詢問引擎 | 即時 |
## 遊戲資料不隨程式碼散布
單位表、技能、物品、英雄、buff、傷害克制表等都來自暴雪的遊戲檔案,**不納入儲存庫**。在遠見「控制中心」設定好遊戲目錄後,會自動從你自己的遊戲中擷取,也可以手動執行:
```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`。
## 幾個具體數字
| 項目 | 值 |
|---|---|
| 一天 | 480 遊戲秒(白天、黑夜各 240 秒),一小時 = 20 遊戲秒;開局是早上 8 點 |
| 白天 | 6:00 ~ 18:00 |
| 護甲係數 | 0.06(來自遊戲資料表) |
| 世界區塊容量 | 16 個玩家、1024 個單位、256 份單位細節、256 個地上物品、128 筆生產 |
| 樹木 | 最多 4096 個可破壞物,每 2 秒刷新 |
| 事件環 | 8192 筆;讀取太慢會遺失(SDK 能偵測到) |
| 地圖格子 | 128 遊戲單位一格,最多 256 × 256 |
## 實測校準過的例子
- 生產耗時:農民 14.9、農場 34.9、鐵製刀劍 59.9 遊戲秒,與執行環境推送的數值一致(誤差小於 0.2 秒);
- 戰鬥屬性對照遊戲面板核對:聖騎士 650 血量、255 魔力、3.9 護甲、攻擊力 24 ~ 34;攻擊升一級的步兵 13 ~ 15;
- 引擎傷害事件中的護甲減免前傷害(14 / 15 / 15)落在 `stats()` 算出的區間內;
- 尋路:Echo Isles 116 × 88 格,到對面大廳的地面路程 10642(直線 9856),建立網格 18 ms、一次 A* 約 1 ms。
---
# 常見問題
> 這是外掛嗎?支援哪些版本?AI 能看到什麼、能做什麼?不會寫程式能用嗎?……
## 這是外掛嗎?
不是。它是給 AI 研究與娛樂使用的開發介面,只用於**你自己合法擁有的用戶端**,在本機、區域網路或自建遊戲中和電腦或其他 AI 對戰。它**不得用於 Battle.net 或任何有反作弊機制的伺服器**,也不提供任何針對真人對局的功能。詳見[使用邊界](https://war3ai.com/zh-tw/docs/legal/)。
## 支援哪些遊戲版本?
目前只支援**《魔獸爭霸 III》1.27**(寒冰霸權)。1.24 ~ 1.28 是同一套引擎結構,多版本相容(依版本選擇符號表、以特徵碼備援、啟動時自我檢查並產出能力清單)列在[路線圖](https://war3ai.com/zh-tw/roadmap/)的 P4 階段。1.29 之後的版本及重製版是另一套引擎,需要另外適配,目前不做承諾。
## 會修改我的遊戲檔案嗎?
不會。執行環境是在遊戲執行時注入,**不修改磁碟上的 Game.dll** 或任何遊戲檔案。多開時只是把原版的 `War3.exe` 啟動殼原封不動地複製並改名。遊戲資料(單位表等)是從你自己的遊戲中擷取,不隨程式碼散布。
## AI 能看到什麼?
基本上就是一位職業選手想知道的一切,每 50 ms 更新一次:
- 所有玩家的黃金、木材、人口;所有單位的位置、血量魔力、目前指令、**正在攻擊誰**、等級經驗;
- 英雄和單位的技能等級與剩餘冷卻、身上的 buff、物品欄;
- 每座建築正在訓練/研究/建造/升級什麼、進度多少、有沒有因為人口而卡住;
- 地上的物品、樹木、地圖的可通行/可建造網格、出生點、遊戲內時間(晝夜);
- 事件流:單位出現與死亡、**每一下傷害**(誰打的、攻擊類型、護甲減免前的傷害)、擊殺、生產完成、英雄升級……
- 還能直接詢問引擎:某件事現在能不能做、為什麼不能;某項科技幾級;某一點看不看得見;金礦還剩多少;電腦對手打算帶兵攻打哪裡。
在此之上,SDK 還幫你算好了戰鬥屬性(克制、護甲、攻防升級)、「擊殺它要幾秒」以及地面尋路。所有 API 請見 [API 目錄](https://war3ai.com/zh-tw/api/)。
## AI 能做什麼?
玩家能做的操作基本上都有:移動、攻擊移動、攻擊指定目標、停止、原地待命、巡邏、攻擊地面、採集、修理、建造(可自動找位置)、訓練/研究/升級、取消、學技能、施法(對單位/對地點/無目標)、集結點、復活英雄、撿起/使用/丟棄/給予/販賣物品、購買物品、戰鬥號召(Call to Arms);Shift 排程、依路徑點行軍、一個工人連續建造多座;還有遊戲速度、暫停、頭頂氣泡。每條命令都有回執。
除了玩家的操作,還能在遊戲畫面上畫出自己的面板和標註([畫板](https://war3ai.com/zh-tw/docs/canvas/)),以及在單人局裡呼叫地圖作者能用的 1291 個 JASS 函式([JASS 通道](https://war3ai.com/zh-tw/docs/jass/))。
## 能用在 RPG/自訂地圖裡嗎?
可以。選一張 RPG 地圖、為實例選擇「玩伴範例」方案,開局後你自己玩,身邊就會跟著一個會助戰、幫你補血、陪你說話的 AI 夥伴,請見 [RPG 玩伴](https://war3ai.com/zh-tw/docs/companion/)。`g.map_data` 能讀出地圖自訂單位的名稱;[JASS 通道](https://war3ai.com/zh-tw/docs/jass/)能建立單位、設定盟友、彈出面板……怎麼玩由你決定。改動世界的操作只在單人局可用(多人局會不同步),畫板在多人局裡也安全。
## 不會寫程式能用嗎?
可以。依照[快速開始](https://war3ai.com/zh-tw/docs/quickstart/)裝好環境,再看[用 LLM 寫一個 Bot](https://war3ai.com/zh-tw/docs/ai-bot/):你用白話描述打法,LLM 負責寫程式碼;執行起來有問題,就把錯誤訊息或你在遊戲裡看到的現象告訴它,讓它修改。
## 只能用 Python 嗎?
SDK 是 Python。執行環境和外部程式之間只有一份共用記憶體協定([W3P](https://war3ai.com/zh-tw/docs/protocol/)),任何能讀寫 Windows 共用記憶體的語言都能接入。更省事的是 [閘道](https://war3ai.com/zh-tw/docs/gateway/)(WebSocket / JSON):JS、C#、Go、Rust、瀏覽器頁面、另一台電腦上的程式都能呼叫同樣的 API;LLM Agent 可以直接掛上 [MCP](https://war3ai.com/zh-tw/docs/mcp/)。
## 用哪個 LLM 最好?
能寫程式碼的主流模型都可以。關鍵不在模型,而在於**給它正確的材料**(手冊 + `api.json` + 一個範例),並要求它只使用 API 目錄裡存在的方法。局內即時決策(參謀、配音)對延遲很敏感,本機 MoE 模型表現很好,請見 [LLM 當參謀](https://war3ai.com/zh-tw/docs/llm-coach/)和[頭頂氣泡與本機模型](https://war3ai.com/zh-tw/docs/speech/)。
## 會拖慢遊戲嗎?
世界狀態每次擷取在遊戲執行緒上的中位數為 0.5 ~ 0.9 ms(100 ~ 120 個單位),每 50 ms 一次。命令在遊戲執行緒上每條只要幾微秒,每次清空有 4 ms 的時間預算,做不完的留到下一次,不會拖住遊戲。所有對遊戲的呼叫都有例外保護,Bot 當掉只會讓那一方停下,不會連帶讓遊戲當掉。
## 能同時開多個遊戲嗎?
可以。`runtime/farm.py` 負責多實例編排,每個實例一個編號;在[遠見指揮台](https://war3ai.com/zh-tw/docs/console/)中啟動或停止。你的 Bot 用 `--inst N` 連線到指定實例。
## 能讓兩個 AI 對戰嗎?
在同一局裡開兩條 `player` 通道(`--player 0`/`--player 1`)就是 AI 對 AI。本機模式下的公平性靠約定;有裁判、視野過濾、歸屬校驗的正式對戰在[對戰平台](https://war3ai.com/zh-tw/arena/)(P6 階段)。
## 支援 Mac/Linux 嗎?
目前只支援 Windows 10/11。
## 採用什麼授權條款?
授權條款會隨正式版一起公布。第三方元件保留各自的授權(例如 MinHook 為 BSD-2);AMAI 採自訂授權,其衍生資料不隨專案散布,安裝時從 AMAI 的公開儲存庫拉取並產生。
## 遇到問題去哪裡回報?
正式版發布後會開放問題回報管道。回報時請附上實例編號、`python -m openwar3 status` 的輸出和重現步驟。先看看[偵錯與效能](https://war3ai.com/zh-tw/docs/debugging/)能不能解決。
---
# 使用邊界
> 能做什麼、不能做什麼,本網站的流量統計,以及商標與第三方授權說明。使用本專案即表示你同意遵守這些邊界。
## 可以
- 在**你自己合法擁有的**《魔獸爭霸 III》1.27 用戶端上使用;
- 在本機、離線、區域網路或自建遊戲中,讓 AI 與電腦對手或其他 AI 對戰;
- 研究、教學、娛樂,以及直播自己的 AI 對局;
- 以 SDK、參考大腦、範例和工具為基礎進行二次開發,並遵守其授權條款。
## 不可以
- **不得用於 Battle.net,或任何有反作弊機制的伺服器與平台**,也不得在反作弊程式的工作階段進行中同時使用;
- 不得用於在真人對局中取得不正當優勢;
- 不得散布暴雪的遊戲檔案或從中擷取的資料(本專案也不散布:遊戲資料由使用者從自己的遊戲中擷取);
- 遵守執行環境的使用授權。
## 技術上我們承諾
- 不修改磁碟上的 `Game.dll` 或任何遊戲檔案;所有改動都發生在執行期間;
- 多開只是把原版 `War3.exe` 啟動殼原封不動地複製並改名;
- 專案中不包含任何暴雪的程式碼或遊戲檔案。
## 你的責任
各地區對逆向工程與遊戲修改的法律規定不同。**使用者須自行確認在所在地區使用本專案是否合法,並自行承擔使用後果。** 本專案依「現狀」提供,不附帶任何明示或默示的擔保。
## 本網站的流量統計
本網站(war3ai.com)使用 Microsoft Clarity 統計造訪情況:看了哪些頁面、從哪裡來、停留多久、點擊和捲動到哪裡,以及匿名的瀏覽重播和熱度圖。我們只用它來改進文件和頁面。
- 不需要註冊,也不蒐集姓名、電子郵件這類身分資訊;輸入框裡的文字預設會被遮蔽,不會被記錄;
- Clarity 會在瀏覽器裡儲存 Cookie,用來區分同一位訪客的多次造訪;資料由微軟處理,請見 [微軟隱私權聲明](https://privacy.microsoft.com/privacystatement);
- 不想被統計:在本站任一網址後面加上 `?stats=off` 開啟一次,這個瀏覽器之後就不再統計(`?stats=on` 恢復);用瀏覽器的追蹤封鎖功能封鎖 `clarity.ms` 也可以,網站照常使用。
本機的遠見、SDK 和執行環境不含這類統計。遠見只在兩種情況下連線到 war3ai.com:啟動時和之後每 6 小時讀取一次版本清單,看看有沒有新版本;你在「反饋與建議」頁點提交時,傳送你寫的回饋和一個本機識別碼(由系統編號加鹽雜湊而得,無法反推出原值,用來防止洗版);診斷資訊只在你勾選時附上,送出前可以預覽。
這兩種請求到達 war3ai.com 時,伺服器會記下 IP 位址、Cloudflare 判斷的國家或地區和用戶端版本(User-Agent),用來防濫用、統計有多少台遠見在用。版本檢查的記錄 90 天後自動刪除;回饋連同這些資訊一直保留到維護者處理完刪除為止。這些資料只有專案維護者能在後台看到,不會提供給別人。
## 商標
Warcraft®、魔獸爭霸® 是暴雪娛樂(Blizzard Entertainment, Inc.)的商標或註冊商標。War3AI / OpenWar3 是獨立的社群專案,與暴雪娛樂沒有關聯,也未獲其認可或贊助。文中提及的其他產品名稱(Claude、GPT、Gemini、Qwen 等)歸其各自的所有者所有,僅用於說明相容性。
## 第三方元件與資料
| 元件/資料 | 授權 | 處理方式 |
|---|---|---|
| MinHook | BSD-2-Clause | 隨執行環境使用,保留其授權聲明 |
| AMAI | 自訂授權 | 不隨專案散布;`start.bat` 部署時從 AMAI 的公開儲存庫拉取後,由工具產生參考大腦所需的資料 |
| 遊戲資料(單位、技能、物品等) | 暴雪 | 不隨專案散布;由使用者從自己的遊戲中擷取 |
| 從公開比賽重播中擷取的事實資料(建築位置、開局順序) | — | 僅限事實性資料,供參考大腦使用 |
---
# API 目錄(api.json)
狀態:verified = 底層路徑已實機驗證;experimental = 新 API,已跑通、還在逐項實機驗證;inferred = 推斷/未完整實測。延遲:推送快照(讀取共用記憶體,不等遊戲執行緒(約 0.05 ms)); 快車道(約 1 幀:在遊戲執行緒上批次執行); 控制通道(20~40 ms(介面類操作的舊路徑)); 直接寫入(不經遊戲執行緒排隊:寫入共用記憶體(畫板),或對遊戲視窗傳送訊息); 本機計算(純計算或讀取檔案,不碰遊戲)
## 觀察
讀取狀態,不改變遊戲。絕大多數直接讀取推送快照,零等待。
- `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:四字碼集合。 (底層: 世界區塊 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] [推送快照] 我方工人(農民/苦工/侍僧/小精靈)。 (底層: 推送快照)
- `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] [推送快照] 這個工人是不是正在蓋房子(或正走去蓋 / 正在幫忙修;含這一拍剛派的)。挑建造工人時要跳過它,否則上一座地基會停工。 (底層: 推送快照(訂單 = 建築四字碼,或施工令/修理令))
- `under_construction(building) -> 'bool'` [verified] [推送快照] 這座建築還沒蓋完(血沒滿)。⚠ 被打殘的建築也不是滿血 —— 開局判斷夠用,開打之後要結合時間來看。 (底層: 推送快照(地基的血量從很低一路漲到滿))
- `gold_mines() -> 'list'` [verified] [推送快照] 地圖上的金礦。⚠ 夜精靈的纏繞金礦和中立金礦在同一座標各有一個單位,採集要派到自己的那一個。 (底層: 推送快照(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' 神聖護盾、'Bslo' 減速)。代碼對應什麼效果見 data/game/buffs.json。 (底層: 世界區塊細節:B 開頭的技能物件)
- `cooldown(u, ability: 'str') -> 'float | None'` [verified] [推送快照] 這個技能還要冷卻幾秒(遊戲秒);0 = 可以施放;沒有這個技能(或這個單位沒有細節)時回傳 None。 (底層: 世界區塊細節:技能剩餘冷卻(技能計時器))
- `inventory(hero) -> 'list | None'` [verified] [推送快照] 6 格物品的四字碼(空格為 None);沒有物品欄時回傳 None。 (底層: 世界區塊細節:物品欄 6 格)
- `current_order(u) -> 'dict | None'` [verified] [推送快照] {'order','target','x','y'}:單位手上的這條訂單(order 是 0x000D00xx 或建築四字碼,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 = 有排隊但沒開始(多半是人口不夠 —— 該蓋農場了);progress 0..1。
對手的建築也能看(公平模式下只有看得見的建築才有)。 (底層: 世界區塊生產表(Aque/ABnP/AUnP 技能物件 + 執行環境追蹤的已進行時間;實測誤差 < 0.2 遊戲秒))
- `queue(building) -> 'list'` [verified] [推送快照] 訓練/研究佇列裡的四字碼([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。島嶼地圖上判斷「這個野點/分礦地面過不過得去」用它,
比直線距離可靠(繞樹林、繞懸崖、繞建築)。精度為一格 128;窄於一格的縫隙判定為不通。 (底層: 地圖區塊(引擎 IsTerrainPathable)+ 樹區塊 + 建築占地,SDK 端 A*(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)}。
職業常識:升到第三級主城、做攻防升級時停在 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 = 總血量。搭配 time_to_kill / path_distance 挑點。 (底層: 推送快照(野怪依 600 的距離連成一群)+ units.json 等級)
- `buff_info(code: 'str') -> 'dict | None'` [verified] [本機計算] buff 代碼是什麼:{'ability','effect','dur','hero_dur','targets'}(例 'Bslo' -> 減速)。一個代碼對應多行時給第一行。 (底層: data/game/buffs.json(AbilityData.slk 的 BuffID -> 技能/效果/時長))
- `stats(u, player: 'int | None' = None)` [verified] [推送快照] 單位的戰鬥屬性 combat.UnitStats:生命/魔力上限、護甲(含攻防升級、英雄敏捷)、護甲類型、移動速度、白天/夜裡視野、
武器(能打什麼、射程、攻擊間隔、傷害區間、攻擊類型、濺射)。u 給單位(自動用它擁有者的科技、英雄等級)或四字碼(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 目前的血量;算入克制、護甲、攻防升級;不算走位、濺射、治療)。
職業用法:集火先打「最快能打死」的那個(time_to_kill 最小),而不是最近的那個。打不到 = None。 (底層: stats() + 即時血量)
- `time_of_day() -> 'float | None'` [verified] [推送快照] 遊戲內時間(小時,0~24)。開局是早上 8 點;一整天 = 480 遊戲秒(白天、黑夜各 240 秒,依晝夜流速縮放)。
讀不到(舊版執行環境 / 不在局內)時回傳 None。 (底層: 世界區塊擴充區:GetFloatGameState(GAME_STATE_TIME_OF_DAY))
- `is_night() -> 'bool | None'` [verified] [推送快照] 現在是不是夜裡(18:00~6:00)。職業打法:夜裡野怪睡著(先手打野不會被包圍)、所有單位視野變短(偷襲的好時機),
夜精靈的哨兵/單位夜裡在樹旁會隱形。讀不到時回傳 None。 (底層: 世界區塊擴充區(6~18 點為白天))
- `seconds_until(hour: 'float') -> 'float | None'` [verified] [推送快照] 離遊戲內時間 hour 點還有幾遊戲秒(例如 seconds_until(18) = 離天黑還有多久,規劃夜裡打野時用)。 (底層: 世界區塊擴充區 + 一天 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(執行環境在遊戲執行緒上當場記下,**每一下**都有):
damage:handle = 挨打的,.source_addr = 打它的(用 snapshot().unit_by_addr 換成單位),.value = 實際扣掉的血,
.raw_damage = 護甲前的傷害,.attack_type(normal/pierce/siege/magic/chaos/hero/spell),.damage_type
killed:這一下把它打死了,.source_addr = 兇手
以及執行環境追蹤生產表得出的 production.done(精度 = 發布週期):單位 = 建築,.done_code = 完成的四字碼,
.done_kind = 'training'(兵/英雄/復活)/ 'research' / 'construction'(建築蓋好)/ 'upgrade'(升級主城/升級塔),.value = 花了幾遊戲秒
09-25 補齊:
spell.cast:單位 = 施法者,.spell 技能四字碼,b 等級,value 冷卻秒數,x,y 施法點(在技能開始冷卻時辨識出來,精度 = 發布週期)
player.left:.player 離開 / 被判定落敗移除的玩家編號;game.ended:離開對局
selection.changed:本機玩家的選取變了(用 g.selection() 取得單位)
message:螢幕訊息框裡的一條(遊戲提示、聊天、系統):.text 全文,.frame 訊息框編號,
.chat = {'channel', 'sender', 'text'}(是聊天時;玩家在聊天框輸入的文字就從這裡讀取)
ui.click / ui.hover / hotkey / mouse.world:介面與輸入(g.ui),.key 是畫板 key / 熱鍵寫法
每筆是 Event(seq, kind, clock, addr, handle, type, owner, a, b, x, y, value, extra)。
公平模式(fair=True)只給:自己單位的事件、此刻看得見(或 1 秒內還看得見)的單位的事件、打我方/我方打的傷害,
以及本機的介面 / 訊息 / 對局類事件。 (底層: 事件環 Local\War3Events_(發布比對 + 執行環境擷取的傷害事件))
- `selection() -> 'list'` [verified] [推送快照] 本機玩家目前選取的單位(主單位排第一;最多 12 個)。選取一有變化就會發出 selection.changed 事件。 (底層: W3P 世界區塊擴充區 selAddrs(執行環境每次發布時帶上本機玩家的選取))
- `messages() -> 'list'` [verified] [推送快照] 上次呼叫之後螢幕訊息框裡新出現的訊息:[{'text', 'frame', 'repeat', 'seq', 'game_ms'}]。
遊戲提示(「需要更多的農場」「不能在那裡建造」)、聊天、系統訊息都在這裡;frame 區分是哪一個訊息框。
和事件流裡的 message 事件是同一批(各有各的游標)。 (底層: 共用記憶體 Local\War3Msgs_(執行環境擷取的螢幕訊息))
- `tech(code: 'str', player: 'int | None' = None) -> 'int | None'` [verified] [快車道] 研究等級 / 已建成的建築數(升級鏈算在內:城堡也算 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 = [(單位, 四字碼), ...],回傳同樣順序的裁決碼列表(問不到的是 None)。
規劃一拍要蓋/訓練什麼時先整體問一遍,比逐個 can_do 快 N 倍(參考大腦 09-23:建造規劃 76 -> 25 ms)。 (底層: W3P 查詢 q_feasible × N,一批提交)
- `tech_many(codes, player: 'int | None' = None) -> 'dict'` [verified] [快車道] 一次查很多個科技/建築計數:{四字碼: 數量或 None}。 (底層: W3P 查詢 q_tech × N,一批提交)
- `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 剛派農民去蓋農場,同一拍 rush_bot 從快照裡看它「閒置」又派去蓋兵營,農場一次次半途而廢。
挑「閒著的/沒在蓋房子的」單位時,用這個而不是 u.order。 (底層: 快照訂單 + 本行程剛被接下的命令(回執))
- `can_afford(code: 'str') -> 'bool'` [verified] [推送快照] 現在的金/木夠不夠買 code(單位、建築;依 units.json 的價格)。價格表裡沒有的一律當作買得起。
⚠ 升級主城的四字碼在表裡是累計價格,這裡會偏保守;最終以引擎的回執為準。 (底層: 推送快照的我方資源 + units.json 的價格)
- `map_data()` [verified] [本機計算] 目前這張地圖的資料(openwar3.mapdata.MapData):name_of('HC07') 自訂單位/物品/技能的名稱、hero_names、tooltip。
RPG 地圖的單位大多是地圖自己做的,內建名稱表裡沒有;不是由啟動器開啟的遊戲(找不到地圖檔案)回傳 None。 (底層: 地圖檔案(啟動器 --map 的路徑):w3u/w3t/w3a + wts,受保護的地圖讀取圖內的 TXT)
## 命令
讓單位做事。約一幀生效,每條都有回執。
- `batch() -> 'Batch'` [verified] [快車道] 把一拍裡的命令合成一批:
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])
每條命令單獨送出都要等遊戲執行緒處理一次(約 10 ms);一批只等一次 —— 參考大腦 09-23 靠這個把一輪從 48 -> 26 ms。
* 仲裁照舊逐條進行(被占用的單位當場得到 held 回執,不進入批次);
* 區塊裡的命令回傳 Pending:區塊結束前讀它的 .ok 會拋出錯誤(回執還不存在),區塊結束後和 Receipt 用法一樣;
* 區塊裡拋出例外 = 整批作廢(status 97 cancelled),被占用的單位會放回去;
* 查詢(can_do / tech / visible …)和 build_near、buy 不進入批次,照舊當場詢問 —— 它們的結果當場就要用;
要一次問很多個就用 can_do_many / tech_many;
* 巢狀的 with g.batch() 併入最外層的那一批;超過 16 條時執行環境會自動分成幾段(每段等待一次)。 (底層: 區塊裡的命令累積成一批,區塊結束時一次提交(同一幀執行、只等一次遊戲執行緒))
- `move(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [快車道] 走到 (x,y),路上不打人(撤退用這個)。可以傳一個單位或一個列表(同一幀一起下令)。
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。預設用右鍵(對敵人 = 攻擊這一個,09-23 實測訂單目標/任務目標都是它)。
⚠ 目標必須在視野內,看不見的會被拒(原因碼 1001)。
force=True 用攻擊令 0x0F(打自己人/中立小動物時需要它)—— 實測它只換上攻擊令、不記住目標,
會去打附近別的敵人,要打特定目標時別用它。 (底層: W3P target:目標命令(右鍵 smart))
- `stop(units)` [verified] [快車道] 停下手上的一切(訂單編號 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(攻城單位 / 迫擊砲 / 投石車))
- `cancel(building)` [verified] [快車道] 取消:訓練/研究佇列的最後一格(退錢)、正在蓋的建築(退 75%)、正在升級的大廳。 (底層: W3P immediate:cancel)
- `path(units, points, attack: 'bool' = False)` [verified] [快車道] 依序走過一串點(Shift 連點:路徑點、繞開塔、偵察路線)。attack=True 時每段都是攻擊移動。
一次提交;每個點一張回執(依 points 順序)。 (底層: 一批:第一段立即執行,其餘倒序用 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] [快車道] 一個工人依序連蓋幾座(Shift 連蓋):plan = [(四字碼, x, y), ...]。一次提交;回執依 plan 順序。
⚠ 錢是開工時才扣的(排隊時不扣)—— 排了 3 座但錢只夠 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] [快車道] 訓練單位 / 研究科技 / 升級大廳(升級主城 = 對大廳本身下目標大廳的四字碼,例如 'hkee')。
被拒時回執的 reason 會說明原因(人口不夠、缺金、缺木、佇列滿、缺前置……)。 (底層: W3P immediate:四字碼,被拒時帶有可行性原因碼)
- `learn(hero, ability: 'str')` [verified] [快車道] 英雄學技能(四字碼,例如 'AHbz' 暴風雪)。 (底層: W3P learn:技能點減少才算學會)
- `cast(u, spell, target=None, x: 'float | None' = None, y: 'float | None' = None)` [verified] [快車道] 施放技能。spell 是訂單名('thunderbolt' 風暴之錘、'blizzard'、'holybolt' 聖光術…,見 data/order-ids.txt)或訂單編號。
給 target = 對單位;給 x,y = 對地面;都不給 = 無目標(雷霆一擊、神聖護盾、召喚水元素)。
回執接下只代表引擎接受了;有沒有放出來要看 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)的物品;可帶目標單位或目標點。
⚠ 對點使用物品(例如象牙塔)引擎成功也回 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,目標是商店(實測避難權杖賣 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] [快車道] 人類的戰鬥號召:農民變民兵(第一級的城鎮大廳沒有這個技能,只對主城/城堡有效)。 (底層: W3P immediate:townbellon/off)
## 遊戲控制
遊戲速度、暫停、發布週期、頭頂氣泡、畫板、介面與輸入、訊息。
- `ui()` [verified] [直接寫入] 介面與輸入(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)。一次採集約 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] [控制通道] 在遊戲左下角的訊息區印出一行字(只有本機看得見)。要等遊戲自己先跳出過一條提示(DLL 從那一次抓取訊息框)。 (底層: 動作 45)
- `end_game() -> 'bool'` [verified] [控制通道] 結束這個遊戲行程(farm.py --keep 會依 next_game.json 自動開下一局)。 (底層: 動作 22)
- `canvas()` [verified] [直接寫入] 畫板:在遊戲畫面上畫文字框、面板、進度條、圖片、地上的圈和路線(openwar3.canvas.Canvas)。
由執行環境自行繪製,不建立遊戲控制代碼、不改變遊戲狀態 —— 多人局也安全;樣式隨你定(中文、圓角、半透明)。 (底層: W3P 73 canvas_enable + 共用記憶體 Local\War3Canvas_(執行環境每幀在遊戲畫滑鼠游標之前繪製,游標蓋在上面))
- `press_to_continue() -> 'bool'` [verified] [直接寫入] 在「按下任意鍵以繼續」的載入畫面上按一下空白鍵。很多 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) 建立一個單位(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] [快車道] 在畫面左下角顯示一行字(地圖觸發器用的那種文字),預設顯示給本機玩家。支援 |cffRRGGBB 顏色碼。 (底層: JASS DisplayTimedTextToPlayer)
## 連線與工具
連線狀態與純計算工具。
- `status() -> 'dict'` [verified] [本機計算] 連線狀態:pid、世界發布(週期、採集耗時)、快車道計數。 (底層: 世界區塊 + 快車道 + 仲裁表)
- `nearest(candidates, to)` [verified] [本機計算] 離 to(單位或 (x,y))最近的一個;沒有候選時回傳 None。 (底層: 純計算)