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>
This commit is contained in:
parent
90431344e8
commit
01da007045
6 changed files with 628 additions and 0 deletions
12
starters/php/Dockerfile
Normal file
12
starters/php/Dockerfile
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
FROM php:8.4-cli-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY index.php strategy.php game.php composer.json ./
|
||||
|
||||
ENV BOT_PORT=8080
|
||||
ENV BOT_SECRET=""
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["php", "index.php"]
|
||||
74
starters/php/README.md
Normal file
74
starters/php/README.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# acb-starter-php
|
||||
|
||||
PHP 8 starter kit for [AI Code Battle](https://aicodebattle.com) — a competitive bot programming platform.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run locally
|
||||
BOT_SECRET=dev-secret php index.php
|
||||
|
||||
# Run with Docker
|
||||
docker build -t my-bot .
|
||||
docker run -e BOT_SECRET=your-secret -p 8080:8080 my-bot
|
||||
```
|
||||
|
||||
Your bot listens on port 8080 and responds to `POST /turn` with move commands.
|
||||
|
||||
## Register Your Bot
|
||||
|
||||
Once your bot is deployed and accessible via HTTPS:
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.aicodebattle.com/api/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "my-php-bot",
|
||||
"endpoint_url": "https://my-bot.example.com",
|
||||
"owner": "your-name",
|
||||
"description": "My awesome bot"
|
||||
}'
|
||||
```
|
||||
|
||||
Save the `bot_id` and `shared_secret` from the response — the secret is shown only once.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
index.php # HTTP server, HMAC auth, and routing
|
||||
strategy.php # Your bot logic (implement compute_moves())
|
||||
game.php # Game state types and grid utilities
|
||||
composer.json # Composer config (no external deps required)
|
||||
Dockerfile # Container build
|
||||
```
|
||||
|
||||
## Grid Helpers
|
||||
|
||||
`game.php` provides utility functions for the toroidal grid:
|
||||
|
||||
- `toroidal_manhattan($a, $b, $rows, $cols)` — Manhattan distance with wrap-around
|
||||
- `toroidal_chebyshev($a, $b, $rows, $cols)` — Chebyshev distance with wrap-around
|
||||
- `neighbors($p, $rows, $cols)` — 8-directional neighbors with wrap
|
||||
- `cardinal_steps($p, $rows, $cols)` — Cardinal directions with positions
|
||||
- `bfs($start, $goal, $passable, $rows, $cols)` — BFS pathfinding, returns path or `null`
|
||||
|
||||
## Customization
|
||||
|
||||
Edit `compute_moves()` in `strategy.php` to implement your strategy. The `GameState` object provides:
|
||||
|
||||
- `bots` — all visible bots (yours and enemies)
|
||||
- `energy` — visible energy pickup locations
|
||||
- `cores` — visible core positions
|
||||
- `walls` — visible wall positions
|
||||
- `you->energy` — your current energy count
|
||||
- `you->score` — your current score
|
||||
- `config` — match parameters (grid size, vision radius, etc.)
|
||||
|
||||
Return an array of `Move` objects, each with the bot's current `position` and a `direction` (`"N"`, `"E"`, `"S"`, or `"W"`). Bots not included in the moves array stay in place.
|
||||
|
||||
## Protocol
|
||||
|
||||
- **Endpoint:** `POST /turn` — receives game state JSON, returns moves JSON
|
||||
- **Health:** `GET /health` — must return 200
|
||||
- **Timeout:** 3 seconds per turn
|
||||
- **Auth:** HMAC-SHA256 via `X-ACB-Signature` header
|
||||
14
starters/php/composer.json
Normal file
14
starters/php/composer.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "acb-starter-php",
|
||||
"description": "PHP starter kit for AI Code Battle",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=8.4"
|
||||
},
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"AcbStarter\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
288
starters/php/game.php
Normal file
288
starters/php/game.php
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
<?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;
|
||||
}
|
||||
178
starters/php/index.php
Normal file
178
starters/php/index.php
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
<?php
|
||||
/**
|
||||
* AI Code Battle - PHP Starter Kit
|
||||
*
|
||||
* A minimal bot scaffold with HMAC authentication and a placeholder
|
||||
* strategy. Implement your strategy in strategy.php.
|
||||
*
|
||||
* Usage:
|
||||
* BOT_SECRET=your-secret php index.php
|
||||
*/
|
||||
|
||||
// Get configuration from environment
|
||||
$port = getenv('BOT_PORT') ?: '8080';
|
||||
$secret = getenv('BOT_SECRET');
|
||||
|
||||
if (!$secret) {
|
||||
fwrite(STDERR, "ERROR: BOT_SECRET environment variable is required\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/game.php';
|
||||
require_once __DIR__ . '/strategy.php';
|
||||
|
||||
// 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, "Bot listening on port $port\n");
|
||||
|
||||
while ($conn = stream_socket_accept($server)) {
|
||||
handle_request($conn, $secret);
|
||||
fclose($conn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming HTTP request
|
||||
*/
|
||||
function handle_request($conn, string $secret): 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, $headers, $body);
|
||||
return;
|
||||
}
|
||||
|
||||
send_response($conn, 404, 'text/plain', 'Not Found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle turn request
|
||||
*/
|
||||
function handle_turn($conn, string $secret, 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);
|
||||
|
||||
// Log match start
|
||||
if ($gameState->turn === 0) {
|
||||
$seasonId = $gameState->config->seasonId ?? '';
|
||||
$rulesVersion = $gameState->config->rulesVersion ?? '';
|
||||
fwrite(STDOUT, "match={$gameState->matchId} season_id={$seasonId} rules_version={$rulesVersion} rows={$gameState->config->rows} cols={$gameState->config->cols}\n");
|
||||
}
|
||||
|
||||
// Compute moves
|
||||
$moves = compute_moves($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);
|
||||
}
|
||||
62
starters/php/strategy.php
Normal file
62
starters/php/strategy.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
/**
|
||||
* Strategy implementation for AI Code Battle.
|
||||
*
|
||||
* This file contains the compute_moves() function which is called each turn.
|
||||
* Implement your bot logic here.
|
||||
*
|
||||
* Available helpers from game.php:
|
||||
* - toroidal_manhattan(Position $a, Position $b, int $rows, int $cols): int
|
||||
* - toroidal_chebyshev(Position $a, Position $b, int $rows, int $cols): int
|
||||
* - bfs(Position $start, Position $goal, callable $passable, int $rows, int $cols): ?array
|
||||
* - cardinal_steps(Position $p, int $rows, int $cols): array
|
||||
*/
|
||||
|
||||
/**
|
||||
* Compute moves for all owned bots.
|
||||
*
|
||||
* @param GameState $state The current game state
|
||||
* @return Move[] Array of move commands (bots not included stay in place)
|
||||
*/
|
||||
function compute_moves(GameState $state): array {
|
||||
$myId = $state->you->id;
|
||||
$config = $state->config;
|
||||
$rows = $config->rows;
|
||||
$cols = $config->cols;
|
||||
|
||||
$moves = [];
|
||||
|
||||
foreach ($state->bots as $bot) {
|
||||
if ($bot->owner !== $myId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Example: Find nearest energy and move toward it
|
||||
if (!empty($state->energy)) {
|
||||
$bestDist = PHP_INT_MAX;
|
||||
$bestDir = null;
|
||||
|
||||
foreach (cardinal_steps($bot->position, $rows, $cols) as $step) {
|
||||
foreach ($state->energy as $energy) {
|
||||
$dist = toroidal_manhattan($step['pos'], $energy, $rows, $cols);
|
||||
if ($dist < $bestDist) {
|
||||
$bestDist = $dist;
|
||||
$bestDir = $step['dir'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($bestDir) {
|
||||
$moves[] = new Move($bot->position, $bestDir);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Default: hold position (don't add a move)
|
||||
// Or move randomly:
|
||||
// $directions = ['N', 'E', 'S', 'W'];
|
||||
// $moves[] = new Move($bot->position, $directions[array_rand($directions)]);
|
||||
}
|
||||
|
||||
return $moves;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue