feat(bot): add Raider bot (Java) — hit-and-run harasser archetype
Implements a hit-and-run strategy that scouts for lone enemy bots, attacks from flanking positions, then retreats after 1-2 engagement turns to avoid reinforcements. Defends own core when under pressure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ed4be7a4d3
commit
53377d577f
5 changed files with 943 additions and 0 deletions
20
bots/raider/Dockerfile
Normal file
20
bots/raider/Dockerfile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
FROM eclipse-temurin:21-jdk-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY pom.xml ./
|
||||
COPY src ./src
|
||||
|
||||
RUN apk add --no-cache maven && \
|
||||
mvn clean package -DskipTests
|
||||
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/target/raider-bot-1.0.0.jar /app/raider-bot.jar
|
||||
|
||||
ENV BOT_PORT=8086
|
||||
ENV BOT_SECRET=""
|
||||
|
||||
EXPOSE 8086
|
||||
|
||||
CMD ["java", "-jar", "raider-bot.jar"]
|
||||
75
bots/raider/pom.xml
Normal file
75
bots/raider/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>raider-bot</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>RaiderBot</name>
|
||||
<description>Hit-and-run harasser strategy bot for AI Code Battle</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<javalin.version>6.3.0</javalin.version>
|
||||
<jackson.version>2.17.0</jackson.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.javalin</groupId>
|
||||
<artifactId>javalin</artifactId>
|
||||
<version>${javalin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>2.0.12</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>com.acb.raider.App</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
127
bots/raider/src/main/java/com/acb/raider/App.java
Normal file
127
bots/raider/src/main/java/com/acb/raider/App.java
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package com.acb.raider;
|
||||
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.http.Context;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HexFormat;
|
||||
|
||||
/**
|
||||
* RaiderBot - Hit-and-run harasser archetype.
|
||||
*
|
||||
* Strategy: Attack weak targets, disengage before reinforcements arrive.
|
||||
* - Units scout for lone enemy bots (no allies within 2 cells)
|
||||
* - On finding one, attack from flank
|
||||
* - After 1-2 attack turns, retreat regardless of outcome
|
||||
* - Never attack groups of >=3 enemies
|
||||
* - Home base rotates: if own core under pressure, abandon raid and defend
|
||||
*/
|
||||
public class App {
|
||||
private static final int DEFAULT_PORT = 8086;
|
||||
private static String SECRET;
|
||||
private static final RaiderStrategy STRATEGY = new RaiderStrategy();
|
||||
|
||||
public static void main(String[] args) {
|
||||
String portStr = System.getenv("BOT_PORT");
|
||||
int port = portStr != null ? Integer.parseInt(portStr) : DEFAULT_PORT;
|
||||
|
||||
SECRET = System.getenv("BOT_SECRET");
|
||||
if (SECRET == null || SECRET.isEmpty()) {
|
||||
System.err.println("ERROR: BOT_SECRET environment variable is required");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
Javalin app = Javalin.create();
|
||||
|
||||
app.get("/health", ctx -> ctx.result("OK"));
|
||||
app.post("/turn", App::handleTurn);
|
||||
|
||||
app.start(port);
|
||||
System.out.println("RaiderBot starting on port " + port);
|
||||
}
|
||||
|
||||
private static void handleTurn(Context ctx) {
|
||||
String matchId = ctx.header("X-ACB-Match-Id");
|
||||
String turnStr = ctx.header("X-ACB-Turn");
|
||||
String timestamp = ctx.header("X-ACB-Timestamp");
|
||||
String signature = ctx.header("X-ACB-Signature");
|
||||
|
||||
if (matchId == null || turnStr == null || timestamp == null || signature == null) {
|
||||
ctx.status(401).result("Missing auth headers");
|
||||
return;
|
||||
}
|
||||
|
||||
String body = ctx.body();
|
||||
|
||||
if (!verifySignature(SECRET, matchId, turnStr, timestamp, body, signature)) {
|
||||
ctx.status(401).result("Invalid signature");
|
||||
return;
|
||||
}
|
||||
|
||||
GameState state;
|
||||
try {
|
||||
state = GameState.fromJson(body);
|
||||
} catch (Exception e) {
|
||||
ctx.status(400).result("Invalid JSON: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
var moves = STRATEGY.computeMoves(state);
|
||||
int turn = Integer.parseInt(turnStr);
|
||||
|
||||
System.out.println("Turn " + turn + ": " + moves.size() + " moves computed");
|
||||
|
||||
String responseBody = MoveResponse.toJson(moves);
|
||||
String responseSig = signResponse(SECRET, matchId, turn, responseBody);
|
||||
|
||||
ctx.header("X-ACB-Signature", responseSig);
|
||||
ctx.contentType("application/json");
|
||||
ctx.result(responseBody);
|
||||
}
|
||||
|
||||
private static boolean verifySignature(String secret, String matchId, String turn,
|
||||
String timestamp, String body, String signature) {
|
||||
try {
|
||||
String bodyHash = sha256Hex(body);
|
||||
String signingString = matchId + "." + turn + "." + timestamp + "." + bodyHash;
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||
mac.init(keySpec);
|
||||
byte[] expected = mac.doFinal(signingString.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
return MessageDigest.isEqual(
|
||||
HexFormat.of().parseHex(signature),
|
||||
expected
|
||||
);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String signResponse(String secret, String matchId, int turn, String body) {
|
||||
try {
|
||||
String bodyHash = sha256Hex(body);
|
||||
String signingString = matchId + "." + turn + "." + bodyHash;
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||
mac.init(keySpec);
|
||||
return HexFormat.of().formatHex(mac.doFinal(signingString.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to sign response", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String sha256Hex(String input) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
return HexFormat.of().formatHex(digest.digest(input.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to hash", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
195
bots/raider/src/main/java/com/acb/raider/GameState.java
Normal file
195
bots/raider/src/main/java/com/acb/raider/GameState.java
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
package com.acb.raider;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Collections;
|
||||
|
||||
public class GameState {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
@JsonProperty("match_id")
|
||||
private String matchId;
|
||||
|
||||
private int turn;
|
||||
private GameConfig config;
|
||||
private PlayerInfo you;
|
||||
|
||||
private List<VisibleBot> bots = Collections.emptyList();
|
||||
private List<Position> energy = Collections.emptyList();
|
||||
private List<VisibleCore> cores = Collections.emptyList();
|
||||
private List<Position> walls = Collections.emptyList();
|
||||
private List<VisibleBot> dead = Collections.emptyList();
|
||||
|
||||
public String getMatchId() { return matchId; }
|
||||
public int getTurn() { return turn; }
|
||||
public GameConfig getConfig() { return config; }
|
||||
public PlayerInfo getYou() { return you; }
|
||||
public List<VisibleBot> getBots() { return bots; }
|
||||
public List<Position> getEnergy() { return energy; }
|
||||
public List<VisibleCore> getCores() { return cores; }
|
||||
public List<Position> getWalls() { return walls; }
|
||||
public List<VisibleBot> getDead() { return dead; }
|
||||
|
||||
public static GameState fromJson(String json) throws Exception {
|
||||
return MAPPER.readValue(json, GameState.class);
|
||||
}
|
||||
}
|
||||
|
||||
class GameConfig {
|
||||
private int rows;
|
||||
private int cols;
|
||||
|
||||
@JsonProperty("max_turns")
|
||||
private int maxTurns;
|
||||
|
||||
@JsonProperty("vision_radius2")
|
||||
private int visionRadius2;
|
||||
|
||||
@JsonProperty("attack_radius2")
|
||||
private int attackRadius2;
|
||||
|
||||
@JsonProperty("spawn_cost")
|
||||
private int spawnCost;
|
||||
|
||||
@JsonProperty("energy_interval")
|
||||
private int energyInterval;
|
||||
|
||||
public int getRows() { return rows; }
|
||||
public int getCols() { return cols; }
|
||||
public int getMaxTurns() { return maxTurns; }
|
||||
public int getVisionRadius2() { return visionRadius2; }
|
||||
public int getAttackRadius2() { return attackRadius2; }
|
||||
public int getSpawnCost() { return spawnCost; }
|
||||
public int getEnergyInterval() { return energyInterval; }
|
||||
}
|
||||
|
||||
class PlayerInfo {
|
||||
private int id;
|
||||
private int energy;
|
||||
private int score;
|
||||
|
||||
public int getId() { return id; }
|
||||
public int getEnergy() { return energy; }
|
||||
public int getScore() { return score; }
|
||||
}
|
||||
|
||||
class Position {
|
||||
private int row;
|
||||
private int col;
|
||||
|
||||
public Position() {}
|
||||
|
||||
public Position(int row, int col) {
|
||||
this.row = row;
|
||||
this.col = col;
|
||||
}
|
||||
|
||||
public int getRow() { return row; }
|
||||
public int getCol() { return col; }
|
||||
|
||||
public Position moveToward(Direction dir, int rows, int cols) {
|
||||
return switch (dir) {
|
||||
case N -> new Position((row - 1 + rows) % rows, col);
|
||||
case E -> new Position(row, (col + 1) % cols);
|
||||
case S -> new Position((row + 1) % rows, col);
|
||||
case W -> new Position(row, (col - 1 + cols) % cols);
|
||||
};
|
||||
}
|
||||
|
||||
public int distance2(Position other, int rows, int cols) {
|
||||
int dr = Math.abs(row - other.row);
|
||||
int dc = Math.abs(col - other.col);
|
||||
dr = Math.min(dr, rows - dr);
|
||||
dc = Math.min(dc, cols - dc);
|
||||
return dr * dr + dc * dc;
|
||||
}
|
||||
|
||||
public int manhattanDistance(Position other, int rows, int cols) {
|
||||
int dr = Math.abs(row - other.row);
|
||||
int dc = Math.abs(col - other.col);
|
||||
dr = Math.min(dr, rows - dr);
|
||||
dc = Math.min(dc, cols - dc);
|
||||
return dr + dc;
|
||||
}
|
||||
|
||||
public String key() {
|
||||
return row + "," + col;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Position position = (Position) o;
|
||||
return row == position.row && col == position.col;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * row + col;
|
||||
}
|
||||
}
|
||||
|
||||
class VisibleBot {
|
||||
private Position position;
|
||||
private int owner;
|
||||
|
||||
public Position getPosition() { return position; }
|
||||
public int getOwner() { return owner; }
|
||||
}
|
||||
|
||||
class VisibleCore {
|
||||
private Position position;
|
||||
private int owner;
|
||||
private boolean active;
|
||||
|
||||
public Position getPosition() { return position; }
|
||||
public int getOwner() { return owner; }
|
||||
public boolean isActive() { return active; }
|
||||
}
|
||||
|
||||
enum Direction {
|
||||
N, E, S, W;
|
||||
|
||||
public static Direction[] all() {
|
||||
return values();
|
||||
}
|
||||
}
|
||||
|
||||
class Move {
|
||||
private final Position position;
|
||||
private final Direction direction;
|
||||
|
||||
public Move(Position position, Direction direction) {
|
||||
this.position = position;
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
public Position getPosition() { return position; }
|
||||
public Direction getDirection() { return direction; }
|
||||
}
|
||||
|
||||
class MoveResponse {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
private final List<Move> moves;
|
||||
|
||||
public MoveResponse(List<Move> moves) {
|
||||
this.moves = moves;
|
||||
}
|
||||
|
||||
public List<Move> getMoves() { return moves; }
|
||||
|
||||
public static String toJson(List<Move> moves) {
|
||||
try {
|
||||
var response = new MoveResponse(moves);
|
||||
return MAPPER.writeValueAsString(response);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to serialize moves", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
526
bots/raider/src/main/java/com/acb/raider/RaiderStrategy.java
Normal file
526
bots/raider/src/main/java/com/acb/raider/RaiderStrategy.java
Normal file
|
|
@ -0,0 +1,526 @@
|
|||
package com.acb.raider;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* RaiderBot strategy: hit-and-run harassment.
|
||||
*
|
||||
* - Scouts for lone enemy bots (no allies within 2 cells)
|
||||
* - Attacks isolated targets from flank
|
||||
* - After 1-2 attack turns, retreats regardless of outcome
|
||||
* - Never attacks groups of >=3 enemies
|
||||
* - Defends own core if under pressure
|
||||
*/
|
||||
public class RaiderStrategy {
|
||||
|
||||
private static final int GROUP_AVOID_THRESHOLD = 3;
|
||||
private static final int MAX_ENGAGEMENT_TURNS = 2;
|
||||
private static final int ISOLATION_MANHATTAN = 4; // 2 cells squared-distance-ish radius
|
||||
|
||||
// Per-bot engagement tracking
|
||||
private final Map<String, EngagementTracker> engagementTrackers = new HashMap<>();
|
||||
|
||||
public List<Move> computeMoves(GameState state) {
|
||||
int myId = state.getYou().getId();
|
||||
GameConfig config = state.getConfig();
|
||||
int rows = config.getRows();
|
||||
int cols = config.getCols();
|
||||
int attackR2 = config.getAttackRadius2();
|
||||
|
||||
List<VisibleBot> myBots = new ArrayList<>();
|
||||
List<VisibleBot> enemyBots = new ArrayList<>();
|
||||
|
||||
for (VisibleBot bot : state.getBots()) {
|
||||
if (bot.getOwner() == myId) {
|
||||
myBots.add(bot);
|
||||
} else {
|
||||
enemyBots.add(bot);
|
||||
}
|
||||
}
|
||||
|
||||
if (myBots.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Set<String> walls = buildPositionSet(state.getWalls());
|
||||
Set<String> enemyPositions = buildPositionSet(
|
||||
enemyBots.stream().map(VisibleBot::getPosition).collect(Collectors.toList())
|
||||
);
|
||||
Set<String> energyPositions = buildPositionSet(state.getEnergy());
|
||||
|
||||
// Identify own active cores
|
||||
List<Position> myCores = new ArrayList<>();
|
||||
for (VisibleCore core : state.getCores()) {
|
||||
if (core.getOwner() == myId && core.isActive()) {
|
||||
myCores.add(core.getPosition());
|
||||
}
|
||||
}
|
||||
|
||||
// Check if own core is under pressure
|
||||
boolean coreUnderPressure = isCoreUnderPressure(myCores, enemyBots, rows, cols);
|
||||
|
||||
// Find lone enemy bots
|
||||
List<VisibleBot> loneEnemies = findLoneEnemies(enemyBots, rows, cols);
|
||||
|
||||
// Build assignments
|
||||
List<Move> moves = new ArrayList<>();
|
||||
Set<String> assignedBots = new HashSet<>();
|
||||
Set<String> claimedDests = new HashSet<>();
|
||||
|
||||
if (coreUnderPressure && !myCores.isEmpty()) {
|
||||
// Core defense: bots near core intercept enemies, others continue raiding
|
||||
List<VisibleBot> defenders = new ArrayList<>();
|
||||
List<VisibleBot> raiders = new ArrayList<>();
|
||||
|
||||
for (VisibleBot bot : myBots) {
|
||||
int nearestCoreDist = minDistToAny(bot.getPosition(), myCores, rows, cols);
|
||||
if (nearestCoreDist <= 25) { // within ~5 tiles of a core
|
||||
defenders.add(bot);
|
||||
} else {
|
||||
raiders.add(bot);
|
||||
}
|
||||
}
|
||||
|
||||
// Defenders move toward nearest enemy near core
|
||||
for (VisibleBot defender : defenders) {
|
||||
Position target = nearestEnemyToCore(myCores, enemyBots, rows, cols);
|
||||
Move move = computeDefendMove(defender, target, walls, enemyPositions,
|
||||
claimedDests, rows, cols);
|
||||
if (move != null) {
|
||||
assignedBots.add(defender.getPosition().key());
|
||||
Position dest = defender.getPosition().moveToward(move.getDirection(), rows, cols);
|
||||
claimedDests.add(dest.key());
|
||||
moves.add(move);
|
||||
}
|
||||
}
|
||||
|
||||
// Raiders continue hit-and-run
|
||||
myBots = raiders;
|
||||
}
|
||||
|
||||
// Assign raiders to lone enemy targets
|
||||
Map<VisibleBot, VisibleBot> raidAssignments = assignRaiders(myBots, loneEnemies,
|
||||
assignedBots, rows, cols);
|
||||
|
||||
for (Map.Entry<VisibleBot, VisibleBot> entry : raidAssignments.entrySet()) {
|
||||
VisibleBot raider = entry.getKey();
|
||||
VisibleBot target = entry.getValue();
|
||||
String trackerKey = raider.getPosition().key() + "->" + target.getPosition().key();
|
||||
EngagementTracker tracker = engagementTrackers.computeIfAbsent(
|
||||
trackerKey, k -> new EngagementTracker());
|
||||
|
||||
Move move;
|
||||
if (tracker.engagementTurns >= MAX_ENGAGEMENT_TURNS) {
|
||||
// Retreat after max engagement turns
|
||||
move = computeRetreatMove(raider, target.getPosition(), walls, enemyPositions,
|
||||
claimedDests, rows, cols);
|
||||
if (tracker.engagementTurns >= MAX_ENGAGEMENT_TURNS + 2) {
|
||||
tracker.engagementTurns = 0; // reset after sufficient retreat
|
||||
}
|
||||
} else {
|
||||
// Attack the lone target
|
||||
move = computeAttackMove(raider, target.getPosition(), walls, enemyPositions,
|
||||
claimedDests, rows, cols, attackR2);
|
||||
}
|
||||
|
||||
if (move != null) {
|
||||
assignedBots.add(raider.getPosition().key());
|
||||
Position dest = raider.getPosition().moveToward(move.getDirection(), rows, cols);
|
||||
claimedDests.add(dest.key());
|
||||
moves.add(move);
|
||||
tracker.engagementTurns++;
|
||||
}
|
||||
}
|
||||
|
||||
// Remaining bots: gather energy or explore
|
||||
for (VisibleBot bot : myBots) {
|
||||
if (assignedBots.contains(bot.getPosition().key())) continue;
|
||||
|
||||
Move move;
|
||||
if (!state.getEnergy().isEmpty()) {
|
||||
move = computeGatherMove(bot, energyPositions, walls, enemyPositions,
|
||||
claimedDests, rows, cols);
|
||||
} else {
|
||||
move = computeExploreMove(bot, walls, claimedDests, rows, cols);
|
||||
}
|
||||
|
||||
if (move != null) {
|
||||
Position dest = bot.getPosition().moveToward(move.getDirection(), rows, cols);
|
||||
if (!claimedDests.contains(dest.key())) {
|
||||
claimedDests.add(dest.key());
|
||||
moves.add(move);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moves;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find lone enemies: no allied bots within ~2 manhattan cells
|
||||
*/
|
||||
private List<VisibleBot> findLoneEnemies(List<VisibleBot> enemyBots, int rows, int cols) {
|
||||
List<VisibleBot> loneEnemies = new ArrayList<>();
|
||||
|
||||
for (VisibleBot bot : enemyBots) {
|
||||
int nearbyAllies = 0;
|
||||
|
||||
// Count enemies within attack_radius2 (same owner = allies of this enemy)
|
||||
for (VisibleBot other : enemyBots) {
|
||||
if (bot == other) continue;
|
||||
int dist2 = bot.getPosition().distance2(other.getPosition(), rows, cols);
|
||||
if (dist2 <= 4) { // ~2 cells squared
|
||||
nearbyAllies++;
|
||||
}
|
||||
}
|
||||
|
||||
// Lone if no nearby allies and check group size
|
||||
int localGroupSize = countLocalGroup(bot, enemyBots, rows, cols);
|
||||
if (nearbyAllies == 0 && localGroupSize < GROUP_AVOID_THRESHOLD) {
|
||||
loneEnemies.add(bot);
|
||||
}
|
||||
}
|
||||
|
||||
return loneEnemies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count how many enemy bots are clustered near this bot (within ~4 manhattan tiles)
|
||||
*/
|
||||
private int countLocalGroup(VisibleBot bot, List<VisibleBot> enemyBots, int rows, int cols) {
|
||||
int count = 1; // include self
|
||||
for (VisibleBot other : enemyBots) {
|
||||
if (bot == other) continue;
|
||||
int dist = bot.getPosition().manhattanDistance(other.getPosition(), rows, cols);
|
||||
if (dist <= ISOLATION_MANHATTAN) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any enemy is close to our core
|
||||
*/
|
||||
private boolean isCoreUnderPressure(List<Position> myCores, List<VisibleBot> enemyBots,
|
||||
int rows, int cols) {
|
||||
for (Position core : myCores) {
|
||||
for (VisibleBot enemy : enemyBots) {
|
||||
if (core.distance2(enemy.getPosition(), rows, cols) <= 36) { // ~6 tiles
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find nearest enemy to any of our cores
|
||||
*/
|
||||
private Position nearestEnemyToCore(List<Position> myCores, List<VisibleBot> enemyBots,
|
||||
int rows, int cols) {
|
||||
Position nearest = null;
|
||||
int bestDist = Integer.MAX_VALUE;
|
||||
|
||||
for (Position core : myCores) {
|
||||
for (VisibleBot enemy : enemyBots) {
|
||||
int dist = core.distance2(enemy.getPosition(), rows, cols);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
nearest = enemy.getPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign raiders to lone targets using greedy closest-first matching
|
||||
*/
|
||||
private Map<VisibleBot, VisibleBot> assignRaiders(List<VisibleBot> myBots,
|
||||
List<VisibleBot> loneEnemies,
|
||||
Set<String> alreadyAssigned,
|
||||
int rows, int cols) {
|
||||
Map<VisibleBot, VisibleBot> assignments = new HashMap<>();
|
||||
if (loneEnemies.isEmpty()) return assignments;
|
||||
|
||||
List<VisibleBot> available = myBots.stream()
|
||||
.filter(b -> !alreadyAssigned.contains(b.getPosition().key()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Sort targets by isolation (most isolated first — easier prey)
|
||||
loneEnemies.sort((a, b) -> {
|
||||
int groupA = countLocalGroup(a, Collections.emptyList(), rows, cols); // 1 always
|
||||
int groupB = countLocalGroup(b, Collections.emptyList(), rows, cols);
|
||||
return Integer.compare(groupA, groupB);
|
||||
});
|
||||
|
||||
for (VisibleBot target : loneEnemies) {
|
||||
if (available.isEmpty()) break;
|
||||
|
||||
// Assign 1-2 raiders per target
|
||||
available.sort((a, b) -> {
|
||||
int distA = a.getPosition().distance2(target.getPosition(), rows, cols);
|
||||
int distB = b.getPosition().distance2(target.getPosition(), rows, cols);
|
||||
return Integer.compare(distA, distB);
|
||||
});
|
||||
|
||||
int assigned = 0;
|
||||
Iterator<VisibleBot> iter = available.iterator();
|
||||
while (iter.hasNext() && assigned < 2) {
|
||||
VisibleBot raider = iter.next();
|
||||
assignments.put(raider, target);
|
||||
iter.remove();
|
||||
assigned++;
|
||||
}
|
||||
}
|
||||
|
||||
return assignments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute attack move toward a target, using flanking when possible.
|
||||
* Approach from an offset angle rather than head-on.
|
||||
*/
|
||||
private Move computeAttackMove(VisibleBot bot, Position target, Set<String> walls,
|
||||
Set<String> enemyPositions, Set<String> claimedDests,
|
||||
int rows, int cols, int attackR2) {
|
||||
Direction bestDir = null;
|
||||
int bestScore = Integer.MIN_VALUE;
|
||||
|
||||
for (Direction dir : Direction.all()) {
|
||||
Position newPos = bot.getPosition().moveToward(dir, rows, cols);
|
||||
String key = newPos.key();
|
||||
|
||||
if (walls.contains(key)) continue;
|
||||
if (claimedDests.contains(key)) continue;
|
||||
|
||||
int score = 0;
|
||||
int distToTarget = newPos.distance2(target, rows, cols);
|
||||
int currentDist = bot.getPosition().distance2(target, rows, cols);
|
||||
|
||||
// Reward getting closer
|
||||
score += (currentDist - distToTarget) * 10;
|
||||
|
||||
// Big bonus for entering attack range
|
||||
if (distToTarget <= attackR2) {
|
||||
score += 50;
|
||||
}
|
||||
|
||||
// Flanking: prefer approaching from sides (perpendicular offset)
|
||||
// rather than directly head-on
|
||||
int dr = newPos.getRow() - target.getRow();
|
||||
int dc = newPos.getCol() - target.getCol();
|
||||
// Wrap differences
|
||||
if (Math.abs(dr) > rows / 2) dr = dr > 0 ? dr - rows : dr + rows;
|
||||
if (Math.abs(dc) > cols / 2) dc = dc > 0 ? dc - cols : dc + cols;
|
||||
// Diagonal approach is flanking
|
||||
if (dr != 0 && dc != 0) {
|
||||
score += 5; // prefer diagonal/flanking approach
|
||||
}
|
||||
|
||||
// Penalty for nearby enemies (more than the target)
|
||||
int nearbyEnemies = 0;
|
||||
for (String epk : enemyPositions) {
|
||||
if (epk.equals(target.key())) continue;
|
||||
String[] parts = epk.split(",");
|
||||
Position ep = new Position(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
|
||||
if (newPos.distance2(ep, rows, cols) <= attackR2) {
|
||||
nearbyEnemies++;
|
||||
}
|
||||
}
|
||||
score -= nearbyEnemies * 30; // heavy penalty for enemy reinforcements
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestDir = dir;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestDir != null) {
|
||||
return new Move(bot.getPosition(), bestDir);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute retreat move away from a target
|
||||
*/
|
||||
private Move computeRetreatMove(VisibleBot bot, Position target, Set<String> walls,
|
||||
Set<String> enemyPositions, Set<String> claimedDests,
|
||||
int rows, int cols) {
|
||||
Direction bestDir = null;
|
||||
int bestScore = Integer.MIN_VALUE;
|
||||
|
||||
for (Direction dir : Direction.all()) {
|
||||
Position newPos = bot.getPosition().moveToward(dir, rows, cols);
|
||||
String key = newPos.key();
|
||||
|
||||
if (walls.contains(key)) continue;
|
||||
if (claimedDests.contains(key)) continue;
|
||||
|
||||
int distFromTarget = newPos.distance2(target, rows, cols);
|
||||
|
||||
// Maximize distance from target
|
||||
int score = distFromTarget;
|
||||
|
||||
// Penalty for being near any other enemy
|
||||
for (String epk : enemyPositions) {
|
||||
String[] parts = epk.split(",");
|
||||
Position ep = new Position(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
|
||||
if (newPos.distance2(ep, rows, cols) <= 5) {
|
||||
score -= 20;
|
||||
}
|
||||
}
|
||||
|
||||
// Reward moving toward nearest energy (opportunistic while retreating)
|
||||
// (energy positions not passed here for simplicity)
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestDir = dir;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestDir != null) {
|
||||
return new Move(bot.getPosition(), bestDir);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute defensive move: intercept enemy approaching core
|
||||
*/
|
||||
private Move computeDefendMove(VisibleBot bot, Position target, Set<String> walls,
|
||||
Set<String> enemyPositions, Set<String> claimedDests,
|
||||
int rows, int cols) {
|
||||
if (target == null) return null;
|
||||
|
||||
Direction bestDir = null;
|
||||
int bestDist = Integer.MAX_VALUE;
|
||||
|
||||
for (Direction dir : Direction.all()) {
|
||||
Position newPos = bot.getPosition().moveToward(dir, rows, cols);
|
||||
String key = newPos.key();
|
||||
|
||||
if (walls.contains(key)) continue;
|
||||
if (claimedDests.contains(key)) continue;
|
||||
|
||||
int dist = newPos.distance2(target, rows, cols);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
bestDir = dir;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestDir != null) {
|
||||
return new Move(bot.getPosition(), bestDir);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute energy-gathering move toward nearest energy
|
||||
*/
|
||||
private Move computeGatherMove(VisibleBot bot, Set<String> energyPositions,
|
||||
Set<String> walls, Set<String> enemyPositions,
|
||||
Set<String> claimedDests, int rows, int cols) {
|
||||
Position nearestEnergy = null;
|
||||
int nearestDist = Integer.MAX_VALUE;
|
||||
|
||||
for (String ek : energyPositions) {
|
||||
String[] parts = ek.split(",");
|
||||
Position ep = new Position(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
|
||||
int dist = bot.getPosition().distance2(ep, rows, cols);
|
||||
if (dist < nearestDist) {
|
||||
nearestDist = dist;
|
||||
nearestEnergy = ep;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearestEnergy != null) {
|
||||
Direction bestDir = null;
|
||||
int bestDist = Integer.MAX_VALUE;
|
||||
|
||||
for (Direction dir : Direction.all()) {
|
||||
Position newPos = bot.getPosition().moveToward(dir, rows, cols);
|
||||
String key = newPos.key();
|
||||
|
||||
if (walls.contains(key)) continue;
|
||||
if (claimedDests.contains(key)) continue;
|
||||
|
||||
int dist = newPos.distance2(nearestEnergy, rows, cols);
|
||||
|
||||
// Avoid enemies while gathering
|
||||
for (String epk : enemyPositions) {
|
||||
String[] parts = epk.split(",");
|
||||
Position ep = new Position(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
|
||||
if (newPos.distance2(ep, rows, cols) <= 5) {
|
||||
dist += 20; // penalty
|
||||
}
|
||||
}
|
||||
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
bestDir = dir;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestDir != null) {
|
||||
return new Move(bot.getPosition(), bestDir);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explore: move toward grid center or away from friendly bots
|
||||
*/
|
||||
private Move computeExploreMove(VisibleBot bot, Set<String> walls,
|
||||
Set<String> claimedDests, int rows, int cols) {
|
||||
Position center = new Position(rows / 2, cols / 2);
|
||||
Direction bestDir = null;
|
||||
int bestDist = Integer.MAX_VALUE;
|
||||
|
||||
for (Direction dir : Direction.all()) {
|
||||
Position newPos = bot.getPosition().moveToward(dir, rows, cols);
|
||||
String key = newPos.key();
|
||||
|
||||
if (walls.contains(key)) continue;
|
||||
if (claimedDests.contains(key)) continue;
|
||||
|
||||
int dist = newPos.distance2(center, rows, cols);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
bestDir = dir;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestDir != null) {
|
||||
return new Move(bot.getPosition(), bestDir);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private int minDistToAny(Position pos, List<Position> targets, int rows, int cols) {
|
||||
int minDist = Integer.MAX_VALUE;
|
||||
for (Position t : targets) {
|
||||
minDist = Math.min(minDist, pos.distance2(t, rows, cols));
|
||||
}
|
||||
return minDist;
|
||||
}
|
||||
|
||||
private Set<String> buildPositionSet(List<Position> positions) {
|
||||
return positions.stream()
|
||||
.map(Position::key)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks how long a raider has been engaged with a target
|
||||
*/
|
||||
private static class EngagementTracker {
|
||||
int engagementTurns = 0;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue