diff --git a/bots/leader-targeter/Dockerfile b/bots/leader-targeter/Dockerfile
new file mode 100644
index 0000000..4e19702
--- /dev/null
+++ b/bots/leader-targeter/Dockerfile
@@ -0,0 +1,23 @@
+# Build stage
+FROM eclipse-temurin:21-jdk-alpine AS builder
+
+WORKDIR /app
+COPY pom.xml ./
+COPY src ./src
+
+# Install Maven and build
+RUN apk add --no-cache maven && \
+ mvn clean package -DskipTests
+
+# Runtime stage
+FROM eclipse-temurin:21-jre-alpine
+
+WORKDIR /app
+COPY --from=builder /app/target/leader-targeter-bot-1.0.0.jar /app/leader-targeter-bot.jar
+
+ENV BOT_PORT=8085
+ENV BOT_SECRET=""
+
+EXPOSE 8085
+
+CMD ["java", "-jar", "leader-targeter-bot.jar"]
diff --git a/bots/leader-targeter/dependency-reduced-pom.xml b/bots/leader-targeter/dependency-reduced-pom.xml
new file mode 100644
index 0000000..3cc8429
--- /dev/null
+++ b/bots/leader-targeter/dependency-reduced-pom.xml
@@ -0,0 +1,49 @@
+
+
+ 4.0.0
+ com.acb
+ leader-targeter-bot
+ LeaderTargeterBot
+ 1.0.0
+ Multi-player score leader targeting bot for AI Code Battle
+
+
+
+ maven-shade-plugin
+ 3.5.2
+
+
+ package
+
+ shade
+
+
+
+
+ com.acb.targeter.App
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+
+
+ 21
+ 21
+ UTF-8
+ 2.17.0
+ 6.3.0
+
+
diff --git a/bots/leader-targeter/pom.xml b/bots/leader-targeter/pom.xml
new file mode 100644
index 0000000..cbf908f
--- /dev/null
+++ b/bots/leader-targeter/pom.xml
@@ -0,0 +1,75 @@
+
+
+ 4.0.0
+
+ com.acb
+ leader-targeter-bot
+ 1.0.0
+ jar
+
+ LeaderTargeterBot
+ Multi-player score leader targeting 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.targeter.App
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+
+
diff --git a/bots/leader-targeter/src/main/java/com/acb/targeter/App.java b/bots/leader-targeter/src/main/java/com/acb/targeter/App.java
new file mode 100644
index 0000000..1e00840
--- /dev/null
+++ b/bots/leader-targeter/src/main/java/com/acb/targeter/App.java
@@ -0,0 +1,137 @@
+package com.acb.targeter;
+
+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;
+
+/**
+ * LeaderTargeterBot - Multi-player score leader targeting bot.
+ *
+ * Strategy: In N>2 games, always direct all units toward the current score leader.
+ * - Identify all visible opponents and their scores (cores count as proxy: each active core ≈ +2 score)
+ * - Pick primary target: opponent with highest inferred score (tiebreak: nearest)
+ * - Send all bots toward primary target's centroid (mean of target's visible bots + cores)
+ * - Exception: if own core is under direct threat (enemy bot within 6 tiles), detach 2 bots to defend
+ * - In 2-player games: fall back to straight aggressor (target the only opponent)
+ *
+ * This creates a natural kingmaker dynamic that prevents any single bot from running away with the game.
+ */
+public class App {
+ private static final int DEFAULT_PORT = 8085;
+ private static String SECRET;
+ private static final LeaderTargeterStrategy STRATEGY = new LeaderTargeterStrategy();
+
+ 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("LeaderTargeterBot starting on port " + port);
+ }
+
+ private static void handleTurn(Context ctx) {
+ // Extract auth headers
+ 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();
+
+ // Verify signature
+ if (!verifySignature(SECRET, matchId, turnStr, timestamp, body, signature)) {
+ ctx.status(401).result("Invalid signature");
+ return;
+ }
+
+ // Parse game state
+ GameState state;
+ try {
+ state = GameState.fromJson(body);
+ } catch (Exception e) {
+ ctx.status(400).result("Invalid JSON: " + e.getMessage());
+ return;
+ }
+
+ // Compute moves
+ var moves = STRATEGY.computeMoves(state);
+ int turn = Integer.parseInt(turnStr);
+
+ System.out.println("Turn " + turn + ": " + moves.size() + " moves computed");
+
+ // Build response
+ String responseBody = MoveResponse.toJson(moves);
+
+ // Sign response
+ 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 + "." + 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/leader-targeter/src/main/java/com/acb/targeter/GameState.java b/bots/leader-targeter/src/main/java/com/acb/targeter/GameState.java
new file mode 100644
index 0000000..aa30695
--- /dev/null
+++ b/bots/leader-targeter/src/main/java/com/acb/targeter/GameState.java
@@ -0,0 +1,211 @@
+package com.acb.targeter;
+
+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;
+
+/**
+ * Game state types for AI Code Battle protocol.
+ */
+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();
+
+ // Getters
+ 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;
+
+ // Getters
+ 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;
+
+ // Getters
+ public int getId() { return id; }
+ public int getEnergy() { return energy; }
+ public int getScore() { return score; }
+}
+
+class Position {
+ private int row;
+ private int col;
+
+ // Default constructor for Jackson
+ public Position() {}
+
+ public Position(int row, int col) {
+ this.row = row;
+ this.col = col;
+ }
+
+ public int getRow() { return row; }
+ public int getCol() { return col; }
+
+ /**
+ * Move in a direction with toroidal wrapping
+ */
+ 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);
+ };
+ }
+
+ /**
+ * Calculate squared distance with toroidal wrapping
+ */
+ 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;
+ }
+
+ /**
+ * Manhattan distance with toroidal wrapping
+ */
+ 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/leader-targeter/src/main/java/com/acb/targeter/LeaderTargeterStrategy.java b/bots/leader-targeter/src/main/java/com/acb/targeter/LeaderTargeterStrategy.java
new file mode 100644
index 0000000..dfb1f13
--- /dev/null
+++ b/bots/leader-targeter/src/main/java/com/acb/targeter/LeaderTargeterStrategy.java
@@ -0,0 +1,386 @@
+package com.acb.targeter;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * LeaderTargeterStrategy - Multi-player score leader targeting bot.
+ *
+ * Strategy: In N>2 games, always direct all units toward the current score leader.
+ * - Identify all visible opponents and their scores (cores count as proxy: each active core ≈ +2 score)
+ * - Pick primary target: opponent with highest inferred score (tiebreak: nearest)
+ * - Send all bots toward primary target's centroid (mean of target's visible bots + cores)
+ * - Exception: if own core is under direct threat (enemy bot within 6 tiles), detach 2 bots to defend
+ * - In 2-player games: fall back to straight aggressor (target the only opponent)
+ *
+ * This creates a natural kingmaker dynamic that prevents any single bot from running away with the game.
+ */
+public class LeaderTargeterStrategy {
+ private static final int CORE_DEFENSE_THRESHOLD = 36; // 6 tiles squared distance
+ private static final int DEFENDERS_COUNT = 2; // Number of bots to detach for core defense
+ private static final int SCORE_PER_ACTIVE_CORE = 2; // Approximate score contribution per active core
+
+ /**
+ * Compute moves for all owned bots
+ */
+ public List 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 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();
+ }
+
+ // Calculate opponent scores
+ Map opponentScores = calculateOpponentScores(enemyBots, state.getCores());
+
+ // Find all unique opponent IDs
+ Set opponentIds = new HashSet<>();
+ for (VisibleBot bot : enemyBots) {
+ opponentIds.add(bot.getOwner());
+ }
+ for (VisibleCore core : state.getCores()) {
+ if (core.getOwner() != myId) {
+ opponentIds.add(core.getOwner());
+ }
+ }
+
+ // Build position lookups
+ Set walls = buildPositionSet(state.getWalls());
+ Set enemyPositions = buildPositionSet(
+ enemyBots.stream().map(VisibleBot::getPosition).collect(Collectors.toList())
+ );
+ Set myBotPositions = buildPositionSet(
+ myBots.stream().map(VisibleBot::getPosition).collect(Collectors.toList())
+ );
+
+ // Check if own core is under threat
+ Position myCorePosition = findOwnCore(state.getCores(), myId);
+ boolean coreUnderThreat = false;
+ if (myCorePosition != null && !enemyBots.isEmpty()) {
+ for (VisibleBot enemyBot : enemyBots) {
+ if (enemyBot.getPosition().distance2(myCorePosition, rows, cols) <= CORE_DEFENSE_THRESHOLD) {
+ coreUnderThreat = true;
+ break;
+ }
+ }
+ }
+
+ // Select target based on game type
+ Position targetPosition;
+ int targetOwnerId;
+
+ if (opponentIds.size() <= 1) {
+ // 2-player game: target the only opponent
+ targetOwnerId = opponentIds.isEmpty() ? -1 : opponentIds.iterator().next();
+ targetPosition = findOpponentCentroid(enemyBots, state.getCores(), targetOwnerId, rows, cols);
+ System.out.println("2-player game: targeting opponent " + targetOwnerId);
+ } else {
+ // Multi-player game: target the score leader
+ int scoreLeaderId = findScoreLeader(opponentScores, opponentIds, rows, cols,
+ enemyBots, state.getCores(), myBots.get(0).getPosition());
+ targetOwnerId = scoreLeaderId;
+ targetPosition = findOpponentCentroid(enemyBots, state.getCores(), scoreLeaderId, rows, cols);
+ System.out.println("Multi-player game: score leader is opponent " + scoreLeaderId +
+ " with score ~" + opponentScores.getOrDefault(scoreLeaderId, 0));
+ }
+
+ if (targetPosition == null) {
+ // No valid target, move toward center
+ targetPosition = new Position(rows / 2, cols / 2);
+ System.out.println("No valid target, moving toward center");
+ }
+
+ // Compute moves
+ List moves = new ArrayList<>();
+ Set assignedBots = new HashSet<>();
+
+ // If core is under threat, assign defenders first
+ if (coreUnderThreat && myCorePosition != null) {
+ List defenders = findNearestBots(myBots, myCorePosition, DEFENDERS_COUNT, rows, cols);
+ for (VisibleBot defender : defenders) {
+ Move move = computeDefensiveMove(defender, myCorePosition, enemyPositions, walls,
+ myBotPositions, rows, cols);
+ if (move != null) {
+ moves.add(move);
+ assignedBots.add(defender.getPosition().key());
+ myBotPositions.remove(defender.getPosition().key());
+ }
+ }
+ System.out.println("Core under threat! Assigned " + defenders.size() + " defenders");
+ }
+
+ // Assign remaining bots to attack the primary target
+ for (VisibleBot bot : myBots) {
+ if (assignedBots.contains(bot.getPosition().key())) {
+ continue; // Already assigned as defender
+ }
+
+ Move move = computeAttackMove(bot, targetPosition, enemyPositions, walls, myBotPositions, rows, cols);
+ if (move != null) {
+ moves.add(move);
+ myBotPositions.remove(bot.getPosition().key());
+ }
+ }
+
+ System.out.println("Computed " + moves.size() + " moves for " + myBots.size() + " bots");
+
+ return moves;
+ }
+
+ /**
+ * Calculate approximate scores for all opponents based on visible bots and cores
+ */
+ private Map calculateOpponentScores(List enemyBots, List cores) {
+ Map scores = new HashMap<>();
+
+ // Count bots per opponent
+ Map botCounts = new HashMap<>();
+ for (VisibleBot bot : enemyBots) {
+ botCounts.merge(bot.getOwner(), 1, Integer::sum);
+ }
+
+ // Count active cores per opponent
+ Map coreCounts = new HashMap<>();
+ for (VisibleCore core : cores) {
+ if (core.isActive()) {
+ coreCounts.merge(core.getOwner(), 1, Integer::sum);
+ }
+ }
+
+ // Calculate approximate scores: botCount * 10 + activeCoreCount * SCORE_PER_ACTIVE_CORE
+ // (Each bot is worth ~10 points based on spawn cost)
+ for (Integer ownerId : botCounts.keySet()) {
+ int botCount = botCounts.get(ownerId);
+ int coreCount = coreCounts.getOrDefault(ownerId, 0);
+ int estimatedScore = botCount * 10 + coreCount * SCORE_PER_ACTIVE_CORE;
+ scores.put(ownerId, estimatedScore);
+ }
+
+ // Include opponents with only cores visible
+ for (Integer ownerId : coreCounts.keySet()) {
+ if (!scores.containsKey(ownerId)) {
+ int coreCount = coreCounts.get(ownerId);
+ scores.put(ownerId, coreCount * SCORE_PER_ACTIVE_CORE);
+ }
+ }
+
+ return scores;
+ }
+
+ /**
+ * Find the score leader among opponents
+ */
+ private int findScoreLeader(Map opponentScores, Set opponentIds,
+ int rows, int cols, List enemyBots,
+ List cores, Position referencePosition) {
+ int leaderId = -1;
+ int maxScore = Integer.MIN_VALUE;
+ int minDistance = Integer.MAX_VALUE;
+
+ for (Integer ownerId : opponentIds) {
+ int score = opponentScores.getOrDefault(ownerId, 0);
+
+ // Find this opponent's centroid for distance calculation
+ Position centroid = findOpponentCentroid(enemyBots, cores, ownerId, rows, cols);
+ int distance = centroid != null ? referencePosition.distance2(centroid, rows, cols) : Integer.MAX_VALUE;
+
+ // Prefer higher score, tiebreak by nearest distance
+ if (score > maxScore || (score == maxScore && distance < minDistance)) {
+ maxScore = score;
+ minDistance = distance;
+ leaderId = ownerId;
+ }
+ }
+
+ return leaderId != -1 ? leaderId : opponentIds.iterator().next();
+ }
+
+ /**
+ * Find the centroid (average position) of an opponent's visible assets
+ */
+ private Position findOpponentCentroid(List enemyBots, List cores,
+ int ownerId, int rows, int cols) {
+ List positions = new ArrayList<>();
+
+ // Add bot positions
+ for (VisibleBot bot : enemyBots) {
+ if (bot.getOwner() == ownerId) {
+ positions.add(bot.getPosition());
+ }
+ }
+
+ // Add core positions
+ for (VisibleCore core : cores) {
+ if (core.getOwner() == ownerId) {
+ positions.add(core.getPosition());
+ }
+ }
+
+ if (positions.isEmpty()) {
+ return null;
+ }
+
+ // Calculate average position
+ double avgRow = 0;
+ double avgCol = 0;
+ for (Position pos : positions) {
+ avgRow += pos.getRow();
+ avgCol += pos.getCol();
+ }
+ avgRow /= positions.size();
+ avgCol /= positions.size();
+
+ return new Position((int) Math.round(avgRow), (int) Math.round(avgCol));
+ }
+
+ /**
+ * Find own core position
+ */
+ private Position findOwnCore(List cores, int myId) {
+ for (VisibleCore core : cores) {
+ if (core.getOwner() == myId) {
+ return core.getPosition();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Find the N nearest bots to a target position
+ */
+ private List findNearestBots(List bots, Position target,
+ int count, int rows, int cols) {
+ List sorted = new ArrayList<>(bots);
+ sorted.sort((a, b) -> {
+ int distA = a.getPosition().distance2(target, rows, cols);
+ int distB = b.getPosition().distance2(target, rows, cols);
+ return Integer.compare(distA, distB);
+ });
+
+ return sorted.stream().limit(count).collect(Collectors.toList());
+ }
+
+ /**
+ * Compute a defensive move to protect own core
+ */
+ private Move computeDefensiveMove(VisibleBot bot, Position corePosition,
+ Set enemyPositions, Set walls,
+ Set myBotPositions, int rows, int cols) {
+ // Move to position core to intercept enemies
+ // Prefer positions that are between core and nearest enemy
+ 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();
+
+ if (walls.contains(newPosKey)) {
+ continue;
+ }
+
+ if (myBotPositions.contains(newPosKey)) {
+ continue;
+ }
+
+ // Score: prefer being close to core but not on it
+ int distToCore = newPos.distance2(corePosition, rows, cols);
+ int score = -distToCore;
+
+ // Bonus for being between core and potential enemies
+ if (distToCore > 0 && distToCore <= 9) { // 2-3 tiles from core
+ score += 50;
+ }
+
+ if (score > bestScore) {
+ bestScore = score;
+ bestDir = dir;
+ }
+ }
+
+ if (bestDir != null) {
+ return new Move(bot.getPosition(), bestDir);
+ }
+
+ return null;
+ }
+
+ /**
+ * Compute an attack move toward the target position
+ */
+ private Move computeAttackMove(VisibleBot bot, Position target,
+ Set enemyPositions, Set walls,
+ Set 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();
+
+ if (walls.contains(newPosKey)) {
+ continue;
+ }
+
+ 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;
+ }
+
+ // Small penalty for moving adjacent to multiple enemies (but less strict than hunter)
+ 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 * 5; // Lower penalty than hunter, we're aggressive
+
+ if (score > bestScore) {
+ bestScore = score;
+ 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 buildPositionSet(List positions) {
+ return positions.stream()
+ .map(Position::key)
+ .collect(Collectors.toSet());
+ }
+}