Compare commits

...

6 Commits

Author SHA1 Message Date
shiva404
b1548ea182 Сделал ещё одну очевидную оптимизацию - кэширование матриц препятствий, результат превзошёл всё ожидания. 300 объектов - плавные 60 фпс, 500 объектов - 60 фпс с микрофризами. Для масштабов игры этого уже более чем достаточно. Теперь следует добиться такого же для более слабых машин, надо выбрать минимальные системные требования. 2026-03-06 04:00:43 +03:00
shiva404
fa189a4c3b Попробовал самую очевидную оптимизацию рендера, теперь отрисовываются только те клетки, которые в камере. Карта корраптится при движении камеры, надо исправить, зато масштаб работает и прирост производительности очень хороший, теперь до 200 объектов обрабатывается при стабильных 60 ФПС. Промежуточный коммит, хочу посмотреть сколько можно ещё выжать кадров на 500 объектах, потом нужно починить движение камеры. Также написал простую функцию спавна эльфов в главном цикле для создания нагрузки. Эльфы создаются в углах карты и по центру каждые сто тактов цикла. Они начинают перемещаться по карте в случайные точки, создавая относительно равномерную нагрузку поиска пути. 2026-03-05 22:00:32 +03:00
shiva404
45f2c71cb8 Добавил систему задач для юнитов, она реализована в методе update класса Creature. Подчистил код, пофиксил по мелочам баги. Остался ещё техдолг Егору и задачи из main. 2026-03-05 16:35:08 +03:00
Your Name
dafa95989f add requirements.txt (pip freeze); readme 2026-02-24 16:03:29 +03:00
Your Name
8834ac997f add .gitignore & rm cache files 2026-02-24 15:54:39 +03:00
shiva404
6b6ca341dc Добавил ускоренный A* из библиотеки pathfinding - произволительность выросла, но несильно. Пока этот вариант закомментил, сейчас реализация BFS + walkable матрица, работает гораздо лучше, с неоптимальным рендером 100 объектов держит, без рендера 300. 2026-02-24 02:03:22 +03:00
22 changed files with 1147 additions and 141 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
__pycache__/
.venv/

19
README.md Normal file
View File

@@ -0,0 +1,19 @@
# ElvenBane
## Запуск с virtualenv
#### Linux/WSL:
```
virtualenv .venv
source .venv/bin/activate
```
#### PowerShell:
```
python -m venv .venv
.venv\Scripts\Activate.ps1
```
#### Общее:
```
pip install -r requirements.txt
```

Binary file not shown.

Binary file not shown.

Binary file not shown.

483
common.py
View File

@@ -1,17 +1,22 @@
import os import os
import json import json
import uuid import uuid
import random
from dataclasses import dataclass, field from dataclasses import dataclass, field
from copy import deepcopy from copy import deepcopy
from functools import partial
from collections import defaultdict
from heapq import heappush, heappop
import pygame import pygame
import pygame_gui import pygame_gui
from classes import Rock 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): def path_exists(data, path):
current = data current = data
for key in path: for key in path:
@@ -21,6 +26,12 @@ def path_exists(data, path):
return False return False
return True return True
def find_way(cells, start, goal, walkable, rocks_only):
result = bfs_quick(cells, start, goal, walkable, rocks_only)
return result
'''
#def find_way(cells, start, goal): #def find_way(cells, start, goal):
# """Находит путь от start=(row, col) к goal=(row, col). row=y (строка), col=x (столбец)""" # """Находит путь от start=(row, col) к goal=(row, col). row=y (строка), col=x (столбец)"""
# rows = len(cells) # rows = len(cells)
@@ -146,87 +157,421 @@ def path_exists(data, path):
# print("Путь не найден (нет связи)") # print("Путь не найден (нет связи)")
# return None # return None
def find_way(cells, start, goal): #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 bfs_quick(cells, start, goal, walkable, rocks_only):
"""★СУПЕРБЫСТРЫЙ BFS: массивы вместо set/deque★"""
rows = len(cells) rows = len(cells)
if rows == 0: if rows == 0:
print("Путь не найден: пустая карта")
return None return None
cols = len(cells[0]) 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 s_row, s_col = start
g_row, g_col = goal 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 return None
start_cell = cells[s_row][s_col] ## ★ МАТРИЦЫ вместо set (10x быстрее хэширования) ★
goal_cell = cells[g_row][g_col] #walkable = [[True] * cols for _ in range(rows)]
if not is_passable(start_cell) or not is_passable(goal_cell): #rocks_only = [[False] * cols for _ in range(rows)]
print(f"Старт/гол непроходимы") #start_creature = cells[s_row][s_col].creature_obj
return None #
#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
directions = [ # ★ ВЫЧИСЛЯЕМЫЕ МАССИВЫ ★
(-1, 0), (1, 0), (0, -1), (0, 1), # ортогональ (1.0) visited = [[False] * cols for _ in range(rows)]
(-1, -1), (-1, 1), (1, -1), (1, 1) # диагональ (1.414) parent = [[None] * cols for _ in range(rows)]
]
def can_move_diagonal(current_r, current_c, dr, dc): # ★ БЫСТРАЯ ОЧЕРЕДЬ: индекс вместо deque ★
"""Проверяет, можно ли двигаться по диагонали (НЕ через угол)""" queue_size = 0
nr, nc = current_r + dr, current_c + dc 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
# Ортогональные соседи ДОЛЖНЫ быть проходимыми для диагонального хода if r == g_row and c == g_col:
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 = [] path = []
current = (row, col) cr, cc = g_row, g_col
while current in came_from: while True:
path.append(current) path.append((cr, cc))
current = came_from[current] if parent[cr][cc] is None:
path.append(start) break
pr, pc = parent[cr][cc]
cr, cc = pr, pc
return path[::-1] return path[::-1]
for dr, dc in directions: for dr, dc in directions:
nr, nc = row + dr, col + dc nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and is_passable(cells[nr][nc]):
if (0 <= nr < rows and 0 <= nc < cols and
walkable[nr][nc] and not visited[nr][nc]):
# ★ ПРОВЕРКА ДИАГОНАЛИ ★ # ★ ПРОВЕРКА ДИАГОНАЛИ ★
if abs(dr) + abs(dc) == 2: # диагональное движение if dr * dc == 0 or can_move_diagonal(r, c, nr, nc, rocks_only, rows, cols):
if not can_move_diagonal(row, col, dr, dc): visited[nr][nc] = True
continue # пропускаем запрещённую диагональ parent[nr][nc] = (r, c)
move_cost = 1.414 if abs(dr) + abs(dc) == 2 else 1.0 # ★ БЫСТРОЕ добавление в очередь ★
tentative_g = g_score[(row, col)] + move_cost queue[queue_size] = [nr, nc]
pos = (nr, nc) queue_size += 1
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("Путь не найден") #print(f"Путь не найден: {start} -> {goal}")
return None 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
'''
#def bfs_quick_d(obstacle_matrix, start, goal):
# rows = len(obstacle_matrix)
# if rows == 0:
# print("❌ DEBUG: ПУСТАЯ МАТРИЦА")
# return None
#
# cols = len(obstacle_matrix[0])
# s_row, s_col = start
# g_row, g_col = goal
#
# print(f"🔍 DEBUG: start={start}, goal={goal}, размер={rows}x{cols}")
#
# if (s_row >= rows or s_col >= cols or
# g_row >= rows or g_col >= cols):
# print(f"❌ DEBUG: ВЫХОД ЗА ГРАНИЦЫ: start({s_row},{s_col}) goal({g_row},{g_col})")
# return None
#
# print(f"✅ DEBUG: Границы OK. obstacle[start]={obstacle_matrix[s_row][s_col]}, obstacle[goal]={obstacle_matrix[g_row][g_col]}")
#
# visited = [[False] * cols for _ in range(rows)]
# parent = [[None] * cols for _ in range(rows)]
#
# 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
# print(f"✅ DEBUG: Старт добавлен в очередь. queue_size={queue_size}")
#
# directions = [(-1,0), (1,0), (0,-1), (0,1), (-1,-1), (-1,1), (1,-1), (1,1)]
#
# DEBUG_COUNTER = 0
#
# while front < queue_size:
# r, c = queue[front]
# front += 1
# DEBUG_COUNTER += 1
#
# print(f"🔄 DEBUG[{DEBUG_COUNTER}]: обрабатываем ({r},{c}), очередь={front}/{queue_size}")
#
# if r == g_row and c == g_col:
# print(f"✅ DEBUG: НАЙДЕН ЦЕЛЬ! Путь: ({r},{c})")
# path = []
# 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 = r + dr, c + dc
#
# print(f" 📍 Проверяем соседа ({nr},{nc}): граница={0<=nr<rows and 0<=nc<cols}, "
# f"visited={visited[nr][nc]}, obstacle={obstacle_matrix[nr][nc]}")
#
# if (0 <= nr < rows and 0 <= nc < cols and
# not visited[nr][nc] and
# not obstacle_matrix[nr][nc]):
#
# diagonal_ok = True
# if dr * dc != 0:
# diagonal_ok = can_move_diagonal(r, c, nr, nc, obstacle_matrix)
# print(f" ↘️ Диагональ: {diagonal_ok}")
#
# if diagonal_ok:
# visited[nr][nc] = True
# parent[nr][nc] = (r, c)
# queue[queue_size] = [nr, nc]
# queue_size += 1
# print(f" ✅ Добавили ({nr},{nc}) в очередь. queue_size={queue_size}")
# else:
# print(f" ❌ Диагональ заблокирована!")
# else:
# print(f" ❌ Сосед отклонен!")
#
# print(f"❌ DEBUG: ОЧЕРЕДЬ ОПУСТЕЛА! Обработано {DEBUG_COUNTER} узлов")
# print(f" Последняя клетка в очереди: {queue[front-1] if front > 0 else 'ПУСТО'}")
# print(f" Цель ({g_row},{g_col}) помечена как visited? {visited[g_row][g_col]}")
# return None
#
#
#def bfs_quick(obstacle_matrix, start, goal):
# """★СУПЕРБЫСТРЫЙ BFS 8-направлений★
# obstacle_matrix - ТОЛЬКО камни
# """
# rows = len(obstacle_matrix)
# if rows == 0:
# return None
#
# cols = len(obstacle_matrix[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):
# return None
#
# # ★ МАТРИЦЫ состояния ★
# visited = [[False] * cols for _ in range(rows)]
# parent = [[None] * cols for _ in range(rows)]
#
# # ★ БЫСТРАЯ ОЧЕРЕДЬ ★
# 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
#
# # ★ 8 НАПРАВЛЕНИЙ ★
# 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
#
# if r == g_row and c == g_col:
# # ★ Реконструкция пути ★
# path = []
# 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 = r + dr, c + dc
#
# if not (0 <= nr < rows and 0 <= nc < cols):
# continue
# if visited[nr][nc] or obstacle_matrix[nr][nc]:
# continue
#
# diagonal_ok = True
# if dr * dc != 0:
# diagonal_ok = can_move_diagonal(r, c, nr, nc, obstacle_matrix)
# if diagonal_ok:
# visited[nr][nc] = True
# parent[nr][nc] = (r, c)
# queue[queue_size] = [nr, nc]
# queue_size += 1
#
# return None
'''
#def can_move_diagonal(r, c, nr, nc, obstacle_matrix):
# """Диагональ БЛОКИРУЕТСЯ только камнями по углам"""
# rows, cols = len(obstacle_matrix), len(obstacle_matrix[0])
# 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 obstacle_matrix[r1][c1])
# check2_ok = (0 <= r2 < rows and 0 <= c2 < cols and not obstacle_matrix[r2][c2])
#
# return check1_ok and check2_ok
#def can_move_diagonal(r, c, nr, nc, obstacle_matrix):
# """Проверка диагонали с границами"""
# rows, cols = len(obstacle_matrix), len(obstacle_matrix[0])
# dr = nr - r
# dc = nc - c
#
# # ★ ПРОВЕРКА ГРАНИЦ ПОПЕРЕДУ ★
# r1, c1 = r + dr, c # вертикальная
# r2, c2 = r, c + dc # горизонтальная
#
# # Если УЖЕ за границей - False
# if not (0 <= r1 < rows and 0 <= c1 < cols):
# return False
# if not (0 <= r2 < rows and 0 <= c2 < cols):
# return False
#
# return not obstacle_matrix[r1][c1] and not obstacle_matrix[r2][c2]

View File

@@ -11,7 +11,9 @@
"name": "2", "name": "2",
"sprite_name": "sword_default" "sprite_name": "sword_default"
}, },
"creature_obj": {} "creature_obj": {"id": "1",
"name": "2",
"sprite_name": "elf_watching"}
}, },
{ {
"terrain_obj": { "terrain_obj": {
@@ -20,17 +22,15 @@
"sprite_name": "grass_small" "sprite_name": "grass_small"
}, },
"item_obj": {}, "item_obj": {},
"creature_obj": { "creature_obj": {"id": "1",
"id": "1",
"name": "2", "name": "2",
"sprite_name": "elf_watching" "sprite_name": "elf_watching"}
}
}, },
{ {
"terrain_obj": { "terrain_obj": {
"id": "1", "id": "1",
"name": "2", "name": "2",
"sprite_name": "rock_small" "sprite_name": "grass_small"
}, },
"item_obj": {}, "item_obj": {},
"creature_obj": {} "creature_obj": {}
@@ -932,7 +932,7 @@
"terrain_obj": { "terrain_obj": {
"id": "1", "id": "1",
"name": "2", "name": "2",
"sprite_name": "rock_small" "sprite_name": "grass_small"
}, },
"item_obj": {}, "item_obj": {},
"creature_obj": {} "creature_obj": {}
@@ -950,7 +950,7 @@
"terrain_obj": { "terrain_obj": {
"id": "1", "id": "1",
"name": "2", "name": "2",
"sprite_name": "rock_small" "sprite_name": "grass_small"
}, },
"item_obj": {}, "item_obj": {},
"creature_obj": {} "creature_obj": {}
@@ -1843,7 +1843,7 @@
"terrain_obj": { "terrain_obj": {
"id": "1", "id": "1",
"name": "2", "name": "2",
"sprite_name": "rock_small" "sprite_name": "grass_small"
}, },
"item_obj": {}, "item_obj": {},
"creature_obj": {} "creature_obj": {}
@@ -1852,7 +1852,7 @@
"terrain_obj": { "terrain_obj": {
"id": "1", "id": "1",
"name": "2", "name": "2",
"sprite_name": "rock_small" "sprite_name": "grass_small"
}, },
"item_obj": {}, "item_obj": {},
"creature_obj": {} "creature_obj": {}

View File

@@ -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, partial
from common import pygame, pygame_gui from common import pygame, pygame_gui
import eb_objects import eb_objects
import eb_terrain_objects import eb_terrain_objects
@@ -37,6 +38,8 @@ class Map:
sprites: dict sprites: dict
sprites_refresh: int = 60 sprites_refresh: int = 60
cells: dict = field(default_factory = dict) cells: dict = field(default_factory = dict)
walkable_matrix: list = field(default_factory = list)
rocks_matrix: list = field(default_factory = list)
color: str = "gray57" color: str = "gray57"
target_color: str = "gold" target_color: str = "gold"
cell_size: int = 150 cell_size: int = 150
@@ -45,7 +48,8 @@ class Map:
cam_x: int = 0 cam_x: int = 0
cam_y: int = 0 cam_y: int = 0
cell_dist: int = 1 cell_dist: int = 1
#effects[]
#action_time_multiplier #action_time_multiplier
def __post_init__(self): def __post_init__(self):
@@ -54,7 +58,7 @@ class Map:
buff = json.load(file) buff = json.load(file)
for line in range(len(buff)): for line in range(len(buff)):
self.cells[line] = [] self.cells[line] = []
for cell in buff[str(line)]: for col, cell in enumerate(buff[str(line)]):
final_cell = Cell(cell_classes[cell["terrain_obj"]["sprite_name"]](**cell["terrain_obj"])) final_cell = Cell(cell_classes[cell["terrain_obj"]["sprite_name"]](**cell["terrain_obj"]))
if cell["item_obj"]: if cell["item_obj"]:
@@ -62,12 +66,25 @@ class Map:
if cell["creature_obj"]: if cell["creature_obj"]:
final_cell.creature_obj = cell_classes[cell["creature_obj"]["sprite_name"]](**cell["creature_obj"]) final_cell.creature_obj = cell_classes[cell["creature_obj"]["sprite_name"]](**cell["creature_obj"])
final_cell.creature_obj.grid_pos = (line, col)
self.cells[line].append(final_cell)
self.compute_walkable_rocks()
for j in range(len(self.cells)):
for cell in self.cells[j]:
if cell.creature_obj:
cell.creature_obj.walkable_matrix = self.walkable_matrix
cell.creature_obj.rocks_matrix = self.rocks_matrix
self.cells[line].append(final_cell)
def move_obj(self, type, start, goal): def move_obj(self, type, start, goal):
"""Перемещает объект типа 'terrain_obj', 'item_obj' или 'creature_obj' """Перемещает объект типа 'terrain_obj', 'item_obj' или 'creature_obj'
из клетки start=(row, col) в goal=(row, col)""" из клетки start=(row, col) в goal=(row, col)"""
if goal is None:
return False
s_y, s_x = start s_y, s_x = start
d_y, d_x = goal d_y, d_x = goal
@@ -79,12 +96,14 @@ class Map:
source_cell = self.cells[s_y][s_x] source_cell = self.cells[s_y][s_x]
dest_cell = self.cells[d_y][d_x] dest_cell = self.cells[d_y][d_x]
obj = getattr(source_cell, type) 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 return False
setattr(source_cell, type, None) setattr(source_cell, type, None)
setattr(dest_cell, type, obj) setattr(dest_cell, type, obj)
#obj.grid_pos = goal
return True return True
def get_cell_at_mouse(self, mouse_pos): def get_cell_at_mouse(self, mouse_pos):
@@ -106,18 +125,109 @@ class Map:
return (row, col) return (row, col)
def update_map(self, time_delta): def update_map(self, time_delta):
self.compute_walkable_rocks()
for j in range(len(self.cells)): for j in range(len(self.cells)):
for cell in self.cells[j]: for cell in self.cells[j]:
if cell.creature_obj and cell.creature_obj.current_target: if cell.creature_obj:
cell.creature_obj.update(time_delta, self.cell_size, self) # self! cell.creature_obj.update(time_delta, self.cell_size, self)
def compute_walkable_rocks(self):
"""Вычисляет матрицы walkable и rocks_only БЕЗ учета стартовой позиции"""
rows = len(self.cells)
if rows == 0:
return None, None
cols = len(self.cells[0])
# ★ ИНИЦИАЛИЗАЦИЯ ★
self.walkable_matrix = [[True] * cols for _ in range(rows)]
self.rocks_matrix = [[False] * cols for _ in range(rows)]
# ★ ПОЛНЫЙ ПРОХОД ПО КАРТЕ
for r in range(rows):
for c in range(cols):
cell = self.cells[r][c]
# Запрещаем ВСЕ существа (включая стартовое!)
if cell.creature_obj or \
(cell.terrain_obj and cell.terrain_obj.sprite_name == "rock_small"):
self.walkable_matrix[r][c] = False
# Отмечаем маленькие камни
if cell.terrain_obj and cell.terrain_obj.sprite_name == "rock_small":
self.rocks_matrix[r][c] = True
# def draw_map(self, screen, current_frame, grid=True):
# terrain_list = []
# creature_list = []
#
# # ★ 1 ПАСС: собираем terrain и creatures ★
# for j in range(len(self.cells)):
# for i, cell in enumerate(self.cells[j]):
# base_x = i * self.cell_size + self.cam_x
# base_y = j * self.cell_size + self.cam_y
#
# # Terrain данные
# terrain_dd = {
# "x": int(base_x * self.scale), "y": int(base_y * self.scale),
# "w": int(self.cell_size * self.scale - self.cell_dist),
# "h": int(self.cell_size * self.scale - self.cell_dist),
# "spr_up": current_frame % self.sprites_refresh,
# "sprites": self.sprites, "scale": self.scale, "screen": screen
# }
# terrain_list.append((cell.terrain_obj, terrain_dd))
#
# # Creature данные (если есть)
# if cell.creature_obj:
# offset_x, offset_y = cell.creature_obj.render_offset
# creature_dd = terrain_dd.copy()
# creature_dd["x"] = int((base_x + offset_x) * self.scale)
# creature_dd["y"] = int((base_y + offset_y) * self.scale)
# creature_list.append((cell.creature_obj, creature_dd))
#
# # ★ 2 ПАСС: рисуем terrain ★
# for obj, dd in terrain_list:
# obj.draw(dd)
#
# # ★ 3 ПАСС: рисуем ВСЕХ creatures ПОСЛЕ terrain ★
# for obj, dd in creature_list:
# obj.draw(dd)
#
# # ★ 4 ПАСС: grid поверх всего ★
# for j in range(len(self.cells)):
# for i, cell in enumerate(self.cells[j]):
# base_x = i * self.cell_size + self.cam_x
# base_y = j * self.cell_size + self.cam_y
# grid_rect = pygame.Rect(
# int(base_x * self.scale), int(base_y * self.scale),
# int(self.cell_size * self.scale - self.cell_dist),
# int(self.cell_size * self.scale - self.cell_dist)
# )
# color = self.target_color if cell.is_target else self.color
# pygame.draw.rect(screen, color, grid_rect, self.bord)
def draw_map(self, screen, current_frame, grid=True): def draw_map(self, screen, current_frame, grid=True):
screen_rect = screen.get_rect()
terrain_list = [] terrain_list = []
creature_list = [] creature_list = []
# ★ 1 ПАСС: собираем terrain и creatures ★ # Вычисляем видимую область в координатах карты (аналогично get_cell_at_mouse)
for j in range(len(self.cells)): left_map = (self.cam_x * self.scale) / self.scale / self.cell_size
for i, cell in enumerate(self.cells[j]): top_map = (self.cam_y * self.scale) / self.scale / self.cell_size
right_map = ((self.cam_x * self.scale + screen_rect.width) / self.scale / self.cell_size)
bottom_map = ((self.cam_y * self.scale + screen_rect.height) / self.scale / self.cell_size)
min_row = max(0, int(top_map - 1)) # -1 для буфера
max_row = min(len(self.cells), int(bottom_map + 2)) # +2 для буфера
min_col = max(0, int(left_map - 1))
max_col = min(len(self.cells[0]) if self.cells and self.cells[0] else 0, int(right_map + 2))
# ★ 1 ПАСС: собираем только видимые terrain и creatures ★
for j in range(min_row, max_row):
if j not in self.cells: continue
row_cells = self.cells[j]
for i in range(min_col, min(max_col, len(row_cells))):
cell = row_cells[i]
base_x = i * self.cell_size + self.cam_x base_x = i * self.cell_size + self.cam_x
base_y = j * self.cell_size + self.cam_y base_y = j * self.cell_size + self.cam_y
@@ -147,22 +257,23 @@ class Map:
for obj, dd in creature_list: for obj, dd in creature_list:
obj.draw(dd) obj.draw(dd)
# ★ 4 ПАСС: grid поверх всего # ★ 4 ПАСС: grid только для видимых ячеек
for j in range(len(self.cells)): for j in range(min_row, max_row):
for i, cell in enumerate(self.cells[j]): if j not in self.cells: continue
row_cells = self.cells[j]
for i in range(min_col, min(max_col, len(row_cells))):
cell = row_cells[i]
base_x = i * self.cell_size + self.cam_x base_x = i * self.cell_size + self.cam_x
base_y = j * self.cell_size + self.cam_y base_y = j * self.cell_size + self.cam_y
grid_rect = pygame.Rect( grid_rect = pygame.Rect(
int(base_x * self.scale), int(base_y * self.scale), int(base_x * self.scale), int(base_y * self.scale),
int(self.cell_size * self.scale - self.cell_dist), int(self.cell_size * self.scale - self.cell_dist),
int(self.cell_size * self.scale - self.cell_dist) int(self.cell_size * self.scale - self.cell_dist) # ИСПРАВЛЕНО
) )
color = self.target_color if cell.is_target else self.color color = self.target_color if cell.is_target else self.color
pygame.draw.rect(screen, color, grid_rect, self.bord) pygame.draw.rect(screen, color, grid_rect, self.bord)
@dataclass @dataclass
class Engine: class Engine:
sprites: dict = field(default_factory = dict) sprites: dict = field(default_factory = dict)
@@ -250,14 +361,6 @@ class Engine:
def main_loop(self): def main_loop(self):
easy_map = Map("def_map.json", self.cached_sprites) easy_map = Map("def_map.json", self.cached_sprites)
#print(easy_map.find_way((1, 1), (5, 5)))
#print(easy_map.find_way((0, 0), (1, 1)))
#print(easy_map.find_way((0, 0), (0, 1)))
#print(easy_map.find_way((0, 0), (0, 0)))
#sp = eb_objects.Sprite(self.sprites, "elf_watching")
#gr = pygame.image.load(file_path).convert_alpha()
background = pygame.Surface((1600, 800)) background = pygame.Surface((1600, 800))
background.fill("chartreuse4") background.fill("chartreuse4")
@@ -287,6 +390,32 @@ class Engine:
gc.collect() gc.collect()
mem_before = process.memory_info().rss / 1024 mem_before = process.memory_info().rss / 1024
NUM_ELVES = 2000
elf_count = 0
space_pressed = False
spawn = False
def spawn_elves():
spawn_positions = [(0,0), (0,99), (99,0), (99,99), (50,50)]
for pos in spawn_positions:
row, col = pos
if (row < len(easy_map.cells) and
col < len(easy_map.cells[row]) and
easy_map.cells[row][col].creature_obj is None):
elf = eb_objects.Creature(id=f"elf_{random.randint(1000,9999)}",
name="Elf", sprite_name="elf_watching",
grid_pos = pos,
walkable_matrix = easy_map.walkable_matrix,
rocks_matrix = easy_map.rocks_matrix)
r_move_short = eb_objects.Action(sprite_name="elf_watching", func=partial(elf.patrol, easy_map.cells, 3), duration=0.01)
r_move_long = eb_objects.Action(sprite_name="elf_watching", func=partial(elf.move_rand, easy_map.cells, 0, 99), duration=0.01)
elf.tasks.append([r_move_short]*5 + [r_move_long])
easy_map.cells[row][col].creature_obj = elf
while running: while running:
time_delta = clock.tick(60)/1000.0 time_delta = clock.tick(60)/1000.0
#pygame.event.clear() #pygame.event.clear()
@@ -295,12 +424,42 @@ class Engine:
mem_after = process.memory_info().rss / 1024 mem_after = process.memory_info().rss / 1024
print(f"Leak: {mem_after - mem_before:.1f} KB per 1000 frames") print(f"Leak: {mem_after - mem_before:.1f} KB per 1000 frames")
if global_counter % 100 == 0 and spawn == True:
spawn_elves()
elf_count += 5
# poll for events # poll for events
# pygame.QUIT event means the user clicked X to close your window # pygame.QUIT event means the user clicked X to close your window
for event in pygame.event.get(): for event in pygame.event.get():
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
running = False running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE and elf_count < NUM_ELVES and not space_pressed:
#spawn_elf()
#elf_count += 1
if spawn == False:
spawn = True
else: spawn = False
space_pressed = True
if event.type == pygame.KEYUP:
if event.key == pygame.K_SPACE:
space_pressed = 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: if event.type == pygame_gui.UI_TEXT_ENTRY_FINISHED and event.ui_element == input_entry:
user_text = input_entry.get_text() user_text = input_entry.get_text()
exec(user_text) exec(user_text)
@@ -357,7 +516,9 @@ class Engine:
easy_map.scale -= self.scale_step easy_map.scale -= self.scale_step
self.spr_scale -= self.scale_step self.spr_scale -= self.scale_step
self.scale_sprites() self.scale_sprites()
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
mouse_pos = pygame.mouse.get_pos() mouse_pos = pygame.mouse.get_pos()
console_rect = console_window.get_abs_rect() console_rect = console_window.get_abs_rect()
@@ -374,7 +535,6 @@ class Engine:
row, col = active_cell row, col = active_cell
easy_map.cells[row][col].is_target = True easy_map.cells[row][col].is_target = True
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 3: if event.type == pygame.MOUSEBUTTONDOWN and event.button == 3:
mouse_pos = pygame.mouse.get_pos() mouse_pos = pygame.mouse.get_pos()
console_rect = console_window.get_abs_rect() console_rect = console_window.get_abs_rect()
@@ -384,8 +544,7 @@ class Engine:
cell_coords = easy_map.get_cell_at_mouse(mouse_pos) cell_coords = easy_map.get_cell_at_mouse(mouse_pos)
if cell_coords: if cell_coords:
#print(f"Движение: {active_cell} -> {cell_coords}") #print(f"Движение: {active_cell} -> {cell_coords}")
easy_map.cells[active_cell[0]][active_cell[1]].creature_obj.move( easy_map.cells[active_cell[0]][active_cell[1]].creature_obj.move(easy_map.cells, cell_coords)
easy_map.cells, active_cell, cell_coords)
if keys[pygame.K_ESCAPE]: if keys[pygame.K_ESCAPE]:
running = False running = False
@@ -401,6 +560,6 @@ class Engine:
if global_counter % 10 == 0: if global_counter % 10 == 0:
current_fps = clock.get_fps() current_fps = clock.get_fps()
print(f"Current FPS: {current_fps:.2f}") print(f"Elves count: {elf_count} - Current FPS: {current_fps:.2f}")
pygame.quit() pygame.quit()

View File

@@ -1,4 +1,21 @@
from common import deepcopy, dataclass, field from common import deepcopy, dataclass, field, random
@dataclass
class Action:
sprite_name: str
func: function
duration: float
progress: float = 0.0
#status: str = "pending"
#взаимодействие с инвентарем
#взять предмет
#бросить предмет
#передать предмет
#собрать ресурс
#активное действие с оружием
#колдовство
#class Task
@dataclass @dataclass
class Object: class Object:
@@ -6,6 +23,7 @@ class Object:
name: str name: str
sprite_name: str sprite_name: str
sprite_state: int = 0 sprite_state: int = 0
grid_pos: tuple = None
# current_map # current_map
# pos # pos
# weight # weight
@@ -31,59 +49,224 @@ class Terrain(Object):
@dataclass @dataclass
class Creature(Object): class Creature(Object):
waypoints: list = field(default_factory = list) direction: int = 0 # tuple?
quick_actions: list = field(default_factory = list) move_progress: float = 0.0 # 0.0 = старт клетки, 1.0 = конец клетки
tasks: list = field(default_factory = list) current_target: tuple = None # (row, col) следующая клетка
inventory: dict = field(default_factory = dict) 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 = 30000
waypoints: list = field(default_factory = list)
walkable_matrix: list = field(default_factory = list)
rocks_matrix: list = field(default_factory = list)
move_progress: float = 0.0 # 0.0 = старт клетки, 1.0 = конец клетки inventory: dict = field(default_factory = dict)
current_target: tuple = None # (row, col) следующая клетка quick_actions: list = field(default_factory = list)
move_speed: float = 0.02 # пикселей/кадр (настройте) tasks: list = field(default_factory = list)
render_offset: tuple = (0.0, 0.0)
start_pos: tuple = None # (row, col) начальная позиция сегмента пути action: Action = None
action_time: float = 0.0
action_counter: int = 0
task_counter: int = 0
new_task: list = field(default_factory = list)
interrupt_task: list = field(default_factory = list)
interrupt_action_status: str = "completed"
def move(self, cells, start, goal):
def replan(self, cells, pos):
from common import find_way from common import find_way
self.start_pos = start path = find_way(cells, pos, self.final_goal, self.walkable_matrix, self.rocks_matrix)
path = find_way(cells, start, goal)
if path and len(path) > 1: if path and len(path) > 1:
self.waypoints = path[1:] # Убираем текущую позицию self.waypoints = path[1:]
self.current_target = self.waypoints[0] self.current_target = self.waypoints[0]
self.move_progress = 0.0 else:
self.start_pos = start # ★ ТУТ - текущая позиция как стартовая для первого шага ★ self.waypoints.clear()
self.render_offset = (0.0, 0.0) self.current_target = None
self.final_goal = None
def calc_step(self, time_delta, cell_size, map_obj):
def update(self, time_delta, cell_size, map_obj):
if self.current_target is None or not self.waypoints: if self.current_target is None or not self.waypoints:
self.render_offset = (0.0, 0.0) self.render_offset = (0.0, 0.0)
return 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 += self.move_speed * time_delta * 60
self.move_progress = min(self.move_progress, 1.0) self.move_progress = min(self.move_progress, 1.0)
if self.move_progress >= 1.0: if self.move_progress >= 1.0:
map_obj.move_obj('creature_obj', self.start_pos, self.current_target) if (map_obj.move_obj('creature_obj', self.start_pos, self.current_target)):
self.grid_pos = self.current_target
else:
self.current_target = None
self.waypoints.clear()
self.render_offset = (0.0, 0.0)
#если в матрице не считаем объекты, то:
#добавляем клетку в матрицу, матрицу периодически чистим
#посчитать как дорого обходится просчёт матрицы
self.replan(map_obj.cells, self.start_pos)
#тут сделать красивый переход в одну из соседних клеток
return
self.waypoints.pop(0) if self.waypoints: self.waypoints.pop(0)
if self.waypoints: if self.waypoints:
self.start_pos = self.current_target # Новая клетка как старт self.start_pos = self.current_target
self.current_target = self.waypoints[0] self.current_target = self.waypoints[0]
self.move_progress = 0.0 self.move_progress = 0.0
self.render_offset = (0.0, 0.0) # ← ДОБАВИТЬ! self.render_offset = (0.0, 0.0)
else: else:
#print(111111111111111)
self.current_target = None self.current_target = None
self.final_goal = None
self.render_offset = (0.0, 0.0) self.render_offset = (0.0, 0.0)
return return
# ★ ТОЛЬКО интерполяция offset ★ if self.current_target is None:
start_row, start_col = self.start_pos #or (0, 0) self.render_offset = (0.0, 0.0)
target_row, target_col = self.current_target return
start_row, start_col = self.start_pos
target_row, target_col = self.current_target
offset_x = (target_col - start_col) * cell_size * self.move_progress offset_x = (target_col - start_col) * cell_size * self.move_progress
offset_y = (target_row - start_row) * cell_size * self.move_progress offset_y = (target_row - start_row) * cell_size * self.move_progress
self.render_offset = (offset_x, offset_y) self.render_offset = (offset_x, offset_y)
def move(self, cells, goal):
from common import find_way
self.final_goal = goal
path = find_way(cells, self.grid_pos, self.final_goal, self.walkable_matrix, self.rocks_matrix)
if path and len(path) > 1:
self.waypoints = path[1:]
self.current_target = self.waypoints[0]
self.move_progress = 0.0
self.start_pos = self.grid_pos
self.render_offset = (0.0, 0.0)
else:
self.final_goal = None
def create_task(self):
self.new_task = []
#add actions_default_durations dict {func1: dur1, ...}
def add_action(self, sprite_name: str, func: function, duration: float):
self.new_task.append(Action(sprite_name, func, duration))
def add_task(self):
self.tasks.append(self.new_task)
def add_interr_task(self):
pass
#param resume_on_interrupt, Bool
#if True
# save goal to buffer
#if False
# action/task_counter = 0
#clear goal/wp
def update(self, time_delta, cell_size, map_obj):
self.walkable_matrix = map_obj.walkable_matrix
self.rocks_matrix = map_obj.rocks_matrix
#quick_actions? here?
#print(self.waypoints, self.final_goal, self.action_counter, self.task_counter)
if self.final_goal is not None:
#print(2)
self.calc_step(time_delta, cell_size, map_obj)
return
if self.interrupt_task and self.interrupt_action_status == "completed":
#print(3)
self.action_time = 0.0
self.action = self.interrupt_task.pop(0)
self.interrupt_action_status = "active"
#print(f" DEBUG: tasks={len(self.tasks)}, "
# f"task_len={len(self.tasks[self.task_counter]) if self.tasks else 0}, "
# f"action={self.action is not None}")
if self.action:
#print(self.action_counter, self.task_counter)
self.action_time += time_delta
self.action.progress = min(1.0, self.action_time / self.action.duration)
if self.action_time >= self.action.duration:
self.action.func()
if self.interrupt_action_status == "active":
self.interrupt_action_status == "completed"
#if not self.inter_task and goal: move to buff_goal from self.pos (add to Object)
self.action = None
self.action_time = 0.0
elif self.tasks:
#print(6)
if self.action_counter < len(self.tasks[self.task_counter]):
self.action = self.tasks[self.task_counter][self.action_counter]
self.action_counter += 1
else:
self.task_counter += 1
self.action_counter = 0
if self.task_counter == len(self.tasks):
self.task_counter = 0
self.action = self.tasks[self.task_counter][self.action_counter]
self.action_counter += 1
self.action_time = 0.0
#elif self.tasks:
# if self.action_counter >= len(self.tasks[self.task_counter]):
# self.task_counter += 1
# self.action_counter = 0
# if self.task_counter >= len(self.tasks):
# self.task_counter = 0
#
# self.action = self.tasks[self.task_counter][self.action_counter]
# self.action_counter += 1
# self.action_time = 0.0
def patrol(self, cells, area):
goal = (random.randint(self.grid_pos[0] - area,
self.grid_pos[0] + area),
random.randint(self.grid_pos[1] - area,
self.grid_pos[1] + area))
while goal == self.grid_pos:
goal = (random.randint(self.grid_pos[0] - area,
self.grid_pos[0] + area),
random.randint(self.grid_pos[1] - area,
self.grid_pos[1] + area))
self.move(cells, goal)
def move_rand(self, cells, area_start, area_end):
goal = (random.randint(area_start, area_end), random.randint(area_start, area_end))
while goal == self.grid_pos:
goal = (random.randint(area_start, area_end), random.randint(area_start, area_end))
self.move(cells, goal)
@dataclass @dataclass
class Item(Object): class Item(Object):
@@ -96,7 +279,6 @@ class Container(Item):
# content = {} # content = {}
pass pass
@dataclass @dataclass
class Building(Object): class Building(Object):
pass pass

231
f.txt Normal file
View File

@@ -0,0 +1,231 @@
Elves count: 500 - Current FPS: 58.82
Elves count: 500 - Current FPS: 54.35
Elves count: 500 - Current FPS: 57.14
Elves count: 500 - Current FPS: 55.56
Elves count: 500 - Current FPS: 58.14
Elves count: 500 - Current FPS: 54.95
Elves count: 500 - Current FPS: 61.35
Elves count: 500 - Current FPS: 58.14
Elves count: 500 - Current FPS: 61.73
Elves count: 500 - Current FPS: 45.87
Elves count: 500 - Current FPS: 54.35
Elves count: 500 - Current FPS: 58.48
Elves count: 500 - Current FPS: 59.52
Elves count: 500 - Current FPS: 58.48
Elves count: 500 - Current FPS: 50.51
Elves count: 500 - Current FPS: 55.25
Elves count: 500 - Current FPS: 52.36
Elves count: 500 - Current FPS: 53.76
Elves count: 500 - Current FPS: 52.36
Elves count: 500 - Current FPS: 49.02
Elves count: 500 - Current FPS: 61.73
Elves count: 500 - Current FPS: 57.80
Elves count: 500 - Current FPS: 59.52
Elves count: 500 - Current FPS: 54.64
Elves count: 500 - Current FPS: 54.95
Elves count: 500 - Current FPS: 59.52
Elves count: 500 - Current FPS: 59.52
Elves count: 500 - Current FPS: 60.24
Elves count: 500 - Current FPS: 61.73
Elves count: 500 - Current FPS: 44.05
Elves count: 500 - Current FPS: 58.82
Elves count: 500 - Current FPS: 54.05
Elves count: 500 - Current FPS: 55.56
Elves count: 500 - Current FPS: 58.82
Elves count: 500 - Current FPS: 49.50
Elves count: 500 - Current FPS: 56.18
Elves count: 500 - Current FPS: 58.82
Elves count: 500 - Current FPS: 57.47
Elves count: 500 - Current FPS: 34.13
Elves count: 500 - Current FPS: 48.54
Elves count: 500 - Current FPS: 54.64
Elves count: 500 - Current FPS: 60.24
Elves count: 500 - Current FPS: 57.14
Elves count: 500 - Current FPS: 56.50
Elves count: 500 - Current FPS: 50.00
Elves count: 500 - Current FPS: 60.98
Elves count: 500 - Current FPS: 59.88
Elves count: 500 - Current FPS: 58.82
Elves count: 500 - Current FPS: 43.10
Leak: 2232.0 KB per 1000 frames
Elves count: 500 - Current FPS: 50.25
Elves count: 500 - Current FPS: 41.67
Elves count: 500 - Current FPS: 57.80
Elves count: 500 - Current FPS: 45.25
Elves count: 500 - Current FPS: 49.50
Elves count: 500 - Current FPS: 58.48
Elves count: 500 - Current FPS: 56.18
Elves count: 500 - Current FPS: 52.63
Elves count: 500 - Current FPS: 49.02
Elves count: 500 - Current FPS: 57.47
Elves count: 500 - Current FPS: 55.56
Elves count: 500 - Current FPS: 56.18
Elves count: 500 - Current FPS: 55.56
Elves count: 500 - Current FPS: 59.52
Elves count: 500 - Current FPS: 60.24
Elves count: 500 - Current FPS: 57.47
Elves count: 500 - Current FPS: 60.61
Elves count: 500 - Current FPS: 60.61
Elves count: 500 - Current FPS: 46.30
Elves count: 500 - Current FPS: 58.82
Elves count: 500 - Current FPS: 59.52
Elves count: 500 - Current FPS: 54.35
Elves count: 500 - Current FPS: 59.17
Elves count: 500 - Current FPS: 54.95
Elves count: 500 - Current FPS: 54.64
Elves count: 500 - Current FPS: 48.08
Elves count: 500 - Current FPS: 41.49
Elves count: 500 - Current FPS: 54.35
Elves count: 500 - Current FPS: 56.50
Elves count: 500 - Current FPS: 60.24
Elves count: 500 - Current FPS: 61.35
Elves count: 500 - Current FPS: 59.17
Elves count: 500 - Current FPS: 50.25
Elves count: 500 - Current FPS: 60.98
Elves count: 500 - Current FPS: 52.63
Elves count: 500 - Current FPS: 57.80
Elves count: 500 - Current FPS: 54.05
Elves count: 500 - Current FPS: 51.28
Elves count: 500 - Current FPS: 52.08
Elves count: 500 - Current FPS: 59.52
Elves count: 500 - Current FPS: 61.73
Elves count: 500 - Current FPS: 54.05
Elves count: 500 - Current FPS: 59.17
Elves count: 500 - Current FPS: 54.64
Elves count: 500 - Current FPS: 50.51
Elves count: 500 - Current FPS: 51.28
Elves count: 500 - Current FPS: 59.52
Elves count: 500 - Current FPS: 59.52
Elves count: 500 - Current FPS: 60.61
Elves count: 500 - Current FPS: 51.28
Elves count: 500 - Current FPS: 60.98
Elves count: 500 - Current FPS: 57.47
Elves count: 500 - Current FPS: 52.63
Elves count: 500 - Current FPS: 50.00
Elves count: 500 - Current FPS: 59.88
Elves count: 500 - Current FPS: 54.95
Elves count: 500 - Current FPS: 58.14
Elves count: 500 - Current FPS: 45.45
Elves count: 500 - Current FPS: 52.91
Elves count: 500 - Current FPS: 32.57
Elves count: 500 - Current FPS: 29.07
Elves count: 500 - Current FPS: 45.25
Elves count: 500 - Current FPS: 30.03
Elves count: 500 - Current FPS: 45.66
Elves count: 500 - Current FPS: 43.86
Elves count: 500 - Current FPS: 38.91
Elves count: 500 - Current FPS: 35.97
Elves count: 500 - Current FPS: 34.01
Elves count: 500 - Current FPS: 44.84
Elves count: 500 - Current FPS: 36.63
Elves count: 500 - Current FPS: 39.84
Elves count: 500 - Current FPS: 42.55
Elves count: 500 - Current FPS: 31.65
Elves count: 500 - Current FPS: 30.96
Elves count: 500 - Current FPS: 37.59
Elves count: 500 - Current FPS: 39.53
Elves count: 500 - Current FPS: 45.87
Elves count: 500 - Current FPS: 37.31
Elves count: 500 - Current FPS: 35.34
Elves count: 500 - Current FPS: 39.22
Elves count: 500 - Current FPS: 47.17
Elves count: 500 - Current FPS: 39.37
Elves count: 500 - Current FPS: 25.25
Elves count: 500 - Current FPS: 35.97
Elves count: 500 - Current FPS: 48.54
Elves count: 500 - Current FPS: 44.64
Elves count: 500 - Current FPS: 43.67
Elves count: 500 - Current FPS: 28.17
Elves count: 500 - Current FPS: 35.97
Elves count: 500 - Current FPS: 31.06
Elves count: 500 - Current FPS: 47.62
Elves count: 500 - Current FPS: 37.59
Elves count: 500 - Current FPS: 42.19
Elves count: 500 - Current FPS: 50.25
Elves count: 500 - Current FPS: 35.59
Elves count: 500 - Current FPS: 45.45
Elves count: 500 - Current FPS: 45.45
Elves count: 500 - Current FPS: 28.65
Elves count: 500 - Current FPS: 24.15
Elves count: 500 - Current FPS: 42.55
Leak: 3284.0 KB per 1000 frames
Elves count: 500 - Current FPS: 42.19
Elves count: 500 - Current FPS: 35.09
Elves count: 500 - Current FPS: 42.19
Elves count: 500 - Current FPS: 51.55
Elves count: 500 - Current FPS: 32.36
Elves count: 500 - Current FPS: 39.22
Elves count: 500 - Current FPS: 35.59
Elves count: 500 - Current FPS: 28.57
Elves count: 500 - Current FPS: 22.73
Elves count: 500 - Current FPS: 31.85
Elves count: 500 - Current FPS: 26.74
Elves count: 500 - Current FPS: 24.94
Elves count: 500 - Current FPS: 22.08
Elves count: 500 - Current FPS: 24.51
Elves count: 500 - Current FPS: 34.97
Elves count: 500 - Current FPS: 30.40
Elves count: 500 - Current FPS: 47.62
Elves count: 500 - Current FPS: 40.16
Elves count: 500 - Current FPS: 55.87
Elves count: 500 - Current FPS: 33.78
Elves count: 500 - Current FPS: 50.00
Elves count: 500 - Current FPS: 50.51
Elves count: 500 - Current FPS: 33.00
Elves count: 500 - Current FPS: 54.64
Elves count: 500 - Current FPS: 61.35
Elves count: 500 - Current FPS: 51.02
Elves count: 500 - Current FPS: 54.35
Elves count: 500 - Current FPS: 50.51
Elves count: 500 - Current FPS: 53.19
Elves count: 500 - Current FPS: 53.76
Elves count: 500 - Current FPS: 39.37
Elves count: 500 - Current FPS: 49.50
Elves count: 500 - Current FPS: 60.24
Elves count: 500 - Current FPS: 47.17
Elves count: 500 - Current FPS: 54.64
Elves count: 500 - Current FPS: 60.61
Elves count: 500 - Current FPS: 56.50
Elves count: 500 - Current FPS: 58.48
Elves count: 500 - Current FPS: 58.82
Elves count: 500 - Current FPS: 52.36
Elves count: 500 - Current FPS: 57.47
Elves count: 500 - Current FPS: 61.73
Elves count: 500 - Current FPS: 60.61
Elves count: 500 - Current FPS: 39.06
Elves count: 500 - Current FPS: 51.55
Elves count: 500 - Current FPS: 56.82
Elves count: 500 - Current FPS: 59.52
Elves count: 500 - Current FPS: 54.64
Elves count: 500 - Current FPS: 54.95
Elves count: 500 - Current FPS: 56.50
Elves count: 500 - Current FPS: 52.63
Elves count: 500 - Current FPS: 59.17
Elves count: 500 - Current FPS: 55.56
Elves count: 500 - Current FPS: 58.82
Elves count: 500 - Current FPS: 55.87
Elves count: 500 - Current FPS: 48.08
Elves count: 500 - Current FPS: 60.24
Elves count: 500 - Current FPS: 51.55
Elves count: 500 - Current FPS: 58.14
Elves count: 500 - Current FPS: 57.14
Elves count: 500 - Current FPS: 47.85
Elves count: 500 - Current FPS: 50.00
Elves count: 500 - Current FPS: 47.62
Elves count: 500 - Current FPS: 56.18
Elves count: 500 - Current FPS: 55.56
Elves count: 500 - Current FPS: 53.48
Elves count: 500 - Current FPS: 52.91
Elves count: 500 - Current FPS: 60.98
Elves count: 500 - Current FPS: 57.14
Elves count: 500 - Current FPS: 34.60
Elves count: 500 - Current FPS: 41.84
Elves count: 500 - Current FPS: 59.88
Elves count: 500 - Current FPS: 49.75
Elves count: 500 - Current FPS: 47.62
Elves count: 500 - Current FPS: 50.51
Elves count: 500 - Current FPS: 35.97
Elves count: 500 - Current FPS: 57.47
Elves count: 500 - Current FPS: 55.56
Elves count: 500 - Current FPS: 45.25
Elves count: 500 - Current FPS: 35.59

BIN
log.txt Normal file

Binary file not shown.

39
main.py
View File

@@ -1,8 +1,45 @@
import eb_engine import eb_engine
#import os
#os.environ['PYTHONJIT'] = '1'
def main(): def main():
e = eb_engine.Engine() e = eb_engine.Engine()
e.main_loop() e.main_loop()
if __name__ == "__main__": if __name__ == "__main__":
main() main()
# todo:
# прокрутка баг консоль и карта
# ОПТИМИЗАЦИИ
# перепроверять путь пореже, или только после столкновения, или локальный поиск
# очередь задач и задача рандомного патруля
# устроить краш тест поиску пути, запустив много объектов на маленьком поле, успел заметить баги
# добавить функцию движения за каким-то объектом
# сделать, чтобы в случае отменненого движения не телепортировался назад, а плавно
# приступаем к логике
# сделать по аналогии с текущей клеткой текущий объект
# посмотреть как в clock = pygame.time.Clock() работает фпс
# перемещать оъект в другую клетку при половине офсета
# техдолг Егору
# убрать cells и mapobject creature - перенести нужную логику в методы Map
# система имен спрайтов и Action - реализовать
# рисовать группой спрайтов как в перпл
# нужен ли теперь start_pos? grid_pos?
# class Task с проверками выполнения экшонов
#вернуть назад апдейт. вернул - работает, сравнить с новым и решить
#final_goal = None - check all!
#рефактор goal = (random.randint(self.grid_pos[0] - area,
# self.grid_pos[0] + area),
# random.randint(self.grid_pos[1] - area,
# self.grid_pos[1] + area))
# отловить баг - иногда всё равно встают - похоже из-за реплана и коллизий с эльфами
# вроде починил, добавив final_goal = None в реплан, проверить
# совет ксюши - не считать коллизии с объектами, только при перемещении в клетку проверять

26
plan.txt Normal file
View File

@@ -0,0 +1,26 @@
1. Доработка системы задач: класс Task, новые задачи, проработка старых;
2. Доработка движка и приведение его структуры в порядок:
- вынос всех функций карты в карту, объекты знают свою позицию и им этого хватит.
взаимодействия с картой и другими объектами должно происходить только в методе движения, остальные методы должны абстрагироваться от карты;
- вынос рендера в класс Render. Карта занимается только своими клеткам. Метод
draw_map должен только готовить текстуру для Render, который будет ее рисовать;
- мини-движок поиска пути. class Pathfinder, который хранит в себе кэши,
выбирает оптимальный способ поиска пути для конкретной ситуации. Для этого
нужно ещё раз протестировать все способы, привести к единообразию интерфейсов.
Задокументировать. Возможно понадобится вынос поиска пути в отдельный движок
на другом языке и многопоточностью;
- привести в порядок Main Loop;
- вынести хранение и обработку объектов из карты в ObjectManager;
- сделать фундамент для MapManager, пока не развивать, но надо исходить из того
что одновременно может рендериться несколько карт;
3. Оптимизация.
- проверяем, есть ли объект в клетке только перед непосредственным перемещением в ячейку
Cells, а не каждый calc_step; + (исправить визуальный баг, см комменты calc_step)
- перестаём учитывать объекты в алгоритме поиска пути; - (попробовать ещё раз, подумать)
- заранее передаем в алгоритм карту препятствий; +

View File

@@ -50,8 +50,7 @@ scale_image() вызывается 150×150=22,500 раз в секунду пр
# проверить у ллм на ошибки - РЕГУЛЯРНАЯ АКТИВНОСТЬ: # проверить у ллм на ошибки - РЕГУЛЯРНАЯ АКТИВНОСТЬ:
# - deepcopy + # - deepcopy +
# - общие + # - общие +
# !!! ДОБАВИТЬ ПРОКРУТКУ И МАСШТАБ КАРТЫ ДЛЯ МЫШИ !!! #
#
# ДОДЕЛАТЬ move для Creature - хранить pos в объекте ??? # ДОДЕЛАТЬ move для Creature - хранить pos в объекте ???
# #
# ПРОВЕРИТЬ МЕНЯЕТСЯ ЛИ ПЕРЕДАННЫЙ В ОБЪЕКТ cells и если да, # ПРОВЕРИТЬ МЕНЯЕТСЯ ЛИ ПЕРЕДАННЫЙ В ОБЪЕКТ cells и если да,

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
pathfinding==1.0.20
psutil==7.2.2
pygame-ce==2.5.6
pygame_gui==0.6.14
python-i18n==0.3.9
typing_extensions==4.15.0