This folder is the bot we submitted as our final version for UNSW Battlecode 2026 "Abyss". It is released exactly as
submitted. Only comments changed. The non-compiled history files were dropped, and the header line of the two weight
files was rewritten. Every code token, macro, #if block and string that reaches the compiler is the same as in the
submitted build. The one other edit is the message of an #error inside a disabled #if V10K_KING_VETO group (it
named an internal design note); the preprocessor skips that group, so the compiler never sees it. The WASM that
unswbc 1.2.9 builds from this folder with --sandbox is bit-for-bit the submitted one (see
Determinism).
For a design walkthrough written from this code, see docs/policy.
The bot is a standard unswbc C++ project (bot.toml with language = "c++", entry point main.cpp, header-only
modules, no helper.hpp, no external libraries). From a unswbc workspace (a folder with maps/; see
bots/README.md):
cp -R path/to/repo/bots/abyss ./abyss # skip this line when the workspace is the repository's own bots/ folder
unswbc run maps/arena.map abyss starter # native build, writes replays/*.replay
unswbc run maps/arena.map abyss starter --sandbox --no-replay # the judge's sandbox: WASM, CPU metered in pointsunswbc run --sandbox caches the compiled WASM in ${XDG_CACHE_HOME:-$HOME/.cache}/unswbc/wasmbots/<bot>-<hash>.wasm.
The key covers only the .c/.cc/.cpp files, so a change to a .hpp file is not picked up. Delete the cached file
before running again:
find "${XDG_CACHE_HOME:-$HOME/.cache}/unswbc/wasmbots" -name 'abyss-*.wasm' -deleteTo debug outside the toolkit, build it natively (one translation unit):
c++ -std=c++20 -O2 -o abyss_native main.cppCompile-time switches can be overridden with -D for experiments (for example -DBC_QGATE=0). This works for every
switch whose default is wrapped in #ifndef. Switches that ablation.hpp sets unconditionally, or redefines in its
final overrides, have to be edited in the file. Any change gives a different binary from the released one.
Every dragon is its own process with no shared memory. A child created by a split starts with an empty world model.
It rebuilds its knowledge from its own 7x7 view and from its teammates' sonar. Each turn, in main.cpp:
- Observe (
world.hpp,board.hpp): update the world model from vision only. It covers walls and portals, pearl beds and their refill gaps, map symmetry (mirrored beds and kelp), our own body, other dragons, enemy sightings and the two queens (unit ids 0 and 1). - Hear (
comms.hpp,region.hpp): decode the tagged 64-bit sonar facts and update the relay table. Facts include positions, the longest dragon, enemies, heat, and the map network's portal gates, closed rooms and trap set, which also feed its danger layer. (The protocol defines a DROPS fact for death drops; this build never sends it, because the scavenger planner is off.) - Parameters (
tuned_params.hpp,common.hpp): pick the tunedParamsfor the map group the dragon believes it is on (judged online, never by map name) and the game phase. - Decide (
policy.hpp):- Plans: tactics, local team (
team.hpp), targets, the opening expansion, the reserve, harvest, portal probes, squads, feeding and regicide. - Then an anytime lookahead over the dragon's own move sequences, deepened until the 75M-point budget is used.
- Two small imitation-learned networks add priors to the root moves and to the split trigger:
rl_net.hppfor the queen process before round 200, andr4net.hppfor every other dragon.
- Plans: tactics, local team (
- Queen safety (
qsk.hpp): our queen's landing is picked by the Queen Safety Kernel's hard tiers. The search values only order moves inside a tier. Teammates follow friend discipline around her (room guard, breathing room, last exit). - Guards and output (
main.cpp,io.hpp): last legality checks (the queen multi-step check, the unit slot, a legal split, a payable sprint). The action and up to four sonar rays go out in one write, quiet before round 300 except for event facts. The 95M fallback runs last.
The king end-game library (v10k.hpp, king/) and the local fight search (fight.hpp) are compiled in but never
change a decision in this build (see Off or inactive).
| file | contents |
|---|---|
bot.toml |
unswbc project file (C++; every source in the folder is included) |
main.cpp |
entry point: the per-turn loop, parameter selection, last legality guards, output and the 95M checks |
common.hpp |
compile-time switches with their defaults, Params (every tunable number), the turn budget and fb95 |
ablation.hpp |
the final build's switch settings and the overrides that define the submitted configuration |
tuned_params.hpp |
generated: tuned Params per (map group, game phase), applied by applyTuned |
io.hpp |
stdin line reader, turn parser and the single buffered write per turn |
board.hpp |
torus geometry, tile edges (kelp, portals), dead-end detection |
nav.hpp |
time-aware BFS and flood fill on the torus (own body frees from the tail) |
world.hpp |
the world model of one dragon process: map knowledge, beds, symmetry, sightings, sonar facts, queens |
comms.hpp |
sonar protocol: fact types, bit layouts, the tag that authenticates our frames, relay planning |
region.hpp |
map network state (BC_MAPNET): portal gate states (probe / cleared / trap / dead) and danger; its incremental room graph (BC_MAPNET_ROOMS) is off |
team.hpp |
local teams: membership, leaders and target assignment among dragons that see each other |
policy.hpp |
the decision policy: roles, plans, the anytime lookahead, scoring, splits, probes, feeding, priors |
qsk.hpp |
the Queen Safety Kernel: reach model, landing tiers T1-T5, margins (its growth arm, BC_QUEEN_GROW, is off) |
fight.hpp |
local fight search (compiled, weights 0 in the final parameters, so inactive) |
rl_features.hpp |
observation features shared by the training featurizer and by play |
rl_net.hpp |
opening prior network (policy / value), evaluated in-bot with int16 weights |
rl_weights.hpp |
generated weights of the opening prior network |
r4net.hpp |
move-prior network of the non-queen dragons (move, multi-step and split heads) |
r4_weights.hpp |
generated weights of the move-prior network |
v10k.hpp |
adapter between the search core and the king end-game library (inactive: V10K_R0 = 501) |
king/core.hpp |
king library entry header and per-turn order |
king/proto.hpp |
king library part 1: its sonar frames, checksum and blackboard |
king/crown.hpp |
king library part 2: who is king (crown claims, heirs) and the mode machine |
king/sigma.hpp |
king library part 3: the lead / lag estimate |
king/hunger.hpp |
king library part 4: the king's growth target over the end game |
king/feed.hpp |
king library part 5: feeding roles and the capped cull |
This is a summary of the effective macro values of the submitted build, read with c++ -std=c++20 -E -dM main.cpp
(that dump also lists BC_BASE_MAX, BC_DIM_MAX, BC_SCALE_MAX and BC_STRING_MAX: POSIX limits from <limits.h>,
not bot switches).
Defaults live in common.hpp and ablation.hpp. The overrides at the end of ablation.hpp set the final values. The
complete list, every macro with its status and file:line pointers, is
docs/policy/switches.md. A switch set to 1 does not always act: some modules are
switched off by a tuned run-time value, shadowed by another module, or only label a group of tuning knobs. Those are
listed separately below.
| switch | meaning |
|---|---|
search core (BC_TUNED) |
anytime lookahead with hand-written value and danger terms; every knob tuned per (map group, phase) |
| the swarm (no switch of its own) | until swarmUntil every dragon of 4+ that is not a king splits in half when the split is safe |
BC_MIRROR |
map symmetry: mirrored beds, fountains and kelp are known before they are seen; a 1.23x stronger pull toward fountains known only from their mirror image (wMirrorHot) |
BC_FAR_EXPLORE |
long-range pull to the nearest unexplored frontier tile |
BC_HEAT |
presence heat map of ours and theirs on a 5 x 4 sector grid: no feeding and no idle bed where the enemy holds the sector, HEAT facts, the kernel's fog. Its spread-out pull is off (wHeatSpread 0) |
BC_ENGAGE |
attack / hold / retreat from a head-on trade value model fitted on ladder replays |
BC_V9_EKING |
early king: from a tuned round (200) our longest dragon, if at least 8 long, keeps its segments and the others leave its food |
BC_V9_FINISH |
from round 300 with 40+ units, each free dragon of length <= 8 heads for the nearest enemy head seen or heard within 6 rounds |
BC_TRAPPED_PORTAL, BC_EAT_NO_DISPERSE |
a doomed dragon tries an unpaired portal; eating steps are not taxed by dispersal |
BC_RL_PRIOR |
opening prior network: queen process only, rounds < 200, root weight wPrior 0.05, split at rlSplitP 0.4 |
BC_QUIET |
quiet opening: no routine sonar before round 300 (event facts still go out) |
BC_PORTAL2 |
portal tactics: a closed, poor region is left through its unpaired portals (hungry probes). The one-guard-per-pocket rule is not part of this switch; it runs under Params::pocketOn (1, not tuned) |
BC_QUEEN |
the queen rule: our queen (id 0 or 1) decides round 500; she never trades and never sacrifices herself; no rule sends feeding to her (she is fed only when she happens to be the team's longest dragon); protection centres on her |
BC_QUEEN_QSK |
Queen Safety Kernel: her landing by hard tiers T1-T5 |
BC_QUEEN_QKILL |
non-queens take the enemy queen when in reach (QKILL) and guard ours from a later striker (QGUARD) |
BC_QUEEN_HUNT |
enemy-queen tracking and sharing, emergent hunters (regicide), free-step pursuit |
BC_QUEEN_ROOMGUARD |
friend discipline: a teammate's step within Chebyshev 2 of the queen that leaves her fewer than max(L, 2) + 2 reachable tiles costs 30 |
BC_QUEEN_EXITFIX |
QSK tier T5 ranks free landings by margins under enemy lengths calibrated on replays |
BC_QUEEN_NODEAD |
the queen's landings avoid dead-end tiles while another landing exists |
BC_QUEEN_BREATHE = 2 |
the queen keeps 2 free sides; teammates pay for stepping into the 8 tiles around her head |
BC_QUEEN_GROWFIRST |
before round 200, while shorter than 5, the queen eats first |
BC_QUEEN_KEEPAWAY |
among the landings QSK keeps, the queen takes the one farther by path from known enemy heads |
BC_QUEEN_BOXGUARD |
the queen's last free side is never taken by a teammate, not even as its trapped fallback |
BC_UNIT_SLOT |
a non-queen never takes the last unit slot (the queen keeps it for her split hold) |
BC_ECON_B0 |
economy guard: the queen's multi-step moves come only from QSK |
BC_ECON_DIP |
dead-end / fountain lanes: a short dragon dips in to eat and leaves by a reverse split |
BC_ECON_BED |
spawn clock: wait value, countdown completion (mirror beds share one countdown), unlock, no-block, idle-to-bed |
BC_HARVEST |
small fountain rooms behind a portal are harvested: eat inside, split the rear out through the portal |
BC_LONGEST_RESERVE = 3 |
from round 300 the team's longest non-queen dragon keeps every segment and eats first (eatBonus x2); the two nearest teammates leave it food |
BC_QGATE |
the queen never probes a portal; unsafe or unknown portal landings are walls for her |
BC_DEADPORTAL |
for every dragon, a portal whose known landing is a 1-tile room is a wall |
BC_MAPNET (FACTS, REGION, EXPLORE, TRAPSET; EVIDENCE = 2, VALUE = 1) |
shared map network: portal gate and room facts, inferred traps with evidence grading, a danger layer, bed-area VALUE facts |
BC_R4NET |
move-prior net for non-queen dragons: weight 0.02, all game (UNTIL 500), split trigger at P 0.4 before round 200 (SPLIT_ON; redundant with the swarm), net-gated free steps (SPRINT 6, SPRINTP 0.3, MAXSTEPS 2) with the cheap landing rule (LAND 1) |
BC_OPEN_EXPAND (W 0.4, GATE 28) |
opening expansion of non-queen dragons toward free bed areas, until round 120. The gate (bits 4, 8, 16) admits maps with <= 2 starting dragons and <= 700 tiles, dragons born later on maps <= 700 tiles, and any map <= 700 tiles; since bit 16 contains bits 4 and 8, in effect: every map of at most 700 tiles (W x H) |
BC_FALLBACK95 |
hard 95M-point CPU fallback (see below) |
End-game feeding uses the search core's one-king rule. It is enabled from round 350 (Params::oneKingFrom) but acts
only inside the feeding window, rounds 400-489 (feedRound 400, feedStop 490 in every tuned context): a non-queen
teammate that sees the king (the dragon named by the LONGEST facts), at least as long as itself, walks to it and dies
next to its head. The queen never feeds anyone, and no rule sends feeding to her (BC_QUEEN_FEED is 0): she is fed
only when she happens to be the dragon the LONGEST facts name.
| switch | status | what runs, and why the rest does not |
|---|---|---|
BC_FULL |
partly | the final push is off (finalFrom 500); its late reverse protection runs (dragons of 10+ from round 350) |
BC_KING |
partly | the longest-enemy record runs; its feeding block is off (kingOn 0) |
BC_SLAY |
partly | the longest-enemy record and EKING facts run; the slay planner is shadowed by the regicide planner (BC_QUEEN_HUNT) |
BC_SCAV |
partly | death drops are detected and feed the danger layer; the scavenger planner is off (wScav 0), so no DROPS facts are sent |
BC_DEADEND |
partly | dead-end tiles are computed (lane dips, NODEAD, the queen's gate); the dead-end veto in the search is off (deadEndOn 0) |
BC_V9_DENSITY |
inert | density cap on voluntary splits; kDensity 1 makes the cap the whole map |
BC_FORAGE_PATH |
inert | path distance for heard teammates, but its result is multiplied by remoteYield 1.0 |
BC_QUEEN_FLEE |
shadowed | a 2-3 step flee; its only call site needs the kernel off and BC_QUEEN_SAFE on |
BC_V9_TAIL |
off | count on a reverse split before the tail is tracked; tailOn 0 |
BC_V9_SCOUT |
off | opening scouts per unexplored sector; scoutUntil 0 |
BC_ROOMCAP |
off | room capacity for closed rooms; roomOn 0 in every tuned context |
BC_QUEEN_LOCKALL |
off | LOCK mode's no-trade scope; needs BC_QUEEN_MODES, which is 0 |
BC_COVER, BC_SWARM, BC_PHASED, BC_FINAL |
label | no code reads them; they name groups of knobs in the tuning tools' search space (the swarm itself always runs) |
| switch | meaning, and why it does not run |
|---|---|
BC_QUEEN_KING, _SPLIT, _SAFE, _ZONE, _FEED, _GROW |
early queen modules, explicitly 0 in ablation.hpp: superseded by QSK and the one-king rule |
king end-game library (v10k.hpp, king/) |
compiled, but V10K_R0 = 501 is past the last round, so only a few constant-time checks per turn run |
FightSearch (BC_FIGHT, fight.hpp) |
compiled, but wFight is 0 in every tuned context, so it never changes a decision |
BC_MAPNET_ROOMS |
the rooms of the known map, recomputed incrementally; no decision reads them |
BC_EXPAND_OPENING |
an opening "risk mode" for few-unit starts with food behind doors |
BC_BED_KEEP |
keep a fast bed and wait for its spawns |
BC_QUEEN_GROWUP (and BC_GROWUP_FEED) |
the queen grows on boxed maps, with teammates feeding her |
BC_V9_KING |
the late-king feeding module (replaced by the one-king rule) |
BC_OPEN_RISK |
the opening's small dragons contest food without yielding |
BC_R6 |
per-dragon map-class nets. Orphan: its headers are not shipped, so it must stay 0 |
BC_EXPAND |
idle dragons contest bed cores no teammate holds |
BC_QUEEN_FEED350 |
from round 350 teammates feed a short queen |
BC_ECON_TRACE |
economy trace tags in LOG lines (trace builds only) |
BC_T64_FIELD |
a stronger move prior in the field of a fast bed |
BC_TIER2 |
keep a second-tier bed when the fast beds are full |
BC_TRAPPOCKET |
trap pockets (a junction whose only way on is a trap) as walls |
BC_QSK_PATHHEAT |
QSK's "enemies about" test by path distance |
BC_QUEEN_YIELD |
teammates next to a cramped queen step away first |
BC_R7_KINDW_ON (and BC_R7_TOPB_ON) |
alternative weightings of the move prior |
BC_R4_NOISE |
control setting: a pseudo-random prior in place of the net |
BC_SPRINT_TARGET |
free steps toward a contested fresh pearl |
BC_R5_GATE (and the other BC_R5_* variants at their neutral defaults) |
a stochastic free-step gate |
BC_BEDRAID |
opening food probes through unknown portals by starving short dragons |
other queen variants: BC_QUEEN_TINY, _HOME, _ESCORT, _MODES, _KINAVOID, _SLACK |
a 2-long budding queen, a home preference, escorts, team modes, a no-contact rule and a reach-slack filter |
BC_V9_ESCAPE, BC_V9_KFEED, BC_KINGGUARD |
unpaired-portal escapes whatever the unit count, king-only feeding, the longest dragon as a guard |
BC_FREESPRINT, BC_R4_CULL, BC_R4_CALM |
free steps toward the nearest food, sacrifices next to our queen, a prior only with no enemy in view |
BC_ECON_CROWN |
defined, but no code reads it |
Code under a 0 switch is compiled out, or reduces to a constant test. It stays in the source on purpose: the turn clock is instruction-metered, and deleting code, even code that never runs, can change inlining and code layout. That shifts how deep the anytime search gets within its budget, so play would change.
rl_weights.hpp holds the opening prior. It is used by the queen process before round 200. r4_weights.hpp holds
the move prior of the non-queen dragons. Both are generated C++ headers: int16 values per row with a float scale,
dequantised once per process.
Data statement: trained by imitation on aggregated public ladder replays of top-ranked teams; contains no per-team model; the training data is not included.
The judge meters each turn in points (about 1 ns of virtual clock per point) and kills a dragon that passes 100M points in a turn.
- The search budget is 75M points (
BC_TURN_BUDGET0.075 s). - QSK's limit is the search deadline + 10M, clamped below the hard line.
- With
BC_FALLBACK95every check point that sees the turn's metered clock past 95M falls back at once. It keeps the action already decided, or at the policy check point (before any search) takes the cheapest legal step: one step to a free neighbour, else straight on, never no action. The sonar rays are dropped, so the single flush (about 2.5M points) stays inside 100M. - The policy check point reuses the clock read of its lean test, and QSK's limit is clamped below the line; the two later check points (before the sonar rays, main.cpp:191, and before the flush, main.cpp:253) read the clock again only after all clock-dependent work. A new clock read inside the search would shift its budget and change play. A fourth check point guards the king library's rays, which never run in this build.
- A fresh child whose setup eats most of its first turn plays a lean one-step turn instead of searching.
- Test builds can force the fallback (
BC_T13_BURN 1) and log it (BC_V11_TRACE 1). Both are 0 in the final build.
A worst-case CPU census and its fixture bots are in tools/abyss_tools/run/ and bots/fixtures/.
The submitted build, compiled by unswbc 1.2.9 with --sandbox (clang to wasm32, -O2 -msimd128), has this
sha256:
0f39d15f10a503b5d5602144660b8c3d3e62cae53ed1fa94e792c88e395e3b0e
To check it from a workspace with unswbc 1.2.9:
cp -R path/to/repo/bots/abyss ./abyss_check
find "${XDG_CACHE_HOME:-$HOME/.cache}/unswbc/wasmbots" -name 'abyss_check-*' -delete
unswbc run maps/arena.map abyss_check starter --sandbox --no-replay
shasum -a 256 "${XDG_CACHE_HOME:-$HOME/.cache}"/unswbc/wasmbots/abyss_check-*.wasmThe bot reads no random source. Where it needs a random choice it hashes the game state (round, id, head). The search
is anytime, so its depth depends on the clock. In the sandbox that clock is the instruction-metered virtual clock, so
a sandboxed game with a fixed unswbc run --seed replays identically. As a check, two sandboxed runs against
starter on default.map with --seed 7 wrote byte-identical replays. In a native run the clock is real time,
so search depths, and with them the moves, can vary from run to run. Another toolkit version can also give different
games if its clang or its CPU metering differs.