From 84fb175c0cbb622d167010af042fab3b76d9ac0c Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Sun, 5 Jul 2026 20:02:13 -0500 Subject: [PATCH] feat: resolve objective control and objective victory --- src/Domain/CombatEngine.php | 45 ++++++++++-------- tests/Unit/Domain/CombatEngineTest.php | 63 ++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/src/Domain/CombatEngine.php b/src/Domain/CombatEngine.php index 77e4375..2880a8c 100644 --- a/src/Domain/CombatEngine.php +++ b/src/Domain/CombatEngine.php @@ -180,12 +180,7 @@ final class CombatEngine actionLog: [...$match->actionLog, "{$endingTeamId} ended turn"], ); - $winner = $this->resolveObjectiveVictory($next); - if ($winner !== null) { - $next = $next->withWinner($winner); - } - - return $next; + return $this->resolveObjectiveVictory($next, $endingTeamId === $teamIds[0]); } /** @@ -291,29 +286,39 @@ final class CombatEngine return $match->withWinner($actingTeamId); } - private function resolveObjectiveVictory(MatchState $match): ?string + /** + * @param bool $endingTeamIsFirstInOrder true when the team that just ended is the first team in + * sorted team order; this identifies the first half of a round. + */ + private function resolveObjectiveVictory(MatchState $match, bool $endingTeamIsFirstInOrder): MatchState { if ($match->victoryCondition !== VictoryCondition::HoldObjective || $match->objectives === []) { - return null; + return $match; } - /** @var list $objectiveList */ - $objectiveList = array_values($match->objectives); - $objective = $objectiveList[0]; - $controller = $this->objectiveController($match, $objective->position); + $next = $match; - if ($controller === null) { - return null; + if ($endingTeamIsFirstInOrder) { + /** @var list $objectiveList */ + $objectiveList = array_values($match->objectives); + $objective = $objectiveList[0]; + $controller = $this->objectiveController($match, $objective->position); + + if ($controller !== null) { + $control = (new ObjectiveControl($match->objectiveControl))->recordRound($controller); + $next = $match->withObjectiveControl($control->roundsByTeam); + } + + return $next; } - $control = (new ObjectiveControl($match->objectiveControl))->recordRound($controller); - $next = $match->withObjectiveControl($control->roundsByTeam); - - if (($control->roundsByTeam[$controller] ?? 0) >= $next->holdRoundsRequired) { - return $controller; + foreach ($match->objectiveControl as $teamId => $count) { + if ($count >= $match->holdRoundsRequired) { + return $next->withWinner($teamId); + } } - return null; + return $next; } private function objectiveController(MatchState $match, Position $tile): ?string diff --git a/tests/Unit/Domain/CombatEngineTest.php b/tests/Unit/Domain/CombatEngineTest.php index b64ddb4..59f0026 100644 --- a/tests/Unit/Domain/CombatEngineTest.php +++ b/tests/Unit/Domain/CombatEngineTest.php @@ -9,9 +9,11 @@ use BattleForge\Domain\Battlefield; use BattleForge\Domain\CombatEngine; use BattleForge\Domain\CombatException; use BattleForge\Domain\MatchState; +use BattleForge\Domain\ObjectiveMarker; use BattleForge\Domain\Position; use BattleForge\Domain\Terrain; use BattleForge\Domain\UnitState; +use BattleForge\Domain\VictoryCondition; use InvalidArgumentException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -973,4 +975,65 @@ final class CombatEngineTest extends TestCase (new CombatEngine())->useAbility($match, 'scout-1', 'heal', new Position(0, 1)); } + + public function testEndTurnTalliesObjectiveControlAfterAFullRound(): void + { + $alpha = $this->unit('alpha-1', 'alpha', new Position(4, 4)); + $bravo = $this->unit('bravo-1', 'bravo', new Position(7, 7)); + $match = new MatchState( + new Battlefield(8, 8), + [$alpha, $bravo], + 'alpha', + objectives: ['objective-1' => new ObjectiveMarker('objective-1', new Position(4, 4))], + victoryCondition: VictoryCondition::HoldObjective, + holdRoundsRequired: 2, + ); + + $afterAlphaEnd = (new CombatEngine())->endTurn($match); + $afterBravoEnd = (new CombatEngine())->endTurn($afterAlphaEnd); + + self::assertSame(['alpha' => 1], $afterAlphaEnd->objectiveControl); + self::assertSame(['alpha' => 1], $afterBravoEnd->objectiveControl); + self::assertNull($afterBravoEnd->winnerTeamId); + } + + public function testEndTurnDeclaresWinnerWhenHoldRoundsReached(): void + { + $alpha = $this->unit('alpha-1', 'alpha', new Position(4, 4)); + $bravo = $this->unit('bravo-1', 'bravo', new Position(7, 7)); + $match = new MatchState( + new Battlefield(8, 8), + [$alpha, $bravo], + 'alpha', + objectives: ['objective-1' => new ObjectiveMarker('objective-1', new Position(4, 4))], + victoryCondition: VictoryCondition::HoldObjective, + holdRoundsRequired: 1, + objectiveControl: ['alpha' => 0], + ); + + $afterAlphaEnd = (new CombatEngine())->endTurn($match); + $afterBravoEnd = (new CombatEngine())->endTurn($afterAlphaEnd); + + self::assertSame('alpha', $afterBravoEnd->winnerTeamId); + self::assertSame(['alpha' => 1], $afterBravoEnd->objectiveControl); + } + + public function testEndTurnDoesNotAwardControlWhenNoTeamStandsOnTheObjective(): void + { + $alpha = $this->unit('alpha-1', 'alpha', new Position(0, 0)); + $bravo = $this->unit('bravo-1', 'bravo', new Position(7, 7)); + $match = new MatchState( + new Battlefield(8, 8), + [$alpha, $bravo], + 'alpha', + objectives: ['objective-1' => new ObjectiveMarker('objective-1', new Position(4, 4))], + victoryCondition: VictoryCondition::HoldObjective, + holdRoundsRequired: 1, + ); + + $next = (new CombatEngine())->endTurn($match); + + self::assertSame([], $next->objectiveControl); + self::assertNull($next->winnerTeamId); + } }