fix: enforce immutable match state invariants

This commit is contained in:
Keith Solomon
2026-07-04 15:33:05 -05:00
parent 94560da5b4
commit 5297b41208
3 changed files with 407 additions and 2 deletions
+72 -2
View File
@@ -20,15 +20,74 @@ final readonly class MatchState
public ?string $winnerTeamId = null,
public array $actionLog = [],
) {
if (!self::isList($units)) {
throw new InvalidArgumentException('Match units must be a list.');
}
if ($units === []) {
throw new InvalidArgumentException('Match must contain at least one unit.');
}
$unitIds = [];
$teamIds = [];
foreach ($units as $unit) {
if (!self::isUnitState($unit)) {
throw new InvalidArgumentException('Match units must contain only UnitState instances.');
}
if (isset($unitIds[$unit->id])) {
throw new InvalidArgumentException("Duplicate unit id: {$unit->id}.");
}
$unitIds[$unit->id] = true;
$teamIds[$unit->teamId] = true;
}
if ($activeTeamId === '') {
throw new InvalidArgumentException('Active team id cannot be empty.');
}
if (!isset($teamIds[$activeTeamId])) {
throw new InvalidArgumentException("Active team has no units: {$activeTeamId}.");
}
if ($round < 1) {
throw new InvalidArgumentException('Match round must be at least 1.');
}
if (!self::isList($actionLog)) {
throw new InvalidArgumentException('Match action log must be a list.');
}
foreach ($actionLog as $entry) {
if (!self::isString($entry)) {
throw new InvalidArgumentException('Match action log entries must be strings.');
}
}
if ($winnerTeamId !== null && $winnerTeamId === '') {
throw new InvalidArgumentException('Winner team id cannot be empty.');
}
if ($winnerTeamId !== null && !isset($teamIds[$winnerTeamId])) {
throw new InvalidArgumentException("Winner team has no units: {$winnerTeamId}.");
}
}
private static function isUnitState(mixed $unit): bool
{
return $unit instanceof UnitState;
}
private static function isString(mixed $value): bool
{
return is_string($value);
}
private static function isList(mixed $value): bool
{
return is_array($value) && array_is_list($value);
}
public function unit(string $id): UnitState
@@ -57,6 +116,18 @@ final readonly class MatchState
throw new InvalidArgumentException("Unknown unit id: {$replacement->id}.");
}
public function withWinner(?string $winnerTeamId): self
{
return new self(
$this->battlefield,
$this->units,
$this->activeTeamId,
$this->round,
$winnerTeamId,
$this->actionLog,
);
}
/**
* @param list<UnitState>|null $units
* @param list<string>|null $actionLog
@@ -65,7 +136,6 @@ final readonly class MatchState
?array $units = null,
?string $activeTeamId = null,
?int $round = null,
?string $winnerTeamId = null,
?array $actionLog = null,
): self {
return new self(
@@ -73,7 +143,7 @@ final readonly class MatchState
$units ?? $this->units,
$activeTeamId ?? $this->activeTeamId,
$round ?? $this->round,
$winnerTeamId ?? $this->winnerTeamId,
$this->winnerTeamId,
$actionLog ?? $this->actionLog,
);
}