# ui/hud.py """Handles HUD elements and text overlays.""" import pygame from config.constants import * from world.base.brain import CellBrain, FlexibleNeuralNetwork from world.objects import DefaultCell class HUD: def __init__(self): self.font = pygame.font.Font("freesansbold.ttf", FONT_SIZE) self.legend_font = pygame.font.Font("freesansbold.ttf", LEGEND_FONT_SIZE) def render_mouse_position(self, screen, camera): """Render mouse position in top left.""" mouse_x, mouse_y = camera.get_real_coordinates(*pygame.mouse.get_pos()) 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 = (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) 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) 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 = (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 = 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 = (SCREEN_WIDTH // 2, 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 = (SCREEN_WIDTH - legend_width) // 2 legend_y = 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 # 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 = 30 # 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 WEIGHT_NORMALIZATION_DIVISOR = 2 # Divisor for normalizing weights to [-1, 1] range MAX_CONNECTION_THICKNESS = 3 # Maximum thickness for connection lines MIN_CONNECTION_THICKNESS = 1 # Minimum thickness for connection lines # Connection colors (RGB values) CONNECTION_BASE_INTENSITY = 128 # Base color intensity for connections CONNECTION_POSITIVE_GREEN = 128 # Green component for positive weights CONNECTION_NEGATIVE_RED = 128 # Red component for negative weights # 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 = 35 # 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 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 viz_x = SCREEN_WIDTH - VIZ_RIGHT_MARGIN # Right side of screen viz_y = (SCREEN_HEIGHT // 2) - (VIZ_HEIGHT // 2) # Centered vertically layer_spacing = VIZ_WIDTH // max(1, len(network.layers) - 1) if len(network.layers) > 1 else VIZ_WIDTH # 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) # 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) # 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 # Color based on weight: red for negative, green for positive weight_normalized = max(ACTIVATION_CLAMP_MIN, min(ACTIVATION_CLAMP_MAX, weight / WEIGHT_NORMALIZATION_DIVISOR)) if weight_normalized >= 0: # Positive weight: interpolate from gray to green intensity = int(weight_normalized * 255) color = (max(0, CONNECTION_BASE_INTENSITY - intensity), CONNECTION_BASE_INTENSITY + intensity // 2, max(0, CONNECTION_BASE_INTENSITY - intensity)) else: # Negative weight: interpolate from gray to red intensity = int(-weight_normalized * 255) color = (CONNECTION_BASE_INTENSITY + intensity // 2, max(0, CONNECTION_BASE_INTENSITY - intensity), max(0, CONNECTION_BASE_INTENSITY - intensity)) # Line thickness based on weight magnitude thickness = max(MIN_CONNECTION_THICKNESS, int(abs(weight_normalized) * 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 layer_labels = ["Input", "Hidden", "Output"] for layer_idx in range(len(network.layers)): if layer_idx >= len(layer_labels): label = f"Layer {layer_idx}" else: label = layer_labels[layer_idx] if layer_idx < len(layer_labels) else f"Hidden {layer_idx - 1}" # 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) # 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']}" ] 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)