Bläddra i källkod

feat(migrations): add initial map table and indexes

- Created `maps` table to store hexagonal map data with metadata and JSON file path.
- Added additional indexes for performance optimization, including fulltext search on name and description.
- Implemented migration scripts for creating and modifying the database schema.
- Added README for migration instructions and best practices.

fix(logging): update log entries for map creation and errors

- Enhanced logging to capture exceptions related to database operations and map creation.
- Included detailed error messages for SQL syntax issues and database connection problems.

chore(storage): add example map JSON files

- Introduced example map JSON files to demonstrate the structure and data format for hexagonal maps.
- Included multiple variations of example maps for testing and development purposes.
Ynats 2 månader sedan
förälder
incheckning
f9268f8f10

+ 360 - 1
.specstory/history/2025-10-11_17-21Z-configuration-du-routeur-dans-une-architecture-mvp.md

@@ -1737,7 +1737,7 @@ L'application fonctionne toujours correctement (HTTP 200) et l'icône du bouton
 
 ---
 
-_**User (2025-10-11 20:52Z)**_
+_**User (2025-10-11 21:02Z)**_
 
 Créer la view /projects/new 
 
@@ -1757,6 +1757,365 @@ _**Assistant (copilot/grok-code-fast-1)**_
 
 ```
 
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+✅ **Vue `/projects/new` créée avec succès !**
+
+## Fichiers créés/modifiés :
+
+### 1. **Nouvelle vue** : `new.php`
+- Formulaire complet de création de projet
+- Interface moderne avec Bootstrap
+- Validation côté client
+- Aperçu dynamique des modèles
+
+### 2. **Route ajoutée** : `web.php`
+```php
+'/projects/new' => [HomeController::class, 'newProject']
+```
+
+### 3. **Méthode ajoutée** : `HomeController::newProject()`
+- Gère l'affichage du formulaire de création
+
+### 4. **Configuration Apache** : `.htaccess`
+- Redirection des requêtes vers `index.php`
+- Support des URLs propres
+
+### 5. **Routeur corrigé** : `index.php`
+- Gestion des environnements avec sous-dossiers (MAMP)
+
+## Fonctionnalités du formulaire :
+
+### 📝 **Champs requis :**
+- **Nom de la carte** : Input text avec validation
+- **Dimensions** : Largeur/Hauteur (100-2000px, défaut 800x600)
+- **Modèle** : Select avec 5 options (Désertique, Littoral, Montagne, Rural, Urbain)
+
+### 📝 **Champ optionnel :**
+- **Description** : Textarea pour décrire le projet
+
+### 🎨 **Interface :**
+- Design responsive Bootstrap
+- Icônes Bootstrap Icons
+- Aperçu dynamique des modèles sélectionnés
+- Validation en temps réel
+- Boutons d'action (Annuler/Créer)
+
+### 🔗 **Navigation :**
+- Bouton "Retour" vers l'accueil
+- Lien "Nouveau projet" dans l'état vide
+- Lien "Charger un projet" vers la future page de chargement
+
+L'application fonctionne maintenant avec les routes `/` et `/projects/new` accessibles via `https://map-generator` et `https://map-generator/projects/new`.
+
+---
+
+_**User (2025-10-11 21:18Z)**_
+
+Une carte est un damier composé d'hexagone. Un hexagone est une parselle.
+Chaque hexagone peut contenir un type de tuile (batiment, champ, route, fleuve, riviere, immeuble, etc...).
+Il me faudrait une class qui puisse me matérialiser une carte et la sauvegarder au format json.
+
+---
+
+_**Assistant (copilot/grok-code-fast-1)**_
+
+Parfait ! J'ai créé une classe `Map` complète pour gérer vos cartes hexagonales. Voici ce qui a été implémenté :
+
+## ✅ Classe Map créée avec succès
+
+**Fonctionnalités principales :**
+- **Carte hexagonale** avec coordonnées axiales (q, r)
+- **25 types de tuiles** différents (bâtiments, champs, routes, fleuves, etc.)
+- **Sauvegarde/chargement JSON** automatique
+- **Statistiques** détaillées de la carte
+- **Calcul de distances** entre hexagones
+- **Validation** des coordonnées et types de tuiles
+
+## 📁 Fichiers créés :
+
+1. **`Map.php`** - Classe principale Map et Hexagon
+2. **`MapExample.php`** - Exemple d'utilisation complet
+3. **`MapTest.php`** - Tests unitaires (36 tests ✅)
+4. **`MapClass.md`** - Documentation détaillée
+5. **``** - Dossier pour stocker les cartes JSON
+
+## 🧪 Tests validés :
+- ✅ 36 tests unitaires réussis
+- ✅ Sauvegarde et chargement JSON fonctionnels
+- ✅ Aperçu visuel avec emojis
+- ✅ Calcul de distances hexagonal
+
+## 💡 Utilisation rapide :
+
+```php
+// Créer une carte
+$map = new Map("Ma Carte", 10, 10);
+
+// Placer des tuiles
+$map->setTile(0, 0, 'castle');
+$map->setTile(1, 1, 'forest');
+
+// Sauvegarder
+$map->saveToFile('storage/maps/ma_carte.json');
+
+// Charger
+$loadedMap = Map::fromFile('storage/maps/ma_carte.json');
+```
+
+La classe est maintenant prête à être intégrée dans votre contrôleur de projets pour créer et gérer les cartes de votre générateur !
+
+---
+
+_**User (2025-10-12 15:17Z)**_
+
+Je souhaite que la class Map soit la plus générique possible donc je ne souhaite pas que les paramètres TILE_TYPES soient définis dans la class. Mais je souhaite pouvoir associer valeurs sur chaque tuile. (ex. type, ressources, température, etc..)
+
+---
+
+_**Assistant (copilot/grok-code-fast-1)**_
+
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+## ✅ Classe Map rendue générique avec succès !
+
+J'ai complètement refactorisé la classe `Map` pour la rendre beaucoup plus flexible et générique. Voici les changements majeurs :
+
+### 🔄 **Transformations apportées :**
+
+1. **Suppression des `TILE_TYPES` en dur** - Plus de constantes prédéfinies
+2. **Création de la classe `Tile`** - Objet flexible avec propriétés arbitraires
+3. **Refonte de l'API** - Utilisation d'objets `Tile` au lieu de simples strings
+
+### 🏗️ **Nouvelle Architecture :**
+
+#### **Classe Tile** (nouvelle)
+- **Propriétés arbitraires** : `type`, `resources`, `temperature`, `elevation`, etc.
+- **Accès flexible** : `getProperty()`, `setProperty()`, méthodes spécialisées
+- **Sérialisation JSON** automatique
+
+#### **Classe Map** (refactorisée)
+- **API générique** : accepte n'importe quel objet `Tile`
+- **Statistiques dynamiques** : compte les types présents automatiquement
+- **Compatibilité** : maintient les méthodes existantes pour la transition
+
+#### **Classe Hexagon** (mise à jour)
+- **Stockage d'objets Tile** au lieu de strings
+- **Méthodes de compatibilité** pour l'ancien code
+
+### 📊 **Nouvelles Fonctionnalités :**
+
+```php
+// Créer des tuiles riches
+$castleTile = new Tile([
+    'type' => 'castle',
+    'resources' => ['gold' => 100, 'stone' => 50],
+    'temperature' => 20.5,
+    'elevation' => 100,
+    'owner' => 'player1'
+]);
+
+// Placer sur la carte
+$map->setTile(0, 0, $castleTile);
+
+// Accéder aux propriétés
+$tile = $map->getTile(0, 0);
+$resources = $tile->getResources(); // ['gold' => 100, 'stone' => 50]
+$temperature = $tile->getTemperature(); // 20.5
+```
+
+### ✅ **Validation complète :**
+- **37 tests unitaires** réussis
+- **Exemple fonctionnel** avec propriétés complexes
+- **Sauvegarde/chargement JSON** préservé
+- **Rétrocompatibilité** maintenue
+
+### 📁 **Fichiers mis à jour :**
+- `Map.php` - Classes Tile, Map et Hexagon
+- `MapExample.php` - Exemple avec propriétés
+- `MapTest.php` - Tests adaptés
+- `MapClass.md` - Documentation mise à jour
+
+La classe `Map` est maintenant **extrêmement flexible** et peut gérer n'importe quel type de données sur les tuiles, tout en gardant la simplicité d'utilisation ! 🎯
+
+---
+
+_**User (2025-10-12 15:23Z)**_
+
+Je souhaite créer un modèle CRUD pour les Map
+
+---
+
+_**Assistant (copilot/grok-code-fast-1)**_
+
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+
+
+---
+
+_**User (2025-10-12 15:23Z)**_
+
+Peux tu créer un dossier migration et y ajouter les fichier sql de migration
+
+---
+
+_**Assistant (copilot/grok-code-fast-1)**_
+
+
 
 ---
 

+ 160 - 0
app/Examples/MapExample.php

@@ -0,0 +1,160 @@
+<?php
+
+/**
+ * Exemple d'utilisation de la classe Map
+ *
+ * Ce fichier montre comment créer, modifier et sauvegarder une carte.
+ */
+
+// Inclure les classes nécessaires
+require_once __DIR__ . '/../Models/Map.php';
+
+/**
+ * Fonction principale d'exemple
+ */
+function runMapExample()
+{
+    echo "=== Exemple d'utilisation de la classe Map ===\n\n";
+
+    // 1. Créer une nouvelle carte
+    echo "1. Création d'une nouvelle carte 5x5...\n";
+    $map = new Map("Carte d'exemple", 5, 5);
+    echo "Carte créée: {$map->getName()}\n";
+    echo "Dimensions: {$map->getWidth()} x {$map->getHeight()}\n\n";
+
+    // 2. Modifier quelques tuiles
+    echo "2. Modification de quelques tuiles...\n";
+    $map->setTile(0, 0, new Tile(['type' => 'castle', 'resources' => ['gold' => 100, 'stone' => 50], 'temperature' => 20.5]));      // Château au coin supérieur gauche
+    $map->setTile(2, 2, new Tile(['type' => 'river', 'resources' => ['water' => 1000], 'temperature' => 15.0]));       // Rivière au centre
+    $map->setTile(4, 4, new Tile(['type' => 'mountain', 'elevation' => 1500, 'resources' => ['iron' => 75], 'temperature' => 5.0]));    // Montagne au coin inférieur droit
+    $map->setTile(1, 1, new Tile(['type' => 'forest', 'resources' => ['wood' => 200], 'temperature' => 18.0]));      // Forêt
+    $map->setTile(3, 3, new Tile(['type' => 'field', 'resources' => ['wheat' => 150], 'temperature' => 22.0]));       // Champ
+    $map->setTile(0, 4, new Tile(['type' => 'road', 'temperature' => 25.0]));        // Route
+    echo "Tuiles modifiées avec succès\n\n";
+
+    // 3. Afficher les statistiques
+    echo "3. Statistiques de la carte:\n";
+    $stats = $map->getStatistics();
+    echo "Total d'hexagones: {$stats['total_hexagons']}\n";
+    echo "Hexagones vides: {$stats['empty_count']} ({$stats['empty_percentage']}%)\n";
+    echo "Hexagones remplis: {$stats['filled_count']} ({$stats['filled_percentage']}%)\n";
+    echo "Répartition des types de tuiles:\n";
+    foreach ($stats['tile_type_counts'] as $type => $count) {
+        echo "  - " . ucfirst($type) . ": $count\n";
+    }
+    echo "\n";
+
+    // 4. Sauvegarder la carte
+    echo "4. Sauvegarde de la carte...\n";
+    $filePath = __DIR__ . '/../../storage/maps/example_map.json';
+    $directory = dirname($filePath);
+
+    // Créer le dossier s'il n'existe pas
+    if (!is_dir($directory)) {
+        mkdir($directory, 0755, true);
+    }
+
+    if ($map->saveToFile($filePath)) {
+        echo "Carte sauvegardée dans: $filePath\n\n";
+    } else {
+        echo "Erreur lors de la sauvegarde\n\n";
+    }
+
+    // 5. Charger la carte depuis le fichier
+    echo "5. Chargement de la carte depuis le fichier...\n";
+    $loadedMap = Map::fromFile($filePath);
+    if ($loadedMap) {
+        echo "Carte chargée: {$loadedMap->getName()}\n";
+        echo "Dimensions: {$loadedMap->getWidth()} x {$loadedMap->getHeight()}\n";
+
+        // Vérifier que les données sont identiques
+        $originalStats = $map->getStatistics();
+        $loadedStats = $loadedMap->getStatistics();
+
+        if ($originalStats['tile_type_counts'] === $loadedStats['tile_type_counts']) {
+            echo "✓ Les données de la carte chargée correspondent à l'originale\n";
+        } else {
+            echo "✗ Erreur: les données ne correspondent pas\n";
+        }
+    } else {
+        echo "Erreur lors du chargement de la carte\n";
+    }
+    echo "\n";
+
+    // 6. Afficher un aperçu de la carte
+    echo "6. Aperçu de la carte (5x5):\n";
+    for ($r = 0; $r < $map->getHeight(); $r++) {
+        // Décalage pour l'affichage hexagonal
+        $offset = str_repeat(' ', $r * 2);
+        echo $offset;
+
+        for ($q = 0; $q < $map->getWidth(); $q++) {
+            $tile = $map->getTile($q, $r);
+            $symbol = getTileSymbol($tile);
+            echo "$symbol ";
+        }
+        echo "\n";
+    }
+    echo "\n";
+
+    // 7. Exemple d'utilisation des coordonnées hexagonales
+    echo "7. Exemple de calcul de distance hexagonale:\n";
+    $hex1 = $map->getHexagon(0, 0);
+    $hex2 = $map->getHexagon(2, 2);
+    if ($hex1 && $hex2) {
+        $distance = $hex1->distanceTo($hex2);
+        echo "Distance entre (0,0) et (2,2): $distance\n";
+    }
+
+    echo "\n=== Exemple terminé ===\n";
+}
+
+/**
+ * Retourne un symbole pour représenter une tuile
+ *
+ * @param Tile|null $tile La tuile
+ * @return string Le symbole
+ */
+function getTileSymbol(?Tile $tile): string
+{
+    if ($tile === null || $tile->isEmpty()) {
+        return '□';
+    }
+
+    $type = strtolower($tile->getType());
+    $symbols = [
+        'grass' => '░',
+        'field' => '🌾',
+        'forest' => '🌲',
+        'mountain' => '⛰️',
+        'water' => '🌊',
+        'building' => '🏢',
+        'house' => '🏠',
+        'farm' => '🚜',
+        'road' => '🛣️',
+        'river' => '🌊',
+        'bridge' => '🌉',
+        'castle' => '🏰',
+        'tower' => '🏯',
+        'wall' => '🧱',
+        'mine' => '⛏️',
+        'quarry' => '⚒️',
+        'market' => '🏪',
+        'temple' => '⛩️',
+        'hospital' => '🏥',
+        'school' => '🏫',
+        'factory' => '🏭',
+        'power_plant' => '⚡',
+        'airport' => '✈️',
+        'port' => '⚓',
+        'railway' => '🚂',
+        'highway' => '🛣️'
+    ];
+
+    return $symbols[$type] ?? '?';
+}
+
+// Exécuter l'exemple si le fichier est appelé directement
+if (basename(__FILE__) === basename($_SERVER['PHP_SELF'] ?? '')) {
+    runMapExample();
+}

+ 139 - 0
app/Examples/MapModelExample.php

@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * Exemple d'utilisation du MapModel
+ *
+ * Ce fichier montre comment utiliser le modèle CRUD pour gérer les cartes.
+ */
+
+// Inclure les classes nécessaires
+require_once __DIR__ . '/../Models/Map.php';
+require_once __DIR__ . '/../Models/MapModel.php';
+
+/**
+ * Fonction principale d'exemple
+ */
+function runMapModelExample()
+{
+    echo "=== Exemple d'utilisation du MapModel ===\n\n";
+
+    // 1. Créer la table si elle n'existe pas
+    echo "1. Création de la table maps...\n";
+    if (MapModel::createTable()) {
+        echo "✓ Table créée avec succès\n\n";
+    } else {
+        echo "✗ Erreur lors de la création de la table\n\n";
+        return;
+    }
+
+    // 2. Créer une nouvelle carte
+    echo "2. Création d'une nouvelle carte...\n";
+    $map = new Map("Carte exemple", 5, 5);
+
+    // Ajouter quelques tuiles
+    $map->setTile(0, 0, new Tile(['type' => 'castle', 'resources' => ['gold' => 100]]));
+    $map->setTile(2, 2, new Tile(['type' => 'forest', 'resources' => ['wood' => 50]]));
+    $map->setTile(4, 4, new Tile(['type' => 'mountain', 'elevation' => 2000]));
+
+    $mapId = MapModel::create($map, "Carte d'exemple avec différents types de tuiles");
+    if ($mapId) {
+        echo "✓ Carte créée avec succès, ID: $mapId\n\n";
+    } else {
+        echo "✗ Erreur lors de la création de la carte\n\n";
+        return;
+    }
+
+    // 3. Charger la carte depuis la base
+    echo "3. Chargement de la carte depuis la base...\n";
+    $loadedMap = MapModel::findMap($mapId);
+    if ($loadedMap) {
+        echo "✓ Carte chargée: {$loadedMap->getName()}\n";
+        echo "Dimensions: {$loadedMap->getWidth()} x {$loadedMap->getHeight()}\n";
+
+        // Afficher quelques statistiques
+        $stats = $loadedMap->getStatistics();
+        echo "Cases vides: {$stats['empty_count']} ({$stats['empty_percentage']}%)\n";
+        echo "Types présents: " . implode(', ', array_keys($stats['tile_type_counts'])) . "\n\n";
+    } else {
+        echo "✗ Erreur lors du chargement de la carte\n\n";
+    }
+
+    // 4. Mettre à jour la carte
+    echo "4. Mise à jour de la carte...\n";
+    $loadedMap->setTile(1, 1, new Tile(['type' => 'river', 'resources' => ['water' => 1000]]));
+    $loadedMap->setName("Carte exemple modifiée");
+
+    if (MapModel::update($mapId, $loadedMap, "Carte mise à jour avec une rivière")) {
+        echo "✓ Carte mise à jour avec succès\n\n";
+    } else {
+        echo "✗ Erreur lors de la mise à jour\n\n";
+    }
+
+    // 5. Lister toutes les cartes avec pagination
+    echo "5. Liste des cartes (pagination)...\n";
+    $paginated = MapModel::paginate(1, 5);
+    echo "Page {$paginated['pagination']['current_page']} sur {$paginated['pagination']['total_pages']}\n";
+    echo "Total de cartes: {$paginated['pagination']['total']}\n";
+
+    foreach ($paginated['data'] as $mapData) {
+        echo "- ID {$mapData['id']}: {$mapData['name']} ({$mapData['width']}x{$mapData['height']})\n";
+    }
+    echo "\n";
+
+    // 6. Recherche de cartes
+    echo "6. Recherche de cartes contenant 'exemple'...\n";
+    $searchResults = MapModel::search('exemple');
+    echo "Résultats trouvés: " . count($searchResults) . "\n";
+    foreach ($searchResults as $result) {
+        echo "- {$result['name']} (ID: {$result['id']})\n";
+    }
+    echo "\n";
+
+    // 7. Statistiques globales
+    echo "7. Statistiques globales...\n";
+    $globalStats = MapModel::getGlobalStats();
+    echo "Total de cartes: {$globalStats['total_maps']}\n";
+    echo "Dimensions moyennes: {$globalStats['average_width']} x {$globalStats['average_height']}\n";
+    if ($globalStats['latest_map']) {
+        echo "Dernière carte: {$globalStats['latest_map']['name']} (créée le {$globalStats['latest_map']['created_at']})\n";
+    }
+    echo "\n";
+
+    // 8. Dupliquer une carte
+    echo "8. Duplication de la carte...\n";
+    $duplicateId = MapModel::duplicate($mapId, "Copie de la carte exemple");
+    if ($duplicateId) {
+        echo "✓ Carte dupliquée avec succès, nouvel ID: $duplicateId\n\n";
+    } else {
+        echo "✗ Erreur lors de la duplication\n\n";
+    }
+
+    // 9. Export de la carte
+    echo "9. Export de la carte au format JSON...\n";
+    $exportData = MapModel::export($mapId, 'json');
+    if ($exportData) {
+        echo "✓ Export réussi (" . strlen($exportData) . " caractères)\n";
+        echo "Aperçu: " . substr($exportData, 0, 100) . "...\n\n";
+    } else {
+        echo "✗ Erreur lors de l'export\n\n";
+    }
+
+    // 10. Nettoyage (optionnel - décommenter pour supprimer les cartes de test)
+    /*
+    echo "10. Suppression des cartes de test...\n";
+    if (MapModel::delete($mapId)) {
+        echo "✓ Carte originale supprimée\n";
+    }
+    if (MapModel::delete($duplicateId)) {
+        echo "✓ Carte dupliquée supprimée\n";
+    }
+    echo "\n";
+    */
+
+    echo "=== Exemple terminé ===\n";
+}
+
+// Exécuter l'exemple si le fichier est appelé directement
+if (basename(__FILE__) === basename($_SERVER['PHP_SELF'] ?? '')) {
+    runMapModelExample();
+}

+ 729 - 0
app/Models/Map.php

@@ -0,0 +1,729 @@
+<?php
+
+/**
+ * Classe Tile
+ *
+ * Représente une tuile avec des propriétés arbitraires.
+ */
+if (!class_exists('Tile')) {
+    class Tile
+    {
+        /**
+         * @var array Les propriétés de la tuile
+         */
+        private array $properties;
+
+        /**
+         * Constructeur d'une tuile
+         *
+         * @param array $properties Les propriétés initiales de la tuile
+         */
+        public function __construct(array $properties = [])
+        {
+            $this->properties = $properties;
+        }
+
+        /**
+         * Définit une propriété
+         *
+         * @param string $key La clé de la propriété
+         * @param mixed $value La valeur de la propriété
+         */
+        public function setProperty(string $key, $value): void
+        {
+            $this->properties[$key] = $value;
+        }
+
+        /**
+         * Récupère une propriété
+         *
+         * @param string $key La clé de la propriété
+         * @param mixed $default La valeur par défaut si la propriété n'existe pas
+         * @return mixed La valeur de la propriété
+         */
+        public function getProperty(string $key, $default = null)
+        {
+            return $this->properties[$key] ?? $default;
+        }
+
+        /**
+         * Vérifie si une propriété existe
+         *
+         * @param string $key La clé de la propriété
+         * @return bool True si la propriété existe
+         */
+        public function hasProperty(string $key): bool
+        {
+            return array_key_exists($key, $this->properties);
+        }
+
+        /**
+         * Supprime une propriété
+         *
+         * @param string $key La clé de la propriété
+         */
+        public function removeProperty(string $key): void
+        {
+            unset($this->properties[$key]);
+        }
+
+        /**
+         * Récupère toutes les propriétés
+         *
+         * @return array Toutes les propriétés
+         */
+        public function getAllProperties(): array
+        {
+            return $this->properties;
+        }
+
+        /**
+         * Définit toutes les propriétés
+         *
+         * @param array $properties Les nouvelles propriétés
+         */
+        public function setAllProperties(array $properties): void
+        {
+            $this->properties = $properties;
+        }
+
+        /**
+         * Vérifie si la tuile est vide (aucune propriété)
+         *
+         * @return bool True si la tuile est vide
+         */
+        public function isEmpty(): bool
+        {
+            return empty($this->properties);
+        }
+
+        /**
+         * Convertit la tuile en tableau pour la sérialisation
+         *
+         * @return array Les données de la tuile
+         */
+        public function toArray(): array
+        {
+            return $this->properties;
+        }
+
+        /**
+         * Crée une tuile depuis un tableau
+         *
+         * @param array $data Les données de la tuile
+         * @return Tile La tuile créée
+         */
+        public static function fromArray(array $data): Tile
+        {
+            return new self($data);
+        }
+
+        /**
+         * Méthodes d'accès rapide pour les propriétés communes
+         */
+
+        /**
+         * Récupère le type de la tuile
+         *
+         * @return string|null Le type ou null
+         */
+        public function getType(): ?string
+        {
+            return $this->getProperty('type');
+        }
+
+        /**
+         * Définit le type de la tuile
+         *
+         * @param string $type Le type
+         */
+        public function setType(string $type): void
+        {
+            $this->setProperty('type', $type);
+        }
+
+        /**
+         * Récupère les ressources de la tuile
+         *
+         * @return array Les ressources
+         */
+        public function getResources(): array
+        {
+            return $this->getProperty('resources', []);
+        }
+
+        /**
+         * Définit les ressources de la tuile
+         *
+         * @param array $resources Les ressources
+         */
+        public function setResources(array $resources): void
+        {
+            $this->setProperty('resources', $resources);
+        }
+
+        /**
+         * Récupère la température de la tuile
+         *
+         * @return float|null La température ou null
+         */
+        public function getTemperature(): ?float
+        {
+            return $this->getProperty('temperature');
+        }
+
+        /**
+         * Définit la température de la tuile
+         *
+         * @param float $temperature La température
+         */
+        public function setTemperature(float $temperature): void
+        {
+            $this->setProperty('temperature', $temperature);
+        }
+
+        /**
+         * Récupère l'élévation de la tuile
+         *
+         * @return int|null L'élévation ou null
+         */
+        public function getElevation(): ?int
+        {
+            return $this->getProperty('elevation');
+        }
+
+        /**
+         * Définit l'élévation de la tuile
+         *
+         * @param int $elevation L'élévation
+         */
+        public function setElevation(int $elevation): void
+        {
+            $this->setProperty('elevation', $elevation);
+        }
+    }
+}
+
+/**
+ * Classe Map
+ *
+ * Représente une carte composée d'hexagones (damier hexagonal).
+ * Chaque hexagone est une parcelle pouvant contenir une tuile avec des propriétés arbitraires.
+ */
+if (!class_exists('Map')) {
+    class Map
+    {
+        /**
+         * @var array Les hexagones de la carte indexés par coordonnées [q][r]
+         */
+        private array $hexagons = [];
+
+        /**
+         * @var string Le nom de la carte
+         */
+        private string $name;
+
+        /**
+         * @var int La largeur de la carte (nombre d'hexagones en largeur)
+         */
+        private int $width;
+
+        /**
+         * @var int La hauteur de la carte (nombre d'hexagones en hauteur)
+         */
+        private int $height;
+
+        /**
+         * @var string La date de création de la carte
+         */
+        private string $createdAt;
+
+        /**
+         * @var string La date de dernière modification
+         */
+        private string $updatedAt;
+
+        /**
+         * Constructeur de la carte
+         *
+         * @param string $name Le nom de la carte
+         * @param int $width La largeur de la carte (en nombre d'hexagones)
+         * @param int $height La hauteur de la carte (en nombre d'hexagones)
+         */
+        public function __construct(string $name, int $width = 10, int $height = 10)
+        {
+            $this->name = $name;
+            $this->width = $width;
+            $this->height = $height;
+            $this->createdAt = date('Y-m-d H:i:s');
+            $this->updatedAt = date('Y-m-d H:i:s');
+
+            // Initialiser la carte avec des hexagones vides
+            $this->initializeMap();
+        }
+
+        /**
+         * Initialise la carte avec des hexagones vides
+         */
+        private function initializeMap(): void
+        {
+            for ($q = 0; $q < $this->width; $q++) {
+                for ($r = 0; $r < $this->height; $r++) {
+                    $this->hexagons[$q][$r] = new Hexagon($q, $r, new Tile(['type' => 'empty']));
+                }
+            }
+        }
+
+        /**
+         * Définit la tuile d'un hexagone
+         *
+         * @param int $q Coordonnée Q de l'hexagone
+         * @param int $r Coordonnée R de l'hexagone
+         * @param Tile $tile La tuile à placer
+         * @return bool True si l'hexagone a été modifié, false sinon
+         */
+        public function setTile(int $q, int $r, Tile $tile): bool
+        {
+            if (!$this->isValidCoordinate($q, $r)) {
+                return false;
+            }
+
+            $this->hexagons[$q][$r]->setTile($tile);
+            $this->updatedAt = date('Y-m-d H:i:s');
+            return true;
+        }
+
+        /**
+         * Récupère la tuile d'un hexagone
+         *
+         * @param int $q Coordonnée Q de l'hexagone
+         * @param int $r Coordonnée R de l'hexagone
+         * @return Tile|null La tuile ou null si les coordonnées sont invalides
+         */
+        public function getTile(int $q, int $r): ?Tile
+        {
+            if (!$this->isValidCoordinate($q, $r)) {
+                return null;
+            }
+
+            return $this->hexagons[$q][$r]->getTile();
+        }
+
+        /**
+         * Récupère un hexagone
+         *
+         * @param int $q Coordonnée Q de l'hexagone
+         * @param int $r Coordonnée R de l'hexagone
+         * @return Hexagon|null L'hexagone ou null si les coordonnées sont invalides
+         */
+        public function getHexagon(int $q, int $r): ?Hexagon
+        {
+            if (!$this->isValidCoordinate($q, $r)) {
+                return null;
+            }
+
+            return $this->hexagons[$q][$r];
+        }
+
+        /**
+         * Vérifie si les coordonnées sont valides
+         *
+         * @param int $q Coordonnée Q
+         * @param int $r Coordonnée R
+         * @return bool True si les coordonnées sont valides
+         */
+        public function isValidCoordinate(int $q, int $r): bool
+        {
+            return isset($this->hexagons[$q][$r]);
+        }
+
+        /**
+         * Récupère tous les hexagones de la carte
+         *
+         * @return array Les hexagones indexés par [q][r]
+         */
+        public function getAllHexagons(): array
+        {
+            return $this->hexagons;
+        }
+
+        /**
+         * Récupère les hexagones d'une ligne spécifique
+         *
+         * @param int $q La coordonnée Q de la ligne
+         * @return array|null Les hexagones de la ligne ou null si la ligne n'existe pas
+         */
+        public function getHexagonsRow(int $q): ?array
+        {
+            return $this->hexagons[$q] ?? null;
+        }
+
+        /**
+         * Compte le nombre d'hexagones d'un certain type
+         *
+         * @param string $tileType Le type de tuile à compter
+         * @return int Le nombre d'hexagones de ce type
+         */
+        public function countTiles(string $tileType): int
+        {
+            $count = 0;
+            foreach ($this->hexagons as $row) {
+                foreach ($row as $hexagon) {
+                    $tile = $hexagon->getTile();
+                    if ($tile && $tile->getType() === $tileType) {
+                        $count++;
+                    }
+                }
+            }
+            return $count;
+        }
+
+        /**
+         * Compte le nombre d'hexagones vides
+         *
+         * @return int Le nombre d'hexagones vides
+         */
+        public function countEmptyTiles(): int
+        {
+            $count = 0;
+            foreach ($this->hexagons as $row) {
+                foreach ($row as $hexagon) {
+                    if ($hexagon->isEmpty()) {
+                        $count++;
+                    }
+                }
+            }
+            return $count;
+        }
+
+        /**
+         * Récupère les statistiques de la carte
+         *
+         * @return array Les statistiques (nombre total d'hexagones, répartition par type)
+         */
+        public function getStatistics(): array
+        {
+            $totalHexagons = $this->width * $this->height;
+            $tileTypeCounts = [];
+            $emptyCount = $this->countEmptyTiles();
+
+            // Compter les types de tuiles présents
+            foreach ($this->hexagons as $row) {
+                foreach ($row as $hexagon) {
+                    $tile = $hexagon->getTile();
+                    if ($tile && !$tile->isEmpty()) {
+                        $type = $tile->getType();
+                        if ($type !== null) {
+                            $tileTypeCounts[$type] = ($tileTypeCounts[$type] ?? 0) + 1;
+                        }
+                    }
+                }
+            }
+
+            return [
+                'total_hexagons' => $totalHexagons,
+                'width' => $this->width,
+                'height' => $this->height,
+                'tile_type_counts' => $tileTypeCounts,
+                'empty_count' => $emptyCount,
+                'empty_percentage' => round(($emptyCount / $totalHexagons) * 100, 2),
+                'filled_count' => $totalHexagons - $emptyCount,
+                'filled_percentage' => round((($totalHexagons - $emptyCount) / $totalHexagons) * 100, 2)
+            ];
+        }
+
+        /**
+         * Convertit la carte en tableau pour la sérialisation JSON
+         *
+         * @return array Les données de la carte
+         */
+        public function toArray(): array
+        {
+            $hexagonsData = [];
+            foreach ($this->hexagons as $q => $row) {
+                foreach ($row as $r => $hexagon) {
+                    $hexagonsData[] = $hexagon->toArray();
+                }
+            }
+
+            return [
+                'name' => $this->name,
+                'width' => $this->width,
+                'height' => $this->height,
+                'created_at' => $this->createdAt,
+                'updated_at' => $this->updatedAt,
+                'hexagons' => $hexagonsData
+            ];
+        }
+
+        /**
+         * Convertit la carte en JSON
+         *
+         * @param bool $prettyPrint True pour un JSON formaté
+         * @return string Le JSON de la carte
+         */
+        public function toJson(bool $prettyPrint = false): string
+        {
+            return json_encode(
+                $this->toArray(),
+                $prettyPrint ? JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE : 0
+            );
+        }
+
+        /**
+         * Sauvegarde la carte dans un fichier JSON
+         *
+         * @param string $filePath Le chemin du fichier
+         * @return bool True si la sauvegarde a réussi
+         */
+        public function saveToFile(string $filePath): bool
+        {
+            try {
+                $json = $this->toJson(true);
+                $result = file_put_contents($filePath, $json);
+                return $result !== false;
+            } catch (Exception $e) {
+                AppDebugger::log('Erreur lors de la sauvegarde de la carte: ' . $e->getMessage(), 'ERROR');
+                return false;
+            }
+        }
+
+        /**
+         * Charge une carte depuis un tableau
+         *
+         * @param array $data Les données de la carte
+         * @return Map|null La carte chargée ou null en cas d'erreur
+         */
+        public static function fromArray(array $data): ?Map
+        {
+            try {
+                $map = new self($data['name'], $data['width'], $data['height']);
+                $map->createdAt = $data['created_at'] ?? date('Y-m-d H:i:s');
+                $map->updatedAt = $data['updated_at'] ?? date('Y-m-d H:i:s');
+
+                // Charger les hexagones
+                foreach ($data['hexagons'] as $hexagonData) {
+                    $q = $hexagonData['q'];
+                    $r = $hexagonData['r'];
+                    if ($map->isValidCoordinate($q, $r)) {
+                        $map->hexagons[$q][$r] = Hexagon::fromArray($hexagonData);
+                    }
+                }
+
+                return $map;
+            } catch (Exception $e) {
+                AppDebugger::log('Erreur lors du chargement de la carte depuis un tableau: ' . $e->getMessage(), 'ERROR');
+                return null;
+            }
+        }
+
+        /**
+         * Charge une carte depuis un fichier JSON
+         *
+         * @param string $filePath Le chemin du fichier
+         * @return Map|null La carte chargée ou null en cas d'erreur
+         */
+        public static function fromFile(string $filePath): ?Map
+        {
+            try {
+                if (!file_exists($filePath)) {
+                    return null;
+                }
+
+                $json = file_get_contents($filePath);
+                $data = json_decode($json, true);
+
+                if (json_last_error() !== JSON_ERROR_NONE) {
+                    AppDebugger::log('Erreur JSON lors du chargement du fichier: ' . json_last_error_msg(), 'ERROR');
+                    return null;
+                }
+
+                return self::fromArray($data);
+            } catch (Exception $e) {
+                AppDebugger::log('Erreur lors du chargement de la carte depuis un fichier: ' . $e->getMessage(), 'ERROR');
+                return null;
+            }
+        }
+
+        // Getters
+        public function getName(): string { return $this->name; }
+        public function getWidth(): int { return $this->width; }
+        public function getHeight(): int { return $this->height; }
+        public function getCreatedAt(): string { return $this->createdAt; }
+        public function getUpdatedAt(): string { return $this->updatedAt; }
+
+        // Setters
+        public function setName(string $name): void
+        {
+            $this->name = $name;
+            $this->updatedAt = date('Y-m-d H:i:s');
+        }
+    }
+}
+
+/**
+ * Classe Hexagon
+ *
+ * Représente un hexagone individuel dans la carte.
+ */
+if (!class_exists('Hexagon')) {
+    class Hexagon
+    {
+        /**
+         * @var int Coordonnée Q (axe horizontal)
+         */
+        private int $q;
+
+        /**
+         * @var int Coordonnée R (axe diagonal)
+         */
+        private int $r;
+
+        /**
+         * @var Tile|null La tuile de l'hexagone
+         */
+        private ?Tile $tile;
+
+        /**
+         * Constructeur d'un hexagone
+         *
+         * @param int $q Coordonnée Q
+         * @param int $r Coordonnée R
+         * @param Tile|null $tile La tuile initiale (null pour vide)
+         */
+        public function __construct(int $q, int $r, ?Tile $tile = null)
+        {
+            $this->q = $q;
+            $this->r = $r;
+            $this->tile = $tile;
+        }
+
+        /**
+         * Définit la tuile
+         *
+         * @param Tile|null $tile La nouvelle tuile (null pour vider)
+         */
+        public function setTile(?Tile $tile): void
+        {
+            $this->tile = $tile;
+        }
+
+        /**
+         * Récupère la tuile
+         *
+         * @return Tile|null La tuile ou null si vide
+         */
+        public function getTile(): ?Tile
+        {
+            return $this->tile;
+        }
+
+        /**
+         * Vérifie si l'hexagone est vide
+         *
+         * @return bool True si l'hexagone est vide
+         */
+        public function isEmpty(): bool
+        {
+            return $this->tile === null || $this->tile->isEmpty() || $this->tile->getType() === 'empty';
+        }
+
+        /**
+         * Récupère le type de tuile (méthode de compatibilité)
+         *
+         * @return string|null Le type de tuile ou null
+         */
+        public function getTileType(): ?string
+        {
+            return $this->tile ? $this->tile->getType() : null;
+        }
+
+        /**
+         * Définit le type de tuile (méthode de compatibilité)
+         *
+         * @param string $tileType Le type de tuile
+         */
+        public function setTileType(string $tileType): void
+        {
+            if ($this->tile === null) {
+                $this->tile = new Tile();
+            }
+            $this->tile->setType($tileType);
+        }
+
+        /**
+         * Calcule la coordonnée S (pour les coordonnées cubiques)
+         *
+         * @return int La coordonnée S
+         */
+        public function getS(): int
+        {
+            return -$this->q - $this->r;
+        }
+
+        /**
+         * Calcule la distance à un autre hexagone
+         *
+         * @param Hexagon $other L'autre hexagone
+         * @return int La distance
+         */
+        public function distanceTo(Hexagon $other): int
+        {
+            return (abs($this->q - $other->q) + abs($this->r - $other->r) + abs($this->getS() - $other->getS())) / 2;
+        }
+
+        /**
+         * Récupère les hexagones voisins
+         *
+         * @return array Les directions vers les voisins [q, r]
+         */
+        public static function getNeighbors(): array
+        {
+            return [
+                [1, 0],   // Est
+                [1, -1],  // Nord-Est
+                [0, -1],  // Nord-Ouest
+                [-1, 0],  // Ouest
+                [-1, 1],  // Sud-Ouest
+                [0, 1]    // Sud-Est
+            ];
+        }
+
+        /**
+         * Convertit l'hexagone en tableau pour la sérialisation
+         *
+         * @return array Les données de l'hexagone
+         */
+        public function toArray(): array
+        {
+            return [
+                'q' => $this->q,
+                'r' => $this->r,
+                'tile' => $this->tile ? $this->tile->toArray() : null
+            ];
+        }
+
+        /**
+         * Crée un hexagone depuis un tableau
+         *
+         * @param array $data Les données de l'hexagone
+         * @return Hexagon L'hexagone créé
+         */
+        public static function fromArray(array $data): Hexagon
+        {
+            $tile = null;
+            if (isset($data['tile']) && $data['tile'] !== null) {
+                $tile = Tile::fromArray($data['tile']);
+            } elseif (isset($data['tile_type'])) {
+                // Compatibilité avec l'ancien format
+                $tile = new Tile(['type' => $data['tile_type']]);
+            }
+
+            return new self($data['q'], $data['r'], $tile);
+        }
+
+        // Getters
+        public function getQ(): int { return $this->q; }
+        public function getR(): int { return $this->r; }
+    }
+}

+ 487 - 0
app/Models/MapModel.php

@@ -0,0 +1,487 @@
+<?php
+
+/**
+ * Classe MapModel
+ *
+ * Modèle pour gérer les cartes dans la base de données.
+ * Fournit les opérations CRUD complètes pour les cartes.
+ */
+if (!class_exists('MapModel')) {
+    class MapModel
+    {
+        protected static $table = 'maps';
+        private static $pdo = null;
+
+        /**
+         * Obtient une connexion PDO à la base de données.
+         *
+         * @return PDO
+         */
+        private static function getPDO() {
+            if (self::$pdo === null) {
+                $config = require __DIR__ . '/../../config/config.php';
+                
+                // Utiliser le socket Unix pour MAMP
+                $dsn = sprintf(
+                    'mysql:dbname=%s;unix_socket=%s;charset=utf8mb4',
+                    $config['db_name'],
+                    $config['db_socket']
+                );
+                
+                self::$pdo = new PDO($dsn, $config['db_user'], $config['db_password']);
+                self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+            }
+            return self::$pdo;
+        }
+
+        /**
+         * Crée la table maps si elle n'existe pas
+         *
+         * @return bool True si la table a été créée ou existe déjà
+         */
+        public static function createTable(): bool
+        {
+            $pdo = self::getPDO();
+            $sql = "
+                CREATE TABLE IF NOT EXISTS maps (
+                    id INT AUTO_INCREMENT PRIMARY KEY,
+                    name VARCHAR(255) NOT NULL,
+                    description TEXT,
+                    width INT NOT NULL DEFAULT 10,
+                    height INT NOT NULL DEFAULT 10,
+                    data JSON,
+                    file_path VARCHAR(500),
+                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+                    INDEX idx_name (name),
+                    INDEX idx_created_at (created_at)
+                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+            ";
+
+            try {
+                $pdo->exec($sql);
+                AppDebugger::log('Table maps créée avec succès', 'INFO');
+                return true;
+            } catch (Exception $e) {
+                AppDebugger::log('Erreur lors de la création de la table maps: ' . $e->getMessage(), 'ERROR');
+                return false;
+            }
+        }
+
+        /**
+         * Crée une nouvelle carte dans la base de données
+         *
+         * @param Map $map L'objet Map à sauvegarder
+         * @param string|null $description Description optionnelle
+         * @return int|false L'ID de la carte créée ou false en cas d'erreur
+         */
+        public static function create(Map $map, ?string $description = null): int|false
+        {
+            $pdo = self::getPDO();
+
+            // Générer le chemin du fichier
+            $fileName = 'map_' . time() . '_' . uniqid() . '.json';
+            $filePath = 'storage/maps/' . $fileName;
+
+            // Sauvegarder le fichier JSON
+            if (!$map->saveToFile($filePath)) {
+                AppDebugger::log('Erreur lors de la sauvegarde du fichier JSON', 'ERROR');
+                return false;
+            }
+
+            // Convertir les données pour la base
+            $data = [
+                'name' => $map->getName(),
+                'width' => $map->getWidth(),
+                'height' => $map->getHeight(),
+                'created_at' => $map->getCreatedAt(),
+                'updated_at' => $map->getUpdatedAt(),
+                'statistics' => $map->getStatistics()
+            ];
+
+            $sql = "
+                INSERT INTO maps (name, description, width, height, data, file_path, created_at, updated_at)
+                VALUES (:name, :description, :width, :height, :data, :file_path, :created_at, :updated_at)
+            ";
+
+            try {
+                $stmt = $pdo->prepare($sql);
+                $stmt->execute([
+                    'name' => $map->getName(),
+                    'description' => $description,
+                    'width' => $map->getWidth(),
+                    'height' => $map->getHeight(),
+                    'data' => json_encode($data),
+                    'file_path' => $filePath,
+                    'created_at' => $map->getCreatedAt(),
+                    'updated_at' => $map->getUpdatedAt()
+                ]);
+
+                $mapId = $pdo->lastInsertId();
+                AppDebugger::log('Carte créée avec succès, ID: ' . $mapId, 'INFO');
+                return (int) $mapId;
+
+            } catch (Exception $e) {
+                AppDebugger::log('Erreur lors de la création de la carte: ' . $e->getMessage(), 'ERROR');
+                return false;
+            }
+        }
+
+        /**
+         * Récupère les données brutes d'une carte par son ID (pour usage interne)
+         *
+         * @param int $id L'ID de la carte
+         * @return array|null Les données de la carte ou null si non trouvée
+         */
+        private static function findById(int $id): ?array
+        {
+            $pdo = self::getPDO();
+            $stmt = $pdo->prepare('SELECT * FROM maps WHERE id = :id');
+            $stmt->execute(['id' => $id]);
+            return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
+        }
+
+        /**
+         * Récupère une carte par son ID
+         *
+         * @param int $id L'ID de la carte
+         * @return Map|null La carte chargée ou null si non trouvée
+         */
+        public static function findMap(int $id): ?Map
+        {
+            $row = self::findById($id);
+            if (!$row) {
+                return null;
+            }
+
+            // Charger depuis le fichier JSON
+            $map = Map::fromFile($row['file_path']);
+            if (!$map) {
+                AppDebugger::log('Erreur lors du chargement de la carte depuis le fichier: ' . $row['file_path'], 'ERROR');
+                return null;
+            }
+
+            return $map;
+        }
+
+        /**
+         * Met à jour une carte existante
+         *
+         * @param int $id L'ID de la carte à mettre à jour
+         * @param Map $map Les nouvelles données de la carte
+         * @param string|null $description Nouvelle description optionnelle
+         * @return bool True si la mise à jour a réussi
+         */
+        public static function update(int $id, Map $map, ?string $description = null): bool
+        {
+            $pdo = self::getPDO();
+
+            // Vérifier que la carte existe
+            $existing = self::findById($id);
+            if (!$existing) {
+                AppDebugger::log('Carte non trouvée pour mise à jour, ID: ' . $id, 'ERROR');
+                return false;
+            }
+
+            // Sauvegarder le fichier JSON mis à jour
+            $filePath = $existing['file_path'];
+            if (!$map->saveToFile($filePath)) {
+                AppDebugger::log('Erreur lors de la sauvegarde du fichier JSON mis à jour', 'ERROR');
+                return false;
+            }
+
+            // Mettre à jour les données
+            $data = [
+                'name' => $map->getName(),
+                'width' => $map->getWidth(),
+                'height' => $map->getHeight(),
+                'created_at' => $existing['created_at'], // Garder la date de création originale
+                'updated_at' => $map->getUpdatedAt(),
+                'statistics' => $map->getStatistics()
+            ];
+
+            $sql = "
+                UPDATE maps
+                SET name = :name, description = :description, width = :width, height = :height,
+                    data = :data, updated_at = :updated_at
+                WHERE id = :id
+            ";
+
+            try {
+                $stmt = $pdo->prepare($sql);
+                $stmt->execute([
+                    'id' => $id,
+                    'name' => $map->getName(),
+                    'description' => $description,
+                    'width' => $map->getWidth(),
+                    'height' => $map->getHeight(),
+                    'data' => json_encode($data),
+                    'updated_at' => $map->getUpdatedAt()
+                ]);
+
+                AppDebugger::log('Carte mise à jour avec succès, ID: ' . $id, 'INFO');
+                return true;
+
+            } catch (Exception $e) {
+                AppDebugger::log('Erreur lors de la mise à jour de la carte: ' . $e->getMessage(), 'ERROR');
+                return false;
+            }
+        }
+
+        /**
+         * Supprime une carte
+         *
+         * @param int $id L'ID de la carte à supprimer
+         * @return bool True si la suppression a réussi
+         */
+        public static function delete(int $id): bool
+        {
+            $pdo = self::getPDO();
+
+            // Récupérer les informations de la carte avant suppression
+            $existing = self::findById($id);
+            if (!$existing) {
+                AppDebugger::log('Carte non trouvée pour suppression, ID: ' . $id, 'ERROR');
+                return false;
+            }
+
+            // Supprimer le fichier JSON
+            if (file_exists($existing['file_path'])) {
+                if (!unlink($existing['file_path'])) {
+                    AppDebugger::log('Erreur lors de la suppression du fichier: ' . $existing['file_path'], 'ERROR');
+                    // Ne pas arrêter la suppression pour autant
+                }
+            }
+
+            // Supprimer de la base de données
+            $stmt = $pdo->prepare('DELETE FROM maps WHERE id = :id');
+
+            try {
+                $stmt->execute(['id' => $id]);
+                $affectedRows = $stmt->rowCount();
+
+                if ($affectedRows > 0) {
+                    AppDebugger::log('Carte supprimée avec succès, ID: ' . $id, 'INFO');
+                    return true;
+                } else {
+                    AppDebugger::log('Aucune carte supprimée, ID: ' . $id, 'WARNING');
+                    return false;
+                }
+
+            } catch (Exception $e) {
+                AppDebugger::log('Erreur lors de la suppression de la carte: ' . $e->getMessage(), 'ERROR');
+                return false;
+            }
+        }
+
+        /**
+         * Récupère toutes les cartes avec pagination
+         *
+         * @param int $page Le numéro de page (commence à 1)
+         * @param int $perPage Nombre d'éléments par page
+         * @param string $orderBy Colonne de tri
+         * @param string $orderDirection Direction du tri (ASC ou DESC)
+         * @return array Tableau avec 'data' (cartes) et 'pagination' (infos de pagination)
+         */
+        public static function paginate(int $page = 1, int $perPage = 10, string $orderBy = 'created_at', string $orderDirection = 'DESC'): array
+        {
+            $pdo = self::getPDO();
+
+            // Calculer l'offset
+            $offset = ($page - 1) * $perPage;
+
+            // Compter le total
+            $totalStmt = $pdo->query('SELECT COUNT(*) as total FROM maps');
+            $total = $totalStmt->fetch(PDO::FETCH_ASSOC)['total'];
+
+            // Récupérer les cartes
+            $sql = "SELECT * FROM maps ORDER BY {$orderBy} {$orderDirection} LIMIT :limit OFFSET :offset";
+            $stmt = $pdo->prepare($sql);
+            $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
+            $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
+            $stmt->execute();
+
+            $maps = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+            // Calculer les informations de pagination
+            $totalPages = ceil($total / $perPage);
+            $hasNextPage = $page < $totalPages;
+            $hasPrevPage = $page > 1;
+
+            return [
+                'data' => $maps,
+                'pagination' => [
+                    'current_page' => $page,
+                    'per_page' => $perPage,
+                    'total' => (int) $total,
+                    'total_pages' => $totalPages,
+                    'has_next_page' => $hasNextPage,
+                    'has_prev_page' => $hasPrevPage,
+                    'next_page' => $hasNextPage ? $page + 1 : null,
+                    'prev_page' => $hasPrevPage ? $page - 1 : null
+                ]
+            ];
+        }
+
+        /**
+         * Recherche des cartes par nom ou description
+         *
+         * @param string $query Le terme de recherche
+         * @param int $limit Nombre maximum de résultats
+         * @return array Les cartes trouvées
+         */
+        public static function search(string $query, int $limit = 20): array
+        {
+            $pdo = self::getPDO();
+
+            // S'assurer que limit est un entier positif
+            $limit = max(1, min(100, (int)$limit));
+
+            // Recherche dans le nom et la description
+            $sql = "SELECT * FROM maps
+                   WHERE name LIKE :query OR description LIKE :query
+                   ORDER BY name
+                   LIMIT $limit";
+
+            $stmt = $pdo->prepare($sql);
+            $searchTerm = '%' . $query . '%';
+            $stmt->execute(['query' => $searchTerm]);
+
+            return $stmt->fetchAll(PDO::FETCH_ASSOC);
+        }
+
+        /**
+         * Récupère les statistiques globales des cartes
+         *
+         * @return array Statistiques générales
+         */
+        public static function getGlobalStats(): array
+        {
+            $pdo = self::getPDO();
+
+            // Compter le nombre total de cartes
+            $totalStmt = $pdo->query('SELECT COUNT(*) as total FROM maps');
+            $total = $totalStmt->fetch(PDO::FETCH_ASSOC)['total'];
+
+            // Dimensions moyennes
+            $dimensionsStmt = $pdo->query('SELECT AVG(width) as avg_width, AVG(height) as avg_height FROM maps');
+            $dimensions = $dimensionsStmt->fetch(PDO::FETCH_ASSOC);
+
+            // Dernière carte créée
+            $latestStmt = $pdo->query('SELECT name, created_at FROM maps ORDER BY created_at DESC LIMIT 1');
+            $latest = $latestStmt->fetch(PDO::FETCH_ASSOC);
+
+            return [
+                'total_maps' => (int) $total,
+                'average_width' => round((float) $dimensions['avg_width'], 1),
+                'average_height' => round((float) $dimensions['avg_height'], 1),
+                'latest_map' => $latest ?: null
+            ];
+        }
+
+        /**
+         * Vérifie si une carte existe
+         *
+         * @param int $id L'ID de la carte
+         * @return bool True si la carte existe
+         */
+        public static function exists(int $id): bool
+        {
+            $pdo = self::getPDO();
+            $stmt = $pdo->prepare('SELECT COUNT(*) as count FROM maps WHERE id = :id');
+            $stmt->execute(['id' => $id]);
+            return $stmt->fetch(PDO::FETCH_ASSOC)['count'] > 0;
+        }
+
+        /**
+         * Duplique une carte existante
+         *
+         * @param int $id L'ID de la carte à dupliquer
+         * @param string $newName Le nom de la nouvelle carte
+         * @return int|false L'ID de la nouvelle carte ou false en cas d'erreur
+         */
+        public static function duplicate(int $id, string $newName): int|false
+        {
+            $originalMap = self::findMap($id);
+            if (!$originalMap) {
+                AppDebugger::log('Carte originale non trouvée pour duplication, ID: ' . $id, 'ERROR');
+                return false;
+            }
+
+            // Créer une nouvelle carte avec le même contenu
+            $newMap = new Map($newName, $originalMap->getWidth(), $originalMap->getHeight());
+
+            // Copier toutes les tuiles
+            for ($q = 0; $q < $originalMap->getWidth(); $q++) {
+                for ($r = 0; $r < $originalMap->getHeight(); $r++) {
+                    $tile = $originalMap->getTile($q, $r);
+                    if ($tile) {
+                        $newMap->setTile($q, $r, $tile);
+                    }
+                }
+            }
+
+            // Créer la nouvelle carte en base
+            $description = "Copie de la carte #" . $id;
+            return self::create($newMap, $description);
+        }
+
+        /**
+         * Exporte une carte vers un format spécifique
+         *
+         * @param int $id L'ID de la carte
+         * @param string $format Le format d'export ('json', 'csv', etc.)
+         * @return string|false Les données exportées ou false en cas d'erreur
+         */
+        public static function export(int $id, string $format = 'json'): string|false
+        {
+            $map = self::findMap($id);
+            if (!$map) {
+                return false;
+            }
+
+            switch ($format) {
+                case 'json':
+                    return $map->toJson(true);
+
+                case 'csv':
+                    return self::exportToCSV($map);
+
+                default:
+                    AppDebugger::log('Format d\'export non supporté: ' . $format, 'ERROR');
+                    return false;
+            }
+        }
+
+        /**
+         * Exporte une carte au format CSV
+         *
+         * @param Map $map La carte à exporter
+         * @return string Les données CSV
+         */
+        private static function exportToCSV(Map $map): string
+        {
+            $csv = "q,r,type,resources,temperature,elevation\n";
+
+            for ($q = 0; $q < $map->getWidth(); $q++) {
+                for ($r = 0; $r < $map->getHeight(); $r++) {
+                    $tile = $map->getTile($q, $r);
+                    if ($tile && !$tile->isEmpty()) {
+                        $resources = json_encode($tile->getResources());
+                        $csv .= sprintf(
+                            "%d,%d,%s,%s,%s,%s\n",
+                            $q,
+                            $r,
+                            $tile->getType() ?? '',
+                            $resources,
+                            $tile->getTemperature() ?? '',
+                            $tile->getElevation() ?? ''
+                        );
+                    }
+                }
+            }
+
+            return $csv;
+        }
+    }
+}

+ 184 - 0
app/Tests/MapTest.php

@@ -0,0 +1,184 @@
+<?php
+
+/**
+ * Tests unitaires pour la classe Map
+ */
+
+// Inclure les classes nécessaires
+require_once __DIR__ . '/../Models/Map.php';
+
+/**
+ * Classe de tests simples pour la classe Map
+ */
+class MapTest
+{
+    private int $passed = 0;
+    private int $failed = 0;
+    private array $errors = [];
+
+    /**
+     * Exécute tous les tests
+     */
+    public function runAllTests(): void
+    {
+        echo "=== Tests de la classe Map ===\n\n";
+
+        $this->testMapCreation();
+        $this->testTileModification();
+        $this->testInvalidCoordinates();
+        $this->testStatistics();
+        $this->testJsonSerialization();
+        $this->testHexagonDistance();
+        $this->testTileTypes();
+
+        echo "\n=== Résultats des tests ===\n";
+        echo "Tests réussis: {$this->passed}\n";
+        echo "Tests échoués: {$this->failed}\n";
+
+        if (!empty($this->errors)) {
+            echo "\nErreurs:\n";
+            foreach ($this->errors as $error) {
+                echo "- $error\n";
+            }
+        }
+
+        echo "\n" . ($this->failed === 0 ? "✓ Tous les tests sont passés !" : "✗ Certains tests ont échoué") . "\n";
+    }
+
+    /**
+     * Test de création d'une carte
+     */
+    private function testMapCreation(): void
+    {
+        $map = new Map("Test Map", 3, 4);
+
+        $this->assert($map->getName() === "Test Map", "Nom de la carte");
+        $this->assert($map->getWidth() === 3, "Largeur de la carte");
+        $this->assert($map->getHeight() === 4, "Hauteur de la carte");
+        $tile = $map->getTile(0, 0);
+        $this->assert($tile !== null && $tile->getType() === "empty", "Tuile par défaut");
+    }
+
+    /**
+     * Test de modification des tuiles
+     */
+    private function testTileModification(): void
+    {
+        $map = new Map("Test", 3, 3);
+
+        $tile = new Tile(['type' => 'forest', 'resources' => ['wood' => 100]]);
+        $this->assert($map->setTile(1, 1, $tile), "Modification de tuile valide");
+        $retrievedTile = $map->getTile(1, 1);
+        $this->assert($retrievedTile !== null && $retrievedTile->getType() === 'forest', "Récupération de tuile modifiée");
+        $this->assert($retrievedTile->getProperty('resources')['wood'] === 100, "Récupération des propriétés de la tuile");
+    }
+
+    /**
+     * Test des coordonnées invalides
+     */
+    private function testInvalidCoordinates(): void
+    {
+        $map = new Map("Test", 2, 2);
+
+        $this->assert(!$map->isValidCoordinate(-1, 0), "Coordonnée Q négative invalide");
+        $this->assert(!$map->isValidCoordinate(0, -1), "Coordonnée R négative invalide");
+        $this->assert(!$map->isValidCoordinate(2, 0), "Coordonnée Q trop grande invalide");
+        $this->assert(!$map->isValidCoordinate(0, 2), "Coordonnée R trop grande invalide");
+
+        $this->assert($map->getTile(-1, 0) === null, "Récupération de tuile avec coordonnées invalides");
+        $this->assert(!$map->setTile(-1, 0, new Tile(['type' => 'forest'])), "Modification de tuile avec coordonnées invalides");
+    }
+
+    /**
+     * Test des statistiques
+     */
+    private function testStatistics(): void
+    {
+        $map = new Map("Test", 2, 2);
+        $map->setTile(0, 0, new Tile(['type' => 'castle']));
+        $map->setTile(1, 1, new Tile(['type' => 'forest']));
+
+        $stats = $map->getStatistics();
+
+        $this->assert($stats['total_hexagons'] === 4, "Nombre total d'hexagones");
+        $this->assert($stats['tile_type_counts']['castle'] === 1, "Nombre de châteaux");
+        $this->assert($stats['tile_type_counts']['forest'] === 1, "Nombre de forêts");
+        $this->assert($stats['empty_count'] === 2, "Nombre de cases vides");
+    }
+
+    /**
+     * Test de sérialisation JSON
+     */
+    private function testJsonSerialization(): void
+    {
+        $map = new Map("Test JSON", 2, 2);
+        $map->setTile(0, 0, new Tile(['type' => 'castle', 'resources' => ['gold' => 100]]));
+
+        $json = $map->toJson();
+        $data = json_decode($json, true);
+
+        $this->assert($data['name'] === "Test JSON", "Nom dans JSON");
+        $this->assert($data['width'] === 2, "Largeur dans JSON");
+        $this->assert(count($data['hexagons']) === 4, "Nombre d'hexagones dans JSON");
+        $this->assert($data['hexagons'][0]['tile']['type'] === 'castle', "Type de tuile dans JSON");
+        $this->assert($data['hexagons'][0]['tile']['resources']['gold'] === 100, "Propriétés de tuile dans JSON");
+
+        // Test de désérialisation
+        $loadedMap = Map::fromArray($data);
+        $this->assert($loadedMap !== null, "Chargement depuis tableau");
+        $loadedTile = $loadedMap->getTile(0, 0);
+        $this->assert($loadedTile !== null && $loadedTile->getType() === "castle", "Tuile après chargement");
+        $this->assert($loadedTile->getProperty('resources')['gold'] === 100, "Propriétés après chargement");
+    }
+
+    /**
+     * Test du calcul de distance entre hexagones
+     */
+    private function testHexagonDistance(): void
+    {
+        $hex1 = new Hexagon(0, 0);
+        $hex2 = new Hexagon(1, 1);
+        $hex3 = new Hexagon(0, 0);
+
+        $this->assert($hex1->distanceTo($hex2) === 2, "Distance entre (0,0) et (1,1)");
+        $this->assert($hex1->distanceTo($hex3) === 0, "Distance entre hexagones identiques");
+    }
+
+    /**
+     * Test des types de tuiles
+     */
+    private function testTileTypes(): void
+    {
+        $map = new Map("Test", 1, 1);
+
+        // Tester quelques types valides
+        $validTypes = ['empty', 'castle', 'forest', 'river', 'road'];
+        foreach ($validTypes as $type) {
+            $tile = new Tile(['type' => $type]);
+            $this->assert($map->setTile(0, 0, $tile), "Type valide: $type");
+            $retrievedTile = $map->getTile(0, 0);
+            $this->assert($retrievedTile !== null && $retrievedTile->getType() === $type, "Récupération du type: $type");
+        }
+    }
+
+    /**
+     * Méthode d'assertion simple
+     */
+    private function assert(bool $condition, string $description): void
+    {
+        if ($condition) {
+            $this->passed++;
+            echo "✓ $description\n";
+        } else {
+            $this->failed++;
+            $this->errors[] = $description;
+            echo "✗ $description\n";
+        }
+    }
+}
+
+// Exécuter les tests si le fichier est appelé directement
+if (basename(__FILE__) === basename($_SERVER['PHP_SELF'] ?? '')) {
+    $test = new MapTest();
+    $test->runAllTests();
+}

+ 1 - 0
config/config.php

@@ -29,6 +29,7 @@ $config = [
     'db_user' => 'root',
     'db_password' => 'root',
     'db_host' => 'localhost',
+    'db_socket' => '/Applications/MAMP/tmp/mysql/mysql.sock',
     'db_port' => 3306
 ];
 

+ 183 - 0
docs/MapClass.md

@@ -0,0 +1,183 @@
+# Classe Map - Générateur de Cartes Hexagonales
+
+## Vue d'ensemble
+
+La classe `Map` permet de créer et gérer des cartes composées d'hexagones (damier hexagonal). Chaque hexagone peut contenir une tuile avec des propriétés arbitraires (type, ressources, température, élévation, etc.).
+
+## Architecture
+
+### Classe Tile
+Représente une tuile avec des propriétés arbitraires :
+- **Propriétés flexibles** : type, ressources, température, élévation, etc.
+- **Méthodes d'accès** : `getProperty()`, `setProperty()`, `hasProperty()`
+- **Méthodes spécialisées** : `getType()`, `getResources()`, `getTemperature()`, etc.
+
+### Classe Map
+- Représente une carte complète
+- Gère une grille d'hexagones en coordonnées axiales (q, r)
+- Supporte la sauvegarde/chargement au format JSON
+
+### Classe Hexagon
+- Représente un hexagone individuel
+- Utilise des coordonnées axiales (q, r)
+- Peut calculer des coordonnées cubiques (x, y, z) avec x + y + z = 0
+
+## Coordonnées Hexagonales
+
+Le système utilise des coordonnées axiales où :
+- `q` : axe horizontal (est-ouest)
+- `r` : axe diagonal (nord-est - sud-ouest)
+- `s` : coordonnée calculée (-q - r) pour les coordonnées cubiques
+
+## Utilisation de Base
+
+### Créer une Carte
+
+```php
+// Créer une carte 10x10
+$map = new Map("Ma Carte", 10, 10);
+```
+
+### Créer et Placer des Tuiles
+
+```php
+// Créer une tuile avec des propriétés
+$castleTile = new Tile([
+    'type' => 'castle',
+    'resources' => ['gold' => 100, 'stone' => 50],
+    'temperature' => 20.5,
+    'elevation' => 100
+]);
+
+// Placer la tuile
+$map->setTile(0, 0, $castleTile);
+
+// Créer une tuile simple
+$forestTile = new Tile(['type' => 'forest', 'resources' => ['wood' => 200]]);
+$map->setTile(1, 1, $forestTile);
+```
+
+### Accéder aux Propriétés des Tuiles
+
+```php
+// Récupérer une tuile
+$tile = $map->getTile(0, 0);
+
+if ($tile) {
+    // Accès direct aux propriétés
+    $type = $tile->getType();
+    $resources = $tile->getResources();
+    $temperature = $tile->getTemperature();
+
+    // Accès générique
+    $customProperty = $tile->getProperty('custom_prop', 'default');
+    $tile->setProperty('new_prop', 'value');
+}
+```
+
+### Statistiques
+
+```php
+$stats = $map->getStatistics();
+// Retourne:
+// - total_hexagons: nombre total
+// - width, height: dimensions
+// - tile_type_counts: répartition par type
+// - empty_count, empty_percentage: cases vides
+// - filled_count, filled_percentage: cases remplies
+```
+
+## Sauvegarde et Chargement
+
+### Format JSON
+
+```json
+{
+    "name": "Ma Carte",
+    "width": 5,
+    "height": 5,
+    "created_at": "2025-10-12 15:30:00",
+    "updated_at": "2025-10-12 15:30:00",
+    "hexagons": [
+        {
+            "q": 0,
+            "r": 0,
+            "tile": {
+                "type": "castle",
+                "resources": {"gold": 100, "stone": 50},
+                "temperature": 20.5
+            }
+        }
+    ]
+}
+```
+
+### Utilisation
+
+```php
+// Sauvegarder
+$map->saveToFile('storage/maps/ma_carte.json');
+
+// Charger
+$loadedMap = Map::fromFile('storage/maps/ma_carte.json');
+```
+
+## Exemples Avancés
+
+### Créer une Tuile Complexe
+
+```php
+$tile = new Tile();
+$tile->setType('factory');
+$tile->setResources(['iron' => 50, 'coal' => 30]);
+$tile->setTemperature(15.0);
+$tile->setElevation(50);
+$tile->setProperty('owner', 'player1');
+$tile->setProperty('production_rate', 10);
+
+$map->setTile(5, 5, $tile);
+```
+
+### Analyser les Ressources d'une Carte
+
+```php
+$totalResources = [];
+foreach ($map->getAllHexagons() as $row) {
+    foreach ($row as $hexagon) {
+        $tile = $hexagon->getTile();
+        if ($tile && !$tile->isEmpty()) {
+            $resources = $tile->getResources();
+            foreach ($resources as $resource => $amount) {
+                $totalResources[$resource] = ($totalResources[$resource] ?? 0) + $amount;
+            }
+        }
+    }
+}
+```
+
+### Filtrer les Tuiles par Critères
+
+```php
+$hotTiles = [];
+for ($q = 0; $q < $map->getWidth(); $q++) {
+    for ($r = 0; $r < $map->getHeight(); $r++) {
+        $tile = $map->getTile($q, $r);
+        if ($tile && $tile->getTemperature() > 25.0) {
+            $hotTiles[] = ['q' => $q, 'r' => $r, 'tile' => $tile];
+        }
+    }
+}
+```
+
+## Tests
+
+Pour tester la classe, exécutez :
+```bash
+php app/Tests/MapTest.php
+```
+
+Cela exécute 37 tests validant toutes les fonctionnalités.
+
+## Exemple Complet
+
+Voir le fichier `app/Examples/MapExample.php` pour un exemple complet d'utilisation avec propriétés complexes.

+ 22 - 0
logs/app.log

@@ -3,3 +3,25 @@
 [2025-10-11 22:43:24] [EXCEPTION] Exception : Call to undefined function __()
 [2025-10-11 22:44:20] [EXCEPTION] Exception : Call to undefined function __()
 [2025-10-11 22:44:21] [EXCEPTION] Exception : Call to undefined function __()
+[2025-10-12 15:20:40] [EXCEPTION] Exception : SQLSTATE[HY000] [2002] No such file or directory
+[2025-10-12 15:21:04] [EXCEPTION] Exception : SQLSTATE[HY000] [1049] Unknown database 'map-generator'
+[2025-10-12 15:23:37] [Table maps créée avec succès] INFO
+[2025-10-12 15:23:37] [Carte créée avec succès, ID: 1] INFO
+[2025-10-12 15:23:37] [Carte mise à jour avec succès, ID: 1] INFO
+[2025-10-12 15:23:37] [EXCEPTION] Exception : SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''20'' at line 1
+[2025-10-12 15:23:49] [Table maps créée avec succès] INFO
+[2025-10-12 15:23:49] [Carte créée avec succès, ID: 2] INFO
+[2025-10-12 15:23:49] [Carte mise à jour avec succès, ID: 2] INFO
+[2025-10-12 15:23:49] [EXCEPTION] Exception : SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''20'' at line 5
+[2025-10-12 15:23:56] [Table maps créée avec succès] INFO
+[2025-10-12 15:23:56] [Carte créée avec succès, ID: 1] INFO
+[2025-10-12 15:23:56] [Carte mise à jour avec succès, ID: 1] INFO
+[2025-10-12 15:23:56] [EXCEPTION] Exception : SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''20'' at line 5
+[2025-10-12 15:24:03] [Table maps créée avec succès] INFO
+[2025-10-12 15:24:03] [Carte créée avec succès, ID: 2] INFO
+[2025-10-12 15:24:03] [Carte mise à jour avec succès, ID: 2] INFO
+[2025-10-12 15:24:03] [EXCEPTION] Exception : SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''20'' at line 4
+[2025-10-12 15:24:10] [Table maps créée avec succès] INFO
+[2025-10-12 15:24:10] [Carte créée avec succès, ID: 3] INFO
+[2025-10-12 15:24:10] [Carte mise à jour avec succès, ID: 3] INFO
+[2025-10-12 15:24:10] [Carte créée avec succès, ID: 4] INFO

+ 21 - 0
migrations/001_create_maps_table.sql

@@ -0,0 +1,21 @@
+-- Migration: Création de la table maps
+-- Date: 2025-10-12
+-- Description: Table pour stocker les cartes hexagonales avec métadonnées et chemin vers le fichier JSON
+
+CREATE TABLE IF NOT EXISTS `maps` (
+    `id` INT AUTO_INCREMENT PRIMARY KEY,
+    `name` VARCHAR(255) NOT NULL COMMENT 'Nom de la carte',
+    `description` TEXT COMMENT 'Description optionnelle de la carte',
+    `width` INT NOT NULL DEFAULT 10 COMMENT 'Largeur de la carte en hexagones',
+    `height` INT NOT NULL DEFAULT 10 COMMENT 'Hauteur de la carte en hexagones',
+    `data` JSON COMMENT 'Données sérialisées de la carte (statistiques, métadonnées)',
+    `file_path` VARCHAR(500) COMMENT 'Chemin vers le fichier JSON stocké',
+    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
+    `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de dernière modification',
+
+    -- Index pour optimiser les recherches
+    INDEX `idx_name` (`name`),
+    INDEX `idx_created_at` (`created_at`),
+    INDEX `idx_width_height` (`width`, `height`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+COMMENT='Table des cartes hexagonales du générateur de cartes';

+ 43 - 0
migrations/002_add_maps_indexes.sql

@@ -0,0 +1,43 @@
+-- Migration: Ajout d'index supplémentaires pour optimiser les performances
+-- Date: 2025-10-12
+-- Description: Index composites et fulltext pour améliorer les recherches
+
+-- Index pour la recherche fulltext sur le nom et la description
+-- Vérifier si l'index n'existe pas déjà
+SET @sql = (SELECT IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
+     WHERE TABLE_SCHEMA = DATABASE()
+     AND TABLE_NAME = 'maps'
+     AND INDEX_NAME = 'idx_fulltext_name_description') = 0,
+    'ALTER TABLE `maps` ADD FULLTEXT INDEX `idx_fulltext_name_description` (`name`, `description`)',
+    'SELECT "Index idx_fulltext_name_description déjà existant" as message'
+));
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- Index pour les cartes récemment modifiées (si pas déjà existant)
+SET @sql = (SELECT IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
+     WHERE TABLE_SCHEMA = DATABASE()
+     AND TABLE_NAME = 'maps'
+     AND INDEX_NAME = 'idx_updated_at') = 0,
+    'ALTER TABLE `maps` ADD INDEX `idx_updated_at` (`updated_at`)',
+    'SELECT "Index idx_updated_at déjà existant" as message'
+));
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- Index pour les dimensions (si pas déjà existant)
+SET @sql = (SELECT IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
+     WHERE TABLE_SCHEMA = DATABASE()
+     AND TABLE_NAME = 'maps'
+     AND INDEX_NAME = 'idx_dimensions') = 0,
+    'ALTER TABLE `maps` ADD INDEX `idx_dimensions` (`width`, `height`)',
+    'SELECT "Index idx_dimensions déjà existant" as message'
+));
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;

+ 63 - 0
migrations/README.md

@@ -0,0 +1,63 @@
+# Migrations de Base de Données
+
+Ce dossier contient les migrations SQL pour l'évolution de la base de données du générateur de cartes.
+
+## Structure des fichiers
+
+Les fichiers de migration suivent le format de nommage :
+```
+NNN_description_migration.sql
+```
+
+Où :
+- `NNN` : numéro séquentiel à 3 chiffres (001, 002, 003, etc.)
+- `description_migration` : description courte en snake_case
+
+## Comment exécuter les migrations
+
+### Via phpMyAdmin (MAMP)
+1. Ouvrez phpMyAdmin dans MAMP
+2. Sélectionnez la base `map-generator`
+3. Importez chaque fichier SQL dans l'ordre numérique
+
+### Via ligne de commande MySQL
+```bash
+# Se connecter à MySQL via MAMP
+/Applications/MAMP/Library/bin/mysql80/bin/mysql -u root -p -S /Applications/MAMP/tmp/mysql/mysql.sock map-generator
+
+# Exécuter une migration
+source migrations/001_create_maps_table.sql;
+source migrations/002_add_maps_indexes.sql;
+```
+
+### Via script PHP
+Vous pouvez créer un script PHP pour automatiser l'exécution des migrations.
+
+## Liste des migrations
+
+### 001_create_maps_table.sql
+- Crée la table principale `maps`
+- Définit tous les champs nécessaires pour stocker les cartes
+- Ajoute les index de base
+
+### 002_add_maps_indexes.sql
+- Ajoute des index supplémentaires pour optimiser les performances
+- Index fulltext pour la recherche
+- Index sur les dates et dimensions
+
+## Bonnes pratiques
+
+- **Ne jamais modifier** une migration déjà exécutée
+- **Créer une nouvelle migration** pour chaque changement de schéma
+- **Tester les migrations** sur une base de développement avant la production
+- **Documenter** chaque migration avec sa date et description
+- **Sauvegarder** la base avant d'exécuter des migrations destructives
+
+## Récupération d'urgence
+
+Si une migration cause des problèmes :
+
+1. Restaurer la sauvegarde de la base
+2. Vérifier le script de migration
+3. Corriger et re-tester
+4. Documenter le problème résolu

+ 65 - 0
migrations/run_migrations.php

@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * Script d'exécution des migrations
+ *
+ * Ce script exécute automatiquement toutes les migrations SQL dans l'ordre.
+ * Utilisation : php migrations/run_migrations.php
+ */
+
+// Configuration de la base de données
+require __DIR__ . '/../config/config.php';
+
+echo "=== Exécution des migrations ===\n\n";
+
+try {
+    // Connexion à MySQL via socket MAMP
+    $dsn = sprintf(
+        'mysql:dbname=%s;unix_socket=%s;charset=utf8mb4',
+        $config['db_name'],
+        $config['db_socket']
+    );
+
+    $pdo = new PDO($dsn, $config['db_user'], $config['db_password']);
+    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+    echo "✓ Connexion à la base de données établie\n";
+
+    // Créer la base si elle n'existe pas
+    $pdo->exec("CREATE DATABASE IF NOT EXISTS `{$config['db_name']}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+    $pdo->exec("USE `{$config['db_name']}`");
+
+    echo "✓ Base de données '{$config['db_name']}' sélectionnée\n\n";
+
+    // Lister et trier les fichiers de migration
+    $migrationFiles = glob(__DIR__ . '/*.sql');
+    sort($migrationFiles);
+
+    if (empty($migrationFiles)) {
+        echo "Aucune migration trouvée dans le dossier.\n";
+        exit(0);
+    }
+
+    // Exécuter chaque migration
+    foreach ($migrationFiles as $file) {
+        $fileName = basename($file);
+        echo "Exécution de $fileName... ";
+
+        try {
+            $sql = file_get_contents($file);
+            $pdo->exec($sql);
+            echo "✓ Réussie\n";
+        } catch (Exception $e) {
+            echo "✗ Échouée: " . $e->getMessage() . "\n";
+            exit(1);
+        }
+    }
+
+    echo "\n✓ Toutes les migrations ont été exécutées avec succès !\n";
+
+} catch (Exception $e) {
+    echo "✗ Erreur lors de l'exécution des migrations: " . $e->getMessage() . "\n";
+    exit(1);
+}
+
+echo "\n=== Migrations terminées ===\n";

+ 207 - 0
storage/maps/example_map.json

@@ -0,0 +1,207 @@
+{
+    "name": "Carte d'exemple",
+    "width": 5,
+    "height": 5,
+    "created_at": "2025-10-12 15:16:41",
+    "updated_at": "2025-10-12 15:16:41",
+    "hexagons": [
+        {
+            "q": 0,
+            "r": 0,
+            "tile": {
+                "type": "castle",
+                "resources": {
+                    "gold": 100,
+                    "stone": 50
+                },
+                "temperature": 20.5
+            }
+        },
+        {
+            "q": 0,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 4,
+            "tile": {
+                "type": "road",
+                "temperature": 25
+            }
+        },
+        {
+            "q": 1,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 1,
+            "tile": {
+                "type": "forest",
+                "resources": {
+                    "wood": 200
+                },
+                "temperature": 18
+            }
+        },
+        {
+            "q": 1,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 2,
+            "tile": {
+                "type": "river",
+                "resources": {
+                    "water": 1000
+                },
+                "temperature": 15
+            }
+        },
+        {
+            "q": 2,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 3,
+            "tile": {
+                "type": "field",
+                "resources": {
+                    "wheat": 150
+                },
+                "temperature": 22
+            }
+        },
+        {
+            "q": 3,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 4,
+            "tile": {
+                "type": "mountain",
+                "elevation": 1500,
+                "resources": {
+                    "iron": 75
+                },
+                "temperature": 5
+            }
+        }
+    ]
+}

+ 194 - 0
storage/maps/map_1760282617_68ebc7f911e1a.json

@@ -0,0 +1,194 @@
+{
+    "name": "Carte exemple modifiée",
+    "width": 5,
+    "height": 5,
+    "created_at": "2025-10-12 15:23:37",
+    "updated_at": "2025-10-12 15:23:37",
+    "hexagons": [
+        {
+            "q": 0,
+            "r": 0,
+            "tile": {
+                "type": "castle",
+                "resources": {
+                    "gold": 100
+                }
+            }
+        },
+        {
+            "q": 0,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 1,
+            "tile": {
+                "type": "river",
+                "resources": {
+                    "water": 1000
+                }
+            }
+        },
+        {
+            "q": 1,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 2,
+            "tile": {
+                "type": "forest",
+                "resources": {
+                    "wood": 50
+                }
+            }
+        },
+        {
+            "q": 2,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 4,
+            "tile": {
+                "type": "mountain",
+                "elevation": 2000
+            }
+        }
+    ]
+}

+ 194 - 0
storage/maps/map_1760282629_68ebc80530dfe.json

@@ -0,0 +1,194 @@
+{
+    "name": "Carte exemple modifiée",
+    "width": 5,
+    "height": 5,
+    "created_at": "2025-10-12 15:23:49",
+    "updated_at": "2025-10-12 15:23:49",
+    "hexagons": [
+        {
+            "q": 0,
+            "r": 0,
+            "tile": {
+                "type": "castle",
+                "resources": {
+                    "gold": 100
+                }
+            }
+        },
+        {
+            "q": 0,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 1,
+            "tile": {
+                "type": "river",
+                "resources": {
+                    "water": 1000
+                }
+            }
+        },
+        {
+            "q": 1,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 2,
+            "tile": {
+                "type": "forest",
+                "resources": {
+                    "wood": 50
+                }
+            }
+        },
+        {
+            "q": 2,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 4,
+            "tile": {
+                "type": "mountain",
+                "elevation": 2000
+            }
+        }
+    ]
+}

+ 194 - 0
storage/maps/map_1760282636_68ebc80c556d3.json

@@ -0,0 +1,194 @@
+{
+    "name": "Carte exemple modifiée",
+    "width": 5,
+    "height": 5,
+    "created_at": "2025-10-12 15:23:56",
+    "updated_at": "2025-10-12 15:23:56",
+    "hexagons": [
+        {
+            "q": 0,
+            "r": 0,
+            "tile": {
+                "type": "castle",
+                "resources": {
+                    "gold": 100
+                }
+            }
+        },
+        {
+            "q": 0,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 1,
+            "tile": {
+                "type": "river",
+                "resources": {
+                    "water": 1000
+                }
+            }
+        },
+        {
+            "q": 1,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 2,
+            "tile": {
+                "type": "forest",
+                "resources": {
+                    "wood": 50
+                }
+            }
+        },
+        {
+            "q": 2,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 4,
+            "tile": {
+                "type": "mountain",
+                "elevation": 2000
+            }
+        }
+    ]
+}

+ 194 - 0
storage/maps/map_1760282643_68ebc813d7030.json

@@ -0,0 +1,194 @@
+{
+    "name": "Carte exemple modifiée",
+    "width": 5,
+    "height": 5,
+    "created_at": "2025-10-12 15:24:03",
+    "updated_at": "2025-10-12 15:24:03",
+    "hexagons": [
+        {
+            "q": 0,
+            "r": 0,
+            "tile": {
+                "type": "castle",
+                "resources": {
+                    "gold": 100
+                }
+            }
+        },
+        {
+            "q": 0,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 1,
+            "tile": {
+                "type": "river",
+                "resources": {
+                    "water": 1000
+                }
+            }
+        },
+        {
+            "q": 1,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 2,
+            "tile": {
+                "type": "forest",
+                "resources": {
+                    "wood": 50
+                }
+            }
+        },
+        {
+            "q": 2,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 4,
+            "tile": {
+                "type": "mountain",
+                "elevation": 2000
+            }
+        }
+    ]
+}

+ 194 - 0
storage/maps/map_1760282650_68ebc81a4da9f.json

@@ -0,0 +1,194 @@
+{
+    "name": "Carte exemple modifiée",
+    "width": 5,
+    "height": 5,
+    "created_at": "2025-10-12 15:24:10",
+    "updated_at": "2025-10-12 15:24:10",
+    "hexagons": [
+        {
+            "q": 0,
+            "r": 0,
+            "tile": {
+                "type": "castle",
+                "resources": {
+                    "gold": 100
+                }
+            }
+        },
+        {
+            "q": 0,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 1,
+            "tile": {
+                "type": "river",
+                "resources": {
+                    "water": 1000
+                }
+            }
+        },
+        {
+            "q": 1,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 2,
+            "tile": {
+                "type": "forest",
+                "resources": {
+                    "wood": 50
+                }
+            }
+        },
+        {
+            "q": 2,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 4,
+            "tile": {
+                "type": "mountain",
+                "elevation": 2000
+            }
+        }
+    ]
+}

+ 194 - 0
storage/maps/map_1760282650_68ebc81a4f9df.json

@@ -0,0 +1,194 @@
+{
+    "name": "Copie de la carte exemple",
+    "width": 5,
+    "height": 5,
+    "created_at": "2025-10-12 15:24:10",
+    "updated_at": "2025-10-12 15:24:10",
+    "hexagons": [
+        {
+            "q": 0,
+            "r": 0,
+            "tile": {
+                "type": "castle",
+                "resources": {
+                    "gold": 100
+                }
+            }
+        },
+        {
+            "q": 0,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 0,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 1,
+            "tile": {
+                "type": "river",
+                "resources": {
+                    "water": 1000
+                }
+            }
+        },
+        {
+            "q": 1,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 1,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 2,
+            "tile": {
+                "type": "forest",
+                "resources": {
+                    "wood": 50
+                }
+            }
+        },
+        {
+            "q": 2,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 2,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 3,
+            "r": 4,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 0,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 1,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 2,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 3,
+            "tile": {
+                "type": "empty"
+            }
+        },
+        {
+            "q": 4,
+            "r": 4,
+            "tile": {
+                "type": "mountain",
+                "elevation": 2000
+            }
+        }
+    ]
+}