/
sususer
/
ColonyGEN
Обзор
Документация
Войти
/
sususer
/
ColonyGEN
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
core/map/map.py
1 303 строки
40 KB
Chekr
f11
03 июн 2026, 13:53
03 июн 2026, 13:53
d260831
Код
Авторство
О чём код?
import random import time import tracemalloc import json import os import math from types import SimpleNamespace try: from noise import pnoise2 except Exception: pnoise2 = None from core.difficulty.evaluator import DifficultyEvaluator from .tile import Tile class GameMap: def __init__(self, width=48, height=48, seed=None, verbose=True, measure_memory=True): self.width = width self.height = height self.seed = seed if seed is not None else random.randint(0, 100000) self.verbose = bool(verbose) self.measure_memory = bool(measure_memory) self.rng = random.Random(self.seed) self.dyn_rng = random.Random(self.seed + 999) self._log(f"[КАРТА] сид={self.seed}") self._log(f"[КАРТА] размер={self.width}x{self.height}") trace_started_here = False peak_memory = 0 if self.measure_memory and not tracemalloc.is_tracing(): tracemalloc.start() trace_started_here = True t0 = time.perf_counter() self.layers = {} self.grid = self.generate() elapsed = time.perf_counter() - t0 if self.measure_memory and tracemalloc.is_tracing(): _, peak_memory = tracemalloc.get_traced_memory() if trace_started_here: tracemalloc.stop() self.generation_stats = { "mode": "procedural", "elapsed_s": round(elapsed, 4), "peak_memory_mb": round(peak_memory / (1024 * 1024), 3), "memory_tracked": self.measure_memory, "width": self.width, "height": self.height, "seed": self.seed, } self._log( f"[КАРТА] время генерации = {elapsed:.3f}s | " f"пик_память={self.generation_stats['peak_memory_mb']:.3f}МБ" ) self.dynamics = self._generate_dynamics() self._log("[КАРТА] готово") def _log(self, message): if self.verbose: print(message) def generate(self): self._log("[ГЕН] старт") try: grid = self._generate_perlin() self._log("[ГЕН] перлин готово") self._generate_lakes(grid) self._log("[ГЕН] озёра готово") river = self._generate_river(grid) self._log("[ГЕН] река готова") self._generate_beach(grid) self._log("[ГЕН] пляж готов") forbidden = self._build_forbidden_zone(river) self._log("[ГЕН] запретная зона готова") self._generate_mountains(grid, forbidden) self._log("[ГЕН] горы готовы") self._boost_forest(grid) self._log("[ГЕН] лес готов") self._generate_base(grid) resource_total = max(280, int(round(self.width * self.height * 0.175))) self._generate_resource_clusters(grid, total=resource_total) self._log("[ГЕН] ресурсы готовы") self._update_influence_layer(grid) if self.verbose: self._debug_tile_stats(grid) self._log("[ГЕН] завершено") return grid except Exception as e: import traceback print("[ГЕН][КРИТИЧЕСКАЯ ОШИБКА]", e) traceback.print_exc() raise def _generate_perlin(self): self._log("[ПЕРЛИН] старт") self.layers = self._generate_world_layers() grid = [] t_start = time.time() elevation_values = [] if self.verbose else None for y in range(self.height): row = [] for x in range(self.width): elevation = self.layers["elevation"][y][x] moisture = self.layers["moisture"][y][x] fertility = self.layers["fertility"][y][x] hazard = self.layers["hazard"][y][x] terrain, biome = self._classify_tile( elevation, moisture, fertility, hazard ) if elevation_values is not None: elevation_values.append(elevation) row.append(Tile(terrain, biome)) grid.append(row) if elevation_values: self._log( f"[ПЕРЛИН] готово | мин={min(elevation_values):.4f} " f"макс={max(elevation_values):.4f}" ) self._log(f"[ПЕРЛИН] время={time.time() - t_start:.3f}s") return grid def _generate_world_layers(self): raw_elevation = [] raw_moisture = [] raw_fertility = [] raw_hazard = [] for y in range(self.height): raw_elevation_row = [] raw_moisture_row = [] raw_fertility_row = [] raw_hazard_row = [] for x in range(self.width): raw_elevation_row.append( self._warped_layer_noise(x, y, scale=18.0, base_offset=11) ) raw_moisture_row.append( self._warped_layer_noise(x, y, scale=21.0, base_offset=37) ) raw_fertility_row.append( self._warped_layer_noise(x, y, scale=16.0, base_offset=73) ) raw_hazard_row.append( self._warped_layer_noise(x, y, scale=12.0, base_offset=109) ) raw_elevation.append(raw_elevation_row) raw_moisture.append(raw_moisture_row) raw_fertility.append(raw_fertility_row) raw_hazard.append(raw_hazard_row) elevation_noise = self._normalize_layer(raw_elevation) moisture_noise = self._normalize_layer(raw_moisture) fertility_noise = self._normalize_layer(raw_fertility) hazard_noise = self._normalize_layer(raw_hazard) elevation = [] moisture = [] fertility = [] hazard = [] influence = [] for y in range(self.height): elevation_row = [] moisture_row = [] fertility_row = [] hazard_row = [] influence_row = [] latitude = y / max(1, self.height - 1) for x in range(self.width): e_noise = elevation_noise[y][x] m_noise = moisture_noise[y][x] f_noise = fertility_noise[y][x] h_noise = hazard_noise[y][x] cx = (x / max(1, self.width - 1)) * 2 - 1 cy = latitude * 2 - 1 center_bias = max(0.0, 1.0 - (cx * cx + cy * cy) ** 0.5) regional_moisture = self._warped_layer_noise( x, y, scale=30.0, base_offset=151, strength=4.0 ) e = self._clamp01( e_noise * 0.82 + (1.0 - center_bias) * 0.12 + center_bias * 0.06 ) m = self._clamp01( m_noise * 0.78 + (1.0 - abs(e - 0.38)) * 0.16 + regional_moisture * 0.06 ) h = self._clamp01( h_noise * 0.58 + max(0.0, e - 0.55) * 0.42 ) f = self._clamp01( f_noise * 0.52 + m * 0.34 + (1.0 - h) * 0.14 ) elevation_row.append(e) moisture_row.append(m) fertility_row.append(f) hazard_row.append(h) influence_row.append(0.0) elevation.append(elevation_row) moisture.append(moisture_row) fertility.append(fertility_row) hazard.append(hazard_row) influence.append(influence_row) return { "elevation": elevation, "moisture": moisture, "fertility": fertility, "hazard": hazard, "influence": influence } def _normalize_layer(self, layer): values = [ value for row in layer for value in row ] min_value = min(values) max_value = max(values) span = max_value - min_value if span <= 0.000001: return [ [0.5 for _ in row] for row in layer ] return [ [ self._clamp01((value - min_value) / span) for value in row ] for row in layer ] def _warped_layer_noise(self, x, y, scale, base_offset, strength=6.0): warp_x = ( self._layer_noise(x, y, scale=34.0, base_offset=base_offset + 211) - 0.5 ) * strength warp_y = ( self._layer_noise(x, y, scale=37.0, base_offset=base_offset + 223) - 0.5 ) * strength return self._layer_noise( x + warp_x, y + warp_y, scale=scale, base_offset=base_offset ) def _layer_noise(self, x, y, scale, base_offset): angle = ((base_offset % 31) / 31.0) * math.pi cos_a = math.cos(angle) sin_a = math.sin(angle) rx = x * cos_a - y * sin_a ry = x * sin_a + y * cos_a offset = (self.seed % 997) * 0.013 + base_offset * 0.17 if pnoise2 is not None: try: value_a = pnoise2( (rx + offset) / scale, (ry - offset * 0.73) / scale, octaves=4, persistence=0.5, lacunarity=2.0, repeatx=2048, repeaty=2048, base=(self.seed + base_offset) & 1023 ) value_b = pnoise2( (x * 0.67 + y * 0.56 + offset * 0.41) / (scale * 1.05), (-x * 0.56 + y * 0.67 - offset * 0.37) / (scale * 0.95), octaves=3, persistence=0.55, lacunarity=2.1, repeatx=2048, repeaty=2048, base=(self.seed + base_offset + 313) & 1023 ) value_c = pnoise2( (x - y * 0.31 + offset * 0.47) / (scale * 1.9), (y + x * 0.27 - offset * 0.53) / (scale * 1.9), octaves=2, persistence=0.6, lacunarity=2.0, repeatx=2048, repeaty=2048, base=(self.seed + base_offset + 617) & 1023 ) value = value_a * 0.52 + value_b * 0.33 + value_c * 0.15 return self._clamp01((value + 1.0) / 2.0) except Exception: pass local_seed = ( self.seed + base_offset * 92821 + int(round(rx * 1000)) * 68917 + int(round(ry * 1000)) * 19373 + int(round(x * 1000)) * 11939 + int(round(y * 1000)) * 15971 ) local_rng = random.Random(local_seed) return local_rng.random() def _classify_tile(self, elevation, moisture, fertility, hazard): if elevation < 0.14 and moisture > 0.55: return "water", "lake" if elevation > 0.90: return "mountain", "mountain_range" if hazard > 0.72 and elevation > 0.64: return "mountain", "highland" if elevation > 0.82: return "mountain", "highland" if moisture < 0.34 and fertility < 0.52 and elevation < 0.58: return "sand", "desert" if moisture > 0.74 and elevation < 0.48: return "forest", "wetland" if moisture > 0.56 and fertility > 0.48: return "forest", "dense_forest" return "plain", "grassland" def _clamp01(self, value): return max(0.0, min(1.0, value)) def _noise_to_tile(self, v): if v < -0.35: return "water" elif v < -0.25: return "beach" elif v < 0.25: return "plain" elif v < 0.45: return "forest" return "mountain" def _generate_base(self, grid): self._log("[БАЗА] старт") candidates = [] center_x = self.width // 2 center_y = self.height // 2 allowed_radius = min(self.width, self.height) // 3 for y in range(self.height): for x in range(self.width): tile = grid[y][x] if tile.type in ["water", "mountain"]: continue dist = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5 if dist > allowed_radius: continue score = 0.0 if tile.type == "plain": score += 8 elif tile.type == "forest": score += 3 elif tile.type == "beach": score += 1 score += max(0, int(allowed_radius - dist)) score += self.layers["fertility"][y][x] * 4 score += (1.0 - self.layers["hazard"][y][x]) * 5 score += self._nearby_count(grid, x, y, "plain", 3) * 0.6 score += self._nearby_count(grid, x, y, "water", 4) * 0.2 score += self._nearby_resource_potential(x, y, 6) * 0.15 score -= self._nearby_count(grid, x, y, "mountain", 2) * 1.2 score -= self._nearby_count(grid, x, y, "water", 1) * 2.0 candidates.append((score, x, y)) if not candidates: self._log("[БАЗА] нет подходящей позиции") return candidates.sort(reverse=True) top_score = candidates[0][0] top_candidates = [ c for c in candidates if c[0] >= top_score - 3 ] score, x, y = self.rng.choice(top_candidates) grid[y][x].type = "base" grid[y][x].biome = "colony_core" grid[y][x].base = True grid[y][x].resource = 0 grid[y][x].locked = False self._log(f"[БАЗА] размещена в ({x}, {y})") def _nearby_count(self, grid, x, y, tile_type, radius): count = 0 for dy in range(-radius, radius + 1): for dx in range(-radius, radius + 1): if dx == 0 and dy == 0: continue ny, nx = y + dy, x + dx if 0 <= ny < self.height and 0 <= nx < self.width: if grid[ny][nx].type == tile_type: count += 1 return count def _nearby_resource_potential(self, x, y, radius): potential = 0.0 for dy in range(-radius, radius + 1): for dx in range(-radius, radius + 1): ny, nx = y + dy, x + dx if 0 <= ny < self.height and 0 <= nx < self.width: elevation = self.layers["elevation"][ny][nx] fertility = self.layers["fertility"][ny][nx] hazard = self.layers["hazard"][ny][nx] potential += elevation * 0.7 + fertility * 0.5 + hazard * 0.2 return potential def _update_influence_layer(self, grid): if "influence" not in self.layers: self.layers["influence"] = [ [0.0 for _ in range(self.width)] for _ in range(self.height) ] base = None for y in range(self.height): for x in range(self.width): self.layers["influence"][y][x] = 0.0 if getattr(grid[y][x], "base", False): base = (x, y) if base is None: return bx, by = base radius = max(6, min(self.width, self.height) // 4) for y in range(self.height): for x in range(self.width): distance = abs(x - bx) + abs(y - by) value = max(0.0, 1.0 - distance / radius) if grid[y][x].type == "mountain": value *= 0.35 elif grid[y][x].type == "water": value *= 0.25 elif grid[y][x].type == "forest": value *= 0.75 self.layers["influence"][y][x] = round(value, 4) def get_base_position(self): for y in range(self.height): for x in range(self.width): tile = self.grid[y][x] if getattr(tile, "base", False): return x, y return None def evaluate_difficulty(self): stats = { "water": 0, "beach": 0, "plain": 0, "forest": 0, "mountain": 0, "sand": 0, "base": 0 } total_resources = 0 for row in self.grid: for tile in row: if tile.type in stats: stats[tile.type] += 1 total_resources += tile.resource total_tiles = self.width * self.height mountain_ratio = stats["mountain"] / total_tiles water_ratio = stats["water"] / total_tiles forest_ratio = stats["forest"] / total_tiles plain_ratio = stats["plain"] / total_tiles difficulty_score = 0.0 difficulty_score += mountain_ratio * 40 difficulty_score += water_ratio * 25 difficulty_score -= plain_ratio * 20 difficulty_score -= forest_ratio * 10 difficulty_score -= min(total_resources / 400, 1.0) * 30 difficulty_score = max(0.0, difficulty_score) if difficulty_score < 15: difficulty_class = "easy" elif difficulty_score < 35: difficulty_class = "medium" else: difficulty_class = "hard" return { "difficulty_score": round(difficulty_score, 2), "difficulty_class": difficulty_class, "tile_stats": stats, "total_resources": total_resources } def _debug_tile_stats(self, grid): stats = { "water": 0, "beach": 0, "plain": 0, "forest": 0, "mountain": 0, "sand": 0, "base": 0 } for row in grid: for t in row: if t.type in stats: stats[t.type] += 1 self._log(f"[СТАТ] {stats}") def _generate_lakes(self, grid): self._log("[ОЗЁРА] старт") for i in range(self.rng.randint(1, 3)): cx = self.rng.randint(5, self.width - 6) cy = self.rng.randint(5, self.height - 6) frontier = [(cy, cx)] visited = set(frontier) steps = self.rng.randint(8, 20) for _ in range(steps): if not frontier: break y, x = self.rng.choice(frontier) if grid[y][x].type != "mountain": grid[y][x].type = "water" grid[y][x].biome = "lake" for dy, dx in [(-1,0),(1,0),(0,-1),(0,1)]: ny, nx = y + dy, x + dx if 0 <= ny < self.height and 0 <= nx < self.width: if (ny, nx) not in visited and self.rng.random() < 0.6: frontier.append((ny, nx)) visited.add((ny, nx)) self._log("[ОЗЁРА] готово") def _generate_river(self, grid): self._log("[РЕКА] старт") side = self.rng.choice(["top", "bottom", "left", "right"]) if side == "top": x, y = self.rng.randint(2, self.width-3), 0 main_dx, main_dy = 0, 1 elif side == "bottom": x, y = self.rng.randint(2, self.width-3), self.height-1 main_dx, main_dy = 0, -1 elif side == "left": x, y = 0, self.rng.randint(2, self.height-3) main_dx, main_dy = 1, 0 else: x, y = self.width-1, self.rng.randint(2, self.height-3) main_dx, main_dy = -1, 0 visited = set() path = [] max_steps = int((self.width + self.height) * 1.35) min_steps = min(self.width, self.height) // 2 prev_dx, prev_dy = main_dx, main_dy for step in range(max_steps): if (y, x) in visited: break visited.add((y, x)) path.append((x, y)) grid[y][x].type = "water" grid[y][x].biome = "river" if step >= min_steps: if side in ("top", "bottom") and y in (0, self.height - 1): break if side in ("left", "right") and x in (0, self.width - 1): break valid_candidates = [] for ndy in (-1, 0, 1): for ndx in (-1, 0, 1): if ndx == 0 and ndy == 0: continue progress = ndx * main_dx + ndy * main_dy if progress < 0: continue nx, ny = x + ndx, y + ndy if not (0 <= nx < self.width and 0 <= ny < self.height): continue if (ny, nx) in visited: continue elevation = self.layers["elevation"][ny][nx] moisture = self.layers["moisture"][ny][nx] bend = self._warped_layer_noise( nx, ny, scale=10.0, base_offset=173, strength=3.0 ) straightness = ndx * prev_dx + ndy * prev_dy score = ( progress * 1.9 + (1.0 - elevation) * 1.8 + moisture * 0.9 + bend * 1.7 - max(0, straightness) * 0.55 + self.rng.random() * 0.8 ) valid_candidates.append((score, ndx, ndy)) if not valid_candidates: break valid_candidates.sort(reverse=True) _, dx, dy = self.rng.choice(valid_candidates[:max(1, min(3, len(valid_candidates)))]) x += dx y += dy prev_dx, prev_dy = dx, dy self._log("[РЕКА] готово") return path def _generate_beach(self, grid): self._log("[ПЛЯЖ] старт") for y in range(self.height): for x in range(self.width): if grid[y][x].type == "water": for dy in range(-1, 2): for dx in range(-1, 2): ny, nx = y + dy, x + dx if 0 <= ny < self.height and 0 <= nx < self.width: if grid[ny][nx].type == "plain": grid[ny][nx].type = "beach" grid[ny][nx].biome = "coast" self._log("[ПЛЯЖ] готово") def _build_forbidden_zone(self, river): forbidden = set() for x, y in river: for dy in range(-1, 2): for dx in range(-1, 2): ny, nx = y + dy, x + dx if 0 <= ny < self.height and 0 <= nx < self.width: forbidden.add((nx, ny)) return forbidden def _generate_mountains(self, grid, forbidden): self._log("[ГОРЫ] старт") directions = [ (-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (-1, 1), (1, -1), (1, 1) ] seeds = [ (y, x) for y in range(4, self.height - 4) for x in range(4, self.width - 4) if ( self.layers["elevation"][y][x] > 0.62 or self.layers["hazard"][y][x] > 0.62 ) ] for _ in range(self.rng.randint(2, 4)): if seeds: y, x = self.rng.choice(seeds) else: x = self.rng.randint(4, self.width - 5) y = self.rng.randint(4, self.height - 5) ridge_angle = self.rng.random() * math.tau ridge_dx = math.cos(ridge_angle) ridge_dy = math.sin(ridge_angle) frontier = [(y, x)] visited = set(frontier) size = self.rng.randint(35, 80) for _ in range(size): if not frontier: break cy, cx = self.rng.choice(frontier) elevation = self.layers["elevation"][cy][cx] hazard = self.layers["hazard"][cy][cx] if ( (cx, cy) not in forbidden and grid[cy][cx].type != "water" and (elevation > 0.50 or hazard > 0.54) ): grid[cy][cx].type = "mountain" grid[cy][cx].biome = "mountain_range" for dy, dx in directions: ny = cy + dy nx = cx + dx if 0 <= ny < self.height and 0 <= nx < self.width: if (ny, nx) in visited: continue elevation = self.layers["elevation"][ny][nx] hazard = self.layers["hazard"][ny][nx] ridge_noise = self._warped_layer_noise( nx, ny, scale=14.0, base_offset=191, strength=3.0 ) alignment = abs(dx * ridge_dx + dy * ridge_dy) chance = ( 0.08 + alignment * 0.18 + max(0.0, elevation - 0.48) * 0.42 + max(0.0, hazard - 0.50) * 0.28 + ridge_noise * 0.10 ) if self.rng.random() < chance: frontier.append((ny, nx)) visited.add((ny, nx)) self._log("[ГОРЫ] готово") def _boost_forest(self, grid): self._log("[ЛЕС] старт") for y in range(self.height): for x in range(self.width): if ( grid[y][x].type == "plain" and self.rng.random() < 0.2 ): grid[y][x].type = "forest" grid[y][x].biome = "young_forest" self._log("[ЛЕС] готово") def _generate_resource_clusters(self, grid, total=400): self._log(f"[РЕС] генерация {total}") candidate_groups = { "forest": [], "mountain": [], "plain": [], "sand": [], } for y in range(self.height): for x in range(self.width): tile = grid[y][x] if tile.type in ("water", "beach", "base"): continue elevation = self.layers["elevation"][y][x] fertility = self.layers["fertility"][y][x] moisture = self.layers["moisture"][y][x] hazard = self.layers["hazard"][y][x] if tile.type == "mountain": local_support = self._nearby_count(grid, x, y, "mountain", 1) weight = 2.8 + elevation * 4.0 + hazard * 1.6 + local_support * 0.35 cap = min(5, 3 + int(elevation > 0.84) + int(hazard > 0.76)) elif tile.type == "forest": local_support = self._nearby_count(grid, x, y, "forest", 1) dense_bonus = 0.9 if tile.biome == "dense_forest" else 0.0 wet_bonus = 0.3 if tile.biome == "wetland" else 0.0 weight = 4.4 + fertility * 4.8 + moisture * 2.6 + local_support * 0.25 + dense_bonus + wet_bonus cap = 1 + int(tile.biome == "dense_forest" or fertility > 0.62 or moisture > 0.66) elif tile.type == "plain": weight = 0.28 + fertility * 0.9 cap = 1 elif tile.type == "sand": weight = 0.10 + hazard * 0.18 cap = 1 else: continue candidate_groups[tile.type].append({ "x": x, "y": y, "weight": weight, "cap": cap, "terrain": tile.type, }) forest_capacity = sum(candidate["cap"] for candidate in candidate_groups["forest"]) mountain_capacity = sum(candidate["cap"] for candidate in candidate_groups["mountain"]) forest_target = min(forest_capacity, int(round(total * 0.60))) mountain_target = min(mountain_capacity, int(round(total * 0.33))) other_target = max(0, total - forest_target - mountain_target) placed = 0 placed += self._allocate_resource_budget(grid, candidate_groups["forest"], forest_target, terrain_hint="forest") placed += self._allocate_resource_budget(grid, candidate_groups["mountain"], mountain_target, terrain_hint="mountain") placed += self._allocate_resource_budget( grid, candidate_groups["plain"] + candidate_groups["sand"], max(0, total - placed if other_target <= 0 else other_target), terrain_hint="mixed", ) remaining = max(0, total - placed) if remaining > 0: placed += self._allocate_resource_budget( grid, candidate_groups["forest"] + candidate_groups["mountain"] + candidate_groups["plain"] + candidate_groups["sand"], remaining, terrain_hint="mixed", ) self._log(f"[РЕС] размещено={placed}") def _allocate_resource_budget(self, grid, candidates, budget, terrain_hint): placed = 0 while budget > 0: weighted = [] total_weight = 0.0 for candidate in candidates: x = candidate["x"] y = candidate["y"] tile = grid[y][x] remaining_capacity = candidate["cap"] - tile.resource if remaining_capacity <= 0: continue local_adjacent = self._adjacent_resource_count(grid, x, y, terrain_filter=("forest" if candidate["terrain"] == "forest" else None)) weight = float(candidate["weight"]) if candidate["terrain"] == "forest": if local_adjacent >= 2: continue weight *= max(0.22, 1.0 - local_adjacent * 0.38) elif candidate["terrain"] == "mountain": weight *= max(0.35, 1.0 - max(0, tile.resource - 2) * 0.16) else: weight *= max(0.28, 1.0 - local_adjacent * 0.55) if weight <= 0.0: continue weighted.append((weight, candidate)) total_weight += weight if not weighted or total_weight <= 0.0: break roll = self.rng.random() * total_weight chosen = weighted[-1][1] cursor = 0.0 for weight, candidate in weighted: cursor += weight if roll <= cursor: chosen = candidate break tile = grid[chosen["y"]][chosen["x"]] remaining_capacity = chosen["cap"] - tile.resource if remaining_capacity <= 0: continue if chosen["terrain"] == "mountain": max_chunk = 2 if tile.resource < 3 else 1 else: max_chunk = 1 add = min(remaining_capacity, budget, max_chunk) if add <= 0: break tile.resource += add tile.locked = tile.type == "mountain" placed += add budget -= add if terrain_hint == "forest" and tile.resource >= 2: tile.locked = False return placed def _adjacent_resource_count(self, grid, x, y, terrain_filter=None): count = 0 for dy, dx in [(-1, 0), (1, 0), (0, -1), (0, 1)]: ny = y + dy nx = x + dx if not (0 <= ny < self.height and 0 <= nx < self.width): continue tile = grid[ny][nx] if terrain_filter is not None and tile.type != terrain_filter: continue if tile.resource > 0: count += 1 return count def get_area(self, x, y, r): tiles = [] for dy in range(-r, r + 1): for dx in range(-r, r + 1): ny, nx = y + dy, x + dx if 0 <= ny < self.height and 0 <= nx < self.width: tiles.append(self.grid[ny][nx]) return tiles def _generate_dynamics(self): return { "water_to_sand": self.dyn_rng.uniform(0.01, 0.03), "sand_to_water": self.dyn_rng.uniform(0.01, 0.03), "sand_to_plain": self.dyn_rng.uniform(0.01, 0.02), "plain_to_forest": self.dyn_rng.uniform(0.01, 0.03), "forest_to_plain": self.dyn_rng.uniform(0.005, 0.02), } def serialize(self): data = { "metadata": { "width": self.width, "height": self.height, "seed": self.seed, }, "difficulty": DifficultyEvaluator(SimpleNamespace(map=self)).evaluate(), "generation_stats": getattr(self, "generation_stats", {}), "dynamics": self.dynamics, "layers": self.layers, "tiles": [] } for y in range(self.height): row = [] for x in range(self.width): tile = self.grid[y][x] row.append({ "type": tile.type, "biome": getattr(tile, "biome", "unknown"), "resource": tile.resource, "locked": tile.locked, "explored": getattr(tile, "explored", False), "base": getattr(tile, "base", False) }) data["tiles"].append(row) return data def save_to_file(self, path): os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding="utf-8") as f: json.dump(self.serialize(), f, indent=4) print(f"[КАРТА] сохранено -> {path}") @classmethod def load_from_file(cls, path): with open(path, "r", encoding="utf-8") as f: data = json.load(f) obj = cls.__new__(cls) metadata = data["metadata"] obj.width = metadata["width"] obj.height = metadata["height"] obj.seed = metadata["seed"] obj.rng = random.Random(obj.seed) obj.dyn_rng = random.Random(obj.seed + 999) obj.dynamics = data["dynamics"] obj.layers = data.get("layers", {}) obj.generation_stats = data.get("generation_stats", {"mode": "loaded_file"}) obj.grid = [] for y in range(obj.height): row = [] for x in range(obj.width): td = data["tiles"][y][x] tile = Tile(td["type"], td.get("biome", "unknown")) tile.resource = td["resource"] tile.locked = td["locked"] tile.explored = td["explored"] tile.base = td["base"] row.append(tile) obj.grid.append(row) print(f"[КАРТА] загружено <- {path}") return obj @classmethod def from_dataset_record(cls, record): map_data = record.get("map", record) tiles_data = map_data["tiles"] height = len(tiles_data) width = len(tiles_data[0]) if height else 0 seed = int(record.get("metadata", {}).get("seed", random.randint(0, 100000))) obj = cls.__new__(cls) obj.width = width obj.height = height obj.seed = seed obj.rng = random.Random(obj.seed) obj.dyn_rng = random.Random(obj.seed + 999) obj.layers = map_data.get("layers", {}) obj.generation_stats = record.get("metadata", {}).get("generation_stats", {"mode": "dataset_record"}) obj.grid = [] for y in range(obj.height): row = [] for x in range(obj.width): td = tiles_data[y][x] tile = Tile(td["type"], td.get("biome", "unknown")) tile.resource = int(td.get("resource", 0)) tile.locked = bool(td.get("locked", False)) tile.explored = bool(td.get("explored", False)) tile.base = bool(td.get("base", False) or td.get("type") == "base") row.append(tile) obj.grid.append(row) obj.dynamics = obj._generate_dynamics() obj._update_influence_layer(obj.grid) return obj def step(self): new_grid = [ [Tile(tile.type, getattr(tile, "biome", "unknown")) for tile in row] for row in self.grid ] for y in range(self.height): for x in range(self.width): old = self.grid[y][x] new = new_grid[y][x] new.resource = old.resource new.building = old.building new.locked = old.locked new.explored = getattr(old, "explored", False) new.base = getattr(old, "base", False) if old.resource > 0 or old.building is not None: new.type = old.type new.biome = getattr(old, "biome", "unknown") continue neighbors = self._get_neighbors(y, x) water = neighbors.count("water") beach = neighbors.count("beach") plain = neighbors.count("plain") forest = neighbors.count("forest") if old.type == "water": if ( beach > 2 and self.dyn_rng.random() < self.dynamics["water_to_sand"] ): new.type = "beach" new.biome = "coast" elif old.type == "beach": if ( water > 2 and self.dyn_rng.random() < self.dynamics["sand_to_water"] ): new.type = "water" new.biome = "lake" elif ( plain > 2 and self.dyn_rng.random() < self.dynamics["sand_to_plain"] ): new.type = "plain" new.biome = "grassland" elif old.type == "plain": if ( forest > 2 and self.dyn_rng.random() < self.dynamics["plain_to_forest"] ): new.type = "forest" new.biome = "young_forest" elif old.type == "forest": if ( self.dyn_rng.random() < self.dynamics["forest_to_plain"] ): new.type = "plain" new.biome = "grassland" self.grid = new_grid self._update_influence_layer(self.grid) def _get_neighbors(self, y, x): res = [] for dy in [-1, 0, 1]: for dx in [-1, 0, 1]: if dy == 0 and dx == 0: continue ny, nx = y + dy, x + dx if 0 <= ny < self.height and 0 <= nx < self.width: res.append(self.grid[ny][nx].type) return res