Browse Source

Adds debug and delete galaxy commands

Introduces new commands to debug galaxy maps and delete galaxies.

The debug command allows users to visualize galaxy layouts, create temporary galaxies for testing, and display statistics.

The delete command provides functionality to remove galaxies, along with their associated data, with options for deleting specific galaxies, all galaxies, and dry-run mode.

Adds a GeometryService to handle coordinate calculations and map generation.

Updates GalaxyFactory to include methods for positioning sectors and counting entities.

Increases PHP memory limit to prevent out of memory errors during galaxy creation.
olinox14 1 month ago
parent
commit
3eebdf4b56

+ 240 - 0
api/src/Commands/DebugMapCommand.php

@@ -0,0 +1,240 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Commands;
+
+use App\Entity\Galaxy;
+use App\Services\GalaxyFactory;
+use App\Services\GeometryService;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+#[AsCommand(
+    name: 'astra:debug:map',
+    description: 'Affiche la carte hexagonale d\'une galaxie'
+)]
+class DebugMapCommand extends Command
+{
+    public function __construct(
+        private EntityManagerInterface $entityManager,
+        private GalaxyFactory $galaxyFactory,
+        private GeometryService $geometryService
+    ) {
+        parent::__construct();
+    }
+
+    protected function configure(): void
+    {
+        $this
+            ->addArgument(
+                'galaxy-id',
+                InputArgument::OPTIONAL,
+                'ID de la galaxie à afficher (optionnel)'
+            )
+            ->addOption(
+                'create',
+                'c',
+                InputOption::VALUE_NONE,
+                'Créer une nouvelle galaxie temporaire pour la démonstration'
+            )
+            ->addOption(
+                'sectors',
+                's',
+                InputOption::VALUE_OPTIONAL,
+                'Nombre de secteurs pour la galaxie temporaire',
+                '200'
+            )
+            ->addOption(
+                'name',
+                null,
+                InputOption::VALUE_OPTIONAL,
+                'Nom de la galaxie temporaire',
+                'Debug Galaxy'
+            )
+            ->setHelp(
+                <<<EOT
+Cette commande affiche la représentation graphique d'une galaxie sous forme de grille hexagonale.
+
+Exemples d'utilisation :
+  <info>php bin/console astra:debug:map</info>                    # Affiche la première galaxie trouvée
+  <info>php bin/console astra:debug:map 1</info>                  # Affiche la galaxie avec l'ID 1
+  <info>php bin/console astra:debug:map --create</info>           # Crée une galaxie temporaire
+  <info>php bin/console astra:debug:map --create --sectors=500</info> # Crée une galaxie avec 500 secteurs
+
+Légende :
+  <comment>X</comment> = Secteur occupé
+  <comment>O</comment> = Position disponible mais vide
+  <comment>-</comment> = Espace vide (pourtour et centre de la galaxie)
+EOT
+            );
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $io = new SymfonyStyle($input, $output);
+        $galaxyId = $input->getArgument('galaxy-id');
+        $createNew = $input->getOption('create');
+
+        if ($createNew) {
+            // Créer une galaxie temporaire
+            $sectors = (int) $input->getOption('sectors');
+            $name = $input->getOption('name');
+
+            $io->title("Création d'une galaxie temporaire");
+            $io->text("Génération de {$sectors} secteurs...");
+
+            $galaxy = $this->galaxyFactory->createGalaxy($sectors, $name);
+
+            $io->success("Galaxie '{$name}' créée avec {$sectors} secteurs");
+        } else {
+            // Récupérer une galaxie existante
+            $galaxyRepository = $this->entityManager->getRepository(Galaxy::class);
+
+            if ($galaxyId) {
+                $galaxy = $galaxyRepository->find($galaxyId);
+                if (!$galaxy) {
+                    $io->error("Aucune galaxie trouvée avec l'ID {$galaxyId}");
+                    return Command::FAILURE;
+                }
+            } else {
+                $galaxy = $galaxyRepository->findOneBy([]);
+                if (!$galaxy) {
+                    $io->error('Aucune galaxie trouvée en base de données. Utilisez --create pour créer une galaxie temporaire.');
+                    return Command::FAILURE;
+                }
+            }
+        }
+
+        $this->displayGalaxyInfo($io, $galaxy);
+        $this->displayMap($io, $galaxy);
+        $this->displayStatistics($io, $galaxy);
+
+        return Command::SUCCESS;
+    }
+
+    private function displayGalaxyInfo(SymfonyStyle $io, Galaxy $galaxy): void
+    {
+        $io->title("Informations de la galaxie");
+
+        $sectorsCount = $galaxy->getSectors()->count();
+        $positionedSectors = 0;
+
+        foreach ($galaxy->getSectors() as $sector) {
+            if ($sector->getX() !== null && $sector->getY() !== null) {
+                $positionedSectors++;
+            }
+        }
+
+        $io->definitionList(
+            ['Nom' => $galaxy->getName()],
+            ['ID' => $galaxy->getId() ?? 'N/A (galaxie temporaire)'],
+            ['Total secteurs' => $sectorsCount],
+            ['Secteurs positionnés' => $positionedSectors],
+            ['Secteurs non positionnés' => $sectorsCount - $positionedSectors]
+        );
+    }
+
+    private function displayMap(SymfonyStyle $io, Galaxy $galaxy): void
+    {
+        $io->section("Carte de la galaxie");
+
+        // Collecter les positions des secteurs
+        $sectorPositions = [];
+        foreach ($galaxy->getSectors() as $sector) {
+            if ($sector->getX() !== null && $sector->getY() !== null) {
+                $sectorPositions[] = [
+                    'x' => $sector->getX(),
+                    'y' => $sector->getY()
+                ];
+            }
+        }
+
+        if (empty($sectorPositions)) {
+            $io->warning('Aucun secteur positionné trouvé. La galaxie n\'a peut-être pas été générée avec les nouvelles coordonnées.');
+            return;
+        }
+
+        // Régénérer la carte pour le GeometryService
+        $this->geometryService->generateGalaxyPositions(count($sectorPositions));
+
+        $map = $this->geometryService->printMap($sectorPositions);
+
+        $io->text('Légende : <fg=green>X</> = Secteur | <fg=yellow>O</> = Disponible | <fg=red>-</> = Vide');
+        $io->newLine();
+
+        // Afficher la carte avec des couleurs
+        $lines = explode("\n", $map);
+        foreach ($lines as $line) {
+            if (empty(trim($line))) continue;
+
+            // Colorier la ligne
+            $coloredLine = str_replace('X', '<fg=green>X</>', $line);
+            $coloredLine = str_replace('O', '<fg=yellow>O</>', $coloredLine);
+            $coloredLine = str_replace('-', '<fg=red>-</>', $coloredLine);
+
+            $io->text($coloredLine);
+        }
+    }
+
+    private function displayStatistics(SymfonyStyle $io, Galaxy $galaxy): void
+    {
+        $io->section("Statistiques");
+
+        $minX = null;
+        $maxX = null;
+        $minY = null;
+        $maxY = null;
+        $totalSystems = 0;
+
+        foreach ($galaxy->getSectors() as $sector) {
+            if ($sector->getX() !== null && $sector->getY() !== null) {
+                $minX = $minX === null ? $sector->getX() : min($minX, $sector->getX());
+                $maxX = $maxX === null ? $sector->getX() : max($maxX, $sector->getX());
+                $minY = $minY === null ? $sector->getY() : min($minY, $sector->getY());
+                $maxY = $maxY === null ? $sector->getY() : max($maxY, $sector->getY());
+            }
+
+            $totalSystems += $sector->getSystems()->count();
+        }
+
+        if ($minX !== null) {
+            $width = $maxX - $minX + 1;
+            $height = $maxY - $minY + 1;
+            $ratio = round($width / $height, 2);
+
+            $io->definitionList(
+                ['Dimensions' => "{$width} × {$height}"],
+                ['Ratio (largeur/hauteur)' => $ratio],
+                ['Coordonnées X' => "de {$minX} à {$maxX}"],
+                ['Coordonnées Y' => "de {$minY} à {$maxY}"],
+                ['Total systèmes' => $totalSystems],
+                ['Moyenne systèmes/secteur' => round($totalSystems / $galaxy->getSectors()->count(), 1)]
+            );
+        }
+
+        // Exemple de calcul de distance et voisins
+        $sectors = $galaxy->getSectors()->toArray();
+        if (count($sectors) >= 2 && $sectors[0]->getX() !== null) {
+            $sector1 = $sectors[0];
+            $sector2 = $sectors[1];
+
+            if ($sector2->getX() !== null) {
+                $distance = $this->geometryService->getManhattanDistance(
+                    $sector1->getX(), $sector1->getY(),
+                    $sector2->getX(), $sector2->getY()
+                );
+
+                $io->text("Distance Manhattan entre les secteurs {$sector1->getName()} et {$sector2->getName()} : {$distance}");
+            }
+
+            $neighbors = $this->geometryService->getHexNeighbors($sector1->getX(), $sector1->getY());
+            $io->text("Le secteur {$sector1->getName()} a " . count($neighbors) . " voisins possibles.");
+        }
+    }
+}

+ 336 - 0
api/src/Commands/DeleteGalaxyCommand.php

@@ -0,0 +1,336 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Commands;
+
+use App\Entity\Galaxy;
+use App\Services\GalaxyFactory;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+
+#[AsCommand(
+    name: 'astra:delete:galaxy',
+    description: 'Supprime une ou plusieurs galaxies et toutes leurs entités associées'
+)]
+class DeleteGalaxyCommand extends Command
+{
+    public function __construct(
+        private EntityManagerInterface $entityManager,
+        private GalaxyFactory $galaxyFactory
+    ) {
+        parent::__construct();
+    }
+
+    protected function configure(): void
+    {
+        $this
+            ->addArgument(
+                'galaxy-ids',
+                InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
+                'IDs des galaxies à supprimer (séparés par des espaces)'
+            )
+            ->addOption(
+                'all',
+                'a',
+                InputOption::VALUE_NONE,
+                'Supprimer toutes les galaxies'
+            )
+            ->addOption(
+                'force',
+                'f',
+                InputOption::VALUE_NONE,
+                'Forcer la suppression sans demander confirmation'
+            )
+            ->addOption(
+                'dry-run',
+                'd',
+                InputOption::VALUE_NONE,
+                'Afficher ce qui serait supprimé sans effectuer la suppression'
+            )
+            ->setHelp(
+                <<<EOT
+Cette commande permet de supprimer des galaxies et toutes leurs entités associées.
+
+Exemples d'utilisation :
+  <info>php bin/console astra:delete:galaxy 1</info>              # Supprime la galaxie avec l'ID 1
+  <info>php bin/console astra:delete:galaxy 1 2 3</info>          # Supprime les galaxies 1, 2 et 3
+  <info>php bin/console astra:delete:galaxy --all</info>          # Supprime toutes les galaxies
+  <info>php bin/console astra:delete:galaxy --all --force</info>  # Supprime toutes les galaxies sans confirmation
+  <info>php bin/console astra:delete:galaxy 1 --dry-run</info>    # Affiche ce qui serait supprimé sans supprimer
+
+<comment>ATTENTION :</comment> Cette opération est <error>irréversible</error> et supprimera définitivement :
+- La/les galaxie(s) spécifiée(s)
+- Tous les secteurs associés
+- Tous les systèmes stellaires
+- Toutes les planètes
+- Les relations avec les jeux (le jeu sera conservé mais détaché)
+EOT
+            );
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $io = new SymfonyStyle($input, $output);
+        $galaxyIds = $input->getArgument('galaxy-ids');
+        $deleteAll = $input->getOption('all');
+        $force = $input->getOption('force');
+        $dryRun = $input->getOption('dry-run');
+
+        if (!$deleteAll && empty($galaxyIds)) {
+            $io->error('Vous devez spécifier au moins un ID de galaxie ou utiliser --all pour supprimer toutes les galaxies.');
+            return Command::FAILURE;
+        }
+
+        if ($deleteAll && !empty($galaxyIds)) {
+            $io->error('Vous ne pouvez pas utiliser --all et spécifier des IDs en même temps.');
+            return Command::FAILURE;
+        }
+
+        // Récupérer les galaxies à supprimer
+        $galaxies = $this->getGalaxiesToDelete($galaxyIds, $deleteAll, $io);
+
+        if (empty($galaxies)) {
+            $io->warning('Aucune galaxie à supprimer.');
+            return Command::SUCCESS;
+        }
+
+        // Afficher les informations sur ce qui va être supprimé
+        $this->displayDeletionPreview($io, $galaxies, $dryRun);
+
+        if ($dryRun) {
+            $io->note('Mode dry-run activé : aucune suppression effectuée.');
+            return Command::SUCCESS;
+        }
+
+        // Demander confirmation si pas forcé
+        if (!$force && !$this->confirmDeletion($io, $galaxies)) {
+            $io->info('Suppression annulée.');
+            return Command::SUCCESS;
+        }
+
+        // Effectuer la suppression
+        return $this->performDeletion($io, $galaxies, $deleteAll);
+    }
+
+    private function getGalaxiesToDelete(array $galaxyIds, bool $deleteAll, SymfonyStyle $io): array
+    {
+        $galaxyRepository = $this->entityManager->getRepository(Galaxy::class);
+
+        if ($deleteAll) {
+            return $galaxyRepository->findAll();
+        }
+
+        $galaxies = [];
+        $notFoundIds = [];
+
+        foreach ($galaxyIds as $id) {
+            $galaxy = $galaxyRepository->find($id);
+            if ($galaxy) {
+                $galaxies[] = $galaxy;
+            } else {
+                $notFoundIds[] = $id;
+            }
+        }
+
+        if (!empty($notFoundIds)) {
+            $io->warning('Galaxies non trouvées avec les IDs : ' . implode(', ', $notFoundIds));
+        }
+
+        return $galaxies;
+    }
+
+    private function displayDeletionPreview(SymfonyStyle $io, array $galaxies, bool $dryRun): void
+    {
+        $title = $dryRun ? 'Aperçu de la suppression (dry-run)' : 'Galaxies à supprimer';
+        $io->title($title);
+
+        $totalStats = [
+            'galaxies' => 0,
+            'sectors' => 0,
+            'systems' => 0,
+            'planets' => 0,
+            'habitablePlanets' => 0,
+            'terraformablePlanets' => 0,
+            'gamesDetached' => 0
+        ];
+
+        $tableData = [];
+
+        foreach ($galaxies as $galaxy) {
+            $counts = $this->galaxyFactory->countGalaxyEntities($galaxy);
+            $totalStats['galaxies']++;
+            $totalStats['sectors'] += $counts['sectors'];
+            $totalStats['systems'] += $counts['systems'];
+            $totalStats['planets'] += $counts['planets'];
+            $totalStats['habitablePlanets'] += $counts['habitablePlanets'];
+            $totalStats['terraformablePlanets'] += $counts['terraformablePlanets'];
+
+            if ($galaxy->getGame()) {
+                $totalStats['gamesDetached']++;
+            }
+
+            $tableData[] = [
+                $galaxy->getId(),
+                $galaxy->getName(),
+                $counts['sectors'],
+                $counts['systems'],
+                $counts['planets'],
+                $counts['habitablePlanets'],
+                $counts['terraformablePlanets'],
+                $galaxy->getGame() ? 'Oui' : 'Non'
+            ];
+        }
+
+        $io->table(
+            ['ID', 'Nom', 'Secteurs', 'Systèmes', 'Planètes', 'Habitables', 'Terraformables', 'Jeu associé'],
+            $tableData
+        );
+
+        $io->section('Résumé total');
+        $io->definitionList(
+            ['Galaxies' => $totalStats['galaxies']],
+            ['Secteurs' => $totalStats['sectors']],
+            ['Systèmes stellaires' => $totalStats['systems']],
+            ['Planètes' => $totalStats['planets']],
+            ['Planètes habitables' => $totalStats['habitablePlanets']],
+            ['Planètes terraformables' => $totalStats['terraformablePlanets']],
+            ['Jeux détachés' => $totalStats['gamesDetached']]
+        );
+    }
+
+    private function confirmDeletion(SymfonyStyle $io, array $galaxies): bool
+    {
+        $count = count($galaxies);
+        $message = $count === 1
+            ? "Êtes-vous sûr de vouloir supprimer cette galaxie ? Cette action est irréversible."
+            : "Êtes-vous sûr de vouloir supprimer ces {$count} galaxies ? Cette action est irréversible.";
+
+        $question = new ConfirmationQuestion($message, false);
+        return $io->askQuestion($question);
+    }
+
+    private function performDeletion(SymfonyStyle $io, array $galaxies, bool $deleteAll): int
+    {
+        $io->section('Suppression en cours...');
+
+        $progressBar = $io->createProgressBar(count($galaxies));
+        $progressBar->start();
+
+        $allStats = [];
+        $errors = [];
+
+        foreach ($galaxies as $galaxy) {
+            try {
+                $stats = $this->deleteGalaxy($galaxy);
+                $allStats[] = $stats;
+                $progressBar->advance();
+            } catch (\Exception $e) {
+                $errors[] = [
+                    'galaxy' => $galaxy->getName() . ' (ID: ' . $galaxy->getId() . ')',
+                    'error' => $e->getMessage()
+                ];
+                $progressBar->advance();
+            }
+        }
+
+        $progressBar->finish();
+        $io->newLine(2);
+
+        // Afficher les résultats
+        $this->displayDeletionResults($io, $allStats, $errors);
+
+        return empty($errors) ? Command::SUCCESS : Command::FAILURE;
+    }
+
+    private function displayDeletionResults(SymfonyStyle $io, array $allStats, array $errors): void
+    {
+        if (!empty($allStats)) {
+            $io->success('Suppression terminée avec succès !');
+
+            $totalDeleted = [
+                'galaxies' => count($allStats),
+                'sectors' => array_sum(array_column($allStats, 'sectorsDeleted')),
+                'systems' => array_sum(array_column($allStats, 'systemsDeleted')),
+                'planets' => array_sum(array_column($allStats, 'planetsDeleted')),
+                'gamesDetached' => count(array_filter($allStats, fn($s) => $s['gameDetached']))
+            ];
+
+            $io->definitionList(
+                ['Galaxies supprimées' => $totalDeleted['galaxies']],
+                ['Secteurs supprimés' => $totalDeleted['sectors']],
+                ['Systèmes supprimés' => $totalDeleted['systems']],
+                ['Planètes supprimées' => $totalDeleted['planets']],
+                ['Jeux détachés' => $totalDeleted['gamesDetached']]
+            );
+
+            // Détail par galaxie
+            if (count($allStats) > 1) {
+                $io->section('Détail par galaxie');
+                $tableData = [];
+                foreach ($allStats as $stats) {
+                    if (!isset($stats['error'])) {
+                        $tableData[] = [
+                            $stats['galaxyId'],
+                            $stats['galaxyName'],
+                            $stats['sectorsDeleted'],
+                            $stats['systemsDeleted'],
+                            $stats['planetsDeleted']
+                        ];
+                    }
+                }
+                $io->table(['ID', 'Nom', 'Secteurs', 'Systèmes', 'Planètes'], $tableData);
+            }
+        }
+
+        if (!empty($errors)) {
+            $io->error('Certaines suppressions ont échoué :');
+            foreach ($errors as $error) {
+                $io->text("• {$error['galaxy']} : {$error['error']}");
+            }
+        }
+    }
+
+    /**
+     * Supprime une galaxie et toutes ses entités associées
+     */
+    public function deleteGalaxy(Galaxy $galaxy): array
+    {
+        $stats = [
+            'galaxyName' => $galaxy->getName(),
+            'galaxyId' => $galaxy->getId(),
+            'sectorsDeleted' => 0,
+            'systemsDeleted' => 0,
+            'planetsDeleted' => 0,
+            'gameDetached' => false
+        ];
+
+        // Détacher le jeu associé s'il existe
+        if ($galaxy->getGame()) {
+            $galaxy->getGame()->setGalaxy(null);
+            $stats['gameDetached'] = true;
+        }
+
+        // Compter les entités avant suppression
+        foreach ($galaxy->getSectors() as $sector) {
+            $stats['sectorsDeleted']++;
+
+            foreach ($sector->getSystems() as $system) {
+                $stats['systemsDeleted']++;
+                $stats['planetsDeleted'] += $system->getPlanets()->count();
+            }
+        }
+
+        // Supprimer la galaxie (cascade supprimera automatiquement les secteurs, systèmes et planètes)
+        $this->entityManager->remove($galaxy);
+        $this->entityManager->flush();
+
+        return $stats;
+    }
+}

+ 65 - 0
api/src/Services/GalaxyFactory.php

@@ -16,6 +16,10 @@ class GalaxyFactory
 {
     const SYSTEMS_PER_SECTOR = 12;
 
+    public function __construct(
+        private GeometryService $geometryService
+    ) {}
+
     public function createGalaxy(int $nbSectors = 1000, string $name = 'Galaxy'): Galaxy
     {
         $galaxy = new Galaxy();
@@ -26,6 +30,8 @@ class GalaxyFactory
             $galaxy->addSector($sector);
         }
 
+        $this->positionSectors($galaxy);
+
         return $galaxy;
     }
 
@@ -303,4 +309,63 @@ class GalaxyFactory
 
         return $prefixes[array_rand($prefixes)] . ' ' . $suffixes[array_rand($suffixes)] . ' ' . mt_rand(1, 999);
     }
+
+    /**
+     * Attribue les coordonnées x et y aux secteurs selon une grille hexagonale
+     * formant un anneau galactique
+     */
+    public function positionSectors(Galaxy $galaxy): void
+    {
+        $sectors = $galaxy->getSectors()->toArray();
+        $totalSectors = count($sectors);
+
+        if ($totalSectors === 0) {
+            return;
+        }
+
+        // Générer les positions avec le service de géométrie
+        $positions = $this->geometryService->generateGalaxyPositions($totalSectors);
+
+        // Assigner les positions aux secteurs
+        foreach ($sectors as $index => $sector) {
+            if (isset($positions[$index])) {
+                $sector->setX($positions[$index]['x']);
+                $sector->setY($positions[$index]['y']);
+            }
+        }
+    }
+
+    /**
+     * Compte le nombre total d'entités dans une galaxie
+     */
+    public function countGalaxyEntities(Galaxy $galaxy): array
+    {
+        $counts = [
+            'sectors' => 0,
+            'systems' => 0,
+            'planets' => 0,
+            'habitablePlanets' => 0,
+            'terraformablePlanets' => 0
+        ];
+
+        foreach ($galaxy->getSectors() as $sector) {
+            $counts['sectors']++;
+
+            foreach ($sector->getSystems() as $system) {
+                $counts['systems']++;
+
+                foreach ($system->getPlanets() as $planet) {
+                    $counts['planets']++;
+
+                    if ($planet->getHabitability() === PlanetHabitabilityEnum::INHABITABLE) {
+                        $counts['habitablePlanets']++;
+                    } elseif ($planet->getHabitability() === PlanetHabitabilityEnum::TERRAFORMABLE) {
+                        $counts['terraformablePlanets']++;
+                    }
+                }
+            }
+        }
+
+        return $counts;
+    }
 }

+ 189 - 0
api/src/Services/GeometryService.php

@@ -0,0 +1,189 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Services;
+
+class GeometryService
+{
+    private array $galaxyMap = [];
+    private int $width;
+    private int $height;
+
+    /**
+     * Génère les positions pour une grille hexagonale en forme d'anneau galactique
+     * Format paysage 4:3, centre et pourtour vides
+     */
+    public function generateGalaxyPositions(int $totalSectors): array
+    {
+        // Calculer les dimensions approximatives (format 4:3)
+        $area = $totalSectors * 2.5; // Facteur pour tenir compte des espaces vides
+        $this->height = (int) sqrt($area / (4/3));
+        $this->width = (int) ($this->height * 4/3);
+
+        // Assurer des dimensions minimales
+        $this->width = max($this->width, 20);
+        $this->height = max($this->height, 15);
+
+        // Initialiser la carte
+        $this->galaxyMap = array_fill(0, $this->height, array_fill(0, $this->width, false));
+
+        // Calculer les centres et rayons pour l'anneau
+        $centerX = $this->width / 2;
+        $centerY = $this->height / 2;
+        $outerRadiusX = $this->width * 0.45;
+        $outerRadiusY = $this->height * 0.45;
+        $innerRadiusX = $this->width * 0.15;
+        $innerRadiusY = $this->height * 0.15;
+
+        $positions = [];
+
+        // Générer les positions dans l'anneau
+        for ($y = 0; $y < $this->height; $y++) {
+            for ($x = 0; $x < $this->width; $x++) {
+                // Calculer la distance normalisée du centre (ellipse)
+                $normalizedDistanceX = ($x - $centerX) / $outerRadiusX;
+                $normalizedDistanceY = ($y - $centerY) / $outerRadiusY;
+                $outerDistance = sqrt($normalizedDistanceX * $normalizedDistanceX + $normalizedDistanceY * $normalizedDistanceY);
+
+                $normalizedInnerDistanceX = ($x - $centerX) / $innerRadiusX;
+                $normalizedInnerDistanceY = ($y - $centerY) / $innerRadiusY;
+                $innerDistance = sqrt($normalizedInnerDistanceX * $normalizedInnerDistanceX + $normalizedInnerDistanceY * $normalizedInnerDistanceY);
+
+                // Vérifier si la position est dans l'anneau
+                if ($outerDistance <= 1.0 && $innerDistance >= 1.0) {
+                    // Ajouter de la variation pour rendre la forme plus organique
+                    $noise = $this->getNoiseValue($x, $y) * 0.3;
+                    if ($outerDistance <= (0.85 + $noise) && $innerDistance >= (1.15 - $noise)) {
+                        // Grille hexagonale : décaler les lignes paires
+                        $hexX = $x;
+                        $hexY = $y;
+                        if ($y % 2 === 1) {
+                            $hexX += 0.5;
+                        }
+
+                        $positions[] = ['x' => $x, 'y' => $y];
+                        $this->galaxyMap[$y][$x] = true;
+                    }
+                }
+            }
+        }
+
+        // Mélanger et limiter au nombre de secteurs demandé
+        shuffle($positions);
+        return array_slice($positions, 0, $totalSectors);
+    }
+
+    /**
+     * Génère une valeur de bruit simple pour rendre la forme plus organique
+     */
+    private function getNoiseValue(int $x, int $y): float
+    {
+        $seed = $x * 374761393 + $y * 668265263;
+        $seed = ($seed ^ ($seed >> 13)) * 1274126177;
+        return (($seed ^ ($seed >> 16)) & 0x7fffffff) / 0x7fffffff * 2.0 - 1.0;
+    }
+
+    /**
+     * Trouve les voisins directs d'une position dans une grille hexagonale
+     */
+    public function getHexNeighbors(int $x, int $y): array
+    {
+        $neighbors = [];
+
+        // Les voisins dépendent de si la ligne est paire ou impaire
+        if ($y % 2 === 0) {
+            // Ligne paire
+            $directions = [
+                [-1, -1], [0, -1],  // Nord-Ouest, Nord-Est
+                [-1, 0],  [1, 0],   // Ouest, Est
+                [-1, 1],  [0, 1]    // Sud-Ouest, Sud-Est
+            ];
+        } else {
+            // Ligne impaire
+            $directions = [
+                [0, -1],  [1, -1],  // Nord-Ouest, Nord-Est
+                [-1, 0],  [1, 0],   // Ouest, Est
+                [0, 1],   [1, 1]    // Sud-Ouest, Sud-Est
+            ];
+        }
+
+        foreach ($directions as [$dx, $dy]) {
+            $newX = $x + $dx;
+            $newY = $y + $dy;
+
+            if ($newX >= 0 && $newX < $this->width && $newY >= 0 && $newY < $this->height) {
+                $neighbors[] = ['x' => $newX, 'y' => $newY];
+            }
+        }
+
+        return $neighbors;
+    }
+
+    /**
+     * Calcule la distance Manhattan entre deux secteurs
+     */
+    public function getManhattanDistance(int $x1, int $y1, int $x2, int $y2): int
+    {
+        return abs($x1 - $x2) + abs($y1 - $y2);
+    }
+
+    /**
+     * Génère une représentation graphique de la carte galactique
+     */
+    public function printMap(array $sectorPositions = []): string
+    {
+        if (empty($this->galaxyMap)) {
+            return "Aucune carte générée";
+        }
+
+        // Créer un index des positions occupées
+        $occupiedPositions = [];
+        foreach ($sectorPositions as $position) {
+            $occupiedPositions[$position['y']][$position['x']] = true;
+        }
+
+        $map = "";
+        for ($y = 0; $y < $this->height; $y++) {
+            // Indentation pour simuler la grille hexagonale
+            if ($y % 2 === 1) {
+                $map .= " ";
+            }
+
+            for ($x = 0; $x < $this->width; $x++) {
+                if (isset($occupiedPositions[$y][$x])) {
+                    $map .= "X ";
+                } elseif ($this->galaxyMap[$y][$x]) {
+                    $map .= "O "; // Position disponible mais non occupée
+                } else {
+                    $map .= "- ";
+                }
+            }
+            $map .= "\n";
+        }
+
+        return $map;
+    }
+
+    /**
+     * Trouve les voisins directs occupés par des secteurs
+     */
+    public function getOccupiedNeighbors(int $x, int $y, array $sectorPositions): array
+    {
+        $neighbors = $this->getHexNeighbors($x, $y);
+        $occupiedNeighbors = [];
+
+        // Créer un index des positions occupées
+        $occupiedPositions = [];
+        foreach ($sectorPositions as $position) {
+            $occupiedPositions[$position['y']][$position['x']] = true;
+        }
+
+        foreach ($neighbors as $neighbor) {
+            if (isset($occupiedPositions[$neighbor['y']][$neighbor['x']])) {
+                $occupiedNeighbors[] = $neighbor;
+            }
+        }
+
+        return $occupiedNeighbors;
+    }
+}

+ 3 - 0
docker/api/Dockerfile

@@ -17,6 +17,9 @@ RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"; \
     php -r "unlink('composer-setup.php');"; \
     mv composer.phar /usr/local/bin/composer;
 
+# Configuration de la limite mémoire PHP
+RUN echo "memory_limit = 2048M" > /usr/local/etc/php/conf.d/memory-limit.ini
+
 ######## XDebug ########
 RUN pecl install xdebug; \
     docker-php-ext-enable xdebug;