import json from pathlib import Path DEFAULT_LEVELS_PER_DAT_FILE = 32 LEVEL_WIDTH = 32 LEVEL_HEIGHT = 32 LEVEL_SIZE = LEVEL_WIDTH * LEVEL_HEIGHT PROJECT_ROOT = Path(__file__).resolve().parent.parent DEFAULT_DAT_PATH = PROJECT_ROOT / "assets" / "Rat" / "level.dat" DEFAULT_JSON_PATH = PROJECT_ROOT / "maze.json" MAP_EMPTY = 0 MAP_WALL = 1 MAP_TUNNEL = 2 VALID_TILE_VALUES = {MAP_EMPTY, MAP_WALL, MAP_TUNNEL} def get_default_map_source(): if DEFAULT_DAT_PATH.exists(): return DEFAULT_DAT_PATH return DEFAULT_JSON_PATH def _level_count_from_dat_size(raw_size, source_path): if raw_size <= 0: raise ValueError(f"Invalid DAT size for {source_path}: archive is empty") if raw_size % LEVEL_SIZE != 0: raise ValueError( f"Invalid DAT size for {source_path}: expected a multiple of {LEVEL_SIZE} bytes, got {raw_size}" ) return raw_size // LEVEL_SIZE def get_dat_level_count(source_path): source_path = Path(source_path) return _level_count_from_dat_size(source_path.stat().st_size, source_path) def get_level_count(source_path): source_path = Path(source_path) if source_path.suffix.lower() == ".dat": return get_dat_level_count(source_path) return 1 def normalize_level_index(level_index, level_count): level_count = int(level_count) if level_count <= 0: raise ValueError(f"Level count must be positive, got {level_count}") return int(level_index) % level_count def normalize_tiles(tiles, width=None, height=None): if not tiles: raise ValueError("Map data cannot be empty") if height is None: height = len(tiles) if height != len(tiles): raise ValueError(f"Invalid map height: expected {height}, got {len(tiles)}") if width is None: width = len(tiles[0]) if width <= 0: raise ValueError("Map width must be greater than zero") normalized = [] for row_index, row in enumerate(tiles): if len(row) != width: raise ValueError( f"Invalid row width at row {row_index}: expected {width}, got {len(row)}" ) normalized_row = [] for column_index, cell in enumerate(row): if isinstance(cell, bool): normalized_cell = MAP_WALL if cell else MAP_TUNNEL else: try: normalized_cell = int(cell) except (TypeError, ValueError) as exc: raise ValueError( f"Invalid tile value at ({column_index}, {row_index}): {cell!r}" ) from exc if normalized_cell not in VALID_TILE_VALUES: raise ValueError( f"Unsupported tile value at ({column_index}, {row_index}): {normalized_cell}" ) normalized_row.append(normalized_cell) normalized.append(normalized_row) return normalized def create_level(fill=MAP_EMPTY, width=LEVEL_WIDTH, height=LEVEL_HEIGHT, border_walls=True): if fill not in VALID_TILE_VALUES: raise ValueError(f"Unsupported fill tile: {fill}") tiles = [[fill for _ in range(width)] for _ in range(height)] if border_walls and width >= 2 and height >= 2: for x in range(width): tiles[0][x] = MAP_WALL tiles[height - 1][x] = MAP_WALL for y in range(height): tiles[y][0] = MAP_WALL tiles[y][width - 1] = MAP_WALL return tiles def load_json_level(source_path): source_path = Path(source_path) with source_path.open("r", encoding="utf-8") as file: matrix = json.load(file) return normalize_tiles(matrix) def save_json_level(destination_path, tiles, indent=2): destination_path = Path(destination_path) normalized_tiles = normalize_tiles(tiles) with destination_path.open("w", encoding="utf-8") as handle: json.dump(normalized_tiles, handle, indent=indent) handle.write("\n") def load_dat_levels(source_path): source_path = Path(source_path) raw_data = source_path.read_bytes() level_count = _level_count_from_dat_size(len(raw_data), source_path) levels = [] for level_index in range(level_count): level_offset = level_index * LEVEL_SIZE level_data = raw_data[level_offset:level_offset + LEVEL_SIZE] matrix = [] for row in range(LEVEL_HEIGHT): row_start = row * LEVEL_WIDTH raw_row = level_data[row_start:row_start + LEVEL_WIDTH] matrix.append(list(raw_row)) levels.append(normalize_tiles(matrix, width=LEVEL_WIDTH, height=LEVEL_HEIGHT)) return levels def load_dat_level(source_path, level_index=0): levels = load_dat_levels(source_path) return levels[normalize_level_index(level_index, len(levels))] def save_dat_levels(destination_path, levels): destination_path = Path(destination_path) if not levels: raise ValueError("DAT archive must contain at least one level") output = bytearray() for level_index, level in enumerate(levels): normalized_level = normalize_tiles(level, width=LEVEL_WIDTH, height=LEVEL_HEIGHT) for row in normalized_level: output.extend(row) expected_size = len(levels) * LEVEL_SIZE if len(output) != expected_size: raise ValueError( f"Invalid DAT payload size after serialization: expected {expected_size}, got {len(output)}" ) destination_path.write_bytes(bytes(output)) class Map: """Classe che rappresenta la mappa del labirinto.""" def __init__(self, maze_file=None, level_index=0): self.source_path = self._resolve_source_path(maze_file) self.level_count = get_level_count(self.source_path) self.level_index = normalize_level_index(level_index, self.level_count) self.tiles = self._load_tiles(self.source_path, self.level_index) self.matrix = [ [cell == MAP_WALL for cell in row] for row in self.tiles ] self.height = len(self.tiles) self.width = len(self.tiles[0]) def _resolve_source_path(self, maze_file): if maze_file is None: return get_default_map_source() candidate = Path(maze_file) if candidate.is_absolute(): return candidate if candidate.exists(): return candidate.resolve() project_candidate = PROJECT_ROOT / candidate if project_candidate.exists(): return project_candidate return project_candidate def _load_tiles(self, source_path, level_index): suffix = source_path.suffix.lower() if suffix == ".dat": return load_dat_level(source_path, level_index) return load_json_level(source_path) def _load_json_level(self, source_path): return load_json_level(source_path) def _load_dat_level(self, source_path, level_index): return load_dat_level(source_path, level_index) def in_bounds(self, x, y): return 0 <= x < self.width and 0 <= y < self.height def get_cell(self, x, y): return self.tiles[y][x] def is_wall(self, x, y): """Restituisce True se la cella รจ un muro, False altrimenti.""" return self.matrix[y][x] def is_traversable(self, x, y): return self.get_cell(x, y) != MAP_WALL def is_empty(self, x, y): return self.get_cell(x, y) == MAP_EMPTY def is_tunnel(self, x, y): return self.get_cell(x, y) == MAP_TUNNEL def get_tunnel_direction(self, x, y): directions = [ ("UP", 0, -1), ("DOWN", 0, 1), ("LEFT", -1, 0), ("RIGHT", 1, 0), ] traversable_neighbors = [] for direction, dx, dy in directions: nx = x + dx ny = y + dy if self.in_bounds(nx, ny) and self.is_traversable(nx, ny): traversable_neighbors.append(direction) if len(traversable_neighbors) == 1: return traversable_neighbors[0] if traversable_neighbors: return traversable_neighbors[0] return "UP"