# 第一個 Bot

> 從 10 行的最小 Bot 開始，加上生產農民、蓋農場補人口、出兵、英雄和出擊，最後看懂回執。

來源: https://war3ai.com/zh-tw/docs/first-bot/

一個 Bot 就是一個繼承 `openwar3.Bot` 的類別。你只需要覆寫需要的掛鉤，`g`（`Game`）負責「看」和「做」。

## 最小的 Bot

```python title="my_bot.py"
from openwar3 import Bot

class MyBot(Bot):
    def on_start(self, g):          # 進入對局後呼叫一次
        g.message("我來了")

    def on_tick(self, g):           # 每秒約 5 次
        for w in g.idle_workers():
            g.gather(w, g.nearest(g.gold_mines(), w))
```

```bash
python tools/play.py --bot my_bot.py
```

閒著的農民會去最近的金礦。四個掛鉤：

| 掛鉤 | 什麼時候呼叫 |
|---|---|
| `on_start(g)` | 進入對局後、第一拍之前呼叫一次 |
| `on_tick(g)` | 每一拍（預設每秒 5 次）。一拍逾時會自動順延，不會越積越多 |
| `on_event(g, ev)` | 每拍的 `on_tick` 之前，把上一拍以來的事件逐一交給你 |
| `on_end(g, reason)` | 這一局結束（遊戲行程不見了 / 我方沒有單位了 / 手動停止）時呼叫一次 |

> **提示**
>
> `on_tick` 裡拋出例外不會中斷整局：執行器會印出堆疊、下一拍繼續；**連續出錯 20 拍**才會停下。

## 加上經濟：生產農民、蓋農場補人口

```python
from openwar3 import Bot

class Economy(Bot):
    def on_tick(self, g):
        res = g.resources()                       # 讀不到時是 None，不是 0
        halls = g.my_buildings({"htow", "hkee", "hcas"})
        if res is None or not halls:
            return
        home = halls[0]

        # 1. 閒著的農民去採金
        for w in g.idle_workers():
            mine = g.nearest(g.gold_mines(), w)
            if mine:
                g.gather(w, mine)

        # 2. 生產農民：佇列裡只排 1 個（排滿會把錢鎖在佇列裡）
        if len(g.my_workers()) < 15 and not g.queue(home):
            g.train(home, "hpea")

        # 3. 人口快滿：找一個沒在蓋房子的農民，在大廳附近蓋農場
        if res["food_cap"] - res["food_used"] <= 6:
            builder = next((w for w in g.my_workers() if not g.is_constructing(w)), None)
            if builder:
                g.build_near(builder, "hhou", home.x, home.y)
```

三個值得注意的寫法：

- **`g.queue(home)` 為空才訓練。** 每拍都下訓練命令會把 7 格佇列排滿、把錢鎖住（實測大廳排了 4 個農民、300 金鎖在佇列裡，開局慢了一大截）。
- **用 `build_near`，而不是寫死座標。** 它會自己由近到遠找放得下的位置、跨拍追蹤結果；錢不夠時什麼也不做。寫死的座標很可能剛好是樹林。
- **不挑正在蓋房子的農民。** 人類的農場要蓋 35 秒，中途把工人派走，地基就停工了。

完整、四個種族都能跑的版本是 `brains/examples/hello_bot.py`：一座礦 5 人、礦滿了就去伐木、續建停工的地基。

## 加上兵營、英雄和出擊

```python
from openwar3 import Bot

WAVE = 8

class Rush(Bot):
    def on_start(self, g):
        self.attacking = False

    def on_tick(self, g):
        halls = g.my_buildings({"htow", "hkee", "hcas"})
        if not halls:
            return
        home = halls[0]

        # 英雄：有祭壇沒英雄 -> 先復活，復活不了再訓練（英雄唯一，陣亡後再訓練會被拒）
        altars = g.my_buildings({"halt"})
        if altars and not g.my_heroes():
            if not g.revive(altars[0]):
                g.train(altars[0], "Hpal")
        for h in g.my_heroes():
            info = g.hero_info(h)
            if info and info["skill_points"]:
                g.learn(h, "AHhb")                # 聖光術

        # 兵營持續生產步兵（佇列只排 1 個）
        for b in g.my_buildings({"hbar"}):
            if not g.queue(b):
                g.train(b, "hfoo")

        # 存夠一波就出擊；打殘了就回家
        army = g.my_army()
        if len(army) >= WAVE:
            self.attacking = True
        elif len(army) < WAVE // 2:
            self.attacking = False
        if self.attacking:
            target = g.nearest([e for e in g.enemies() if g.is_building(e)], home)
            if target:
                idle = [u for u in army if not g.order_of(u)]      # 只對閒著的下令
                g.attack_move(idle, target.x, target.y)
```

完整版見 `brains/examples/rush_bot.py`（以 `hello_bot` 為基礎繼承，沒有兵營 / 祭壇就蓋）。

## 看懂回執

每條命令都會回傳一張回執。`if r:` 就代表「引擎接下了」；沒接下時 `r.reason` 寫著原因：

```python
r = g.train(barracks, "hfoo")
if not r:
    print(r.reason)        # rejected（人口不够）（= 人口不夠）
    print(r.verdict)       # 3
```

常見的原因碼：`3` 人口不夠、`8` 金不夠、`9` 木不夠、`32` 佇列已滿、`183` 缺少前置條件、`221` 沒有這一項 / 正在建造 / 英雄已經有了、`1001` 目標看不見。完整列表見 [回執與原因碼](https://war3ai.com/zh-tw/docs/reason-codes/)。

> **接下 ≠ 做成**
>
> 回執只說明「引擎接下了這條命令」。樹林裡的建造點引擎也會當場接下，工人走到才失敗；技能可能被打斷。效果要看快照和事件：蓋房子用 `build_near`（它會追蹤地基是否出現），放技能看 `g.cooldown()` 有沒有進入冷卻。

## 下一步

  - [心智模型](https://war3ai.com/zh-tw/docs/concepts/): 快照、命令、事件、一拍、批次 —— 為什麼這樣設計。
  - [職業打法食譜](https://war3ai.com/zh-tw/docs/cookbook/): 21 招：飽和採集、人口不卡、集火、拉殘血、夜裡打野……
