feat(bots): add leader-targeter bot with multiplayer score-aware targeting

Implement a new Java bot that applies multi-player game theory: in N>2
games, always direct all units toward the current score leader rather than
the nearest enemy. This creates a natural kingmaker dynamic that prevents
any single bot from running away with the game.

Strategy:
- Identify all visible opponents and their scores (cores count as proxy)
- Pick primary target: opponent with highest inferred score (tiebreak: nearest)
- Send all bots toward primary target's centroid (mean of visible bots + cores)
- Exception: if own core is under threat (enemy bot within 6 tiles), detach 2 bots to defend
- In 2-player games: fall back to straight aggressor strategy

Novelty: No current bot does multi-player score-aware target selection. Most
bots target nearest enemy, which lets a distant leader accumulate score
unmolested. Leader-targeter explicitly models the N-player problem.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-06-17 04:37:30 -04:00
parent 8af1c03aca
commit 91e272f56a
6 changed files with 881 additions and 0 deletions

View file

@ -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"]

View file

@ -0,0 +1,49 @@
<?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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acb</groupId>
<artifactId>leader-targeter-bot</artifactId>
<name>LeaderTargeterBot</name>
<version>1.0.0</version>
<description>Multi-player score leader targeting bot for AI Code Battle</description>
<build>
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer>
<mainClass>com.acb.targeter.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>
<properties>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>21</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jackson.version>2.17.0</jackson.version>
<javalin.version>6.3.0</javalin.version>
</properties>
</project>

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>leader-targeter-bot</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>LeaderTargeterBot</name>
<description>Multi-player score leader targeting 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.targeter.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,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);
}
}
}

View file

@ -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<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();
// Getters
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;
// 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<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,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<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();
}
// Calculate opponent scores
Map<Integer, Integer> opponentScores = calculateOpponentScores(enemyBots, state.getCores());
// Find all unique opponent IDs
Set<Integer> 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<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())
);
// 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<Move> moves = new ArrayList<>();
Set<String> assignedBots = new HashSet<>();
// If core is under threat, assign defenders first
if (coreUnderThreat && myCorePosition != null) {
List<VisibleBot> 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<Integer, Integer> calculateOpponentScores(List<VisibleBot> enemyBots, List<VisibleCore> cores) {
Map<Integer, Integer> scores = new HashMap<>();
// Count bots per opponent
Map<Integer, Integer> botCounts = new HashMap<>();
for (VisibleBot bot : enemyBots) {
botCounts.merge(bot.getOwner(), 1, Integer::sum);
}
// Count active cores per opponent
Map<Integer, Integer> 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<Integer, Integer> opponentScores, Set<Integer> opponentIds,
int rows, int cols, List<VisibleBot> enemyBots,
List<VisibleCore> 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<VisibleBot> enemyBots, List<VisibleCore> cores,
int ownerId, int rows, int cols) {
List<Position> 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<VisibleCore> 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<VisibleBot> findNearestBots(List<VisibleBot> bots, Position target,
int count, int rows, int cols) {
List<VisibleBot> 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<String> enemyPositions, Set<String> walls,
Set<String> 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<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();
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<String> buildPositionSet(List<Position> positions) {
return positions.stream()
.map(Position::key)
.collect(Collectors.toSet());
}
}