diff --git a/bots/raider/Dockerfile b/bots/raider/Dockerfile
new file mode 100644
index 0000000..e8b524a
--- /dev/null
+++ b/bots/raider/Dockerfile
@@ -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"]
diff --git a/bots/raider/pom.xml b/bots/raider/pom.xml
new file mode 100644
index 0000000..4cbc579
--- /dev/null
+++ b/bots/raider/pom.xml
@@ -0,0 +1,75 @@
+
+
+ 4.0.0
+
+ com.acb
+ raider-bot
+ 1.0.0
+ jar
+
+ RaiderBot
+ Hit-and-run harasser strategy bot for AI Code Battle
+
+
+ 21
+ 21
+ UTF-8
+ 6.3.0
+ 2.17.0
+
+
+
+
+ io.javalin
+ javalin
+ ${javalin.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.12
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.2
+
+
+ package
+
+ shade
+
+
+
+
+ com.acb.raider.App
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+
+
diff --git a/bots/raider/src/main/java/com/acb/raider/App.java b/bots/raider/src/main/java/com/acb/raider/App.java
new file mode 100644
index 0000000..685cd60
--- /dev/null
+++ b/bots/raider/src/main/java/com/acb/raider/App.java
@@ -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);
+ }
+ }
+}
diff --git a/bots/raider/src/main/java/com/acb/raider/GameState.java b/bots/raider/src/main/java/com/acb/raider/GameState.java
new file mode 100644
index 0000000..d82ed53
--- /dev/null
+++ b/bots/raider/src/main/java/com/acb/raider/GameState.java
@@ -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 bots = Collections.emptyList();
+ private List energy = Collections.emptyList();
+ private List cores = Collections.emptyList();
+ private List walls = Collections.emptyList();
+ private List 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 getBots() { return bots; }
+ public List getEnergy() { return energy; }
+ public List getCores() { return cores; }
+ public List getWalls() { return walls; }
+ public List 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 moves;
+
+ public MoveResponse(List moves) {
+ this.moves = moves;
+ }
+
+ public List getMoves() { return moves; }
+
+ public static String toJson(List moves) {
+ try {
+ var response = new MoveResponse(moves);
+ return MAPPER.writeValueAsString(response);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to serialize moves", e);
+ }
+ }
+}
diff --git a/bots/raider/src/main/java/com/acb/raider/RaiderStrategy.java b/bots/raider/src/main/java/com/acb/raider/RaiderStrategy.java
new file mode 100644
index 0000000..8929289
--- /dev/null
+++ b/bots/raider/src/main/java/com/acb/raider/RaiderStrategy.java
@@ -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 engagementTrackers = new HashMap<>();
+
+ public List 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 myBots = new ArrayList<>();
+ List 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 walls = buildPositionSet(state.getWalls());
+ Set enemyPositions = buildPositionSet(
+ enemyBots.stream().map(VisibleBot::getPosition).collect(Collectors.toList())
+ );
+ Set energyPositions = buildPositionSet(state.getEnergy());
+
+ // Identify own active cores
+ List 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 loneEnemies = findLoneEnemies(enemyBots, rows, cols);
+
+ // Build assignments
+ List moves = new ArrayList<>();
+ Set assignedBots = new HashSet<>();
+ Set claimedDests = new HashSet<>();
+
+ if (coreUnderPressure && !myCores.isEmpty()) {
+ // Core defense: bots near core intercept enemies, others continue raiding
+ List defenders = new ArrayList<>();
+ List 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 raidAssignments = assignRaiders(myBots, loneEnemies,
+ assignedBots, rows, cols);
+
+ for (Map.Entry 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 findLoneEnemies(List enemyBots, int rows, int cols) {
+ List 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 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 myCores, List 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 myCores, List 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 assignRaiders(List myBots,
+ List loneEnemies,
+ Set alreadyAssigned,
+ int rows, int cols) {
+ Map assignments = new HashMap<>();
+ if (loneEnemies.isEmpty()) return assignments;
+
+ List 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 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 walls,
+ Set enemyPositions, Set 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 walls,
+ Set enemyPositions, Set 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 walls,
+ Set enemyPositions, Set 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 energyPositions,
+ Set walls, Set enemyPositions,
+ Set 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 walls,
+ Set 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 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 buildPositionSet(List 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;
+ }
+}