feat: resolve objective control and objective victory

This commit is contained in:
Keith Solomon
2026-07-05 20:06:27 -05:00
parent 6befe7b3e2
commit 84fb175c0c
2 changed files with 88 additions and 20 deletions
+25 -20
View File
@@ -180,12 +180,7 @@ final class CombatEngine
actionLog: [...$match->actionLog, "{$endingTeamId} ended turn"], actionLog: [...$match->actionLog, "{$endingTeamId} ended turn"],
); );
$winner = $this->resolveObjectiveVictory($next); return $this->resolveObjectiveVictory($next, $endingTeamId === $teamIds[0]);
if ($winner !== null) {
$next = $next->withWinner($winner);
}
return $next;
} }
/** /**
@@ -291,29 +286,39 @@ final class CombatEngine
return $match->withWinner($actingTeamId); 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 === []) { if ($match->victoryCondition !== VictoryCondition::HoldObjective || $match->objectives === []) {
return null; return $match;
} }
/** @var list<ObjectiveMarker> $objectiveList */ $next = $match;
$objectiveList = array_values($match->objectives);
$objective = $objectiveList[0];
$controller = $this->objectiveController($match, $objective->position);
if ($controller === null) { if ($endingTeamIsFirstInOrder) {
return null; /** @var list<ObjectiveMarker> $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); foreach ($match->objectiveControl as $teamId => $count) {
$next = $match->withObjectiveControl($control->roundsByTeam); if ($count >= $match->holdRoundsRequired) {
return $next->withWinner($teamId);
if (($control->roundsByTeam[$controller] ?? 0) >= $next->holdRoundsRequired) { }
return $controller;
} }
return null; return $next;
} }
private function objectiveController(MatchState $match, Position $tile): ?string private function objectiveController(MatchState $match, Position $tile): ?string
+63
View File
@@ -9,9 +9,11 @@ use BattleForge\Domain\Battlefield;
use BattleForge\Domain\CombatEngine; use BattleForge\Domain\CombatEngine;
use BattleForge\Domain\CombatException; use BattleForge\Domain\CombatException;
use BattleForge\Domain\MatchState; use BattleForge\Domain\MatchState;
use BattleForge\Domain\ObjectiveMarker;
use BattleForge\Domain\Position; use BattleForge\Domain\Position;
use BattleForge\Domain\Terrain; use BattleForge\Domain\Terrain;
use BattleForge\Domain\UnitState; use BattleForge\Domain\UnitState;
use BattleForge\Domain\VictoryCondition;
use InvalidArgumentException; use InvalidArgumentException;
use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@@ -973,4 +975,65 @@ final class CombatEngineTest extends TestCase
(new CombatEngine())->useAbility($match, 'scout-1', 'heal', new Position(0, 1)); (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);
}
} }