Compare commits

...

4 Commits

5 changed files with 387 additions and 133 deletions

View File

@ -6,7 +6,7 @@ from config.constants import *
class InputHandler: class InputHandler:
def __init__(self, camera, world): def __init__(self, camera, world, sim_view_rect):
self.camera = camera self.camera = camera
self.world = world self.world = world
@ -27,6 +27,13 @@ class InputHandler:
self.default_tps = DEFAULT_TPS self.default_tps = DEFAULT_TPS
self.sprint_mode = False self.sprint_mode = False
# sim-view rect for mouse position calculations
self.sim_view_rect = sim_view_rect
def update_sim_view_rect(self, sim_view_rect):
"""Update the sim_view rectangle."""
self.sim_view_rect = sim_view_rect
def handle_events(self, events, ui_manager): def handle_events(self, events, ui_manager):
"""Process all pygame events and return game state.""" """Process all pygame events and return game state."""
running = True running = True
@ -115,20 +122,27 @@ class InputHandler:
"""Process object selection logic.""" """Process object selection logic."""
self.selecting = False self.selecting = False
# Convert screen to world coordinates # Map screen to sim_view coordinates
x1, y1 = self.camera.get_real_coordinates(*self.select_start) sx1 = self.select_start[0] - self.sim_view_rect.left
x2, y2 = self.camera.get_real_coordinates(*self.select_end) sy1 = self.select_start[1] - self.sim_view_rect.top
sx2 = self.select_end[0] - self.sim_view_rect.left
sy2 = self.select_end[1] - self.sim_view_rect.top
# Convert sim_view to world coordinates
x1, y1 = self.camera.get_real_coordinates(sx1, sy1)
x2, y2 = self.camera.get_real_coordinates(sx2, sy2)
# Check if selection is a click or drag # Check if selection is a click or drag
if (abs(self.select_start[0] - self.select_end[0]) < SELECTION_THRESHOLD and if (abs(sx1 - sx2) < SELECTION_THRESHOLD and
abs(self.select_start[1] - self.select_end[1]) < SELECTION_THRESHOLD): abs(sy1 - sy2) < SELECTION_THRESHOLD):
self._handle_click_selection() self._handle_click_selection()
else: else:
self._handle_drag_selection(x1, y1, x2, y2) self._handle_drag_selection(x1, y1, x2, y2)
def _handle_click_selection(self): def _handle_click_selection(self):
"""Handle single click selection.""" """Handle single click selection."""
mouse_world_x, mouse_world_y = self.camera.get_real_coordinates(*self.select_start) sx, sy = self.select_start[0] - self.sim_view_rect.left, self.select_start[1] - self.sim_view_rect.top
mouse_world_x, mouse_world_y = self.camera.get_real_coordinates(sx, sy)
obj = self.world.query_closest_object(mouse_world_x, mouse_world_y) obj = self.world.query_closest_object(mouse_world_x, mouse_world_y)
self.selected_objects = [] self.selected_objects = []

View File

@ -8,12 +8,14 @@ from world.base.brain import CellBrain
class Renderer: class Renderer:
def __init__(self, screen): def __init__(self, render_area):
self.screen = screen self.render_area = render_area
self.render_height = render_area.get_height()
self.render_width = render_area.get_width()
def clear_screen(self): def clear_screen(self):
"""Clear the screen with a black background.""" """Clear the screen with a black background."""
self.screen.fill(BLACK) self.render_area.fill(BLACK)
def draw_grid(self, camera, showing_grid=True): def draw_grid(self, camera, showing_grid=True):
"""Draw the reference grid.""" """Draw the reference grid."""
@ -28,8 +30,8 @@ class Renderer:
grid_world_height = GRID_HEIGHT * effective_cell_size grid_world_height = GRID_HEIGHT * effective_cell_size
# Calculate grid position relative to camera (with grid centered at 0,0) # Calculate grid position relative to camera (with grid centered at 0,0)
grid_center_x = SCREEN_WIDTH // 2 - camera.x * camera.zoom grid_center_x = self.render_width // 2 - camera.x * camera.zoom
grid_center_y = SCREEN_HEIGHT // 2 - camera.y * camera.zoom grid_center_y = self.render_height // 2 - camera.y * camera.zoom
grid_left = grid_center_x - grid_world_width // 2 grid_left = grid_center_x - grid_world_width // 2
grid_top = grid_center_y - grid_world_height // 2 grid_top = grid_center_y - grid_world_height // 2
@ -37,20 +39,20 @@ class Renderer:
grid_bottom = grid_top + grid_world_height grid_bottom = grid_top + grid_world_height
# Check if grid is visible on screen # Check if grid is visible on screen
if (grid_right < 0 or grid_left > SCREEN_WIDTH or if (grid_right < 0 or grid_left > self.render_width or
grid_bottom < 0 or grid_top > SCREEN_HEIGHT): grid_bottom < 0 or grid_top > self.render_height):
return return
# Fill the grid area with dark gray background # Fill the grid area with dark gray background
grid_rect = pygame.Rect( grid_rect = pygame.Rect(
max(0, grid_left), max(0, grid_left),
max(0, grid_top), max(0, grid_top),
min(SCREEN_WIDTH, grid_right) - max(0, grid_left), min(self.render_width, grid_right) - max(0, grid_left),
min(SCREEN_HEIGHT, grid_bottom) - max(0, grid_top), min(self.render_height, grid_bottom) - max(0, grid_top),
) )
if grid_rect.width > 0 and grid_rect.height > 0: if grid_rect.width > 0 and grid_rect.height > 0:
pygame.draw.rect(self.screen, DARK_GRAY, grid_rect) pygame.draw.rect(self.render_area, DARK_GRAY, grid_rect)
# Draw grid lines only if zoom is high enough # Draw grid lines only if zoom is high enough
if effective_cell_size > 4: if effective_cell_size > 4:
@ -65,30 +67,30 @@ class Renderer:
# Vertical lines # Vertical lines
if i <= GRID_WIDTH: if i <= GRID_WIDTH:
line_x = grid_left + i * effective_cell_size line_x = grid_left + i * effective_cell_size
if 0 <= line_x <= SCREEN_WIDTH: if 0 <= line_x <= self.render_width:
start_y = max(0, grid_top) start_y = max(0, grid_top)
end_y = min(SCREEN_HEIGHT, grid_bottom) end_y = min(self.render_height, grid_bottom)
if start_y < end_y: if start_y < end_y:
vertical_lines.append(((line_x, start_y), (line_x, end_y))) vertical_lines.append(((line_x, start_y), (line_x, end_y)))
# Horizontal lines # Horizontal lines
if i <= GRID_HEIGHT: if i <= GRID_HEIGHT:
line_y = grid_top + i * effective_cell_size line_y = grid_top + i * effective_cell_size
if 0 <= line_y <= SCREEN_HEIGHT: if 0 <= line_y <= self.render_height:
start_x = max(0, grid_left) start_x = max(0, grid_left)
end_x = min(SCREEN_WIDTH, grid_right) end_x = min(self.render_width, grid_right)
if start_x < end_x: if start_x < end_x:
horizontal_lines.append(((start_x, line_y), (end_x, line_y))) horizontal_lines.append(((start_x, line_y), (end_x, line_y)))
# Draw all lines # Draw all lines
for start, end in vertical_lines: for start, end in vertical_lines:
pygame.draw.line(self.screen, GRAY, start, end) pygame.draw.line(self.render_area, GRAY, start, end)
for start, end in horizontal_lines: for start, end in horizontal_lines:
pygame.draw.line(self.screen, GRAY, start, end) pygame.draw.line(self.render_area, GRAY, start, end)
def render_world(self, world, camera): def render_world(self, world, camera):
"""Render all world objects.""" """Render all world objects."""
world.render_all(camera, self.screen) world.render_all(camera, self.render_area)
def render_interaction_radius(self, world, camera, selected_objects, show_radius=False): def render_interaction_radius(self, world, camera, selected_objects, show_radius=False):
"""Render interaction radius and debug vectors for objects.""" """Render interaction radius and debug vectors for objects."""
@ -108,7 +110,7 @@ class Renderer:
if screen_radius > 0: if screen_radius > 0:
# Draw interaction radius circle # Draw interaction radius circle
pygame.draw.circle(self.screen, RED, (screen_x, screen_y), screen_radius, 1) pygame.draw.circle(self.render_area, RED, (screen_x, screen_y), screen_radius, 1)
# Draw direction arrow # Draw direction arrow
self._draw_direction_arrow(obj, screen_x, screen_y, camera) self._draw_direction_arrow(obj, screen_x, screen_y, camera)
@ -125,7 +127,7 @@ class Renderer:
end_y = screen_y + arrow_length * math.sin(math.radians(rotation_angle)) end_y = screen_y + arrow_length * math.sin(math.radians(rotation_angle))
# Draw arrow line # Draw arrow line
pygame.draw.line(self.screen, WHITE, (screen_x, screen_y), (end_x, end_y), 2) pygame.draw.line(self.render_area, WHITE, (screen_x, screen_y), (end_x, end_y), 2)
# Draw arrowhead # Draw arrowhead
tip_size = DIRECTION_TIP_SIZE * camera.zoom tip_size = DIRECTION_TIP_SIZE * camera.zoom
@ -135,7 +137,7 @@ class Renderer:
right_tip_y = end_y - tip_size * math.sin(math.radians(rotation_angle - 150 + 180)) right_tip_y = end_y - tip_size * math.sin(math.radians(rotation_angle - 150 + 180))
pygame.draw.polygon( pygame.draw.polygon(
self.screen, WHITE, self.render_area, WHITE,
[(end_x, end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)] [(end_x, end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)]
) )
@ -167,7 +169,7 @@ class Renderer:
angular_acc_end_x = end_x + angular_accel_magnitude * math.cos(math.radians(angular_direction)) angular_acc_end_x = end_x + angular_accel_magnitude * math.cos(math.radians(angular_direction))
angular_acc_end_y = end_y + angular_accel_magnitude * math.sin(math.radians(angular_direction)) angular_acc_end_y = end_y + angular_accel_magnitude * math.sin(math.radians(angular_direction))
pygame.draw.line(self.screen, LIGHT_BLUE, (end_x, end_y), (angular_acc_end_x, angular_acc_end_y), 2) pygame.draw.line(self.render_area, LIGHT_BLUE, (end_x, end_y), (angular_acc_end_x, angular_acc_end_y), 2)
# Draw arrowhead # Draw arrowhead
self._draw_arrowhead(angular_acc_end_x, angular_acc_end_y, angular_direction, self._draw_arrowhead(angular_acc_end_x, angular_acc_end_y, angular_direction,
@ -184,7 +186,7 @@ class Renderer:
acc_end_x = screen_x + acc_vector_length * math.cos(math.radians(acc_direction)) acc_end_x = screen_x + acc_vector_length * math.cos(math.radians(acc_direction))
acc_end_y = screen_y + acc_vector_length * math.sin(math.radians(acc_direction)) acc_end_y = screen_y + acc_vector_length * math.sin(math.radians(acc_direction))
pygame.draw.line(self.screen, RED, (screen_x, screen_y), (acc_end_x, acc_end_y), 2) pygame.draw.line(self.render_area, RED, (screen_x, screen_y), (acc_end_x, acc_end_y), 2)
self._draw_arrowhead(acc_end_x, acc_end_y, acc_direction, self._draw_arrowhead(acc_end_x, acc_end_y, acc_direction,
ARROW_TIP_SIZE * camera.zoom, RED) ARROW_TIP_SIZE * camera.zoom, RED)
@ -199,7 +201,7 @@ class Renderer:
vel_end_x = screen_x + vel_vector_length * math.cos(math.radians(vel_direction)) vel_end_x = screen_x + vel_vector_length * math.cos(math.radians(vel_direction))
vel_end_y = screen_y + vel_vector_length * math.sin(math.radians(vel_direction)) vel_end_y = screen_y + vel_vector_length * math.sin(math.radians(vel_direction))
pygame.draw.line(self.screen, BLUE, (screen_x, screen_y), (vel_end_x, vel_end_y), 2) pygame.draw.line(self.render_area, BLUE, (screen_x, screen_y), (vel_end_x, vel_end_y), 2)
self._draw_arrowhead(vel_end_x, vel_end_y, vel_direction, self._draw_arrowhead(vel_end_x, vel_end_y, vel_direction,
ARROW_TIP_SIZE * camera.zoom, BLUE) ARROW_TIP_SIZE * camera.zoom, BLUE)
@ -211,24 +213,29 @@ class Renderer:
right_tip_y = end_y - tip_size * math.sin(math.radians(direction - 150 + 180)) right_tip_y = end_y - tip_size * math.sin(math.radians(direction - 150 + 180))
pygame.draw.polygon( pygame.draw.polygon(
self.screen, color, self.render_area, color,
[(end_x, end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)] [(end_x, end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)]
) )
def render_selection_rectangle(self, selection_rect): def render_selection_rectangle(self, selection_rect, sim_view_rect=None):
"""Render the selection rectangle.""" """Render the selection rectangle, offset for sim_view if sim_view_rect is provided."""
if not selection_rect: if not selection_rect:
return return
left, top, width, height = selection_rect left, top, width, height = selection_rect
# Offset for sim_view if sim_view_rect is given
if sim_view_rect is not None:
left -= sim_view_rect.left
top -= sim_view_rect.top
# Draw semi-transparent fill # Draw semi-transparent fill
s = pygame.Surface((width, height), pygame.SRCALPHA) s = pygame.Surface((width, height), pygame.SRCALPHA)
s.fill(SELECTION_GRAY) s.fill(SELECTION_GRAY)
self.screen.blit(s, (left, top)) self.render_area.blit(s, (left, top))
# Draw border # Draw border
pygame.draw.rect(self.screen, SELECTION_BORDER, pygame.draw.rect(self.render_area, SELECTION_BORDER,
pygame.Rect(left, top, width, height), 1) pygame.Rect(left, top, width, height), 1)
def render_selected_objects_outline(self, selected_objects, camera): def render_selected_objects_outline(self, selected_objects, camera):
@ -239,4 +246,4 @@ class Renderer:
screen_x, screen_y = camera.world_to_screen(obj_x, obj_y) screen_x, screen_y = camera.world_to_screen(obj_x, obj_y)
size = camera.get_relative_size(width) size = camera.get_relative_size(width)
rect = pygame.Rect(screen_x - size // 2, screen_y - size // 2, size, size) rect = pygame.Rect(screen_x - size // 2, screen_y - size // 2, size, size)
pygame.draw.rect(self.screen, SELECTION_BLUE, rect, 1) pygame.draw.rect(self.render_area, SELECTION_BLUE, rect, 1)

View File

@ -3,6 +3,8 @@ import time
import random import random
import sys import sys
from pygame_gui import UIManager
from world.base.brain import CellBrain, FlexibleNeuralNetwork from world.base.brain import CellBrain, FlexibleNeuralNetwork
from world.world import World, Position, Rotation from world.world import World, Position, Rotation
from world.objects import FoodObject, DefaultCell from world.objects import FoodObject, DefaultCell
@ -16,11 +18,29 @@ from ui.hud import HUD
class SimulationEngine: class SimulationEngine:
def __init__(self): def __init__(self):
pygame.init() pygame.init()
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), vsync=1) 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
)
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()
self.camera = Camera(SCREEN_WIDTH, SCREEN_HEIGHT, RENDER_BUFFER)
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
@ -28,11 +48,28 @@ class SimulationEngine:
self.total_ticks = 0 self.total_ticks = 0
self.world = self._setup_world() self.world = self._setup_world()
self.input_handler = InputHandler(self.camera, self.world) self.input_handler = InputHandler(self.camera, self.world, self.sim_view_rect)
self.renderer = Renderer(self.screen) self.renderer = Renderer(self.sim_view)
self.hud = HUD()
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 = 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.ui_manager.set_window_resolution((self.window_width, self.window_height))
self.renderer = Renderer(self.sim_view)
if hasattr(self, 'camera'):
self.camera.screen_width = self.sim_view_width
self.camera.screen_height = self.sim_view_height
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 @staticmethod
def _setup_world(): def _setup_world():
@ -49,10 +86,11 @@ class SimulationEngine:
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(300):
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, half_width), y=random.randint(-half_height, half_height)),
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
@ -68,7 +106,6 @@ class SimulationEngine:
def run(self): def run(self):
while self.running: while self.running:
self._handle_frame() self._handle_frame()
pygame.quit() pygame.quit()
sys.exit() sys.exit()
@ -76,50 +113,16 @@ 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)
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()
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)
return return
if not self.input_handler.is_paused: if not self.input_handler.is_paused:
current_time = time.perf_counter() self._handle_simulation_ticks(tick_interval, deltatime)
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
else: else:
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()
@ -128,26 +131,82 @@ 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
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()
self.hud.render_sprint_debug(self.screen, self.actual_tps, self.total_ticks)
pygame.display.flip()
self.clock.tick(MAX_FPS)
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.screen.fill(BLACK)
self.renderer.clear_screen() self.renderer.clear_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.renderer.render_selected_objects_outline(self.input_handler.selected_objects, self.camera)
self.hud.render_mouse_position(self.screen, 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))
self.hud.manager.draw_ui(self.screen)
self.hud.draw_splitters(self.screen) # <-- Add this line
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])

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

253
ui/hud.py
View File

@ -6,19 +6,196 @@ 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): 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 = pygame_gui.UIManager((SCREEN_WIDTH, SCREEN_HEIGHT), "ui/theme.json") self.manager = ui_manager
self.screen_width = screen_width
self.screen_height = screen_height
def render_mouse_position(self, screen, camera): # 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):
"""Render mouse position in top left.""" """Render mouse position in top left."""
mouse_x, mouse_y = camera.get_real_coordinates(*pygame.mouse.get_pos()) mouse_x, mouse_y = pygame.mouse.get_pos()
sim_view_x = mouse_x - sim_view_rect.left
sim_view_y = mouse_y - sim_view_rect.top
world_x, world_y = camera.get_real_coordinates(sim_view_x, sim_view_y)
mouse_text = self.font.render(f"Mouse: ({mouse_x:.2f}, {mouse_y:.2f})", True, WHITE) mouse_text = self.font.render(f"Mouse: ({mouse_x:.2f}, {mouse_y:.2f})", True, WHITE)
text_rect = mouse_text.get_rect() text_rect = mouse_text.get_rect()
text_rect.topleft = (HUD_MARGIN, HUD_MARGIN) text_rect.topleft = (HUD_MARGIN, HUD_MARGIN)
@ -28,21 +205,21 @@ class HUD:
"""Render FPS in top right.""" """Render FPS in top right."""
fps_text = self.font.render(f"FPS: {int(clock.get_fps())}", True, WHITE) fps_text = self.font.render(f"FPS: {int(clock.get_fps())}", True, WHITE)
fps_rect = fps_text.get_rect() fps_rect = fps_text.get_rect()
fps_rect.topright = (SCREEN_WIDTH - HUD_MARGIN, HUD_MARGIN) fps_rect.topright = (self.screen_width - HUD_MARGIN, HUD_MARGIN)
screen.blit(fps_text, fps_rect) screen.blit(fps_text, fps_rect)
def render_tps(self, screen, actual_tps): def render_tps(self, screen, actual_tps):
"""Render TPS in bottom right.""" """Render TPS in bottom right."""
tps_text = self.font.render(f"TPS: {actual_tps}", True, WHITE) tps_text = self.font.render(f"TPS: {actual_tps}", True, WHITE)
tps_rect = tps_text.get_rect() tps_rect = tps_text.get_rect()
tps_rect.bottomright = (SCREEN_WIDTH - HUD_MARGIN, SCREEN_HEIGHT - HUD_MARGIN) tps_rect.bottomright = (self.screen_width - HUD_MARGIN, self.screen_height - HUD_MARGIN)
screen.blit(tps_text, tps_rect) screen.blit(tps_text, tps_rect)
def render_tick_count(self, screen, total_ticks): def render_tick_count(self, screen, total_ticks):
"""Render total tick count in bottom left.""" """Render total tick count in bottom left."""
tick_text = self.font.render(f"Ticks: {total_ticks}", True, WHITE) tick_text = self.font.render(f"Ticks: {total_ticks}", True, WHITE)
tick_rect = tick_text.get_rect() tick_rect = tick_text.get_rect()
tick_rect.bottomleft = (HUD_MARGIN, SCREEN_HEIGHT - HUD_MARGIN) tick_rect.bottomleft = (HUD_MARGIN, self.screen_height - HUD_MARGIN)
screen.blit(tick_text, tick_rect) screen.blit(tick_text, tick_rect)
def render_pause_indicator(self, screen, is_paused): def render_pause_indicator(self, screen, is_paused):
@ -50,7 +227,7 @@ class HUD:
if is_paused: if is_paused:
pause_text = self.font.render("Press 'Space' to unpause", True, WHITE) pause_text = self.font.render("Press 'Space' to unpause", True, WHITE)
pause_rect = pause_text.get_rect() pause_rect = pause_text.get_rect()
pause_rect.center = (SCREEN_WIDTH // 2, 20) pause_rect.center = (self.screen_width // 2, 20)
screen.blit(pause_text, pause_rect) screen.blit(pause_text, pause_rect)
def render_selected_objects_info(self, screen, selected_objects): def render_selected_objects_info(self, screen, selected_objects):
@ -58,7 +235,7 @@ class HUD:
if len(selected_objects) < 1: if len(selected_objects) < 1:
return return
max_width = SCREEN_WIDTH - 20 max_width = self.screen_width - 20
i = 0 i = 0
for obj in selected_objects: for obj in selected_objects:
@ -94,7 +271,7 @@ class HUD:
if not showing_legend: if not showing_legend:
legend_text = self.legend_font.render("Press 'L' to show controls", True, WHITE) legend_text = self.legend_font.render("Press 'L' to show controls", True, WHITE)
legend_rect = legend_text.get_rect() legend_rect = legend_text.get_rect()
legend_rect.center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT - 20) legend_rect.center = (self.screen_width // 2, self.screen_height - 20)
screen.blit(legend_text, legend_rect) screen.blit(legend_text, legend_rect)
return return
@ -112,8 +289,8 @@ class HUD:
legend_width = left_width + right_width + column_gap legend_width = left_width + right_width + column_gap
legend_height = max(len(left_col), len(right_col)) * legend_font_height + 10 legend_height = max(len(left_col), len(right_col)) * legend_font_height + 10
legend_x = (SCREEN_WIDTH - legend_width) // 2 legend_x = (self.screen_width - legend_width) // 2
legend_y = SCREEN_HEIGHT - legend_height - 10 legend_y = self.screen_height - legend_height - 10
# Draw left column # Draw left column
for i, (key, desc) in enumerate(left_col): for i, (key, desc) in enumerate(left_col):
@ -138,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
@ -189,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
@ -199,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 = SCREEN_WIDTH - VIZ_RIGHT_MARGIN # Right side of screen viz_x = self.screen_width - VIZ_RIGHT_MARGIN # Right side of screen
viz_y = (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
@ -211,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()
@ -218,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]
@ -376,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
@ -453,10 +627,10 @@ class HUD:
tooltip_y = mouse_y + TOOLTIP_Y_OFFSET tooltip_y = mouse_y + TOOLTIP_Y_OFFSET
# Adjust if off right edge # Adjust if off right edge
if tooltip_x + width > SCREEN_WIDTH: if tooltip_x + width > self.screen_width:
tooltip_x = mouse_x - width - TOOLTIP_X_OFFSET tooltip_x = mouse_x - width - TOOLTIP_X_OFFSET
# Adjust if off bottom edge # Adjust if off bottom edge
if tooltip_y + height > SCREEN_HEIGHT: if tooltip_y + height > self.screen_height:
tooltip_y = mouse_y - height - TOOLTIP_Y_OFFSET tooltip_y = mouse_y - height - TOOLTIP_Y_OFFSET
tooltip_rect = pygame.Rect(tooltip_x, tooltip_y, width, height) tooltip_rect = pygame.Rect(tooltip_x, tooltip_y, width, height)
@ -467,20 +641,17 @@ 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, cell_count): def render_sprint_debug(self, screen, actual_tps, total_ticks):
"""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_count = self.font.render(f"Cells: {cell_count}", True, (255, 255, 255))
y = SCREEN_HEIGHT // 2 - 40 y = self.screen_height // 2 - 40
header_rect = header.get_rect(center=(SCREEN_WIDTH // 2, y)) header_rect = header.get_rect(center=(self.screen_width // 2, y))
tps_rect = tps_text.get_rect(center=(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=(SCREEN_WIDTH // 2, y + 80)) ticks_rect = ticks_text.get_rect(center=(self.screen_width // 2, y + 80))
cell_rect = ticks_text.get_rect(center=(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_count, cell_rect)