ai-code-battle/cmd/acb-evolver/internal/db/seeds/hunter_strategy.java.txt
jedarden 5669688984 Add validation pipeline, sandbox, and evolution DB layer (Phase 7)
Three-stage fail-fast validator for LLM-generated bot candidates:
- syntax.go: language-aware parse (go/parser for Go; py_compile, rustfmt,
  tsc, javac, php -l for others; brace-balance fallback)
- schema.go: regex detection of /health + /turn endpoints and "moves" field
- sandbox.go: nsjail-isolated smoke test — builds bot, polls /health, sends
  5 signed /turn requests, verifies JSON moves responses
- validator.go: orchestrates stages with fail-fast short-circuit

DB layer:
- programs table + CRUD (create, get, list, updateFitness, setPromoted)
- validation_log table with RecordValidation, IslandPassRates,
  IslandValidationStats for per-island pass-rate tracking
- seed.go: 6 generation-0 bots across alpha/beta/gamma/delta islands

MAP-Elites grid (mapelites/grid.go): 2-D behavior grid on aggression×economy
axes; TryPlace keeps the fittest occupant per niche.

acb-evolver CLI gains two new subcommands:
  validate <file> -lang <lang> [-island <island>] [-nsjail] [-nolog]
  validation-stats (tabular per-island pass-rate breakdown)

cmd/acb-api/db.go: add programs table to API schema so the API can query
promoted evolved bots.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 22:45:13 -04:00

394 lines
13 KiB
Text

package com.acb.hunter;
import java.util.*;
import java.util.stream.Collectors;
/**
* HunterBot strategy: target isolated enemies for efficient kills.
*
* Strategy: Target isolated enemy bots for efficient kills.
* - Identify enemy bots that are >=4 tiles from their nearest friendly bot (isolated targets)
* - Send pairs of bots to intercept isolated enemies (2v1 wins cleanly)
* - If no isolated targets, default to gatherer behavior
* - Maintain a map of known enemy positions across turns, predict movement
* - Avoid engaging formations of 3+ enemy bots
* - Opportunistic energy collection when not actively hunting
*/
public class HunterStrategy {
private static final int ISOLATION_THRESHOLD = 16; // Squared distance (4 tiles)
private static final int FORMATION_SIZE = 3; // Avoid groups of 3+ enemies
// Track known enemy positions for prediction
private final Map<String, EnemyTracker> enemyTrackers = new HashMap<>();
/**
* Compute moves for all owned bots
*/
public List<Move> computeMoves(GameState state) {
int myId = state.getYou().getId();
GameConfig config = state.getConfig();
int rows = config.getRows();
int cols = config.getCols();
// Separate my bots from enemies
List<VisibleBot> myBots = new ArrayList<>();
List<VisibleBot> enemyBots = new ArrayList<>();
for (VisibleBot bot : state.getBots()) {
if (bot.getOwner() == myId) {
myBots.add(bot);
} else {
enemyBots.add(bot);
}
}
if (myBots.isEmpty()) {
return Collections.emptyList();
}
// Update enemy trackers
updateEnemyTrackers(enemyBots, rows, cols);
// Build position lookups
Set<String> walls = buildPositionSet(state.getWalls());
Set<String> enemyPositions = buildPositionSet(
enemyBots.stream().map(VisibleBot::getPosition).collect(Collectors.toList())
);
Set<String> myBotPositions = buildPositionSet(
myBots.stream().map(VisibleBot::getPosition).collect(Collectors.toList())
);
// Find isolated enemy targets
List<VisibleBot> isolatedEnemies = findIsolatedEnemies(enemyBots, rows, cols);
// Find energy positions
Set<String> energyPositions = buildPositionSet(state.getEnergy());
// Assign bots to targets
List<Move> moves = new ArrayList<>();
Set<String> usedEnergy = new HashSet<>();
Set<Position> assignedTargets = new HashSet<>();
// First, assign hunters to isolated enemies
Map<VisibleBot, VisibleBot> hunterAssignments = assignHunters(myBots, isolatedEnemies, rows, cols);
for (Map.Entry<VisibleBot, VisibleBot> entry : hunterAssignments.entrySet()) {
VisibleBot hunter = entry.getKey();
VisibleBot target = entry.getValue();
// Get predicted position of target
Position predictedPos = predictPosition(target, rows, cols);
assignedTargets.add(predictedPos);
Move move = computeHunterMove(hunter, predictedPos, enemyPositions, walls, myBotPositions, rows, cols);
if (move != null) {
moves.add(move);
// Mark this bot as assigned
myBotPositions.remove(hunter.getPosition().key());
}
}
// Second, assign remaining bots to gather or explore
for (VisibleBot bot : myBots) {
if (!myBotPositions.contains(bot.getPosition().key())) {
continue; // Already assigned
}
Move move;
if (!energyPositions.isEmpty()) {
move = computeGatherMove(bot, energyPositions, usedEnergy, enemyPositions, walls, rows, cols);
} else {
move = computeExploreMove(bot, enemyPositions, walls, rows, cols);
}
if (move != null) {
moves.add(move);
}
}
return moves;
}
/**
* Update enemy position trackers for prediction
*/
private void updateEnemyTrackers(List<VisibleBot> enemyBots, int rows, int cols) {
for (VisibleBot bot : enemyBots) {
String key = bot.getPosition().key();
EnemyTracker tracker = enemyTrackers.computeIfAbsent(key, k -> new EnemyTracker());
tracker.update(bot.getPosition(), rows, cols);
}
}
/**
* Find isolated enemy bots (>=4 tiles from nearest friendly)
*/
private List<VisibleBot> findIsolatedEnemies(List<VisibleBot> enemyBots, int rows, int cols) {
List<VisibleBot> isolated = new ArrayList<>();
for (VisibleBot bot : enemyBots) {
boolean isIsolated = true;
int nearestDist = Integer.MAX_VALUE;
for (VisibleBot other : enemyBots) {
if (bot == other) continue;
int dist = bot.getPosition().distance2(other.getPosition(), rows, cols);
nearestDist = Math.min(nearestDist, dist);
}
// Isolated if nearest friendly is >= 4 tiles away (squared distance 16)
// or if it's the only enemy bot
if (nearestDist >= ISOLATION_THRESHOLD || enemyBots.size() == 1) {
isolated.add(bot);
}
}
return isolated;
}
/**
* Assign hunters to isolated targets using greedy matching
*/
private Map<VisibleBot, VisibleBot> assignHunters(
List<VisibleBot> myBots,
List<VisibleBot> isolatedEnemies,
int rows, int cols
) {
Map<VisibleBot, VisibleBot> assignments = new HashMap<>();
if (isolatedEnemies.isEmpty()) {
return assignments;
}
// Sort my bots by distance to nearest isolated enemy
List<VisibleBot> availableHunters = new ArrayList<>(myBots);
// Assign 2 hunters per target when possible
for (VisibleBot target : isolatedEnemies) {
int huntersNeeded = 2;
// Sort available hunters by distance to target
availableHunters.sort((a, b) -> {
int distA = a.getPosition().distance2(target.getPosition(), rows, cols);
int distB = b.getPosition().distance2(target.getPosition(), rows, cols);
return Integer.compare(distA, distB);
});
int assigned = 0;
Iterator<VisibleBot> iter = availableHunters.iterator();
while (iter.hasNext() && assigned < huntersNeeded) {
VisibleBot hunter = iter.next();
assignments.put(hunter, target);
iter.remove();
assigned++;
}
}
return assignments;
}
/**
* Predict where an enemy will be next turn
*/
private Position predictPosition(VisibleBot enemy, int rows, int cols) {
String key = enemy.getPosition().key();
EnemyTracker tracker = enemyTrackers.get(key);
if (tracker != null && tracker.hasPrediction()) {
return tracker.predictNextPosition(rows, cols);
}
return enemy.getPosition();
}
/**
* Compute move for a hunter bot toward a target
*/
private Move computeHunterMove(
VisibleBot bot,
Position target,
Set<String> enemyPositions,
Set<String> walls,
Set<String> myBotPositions,
int rows, int cols
) {
Direction bestDir = null;
int bestScore = Integer.MIN_VALUE;
for (Direction dir : Direction.all()) {
Position newPos = bot.getPosition().moveToward(dir, rows, cols);
String newPosKey = newPos.key();
// Can't move into walls
if (walls.contains(newPosKey)) {
continue;
}
// Avoid self-collision
if (myBotPositions.contains(newPosKey)) {
continue;
}
// Score: prefer getting closer to target
int distToTarget = newPos.distance2(target, rows, cols);
int currentDistToTarget = bot.getPosition().distance2(target, rows, cols);
int score = currentDistToTarget - distToTarget;
// Bonus for being in attack range of target
if (distToTarget <= 5) { // attack_radius2
score += 20;
}
// Penalty for moving adjacent to multiple enemies
int adjacentEnemies = 0;
for (String enemyPosKey : enemyPositions) {
String[] parts = enemyPosKey.split(",");
Position enemyPos = new Position(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
if (newPos.distance2(enemyPos, rows, cols) <= 2) {
adjacentEnemies++;
}
}
score -= adjacentEnemies * 10;
if (score > bestScore) {
bestScore = score;
bestDir = dir;
}
}
if (bestDir != null) {
return new Move(bot.getPosition(), bestDir);
}
return null;
}
/**
* Compute move for a gatherer bot
*/
private Move computeGatherMove(
VisibleBot bot,
Set<String> energyPositions,
Set<String> usedEnergy,
Set<String> enemyPositions,
Set<String> walls,
int rows, int cols
) {
// Find nearest untargeted energy
Position nearestEnergy = null;
int nearestDist = Integer.MAX_VALUE;
for (String energyKey : energyPositions) {
if (usedEnergy.contains(energyKey)) continue;
String[] parts = energyKey.split(",");
Position energyPos = new Position(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
int dist = bot.getPosition().distance2(energyPos, rows, cols);
if (dist < nearestDist) {
nearestDist = dist;
nearestEnergy = energyPos;
}
}
if (nearestEnergy != null) {
usedEnergy.add(nearestEnergy.key());
return computeMoveToward(bot, nearestEnergy, walls, rows, cols);
}
return null;
}
/**
* Compute move for exploration
*/
private Move computeExploreMove(
VisibleBot bot,
Set<String> enemyPositions,
Set<String> walls,
int rows, int cols
) {
// Move toward center if no other target
Position center = new Position(rows / 2, cols / 2);
return computeMoveToward(bot, center, walls, rows, cols);
}
/**
* Compute move toward a target position
*/
private Move computeMoveToward(VisibleBot bot, Position target, Set<String> walls, int rows, int cols) {
Direction bestDir = null;
int bestDist = Integer.MAX_VALUE;
for (Direction dir : Direction.all()) {
Position newPos = bot.getPosition().moveToward(dir, rows, cols);
if (walls.contains(newPos.key())) {
continue;
}
int dist = newPos.distance2(target, rows, cols);
if (dist < bestDist) {
bestDist = dist;
bestDir = dir;
}
}
if (bestDir != null) {
return new Move(bot.getPosition(), bestDir);
}
return null;
}
/**
* Build a set of position keys for O(1) lookup
*/
private Set<String> buildPositionSet(List<Position> positions) {
return positions.stream()
.map(Position::key)
.collect(Collectors.toSet());
}
}
/**
* Tracks enemy position history for movement prediction
*/
class EnemyTracker {
private Position lastPosition;
private Position currentPosition;
private int sightings;
public void update(Position position, int rows, int cols) {
lastPosition = currentPosition;
currentPosition = position;
sightings++;
}
public boolean hasPrediction() {
return lastPosition != null && currentPosition != null;
}
public Position predictNextPosition(int rows, int cols) {
if (!hasPrediction()) {
return currentPosition;
}
// Simple prediction: continue in same direction
int dr = currentPosition.getRow() - lastPosition.getRow();
int dc = currentPosition.getCol() - lastPosition.getCol();
// Handle wrap
if (dr > rows / 2) dr -= rows;
if (dr < -rows / 2) dr += rows;
if (dc > cols / 2) dc -= cols;
if (dc < -cols / 2) dc += cols;
// Predict next position
int newRow = (currentPosition.getRow() + dr + rows) % rows;
int newCol = (currentPosition.getCol() + dc + cols) % cols;
return new Position(newRow, newCol);
}
}