diff --git a/src/Domain/CombatEngine.php b/src/Domain/CombatEngine.php index edc23b2..cfcaeee 100644 --- a/src/Domain/CombatEngine.php +++ b/src/Domain/CombatEngine.php @@ -93,8 +93,25 @@ final class CombatEngine throw new CombatException('Matches require alpha and bravo teams.'); } + $livingTeams = ['alpha' => false, 'bravo' => false]; + + foreach ($match->units as $unit) { + if (!$unit->isDefeated()) { + $livingTeams[$unit->teamId] = true; + } + } + + if (!$livingTeams['alpha'] || !$livingTeams['bravo']) { + throw new CombatException('Cannot end turn while a team is eliminated.'); + } + $endingTeamId = $match->activeTeamId; $nextTeamId = $endingTeamId === 'alpha' ? 'bravo' : 'alpha'; + + if ($nextTeamId === 'alpha' && $match->round === PHP_INT_MAX) { + throw new CombatException('Match round limit reached.'); + } + $units = []; foreach ($match->units as $unit) { diff --git a/tests/Unit/Domain/CombatEngineTest.php b/tests/Unit/Domain/CombatEngineTest.php index 8c9b1aa..e2bcd63 100644 --- a/tests/Unit/Domain/CombatEngineTest.php +++ b/tests/Unit/Domain/CombatEngineTest.php @@ -564,14 +564,21 @@ final class CombatEngineTest extends TestCase (new CombatEngine())->endTurn($match); } - public function testEndTurnRejectsMatchesWithoutExactlyAlphaAndBravoTeams(): void + /** + * @param list $teamIds + */ + #[DataProvider('invalidEndTurnTeamProvider')] + public function testEndTurnRejectsMatchesWithoutExactlyAlphaAndBravoTeams(array $teamIds): void { + $units = []; + + foreach ($teamIds as $index => $teamId) { + $units[] = $this->unit("{$teamId}-{$index}", $teamId, new Position($index, 0)); + } + $match = new MatchState( new Battlefield(8, 8), - [ - $this->unit('alpha-1', 'alpha', new Position(0, 0)), - $this->unit('charlie-1', 'charlie', new Position(7, 7)), - ], + $units, 'alpha', ); @@ -581,6 +588,109 @@ final class CombatEngineTest extends TestCase (new CombatEngine())->endTurn($match); } + /** + * @return iterable}> + */ + public static function invalidEndTurnTeamProvider(): iterable + { + yield 'only alpha' => [['alpha']]; + yield 'alpha and charlie' => [['alpha', 'charlie']]; + yield 'alpha, bravo, and charlie' => [['alpha', 'bravo', 'charlie']]; + } + + #[DataProvider('eliminatedEndTurnTeamProvider')] + public function testEndTurnRejectsEliminatedTeam(string $activeTeamId, string $eliminatedTeamId): void + { + $match = new MatchState( + new Battlefield(8, 8), + [ + $this->unit( + 'alpha-1', + 'alpha', + new Position(0, 0), + health: $eliminatedTeamId === 'alpha' ? 0 : 10, + ), + $this->unit( + 'bravo-1', + 'bravo', + new Position(7, 7), + health: $eliminatedTeamId === 'bravo' ? 0 : 10, + ), + ], + $activeTeamId, + ); + + $this->expectException(CombatException::class); + $this->expectExceptionMessage('Cannot end turn while a team is eliminated.'); + + (new CombatEngine())->endTurn($match); + } + + /** + * @return iterable + */ + public static function eliminatedEndTurnTeamProvider(): iterable + { + yield 'incoming team' => ['alpha', 'bravo']; + yield 'active team' => ['alpha', 'alpha']; + } + + public function testEndTurnRejectsEliminatedTeamBeforeRoundLimit(): void + { + $match = new MatchState( + new Battlefield(8, 8), + [ + $this->unit('alpha-1', 'alpha', new Position(0, 0), health: 0), + $this->unit('bravo-1', 'bravo', new Position(7, 7)), + ], + 'bravo', + round: PHP_INT_MAX, + ); + + $this->expectException(CombatException::class); + $this->expectExceptionMessage('Cannot end turn while a team is eliminated.'); + + (new CombatEngine())->endTurn($match); + } + + public function testEndTurnRejectsRoundIncrementBeyondIntegerLimit(): void + { + $match = new MatchState( + new Battlefield(8, 8), + [ + $this->unit('alpha-1', 'alpha', new Position(0, 0)), + $this->unit('bravo-1', 'bravo', new Position(7, 7)), + ], + 'bravo', + round: PHP_INT_MAX, + ); + + $this->expectException(CombatException::class); + $this->expectExceptionMessage('Match round limit reached.'); + + (new CombatEngine())->endTurn($match); + } + + public function testEndTurnAllowsAlphaToPassAtIntegerRoundLimit(): void + { + $match = new MatchState( + new Battlefield(8, 8), + [ + $this->unit('alpha-1', 'alpha', new Position(0, 0)), + $this->unit('bravo-1', 'bravo', new Position(7, 7), actionsRemaining: 0), + ], + 'alpha', + round: PHP_INT_MAX, + ); + + $next = (new CombatEngine())->endTurn($match); + + self::assertSame('bravo', $next->activeTeamId); + self::assertSame(PHP_INT_MAX, $next->round); + self::assertSame(2, $next->unit('bravo-1')->actionsRemaining); + self::assertSame(['alpha ended turn'], $next->actionLog); + } + public function testEndTurnDoesNotRefreshDefeatedUnitsOnIncomingTeam(): void { $match = new MatchState(