Первый бот

Начинаем с минимального бота на 10 строк, добавляем найм крестьян, постройку ферм, армию, героя и атаку, а в конце разбираемся с квитанциями.

Бот — это класс, унаследованный от openwar3.Bot. Вам нужно переопределить только нужные хуки, а g (Game) отвечает за то, чтобы «видеть» и «делать».

Минимальный бот

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().

Что дальше