diff --git a/src/Domain/Archetype.php b/src/Domain/Archetype.php new file mode 100644 index 0000000..f904493 --- /dev/null +++ b/src/Domain/Archetype.php @@ -0,0 +1,13 @@ + + */ + public static function templates(): array + { + static $cache = null; + + if ($cache !== null) { + return $cache; + } + + $cache = [ + Archetype::Defender->value => new ArchetypeTemplate( + minHealth: 10, + maxHealth: 14, + minAttack: 2, + maxAttack: 4, + minDefense: 3, + maxDefense: 5, + minSpeed: 1, + maxSpeed: 3, + allowedAbilities: ['buff'], + ), + Archetype::Striker->value => new ArchetypeTemplate( + minHealth: 6, + maxHealth: 10, + minAttack: 4, + maxAttack: 6, + minDefense: 1, + maxDefense: 2, + minSpeed: 2, + maxSpeed: 4, + allowedAbilities: ['area_damage'], + ), + Archetype::Support->value => new ArchetypeTemplate( + minHealth: 5, + maxHealth: 9, + minAttack: 1, + maxAttack: 3, + minDefense: 1, + maxDefense: 3, + minSpeed: 2, + maxSpeed: 4, + allowedAbilities: ['heal', 'buff'], + ), + Archetype::Scout->value => new ArchetypeTemplate( + minHealth: 4, + maxHealth: 7, + minAttack: 2, + maxAttack: 4, + minDefense: 0, + maxDefense: 2, + minSpeed: 4, + maxSpeed: 6, + allowedAbilities: [], + ), + ]; + + return $cache; + } +} diff --git a/src/Domain/ArchetypeTemplate.php b/src/Domain/ArchetypeTemplate.php new file mode 100644 index 0000000..a447912 --- /dev/null +++ b/src/Domain/ArchetypeTemplate.php @@ -0,0 +1,50 @@ + $allowedAbilities */ + public function __construct( + public int $minHealth, + public int $maxHealth, + public int $minAttack, + public int $maxAttack, + public int $minDefense, + public int $maxDefense, + public int $minSpeed, + public int $maxSpeed, + public array $allowedAbilities, + ) { + if ($this->minHealth > $this->maxHealth) { + throw new InvalidArgumentException('Health range is inverted.'); + } + if ($this->minAttack > $this->maxAttack) { + throw new InvalidArgumentException('Attack range is inverted.'); + } + if ($this->minDefense > $this->maxDefense) { + throw new InvalidArgumentException('Defense range is inverted.'); + } + if ($this->minSpeed > $this->maxSpeed || $this->minSpeed < 1) { + throw new InvalidArgumentException('Speed range must be at least 1 and non-inverted.'); + } + if ($this->minHealth < 1) { + throw new InvalidArgumentException('Minimum health must be at least 1.'); + } + } + + /** @return array{maxHealth: int, attack: int, defense: int, speed: int} */ + public function defaultStats(): array + { + return [ + 'maxHealth' => (int) ceil(($this->minHealth + $this->maxHealth) / 2), + 'attack' => (int) ceil(($this->minAttack + $this->maxAttack) / 2), + 'defense' => (int) ceil(($this->minDefense + $this->maxDefense) / 2), + 'speed' => (int) ceil(($this->minSpeed + $this->maxSpeed) / 2), + ]; + } +} diff --git a/tests/Unit/Domain/ArchetypeCatalogTest.php b/tests/Unit/Domain/ArchetypeCatalogTest.php new file mode 100644 index 0000000..7331be8 --- /dev/null +++ b/tests/Unit/Domain/ArchetypeCatalogTest.php @@ -0,0 +1,56 @@ + $template) { + self::assertInstanceOf(ArchetypeTemplate::class, $template); + self::assertLessThanOrEqual($template->maxHealth, $template->minHealth); + self::assertLessThanOrEqual($template->maxAttack, $template->minAttack); + self::assertLessThanOrEqual($template->maxDefense, $template->minDefense); + self::assertLessThanOrEqual($template->maxSpeed, $template->minSpeed); + self::assertGreaterThanOrEqual(1, $template->minSpeed); + } + } + + public function testEveryArchetypeTemplateDefaultsToTheCenterOfItsRanges(): void + { + foreach (ArchetypeCatalog::templates() as $template) { + $defaults = $template->defaultStats(); + + self::assertSame((int) ceil(($template->minHealth + $template->maxHealth) / 2), $defaults['maxHealth']); + self::assertSame((int) ceil(($template->minAttack + $template->maxAttack) / 2), $defaults['attack']); + self::assertSame((int) ceil(($template->minDefense + $template->maxDefense) / 2), $defaults['defense']); + self::assertSame((int) ceil(($template->minSpeed + $template->maxSpeed) / 2), $defaults['speed']); + } + } + + public function testDefaultStatsReturnAllFourKeys(): void + { + $template = ArchetypeCatalog::templates()[Archetype::Defender->value]; + + self::assertSame( + ['maxHealth', 'attack', 'defense', 'speed'], + array_keys($template->defaultStats()), + ); + } +} diff --git a/tests/Unit/Domain/ArchetypeTemplateTest.php b/tests/Unit/Domain/ArchetypeTemplateTest.php new file mode 100644 index 0000000..2324c5a --- /dev/null +++ b/tests/Unit/Domain/ArchetypeTemplateTest.php @@ -0,0 +1,62 @@ +expectException(\InvalidArgumentException::class); + + new ArchetypeTemplate( + minHealth: 10, + maxHealth: 5, + minAttack: 1, + maxAttack: 3, + minDefense: 0, + maxDefense: 2, + minSpeed: 1, + maxSpeed: 4, + allowedAbilities: [], + ); + } + + public function testItRejectsZeroMinimumSpeed(): void + { + $this->expectException(\InvalidArgumentException::class); + + new ArchetypeTemplate( + minHealth: 5, + maxHealth: 10, + minAttack: 1, + maxAttack: 3, + minDefense: 0, + maxDefense: 2, + minSpeed: 0, + maxSpeed: 4, + allowedAbilities: [], + ); + } + + public function testItExposesAllowedAbilitiesVerbatim(): void + { + $template = new ArchetypeTemplate( + minHealth: 5, + maxHealth: 10, + minAttack: 1, + maxAttack: 3, + minDefense: 0, + maxDefense: 2, + minSpeed: 1, + maxSpeed: 4, + allowedAbilities: ['heal', 'buff'], + ); + + self::assertSame(['heal', 'buff'], $template->allowedAbilities); + } +}