diff --git a/src/Domain/Battlefield.php b/src/Domain/Battlefield.php index 201904f..5f70143 100644 --- a/src/Domain/Battlefield.php +++ b/src/Domain/Battlefield.php @@ -9,29 +9,46 @@ use SplQueue; final readonly class Battlefield { + /** @var array */ + private array $terrain; + /** - * @param array $terrain + * @param array $terrain */ public function __construct( public int $width, public int $height, - private array $terrain = [], + 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) { + $validatedTerrain = []; + + foreach ($terrain as $key => $terrainType) { + if (!$terrainType instanceof Terrain) { + throw new InvalidArgumentException("Invalid terrain value at coordinate {$key}."); + } + + 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}."); + } + if (!$this->contains($position)) { throw new InvalidArgumentException("Terrain coordinate {$key} is outside the battlefield."); } + + $validatedTerrain[$key] = $terrainType; } + + $this->terrain = $validatedTerrain; } public function contains(Position $position): bool @@ -53,9 +70,21 @@ final readonly class Battlefield */ 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; } diff --git a/tests/Unit/Domain/BattlefieldTest.php b/tests/Unit/Domain/BattlefieldTest.php index 325edc8..f6061d0 100644 --- a/tests/Unit/Domain/BattlefieldTest.php +++ b/tests/Unit/Domain/BattlefieldTest.php @@ -19,6 +19,56 @@ final class BattlefieldTest extends TestCase new Battlefield(7, 16); } + public function testItRejectsNonCanonicalTerrainCoordinates(): void + { + $this->expectException(InvalidArgumentException::class); + + new Battlefield(8, 8, ['01:0' => Terrain::Forest]); + } + + public function testItRejectsMalformedTerrainCoordinates(): void + { + $this->expectException(InvalidArgumentException::class); + + new Battlefield(8, 8, ['not-a-coordinate' => Terrain::Forest]); + } + + public function testItRejectsInvalidTerrainValues(): void + { + $this->expectException(InvalidArgumentException::class); + + $terrain = ['0:0' => 'forest']; + + new Battlefield(8, 8, $terrain); + } + + public function testReachableRejectsAStartOutsideTheBattlefield(): void + { + $battlefield = new Battlefield(8, 8); + + $this->expectException(InvalidArgumentException::class); + + $battlefield->reachable(new Position(-1, 0), 2, []); + } + + public function testReachableRejectsANegativeBudget(): void + { + $battlefield = new Battlefield(8, 8); + + $this->expectException(InvalidArgumentException::class); + + $battlefield->reachable(new Position(0, 0), -1, []); + } + + public function testReachableRejectsOccupiedPositionsOutsideTheBattlefield(): void + { + $battlefield = new Battlefield(8, 8); + + $this->expectException(InvalidArgumentException::class); + + $battlefield->reachable(new Position(0, 0), 2, [new Position(8, 0)]); + } + public function testItFindsReachableTilesUsingTerrainCostsAndObstacles(): void { $battlefield = new Battlefield(8, 8, [ @@ -34,11 +84,10 @@ final class BattlefieldTest extends TestCase [new Position(0, 2)], ); - self::assertSame(0, $reachable['0:0']); - self::assertSame(2, $reachable['1:0']); - self::assertSame(2, $reachable['0:1']); - self::assertArrayNotHasKey('2:0', $reachable); - self::assertArrayNotHasKey('1:1', $reachable); - self::assertArrayNotHasKey('0:2', $reachable); + self::assertSame([ + '0:0' => 0, + '1:0' => 2, + '0:1' => 2, + ], $reachable); } }