From 26f166ebeeba69b51f178ad826d4b80b082aa423 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 15 Jun 2025 23:44:50 -0500 Subject: [PATCH] refactored a lot of stuff into different files and generally fixed force application. --- config/constants.py | 72 +++++ core/input_handler.py | 163 ++++++++++ core/renderer.py | 241 +++++++++++++++ main.py | 546 ++-------------------------------- ui/hud.py | 126 ++++++++ world/base/brain.py | 14 +- world/objects.py | 44 ++- world/simulation_interface.py | 7 + 8 files changed, 678 insertions(+), 535 deletions(-) create mode 100644 config/constants.py create mode 100644 core/input_handler.py create mode 100644 core/renderer.py create mode 100644 ui/hud.py diff --git a/config/constants.py b/config/constants.py new file mode 100644 index 0000000..780dc59 --- /dev/null +++ b/config/constants.py @@ -0,0 +1,72 @@ +# config/constants.py +"""Configuration constants for the simulation.""" + +# Screen settings +SCREEN_WIDTH = 1920 // 2 +SCREEN_HEIGHT = 1080 // 2 + +# Colors +BLACK = (0, 0, 0) +DARK_GRAY = (64, 64, 64) +GRAY = (128, 128, 128) +WHITE = (255, 255, 255) +RED = (255, 0, 0) +BLUE = (0, 0, 255) +GREEN = (0, 255, 0) +LIGHT_BLUE = (52, 134, 235) +SELECTION_BLUE = (0, 128, 255) +SELECTION_GRAY = (128, 128, 128, 80) +SELECTION_BORDER = (80, 80, 90) + +# Grid settings +GRID_WIDTH = 30 +GRID_HEIGHT = 25 +CELL_SIZE = 20 +RENDER_BUFFER = 50 + +# Performance settings +DEFAULT_TPS = 20 +MAX_FPS = 180 +TURBO_MULTIPLIER = 4 + +# Camera settings +DEFAULT_CAMERA_SPEED = 700 +CAMERA_SPEED_INCREMENT = 350 +MIN_CAMERA_SPEED = 350 +MAX_CAMERA_SPEED = 2100 + +# UI settings +FONT_SIZE = 16 +LEGEND_FONT_SIZE = 14 +HUD_MARGIN = 10 +LINE_HEIGHT = 20 +SELECTION_THRESHOLD = 3 # pixels + +# Simulation settings +FOOD_SPAWNING = True +RANDOM_SEED = 0 + +# Vector visualization settings +ACCELERATION_SCALE = 1000 +VELOCITY_SCALE = 50 +ANGULAR_ACCELERATION_SCALE = 50 +ARROW_TIP_SIZE = 5 +ANGULAR_TIP_SIZE = 2.5 +DIRECTION_TIP_SIZE = 3 + +KEYMAP_LEGEND = [ + ("WASD", "Move camera"), + ("Mouse wheel", "Zoom in/out"), + ("Middle mouse", "Pan camera"), + ("R", "Reset camera"), + ("G", "Toggle grid"), + ("I", "Toggle interaction radius"), + ("ESC", "Deselect/Exit"), + ("Left click", "Select object(s)"), + ("Drag select", "Select multiple objects"), + ("Click on object", "Select closest object in range"), + ("Up/Down", "Increase/Decrease camera speed"), + ("Shift", "Double TPS (for testing)"), + ("L", "Toggle this legend"), + ("Space", "Pause/Resume simulation"), +] \ No newline at end of file diff --git a/core/input_handler.py b/core/input_handler.py new file mode 100644 index 0000000..45a73ca --- /dev/null +++ b/core/input_handler.py @@ -0,0 +1,163 @@ +# core/input_handler.py +"""Handles all input events and camera controls.""" + +import pygame +from config.constants import * + + +class InputHandler: + def __init__(self, camera, world): + self.camera = camera + self.world = world + + # Selection state + self.selecting = False + self.select_start = None + self.select_end = None + self.selected_objects = [] + + # UI state flags + self.show_grid = True + self.show_interaction_radius = False + self.show_legend = False + self.is_paused = False + + # Speed control + self.tps = DEFAULT_TPS + self.default_tps = DEFAULT_TPS + + def handle_events(self, events): + """Process all pygame events and return game state.""" + running = True + + for event in events: + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.KEYDOWN: + running = self._handle_keydown(event, running) + elif event.type == pygame.KEYUP: + self._handle_keyup(event) + elif event.type == pygame.MOUSEWHEEL: + self.camera.handle_zoom(event.y) + elif event.type == pygame.MOUSEBUTTONDOWN: + self._handle_mouse_down(event) + elif event.type == pygame.MOUSEBUTTONUP: + self._handle_mouse_up(event) + elif event.type == pygame.MOUSEMOTION: + self._handle_mouse_motion(event) + + return running + + def _handle_keydown(self, event, running): + """Handle keydown events.""" + if event.key == pygame.K_ESCAPE: + if len(self.selected_objects) == 0: + running = False + else: + self.selecting = False + self.selected_objects = [] + elif event.key == pygame.K_g: + self.show_grid = not self.show_grid + elif event.key == pygame.K_UP: + if self.camera.speed < MAX_CAMERA_SPEED: + self.camera.speed += CAMERA_SPEED_INCREMENT + elif event.key == pygame.K_DOWN: + if self.camera.speed > MIN_CAMERA_SPEED: + self.camera.speed -= CAMERA_SPEED_INCREMENT + elif event.key == pygame.K_i: + self.show_interaction_radius = not self.show_interaction_radius + elif event.key == pygame.K_l: + self.show_legend = not self.show_legend + elif event.key == pygame.K_SPACE: + self.is_paused = not self.is_paused + elif event.key == pygame.K_LSHIFT: + self.tps = self.default_tps * TURBO_MULTIPLIER + elif event.key == pygame.K_r: + self.camera.reset_position() + + return running + + def _handle_keyup(self, event): + """Handle keyup events.""" + if event.key == pygame.K_LSHIFT: + self.tps = self.default_tps + + def _handle_mouse_down(self, event): + """Handle mouse button down events.""" + if event.button == 2: # Middle mouse button + self.camera.start_panning(event.pos) + elif event.button == 1: # Left mouse button + self.selecting = True + self.select_start = event.pos + self.select_end = event.pos + + def _handle_mouse_up(self, event): + """Handle mouse button up events.""" + if event.button == 2: + self.camera.stop_panning() + elif event.button == 1 and self.selecting: + self._handle_selection() + + def _handle_mouse_motion(self, event): + """Handle mouse motion events.""" + self.camera.pan(event.pos) + if self.selecting: + self.select_end = event.pos + + def _handle_selection(self): + """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) + + # 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): + 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) + obj = self.world.query_closest_object(mouse_world_x, mouse_world_y) + self.selected_objects = [] + + if obj: + obj_x, obj_y = obj.position.get_position() + dx = obj_x - mouse_world_x + dy = obj_y - mouse_world_y + dist = (dx ** 2 + dy ** 2) ** 0.5 + if dist <= obj.max_visual_width / 2: + self.selected_objects = [obj] + + print(f"Clicked: selected {len(self.selected_objects)} object(s)") + + def _handle_drag_selection(self, x1, y1, x2, y2): + """Handle drag selection.""" + min_x, max_x = min(x1, x2), max(x1, x2) + min_y, max_y = min(y1, y2), max(y1, y2) + self.selected_objects = self.world.query_objects_in_range(min_x, min_y, max_x, max_y) + print(f"Selected {len(self.selected_objects)} objects in range: {min_x}, {min_y} to {max_x}, {max_y}") + + def update_camera(self, keys, deltatime): + """Update camera based on currently pressed keys.""" + self.camera.update(keys, deltatime) + + def update_selected_objects(self): + """Ensure selected objects are still valid.""" + self.selected_objects = [ + obj for obj in self.selected_objects if obj in self.world.get_objects() + ] + + def get_selection_rect(self): + """Get current selection rectangle for rendering.""" + if self.selecting and self.select_start and self.select_end: + left = min(self.select_start[0], self.select_end[0]) + top = min(self.select_start[1], self.select_end[1]) + width = abs(self.select_end[0] - self.select_start[0]) + height = abs(self.select_end[1] - self.select_start[1]) + return (left, top, width, height) + return None \ No newline at end of file diff --git a/core/renderer.py b/core/renderer.py new file mode 100644 index 0000000..509b9bc --- /dev/null +++ b/core/renderer.py @@ -0,0 +1,241 @@ +# core/renderer.py +"""Handles all rendering operations.""" + +import pygame +import math +from config.constants import * + + +class Renderer: + def __init__(self, screen): + self.screen = screen + + def clear_screen(self): + """Clear the screen with a black background.""" + self.screen.fill(BLACK) + + def draw_grid(self, camera, showing_grid=True): + """Draw the reference grid.""" + if not showing_grid: + return + + # Calculate effective cell size with zoom + effective_cell_size = CELL_SIZE * camera.zoom + + # Calculate grid boundaries in world coordinates (centered at 0,0) + grid_world_width = GRID_WIDTH * effective_cell_size + 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_left = grid_center_x - grid_world_width // 2 + grid_top = grid_center_y - grid_world_height // 2 + grid_right = grid_left + grid_world_width + 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): + 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), + ) + + if grid_rect.width > 0 and grid_rect.height > 0: + pygame.draw.rect(self.screen, DARK_GRAY, grid_rect) + + # Draw grid lines only if zoom is high enough + if effective_cell_size > 4: + self._draw_grid_lines(grid_left, grid_top, grid_right, grid_bottom, effective_cell_size) + + def _draw_grid_lines(self, grid_left, grid_top, grid_right, grid_bottom, effective_cell_size): + """Draw the grid lines.""" + vertical_lines = [] + horizontal_lines = [] + + for i in range(max(GRID_WIDTH, GRID_HEIGHT) + 1): + # Vertical lines + if i <= GRID_WIDTH: + line_x = grid_left + i * effective_cell_size + if 0 <= line_x <= SCREEN_WIDTH: + start_y = max(0, grid_top) + end_y = min(SCREEN_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: + start_x = max(0, grid_left) + end_x = min(SCREEN_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) + for start, end in horizontal_lines: + pygame.draw.line(self.screen, GRAY, start, end) + + def render_world(self, world, camera): + """Render all world objects.""" + world.render_all(camera, self.screen) + + def render_interaction_radius(self, world, camera, selected_objects, show_radius=False): + """Render interaction radius and debug vectors for objects.""" + if not show_radius: + return + + for obj in world.get_objects(): + obj_x, obj_y = obj.position.get_position() + radius = obj.interaction_radius + + if radius > 0 and camera.is_in_view(obj_x, obj_y, margin=radius): + if selected_objects and obj not in selected_objects: + continue + + screen_x, screen_y = camera.world_to_screen(obj_x, obj_y) + screen_radius = int(radius * camera.zoom) + + if screen_radius > 0: + # Draw interaction radius circle + pygame.draw.circle(self.screen, RED, (screen_x, screen_y), screen_radius, 1) + + # Draw direction arrow + self._draw_direction_arrow(obj, screen_x, screen_y, camera) + + # Draw debug vectors + self._draw_debug_vectors(obj, screen_x, screen_y, camera) + + def _draw_direction_arrow(self, obj, screen_x, screen_y, camera): + """Draw direction arrow for an object.""" + rotation_angle = obj.rotation.get_rotation() + arrow_length = obj.max_visual_width / 2 * camera.zoom + + end_x = screen_x + arrow_length * math.cos(math.radians(rotation_angle)) + 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) + + # Draw arrowhead + tip_size = DIRECTION_TIP_SIZE * camera.zoom + left_tip_x = end_x - tip_size * math.cos(math.radians(rotation_angle + 150 + 180)) + left_tip_y = end_y - tip_size * math.sin(math.radians(rotation_angle + 150 + 180)) + right_tip_x = end_x - tip_size * math.cos(math.radians(rotation_angle - 150 + 180)) + right_tip_y = end_y - tip_size * math.sin(math.radians(rotation_angle - 150 + 180)) + + pygame.draw.polygon( + self.screen, WHITE, + [(end_x, end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)] + ) + + def _draw_debug_vectors(self, obj, screen_x, screen_y, camera): + """Draw debug vectors (acceleration, velocity, angular acceleration).""" + # Draw angular acceleration + if hasattr(obj, 'angular_acceleration'): + self._draw_angular_acceleration(obj, screen_x, screen_y, camera) + + # Draw acceleration vector + if hasattr(obj, 'acceleration') and isinstance(obj.acceleration, tuple) and len(obj.acceleration) == 2: + self._draw_acceleration_vector(obj, screen_x, screen_y, camera) + + # Draw velocity vector + if hasattr(obj, 'velocity') and isinstance(obj.velocity, tuple) and len(obj.velocity) == 2: + self._draw_velocity_vector(obj, screen_x, screen_y, camera) + + def _draw_angular_acceleration(self, obj, screen_x, screen_y, camera): + """Draw angular acceleration vector.""" + rotation_angle = obj.rotation.get_rotation() + arrow_length = obj.max_visual_width / 2 * camera.zoom + end_x = screen_x + arrow_length * math.cos(math.radians(rotation_angle)) + end_y = screen_y + arrow_length * math.sin(math.radians(rotation_angle)) + + angular_acceleration = obj.angular_acceleration + angular_accel_magnitude = abs(angular_acceleration) * ANGULAR_ACCELERATION_SCALE * camera.zoom + angular_direction = rotation_angle + 90 if angular_acceleration >= 0 else rotation_angle - 90 + + 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) + + # Draw arrowhead + self._draw_arrowhead(angular_acc_end_x, angular_acc_end_y, angular_direction, + ANGULAR_TIP_SIZE * camera.zoom, LIGHT_BLUE) + + def _draw_acceleration_vector(self, obj, screen_x, screen_y, camera): + """Draw acceleration vector.""" + acc_x, acc_y = obj.acceleration + acc_magnitude = math.sqrt(acc_x ** 2 + acc_y ** 2) + + if acc_magnitude > 0: + acc_direction = math.degrees(math.atan2(acc_y, acc_x)) + acc_vector_length = acc_magnitude * ACCELERATION_SCALE * camera.zoom + 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) + self._draw_arrowhead(acc_end_x, acc_end_y, acc_direction, + ARROW_TIP_SIZE * camera.zoom, RED) + + def _draw_velocity_vector(self, obj, screen_x, screen_y, camera): + """Draw velocity vector.""" + vel_x, vel_y = obj.velocity + vel_magnitude = math.sqrt(vel_x ** 2 + vel_y ** 2) + + if vel_magnitude > 0: + vel_direction = math.degrees(math.atan2(vel_y, vel_x)) + vel_vector_length = vel_magnitude * VELOCITY_SCALE * camera.zoom + 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) + self._draw_arrowhead(vel_end_x, vel_end_y, vel_direction, + ARROW_TIP_SIZE * camera.zoom, BLUE) + + def _draw_arrowhead(self, end_x, end_y, direction, tip_size, color): + """Draw an arrowhead at the specified position.""" + left_tip_x = end_x - tip_size * math.cos(math.radians(direction + 150 + 180)) + left_tip_y = end_y - tip_size * math.sin(math.radians(direction + 150 + 180)) + right_tip_x = end_x - tip_size * math.cos(math.radians(direction - 150 + 180)) + right_tip_y = end_y - tip_size * math.sin(math.radians(direction - 150 + 180)) + + pygame.draw.polygon( + self.screen, 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.""" + if not selection_rect: + return + + left, top, width, height = selection_rect + + # Draw semi-transparent fill + s = pygame.Surface((width, height), pygame.SRCALPHA) + s.fill(SELECTION_GRAY) + self.screen.blit(s, (left, top)) + + # Draw border + pygame.draw.rect(self.screen, SELECTION_BORDER, + pygame.Rect(left, top, width, height), 1) + + def render_selected_objects_outline(self, selected_objects, camera): + """Render blue outline for selected objects.""" + for obj in selected_objects: + obj_x, obj_y = obj.position.get_position() + width = obj.max_visual_width if hasattr(obj, "max_visual_width") else 10 + 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) \ No newline at end of file diff --git a/main.py b/main.py index 0d23739..7e8fe6a 100644 --- a/main.py +++ b/main.py @@ -6,116 +6,26 @@ import sys import random from world.world import World, Position, Rotation -from world.objects import DebugRenderObject, FoodObject, TestVelocityObject, DefaultCell +from world.objects import FoodObject, TestVelocityObject, DefaultCell from world.simulation_interface import Camera +from config.constants import * + +from core.input_handler import InputHandler +from core.renderer import Renderer + +from ui.hud import HUD + # Initialize Pygame pygame.init() -# Constants -SCREEN_WIDTH = 1920 / 2 -SCREEN_HEIGHT = 1080 / 2 -BLACK = (0, 0, 0) -DARK_GRAY = (64, 64, 64) -GRAY = (128, 128, 128) -WHITE = (255, 255, 255) -RENDER_BUFFER = 50 -SPEED = 700 # Pixels per second - -# Grid settings -GRID_WIDTH = 30 # Number of cells horizontally -GRID_HEIGHT = 25 # Number of cells vertically -CELL_SIZE = 20 # Size of each cell in pixels - -DEFAULT_TPS = 20 # Number of ticks per second for the simulation -FOOD_SPAWNING = True - - -def draw_grid(screen, camera, showing_grid=True): - # Fill the screen with black - screen.fill(BLACK) - - # Calculate effective cell size with zoom - effective_cell_size = CELL_SIZE * camera.zoom - - # Calculate grid boundaries in world coordinates (centered at 0,0) - grid_world_width = GRID_WIDTH * effective_cell_size - 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_left = grid_center_x - grid_world_width // 2 - grid_top = grid_center_y - grid_world_height // 2 - grid_right = grid_left + grid_world_width - grid_bottom = grid_top + grid_world_height - - # Check if grid should be shown - if not showing_grid: - return # Exit early if grid is not visible - - # 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 - ): - return # Grid is completely off-screen - - # Fill the grid area awith 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), - ) - - # Only draw if the rectangle has positive dimensions - if grid_rect.width > 0 and grid_rect.height > 0: - pygame.draw.rect(screen, DARK_GRAY, grid_rect) - - # Draw vertical grid lines (only if zoom is high enough to see them clearly) - if effective_cell_size > 4: - # Precompute grid boundaries - vertical_lines = [] - horizontal_lines = [] - - for i in range(max(GRID_WIDTH, GRID_HEIGHT) + 1): - # Vertical lines - if i <= GRID_WIDTH: - line_x = grid_left + i * effective_cell_size - if 0 <= line_x <= SCREEN_WIDTH: - start_y = max(0, grid_top) - end_y = min(SCREEN_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: - start_x = max(0, grid_left) - end_x = min(SCREEN_WIDTH, grid_right) - if start_x < end_x: - horizontal_lines.append(((start_x, line_y), (end_x, line_y))) - - # Draw all vertical lines in one batch - for start, end in vertical_lines: - pygame.draw.line(screen, GRAY, start, end) - - # Draw all horizontal lines in one batch - for start, end in horizontal_lines: - pygame.draw.line(screen, GRAY, start, end) def setup(world: World): if FOOD_SPAWNING: world.add_object(FoodObject(Position(x=random.randint(-100, 100), y=random.randint(-100, 100)))) - world.add_object(TestVelocityObject(Position(x=random.randint(-100, 100), y=random.randint(-100, 100)))) - - world.add_object(DefaultCell(Position(x=0,y=0), Rotation(angle=0))) + for i in range(100): + world.add_object(DefaultCell(Position(x=random.randint(-100, 100),y=random.randint(-100, 100)), Rotation(angle=0))) return world @@ -126,34 +36,12 @@ def main(): clock = pygame.time.Clock() camera = Camera(SCREEN_WIDTH, SCREEN_HEIGHT, RENDER_BUFFER) - is_showing_grid = True # Flag to control grid visibility - show_interaction_radius = False # Flag to control interaction radius visibility - showing_legend = False # Flag to control legend visibility - is_paused = False # Flag to control simulation pause state - - font = pygame.font.Font("freesansbold.ttf", 16) - - tps = DEFAULT_TPS # Default ticks per second - last_tick_time = time.perf_counter() # Tracks the last tick time last_tps_time = time.perf_counter() # Tracks the last TPS calculation time tick_counter = 0 # Counts ticks executed actual_tps = 0 # Stores the calculated TPS total_ticks = 0 # Total ticks executed - # Selection state - selecting = False - select_start = None # (screen_x, screen_y) - select_end = None # (screen_x, screen_y) - selected_objects = [] - - print("Controls:") - print("WASD - Move camera") - print("Mouse wheel - Zoom in/out") - print("Middle mouse button - Pan camera") - print("R - Reset camera to origin") - print("ESC or close window - Exit") - # Initialize world world = World(CELL_SIZE, (CELL_SIZE * GRID_WIDTH, CELL_SIZE * GRID_HEIGHT)) @@ -162,93 +50,19 @@ def main(): world = setup(world) + input_handler = InputHandler(camera, world) + renderer = Renderer(screen) + hud = HUD() + running = True while running: deltatime = clock.get_time() / 1000.0 # Convert milliseconds to seconds - tick_interval = 1.0 / tps # Time per tick + tick_interval = 1.0 / input_handler.tps # Time per tick # Handle events - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_ESCAPE: - if len(selected_objects) == 0: - running = False - selecting = False - selected_objects = [] - if event.key == pygame.K_g: - is_showing_grid = not is_showing_grid - if event.key == pygame.K_UP: - if camera.speed < 2100: - camera.speed += 350 - if event.key == pygame.K_DOWN: - if camera.speed > 350: - camera.speed -= 350 - if event.key == pygame.K_i: - show_interaction_radius = not show_interaction_radius - if event.key == pygame.K_l: - showing_legend = not showing_legend - if event.key == pygame.K_SPACE: - is_paused = not is_paused - if event.key == pygame.K_LSHIFT: - tps = DEFAULT_TPS * 4 - elif event.type == pygame.KEYUP: - if event.key == pygame.K_LSHIFT: - tps = DEFAULT_TPS - elif event.type == pygame.MOUSEWHEEL: - camera.handle_zoom(event.y) - elif event.type == pygame.MOUSEBUTTONDOWN: - if event.button == 2: # Middle mouse button - camera.start_panning(event.pos) - elif event.button == 1: # Left mouse button - selecting = True - select_start = event.pos - select_end = event.pos - elif event.type == pygame.MOUSEBUTTONUP: - if event.button == 2: - camera.stop_panning() - elif event.button == 1 and selecting: - selecting = False - # Convert screen to world coordinates - x1, y1 = camera.get_real_coordinates(*select_start) - x2, y2 = camera.get_real_coordinates(*select_end) - # If the selection rectangle is very small, treat as a click - if ( - abs(select_start[0] - select_end[0]) < 3 - and abs(select_start[1] - select_end[1]) < 3 - ): - # Single click: select closest object if in range - mouse_world_x, mouse_world_y = camera.get_real_coordinates( - *select_start - ) - obj = world.query_closest_object(mouse_world_x, mouse_world_y) - selected_objects = [] - if obj: - obj_x, obj_y = obj.position.get_position() - # Calculate distance in world coordinates - dx = obj_x - mouse_world_x - dy = obj_y - mouse_world_y - dist = (dx ** 2 + dy ** 2) ** 0.5 - if dist <= obj.max_visual_width / 2: - selected_objects = [obj] - print(f"Clicked: selected {len(selected_objects)} object(s)") - else: - # Drag select: select all in rectangle - min_x, max_x = min(x1, x2), max(x1, x2) - min_y, max_y = min(y1, y2), max(y1, y2) - selected_objects = world.query_objects_in_range( - min_x, min_y, max_x, max_y - ) - print( - f"Selected {len(selected_objects)} objects in range: {min_x}, {min_y} to {max_x}, {max_y}" - ) - elif event.type == pygame.MOUSEMOTION: - camera.pan(event.pos) - if selecting: - select_end = event.pos + running = input_handler.handle_events(pygame.event.get()) - if not is_paused: + if not input_handler.is_paused: # Tick logic (runs every tick interval) current_time = time.perf_counter() while current_time - last_tick_time >= tick_interval: @@ -256,15 +70,8 @@ def main(): tick_counter += 1 total_ticks += 1 - # gets every object in the world and returns amount of FoodObjects - objects = world.get_objects() - food = len([obj for obj in objects if isinstance(obj, FoodObject)]) - - # ensure selected objects are still valid or have not changed position, if so, reselect them - selected_objects = [ - obj for obj in selected_objects if obj in world.get_objects() - ] + input_handler.update_selected_objects() world.tick_all() @@ -279,315 +86,28 @@ def main(): # Get pressed keys for smooth movement keys = pygame.key.get_pressed() - camera.update(keys, deltatime) + input_handler.update_camera(keys, deltatime) - # Draw the reference grid - draw_grid(screen, camera, is_showing_grid) + renderer.clear_screen() + renderer.draw_grid(camera, input_handler.show_grid) + renderer.render_world(world, camera) - # Render everything in the world - world.render_all(camera, screen) + renderer.render_interaction_radius(world, camera, input_handler.selected_objects, input_handler.show_interaction_radius) - if show_interaction_radius: - for obj in world.get_objects(): - obj_x, obj_y = obj.position.get_position() - radius = obj.interaction_radius - if radius > 0 and camera.is_in_view(obj_x, obj_y, margin=radius): - if selected_objects and obj not in selected_objects: - continue # Skip if not selected and selecting - screen_x, screen_y = camera.world_to_screen(obj_x, obj_y) - screen_radius = int(radius * camera.zoom) - if screen_radius > 0: - pygame.draw.circle( - screen, - (255, 0, 0), # Red - (screen_x, screen_y), - screen_radius, - 1 # 1 pixel thick - ) + renderer.render_selection_rectangle(input_handler.get_selection_rect()) + renderer.render_selected_objects_outline(input_handler.selected_objects, camera) - # Draw direction arrow - rotation_angle = obj.rotation.get_rotation() - arrow_length = obj.max_visual_width/2 * camera.zoom # Scale arrow length with zoom - arrow_color = (255, 255, 255) # Green - - # Calculate the arrow's end-point based on rotation angle - end_x = screen_x + arrow_length * math.cos(math.radians(rotation_angle)) - end_y = screen_y + arrow_length * math.sin(math.radians(rotation_angle)) - - # Draw the arrow line - pygame.draw.line(screen, arrow_color, (screen_x, screen_y), (end_x, end_y), 2) - - # Draw a rotated triangle for the arrowhead - tip_size = 3 * camera.zoom # Scale triangle tip size with zoom - left_tip_x = end_x - tip_size * math.cos(math.radians(rotation_angle + 150 + 180)) - left_tip_y = end_y - tip_size * math.sin(math.radians(rotation_angle + 150 + 180)) - right_tip_x = end_x - tip_size * math.cos(math.radians(rotation_angle - 150 + 180)) - right_tip_y = end_y - tip_size * math.sin(math.radians(rotation_angle - 150 + 180)) - - # Draw arrowhead (triangle) for direction - pygame.draw.polygon( - screen, - arrow_color, - [(end_x, end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)] - ) - - # Draw angular acceleration arrow (if present) - if hasattr(obj, 'angular_acceleration'): - angular_acceleration = obj.angular_acceleration - - # Scale the angular acceleration value for visibility - angular_accel_magnitude = abs( - angular_acceleration) * 50 * camera.zoom # Use absolute magnitude for scaling - - # Determine the perpendicular direction based on the sign of angular_acceleration - angular_direction = rotation_angle + 90 if angular_acceleration >= 0 else rotation_angle - 90 - - # Calculate the end of the angular acceleration vector - 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)) - - # Draw the angular acceleration vector as a red line - pygame.draw.line(screen, (52, 134, 235), (end_x, end_y), - (angular_acc_end_x, angular_acc_end_y), 2) - - # Add an arrowhead to the angular acceleration vector - angular_tip_size = 2.5 * camera.zoom - left_angular_tip_x = angular_acc_end_x - angular_tip_size * math.cos( - math.radians(angular_direction + 150 + 180)) - left_angular_tip_y = angular_acc_end_y - angular_tip_size * math.sin( - math.radians(angular_direction + 150 + 180)) - right_angular_tip_x = angular_acc_end_x - angular_tip_size * math.cos( - math.radians(angular_direction - 150 + 180)) - right_angular_tip_y = angular_acc_end_y - angular_tip_size * math.sin( - math.radians(angular_direction - 150 + 180)) - - # Draw arrowhead (triangle) for angular acceleration - pygame.draw.polygon( - screen, - (52, 134, 235), # Red arrowhead - [(angular_acc_end_x, angular_acc_end_y), (left_angular_tip_x, left_angular_tip_y), - (right_angular_tip_x, right_angular_tip_y)] - ) - - # If object has an acceleration attribute, draw a red vector with arrowhead - if hasattr(obj, 'acceleration') and isinstance(obj.acceleration, tuple) and len( - obj.acceleration) == 2: - acc_x, acc_y = obj.acceleration - - # Calculate acceleration magnitude and direction - acc_magnitude = math.sqrt(acc_x ** 2 + acc_y ** 2) - if acc_magnitude > 0: - acc_direction = math.degrees(math.atan2(acc_y, acc_x)) # Get the angle in degrees - - # Calculate scaled acceleration vector's end point - acc_vector_length = acc_magnitude * 1000 * camera.zoom # Scale length with zoom - 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)) - - # Draw the acceleration vector as a red line - pygame.draw.line(screen, (255, 0, 0), (screen_x, screen_y), (acc_end_x, acc_end_y), 2) - - # Add arrowhead to acceleration vector - acc_tip_size = 5 * camera.zoom - left_tip_x = acc_end_x - acc_tip_size * math.cos(math.radians(acc_direction + 150 + 180)) - left_tip_y = acc_end_y - acc_tip_size * math.sin(math.radians(acc_direction + 150 + 180)) - right_tip_x = acc_end_x - acc_tip_size * math.cos(math.radians(acc_direction - 150 + 180)) - right_tip_y = acc_end_y - acc_tip_size * math.sin(math.radians(acc_direction - 150 + 180)) - - pygame.draw.polygon( - screen, - (255, 0, 0), # Red arrowhead - [(acc_end_x, acc_end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)] - ) - - # If object has a velocity attribute, draw a blue vector with arrowhead - if hasattr(obj, 'velocity') and isinstance(obj.velocity, tuple) and len(obj.velocity) == 2: - vel_x, vel_y = obj.velocity - - # Calculate velocity magnitude and direction - vel_magnitude = math.sqrt(vel_x ** 2 + vel_y ** 2) - if vel_magnitude > 0: - vel_direction = math.degrees(math.atan2(vel_y, vel_x)) # Get the angle in degrees - - # Calculate scaled velocity vector's end point - vel_vector_length = vel_magnitude * 50 * camera.zoom # Scale length with zoom - 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)) - - # Draw the velocity vector as a blue line - pygame.draw.line(screen, (0, 0, 255), (screen_x, screen_y), (vel_end_x, vel_end_y), 2) - - # Add arrowhead to velocity vector - vel_tip_size = 5 * camera.zoom - left_tip_x = vel_end_x - vel_tip_size * math.cos(math.radians(vel_direction + 150 + 180)) - left_tip_y = vel_end_y - vel_tip_size * math.sin(math.radians(vel_direction + 150 + 180)) - right_tip_x = vel_end_x - vel_tip_size * math.cos(math.radians(vel_direction - 150 + 180)) - right_tip_y = vel_end_y - vel_tip_size * math.sin(math.radians(vel_direction - 150 + 180)) - - pygame.draw.polygon( - screen, - (0, 0, 255), # Blue arrowhead - [(vel_end_x, vel_end_y), (left_tip_x, left_tip_y), (right_tip_x, right_tip_y)] - ) - - # Draw selection rectangle if selecting - if selecting and select_start and select_end: - rect_color = (128, 128, 128, 80) # Gray, semi-transparent - border_color = (80, 80, 90) # Slightly darker gray for border - - left = min(select_start[0], select_end[0]) - top = min(select_start[1], select_end[1]) - width = abs(select_end[0] - select_start[0]) - height = abs(select_end[1] - select_start[1]) - - s = pygame.Surface((width, height), pygame.SRCALPHA) - s.fill(rect_color) - screen.blit(s, (left, top)) - - # Draw 1-pixel border - pygame.draw.rect( - screen, border_color, pygame.Rect(left, top, width, height), 1 - ) - - # Draw blue outline for selected objects - for obj in selected_objects: - obj_x, obj_y = obj.position.get_position() - width = obj.max_visual_width if hasattr(obj, "max_visual_width") else 10 - 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(screen, (0, 128, 255), rect, 1) # Blue, 1px wide - - # Render mouse position as text in top left of screen - mouse_x, mouse_y = camera.get_real_coordinates(*pygame.mouse.get_pos()) - mouse_text = font.render(f"Mouse: ({mouse_x:.2f}, {mouse_y:.2f})", True, WHITE) - text_rect = mouse_text.get_rect() - text_rect.topleft = (10, 10) - screen.blit(mouse_text, text_rect) - - # Render FPS in top right - fps_text = font.render(f"FPS: {int(clock.get_fps())}", True, WHITE) - fps_rect = fps_text.get_rect() - fps_rect.topright = (SCREEN_WIDTH - 10, 10) - screen.blit(fps_text, fps_rect) - - # Render TPS in bottom right - tps_text = font.render(f"TPS: {actual_tps}", True, WHITE) - tps_rect = tps_text.get_rect() - tps_rect.bottomright = (SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10) - screen.blit(tps_text, tps_rect) - - # Render tick count in bottom left - tick_text = font.render(f"Ticks: {total_ticks}", True, WHITE) - tick_rect = tick_text.get_rect() - tick_rect.bottomleft = (10, SCREEN_HEIGHT - 10) - screen.blit(tick_text, tick_rect) - - if len(selected_objects) >= 1: - i = 0 - max_width = SCREEN_WIDTH - 20 # Leave some padding from the right edge - for each in selected_objects: - obj = each - text = f"Object: {str(obj)}" - words = text.split() # Split text into words - line = "" - line_height = 20 # Height of each line of text - line_offset = 0 - - for word in words: - test_line = f"{line} {word}".strip() - test_width, _ = font.size(test_line) - - # Check if the line width exceeds the limit - if test_width > max_width and line: - obj_text = font.render(line, True, WHITE) - obj_rect = obj_text.get_rect() - obj_rect.topleft = (10, 30 + i * line_height + line_offset) - screen.blit(obj_text, obj_rect) - line = word # Start a new line - line_offset += line_height - else: - line = test_line - - # Render the last line - if line: - obj_text = font.render(line, True, WHITE) - obj_rect = obj_text.get_rect() - obj_rect.topleft = (10, 30 + i * line_height + line_offset) - screen.blit(obj_text, obj_rect) - - i += 1 - - legend_font = pygame.font.Font("freesansbold.ttf", 14) - - keymap_legend = [ - ("WASD", "Move camera"), - ("Mouse wheel", "Zoom in/out"), - ("Middle mouse", "Pan camera"), - ("R", "Reset camera"), - ("G", "Toggle grid"), - ("I", "Toggle interaction radius"), - ("ESC", "Deselect/Exit"), - ("Left click", "Select object(s)"), - ("Drag select", "Select multiple objects"), - ("Click on object", "Select closest object in range"), - ("Up/Down", "Increase/Decrease camera speed"), - ("Shift", "Double TPS (for testing)"), - ("L", "Toggle this legend"), - ("Space", "Pause/Resume simulation"), - ] - - if showing_legend: - # Split into two columns - mid = (len(keymap_legend) + 1) // 2 - left_col = keymap_legend[:mid] - right_col = keymap_legend[mid:] - - legend_font_height = legend_font.get_height() - column_gap = 40 # Space between columns - - # Calculate max width for each column - left_width = max(legend_font.size(f"{k}: {v}")[0] for k, v in left_col) - right_width = max(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 = 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 = 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) - else: - # just show l to toggle legend - legend_text = 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) - - if is_paused: - pause_text = 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) + hud.render_mouse_position(screen, camera) + hud.render_fps(screen, clock) + hud.render_tps(screen, actual_tps) + hud.render_tick_count(screen, total_ticks) + hud.render_selected_objects_info(screen, input_handler.selected_objects) + hud.render_legend(screen, input_handler.show_legend) + hud.render_pause_indicator(screen, input_handler.is_paused) # Update display pygame.display.flip() - clock.tick(180) + clock.tick(MAX_FPS) pygame.quit() sys.exit() diff --git a/ui/hud.py b/ui/hud.py new file mode 100644 index 0000000..f25701e --- /dev/null +++ b/ui/hud.py @@ -0,0 +1,126 @@ +# ui/hud.py +"""Handles HUD elements and text overlays.""" + +import pygame +from config.constants import * + + +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) \ No newline at end of file diff --git a/world/base/brain.py b/world/base/brain.py index 5bc528a..064cbc6 100644 --- a/world/base/brain.py +++ b/world/base/brain.py @@ -17,7 +17,7 @@ class CellBrain(BehavioralModel): } self.weights = { - 'distance': 1, + 'distance': 0.1, 'angle': 0.5 } @@ -33,15 +33,13 @@ class CellBrain(BehavioralModel): self.inputs['angle'] = input_data.get('angle', 0.0) # Initialize output dictionary - output_data = {'linear_acceleration': self.inputs['distance'] * self.weights['distance'], + self.outputs = {'linear_acceleration': self.inputs['distance'] * self.weights['distance'], 'angular_acceleration': self.inputs['angle'] * self.weights['angle']} - self.outputs = output_data - - return output_data + return self.outputs def __repr__(self): - inputs = {key: round(value, 1) for key, value in self.inputs.items()} - outputs = {key: round(value, 1) for key, value in self.outputs.items()} - weights = {key: round(value, 1) for key, value in self.weights.items()} + inputs = {key: round(value, 5) for key, value in self.inputs.items()} + outputs = {key: round(value, 5) for key, value in self.outputs.items()} + weights = {key: round(value, 5) for key, value in self.weights.items()} return f"CellBrain(inputs={inputs}, outputs={outputs}, weights={weights})" diff --git a/world/objects.py b/world/objects.py index 55a923f..57b538a 100644 --- a/world/objects.py +++ b/world/objects.py @@ -122,7 +122,9 @@ class FoodObject(BaseEntity): if interactable is None: interactable = [] - self.neighbors = len(interactable) + # filter neighbors to only other food objects + food_neighbors = [obj for obj in interactable if isinstance(obj, FoodObject)] + self.neighbors = len(food_neighbors) if self.neighbors > 0: self.decay += self.decay_rate * (1 + (self.neighbors / 10)) @@ -306,25 +308,39 @@ class DefaultCell(BaseEntity): output_data["linear_acceleration"] = max(-0.1, min(0.02, output_data["linear_acceleration"])) output_data["angular_acceleration"] = max(-0.1, min(0.1, output_data["angular_acceleration"])) - # output acceleration is acceleration along its current rotation. - x_component = output_data["linear_acceleration"] * math.cos(math.radians(self.rotation.get_rotation())) - y_component = output_data["linear_acceleration"] * math.sin(math.radians(self.rotation.get_rotation())) + # 2. Apply drag force + drag_coefficient = 0.02 + drag_x = -self.velocity[0] * drag_coefficient + drag_y = -self.velocity[1] * drag_coefficient - self.acceleration = (x_component, y_component) - - # # add drag according to current velocity - # drag_coefficient = 0.3 - # drag_x = -self.velocity[0] * drag_coefficient - # drag_y = -self.velocity[1] * drag_coefficient - # self.acceleration = (self.acceleration[0] + drag_x, self.acceleration[1] + drag_y) + # 3. Combine all forces + total_linear_accel = output_data["linear_acceleration"] + total_linear_accel = max(-0.1, min(0.02, total_linear_accel)) + + # 4. Convert to world coordinates + x_component = total_linear_accel * math.cos(math.radians(self.rotation.get_rotation())) + y_component = total_linear_accel * math.sin(math.radians(self.rotation.get_rotation())) + + # 5. Add drag to total acceleration + total_accel_x = x_component + drag_x + total_accel_y = y_component + drag_y + + self.acceleration = (total_accel_x, total_accel_y) + + rotational_drag = 0.05 + self.angular_acceleration = output_data["angular_acceleration"] - self.rotational_velocity * rotational_drag # tick acceleration velocity_x = self.velocity[0] + self.acceleration[0] velocity_y = self.velocity[1] + self.acceleration[1] self.velocity = (velocity_x, velocity_y) - # clamp velocity - self.velocity = (max(-0.5, min(0.5, self.velocity[0])), max(-0.5, min(0.5, self.velocity[1]))) + # # clamp velocity + max_speed = 0.5 + speed = math.sqrt(self.velocity[0] ** 2 + self.velocity[1] ** 2) + if speed > max_speed: + scale = max_speed / speed + self.velocity = (self.velocity[0] * scale, self.velocity[1] * scale) # tick velocity x, y = self.position.get_position() @@ -338,7 +354,7 @@ class DefaultCell(BaseEntity): self.rotational_velocity += self.angular_acceleration # clamp rotational velocity - self.rotational_velocity = max(-0.5, min(0.5, self.rotational_velocity)) + self.rotational_velocity = max(-3, min(3, self.rotational_velocity)) # tick rotational velocity self.rotation.set_rotation(self.rotation.get_rotation() + self.rotational_velocity) diff --git a/world/simulation_interface.py b/world/simulation_interface.py index 352bc7a..82c3914 100644 --- a/world/simulation_interface.py +++ b/world/simulation_interface.py @@ -114,6 +114,13 @@ class Camera: self.is_panning = False self.last_mouse_pos = None + def reset_position(self) -> None: + """ + Resets the camera position to the origin. + """ + self.target_x = 0 + self.target_y = 0 + def pan(self, mouse_pos: Sequence[int]) -> None: """ Pans the camera based on mouse movement. -- 2.47.2