Compare commits

...

16 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
shiva404
a707638e09 1 2026-02-23 14:59:58 +03:00
shiva404
0d9b052aca Cleanup. 2026-02-22 23:30:55 +03:00
shiva404
2b114ddd2d Diagonal pathfinding and moving works great! Fixed teleport bug and terrain overlay. 2026-02-20 04:53:33 +03:00
shiva404
7fbc1b38c2 slop-commit. check out all code, test and take a look at notes in main, need to clean up it 2026-02-20 04:02:51 +03:00
shiva404
171eff1434 pre commit, make CAPS tasks from main 2026-02-20 02:09:43 +03:00
shiva404
197469350d move_obj func was added to Map class. Also console now can execute any command with exec. 2026-02-19 19:55:07 +03:00
shiva404
bf4a80a54a Nevermind. 2026-02-18 22:31:31 +03:00
shiva404
b34aaa1664 shameslop, but gui still works. 2026-02-16 17:20:37 +03:00
shiva404
0e0bf1f84e Finally fixed sprite caching. For now it works only for one map, need to develop mapmanager and have a list of cached sprites for each map. 2026-02-16 02:46:28 +03:00
shiva404
f89e0a86c4 Profiling functions were added. 2026-02-16 01:31:16 +03:00
35 changed files with 91115 additions and 118 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.

1
classes.py Normal file
View File

@@ -0,0 +1 @@
from eb_terrain_objects import Rock

564
common.py
View File

@@ -1,15 +1,21 @@
import os
import json
import uuid
import random
from dataclasses import dataclass, field
from copy import deepcopy
from functools import partial
import pygame
import pygame_gui
from classes import Rock
def scale_image(image, n):
orig_size = image.get_size()
new_size = (int(orig_size[0] * n), int(orig_size[1] * n))
return pygame.transform.smoothscale(image, new_size)
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
@@ -19,3 +25,553 @@ def path_exists(data, path):
else:
return False
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):
# """Находит путь от start=(row, col) к goal=(row, col). row=y (строка), col=x (столбец)"""
# rows = len(cells)
# if rows == 0:
# return None
# cols = len(cells[0])
# def is_passable(cell):
# # Проходимо, если НЕ Rock в terrain И creature_obj отсутствует
# 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"Старт/гол непроходимы: start={is_passable(start_cell)}, goal={is_passable(goal_cell)}")
# return None
# # A* поиск (используем прямые row,col вместо ID для простоты)
# directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] # вверх, вниз, лево, право
# open_set = []
# # f_score = g + h (h=манхэттен)
# h = abs(s_row - g_row) + abs(s_col - g_col)
# heappush(open_set, (h, 0, s_row, s_col)) # f, g, row, 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:
# if is_passable(cells[nr][nc]):
# tentative_g = g_score[(row, col)] + 1
# pos = (nr, nc)
# if tentative_g < g_score[pos]:
# came_from[pos] = (row, col)
# g_score[pos] = tentative_g
# f = tentative_g + abs(nr - g_row) + abs(nc - g_col)
# heappush(open_set, (f, tentative_g, nr, nc))
# print("Путь не найден (нет связи)")
# return None
#def find_way(cells, start, goal):
# """Находит путь от start=(row, col) к goal=(row, col) с диагональным движением"""
# 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"Старт/гол непроходимы: start={is_passable(start_cell)}, goal={is_passable(goal_cell)}")
# return None
#
# # ★ 8 НАПРАВЛЕНИЙ: 4 основных + 4 диагональных ★
# directions = [
# (-1, 0), (1, 0), (0, -1), (0, 1), # вверх, вниз, лево, право (стоимость 1.0)
# (-1, -1), (-1, 1), (1, -1), (1, 1) # диагонали (стоимость √2 ≈ 1.414)
# ]
#
# open_set = []
# h = abs(s_row - g_row) + abs(s_col - g_col) # эвристика манхэттен
# heappush(open_set, (h, 0, s_row, s_col)) # f, g, row, 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:
# if is_passable(cells[nr][nc]):
# # ★ РАЗНЫЕ СТОИМОСТИ ДЛЯ ДИАГОНАЛЕЙ ★
# if abs(dr) + abs(dc) == 2: # диагональ
# move_cost = 1.414 # √2
# else: # ортогональ
# move_cost = 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):
# """Находит путь с диагональным движением, но БЕЗ прохода через углы"""
# 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)
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
## ★ МАТРИЦЫ вместо 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
#
#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
# ★ ВЫЧИСЛЯЕМЫЕ МАССИВЫ ★
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
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 (0 <= nr < rows and 0 <= nc < cols and
walkable[nr][nc] and not visited[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(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
'''
#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]

89468
def_map.json

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,36 @@
from common import pygame, 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
import eb_objects
import eb_terrain_objects
import eb_creature_objects
#from pympler import muppy, summary
import gc, psutil, os
cell_classes = {"grass_small": eb_terrain_objects.Ground,
"sword_default": eb_objects.Item}
"sword_default": eb_objects.Item, "elf_watching": eb_creature_objects.Unit,
"rock_small": eb_terrain_objects.Rock}
main_dir = os.path.dirname(os.path.abspath(__file__))
sprites_dir = os.path.join(main_dir, "res", "sprites")
#class Render
#class ObjectManager
#class MapManager
#class Event
#class EventManager
#class Control
@dataclass
class Cell:
terrain_obj: any
terrain_obj: None = None
item_obj: None = None
creature_obj: None = None
is_target: bool = False
render_offset: tuple = (0.0, 0.0)
@dataclass
class Map:
@@ -22,65 +38,263 @@ class Map:
sprites: dict
sprites_refresh: int = 60
cells: dict = field(default_factory = dict)
walkable_matrix: list = field(default_factory = list)
rocks_matrix: list = field(default_factory = list)
color: str = "gray57"
target_color: str = "gold"
size: int = 150
cell_size: int = 150
bord: int = 3
scale: float = 1
cam_x: int = 0
cam_y: int = 0
cell_dist: int = 1
#effects[]
#action_time_multiplier
def __post_init__(self):
self.cells = {}
with open(self.name, 'r') as file:
buff = json.load(file)
for line in range(len(buff)):
self.cells[line] = []
for cell in buff[str(line)]:
final_cell = deepcopy(Cell(cell_classes[cell["terrain_obj"]["sprite_name"]](**cell["terrain_obj"])))
for col, cell in enumerate(buff[str(line)]):
final_cell = Cell(cell_classes[cell["terrain_obj"]["sprite_name"]](**cell["terrain_obj"]))
if cell["item_obj"]:
final_cell.item_obj = deepcopy(cell_classes[cell["item_obj"]["sprite_name"]](**cell["item_obj"]))
final_cell.item_obj = cell_classes[cell["item_obj"]["sprite_name"]](**cell["item_obj"])
self.cells[line].append(deepcopy(final_cell))
if 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
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
# Проверка границ
if (s_y >= len(self.cells) or s_x >= len(self.cells[s_y]) or
d_y >= len(self.cells) or d_x >= len(self.cells[d_y])):
return False
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 or check is not None:
return False
setattr(source_cell, type, None)
setattr(dest_cell, type, obj)
#obj.grid_pos = goal
return True
def get_cell_at_mouse(self, mouse_pos):
"""Возвращает индексы клетки (row, col) по позиции мыши или None если вне карты"""
mx, my = mouse_pos
# Переводим экранные координаты в координаты карты с учетом камеры и масштаба
map_x = (mx - self.cam_x * self.scale) / self.scale / self.cell_size
map_y = (my - self.cam_y * self.scale) / self.scale / self.cell_size
# Получаем индексы ближайшей клетки
col = int(map_x)
row = int(map_y)
# Проверяем границы карты
if (row < 0 or row >= len(self.cells) or
col < 0 or col >= len(self.cells[row])):
return None
return (row, col)
def update_map(self, time_delta):
self.compute_walkable_rocks()
for j in range(len(self.cells)):
for cell in self.cells[j]:
if cell.creature_obj:
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):
for j in range(len(self.cells)):
for i, cell in enumerate(self.cells[j]):
dd = {"x": int((i * self.size + self.cam_x) * self.scale),
"y": int((j * self.size + self.cam_y) * self.scale),
"w": int(self.size * self.scale - self.cell_dist),
"h": int(self.size * self.scale - self.cell_dist),
screen_rect = screen.get_rect()
terrain_list = []
creature_list = []
# Вычисляем видимую область в координатах карты (аналогично get_cell_at_mouse)
left_map = (self.cam_x * self.scale) / self.scale / self.cell_size
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_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}
"sprites": self.sprites, "scale": self.scale, "screen": screen
}
terrain_list.append((cell.terrain_obj, terrain_dd))
cell.terrain_obj.draw(dd)
if cell.item_obj:
cell.item_obj.draw(dd)
# Creature данные (если есть)
if cell.creature_obj:
cell.creature_obj.draw(dd)
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(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_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)
if grid:
pygame.draw.rect(screen, self.color, pygame.Rect(dd["x"], dd["y"], dd["w"], dd["h"]), self.bord)
@dataclass
class Engine:
sprites: dict = field(default_factory = dict)
cached_sprites: dict = field(default_factory = dict)
screen: pygame.Surface = ((1, 1))
width: int = 1600
height: int = 800
camera_step: int = 10
scale_step: float = 0.01
spr_scale: float = 1
def __post_init__(self):
self.sprites = {}
pygame.init()
pygame.display.set_caption('Elvenbane')
self.screen = pygame.display.set_mode((self.width, self.height))
self.load_sprites()
self.screen = pygame.display.set_mode((self.width, self.height), pygame.HWSURFACE | pygame.DOUBLEBUF)
print("The engine has started. Sprites were successfully loaded.\n")
self.load_sprites()
print("Sprites were successfully loaded.\n")
self.cached_sprites = deepcopy(self.sprites)
print("Sprites were successfully cached.\n")
def load_sprites(self, folder_path = sprites_dir):
self.sprites = {}
@@ -101,10 +315,64 @@ class Engine:
for num, f in items
]
def scale_image(self, image):
orig_size = image.get_size()
new_size = (int(orig_size[0] * self.spr_scale), int(orig_size[1] * self.spr_scale))
return pygame.transform.smoothscale(image, new_size)
def scale_sprites(self):
for sp_name, sprite_list in self.sprites.items():
for i, sprite in enumerate(sprite_list):
scaled = self.scale_image(sprite)
self.cached_sprites[sp_name][i] = scaled
def create_console(self, manager):
console_window = pygame_gui.elements.UIWindow(
pygame.Rect(100, 100, 400, 300),
manager=manager,
window_display_title='Console',
resizable=True
)
input_entry = pygame_gui.elements.UITextEntryLine(
relative_rect=pygame.Rect(10, 250, 380, 30),
container=console_window,
manager=manager
)
# ★ UIScrollingContainer ★
scroll_container = pygame_gui.elements.UIScrollingContainer(
relative_rect=pygame.Rect(10, 10, 380, 230),
container=console_window,
manager=manager,
allow_scroll_x=False
)
# ★ UITextBox ВНУТРИ контейнера ★
output_box = pygame_gui.elements.UITextBox(
html_text=">>> hlwrld1\n",
relative_rect=pygame.Rect(0, 0, 380, 1000), # ← Увеличьте высоту!
container=scroll_container,
manager=manager
)
scroll_container.set_scrollable_area_dimensions((380, 2000))
return console_window, input_entry, scroll_container, output_box
def main_loop(self):
easy_map = Map("def_map.json", self.sprites)
#sp = eb_objects.Sprite(self.sprites, "elf_watching")
#gr = pygame.image.load(file_path).convert_alpha()
easy_map = Map("def_map.json", self.cached_sprites)
background = pygame.Surface((1600, 800))
background.fill("chartreuse4")
manager = pygame_gui.UIManager((1600, 800))
#hello_button = pygame_gui.elements.UIButton(relative_rect=pygame.Rect((350, 275), (100, 50)),
# text='Say Hello',
# manager=manager)
console_window, input_entry, scroll_container, output_box = self.create_console(manager)
output_log = ">>> hlwrld1\n"
console_active = False
clock = pygame.time.Clock()
running = True
@@ -114,21 +382,123 @@ class Engine:
global_counter = 0
global_counter_cap = 100000
active_cell = None
# profiling
process = psutil.Process(os.getpid())
gc.collect()
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:
time_delta = clock.tick(60)/1000.0
#pygame.event.clear()
if global_counter % 1000 == 0:
gc.collect()
mem_after = process.memory_info().rss / 1024
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
# pygame.QUIT event means the user clicked X to close your window
for event in pygame.event.get():
if event.type == pygame.QUIT:
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:
user_text = input_entry.get_text()
exec(user_text)
if user_text.strip():
output_log += f">>> {user_text}\n"
output_box.set_text(output_log)
input_entry.set_text("")
user_text = ""
console_active = False
console_active = bool(input_entry.get_text().strip())
if event.type == pygame.VIDEORESIZE:
new_size = event.size
self.screen = pygame.display.set_mode(new_size, pygame.RESIZABLE)
manager.clear_and_set_new_size((new_size[0], new_size[1]))
# ★ ПЕРЕСОЗДАНИЕ ★
console_window.kill()
input_entry.kill()
scroll_container.kill()
output_box.kill()
console_window, input_entry, scroll_container, output_box = self.create_console(manager)
manager.process_events(event)
manager.update(time_delta)
# fill the screen with a color to wipe away anything from last frame
self.screen.fill("chartreuse4")
#self.screen.fill("chartreuse4")
self.screen.blit(background, (0, 0))
easy_map.update_map(time_delta)
easy_map.draw_map(self.screen, current_frame + 1)
#print(easy_map.cells[0][0].item_obj.sprite_cache)
manager.draw_ui(self.screen)
if unlock:
if not console_active:
keys = pygame.key.get_pressed()
if keys[pygame.K_w]:
easy_map.cam_y += self.camera_step
if keys[pygame.K_s]:
@@ -137,10 +507,45 @@ class Engine:
easy_map.cam_x += self.camera_step
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 event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
mouse_pos = pygame.mouse.get_pos()
console_rect = console_window.get_abs_rect()
if not console_rect.collidepoint(mouse_pos):
cell_coords = easy_map.get_cell_at_mouse(mouse_pos)
if cell_coords:
# Сбрасываем предыдущую цель
if active_cell is not None:
row, col = active_cell
if row in easy_map.cells and col < len(easy_map.cells[row]):
easy_map.cells[row][col].is_target = False
active_cell = cell_coords # Теперь кортеж (row, col)
row, col = active_cell
easy_map.cells[row][col].is_target = True
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 3:
mouse_pos = pygame.mouse.get_pos()
console_rect = console_window.get_abs_rect()
if (not console_rect.collidepoint(mouse_pos) and
active_cell is not None and
easy_map.cells[active_cell[0]][active_cell[1]].creature_obj is not None):
cell_coords = easy_map.get_cell_at_mouse(mouse_pos)
if cell_coords:
#print(f"Движение: {active_cell} -> {cell_coords}")
easy_map.cells[active_cell[0]][active_cell[1]].creature_obj.move(easy_map.cells, cell_coords)
if keys[pygame.K_ESCAPE]:
running = False
@@ -151,8 +556,10 @@ class Engine:
global_counter = 0
# flip() the display to put your work on screen
pygame.display.flip()
# limits FPS to 60
clock.tick(max_fps)
pygame.display.update()
if global_counter % 10 == 0:
current_fps = clock.get_fps()
print(f"Elves count: {elf_count} - Current FPS: {current_fps:.2f}")
pygame.quit()

View File

@@ -1,4 +1,21 @@
from common import deepcopy, dataclass, field, scale_image, path_exists
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
class Object:
@@ -6,40 +23,25 @@ class Object:
name: str
sprite_name: str
sprite_state: int = 0
#sprite_scale: int = 1
#sprite_cache: dict = field(default_factory = dict)
#sprite_cache_upd: int = 100
#
#def cache_sprite(self, sprites):
# if self.sprite_name not in self.sprite_cache:
# self.sprite_cache[self.sprite_name] = {}
# self.sprite_cache[self.sprite_name][self.sprite_state] = deepcopy(sprites[self.sprite_name][self.sprite_state])
#
#def scale_cached(self, draw_data):
# if self.sprite_scale != draw_data["scale"]:
# self.sprite_scale = draw_data["scale"]
# self.sprite_cache[self.sprite_name][self.sprite_state] = deepcopy(scale_image(draw_data["sprites"][self.sprite_name][self.sprite_state], draw_data["scale"]))
grid_pos: tuple = None
# current_map
# pos
# weight
# effects = {}
def draw(self, draw_data):
#if draw_data["global_counter"] > self.sprite_cache_upd:
# self.sprite_cache = {}
if draw_data["spr_up"] == 0:
if self.sprite_state == len(draw_data["sprites"][self.sprite_name]) - 1:
self.sprite_state = 0
else:
self.sprite_state += 1
#if path_exists(self.sprite_cache, [self.sprite_name, self.sprite_state]):
# self.scale_cached(draw_data)
#else:
# self.cache_sprite(draw_data["sprites"])
sp = scale_image(draw_data["sprites"][self.sprite_name][self.sprite_state], draw_data["scale"])
sp = draw_data["sprites"][self.sprite_name][self.sprite_state]
rect = sp.get_rect(center = (draw_data["x"] + draw_data["w"] /2, draw_data["y"] + draw_data["h"]/ 2))
draw_data["screen"].blit(sp, rect)
def update(self):
pass
@dataclass
class Terrain(Object):
@@ -47,14 +49,234 @@ class Terrain(Object):
@dataclass
class Creature(Object):
direction: int = 0 # tuple?
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 = 30000
waypoints: list = field(default_factory = list)
walkable_matrix: list = field(default_factory = list)
rocks_matrix: list = field(default_factory = list)
inventory: dict = field(default_factory = dict)
quick_actions: list = field(default_factory = list)
tasks: list = field(default_factory = list)
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 replan(self, cells, pos):
from common import find_way
path = find_way(cells, 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]
else:
self.waypoints.clear()
self.current_target = None
self.final_goal = None
def calc_step(self, time_delta, cell_size, map_obj):
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)
if self.move_progress >= 1.0:
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
if self.waypoints: self.waypoints.pop(0)
if self.waypoints:
self.start_pos = self.current_target
self.current_target = self.waypoints[0]
self.move_progress = 0.0
self.render_offset = (0.0, 0.0)
else:
#print(111111111111111)
self.current_target = None
self.final_goal = None
self.render_offset = (0.0, 0.0)
return
if self.current_target is None:
self.render_offset = (0.0, 0.0)
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_y = (target_row - start_row) * cell_size * self.move_progress
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
#status
#actions
#tasks
#items
#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
class Item(Object):
# passive_abilities = {}
# active_abilities = {}
pass
@dataclass
class Container(Item):
# content = {}
pass
@dataclass

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.

64
main.py
View File

@@ -1,35 +1,45 @@
import eb_engine
#import os
#os.environ['PYTHONJIT'] = '1'
def main():
e = eb_engine.Engine()
e.main_loop()
if __name__ == "__main__":
# pydantic instead of dataclasses?
# Отрисовка голой сетки, прокрутка, масштаб +
# Отрисовка спрайтов:
# - сделать масштабирование в соотв. с клеткой +
# - посмотреть класс спрайта или сделать свой +
# - добавить отрисовку существ и предметов с анимацией +
# почитать про Surface, Display, доку к pygame-gui
# Начало гуя:
# - общая идея гуя
# - кнопка отключить сетку
# - строка ввода
# - клик
# Поиск пути, очередь задач для существ
# Редактор карты
# Охотник -> деревня
# Простой, но основательный гуй внизу экрана, глобальная карта и перемещение
# деревня на соседской локации и торговля с ней
# перемещение по воде, течение
#техдолг:
#проверить дефолтдикт field и None = None
#не взлетело кэширование - потом доделать
# проверить у ллм на ошибки - РЕГУЛЯРНАЯ АКТИВНОСТЬ:
# - deepcopy +
# - общие +
main()
#техдолг - draw_data to dd
# 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

@@ -35,3 +35,52 @@ scaled = scale_image(sprites[cell.terrain_obj.sprite], self.scale) # KeyError!
7. Масштабирование каждый кадр
scale_image() вызывается 150×150=22,500 раз в секунду при 60 FPS. Кэшируйте масштабированные спрайты.
=========================================================================================================
#
#техдолг:
# pydantic instead of dataclasses?
# почитать про Surface, Display, доку к pygame-gui
# проверить дефолтдикт field и None = None
# изучить pypmler
# настроить логирование всего
# SLOP: load_sprites
# проверить у ллм на ошибки - РЕГУЛЯРНАЯ АКТИВНОСТЬ:
# - deepcopy +
# - общие +
#
# ДОДЕЛАТЬ move для Creature - хранить pos в объекте ???
#
# ПРОВЕРИТЬ МЕНЯЕТСЯ ЛИ ПЕРЕДАННЫЙ В ОБЪЕКТ cells и если да,
# перенести всё взаимодействие с картой в объекты, карта только хранит cells
# и готовит данные для отрисовки Render'ом
#
# ИГРОВОЙ ТАКТ? или только для действий их длительность?
#
# ПОСМОТРЕТЬ ПО КОММИТАМ ЗАЧЕМ БЫЛ НУЖЕН path_exists, удалить?
#
# добавил гуй, динамическая консоль, всё работает, но:
# - слоп, почистить
# - мини-баг - если первые вводимые буквы совпадают с клавишами управления, один раз успевает проскочить до лока. некритично.
# - при вводе текста нет прокрутки к концу
# - плавающий баг - если повводить текст, а потом закрыть консоль, игра не закроется по эскейпу.
#
#
# в дальнейшем вся отрисовка переедет в класс рендер,
# карта будет только вовзращать поверхность для отрисовки или даже просто Cells
# active_cell переедет в класс Control
#
# НАЙТИ В КОДЕ ГДЕ Я ТАК НЕ СДЕЛАЛ И ИСПРАВИТЬ - НАШЕЛ ОДНУ, ПОИСКАТЬ ЕЩЕ
#if a is None:
# print("a это точно None")
#
# Альтернатива
#if a is not None:
# print("a не None")
#
# Встреча с Егором:
#
# сборщик данных в цикле и перекладчик
# модуль автоинпута, принимает поток данных и переводит их в команды движка

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

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,8 +1,8 @@
import json
from copy import deepcopy
width = 10
height = 8
width = 100
height = 100
grass_def = {"id": "1", "name": "2", "sprite_name": "grass_small"}
cell_def = {"terrain_obj": grass_def, "item_obj": {}, "creature_obj": {}}

BIN
res/rocks-2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB