ai-code-battle/starters/php/game.php
jedarden 01da007045 feat(starter-php): add PHP starter kit (acb-starter-php)
Add starters/php/ with complete starter kit for AI Code Battle:

- index.php: HTTP server with HMAC verification, routing for /turn and /health
- strategy.php: Stub compute_moves() function with example energy-seeking logic
- game.php: Game state types (GameState, Position, VisibleBot, etc.) and grid utilities (toroidal_manhattan, toroidal_chebyshev, bfs, neighbors, cardinal_steps)
- Dockerfile: Alpine-based PHP 8.4 container
- README.md: Quickstart documentation with local/Docker run instructions
- composer.json: Minimal composer config (no external dependencies)

Follows same contract as other starters:
- Listens on port 8080 (BOT_PORT env var)
- POST /turn: Receives game state JSON, returns moves JSON
- GET /health: Health check endpoint
- HMAC-SHA256 signature verification on requests/responses

Reference implementation: bots/guardian/ (GuardianBot in PHP)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 03:35:29 -04:00

288 lines
7.3 KiB
PHP

<?php
/**
* Game state types and grid utilities for AI Code Battle protocol.
*/
/**
* Position on the grid
*/
class Position {
public int $row;
public int $col;
public function __construct(int $row, int $col) {
$this->row = $row;
$this->col = $col;
}
public static function fromArray(array $data): self {
return new self($data['row'], $data['col']);
}
public function toArray(): array {
return ['row' => $this->row, 'col' => $this->col];
}
public function equals(Position $other): bool {
return $this->row === $other->row && $this->col === $other->col;
}
public function __toString(): string {
return "{$this->row},{$this->col}";
}
}
/**
* Game configuration
*/
class GameConfig {
public int $rows;
public int $cols;
public int $maxTurns;
public int $visionRadius2;
public int $attackRadius2;
public int $spawnCost;
public int $energyInterval;
public ?string $seasonId;
public ?string $rulesVersion;
public static function fromArray(array $data): self {
$config = new self();
$config->rows = $data['rows'];
$config->cols = $data['cols'];
$config->maxTurns = $data['max_turns'];
$config->visionRadius2 = $data['vision_radius2'];
$config->attackRadius2 = $data['attack_radius2'];
$config->spawnCost = $data['spawn_cost'];
$config->energyInterval = $data['energy_interval'];
$config->seasonId = $data['season_id'] ?? null;
$config->rulesVersion = $data['rules_version'] ?? null;
return $config;
}
}
/**
* Player info
*/
class PlayerInfo {
public int $id;
public int $energy;
public int $score;
public static function fromArray(array $data): self {
$info = new self();
$info->id = $data['id'];
$info->energy = $data['energy'];
$info->score = $data['score'];
return $info;
}
}
/**
* Visible bot
*/
class VisibleBot {
public Position $position;
public int $owner;
public static function fromArray(array $data): self {
$bot = new self();
$bot->position = Position::fromArray($data['position']);
$bot->owner = $data['owner'];
return $bot;
}
}
/**
* Visible core
*/
class VisibleCore {
public Position $position;
public int $owner;
public bool $active;
public static function fromArray(array $data): self {
$core = new self();
$core->position = Position::fromArray($data['position']);
$core->owner = $data['owner'];
$core->active = $data['active'];
return $core;
}
}
/**
* Fog-filtered game state
*/
class GameState {
public string $matchId;
public int $turn;
public GameConfig $config;
public PlayerInfo $you;
/** @var VisibleBot[] */
public array $bots = [];
/** @var Position[] */
public array $energy = [];
/** @var VisibleCore[] */
public array $cores = [];
/** @var Position[] */
public array $walls = [];
/** @var VisibleBot[] */
public array $dead = [];
public static function fromArray(array $data): self {
$state = new self();
$state->matchId = $data['match_id'];
$state->turn = $data['turn'];
$state->config = GameConfig::fromArray($data['config']);
$state->you = PlayerInfo::fromArray($data['you']);
foreach ($data['bots'] ?? [] as $bot) {
$state->bots[] = VisibleBot::fromArray($bot);
}
foreach ($data['energy'] ?? [] as $pos) {
$state->energy[] = Position::fromArray($pos);
}
foreach ($data['cores'] ?? [] as $core) {
$state->cores[] = VisibleCore::fromArray($core);
}
foreach ($data['walls'] ?? [] as $pos) {
$state->walls[] = Position::fromArray($pos);
}
foreach ($data['dead'] ?? [] as $bot) {
$state->dead[] = VisibleBot::fromArray($bot);
}
return $state;
}
}
/**
* A single move command
*/
class Move {
public Position $position;
public string $direction;
public function __construct(Position $position, string $direction) {
$this->position = $position;
$this->direction = $direction;
}
public function toArray(): array {
return [
'position' => $this->position->toArray(),
'direction' => $this->direction
];
}
}
// ============================================================================
// Grid Utilities
// ============================================================================
/**
* Calculate Manhattan distance with toroidal wrapping
*/
function toroidal_manhattan(Position $a, Position $b, int $rows, int $cols): int {
$dr = abs($a->row - $b->row);
$dc = abs($a->col - $b->col);
$dr = min($dr, $rows - $dr);
$dc = min($dc, $cols - $dc);
return $dr + $dc;
}
/**
* Calculate Chebyshev distance with toroidal wrapping
*/
function toroidal_chebyshev(Position $a, Position $b, int $rows, int $cols): int {
$dr = abs($a->row - $b->row);
$dc = abs($a->col - $b->col);
$dr = min($dr, $rows - $dr);
$dc = min($dc, $cols - $dc);
return max($dr, $dc);
}
/**
* Get 8-directional neighbors with wrap-around
* @return Position[]
*/
function neighbors(Position $p, int $rows, int $cols): array {
$offsets = [
[-1, -1], [-1, 0], [-1, 1],
[0, -1], [0, 1],
[1, -1], [1, 0], [1, 1]
];
$result = [];
foreach ($offsets as [$dr, $dc]) {
$result[] = new Position(
($p->row + $dr + $rows) % $rows,
($p->col + $dc + $cols) % $cols
);
}
return $result;
}
/**
* Get cardinal direction steps with wrap-around
* @return array Array of ['pos' => Position, 'dir' => string]
*/
function cardinal_steps(Position $p, int $rows, int $cols): array {
$steps = [
[-1, 0, 'N'],
[0, 1, 'E'],
[1, 0, 'S'],
[0, -1, 'W']
];
$result = [];
foreach ($steps as [$dr, $dc, $dir]) {
$result[] = [
'pos' => new Position(
($p->row + $dr + $rows) % $rows,
($p->col + $dc + $cols) % $cols
),
'dir' => $dir
];
}
return $result;
}
/**
* BFS pathfinding on a toroidal grid
*
* @param Position $start Starting position
* @param Position $goal Goal position
* @param callable $passable callable(Position): bool - returns true if cell can be entered
* @param int $rows Grid rows
* @param int $cols Grid cols
* @return Position[]|null Array of positions from start to goal (excluding start), or null if unreachable
*/
function bfs(Position $start, Position $goal, callable $passable, int $rows, int $cols): ?array {
if ($start->equals($goal)) {
return [];
}
$queue = [[$start, []]];
$visited = [(string)$start => true];
while (!empty($queue)) {
[$current, $path] = array_shift($queue);
foreach (neighbors($current, $rows, $cols) as $next) {
if ($next->equals($goal)) {
return [...$path, $next];
}
$key = (string)$next;
if (!isset($visited[$key]) && $passable($next)) {
$visited[$key] = true;
$queue[] = [$next, [...$path, $next]];
}
}
}
return null;
}