Skip to content

Latest commit

 

History

History
247 lines (205 loc) · 19.4 KB

search-core.md

File metadata and controls

247 lines (205 loc) · 19.4 KB

Search core: the per-dragon anytime lookahead

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 clock

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.

Parameters: map group and phase

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, hotGap of the untuned kParams, 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.cpp passes kParams to World::observe and World::mapGroup, so the tuned hotGap 20 / poorRate 0.0038 in tuned_params.hpp are 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.

Policy::decide, in order

decide starts at policy.hpp:264. A new Policy object is built every turn; state that must survive lives in World.

  1. 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.
  2. 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.
  3. 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.
  4. Lean test and 95 M check point (policy.hpp:343): one clock read decides both.
  5. 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.
  6. 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.
  7. 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.
  8. Remaining plans (from policy.hpp:682): regicide targets, late hunts of enemy heads, the idle-bed choice.
  9. Anytime lookahead (policy.hpp:722), below.
  10. Root adjustments (from policy.hpp:753): the network priors are added to the root totals; the local fight search block that follows is inactive (wFight is 0 and fightTradeMin is -10 in every context).
  11. Pick and trapped handling (from policy.hpp:833), below.
  12. 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.
  13. 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.

The lookahead

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

scoreFrom: one step from a simulated state

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

Head-on trades: the engagement model

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.

Picking the move and trapped handling

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 MOVE onto 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).

Team tactics inside the value function

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 wDisperse x 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 (assignOn 0);
  • 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.