Every dragon decides alone, every turn, by scoring its four possible first steps with a depth-limited lookahead over its
own move sequences. Other dragons are held still during the lookahead; everything the dragon knows about them enters as
value and danger terms. This page follows main.cpp and Policy::decide (policy.hpp) in execution order. Pointers
are file:line in bots/abyss.
The judge prices every executed WASM instruction (1 to 50 points each; see
../rules/game-rules.md) and kills a dragon whose turn costs more than 100 M
points. Inside the sandbox the process clock is virtual: std::chrono::steady_clock advances 1 ns per point, so the
100 M-point limit is 0.1 s on clockNow() (common.hpp:1204). Every deadline in the bot is a time on that clock:
| budget | value | where |
|---|---|---|
| search deadline | turnStart + 0.075 s = 75 M points (BC_TURN_BUDGET) |
main.cpp:60, common.hpp:1212 |
| lean turn | if the clock already reads more than deadline - 0.045 s (Params::leanMargin) at the lean test in decide() (after the queen strikes and roles, before the networks), i.e. 30 M points are gone, only the one-step evaluation runs and no network is evaluated |
policy.hpp:343 |
| Queen Safety Kernel | until min(search deadline + 10 M, hard line - 3 M) | policy.hpp:987 |
| hard line | 95 M points from the start of the metered turn: every check point that sees it falls back at once | common.hpp:1234, cpu-safety.md |
turnStart is 0 (process start) on a process's first turn, so map setup and the networks' de-quantisation are charged
to it, as the meter charges them; on later turns it is the clock read right after the round block was parsed
(main.cpp:30). The 95 M line is measured from the meter's own start, the ROUND line (io.hpp:98). A native build runs
on the wall clock, so native games search to a different depth than sandbox games; every measurement quoted in these
documents is a sandbox run.
Params (common.hpp:512) holds every tunable number. tuned_params.hpp is generated by the tuning tools and assigns
values per (map group, phase), 12 contexts. main.cpp re-selects the set whenever the key changes (main.cpp:42):
- map group, judged online by
World::mapGroup(world.hpp:294) from what this dragon has seen, never from a map name: 1 "fountain map" once at least 2 beds are hot (their countdown was seen at least 3 times and never above 10,hotGapof the untunedkParams, common.hpp:847); 0 "pearl-poor" once 150 or more tiles were seen and the summed refill rate of the seen beds per seen tile is below 0.004 (common.hpp:850); 2 otherwise (also the start state).main.cpppasseskParamstoWorld::observeandWorld::mapGroup, so the tunedhotGap20 /poorRate0.0038 intuned_params.hppare never read; - phase (common.hpp:1189): rounds 0-99, 100-299, 300-399, 400-499.
In the final file the four phases of a group share their values; only the map group changes a value. The knobs that differ by group:
| knob | pearl-poor (0) | fountain (1) | rest (2) | meaning |
|---|---|---|---|---|
wHot |
0 | 0.75 | 1.99 | long-range pull of fast-refilling beds |
exploreUntil |
383 | 184 | 50 | the exploration bonuses fade to 0 by this round |
wExploreSector |
0.007 | 0.125 | 0 | bonus of a never-seen tile in a sector the team has not explored |
wDisperse |
1.76 | 0.89 | 1.73 | cost of stepping through a crowd of our own heads |
tradeMinUnits / tradeSlack |
2 / -2 | 12 / 0 | 2 / 0 | squad targets (and trades the lookahead accepts) need this many units and our length at most theirs + slack; the engagement model's own trades do not use them |
growerMinUnits / growRound |
50 / 301 | 63 / 480 | 35 / 150 | when starting and long dragons turn into "growers" |
growerKeepBase / growerKeepEvery |
2 / 80 | 2 / 10 | 10 / 50 | a grower's budding keeps base + round / every segments |
lateSplitUnits |
4 | 4 | 64 | after growRound plain splits stop at this many units |
earlyEnd |
0 | 249 | 74 | idle escorting of growers starts here (its weight is 0, see below) |
Values common to every context (selection): gamma 0.7, eatBonus 3.0, gammaFar 0.85, spaceFactor 1.56 and
spaceMargin 1, wDanger 0.3, wPocket 0.37, wReverse 0.69, wEngage 2.49, engageAttack 1.0, sprintMax 4,
seenDecay 0.943, spawnDecay 0.993, wExploreFar 0.143, wMirrorHot 1.23, wHunt 1.21, wSupport 0.69,
squadSize 5, swarmUntil 400, lateNoSplit 400, feedRound 400, feedStop 490, lateRevFrom 350, finalRevLen
10, sonarFrom 300, wPrior 0.05, rlSplitP 0.4, rlUntil 200. Untuned defaults that matter: horizon 48 (BFS
depth), maxDepth 10, leanMargin 0.045, kingSpace 12, kingDanger 3, oneKingFrom 350.
Several knobs are 0 (or a value that disables them) in every context and switch their component off at run time:
wFight, wScav, wHeatSpread, wEscort, wProbe, wSpace, wBlind, wBlindQuiet, scoutUntil, p2Early,
kingOn, deadEndOn, tailOn, roomOn, assignOn, sitEnemyR, sitKingEvery, sitPortal; kDensity 1 (the
density cap becomes the whole map), fightTradeMin -10, finalFrom 500 (never). enemyCloserFactor and remoteYield
are 1.0, which makes their multiplications identities (see economy-and-expansion.md for what
that means for FORAGE_PATH).
The values were found by self-play evolution strategies over these contexts (an island model), later complemented by small sweeps of single knobs; the tuning infrastructure is documented with the training tools, not here.
decide starts at policy.hpp:264. A new Policy object is built every turn; state that must survive lives in World.
- Queen strikes (policy.hpp:278, dragons with id 2 or more only): QKILL (take the enemy queen's head when a free or paid path reaches it this turn), QGUARD (trade with, or block, an enemy head that moves after us and can reach our queen this round), then free-step pursuit of a visible enemy queen. Any of them returns at once. See endgame.md.
- Survival reproduction (policy.hpp:281): with 4 units or fewer, or isolated (no teammate in view for 15 rounds)
with 8 or fewer, before round 400, the dragon splits as soon as it can (in practice the swarm's halving, below),
values pearls x1.5 while shorter than 4, and never trades or hunts. The thresholds are constants (common.hpp:928),
not
Params, so tuning cannot remove them. - Roles and tactics (from policy.hpp:283): the crowd level and dispersal weight (
computeTactics), the grower role, the early king (from round 200 the longest known dragon of length 8 or more), our queen's head in view for the friend-discipline rules, her last free side (BOXGUARD), the early king's yield zone. - Lean test and 95 M check point (policy.hpp:343): one clock read decides both.
- Network priors (from policy.hpp:366): the RL net for the queen's process before round 200, the R4 net for every other dragon. See net-priors.md.
- Fields and plans (from policy.hpp:444):
buildBase(other bodies are walls; a teammate's only free exit is a wall for us; the early king does not give way),buildTargets(pearls, bed countdowns, unseen tiles), BFS distance fields of the 12 nearest heads (skipped on lean turns), the local team, the VALUE target, the opening-expansion target, reserve feeders, harvest (which may return its own action), portals and pockets, squads. - Feeding (from policy.hpp:483): in the feeding window a dragon close enough to the dragon it feeds steps into its own neck and dies there. See endgame.md.
- Remaining plans (from policy.hpp:682): regicide targets, late hunts of enemy heads, the idle-bed choice.
- Anytime lookahead (policy.hpp:722), below.
- Root adjustments (from policy.hpp:753): the network priors are added to the root totals; the local fight search
block that follows is inactive (
wFightis 0 andfightTradeMinis -10 in every context). - Pick and trapped handling (from policy.hpp:833), below.
- Queen Safety Kernel for our queen (policy.hpp:942): the kernel picks her landing by tiers and returns it, unless its pick is the search's own single step in tier 1 or 2 (then the remaining rules below still apply to her) or it finds no landing at all ("tier 6": the trapped handling above stands). See queen-safety.md.
- Post-search actions (from policy.hpp:1042): the probe step into an unpaired portal, a sprint trade, the lane exit of a dip, a split, the reverse split when boxed in, and the R4 free steps.
main.cpp then applies the last guards (main.cpp:85 to 111): a queen multi-step move must come from the kernel
(otherwise it is cut to one step), a non-queen leaves the last unit slot to the queen, an illegal split becomes a step
to a free neighbour, and a sprint is cut where it could not be paid.
depth 1: first[d] = scoreFrom(body, L, d, k = 0) for the 4 directions
depth n: total[d] = first[d].stepTerm + searchFrom(body after d, depth n - 1, k = 1)
searchFrom = max over the 4 directions of stepTerm * gamma^k + searchFrom(..., k + 1)
leaves: score = value + stepTerm * gamma^k
no legal move below: -900
(a lane tip that can leave by a reverse split instead: -(2 - 0.5) * eatBonus * gamma^k)
Iterative deepening runs depth 2, 3, ... up to maxDepth 10 while the clock is before the deadline; a depth that was
cut by the deadline is discarded and the last complete one is kept (policy.hpp:729). Typical sandbox turns use the whole
75 M budget. A trade, a vetoed step, a probe entry or a harvest entry is terminal: nothing is searched past it.
The simulated body is exact for our own dragon: each step adds the new head, and the tail stays only on a step that eats (policy.hpp:3471). Other dragons' cells are walls at every depth; beyond the first step the lookahead only pays for "stopping next to an enemy head".
policy.hpp:3409. For a step to tile dest at depth k:
Legality and safety veto. Kelp, unpaired portals, our own body (tail included), our own stray segments and a
teammate's last free exit are illegal. At k = 0 a step onto an enemy head is a head-on trade, scored by the engagement
model below; any other dragon's cell is illegal. Every other step floods the free space from dest with the new body as
a time-aware wall (nav.hpp: our segment i frees after L - i moves; policy.hpp:3482). If fewer than need = min(int(1.56 L) + 1, tiles / 2) tiles are reachable (the early king needs at most 12) the step is vetoed (score -1000 +
space), unless the dragon can leave by a reverse split: length 4 or more and a known tail with room for the child. The
probe entry and the harvest entry need no room.
Value (summed over targets; m = BFS steps from dest + 1 + k, d = BFS steps):
| term | formula and weight | notes |
|---|---|---|
| pearl | belief x 0.7^m | belief 1 in view; a remembered pearl decays 0.943 a round; a bed that refilled unseen 0.993 a round |
| bed countdown | max(pearl, 0.9 x 0.7^m x 0.9^wait), wait = max(0, countdown + 1 - m), before round 300 | the spawn clock, see economy-and-expansion.md; later 0.9 x 0.7^max(m, countdown + 1) |
| yield to teammates | 0 if a visible teammate reaches the tile first (ties: the lower id) | the rule that spreads dragons without messages |
| unseen tile | (0.03 + P(pearl) + sector bonus) x 0.7^m | P(pearl) from the bed share seen so far, or from the mirror; 0 on tiles of map-network danger above 0.3 |
| frontier pull | 0.143 x fade x 0.85^m to the nearest never-seen tile of an unexplored sector | fades linearly to 0 by exploreUntil |
| fountains | wHot x rate x 0.85^m (x 1.23 for a fountain known only from its mirror) |
skipped when a visible teammate is nearer to it and within 10 steps of it |
| VALUE area | 0.15 x min(beds, 6) / 6 x 0.85^d | a bed area none of ours holds (mapnet.md) |
| opening expansion | 0.4 x max(0, max(40, W + H) - d) | until round 120, maps of at most 700 tiles |
| harvest entry | 3.0 x max(0, 1 - d / 40) | fountain rooms behind portals |
| idle bed | 0.5 x 0.85^d | a dragon of length 3 or less with no meal for 30 rounds, before round 300 |
| regicide | 3.0 x 0.85^d to the enemy queen's last known head | the 1-3 hunters with the shortest ETA (endgame.md) |
| hunts | attack 1.21 x (1 + gain / 4) x 0.7^d; support 0.69 x 0.7^abs(d - 5) | squads on enemies worth a trade, and the enemy queen in view |
| late hunt | 2.0 x 0.85^d to the nearest enemy head heard of within 6 rounds | dragons of length 8 or less, from round 300 with 40 or more units |
| feeding | 1.55 x 0.7^d to the head of the dragon it feeds | the feeding window |
| leaving a pocket / probing | 1.5 x 0.7^d to an exit; 3.0 x max(0, 1 - d / 40) to an unpaired portal of a closed, poor region | plain probes have no pull (wProbe 0) |
| detached team member | 0.2 x min(8, Chebyshev distance to the team leader) / 8 | leave a crowd of more than 5 |
Step term (stepTerm; the real first step and each simulated step pay it, discounted by 0.7^k): eatBonus 3.0 x
eat weight if the step eats, minus the danger below, plus the bed terms before round 300 (eating a pearl that sits on a
bed frees the bed: + min(0.5, 1 / gap); a step onto a visible bed that spawns next round, without eating: -1.5), minus
the dispersal cost (wDisperse x crowd field, not on a step that eats), minus wReverse 0.69 if only a reverse split
saves the step, minus the early king's zone (1.5 x (3 - distance) / 3 within Chebyshev 2 of its head). The eat weight is
0 within 3 of the early king's head and, for its two feeders, within 3 of the reserve's head; it is doubled for our
queen before round 200 while she is shorter than 5.
Danger (at k = 0 unless noted):
| term | value |
|---|---|
| engagement (retreat or hold) | for each visible enemy that can reach dest this round: 2.49 x max(0, its trade value) x mult |
| heard enemy heads out of sight | 0.5 x L x nearness x freshness, within Chebyshev 4 and 8 rounds |
0.37 if dest has at most one way on |
|
| portal loiter | 2.0 for pacing beside a portal without crossing |
| map-network danger | 0.6 x danger(dest); danger 1 at a death seen or a dangerous room heard, fading to 0 over 200 rounds |
| other local teams | 0.1 x nearness to another team's leader, within 6 |
| grower buffer | 0.3 per visible enemy head within two steps of dest (growers only) |
| queen friend discipline | ROOMGUARD 30, BREATHE 60 (queen-safety.md) |
lookahead, k >= 1 |
0.3 x L x mult for stopping next to an enemy head, unless it is a trade we would take |
mult is 1, x0.5 for dragons of length 3 or less (the swarm spends its small dragons), x2 from round 400, x3 for the
early king (who also pays 0.3 x L x mult for each enemy whose sprint reach, up to 3 steps, covers dest), x6 for the
team's last dragon.
Score: value + stepTerm x 0.7^k (policy.hpp:4014; the wSpace bonus is 0 in every context). The why label of
the step ("forage", "eat", "hunt", "to-feed", "idle-bed", "cramped", ...) is kept for the post-search rules and for
trace builds.
Two heads that meet both die and drop ceil(L/2) pearls each, so a trade is worth what the moving side's team eats back.
The model (policy.hpp:3241, BC_ENGAGE) was fitted on about 17,900 head-on trades from public ladder replays (games of
ours and games between top-ranked teams): the share p of the drops the attacker's side eats is
logit p = 0.273 + 0.038 * (heads within 3 of the site, its side minus theirs) + 0.098 * (the same within 5)
+ 0.278 * (steps its nearest other head is nearer to the site than theirs, capped at 4)
value = (victim length - attacker length) + (2p - 1) * (ceil(la / 2) + ceil(lv / 2))
On held-out trades the predicted value bands matched what happened: predicted -3 or less came out ahead in 3% of the
trades, 1 to 3 in 81%, 3 or more in 97%. A dragon attacks (a step, or a sprint of up to min(sprintMax 4, its reach)
steps) when the value for us is at least engageAttack 1.0 and the team has more than 4 units; growers and the queen
never trade, and a threat to a grower is fought whatever the value. The same model, seen from the enemy, is the retreat
term above. 62% of the enemy's kills of our dragons came from 2-3 tiles away, which is why the retreat term covers every
tile an enemy can reach this round, not only adjacent ones.
The best root total wins. With no legal move at all the dragon is doomed and dies in the least harmful way (policy.hpp:873): first it takes a tile it had left free only as a teammate's last exit ("shared-exit"), but never our queen's last free side (BOXGUARD); otherwise an enemy head (a trade), then an unpaired portal (its landing is unknown, not certain death; for our queen the portal comes first), then kelp or our own body, and a teammate's head last.
After the queen's kernel, the post-search rules may replace the step:
- probe: at the entry tile of an unpaired portal it has chosen to probe, step in (landing unknown);
- sprint trade (policy.hpp:4171): a multi-step
MOVEonto an enemy head whose trade value is at least 1.0; - lane exit of a dip and split (
trySplit, economy-and-expansion.md); - reverse split (policy.hpp:1082) when the best score is below -500 (-996 for the early king, the reserve, and dragons of length 10 or more from round 350): split L - 2, leaving a 2-long stub; the child (our old tail, facing out) leaves. The queen does this only when trapped;
- R4 free steps: continue a plain foraging step for a second free step where the R4 net predicts that strong teams would (net-priors.md).
Teammates coordinate by running the same rules on the same facts, so they agree without negotiating:
- closer-teammate yield: a pearl a visible teammate reaches first is worth 0 to us;
- dispersal (policy.hpp:2634): with 5 or more of our heads within Chebyshev 3, every non-eating step pays
wDispersex min(4, crowd - 4) x (1 + 0.15 x min(longer heads, 5)) x the crowd field. In replays self-deaths rose from about 4 per 1000 turns for a dragon alone to 54 with 8 or more own heads around, which is what this rule targets. The plain-split rule "no split in a crowd" exists too, but the swarm bypasses it until round 400, and after that no dragon splits voluntarily (a boxed-in dragon may still reverse-split); - local teams (
team.hpp): teammates in view or heard last round within Chebyshev 5 form a team led by the longest; members beyond the fifth are "detached" and pulled away from the leader (0.2); leaders announce TEAM facts and other teams' areas cost a little (0.1). The pearl assignment inside a team is off (assignOn0); - squads: an enemy worth a trade (our length at most theirs +
tradeSlack, enough units) is attacked by our nearest non-grower dragon while the next four hold 5 tiles back to eat the drops; enemy heads within 8 of one of our growers are intercepted whatever their length; - survival policy: no voluntary trades or hunts with 4 units or fewer, whatever the tuned values say.