diff --git a/src/Domain/Battlefield.php b/src/Domain/Battlefield.php index 5f70143..c22eb2e 100644 --- a/src/Domain/Battlefield.php +++ b/src/Domain/Battlefield.php @@ -13,7 +13,7 @@ final readonly class Battlefield private array $terrain; /** - * @param array $terrain + * @param array $terrain */ public function __construct( public int $width, @@ -27,19 +27,11 @@ final readonly class Battlefield $validatedTerrain = []; foreach ($terrain as $key => $terrainType) { - if (!$terrainType instanceof Terrain) { + if (!self::isTerrain($terrainType)) { 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}."); - } + $position = self::positionFromTerrainKey($key); if (!$this->contains($position)) { throw new InvalidArgumentException("Terrain coordinate {$key} is outside the battlefield."); @@ -51,6 +43,26 @@ final readonly class Battlefield $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 diff --git a/tests/Unit/Domain/BattlefieldTest.php b/tests/Unit/Domain/BattlefieldTest.php index f6061d0..d9089f4 100644 --- a/tests/Unit/Domain/BattlefieldTest.php +++ b/tests/Unit/Domain/BattlefieldTest.php @@ -19,6 +19,22 @@ final class BattlefieldTest extends TestCase new Battlefield(7, 16); } + public function testItAcceptsMinimumAndMaximumDimensions(): void + { + $minimum = new Battlefield(8, 8); + $maximum = new Battlefield(16, 16); + + self::assertSame([8, 8], [$minimum->width, $minimum->height]); + self::assertSame([16, 16], [$maximum->width, $maximum->height]); + } + + public function testItRejectsDimensionsAboveTheSupportedRange(): void + { + $this->expectException(InvalidArgumentException::class); + + new Battlefield(16, 17); + } + public function testItRejectsNonCanonicalTerrainCoordinates(): void { $this->expectException(InvalidArgumentException::class); @@ -37,9 +53,8 @@ final class BattlefieldTest extends TestCase { $this->expectException(InvalidArgumentException::class); - $terrain = ['0:0' => 'forest']; - - new Battlefield(8, 8, $terrain); + // @phpstan-ignore argument.type + new Battlefield(8, 8, ['0:0' => 'forest']); } public function testReachableRejectsAStartOutsideTheBattlefield(): void @@ -90,4 +105,17 @@ final class BattlefieldTest extends TestCase '0:1' => 2, ], $reachable); } + + public function testReachableReplacesAHighCostRouteWithALowerCostDetour(): void + { + $battlefield = new Battlefield(8, 8, [ + '1:0' => Terrain::Forest, + '2:0' => Terrain::Forest, + '3:0' => Terrain::Forest, + ]); + + $reachable = $battlefield->reachable(new Position(0, 0), 7, []); + + self::assertSame(6, $reachable['4:0']); + } }