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:
parent
8af1c03aca
commit
91e272f56a
6 changed files with 881 additions and 0 deletions
23
bots/leader-targeter/Dockerfile
Normal file
23
bots/leader-targeter/Dockerfile
Normal 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"]
|
||||
49
bots/leader-targeter/dependency-reduced-pom.xml
Normal file
49
bots/leader-targeter/dependency-reduced-pom.xml
Normal 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>
|
||||
75
bots/leader-targeter/pom.xml
Normal file
75
bots/leader-targeter/pom.xml
Normal 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>
|
||||
137
bots/leader-targeter/src/main/java/com/acb/targeter/App.java
Normal file
137
bots/leader-targeter/src/main/java/com/acb/targeter/App.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue