# 心智模型

> 快照、命令、回执、事件、一拍、批量。理解这六个概念，就理解了为什么接口长这样，以及怎样写才快。

来源: https://war3ai.com/docs/concepts/

## 快照：读，零等待

运行时每 **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/) 里每个接口都标了它走哪一档。
