diff --git a/__pycache__/common.cpython-314.pyc b/__pycache__/common.cpython-314.pyc index 17ef8f3..1783461 100644 Binary files a/__pycache__/common.cpython-314.pyc and b/__pycache__/common.cpython-314.pyc differ diff --git a/__pycache__/eb_engine.cpython-314.pyc b/__pycache__/eb_engine.cpython-314.pyc index 19cabcd..e52359c 100644 Binary files a/__pycache__/eb_engine.cpython-314.pyc and b/__pycache__/eb_engine.cpython-314.pyc differ diff --git a/__pycache__/eb_objects.cpython-314.pyc b/__pycache__/eb_objects.cpython-314.pyc index 8f5f73e..0711fc9 100644 Binary files a/__pycache__/eb_objects.cpython-314.pyc and b/__pycache__/eb_objects.cpython-314.pyc differ diff --git a/common.py b/common.py index bfeb276..a892229 100644 --- a/common.py +++ b/common.py @@ -1,6 +1,7 @@ import os import json import uuid +import random from dataclasses import dataclass, field from copy import deepcopy @@ -12,6 +13,14 @@ import pygame_gui from classes import Rock +from pathfinding.core.grid import Grid +from pathfinding.finder.a_star import AStarFinder + +from collections import defaultdict +from heapq import heappush, heappop +from math import sqrt, inf + + def path_exists(data, path): current = data for key in path: @@ -146,87 +155,233 @@ def path_exists(data, path): # print("Путь не найден (нет связи)") # return None +#def find_way(cells, start, goal): +# """Находит путь с диагональным движением, но БЕЗ прохода через углы""" +# rows = len(cells) +# if rows == 0: +# return None +# cols = len(cells[0]) +# +# def is_passable(cell): +# return (cell.terrain_obj is None or not isinstance(cell.terrain_obj, Rock)) +# +# s_row, s_col = start +# g_row, g_col = goal +# if not (0 <= s_row < rows and 0 <= s_col < cols and 0 <= g_row < rows and 0 <= g_col < cols): +# return None +# +# start_cell = cells[s_row][s_col] +# goal_cell = cells[g_row][g_col] +# if not is_passable(start_cell) or not is_passable(goal_cell): +# print(f"Старт/гол непроходимы") +# return None +# +# directions = [ +# (-1, 0), (1, 0), (0, -1), (0, 1), # ортогональ (1.0) +# (-1, -1), (-1, 1), (1, -1), (1, 1) # диагональ (1.414) +# ] +# +# def can_move_diagonal(current_r, current_c, dr, dc): +# """Проверяет, можно ли двигаться по диагонали (НЕ через угол)""" +# nr, nc = current_r + dr, current_c + dc +# +# # Ортогональные соседи ДОЛЖНЫ быть проходимыми для диагонального хода +# ortho1_r, ortho1_c = current_r + dr, current_c # вертикальный сосед +# ortho2_r, ortho2_c = current_r, current_c + dc # горизонтальный сосед +# +# # Проверяем границы для ортососедей +# if not (0 <= ortho1_r < rows and 0 <= ortho1_c < cols): +# return False +# if not (0 <= ortho2_r < rows and 0 <= ortho2_c < cols): +# return False +# +# return (is_passable(cells[ortho1_r][ortho1_c]) and +# is_passable(cells[ortho2_r][ortho2_c])) +# +# open_set = [] +# h = max(abs(s_row - g_row), abs(s_col - g_col)) +# heappush(open_set, (h, 0, s_row, s_col)) +# +# came_from = {} +# g_score = defaultdict(lambda: float('inf')) +# g_score[(s_row, s_col)] = 0 +# +# while open_set: +# _, _, row, col = heappop(open_set) +# if (row, col) == (g_row, g_col): +# path = [] +# current = (row, col) +# while current in came_from: +# path.append(current) +# current = came_from[current] +# path.append(start) +# return path[::-1] +# +# for dr, dc in directions: +# nr, nc = row + dr, col + dc +# if 0 <= nr < rows and 0 <= nc < cols: +# target_cell = cells[nr][nc] +# +# if (nr, nc) != start and target_cell.creature_obj is not None: +# continue +# if not is_passable(target_cell): +# continue +# +# if abs(dr) + abs(dc) == 2: +# if not can_move_diagonal(row, col, dr, dc): +# continue +# +# move_cost = 1.414 if abs(dr) + abs(dc) == 2 else 1.0 +# tentative_g = g_score[(row, col)] + move_cost +# pos = (nr, nc) +# +# if tentative_g < g_score[pos]: +# came_from[pos] = (row, col) +# g_score[pos] = tentative_g +# h = max(abs(nr - g_row), abs(nc - g_col)) +# f = tentative_g + h +# heappush(open_set, (f, tentative_g, nr, nc)) +# +# print("Путь не найден") +# return None + + +#def find_way(cells, start, goal): +# """A* pathfinding — только новая библиотека""" +# rows = len(cells) +# if rows == 0: +# print("Путь не найден: пустая карта") +# return None +# +# cols = len(cells[0]) +# +# # ★ Проверка границ ★ +# s_row, s_col = start +# g_row, g_col = goal +# if (s_row >= rows or s_col >= cols or +# g_row >= rows or g_col >= cols): +# print(f"Путь не найден: выход за границы карты {start} -> {goal}") +# return None +# +# # ★ НАХОДИМ существо в start ★ +# start_creature = cells[s_row][s_col].creature_obj +# +# # Матрица препятствий +# matrix = [[1 for _ in cells[row]] for row in range(rows)] +# +# for r in range(rows): +# for c in range(cols): +# cell_creature = cells[r][c].creature_obj +# if cell_creature and cell_creature != start_creature: +# matrix[r][c] = 0 +# +# from pathfinding.core.grid import Grid +# from pathfinding.finder.a_star import AStarFinder +# +# grid = Grid(matrix=matrix) +# start_node = grid.node(s_row, s_col) +# end_node = grid.node(g_row, g_col) +# +# finder = AStarFinder() +# path_nodes, _ = finder.find_path(start_node, end_node, grid) +# +# if not path_nodes or len(path_nodes) <= 1: +# print(f"Путь не найден: {start} -> {goal}") +# return None +# +# path = [(node.x, node.y) for node in path_nodes] +# +# return path + + def find_way(cells, start, goal): - """Находит путь с диагональным движением, но БЕЗ прохода через углы""" + """★СУПЕРБЫСТРЫЙ BFS: массивы вместо set/deque★""" rows = len(cells) if rows == 0: + print("Путь не найден: пустая карта") return None + cols = len(cells[0]) - - def is_passable(cell): - return (cell.terrain_obj is None or not isinstance(cell.terrain_obj, Rock)) - s_row, s_col = start g_row, g_col = goal - if not (0 <= s_row < rows and 0 <= s_col < cols and 0 <= g_row < rows and 0 <= g_col < cols): + + if (s_row >= rows or s_col >= cols or + g_row >= rows or g_col >= cols): + print(f"Путь не найден: выход за границы {start} -> {goal}") return None - start_cell = cells[s_row][s_col] - goal_cell = cells[g_row][g_col] - if not is_passable(start_cell) or not is_passable(goal_cell): - print(f"Старт/гол непроходимы") - return None + # ★ МАТРИЦЫ вместо set (10x быстрее хэширования) ★ + walkable = [[True] * cols for _ in range(rows)] + rocks_only = [[False] * cols for _ in range(rows)] + start_creature = cells[s_row][s_col].creature_obj - directions = [ - (-1, 0), (1, 0), (0, -1), (0, 1), # ортогональ (1.0) - (-1, -1), (-1, 1), (1, -1), (1, 1) # диагональ (1.414) - ] + for r in range(rows): + for c in range(cols): + cell = cells[r][c] + if (cell.creature_obj and cell.creature_obj != start_creature) or \ + (cell.terrain_obj and cell.terrain_obj.sprite_name == "rock_small"): + walkable[r][c] = False + if cell.terrain_obj and cell.terrain_obj.sprite_name == "rock_small": + rocks_only[r][c] = True - def can_move_diagonal(current_r, current_c, dr, dc): - """Проверяет, можно ли двигаться по диагонали (НЕ через угол)""" - nr, nc = current_r + dr, current_c + dc + # ★ ВЫЧИСЛЯЕМЫЕ МАССИВЫ ★ + visited = [[False] * cols for _ in range(rows)] + parent = [[None] * cols for _ in range(rows)] + + # ★ БЫСТРАЯ ОЧЕРЕДЬ: индекс вместо deque ★ + queue_size = 0 + queue = [[0, 0] for _ in range(rows * cols)] # Предварительно выделяем + queue[0] = [s_row, s_col] + queue_size = 1 + front = 0 + + visited[s_row][s_col] = True + + directions = [(-1,0), (1,0), (0,-1), (0,1), + (-1,-1), (-1,1), (1,-1), (1,1)] + + while front < queue_size: + # ★ БЫСТРОЕ извлечение ★ + r, c = queue[front] + front += 1 - # Ортогональные соседи ДОЛЖНЫ быть проходимыми для диагонального хода - ortho1_r, ortho1_c = current_r + dr, current_c # вертикальный сосед - ortho2_r, ortho2_c = current_r, current_c + dc # горизонтальный сосед - - # Проверяем границы для ортососедей - if not (0 <= ortho1_r < rows and 0 <= ortho1_c < cols): - return False - if not (0 <= ortho2_r < rows and 0 <= ortho2_c < cols): - return False - - return (is_passable(cells[ortho1_r][ortho1_c]) and - is_passable(cells[ortho2_r][ortho2_c])) - - open_set = [] - h = max(abs(s_row - g_row), abs(s_col - g_col)) - heappush(open_set, (h, 0, s_row, s_col)) - - came_from = {} - g_score = defaultdict(lambda: float('inf')) - g_score[(s_row, s_col)] = 0 - - while open_set: - _, _, row, col = heappop(open_set) - if (row, col) == (g_row, g_col): + if r == g_row and c == g_col: path = [] - current = (row, col) - while current in came_from: - path.append(current) - current = came_from[current] - path.append(start) + cr, cc = g_row, g_col + while True: + path.append((cr, cc)) + if parent[cr][cc] is None: + break + pr, pc = parent[cr][cc] + cr, cc = pr, pc return path[::-1] for dr, dc in directions: - nr, nc = row + dr, col + dc - if 0 <= nr < rows and 0 <= nc < cols and is_passable(cells[nr][nc]): + nr, nc = r + dr, c + dc + + if (0 <= nr < rows and 0 <= nc < cols and + walkable[nr][nc] and not visited[nr][nc]): # ★ ПРОВЕРКА ДИАГОНАЛИ ★ - if abs(dr) + abs(dc) == 2: # диагональное движение - if not can_move_diagonal(row, col, dr, dc): - continue # пропускаем запрещённую диагональ - - move_cost = 1.414 if abs(dr) + abs(dc) == 2 else 1.0 - tentative_g = g_score[(row, col)] + move_cost - pos = (nr, nc) - - if tentative_g < g_score[pos]: - came_from[pos] = (row, col) - g_score[pos] = tentative_g - h = max(abs(nr - g_row), abs(nc - g_col)) - f = tentative_g + h - heappush(open_set, (f, tentative_g, nr, nc)) + if dr * dc == 0 or can_move_diagonal(r, c, nr, nc, rocks_only, rows, cols): + visited[nr][nc] = True + parent[nr][nc] = (r, c) + + # ★ БЫСТРОЕ добавление в очередь ★ + queue[queue_size] = [nr, nc] + queue_size += 1 - print("Путь не найден") - return None \ No newline at end of file + print(f"Путь не найден: {start} -> {goal}") + return None + +def can_move_diagonal(r, c, nr, nc, rocks_only, rows, cols): + """Запрет среза только около rock""" + dr = nr - r + dc = nc - c + r1, c1 = r + dr, c + r2, c2 = r, c + dc + + check1_ok = (0 <= r1 < rows and 0 <= c1 < cols and not rocks_only[r1][c1]) + check2_ok = (0 <= r2 < rows and 0 <= c2 < cols and not rocks_only[r2][c2]) + + return check1_ok and check2_ok \ No newline at end of file diff --git a/def_map.json b/def_map.json index 0f2b5e2..4b2f92c 100644 --- a/def_map.json +++ b/def_map.json @@ -926,7 +926,11 @@ "sprite_name": "grass_small" }, "item_obj": {}, - "creature_obj": {} + "creature_obj": { + "id": "1", + "name": "2", + "sprite_name": "elf_watching" + } }, { "terrain_obj": { diff --git a/eb_engine.py b/eb_engine.py index 2d2eddb..56e8cdb 100644 --- a/eb_engine.py +++ b/eb_engine.py @@ -1,4 +1,5 @@ -from common import os, json, uuid, deepcopy, dataclass, field +from common import os, json, uuid, deepcopy, random +from common import dataclass, field from common import pygame, pygame_gui import eb_objects import eb_terrain_objects @@ -68,6 +69,9 @@ class Map: def move_obj(self, type, start, goal): """Перемещает объект типа 'terrain_obj', 'item_obj' или 'creature_obj' из клетки start=(row, col) в goal=(row, col)""" + if goal is None: + return False + s_y, s_x = start d_y, d_x = goal @@ -79,8 +83,9 @@ class Map: source_cell = self.cells[s_y][s_x] dest_cell = self.cells[d_y][d_x] obj = getattr(source_cell, type) + check = getattr(dest_cell, type) - if obj is None: + if obj is None or check is not None: return False setattr(source_cell, type, None) @@ -287,6 +292,11 @@ class Engine: gc.collect() mem_before = process.memory_info().rss / 1024 + + NUM_ELVES = 1000 + elf_count = 0 + + while running: time_delta = clock.tick(60)/1000.0 #pygame.event.clear() @@ -301,6 +311,19 @@ class Engine: if event.type == pygame.QUIT: running = False + if event.type == pygame.MOUSEWHEEL: + scroll_y = event.y + + if scroll_y > 0: + easy_map.scale += self.scale_step * 5 + self.spr_scale += self.scale_step * 5 + self.scale_sprites() + elif scroll_y < 0 and easy_map.scale >= self.scale_step: + easy_map.scale -= self.scale_step * 5 + self.spr_scale -= self.scale_step * 5 + self.scale_sprites() + + if event.type == pygame_gui.UI_TEXT_ENTRY_FINISHED and event.ui_element == input_entry: user_text = input_entry.get_text() exec(user_text) @@ -349,15 +372,44 @@ class Engine: if keys[pygame.K_d]: easy_map.cam_x -= self.camera_step - if keys[pygame.K_q]: - easy_map.scale += self.scale_step - self.spr_scale += self.scale_step - self.scale_sprites() - if keys[pygame.K_e] and easy_map.scale >= self.scale_step: - easy_map.scale -= self.scale_step - self.spr_scale -= self.scale_step - self.scale_sprites() + #if keys[pygame.K_q]: + # easy_map.scale += self.scale_step + # self.spr_scale += self.scale_step + # self.scale_sprites() + #if keys[pygame.K_e] and easy_map.scale >= self.scale_step: + # easy_map.scale -= self.scale_step + # self.spr_scale -= self.scale_step + # self.scale_sprites() + + if keys[pygame.K_SPACE] and elf_count < NUM_ELVES: + # Находим свободные клетки + # ★ МАССОВЫЙ СПАВН 50 эльфов ★ + free_cells = [] + for j in range(len(easy_map.cells)): # ★ ЛИМИТ строк ★ + for i in range(len(easy_map.cells[j])): # ★ ЛИМИТ колонок ★ + cell = easy_map.cells[j][i] + if (cell.creature_obj is None and + cell.terrain_obj and + cell.terrain_obj.sprite_name == "grass_small"): + free_cells.append((j, i)) + + spawn_count = min(50, len(free_cells)) # ★ Не больше свободных клеток ★ + for _ in range(spawn_count): + row, col = random.choice(free_cells) + elf = eb_objects.Creature(id=f"elf_{random.randint(1000,9999)}", name="Elf", sprite_name="elf_watching") + easy_map.cells[row][col].creature_obj = elf + + # ★ БЕЗОПАСНЫЙ выбор цели ★ + possible_targets = [c for c in free_cells if c != (row, col)] + if possible_targets: + target_cell = random.choice(possible_targets) + elf.move(easy_map.cells, (row, col), target_cell) + + free_cells.remove((row, col)) # Убираем занятые клетки + + + if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: mouse_pos = pygame.mouse.get_pos() console_rect = console_window.get_abs_rect() diff --git a/eb_objects.py b/eb_objects.py index db98d10..c24e56c 100644 --- a/eb_objects.py +++ b/eb_objects.py @@ -38,13 +38,28 @@ class Creature(Object): move_progress: float = 0.0 # 0.0 = старт клетки, 1.0 = конец клетки current_target: tuple = None # (row, col) следующая клетка + final_goal: tuple = None move_speed: float = 0.02 # пикселей/кадр (настройте) render_offset: tuple = (0.0, 0.0) start_pos: tuple = None # (row, col) начальная позиция сегмента пути + replan_counter: int = 0 + REPLAN_INTERVAL: int = 300 + + + def replan(self, cells, pos): + from common import find_way + path = find_way(cells, pos, self.final_goal) + if path and len(path) > 1: + self.waypoints = path[1:] + self.current_target = self.waypoints[0] + else: + self.waypoints.clear() + self.current_target = None def move(self, cells, start, goal): from common import find_way + self.final_goal = goal self.start_pos = start path = find_way(cells, start, goal) if path and len(path) > 1: @@ -59,6 +74,24 @@ class Creature(Object): if self.current_target is None or not self.waypoints: self.render_offset = (0.0, 0.0) return + + self.replan_counter += 1 + if self.replan_counter >= self.REPLAN_INTERVAL: + self.replan_counter = 0 + self.replan(map_obj.cells, self.start_pos) + + + if self.current_target is None: return + target_row, target_col = self.current_target + if (target_row in map_obj.cells and + target_col < len(map_obj.cells[target_row])): + target_cell = map_obj.cells[target_row][target_col] + if target_cell.creature_obj is not None: + self.current_target = None + self.waypoints.clear() + self.render_offset = (0.0, 0.0) + self.replan(map_obj.cells, self.start_pos) + return self.move_progress += self.move_speed * time_delta * 60 self.move_progress = min(self.move_progress, 1.0) @@ -66,7 +99,7 @@ class Creature(Object): if self.move_progress >= 1.0: map_obj.move_obj('creature_obj', self.start_pos, self.current_target) - self.waypoints.pop(0) + if self.waypoints: self.waypoints.pop(0) if self.waypoints: self.start_pos = self.current_target # Новая клетка как старт self.current_target = self.waypoints[0] @@ -79,7 +112,10 @@ class Creature(Object): # ★ ТОЛЬКО интерполяция offset ★ start_row, start_col = self.start_pos #or (0, 0) - target_row, target_col = self.current_target + if self.current_target is None: + self.render_offset = (0.0, 0.0) + return + target_row, target_col = self.current_target offset_x = (target_col - start_col) * cell_size * self.move_progress offset_y = (target_row - start_row) * cell_size * self.move_progress self.render_offset = (offset_x, offset_y) diff --git a/main.py b/main.py index e9d4166..4b0d6d7 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,18 @@ import eb_engine +#import os +#os.environ['PYTHONJIT'] = '1' def main(): e = eb_engine.Engine() e.main_loop() if __name__ == "__main__": - main() \ No newline at end of file + main() + # todo: + # прокрутка баг консоль и карта + # ОПТИМИЗАЦИИ + # очередь задач и задача рандомного патруля + # устроить краш тест поиску пути, запустив много объектов на маленьком поле, успел заметить баги + # добавить функцию движения за каким-то объектом + # сделать, чтобы в случае отменненого движения не телепортировался назад, а плавно + # приступаем к логике \ No newline at end of file diff --git a/problems.txt b/problems.txt index 6721b95..d95cc90 100644 --- a/problems.txt +++ b/problems.txt @@ -50,8 +50,7 @@ scale_image() вызывается 150×150=22,500 раз в секунду пр # проверить у ллм на ошибки - РЕГУЛЯРНАЯ АКТИВНОСТЬ: # - deepcopy + # - общие + - # !!! ДОБАВИТЬ ПРОКРУТКУ И МАСШТАБ КАРТЫ ДЛЯ МЫШИ !!! - # + # # ДОДЕЛАТЬ move для Creature - хранить pos в объекте ??? # # ПРОВЕРИТЬ МЕНЯЕТСЯ ЛИ ПЕРЕДАННЫЙ В ОБЪЕКТ cells и если да,