ai-code-battle/bots/assassin/src/strategy.rs
jedarden f11d00a177 feat(bot): add Assassin bot (Rust) — decapitation archetype
All units rush the nearest enemy core via BFS, ignoring enemy units
and economy. No perimeter defense — full commitment to core destruction.
Targets persist across turns for consistent pathing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 16:55:33 -04:00

206 lines
6.7 KiB
Rust

//! Assassin strategy: decapitation archetype — all units rush the enemy core.
//!
//! Ignores economy, ignores enemy units (unless directly blocking), and commits
//! fully to core destruction. No perimeter defense. Relies on speed and mass.
use crate::game::{Direction, GameConfig, GameState, Move, Position};
use std::collections::{HashMap, HashSet, VecDeque};
pub struct AssassinStrategy {
/// Enemy cores discovered so far (persisted across turns)
known_targets: HashMap<Position, bool>,
}
impl AssassinStrategy {
pub fn new() -> Self {
Self {
known_targets: HashMap::new(),
}
}
pub fn compute_moves(&mut self, state: &GameState) -> Vec<Move> {
let my_id = state.you.id;
let config = &state.config;
let rows = config.rows as i32;
let cols = config.cols as i32;
self.update_targets(state, my_id);
let my_bots: Vec<_> = state.bots.iter().filter(|b| b.owner == my_id).collect();
if my_bots.is_empty() {
return vec![];
}
let walls: HashSet<Position> = state.walls.iter().copied().collect();
// Active enemy core targets, sorted by distance from our center of mass
let targets = self.active_targets();
let center = center_of_mass(&my_bots);
let mut sorted_targets: Vec<Position> = targets
.iter()
.filter(|(_, active)| **active)
.map(|(pos, _)| *pos)
.collect();
sorted_targets.sort_by_key(|t| center.distance2(t, rows, cols));
// If no known targets, explore outward
if sorted_targets.is_empty() {
return self.explore_moves(&my_bots, &walls, config);
}
// Primary target: nearest active enemy core to our center of mass
let primary = sorted_targets[0];
// BFS from each bot to the primary target, walking through enemies
let mut moves = Vec::with_capacity(my_bots.len());
let mut destinations: HashSet<Position> = HashSet::new();
for bot in &my_bots {
if let Some(dir) = self.bfs_toward(bot.position, primary, &walls, &destinations, rows, cols) {
let dest = bot.position.move_toward(dir, rows, cols);
destinations.insert(dest);
moves.push(Move {
position: bot.position,
direction: dir,
});
}
}
moves
}
/// Update known targets from visible cores
fn update_targets(&mut self, state: &GameState, my_id: u32) {
for core in &state.cores {
if core.owner != my_id {
self.known_targets
.entry(core.position)
.and_modify(|a| *a = core.active)
.or_insert(core.active);
}
}
}
fn active_targets(&self) -> &HashMap<Position, bool> {
&self.known_targets
}
/// BFS toward a target position. Unlike rusher, does NOT avoid enemy bots —
/// we walk straight through them. Only walls block movement.
/// Avoids self-collision by checking already-claimed destinations.
fn bfs_toward(
&self,
start: Position,
goal: Position,
walls: &HashSet<Position>,
claimed: &HashSet<Position>,
rows: i32,
cols: i32,
) -> Option<Direction> {
if start == goal {
return None;
}
let mut visited: HashSet<Position> = HashSet::new();
let mut queue: VecDeque<(Position, Option<Direction>)> = VecDeque::new();
visited.insert(start);
queue.push_back((start, None));
while let Some((pos, first_dir)) = queue.pop_front() {
if pos == goal {
return first_dir;
}
for dir in Direction::all() {
let next = pos.move_toward(dir, rows, cols);
if visited.contains(&next) || walls.contains(&next) {
continue;
}
visited.insert(next);
queue.push_back((next, first_dir.or(Some(dir))));
}
}
// No path to goal — pick the direction that gets us closest
let mut best_dir = None;
let mut best_dist = i32::MAX;
for dir in Direction::all() {
let next = start.move_toward(dir, rows, cols);
if walls.contains(&next) || claimed.contains(&next) {
continue;
}
let dr = (next.row - goal.row).abs();
let dc = (next.col - goal.col).abs();
let dist = dr.min(rows - dr) + dc.min(cols - dc);
if dist < best_dist {
best_dist = dist;
best_dir = Some(dir);
}
}
best_dir
}
/// When no targets are known, spread bots outward to find enemy cores
fn explore_moves(
&self,
my_bots: &[&crate::game::VisibleBot],
walls: &HashSet<Position>,
config: &GameConfig,
) -> Vec<Move> {
let rows = config.rows as i32;
let cols = config.cols as i32;
let mut moves = Vec::with_capacity(my_bots.len());
// Spread in a line toward the opposite side of the map
for (i, bot) in my_bots.iter().enumerate() {
let target_col = if i % 2 == 0 { cols - 1 } else { 0 };
let target_row = if i % 3 == 0 { rows / 2 } else { rows - 1 };
let target = Position { row: target_row, col: target_col };
let mut best_dir = None;
let mut best_dist = i32::MAX;
for dir in Direction::all() {
let next = bot.position.move_toward(dir, rows, cols);
if walls.contains(&next) {
continue;
}
let dr = (next.row - target.row).abs();
let dc = (next.col - target.col).abs();
let dist = dr.min(rows - dr) + dc.min(cols - dc);
if dist < best_dist {
best_dist = dist;
best_dir = Some(dir);
}
}
if let Some(dir) = best_dir {
moves.push(Move {
position: bot.position,
direction: dir,
});
}
}
moves
}
}
impl Default for AssassinStrategy {
fn default() -> Self {
Self::new()
}
}
/// Compute center of mass of our bots
fn center_of_mass(bots: &[&crate::game::VisibleBot]) -> Position {
if bots.is_empty() {
return Position { row: 0, col: 0 };
}
let sum_r: i32 = bots.iter().map(|b| b.position.row).sum();
let sum_c: i32 = bots.iter().map(|b| b.position.col).sum();
Position {
row: sum_r / bots.len() as i32,
col: sum_c / bots.len() as i32,
}
}