fix: guard invalid turn transitions

This commit is contained in:
Keith Solomon
2026-07-04 16:07:36 -05:00
parent acfbbf4a91
commit fef22d7b56
2 changed files with 132 additions and 5 deletions
+17
View File
@@ -93,8 +93,25 @@ final class CombatEngine
throw new CombatException('Matches require alpha and bravo teams.'); 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; $endingTeamId = $match->activeTeamId;
$nextTeamId = $endingTeamId === 'alpha' ? 'bravo' : 'alpha'; $nextTeamId = $endingTeamId === 'alpha' ? 'bravo' : 'alpha';
if ($nextTeamId === 'alpha' && $match->round === PHP_INT_MAX) {
throw new CombatException('Match round limit reached.');
}
$units = []; $units = [];
foreach ($match->units as $unit) { foreach ($match->units as $unit) {
+115 -5
View File
@@ -564,14 +564,21 @@ final class CombatEngineTest extends TestCase
(new CombatEngine())->endTurn($match); (new CombatEngine())->endTurn($match);
} }
public function testEndTurnRejectsMatchesWithoutExactlyAlphaAndBravoTeams(): void /**
* @param list<string> $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( $match = new MatchState(
new Battlefield(8, 8), new Battlefield(8, 8),
[ $units,
$this->unit('alpha-1', 'alpha', new Position(0, 0)),
$this->unit('charlie-1', 'charlie', new Position(7, 7)),
],
'alpha', 'alpha',
); );
@@ -581,6 +588,109 @@ final class CombatEngineTest extends TestCase
(new CombatEngine())->endTurn($match); (new CombatEngine())->endTurn($match);
} }
/**
* @return iterable<string, array{list<string>}>
*/
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<string, array{string, string}>
*/
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 public function testEndTurnDoesNotRefreshDefeatedUnitsOnIncomingTeam(): void
{ {
$match = new MatchState( $match = new MatchState(