A competitive AI for Cambridge Battlecode 2026, a real-time strategy game where each robot runs as its own isolated Python process with a 2 ms CPU budget per round and no shared memory. Robots harvest titanium and axionite on a 20×20–50×50 grid, build out a conveyor logistics network, defend their core, and race to destroy the enemy's before round 2000.
The hard part isn't the strategy — it's that there is no central controller. Every unit sees only its local vision, thinks independently inside a few milliseconds, and the fleet has to behave coherently anyway. This bot solves that with a decentralized priority-arbitration architecture, a typed gossip protocol layered over a 1-marker-per-round communication primitive, and a single-pass perception sweep that feeds dozens of behaviors.
🏆 Result: Top 8 in the international qualifier (of 322 teams), peaking at rank 6 of 587 teams.
- Decentralized priority arbitration. Each builder evaluates ~30 independent policies (harvest, defend, rush, rambo-attack, heal, foundry logistics, …) every round and runs the highest-scoring one. A two-tier loop separates cheap interrupts (heal, retreat, react to an enemy) from the time-budgeted main decision. No state machine — behavior emerges from scoring under a shared world model.
- Personalities shape the fleet. The core assigns each spawned bot a tactic
(
STANDARD/RUSHER/PATROL/HEALER) which reweights its policy scores, so a single codebase produces specialized, complementary roles without per-unit code paths. - Gossip: a typed protocol over one marker per round. Units can only emit a single u32 marker per round, so messages are packed into envelopes through per-type codecs (claims, danger, explore hints, tactic assignments, launch requests, ore/map info). A scored outbox picks the single most valuable thing to say each turn; an inbox decodes neighbors' markers into queryable intel and claim arbitration (epoch + owner tag) keeps two bots from fighting over the same ore.
- One perception sweep, many behaviors. A single time-bounded BFS from each bot feeds ~20 collectors that simultaneously surface frontier tiles, ti/ax ore, enemy buildings, vulnerable harvesters/foundries, heal targets, splitter sites, wall-hops, and more — instead of N separate scans burning the CPU budget.
- Infrastructure-aware pathfinding. Several specialized algorithms share pre-allocated, pooled arrays: BFS frontier exploration, A* that lays conveyor infrastructure backward from a target toward the core, A* walking, and an offensive "rambo" A*.
- Enemy-core prediction from symmetry. Maps are reflection- or rotation-symmetric, so the bot infers the enemy core's location before ever scouting it and starts pressuring early.
- A custom optimizing compiler for the bot. Because every microsecond counts under the
2 ms budget, submissions are run through a bespoke AST-based source-to-source preprocessor
(
scripts/preprocess.py) with passes for function inlining (@inline), cross-module constant folding, attribute-lookup hoisting ("shadowing"),==→isrewriting for singleton/enum compares, guard propagation, dead-branch elimination, debug stripping, and optional minification — keeping the source readable while the shipped bot stays fast. - Built for the budget. GC disabled, object pools sized to the max map (2500 tiles),
shadowed hot-loop locals, a
@profile_runharness, and explicit per-round time deadlines — all to stay under 2 ms/unit and 1 GB/bot.
spawn ─▶ core assigns a tactic via gossip ─▶ personality weights chosen
│
each round, per builder: ▼
1. update local vision into the shared MapTile grid
2. ingest neighbors' gossip markers → intel + claims
3. interrupt check: any high-priority reaction? (heal, retreat, deactivate) ── yes ─▶ act
4. otherwise: run one budgeted BFS sweep → ~20 collectors gather candidate targets
5. score every policy × personality weight → run the winner's action
6. flush gossip: emit the single highest-value marker this turn
The core runs its own phased spawn logic (start → join → normal): a scripted opening, then economy-gated and flow-paced spawning, with emergency healer spawns on HP loss and defensive patrol spawns when it detects it's outnumbered in vision.
bots/starter/ The bot (entry point: main.py)
main.py Per-unit dispatch → Core / Builder / Gunner / Sentinel / Launcher
personalities.py Tactic → policy-weight vectors
entities/ Per-unit-type handlers (core, builder, turrets)
policies/ ~30 policies: attack/ defense/ harvest/ + utility
gossip/ Typed messaging over markers: codecs, envelope, store, queries
game_map/ Map model + pathfinding (BFS, A* build/walk/rambo) + collectors
infra/ Profiler, constants, params, inlining helpers
bots/baseline/ Last known-good snapshot, used as the test opponent
maps/ Match maps (.map26)
scripts/preprocess/ AST-based source-to-source optimizer (build step)
engine/, testing/ Game engine + Rust replay parser
quicktest.sh, test.sh Match-based regression tests
Requires Python and uv.
uv sync # install dependencies
uv run cambc run starter starter # run a match (first map in maps/)
uv run cambc run starter starter --watch # ...and open the visualizer
uv run cambc run starter baseline maps/default_large1.map26 --seed 42Config (bot/map directories, replay path, seed) lives in cambc.toml.
Changes are validated by playing the working bot against the baseline snapshot across the
map pool:
./quicktest.sh # single fast match (small map, seed 1)
./test.sh # full suite: all maps × multiple seedstest.sh exits 0 if the new bot wins the majority of matches, 1 if the baseline wins.
The bot is developed as ordinary, readable Python and compiled to a faster equivalent before submission by an AST-based source-to-source preprocessor:
python scripts/preprocess.py bots/starter bots/starter_pp --force # optimize → output dir
python scripts/preprocess.py --selftest # run the pass fixturesPasses run in order — strip-debug → inline → cross-module constant fold → guard-propagate →
shadow → fold — each independently toggleable via --no-<stage>, with an optional
--obfuscate minification step. Every pass is semantics-preserving; point cambc.toml at the
preprocessed directory and re-run the tests to confirm no regression.
- GC is disabled for performance (
gc.disable()inmain.py) — do not create reference cycles; they will leak under the 1 GB memory cap. extract_maps.pyrecovers the embedded.map26from a replay. Core positions come from the embedded map seed, not from replay spawn events.- Tunable constants live in
params.py; profiling output goes tocambc_profile.txtvia the@profile/@profile_rundecorators.
Full game spec: https://docs.battlecode.cam/spec/overview · API reference: https://docs.battlecode.cam/api/controller
