A bot's daily routine is simple: look around, pick a target, move, aim, repeat. This chapter builds that loop one decision at a time, then reads the real diver bot. Steering matters as much as power. Huge damage cannot help a knight that never reaches an enemy.
Starts with: Snapshots of the knight, enemies, gems, and map.
Ends with: Aim and movement requests.
Question: How does each added rule change a decision and its cost?
Step 1: make the smallest useful loop
This teaching example reads the knight's status and scans for an enemy. If it finds one, it requests aim and movement toward that enemy. It uses only the public interface. The world handles swings and gem pickups automatically, so the bot needs no explicit attack or collect call.
fn main() {
sense();
let e = nearest_enemy();
if (e >= 0) {
let x = enemy_x(e);
let y = enemy_y(e);
aim_at(x, y);
step_toward(x, y);
}
}e is an index into the latest scan, not a permanent enemy identity. The call nearest_enemy() refreshes the scan and returns its first index if an enemy exists. A negative result means no enemy. The if statement stops the bot from targeting an enemy that is not there.
The scan has a limit, so the “best enemy” means the best in that scan, not in the whole arena. This tiny bot also walks forward when surrounded and ignores gems while it sees any enemy. The next steps improve these choices one at a time.
Step 2: give aim and movement different goals
Suppose a valuable enemy stands east and a gem lies northwest. The knight can aim east and walk northwest at once, because aim and movement are separate requests. Think of a game's mouse cursor and WASD keys. Where you point and where you go are separate choices.
An aim target sets the attack direction. A movement target changes the knight's position, and the attack area moves with it. A full disc attack can reduce the need to aim. Position still matters, though, because the disc has a limited radius.
Two requests from one decision
These are requests, not promises. The host may limit the move, and pointing at an enemy does not guarantee a hit.
A defensive bot might aim at a valuable enemy while retreating. An aggressive bot might aim at the same enemy while orbiting the pack. Same target, different policy: movement makes the difference.
Step 3: build diver from small decisions
diver.bot has five parts: enemy weights, a group-center calculation, target selection, map-edge limits, and main. It does not earn CLASSIC or ABYSS power. It is steering code: it lets the knight use power that another part earns.
The bot stays close to groups of enemies. That aggression makes sense when survival boosts let the knight absorb nearby hits and a wide attack strikes many enemies at once. Without enough survival power, the same position can be deadly.
Give aiming a value model
fn value_of(t) {
if (t == THIEF) { return 100; }
if (t == GNOME) { return 60; }
if (t == SHAMAN) { return 40; }
if (t == PANDA) { return 35; }
if (t == BEAR) { return 30; }
return 10;
}These values are target-choice weights, not gem values. A thief gets ten times the default weight, so the bot aims at it more readily. That weight does not make the thief drop ten times the loot. The number is the bot's opinion, not the game's payout.
fn best_target(n) {
let best = 0 - 1;
let bs = 0 - 1;
let i = 0;
while (i < n) {
let dx = enemy_x(i) - my_x();
let dy = enemy_y(i) - my_y();
let d2 = dx*dx + dy*dy;
let s = value_of(enemy_type(i)) * 100000 / (d2 + 1);
if (s > bs) { bs = s; best = i; }
i = i + 1;
}
return best;
}For each scanned enemy, the selector calculates dx, dy, and the squared distance. Its score is the integer weight * 100000 / (d2 + 1). Read that as valuable and close beats cheap and far. A high weight raises the score, and a long distance lowers it.
Multiplying before dividing keeps more precision in the integer result. The + 1 prevents division by zero when the knight and enemy share a position. Using squared distance avoids a costly square root.
Work one decision by hand. An enemy with weight 10 at distance 40 scores 1,000,000 / 1,601, or 624 after integer division. An enemy with weight 60 at distance 100 scores 6,000,000 / 10,001, or 599. So the nearer enemy wins. Move the weight-60 enemy to distance 80, and it scores 937 and wins instead.
Why the closer target sometimes wins
The actual diver heuristic: integer weight × 100,000 / (distance² + 1). Enemy A has weight 10 and stays 40 pixels away. Enemy B has weight 60; move B to change the aim decision.
bs starts at −1, so the first nonnegative score can become the best. A tie keeps the earlier enemy, because the comparison requires a strictly greater value. The guard in main ensures at least one enemy. The formula ignores enemy HP, walls, wind-up state, and whether its gem would be reachable. That keeps it cheap. It can also pick a poor target.
Summarize the pack without following the aim target
fn pack_cx(n) { let s=0; let i=0; while (i<n) { s = s + enemy_x(i); i = i + 1; } return s / (n>0?n:1); }
fn pack_cy(n) { let s=0; let i=0; while (i<n) { s = s + enemy_y(i); i = i + 1; } return s / (n>0?n:1); }The centroid is the average position of the scanned enemies: a rough “middle of the crowd” pin. For (900,1000), (1100,1000), and (1000,1120), that pin lands at (1000,1040). A knight at (1080,1040) is (80,0) from it. Movement follows this vector. Aim can still point elsewhere.
The helper divides by n > 0 ? n : 1 to prevent division by zero. The caller already checks for an empty scan, so this is a second guard. Integer division can round the result. The centroid summarizes the current scan, not the whole arena.
If enemies stand on both sides of a wall, their centroid can land inside the wall. The scan can also miss much of the swarm. This rule means “orbit the enemies I can see,” not “find the safest point in the arena.”
Turn displacement into three movement regions
let dx = my_x() - cx;
let dy = my_y() - cy;
let d2 = dx*dx + dy*dy;
let ORBIT = 80;
if (d2 > ORBIT*ORBIT*4) {
step_toward(cx, cy);
} else if (d2 < 400) {
if (d2 < 100) { dx = my_x() - enemy_x(0); dy = my_y() - enemy_y(0); }
step_toward(clampw(my_x() + dx), clampw(my_y() + dy));
} else {
step_toward(clampw(my_x() - dy), clampw(my_y() + dx));
}ORBIT equals 80, but d2 > ORBIT*ORBIT*4 means a distance greater than 160, not 80. The d2 < 400 test means a distance less than 20. Between those limits, including exactly 20 and 160, the bot moves sideways.
| Distance from center | Requested target | Reason |
|---|---|---|
| Greater than 160 | Pack centroid | Close the gap to useful attack range |
| Less than 20 | Farther along the outward vector | Ease out of the very center |
| 20 through 160 | Knight position plus (-dy,dx) |
Move sideways around the pack |
For displacement (80,0), the sideways vector is (0,80): 80 pixels down in map coordinates. The host moves the knight only a short step toward that target. It does not jump 80 pixels. Because the bot recomputes the sideways target every tick, those small steps add up to an orbit.
The dot product is dx*(-dy) + dy*dx = 0. Zero means the two vectors meet at a right angle, so at that moment the request pushes neither inward nor outward. Limited steps, moving enemies, walls, and old scans make the real path less exact.
The orbit-dive decision
Move the distance slider to see the three regions of the repository’s ordinary diver.bot. This diagram shows movement regions. It does not simulate a match.
Within 10 pixels of the centroid (d2 < 100), the outward vector can be too small to help. The bot then uses its displacement from the nearest scanned enemy instead. That gives it a direction, not safety.
clampw limits requested coordinates to 48 through MAP_SIZE-48, keeping targets away from the outer map edge. It does not check interior walls, so it cannot guarantee a clear route.
Put the pieces back together
main senses the world and scans for enemies, returning early if it finds none. Otherwise, it computes the centroid and chooses an aim target and a movement target. The host handles attacks and pickups. Here is the complete bot source without its original comments:
Complete ordinary diver.bot — every function
fn value_of(t) {
if (t == THIEF) { return 100; }
if (t == GNOME) { return 60; }
if (t == SHAMAN) { return 40; }
if (t == PANDA) { return 35; }
if (t == BEAR) { return 30; }
return 10;
}
fn pack_cx(n) { let s=0; let i=0; while (i<n) { s = s + enemy_x(i); i = i + 1; } return s / (n>0?n:1); }
fn pack_cy(n) { let s=0; let i=0; while (i<n) { s = s + enemy_y(i); i = i + 1; } return s / (n>0?n:1); }
fn best_target(n) {
let best = 0 - 1;
let bs = 0 - 1;
let i = 0;
while (i < n) {
let dx = enemy_x(i) - my_x();
let dy = enemy_y(i) - my_y();
let d2 = dx*dx + dy*dy;
let s = value_of(enemy_type(i)) * 100000 / (d2 + 1);
if (s > bs) { bs = s; best = i; }
i = i + 1;
}
return best;
}
fn clampw(v) { if (v < 48) { return 48; } if (v > MAP_SIZE-48) { return MAP_SIZE-48; } return v; }
fn main() {
sense();
let n = enemy_count();
if (n <= 0) { return; }
let cx = pack_cx(n);
let cy = pack_cy(n);
let t = best_target(n);
aim_at(enemy_x(t), enemy_y(t));
let dx = my_x() - cx;
let dy = my_y() - cy;
let d2 = dx*dx + dy*dy;
let ORBIT = 80;
if (d2 > ORBIT*ORBIT*4) {
step_toward(cx, cy);
} else if (d2 < 400) {
if (d2 < 100) { dx = my_x() - enemy_x(0); dy = my_y() - enemy_y(0); }
step_toward(clampw(my_x() + dx), clampw(my_y() + dy));
} else {
step_toward(clampw(my_x() - dy), clampw(my_y() + dx));
}
}
When the scan is empty, diver returns without chasing distant gems. Automatic pickup only means that collection needs no extra call. A gem must still enter the knight's pickup radius before the magnet pulls it to the collection distance. A bot that wants distant gems needs its own rule here.
Step 4: understand what gapkiter adds
gapkiter adds three things. It tests nearby walls, compares escape directions, and heads for nearby gems when pressure is low. Its crowd-avoidance rule runs before its gem rule. Each extra decision costs instructions, and more complex code does not always earn more EXP.
Look ahead for walls
fn path_clear(hx, hy, dx, dy) {
let ax = dx; if (ax < 0) { ax = 0 - ax; }
let ay = dy; if (ay < 0) { ay = 0 - ay; }
let mx = ax; let mn = ay; if (ay > ax) { mx = ay; mn = ax; }
let m = mx + (mn * 7 / 16); if (m < 1) { m = 1; }
let ux = (dx * 64) / m; let uy = (dy * 64) / m;
if (is_solid(hx + ux, hy + uy)) { return 0; }
if (is_solid(hx + ux * 2, hy + uy * 2)) { return 0; }
if (is_solid(hx + ux * 3, hy + uy * 3)) { return 0; }
return 1;
}
The helper first estimates the vector's length with max(abs(dx),abs(dy)) + 7*min(abs(dx),abs(dy))/16. From that, it builds a step of about 64 pixels along the vector. A wall at one, two, or three steps ahead rejects the direction.
For (100,0), the scale is 100 and the step is (64,0). For (100,100), the integer scale is 143, giving a step of about (44,44). That is still close to one tile long. These three point tests are only a short look-ahead. They do not test the knight's full width or find a complete path.
step_heading tries the original direction first, then turns of ±45°, ±90°, and ±135°, and only then reverses. For example, (dx-dy,dx+dy) turns the vector by 45° and also changes its length. The host normalizes the request, though, so direction matters more than length. The first clear option wins. The function does not weigh the future rewards of different routes.
Compare pressure in eight directions
The escape rule tests eight directions: cardinal vectors use components of 100, and diagonals use 71. For each direction (dx,dy) and enemy displacement (ex,ey), it computes ex*dx + ey*dy. A positive result means the enemy is ahead. Enemies behind the direction add no pressure.
Each enemy ahead adds 4000000 / (distanceSquared + 400) * dot / 100 to the pressure, in that integer order. Nearby enemies directly ahead add the most pressure, while the added 400 keeps the result bounded at short range. A blocked route gets a large penalty. The lowest total wins.
For eastward escape (100,0), an enemy 50 pixels east gives dot=5000. The first division gives 4000000/2900=1379, and the contribution is 68,950. For westward escape the dot product is negative, so the same enemy adds no pressure there. An enemy to the west can reverse the comparison. The rule finds a local opening. It cannot prove a direction safe.
Place survival before opportunistic collection
The main loop chooses aim first, then counts enemies within reach + 53. Health below 99% of the ordinary level-based maximum counts as low. If at least two enemies are in that radius, or one while the knight is hurt, the bot escapes. Then main returns early, so the gem rule never runs that tick.
When pressure is low, the bot can head for a gem instead. The gem's squared distance must be below both the nearest enemy's squared distance and 340*340. Otherwise, the bot approaches the nearest enemy while outside weapon reach and backs away once inside. Aim, meanwhile, can stay on a different enemy. It uses diver's weight-and-distance score, so a valuable enemy a little farther away can still win.
With no enemies in the scan, gapkiter heads for the first gem. That is something diver never does. But gapkiter also fights more cautiously and spends instructions on extra checks. That is why a feature that looks useful can still mean fewer kills over a full match.
Complete ordinary gapkiter.bot — wall sampling, escape, and main policy
fn path_clear(hx, hy, dx, dy) {
let ax = dx; if (ax < 0) { ax = 0 - ax; }
let ay = dy; if (ay < 0) { ay = 0 - ay; }
let mx = ax; let mn = ay; if (ay > ax) { mx = ay; mn = ax; }
let m = mx + (mn * 7 / 16); if (m < 1) { m = 1; }
let ux = (dx * 64) / m; let uy = (dy * 64) / m;
if (is_solid(hx + ux, hy + uy)) { return 0; }
if (is_solid(hx + ux * 2, hy + uy * 2)) { return 0; }
if (is_solid(hx + ux * 3, hy + uy * 3)) { return 0; }
return 1;
}
fn value_of(t) {
if (t == THIEF) { return 100; }
if (t == GNOME) { return 60; }
if (t == SHAMAN) { return 40; }
if (t == PANDA) { return 35; }
if (t == BEAR) { return 30; }
return 10;
}
fn best_target(n, mx, my) {
let best = 0; let bs = 0 - 1; let i = 0;
while (i < n) {
let dx = enemy_x(i) - mx; let dy = enemy_y(i) - my;
let d2 = dx*dx + dy*dy;
let s = value_of(enemy_type(i)) * 100000 / (d2 + 1);
if (s > bs) { bs = s; best = i; }
i = i + 1;
}
return best;
}
fn count_near(n, mx, my, r) {
let c = 0; let i = 0; let r2 = r*r;
while (i < n) {
let dx = enemy_x(i) - mx; let dy = enemy_y(i) - my;
if (dx*dx + dy*dy <= r2) { c = c + 1; }
i = i + 1;
}
return c;
}
fn clampw(v) { if (v < 48) { return 48; } if (v > MAP_SIZE-48) { return MAP_SIZE-48; } return v; }
fn step_heading(mx, my, dx, dy) {
if (path_clear(mx, my, dx, dy)) {
step_toward(clampw(mx + dx), clampw(my + dy)); return 1;
}
let a = dx - dy; let b = dx + dy;
if (path_clear(mx, my, a, b)) {
step_toward(clampw(mx + a), clampw(my + b)); return 1;
}
let c = dx + dy; let d = dy - dx;
if (path_clear(mx, my, c, d)) {
step_toward(clampw(mx + c), clampw(my + d)); return 1;
}
let e = 0 - dy; let f = dx;
if (path_clear(mx, my, e, f)) {
step_toward(clampw(mx + e), clampw(my + f)); return 1;
}
let g = dy; let h = 0 - dx;
if (path_clear(mx, my, g, h)) {
step_toward(clampw(mx + g), clampw(my + h)); return 1;
}
let i = 0 - dx - dy; let j = dx - dy;
if (path_clear(mx, my, i, j)) {
step_toward(clampw(mx + i), clampw(my + j)); return 1;
}
let k = dy - dx; let l = 0 - dx - dy;
if (path_clear(mx, my, k, l)) {
step_toward(clampw(mx + k), clampw(my + l)); return 1;
}
step_toward(clampw(mx - dx), clampw(my - dy)); return 0;
}
fn step_escape(n, mx, my) {
let bestscore = 1 << 30;
let bx = 0; let by = 0;
let k = 0;
while (k < 8) {
let dx = 0; let dy = 0;
if (k == 0) { dx = 100; dy = 0; }
if (k == 1) { dx = 71; dy = 71; }
if (k == 2) { dx = 0; dy = 100; }
if (k == 3) { dx = 0 - 71; dy = 71; }
if (k == 4) { dx = 0 - 100; dy = 0; }
if (k == 5) { dx = 0 - 71; dy = 0 - 71; }
if (k == 6) { dx = 0; dy = 0 - 100; }
if (k == 7) { dx = 71; dy = 0 - 71; }
let thr = 0; let i = 0;
while (i < n) {
let ex = enemy_x(i) - mx; let ey = enemy_y(i) - my;
let dot = ex * dx + ey * dy;
if (dot > 0) {
let d2 = ex*ex + ey*ey;
thr = thr + 4000000 / (d2 + 400) * dot / 100;
}
i = i + 1;
}
if (!path_clear(mx, my, dx, dy)) { thr = thr + 100000000; }
if (thr < bestscore) { bestscore = thr; bx = dx; by = dy; }
k = k + 1;
}
step_toward(clampw(mx + bx), clampw(my + by));
return 0;
}
fn main() {
sense();
let n = enemy_count();
if (n <= 0) {
let mx = my_x(); let my = my_y();
if (gem_count() > 0) {
step_heading(mx, my, gem_x(0) - mx, gem_y(0) - my);
}
return;
}
let mx = my_x(); let my = my_y();
let reach = my_reach();
let t = best_target(n, mx, my);
aim_at(enemy_x(t), enemy_y(t));
let ndx = enemy_x(0) - mx; let ndy = enemy_y(0) - my;
let nd2 = ndx*ndx + ndy*ndy;
let danger_r = reach + 53;
let crowd = count_near(n, mx, my, danger_r);
let lowhp = my_hp() * 100 / (100 + 18*(my_level()-1)) < 99;
if (crowd >= 2 || (lowhp && crowd >= 1)) {
step_escape(n, mx, my);
return;
}
let ddx = 0; let ddy = 0;
let g = gem_count();
let gx = 0; let gy = 0; let gd2 = 1 << 30;
if (g > 0) { gx = gem_x(0)-mx; gy = gem_y(0)-my; gd2 = gx*gx + gy*gy; }
if (g > 0 && gd2 < nd2 && gd2 < 340*340) {
ddx = gx; ddy = gy;
} else {
let want = reach;
if (nd2 > want*want) { ddx = ndx; ddy = ndy; }
else { ddx = mx - enemy_x(0); ddy = my - enemy_y(0); }
}
if (ddx == 0 && ddy == 0) { return; }
step_heading(mx, my, ddx, ddy);
}
Why these policies interact with power
Kills create gems. Movement and magnet power turn them into pickups. Pickups can raise the knight's level, and each level increase is a full heal. Longer survival then means more time to kill and collect.
But each extreme breaks the loop somewhere. Escaping every fight can mean too few kills. Charging into every group can kill the knight before it collects the gems.
This loop helps explain why diver moves into enemy groups: it is built for a knight with strong local attacks and survival power. The kiter bot backs away from dense groups. And gapkiter looks for a clear escape route and nearby gems. Each bot accepts a different amount of risk. Code complexity does not rank them.
An older ordinary-bot table records hidden medians of 160 for sample and 173 for idle. It records 194 for gapkiter, 318 for kiter, and 336 for diver. These values compare specific artifacts under specific test conditions. They are not maximum scores. In that test, the larger gapkiter program scored below diver.
Where the strongest bots add another component
Ordinary pilots read observations and request movement and aim, but the strongest reference uploads also earn and maintain engine-specific power. So diver alone cannot explain a 336,338 median. Nor does every aggressive reference binary contain this exact source.
The CLASSIC apex is a recovered compiled upload without its complete original source, while the ABYSS apex includes generated source and build scripts. The reference-bot chapter explains what each saved file shows and then connects power and steering to the score.
Tests that block parts of the ABYSS pilot make the point clear. Blocking movement or aim on its own drops the median to 91. Blocking gem-directed moves drops it to 2,322, against 336,338 for the complete bot. Blocking movement and aim together gives 14,677, more than either alone, because the changed match can follow a different path. Power alone does not explain the score.
Improve a pilot by tracing consequences
Change one decision at a time. To study target selection, keep movement and seeds fixed. To study routing, keep weights and combat behavior fixed. Record each seed's score, not just the median: a rule can help some runs and cause early deaths in others.
Then find where two replays first differ. Did the bot stay outside attack reach longer? Did it leave gems behind, or collect one earlier and get a full heal? Did it avoid a wall but abandon a useful enemy group?
Events like these connect a source change to the score. The final death alone usually cannot explain the whole result.
Count instruction cost too. Each call to enemy_count() refreshes the scan, so calling it in a loop condition repeats that work. By contrast, my_x() reads saved bytes, and repeated calls do not refresh the world. A long decision can run across a tick boundary. The compiler chapter shows how these source choices become VM work.