Max-aggression strategy with unconditional attack: every unit charges the nearest enemy, never retreats, and presses engagements within attack range. Fixed self-collision bug by reserving all starting positions and freeing them as bots move away. Falls back to enemy core rushing when no enemies are visible, with exploratory spreading when no targets exist. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
165 lines
4.5 KiB
JavaScript
165 lines
4.5 KiB
JavaScript
const { distance2, manhattan, moveDir, posKey } = require("./grid");
|
|
|
|
const DIRECTIONS = ["N", "E", "S", "W"];
|
|
|
|
function computeMoves(state) {
|
|
const { rows, cols, attack_radius2 } = state.config;
|
|
const myId = state.you.id;
|
|
|
|
const myBots = [];
|
|
const enemyBots = [];
|
|
for (const bot of state.bots) {
|
|
if (bot.owner === myId) myBots.push(bot);
|
|
else enemyBots.push(bot);
|
|
}
|
|
if (myBots.length === 0) return [];
|
|
|
|
const walls = new Set(state.walls.map((w) => posKey(w.row, w.col)));
|
|
const enemyCores = state.cores.filter(
|
|
(c) => c.owner !== myId && c.active
|
|
);
|
|
const energySet = new Set(
|
|
(state.energy || []).map((e) => posKey(e.row, e.col))
|
|
);
|
|
|
|
// Reserve all starting positions so no two bots end up on the same tile.
|
|
// When a bot moves away, its old cell is freed for others.
|
|
const committed = new Set(
|
|
myBots.map((b) => posKey(b.position.row, b.position.col))
|
|
);
|
|
const moves = [];
|
|
|
|
// Bots closest to enemies decide first — they get priority on attack positions
|
|
myBots.sort((a, b) => {
|
|
const da = nearestEnemyDist2(a.position, enemyBots, rows, cols);
|
|
const db = nearestEnemyDist2(b.position, enemyBots, rows, cols);
|
|
return da - db;
|
|
});
|
|
|
|
for (const bot of myBots) {
|
|
const br = bot.position.row;
|
|
const bc = bot.position.col;
|
|
|
|
const target = findNearestEnemy(br, bc, enemyBots, rows, cols);
|
|
|
|
let bestDir = null;
|
|
let bestScore = -Infinity;
|
|
|
|
for (const dir of DIRECTIONS) {
|
|
const [nr, nc] = moveDir(br, bc, dir, rows, cols);
|
|
const nk = posKey(nr, nc);
|
|
|
|
if (walls.has(nk)) continue;
|
|
if (committed.has(nk)) continue;
|
|
|
|
let score = 0;
|
|
|
|
if (target) {
|
|
const distToTarget = distance2(
|
|
nr, nc, target.position.row, target.position.col, rows, cols
|
|
);
|
|
|
|
// Close distance to nearest enemy as fast as possible
|
|
score -= distToTarget * 100;
|
|
|
|
// Heavy bonus for staying in attack range — press the engagement
|
|
if (distToTarget <= attack_radius2) {
|
|
score += 200;
|
|
}
|
|
|
|
// Also prefer closing distance to other enemies (don't tunnel-vision)
|
|
let totalEnemyDist = 0;
|
|
for (const e of enemyBots) {
|
|
totalEnemyDist += distance2(
|
|
nr, nc, e.position.row, e.position.col, rows, cols
|
|
);
|
|
}
|
|
score -= totalEnemyDist;
|
|
|
|
// Grab energy only when it's directly along the attack path
|
|
if (energySet.has(nk)) {
|
|
score += 5;
|
|
}
|
|
} else {
|
|
// No enemies visible — rush enemy core to raze it
|
|
if (enemyCores.length > 0) {
|
|
const coreDist = nearestCoreDist(
|
|
nr, nc, enemyCores, rows, cols
|
|
);
|
|
score -= coreDist * 100;
|
|
} else {
|
|
// No targets at all — spread outward to explore
|
|
// Prefer moving away from other friendly bots
|
|
let friendProximity = 0;
|
|
for (const other of myBots) {
|
|
if (other === bot) continue;
|
|
friendProximity += distance2(
|
|
nr, nc, other.position.row, other.position.col, rows, cols
|
|
);
|
|
}
|
|
score += friendProximity * 0.5;
|
|
}
|
|
|
|
// Collect energy while roaming (we need it to keep spawning)
|
|
if (energySet.has(nk)) {
|
|
score += 10;
|
|
}
|
|
}
|
|
|
|
if (score > bestScore) {
|
|
bestScore = score;
|
|
bestDir = dir;
|
|
}
|
|
}
|
|
|
|
if (bestDir) {
|
|
const [nr, nc] = moveDir(br, bc, bestDir, rows, cols);
|
|
committed.delete(posKey(br, bc));
|
|
committed.add(posKey(nr, nc));
|
|
moves.push({
|
|
position: { row: br, col: bc },
|
|
direction: bestDir,
|
|
});
|
|
}
|
|
// If no direction is viable the bot holds; its starting cell stays in committed
|
|
}
|
|
|
|
return moves;
|
|
}
|
|
|
|
function findNearestEnemy(r, c, enemyBots, rows, cols) {
|
|
let best = null;
|
|
let bestDist = Infinity;
|
|
for (const e of enemyBots) {
|
|
const d = distance2(r, c, e.position.row, e.position.col, rows, cols);
|
|
if (d < bestDist) {
|
|
bestDist = d;
|
|
best = e;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function nearestEnemyDist2(pos, enemyBots, rows, cols) {
|
|
let minD = Infinity;
|
|
for (const e of enemyBots) {
|
|
const d = distance2(
|
|
pos.row, pos.col, e.position.row, e.position.col, rows, cols
|
|
);
|
|
if (d < minD) minD = d;
|
|
}
|
|
return minD;
|
|
}
|
|
|
|
function nearestCoreDist(r, c, cores, rows, cols) {
|
|
let minD = Infinity;
|
|
for (const core of cores) {
|
|
const d = manhattan(
|
|
r, c, core.position.row, core.position.col, rows, cols
|
|
);
|
|
if (d < minD) minD = d;
|
|
}
|
|
return minD;
|
|
}
|
|
|
|
module.exports = { computeMoves };
|