""" map_graph.py Gère la structure du graphe, la persistance JSON et les algorithmes spatiaux (BFS). """ import json import os from dataclasses import dataclass, field, asdict from typing import Dict, List, Optional, Set, Tuple from collections import deque @dataclass class Connection: to_id: int direction: str is_verified: bool = True # True si on l'a vraiment traversé @dataclass class RoomNode: id: int name: str # Connexions: direction -> Connection object exits: Dict[str, Connection] = field(default_factory=dict) # Murs/Bloquages: direction -> nombre d'échecs failed_exits: Dict[str, int] = field(default_factory=dict) visited_count: int = 1 class MapGraph: def __init__(self): self.rooms: Dict[int, RoomNode] = {} # Mapping des directions inverses pour l'inférence self.inverse_dirs = { "n": "s", "s": "n", "e": "w", "w": "e", "north": "south", "south": "north", "east": "west", "west": "east", "up": "down", "down": "up", "ne": "sw", "sw": "ne", "nw": "se", "se": "nw", "enter": "exit", "exit": "enter", "in": "out", "out": "in" } def add_or_update_room(self, room_id: int, name: str): """Crée ou met à jour un noeud.""" if room_id not in self.rooms: self.rooms[room_id] = RoomNode(id=room_id, name=name) else: self.rooms[room_id].visited_count += 1 # On garde le nom le plus long (souvent le plus descriptif) if len(name) > len(self.rooms[room_id].name): self.rooms[room_id].name = name def add_connection(self, from_id: int, to_id: int, direction: str): """Ajoute une arête dirigée.""" if from_id not in self.rooms or to_id not in self.rooms: return # 1. Connexion directe (Vérifiée) self.rooms[from_id].exits[direction] = Connection(to_id=to_id, direction=direction, is_verified=True) # 2. Si une connexion précédente échouée existait, on la nettoie (Pruning) if direction in self.rooms[from_id].failed_exits: del self.rooms[from_id].failed_exits[direction] # 3. Connexion inverse (Inférée/Non vérifiée) # On suppose que si on va au Nord, le Sud ramène au départ. rev_dir = self.inverse_dirs.get(direction) if rev_dir: target_room = self.rooms[to_id] # On ajoute seulement si l'exit n'existe pas déjà if rev_dir not in target_room.exits: target_room.exits[rev_dir] = Connection(to_id=from_id, direction=rev_dir, is_verified=False) def record_failure(self, room_id: int, direction: str): """Enregistre un mur/bloquage.""" if room_id in self.rooms: self.rooms[room_id].failed_exits[direction] = self.rooms[room_id].failed_exits.get(direction, 0) + 1 def get_shortest_path(self, start_id: int, target_id: int) -> Optional[List[str]]: """BFS pour trouver le chemin le plus court.""" queue = deque([(start_id, [])]) visited = {start_id} while queue: curr, path = queue.popleft() if curr == target_id: return path if curr in self.rooms: for d, conn in self.rooms[curr].exits.items(): if conn.to_id not in visited: visited.add(conn.to_id) queue.append((conn.to_id, path + [d])) return None def get_hubs(self) -> List[str]: """Retourne les noms des salles importantes (Hubs) avec >3 sorties.""" return [r.name for r in self.rooms.values() if len(r.exits) > 3] # --- Persistance JSON --- def save(self, filepath: str): data = { str(rid): asdict(node) for rid, node in self.rooms.items() } with open(filepath, 'w') as f: json.dump(data, f, indent=2) def load(self, filepath: str): if not os.path.exists(filepath): return try: with open(filepath, 'r') as f: data = json.load(f) self.rooms = {} for rid, rdata in data.items(): node = RoomNode(id=int(rid), name=rdata['name'], visited_count=rdata['visited_count']) node.failed_exits = rdata['failed_exits'] # Reconstruire les objets Connection node.exits = {d: Connection(**c) for d, c in rdata['exits'].items()} self.rooms[int(rid)] = node except Exception as e: print(f"Error loading map: {e}")