C and C++ bots

Any language that can call a C function can play: the Python package itself is a wrapper over ibf_client.dll. C++ has ibf.hpp, a header-only C++20 wrapper with exceptions and spans. A bot is a program of its own, 64-bit or 32-bit.

The SDK

It comes two ways: inside the Python package (ibf new --lang cpp points at it), and as ibf-sdk-<version>.zip. Either holds:

include\ ibf_client.h (the C API), ibf.hpp (C++20), ibf_plugin.h (plugins), ibf_schema.h (the state's structs)
lib\x86, lib\x64 ibf_client.dll and its import library
lib/linux-x86_64 libibf_client.so: the client for Linux, which speaks TCP only (below)
cmake\ a CMake package: find_package(ibf CONFIG) gives ibf::client
examples\ (zip) cpp_bot and the hello plugin

You need Visual Studio 2022 or its Build Tools (the C++ workload), and CMake, which comes with Visual Studio.

A bot from the template

ibf new mybot --lang cpp
cmake -S mybot -B mybot\build -DCMAKE_PREFIX_PATH="<the SDK folder ibf new printed>"
cmake --build mybot\build --config Release
ibf match mybot\build\Release\mybot.exe idle

ibf run and ibf match start a .exe like a .py, and tell it its pad and the game through the environment (IBF_PAD, IBF_INSTANCE, IBF_BOT_NAME, IBF_FORCE_PAD; over TCP IBF_URL, IBF_TOKEN and IBF_WAIT), which ibf::RunEnv::read reads. The build copies ibf_client.dll next to the program.

Over TCP, and on Linux

ibf_connect("tcp://host:47800", ...) reaches a game that listens on TCP ([bridge.tcp] in the configuration), on this machine or another; the handle works like one over shared memory, every call the same. ibf_connect_opts.token carries the game's token, or one pinned to a pad (then the pad is bound already: ibf_pad says which), and connect_timeout_ms keeps trying a game that is not listening yet, or a token not pinned yet. Over TCP a client blocks rather than spins, and each call that reads the game's state (ibf_pad, ibf_get_run_mode, ...) asks the game first, a round trip, so that it is as up to date as over shared memory.

On Linux the client speaks TCP only: libibf_client.so in the SDK and the Linux wheel, or built from a checkout with cmake --preset linux-client && cmake --build --preset linux-client (build/linux-x64/bin, with the C++ example bot). A Linux bot is built the same way with the SDK's CMake package, and finds libibf_client.so next to itself (the template sets the run path to $ORIGIN). That is how a native bot plays in the arena's bot containers (the arena): the program named bot, its client beside it.

ibf.hpp

#include "ibf.hpp"

int main() {
    const ibf::RunEnv env = ibf::RunEnv::read(0);           // what ibf run, ibf match and the arena set
    ibf::Client client = ibf::Client::connect({.name = env.name_or("walker"),
                                               .instance = env.instance,
                                               .connect_timeout_ms = env.wait_ms > 0 ? env.wait_ms : 10000,
                                               .url = env.url,
                                               .token = env.token});
    if (env.pad >= 0 && client.pad() < 0) client.bind_pad(env.pad, env.force);  // IBF_PAD=none: watch only
    const int pad = client.pad();                           // a pinned token's pad comes bound
    const ibf_controls controls = client.controls();
    for (;;) {
        const std::optional<ibf::Snapshot> s = client.wait(std::chrono::milliseconds(100));
        if (!s || pad < 0) continue;                         // no tick within 100 ms, or no pad
        const ibf_ship* me = s->player(pad);
        if (!s->in_match() || me == nullptr) {
            client.set_pad(ibf::Pad::neutral());             // menus are not the bot's
            continue;
        }
        client.set_pad(ibf::Pad::action(ibf::Dir::right, ibf::Dir::none, controls));
    }
}
  • Client connects (Client::connect), waits for ticks (wait, latest), holds a pad (bind_pad, set_pad), runs console commands (command, which throws CommandError with the game's answer), draws (draw()), and declares parameters and stats. It disconnects when destroyed.
  • Snapshot reads the state in place: tick(), phase(), in_match(), ships(), projectiles(), pickups() (as std::spans of the schema's structs), player(pad), enemies(me), board(), match(), events_since(seen). It is valid until the client's next wait.
  • Board: tile, to_tile, center, walkable.
  • Pad: Pad::action(move, shoot, controls) maps game actions through the game's controls; Pad::stick, press for raw input.
  • Errors are ibf::Error exceptions carrying the ibf_status. When the game closes, wait throws with IBF_HOST_GONE.

examples\cpp_bot\main.cpp is a complete bot (a port of the Python hunter): it draws its aim, publishes a stat and declares a parameter.

The C API

ibf_client* c = NULL;
ibf_connect_opts opts = {sizeof(opts), "my-bot", 0, IBF_CLIENT_CAPS_TICKS};
/* "tcp://host:47800" for a game over TCP; opts.token for its token */
if (ibf_connect("shm://", &opts, &c) != IBF_OK) { puts(ibf_connect_error()); return 1; }
ibf_bind_pad(c, 0, 0);
for (;;) {
    const ibf_snapshot* s = NULL;
    ibf_status st = ibf_wait_tick(c, 100, &s);
    if (st == IBF_TIMEOUT) continue;
    if (st != IBF_OK) break;              /* the game closed */
    ibf_pad_input in = {0};
    in.lx = 32767;                        /* full right */
    ibf_set_pad(c, &in);
}
ibf_disconnect(c);

Every call returns an ibf_status; ibf_last_error(c) says why one failed. The C API reference has every call, and schema.md every field of ibf_snapshot. Other languages (Rust, C#, Zig, ...) can wrap the same DLL.