ai-code-battle/bots/leader-targeter/src/main/java/com/acb/targeter/App.java
jedarden 91e272f56a 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>
2026-06-17 04:37:55 -04:00

137 lines
5 KiB
Java

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);
}
}
}