# 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。 (底層: 純計算)