ai-code-battle/bots/guardian/index.php
jedarden 6f1b50384c Complete Phase 2: HTTP protocol and 6 strategy bots
Phase 2 Implementation:
- HMAC authentication for engine-to-bot communication
  - Request signing with timestamp anti-replay
  - Response signing for integrity verification
- HTTP bot client with timeout and crash detection
  - Per-turn 3s timeout, 10 consecutive failure crash threshold
  - Move validation (position ownership, direction validity)
- Integration tests for HTTP match execution
- 6 strategy bots in 6 languages:
  - RandomBot (Python): Random valid moves - rating floor
  - GathererBot (Go): Energy-focused with combat avoidance
  - RusherBot (Rust): Aggressive core rushing via BFS
  - GuardianBot (PHP): Defensive core protection
  - SwarmBot (TypeScript): Formation-based group combat
  - HunterBot (Java): Target isolation and hunting

All bots include:
- HMAC signature verification
- Dockerfile for containerization
- README documentation

All engine tests passing (32+ tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 07:00:38 -04:00

174 lines
5 KiB
PHP

<?php
/**
* GuardianBot - A defensive bot that protects cores and gathers nearby energy.
*
* Strategy: Defend own core, gather nearby energy, cautious expansion.
* - Maintain a perimeter of bots within 5 tiles of each owned core
* - Assign excess bots to gather energy within 10 tiles of a core
* - Consolidate defenders when enemies approach
* - Only send scouts to explore beyond the safe zone
* - Conservative spawning - maintains energy reserve of 6
*/
require_once __DIR__ . '/game.php';
require_once __DIR__ . '/strategy.php';
// Get configuration from environment
$port = getenv('BOT_PORT') ?: '8083';
$secret = getenv('BOT_SECRET');
if (!$secret) {
fwrite(STDERR, "ERROR: BOT_SECRET environment variable is required\n");
exit(1);
}
$strategy = new GuardianStrategy();
// Build HTTP server using PHP built-in
$server = stream_socket_server("tcp://0.0.0.0:$port", $errno, $errstr);
if (!$server) {
fwrite(STDERR, "Failed to create server: $errstr ($errno)\n");
exit(1);
}
fwrite(STDOUT, "GuardianBot starting on port $port\n");
while ($conn = stream_socket_accept($server)) {
handle_request($conn, $secret, $strategy);
fclose($conn);
}
/**
* Handle an incoming HTTP request
*/
function handle_request($conn, string $secret, GuardianStrategy $strategy): void {
// Read request
$request = fread($conn, 65536);
// Parse request line
$lines = explode("\r\n", $request);
$requestLine = explode(' ', $lines[0] ?? '');
$method = $requestLine[0] ?? '';
$path = $requestLine[1] ?? '/';
// Parse headers
$headers = [];
$bodyStart = 0;
for ($i = 1; $i < count($lines); $i++) {
if ($lines[$i] === '') {
$bodyStart = $i + 1;
break;
}
$parts = explode(': ', $lines[$i], 2);
if (count($parts) === 2) {
$headers[$parts[0]] = $parts[1];
}
}
// Extract body
$body = implode("\r\n", array_slice($lines, $bodyStart));
// Route request
if ($method === 'GET' && $path === '/health') {
send_response($conn, 200, 'text/plain', 'OK');
return;
}
if ($method === 'POST' && $path === '/turn') {
handle_turn($conn, $secret, $strategy, $headers, $body);
return;
}
send_response($conn, 404, 'text/plain', 'Not Found');
}
/**
* Handle turn request
*/
function handle_turn($conn, string $secret, GuardianStrategy $strategy, array $headers, string $body): void {
// Extract auth headers
$matchId = $headers['X-ACB-Match-Id'] ?? '';
$turnStr = $headers['X-ACB-Turn'] ?? '';
$timestamp = $headers['X-ACB-Timestamp'] ?? '';
$signature = $headers['X-ACB-Signature'] ?? '';
if (!$matchId || !$turnStr || !$timestamp || !$signature) {
send_response($conn, 401, 'text/plain', 'Missing auth headers');
return;
}
// Verify signature
if (!verify_signature($secret, $matchId, $turnStr, $timestamp, $body, $signature)) {
send_response($conn, 401, 'text/plain', 'Invalid signature');
return;
}
// Parse game state
$state = json_decode($body, true);
if (!$state) {
send_response($conn, 400, 'text/plain', 'Invalid JSON');
return;
}
$gameState = GameState::fromArray($state);
// Compute moves
$moves = $strategy->computeMoves($gameState);
// Build response
$response = ['moves' => array_map(fn($m) => $m->toArray(), $moves)];
$responseBody = json_encode($response);
// Sign response
$turn = (int)$turnStr;
$responseSig = sign_response($secret, $matchId, $turn, $responseBody);
$headers = [
'Content-Type: application/json',
"X-ACB-Signature: $responseSig"
];
send_response($conn, 200, 'application/json', $responseBody, $headers);
}
/**
* Verify HMAC signature
*/
function verify_signature(string $secret, string $matchId, string $turn, string $timestamp, string $body, string $signature): bool {
$bodyHash = hash('sha256', $body);
$signingString = "$matchId.$turn.$timestamp.$bodyHash";
$expected = hash_hmac('sha256', $signingString, $secret);
return hash_equals($expected, $signature);
}
/**
* Sign response body
*/
function sign_response(string $secret, string $matchId, int $turn, string $body): string {
$bodyHash = hash('sha256', $body);
$signingString = "$matchId.$turn.$bodyHash";
return hash_hmac('sha256', $signingString, $secret);
}
/**
* Send HTTP response
*/
function send_response($conn, int $status, string $contentType, string $body, array $extraHeaders = []): void {
$statusText = [
200 => 'OK',
400 => 'Bad Request',
401 => 'Unauthorized',
404 => 'Not Found',
][$status] ?? 'Unknown';
$response = "HTTP/1.1 $status $statusText\r\n";
$response .= "Content-Type: $contentType\r\n";
$response .= "Content-Length: " . strlen($body) . "\r\n";
foreach ($extraHeaders as $header) {
$response .= "$header\r\n";
}
$response .= "\r\n";
$response .= $body;
fwrite($conn, $response);
}