文档 开始

第一个 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() 有没有进冷却。

下一步