Добавил систему задач для юнитов, она реализована в методе update класса Creature. Подчистил код, пофиксил по мелочам баги. Остался ещё техдолг Егору и задачи из main.

This commit is contained in:
shiva404
2026-03-05 16:35:08 +03:00
parent dafa95989f
commit 45f2c71cb8
6 changed files with 266 additions and 95 deletions

View File

@@ -4,9 +4,7 @@ import uuid
import random 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
@@ -15,12 +13,10 @@ from classes import Rock
from pathfinding.core.grid import Grid from pathfinding.core.grid import Grid
from pathfinding.finder.a_star import AStarFinder from pathfinding.finder.a_star import AStarFinder
from collections import defaultdict from collections import defaultdict
from heapq import heappush, heappop from heapq import heappush, heappop
from math import sqrt, inf 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:
@@ -371,7 +367,7 @@ def find_way(cells, start, goal):
queue[queue_size] = [nr, nc] queue[queue_size] = [nr, nc]
queue_size += 1 queue_size += 1
print(f"Путь не найден: {start} -> {goal}") #print(f"Путь не найден: {start} -> {goal}")
return None return None
def can_move_diagonal(r, c, nr, nc, rocks_only, rows, cols): def can_move_diagonal(r, c, nr, nc, rocks_only, rows, cols):

View File

@@ -20,11 +20,7 @@
"sprite_name": "grass_small" "sprite_name": "grass_small"
}, },
"item_obj": {}, "item_obj": {},
"creature_obj": { "creature_obj": {}
"id": "1",
"name": "2",
"sprite_name": "elf_watching"
}
}, },
{ {
"terrain_obj": { "terrain_obj": {
@@ -926,11 +922,7 @@
"sprite_name": "grass_small" "sprite_name": "grass_small"
}, },
"item_obj": {}, "item_obj": {},
"creature_obj": { "creature_obj": {}
"id": "1",
"name": "2",
"sprite_name": "elf_watching"
}
}, },
{ {
"terrain_obj": { "terrain_obj": {

View File

@@ -1,5 +1,5 @@
from common import os, json, uuid, deepcopy, random from common import os, json, uuid, deepcopy, random
from common import dataclass, field 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
@@ -47,6 +47,7 @@ class Map:
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):
@@ -55,7 +56,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"]:
@@ -63,6 +64,7 @@ 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.cells[line].append(final_cell)
@@ -90,6 +92,7 @@ class Map:
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):
@@ -113,7 +116,7 @@ class Map:
def update_map(self, time_delta): def update_map(self, time_delta):
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) # self!
def draw_map(self, screen, current_frame, grid=True): def draw_map(self, screen, current_frame, grid=True):
@@ -293,8 +296,17 @@ class Engine:
mem_before = process.memory_info().rss / 1024 mem_before = process.memory_info().rss / 1024
NUM_ELVES = 1000 NUM_ELVES = 2000
elf_count = 0 elf_count = 0
space_pressed = False
spawn = False
def spawn_elf():
elf = eb_objects.Creature(id=f"elf_{random.randint(1000,9999)}", name="Elf", sprite_name="elf_watching", grid_pos = (0, 0))
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, r_move_short, r_move_short, r_move_short, r_move_short, r_move_long])
easy_map.cells[0][0].creature_obj = elf
while running: while running:
@@ -305,12 +317,29 @@ 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_elf()
elf_count += 1
# 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: if event.type == pygame.MOUSEWHEEL:
scroll_y = event.y scroll_y = event.y
@@ -372,41 +401,39 @@ class Engine:
if keys[pygame.K_d]: if keys[pygame.K_d]:
easy_map.cam_x -= self.camera_step easy_map.cam_x -= self.camera_step
#if keys[pygame.K_q]: if keys[pygame.K_q]:
# 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 keys[pygame.K_e] and easy_map.scale >= self.scale_step: if keys[pygame.K_e] and easy_map.scale >= self.scale_step:
# 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 keys[pygame.K_SPACE] and elf_count < NUM_ELVES: # # ★ МАССОВЫЙ СПАВН 50 эльфов ★
# Находим свободные клетки # free_cells = []
# ★ МАССОВЫЙ СПАВН 50 эльфов # for j in range(len(easy_map.cells)): # ★ ЛИМИТ строк
free_cells = [] # for i in range(len(easy_map.cells[j])): # ★ ЛИМИТ колонок ★
for j in range(len(easy_map.cells)): # ★ ЛИМИТ строк ★ # cell = easy_map.cells[j][i]
for i in range(len(easy_map.cells[j])): # ★ ЛИМИТ колонок ★ # if (cell.creature_obj is None and
cell = easy_map.cells[j][i] # cell.terrain_obj and
if (cell.creature_obj is None and # cell.terrain_obj.sprite_name == "grass_small"):
cell.terrain_obj and # free_cells.append((j, i))
cell.terrain_obj.sprite_name == "grass_small"): #
free_cells.append((j, i)) # spawn_count = min(50, len(free_cells)) # ★ Не больше свободных клеток ★
# for _ in range(spawn_count):
spawn_count = min(50, len(free_cells)) # ★ Не больше свободных клеток ★ # row, col = random.choice(free_cells)
for _ in range(spawn_count): # elf = eb_objects.Creature(id=f"elf_{random.randint(1000,9999)}", name="Elf", sprite_name="elf_watching")
row, col = random.choice(free_cells) # easy_map.cells[row][col].creature_obj = elf
elf = eb_objects.Creature(id=f"elf_{random.randint(1000,9999)}", name="Elf", sprite_name="elf_watching") #
easy_map.cells[row][col].creature_obj = elf # # ★ БЕЗОПАСНЫЙ выбор цели ★
# possible_targets = [c for c in free_cells if c != (row, col)]
# ★ БЕЗОПАСНЫЙ выбор цели ★ # if possible_targets:
possible_targets = [c for c in free_cells if c != (row, col)] # target_cell = random.choice(possible_targets)
if possible_targets: # elf.move(easy_map.cells, (row, col), target_cell)
target_cell = random.choice(possible_targets) #
elf.move(easy_map.cells, (row, col), target_cell) # free_cells.remove((row, col)) # Убираем занятые клетки
free_cells.remove((row, col)) # Убираем занятые клетки
@@ -437,7 +464,7 @@ class Engine:
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, active_cell, cell_coords) easy_map.cells, cell_coords)
if keys[pygame.K_ESCAPE]: if keys[pygame.K_ESCAPE]:
running = False running = False
@@ -453,6 +480,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,21 +49,30 @@ 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)
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)
final_goal: tuple = None tasks: list = field(default_factory = list)
move_speed: float = 0.02 # пикселей/кадр (настройте)
render_offset: tuple = (0.0, 0.0)
start_pos: tuple = None # (row, col) начальная позиция сегмента пути
replan_counter: int = 0 action: Action = None
REPLAN_INTERVAL: int = 300 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): def replan(self, cells, pos):
from common import find_way from common import find_way
@@ -56,21 +83,9 @@ class Creature(Object):
else: else:
self.waypoints.clear() self.waypoints.clear()
self.current_target = None self.current_target = None
self.final_goal = None
def move(self, cells, start, goal): def calc_step(self, time_delta, cell_size, map_obj):
from common import find_way
self.final_goal = goal
self.start_pos = start
path = find_way(cells, start, goal)
if path and len(path) > 1:
self.waypoints = path[1:] # Убираем текущую позицию
self.current_target = self.waypoints[0]
self.move_progress = 0.0
self.start_pos = start # ★ ТУТ - текущая позиция как стартовая для первого шага ★
self.render_offset = (0.0, 0.0)
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
@@ -98,28 +113,143 @@ class Creature(Object):
if self.move_progress >= 1.0: if self.move_progress >= 1.0:
map_obj.move_obj('creature_obj', self.start_pos, self.current_target) map_obj.move_obj('creature_obj', self.start_pos, self.current_target)
self.grid_pos = self.current_target
if self.waypoints: 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 ★
start_row, start_col = self.start_pos #or (0, 0)
if self.current_target is None: if self.current_target is None:
self.render_offset = (0.0, 0.0) self.render_offset = (0.0, 0.0)
return return
start_row, start_col = self.start_pos
target_row, target_col = self.current_target 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, goal)
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):
#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):
@@ -132,7 +262,6 @@ class Container(Item):
# content = {} # content = {}
pass pass
@dataclass @dataclass
class Building(Object): class Building(Object):
pass pass

BIN
log.txt Normal file

Binary file not shown.

27
main.py
View File

@@ -11,8 +11,35 @@ if __name__ == "__main__":
# todo: # 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 в реплан, проверить
# совет ксюши - не считать коллизии с объектами, только при перемещении в клетку проверять