Docs

Get started

Quickstart

Four steps to run a match locally.

  1. Clone

    git clone https://github.com/strakam/generals-bots.git
    cd generals-bots
  2. Install

    Two ways to install — pick one:

    Option A — virtual environment (recommended). Installs the engine into an isolated .venv, so its dependencies can't clash with other Python projects on your machine. Choose this if you work on more than one Python project — which is almost everyone.

    python -m venv .venv
    source .venv/bin/activate     # Windows: .venv\Scripts\activate
    pip install -e .

    Re-run source .venv/bin/activate in any new terminal to get back into the env.

    Option B — direct install. A single command into your current Python. Fewer steps, but the engine's dependencies mix with your global packages and can cause version conflicts. Fine for a throwaway machine or an already-isolated container.

    pip install -e .
  3. Copy a starter, edit the strategy

    cp -r competition/agents/expander_python competition/agents/my_bot
    # open competition/agents/my_bot/agent.py and edit Agent.act()

    The starter is a working baseline that expands into neighboring tiles. Only the body of one function needs to be replaced.

  4. Play a match against the baseline

    python competition/matchup.py \
      competition/agents/my_bot/run.sh \
      competition/agents/expander_python/run.sh --gui

    The --gui flag opens a Pygame window and renders the match so you can watch it play out — great for debugging, but slower because of the rendering. Drop --gui to run headless: with rendering disabled the match finishes much faster, which is what you want for quick iteration or running many games. Add --mode competition to play the exact competition ruleset — rectangular maps, castles, fog of war, and deathtouch.

Bot evaluation interface

A bot communicates with the game engine using stdin to read the board and stdout to write an action. The simplest bot passes every turn. Here is an example:

import sys

# Handshake: one line at startup → "player_id H W"
_, H, W = map(int, sys.stdin.readline().split())

while True:
    scalars = sys.stdin.readline()
    if not scalars: break              # EOF means game over
    for _ in range(3 * H):             # three grids of H lines each
        sys.stdin.readline()
    sys.stdout.write("1 0 0 0 0\n")    # pass
    sys.stdout.flush()                 # ← do not forget this
#include <cstdio>

int main() {
    int pid, H, W;
    scanf("%d %d %d", &pid, &H, &W);
    int t, ml, ma, ol, oa, x;
    while (scanf("%d", &t) == 1) {           // EOF means game over
        scanf("%d %d %d %d", &ml, &ma, &ol, &oa);
        for (int i = 0; i < 3 * H * W; ++i) scanf("%d", &x);
        printf("1 0 0 0 0\n");                // pass
        fflush(stdout);                       // ← do not forget this
    }
}
use std::io::{self, BufRead, Write};

fn main() {
    let stdin = io::stdin();
    let mut lines = stdin.lock().lines();
    let hs = lines.next().unwrap().unwrap();   // handshake "player_id H W"
    let H: usize = hs.split_whitespace().nth(1).unwrap().parse().unwrap();
    let out = io::stdout();
    let mut o = out.lock();
    while let Some(Ok(_scalars)) = lines.next() {   // EOF means game over
        for _ in 0..3 * H { lines.next(); }
        writeln!(o, "1 0 0 0 0").unwrap();          // pass
        o.flush().unwrap();                         // ← do not forget this
    }
}

The remaining work is to parse the grids into a suitable data structure and select a move.

Observation format

Once per turn the engine sends 1 scalar line and 3 grids of H rows × W columns each. All values are whitespace-separated integers.

turn  my_land  my_army  opp_land  opp_army
<H lines of W ints>     # type:  what's on each cell
<H lines of W ints>     # owner: who owns each cell
<H lines of W ints>     # army:  army count on each cell

Type codes

0  fog                   (cell not visible)
1  plain                 (empty passable cell)
2  mountain              (impassable)
3  castle                (produces army for its owner)
4  general               (a player's home base)
5  structure-in-fog      (castle or mountain you can't see clearly)

Owner codes (perspective-relative)

0  neutral / unknown
1  ME      ← you are always "1"
2  OPPONENT
Note: the owner grid is expressed from the bot's own perspective. The bot is always 1 and the opponent is always 2, regardless of player index. No conversion is required.

Action format

One line per turn, written to stdout (and flushed):

kind row col dir split
  • kind0 to move, 1 to skip the turn, 2 to build a castle.
  • row, col — the cell you move from, or build on. Must be yours; a move also needs army > 1 there.
  • dir0 up, 1 down, 2 left, 3 right. Ignored for pass and build.
  • split0 moves the whole army but one; 1 moves half of the army (floor). Ignored for pass and build.

Examples:

0 3 5 1 0     # move the whole army from (3,5) down
0 7 2 3 1     # move half of the army from (7,2) right
2 4 4 0 0     # build a castle on (4,4) — needs the price in army on that cell
1 0 0 0 0     # pass

Invalid moves (out of bounds, into a mountain, source not yours, source has ≤ 1 army) are silently treated as a pass, and so are invalid builds (cell not yours, not plain land, price not met). No penalty beyond the wasted turn. Castle prices and build mechanics are on the Rules page.

Timing: reply within the 150 ms wall-clock limit (the first move gets 10 seconds so a model can load). A late, missing, or malformed reply is replaced with a pass and adds one fault; stale replies are discarded, never consumed on a later turn. Reaching 50 faults in one game forfeits it, while a bot process that crashes or exits forfeits immediately. Invalid but correctly formatted game actions are silent passes and do not add a runner fault. Keep replies fast and always write exactly one line per observation.

Training interface

For training a reinforcement-learning agent rather than a handcrafted heuristic, the same repository provides a vectorized environment built for fast iteration:

  • JAX environment — jittable, with batched stepping and auto-reset for high-throughput rollouts (observations come out as arrays; convert with numpy.asarray if the rest of your stack is NumPy).
  • Same observation semantics as evaluation — the training environment exposes the same grids and scalars provided over stdio during matches. One difference to plan for: training pools pad every board to a fixed 21×21 with mountains, while graded matches are exact 18–21 rectangles (the handshake gives you the true H and W).

The environment API, usage examples, and training scripts are documented in the repository.

View the generals-bots repo

Submit

A submission is a zip of a directory. The only required file is run.sh, which is executed to start the bot.

my_bot/
  run.sh         # required: launches your bot
  build.sh       # optional: one-time compile step at intake
  ... your code ...

An example run.sh for each language:

run.sh — Python

#!/usr/bin/env bash
exec python -u main.py

run.sh — C++ (compiled once by build.sh)

#!/usr/bin/env bash
# build.sh did: g++ -O2 -std=c++17 -o agent main.cpp
exec ./agent

run.sh — Rust (compiled once by build.sh)

#!/usr/bin/env bash
# build.sh did: cargo build --release
exec target/release/agent

Upload from your dashboard. We compile, sandbox, and start running matches within a couple of minutes.

Environment

The exact runtime your bot executes in. Everything on this page is verified against the live sandbox image, not aspirational — if it's listed, it works.

No network, ever. Both build.sh and the match itself run fully offline. You cannot pip install, apt install, cargo fetch, or download anything at build or run time. Everything your bot needs must either be baked into the image (tables below) or shipped inside your zip (vendoring).

Languages & toolchains

LanguageToolchain
Python CPython 3.12.10 — scientific stack below
C++ g++ 12.2 (-std=c++17/20), make, cmake, Eigen (/usr/include/eigen3)
Rust rustc / cargo 1.97 (stable)

Other languages work too — ship a self-contained binary or a vendored runtime in your zip, started by run.sh. The engine repository's expander_python / expander_cpp / expander_rust baselines are complete working examples for the three first-class languages.

Python libraries (pre-installed)

Import any of these directly — no install step, no wheels to ship. To mirror the sandbox locally, install the pinned versions from the engine repo's competition/requirements.txt:

pip install -r competition/requirements.txt
LibraryVersion
numpy2.4.6
scipy1.18.0
pandas3.0.5
scikit-learn1.9.0
torch2.13.0 (CPU)
jax0.11.0
numba0.66.0
networkx3.6.1
safetensors0.8.0
gymnasium1.3.0

Not installed: TensorFlow, CUDA builds of anything, torchvision/torchaudio, RL training frameworks (stable-baselines3, RLlib). The match container runs inference — train on your own hardware and submit the trained weights.

Compute & limits

  • CPU only, one dedicated core per bot. No GPU at match time — use CPU builds of ML libraries.
  • Memory: hard 2 GB cap per bot. Exceeding it crashes your process (a forfeit); it can never affect your opponent.
  • Size: zip ≤ 50 MB; unpacked ≤ 512 MB and ≤ 10,000 files.
  • $HOME is your own writable directory — safe for ~/.cache, ~/.cargo, on-disk JIT caches, etc.

Bringing your own dependencies

Because there is no network at build time, extra dependencies ship inside the zip and install offline in build.sh:

  • Python — put wheels in ./wheels/ and run pip install --no-index --find-links=./wheels -r requirements.txt. Wheels must be Linux (manylinux), CPython 3.12. Mind the 50 MB zip cap — this is exactly why torch is pre-installed rather than vendored.
  • Rust — run cargo vendor locally, include the vendor/ directory and .cargo/config.toml, then cargo build --release --offline.
  • C++ — header-only libraries drop straight into your tree (Eigen is already provided).

Startup cost

import torch costs ~2.3s and the full scientific stack ~3s of your 10-second first-move budget; a JAX or torch model's first forward pass triggers JIT compilation then, too. Warm what you can in build.sh — but note an in-memory JIT cache does not survive from build.sh to run.sh, so persist caches to disk inside your tree.

The competition ruleset

Matches run under one pinned ruleset — locally it's --mode competition on the runner:

  • Rectangular maps, each side 18–21 — read the size from the handshake, never hard-code it.
  • Fog of war — like the original generals.io, you see only the cells next to tiles you own, so type codes 0 (fog) and 5 (structure-in-fog) do appear; everything beyond your vision is hidden.
  • No neutral castles — you build your own with the 2 r c 0 0 action (pricing on the Rules page).
  • Deathtouch from turn 800, and a 1200-turn cap — see Rules for the exact semantics.

Common pitfalls

  • Flush stdout after every action. Pipes are fully buffered by default. Without a flush, the runner does not receive the move and the game deadlocks. (python -u or sys.stdout.flush(); fflush(stdout); .flush().)
  • EOF means game over. stdin closes when the match ends — there is no "winner" line. An empty result from readline() indicates the bot should exit.
  • Owner is relative to the bot. Owned cells are 1 and opponent cells are 2, regardless of player index. Remapping is unnecessary.
  • Invalid moves do not crash. They are treated as a silent pass. Local testing is recommended to avoid wasted turns.