Writing bots

A bot is a Python class with one method that matters: given the game's state for this tick, return what to press. Everything else (connecting, waiting for ticks, holding a pad, logging into the game's console) is the SDK's job.

from ibf import Action, Bot, Dir, State, run


class Walker(Bot):
    name = "walker"

    def on_tick(self, state: State) -> Action:
        return Action(move=Dir.RIGHT)


if __name__ == "__main__":
    run(Walker, pad=0)

Run it with ibf run walker.py --pad 0, or python walker.py. A file can define one Bot subclass; ibf run and ibf match find it, whatever its __main__ block says.

The game in brief

Two to four ships, black or white, move only on tiles of the other colour. A shot flips the tiles it crosses to the shooter's walkable colour and kills an enemy ship it reaches. Each ship holds a small clip of shots that refills over time; holding a shoot button charges a wider shot. In versus, a kill ends the round, and a match is first to three rounds. Players 1 and 2 (pads 0 and 1) are black and white; in two versus two, players 1 and 2 are the black team.

The tick

The game steps its simulation 60 times a second. After each step the framework publishes a snapshot, and run calls your bot:

  1. on_event(event) for each event since the last tick (a ship died, a round ended, ...);
  2. the matching callbacks: on_match_start(state), on_round_start(state, number), on_round_end(state, winner), on_match_end(state, winner);
  3. on_tick(state), whose Action becomes the pad's state until the next tick.

A bot that takes longer than a tick only misses ticks: the next call gets the newest state (state.skipped counts what it missed), never a queue of old ones. Outside play (menus, the pause menu, game over) the pad stays neutral, so a bot cannot wander the menus by accident; menus = True on the class lets its input through there too.

on_connect(info) comes first, once connected; on_disconnect(reason) last, when the game closes. An exception in a callback is logged and costs that tick, not the bot.

While it runs, a bot has: self.state (the newest state), self.log (a logger that writes to your terminal and to the game's console as bot:<name>), self.input (timed presses), self.draw, self.stats, self.game (commands), self.controls and self.pad.

The state

State is a read-only view of one snapshot. The main parts:

state.tick, state.time the simulation step's number, and the round's clock in seconds
state.phase, state.in_match Phase.MENU, Phase.PLAYING, Phase.ROUND_OVER, ...; whether a match is on
state.me, state.enemies, state.nearest_enemy() your ship (the pad's player), the living ships of the other colour
state.ships, state.players every ship; the ships by player index
state.projectiles, state.pickups shots in flight; pickups on the board
state.pickup_spawns, state.spawn_markers where pickups come next, and in how long; in arcade, where enemies are about to appear (1.5 s ahead)
state.board the tiles, and path helpers (below)
state.match the map, its wrapping, rounds to win, the round's state
state.events what happened since the last tick

A Ship has pos and velocity (in tiles, the board's centre at 0, +y up), tile, color, team, alive, ammo and max_ammo, fast_ammo (red shots), charging, score, and the power-up timers. A Projectile has pos, dir, speed, kind, owner and team. Coordinates are Vec2 (+, -, *, length, distance, normalized).

The board's tiles are (x, y) pairs counted from the lower left. Its helpers know the rules:

board.tile(x, y) Tile.WHITE, Tile.BLACK or Tile.NONE (a hole, or off the board)
board.walkable(tile, color) whether a ship of that colour may stand there
board.next_step(a, b, for_color) the first step of a shortest walk from a to b, or Dir.NONE
board.clear_lane(a, b) the direction from a to b when a shot from a reaches b, or None
board.raycast(tile, dir) the tiles a shot from tile crosses
board.array the tiles as a NumPy array, row 0 at the bottom (with NumPy installed)

On a map that wraps (state.match.wrap), ships and shots leave one edge and come back on the other side; the helpers follow that.

The Python API reference lists every name, and schema.md every field of the snapshot underneath.

Actions

on_tick returns an Action: the pad's state for this tick, in game terms.

Action(move=Dir.UP_LEFT)  # 8-way movement at full speed
Action(move=Vec2(0.3, -1.0))  # any direction
Action(move=Dir.LEFT, shoot=Dir.UP)  # a held shot: holding charges it
Action.idle()  # nothing pressed

A shot is up, down, left or right. Since an action is held, a quick shot needs a press and a release: the input sequencer does that over the next ticks, on top of the action you return.

self.input.tap(Dir.RIGHT)  # a quick shot (held for min_press_ticks, then released)
self.input.charge(Dir.UP, ticks=40)  # hold to charge, then release
if not self.input.busy:
    ...  # nothing waiting

Actions map onto the game's controls as configured ([controls], see Configuration); Action.raw(Pad(...)) sets the pad's buttons and sticks directly.

Drawing over the game

self.draw is the tick's drawing, shown over the board until the next tick replaces it. World coordinates are tiles, like the state's.

self.draw.line(me.pos, foe.pos, "#ff3355", width=2)
self.draw.circle(me.pos, 0.6, "cyan")
self.draw.tile(x, y, (1, 0, 0, 0.4), fill=True)  # a tile, as (r, g, b, a) in 0-1
self.draw.path(tiles, "lime")  # through tile centres
self.draw.text(foe.pos, "target", "white", size=14)
self.draw.heatmap(danger, palette="inferno", alpha=0.35)  # a board-sized grid of numbers
self.draw.screen.text((12, 40), f"tick {state.tick}")  # in pixels

Colours are "#rrggbb", "#rrggbbaa", CSS names or (r, g, b[, a]) tuples. The drawing is cleared before each on_tick; self.draw.retain = True keeps it. F4 hides the panels; overlay.drawings off in the console hides every bot's drawings.

Parameters and stats

Parameters become controls in the overlay's Bots panel, so you can tune a bot while it plays:

class Hunter(Bot):
    aggression = Param.float(0.6, 0.0, 1.0, help="how eagerly to close in")
    show = Param.bool(True)
    style = Param.enum(["calm", "wild"], "calm")

    def on_param_changed(self, name, value):
        self.log.info("%s is now %s", name, value)

Each reads as its value (self.aggression). Param.int, Param.button() and Param.color() exist too. Stats are numbers or short texts the panel shows; a plotted one gets a sparkline:

self.stats["distance"] = Stat(round(d, 1), plot=True)
self.stats["mode"] = "hunting"

Controlling the game

self.game runs console commands and drives the menus (the Navigator):

self.game.start_match("versus", players=1, map=0)
self.game.quit()  # back to the main menu
self.game.command("status")  # any console command; `help` lists them
self.game.pause()
self.game.step(1)
self.game.resume()

Each blocks until done. While ibf match runs a match it holds the match lock, and the bots' commands that change the game are refused.

A bot's process

A bot is a process of its own, in any Python (32- or 64-bit), with any packages: NumPy, PyTorch and so on. A crash or a hang in it never stops the game; its pad goes neutral within a quarter of a second. Several bots can play at once, one pad each (up to four). Lower down, ibf.connect() gives a Client for tools that only read the state; see the API reference.

A bot can also play a game on another machine, or from Linux, over TCP: ibf run mybot.py --url tcp://gamehost:47800 --token <token> (or the environment variables IBF_URL and IBF_TOKEN), when that game listens ([bridge.tcp] in the configuration). Nothing in the bot changes. The Linux wheel (ibf-<version>-py3-none-linux_x86_64.whl) speaks TCP only; the arena's bot containers use it (the arena).

One thing costs more over TCP: the calls that read the host's state between ticks ask the host first, a round trip (up to a few milliseconds over a network), so that they are as up to date as over shared memory: client.pad, client.pads(), client.run_mode, client.sim_paused, client.controls and client.info. Read them once, or once a second, not every tick: the snapshot itself (state) and self.controls carry what a tick needs.