feat(bot): add Raider bot (Java) — hit-and-run harasser archetype

Implements a hit-and-run strategy that scouts for lone enemy bots,
attacks from flanking positions, then retreats after 1-2 engagement
turns to avoid reinforcements. Defends own core when under pressure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-22 16:44:34 -04:00
parent ed4be7a4d3
commit 53377d577f
5 changed files with 943 additions and 0 deletions

20
bots/raider/Dockerfile Normal file
View file

@ -0,0 +1,20 @@
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY pom.xml ./
COPY src ./src
RUN apk add --no-cache maven && \
mvn clean package -DskipTests
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/raider-bot-1.0.0.jar /app/raider-bot.jar
ENV BOT_PORT=8086
ENV BOT_SECRET=""
EXPOSE 8086
CMD ["java", "-jar", "raider-bot.jar"]

75
bots/raider/pom.xml Normal file
View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acb</groupId>
<artifactId>raider-bot</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>RaiderBot</name>
<description>Hit-and-run harasser strategy bot for AI Code Battle</description>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javalin.version>6.3.0</javalin.version>
<jackson.version>2.17.0</jackson.version>
</properties>
<dependencies>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>${javalin.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.acb.raider.App</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,127 @@
package com.acb.raider;
import io.javalin.Javalin;
import io.javalin.http.Context;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
/**
* RaiderBot - Hit-and-run harasser archetype.
*
* Strategy: Attack weak targets, disengage before reinforcements arrive.
* - Units scout for lone enemy bots (no allies within 2 cells)
* - On finding one, attack from flank
* - After 1-2 attack turns, retreat regardless of outcome
* - Never attack groups of >=3 enemies
* - Home base rotates: if own core under pressure, abandon raid and defend
*/
public class App {
private static final int DEFAULT_PORT = 8086;
private static String SECRET;
private static final RaiderStrategy STRATEGY = new RaiderStrategy();
public static void main(String[] args) {
String portStr = System.getenv("BOT_PORT");
int port = portStr != null ? Integer.parseInt(portStr) : DEFAULT_PORT;
SECRET = System.getenv("BOT_SECRET");
if (SECRET == null || SECRET.isEmpty()) {
System.err.println("ERROR: BOT_SECRET environment variable is required");
System.exit(1);
}
Javalin app = Javalin.create();
app.get("/health", ctx -> ctx.result("OK"));
app.post("/turn", App::handleTurn);
app.start(port);
System.out.println("RaiderBot starting on port " + port);
}
private static void handleTurn(Context ctx) {
String matchId = ctx.header("X-ACB-Match-Id");
String turnStr = ctx.header("X-ACB-Turn");
String timestamp = ctx.header("X-ACB-Timestamp");
String signature = ctx.header("X-ACB-Signature");
if (matchId == null || turnStr == null || timestamp == null || signature == null) {
ctx.status(401).result("Missing auth headers");
return;
}
String body = ctx.body();
if (!verifySignature(SECRET, matchId, turnStr, timestamp, body, signature)) {
ctx.status(401).result("Invalid signature");
return;
}
GameState state;
try {
state = GameState.fromJson(body);
} catch (Exception e) {
ctx.status(400).result("Invalid JSON: " + e.getMessage());
return;
}
var moves = STRATEGY.computeMoves(state);
int turn = Integer.parseInt(turnStr);
System.out.println("Turn " + turn + ": " + moves.size() + " moves computed");
String responseBody = MoveResponse.toJson(moves);
String responseSig = signResponse(SECRET, matchId, turn, responseBody);
ctx.header("X-ACB-Signature", responseSig);
ctx.contentType("application/json");
ctx.result(responseBody);
}
private static boolean verifySignature(String secret, String matchId, String turn,
String timestamp, String body, String signature) {
try {
String bodyHash = sha256Hex(body);
String signingString = matchId + "." + turn + "." + timestamp + "." + bodyHash;
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(keySpec);
byte[] expected = mac.doFinal(signingString.getBytes(StandardCharsets.UTF_8));
return MessageDigest.isEqual(
HexFormat.of().parseHex(signature),
expected
);
} catch (Exception e) {
return false;
}
}
private static String signResponse(String secret, String matchId, int turn, String body) {
try {
String bodyHash = sha256Hex(body);
String signingString = matchId + "." + turn + "." + bodyHash;
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(keySpec);
return HexFormat.of().formatHex(mac.doFinal(signingString.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new RuntimeException("Failed to sign response", e);
}
}
private static String sha256Hex(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(digest.digest(input.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new RuntimeException("Failed to hash", e);
}
}
}

View file

@ -0,0 +1,195 @@
package com.acb.raider;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature;
import java.util.List;
import java.util.Collections;
public class GameState {
private static final ObjectMapper MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@JsonProperty("match_id")
private String matchId;
private int turn;
private GameConfig config;
private PlayerInfo you;
private List<VisibleBot> bots = Collections.emptyList();
private List<Position> energy = Collections.emptyList();
private List<VisibleCore> cores = Collections.emptyList();
private List<Position> walls = Collections.emptyList();
private List<VisibleBot> dead = Collections.emptyList();
public String getMatchId() { return matchId; }
public int getTurn() { return turn; }
public GameConfig getConfig() { return config; }
public PlayerInfo getYou() { return you; }
public List<VisibleBot> getBots() { return bots; }
public List<Position> getEnergy() { return energy; }
public List<VisibleCore> getCores() { return cores; }
public List<Position> getWalls() { return walls; }
public List<VisibleBot> getDead() { return dead; }
public static GameState fromJson(String json) throws Exception {
return MAPPER.readValue(json, GameState.class);
}
}
class GameConfig {
private int rows;
private int cols;
@JsonProperty("max_turns")
private int maxTurns;
@JsonProperty("vision_radius2")
private int visionRadius2;
@JsonProperty("attack_radius2")
private int attackRadius2;
@JsonProperty("spawn_cost")
private int spawnCost;
@JsonProperty("energy_interval")
private int energyInterval;
public int getRows() { return rows; }
public int getCols() { return cols; }
public int getMaxTurns() { return maxTurns; }
public int getVisionRadius2() { return visionRadius2; }
public int getAttackRadius2() { return attackRadius2; }
public int getSpawnCost() { return spawnCost; }
public int getEnergyInterval() { return energyInterval; }
}
class PlayerInfo {
private int id;
private int energy;
private int score;
public int getId() { return id; }
public int getEnergy() { return energy; }
public int getScore() { return score; }
}
class Position {
private int row;
private int col;
public Position() {}
public Position(int row, int col) {
this.row = row;
this.col = col;
}
public int getRow() { return row; }
public int getCol() { return col; }
public Position moveToward(Direction dir, int rows, int cols) {
return switch (dir) {
case N -> new Position((row - 1 + rows) % rows, col);
case E -> new Position(row, (col + 1) % cols);
case S -> new Position((row + 1) % rows, col);
case W -> new Position(row, (col - 1 + cols) % cols);
};
}
public int distance2(Position other, int rows, int cols) {
int dr = Math.abs(row - other.row);
int dc = Math.abs(col - other.col);
dr = Math.min(dr, rows - dr);
dc = Math.min(dc, cols - dc);
return dr * dr + dc * dc;
}
public int manhattanDistance(Position other, int rows, int cols) {
int dr = Math.abs(row - other.row);
int dc = Math.abs(col - other.col);
dr = Math.min(dr, rows - dr);
dc = Math.min(dc, cols - dc);
return dr + dc;
}
public String key() {
return row + "," + col;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Position position = (Position) o;
return row == position.row && col == position.col;
}
@Override
public int hashCode() {
return 31 * row + col;
}
}
class VisibleBot {
private Position position;
private int owner;
public Position getPosition() { return position; }
public int getOwner() { return owner; }
}
class VisibleCore {
private Position position;
private int owner;
private boolean active;
public Position getPosition() { return position; }
public int getOwner() { return owner; }
public boolean isActive() { return active; }
}
enum Direction {
N, E, S, W;
public static Direction[] all() {
return values();
}
}
class Move {
private final Position position;
private final Direction direction;
public Move(Position position, Direction direction) {
this.position = position;
this.direction = direction;
}
public Position getPosition() { return position; }
public Direction getDirection() { return direction; }
}
class MoveResponse {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final List<Move> moves;
public MoveResponse(List<Move> moves) {
this.moves = moves;
}
public List<Move> getMoves() { return moves; }
public static String toJson(List<Move> moves) {
try {
var response = new MoveResponse(moves);
return MAPPER.writeValueAsString(response);
} catch (Exception e) {
throw new RuntimeException("Failed to serialize moves", e);
}
}
}

View file

@ -0,0 +1,526 @@
package com.acb.raider;
import java.util.*;
import java.util.stream.Collectors;
/**
* RaiderBot strategy: hit-and-run harassment.
*
* - Scouts for lone enemy bots (no allies within 2 cells)
* - Attacks isolated targets from flank
* - After 1-2 attack turns, retreats regardless of outcome
* - Never attacks groups of >=3 enemies
* - Defends own core if under pressure
*/
public class RaiderStrategy {
private static final int GROUP_AVOID_THRESHOLD = 3;
private static final int MAX_ENGAGEMENT_TURNS = 2;
private static final int ISOLATION_MANHATTAN = 4; // 2 cells squared-distance-ish radius
// Per-bot engagement tracking
private final Map<String, EngagementTracker> engagementTrackers = new HashMap<>();
public List<Move> computeMoves(GameState state) {
int myId = state.getYou().getId();
GameConfig config = state.getConfig();
int rows = config.getRows();
int cols = config.getCols();
int attackR2 = config.getAttackRadius2();
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();
}
Set<String> walls = buildPositionSet(state.getWalls());
Set<String> enemyPositions = buildPositionSet(
enemyBots.stream().map(VisibleBot::getPosition).collect(Collectors.toList())
);
Set<String> energyPositions = buildPositionSet(state.getEnergy());
// Identify own active cores
List<Position> myCores = new ArrayList<>();
for (VisibleCore core : state.getCores()) {
if (core.getOwner() == myId && core.isActive()) {
myCores.add(core.getPosition());
}
}
// Check if own core is under pressure
boolean coreUnderPressure = isCoreUnderPressure(myCores, enemyBots, rows, cols);
// Find lone enemy bots
List<VisibleBot> loneEnemies = findLoneEnemies(enemyBots, rows, cols);
// Build assignments
List<Move> moves = new ArrayList<>();
Set<String> assignedBots = new HashSet<>();
Set<String> claimedDests = new HashSet<>();
if (coreUnderPressure && !myCores.isEmpty()) {
// Core defense: bots near core intercept enemies, others continue raiding
List<VisibleBot> defenders = new ArrayList<>();
List<VisibleBot> raiders = new ArrayList<>();
for (VisibleBot bot : myBots) {
int nearestCoreDist = minDistToAny(bot.getPosition(), myCores, rows, cols);
if (nearestCoreDist <= 25) { // within ~5 tiles of a core
defenders.add(bot);
} else {
raiders.add(bot);
}
}
// Defenders move toward nearest enemy near core
for (VisibleBot defender : defenders) {
Position target = nearestEnemyToCore(myCores, enemyBots, rows, cols);
Move move = computeDefendMove(defender, target, walls, enemyPositions,
claimedDests, rows, cols);
if (move != null) {
assignedBots.add(defender.getPosition().key());
Position dest = defender.getPosition().moveToward(move.getDirection(), rows, cols);
claimedDests.add(dest.key());
moves.add(move);
}
}
// Raiders continue hit-and-run
myBots = raiders;
}
// Assign raiders to lone enemy targets
Map<VisibleBot, VisibleBot> raidAssignments = assignRaiders(myBots, loneEnemies,
assignedBots, rows, cols);
for (Map.Entry<VisibleBot, VisibleBot> entry : raidAssignments.entrySet()) {
VisibleBot raider = entry.getKey();
VisibleBot target = entry.getValue();
String trackerKey = raider.getPosition().key() + "->" + target.getPosition().key();
EngagementTracker tracker = engagementTrackers.computeIfAbsent(
trackerKey, k -> new EngagementTracker());
Move move;
if (tracker.engagementTurns >= MAX_ENGAGEMENT_TURNS) {
// Retreat after max engagement turns
move = computeRetreatMove(raider, target.getPosition(), walls, enemyPositions,
claimedDests, rows, cols);
if (tracker.engagementTurns >= MAX_ENGAGEMENT_TURNS + 2) {
tracker.engagementTurns = 0; // reset after sufficient retreat
}
} else {
// Attack the lone target
move = computeAttackMove(raider, target.getPosition(), walls, enemyPositions,
claimedDests, rows, cols, attackR2);
}
if (move != null) {
assignedBots.add(raider.getPosition().key());
Position dest = raider.getPosition().moveToward(move.getDirection(), rows, cols);
claimedDests.add(dest.key());
moves.add(move);
tracker.engagementTurns++;
}
}
// Remaining bots: gather energy or explore
for (VisibleBot bot : myBots) {
if (assignedBots.contains(bot.getPosition().key())) continue;
Move move;
if (!state.getEnergy().isEmpty()) {
move = computeGatherMove(bot, energyPositions, walls, enemyPositions,
claimedDests, rows, cols);
} else {
move = computeExploreMove(bot, walls, claimedDests, rows, cols);
}
if (move != null) {
Position dest = bot.getPosition().moveToward(move.getDirection(), rows, cols);
if (!claimedDests.contains(dest.key())) {
claimedDests.add(dest.key());
moves.add(move);
}
}
}
return moves;
}
/**
* Find lone enemies: no allied bots within ~2 manhattan cells
*/
private List<VisibleBot> findLoneEnemies(List<VisibleBot> enemyBots, int rows, int cols) {
List<VisibleBot> loneEnemies = new ArrayList<>();
for (VisibleBot bot : enemyBots) {
int nearbyAllies = 0;
// Count enemies within attack_radius2 (same owner = allies of this enemy)
for (VisibleBot other : enemyBots) {
if (bot == other) continue;
int dist2 = bot.getPosition().distance2(other.getPosition(), rows, cols);
if (dist2 <= 4) { // ~2 cells squared
nearbyAllies++;
}
}
// Lone if no nearby allies and check group size
int localGroupSize = countLocalGroup(bot, enemyBots, rows, cols);
if (nearbyAllies == 0 && localGroupSize < GROUP_AVOID_THRESHOLD) {
loneEnemies.add(bot);
}
}
return loneEnemies;
}
/**
* Count how many enemy bots are clustered near this bot (within ~4 manhattan tiles)
*/
private int countLocalGroup(VisibleBot bot, List<VisibleBot> enemyBots, int rows, int cols) {
int count = 1; // include self
for (VisibleBot other : enemyBots) {
if (bot == other) continue;
int dist = bot.getPosition().manhattanDistance(other.getPosition(), rows, cols);
if (dist <= ISOLATION_MANHATTAN) {
count++;
}
}
return count;
}
/**
* Check if any enemy is close to our core
*/
private boolean isCoreUnderPressure(List<Position> myCores, List<VisibleBot> enemyBots,
int rows, int cols) {
for (Position core : myCores) {
for (VisibleBot enemy : enemyBots) {
if (core.distance2(enemy.getPosition(), rows, cols) <= 36) { // ~6 tiles
return true;
}
}
}
return false;
}
/**
* Find nearest enemy to any of our cores
*/
private Position nearestEnemyToCore(List<Position> myCores, List<VisibleBot> enemyBots,
int rows, int cols) {
Position nearest = null;
int bestDist = Integer.MAX_VALUE;
for (Position core : myCores) {
for (VisibleBot enemy : enemyBots) {
int dist = core.distance2(enemy.getPosition(), rows, cols);
if (dist < bestDist) {
bestDist = dist;
nearest = enemy.getPosition();
}
}
}
return nearest;
}
/**
* Assign raiders to lone targets using greedy closest-first matching
*/
private Map<VisibleBot, VisibleBot> assignRaiders(List<VisibleBot> myBots,
List<VisibleBot> loneEnemies,
Set<String> alreadyAssigned,
int rows, int cols) {
Map<VisibleBot, VisibleBot> assignments = new HashMap<>();
if (loneEnemies.isEmpty()) return assignments;
List<VisibleBot> available = myBots.stream()
.filter(b -> !alreadyAssigned.contains(b.getPosition().key()))
.collect(Collectors.toList());
// Sort targets by isolation (most isolated first easier prey)
loneEnemies.sort((a, b) -> {
int groupA = countLocalGroup(a, Collections.emptyList(), rows, cols); // 1 always
int groupB = countLocalGroup(b, Collections.emptyList(), rows, cols);
return Integer.compare(groupA, groupB);
});
for (VisibleBot target : loneEnemies) {
if (available.isEmpty()) break;
// Assign 1-2 raiders per target
available.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 = available.iterator();
while (iter.hasNext() && assigned < 2) {
VisibleBot raider = iter.next();
assignments.put(raider, target);
iter.remove();
assigned++;
}
}
return assignments;
}
/**
* Compute attack move toward a target, using flanking when possible.
* Approach from an offset angle rather than head-on.
*/
private Move computeAttackMove(VisibleBot bot, Position target, Set<String> walls,
Set<String> enemyPositions, Set<String> claimedDests,
int rows, int cols, int attackR2) {
Direction bestDir = null;
int bestScore = Integer.MIN_VALUE;
for (Direction dir : Direction.all()) {
Position newPos = bot.getPosition().moveToward(dir, rows, cols);
String key = newPos.key();
if (walls.contains(key)) continue;
if (claimedDests.contains(key)) continue;
int score = 0;
int distToTarget = newPos.distance2(target, rows, cols);
int currentDist = bot.getPosition().distance2(target, rows, cols);
// Reward getting closer
score += (currentDist - distToTarget) * 10;
// Big bonus for entering attack range
if (distToTarget <= attackR2) {
score += 50;
}
// Flanking: prefer approaching from sides (perpendicular offset)
// rather than directly head-on
int dr = newPos.getRow() - target.getRow();
int dc = newPos.getCol() - target.getCol();
// Wrap differences
if (Math.abs(dr) > rows / 2) dr = dr > 0 ? dr - rows : dr + rows;
if (Math.abs(dc) > cols / 2) dc = dc > 0 ? dc - cols : dc + cols;
// Diagonal approach is flanking
if (dr != 0 && dc != 0) {
score += 5; // prefer diagonal/flanking approach
}
// Penalty for nearby enemies (more than the target)
int nearbyEnemies = 0;
for (String epk : enemyPositions) {
if (epk.equals(target.key())) continue;
String[] parts = epk.split(",");
Position ep = new Position(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
if (newPos.distance2(ep, rows, cols) <= attackR2) {
nearbyEnemies++;
}
}
score -= nearbyEnemies * 30; // heavy penalty for enemy reinforcements
if (score > bestScore) {
bestScore = score;
bestDir = dir;
}
}
if (bestDir != null) {
return new Move(bot.getPosition(), bestDir);
}
return null;
}
/**
* Compute retreat move away from a target
*/
private Move computeRetreatMove(VisibleBot bot, Position target, Set<String> walls,
Set<String> enemyPositions, Set<String> claimedDests,
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 key = newPos.key();
if (walls.contains(key)) continue;
if (claimedDests.contains(key)) continue;
int distFromTarget = newPos.distance2(target, rows, cols);
// Maximize distance from target
int score = distFromTarget;
// Penalty for being near any other enemy
for (String epk : enemyPositions) {
String[] parts = epk.split(",");
Position ep = new Position(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
if (newPos.distance2(ep, rows, cols) <= 5) {
score -= 20;
}
}
// Reward moving toward nearest energy (opportunistic while retreating)
// (energy positions not passed here for simplicity)
if (score > bestScore) {
bestScore = score;
bestDir = dir;
}
}
if (bestDir != null) {
return new Move(bot.getPosition(), bestDir);
}
return null;
}
/**
* Compute defensive move: intercept enemy approaching core
*/
private Move computeDefendMove(VisibleBot bot, Position target, Set<String> walls,
Set<String> enemyPositions, Set<String> claimedDests,
int rows, int cols) {
if (target == null) return null;
Direction bestDir = null;
int bestDist = Integer.MAX_VALUE;
for (Direction dir : Direction.all()) {
Position newPos = bot.getPosition().moveToward(dir, rows, cols);
String key = newPos.key();
if (walls.contains(key)) continue;
if (claimedDests.contains(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;
}
/**
* Compute energy-gathering move toward nearest energy
*/
private Move computeGatherMove(VisibleBot bot, Set<String> energyPositions,
Set<String> walls, Set<String> enemyPositions,
Set<String> claimedDests, int rows, int cols) {
Position nearestEnergy = null;
int nearestDist = Integer.MAX_VALUE;
for (String ek : energyPositions) {
String[] parts = ek.split(",");
Position ep = new Position(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
int dist = bot.getPosition().distance2(ep, rows, cols);
if (dist < nearestDist) {
nearestDist = dist;
nearestEnergy = ep;
}
}
if (nearestEnergy != null) {
Direction bestDir = null;
int bestDist = Integer.MAX_VALUE;
for (Direction dir : Direction.all()) {
Position newPos = bot.getPosition().moveToward(dir, rows, cols);
String key = newPos.key();
if (walls.contains(key)) continue;
if (claimedDests.contains(key)) continue;
int dist = newPos.distance2(nearestEnergy, rows, cols);
// Avoid enemies while gathering
for (String epk : enemyPositions) {
String[] parts = epk.split(",");
Position ep = new Position(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
if (newPos.distance2(ep, rows, cols) <= 5) {
dist += 20; // penalty
}
}
if (dist < bestDist) {
bestDist = dist;
bestDir = dir;
}
}
if (bestDir != null) {
return new Move(bot.getPosition(), bestDir);
}
}
return null;
}
/**
* Explore: move toward grid center or away from friendly bots
*/
private Move computeExploreMove(VisibleBot bot, Set<String> walls,
Set<String> claimedDests, int rows, int cols) {
Position center = new Position(rows / 2, cols / 2);
Direction bestDir = null;
int bestDist = Integer.MAX_VALUE;
for (Direction dir : Direction.all()) {
Position newPos = bot.getPosition().moveToward(dir, rows, cols);
String key = newPos.key();
if (walls.contains(key)) continue;
if (claimedDests.contains(key)) continue;
int dist = newPos.distance2(center, rows, cols);
if (dist < bestDist) {
bestDist = dist;
bestDir = dir;
}
}
if (bestDir != null) {
return new Move(bot.getPosition(), bestDir);
}
return null;
}
private int minDistToAny(Position pos, List<Position> targets, int rows, int cols) {
int minDist = Integer.MAX_VALUE;
for (Position t : targets) {
minDist = Math.min(minDist, pos.distance2(t, rows, cols));
}
return minDist;
}
private Set<String> buildPositionSet(List<Position> positions) {
return positions.stream()
.map(Position::key)
.collect(Collectors.toSet());
}
/**
* Tracks how long a raider has been engaged with a target
*/
private static class EngagementTracker {
int engagementTurns = 0;
}
}