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:
def __init__(self, camera, world):
def __init__(self, camera, world, sim_view_rect):
self.camera = camera
self.world = world
@ -27,6 +27,13 @@ class InputHandler:
self.default_tps = DEFAULT_TPS
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):
"""Process all pygame events and return game state."""
running = True
@ -115,20 +122,27 @@ class InputHandler:
"""Process object selection logic."""
self.selecting = False
# Convert screen to world coordinates
x1, y1 = self.camera.get_real_coordinates(*self.select_start)
x2, y2 = self.camera.get_real_coordinates(*self.select_end)
# Map screen to sim_view coordinates
sx1 = self.select_start[0] - self.sim_view_rect.left
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
if (abs(self.select_start[0] - self.select_end[0]) < SELECTION_THRESHOLD and
abs(self.select_start[1] - self.select_end[1]) < SELECTION_THRESHOLD):
if (abs(sx1 - sx2) < SELECTION_THRESHOLD and
abs(sy1 - sy2) < SELECTION_THRESHOLD):
self._handle_click_selection()
else:
self._handle_drag_selection(x1, y1, x2, y2)
def _handle_click_selection(self):
"""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)
self.selected_objects = []

View File

@ -8,12 +8,14 @@ from world.base.brain import CellBrain
class Renderer:
def __init__(self, screen):
self.screen = screen
def __init__(self, render_area):
self.render_area = render_area
self.render_height = render_area.get_height()
self.render_width = render_area.get_width()
def clear_screen(self):
"""Clear the screen with a black background."""
self.screen.fill(BLACK)
self.render_area.fill(BLACK)
def draw_grid(self, camera, showing_grid=True):
"""Draw the reference grid."""
@ -28,8 +30,8 @@ class Renderer:
grid_world_height = GRID_HEIGHT * effective_cell_size
# Calculate grid position relative to camera (with grid centered at 0,0)
grid_center_x = SCREEN_WIDTH // 2 - camera.x * camera.zoom
grid_center_y = SCREEN_HEIGHT // 2 - camera.y * camera.zoom
grid_center_x = self.render_width // 2 - camera.x * camera.zoom
grid_center_y = self.render_height // 2 - camera.y * camera.zoom
grid_left = grid_center_x - grid_world_width // 2
grid_top = grid_center_y - grid_world_height // 2
@ -37,20 +39,20 @@ class Renderer:
grid_bottom = grid_top + grid_world_height
# Check if grid is visible on screen
if (grid_right < 0 or grid_left > SCREEN_WIDTH or
grid_bottom < 0 or grid_top > SCREEN_HEIGHT):
if (grid_right < 0 or grid_left > self.render_width or
grid_bottom < 0 or grid_top > self.render_height):
return
# Fill the grid area with dark gray background
grid_rect = pygame.Rect(
max(0, grid_left),
max(0, grid_top),
min(SCREEN_WIDTH, grid_right) - max(0, grid_left),
min(SCREEN_HEIGHT, grid_bottom) - max(0, grid_top),
min(self.render_width, grid_right) - max(0, grid_left),
min(self.render_height, grid_bottom) - max(0, grid_top),
)
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
if effective_cell_size > 4:
@ -65,30 +67,30 @@ class Renderer:
# Vertical lines
if i <= GRID_WIDTH:
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)
end_y = min(SCREEN_HEIGHT, grid_bottom)
end_y = min(self.render_height, grid_bottom)
if start_y < end_y:
vertical_lines.append(((line_x, start_y), (line_x, end_y)))
# Horizontal lines
if i <= GRID_HEIGHT:
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)
end_x = min(SCREEN_WIDTH, grid_right)
end_x = min(self.render_width, grid_right)
if start_x < end_x:
horizontal_lines.append(((start_x, line_y), (end_x, line_y)))
# Draw all 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:
pygame.draw.line(self.screen, GRAY, start, end)
pygame.draw.line(self.render_area, GRAY, start, end)
def render_world(self, world, camera):
"""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):
"""Render interaction radius and debug vectors for objects."""
@ -108,7 +110,7 @@ class Renderer:
if screen_radius > 0:
# 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
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))
# 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
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))
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)]
)
@ -167,7 +169,7 @@ class Renderer:
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))
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
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_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,
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_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,
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))
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)]
)
def render_selection_rectangle(self, selection_rect):
"""Render the selection rectangle."""
def render_selection_rectangle(self, selection_rect, sim_view_rect=None):
"""Render the selection rectangle, offset for sim_view if sim_view_rect is provided."""
if not selection_rect:
return
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
s = pygame.Surface((width, height), pygame.SRCALPHA)
s.fill(SELECTION_GRAY)
self.screen.blit(s, (left, top))
self.render_area.blit(s, (left, top))
# Draw border
pygame.draw.rect(self.screen, SELECTION_BORDER,
pygame.draw.rect(self.render_area, SELECTION_BORDER,
pygame.Rect(left, top, width, height), 1)
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)
size = camera.get_relative_size(width)
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 sys
from pygame_gui import UIManager
from world.base.brain import CellBrain, FlexibleNeuralNetwork
from world.world import World, Position, Rotation
from world.objects import FoodObject, DefaultCell
@ -16,11 +18,29 @@ from ui.hud import HUD
class SimulationEngine:
def __init__(self):
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")
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_tps_time = time.perf_counter()
self.tick_counter = 0
@ -28,11 +48,28 @@ class SimulationEngine:
self.total_ticks = 0
self.world = self._setup_world()
self.input_handler = InputHandler(self.camera, self.world)
self.renderer = Renderer(self.screen)
self.hud = HUD()
self.input_handler = InputHandler(self.camera, self.world, self.sim_view_rect)
self.renderer = Renderer(self.sim_view)
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
def _setup_world():
@ -49,10 +86,11 @@ class SimulationEngine:
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 = 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
@ -68,7 +106,6 @@ class SimulationEngine:
def run(self):
while self.running:
self._handle_frame()
pygame.quit()
sys.exit()
@ -76,50 +113,16 @@ class SimulationEngine:
deltatime = self.clock.get_time() / 1000.0
tick_interval = 1.0 / self.input_handler.tps
# Handle events
events = pygame.event.get()
self.running = self.input_handler.handle_events(events, self.hud.manager)
self._handle_window_events(events)
if self.input_handler.sprint_mode:
# Sprint mode: run as many ticks as possible, skip rendering
current_time = time.perf_counter()
while True:
self.input_handler.update_selected_objects()
self.world.tick_all()
self.tick_counter += 1
self.total_ticks += 1
# Optionally break after some time to allow event processing
if time.perf_counter() - current_time > 0.05: # ~50ms per batch
break
# Update TPS every second
if time.perf_counter() - self.last_tps_time >= 1.0:
self.actual_tps = self.tick_counter
self.tick_counter = 0
self.last_tps_time = time.perf_counter()
# No rendering or camera update
self.renderer.clear_screen()
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._handle_sprint_mode()
return
if not self.input_handler.is_paused:
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
self._handle_simulation_ticks(tick_interval, deltatime)
else:
self.last_tick_time = time.perf_counter()
self.last_tps_time = time.perf_counter()
@ -128,26 +131,82 @@ class SimulationEngine:
self._update(deltatime)
self._render()
def _handle_window_events(self, events):
for event in events:
self.hud.process_event(event)
if event.type == pygame.VIDEORESIZE:
self.window_width, self.window_height = event.w, event.h
self.screen = pygame.display.set_mode(
(self.window_width, self.window_height),
pygame.RESIZABLE
)
self._update_simulation_view()
self.hud.update_layout(self.window_width, self.window_height)
self.hud.update_layout(self.window_width, self.window_height)
self._update_simulation_view()
def _handle_sprint_mode(self):
current_time = time.perf_counter()
while True:
self.input_handler.update_selected_objects()
self.world.tick_all()
self.tick_counter += 1
self.total_ticks += 1
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):
keys = pygame.key.get_pressed()
self.input_handler.update_camera(keys, deltatime)
def _render(self):
self.screen.fill(BLACK)
self.renderer.clear_screen()
self.renderer.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_tps(self.screen, self.actual_tps)
self.hud.render_tick_count(self.screen, self.total_ticks)
self.hud.render_selected_objects_info(self.screen, self.input_handler.selected_objects)
self.hud.render_legend(self.screen, self.input_handler.show_legend)
self.hud.render_pause_indicator(self.screen, self.input_handler.is_paused)
if self.input_handler.selected_objects:
self.hud.render_neural_network_visualization(self.screen, self.input_handler.selected_objects[0])

View File

@ -1,5 +1,8 @@
from core.simulation_engine import SimulationEngine
if __name__ == "__main__":
def main():
engine = SimulationEngine()
engine.run()
if __name__ == "__main__":
main()

253
ui/hud.py
View File

@ -6,19 +6,196 @@ import pygame_gui
from config.constants import *
from world.base.brain import CellBrain, FlexibleNeuralNetwork
from world.objects import DefaultCell
from pygame_gui.elements import UIPanel
import math
DARK_GRAY = (40, 40, 40)
DARKER_GRAY = (25, 25, 25)
class HUD:
def __init__(self):
def __init__(self, ui_manager, screen_width=SCREEN_WIDTH, screen_height=SCREEN_HEIGHT):
self.font = pygame.font.Font("freesansbold.ttf", FONT_SIZE)
self.legend_font = pygame.font.Font("freesansbold.ttf", LEGEND_FONT_SIZE)
self.manager = 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."""
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)
text_rect = mouse_text.get_rect()
text_rect.topleft = (HUD_MARGIN, HUD_MARGIN)
@ -28,21 +205,21 @@ class HUD:
"""Render FPS in top right."""
fps_text = self.font.render(f"FPS: {int(clock.get_fps())}", True, WHITE)
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)
def render_tps(self, screen, actual_tps):
"""Render TPS in bottom right."""
tps_text = self.font.render(f"TPS: {actual_tps}", True, WHITE)
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)
def render_tick_count(self, screen, total_ticks):
"""Render total tick count in bottom left."""
tick_text = self.font.render(f"Ticks: {total_ticks}", True, WHITE)
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)
def render_pause_indicator(self, screen, is_paused):
@ -50,7 +227,7 @@ class HUD:
if is_paused:
pause_text = self.font.render("Press 'Space' to unpause", True, WHITE)
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)
def render_selected_objects_info(self, screen, selected_objects):
@ -58,7 +235,7 @@ class HUD:
if len(selected_objects) < 1:
return
max_width = SCREEN_WIDTH - 20
max_width = self.screen_width - 20
i = 0
for obj in selected_objects:
@ -94,7 +271,7 @@ class HUD:
if not showing_legend:
legend_text = self.legend_font.render("Press 'L' to show controls", True, WHITE)
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)
return
@ -112,8 +289,8 @@ class HUD:
legend_width = left_width + right_width + column_gap
legend_height = max(len(left_col), len(right_col)) * legend_font_height + 10
legend_x = (SCREEN_WIDTH - legend_width) // 2
legend_y = SCREEN_HEIGHT - legend_height - 10
legend_x = (self.screen_width - legend_width) // 2
legend_y = self.screen_height - legend_height - 10
# Draw left column
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_HEIGHT = 300 # Height of the neural network visualization area
VIZ_RIGHT_MARGIN = VIZ_WIDTH + 50 # Distance from right edge of screen to visualization
VIZ_BOTTOM_MARGIN = 50 # Distance from the bottom of the screen
# Background styling constants
BACKGROUND_PADDING = 30 # Padding around the visualization background
@ -189,6 +367,9 @@ class HUD:
TOOLTIP_MARGIN = 10
TOOLTIP_LINE_SPACING = 0 # No extra spacing between lines
if self.properties_width < VIZ_RIGHT_MARGIN + 50:
self.properties_width = VIZ_RIGHT_MARGIN + 50 # Ensure properties panel is wide enough for tooltip
if not hasattr(cell, 'behavioral_model'):
return
@ -199,9 +380,9 @@ class HUD:
network: FlexibleNeuralNetwork = cell_brain.neural_network
# Calculate visualization position
viz_x = SCREEN_WIDTH - VIZ_RIGHT_MARGIN # Right side of screen
viz_y = (SCREEN_HEIGHT // 2) - (VIZ_HEIGHT // 2) # Centered vertically
# Calculate visualization position (bottom right)
viz_x = self.screen_width - VIZ_RIGHT_MARGIN # Right side of screen
viz_y = self.screen_height - VIZ_HEIGHT - VIZ_BOTTOM_MARGIN # Above the bottom margin
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, WHITE, background_rect, BACKGROUND_BORDER_WIDTH)
info = network.get_structure_info()
# Title
title_text = self.font.render("Neural Network", True, WHITE)
title_rect = title_text.get_rect()
@ -218,6 +401,13 @@ class HUD:
title_rect.top = viz_y - TITLE_TOP_MARGIN
screen.blit(title_text, title_rect)
# Render network cost under the title
cost_text = self.font.render(f"Cost: {info['network_cost']}", True, WHITE)
cost_rect = cost_text.get_rect()
cost_rect.centerx = title_rect.centerx
cost_rect.top = title_rect.bottom + 4 # Small gap below the title
screen.blit(cost_text, cost_rect)
# Get current activations by running a forward pass with current inputs
input_values = [cell_brain.inputs[key] for key in cell_brain.input_keys]
@ -376,22 +566,6 @@ class HUD:
label_rect.bottom = viz_y + VIZ_HEIGHT + LAYER_LABEL_BOTTOM_MARGIN
screen.blit(label_text, label_rect)
# Draw network info
info = network.get_structure_info()
info_lines = [
f"Layers: {info['total_layers']}",
f"Neurons: {info['total_neurons']}",
f"Connections: {info['total_connections']}",
f"Network Cost: {info['network_cost']}",
]
for i, line in enumerate(info_lines):
info_text = self.legend_font.render(line, True, WHITE)
info_rect = info_text.get_rect()
info_rect.left = viz_x
info_rect.top = viz_y + VIZ_HEIGHT + INFO_TEXT_TOP_MARGIN + i * INFO_TEXT_LINE_SPACING
screen.blit(info_text, info_rect)
# --- Tooltip logic for neuron hover ---
mouse_x, mouse_y = pygame.mouse.get_pos()
tooltip_text = None
@ -453,10 +627,10 @@ class HUD:
tooltip_y = mouse_y + TOOLTIP_Y_OFFSET
# 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
# 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_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))
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."""
header = self.font.render("Sprinting...", True, (255, 200, 0))
tps_text = self.font.render(f"TPS: {actual_tps}", True, (255, 255, 255))
ticks_text = self.font.render(f"Ticks: {total_ticks}", True, (255, 255, 255))
cell_count = self.font.render(f"Cells: {cell_count}", True, (255, 255, 255))
y = SCREEN_HEIGHT // 2 - 40
header_rect = header.get_rect(center=(SCREEN_WIDTH // 2, y))
tps_rect = tps_text.get_rect(center=(SCREEN_WIDTH // 2, y + 40))
ticks_rect = ticks_text.get_rect(center=(SCREEN_WIDTH // 2, y + 80))
cell_rect = ticks_text.get_rect(center=(SCREEN_WIDTH // 2, y + 120))
y = self.screen_height // 2 - 40
header_rect = header.get_rect(center=(self.screen_width // 2, y))
tps_rect = tps_text.get_rect(center=(self.screen_width // 2, y + 40))
ticks_rect = ticks_text.get_rect(center=(self.screen_width // 2, y + 80))
screen.blit(header, header_rect)
screen.blit(tps_text, tps_rect)
screen.blit(ticks_text, ticks_rect)
screen.blit(cell_count, cell_rect)