*/ public static function scenarioToArray(Scenario $scenario): array { $terrainMap = []; for ($y = 0; $y < $scenario->battlefield->height; $y++) { for ($x = 0; $x < $scenario->battlefield->width; $x++) { $position = new Position($x, $y); $tile = $scenario->battlefield->terrainAt($position); if ($tile === Terrain::Open) { continue; } $terrainMap[$position->key()] = $tile->value; } } $units = []; foreach ($scenario->units as $unit) { $units[] = self::unitToArray($unit); } $deploymentZones = []; foreach ($scenario->deploymentZones as $teamId => $zone) { $deploymentZones[$teamId] = self::deploymentZoneToArray($zone); } $objectives = []; foreach ($scenario->objectives as $id => $objective) { $objectives[$id] = ['id' => $objective->id, 'position' => self::positionToArray($objective->position)]; } return [ 'id' => $scenario->id, 'name' => $scenario->name, 'battlefield' => [ 'width' => $scenario->battlefield->width, 'height' => $scenario->battlefield->height, 'terrain' => $terrainMap, ], 'units' => $units, 'deploymentZones' => $deploymentZones, 'objectives' => $objectives, 'victoryCondition' => $scenario->victoryCondition->value, 'holdRoundsRequired' => $scenario->holdRoundsRequired, ]; } /** @param array $data */ public static function scenarioFromArray(array $data): Scenario { $terrainMap = []; foreach (($data['battlefield']['terrain'] ?? []) as $key => $value) { $terrainMap[(string) $key] = Terrain::from((string) $value); } $battlefield = new Battlefield( (int) $data['battlefield']['width'], (int) $data['battlefield']['height'], $terrainMap, ); $units = []; foreach (($data['units'] ?? []) as $row) { $units[] = self::unitFromArray($row); } $deploymentZones = []; foreach (($data['deploymentZones'] ?? []) as $teamId => $row) { $deploymentZones[(string) $teamId] = self::deploymentZoneFromArray($row); } $objectives = []; foreach (($data['objectives'] ?? []) as $id => $row) { $objectives[(string) $id] = new ObjectiveMarker( (string) $row['id'], self::positionFromArray($row['position']), ); } return new Scenario( id: (string) $data['id'], name: (string) $data['name'], battlefield: $battlefield, units: $units, deploymentZones: $deploymentZones, objectives: $objectives, victoryCondition: VictoryCondition::from((string) $data['victoryCondition']), holdRoundsRequired: (int) $data['holdRoundsRequired'], ); } /** @return array */ public static function matchToArray(MatchState $match): array { $terrainMap = []; for ($y = 0; $y < $match->battlefield->height; $y++) { for ($x = 0; $x < $match->battlefield->width; $x++) { $position = new Position($x, $y); $tile = $match->battlefield->terrainAt($position); if ($tile === Terrain::Open) { continue; } $terrainMap[$position->key()] = $tile->value; } } $units = []; foreach ($match->units as $unit) { $units[] = self::unitToArray($unit); } $objectives = []; foreach ($match->objectives as $id => $objective) { $objectives[(string) $id] = [ 'id' => $objective->id, 'position' => self::positionToArray($objective->position), ]; } return [ 'battlefield' => [ 'width' => $match->battlefield->width, 'height' => $match->battlefield->height, 'terrain' => $terrainMap, ], 'units' => $units, 'activeTeamId' => $match->activeTeamId, 'round' => $match->round, 'winnerTeamId' => $match->winnerTeamId, 'actionLog' => $match->actionLog, 'objectives' => $objectives, 'victoryCondition' => $match->victoryCondition->value, 'holdRoundsRequired' => $match->holdRoundsRequired, 'objectiveControl' => $match->objectiveControl, ]; } /** @param array $data */ public static function matchFromArray(array $data): MatchState { $terrainMap = []; foreach (($data['battlefield']['terrain'] ?? []) as $key => $value) { $terrainMap[(string) $key] = Terrain::from((string) $value); } $battlefield = new Battlefield( (int) $data['battlefield']['width'], (int) $data['battlefield']['height'], $terrainMap, ); $units = []; foreach (($data['units'] ?? []) as $row) { $units[] = self::unitFromArray($row); } $objectives = []; foreach (($data['objectives'] ?? []) as $id => $row) { $objectives[(string) $id] = new ObjectiveMarker( (string) $row['id'], self::positionFromArray($row['position']), ); } return new MatchState( battlefield: $battlefield, units: $units, activeTeamId: (string) $data['activeTeamId'], round: (int) $data['round'], winnerTeamId: $data['winnerTeamId'] ?? null, actionLog: array_map(static fn (mixed $entry): string => (string) $entry, $data['actionLog'] ?? []), objectives: $objectives, victoryCondition: VictoryCondition::from((string) $data['victoryCondition']), holdRoundsRequired: (int) $data['holdRoundsRequired'], objectiveControl: $data['objectiveControl'] ?? [], ); } /** @return array */ private static function unitToArray(UnitState $unit): array { return [ 'id' => $unit->id, 'teamId' => $unit->teamId, 'position' => self::positionToArray($unit->position), 'maxHealth' => $unit->maxHealth, 'health' => $unit->health, 'attack' => $unit->attack, 'defense' => $unit->defense, 'speed' => $unit->speed, 'archetype' => $unit->archetype->value, 'abilities' => $unit->abilities, ]; } /** @param array $row */ private static function unitFromArray(array $row): UnitState { $archetype = Archetype::from((string) $row['archetype']); // Validate the ability allowlist against the catalog before constructing, // because the runtime guard in UnitState only catches non-string entries. $template = ArchetypeCatalog::templates()[$archetype->value] ?? null; if ($template !== null) { $abilities = array_map(static fn (mixed $a): string => (string) $a, $row['abilities'] ?? []); foreach ($abilities as $ability) { if (!in_array($ability, $template->allowedAbilities, true)) { throw new InvalidArgumentException("Ability {$ability} is not in archetype {$archetype->value}'s allowlist."); } } } return new UnitState( id: (string) $row['id'], teamId: (string) $row['teamId'], position: self::positionFromArray($row['position']), maxHealth: (int) $row['maxHealth'], health: (int) $row['health'], attack: (int) $row['attack'], defense: (int) $row['defense'], speed: (int) $row['speed'], actionsRemaining: 2, hasAttacked: false, archetype: $archetype, abilities: $abilities ?? [], attackBonus: 0, hasUsedAbility: false, ); } /** @return array */ private static function positionToArray(Position $position): array { return ['x' => $position->x, 'y' => $position->y]; } private static function positionFromArray(mixed $row): Position { if (!is_array($row)) { throw new InvalidArgumentException('Expected position to be an array.'); } return new Position((int) $row['x'], (int) $row['y']); } /** @return array */ private static function deploymentZoneToArray(DeploymentZone $zone): array { $positions = []; foreach ($zone->positions as $position) { $positions[] = self::positionToArray($position); } return ['teamId' => $zone->teamId, 'positions' => $positions]; } /** @param array $row */ private static function deploymentZoneFromArray(array $row): DeploymentZone { $positions = []; foreach (($row['positions'] ?? []) as $positionRow) { $positions[] = self::positionFromArray($positionRow); } return new DeploymentZone((string) $row['teamId'], $positions); } }