Docs Get started

Your First Bot

Start with a 10-line minimal Bot, then add training peasants, building for food, producing an army, heroes and attacking — and finally learn to read receipts.

A Bot is a class that subclasses openwar3.Bot. You only override the hooks you need; g (Game) handles “observing” and “acting”.

The minimal Bot

from openwar3 import Bot

class MyBot(Bot):
    def on_start(self, g):          # called once after the game starts
        g.message("I'm here")

    def on_tick(self, g):           # about 5 times per second
        for w in g.idle_workers():
            g.gather(w, g.nearest(g.gold_mines(), w))
python tools/play.py --bot my_bot.py

Idle peasants head to the nearest gold mine. The four hooks:

HookWhen it’s called
on_start(g)Once after the game starts, before the first tick
on_tick(g)Every tick (5 times per second by default). If a tick runs over, the next one is pushed back automatically — they never pile up
on_event(g, ev)Before each on_tick; hands you every event since the last tick, one by one
on_end(g, reason)Once when the game ends (the game process is gone / we have no units left / stopped manually)

An exception in on_tick doesn’t end the game: the runner prints the stack trace and continues with the next tick; it only stops after 20 consecutive ticks with errors.

Adding an economy: training peasants, building for food

from openwar3 import Bot

class Economy(Bot):
    def on_tick(self, g):
        res = g.resources()                       # None if it can't be read, not 0
        halls = g.my_buildings({"htow", "hkee", "hcas"})
        if res is None or not halls:
            return
        home = halls[0]

        # 1. Idle peasants go mine gold
        for w in g.idle_workers():
            mine = g.nearest(g.gold_mines(), w)
            if mine:
                g.gather(w, mine)

        # 2. Train peasants: only 1 in the queue at a time (a full queue locks gold up in it)
        if len(g.my_workers()) < 15 and not g.queue(home):
            g.train(home, "hpea")

        # 3. Food almost capped: find a peasant who isn't building and build a Farm near the hall
        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)

Three things worth noting:

  • Only train when g.queue(home) is empty. Issuing a train order every tick fills the 7-slot queue and locks up your gold (measured: the hall queued 4 peasants with 300 gold locked in the queue, and the opening was much slower).
  • build_near instead of hard-coded coordinates. It searches outward from near to far for a spot that fits and tracks the result across ticks; it does nothing when you can’t afford it. Hard-coded coordinates may well land right in a forest.
  • Don’t pick peasants who are already building. A Human Farm takes 35 seconds to build; if you send the worker away partway through, construction stops.

The complete version that works for all four races is brains/examples/hello_bot.py: 5 per mine, extra workers chop trees once the mine is full, and it resumes stalled construction.

Adding Barracks, a hero and attacks

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]

        # Hero: have an altar but no hero -> revive first, train only if revive fails (heroes are unique; training again after death is rejected)
        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")                # Holy Light

        # Barracks keep producing Footmen (only 1 in the queue at a time)
        for b in g.my_buildings({"hbar"}):
            if not g.queue(b):
                g.train(b, "hfoo")

        # Attack once a wave is ready; go home after heavy losses
        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)]      # only order idle units
                g.attack_move(idle, target.x, target.y)

For the full version, see brains/examples/rush_bot.py (it subclasses hello_bot and builds the Barracks / altar if missing).

Reading receipts

Every command returns a receipt. if r: means “the engine accepted it”; when it wasn’t accepted, r.reason says why:

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

Common reason codes: 3 not enough food, 8 not enough gold, 9 not enough lumber, 32 queue full, 183 missing prerequisite, 221 no such item / under construction / you already have this hero, 1001 target not visible. For the full table, see Receipts and reason codes.

Accepted ≠ done

A receipt only tells you “the engine accepted this command”. The engine also accepts a build spot inside a forest on the spot, and the worker only fails once it gets there; spells can be interrupted. To see the actual effect, look at snapshots and events: for buildings use build_near (it tracks whether the foundation appears), and for spells check whether g.cooldown() shows a cooldown.

Next steps