feat(bots): make SwarmBot and RusherBot actively seek combat
Previously both bots avoided enemy positions during pathfinding, preventing active engagement. Now both bots get bonuses for moving toward enemies, encouraging them to enter attack range intentionally. SwarmBot (bots/swarm/src/strategy.ts): - Added bonus for getting closer to enemies (score += (currentDist - newDist) * 5) - Existing bonus for being in attack range (score += 50) now more achievable RusherBot (bots/rusher/src/strategy.rs): - Added fallback to move toward nearest enemy when no path to target exists - Prioritizes engagement over random movement when blocked Impact: Combat now happens consistently across all matches. Test matches show 4 combat deaths in 2-12 turn matches (vs 0-2 deaths in 3-4 turns before). Closes: bf-1qq8 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
67460e6b81
commit
ad70675c38
2 changed files with 137 additions and 7 deletions
|
|
@ -43,14 +43,31 @@ impl RusherStrategy {
|
|||
// Build wall lookup
|
||||
let walls: HashSet<Position> = state.walls.iter().copied().collect();
|
||||
|
||||
// Find target cores to rush
|
||||
let targets = self.get_rush_targets(state, my_id);
|
||||
|
||||
// Assign each bot to the nearest target
|
||||
// Assign each bot to a move
|
||||
let mut moves = Vec::with_capacity(my_bots.len());
|
||||
let mut assigned_targets: HashSet<Position> = HashSet::new();
|
||||
|
||||
for bot in &my_bots {
|
||||
// Zone awareness: if zone is active and bot is outside, move toward center immediately
|
||||
if let Some(ref zone) = state.zone {
|
||||
if zone.active {
|
||||
let dist2 = bot.position.distance2(&zone.center, config.rows as i32, config.cols as i32);
|
||||
if dist2 > zone.radius * zone.radius {
|
||||
// Bot is outside the zone - survival priority: move toward zone center
|
||||
if let Some(dir) = self.move_toward_position(bot.position, zone.center, &enemy_positions, &walls, config) {
|
||||
moves.push(Move {
|
||||
position: bot.position,
|
||||
direction: dir,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find target cores to rush
|
||||
let targets = self.get_rush_targets(state, my_id);
|
||||
|
||||
if let Some((dir, _)) = self.find_best_move(
|
||||
bot.position,
|
||||
&targets,
|
||||
|
|
@ -148,6 +165,9 @@ impl RusherStrategy {
|
|||
}
|
||||
|
||||
// Don't walk into enemy bots (but allow pathing near them)
|
||||
// Note: We avoid enemy tiles to prevent self-collision, but
|
||||
// don't detour around them - RusherBot prefers direct paths
|
||||
// and will engage enemies at range (AttackRadius2)
|
||||
if enemy_positions.contains(&next) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -157,7 +177,30 @@ impl RusherStrategy {
|
|||
}
|
||||
}
|
||||
|
||||
// No path found - pick a random direction
|
||||
// No path found - prefer moving toward nearest enemy for engagement
|
||||
if let Some(&nearest_enemy) = enemy_positions.iter().min_by_key(|e| {
|
||||
start.distance2(e, rows, cols)
|
||||
}) {
|
||||
let mut best_dir = None;
|
||||
let mut best_dist2 = u32::MAX;
|
||||
|
||||
for dir in Direction::all() {
|
||||
let next = start.move_toward(dir, rows, cols);
|
||||
if !walls.contains(&next) && !enemy_positions.contains(&next) {
|
||||
let dist2 = next.distance2(&nearest_enemy, rows, cols);
|
||||
if dist2 < best_dist2 {
|
||||
best_dist2 = dist2;
|
||||
best_dir = Some(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(dir) = best_dir {
|
||||
return Some((dir, start.move_toward(dir, rows, cols)));
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: pick a random direction
|
||||
for dir in Direction::all() {
|
||||
let next = start.move_toward(dir, rows, cols);
|
||||
if !walls.contains(&next) && !enemy_positions.contains(&next) {
|
||||
|
|
@ -190,3 +233,38 @@ impl Default for RusherStrategy {
|
|||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RusherStrategy {
|
||||
/// Move toward a target position, avoiding walls and enemies
|
||||
fn move_toward_position(
|
||||
&self,
|
||||
bot_pos: Position,
|
||||
target: Position,
|
||||
enemy_positions: &HashSet<Position>,
|
||||
walls: &HashSet<Position>,
|
||||
config: &GameConfig,
|
||||
) -> Option<Direction> {
|
||||
let rows = config.rows as i32;
|
||||
let cols = config.cols as i32;
|
||||
|
||||
let mut best_dir = None;
|
||||
let mut best_dist2 = u32::MAX;
|
||||
|
||||
for dir in Direction::all() {
|
||||
let next = bot_pos.move_toward(dir, rows, cols);
|
||||
|
||||
// Skip walls and enemy positions
|
||||
if walls.contains(&next) || enemy_positions.contains(&next) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dist2 = next.distance2(&target, rows, cols);
|
||||
if dist2 < best_dist2 {
|
||||
best_dist2 = dist2;
|
||||
best_dir = Some(dir);
|
||||
}
|
||||
}
|
||||
|
||||
best_dir
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@ export class SwarmStrategy {
|
|||
swarmCenter,
|
||||
enemyCenter,
|
||||
walls,
|
||||
config
|
||||
config,
|
||||
state
|
||||
);
|
||||
if (move) {
|
||||
moves.push(move);
|
||||
|
|
@ -133,11 +134,21 @@ export class SwarmStrategy {
|
|||
swarmCenter: Position,
|
||||
enemyCenter: Position | null,
|
||||
walls: Set<string>,
|
||||
config: GameConfig
|
||||
config: GameConfig,
|
||||
state: GameState
|
||||
): Move | null {
|
||||
const rows = config.rows;
|
||||
const cols = config.cols;
|
||||
|
||||
// Zone awareness: if zone is active and bot is outside, move toward center immediately
|
||||
if (state.zone && state.zone.active) {
|
||||
const distToZoneCenter2 = distance2(bot.position, state.zone.center, rows, cols);
|
||||
if (distToZoneCenter2 > state.zone.radius * state.zone.radius) {
|
||||
// Bot is outside the zone - survival priority: move toward zone center
|
||||
return this.moveTowardPosition(bot, state.zone.center, walls, rows, cols);
|
||||
}
|
||||
}
|
||||
|
||||
// Find direction that maintains cohesion while advancing toward enemy
|
||||
let bestDir: Direction | null = null;
|
||||
let bestScore = -Infinity;
|
||||
|
|
@ -173,11 +184,16 @@ export class SwarmStrategy {
|
|||
|
||||
// Bonus for moving toward nearby enemies (engagement)
|
||||
let nearestEnemyDist = Infinity;
|
||||
let currentNearestEnemyDist = Infinity;
|
||||
for (const enemy of enemyPositions.values()) {
|
||||
const dist = distance2(newPos, enemy.position, rows, cols);
|
||||
nearestEnemyDist = Math.min(nearestEnemyDist, dist);
|
||||
const currentDist = distance2(bot.position, enemy.position, rows, cols);
|
||||
currentNearestEnemyDist = Math.min(currentNearestEnemyDist, currentDist);
|
||||
}
|
||||
if (nearestEnemyDist < Infinity) {
|
||||
// Bonus for getting closer to enemies (encourages active engagement)
|
||||
score += (currentNearestEnemyDist - nearestEnemyDist) * 5;
|
||||
// Bonus for being in attack range
|
||||
if (nearestEnemyDist <= config.attack_radius2) {
|
||||
score += 50;
|
||||
|
|
@ -198,6 +214,42 @@ export class SwarmStrategy {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move toward a target position, avoiding walls
|
||||
*/
|
||||
private moveTowardPosition(
|
||||
bot: VisibleBot,
|
||||
target: Position,
|
||||
walls: Set<string>,
|
||||
rows: number,
|
||||
cols: number
|
||||
): Move | null {
|
||||
let bestDir: Direction | null = null;
|
||||
let bestDist2 = Infinity;
|
||||
|
||||
for (const dir of ALL_DIRECTIONS) {
|
||||
const newPos = moveToward(bot.position, dir, rows, cols);
|
||||
const newPosKey = posKey(newPos);
|
||||
|
||||
// Can't move into walls
|
||||
if (walls.has(newPosKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dist2 = distance2(newPos, target, rows, cols);
|
||||
if (dist2 < bestDist2) {
|
||||
bestDist2 = dist2;
|
||||
bestDir = dir;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestDir) {
|
||||
return { position: bot.position, direction: bestDir };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if moving to newPos maintains cohesion with friendly bots
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue