/
sususer
/
ColonyGEN
Обзор
Документация
Войти
/
sususer
/
ColonyGEN
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
client/game_loop.py
1 185 строк
43 KB
Chekr
f11
03 июн 2026, 13:53
03 июн 2026, 13:53
d260831
Код
Авторство
О чём код?
import json import os import random import subprocess import sys import time import pygame from core.map.map import GameMap from client.render.map_renderer import MapRenderer from core.game_state import GameState from ml.vae.integration import generate_record_by_targets def _runtime_base_dir(): if getattr(sys, "frozen", False): return os.path.dirname(sys.executable) return os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) class Game: def __init__(self): self.width = 800 self.height = 600 self.windowed_size = (self.width, self.height) self.fullscreen = False self.screen = pygame.display.set_mode(self.windowed_size) pygame.display.set_caption("ColonyGEN") self.clock = pygame.time.Clock() self.running = True self.game_over = False self.victory = False self.game_over_new_game_button = pygame.Rect(300, 400, 200, 50) self.state = GameState() self.game_map = self.state.map self.simulation = self.state.turn_system self.rules = self.state.turn_system.rules self.map_renderer = MapRenderer() self.show_menu = False self.debug = True self.debug_focus_tile = None self.debug_hover_tile = None self.ui_font = pygame.font.SysFont(None, 28) self.small_ui_font = pygame.font.SysFont(None, 20) self.build_labels = { "generator": "Генератор", "mine": "Шахта", "researchstation": "Лаборатория", "wire": "Провод", "bridge": "Мост", "road": "Дорога" } self.build_colors = { "generator": (85, 95, 140), "mine": (115, 85, 65), "researchstation": (80, 95, 125), "wire": (80, 115, 135), "bridge": (95, 90, 75), "road": (90, 85, 70) } self.build_costs = { "generator": (3, 4), "mine": (3, 5), "researchstation": (3, 3), "wire": (2, 2), "bridge": (3, 3), "road": (0, 0) } self.road_tile_costs = { "grass": (1, 0), "sand": (1, 0), "forest": (2, 0), "mountain": (3, 1), "water": (0, 0) } self.debug_buttons = [ {"label": "+1", "value": 1, "rect": pygame.Rect(0, 0, 70, 30)}, {"label": "+5", "value": 5, "rect": pygame.Rect(0, 0, 70, 30)}, {"label": "+10", "value": 10, "rect": pygame.Rect(0, 0, 70, 30)}, {"label": "+50", "value": 50, "rect": pygame.Rect(0, 0, 70, 30)}, ] self.build_menu = None self.build_menu_rects = [] self.build_menu_close = None self.preview_building = None self.next_turn_button = pygame.Rect(10, 40, 140, 35) self.menu_buttons = { "save": pygame.Rect(260, 220, 280, 45), "load": pygame.Rect(260, 280, 280, 45), "new": pygame.Rect(260, 340, 280, 45), "vae": pygame.Rect(260, 400, 280, 45), "exit": pygame.Rect(260, 460, 280, 45), } self.load_menu_active = False self.load_menu_rects = [] self.load_menu_files = [] self.vae_menu_active = False self.vae_menu_buttons = { "back": pygame.Rect(110, 160, 140, 40), "dataset": pygame.Rect(110, 220, 250, 40), "train": pygame.Rect(110, 270, 250, 40), "new_map": pygame.Rect(110, 320, 250, 40), "simple_mode": pygame.Rect(410, 160, 280, 40), } self.vae_difficulty_slider = { "rect": pygame.Rect(410, 240, 280, 50), "min": 10.0, "max": 100.0, "value": 45.0, "label": "СЛОЖНОСТЬ", } self.vae_entropy_slider = { "rect": pygame.Rect(410, 320, 280, 50), "min": 0.35, "max": 0.95, "value": 0.62, "label": "ЭНТРОПИЯ", } self.vae_slider_dragging = None # "difficulty" or "entropy" or None self.vae_job_process = None self.vae_job_name = None self.vae_job_log_path = None self.vae_job_log_handle = None self.vae_status_lines = ["Нейросеть не запущена."] self.vae_status_summary = "ОЖИДАНИЕ: Нейросеть не запущена." self.vae_status_summary_color = (180, 180, 190) self.runtime_base_dir = _runtime_base_dir() self.vae_paths = { "dataset": os.path.join(self.runtime_base_dir, "data", "datasets", "maps"), "model": os.path.join(self.runtime_base_dir, "data", "models", "vae", "vae_best.npz"), "model_dir": os.path.join(self.runtime_base_dir, "data", "models", "vae"), "generated": os.path.join(self.runtime_base_dir, "data", "generated", "vae"), "reconstructed": os.path.join(self.runtime_base_dir, "data", "generated", "vae_recon"), "selected": os.path.join(self.runtime_base_dir, "data", "generated", "vae_selected"), "logs": os.path.join(self.runtime_base_dir, "data", "logs"), } def _apply_display_mode(self): if self.fullscreen: self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN) self.width, self.height = self.screen.get_size() else: self.screen = pygame.display.set_mode(self.windowed_size) self.width, self.height = self.windowed_size def toggle_fullscreen(self): self.fullscreen = not self.fullscreen self._apply_display_mode() def _set_debug_tile(self, pos): self.debug_hover_tile = self.screen_to_map(pos) if self.debug_hover_tile is not None: self.debug_focus_tile = self.debug_hover_tile def run(self): while self.running: self.handle_events() self.handle_input() self.update() self.render() self.clock.tick(60) def reset_world(self): self.state = GameState() self.game_map = self.state.map self.simulation = self.state.turn_system self.rules = self.state.turn_system.rules self.preview_building = None self.build_menu = None self.game_over = False self.victory = False print("[ИГРА] создан новый мир") def save_current_map(self): maps_dir = os.path.join(self.runtime_base_dir, "data", "maps") os.makedirs(maps_dir, exist_ok=True) filename = f"map_{self.game_map.seed}.json" path = os.path.join(maps_dir, filename) self.game_map.save_to_file(path) print(f"[СОХР] карта сохранена -> {path}") def open_load_menu(self): folder = os.path.join(self.runtime_base_dir, "data", "maps") if not os.path.exists(folder): print("[ЗАГР] папка с картами не найдена") return files = [ f for f in os.listdir(folder) if f.endswith(".json") ] files.sort(reverse=True) files = files[:10] self.load_menu_files = files self.load_menu_active = True self.load_menu_rects = [] print("[ЗАГР] открыто меню выбора карты") def load_selected_map(self, filename): path = os.path.join(self.runtime_base_dir, "data", "maps", filename) loaded_map = GameMap.load_from_file(path) self.state = GameState(game_map=loaded_map) self.game_map = self.state.map def _set_vae_status(self, *lines): self.vae_status_lines = [str(line) for line in lines if str(line).strip()] def _set_vae_summary(self, message, color): self.vae_status_summary = str(message) self.vae_status_summary_color = color def _read_vae_log_tail(self, max_lines=8): if not self.vae_job_log_path or not os.path.exists(self.vae_job_log_path): return [] with open(self.vae_job_log_path, "r", encoding="utf-8", errors="replace") as f: lines = [line.rstrip() for line in f.readlines() if line.strip()] return lines[-max_lines:] def _start_vae_job(self, job_name, module_name, cli_args): if self.vae_job_process is not None and self.vae_job_process.poll() is None: self._set_vae_summary(f"В РАБОТЕ: {self.vae_job_name}", (235, 205, 120)) self._set_vae_status( f"Задача уже выполняется: {self.vae_job_name}", "Дождитесь завершения перед запуском новой задачи." ) return os.makedirs(self.vae_paths["logs"], exist_ok=True) log_name = f"{job_name}_{int(time.time())}.log" log_path = os.path.join(self.vae_paths["logs"], log_name) log_handle = open(log_path, "w", encoding="utf-8") command = [sys.executable, "-m", module_name, *cli_args] self.vae_job_process = subprocess.Popen( command, cwd=self.runtime_base_dir, stdout=log_handle, stderr=subprocess.STDOUT ) self.vae_job_name = job_name self.vae_job_log_path = log_path self.vae_job_log_handle = log_handle self._set_vae_summary(f"В РАБОТЕ: {job_name}", (235, 205, 120)) self._set_vae_status( f"Запущено: {job_name}", f"Лог: {log_path}" ) def _update_vae_job(self): if self.vae_job_process is None: return if self.vae_job_log_handle is not None: self.vae_job_log_handle.flush() tail_lines = self._read_vae_log_tail() if tail_lines: self.vae_status_lines = tail_lines return_code = self.vae_job_process.poll() if return_code is None: return if self.vae_job_log_handle is not None: self.vae_job_log_handle.close() self.vae_job_log_handle = None tail_lines = self._read_vae_log_tail() status_message = f"{self.vae_job_name} завершено с кодом {return_code}" self.vae_status_lines = [ status_message, *tail_lines[-7:] ] if return_code == 0: self._set_vae_summary(f"ГОТОВО: {self.vae_job_name} (код 0)", (120, 220, 140)) else: self._set_vae_summary(f"ОШИБКА: {self.vae_job_name} (код {return_code})", (230, 120, 120)) self.vae_job_process = None self.vae_job_name = None def _load_game_map(self, loaded_map): self.state = GameState(game_map=loaded_map) self.game_map = self.state.map self.simulation = self.state.turn_system self.rules = self.state.turn_system.rules self.preview_building = None self.build_menu = None def generate_dataset(self): self._start_vae_job( "dataset_generation", "ml.dataset_generator_cli", [ "--output", self.vae_paths["dataset"], "--map-width", "48", "--map-height", "48", "--max-maps", "2000", ] ) def generate_vae_world(self): model_path = self.vae_paths["model"] if not os.path.exists(model_path): self._set_vae_summary("ОЖИДАНИЕ: сначала обучите модель", (230, 120, 120)) self._set_vae_status( "Файл модели не найден.", f"Сначала обучите нейросеть: {model_path}" ) return target_difficulty = self.vae_difficulty_slider["value"] target_entropy = self.vae_entropy_slider["value"] entropy_norm = (target_entropy - 0.35) / (0.95 - 0.35) temperature = 0.65 + entropy_norm * 0.65 simple_mode = getattr(self, "vae_simple_mode", False) seed = random.randint(0, 99999999) try: self._set_vae_summary("В РАБОТЕ: генерация карты нейросетью", (235, 205, 120)) mode_str = "ПРОСТОЙ" if simple_mode else "ПОЛНЫЙ" self._set_vae_status( f"Генерация ({mode_str} режим)...", f"сложность={target_difficulty:.1f} энтропия={target_entropy:.2f} темп={temperature:.2f}" ) selected_record, selection = generate_record_by_targets( model_path=model_path, dataset_root=self.vae_paths["dataset"], target_difficulty=target_difficulty, target_entropy=target_entropy, temperature=temperature, seed=seed, simple_mode=simple_mode, ) os.makedirs(self.vae_paths["selected"], exist_ok=True) selected_path = os.path.join( self.vae_paths["selected"], f"vae_selected_{seed}.json" ) with open(selected_path, "w", encoding="utf-8") as f: json.dump(selected_record, f, ensure_ascii=False, indent=2) self._load_game_map(GameMap.from_dataset_record(selected_record)) self.show_menu = False self.vae_menu_active = False self.load_menu_active = False generation_stats = selected_record.get("metadata", {}).get("generation_stats", {}) generation_time = generation_stats.get("elapsed_s") generation_memory = generation_stats.get("peak_memory_mb") self._set_vae_summary("ГОТОВО: мир сгенерирован", (120, 220, 140)) mode_str = "ПРОСТОЙ" if simple_mode else "ПОЛНЫЙ" self._set_vae_status( f"Загружен мир из нейросети", f"цел_слож={target_difficulty:.1f} цел_энтр={target_entropy:.2f}", f"факт_слож={selection['difficulty_score']:.2f} факт_энтр={selection['entropy_score']:.3f}", f"кандидаты={selection['evaluated_candidates']}", *( [f"время={generation_time:.3f}s память={generation_memory:.3f}МБ"] if generation_time is not None and generation_memory is not None else [] ), f"сохранено={self._truncate_path(selected_path)}" ) except Exception as e: self._set_vae_summary("ОШИБКА: генерация мира нейросетью", (230, 120, 120)) self._set_vae_status( "Не удалось сгенерировать мир через нейросеть.", str(e) ) def screen_to_map(self, pos): mx, my = pos x = mx // self.map_renderer.tile_size + self.map_renderer.camera_x y = my // self.map_renderer.tile_size + self.map_renderer.camera_y if 0 <= x < self.game_map.width and 0 <= y < self.game_map.height: return x, y return None def handle_events(self): if self.game_over: for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False elif event.type == pygame.MOUSEBUTTONDOWN: if event.button == 1: self.handle_game_over_click(event.pos) elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: self.running = False if event.key == pygame.K_RETURN: self.reset_world() self.game_over = False self.victory = False return for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False elif event.type == pygame.MOUSEWHEEL: self.map_renderer.zoom(event.y) elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: self.show_menu = not self.show_menu self.preview_building = None self.load_menu_active = False if not self.show_menu: self.vae_menu_active = False if event.key == pygame.K_F3: self.debug = not self.debug self.map_renderer.debug = self.debug if event.key == pygame.K_F11: self.toggle_fullscreen() elif event.type == pygame.MOUSEMOTION: self.debug_hover_tile = self.screen_to_map(event.pos) if self.vae_slider_dragging and self.vae_menu_active: self._update_slider_from_pos(self.vae_slider_dragging, event.pos[0]) elif event.type == pygame.MOUSEBUTTONUP: if event.button == 1: self.vae_slider_dragging = None elif event.type == pygame.MOUSEBUTTONDOWN: if event.button == 1: if self.load_menu_active: for rect, file in self.load_menu_rects: if rect.collidepoint(event.pos): self.load_selected_map(file) return if self.show_menu: if self.vae_menu_active: self.handle_vae_menu_click(event.pos) return if self.menu_buttons["save"].collidepoint(event.pos): self.save_current_map() elif self.menu_buttons["load"].collidepoint(event.pos): self.open_load_menu() return elif self.menu_buttons["new"].collidepoint(event.pos): self.reset_world() elif self.menu_buttons["vae"].collidepoint(event.pos): self.vae_menu_active = True elif self.menu_buttons["exit"].collidepoint(event.pos): self.running = False return if self.preview_building: self.handle_preview_click(event.pos) return if self.next_turn_button.collidepoint(event.pos): self.simulation.next_turn() self._check_game_end() return cell = self.screen_to_map(event.pos) if cell: x, y = cell if self.debug: self._set_debug_tile(event.pos) tile = self.game_map.grid[y][x] if ( tile.type == "forest" and tile.explored and tile.resource > 0 ): self.simulation.harvest_resource(x, y) return self.handle_build_click(event.pos) if self.debug: self.handle_debug_click(event.pos) elif event.button == 3: if self.debug: self._set_debug_tile(event.pos) self.open_build_menu(event.pos) def handle_vae_menu_click(self, pos): if self.vae_menu_buttons["back"].collidepoint(pos): self.vae_menu_active = False return if self.vae_menu_buttons["simple_mode"].collidepoint(pos): self.vae_simple_mode = not getattr(self, "vae_simple_mode", False) mode_str = "ПРОСТОЙ" if self.vae_simple_mode else "ПОЛНЫЙ" self._set_vae_status(f"Режим генерации: {mode_str}", "Нажмите для переключения") return diff_rect = self.vae_difficulty_slider["rect"] if diff_rect.collidepoint(pos): self.vae_slider_dragging = "difficulty" self._update_slider_from_pos("difficulty", pos[0]) return ent_rect = self.vae_entropy_slider["rect"] if ent_rect.collidepoint(pos): self.vae_slider_dragging = "entropy" self._update_slider_from_pos("entropy", pos[0]) return if self.vae_menu_buttons["dataset"].collidepoint(pos): self.generate_dataset() return if self.vae_menu_buttons["train"].collidepoint(pos): self._start_vae_job( "vae_train", "ml.vae_cli", [ "train", "--dataset", self.vae_paths["dataset"], "--output", self.vae_paths["model_dir"], "--epochs", "40", "--batch-size", "16", "--learning-rate", "0.001", "--beta-max", "0.02", "--beta-warmup", "10", ] ) return if self.vae_menu_buttons["new_map"].collidepoint(pos): self.generate_vae_world() return def _update_slider_from_pos(self, slider_name: str, x_pos: int) -> None: if slider_name == "difficulty": slider = self.vae_difficulty_slider rel_x = x_pos - slider["rect"].x width = slider["rect"].width ratio = max(0.0, min(1.0, rel_x / width)) slider["value"] = slider["min"] + ratio * (slider["max"] - slider["min"]) elif slider_name == "entropy": slider = self.vae_entropy_slider rel_x = x_pos - slider["rect"].x width = slider["rect"].width ratio = max(0.0, min(1.0, rel_x / width)) slider["value"] = slider["min"] + ratio * (slider["max"] - slider["min"]) def open_build_menu(self, pos): mx, my = pos x = mx // self.map_renderer.tile_size + self.map_renderer.camera_x y = my // self.map_renderer.tile_size + self.map_renderer.camera_y if not (0 <= x < self.game_map.width and 0 <= y < self.game_map.height): return options = self.rules.get_available_buildings(x, y) self.build_menu = { "x": x, "y": y, "options": options } self.build_menu_rects = [] self.build_menu_close = None def handle_debug_click(self, pos): mx, my = pos for btn in self.debug_buttons: if btn["rect"].collidepoint(mx, my): for _ in range(btn["value"]): self.simulation.tick() def handle_build_click(self, pos): if not self.build_menu: return if self.build_menu_close and self.build_menu_close.collidepoint(pos): self.build_menu = None return for rect, name in self.build_menu_rects: if rect.collidepoint(pos): x = self.build_menu["x"] y = self.build_menu["y"] from core.simulation.buildings import ( Generator, Mine, ResearchStation, Wire, Bridge, Road ) name_l = name.lower() preview_set = {"mine", "researchstation", "wire", "generator"} cls_map = { "mine": Mine, "researchstation": ResearchStation, "wire": Wire, "generator": Generator } if name_l in preview_set: self.preview_building = (cls_map[name_l], name_l, x, y) self.build_menu = None return class_map = { "generator": Generator, "mine": Mine, "researchstation": ResearchStation, "wire": Wire, "bridge": Bridge, "road": Road } cls = class_map.get(name_l) if cls: self.simulation.build_class(cls, x, y) self.build_menu = None return def handle_preview_click(self, pos): ok_rect = pygame.Rect(10, self.height - 80, 120, 40) cancel_rect = pygame.Rect(140, self.height - 80, 120, 40) cls, name, x, y = self.preview_building if ok_rect.collidepoint(pos): self.simulation.build_class(cls, x, y) self.preview_building = None elif cancel_rect.collidepoint(pos): self.preview_building = None def handle_input(self): if self.show_menu: return keys = pygame.key.get_pressed() speed = 1 if keys[pygame.K_w]: self.map_renderer.camera_y -= speed if keys[pygame.K_s]: self.map_renderer.camera_y += speed if keys[pygame.K_a]: self.map_renderer.camera_x -= speed if keys[pygame.K_d]: self.map_renderer.camera_x += speed def update(self): self._update_vae_job() def render(self): self.screen.fill((18, 18, 28)) self.map_renderer.render(self.screen, self.game_map, self.simulation) self.draw_preview() self.draw_ui() if self.build_menu: self.draw_build_menu() if self.show_menu: self.draw_menu() if self.load_menu_active: self.draw_load_menu() if self.game_over: self.draw_game_over() pygame.display.flip() def draw_load_menu(self): panel = pygame.Surface((400, 300), pygame.SRCALPHA) panel.fill((30, 30, 40, 230)) self.screen.blit(panel, (200, 150)) self.load_menu_rects = [] y = 160 for file in self.load_menu_files: rect = pygame.Rect(220, y, 360, 25) self.load_menu_rects.append((rect, file)) pygame.draw.rect(self.screen, (80, 80, 100), rect) pygame.draw.rect(self.screen, (140, 140, 160), rect, 1) txt = self.ui_font.render(file, True, (255, 255, 255)) self.screen.blit(txt, (230, y)) y += 30 def draw_build_menu(self): menu = self.build_menu base_x = 200 base_y = 150 w, h = 140, 46 gap = 10 self.build_menu_rects = [] panel = pygame.Surface((500, 240), pygame.SRCALPHA) panel.fill((25, 25, 35, 230)) self.screen.blit(panel, (base_x - 20, base_y - 20)) self.build_menu_close = pygame.Rect(base_x + 450, base_y - 16, 26, 26) font = self.ui_font tile = self.game_map.grid[menu["y"]][menu["x"]] title = self.small_ui_font.render( f"Строительство ({menu['x']}, {menu['y']}) · {tile.type} · {getattr(tile, 'biome', 'unknown')}", True, (220, 220, 230) ) self.screen.blit(title, (base_x, base_y - 10)) pygame.draw.rect( self.screen, (120, 55, 55), self.build_menu_close, border_radius=4 ) pygame.draw.rect( self.screen, (210, 150, 150), self.build_menu_close, 2, border_radius=4 ) close_text = font.render("×", True, (255, 255, 255)) self.screen.blit( close_text, close_text.get_rect(center=self.build_menu_close.center) ) for i, name in enumerate(menu["options"]): rect = pygame.Rect( base_x + (i % 3) * (w + gap), base_y + 22 + (i // 3) * (h + gap), w, h ) self.build_menu_rects.append((rect, name)) color = self.build_colors.get(name, (60, 60, 80)) pygame.draw.rect(self.screen, color, rect, border_radius=6) pygame.draw.rect(self.screen, (165, 165, 185), rect, 2, border_radius=6) text = font.render(self.build_labels.get(name, name), True, (255, 255, 255)) self.screen.blit(text, (rect.x + 10, rect.y + 4)) ap_cost, res_cost = self._get_building_cost(name, menu["x"], menu["y"]) cost_text = f"ОД:{ap_cost} Р:{res_cost}" cost_surface = self.small_ui_font.render(cost_text, True, (200, 200, 210)) self.screen.blit(cost_surface, (rect.x + 10, rect.y + 24)) def draw_ui(self): total_resources = sum(tile.resource for row in self.game_map.grid for tile in row) generation_stats = getattr(self.game_map, "generation_stats", {}) self.screen.blit( self.ui_font.render( f"Ресурсы: {self.simulation.resources}", True, (255, 255, 255) ), (10, 10) ) self.screen.blit( self.ui_font.render( f"ОД: {self.simulation.ap}/{self.simulation.max_ap}", True, (255, 255, 255) ), (10, 40) ) turn_font = pygame.font.SysFont(None, 28) turn_text = turn_font.render( f"ХОД: {self.simulation.turn}/50", True, (255, 255, 255) ) self.screen.blit(turn_text, (10, 70)) self.next_turn_button = pygame.Rect(10, 125, 140, 35) pygame.draw.rect(self.screen, (70, 70, 90), self.next_turn_button) pygame.draw.rect(self.screen, (140, 140, 160), self.next_turn_button, 2) txt = self.ui_font.render("След. ход", True, (255, 255, 255)) self.screen.blit(txt, txt.get_rect(center=self.next_turn_button.center)) if self.debug: base_x = self.width - 120 base_y = self.height - 200 for i, btn in enumerate(self.debug_buttons): rect = pygame.Rect(base_x, base_y + i * 40, 70, 30) btn["rect"] = rect pygame.draw.rect(self.screen, (50, 50, 50), rect) pygame.draw.rect(self.screen, (120, 120, 120), rect, 2) label = self.ui_font.render(btn["label"], True, (255, 255, 255)) self.screen.blit(label, label.get_rect(center=rect.center)) debug_lines = [ f"ОТЛАДКА ВКЛ | масштаб={self.map_renderer.tile_size}", f"камера=({self.map_renderer.camera_x}, {self.map_renderer.camera_y})", f"экран={'ПОЛНЫЙ' if self.fullscreen else 'ОКОННЫЙ'}", f"всего_рес={total_resources}", f"ген={generation_stats.get('elapsed_s', 0.0):.3f}s память={generation_stats.get('peak_memory_mb', 0.0):.3f}МБ", "F3 отладка | F11 полный экран", ] focus = self.debug_focus_tile or self.debug_hover_tile if focus is not None: fx, fy = focus if 0 <= fx < self.game_map.width and 0 <= fy < self.game_map.height: tile = self.game_map.grid[fy][fx] building = tile.building.__class__.__name__ if tile.building is not None else "-" debug_lines.extend([ f"клетка=({fx}, {fy}) {tile.type}/{getattr(tile, 'biome', 'unknown')}", f"ресурс={tile.resource} исследовано={int(bool(tile.explored))}", f"строение={building}", ]) panel_width = 260 panel_height = 18 + len(debug_lines) * 20 panel_x = self.width - panel_width - 12 panel_y = 12 panel_rect = pygame.Rect(panel_x, panel_y, panel_width, panel_height) pygame.draw.rect(self.screen, (18, 18, 24), panel_rect, border_radius=6) pygame.draw.rect(self.screen, (110, 110, 130), panel_rect, 1, border_radius=6) for idx, line in enumerate(debug_lines): color = (235, 235, 240) if idx < 5 else (205, 220, 235) text = self.small_ui_font.render(line[:36], True, color) self.screen.blit(text, (panel_x + 10, panel_y + 8 + idx * 20)) def _get_building_cost(self, name, x, y): tile = self.game_map.grid[y][x] if name == "road": return self.road_tile_costs.get(tile.type, (1, 0)) return self.build_costs.get(name, (0, 0)) def _check_game_end(self): if self.simulation.turn >= 50: self.game_over = True if self.simulation.resources >= 70: self.victory = True else: self.victory = False def draw_game_over(self): overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA) overlay.fill((15, 15, 25, 200)) self.screen.blit(overlay, (0, 0)) if self.victory: title_text = "ПОБЕДА" subtitle_text = f"Вы собрали {self.simulation.resources} ресурсов" title_color = (80, 200, 120) panel_color = (30, 60, 45) else: title_text = "ПОРАЖЕНИЕ" subtitle_text = f"Вы собрали {self.simulation.resources} ресурсов из 70" title_color = (200, 80, 80) panel_color = (60, 35, 35) panel = pygame.Surface((500, 300), pygame.SRCALPHA) panel.fill(panel_color + (240,)) self.screen.blit(panel, (150, 150)) pygame.draw.rect(self.screen, (60, 60, 80), (150, 150, 500, 300), 3) title_font = pygame.font.SysFont(None, 56, bold=True) subtitle_font = pygame.font.SysFont(None, 28) stats_font = pygame.font.SysFont(None, 24) title = title_font.render(title_text, True, title_color) title_rect = title.get_rect(center=(self.width // 2, 220)) self.screen.blit(title, title_rect) subtitle = subtitle_font.render(subtitle_text, True, (220, 220, 220)) sub_rect = subtitle.get_rect(center=(self.width // 2, 270)) self.screen.blit(subtitle, sub_rect) stats_lines = [ f"Ресурсы: {self.simulation.resources}", f"ОД: {self.simulation.ap}/{self.simulation.max_ap}" ] y_offset = 310 for line in stats_lines: stat = stats_font.render(line, True, (180, 180, 180)) stat_rect = stat.get_rect(center=(self.width // 2, y_offset)) self.screen.blit(stat, stat_rect) y_offset += 28 pygame.draw.rect(self.screen, (70, 70, 90), self.game_over_new_game_button, border_radius=8) pygame.draw.rect(self.screen, (140, 140, 160), self.game_over_new_game_button, 2, border_radius=8) btn_font = pygame.font.SysFont(None, 32) btn_text = btn_font.render("НОВАЯ ИГРА", True, (255, 255, 255)) btn_rect = btn_text.get_rect(center=self.game_over_new_game_button.center) self.screen.blit(btn_text, btn_rect) def handle_game_over_click(self, pos): if self.game_over_new_game_button.collidepoint(pos): self.reset_world() self.game_over = False self.victory = False def draw_menu(self): overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA) overlay.fill((0, 0, 0, 180)) self.screen.blit(overlay, (0, 0)) if self.vae_menu_active: self.draw_vae_menu() return title = self.ui_font.render("МЕНЮ", True, (255, 255, 255)) self.screen.blit(title, (350, 150)) items = [ ("СОХРАНИТЬ", "save"), ("ЗАГРУЗИТЬ", "load"), ("НОВАЯ КАРТА", "new"), ("НЕЙРОСЕТЬ", "vae"), ("ВЫХОД", "exit"), ] for text, key in items: rect = self.menu_buttons[key] pygame.draw.rect(self.screen, (60, 60, 80), rect) pygame.draw.rect(self.screen, (140, 140, 160), rect, 2) label = self.ui_font.render(text, True, (255, 255, 255)) self.screen.blit( label, label.get_rect(center=rect.center) ) def _truncate_path(self, path, max_length=32): if len(path) <= max_length: return path return "..." + path[-(max_length-3):] def _draw_slider(self, slider_config: dict, y_offset: int) -> None: rect = slider_config["rect"] min_val = slider_config["min"] max_val = slider_config["max"] value = slider_config["value"] label = slider_config["label"] ratio = (value - min_val) / (max_val - min_val) handle_x = rect.x + int(ratio * rect.width) track_rect = pygame.Rect(rect.x, rect.y + 20, rect.width, 8) pygame.draw.rect(self.screen, (45, 45, 60), track_rect, border_radius=4) pygame.draw.rect(self.screen, (100, 100, 120), track_rect, 1, border_radius=4) filled_rect = pygame.Rect(rect.x, rect.y + 20, handle_x - rect.x, 8) pygame.draw.rect(self.screen, (90, 130, 180), filled_rect, border_radius=4) handle_rect = pygame.Rect(handle_x - 6, rect.y + 16, 12, 16) pygame.draw.rect(self.screen, (200, 200, 220), handle_rect, border_radius=3) pygame.draw.rect(self.screen, (255, 255, 255), handle_rect, 1, border_radius=3) label_text = self.small_ui_font.render(f"{label}: {value:.1f}" if max_val > 1 else f"{label}: {value:.2f}", True, (220, 220, 230)) self.screen.blit(label_text, (rect.x, rect.y)) min_text = self.small_ui_font.render(f"{min_val:.0f}" if max_val > 1 else f"{min_val:.2f}", True, (150, 150, 160)) max_text = self.small_ui_font.render(f"{max_val:.0f}" if max_val > 1 else f"{max_val:.2f}", True, (150, 150, 160)) self.screen.blit(min_text, (rect.x, rect.y + 32)) self.screen.blit(max_text, (rect.x + rect.width - max_text.get_width(), rect.y + 32)) def draw_vae_menu(self): panel = pygame.Surface((640, 420), pygame.SRCALPHA) panel.fill((24, 24, 34, 235)) self.screen.blit(panel, (80, 140)) button_labels = { "back": "НАЗАД", "dataset": "СОЗДАТЬ ДАТАСЕТ", "train": "ОБУЧИТЬ МОДЕЛЬ", "new_map": "СГЕНЕРИРОВАТЬ КАРТУ", } for key, rect in self.vae_menu_buttons.items(): if key in button_labels: color = (65, 65, 90) if key == "new_map": color = (70, 95, 70) pygame.draw.rect(self.screen, color, rect, border_radius=6) pygame.draw.rect(self.screen, (155, 155, 175), rect, 2, border_radius=6) label = self.small_ui_font.render(button_labels[key], True, (255, 255, 255)) self.screen.blit(label, label.get_rect(center=rect.center)) simple_rect = self.vae_menu_buttons["simple_mode"] simple_mode = getattr(self, "vae_simple_mode", False) color = (100, 80, 120) if simple_mode else (65, 65, 90) border = (180, 160, 200) if simple_mode else (155, 155, 175) pygame.draw.rect(self.screen, color, simple_rect, border_radius=6) pygame.draw.rect(self.screen, border, simple_rect, 2, border_radius=6) mode_text = "ПРОСТОЙ РЕЖИМ: ВКЛ" if simple_mode else "ПРОСТОЙ РЕЖИМ: ВЫКЛ" label = self.small_ui_font.render(mode_text, True, (255, 255, 255)) self.screen.blit(label, label.get_rect(center=simple_rect.center)) self._draw_slider(self.vae_difficulty_slider, 0) self._draw_slider(self.vae_entropy_slider, 0) entropy_norm = (self.vae_entropy_slider["value"] - 0.35) / (0.95 - 0.35) temperature = 0.65 + entropy_norm * 0.65 info_lines = [ f"Модель: {self._truncate_path(self.vae_paths['model'])}", f"Датасет: {self._truncate_path(self.vae_paths['dataset'])}", ] for idx, line in enumerate(info_lines): text = self.small_ui_font.render(line, True, (210, 210, 220)) self.screen.blit(text, (410, 380 + idx * 22)) status_title = self.small_ui_font.render("СТАТУС", True, (255, 255, 255)) self.screen.blit(status_title, (110, 380)) # Разбиваем длинный текст статуса на строки summary_words = self.vae_status_summary.split() summary_lines = [] current_line = "" for word in summary_words: test_line = current_line + " " + word if current_line else word test_width = self.small_ui_font.size(test_line)[0] if test_width <= 500: # Максимальная ширина для текста current_line = test_line else: if current_line: summary_lines.append(current_line) current_line = word if current_line: summary_lines.append(current_line) # Отрисовываем строки статуса for idx, line in enumerate(summary_lines[:2]): # Максимум 2 строки summary_text = self.small_ui_font.render(line, True, self.vae_status_summary_color) self.screen.blit(summary_text, (110, 400 + idx * 18)) visible_lines = self.vae_status_lines[-3:] # Уменьшил до 3 строк for idx, line in enumerate(visible_lines): text = self.small_ui_font.render(line[:70], True, (220, 220, 225)) self.screen.blit(text, (110, 436 + idx * 18)) def get_total_resources(self): total = 0 for row in self.game_map.grid: for tile in row: total += tile.resource return total def draw_preview(self): if not self.preview_building: return cls, name, x, y = self.preview_building ts = self.map_renderer.tile_size cx = self.map_renderer.camera_x cy = self.map_renderer.camera_y instance = cls(x, y) area = instance.get_area(self.simulation) for rx, ry in area: if 0 <= rx < self.game_map.width and 0 <= ry < self.game_map.height: rect = pygame.Rect( (rx - cx) * ts, (ry - cy) * ts, ts, ts ) pygame.draw.rect(self.screen, (0, 255, 0), rect, 2) ok = pygame.Rect(10, self.height - 80, 120, 40) cancel = pygame.Rect(140, self.height - 80, 120, 40) pygame.draw.rect(self.screen, (50, 200, 50), ok) pygame.draw.rect(self.screen, (200, 50, 50), cancel) self.screen.blit( self.ui_font.render("ПОДТВЕРДИТЬ", True, (255, 255, 255)), (20, self.height - 70) ) self.screen.blit( self.ui_font.render("ОТМЕНА", True, (255, 255, 255)), (150, self.height - 70) )