Skip to content

Scr1pting/cambridge-battlecode-26

Repository files navigation

Cambridge Battlecode 2026

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.

img.png


Highlights

  • 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"), ==is rewriting 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_run harness, and explicit per-round time deadlines — all to stay under 2 ms/unit and 1 GB/bot.

How a turn works

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.


Repository layout

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

Running it

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 42

Config (bot/map directories, replay path, seed) lives in cambc.toml.

Testing

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 seeds

test.sh exits 0 if the new bot wins the majority of matches, 1 if the baseline wins.

Build pipeline

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 fixtures

Passes 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.


Notes for contributors

  • GC is disabled for performance (gc.disable() in main.py) — do not create reference cycles; they will leak under the 1 GB memory cap.
  • extract_maps.py recovers the embedded .map26 from a replay. Core positions come from the embedded map seed, not from replay spawn events.
  • Tunable constants live in params.py; profiling output goes to cambc_profile.txt via the @profile / @profile_run decorators.

Full game spec: https://docs.battlecode.cam/spec/overview · API reference: https://docs.battlecode.cam/api/controller

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors