feat: model scenarios and start match snapshots
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BattleForge\Tests\Unit\Domain;
|
||||
|
||||
use BattleForge\Domain\Archetype;
|
||||
use BattleForge\Domain\Battlefield;
|
||||
use BattleForge\Domain\DeploymentZone;
|
||||
use BattleForge\Domain\MatchState;
|
||||
use BattleForge\Domain\ObjectiveMarker;
|
||||
use BattleForge\Domain\Position;
|
||||
use BattleForge\Domain\Scenario;
|
||||
use BattleForge\Domain\UnitState;
|
||||
use BattleForge\Domain\VictoryCondition;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ScenarioTest extends TestCase
|
||||
{
|
||||
public function testItRejectsNonListUnits(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
new Scenario(
|
||||
id: 'demo',
|
||||
name: 'Demo',
|
||||
battlefield: new Battlefield(8, 8),
|
||||
units: ['alpha-1' => $this->unit('alpha-1', 'alpha', new Position(0, 0))], // @phpstan-ignore argument.type (Test fixture intentionally passes a non-list units array to verify constructor validation.)
|
||||
deploymentZones: ['alpha' => new DeploymentZone('alpha', [new Position(0, 0)])],
|
||||
objectives: [],
|
||||
victoryCondition: VictoryCondition::EliminateAll,
|
||||
holdRoundsRequired: 1,
|
||||
);
|
||||
}
|
||||
|
||||
public function testItRejectsTeamsOutsideTheThreeToSixSizeWindow(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Team alpha must have between 3 and 6 units.');
|
||||
|
||||
$units = [
|
||||
$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)),
|
||||
$this->unit('bravo-2', 'bravo', new Position(6, 7)),
|
||||
$this->unit('bravo-3', 'bravo', new Position(5, 7)),
|
||||
];
|
||||
|
||||
new Scenario(
|
||||
id: 'demo',
|
||||
name: 'Demo',
|
||||
battlefield: new Battlefield(8, 8),
|
||||
units: $units,
|
||||
deploymentZones: [
|
||||
'alpha' => new DeploymentZone('alpha', [new Position(0, 0), new Position(1, 0)]),
|
||||
'bravo' => new DeploymentZone('bravo', [new Position(7, 7), new Position(6, 7), new Position(5, 7)]),
|
||||
],
|
||||
objectives: [],
|
||||
victoryCondition: VictoryCondition::EliminateAll,
|
||||
holdRoundsRequired: 1,
|
||||
);
|
||||
}
|
||||
|
||||
public function testItRejectsAbilitiesOutsideTheArchetypeAllowlist(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unit alpha-1 has an ability that its archetype forbids.');
|
||||
|
||||
$bad = $this->unit('alpha-1', 'alpha', new Position(0, 0), archetype: Archetype::Scout, abilities: ['heal'], attack: 3, defense: 1, speed: 5, maxHealth: 6);
|
||||
$alpha2 = $this->unit('alpha-2', 'alpha', new Position(1, 0));
|
||||
$alpha3 = $this->unit('alpha-3', 'alpha', new Position(2, 0));
|
||||
$bravo = $this->unit('bravo-1', 'bravo', new Position(7, 7));
|
||||
$bravo2 = $this->unit('bravo-2', 'bravo', new Position(6, 7));
|
||||
$bravo3 = $this->unit('bravo-3', 'bravo', new Position(5, 7));
|
||||
|
||||
new Scenario(
|
||||
id: 'demo',
|
||||
name: 'Demo',
|
||||
battlefield: new Battlefield(8, 8),
|
||||
units: [$bad, $alpha2, $alpha3, $bravo, $bravo2, $bravo3],
|
||||
deploymentZones: [
|
||||
'alpha' => new DeploymentZone('alpha', [new Position(0, 0), new Position(1, 0), new Position(2, 0)]),
|
||||
'bravo' => new DeploymentZone('bravo', [new Position(7, 7), new Position(6, 7), new Position(5, 7)]),
|
||||
],
|
||||
objectives: [],
|
||||
victoryCondition: VictoryCondition::EliminateAll,
|
||||
holdRoundsRequired: 1,
|
||||
);
|
||||
}
|
||||
|
||||
public function testItRejectsUnitsOutsideTheirDeploymentZone(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unit alpha-1 must start inside team alpha deployment zone.');
|
||||
|
||||
$bad = $this->unit('alpha-1', 'alpha', new Position(3, 0));
|
||||
$alpha2 = $this->unit('alpha-2', 'alpha', new Position(0, 0));
|
||||
$alpha3 = $this->unit('alpha-3', 'alpha', new Position(1, 0));
|
||||
$bravo = $this->unit('bravo-1', 'bravo', new Position(7, 7));
|
||||
$bravo2 = $this->unit('bravo-2', 'bravo', new Position(6, 7));
|
||||
$bravo3 = $this->unit('bravo-3', 'bravo', new Position(5, 7));
|
||||
|
||||
new Scenario(
|
||||
id: 'demo',
|
||||
name: 'Demo',
|
||||
battlefield: new Battlefield(8, 8),
|
||||
units: [$bad, $alpha2, $alpha3, $bravo, $bravo2, $bravo3],
|
||||
deploymentZones: [
|
||||
'alpha' => new DeploymentZone('alpha', [new Position(0, 0), new Position(1, 0), new Position(2, 0)]),
|
||||
'bravo' => new DeploymentZone('bravo', [new Position(7, 7), new Position(6, 7), new Position(5, 7)]),
|
||||
],
|
||||
objectives: [],
|
||||
victoryCondition: VictoryCondition::EliminateAll,
|
||||
holdRoundsRequired: 1,
|
||||
);
|
||||
}
|
||||
|
||||
public function testHoldObjectiveRequiresExactlyOneObjective(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Hold objective victory requires exactly one objective.');
|
||||
|
||||
$units = $this->validUnitSet();
|
||||
$zones = $this->validZones();
|
||||
|
||||
new Scenario(
|
||||
id: 'demo',
|
||||
name: 'Demo',
|
||||
battlefield: new Battlefield(8, 8),
|
||||
units: $units,
|
||||
deploymentZones: $zones,
|
||||
objectives: [],
|
||||
victoryCondition: VictoryCondition::HoldObjective,
|
||||
holdRoundsRequired: 3,
|
||||
);
|
||||
}
|
||||
|
||||
public function testStartMatchReturnsAFreshMatchStateWithResetUnits(): void
|
||||
{
|
||||
$scenario = $this->validScenario();
|
||||
|
||||
$match = $scenario->startMatch('alpha');
|
||||
|
||||
self::assertInstanceOf(MatchState::class, $match);
|
||||
self::assertSame('alpha', $match->activeTeamId);
|
||||
self::assertSame(1, $match->round);
|
||||
self::assertNull($match->winnerTeamId);
|
||||
foreach ($match->units as $unit) {
|
||||
self::assertSame(2, $unit->actionsRemaining);
|
||||
self::assertFalse($unit->hasAttacked);
|
||||
self::assertSame(0, $unit->attackBonus);
|
||||
self::assertFalse($unit->hasUsedAbility);
|
||||
}
|
||||
}
|
||||
|
||||
public function testStartMatchDoesNotShareUnitInstancesWithTheScenario(): void
|
||||
{
|
||||
$scenario = $this->validScenario();
|
||||
|
||||
$match = $scenario->startMatch('alpha');
|
||||
|
||||
$original = $scenario->units[0];
|
||||
$fromMatch = $match->unit($original->id);
|
||||
|
||||
self::assertNotSame($original, $fromMatch);
|
||||
self::assertSame($original->id, $fromMatch->id);
|
||||
}
|
||||
|
||||
/** @return list<UnitState> */
|
||||
private function validUnitSet(): array
|
||||
{
|
||||
return [
|
||||
$this->unit('alpha-1', 'alpha', new Position(0, 0)),
|
||||
$this->unit('alpha-2', 'alpha', new Position(1, 0)),
|
||||
$this->unit('alpha-3', 'alpha', new Position(2, 0)),
|
||||
$this->unit('bravo-1', 'bravo', new Position(7, 7)),
|
||||
$this->unit('bravo-2', 'bravo', new Position(6, 7)),
|
||||
$this->unit('bravo-3', 'bravo', new Position(5, 7)),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, DeploymentZone> */
|
||||
private function validZones(): array
|
||||
{
|
||||
return [
|
||||
'alpha' => new DeploymentZone('alpha', [
|
||||
new Position(0, 0),
|
||||
new Position(1, 0),
|
||||
new Position(2, 0),
|
||||
]),
|
||||
'bravo' => new DeploymentZone('bravo', [
|
||||
new Position(7, 7),
|
||||
new Position(6, 7),
|
||||
new Position(5, 7),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function validScenario(): Scenario
|
||||
{
|
||||
return new Scenario(
|
||||
id: 'demo',
|
||||
name: 'Demo',
|
||||
battlefield: new Battlefield(8, 8),
|
||||
units: $this->validUnitSet(),
|
||||
deploymentZones: $this->validZones(),
|
||||
objectives: [],
|
||||
victoryCondition: VictoryCondition::EliminateAll,
|
||||
holdRoundsRequired: 1,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $abilities
|
||||
*/
|
||||
private function unit(
|
||||
string $id,
|
||||
string $team,
|
||||
Position $position,
|
||||
Archetype $archetype = Archetype::Defender,
|
||||
array $abilities = [],
|
||||
int $attack = 4,
|
||||
int $defense = 3,
|
||||
int $speed = 3,
|
||||
int $maxHealth = 10,
|
||||
): UnitState {
|
||||
return new UnitState(
|
||||
$id,
|
||||
$team,
|
||||
$position,
|
||||
$maxHealth,
|
||||
$maxHealth,
|
||||
$attack,
|
||||
$defense,
|
||||
$speed,
|
||||
2,
|
||||
false,
|
||||
$archetype,
|
||||
$abilities,
|
||||
0,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user