# 心智模型

> 快照、命令、回執、事件、一拍、批次。理解這六個概念，就理解了 API 為什麼長這樣，以及怎麼寫才快。

來源: https://war3ai.com/zh-tw/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()` 這類 API 都是從同一份快照裡取值，一拍裡呼叫多少次都不貴。

> **提示**
>
> 發布週期可以調整：`g.set_publish_period(ms)`，16 ~ 1000 毫秒。一次採集在遊戲執行緒上約 0.5 ~ 0.9 ms，設成 33 ms 也沒問題。整台機器共用一個值，以最後寫入的為準。

## 命令：寫入，約一幀

`g.move / attack / gather / build / train / cast …` 交給遊戲執行緒執行。執行環境在遊戲執行緒的**事件分派**裡批次執行用戶端提交的命令，所以一條命令大約等**一幀**（落在事件叢集裡時約 0.1 ms，否則等到下一次分派）。

- 命令可以傳入**一個單位或一個清單**，清單裡的單位在同一幀一起下令；
- 加上 `queue='after'` 就是 Shift 排隊：做完手上這件再做；
- 命令裡的單位直接用從快照拿到的物件即可，SDK 用**控制代碼對**核對身分（位址會被新單位重複使用，控制代碼不會）。

## 回執：每條命令都有

```python
r = g.build(worker, "hbar", x, y)
if r:                      # 引擎接下了
    ...
else:
    r.reason               # 'rejected（金不够）'（= 金不夠）
    r.verdict              # 8
r.exec_us                  # 這一條在遊戲執行緒上執行了幾微秒
```

回執是在**同一幀**裡讀回來的：下令前後單位的訂單、引擎函式的回傳值、可行性檢查的原因碼。它回答「引擎有沒有接下這條命令、為什麼沒接下」，但**不回答**「最後做成了沒有」—— 做成了沒有要看快照和事件。

全部狀態碼和原因碼見 [回執與原因碼](https://war3ai.com/zh-tw/docs/reason-codes/)。

## 事件：發生了什麼

`on_event(g, ev)` 會在每拍的 `on_tick` 之前，把上一拍以來的事件逐一交給你：

| 事件 | 含義 |
|---|---|
| `unit.appeared` / `unit.died` / `unit.removed` | 單位出現、死亡、消失（進入金礦、被轉化、屍體腐爛也算消失，不等於死亡） |
| `unit.damaged` / `order.changed` / `owner.changed` | 掉血、換訂單、換擁有者 |
| `hero.levelup` | 英雄升級 |
| `item.appeared` / `item.removed` | 地上的物品出現、被撿走或用掉 |
| `damage` | 引擎層級：**每一下**傷害。來源單位、攻擊類型、傷害類型、實際扣血、護甲前傷害 |
| `killed` | 引擎層級：這一下把它打死了，帶有兇手 |
| `production.done` | 訓練 / 研究 / 建造 / 升級完成，帶有四字碼和花了多少遊戲秒。對手的也有 |
| `spell.cast` | 單位放了技能：技能四字碼、等級、冷卻秒數、施法點 |
| `message` | 螢幕訊息框裡出現一條：遊戲提示（「需要更多的農場」）、聊天（`.chat` 裡有發言者和內容）、系統訊息 |
| `selection.changed` / `player.left` | 本機玩家的選取變了 / 有玩家離開或被判定落敗移除 |
| `game.started` / `game.ended` | 新的一局開始 / 離開對局 |

畫板按鈕被點擊、熱鍵、點地面這些輸入事件，請見 [介面與輸入](https://war3ai.com/zh-tw/docs/ui-input/)。

> **注意**
>
> 事件流是**全域**的：對手的生產完成、野怪的死亡都在裡面。請用 `ev.owner` 或單位控制代碼過濾。

## 一拍：Bot 的節奏

`on_tick` 預設每秒呼叫 5 次（依實際時間）。一拍的耗時基本上就是你自己的運算：快照零等待，命令約一幀。一拍超過週期會自動順延，不會越積越多。

- **2 倍速下別依實際時間等待。** 想等 3 遊戲秒，就看 `g.clock()` 增加了 3，不要 `sleep(1.5)`。
- **別在 `on_tick` 裡 `sleep`。** 需要「過一會兒再做」，就記下目前的遊戲時間，下一拍再判斷。

## 批次：幾十條命令只等一次

一拍要下很多條命令時，包在 `with g.batch():` 裡：

```python
with g.batch():
    g.attack(melee, target_a)
    g.attack(ranged, target_b)
    g.move(wounded, home.x, home.y)
    g.cast(hero, "thunderclap")
# 區塊結束時整批提交：同一幀執行，只等一次遊戲執行緒
```

- 區塊裡的命令回傳 `Pending`，區塊結束後變成回執；區塊結束前讀取它會拋出錯誤；
- 區塊裡拋出例外時，**整批作廢**（只送出一半的命令比不送更危險）；
- 實測 8 條移動：逐條 68 ~ 99 ms，一批 **6.5 ~ 10 ms**。

同樣的思路也適用於查詢：`g.can_do_many([(u, code), ...])`、`g.tech_many([...])` 一次查詢很多個。

## 讀取自己剛寫入的

同一拍裡，快照還看不到你剛下的命令（下一次發布才會跟上）。兩段邏輯可能會搶同一個工人：一段剛派它去蓋農場，另一段看快照以為它還閒著。

`g.order_of(u)` 解決這個問題：快照跟上之前，它以回執裡的新訂單為準。**判斷「是否閒置」用 `g.order_of(u)`，不要用 `u.order`。** `g.idle_workers()` 已經把「這一拍剛被派了工作的」排除掉了。

## 延遲檔位

| 檔位 | 通道 | 延遲 | 用於 |
|---|---|---|---|
| 0 | 推送快照 + 事件流 | 讀一份約 0.4 ms；資料每 50 ms 更新一份 | 所有「看」的 API |
| 1 | 快車道 | 約 1 幀；6 個行程並行時中位數 0.06 ms | 所有命令和查詢（SDK 預設） |
| 2 | 控制通道 | 20 ~ 40 ms | 備援、少數介面類操作（倍速、氣泡、訊息） |
| 3 | [閘道](https://war3ai.com/zh-tw/docs/gateway/)（WebSocket / JSON） | 檔位 1 + 約 1 ms | 任何語言、瀏覽器、LLM、另一台電腦上的程式 |

[API 目錄](https://war3ai.com/zh-tw/api/) 裡每個 API 都標明了它走哪一個檔位。
