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>
137 lines
5 KiB
Java
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);
|
|
}
|
|
}
|
|
}
|