Compare commits

...

19 Commits

Author SHA1 Message Date
Sam
b9027ab935 Add time complexity annotations to methods in world.py
All checks were successful
Build Simulation and Test / Run All Tests (push) Successful in 1m2s
2025-06-25 01:31:21 -05:00
Sam
b775813cbd adjust food benefit
All checks were successful
Build Simulation and Test / Run All Tests (push) Successful in 1m3s
2025-06-25 00:46:10 -05:00
Sam
bce07db40e adjust mutation rate
All checks were successful
Build Simulation and Test / Run All Tests (push) Successful in 1m7s
2025-06-25 00:34:21 -05:00
Sam
0d95e85d83 remove debug print 2025-06-25 00:32:20 -05:00
Sam
22406420c2 adjust pt 2
All checks were successful
Build Simulation and Test / Run All Tests (push) Successful in 1m5s
2025-06-25 00:31:36 -05:00
Sam
31c3244b5a Adjust energy cost calculation in movement and increase initial cell count in simulation 2025-06-25 00:31:05 -05:00
Sam
8bb5c3edfc Adjust grid dimensions and refine cell mutation parameters for improved simulation behavior
All checks were successful
Build Simulation and Test / Run All Tests (push) Successful in 1m5s
2025-06-25 00:30:05 -05:00
Sam
4e90ecb885 Refine simulation tick handling to ensure consistent timing and update HUD rendering
All checks were successful
Build Simulation and Test / Run All Tests (push) Successful in 1m11s
2025-06-25 00:19:22 -05:00
Sam
8bb669d6b2 Add cell count display to sprint debug info in HUD 2025-06-25 00:11:28 -05:00
Sam
d0f01c0a48 Prevent event queue overflow by pumping pygame events during simulation ticks
All checks were successful
Build Simulation and Test / Run All Tests (push) Successful in 1m5s
2025-06-25 00:09:23 -05:00
Sam
15bc179410 Update grid dimensions and adjust energy cost calculation in movement
Some checks failed
Build Simulation and Test / Run All Tests (push) Failing after 2m14s
2025-06-25 00:05:28 -05:00
Sam
f74aa1f633 Enhance Physics class with detailed docstrings for initialization and movement methods 2025-06-24 23:56:37 -05:00
dee0eaa9f9 moved physics into its own class in physics.py
All checks were successful
Build Simulation and Test / Run All Tests (push) Successful in 1m8s
2025-06-24 11:08:52 -05:00
7289543f6a moved physics into its own class in physics.py
All checks were successful
Build Simulation and Test / Run All Tests (push) Successful in 2m56s
2025-06-24 02:00:54 -05:00
Sam
8f17498b88 Add headless simulation benchmarking and tests for determinism
All checks were successful
Build Simulation and Test / Run All Tests (push) Successful in 2m53s
2025-06-21 18:36:02 -05:00
Sam
f56192ab8f Add HUD splitter rendering to simulation engine
Some checks failed
Build Simulation and Test / Run All Tests (push) Failing after 2m42s
2025-06-19 16:58:21 -05:00
Sam
4a4f7a75c5 Merge branch 'master' of https://git.apisb.me/Thaen/DynamicAbstractionSystem
Some checks failed
Build Simulation and Test / Run All Tests (push) Has been cancelled
# Conflicts:
#	core/simulation_engine.py
#	ui/hud.py
2025-06-19 16:57:43 -05:00
Sam
6f9e1e84f0 Refactor HUD to support dynamic panel resizing and improve layout management 2025-06-19 16:52:38 -05:00
Sam
8c8d8f7925 Add cell count display to sprint debug info and adjust energy cost calculation
Some checks failed
Build Simulation and Test / Run All Tests (push) Failing after 40s
2025-06-18 18:37:11 -05:00
13 changed files with 591 additions and 161 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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
View 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
View 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"

View File

@ -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
View File

@ -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
View File

@ -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"

View File

@ -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
View 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

View File

@ -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 = (