*/ private array $terrain; /** * @param array $terrain */ public function __construct( public int $width, public int $height, array $terrain = [], ) { if ($width < 8 || $width > 16 || $height < 8 || $height > 16) { throw new InvalidArgumentException('Battlefield dimensions must each be between 8 and 16.'); } $validatedTerrain = []; foreach ($terrain as $key => $terrainType) { if (!self::isTerrain($terrainType)) { throw new InvalidArgumentException("Invalid terrain value at coordinate {$key}."); } $position = self::positionFromTerrainKey($key); if (!$this->contains($position)) { throw new InvalidArgumentException("Terrain coordinate {$key} is outside the battlefield."); } $validatedTerrain[$key] = $terrainType; } $this->terrain = $validatedTerrain; } private static function isTerrain(mixed $terrain): bool { return $terrain instanceof Terrain; } private static function positionFromTerrainKey(mixed $key): Position { if (!is_string($key) || preg_match('/^(\d+):(\d+)$/', $key, $matches) !== 1) { throw new InvalidArgumentException("Invalid terrain coordinate: {$key}."); } $position = new Position((int) $matches[1], (int) $matches[2]); if ($key !== $position->key()) { throw new InvalidArgumentException("Invalid terrain coordinate: {$key}."); } return $position; } public function contains(Position $position): bool { return $position->x >= 0 && $position->x < $this->width && $position->y >= 0 && $position->y < $this->height; } public function terrainAt(Position $position): Terrain { return $this->terrain[$position->key()] ?? Terrain::Open; } /** * @param list $occupied * @return array */ public function reachable(Position $start, int $budget, array $occupied): array { if (!$this->contains($start)) { throw new InvalidArgumentException('Reachability start position is outside the battlefield.'); } if ($budget < 0) { throw new InvalidArgumentException('Reachability budget cannot be negative.'); } $occupiedKeys = []; foreach ($occupied as $position) { if (!$this->contains($position)) { throw new InvalidArgumentException('Occupied position is outside the battlefield.'); } $occupiedKeys[$position->key()] = true; } $costs = [$start->key() => 0]; /** @var SplQueue $queue */ $queue = new SplQueue(); $queue->enqueue($start); while (!$queue->isEmpty()) { $current = $queue->dequeue(); $currentCost = $costs[$current->key()]; foreach ($this->neighbors($current) as $neighbor) { $key = $neighbor->key(); $movementCost = $this->terrainAt($neighbor)->movementCost(); if ($movementCost === null || isset($occupiedKeys[$key])) { continue; } $cost = $currentCost + $movementCost; if ($cost > $budget || (isset($costs[$key]) && $costs[$key] <= $cost)) { continue; } $costs[$key] = $cost; $queue->enqueue($neighbor); } } return $costs; } /** * @return list */ private function neighbors(Position $position): array { $neighbors = [ new Position($position->x + 1, $position->y), new Position($position->x - 1, $position->y), new Position($position->x, $position->y + 1), new Position($position->x, $position->y - 1), ]; return array_values(array_filter($neighbors, $this->contains(...))); } }