661 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			661 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # ui/hud.py
 | |
| """Handles HUD elements and text overlays."""
 | |
| 
 | |
| import pygame
 | |
| import pygame_gui
 | |
| from config.constants import *
 | |
| from world.base.brain import CellBrain, FlexibleNeuralNetwork
 | |
| from world.objects import DefaultCell
 | |
| from pygame_gui.elements import UIPanel
 | |
| import math
 | |
| 
 | |
| DARK_GRAY = (40, 40, 40)
 | |
| DARKER_GRAY = (25, 25, 25)
 | |
| 
 | |
| class HUD:
 | |
|     def __init__(self, ui_manager, screen_width=SCREEN_WIDTH, screen_height=SCREEN_HEIGHT):
 | |
|         self.font = pygame.font.Font("freesansbold.ttf", FONT_SIZE)
 | |
|         self.legend_font = pygame.font.Font("freesansbold.ttf", LEGEND_FONT_SIZE)
 | |
| 
 | |
|         self.manager = ui_manager
 | |
|         self.screen_width = screen_width
 | |
|         self.screen_height = screen_height
 | |
| 
 | |
|         # Panel size defaults
 | |
|         self.control_bar_height = 48
 | |
|         self.inspector_width = 260
 | |
|         self.properties_width = 320
 | |
|         self.console_height = 120
 | |
|         self.splitter_thickness = 6
 | |
| 
 | |
|         self.dragging_splitter = None
 | |
|         self._create_panels()
 | |
| 
 | |
|     def _create_panels(self):
 | |
|         # Top control bar
 | |
|         self.control_bar = UIPanel(
 | |
|             relative_rect=pygame.Rect(0, 0, self.screen_width, self.control_bar_height),
 | |
|             manager=self.manager,
 | |
|             object_id="#control_bar",
 | |
|         )
 | |
| 
 | |
|         # Left inspector
 | |
|         self.inspector_panel = UIPanel(
 | |
|             relative_rect=pygame.Rect(
 | |
|                 0, self.control_bar_height,
 | |
|                 self.inspector_width,
 | |
|                 self.screen_height - self.control_bar_height
 | |
|             ),
 | |
|             manager=self.manager,
 | |
|             object_id="#inspector_panel",
 | |
|         )
 | |
| 
 | |
|         # Right properties
 | |
|         self.properties_panel = UIPanel(
 | |
|             relative_rect=pygame.Rect(
 | |
|                 self.screen_width - self.properties_width,
 | |
|                 self.control_bar_height,
 | |
|                 self.properties_width,
 | |
|                 self.screen_height - self.control_bar_height
 | |
|             ),
 | |
|             manager=self.manager,
 | |
|             object_id="#properties_panel",
 | |
|         )
 | |
| 
 | |
|         # Bottom console
 | |
|         self.console_panel = UIPanel(
 | |
|             relative_rect=pygame.Rect(
 | |
|                 self.inspector_width,
 | |
|                 self.screen_height - self.console_height,
 | |
|                 self.screen_width - self.inspector_width - self.properties_width,
 | |
|                 self.console_height
 | |
|             ),
 | |
|             manager=self.manager,
 | |
|             object_id="#console_panel",
 | |
|         )
 | |
| 
 | |
|         self.panels = [
 | |
|             self.control_bar,
 | |
|             self.inspector_panel,
 | |
|             self.properties_panel,
 | |
|             self.console_panel
 | |
|         ]
 | |
|         self.dragging_splitter = None
 | |
| 
 | |
|     def get_viewport_rect(self):
 | |
|         # Returns the rect for the simulation viewport
 | |
|         x = self.inspector_width
 | |
|         y = self.control_bar_height
 | |
|         w = self.screen_width - self.inspector_width - self.properties_width
 | |
|         h = self.screen_height - self.control_bar_height - self.console_height
 | |
|         return pygame.Rect(x, y, w, h)
 | |
| 
 | |
|     def update_layout(self, window_width, window_height):
 | |
|         self.screen_width = window_width
 | |
|         self.screen_height = window_height
 | |
| 
 | |
|         # Control bar (top)
 | |
|         self.control_bar.set_relative_position((0, 0))
 | |
|         self.control_bar.set_dimensions((self.screen_width, self.control_bar_height))
 | |
| 
 | |
|         # Inspector panel (left) - goes all the way to the bottom
 | |
|         self.inspector_panel.set_relative_position((0, self.control_bar_height))
 | |
|         self.inspector_panel.set_dimensions((self.inspector_width, self.screen_height - self.control_bar_height))
 | |
| 
 | |
|         # Properties panel (right) - goes all the way to the bottom
 | |
|         self.properties_panel.set_relative_position(
 | |
|             (self.screen_width - self.properties_width, self.control_bar_height))
 | |
|         self.properties_panel.set_dimensions((self.properties_width, self.screen_height - self.control_bar_height))
 | |
| 
 | |
|         # Console panel (bottom, spans between inspector and properties)
 | |
|         self.console_panel.set_relative_position((self.inspector_width, self.screen_height - self.console_height))
 | |
|         self.console_panel.set_dimensions(
 | |
|             (self.screen_width - self.inspector_width - self.properties_width, self.console_height))
 | |
| 
 | |
|     def process_event(self, event):
 | |
|         # Handle splitter dragging for resizing panels
 | |
|         if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
 | |
|             mx, my = event.pos
 | |
|             # Check if mouse is on a splitter (left/right/bottom)
 | |
|             if abs(mx - self.inspector_width) < self.splitter_thickness and self.control_bar_height < my < self.screen_height - self.console_height:
 | |
|                 self.dragging_splitter = "inspector"
 | |
|             elif abs(mx - (self.screen_width - self.properties_width)) < self.splitter_thickness and self.control_bar_height < my < self.screen_height - self.console_height:
 | |
|                 self.dragging_splitter = "properties"
 | |
|             elif abs(my - (self.screen_height - self.console_height)) < self.splitter_thickness and self.inspector_width < mx < self.screen_width - self.properties_width:
 | |
|                 self.dragging_splitter = "console"
 | |
|             self.update_layout(self.screen_width, self.screen_height)
 | |
|         elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
 | |
|             self.dragging_splitter = None
 | |
|         elif event.type == pygame.MOUSEMOTION and self.dragging_splitter:
 | |
|             mx, my = event.pos
 | |
|             if self.dragging_splitter == "inspector":
 | |
|                 self.inspector_width = max(100, min(mx, self.screen_width - self.properties_width - 100))
 | |
|             elif self.dragging_splitter == "properties":
 | |
|                 self.properties_width = max(100, min(self.screen_width - mx, self.screen_width - self.inspector_width - 100))
 | |
|             elif self.dragging_splitter == "console":
 | |
|                 self.console_height = max(60, min(self.screen_height - my, self.screen_height - self.control_bar_height - 60))
 | |
|             self.update_layout(self.screen_width, self.screen_height)
 | |
| 
 | |
|     def draw_splitters(self, screen):
 | |
|         # Draw draggable splitters for visual feedback
 | |
|         indicator_color = (220, 220, 220)
 | |
|         indicator_size = 6  # Length of indicator line
 | |
|         indicator_gap = 4    # Gap between indicator lines
 | |
|         indicator_count = 3  # Number of indicator lines
 | |
| 
 | |
|         # Vertical splitter (inspector/properties)
 | |
|         # Inspector/properties only if wide enough
 | |
|         if self.inspector_width > 0:
 | |
|             x = self.inspector_width - 2
 | |
|             y1 = self.control_bar_height
 | |
|             y2 = self.screen_height - self.console_height
 | |
|             # Draw indicator (horizontal lines) in the middle
 | |
|             mid_y = (y1 + y2) // 2
 | |
|             for i in range(indicator_count):
 | |
|                 offset = (i - 1) * (indicator_gap + 1)
 | |
|                 pygame.draw.line(
 | |
|                     screen, indicator_color,
 | |
|                     (x - indicator_size // 2, mid_y + offset),
 | |
|                     (x + indicator_size // 2, mid_y + offset),
 | |
|                     2
 | |
|                 )
 | |
| 
 | |
|         if self.properties_width > 0:
 | |
|             x = self.screen_width - self.properties_width + 2
 | |
|             y1 = self.control_bar_height
 | |
|             y2 = self.screen_height - self.console_height
 | |
|             mid_y = (y1 + y2) // 2
 | |
|             for i in range(indicator_count):
 | |
|                 offset = (i - 1) * (indicator_gap + 1)
 | |
|                 pygame.draw.line(
 | |
|                     screen, indicator_color,
 | |
|                     (x - indicator_size // 2, mid_y + offset),
 | |
|                     (x + indicator_size // 2, mid_y + offset),
 | |
|                     2
 | |
|                 )
 | |
| 
 | |
|         # Horizontal splitter (console)
 | |
|         if self.console_height > 0:
 | |
|             y = self.screen_height - self.console_height + 2
 | |
|             x1 = self.inspector_width
 | |
|             x2 = self.screen_width - self.properties_width
 | |
|             mid_x = (x1 + x2) // 2
 | |
|             for i in range(indicator_count):
 | |
|                 offset = (i - 1) * (indicator_gap + 1)
 | |
|                 pygame.draw.line(
 | |
|                     screen, indicator_color,
 | |
|                     (mid_x + offset, y - indicator_size // 2),
 | |
|                     (mid_x + offset, y + indicator_size // 2),
 | |
|                     2
 | |
|                 )
 | |
| 
 | |
|     def render_mouse_position(self, screen, camera, sim_view_rect):
 | |
|         """Render mouse position in top left."""
 | |
|         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)
 | |
|         screen.blit(mouse_text, text_rect)
 | |
| 
 | |
|     def render_fps(self, screen, clock):
 | |
|         """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 = (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 = (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, self.screen_height - HUD_MARGIN)
 | |
|         screen.blit(tick_text, tick_rect)
 | |
| 
 | |
|     def render_pause_indicator(self, screen, is_paused):
 | |
|         """Render pause indicator when paused."""
 | |
|         if is_paused:
 | |
|             pause_text = self.font.render("Press 'Space' to unpause", True, WHITE)
 | |
|             pause_rect = pause_text.get_rect()
 | |
|             pause_rect.center = (self.screen_width // 2, 20)
 | |
|             screen.blit(pause_text, pause_rect)
 | |
| 
 | |
|     def render_selected_objects_info(self, screen, selected_objects):
 | |
|         """Render information about selected objects."""
 | |
|         if len(selected_objects) < 1:
 | |
|             return
 | |
| 
 | |
|         max_width = self.screen_width - 20
 | |
|         i = 0
 | |
| 
 | |
|         for obj in selected_objects:
 | |
|             text = f"Object: {str(obj)}"
 | |
|             words = text.split()
 | |
|             line = ""
 | |
|             line_offset = 0
 | |
| 
 | |
|             for word in words:
 | |
|                 test_line = f"{line} {word}".strip()
 | |
|                 test_width, _ = self.font.size(test_line)
 | |
| 
 | |
|                 if test_width > max_width and line:
 | |
|                     obj_text = self.font.render(line, True, WHITE)
 | |
|                     obj_rect = obj_text.get_rect()
 | |
|                     obj_rect.topleft = (HUD_MARGIN, 30 + i * LINE_HEIGHT + line_offset)
 | |
|                     screen.blit(obj_text, obj_rect)
 | |
|                     line = word
 | |
|                     line_offset += LINE_HEIGHT
 | |
|                 else:
 | |
|                     line = test_line
 | |
| 
 | |
|             if line:
 | |
|                 obj_text = self.font.render(line, True, WHITE)
 | |
|                 obj_rect = obj_text.get_rect()
 | |
|                 obj_rect.topleft = (HUD_MARGIN, 30 + i * LINE_HEIGHT + line_offset)
 | |
|                 screen.blit(obj_text, obj_rect)
 | |
| 
 | |
|             i += 1
 | |
| 
 | |
|     def render_legend(self, screen, showing_legend):
 | |
|         """Render the controls legend."""
 | |
|         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 = (self.screen_width // 2, self.screen_height - 20)
 | |
|             screen.blit(legend_text, legend_rect)
 | |
|             return
 | |
| 
 | |
|         # Split into two columns
 | |
|         mid = (len(KEYMAP_LEGEND) + 1) // 2
 | |
|         left_col = KEYMAP_LEGEND[:mid]
 | |
|         right_col = KEYMAP_LEGEND[mid:]
 | |
| 
 | |
|         legend_font_height = self.legend_font.get_height()
 | |
|         column_gap = 40  # Space between columns
 | |
| 
 | |
|         # Calculate max width for each column
 | |
|         left_width = max(self.legend_font.size(f"{k}: {v}")[0] for k, v in left_col)
 | |
|         right_width = max(self.legend_font.size(f"{k}: {v}")[0] for k, v in right_col)
 | |
|         legend_width = left_width + right_width + column_gap
 | |
|         legend_height = max(len(left_col), len(right_col)) * legend_font_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):
 | |
|             text = self.legend_font.render(f"{key}: {desc}", True, WHITE)
 | |
|             text_rect = text.get_rect()
 | |
|             text_rect.left = legend_x
 | |
|             text_rect.top = legend_y + 5 + i * legend_font_height
 | |
|             screen.blit(text, text_rect)
 | |
| 
 | |
|         # Draw right column
 | |
|         for i, (key, desc) in enumerate(right_col):
 | |
|             text = self.legend_font.render(f"{key}: {desc}", True, WHITE)
 | |
|             text_rect = text.get_rect()
 | |
|             text_rect.left = legend_x + left_width + column_gap
 | |
|             text_rect.top = legend_y + 5 + i * legend_font_height
 | |
|             screen.blit(text, text_rect)
 | |
| 
 | |
|     def render_neural_network_visualization(self, screen, cell: DefaultCell) -> None:
 | |
|         """Render neural network visualization. This is fixed to the screen size and is not dependent on zoom or camera position."""
 | |
| 
 | |
|         # Visualization layout constants
 | |
|         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
 | |
|         BACKGROUND_BORDER_WIDTH = 2  # Width of the background border
 | |
|         BACKGROUND_COLOR = (30, 30, 30)  # Dark gray background color
 | |
| 
 | |
|         # Title positioning constants
 | |
|         TITLE_TOP_MARGIN = 20  # Distance above visualization for title
 | |
| 
 | |
|         # Neuron appearance constants
 | |
|         NEURON_RADIUS = 8  # Radius of neuron circles
 | |
|         NEURON_BORDER_WIDTH = 2  # Width of neuron circle borders
 | |
| 
 | |
|         # Layer spacing constants
 | |
|         LAYER_VERTICAL_MARGIN = 30  # Top and bottom margin within visualization for neurons
 | |
| 
 | |
|         # Connection appearance constants
 | |
|         MAX_CONNECTION_THICKNESS = 4  # Maximum thickness for connection lines
 | |
|         MIN_CONNECTION_THICKNESS = 1  # Minimum thickness for connection lines
 | |
| 
 | |
|         # Neuron activation colors
 | |
|         NEURON_BASE_INTENSITY = 100  # Base color intensity for neurons
 | |
|         NEURON_ACTIVATION_INTENSITY = 155  # Additional intensity based on activation
 | |
| 
 | |
|         # Text positioning constants
 | |
|         ACTIVATION_TEXT_OFFSET = 15  # Distance below neuron for activation value text
 | |
|         ACTIVATION_DISPLAY_THRESHOLD = 0.01  # Minimum activation value to display as text
 | |
|         ACTIVATION_TEXT_PRECISION = 2  # Decimal places for activation values
 | |
| 
 | |
|         # Layer label positioning constants
 | |
|         LAYER_LABEL_BOTTOM_MARGIN = 15  # Distance below visualization for layer labels
 | |
| 
 | |
|         # Info text positioning constants
 | |
|         INFO_TEXT_TOP_MARGIN = 40  # Distance below visualization for info text
 | |
|         INFO_TEXT_LINE_SPACING = 15  # Vertical spacing between info text lines
 | |
| 
 | |
|         # Activation value clamping
 | |
|         ACTIVATION_CLAMP_MIN = -1  # Minimum activation value for visualization
 | |
|         ACTIVATION_CLAMP_MAX = 1  # Maximum activation value for visualization
 | |
| 
 | |
|         # --- Tooltip constants ---
 | |
|         TOOLTIP_X_OFFSET = 12
 | |
|         TOOLTIP_Y_OFFSET = 8
 | |
|         TOOLTIP_PADDING_X = 5
 | |
|         TOOLTIP_PADDING_Y = 3
 | |
|         TOOLTIP_BG_COLOR = (40, 40, 40)
 | |
|         TOOLTIP_BORDER_COLOR = WHITE
 | |
|         TOOLTIP_BORDER_WIDTH = 1
 | |
|         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
 | |
| 
 | |
|         cell_brain: CellBrain = cell.behavioral_model
 | |
| 
 | |
|         if not hasattr(cell_brain, 'neural_network'):
 | |
|             return
 | |
| 
 | |
|         network: FlexibleNeuralNetwork = cell_brain.neural_network
 | |
| 
 | |
|         # 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
 | |
| 
 | |
|         # Draw background
 | |
|         background_rect = pygame.Rect(viz_x - BACKGROUND_PADDING, viz_y - BACKGROUND_PADDING,
 | |
|                                       VIZ_WIDTH + 2 * BACKGROUND_PADDING, VIZ_HEIGHT + 2 * BACKGROUND_PADDING)
 | |
|         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()
 | |
|         title_rect.centerx = viz_x + VIZ_WIDTH // 2
 | |
|         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]
 | |
| 
 | |
|         # Store activations for each layer
 | |
|         activations = [input_values]  # Input layer
 | |
| 
 | |
|         # Calculate activations for each layer
 | |
|         for layer_idx in range(1, len(network.layers)):
 | |
|             layer_activations = []
 | |
| 
 | |
|             for neuron in network.layers[layer_idx]:
 | |
|                 if neuron['type'] == 'input':
 | |
|                     continue
 | |
| 
 | |
|                 # Calculate weighted sum
 | |
|                 weighted_sum = neuron.get('bias', 0)
 | |
| 
 | |
|                 for source_layer, source_neuron, weight in neuron.get('connections', []):
 | |
|                     if source_layer < len(activations) and source_neuron < len(activations[source_layer]):
 | |
|                         weighted_sum += activations[source_layer][source_neuron] * weight
 | |
| 
 | |
|                 # Apply activation function
 | |
|                 activation = max(ACTIVATION_CLAMP_MIN, min(ACTIVATION_CLAMP_MAX, weighted_sum))
 | |
|                 layer_activations.append(activation)
 | |
| 
 | |
|             activations.append(layer_activations)
 | |
| 
 | |
|         # Calculate neuron positions
 | |
|         neuron_positions = {}
 | |
| 
 | |
|         for layer_idx, layer in enumerate(network.layers):
 | |
|             layer_neurons = [n for n in layer if n['type'] != 'input' or layer_idx == 0]
 | |
|             layer_size = len(layer_neurons)
 | |
| 
 | |
|             if layer_size == 0:
 | |
|                 continue
 | |
| 
 | |
|             # X position based on layer
 | |
|             if len(network.layers) == 1:
 | |
|                 x = viz_x + VIZ_WIDTH // 2
 | |
|             else:
 | |
|                 x = viz_x + (layer_idx * layer_spacing)
 | |
| 
 | |
|             # Y positions distributed vertically
 | |
|             if layer_size == 1:
 | |
|                 y_positions = [viz_y + VIZ_HEIGHT // 2]
 | |
|             else:
 | |
|                 y_start = viz_y + LAYER_VERTICAL_MARGIN
 | |
|                 y_end = viz_y + VIZ_HEIGHT - LAYER_VERTICAL_MARGIN
 | |
|                 y_positions = [y_start + i * (y_end - y_start) / (layer_size - 1) for i in range(layer_size)]
 | |
| 
 | |
|             for neuron_idx, neuron in enumerate(layer_neurons):
 | |
|                 if neuron_idx < len(y_positions):
 | |
|                     neuron_positions[(layer_idx, neuron_idx)] = (int(x), int(y_positions[neuron_idx]))
 | |
| 
 | |
|         # Draw connections first (so they appear behind neurons)
 | |
|         for layer_idx in range(1, len(network.layers)):
 | |
|             for neuron_idx, neuron in enumerate(network.layers[layer_idx]):
 | |
|                 if neuron['type'] == 'input':
 | |
|                     continue
 | |
| 
 | |
|                 target_pos = neuron_positions.get((layer_idx, neuron_idx))
 | |
|                 if not target_pos:
 | |
|                     continue
 | |
| 
 | |
|                 for source_layer, source_neuron, weight in neuron.get('connections', []):
 | |
|                     source_pos = neuron_positions.get((source_layer, source_neuron))
 | |
|                     if not source_pos:
 | |
|                         continue
 | |
| 
 | |
|                     # Get activation value of the source neuron
 | |
|                     if source_layer < len(activations) and source_neuron < len(activations[source_layer]):
 | |
|                         activation = activations[source_layer][source_neuron]
 | |
|                     else:
 | |
|                         activation = 0.0
 | |
| 
 | |
|                     # Clamp activation to [-1, 1]
 | |
|                     activation = max(ACTIVATION_CLAMP_MIN, min(ACTIVATION_CLAMP_MAX, activation))
 | |
| 
 | |
|                     # Color: interpolate from red (-1) to yellow (0) to green (+1)
 | |
|                     if activation <= 0:
 | |
|                         # Red to yellow
 | |
|                         r = 255
 | |
|                         g = int(255 * (activation + 1))
 | |
|                         b = 0
 | |
|                     else:
 | |
|                         # Yellow to green
 | |
|                         r = int(255 * (1 - activation))
 | |
|                         g = 255
 | |
|                         b = 0
 | |
|                     color = (r, g, b)
 | |
| 
 | |
|                     # Thickness: proportional to abs(weight)
 | |
|                     thickness = max(MIN_CONNECTION_THICKNESS, int(abs(weight) * MAX_CONNECTION_THICKNESS))
 | |
| 
 | |
|                     pygame.draw.line(screen, color, source_pos, target_pos, thickness)
 | |
| 
 | |
|         # Draw neurons
 | |
|         for layer_idx, layer in enumerate(network.layers):
 | |
|             layer_activations = activations[layer_idx] if layer_idx < len(activations) else []
 | |
| 
 | |
|             for neuron_idx, neuron in enumerate(layer):
 | |
|                 if neuron['type'] == 'input' and layer_idx != 0:
 | |
|                     continue
 | |
| 
 | |
|                 pos = neuron_positions.get((layer_idx, neuron_idx))
 | |
|                 if not pos:
 | |
|                     continue
 | |
| 
 | |
|                 # Get activation value
 | |
|                 activation = 0
 | |
|                 if neuron_idx < len(layer_activations):
 | |
|                     activation = layer_activations[neuron_idx]
 | |
| 
 | |
|                 # Color based on activation: brightness represents magnitude
 | |
|                 activation_normalized = max(ACTIVATION_CLAMP_MIN, min(ACTIVATION_CLAMP_MAX, activation))
 | |
|                 activation_intensity = int(abs(activation_normalized) * NEURON_ACTIVATION_INTENSITY)
 | |
| 
 | |
|                 if activation_normalized >= 0:
 | |
|                     # Positive activation: blue tint
 | |
|                     color = (NEURON_BASE_INTENSITY, NEURON_BASE_INTENSITY, NEURON_BASE_INTENSITY + activation_intensity)
 | |
|                 else:
 | |
|                     # Negative activation: red tint
 | |
|                     color = (NEURON_BASE_INTENSITY + activation_intensity, NEURON_BASE_INTENSITY, NEURON_BASE_INTENSITY)
 | |
| 
 | |
|                 # Draw neuron
 | |
|                 pygame.draw.circle(screen, color, pos, NEURON_RADIUS)
 | |
|                 pygame.draw.circle(screen, WHITE, pos, NEURON_RADIUS, NEURON_BORDER_WIDTH)
 | |
| 
 | |
|                 # Draw activation value as text
 | |
|                 if abs(activation) > ACTIVATION_DISPLAY_THRESHOLD:
 | |
|                     activation_text = self.legend_font.render(f"{activation:.{ACTIVATION_TEXT_PRECISION}f}", True,
 | |
|                                                               WHITE)
 | |
|                     text_rect = activation_text.get_rect()
 | |
|                     text_rect.center = (pos[0], pos[1] + NEURON_RADIUS + ACTIVATION_TEXT_OFFSET)
 | |
|                     screen.blit(activation_text, text_rect)
 | |
| 
 | |
|         # Draw layer labels
 | |
|         num_layers = len(network.layers)
 | |
|         for layer_idx in range(num_layers):
 | |
|             if layer_idx == 0:
 | |
|                 label = "Input"
 | |
|             elif layer_idx == num_layers - 1:
 | |
|                 label = "Output"
 | |
|             else:
 | |
|                 label = f"Hidden {layer_idx}" if num_layers > 3 else "Hidden"
 | |
| 
 | |
|             # Find average x position for this layer
 | |
|             x_positions = [pos[0] for (l_idx, n_idx), pos in neuron_positions.items() if l_idx == layer_idx]
 | |
|             if x_positions:
 | |
|                 avg_x = sum(x_positions) // len(x_positions)
 | |
| 
 | |
|                 label_text = self.legend_font.render(label, True, WHITE)
 | |
|                 label_rect = label_text.get_rect()
 | |
|                 label_rect.centerx = avg_x
 | |
|                 label_rect.bottom = viz_y + VIZ_HEIGHT + LAYER_LABEL_BOTTOM_MARGIN
 | |
|                 screen.blit(label_text, label_rect)
 | |
| 
 | |
|         # --- Tooltip logic for neuron hover ---
 | |
|         mouse_x, mouse_y = pygame.mouse.get_pos()
 | |
|         tooltip_text = None
 | |
| 
 | |
|         for (layer_idx, neuron_idx), pos in neuron_positions.items():
 | |
|             dx = mouse_x - pos[0]
 | |
|             dy = mouse_y - pos[1]
 | |
|             dist = math.hypot(dx, dy)
 | |
|             if dist <= NEURON_RADIUS + 3:
 | |
|                 neuron = network.layers[layer_idx][neuron_idx]
 | |
|                 label = None
 | |
|                 value_str = None
 | |
| 
 | |
|                 # Show input/output name if applicable
 | |
|                 if neuron['type'] == 'input' and layer_idx == 0:
 | |
|                     if neuron_idx < len(cell_brain.input_keys):
 | |
|                         key = cell_brain.input_keys[neuron_idx]
 | |
|                         label = f"Input: {key}"
 | |
|                         # Show normalized input value
 | |
|                         raw_value = cell_brain.inputs.get(key, 0.0)
 | |
|                         normalized_value = cell_brain._normalize_input(key, raw_value)
 | |
|                         value_str = f"Value: {normalized_value:.2f}"
 | |
|                 elif neuron['type'] == 'output':
 | |
|                     if neuron_idx < len(cell_brain.output_keys):
 | |
|                         key = cell_brain.output_keys[neuron_idx]
 | |
|                         label = f"Output: {key}"
 | |
|                         # Show output value (already actual, not normalized)
 | |
|                         value = cell_brain.outputs.get(key, 0.0)
 | |
|                         value_str = f"Value: {value:.2f}"
 | |
|                 else:
 | |
|                     # For hidden neurons, show activation value
 | |
|                     if layer_idx < len(activations) and neuron_idx < len(activations[layer_idx]):
 | |
|                         value = activations[layer_idx][neuron_idx]
 | |
|                         value_str = f"Value: {value:.2f}"
 | |
| 
 | |
|                 # Show bias if present
 | |
|                 bias = neuron.get('bias', None)
 | |
|                 bias_str = f"Bias: {bias:.2f}" if bias is not None else None
 | |
| 
 | |
|                 # Compose tooltip text
 | |
|                 tooltip_lines = []
 | |
|                 if label:
 | |
|                     tooltip_lines.append(label)
 | |
|                 if value_str:
 | |
|                     tooltip_lines.append(value_str)
 | |
|                 if bias_str:
 | |
|                     tooltip_lines.append(bias_str)
 | |
|                 tooltip_text = "\n".join(tooltip_lines) if tooltip_lines else None
 | |
|                 break
 | |
| 
 | |
|         if tooltip_text:
 | |
|             lines = tooltip_text.split('\n')
 | |
|             tooltip_surfs = [self.legend_font.render(line, True, WHITE) for line in lines]
 | |
|             width = max(surf.get_width() for surf in tooltip_surfs) + TOOLTIP_MARGIN
 | |
|             height = sum(surf.get_height() for surf in tooltip_surfs) + TOOLTIP_MARGIN
 | |
| 
 | |
|             # Default position: right and below cursor
 | |
|             tooltip_x = mouse_x + TOOLTIP_X_OFFSET
 | |
|             tooltip_y = mouse_y + TOOLTIP_Y_OFFSET
 | |
| 
 | |
|             # Adjust if off right edge
 | |
|             if tooltip_x + width > self.screen_width:
 | |
|                 tooltip_x = mouse_x - width - TOOLTIP_X_OFFSET
 | |
|             # Adjust if off bottom edge
 | |
|             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)
 | |
|             pygame.draw.rect(screen, TOOLTIP_BG_COLOR, tooltip_rect)
 | |
|             pygame.draw.rect(screen, TOOLTIP_BORDER_COLOR, tooltip_rect, TOOLTIP_BORDER_WIDTH)
 | |
|             y = tooltip_rect.top + TOOLTIP_PADDING_Y
 | |
|             for surf in tooltip_surfs:
 | |
|                 screen.blit(surf, (tooltip_rect.left + TOOLTIP_PADDING_X, y))
 | |
|                 y += surf.get_height() + TOOLTIP_LINE_SPACING
 | |
| 
 | |
|     def render_sprint_debug(self, screen, actual_tps, total_ticks, cell_count=None):
 | |
|         """Render sprint debug info: header, TPS, and tick count."""
 | |
|         header = self.font.render("Sprinting...", True, (255, 200, 0))
 | |
|         tps_text = self.font.render(f"TPS: {actual_tps}", True, (255, 255, 255))
 | |
|         ticks_text = self.font.render(f"Ticks: {total_ticks}", True, (255, 255, 255))
 | |
|         cell_text = self.font.render(f"Cells: {cell_count}" if cell_count is not None else "Cells: N/A", True, (255, 255, 255))
 | |
| 
 | |
|         y = self.screen_height // 2 - 80
 | |
|         header_rect = header.get_rect(center=(self.screen_width // 2, y))
 | |
|         tps_rect = tps_text.get_rect(center=(self.screen_width // 2, y + 40))
 | |
|         ticks_rect = ticks_text.get_rect(center=(self.screen_width // 2, y + 80))
 | |
|         cell_rect = cell_text.get_rect(center=(self.screen_width // 2, y + 120))
 | |
| 
 | |
|         screen.blit(header, header_rect)
 | |
|         screen.blit(tps_text, tps_rect)
 | |
|         screen.blit(ticks_text, ticks_rect)
 | |
|         screen.blit(cell_text, cell_rect)
 |