Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9027ab935 | |||
| b775813cbd | |||
| bce07db40e | |||
| 0d95e85d83 | |||
| 22406420c2 | |||
| 31c3244b5a | |||
| 8bb5c3edfc | |||
| 4e90ecb885 | |||
| 8bb669d6b2 | |||
| d0f01c0a48 | |||
| 15bc179410 | |||
| f74aa1f633 | |||
| dee0eaa9f9 | |||
| 7289543f6a | |||
| 8f17498b88 | |||
| f56192ab8f | |||
| 4a4f7a75c5 | |||
| 6f9e1e84f0 | |||
| 8c8d8f7925 |
@ -19,8 +19,8 @@ SELECTION_GRAY = (128, 128, 128, 80)
|
|||||||
SELECTION_BORDER = (80, 80, 90)
|
SELECTION_BORDER = (80, 80, 90)
|
||||||
|
|
||||||
# Grid settings
|
# Grid settings
|
||||||
GRID_WIDTH = 30
|
GRID_WIDTH = 50
|
||||||
GRID_HEIGHT = 25
|
GRID_HEIGHT = 50
|
||||||
CELL_SIZE = 20
|
CELL_SIZE = 20
|
||||||
RENDER_BUFFER = 50
|
RENDER_BUFFER = 50
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import pygame
|
|||||||
import math
|
import math
|
||||||
from config.constants import *
|
from config.constants import *
|
||||||
from world.base.brain import CellBrain
|
from world.base.brain import CellBrain
|
||||||
|
from world.objects import DefaultCell
|
||||||
|
|
||||||
|
|
||||||
class Renderer:
|
class Renderer:
|
||||||
@ -13,11 +14,8 @@ class Renderer:
|
|||||||
self.render_height = render_area.get_height()
|
self.render_height = render_area.get_height()
|
||||||
self.render_width = render_area.get_width()
|
self.render_width = render_area.get_width()
|
||||||
|
|
||||||
def clear_screen(self, main_screen=None):
|
def clear_screen(self):
|
||||||
"""Clear the screen with a black background."""
|
"""Clear the screen with a black background."""
|
||||||
if main_screen:
|
|
||||||
main_screen.fill(BLACK)
|
|
||||||
|
|
||||||
self.render_area.fill(BLACK)
|
self.render_area.fill(BLACK)
|
||||||
|
|
||||||
def draw_grid(self, camera, showing_grid=True):
|
def draw_grid(self, camera, showing_grid=True):
|
||||||
@ -101,6 +99,9 @@ class Renderer:
|
|||||||
return
|
return
|
||||||
|
|
||||||
for obj in world.get_objects():
|
for obj in world.get_objects():
|
||||||
|
if not isinstance(obj, DefaultCell):
|
||||||
|
continue
|
||||||
|
|
||||||
obj_x, obj_y = obj.position.get_position()
|
obj_x, obj_y = obj.position.get_position()
|
||||||
radius = obj.interaction_radius
|
radius = obj.interaction_radius
|
||||||
|
|
||||||
|
|||||||
@ -18,21 +18,29 @@ from ui.hud import HUD
|
|||||||
class SimulationEngine:
|
class SimulationEngine:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pygame.init()
|
pygame.init()
|
||||||
|
self._init_window()
|
||||||
|
self._init_ui()
|
||||||
|
self._init_simulation()
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
def _init_window(self):
|
||||||
info = pygame.display.Info()
|
info = pygame.display.Info()
|
||||||
self.window_width, self.window_height = info.current_w // 2, info.current_h // 2
|
self.window_width = int(info.current_w // 1.5)
|
||||||
self.screen = pygame.display.set_mode((self.window_width, self.window_height),
|
self.window_height = int(info.current_h // 1.5)
|
||||||
pygame.RESIZABLE, vsync=1)
|
self.screen = pygame.display.set_mode(
|
||||||
|
(self.window_width, self.window_height),
|
||||||
self.ui_manager = UIManager((self.window_width, self.window_height))
|
pygame.RESIZABLE, vsync=1
|
||||||
|
)
|
||||||
self.camera = Camera(SCREEN_WIDTH, SCREEN_HEIGHT, RENDER_BUFFER)
|
|
||||||
self._update_simulation_view()
|
|
||||||
|
|
||||||
# self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), vsync=1)
|
|
||||||
pygame.display.set_caption("Dynamic Abstraction System Testing")
|
pygame.display.set_caption("Dynamic Abstraction System Testing")
|
||||||
self.clock = pygame.time.Clock()
|
self.clock = pygame.time.Clock()
|
||||||
|
|
||||||
|
def _init_ui(self):
|
||||||
|
self.ui_manager = UIManager((self.window_width, self.window_height))
|
||||||
|
self.hud = HUD(self.ui_manager, self.window_width, self.window_height)
|
||||||
|
self.hud.update_layout(self.window_width, self.window_height)
|
||||||
|
self._update_simulation_view()
|
||||||
|
|
||||||
|
def _init_simulation(self):
|
||||||
self.last_tick_time = time.perf_counter()
|
self.last_tick_time = time.perf_counter()
|
||||||
self.last_tps_time = time.perf_counter()
|
self.last_tps_time = time.perf_counter()
|
||||||
self.tick_counter = 0
|
self.tick_counter = 0
|
||||||
@ -42,21 +50,17 @@ class SimulationEngine:
|
|||||||
self.world = self._setup_world()
|
self.world = self._setup_world()
|
||||||
self.input_handler = InputHandler(self.camera, self.world, self.sim_view_rect)
|
self.input_handler = InputHandler(self.camera, self.world, self.sim_view_rect)
|
||||||
self.renderer = Renderer(self.sim_view)
|
self.renderer = Renderer(self.sim_view)
|
||||||
self.hud = HUD(self.ui_manager, self.window_width, self.window_height)
|
|
||||||
self.hud.update_layout(self.window_width, self.window_height)
|
|
||||||
|
|
||||||
self.running = True
|
|
||||||
|
|
||||||
def _update_simulation_view(self):
|
def _update_simulation_view(self):
|
||||||
self.sim_view_width = int(self.window_width * 0.75)
|
viewport_rect = self.hud.get_viewport_rect()
|
||||||
self.sim_view_height = int(self.window_height * 0.75)
|
self.sim_view_width = viewport_rect.width
|
||||||
|
self.sim_view_height = viewport_rect.height
|
||||||
self.sim_view = pygame.Surface((self.sim_view_width, self.sim_view_height))
|
self.sim_view = pygame.Surface((self.sim_view_width, self.sim_view_height))
|
||||||
self.sim_view_rect = self.sim_view.get_rect(center=(self.window_width // 2, self.window_height // 2))
|
self.sim_view_rect = self.sim_view.get_rect(topleft=(viewport_rect.left, viewport_rect.top))
|
||||||
|
|
||||||
self.ui_manager.set_window_resolution((self.window_width, self.window_height))
|
self.ui_manager.set_window_resolution((self.window_width, self.window_height))
|
||||||
self.renderer = Renderer(self.sim_view)
|
self.renderer = Renderer(self.sim_view)
|
||||||
|
|
||||||
# Update camera to match new sim_view size
|
|
||||||
if hasattr(self, 'camera'):
|
if hasattr(self, 'camera'):
|
||||||
self.camera.screen_width = self.sim_view_width
|
self.camera.screen_width = self.sim_view_width
|
||||||
self.camera.screen_height = self.sim_view_height
|
self.camera.screen_height = self.sim_view_height
|
||||||
@ -64,6 +68,8 @@ class SimulationEngine:
|
|||||||
if hasattr(self, 'input_handler'):
|
if hasattr(self, 'input_handler'):
|
||||||
self.input_handler.update_sim_view_rect(self.sim_view_rect)
|
self.input_handler.update_sim_view_rect(self.sim_view_rect)
|
||||||
|
|
||||||
|
if not hasattr(self, 'camera'):
|
||||||
|
self.camera = Camera(self.sim_view_width, self.sim_view_height, RENDER_BUFFER)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _setup_world():
|
def _setup_world():
|
||||||
@ -75,23 +81,32 @@ class SimulationEngine:
|
|||||||
|
|
||||||
if FOOD_SPAWNING:
|
if FOOD_SPAWNING:
|
||||||
for _ in range(FOOD_OBJECTS_COUNT):
|
for _ in range(FOOD_OBJECTS_COUNT):
|
||||||
x = random.randint(-half_width, half_width)
|
x = random.randint(-half_width // 2, half_width // 2)
|
||||||
y = random.randint(-half_height, half_height)
|
y = random.randint(-half_height // 2, half_height // 2)
|
||||||
world.add_object(FoodObject(Position(x=x, y=y)))
|
world.add_object(FoodObject(Position(x=x, y=y)))
|
||||||
|
|
||||||
for _ in range(300):
|
for _ in range(350):
|
||||||
new_cell = DefaultCell(Position(x=random.randint(-half_width, half_width), y=random.randint(-half_height, half_height)), Rotation(angle=0))
|
new_cell = DefaultCell(
|
||||||
|
Position(x=random.randint(-half_width // 2, half_width // 2), y=random.randint(-half_height // 2, half_height // 2)),
|
||||||
|
Rotation(angle=0)
|
||||||
|
)
|
||||||
new_cell.behavioral_model = new_cell.behavioral_model.mutate(3)
|
new_cell.behavioral_model = new_cell.behavioral_model.mutate(3)
|
||||||
|
|
||||||
world.add_object(new_cell)
|
world.add_object(new_cell)
|
||||||
|
|
||||||
return world
|
return world
|
||||||
|
|
||||||
|
def _count_cells(self):
|
||||||
|
count = 0
|
||||||
|
for entity in self.world.get_objects():
|
||||||
|
if isinstance(entity, DefaultCell):
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
print(self.world.current_buffer)
|
||||||
while self.running:
|
while self.running:
|
||||||
self._handle_frame()
|
self._handle_frame()
|
||||||
|
|
||||||
pygame.quit()
|
pygame.quit()
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
@ -99,53 +114,24 @@ class SimulationEngine:
|
|||||||
deltatime = self.clock.get_time() / 1000.0
|
deltatime = self.clock.get_time() / 1000.0
|
||||||
tick_interval = 1.0 / self.input_handler.tps
|
tick_interval = 1.0 / self.input_handler.tps
|
||||||
|
|
||||||
# Handle events
|
|
||||||
events = pygame.event.get()
|
events = pygame.event.get()
|
||||||
self.running = self.input_handler.handle_events(events, self.hud.manager)
|
self.running = self.input_handler.handle_events(events, self.hud.manager)
|
||||||
|
self._handle_window_events(events)
|
||||||
for event in events:
|
|
||||||
if event.type == pygame.VIDEORESIZE:
|
|
||||||
self.window_width, self.window_height = event.w, event.h
|
|
||||||
self.screen = pygame.display.set_mode((self.window_width, self.window_height),
|
|
||||||
pygame.RESIZABLE)
|
|
||||||
self._update_simulation_view()
|
|
||||||
self.hud.update_layout(self.window_width, self.window_height)
|
|
||||||
|
|
||||||
if self.input_handler.sprint_mode:
|
if self.input_handler.sprint_mode:
|
||||||
# Sprint mode: run as many ticks as possible, skip rendering
|
self._handle_sprint_mode()
|
||||||
current_time = time.perf_counter()
|
|
||||||
while True:
|
|
||||||
self.input_handler.update_selected_objects()
|
|
||||||
self.world.tick_all()
|
|
||||||
self.tick_counter += 1
|
|
||||||
self.total_ticks += 1
|
|
||||||
# Optionally break after some time to allow event processing
|
|
||||||
if time.perf_counter() - current_time > 0.05: # ~50ms per batch
|
|
||||||
break
|
|
||||||
# Update TPS every second
|
|
||||||
if time.perf_counter() - self.last_tps_time >= 1.0:
|
|
||||||
self.actual_tps = self.tick_counter
|
|
||||||
self.tick_counter = 0
|
|
||||||
self.last_tps_time = time.perf_counter()
|
|
||||||
# No rendering or camera update
|
|
||||||
|
|
||||||
self.renderer.clear_screen()
|
|
||||||
self.hud.render_sprint_debug(self.screen, self.actual_tps, self.total_ticks)
|
|
||||||
pygame.display.flip()
|
|
||||||
self.clock.tick(MAX_FPS)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Only process one tick per frame if enough time has passed
|
||||||
if not self.input_handler.is_paused:
|
if not self.input_handler.is_paused:
|
||||||
current_time = time.perf_counter()
|
current_time = time.perf_counter()
|
||||||
while current_time - self.last_tick_time >= tick_interval:
|
if current_time - self.last_tick_time >= tick_interval:
|
||||||
self.last_tick_time += tick_interval
|
self.last_tick_time += tick_interval
|
||||||
self.tick_counter += 1
|
self.tick_counter += 1
|
||||||
self.total_ticks += 1
|
self.total_ticks += 1
|
||||||
|
|
||||||
self.input_handler.update_selected_objects()
|
self.input_handler.update_selected_objects()
|
||||||
self.world.tick_all()
|
self.world.tick_all()
|
||||||
self.hud.manager.update(deltatime)
|
self.hud.manager.update(deltatime)
|
||||||
|
|
||||||
if current_time - self.last_tps_time >= 1.0:
|
if current_time - self.last_tps_time >= 1.0:
|
||||||
self.actual_tps = self.tick_counter
|
self.actual_tps = self.tick_counter
|
||||||
self.tick_counter = 0
|
self.tick_counter = 0
|
||||||
@ -158,36 +144,87 @@ class SimulationEngine:
|
|||||||
self._update(deltatime)
|
self._update(deltatime)
|
||||||
self._render()
|
self._render()
|
||||||
|
|
||||||
|
def _handle_window_events(self, events):
|
||||||
|
for event in events:
|
||||||
|
self.hud.process_event(event)
|
||||||
|
if event.type == pygame.VIDEORESIZE:
|
||||||
|
self.window_width, self.window_height = event.w, event.h
|
||||||
|
self.screen = pygame.display.set_mode(
|
||||||
|
(self.window_width, self.window_height),
|
||||||
|
pygame.RESIZABLE
|
||||||
|
)
|
||||||
|
self._update_simulation_view()
|
||||||
|
self.hud.update_layout(self.window_width, self.window_height)
|
||||||
|
|
||||||
|
self.hud.update_layout(self.window_width, self.window_height)
|
||||||
|
self._update_simulation_view()
|
||||||
|
|
||||||
|
def _handle_sprint_mode(self):
|
||||||
|
current_time = time.perf_counter()
|
||||||
|
while True:
|
||||||
|
self.input_handler.update_selected_objects()
|
||||||
|
self.world.tick_all()
|
||||||
|
self.tick_counter += 1
|
||||||
|
self.total_ticks += 1
|
||||||
|
pygame.event.pump() # Prevent event queue overflow
|
||||||
|
if time.perf_counter() - current_time > 0.05:
|
||||||
|
break
|
||||||
|
if time.perf_counter() - self.last_tps_time >= 1.0:
|
||||||
|
self.actual_tps = self.tick_counter
|
||||||
|
self.tick_counter = 0
|
||||||
|
self.last_tps_time = time.perf_counter()
|
||||||
|
self.screen.fill(BLACK)
|
||||||
|
self.renderer.clear_screen()
|
||||||
|
cell_count = self._count_cells()
|
||||||
|
self.hud.render_sprint_debug(self.screen, self.actual_tps, self.total_ticks, cell_count)
|
||||||
|
pygame.display.flip()
|
||||||
|
self.clock.tick(MAX_FPS)
|
||||||
|
self.last_tick_time = time.perf_counter()
|
||||||
|
|
||||||
|
def _handle_simulation_ticks(self, tick_interval, deltatime):
|
||||||
|
current_time = time.perf_counter()
|
||||||
|
while current_time - self.last_tick_time >= tick_interval:
|
||||||
|
self.last_tick_time += tick_interval
|
||||||
|
self.tick_counter += 1
|
||||||
|
self.total_ticks += 1
|
||||||
|
self.input_handler.update_selected_objects()
|
||||||
|
self.world.tick_all()
|
||||||
|
self.hud.manager.update(deltatime)
|
||||||
|
if current_time - self.last_tps_time >= 1.0:
|
||||||
|
self.actual_tps = self.tick_counter
|
||||||
|
self.tick_counter = 0
|
||||||
|
self.last_tps_time += 1.0
|
||||||
|
|
||||||
def _update(self, deltatime):
|
def _update(self, deltatime):
|
||||||
keys = pygame.key.get_pressed()
|
keys = pygame.key.get_pressed()
|
||||||
self.input_handler.update_camera(keys, deltatime)
|
self.input_handler.update_camera(keys, deltatime)
|
||||||
|
|
||||||
def _render(self):
|
def _render(self):
|
||||||
self.renderer.clear_screen(self.screen)
|
self.screen.fill(BLACK)
|
||||||
self.renderer.draw_grid(self.camera, self.input_handler.show_grid)
|
self.renderer.clear_screen()
|
||||||
self.renderer.render_world(self.world, self.camera)
|
|
||||||
self.renderer.render_interaction_radius(self.world, self.camera, self.input_handler.selected_objects, self.input_handler.show_interaction_radius)
|
|
||||||
self.renderer.render_selection_rectangle(self.input_handler.get_selection_rect(), self.sim_view_rect)
|
|
||||||
self.renderer.render_selected_objects_outline(self.input_handler.selected_objects, self.camera)
|
|
||||||
|
|
||||||
# In core/simulation_engine.py, in _render():
|
if not self.hud.dragging_splitter:
|
||||||
self.screen.blit(self.sim_view, (self.sim_view_rect.left, self.sim_view_rect.top))
|
self.renderer.draw_grid(self.camera, self.input_handler.show_grid)
|
||||||
|
self.renderer.render_world(self.world, self.camera)
|
||||||
|
self.renderer.render_interaction_radius(
|
||||||
|
self.world, self.camera, self.input_handler.selected_objects, self.input_handler.show_interaction_radius
|
||||||
|
)
|
||||||
|
self.renderer.render_selection_rectangle(self.input_handler.get_selection_rect(), self.sim_view_rect)
|
||||||
|
self.renderer.render_selected_objects_outline(self.input_handler.selected_objects, self.camera)
|
||||||
|
self.screen.blit(self.sim_view, (self.sim_view_rect.left, self.sim_view_rect.top))
|
||||||
|
|
||||||
# Draw border around sim_view
|
self.hud.manager.draw_ui(self.screen)
|
||||||
border_color = (255, 255, 255) # White
|
self.hud.draw_splitters(self.screen)
|
||||||
border_width = 3
|
|
||||||
pygame.draw.rect(self.screen, border_color, self.sim_view_rect, border_width)
|
|
||||||
|
|
||||||
self.hud.render_mouse_position(self.screen, self.camera, self.sim_view_rect)
|
# self.hud.render_mouse_position(self.screen, self.camera, self.sim_view_rect)
|
||||||
self.hud.render_fps(self.screen, self.clock)
|
self.hud.render_fps(self.screen, self.clock)
|
||||||
self.hud.render_tps(self.screen, self.actual_tps)
|
self.hud.render_tps(self.screen, self.actual_tps)
|
||||||
self.hud.render_tick_count(self.screen, self.total_ticks)
|
# self.hud.render_tick_count(self.screen, self.total_ticks)
|
||||||
self.hud.render_selected_objects_info(self.screen, self.input_handler.selected_objects)
|
# self.hud.render_selected_objects_info(self.screen, self.input_handler.selected_objects)
|
||||||
self.hud.render_legend(self.screen, self.input_handler.show_legend)
|
self.hud.render_legend(self.screen, self.input_handler.show_legend)
|
||||||
self.hud.render_pause_indicator(self.screen, self.input_handler.is_paused)
|
self.hud.render_pause_indicator(self.screen, self.input_handler.is_paused)
|
||||||
|
|
||||||
if self.input_handler.selected_objects:
|
if self.input_handler.selected_objects:
|
||||||
self.hud.render_neural_network_visualization(self.screen, self.input_handler.selected_objects[0])
|
self.hud.render_neural_network_visualization(self.screen, self.input_handler.selected_objects[0])
|
||||||
|
|
||||||
pygame.display.flip()
|
pygame.display.flip()
|
||||||
self.clock.tick(MAX_FPS)
|
self.clock.tick(MAX_FPS)
|
||||||
5
main.py
5
main.py
@ -1,5 +1,8 @@
|
|||||||
from core.simulation_engine import SimulationEngine
|
from core.simulation_engine import SimulationEngine
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def main():
|
||||||
engine = SimulationEngine()
|
engine = SimulationEngine()
|
||||||
engine.run()
|
engine.run()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -14,5 +14,6 @@ dependencies = [
|
|||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
"psutil>=7.0.0",
|
||||||
"ruff>=0.11.12",
|
"ruff>=0.11.12",
|
||||||
]
|
]
|
||||||
|
|||||||
93
tests/benchmarking.py
Normal file
93
tests/benchmarking.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import time
|
||||||
|
import random
|
||||||
|
import statistics
|
||||||
|
import hashlib
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
class HeadlessSimulationBenchmark:
|
||||||
|
def __init__(self, setup_world, random_seed=42):
|
||||||
|
"""
|
||||||
|
:param setup_world: Callable that returns a World instance.
|
||||||
|
:param random_seed: Seed for random number generation.
|
||||||
|
"""
|
||||||
|
self.setup_world = setup_world
|
||||||
|
self.random_seed = random_seed
|
||||||
|
self.world = None
|
||||||
|
self.tps_history = []
|
||||||
|
self._running = False
|
||||||
|
self.ticks_elapsed_time = None # Track time for designated ticks
|
||||||
|
|
||||||
|
def set_random_seed(self, seed):
|
||||||
|
self.random_seed = seed
|
||||||
|
random.seed(seed)
|
||||||
|
|
||||||
|
def start(self, ticks=100, max_seconds=None):
|
||||||
|
self.set_random_seed(self.random_seed)
|
||||||
|
self.world = self.setup_world(self.random_seed)
|
||||||
|
self.tps_history.clear()
|
||||||
|
self._running = True
|
||||||
|
|
||||||
|
tick_count = 0
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
last_time = start_time
|
||||||
|
|
||||||
|
# For precise tick timing
|
||||||
|
tick_timing_start = None
|
||||||
|
|
||||||
|
if ticks is not None:
|
||||||
|
tick_timing_start = time.perf_counter()
|
||||||
|
|
||||||
|
while self._running and (ticks is None or tick_count < ticks):
|
||||||
|
self.world.tick_all()
|
||||||
|
tick_count += 1
|
||||||
|
now = time.perf_counter()
|
||||||
|
elapsed = now - last_time
|
||||||
|
if elapsed > 0:
|
||||||
|
self.tps_history.append(1.0 / elapsed)
|
||||||
|
last_time = now
|
||||||
|
if max_seconds and (now - start_time) > max_seconds:
|
||||||
|
break
|
||||||
|
|
||||||
|
if ticks is not None:
|
||||||
|
tick_timing_end = time.perf_counter()
|
||||||
|
self.ticks_elapsed_time = tick_timing_end - tick_timing_start
|
||||||
|
else:
|
||||||
|
self.ticks_elapsed_time = None
|
||||||
|
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
def get_tps_history(self):
|
||||||
|
return self.tps_history
|
||||||
|
|
||||||
|
def get_tps_average(self):
|
||||||
|
return statistics.mean(self.tps_history) if self.tps_history else 0.0
|
||||||
|
|
||||||
|
def get_tps_stddev(self):
|
||||||
|
return statistics.stdev(self.tps_history) if len(self.tps_history) > 1 else 0.0
|
||||||
|
|
||||||
|
def get_simulation_hash(self):
|
||||||
|
# Serialize the world state and hash it for determinism checks
|
||||||
|
state = []
|
||||||
|
for obj in self.world.get_objects():
|
||||||
|
state.append((
|
||||||
|
type(obj).__name__,
|
||||||
|
getattr(obj, "position", None),
|
||||||
|
getattr(obj, "rotation", None),
|
||||||
|
getattr(obj, "flags", None),
|
||||||
|
getattr(obj, "interaction_radius", None),
|
||||||
|
getattr(obj, "max_visual_width", None),
|
||||||
|
))
|
||||||
|
state_bytes = pickle.dumps(state)
|
||||||
|
return hashlib.sha256(state_bytes).hexdigest()
|
||||||
|
|
||||||
|
def get_summary(self):
|
||||||
|
return {
|
||||||
|
"tps_avg": self.get_tps_average(),
|
||||||
|
"tps_stddev": self.get_tps_stddev(),
|
||||||
|
"ticks": len(self.tps_history),
|
||||||
|
"simulation_hash": self.get_simulation_hash(),
|
||||||
|
"ticks_elapsed_time": self.ticks_elapsed_time,
|
||||||
|
}
|
||||||
57
tests/test_determinism.py
Normal file
57
tests/test_determinism.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import pytest
|
||||||
|
import random
|
||||||
|
|
||||||
|
from world.world import World, Position, Rotation
|
||||||
|
from world.objects import FoodObject, DefaultCell
|
||||||
|
from tests.benchmarking import HeadlessSimulationBenchmark
|
||||||
|
|
||||||
|
# Hardcoded simulation parameters (copied from config/constants.py)
|
||||||
|
CELL_SIZE = 20
|
||||||
|
GRID_WIDTH = 30
|
||||||
|
GRID_HEIGHT = 25
|
||||||
|
FOOD_OBJECTS_COUNT = 500
|
||||||
|
RANDOM_SEED = 12345
|
||||||
|
|
||||||
|
def _setup_world(seed=RANDOM_SEED):
|
||||||
|
world = World(CELL_SIZE, (CELL_SIZE * GRID_WIDTH, CELL_SIZE * GRID_HEIGHT))
|
||||||
|
random.seed(seed)
|
||||||
|
|
||||||
|
half_width = GRID_WIDTH * CELL_SIZE // 2
|
||||||
|
half_height = GRID_HEIGHT * CELL_SIZE // 2
|
||||||
|
|
||||||
|
for _ in range(FOOD_OBJECTS_COUNT):
|
||||||
|
x = random.randint(-half_width, half_width)
|
||||||
|
y = random.randint(-half_height, half_height)
|
||||||
|
world.add_object(FoodObject(Position(x=x, y=y)))
|
||||||
|
|
||||||
|
for _ in range(300):
|
||||||
|
new_cell = DefaultCell(
|
||||||
|
Position(x=random.randint(-half_width, half_width), y=random.randint(-half_height, half_height)),
|
||||||
|
Rotation(angle=0)
|
||||||
|
)
|
||||||
|
new_cell.behavioral_model = new_cell.behavioral_model.mutate(3)
|
||||||
|
world.add_object(new_cell)
|
||||||
|
|
||||||
|
return world
|
||||||
|
|
||||||
|
def test_simulation_determinism():
|
||||||
|
bench1 = HeadlessSimulationBenchmark(lambda seed: _setup_world(seed), random_seed=RANDOM_SEED)
|
||||||
|
bench2 = HeadlessSimulationBenchmark(lambda seed: _setup_world(seed), random_seed=RANDOM_SEED)
|
||||||
|
|
||||||
|
bench1.start(ticks=100)
|
||||||
|
bench2.start(ticks=100)
|
||||||
|
|
||||||
|
hash1 = bench1.get_simulation_hash()
|
||||||
|
hash2 = bench2.get_simulation_hash()
|
||||||
|
|
||||||
|
assert hash1 == hash2, f"Simulation hashes differ: {hash1} != {hash2}"
|
||||||
|
|
||||||
|
def test_simulation_benchmark():
|
||||||
|
bench = HeadlessSimulationBenchmark(lambda seed: _setup_world(seed), random_seed=RANDOM_SEED+1)
|
||||||
|
tick_count = 100
|
||||||
|
bench.start(ticks=tick_count)
|
||||||
|
summary = bench.get_summary()
|
||||||
|
print(f"{tick_count} ticks took {summary.get('ticks_elapsed_time', 0):.4f} seconds, TPS avg: {summary['tps_avg']:.2f}, stddev: {summary['tps_stddev']:.2f}")
|
||||||
|
|
||||||
|
assert summary['tps_avg'] > 0, "Average TPS should be greater than zero"
|
||||||
|
assert summary['ticks_elapsed_time'] > 0, "Elapsed time should be greater than zero"
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from world.world import World, Position, BaseEntity
|
from world.world import World, Position, BaseEntity, Rotation
|
||||||
|
|
||||||
|
|
||||||
class DummyEntity(BaseEntity):
|
class DummyEntity(BaseEntity):
|
||||||
def __init__(self, position):
|
def __init__(self, position, rotation=None):
|
||||||
super().__init__(position)
|
if rotation is None:
|
||||||
|
rotation = Rotation(angle=0)
|
||||||
|
super().__init__(position, rotation)
|
||||||
self.ticked = False
|
self.ticked = False
|
||||||
self.rendered = False
|
self.rendered = False
|
||||||
|
|
||||||
@ -83,9 +85,6 @@ def test_tick_all_calls_tick(world):
|
|||||||
|
|
||||||
def test_add_object_out_of_bounds(world):
|
def test_add_object_out_of_bounds(world):
|
||||||
entity = DummyEntity(Position(x=1000, y=1000))
|
entity = DummyEntity(Position(x=1000, y=1000))
|
||||||
|
|
||||||
world.add_object(entity)
|
world.add_object(entity)
|
||||||
|
|
||||||
entity = world.get_objects()[0]
|
entity = world.get_objects()[0]
|
||||||
|
assert entity.position.x == 49 and entity.position.y == 49
|
||||||
assert entity.position.x == 49 and entity.position.y == 49
|
|
||||||
217
ui/hud.py
217
ui/hud.py
@ -6,18 +6,188 @@ import pygame_gui
|
|||||||
from config.constants import *
|
from config.constants import *
|
||||||
from world.base.brain import CellBrain, FlexibleNeuralNetwork
|
from world.base.brain import CellBrain, FlexibleNeuralNetwork
|
||||||
from world.objects import DefaultCell
|
from world.objects import DefaultCell
|
||||||
|
from pygame_gui.elements import UIPanel
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
DARK_GRAY = (40, 40, 40)
|
||||||
|
DARKER_GRAY = (25, 25, 25)
|
||||||
|
|
||||||
class HUD:
|
class HUD:
|
||||||
def __init__(self, ui_manager, screen_width=SCREEN_WIDTH, screen_height=SCREEN_HEIGHT):
|
def __init__(self, ui_manager, screen_width=SCREEN_WIDTH, screen_height=SCREEN_HEIGHT):
|
||||||
self.font = pygame.font.Font("freesansbold.ttf", FONT_SIZE)
|
self.font = pygame.font.Font("freesansbold.ttf", FONT_SIZE)
|
||||||
self.legend_font = pygame.font.Font("freesansbold.ttf", LEGEND_FONT_SIZE)
|
self.legend_font = pygame.font.Font("freesansbold.ttf", LEGEND_FONT_SIZE)
|
||||||
|
|
||||||
|
self.manager = ui_manager
|
||||||
self.screen_width = screen_width
|
self.screen_width = screen_width
|
||||||
self.screen_height = screen_height
|
self.screen_height = screen_height
|
||||||
|
|
||||||
self.manager = ui_manager
|
# Panel size defaults
|
||||||
|
self.control_bar_height = 48
|
||||||
|
self.inspector_width = 260
|
||||||
|
self.properties_width = 320
|
||||||
|
self.console_height = 120
|
||||||
|
self.splitter_thickness = 6
|
||||||
|
|
||||||
|
self.dragging_splitter = None
|
||||||
|
self._create_panels()
|
||||||
|
|
||||||
|
def _create_panels(self):
|
||||||
|
# Top control bar
|
||||||
|
self.control_bar = UIPanel(
|
||||||
|
relative_rect=pygame.Rect(0, 0, self.screen_width, self.control_bar_height),
|
||||||
|
manager=self.manager,
|
||||||
|
object_id="#control_bar",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Left inspector
|
||||||
|
self.inspector_panel = UIPanel(
|
||||||
|
relative_rect=pygame.Rect(
|
||||||
|
0, self.control_bar_height,
|
||||||
|
self.inspector_width,
|
||||||
|
self.screen_height - self.control_bar_height
|
||||||
|
),
|
||||||
|
manager=self.manager,
|
||||||
|
object_id="#inspector_panel",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Right properties
|
||||||
|
self.properties_panel = UIPanel(
|
||||||
|
relative_rect=pygame.Rect(
|
||||||
|
self.screen_width - self.properties_width,
|
||||||
|
self.control_bar_height,
|
||||||
|
self.properties_width,
|
||||||
|
self.screen_height - self.control_bar_height
|
||||||
|
),
|
||||||
|
manager=self.manager,
|
||||||
|
object_id="#properties_panel",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bottom console
|
||||||
|
self.console_panel = UIPanel(
|
||||||
|
relative_rect=pygame.Rect(
|
||||||
|
self.inspector_width,
|
||||||
|
self.screen_height - self.console_height,
|
||||||
|
self.screen_width - self.inspector_width - self.properties_width,
|
||||||
|
self.console_height
|
||||||
|
),
|
||||||
|
manager=self.manager,
|
||||||
|
object_id="#console_panel",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.panels = [
|
||||||
|
self.control_bar,
|
||||||
|
self.inspector_panel,
|
||||||
|
self.properties_panel,
|
||||||
|
self.console_panel
|
||||||
|
]
|
||||||
|
self.dragging_splitter = None
|
||||||
|
|
||||||
|
def get_viewport_rect(self):
|
||||||
|
# Returns the rect for the simulation viewport
|
||||||
|
x = self.inspector_width
|
||||||
|
y = self.control_bar_height
|
||||||
|
w = self.screen_width - self.inspector_width - self.properties_width
|
||||||
|
h = self.screen_height - self.control_bar_height - self.console_height
|
||||||
|
return pygame.Rect(x, y, w, h)
|
||||||
|
|
||||||
|
def update_layout(self, window_width, window_height):
|
||||||
|
self.screen_width = window_width
|
||||||
|
self.screen_height = window_height
|
||||||
|
|
||||||
|
# Control bar (top)
|
||||||
|
self.control_bar.set_relative_position((0, 0))
|
||||||
|
self.control_bar.set_dimensions((self.screen_width, self.control_bar_height))
|
||||||
|
|
||||||
|
# Inspector panel (left) - goes all the way to the bottom
|
||||||
|
self.inspector_panel.set_relative_position((0, self.control_bar_height))
|
||||||
|
self.inspector_panel.set_dimensions((self.inspector_width, self.screen_height - self.control_bar_height))
|
||||||
|
|
||||||
|
# Properties panel (right) - goes all the way to the bottom
|
||||||
|
self.properties_panel.set_relative_position(
|
||||||
|
(self.screen_width - self.properties_width, self.control_bar_height))
|
||||||
|
self.properties_panel.set_dimensions((self.properties_width, self.screen_height - self.control_bar_height))
|
||||||
|
|
||||||
|
# Console panel (bottom, spans between inspector and properties)
|
||||||
|
self.console_panel.set_relative_position((self.inspector_width, self.screen_height - self.console_height))
|
||||||
|
self.console_panel.set_dimensions(
|
||||||
|
(self.screen_width - self.inspector_width - self.properties_width, self.console_height))
|
||||||
|
|
||||||
|
def process_event(self, event):
|
||||||
|
# Handle splitter dragging for resizing panels
|
||||||
|
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||||
|
mx, my = event.pos
|
||||||
|
# Check if mouse is on a splitter (left/right/bottom)
|
||||||
|
if abs(mx - self.inspector_width) < self.splitter_thickness and self.control_bar_height < my < self.screen_height - self.console_height:
|
||||||
|
self.dragging_splitter = "inspector"
|
||||||
|
elif abs(mx - (self.screen_width - self.properties_width)) < self.splitter_thickness and self.control_bar_height < my < self.screen_height - self.console_height:
|
||||||
|
self.dragging_splitter = "properties"
|
||||||
|
elif abs(my - (self.screen_height - self.console_height)) < self.splitter_thickness and self.inspector_width < mx < self.screen_width - self.properties_width:
|
||||||
|
self.dragging_splitter = "console"
|
||||||
|
self.update_layout(self.screen_width, self.screen_height)
|
||||||
|
elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
|
||||||
|
self.dragging_splitter = None
|
||||||
|
elif event.type == pygame.MOUSEMOTION and self.dragging_splitter:
|
||||||
|
mx, my = event.pos
|
||||||
|
if self.dragging_splitter == "inspector":
|
||||||
|
self.inspector_width = max(100, min(mx, self.screen_width - self.properties_width - 100))
|
||||||
|
elif self.dragging_splitter == "properties":
|
||||||
|
self.properties_width = max(100, min(self.screen_width - mx, self.screen_width - self.inspector_width - 100))
|
||||||
|
elif self.dragging_splitter == "console":
|
||||||
|
self.console_height = max(60, min(self.screen_height - my, self.screen_height - self.control_bar_height - 60))
|
||||||
|
self.update_layout(self.screen_width, self.screen_height)
|
||||||
|
|
||||||
|
def draw_splitters(self, screen):
|
||||||
|
# Draw draggable splitters for visual feedback
|
||||||
|
indicator_color = (220, 220, 220)
|
||||||
|
indicator_size = 6 # Length of indicator line
|
||||||
|
indicator_gap = 4 # Gap between indicator lines
|
||||||
|
indicator_count = 3 # Number of indicator lines
|
||||||
|
|
||||||
|
# Vertical splitter (inspector/properties)
|
||||||
|
# Inspector/properties only if wide enough
|
||||||
|
if self.inspector_width > 0:
|
||||||
|
x = self.inspector_width - 2
|
||||||
|
y1 = self.control_bar_height
|
||||||
|
y2 = self.screen_height - self.console_height
|
||||||
|
# Draw indicator (horizontal lines) in the middle
|
||||||
|
mid_y = (y1 + y2) // 2
|
||||||
|
for i in range(indicator_count):
|
||||||
|
offset = (i - 1) * (indicator_gap + 1)
|
||||||
|
pygame.draw.line(
|
||||||
|
screen, indicator_color,
|
||||||
|
(x - indicator_size // 2, mid_y + offset),
|
||||||
|
(x + indicator_size // 2, mid_y + offset),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.properties_width > 0:
|
||||||
|
x = self.screen_width - self.properties_width + 2
|
||||||
|
y1 = self.control_bar_height
|
||||||
|
y2 = self.screen_height - self.console_height
|
||||||
|
mid_y = (y1 + y2) // 2
|
||||||
|
for i in range(indicator_count):
|
||||||
|
offset = (i - 1) * (indicator_gap + 1)
|
||||||
|
pygame.draw.line(
|
||||||
|
screen, indicator_color,
|
||||||
|
(x - indicator_size // 2, mid_y + offset),
|
||||||
|
(x + indicator_size // 2, mid_y + offset),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Horizontal splitter (console)
|
||||||
|
if self.console_height > 0:
|
||||||
|
y = self.screen_height - self.console_height + 2
|
||||||
|
x1 = self.inspector_width
|
||||||
|
x2 = self.screen_width - self.properties_width
|
||||||
|
mid_x = (x1 + x2) // 2
|
||||||
|
for i in range(indicator_count):
|
||||||
|
offset = (i - 1) * (indicator_gap + 1)
|
||||||
|
pygame.draw.line(
|
||||||
|
screen, indicator_color,
|
||||||
|
(mid_x + offset, y - indicator_size // 2),
|
||||||
|
(mid_x + offset, y + indicator_size // 2),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
|
||||||
def render_mouse_position(self, screen, camera, sim_view_rect):
|
def render_mouse_position(self, screen, camera, sim_view_rect):
|
||||||
"""Render mouse position in top left."""
|
"""Render mouse position in top left."""
|
||||||
@ -145,6 +315,7 @@ class HUD:
|
|||||||
VIZ_WIDTH = 280 # Width of the neural network visualization area
|
VIZ_WIDTH = 280 # Width of the neural network visualization area
|
||||||
VIZ_HEIGHT = 300 # Height of the neural network visualization area
|
VIZ_HEIGHT = 300 # Height of the neural network visualization area
|
||||||
VIZ_RIGHT_MARGIN = VIZ_WIDTH + 50 # Distance from right edge of screen to visualization
|
VIZ_RIGHT_MARGIN = VIZ_WIDTH + 50 # Distance from right edge of screen to visualization
|
||||||
|
VIZ_BOTTOM_MARGIN = 50 # Distance from the bottom of the screen
|
||||||
|
|
||||||
# Background styling constants
|
# Background styling constants
|
||||||
BACKGROUND_PADDING = 30 # Padding around the visualization background
|
BACKGROUND_PADDING = 30 # Padding around the visualization background
|
||||||
@ -196,6 +367,9 @@ class HUD:
|
|||||||
TOOLTIP_MARGIN = 10
|
TOOLTIP_MARGIN = 10
|
||||||
TOOLTIP_LINE_SPACING = 0 # No extra spacing between lines
|
TOOLTIP_LINE_SPACING = 0 # No extra spacing between lines
|
||||||
|
|
||||||
|
if self.properties_width < VIZ_RIGHT_MARGIN + 50:
|
||||||
|
self.properties_width = VIZ_RIGHT_MARGIN + 50 # Ensure properties panel is wide enough for tooltip
|
||||||
|
|
||||||
if not hasattr(cell, 'behavioral_model'):
|
if not hasattr(cell, 'behavioral_model'):
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -206,9 +380,9 @@ class HUD:
|
|||||||
|
|
||||||
network: FlexibleNeuralNetwork = cell_brain.neural_network
|
network: FlexibleNeuralNetwork = cell_brain.neural_network
|
||||||
|
|
||||||
# Calculate visualization position
|
# Calculate visualization position (bottom right)
|
||||||
viz_x = self.screen_width - VIZ_RIGHT_MARGIN # Right side of screen
|
viz_x = self.screen_width - VIZ_RIGHT_MARGIN # Right side of screen
|
||||||
viz_y = (self.screen_height // 2) - (VIZ_HEIGHT // 2) # Centered vertically
|
viz_y = self.screen_height - VIZ_HEIGHT - VIZ_BOTTOM_MARGIN # Above the bottom margin
|
||||||
|
|
||||||
layer_spacing = VIZ_WIDTH // max(1, len(network.layers) - 1) if len(network.layers) > 1 else VIZ_WIDTH
|
layer_spacing = VIZ_WIDTH // max(1, len(network.layers) - 1) if len(network.layers) > 1 else VIZ_WIDTH
|
||||||
|
|
||||||
@ -218,6 +392,8 @@ class HUD:
|
|||||||
pygame.draw.rect(screen, BACKGROUND_COLOR, background_rect)
|
pygame.draw.rect(screen, BACKGROUND_COLOR, background_rect)
|
||||||
pygame.draw.rect(screen, WHITE, background_rect, BACKGROUND_BORDER_WIDTH)
|
pygame.draw.rect(screen, WHITE, background_rect, BACKGROUND_BORDER_WIDTH)
|
||||||
|
|
||||||
|
info = network.get_structure_info()
|
||||||
|
|
||||||
# Title
|
# Title
|
||||||
title_text = self.font.render("Neural Network", True, WHITE)
|
title_text = self.font.render("Neural Network", True, WHITE)
|
||||||
title_rect = title_text.get_rect()
|
title_rect = title_text.get_rect()
|
||||||
@ -225,6 +401,13 @@ class HUD:
|
|||||||
title_rect.top = viz_y - TITLE_TOP_MARGIN
|
title_rect.top = viz_y - TITLE_TOP_MARGIN
|
||||||
screen.blit(title_text, title_rect)
|
screen.blit(title_text, title_rect)
|
||||||
|
|
||||||
|
# Render network cost under the title
|
||||||
|
cost_text = self.font.render(f"Cost: {info['network_cost']}", True, WHITE)
|
||||||
|
cost_rect = cost_text.get_rect()
|
||||||
|
cost_rect.centerx = title_rect.centerx
|
||||||
|
cost_rect.top = title_rect.bottom + 4 # Small gap below the title
|
||||||
|
screen.blit(cost_text, cost_rect)
|
||||||
|
|
||||||
# Get current activations by running a forward pass with current inputs
|
# Get current activations by running a forward pass with current inputs
|
||||||
input_values = [cell_brain.inputs[key] for key in cell_brain.input_keys]
|
input_values = [cell_brain.inputs[key] for key in cell_brain.input_keys]
|
||||||
|
|
||||||
@ -383,22 +566,6 @@ class HUD:
|
|||||||
label_rect.bottom = viz_y + VIZ_HEIGHT + LAYER_LABEL_BOTTOM_MARGIN
|
label_rect.bottom = viz_y + VIZ_HEIGHT + LAYER_LABEL_BOTTOM_MARGIN
|
||||||
screen.blit(label_text, label_rect)
|
screen.blit(label_text, label_rect)
|
||||||
|
|
||||||
# Draw network info
|
|
||||||
info = network.get_structure_info()
|
|
||||||
info_lines = [
|
|
||||||
f"Layers: {info['total_layers']}",
|
|
||||||
f"Neurons: {info['total_neurons']}",
|
|
||||||
f"Connections: {info['total_connections']}",
|
|
||||||
f"Network Cost: {info['network_cost']}",
|
|
||||||
]
|
|
||||||
|
|
||||||
for i, line in enumerate(info_lines):
|
|
||||||
info_text = self.legend_font.render(line, True, WHITE)
|
|
||||||
info_rect = info_text.get_rect()
|
|
||||||
info_rect.left = viz_x
|
|
||||||
info_rect.top = viz_y + VIZ_HEIGHT + INFO_TEXT_TOP_MARGIN + i * INFO_TEXT_LINE_SPACING
|
|
||||||
screen.blit(info_text, info_rect)
|
|
||||||
|
|
||||||
# --- Tooltip logic for neuron hover ---
|
# --- Tooltip logic for neuron hover ---
|
||||||
mouse_x, mouse_y = pygame.mouse.get_pos()
|
mouse_x, mouse_y = pygame.mouse.get_pos()
|
||||||
tooltip_text = None
|
tooltip_text = None
|
||||||
@ -474,22 +641,20 @@ class HUD:
|
|||||||
screen.blit(surf, (tooltip_rect.left + TOOLTIP_PADDING_X, y))
|
screen.blit(surf, (tooltip_rect.left + TOOLTIP_PADDING_X, y))
|
||||||
y += surf.get_height() + TOOLTIP_LINE_SPACING
|
y += surf.get_height() + TOOLTIP_LINE_SPACING
|
||||||
|
|
||||||
def render_sprint_debug(self, screen, actual_tps, total_ticks):
|
def render_sprint_debug(self, screen, actual_tps, total_ticks, cell_count=None):
|
||||||
"""Render sprint debug info: header, TPS, and tick count."""
|
"""Render sprint debug info: header, TPS, and tick count."""
|
||||||
header = self.font.render("Sprinting...", True, (255, 200, 0))
|
header = self.font.render("Sprinting...", True, (255, 200, 0))
|
||||||
tps_text = self.font.render(f"TPS: {actual_tps}", True, (255, 255, 255))
|
tps_text = self.font.render(f"TPS: {actual_tps}", True, (255, 255, 255))
|
||||||
ticks_text = self.font.render(f"Ticks: {total_ticks}", True, (255, 255, 255))
|
ticks_text = self.font.render(f"Ticks: {total_ticks}", True, (255, 255, 255))
|
||||||
|
cell_text = self.font.render(f"Cells: {cell_count}" if cell_count is not None else "Cells: N/A", True, (255, 255, 255))
|
||||||
|
|
||||||
y = self.screen_height // 2 - 40
|
y = self.screen_height // 2 - 80
|
||||||
header_rect = header.get_rect(center=(self.screen_width // 2, y))
|
header_rect = header.get_rect(center=(self.screen_width // 2, y))
|
||||||
tps_rect = tps_text.get_rect(center=(self.screen_width // 2, y + 40))
|
tps_rect = tps_text.get_rect(center=(self.screen_width // 2, y + 40))
|
||||||
ticks_rect = ticks_text.get_rect(center=(self.screen_width // 2, y + 80))
|
ticks_rect = ticks_text.get_rect(center=(self.screen_width // 2, y + 80))
|
||||||
|
cell_rect = cell_text.get_rect(center=(self.screen_width // 2, y + 120))
|
||||||
|
|
||||||
screen.blit(header, header_rect)
|
screen.blit(header, header_rect)
|
||||||
screen.blit(tps_text, tps_rect)
|
screen.blit(tps_text, tps_rect)
|
||||||
screen.blit(ticks_text, ticks_rect)
|
screen.blit(ticks_text, ticks_rect)
|
||||||
|
screen.blit(cell_text, cell_rect)
|
||||||
def update_layout(self, window_width, window_height):
|
|
||||||
"""Update HUD layout on window resize."""
|
|
||||||
self.screen_width = window_width
|
|
||||||
self.screen_height = window_height
|
|
||||||
|
|||||||
21
uv.lock
generated
21
uv.lock
generated
@ -53,6 +53,7 @@ dependencies = [
|
|||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "psutil" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -67,7 +68,10 @@ requires-dist = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [{ name = "ruff", specifier = ">=0.11.12" }]
|
dev = [
|
||||||
|
{ name = "psutil", specifier = ">=7.0.0" },
|
||||||
|
{ name = "ruff", specifier = ">=0.11.12" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
@ -206,6 +210,21 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
|
{ url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psutil"
|
||||||
|
version = "7.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.11.5"
|
version = "2.11.5"
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import pygame
|
|||||||
from typing import Optional, List, Any, Union
|
from typing import Optional, List, Any, Union
|
||||||
|
|
||||||
from world.utils import get_distance_between_objects
|
from world.utils import get_distance_between_objects
|
||||||
|
from world.physics import Physics
|
||||||
|
|
||||||
from math import atan2, degrees
|
from math import atan2, degrees
|
||||||
|
|
||||||
@ -263,6 +264,8 @@ class DefaultCell(BaseEntity):
|
|||||||
|
|
||||||
self.tick_count = 0
|
self.tick_count = 0
|
||||||
|
|
||||||
|
self.physics = Physics(0.02, 0.05)
|
||||||
|
|
||||||
|
|
||||||
def set_brain(self, behavioral_model: CellBrain) -> None:
|
def set_brain(self, behavioral_model: CellBrain) -> None:
|
||||||
self.behavioral_model = behavioral_model
|
self.behavioral_model = behavioral_model
|
||||||
@ -297,7 +300,7 @@ class DefaultCell(BaseEntity):
|
|||||||
distance_to_food = get_distance_between_objects(self, food_object)
|
distance_to_food = get_distance_between_objects(self, food_object)
|
||||||
|
|
||||||
if distance_to_food < self.max_visual_width and food_objects:
|
if distance_to_food < self.max_visual_width and food_objects:
|
||||||
self.energy += 110
|
self.energy += 130
|
||||||
food_object.flag_for_death()
|
food_object.flag_for_death()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -312,10 +315,10 @@ class DefaultCell(BaseEntity):
|
|||||||
duplicate_y_2 += random.randint(-self.max_visual_width, self.max_visual_width)
|
duplicate_y_2 += random.randint(-self.max_visual_width, self.max_visual_width)
|
||||||
|
|
||||||
new_cell = DefaultCell(Position(x=int(duplicate_x), y=int(duplicate_y)), Rotation(angle=random.randint(0, 359)))
|
new_cell = DefaultCell(Position(x=int(duplicate_x), y=int(duplicate_y)), Rotation(angle=random.randint(0, 359)))
|
||||||
new_cell.set_brain(self.behavioral_model.mutate(0.4))
|
new_cell.set_brain(self.behavioral_model.mutate(0.05))
|
||||||
|
|
||||||
new_cell_2 = DefaultCell(Position(x=int(duplicate_x_2), y=int(duplicate_y_2)), Rotation(angle=random.randint(0, 359)))
|
new_cell_2 = DefaultCell(Position(x=int(duplicate_x_2), y=int(duplicate_y_2)), Rotation(angle=random.randint(0, 359)))
|
||||||
new_cell_2.set_brain(self.behavioral_model.mutate(0.4))
|
new_cell_2.set_brain(self.behavioral_model.mutate(0.05))
|
||||||
|
|
||||||
return [new_cell, new_cell_2]
|
return [new_cell, new_cell_2]
|
||||||
|
|
||||||
@ -328,44 +331,12 @@ class DefaultCell(BaseEntity):
|
|||||||
|
|
||||||
output_data = self.behavioral_model.tick(input_data)
|
output_data = self.behavioral_model.tick(input_data)
|
||||||
|
|
||||||
# everything below this point is physics simulation and needs to be extracted to a separate class
|
|
||||||
|
|
||||||
# clamp accelerations
|
# clamp accelerations
|
||||||
output_data["linear_acceleration"] = max(-MAX_ACCELERATION, min(MAX_ACCELERATION, output_data["linear_acceleration"]))
|
output_data["linear_acceleration"] = max(-MAX_ACCELERATION, min(MAX_ACCELERATION, output_data["linear_acceleration"]))
|
||||||
output_data["angular_acceleration"] = max(-MAX_ANGULAR_ACCELERATION, min(MAX_ANGULAR_ACCELERATION, output_data["angular_acceleration"]))
|
output_data["angular_acceleration"] = max(-MAX_ANGULAR_ACCELERATION, min(MAX_ANGULAR_ACCELERATION, output_data["angular_acceleration"]))
|
||||||
|
|
||||||
# 2. Apply drag force
|
# request physics data from Physics class
|
||||||
drag_coefficient = 0.02
|
self.velocity, self.acceleration, self.rotational_velocity, self.angular_acceleration = self.physics.move(output_data["linear_acceleration"], output_data["angular_acceleration"], self.rotation.get_rotation())
|
||||||
drag_x = -self.velocity[0] * drag_coefficient
|
|
||||||
drag_y = -self.velocity[1] * drag_coefficient
|
|
||||||
|
|
||||||
# 3. Combine all forces
|
|
||||||
total_linear_accel = output_data["linear_acceleration"]
|
|
||||||
total_linear_accel = max(-0.1, min(0.1, total_linear_accel))
|
|
||||||
|
|
||||||
# 4. Convert to world coordinates
|
|
||||||
x_component = total_linear_accel * math.cos(math.radians(self.rotation.get_rotation()))
|
|
||||||
y_component = total_linear_accel * math.sin(math.radians(self.rotation.get_rotation()))
|
|
||||||
|
|
||||||
# 5. Add drag to total acceleration
|
|
||||||
total_accel_x = x_component + drag_x
|
|
||||||
total_accel_y = y_component + drag_y
|
|
||||||
|
|
||||||
self.acceleration = (total_accel_x, total_accel_y)
|
|
||||||
|
|
||||||
rotational_drag = 0.05
|
|
||||||
self.angular_acceleration = output_data["angular_acceleration"] - self.rotational_velocity * rotational_drag
|
|
||||||
|
|
||||||
# tick acceleration
|
|
||||||
velocity_x = self.velocity[0] + self.acceleration[0]
|
|
||||||
velocity_y = self.velocity[1] + self.acceleration[1]
|
|
||||||
self.velocity = (velocity_x, velocity_y)
|
|
||||||
|
|
||||||
# # clamp velocity
|
|
||||||
speed = math.sqrt(self.velocity[0] ** 2 + self.velocity[1] ** 2)
|
|
||||||
if speed > MAX_VELOCITY:
|
|
||||||
scale = MAX_VELOCITY / speed
|
|
||||||
self.velocity = (self.velocity[0] * scale, self.velocity[1] * scale)
|
|
||||||
|
|
||||||
# tick velocity
|
# tick velocity
|
||||||
x, y = self.position.get_position()
|
x, y = self.position.get_position()
|
||||||
@ -374,19 +345,12 @@ class DefaultCell(BaseEntity):
|
|||||||
|
|
||||||
self.position.set_position(x, y)
|
self.position.set_position(x, y)
|
||||||
|
|
||||||
# tick rotational acceleration
|
|
||||||
self.angular_acceleration = output_data["angular_acceleration"]
|
|
||||||
self.rotational_velocity += self.angular_acceleration
|
|
||||||
|
|
||||||
# clamp rotational velocity
|
|
||||||
self.rotational_velocity = max(-MAX_ROTATIONAL_VELOCITY, min(MAX_ROTATIONAL_VELOCITY, self.rotational_velocity))
|
|
||||||
|
|
||||||
# tick rotational velocity
|
# tick rotational velocity
|
||||||
self.rotation.set_rotation(self.rotation.get_rotation() + self.rotational_velocity)
|
self.rotation.set_rotation(self.rotation.get_rotation() + self.rotational_velocity)
|
||||||
|
|
||||||
movement_cost = abs(output_data["angular_acceleration"]) + abs(output_data["linear_acceleration"])
|
movement_cost = abs(output_data["angular_acceleration"]) + abs(output_data["linear_acceleration"])
|
||||||
|
|
||||||
self.energy -= (self.behavioral_model.neural_network.network_cost * 0.01) + 1 + (0.5 * movement_cost)
|
self.energy -= (self.behavioral_model.neural_network.network_cost * 0.1) + 1.2 + (0.15 * movement_cost)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|||||||
82
world/physics.py
Normal file
82
world/physics.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import math
|
||||||
|
|
||||||
|
from config.constants import MAX_VELOCITY, MAX_ROTATIONAL_VELOCITY
|
||||||
|
|
||||||
|
|
||||||
|
class Physics:
|
||||||
|
"""
|
||||||
|
Simulates basic 2D physics for an object, including linear and rotational motion
|
||||||
|
with drag effects.
|
||||||
|
"""
|
||||||
|
def __init__(self, drag_coefficient: float, rotational_drag: float):
|
||||||
|
"""
|
||||||
|
Initialize the Physics object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
drag_coefficient (float): Linear drag coefficient.
|
||||||
|
rotational_drag (float): Rotational drag coefficient.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.drag_coefficient: float = drag_coefficient
|
||||||
|
self.rotational_drag: float = rotational_drag
|
||||||
|
|
||||||
|
self.velocity: tuple[int, int] = (0, 0)
|
||||||
|
self.acceleration: tuple[int, int] = (0, 0)
|
||||||
|
|
||||||
|
self.rotational_velocity: int = 0
|
||||||
|
self.angular_acceleration: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
def move(self, linear_acceleration: float, angular_acceleration: int, rotational_position):
|
||||||
|
"""
|
||||||
|
Update the object's velocity and acceleration based on input forces and drag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
linear_acceleration (float): The applied linear acceleration.
|
||||||
|
angular_acceleration (int): The applied angular acceleration.
|
||||||
|
rotational_position: The current rotational position in degrees.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: Updated (velocity, acceleration, rotational_velocity, angular_acceleration).
|
||||||
|
"""
|
||||||
|
# Apply drag force
|
||||||
|
drag_coefficient = self.drag_coefficient
|
||||||
|
drag_x = -self.velocity[0] * drag_coefficient
|
||||||
|
drag_y = -self.velocity[1] * drag_coefficient
|
||||||
|
|
||||||
|
# Combine all forces
|
||||||
|
total_linear_accel = linear_acceleration
|
||||||
|
total_linear_accel = max(-0.1, min(0.1, total_linear_accel))
|
||||||
|
|
||||||
|
# Convert to world coordinates
|
||||||
|
x_component = total_linear_accel * math.cos(math.radians(rotational_position))
|
||||||
|
y_component = total_linear_accel * math.sin(math.radians(rotational_position))
|
||||||
|
|
||||||
|
# Add drag to total acceleration
|
||||||
|
total_accel_x = x_component + drag_x
|
||||||
|
total_accel_y = y_component + drag_y
|
||||||
|
|
||||||
|
self.acceleration = (total_accel_x, total_accel_y)
|
||||||
|
|
||||||
|
# Apply drag force to angular acceleration
|
||||||
|
rotational_drag = self.rotational_drag
|
||||||
|
self.angular_acceleration = angular_acceleration - self.rotational_velocity * rotational_drag
|
||||||
|
|
||||||
|
# tick acceleration
|
||||||
|
velocity_x = self.velocity[0] + self.acceleration[0]
|
||||||
|
velocity_y = self.velocity[1] + self.acceleration[1]
|
||||||
|
self.velocity = (velocity_x, velocity_y)
|
||||||
|
|
||||||
|
# clamp velocity
|
||||||
|
speed = math.sqrt(self.velocity[0] ** 2 + self.velocity[1] ** 2)
|
||||||
|
if speed > MAX_VELOCITY:
|
||||||
|
scale = MAX_VELOCITY / speed
|
||||||
|
self.velocity = (self.velocity[0] * scale, self.velocity[1] * scale)
|
||||||
|
|
||||||
|
self.angular_acceleration = angular_acceleration
|
||||||
|
self.rotational_velocity += self.angular_acceleration
|
||||||
|
|
||||||
|
# clamp rotational velocity
|
||||||
|
self.rotational_velocity = max(-MAX_ROTATIONAL_VELOCITY, min(MAX_ROTATIONAL_VELOCITY, self.rotational_velocity))
|
||||||
|
|
||||||
|
return self.velocity, self.acceleration, self.rotational_velocity, self.angular_acceleration
|
||||||
@ -153,6 +153,8 @@ class World:
|
|||||||
|
|
||||||
:param camera: The camera object for coordinate transformation.
|
:param camera: The camera object for coordinate transformation.
|
||||||
:param screen: The Pygame screen surface.
|
:param screen: The Pygame screen surface.
|
||||||
|
|
||||||
|
Time complexity: O(n), where n is the number of objects in the current buffer.
|
||||||
"""
|
"""
|
||||||
for obj_list in self.buffers[self.current_buffer].values():
|
for obj_list in self.buffers[self.current_buffer].values():
|
||||||
for obj in obj_list:
|
for obj in obj_list:
|
||||||
@ -161,6 +163,9 @@ class World:
|
|||||||
def tick_all(self) -> None:
|
def tick_all(self) -> None:
|
||||||
"""
|
"""
|
||||||
Advances all objects in the world by one tick, updating their state and handling interactions.
|
Advances all objects in the world by one tick, updating their state and handling interactions.
|
||||||
|
|
||||||
|
Time complexity: O(N + K) / O(N*M), where N is the number of objects in the current buffer,
|
||||||
|
K is the number of objects that can interact with each object, and M is number of objects in checked cells where C is the number of cells checked within the interaction radius.
|
||||||
"""
|
"""
|
||||||
next_buffer: int = 1 - self.current_buffer
|
next_buffer: int = 1 - self.current_buffer
|
||||||
self.buffers[next_buffer].clear()
|
self.buffers[next_buffer].clear()
|
||||||
@ -208,6 +213,8 @@ class World:
|
|||||||
:param y: Y coordinate of the center.
|
:param y: Y coordinate of the center.
|
||||||
:param radius: Search radius.
|
:param radius: Search radius.
|
||||||
:return: List of objects within the radius.
|
:return: List of objects within the radius.
|
||||||
|
|
||||||
|
Time complexity: O(C * M) / O(N), where C is the number of cells checked within the radius and M is the number of objects in those cells.
|
||||||
"""
|
"""
|
||||||
result: List[BaseEntity] = []
|
result: List[BaseEntity] = []
|
||||||
cell_x, cell_y = int(x // self.partition_size), int(y // self.partition_size)
|
cell_x, cell_y = int(x // self.partition_size), int(y // self.partition_size)
|
||||||
@ -234,6 +241,8 @@ class World:
|
|||||||
:param x2: Maximum X coordinate.
|
:param x2: Maximum X coordinate.
|
||||||
:param y2: Maximum Y coordinate.
|
:param y2: Maximum Y coordinate.
|
||||||
:return: List of objects within the rectangle.
|
:return: List of objects within the rectangle.
|
||||||
|
|
||||||
|
Time complexity: O(C * M) / O(N), where C is the number of cells checked within the rectangle and M is the number of objects in those cells.
|
||||||
"""
|
"""
|
||||||
result: List[BaseEntity] = []
|
result: List[BaseEntity] = []
|
||||||
cell_x1, cell_y1 = (
|
cell_x1, cell_y1 = (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user