fix: guard invalid turn transitions
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user