文件 開始

第一個 Bot

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

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

最小的 Bot

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))
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 拍才會停下。

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

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 人、礦滿了就去伐木、續建停工的地基。

加上兵營、英雄和出擊

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 寫著原因:

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

常見的原因碼:3 人口不夠、8 金不夠、9 木不夠、32 佇列已滿、183 缺少前置條件、221 沒有這一項 / 正在建造 / 英雄已經有了、1001 目標看不見。完整列表見 回執與原因碼。

接下 ≠ 做成

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

下一步