text-adventure-template / map_graph.py
OctaveLeroy's picture
Upload 9 files
1f5351c verified
"""
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}")