// Kiter: keeps its distance, fires, leads a moving target, and backs off or dashes away when the // enemy closes in. It can step and fire in the same tick, and takes the shot that can't miss when // the enemy is pinned (stunned, recovering from a dash, or stepped with its dash on cooldown). import { chooseAttack, endTile, enemyStepIn, escapeDash, evadeDanger, cheb, inSafeZone, openness, pickupStep, retreatStep, searchGoal, shotArrival, stepToward, watchEnemy, type Ctx, type Observation, type Pos, type Policy, type Turn } from "../lib/bot.ts"; /** Movement phase. */ function movement(obs: Observation, ctx: Ctx): Turn { const escape = evadeDanger(obs); if (escape) return escape; const pos = obs.self.pos; const cd = obs.self.cooldowns; const enemy = obs.enemies[0]; if (!inSafeZone(obs, pos, 1) && cd.move === 0) { const d = stepToward(ctx.map, pos, obs.zone.center, 0, enemy ? [enemy.pos] : []); if (d) return { move: d }; } // Detour for pickups the enemy can't reach first; farther when nobody is in sight. const grab = cd.move === 0 && (!enemy || cheb(pos, enemy.pos) > ctx.memory.minRange) ? pickupStep(obs, ctx, enemy ? 4 : 8) : null; if (grab) return { move: grab }; if (!enemy) { // Search, but stop short: the kiter wants to see the enemy from range, not bump into it. const d = cd.move === 0 ? stepToward(ctx.map, pos, searchGoal(obs, ctx), 3) : null; return d ? { move: d } : {}; } const dist = cheb(pos, enemy.pos); if (dist <= ctx.memory.minRange) { // Attacks land the tick they're sent, so an enemy that isn't recovering is a threat up close. const threatened = enemy.state === "idle"; // Openness ≤ 11 means a corner or a tight spot against walls (open floor scores 24). const cornered = openness(ctx.map, pos) <= 11; if (threatened || cornered) { const d = escapeDash(obs, ctx, enemy.pos); if (d) return { dash: d }; } // Retreat toward open space and the centre rather than straight back into a wall. const d = retreatStep(obs, ctx, enemy.pos); if (d) return { move: d }; } if (dist > ctx.memory.maxRange && cd.move === 0) { const d = stepToward(ctx.map, pos, enemy.pos, ctx.memory.maxRange - 1, [enemy.pos]); if (d) return { move: d }; } return {}; } /** * Where to aim the standard shot: one tile on along the enemy's last step, if it can step again * before the shot arrives. A shot that lands sooner is aimed at the enemy itself: it can't have * moved yet (bar a dash), so leading it would only miss. */ function leadAim(obs: Observation, ctx: Ctx, from: Pos): Pos | undefined { const enemy = obs.enemies[0]; const step: Pos | null = ctx.memory.enemyStep; if (!enemy || !step || ctx.rand() >= ctx.memory.lead) return undefined; const led: Pos = [enemy.pos[0] + step[0], enemy.pos[1] + step[1]]; if (led[0] === from[0] && led[1] === from[1]) return undefined; return shotArrival(ctx.hello.rules, from, led) >= enemyStepIn(obs, ctx) ? led : undefined; } export const kiter: Policy = { version: "kiter-17", decide(obs, ctx): Turn { // Per-match temperament, so repeated matches on one map play out differently. ctx.memory.minRange ??= 2 + Math.floor(ctx.rand() * 2); // back off inside this distance ctx.memory.maxRange ??= 6 + Math.floor(ctx.rand() * 3); // close in beyond this distance ctx.memory.lead ??= ctx.rand() * 0.6; // chance to aim where the enemy is heading watchEnemy(obs, ctx); if (obs.self.state !== "idle") return {}; const turn = movement(obs, ctx); const from = endTile(obs, turn); // An adjacent enemy is always shoved: it lands 3 tiles away, the kiter's range. const attack = chooseAttack(obs, ctx, from, leadAim(obs, ctx, from), 1); if (attack) turn.attack = attack; return turn; }, };