2 コミット 9cd179f5fb ... 3eebdf4b56

作者 SHA1 メッセージ 日付
  olinox14 3eebdf4b56 Adds debug and delete galaxy commands 2 ヶ月 前
  olinox14 5a8a141a77 Introduces game creation command 2 ヶ月 前

+ 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;
+    }
+}

+ 2 - 2
api/src/Commands/GalaxyGenerateCommand.php → api/src/Commands/MakeGalaxyCommand.php

@@ -13,10 +13,10 @@ use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Style\SymfonyStyle;
 
 #[AsCommand(
-    name: 'astra:galaxy:generate',
+    name: 'astra:make:galaxy',
     description: 'Génère une nouvelle galaxie avec le nombre de secteurs spécifié'
 )]
-class GalaxyGenerateCommand extends Command
+class MakeGalaxyCommand extends Command
 {
     public function __construct(
         private readonly GalaxyFactory $galaxyFactory,

+ 120 - 0
api/src/Commands/MakeGameCommand.php

@@ -0,0 +1,120 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Commands;
+
+use App\Services\GalaxyFactory;
+use App\Services\GameFactory;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+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:make:game',
+    description: 'Génère une nouvelle partie'
+)]
+class MakeGameCommand extends Command
+{
+    public function __construct(
+        private readonly GameFactory $gameFactory,
+        private readonly EntityManagerInterface $entityManager
+    ) {
+        parent::__construct();
+    }
+
+    protected function configure(): void
+    {
+        $this
+            ->addOption(
+                'sectors',
+                's',
+                InputOption::VALUE_OPTIONAL,
+                'Nombre de secteurs à générer pour la galaxie',
+                1000
+            )
+            ->addOption(
+                'name',
+                null,
+                InputOption::VALUE_OPTIONAL,
+                'Nom du jeu et de la galaxie à générer',
+                'New Game'
+            )
+            ->setHelp('Créé une nouvelle partie de Astra Corp');
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $io = new SymfonyStyle($input, $output);
+
+        $sectors = (int) $input->getOption('sectors');
+        $name = $input->getOption('name');
+
+        if (!$name) {
+            $io->error('Le nom du jeu est requis. Utilisez l\'option --name');
+            return Command::FAILURE;
+        }
+
+        if ($sectors <= 0) {
+            $io->error('Le nombre de secteurs doit être supérieur à 0');
+            return Command::FAILURE;
+        }
+
+        // Vérifier si une galaxie avec ce nom existe déjà
+        $existingGalaxy = $this->entityManager->getRepository(\App\Entity\Galaxy::class)
+            ->findOneBy(['name' => $name]);
+
+        if ($existingGalaxy) {
+            $io->error(sprintf('Une galaxie avec le nom "%s" existe déjà', $name));
+            return Command::FAILURE;
+        }
+
+        $io->title('Génération de la galaxie');
+        $io->text(sprintf('Nom : %s', $name));
+        $io->text(sprintf('Nombre de secteurs : %d', $sectors));
+
+        // Créer la galaxie
+        $game = $this->gameFactory->createGame($name, $sectors);
+        $galaxy = $game->getGalaxy();
+
+        // Sauvegarder en base de données
+        $io->text('Sauvegarde en base de données...');
+
+        $this->entityManager->persist($galaxy);
+        $this->entityManager->flush();
+
+        $io->success(sprintf(
+            'Galaxie "%s" générée avec succès ! %d secteurs créés avec %d systèmes au total.',
+            $name,
+            $galaxy->getSectors()->count(),
+            $galaxy->getSectors()->count() * GalaxyFactory::SYSTEMS_PER_SECTOR
+        ));
+
+        // Statistiques détaillées
+        $totalSystems = 0;
+        $totalPlanets = 0;
+
+        foreach ($galaxy->getSectors() as $sector) {
+            $totalSystems += $sector->getSystems()->count();
+            foreach ($sector->getSystems() as $system) {
+                $totalPlanets += $system->getPlanets()->count();
+            }
+        }
+
+        $io->section('Statistiques de génération');
+        $io->table(
+            ['Élément', 'Quantité'],
+            [
+                ['Secteurs', $galaxy->getSectors()->count()],
+                ['Systèmes', $totalSystems],
+                ['Planètes', $totalPlanets],
+                ['ID de la galaxie', $galaxy->getId()],
+            ]
+        );
+
+        return Command::SUCCESS;
+    }
+}

+ 2 - 2
api/src/Entity/Galaxy.php

@@ -21,10 +21,10 @@ class Galaxy
     private string $name;
 
     /** @var Collection<int, Sector> */
-    #[ORM\OneToMany(targetEntity: Sector::class, mappedBy: 'galaxy', orphanRemoval: true, cascade: ['persist', 'remove'])]
+    #[ORM\OneToMany(targetEntity: Sector::class, mappedBy: 'galaxy', cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $sectors;
 
-    #[ORM\OneToOne(inversedBy: 'galaxy', targetEntity: Game::class)]
+    #[ORM\OneToOne(targetEntity: Game::class, inversedBy: 'galaxy')]
     #[ORM\JoinColumn(nullable: true)]
     private ?Game $game = null;
 

+ 14 - 0
api/src/Entity/Game.php

@@ -34,6 +34,9 @@ class Game
     #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
     private ?\DateTimeImmutable $openedAt = null;
 
+    #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
+    private ?\DateTimeImmutable $closedAt = null;
+
     public function __construct()
     {
         $this->players = new ArrayCollection();
@@ -114,4 +117,15 @@ class Game
         $this->status = $status;
         return $this;
     }
+
+    public function getClosedAt(): ?\DateTimeImmutable
+    {
+        return $this->closedAt;
+    }
+
+    public function setClosedAt(?\DateTimeImmutable $closedAt): self
+    {
+        $this->closedAt = $closedAt;
+        return $this;
+    }
 }

+ 2 - 1
api/src/Entity/Sector.php

@@ -5,6 +5,7 @@ namespace App\Entity;
 
 use ApiPlatform\Metadata\ApiResource;
 use App\Enum\SectorStatusEnum;
+use App\Enum\StartingStateEnum;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
@@ -27,7 +28,7 @@ class Sector
     private string $name;
 
     /** @var Collection<int, System> */
-    #[ORM\OneToMany(targetEntity: System::class, mappedBy: 'sector', orphanRemoval: true, cascade: ['persist', 'remove'])]
+    #[ORM\OneToMany(targetEntity: System::class, mappedBy: 'sector', cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $systems;
 
     #[ORM\ManyToOne(targetEntity: Galaxy::class, inversedBy: 'sectors')]

+ 16 - 0
api/src/Services/CivilizationFactory.php

@@ -0,0 +1,16 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Services;
+
+use App\Entity\Galaxy;
+
+/**
+ * Génère une civilisation préalable dans une galaxie vierge
+ */
+class CivilizationFactory
+{
+    public function populate(Galaxy $galaxy) {
+
+    }
+}

+ 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;
+    }
 }

+ 37 - 0
api/src/Services/GameFactory.php

@@ -0,0 +1,37 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Services;
+
+use App\Entity\Game;
+use App\Enum\GameStatusEnum;
+
+class GameFactory
+{
+    public function __construct(
+        private readonly GalaxyFactory $galaxyFactory
+    ) {}
+
+    public function createGame(string $name, int $nbSectors = 1000): Game {
+        $game = new Game();
+        $game->setName($name);
+
+        $galaxy = $this->galaxyFactory->createGalaxy($nbSectors, $name);
+
+        $game->setGalaxy($galaxy);
+        return $game;
+    }
+
+    public function openGame(Game $game): Game {
+        $game->setOpenedAt(new \DateTimeImmutable());
+        $game->setStatus(GameStatusEnum::OPEN);
+        return $game;
+    }
+
+    public function closeGame(Game $game): Game {
+        $game->setClosedAt(new \DateTimeImmutable());
+        $game->setStatus(GameStatusEnum::CLOSED);
+        return $game;
+    }
+
+}

+ 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;