feat: model battlefield terrain and movement costs

This commit is contained in:
Keith Solomon
2026-07-04 15:08:40 -05:00
parent 7b82adbd71
commit 189be5bc64
4 changed files with 203 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace BattleForge\Domain;
use InvalidArgumentException;
use SplQueue;
final readonly class Battlefield
{
/**
* @param array<string, Terrain> $terrain
*/
public function __construct(
public int $width,
public int $height,
private array $terrain = [],
) {
if ($width < 8 || $width > 16 || $height < 8 || $height > 16) {
throw new InvalidArgumentException('Battlefield dimensions must each be between 8 and 16.');
}
foreach (array_keys($terrain) as $key) {
if (preg_match('/^(\d+):(\d+)$/', $key, $matches) !== 1) {
throw new InvalidArgumentException("Invalid terrain coordinate: {$key}.");
}
$position = new Position((int) $matches[1], (int) $matches[2]);
if (!$this->contains($position)) {
throw new InvalidArgumentException("Terrain coordinate {$key} is outside the battlefield.");
}
}
}
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<Position> $occupied
* @return array<string, int>
*/
public function reachable(Position $start, int $budget, array $occupied): array
{
$occupiedKeys = [];
foreach ($occupied as $position) {
$occupiedKeys[$position->key()] = true;
}
$costs = [$start->key() => 0];
/** @var SplQueue<Position> $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<Position>
*/
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(...)));
}
}