# War3AI / OpenWar3 完整文档 > 来源 https://war3ai.com/ 。面向 AI Agent 的《魔兽争霸 III》1.27 开放接口。写 Bot 时只能使用文末「接口目录」里列出的 Game 方法。 --- # 文档概览 > OpenWar3 文档:是什么、能做什么;快速开始、写第一个 Bot、让大模型写 AI、接口和协议、网关与 MCP,按你的情况该从哪一页读起。 **OpenWar3** 是 War3AI 的开放接口层:一个注入进《魔兽争霸 III》1.27 的运行时,加一套 Python SDK。 - 运行时每 **50 ms** 把整张地图的完整状态推进共享内存:全部玩家的资源和人口,所有单位的血蓝、订单、正在打谁、技能冷却、buff、背包,地上物品,树,生产队列,昼夜。还有一条**事件流**:单位出现和死亡、每一下伤害、生产完成…… - 外部程序以**约一帧**的延迟下**语义命令**:移动、攻击、采集、建造、训练、施法、学技能、复活、用物品、买东西……每条命令都有**回执**,写明引擎接没接、没接的原因码。 - 你只需要说「做什么」:单位用四字码、技能用订单名,和游戏里的叫法一致;「怎么做到」由运行时负责。 因此大模型不需要任何底层知识,也不需要看画面。读完文档,它就能写出一个会运营、会打架的 Bot,上场之后再根据回执和事件自己修改。 不只是对战:[画板](https://war3ai.com/docs/canvas/) 能往游戏画面上画自己的面板和标注,[界面与输入](https://war3ai.com/docs/ui-input/) 让画出来的按钮能点、热键能响,[JASS 通道](https://war3ai.com/docs/jass/) 能从外面调用游戏里的 1291 个函数,在 RPG 地图里还能给自己配一个 [AI 玩伴](https://war3ai.com/docs/companion/)。写好的 AI 可以做成 [方案](https://war3ai.com/docs/schemes/),一键切换、导出分享;一整套新玩法可以写成 [玩法模组](https://war3ai.com/docs/mods/)。 不写 Python 也能接:[网关](https://war3ai.com/docs/gateway/) 让任何语言、浏览器页面用 WebSocket / JSON 调同样的接口,[MCP 服务器](https://war3ai.com/docs/mcp/) 让 Claude Code 这类 Agent 直接调工具看局面、下命令。 - [快速开始](https://war3ai.com/docs/quickstart/): 装好环境,一条命令起一局,看示例 Bot 接管。 - [用大模型写一个 Bot](https://war3ai.com/docs/ai-bot/): 不会编程也行:复制提示词,描述打法,交给 Agent。 - [心智模型](https://war3ai.com/docs/concepts/): 快照、命令、回执、事件、一拍。写 Bot 前花五分钟读一遍。 - [接口目录](https://war3ai.com/api/): 全部接口,每个都标了实测状态、延迟档位和底层机制。 ## 按你的情况选一条路 | 你是 | 从这里读 | 然后 | |---|---|---| | 会玩魔兽,不会编程 | [快速开始](https://war3ai.com/docs/quickstart/) → [用大模型写一个 Bot](https://war3ai.com/docs/ai-bot/) | 遇到问题看 [常见问题](https://war3ai.com/docs/faq/) | | 会 Python | [第一个 Bot](https://war3ai.com/docs/first-bot/) → [心智模型](https://war3ai.com/docs/concepts/) → [十五条规矩](https://war3ai.com/docs/rules/) | [职业打法食谱](https://war3ai.com/docs/cookbook/)、[示例 Bot](https://war3ai.com/docs/examples/) | | 在做 Coding Agent / 自动化 | [Agent 自主迭代](https://war3ai.com/docs/agent-loop/) | [回执与原因码](https://war3ai.com/docs/reason-codes/)、[`llms-full.txt`](https://war3ai.com/llms-full.txt) | | 想让大模型在局内做决策 | [大模型当参谋](https://war3ai.com/docs/llm-coach/) | [头顶气泡与本地模型](https://war3ai.com/docs/speech/) | | 想让 Agent 直接上手操作(Claude Code 等) | [大模型直接调工具(MCP)](https://war3ai.com/docs/mcp/) | [界面与输入](https://war3ai.com/docs/ui-input/) | | 用别的语言(JS、C#、Go、Rust……) | [网关](https://war3ai.com/docs/gateway/) | 更底层:[W3P 协议](https://war3ai.com/docs/protocol/) | | 想让不同人的 AI 对打 | [公平模式](https://war3ai.com/docs/fair-mode/) | [对战平台](https://war3ai.com/arena/) | | 想在 RPG / 自定义地图里造自己的玩法 | [玩法模组](https://war3ai.com/docs/mods/) | [界面与输入](https://war3ai.com/docs/ui-input/)、[画板](https://war3ai.com/docs/canvas/)、[JASS 通道](https://war3ai.com/docs/jass/)、[RPG 玩伴](https://war3ai.com/docs/companion/) | | 想把自己的 AI 分享给别人 | [AI 方案](https://war3ai.com/docs/schemes/) | [远见指挥台](https://war3ai.com/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/ 头顶聊天气泡 + 本地大模型 runtime/ 多实例编排(每局按设置重开) data/ order-ids.txt;从你自己的游戏提取数据的工具 schemes/ 你的 AI 方案(mine/)和别人分享的方案(installed/),不进仓库 tools/ play.py(一条命令起局)、run_scheme.py(方案运行器)、war3_mcp.py(MCP 服务器)、run_tests.py、实机核对脚本 docs/ 接口目录 api.json(从代码生成)、协议、手册 ``` 运行时和你的代码之间只隔着一份带版本的 [W3P 协议](https://war3ai.com/docs/protocol/):用 Python SDK 最省事,用别的语言照着协议接入也可以。 ## 接口的「实测状态」是什么意思 接口目录里每个接口都标了三种状态之一: - **实机验证过**:底层那条路(动作号、参数形状、读回的效果)在真实对局里验证过,有核对脚本守着。 - **实验**:新加的接口,已经在演练实例上跑通,还在逐项实机验证。可以用,接口细节可能还会调整。 - **推断 / 未完整实测**:底层机制照抄引擎自己的做法(比如 JASS 的等价函数),但还没在对局里逐项核对。用之前先看回执。 > **说明** > > 目前只支持**魔兽争霸 III 1.27**(冰封王座)。1.24 ~ 1.28 是同一套引擎结构,多版本适配在 [路线图](https://war3ai.com/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 把本地模型留在显存里 ``` 网关、头顶气泡、本地大模型也都在远见「控制中心」里起停,不用再找别的脚本。**彻底停止**:双击 `stop.bat`,或者在控制中心右上角点「全部停止」——游戏实例、AI、网关、气泡、本系统用的本地模型、远见后台依次全停。MCP 服务器归 Claude 等客户端管,不会被停。 > **配置文件** > > `openwar3.json` 由 `start.bat` 和远见自动写,只存本机路径,不进 git。要改端口、本地大模型的地址和模型名时,照 `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/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 # 打印接口目录 ``` ## 跑通之后 - [写第一个 Bot](https://war3ai.com/docs/first-bot/): 从 10 行的最小 Bot 开始,一步步加上造兵和出击。 - [让大模型替你写](https://war3ai.com/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/docs/reason-codes/)。 > **接下 ≠ 做成** > > 回执只说明「引擎接了这条命令」。树林里的建造点引擎也会当场接下,工人走到才失败;技能可能被打断。效果要看快照和事件:盖房子用 `build_near`(它会跟踪地基是否出现),放技能看 `g.cooldown()` 有没有进冷却。 ## 下一步 - [心智模型](https://war3ai.com/docs/concepts/): 快照、命令、事件、一拍、批量 —— 为什么这样设计。 - [职业打法食谱](https://war3ai.com/docs/cookbook/): 21 招:饱和采集、人口不卡、集火、拉残血、夜里打野…… --- # 用大模型写一个 Bot > 不会编程也能做:你负责说清楚想要它怎么打,大模型负责写代码。复制提示词模板,描述打法,跑起来,再让它改。 适合会玩魔兽、不会编程的人,也适合想省时间的开发者。整个过程是一个对话:**你描述打法 → 模型写代码 → 你跑一局 → 把现象告诉模型 → 它改**。 > **提示** > > 先按 [快速开始](https://war3ai.com/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/llms-full.txt),全站文档都在这一个文件里 | | **网页对话、不能上网** | 把手册、[`api.json`](https://war3ai.com/api.json) 和一个示例文件贴在提示词后面 | | **本地模型**(LM Studio、Ollama) | 同上。上下文窗口建议 32K token 以上,否则手册和接口目录装不下 | 想要某种职业打法,再把 [职业打法食谱](https://war3ai.com/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/docs/agent-loop/)。 ## 5. 常见问题 | 现象 | 多半是 | |---|---| | 什么都不动 | 实例号不对(`--inst`),或者游戏还没进局 | | 农民不采矿 | 给了正在干活的农民下令;只给 `idle_workers()` 派 | | 一直盖不出房子 | 用 `build_near`,别写死坐标;看回执 `reason` 是不是钱不够 | | 英雄不出来 | 看 `train` 的回执:人口不够?还是英雄死了(要 `revive`)? | | 英雄不放技能 | 没学(`learn`)或者没蓝;放完看 `cooldown()` 有没有进冷却 | | 兵一拍一拍地抽搐 | 每拍都在重下命令;只给闲着的单位下令 | | 兵出不来、钱一直涨 | 人口卡住了:看 `g.production(兵营).blocked` | | 模型用了不存在的方法 | 在提示词里再强调一遍「只能用 api.json 里的方法」,并把 api.json 贴全 | ## 进阶 - 全部接口和每个接口的底层机制:[接口目录](https://war3ai.com/api/); - 参考大脑(`brains/xwar3/strategy`)是一个完整的、会开矿打野出击的 AI,可以让模型读它的思路,但它用的是更底层的接口,不建议直接照抄; - 将来上 [对战平台](https://war3ai.com/arena/) 时只能看见视野内的敌人 —— 现在就加 `--fair` 自我约束,将来不用改。 --- # Agent 自主迭代 > 让 Coding Agent 自己跑局、读结果、改代码、再跑:它需要一条能无人值守的命令、一份结构化的对局报告和一个明确的目标。 [用大模型写一个 Bot](https://war3ai.com/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/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 里的方法,不要编造接口 - 不要每拍给同一个单位重复下同一条命令;只给闲着的单位下令 - 保持 --fair(只用视野里看得见的敌人) - 改代码前先跑 python tools/run_tests.py,确认没有把示例改坏 ``` ## 让闭环更快收敛的几个习惯 - **一次只改一处。** 同时改三处,赢了不知道是哪处起作用,输了也不知道是哪处搞坏的。 - **对比要打够局数。** 同一局面随机性很大;两局只能看出很大的差距。判断「是否进步」至少看几局的趋势。 - **先修「被拒」再调策略。** 回执里被拒最多的原因,往往就是 Bot 最大的 bug。 - **把判断写进注释。** 下一轮的 Agent(或者下一次对话)能从注释里知道为什么这么写,不会把修好的地方改回去。 - **离线测试兜底。** 给关键逻辑写不需要开游戏的单元测试(示例 Bot 的测试在 `brains/examples/tests/`),Agent 每次改完先跑一遍。 --- # 大模型当参谋 > 把「该攒什么、该往哪投人、这一分钟该打还是该缓」交给大模型,规则层只负责执行和否决。参考大脑已经这样做了,这一页讲清楚模式和坑。 写 Bot 写到一定程度,你会发现运营层的规则是一层一层贴上去的:伐木人数一条、每矿 5 人一条、木头多了减半一条、金多木少多派一条……每条单独看都对,合起来却会出现「矿上缺人,而所有农民都在砍树」这种**没有任何一条规则负责**的局面。 这类「看全局、排优先级」的判断本来就不适合写成 `if / else`,却正好是大模型擅长的。参考大脑(`brains/xwar3/strategy/brain/coach.py`)用的就是下面这套分层。 ## 分层 ```text 大模型(顾问) 每 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/docs/console/) 的「运营顾问」页就是干这个的。 ## 在你自己的 Bot 里实现 下面是一个最小的骨架,走任何 OpenAI 兼容的接口(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 = """你是《魔兽争霸3》的运营教练,只管运营和战略,不管微操。 只输出 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/docs/speech/)。想让模型直接逐拍下令(而不是当参谋),等 [对战平台](https://war3ai.com/arena/) 的 JSON 网关。 --- # 大模型直接调工具(MCP) > tools/war3_mcp.py 是一个 MCP 服务器。Claude Code、Claude Desktop 或任何支持 MCP 的客户端挂上它,大模型就能直接看局面、下命令、在屏幕上跟玩家说话、弹卡片问玩家、截图看画面,不用先写代码。 `tools/war3_mcp.py` 是一个 **MCP 服务器**(stdio)。Claude Code、Claude Desktop、本地模型的 Agent 框架 —— 任何支持 MCP 的客户端挂上它,大模型就能**直接**看局面、下命令、在游戏屏幕上跟玩家说话、问玩家、截图看画面,不用先写代码。 写 Bot、当参谋、让单位说话之外,这是又一种接法:**大模型自己当工具的使用者**。 ## 挂上 ```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` 限定大模型能做什么: | 角色 | 能用 | |---|---| | `dev`(默认) | 全部工具,包括 `war3_jass` | | `player --player N` | 只能指挥 N 号玩家的单位、只看得见它的视野(公平模式);没有 JASS | | `observer` | 只读,不能往屏幕上画、不能让单位说话;运行时直接拒绝它下的命令 | `player` 角色和[网关](https://war3ai.com/docs/gateway/)的限制一样:拿不到结束游戏、改速度、暂停,拿不到看得见别人底牌的接口,带玩家号的查询只能查自己。 几个上限:一个工具结果最多 20 万个字符,超出的截断并提示怎么缩小范围;`war3_ask_player` 最多等 120 秒;截图的 `scale` 在 0.1 到 1 之间。 ## 工具 | 工具 | 做什么 | |---|---| | `war3_overview` | 一页局面:时间、资源、人口、我方各兵种数、英雄(血、蓝、等级、冷却)、看得见的敌方兵种、生产。**先调它** | | `war3_units` | 单位列表(`owner` 取 me / enemy / creep / all,`types` 过滤);`addr` 用来下命令 | | `war3_events` | 上次调用之后发生的事:死亡、升级、施法、生产完成、聊天、玩家点了按钮……(默认去掉刷屏的几种) | | `war3_call` | 调任意公开接口(`move`、`attack_move`、`train`、`build`、`cast`、`learn`、`ui.button`、`canvas.text`……),单位写 `{"unit": addr}` | | `war3_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/docs/gateway/) 共用同一套角色白名单、单位参数格式和「一页局面」。 - 日志走 stderr,stdout 只有协议。 --- # 心智模型 > 快照、命令、回执、事件、一拍、批量。理解这六个概念,就理解了为什么接口长这样,以及怎样写才快。 ## 快照:读,零等待 运行时每 **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()` 这类接口都是从同一份快照里取,一拍里调多少次都不贵。 > **提示** > > 发布周期可以调:`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/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/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 新一份 | 所有「看」的接口 | | 1 | 快车道 | 约 1 帧;6 进程并发时中位 0.06 ms | 所有命令和查询(SDK 默认) | | 2 | 控制通道 | 20 ~ 40 ms | 兜底、少数界面类操作(倍速、气泡、消息) | | 3 | [网关](https://war3ai.com/docs/gateway/)(WebSocket / JSON) | 档 1 + 约 1 ms | 任何语言、浏览器、大模型、另一台机器上的程序 | [接口目录](https://war3ai.com/api/) 里每个接口都标了它走哪一档。 --- # 十五条规矩 > 每一条都是在真实对局里踩出来的。写 Bot 时对照一遍,能省掉大部分排查时间。 > **提示** > > 把这一页和 [`api.json`](https://war3ai.com/api.json) 一起交给大模型,它写出来的 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/arena/) 不用改。详见 [公平模式](https://war3ai.com/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/arena/) 的裁判进程保证:Bot 永远碰不到共享内存,只能拿到裁判按视野过滤过的观察,只能提交动作,每条动作先校验单位归属。 ## 为什么现在就该开 - 将来上擂台时规则就是这样,现在按公平模式写,到时候一行不用改; - 离开全图信息,才知道你的 Bot 真实水平如何(参考大脑目前大量依赖全图信息,比如电脑队长的目标点 —— 这正好是一次检验); - 公平模式下写出来的侦察、记忆、判断,才是真正有价值的 AI 能力。 --- # 职业打法食谱 > 高手的优势大多来自几十个「小习惯」。这一页把常见的职业打法逐条落到 SDK 代码上,每段都能直接抄进 on_tick。 约定:`g` 是 `Game`,`home` 是我方主基地(`g.my_buildings({"htow", "hkee", "hcas"})[0]`),`now = g.clock()`。接口细节见 [接口目录](https://war3ai.com/api/),完整可跑的例子在 [示例 Bot](https://war3ai.com/docs/examples/)。 > **提示** > > 让大模型加某种打法时,把对应那一招连同代码一起贴给它,比描述「打得职业一点」有效得多。 ## 一、运营 ### 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/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` 问引擎能不能研究下一级攻防;升本和高级兵(实机教训:一直停在一本,23 分钟被三本的骑士狮鹫推平);按 `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 式的多战略选择与切换、建造表、反制兵种、选英雄;可选的[大模型运营顾问](https://war3ai.com/docs/llm-coach/) | | 毫秒层 | `reflex/`(4 个独立进程) | 100 ms 级 | 保命、施法、集火、捡装备 | | 胜率模型 | `worldmodel/` | — | 打不打得过(推理子集) | 多个进程通过**仲裁表**共享单位,按优先级决定谁说了算:人手 95 > 保命 90 > 躲技能 85 > 施法 80 > 捡装备 70 > … > 策略 50 > 派工 45。你自己的 Bot 在表里的身份是 `bot`,默认优先级 50。 > **注意** > > 参考大脑直接用 SDK 的底层(`w3cmd` / `act`),并且大量依赖全图信息。它适合作为「思路」的参考,不建议让大模型直接照抄。它需要 AMAI 数据:`start.bat` 第一次部署时会从 AMAI 的公开仓库拉取并生成(AMAI 为自定义许可,生成物不进 git;没成功的话 `start.bat setup` 重试)。 启动参考大脑最简单的方法是用 [远见指挥台](https://war3ai.com/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 伙伴**:它跟着你走、帮你打怪,你残血时给你加血,没事的时候陪你说两句 —— 台词还能接本地大模型。 **怎么用由你自己决定。** 这件事拆成三层接口,从底到顶,哪一层都能直接用: | 层 | 是什么 | 适合 | |---|---|---| | **JASS 通道** `g.jass` | 地图作者能用的 1291 个 JASS 函数,按名字直接调(造单位、设盟友、给物品、改名、显示文字、复活英雄……) | 想自己造玩法 | | **便捷接口** | `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/docs/canvas/) 画,多人局也安全。 ### 说话,和本地大模型 `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`。 **接本地大模型**:`Talk(llm=LocalLLM(url, model))`,任何 OpenAI 兼容接口都行(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/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/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/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/docs/protocol/)。 ## 好几个程序同时画 模组、远见、MCP、网关可能同时往同一局里画东西,画板只有一块。规矩是:**每个程序只动自己的元素**。 - 写之前先拿一把命名锁,读出现有的元素,留下别人的,换上自己的,再写回; - 每个元素记着是谁画的(进程号 + 进程内序号),画它的程序退出了,下一次有人写时顺手清掉;它的按钮也不再拦点击; - 元素编号从共用的计数器分配,不会撞号。 Python SDK 已经这么做了,`clear()` 也只清自己的。自己直接写共享内存的话照这个来,否则会把别人的东西冲掉。布局细节见 [W3P 协议](https://war3ai.com/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/docs/companion/) 的状态面板就是用画板画的:血条、正在干什么、心情、击杀和加血次数。 --- # 界面与输入 > 画板上的按钮、选项卡能点,悬停自动高亮;登记热键、点地面选位置、读鼠标指着哪儿、知道本机玩家选中了谁。点击、热键、放技能、聊天全文、玩家离开,都进事件流。 [画板](https://war3ai.com/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/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/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/docs/gateway/) 上同名可用。远端给不了回调函数,点击和热键从事件推送里收(`ui.click` 事件带 `key`)。 - **直接写共享内存**:先发语义命令 `input_enable`(W3P 操作码 74),运行时开始接管输入;在输入块 `Local\War3Input_` 里你写热键表和鼠标开关,它回写鼠标位置、指着的地面点和悬停项。画板项的标志位 `0x40` 表示「可交互」。布局见 [W3P 协议](https://war3ai.com/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/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/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/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/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/docs/protocol/)。 --- # 玩法模组 > 方案不只是一个替你打的 AI,也可以是一套规则:你自己在游戏窗口里玩,模组负责布置开局、刷怪、给奖励、在屏幕上给你按钮和选项卡、判胜负。继承 openwar3.Mod,一个文件就是一套玩法。 [AI 方案](https://war3ai.com/docs/schemes/) 有两种:`kind: bot` 是一个 AI,替你打;`kind: mod` 是**一套规则** —— 你自己在游戏窗口里玩,模组出题:开局怎么布置、按时间或事件刷怪、给什么奖励、屏幕上给你哪些按钮和选项卡、什么时候算赢。 模组用到的全是现成的能力:[界面与输入](https://war3ai.com/docs/ui-input/)(能点的按钮、卡片、热键、点地面)、[画板](https://war3ai.com/docs/canvas/)(面板、进度条、路线)、[JASS 通道](https://war3ai.com/docs/jass/)(刷单位、改属性、给物品)、事件流(死亡、升级、放技能、聊天)。 ## 两个示例 在远见的「AI 方案」→「内置」里就能选: | 模组 | 玩法 | 用到的能力 | |---|---|---| | **英雄肉鸽** `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/docs/schemes/)。模组也是代码,别人的模组第一次运行前同样要确认信任。 ## 实测 2026-09-25,演练实例上: - **英雄肉鸽**:第一波刷出来,右上角面板在走;把英雄提到 3 级 → 屏幕中间弹出卡片、游戏时钟停住;点两次卡片 → 两次强化生效(力量 22 → 27),时钟恢复。 - **无尽守城**:面板、地上的路线、按钮都在;F8 + 点地面 → 主城边上多了一座防御塔;这一波还没清完就点按钮 → 提示「这一波还没清完」。 ## 边界 - **只能单人局**:刷单位、改属性走 JASS 通道,多人局里会不同步。这是锁步模型决定的,多人玩法要等同步通道(见 [路线图](https://war3ai.com/roadmap/))。 - 模组看全图 —— 它是出题的,不是玩家。 - 对战图里的电脑对手只是被「压住」,没有被移除(移除会触发对战规则的胜利判定)。 --- # 远见指挥台 > 本机网页控制台,也是唯一的入口:设置游戏目录、起停各项服务、起停游戏实例、设置下一局、看 AI 在想什么、手动下令、导播、对局记录。 远见(Farsight)是跑在你本机的网页指挥台,**只监听 127.0.0.1**。它也是整套系统唯一的入口:开局、换 AI、网关、头顶气泡、本地大模型都在这里点,不用再找别的脚本。 ```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/docs/gateway/)、[头顶聊天气泡](https://war3ai.com/docs/speech/)、本地大模型(LM Studio)、官网本地预览,每张卡片上都能启动、停止、重启、看日志;另外显示 [MCP](https://war3ai.com/docs/mcp/) 服务器有没有被客户端挂上。 - **环境自检**:Python、运行时文件、游戏数据、AMAI 数据等各部分装好了没有。 - **全部停止**(右上角):游戏实例、AI、网关、气泡、本系统用的本地模型、远见后台依次全停;和双击 `stop.bat` 一样。MCP 服务器归 Claude 等客户端管,不会被停;LM Studio 程序本身也不会被关。 ## 页面 | 分组 | 页面 | 做什么 | |---|---|---| | 总控 | 控制中心 | 见上一节 | | 对局 | 总览 | 当前实例的对局概况 | | | 战场指挥 | 地图视图;可以手动下令(手动命令在仲裁表里优先级最高:95) | | | 单位数据 | 每个单位的订单、任务目标、蓝量、英雄等级经验、技能冷却、背包 | | | AI 决策 / 战斗决策 | 参考大脑这一拍在想什么、每一次战斗决策的明细 | | | 运营顾问 | [大模型参谋](https://war3ai.com/docs/llm-coach/) 的状态:模型服务在不在、每个实例连上没有、最近一条建议和它看到的输入 | | | 导播台 | 自动运镜、头顶血条 | | | 头顶气泡 | 让单位说话、和本地模型对话、农民茶话会、镜头对白、战况触发、模型设置。见 [头顶气泡](https://war3ai.com/docs/speech/) | | | 指令速度 | APM 与命令吞吐 | | | 事件与输入 | 这一局发生了什么:放技能、聊天和屏幕消息、点按钮、热键、点地面、选中、玩家离开,按类别筛选;旁边是鼠标位置、悬停项和本机选中。见 [界面与输入](https://war3ai.com/docs/ui-input/) | | 记录 | 日志 / 对局记录 | 每个实例的日志源;每一局的结局、时长、兵力峰值 | | | 问题备忘 | 在游戏里按 Pause/Break 暂停并记下时间点,回头在这里补描述 | | 系统 | 实例与开局 | 起停实例;设置**下一局**的地图(对战图,也可以选 RPG / 自定义地图)、双方种族、难度、倍速;每个实例选一个 AI 方案;「开始测试」一键起游戏 + AI | | | AI 方案 | 导入、导出、复制、信任、删除方案,给实例切换方案(正在打的这一局也能立刻换人接管),看每个方案的战绩。见 [AI 方案](https://war3ai.com/docs/schemes/) | | | JASS 控制台 | 写 JASS 脚本点运行,右边按分类查 1291 个函数、点一下插进脚本;变量在同一局里一直记着。见 [JASS 通道](https://war3ai.com/docs/jass/) | | | 接入与扩展 | 网关状态和一键启动;按开发 / 选手 / 观众角色生成的连接地址、MCP 挂接命令和配置;JS、Python 示例。见 [网关](https://war3ai.com/docs/gateway/)、[MCP](https://war3ai.com/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 的决策和战况。 ## 接口 指挥台的服务端是一组本机 REST + WebSocket 接口(实例状态、下一局设置、单位详情、手动下令、日志、对局记录、导播、AI 方案、JASS 调用、画板……),网页只是其中一个客户端,任何语言的程序都能直接调。接口清单写在 `console/server/app.py` 的文件头里;方案、JASS、画板这三组的用法分别见 [AI 方案](https://war3ai.com/docs/schemes/)、[JASS 通道](https://war3ai.com/docs/jass/)、[画板](https://war3ai.com/docs/canvas/)。 --- # AI 方案 > 一个方案就是一套完整的 AI。在远见里一键切换,正在打的这一局也能立刻换人接管;导出 zip 分享给别人,导入别人的方案来测,每个方案的战绩自动统计。 一个**方案** = 一套完整的 AI:一个文件夹 + 一份说明书 `scheme.json` + 代码。每个游戏实例选一个方案;在远见里一键切换,**正在打的这一局也能立刻换人接管**。 别人分享的方案导入进来是**单独的一块**,和你自己的方案互不影响;想改就「复制到我的」。 ```text schemes/ mine// 我的方案:自己写的,或从别的方案复制来改的(随便改,下一局生效) installed// 已安装:别人分享的 zip 解开在这里(第一次运行前要确认信任) brains/xwar3/ 内置:参考大脑(完整 AI) brains/examples/ 内置:四个教学示例 hello / rush / macro / micro,玩伴示例 buddy,两个玩法模组(英雄肉鸽、无尽守城) ``` 方案不一定是替你打的 AI:`kind: mod` 的方案是一套**玩法规则**,你自己玩,它出题,见 [玩法模组](https://war3ai.com/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/docs/mods/)(`openwar3.Mod` 的子类,固定不走公平模式、不按对战规则判胜负) | | `class` | | 入口文件里的 Bot(或 Mod)子类名;不写就取入口文件里最后一个 `openwar3.Bot` 子类 | | `fair` | | 默认 `true`:只看得见视野里的东西,和对战平台同一个规则。`false` = 全图可见,也才能用 [JASS 通道](https://war3ai.com/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 个文件;不许绝对路径和 `..`(防止写到方案目录外面);说明书不合法或入口文件不存在直接拒绝。 > **注意** > > 信任之前先「打开文件夹」读一遍代码。只从你信得过的人那里拿方案。 ## 接口(给脚本用) | 接口 | 说明 | |---|---| | `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/roadmap/)。 --- # 头顶气泡与本地模型 > 让游戏里任意单位以任意身份头顶弹出对话气泡;接上本地大模型,一句话进、一句回复出在单位头上。 气泡是观赏层:不影响胜负,适合直播、解说、调试。 - 任意单位、任意身份说话;多个单位可以同时说; - 每个气泡的字号、颜色、宽度、尾巴、透明度、打字速度都能单独定制; - 能直接接本地大模型(LM Studio),支持流式输出:边生成边更新气泡。 ## 从 Bot 里用 最简单的方式是 SDK 自带的 `say`: ```python g.say(hero, "跟我冲!", seconds=4) ``` ## 启动和界面 **最省事:远见首页「控制中心」**——先点「本地大模型 → 启动并载入模型」(LM Studio 本地服务 + 把配好的模型载进显存),再点「头顶聊天气泡 → 启动」。卡片上能看日志、停止、重启。 界面在远见左侧的「头顶气泡」页:让单位说话(选单位、写字、调样式、和模型对话)、农民茶话会、镜头对白、战况触发、模型设置,按顶栏选的实例操作。 命令行也可以: ```bash python speech/speak_launch.py # 起本地模型服务 + 载入模型并预热 + 起气泡接口 python speech/speak_launch.py --restart # 改了代码后重起接口 python speech/speak_launch.py --stop # 停接口,并把模型从显存卸掉 ``` 每一步都是「在就跳过」,重复运行没有副作用。 ## HTTP 接口 默认 `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 轮对话。 ### 其它 | 接口 | 说明 | |---|---| | `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 能调的公开接口,JS、C#、Go、Rust、浏览器页面、另一台机器上的程序都能调。三种角色,自带 JS 客户端和浏览器演示页;延迟是快车道再加约 1 ms。 网关把快车道和推送状态包成 **WebSocket / JSON**。[接口目录](https://war3ai.com/api/) 里 Python SDK 能调的公开接口,JS、C#、Go、Rust、浏览器页面、另一台机器上的程序、大模型都能调,方法名和参数都一样。延迟是快车道再加约 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/docs/mods/)、玩伴 | | `player`(加 `&player=N`) | 观察、指挥 N 号玩家的单位、画界面;**默认公平模式**,只看得见 N 号玩家视野里的东西(`&fair=0` 关掉) | 替某个玩家下场的 Bot 或大模型 | | `observer` | 只读(运行时直接拒绝它下的命令) | 观战、解说、数据采集 | `player` 拿不到:结束游戏、改速度、暂停这类游戏控制,看得见别人底牌的 `players`、`enemy_ai_plan`,会让游戏进程打开本机文件的 `canvas.image`,还有 JASS。`resources`、`tech`、`stats` 这类带玩家号的查询只能查自己。 一个连接就是一个会话,占一条快车道(运行时一共 16 条)。网关同时最多 12 个会话,给 Bot、模组和远见留几条。断开时只收掉这个会话自己画的东西和热键,别的程序画的不动。 ## 消息 连上之后先收到 `hello`:协议版本、角色、游戏进程号、这个角色能调的方法列表。之后每条请求带一个 `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/docs/ui-input/)。 - 一条调用出错只回这一条(`ok: false` 加 `error`),连接不断;发来的不是 JSON 也一样。 - 事件 JSON 的字段和 [W3P 协议](https://war3ai.com/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 就够了,不用碰共享内存。 **大模型**:直接用 [MCP 服务器](https://war3ai.com/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/arena/)。 --- # W3P 协议 > 运行时和外部程序之间的全部契约:八块共享内存、读世界状态、读事件、下命令、回执、车道角色、画板、界面与输入。用 Python 以外的语言接入,看这一页。 运行时和外部程序之间**只通过共享内存**交换数据,下面这些就是全部。 - 参考实现是 Python 的 `sdk/python/w3world.py`(读)和 `sdk/python/w3fast.py`(写),每个结构的大小和偏移都写在里面,有测试钉住; - **协议只描述语义,和游戏版本无关。** 换游戏版本时运行时自己适配,协议不变;新字段只追加在块尾,老客户端照常能用。 > **说明** > > 大多数人不需要读这一页 —— 用 Python SDK 就好。只有当你想用 C++ / C# / Rust / Go 等语言直接接入,或者想知道 SDK 底下发生了什么时,才需要它。 ## 1. 八块共享内存 `` 是游戏进程号。 | 名字 | 方向 | 内容 | 同步方式 | |---|---|---|---| | `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/docs/canvas/):头 64 字节 + 256 个元素 × 112 字节 + 64 KB 文字 / 点池;发一次 `canvas_enable` 后才建 | seqlock(你写,运行时每帧读) | | `Local\War3Msgs_` | 运行时 → 你 | 屏幕消息环:游戏提示、聊天、系统消息的全文,128 条 × 256 字节 | 每条自带序号 | | `Local\War3Input_` | 双向 | [界面与输入](https://war3ai.com/docs/ui-input/):运行时回写鼠标位置、指着的地面点、悬停项;你写热键表和鼠标开关;发一次 `input_enable` 后运行时才开始接管输入 | 热键表 seqlock | **好几个客户端同时用画板和输入**:这两块都只有一份,各写各的会互相覆盖。约定如下,自己写客户端也要照做: - **画板**:持命名互斥量 `Local\War3CanvasMutex_` 读 - 改 - 写,只换自己的元素,别人的原样留着(重排池偏移);主人进程已退出的和没有主人的清掉。元素的 `reserved[1]` = 主人进程号、`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` 单位(地址、句柄 lo、句柄 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/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/docs/ui-input/) | ## 5. 回执 回执 52 字节(+8 字节耗时):`status`、`engineReturn`、`verdict`(被拒原因码)、`orderBefore / orderAfter`(同一帧读回的单位订单)、`value[8]`(查询结果)、`execUs`(这条在游戏线程上执行了多少微秒)、`engineUs`(其中引擎下令函数本身)。 全部状态码和原因码见 [回执与原因码](https://war3ai.com/docs/reason-codes/)。 ## 6. 车道角色 | 角色 | 能做什么 | |---|---| | `dev` | 本机工具:语义命令(指挥本机玩家的单位)+ JASS 通道 | | `player` | 只能语义命令,只能指挥车道所属玩家的单位(别人的 = `not_owner`) | | `observer` | 只能查询、镜头、读 HUD 面板状态、开画板和本机输入;别的一律 `forbidden` | 两个 AI 对打 = 同一局里开两条 `player` 车道(player 0 / player 1)。 > **注意** > > 本机模式下角色是客户端自己声明的(约定,不是安全边界)。[对战平台](https://war3ai.com/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)` | ## 查询接口 以下接口不下令,只问引擎,结果也放在回执的 `value` 里(SDK 直接返回值): | 接口 | 返回 | |---|---| | `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/docs/legal/)。 ## 支持哪些游戏版本? 目前只支持**魔兽争霸 III 1.27**(冰封王座)。1.24 ~ 1.28 是同一套引擎结构,多版本适配(按版本选符号表、特征码兜底、开机自检出能力清单)在 [路线图](https://war3ai.com/roadmap/) 的 P4 阶段。1.29 之后的版本和重制版是另一套引擎,需要单独适配,目前不承诺。 ## 会修改我的游戏文件吗? 不会。运行时在游戏运行时注入,**不修改磁盘上的 Game.dll** 或任何游戏文件。多开时只是把原版的 `War3.exe` 启动壳原样拷贝改名。游戏数据(单位表等)从你自己的游戏里提取,不随代码分发。 ## AI 能看到什么? 基本上是一个职业选手想知道的一切,每 50 ms 更新一次: - 所有玩家的金、木、人口;所有单位的位置、血蓝、当前订单、**正在打谁**、等级经验; - 英雄和单位的技能等级与剩余冷却、身上的 buff、背包; - 每座建筑在训练 / 研究 / 建造 / 升级什么、进度多少、有没有因为人口卡住; - 地上的物品、树、地图的可走 / 可建网格、出生点、游戏内时间(昼夜); - 事件流:单位出现与死亡、**每一下伤害**(谁打的、攻击类型、护甲前伤害)、击杀、生产完成、英雄升级…… - 还能直接问引擎:某件事现在能不能做、为什么不能;某个科技几级;某一点看不看得见;金矿还剩多少;电脑对手要带兵打哪。 在此之上,SDK 还算好了战斗属性(克制、护甲、攻防升级)、「打死它要几秒」和地面寻路。全部接口见 [接口目录](https://war3ai.com/api/)。 ## AI 能做什么? 玩家能做的操作基本都有:移动、攻击移动、攻击指定目标、停止、原地待命、巡逻、攻击地面、采集、修理、建造(可自动找落点)、训练 / 研究 / 升级、取消、学技能、施法(对单位 / 对点 / 无目标)、集结点、复活英雄、捡 / 用 / 丢 / 给 / 卖物品、买东西、战斗号召;Shift 排队、按路径点行军、一个工人连造几座;还有倍速、暂停、头顶气泡。每条命令都有回执。 除了玩家的操作,还能往游戏画面上画自己的面板和标注([画板](https://war3ai.com/docs/canvas/)),以及在单人局里调用地图作者能用的 1291 个 JASS 函数([JASS 通道](https://war3ai.com/docs/jass/))。 ## 能用在 RPG / 自定义地图里吗? 能。选一张 RPG 地图、给实例选「玩伴示例」方案,开局后你自己玩,身边会跟着一个会助战、给你加血、陪你说话的 AI 伙伴,见 [RPG 玩伴](https://war3ai.com/docs/companion/)。`g.map_data` 能读出地图自定义单位的名字;[JASS 通道](https://war3ai.com/docs/jass/) 能造单位、设盟友、弹面板……怎么玩由你决定。改世界的操作只在单人局可用(多人局会不同步),画板在多人局里也安全。 ## 不会编程能用吗? 能。按 [快速开始](https://war3ai.com/docs/quickstart/) 装好环境,然后看 [用大模型写一个 Bot](https://war3ai.com/docs/ai-bot/):你用大白话描述打法,大模型写代码;跑起来有问题,把报错或你在游戏里看到的现象告诉它,让它改。 ## 只能用 Python 吗? SDK 是 Python。运行时和外部程序之间只有一份共享内存协议([W3P](https://war3ai.com/docs/protocol/)),任何能读写 Windows 共享内存的语言都能接入。更省事的是 [网关](https://war3ai.com/docs/gateway/)(WebSocket / JSON):JS、C#、Go、Rust、浏览器页面、另一台机器上的程序都能调同样的接口;大模型 Agent 可以直接挂 [MCP](https://war3ai.com/docs/mcp/)。 ## 用哪个大模型最好? 能写代码的主流模型都可以。关键不在模型,而在**给它正确的材料**(手册 + `api.json` + 一个示例),并要求它只用接口目录里存在的方法。局内实时决策(参谋、配音)对延迟敏感,本地 MoE 模型表现很好,见 [大模型当参谋](https://war3ai.com/docs/llm-coach/) 和 [头顶气泡与本地模型](https://war3ai.com/docs/speech/)。 ## 会拖慢游戏吗? 世界状态一次采集在游戏线程上中位 0.5 ~ 0.9 ms(100 ~ 120 个单位),每 50 ms 一次。命令在游戏线程上每条几微秒,一次排空有 4 ms 的时间预算,做不完的留到下一次,不会拖住游戏。所有对游戏的调用都有异常保护,Bot 崩了只是那一方停了,不会带崩游戏。 ## 能同时开多个游戏吗? 能。`runtime/farm.py` 负责多实例编排,每个实例一个编号;在 [远见指挥台](https://war3ai.com/docs/console/) 里起停。你的 Bot 用 `--inst N` 连到指定实例。 ## 能让两个 AI 对打吗? 在同一局里开两条 `player` 通道(`--player 0` / `--player 1`)就是 AI 对 AI。本机模式下公平靠约定;有裁判、视野过滤、归属校验的正式对战在 [对战平台](https://war3ai.com/arena/)(P6 阶段)。 ## 支持 Mac / Linux 吗? 目前只支持 Windows 10 / 11。 ## 用什么许可证? 许可证会随正式版一起公布。第三方组件保留各自的许可(例如 MinHook 为 BSD-2);AMAI 为自定义许可,其衍生数据不随项目分发,安装时从 AMAI 公开仓库拉取生成。 ## 遇到问题去哪反馈? 正式版发布后会开放问题反馈渠道。反馈时请附上实例号、`python -m openwar3 status` 的输出和复现步骤。先看看 [调试与性能](https://war3ai.com/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.json) 状态:verified = 底层路径实机验证过;experimental = 新接口,已跑通、还在逐项实机验证;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。 (底层: 纯计算)