diff --git a/config/constants.py b/config/constants.py index 2aaa134..93e481d 100644 --- a/config/constants.py +++ b/config/constants.py @@ -25,9 +25,9 @@ CELL_SIZE = 20 RENDER_BUFFER = 50 # Performance settings -DEFAULT_TPS = 20 +DEFAULT_TPS = 40 MAX_FPS = 180 -TURBO_MULTIPLIER = 4 +TURBO_MULTIPLIER = 8 # Camera settings DEFAULT_CAMERA_SPEED = 700 @@ -44,6 +44,7 @@ SELECTION_THRESHOLD = 3 # pixels # Simulation settings FOOD_SPAWNING = True +FOOD_OBJECTS_COUNT = 100 RANDOM_SEED = 0 # Vector visualization settings @@ -58,7 +59,7 @@ DIRECTION_TIP_SIZE = 3 MAX_ACCELERATION = 0.1 MAX_ANGULAR_ACCELERATION = 0.25 MAX_VELOCITY = 0.5 -MAX_ROTATIONAL_VELOCITY = 6 +MAX_ROTATIONAL_VELOCITY = 3 KEYMAP_LEGEND = [ ("WASD", "Move camera"), diff --git a/core/input_handler.py b/core/input_handler.py index 45a73ca..deccff0 100644 --- a/core/input_handler.py +++ b/core/input_handler.py @@ -25,6 +25,7 @@ class InputHandler: # Speed control self.tps = DEFAULT_TPS self.default_tps = DEFAULT_TPS + self.sprint_mode = False def handle_events(self, events): """Process all pygame events and return game state.""" @@ -74,6 +75,8 @@ class InputHandler: self.tps = self.default_tps * TURBO_MULTIPLIER elif event.key == pygame.K_r: self.camera.reset_position() + elif event.key == pygame.K_RSHIFT: + self.sprint_mode = True # Enter sprint mode return running @@ -81,6 +84,8 @@ class InputHandler: """Handle keyup events.""" if event.key == pygame.K_LSHIFT: self.tps = self.default_tps + if event.key == pygame.K_RSHIFT: + self.sprint_mode = False # Exit sprint mode def _handle_mouse_down(self, event): """Handle mouse button down events.""" diff --git a/core/simulation_engine.py b/core/simulation_engine.py index 13f8342..0791d36 100644 --- a/core/simulation_engine.py +++ b/core/simulation_engine.py @@ -39,9 +39,12 @@ class SimulationEngine: random.seed(RANDOM_SEED) if FOOD_SPAWNING: - world.add_object(FoodObject(Position(x=random.randint(-100, 100), y=random.randint(-100, 100)))) + for _ in range(FOOD_OBJECTS_COUNT): + x = random.randint(-100, 100) + y = random.randint(-100, 100) + world.add_object(FoodObject(Position(x=x, y=y))) - for _ in range(1): + for _ in range(20): world.add_object(DefaultCell(Position(x=random.randint(-100, 100), y=random.randint(-100, 100)), Rotation(angle=0))) return world @@ -60,6 +63,30 @@ class SimulationEngine: # Handle events self.running = self.input_handler.handle_events(pygame.event.get()) + if self.input_handler.sprint_mode: + # Sprint mode: run as many ticks as possible, skip rendering + current_time = time.perf_counter() + while True: + self.input_handler.update_selected_objects() + self.world.tick_all() + self.tick_counter += 1 + self.total_ticks += 1 + # Optionally break after some time to allow event processing + if time.perf_counter() - current_time > 0.05: # ~50ms per batch + break + # Update TPS every second + if time.perf_counter() - self.last_tps_time >= 1.0: + self.actual_tps = self.tick_counter + self.tick_counter = 0 + self.last_tps_time = time.perf_counter() + # No rendering or camera update + + self.renderer.clear_screen() + self.hud.render_sprint_debug(self.screen, self.actual_tps, self.total_ticks) + pygame.display.flip() + self.clock.tick(MAX_FPS) + return + if not self.input_handler.is_paused: current_time = time.perf_counter() while current_time - self.last_tick_time >= tick_interval: diff --git a/ui/hud.py b/ui/hud.py index a0b31f7..ef7dff7 100644 --- a/ui/hud.py +++ b/ui/hud.py @@ -462,3 +462,18 @@ class HUD: 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): + """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)) + + y = SCREEN_HEIGHT // 2 - 40 + header_rect = header.get_rect(center=(SCREEN_WIDTH // 2, y)) + tps_rect = tps_text.get_rect(center=(SCREEN_WIDTH // 2, y + 40)) + ticks_rect = ticks_text.get_rect(center=(SCREEN_WIDTH // 2, y + 80)) + + screen.blit(header, header_rect) + screen.blit(tps_text, tps_rect) + screen.blit(ticks_text, ticks_rect) diff --git a/world/objects.py b/world/objects.py index 5ad3380..b061af0 100644 --- a/world/objects.py +++ b/world/objects.py @@ -250,6 +250,8 @@ class DefaultCell(BaseEntity): self.rotational_velocity: int = 0 self.angular_acceleration: int = 0 + self.energy: int = 1000 + self.behavioral_model: CellBrain = CellBrain() self.max_visual_width: int = 10 @@ -274,6 +276,17 @@ class DefaultCell(BaseEntity): :return: Self. """ + if self.energy == 0: + # too hungry lmao + self.flag_for_death() + return self + + self.energy -= 1 + + if self.tick_count > 2000: + # too old lmao + self.flag_for_death() + if interactable is None: interactable = [] @@ -287,14 +300,40 @@ class DefaultCell(BaseEntity): food_object = FoodObject(self.position) angle_between_food = self.calculate_angle_between_food(self.position.get_position(), self.rotation.get_rotation(), food_object.position.get_position()) + distance_to_food = get_distance_between_objects(self, food_object) + + if distance_to_food < self.max_visual_width and food_objects: + self.energy += 100 + food_object.flag_for_death() + return self + + if self.energy >= 1500: + # too much energy, split + duplicate_x, duplicate_y = self.position.get_position() + duplicate_x += random.randint(-self.interaction_radius, self.interaction_radius) + duplicate_y += random.randint(-self.interaction_radius, self.interaction_radius) + + duplicate_x_2, duplicate_y_2 = self.position.get_position() + duplicate_x_2 += random.randint(-self.interaction_radius, self.interaction_radius) + duplicate_y_2 += random.randint(-self.interaction_radius, self.interaction_radius) + + new_cell = DefaultCell(Position(x=int(duplicate_x), y=int(duplicate_y)), Rotation(angle=random.randint(0, 359))) + new_cell.set_brain(self.behavioral_model.mutate(1)) + + new_cell_2 = DefaultCell(Position(x=int(duplicate_x_2), y=int(duplicate_y_2)), Rotation(angle=random.randint(0, 359))) + new_cell_2.set_brain(self.behavioral_model.mutate(1)) + + return [new_cell, new_cell_2] input_data = { - "distance": get_distance_between_objects(self, food_object), + "distance": distance_to_food, "angle": angle_between_food, } output_data = self.behavioral_model.tick(input_data) + # everything below this point is physics simulation and needs to be extracted to a separate class + # clamp accelerations output_data["linear_acceleration"] = max(-MAX_ACCELERATION, min(MAX_ACCELERATION, output_data["linear_acceleration"])) output_data["angular_acceleration"] = max(-MAX_ANGULAR_ACCELERATION, min(MAX_ANGULAR_ACCELERATION, output_data["angular_acceleration"])) @@ -408,4 +447,5 @@ class DefaultCell(BaseEntity): position = f"({round(self.position.x, 1)}, {round(self.position.y, 1)})" velocity = tuple(round(value, 1) for value in self.velocity) acceleration = tuple(round(value, 1) for value in self.acceleration) - return f"DefaultCell(position={position}, velocity={velocity}, acceleration={acceleration}" + rotation = round(self.rotation.get_rotation(), 1) + return f"DefaultCell(position={position}, velocity={velocity}, acceleration={acceleration}, rotation={rotation}, energy={self.energy}, age={self.tick_count})"