654 lines
23 KiB
PHP
654 lines
23 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace BattleForge\Tests\Unit\Domain;
|
|
|
|
use BattleForge\Domain\Battlefield;
|
|
use BattleForge\Domain\CombatEngine;
|
|
use BattleForge\Domain\CombatException;
|
|
use BattleForge\Domain\MatchState;
|
|
use BattleForge\Domain\Position;
|
|
use BattleForge\Domain\Terrain;
|
|
use BattleForge\Domain\UnitState;
|
|
use InvalidArgumentException;
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class CombatEngineTest extends TestCase
|
|
{
|
|
public function testMatchAndUnitStateAreImmutable(): void
|
|
{
|
|
$unit = $this->unit('unit-1', 'alpha', new Position(0, 0));
|
|
$match = new MatchState(new Battlefield(8, 8), [$unit], 'alpha');
|
|
|
|
$moved = $unit->moveTo(new Position(1, 0))->spendAction();
|
|
$next = $match->withUnit($moved);
|
|
|
|
self::assertSame('0:0', $match->unit('unit-1')->position->key());
|
|
self::assertSame('1:0', $next->unit('unit-1')->position->key());
|
|
self::assertSame(1, $next->unit('unit-1')->actionsRemaining);
|
|
}
|
|
|
|
public function testMoveUpdatesUnitAndAppendsActionLogWithoutMutatingMatch(): 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)),
|
|
],
|
|
'alpha',
|
|
actionLog: ['match started'],
|
|
);
|
|
|
|
$next = (new CombatEngine())->move($match, 'alpha-1', new Position(2, 0));
|
|
|
|
self::assertSame('0:0', $match->unit('alpha-1')->position->key());
|
|
self::assertSame(2, $match->unit('alpha-1')->actionsRemaining);
|
|
self::assertSame(['match started'], $match->actionLog);
|
|
self::assertSame('2:0', $next->unit('alpha-1')->position->key());
|
|
self::assertSame(1, $next->unit('alpha-1')->actionsRemaining);
|
|
self::assertSame(['match started', 'alpha-1 moved to 2:0'], $next->actionLog);
|
|
}
|
|
|
|
public function testMoveTranslatesUnknownUnitAtCommandBoundary(): void
|
|
{
|
|
$match = $this->match();
|
|
|
|
try {
|
|
(new CombatEngine())->move($match, 'missing', new Position(1, 0));
|
|
self::fail('Expected an unknown unit exception.');
|
|
} catch (CombatException $exception) {
|
|
self::assertSame('Unknown unit: missing.', $exception->getMessage());
|
|
self::assertInstanceOf(InvalidArgumentException::class, $exception->getPrevious());
|
|
self::assertSame('Unknown unit id: missing.', $exception->getPrevious()->getMessage());
|
|
}
|
|
}
|
|
|
|
public function testMoveRejectsCompletedMatchBeforeResolvingUnit(): void
|
|
{
|
|
$match = $this->match()->withWinner('alpha');
|
|
|
|
$this->expectException(CombatException::class);
|
|
$this->expectExceptionMessage('The match is already complete.');
|
|
|
|
(new CombatEngine())->move($match, 'missing', new Position(1, 0));
|
|
}
|
|
|
|
public function testMoveRejectsDefeatedActor(): 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)),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$this->expectException(CombatException::class);
|
|
$this->expectExceptionMessage('Defeated units cannot act.');
|
|
|
|
(new CombatEngine())->move($match, 'alpha-1', new Position(1, 0));
|
|
}
|
|
|
|
public function testMoveRejectsExhaustedActor(): void
|
|
{
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8),
|
|
[
|
|
$this->unit('alpha-1', 'alpha', new Position(0, 0), actionsRemaining: 0),
|
|
$this->unit('bravo-1', 'bravo', new Position(7, 7)),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$this->expectException(CombatException::class);
|
|
$this->expectExceptionMessage('Unit has no actions remaining.');
|
|
|
|
(new CombatEngine())->move($match, 'alpha-1', new Position(1, 0));
|
|
}
|
|
|
|
public function testMoveRejectsCurrentPosition(): void
|
|
{
|
|
$match = $this->match();
|
|
|
|
$this->expectException(CombatException::class);
|
|
$this->expectExceptionMessage('Destination is not reachable.');
|
|
|
|
(new CombatEngine())->move($match, 'alpha-1', new Position(0, 0));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, Terrain> $terrain
|
|
*/
|
|
#[DataProvider('unreachableDestinationProvider')]
|
|
public function testMoveRejectsUnreachableDestination(array $terrain, Position $destination): void
|
|
{
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8, $terrain),
|
|
[
|
|
$this->unit('alpha-1', 'alpha', new Position(0, 0)),
|
|
$this->unit('bravo-1', 'bravo', new Position(7, 7)),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$this->expectException(CombatException::class);
|
|
$this->expectExceptionMessage('Destination is not reachable.');
|
|
|
|
(new CombatEngine())->move($match, 'alpha-1', $destination);
|
|
}
|
|
|
|
/**
|
|
* @return iterable<string, array{array<string, Terrain>, Position}>
|
|
*/
|
|
public static function unreachableDestinationProvider(): iterable
|
|
{
|
|
yield 'outside battlefield' => [[], new Position(8, 0)];
|
|
yield 'outside speed budget' => [[], new Position(5, 0)];
|
|
yield 'impassable terrain' => [['1:0' => Terrain::Blocking], new Position(1, 0)];
|
|
}
|
|
|
|
public function testDefeatedUnitDoesNotBlockItsPosition(): void
|
|
{
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8),
|
|
[
|
|
$this->unit('alpha-1', 'alpha', new Position(0, 0)),
|
|
$this->unit('alpha-2', 'alpha', new Position(1, 0), health: 0),
|
|
$this->unit('bravo-1', 'bravo', new Position(7, 7)),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$next = (new CombatEngine())->move($match, 'alpha-1', new Position(1, 0));
|
|
|
|
self::assertSame('1:0', $next->unit('alpha-1')->position->key());
|
|
}
|
|
|
|
public function testMoveHonorsTerrainCostWithinSpeedBudget(): void
|
|
{
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8, ['1:0' => Terrain::Forest]),
|
|
[
|
|
$this->unit('alpha-1', 'alpha', new Position(0, 0), speed: 3),
|
|
$this->unit('bravo-1', 'bravo', new Position(7, 7)),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$next = (new CombatEngine())->move($match, 'alpha-1', new Position(2, 0));
|
|
|
|
self::assertSame('2:0', $next->unit('alpha-1')->position->key());
|
|
}
|
|
|
|
public function testMoveRejectsUnitFromInactiveTeam(): 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)),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$this->expectException(CombatException::class);
|
|
$this->expectExceptionMessage("It is not this unit's turn.");
|
|
|
|
(new CombatEngine())->move($match, 'bravo-1', new Position(6, 7));
|
|
}
|
|
|
|
public function testMoveRejectsOccupiedDestination(): void
|
|
{
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8),
|
|
[
|
|
$this->unit('alpha-1', 'alpha', new Position(0, 0)),
|
|
$this->unit('alpha-2', 'alpha', new Position(1, 0)),
|
|
$this->unit('bravo-1', 'bravo', new Position(7, 7)),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$this->expectException(CombatException::class);
|
|
$this->expectExceptionMessage('Destination is not reachable.');
|
|
|
|
(new CombatEngine())->move($match, 'alpha-1', new Position(1, 0));
|
|
}
|
|
|
|
public function testAttackAppliesForestDefenseAndAppendsLogWithoutMutatingMatch(): void
|
|
{
|
|
$attacker = $this->unit('alpha-1', 'alpha', new Position(0, 0));
|
|
$target = $this->unit('bravo-1', 'bravo', new Position(1, 0));
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8, ['1:0' => Terrain::Forest]),
|
|
[$attacker, $target],
|
|
'alpha',
|
|
actionLog: ['match started'],
|
|
);
|
|
|
|
$next = (new CombatEngine())->attack($match, 'alpha-1', 'bravo-1');
|
|
|
|
self::assertSame(10, $target->health);
|
|
self::assertSame(2, $attacker->actionsRemaining);
|
|
self::assertFalse($attacker->hasAttacked);
|
|
self::assertSame(10, $match->unit('bravo-1')->health);
|
|
self::assertSame(2, $match->unit('alpha-1')->actionsRemaining);
|
|
self::assertFalse($match->unit('alpha-1')->hasAttacked);
|
|
self::assertSame(['match started'], $match->actionLog);
|
|
self::assertSame(9, $next->unit('bravo-1')->health);
|
|
self::assertSame(1, $next->unit('alpha-1')->actionsRemaining);
|
|
self::assertTrue($next->unit('alpha-1')->hasAttacked);
|
|
self::assertSame(
|
|
['match started', 'alpha-1 attacked bravo-1 for 1 damage'],
|
|
$next->actionLog,
|
|
);
|
|
}
|
|
|
|
public function testAttackRejectsTargetOutsideRange(): void
|
|
{
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8),
|
|
[
|
|
$this->unit('alpha-1', 'alpha', new Position(0, 0)),
|
|
$this->unit('bravo-1', 'bravo', new Position(2, 0)),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$this->expectException(CombatException::class);
|
|
$this->expectExceptionMessage('Target is outside attack range.');
|
|
|
|
(new CombatEngine())->attack($match, 'alpha-1', 'bravo-1');
|
|
}
|
|
|
|
public function testAttackDefeatingLastEnemyAwardsVictory(): void
|
|
{
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8),
|
|
[
|
|
$this->unit('alpha-1', 'alpha', new Position(0, 0), attack: 20),
|
|
$this->unit('bravo-1', 'bravo', new Position(1, 0)),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$next = (new CombatEngine())->attack($match, 'alpha-1', 'bravo-1');
|
|
|
|
self::assertSame(0, $next->unit('bravo-1')->health);
|
|
self::assertSame('alpha', $next->winnerTeamId);
|
|
self::assertNull($match->winnerTeamId);
|
|
}
|
|
|
|
public function testAttackRejectsUnitThatAlreadyAttacked(): void
|
|
{
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8),
|
|
[
|
|
$this->unit('alpha-1', 'alpha', new Position(0, 0), hasAttacked: true),
|
|
$this->unit('bravo-1', 'bravo', new Position(1, 0)),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$this->expectException(CombatException::class);
|
|
$this->expectExceptionMessage('Unit has already attacked this turn.');
|
|
|
|
(new CombatEngine())->attack($match, 'alpha-1', 'bravo-1');
|
|
}
|
|
|
|
public function testAttackTranslatesUnknownAttackerAtCommandBoundary(): void
|
|
{
|
|
$match = $this->match();
|
|
|
|
try {
|
|
(new CombatEngine())->attack($match, 'missing', 'bravo-1');
|
|
self::fail('Expected an unknown unit exception.');
|
|
} catch (CombatException $exception) {
|
|
self::assertSame('Unknown unit: missing.', $exception->getMessage());
|
|
self::assertInstanceOf(InvalidArgumentException::class, $exception->getPrevious());
|
|
self::assertSame('Unknown unit id: missing.', $exception->getPrevious()->getMessage());
|
|
}
|
|
}
|
|
|
|
public function testAttackTranslatesUnknownTargetAtCommandBoundary(): void
|
|
{
|
|
$match = $this->match();
|
|
|
|
try {
|
|
(new CombatEngine())->attack($match, 'alpha-1', 'missing');
|
|
self::fail('Expected an unknown unit exception.');
|
|
} catch (CombatException $exception) {
|
|
self::assertSame('Unknown unit: missing.', $exception->getMessage());
|
|
self::assertInstanceOf(InvalidArgumentException::class, $exception->getPrevious());
|
|
self::assertSame('Unknown unit id: missing.', $exception->getPrevious()->getMessage());
|
|
}
|
|
}
|
|
|
|
public function testAttackRejectsCompletedMatchBeforeResolvingAttacker(): void
|
|
{
|
|
$match = $this->match()->withWinner('alpha');
|
|
|
|
$this->expectException(CombatException::class);
|
|
$this->expectExceptionMessage('The match is already complete.');
|
|
|
|
(new CombatEngine())->attack($match, 'missing', 'also-missing');
|
|
}
|
|
|
|
/**
|
|
* @param array{team?: string, health?: int, actionsRemaining?: int, hasAttacked?: bool} $overrides
|
|
*/
|
|
#[DataProvider('invalidAttackActorProvider')]
|
|
public function testAttackRejectsInvalidActor(array $overrides, string $message): void
|
|
{
|
|
$attacker = $this->unit(
|
|
'attacker',
|
|
$overrides['team'] ?? 'alpha',
|
|
new Position(0, 0),
|
|
health: $overrides['health'] ?? 10,
|
|
actionsRemaining: $overrides['actionsRemaining'] ?? 2,
|
|
hasAttacked: $overrides['hasAttacked'] ?? false,
|
|
);
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8),
|
|
[
|
|
$attacker,
|
|
$this->unit('bravo-1', 'bravo', new Position(1, 0)),
|
|
$this->unit('alpha-2', 'alpha', new Position(7, 7)),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$this->expectException(CombatException::class);
|
|
$this->expectExceptionMessage($message);
|
|
|
|
(new CombatEngine())->attack($match, 'attacker', 'missing');
|
|
}
|
|
|
|
/**
|
|
* @return iterable<string, array{
|
|
* array{team?: string, health?: int, actionsRemaining?: int, hasAttacked?: bool},
|
|
* string
|
|
* }>
|
|
*/
|
|
public static function invalidAttackActorProvider(): iterable
|
|
{
|
|
yield 'inactive and already attacked' => [
|
|
['team' => 'bravo', 'hasAttacked' => true],
|
|
"It is not this unit's turn.",
|
|
];
|
|
yield 'defeated and already attacked' => [
|
|
['health' => 0, 'hasAttacked' => true],
|
|
'Defeated units cannot act.',
|
|
];
|
|
yield 'exhausted and already attacked' => [
|
|
['actionsRemaining' => 0, 'hasAttacked' => true],
|
|
'Unit has no actions remaining.',
|
|
];
|
|
}
|
|
|
|
#[DataProvider('invalidAttackTargetProvider')]
|
|
public function testAttackRejectsInvalidTarget(string $targetTeam, int $targetHealth): void
|
|
{
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8),
|
|
[
|
|
$this->unit('alpha-1', 'alpha', new Position(0, 0)),
|
|
$this->unit('target', $targetTeam, new Position(1, 0), health: $targetHealth),
|
|
$this->unit('bravo-2', 'bravo', new Position(7, 7)),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$this->expectException(CombatException::class);
|
|
$this->expectExceptionMessage('Target must be an active enemy unit.');
|
|
|
|
(new CombatEngine())->attack($match, 'alpha-1', 'target');
|
|
}
|
|
|
|
/**
|
|
* @return iterable<string, array{string, int}>
|
|
*/
|
|
public static function invalidAttackTargetProvider(): iterable
|
|
{
|
|
yield 'friendly' => ['alpha', 10];
|
|
yield 'defeated enemy' => ['bravo', 0];
|
|
}
|
|
|
|
public function testAttackDealsCalculatedOpenTerrainDamage(): void
|
|
{
|
|
$target = $this->unit('bravo-1', 'bravo', new Position(1, 0), defense: 3);
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8),
|
|
[
|
|
$this->unit('alpha-1', 'alpha', new Position(0, 0), attack: 8),
|
|
$target,
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$next = (new CombatEngine())->attack($match, 'alpha-1', 'bravo-1');
|
|
|
|
self::assertSame(10, $target->health);
|
|
self::assertSame(5, $next->unit('bravo-1')->health);
|
|
self::assertSame(['alpha-1 attacked bravo-1 for 5 damage'], $next->actionLog);
|
|
}
|
|
|
|
public function testAttackAlwaysDealsAtLeastOneDamage(): void
|
|
{
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8, ['1:0' => Terrain::Forest]),
|
|
[
|
|
$this->unit('alpha-1', 'alpha', new Position(0, 0), attack: 0),
|
|
$this->unit('bravo-1', 'bravo', new Position(1, 0), defense: 20),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$next = (new CombatEngine())->attack($match, 'alpha-1', 'bravo-1');
|
|
|
|
self::assertSame(9, $next->unit('bravo-1')->health);
|
|
self::assertSame(['alpha-1 attacked bravo-1 for 1 damage'], $next->actionLog);
|
|
}
|
|
|
|
public function testAttackDoesNotAwardVictoryWhileAnotherEnemyLives(): void
|
|
{
|
|
$match = new MatchState(
|
|
new Battlefield(8, 8),
|
|
[
|
|
$this->unit('alpha-1', 'alpha', new Position(0, 0), attack: 20),
|
|
$this->unit('bravo-1', 'bravo', new Position(1, 0)),
|
|
$this->unit('bravo-2', 'bravo', new Position(7, 7)),
|
|
],
|
|
'alpha',
|
|
);
|
|
|
|
$next = (new CombatEngine())->attack($match, 'alpha-1', 'bravo-1');
|
|
|
|
self::assertSame(0, $next->unit('bravo-1')->health);
|
|
self::assertSame(10, $next->unit('bravo-2')->health);
|
|
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(
|
|
new Battlefield(8, 8),
|
|
[
|
|
$this->unit('alpha-1', 'alpha', new Position(0, 0)),
|
|
$this->unit('bravo-1', 'bravo', new Position(7, 7)),
|
|
],
|
|
'alpha',
|
|
);
|
|
}
|
|
|
|
private function unit(
|
|
string $id,
|
|
string $team,
|
|
Position $position,
|
|
int $health = 10,
|
|
int $speed = 4,
|
|
int $actionsRemaining = 2,
|
|
int $attack = 4,
|
|
int $defense = 2,
|
|
bool $hasAttacked = false,
|
|
): UnitState {
|
|
return new UnitState(
|
|
$id,
|
|
$team,
|
|
$position,
|
|
10,
|
|
$health,
|
|
$attack,
|
|
$defense,
|
|
$speed,
|
|
$actionsRemaining,
|
|
$hasAttacked,
|
|
);
|
|
}
|
|
}
|