diff --git a/src/Domain/CombatEngine.php b/src/Domain/CombatEngine.php index b8e65d2..edc23b2 100644 --- a/src/Domain/CombatEngine.php +++ b/src/Domain/CombatEngine.php @@ -76,6 +76,39 @@ final class CombatEngine return $next->withWinner($attacker->teamId); } + public function endTurn(MatchState $match): MatchState + { + $this->assertMatchActive($match); + + $teamIds = []; + + foreach ($match->units as $unit) { + $teamIds[$unit->teamId] = true; + } + + $teamIds = array_keys($teamIds); + sort($teamIds); + + if ($teamIds !== ['alpha', 'bravo']) { + throw new CombatException('Matches require alpha and bravo teams.'); + } + + $endingTeamId = $match->activeTeamId; + $nextTeamId = $endingTeamId === 'alpha' ? 'bravo' : 'alpha'; + $units = []; + + foreach ($match->units as $unit) { + $units[] = $unit->teamId === $nextTeamId ? $unit->startTurn() : $unit; + } + + return $match->copy( + units: $units, + activeTeamId: $nextTeamId, + round: $match->round + ($nextTeamId === 'alpha' ? 1 : 0), + actionLog: [...$match->actionLog, "{$endingTeamId} ended turn"], + ); + } + private function unitOrFail(MatchState $match, string $unitId): UnitState { try { diff --git a/tests/Unit/Domain/CombatEngineTest.php b/tests/Unit/Domain/CombatEngineTest.php index 75b11ba..8c9b1aa 100644 --- a/tests/Unit/Domain/CombatEngineTest.php +++ b/tests/Unit/Domain/CombatEngineTest.php @@ -473,6 +473,147 @@ final class CombatEngineTest extends TestCase self::assertNull($next->winnerTeamId); } + public function testEndTurnPassesControlToBravoAndRefreshesItsLivingUnits(): void + { + $match = new MatchState( + new Battlefield(8, 8), + [ + $this->unit( + 'alpha-1', + 'alpha', + new Position(0, 0), + actionsRemaining: 1, + hasAttacked: true, + ), + $this->unit( + 'bravo-1', + 'bravo', + new Position(7, 7), + actionsRemaining: 0, + hasAttacked: true, + ), + ], + 'alpha', + actionLog: ['match started'], + ); + + $next = (new CombatEngine())->endTurn($match); + + self::assertSame('alpha', $match->activeTeamId); + self::assertSame(1, $match->round); + self::assertSame(1, $match->unit('alpha-1')->actionsRemaining); + self::assertTrue($match->unit('alpha-1')->hasAttacked); + self::assertSame(0, $match->unit('bravo-1')->actionsRemaining); + self::assertTrue($match->unit('bravo-1')->hasAttacked); + self::assertSame(['match started'], $match->actionLog); + self::assertSame('bravo', $next->activeTeamId); + self::assertSame(1, $next->round); + self::assertSame(1, $next->unit('alpha-1')->actionsRemaining); + self::assertTrue($next->unit('alpha-1')->hasAttacked); + self::assertSame(2, $next->unit('bravo-1')->actionsRemaining); + self::assertFalse($next->unit('bravo-1')->hasAttacked); + self::assertSame(['match started', 'alpha ended turn'], $next->actionLog); + } + + public function testEndTurnPassesControlToAlphaAndStartsANewRound(): void + { + $match = new MatchState( + new Battlefield(8, 8), + [ + $this->unit( + 'alpha-1', + 'alpha', + new Position(0, 0), + actionsRemaining: 0, + hasAttacked: true, + ), + $this->unit('bravo-1', 'bravo', new Position(7, 7), actionsRemaining: 1), + ], + 'bravo', + round: 1, + actionLog: ['alpha ended turn'], + ); + + $next = (new CombatEngine())->endTurn($match); + + self::assertSame('alpha', $next->activeTeamId); + self::assertSame(2, $next->round); + self::assertSame(2, $next->unit('alpha-1')->actionsRemaining); + self::assertFalse($next->unit('alpha-1')->hasAttacked); + self::assertSame(1, $next->unit('bravo-1')->actionsRemaining); + self::assertSame(['alpha ended turn', 'bravo ended turn'], $next->actionLog); + self::assertSame('bravo', $match->activeTeamId); + self::assertSame(1, $match->round); + self::assertSame(0, $match->unit('alpha-1')->actionsRemaining); + self::assertTrue($match->unit('alpha-1')->hasAttacked); + self::assertSame(['alpha ended turn'], $match->actionLog); + } + + public function testEndTurnRejectsCompletedMatchBeforeValidatingTeams(): void + { + $match = new MatchState( + new Battlefield(8, 8), + [$this->unit('alpha-1', 'alpha', new Position(0, 0))], + 'alpha', + winnerTeamId: 'alpha', + ); + + $this->expectException(CombatException::class); + $this->expectExceptionMessage('The match is already complete.'); + + (new CombatEngine())->endTurn($match); + } + + public function testEndTurnRejectsMatchesWithoutExactlyAlphaAndBravoTeams(): void + { + $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)), + ], + 'alpha', + ); + + $this->expectException(CombatException::class); + $this->expectExceptionMessage('Matches require alpha and bravo teams.'); + + (new CombatEngine())->endTurn($match); + } + + public function testEndTurnDoesNotRefreshDefeatedUnitsOnIncomingTeam(): void + { + $match = new MatchState( + new Battlefield(8, 8), + [ + $this->unit('alpha-1', 'alpha', new Position(0, 0)), + $this->unit( + 'bravo-defeated', + 'bravo', + new Position(6, 7), + health: 0, + actionsRemaining: 0, + hasAttacked: true, + ), + $this->unit( + 'bravo-living', + 'bravo', + new Position(7, 7), + actionsRemaining: 0, + hasAttacked: true, + ), + ], + 'alpha', + ); + + $next = (new CombatEngine())->endTurn($match); + + self::assertSame(0, $next->unit('bravo-defeated')->actionsRemaining); + self::assertTrue($next->unit('bravo-defeated')->hasAttacked); + self::assertSame(2, $next->unit('bravo-living')->actionsRemaining); + self::assertFalse($next->unit('bravo-living')->hasAttacked); + } + private function match(): MatchState { return new MatchState(