Compare commits
	
		
			No commits in common. "master" and "ui-rework" have entirely different histories.
		
	
	
		
	
		
| @ -19,8 +19,8 @@ SELECTION_GRAY = (128, 128, 128, 80) | ||||
| SELECTION_BORDER = (80, 80, 90) | ||||
| 
 | ||||
| # Grid settings | ||||
| GRID_WIDTH = 50 | ||||
| GRID_HEIGHT = 50 | ||||
| GRID_WIDTH = 30 | ||||
| GRID_HEIGHT = 25 | ||||
| CELL_SIZE = 20 | ||||
| RENDER_BUFFER = 50 | ||||
| 
 | ||||
|  | ||||
| @ -5,7 +5,6 @@ import pygame | ||||
| import math | ||||
| from config.constants import * | ||||
| from world.base.brain import CellBrain | ||||
| from world.objects import DefaultCell | ||||
| 
 | ||||
| 
 | ||||
| class Renderer: | ||||
| @ -14,8 +13,11 @@ class Renderer: | ||||
|         self.render_height = render_area.get_height() | ||||
|         self.render_width = render_area.get_width() | ||||
| 
 | ||||
|     def clear_screen(self): | ||||
|     def clear_screen(self, main_screen=None): | ||||
|         """Clear the screen with a black background.""" | ||||
|         if main_screen: | ||||
|             main_screen.fill(BLACK) | ||||
| 
 | ||||
|         self.render_area.fill(BLACK) | ||||
| 
 | ||||
|     def draw_grid(self, camera, showing_grid=True): | ||||
| @ -99,9 +101,6 @@ class Renderer: | ||||
|             return | ||||
| 
 | ||||
|         for obj in world.get_objects(): | ||||
|             if not isinstance(obj, DefaultCell): | ||||
|                 continue | ||||
| 
 | ||||
|             obj_x, obj_y = obj.position.get_position() | ||||
|             radius = obj.interaction_radius | ||||
| 
 | ||||
|  | ||||
| @ -18,29 +18,21 @@ from ui.hud import HUD | ||||
| class SimulationEngine: | ||||
|     def __init__(self): | ||||
|         pygame.init() | ||||
|         self._init_window() | ||||
|         self._init_ui() | ||||
|         self._init_simulation() | ||||
|         self.running = True | ||||
| 
 | ||||
|     def _init_window(self): | ||||
|         info = pygame.display.Info() | ||||
|         self.window_width = int(info.current_w // 1.5) | ||||
|         self.window_height = int(info.current_h // 1.5) | ||||
|         self.screen = pygame.display.set_mode( | ||||
|             (self.window_width, self.window_height), | ||||
|             pygame.RESIZABLE, vsync=1 | ||||
|         ) | ||||
|         self.window_width, self.window_height = info.current_w // 2, info.current_h // 2 | ||||
|         self.screen = pygame.display.set_mode((self.window_width, self.window_height), | ||||
|                                               pygame.RESIZABLE, vsync=1) | ||||
| 
 | ||||
|         self.ui_manager = UIManager((self.window_width, self.window_height)) | ||||
| 
 | ||||
|         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") | ||||
|         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_tps_time = time.perf_counter() | ||||
|         self.tick_counter = 0 | ||||
| @ -50,17 +42,21 @@ class SimulationEngine: | ||||
|         self.world = self._setup_world() | ||||
|         self.input_handler = InputHandler(self.camera, self.world, self.sim_view_rect) | ||||
|         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): | ||||
|         viewport_rect = self.hud.get_viewport_rect() | ||||
|         self.sim_view_width = viewport_rect.width | ||||
|         self.sim_view_height = viewport_rect.height | ||||
|         self.sim_view_width = int(self.window_width * 0.75) | ||||
|         self.sim_view_height = int(self.window_height * 0.75) | ||||
|         self.sim_view = pygame.Surface((self.sim_view_width, self.sim_view_height)) | ||||
|         self.sim_view_rect = self.sim_view.get_rect(topleft=(viewport_rect.left, viewport_rect.top)) | ||||
|         self.sim_view_rect = self.sim_view.get_rect(center=(self.window_width // 2, self.window_height // 2)) | ||||
| 
 | ||||
|         self.ui_manager.set_window_resolution((self.window_width, self.window_height)) | ||||
|         self.renderer = Renderer(self.sim_view) | ||||
| 
 | ||||
|         # Update camera to match new sim_view size | ||||
|         if hasattr(self, 'camera'): | ||||
|             self.camera.screen_width = self.sim_view_width | ||||
|             self.camera.screen_height = self.sim_view_height | ||||
| @ -68,8 +64,6 @@ class SimulationEngine: | ||||
|         if hasattr(self, 'input_handler'): | ||||
|             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 | ||||
|     def _setup_world(): | ||||
| @ -81,32 +75,23 @@ class SimulationEngine: | ||||
| 
 | ||||
|         if FOOD_SPAWNING: | ||||
|             for _ in range(FOOD_OBJECTS_COUNT): | ||||
|                 x = random.randint(-half_width // 2, half_width // 2) | ||||
|                 y = random.randint(-half_height // 2, half_height // 2) | ||||
|                 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(350): | ||||
|             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) | ||||
|             ) | ||||
|         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 _count_cells(self): | ||||
|         count = 0 | ||||
|         for entity in self.world.get_objects(): | ||||
|             if isinstance(entity, DefaultCell): | ||||
|                 count += 1 | ||||
|         return count | ||||
| 
 | ||||
| 
 | ||||
|     def run(self): | ||||
|         print(self.world.current_buffer) | ||||
|         while self.running: | ||||
|             self._handle_frame() | ||||
| 
 | ||||
|         pygame.quit() | ||||
|         sys.exit() | ||||
| 
 | ||||
| @ -114,24 +99,53 @@ class SimulationEngine: | ||||
|         deltatime = self.clock.get_time() / 1000.0 | ||||
|         tick_interval = 1.0 / self.input_handler.tps | ||||
| 
 | ||||
|         # Handle events | ||||
|         events = pygame.event.get() | ||||
|         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: | ||||
|             self._handle_sprint_mode() | ||||
|             # Sprint mode: run as many ticks as possible, skip rendering | ||||
|             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 | ||||
| 
 | ||||
|         # Only process one tick per frame if enough time has passed | ||||
|         if not self.input_handler.is_paused: | ||||
|             current_time = time.perf_counter() | ||||
|             if current_time - self.last_tick_time >= tick_interval: | ||||
|             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 | ||||
| @ -144,87 +158,36 @@ class SimulationEngine: | ||||
|         self._update(deltatime) | ||||
|         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): | ||||
|         keys = pygame.key.get_pressed() | ||||
|         self.input_handler.update_camera(keys, deltatime) | ||||
| 
 | ||||
|     def _render(self): | ||||
|         self.screen.fill(BLACK) | ||||
|         self.renderer.clear_screen() | ||||
|         self.renderer.clear_screen(self.screen) | ||||
|         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) | ||||
| 
 | ||||
|         if not self.hud.dragging_splitter: | ||||
|             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)) | ||||
|         # In core/simulation_engine.py, in _render(): | ||||
|         self.screen.blit(self.sim_view, (self.sim_view_rect.left, self.sim_view_rect.top)) | ||||
| 
 | ||||
|         self.hud.manager.draw_ui(self.screen) | ||||
|         self.hud.draw_splitters(self.screen) | ||||
|         # Draw border around sim_view | ||||
|         border_color = (255, 255, 255)  # White | ||||
|         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_tps(self.screen, self.actual_tps) | ||||
|         # 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_tick_count(self.screen, self.total_ticks) | ||||
|         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_pause_indicator(self.screen, self.input_handler.is_paused) | ||||
| 
 | ||||
|         if self.input_handler.selected_objects: | ||||
|             self.hud.render_neural_network_visualization(self.screen, self.input_handler.selected_objects[0]) | ||||
| 
 | ||||
|         pygame.display.flip() | ||||
|         self.clock.tick(MAX_FPS) | ||||
|         self.clock.tick(MAX_FPS) | ||||
|  | ||||
							
								
								
									
										5
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								main.py
									
									
									
									
									
								
							| @ -1,8 +1,5 @@ | ||||
| from core.simulation_engine import SimulationEngine | ||||
| 
 | ||||
| def main(): | ||||
| if __name__ == "__main__": | ||||
|     engine = SimulationEngine() | ||||
|     engine.run() | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
| @ -14,6 +14,5 @@ dependencies = [ | ||||
| 
 | ||||
| [dependency-groups] | ||||
| dev = [ | ||||
|     "psutil>=7.0.0", | ||||
|     "ruff>=0.11.12", | ||||
| ] | ||||
|  | ||||
| @ -1,93 +0,0 @@ | ||||
| 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, | ||||
|         } | ||||
| @ -1,57 +0,0 @@ | ||||
| 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,12 +1,10 @@ | ||||
| import pytest | ||||
| from world.world import World, Position, BaseEntity, Rotation | ||||
| from world.world import World, Position, BaseEntity | ||||
| 
 | ||||
| 
 | ||||
| class DummyEntity(BaseEntity): | ||||
|     def __init__(self, position, rotation=None): | ||||
|         if rotation is None: | ||||
|             rotation = Rotation(angle=0) | ||||
|         super().__init__(position, rotation) | ||||
|     def __init__(self, position): | ||||
|         super().__init__(position) | ||||
|         self.ticked = False | ||||
|         self.rendered = False | ||||
| 
 | ||||
| @ -85,6 +83,9 @@ def test_tick_all_calls_tick(world): | ||||
| 
 | ||||
| def test_add_object_out_of_bounds(world): | ||||
|     entity = DummyEntity(Position(x=1000, y=1000)) | ||||
| 
 | ||||
|     world.add_object(entity) | ||||
| 
 | ||||
|     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,188 +6,18 @@ import pygame_gui | ||||
| from config.constants import * | ||||
| from world.base.brain import CellBrain, FlexibleNeuralNetwork | ||||
| from world.objects import DefaultCell | ||||
| from pygame_gui.elements import UIPanel | ||||
| import math | ||||
| 
 | ||||
| DARK_GRAY = (40, 40, 40) | ||||
| DARKER_GRAY = (25, 25, 25) | ||||
| 
 | ||||
| class HUD: | ||||
|     def __init__(self, ui_manager, screen_width=SCREEN_WIDTH, screen_height=SCREEN_HEIGHT): | ||||
|         self.font = pygame.font.Font("freesansbold.ttf", FONT_SIZE) | ||||
|         self.legend_font = pygame.font.Font("freesansbold.ttf", LEGEND_FONT_SIZE) | ||||
| 
 | ||||
|         self.manager = ui_manager | ||||
|         self.screen_width = screen_width | ||||
|         self.screen_height = screen_height | ||||
| 
 | ||||
|         # 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 | ||||
|                 ) | ||||
|         self.manager = ui_manager | ||||
| 
 | ||||
|     def render_mouse_position(self, screen, camera, sim_view_rect): | ||||
|         """Render mouse position in top left.""" | ||||
| @ -315,7 +145,6 @@ class HUD: | ||||
|         VIZ_WIDTH = 280  # Width 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_BOTTOM_MARGIN = 50  # Distance from the bottom of the screen | ||||
| 
 | ||||
|         # Background styling constants | ||||
|         BACKGROUND_PADDING = 30  # Padding around the visualization background | ||||
| @ -367,9 +196,6 @@ class HUD: | ||||
|         TOOLTIP_MARGIN = 10 | ||||
|         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'): | ||||
|             return | ||||
| 
 | ||||
| @ -380,9 +206,9 @@ class HUD: | ||||
| 
 | ||||
|         network: FlexibleNeuralNetwork = cell_brain.neural_network | ||||
| 
 | ||||
|         # Calculate visualization position (bottom right) | ||||
|         # Calculate visualization position | ||||
|         viz_x = self.screen_width - VIZ_RIGHT_MARGIN  # Right side of screen | ||||
|         viz_y = self.screen_height - VIZ_HEIGHT - VIZ_BOTTOM_MARGIN  # Above the bottom margin | ||||
|         viz_y = (self.screen_height // 2) - (VIZ_HEIGHT // 2)  # Centered vertically | ||||
| 
 | ||||
|         layer_spacing = VIZ_WIDTH // max(1, len(network.layers) - 1) if len(network.layers) > 1 else VIZ_WIDTH | ||||
| 
 | ||||
| @ -392,8 +218,6 @@ class HUD: | ||||
|         pygame.draw.rect(screen, BACKGROUND_COLOR, background_rect) | ||||
|         pygame.draw.rect(screen, WHITE, background_rect, BACKGROUND_BORDER_WIDTH) | ||||
| 
 | ||||
|         info = network.get_structure_info() | ||||
| 
 | ||||
|         # Title | ||||
|         title_text = self.font.render("Neural Network", True, WHITE) | ||||
|         title_rect = title_text.get_rect() | ||||
| @ -401,13 +225,6 @@ class HUD: | ||||
|         title_rect.top = viz_y - TITLE_TOP_MARGIN | ||||
|         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 | ||||
|         input_values = [cell_brain.inputs[key] for key in cell_brain.input_keys] | ||||
| 
 | ||||
| @ -566,6 +383,22 @@ class HUD: | ||||
|                 label_rect.bottom = viz_y + VIZ_HEIGHT + LAYER_LABEL_BOTTOM_MARGIN | ||||
|                 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 --- | ||||
|         mouse_x, mouse_y = pygame.mouse.get_pos() | ||||
|         tooltip_text = None | ||||
| @ -641,20 +474,22 @@ class HUD: | ||||
|                 screen.blit(surf, (tooltip_rect.left + TOOLTIP_PADDING_X, y)) | ||||
|                 y += surf.get_height() + TOOLTIP_LINE_SPACING | ||||
| 
 | ||||
|     def render_sprint_debug(self, screen, actual_tps, total_ticks, cell_count=None): | ||||
|     def render_sprint_debug(self, screen, actual_tps, total_ticks): | ||||
|         """Render sprint debug info: header, TPS, and tick count.""" | ||||
|         header = self.font.render("Sprinting...", True, (255, 200, 0)) | ||||
|         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)) | ||||
|         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 - 80 | ||||
|         y = self.screen_height // 2 - 40 | ||||
|         header_rect = header.get_rect(center=(self.screen_width // 2, y)) | ||||
|         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)) | ||||
|         cell_rect = cell_text.get_rect(center=(self.screen_width // 2, y + 120)) | ||||
| 
 | ||||
|         screen.blit(header, header_rect) | ||||
|         screen.blit(tps_text, tps_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,7 +53,6 @@ dependencies = [ | ||||
| 
 | ||||
| [package.dev-dependencies] | ||||
| dev = [ | ||||
|     { name = "psutil" }, | ||||
|     { name = "ruff" }, | ||||
| ] | ||||
| 
 | ||||
| @ -68,10 +67,7 @@ requires-dist = [ | ||||
| ] | ||||
| 
 | ||||
| [package.metadata.requires-dev] | ||||
| dev = [ | ||||
|     { name = "psutil", specifier = ">=7.0.0" }, | ||||
|     { name = "ruff", specifier = ">=0.11.12" }, | ||||
| ] | ||||
| dev = [{ name = "ruff", specifier = ">=0.11.12" }] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "filelock" | ||||
| @ -210,21 +206,6 @@ 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" }, | ||||
| ] | ||||
| 
 | ||||
| [[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]] | ||||
| name = "pydantic" | ||||
| version = "2.11.5" | ||||
|  | ||||
| @ -9,7 +9,6 @@ import pygame | ||||
| from typing import Optional, List, Any, Union | ||||
| 
 | ||||
| from world.utils import get_distance_between_objects | ||||
| from world.physics import Physics | ||||
| 
 | ||||
| from math import atan2, degrees | ||||
| 
 | ||||
| @ -264,8 +263,6 @@ class DefaultCell(BaseEntity): | ||||
| 
 | ||||
|         self.tick_count = 0 | ||||
| 
 | ||||
|         self.physics = Physics(0.02, 0.05) | ||||
| 
 | ||||
| 
 | ||||
|     def set_brain(self, behavioral_model: CellBrain) -> None: | ||||
|         self.behavioral_model = behavioral_model | ||||
| @ -300,7 +297,7 @@ class DefaultCell(BaseEntity): | ||||
|         distance_to_food = get_distance_between_objects(self, food_object) | ||||
| 
 | ||||
|         if distance_to_food < self.max_visual_width and food_objects: | ||||
|             self.energy += 130 | ||||
|             self.energy += 110 | ||||
|             food_object.flag_for_death() | ||||
|             return self | ||||
| 
 | ||||
| @ -315,10 +312,10 @@ class DefaultCell(BaseEntity): | ||||
|             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.set_brain(self.behavioral_model.mutate(0.05)) | ||||
|             new_cell.set_brain(self.behavioral_model.mutate(0.4)) | ||||
| 
 | ||||
|             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.05)) | ||||
|             new_cell_2.set_brain(self.behavioral_model.mutate(0.4)) | ||||
| 
 | ||||
|             return [new_cell, new_cell_2] | ||||
| 
 | ||||
| @ -331,12 +328,44 @@ class DefaultCell(BaseEntity): | ||||
| 
 | ||||
|         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 | ||||
|         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"])) | ||||
| 
 | ||||
|         # request physics data from Physics class | ||||
|         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()) | ||||
|         # 2. Apply drag force | ||||
|         drag_coefficient = 0.02 | ||||
|         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 | ||||
|         x, y = self.position.get_position() | ||||
| @ -345,12 +374,19 @@ class DefaultCell(BaseEntity): | ||||
| 
 | ||||
|         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 | ||||
|         self.rotation.set_rotation(self.rotation.get_rotation() + self.rotational_velocity) | ||||
| 
 | ||||
|         movement_cost = abs(output_data["angular_acceleration"]) + abs(output_data["linear_acceleration"]) | ||||
| 
 | ||||
|         self.energy -= (self.behavioral_model.neural_network.network_cost * 0.1) + 1.2 + (0.15 * movement_cost) | ||||
|         self.energy -= (self.behavioral_model.neural_network.network_cost * 0.01) + 1 + (0.5 * movement_cost) | ||||
| 
 | ||||
|         return self | ||||
| 
 | ||||
|  | ||||
| @ -1,82 +0,0 @@ | ||||
| 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,8 +153,6 @@ class World: | ||||
| 
 | ||||
|         :param camera: The camera object for coordinate transformation. | ||||
|         :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 in obj_list: | ||||
| @ -163,9 +161,6 @@ class World: | ||||
|     def tick_all(self) -> None: | ||||
|         """ | ||||
|         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 | ||||
|         self.buffers[next_buffer].clear() | ||||
| @ -213,8 +208,6 @@ class World: | ||||
|         :param y: Y coordinate of the center. | ||||
|         :param radius: Search 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] = [] | ||||
|         cell_x, cell_y = int(x // self.partition_size), int(y // self.partition_size) | ||||
| @ -241,8 +234,6 @@ class World: | ||||
|         :param x2: Maximum X coordinate. | ||||
|         :param y2: Maximum Y coordinate. | ||||
|         :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] = [] | ||||
|         cell_x1, cell_y1 = ( | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user