Compare commits
	
		
			16 Commits
		
	
	
		
			fc171cd523
			...
			bb521371fa
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| bb521371fa | |||
| 4e05a62274 | |||
| ae88bf0dd2 | |||
|   | 56415bb07a | ||
|   | 9a8217a034 | ||
|   | 0202fe59a5 | ||
|   | 9e7ad041b4 | ||
|   | 55633706a2 | ||
|   | dad79942c2 | ||
|   | f0e00d66b6 | ||
|   | 11bd766e62 | ||
|   | 3a4301f7cb | ||
|   | 9e3c34fd3e | ||
| 5eb2079da1 | |||
| da355dbc6c | |||
|   | b80a5afc4a | 
| @ -19,10 +19,10 @@ jobs: | |||||||
|       run: uv python install |       run: uv python install | ||||||
| 
 | 
 | ||||||
|     - name: Install the project |     - name: Install the project | ||||||
|       run: uv sync --locked --all-extras --dev |       run: uv sync --locked --all-extras --dev --link-mode=copy | ||||||
| 
 | 
 | ||||||
|     - name: Run tests |     - name: Run tests | ||||||
|       run: uv run pytest tests --junit-xml=pytest-results.xml |       run: PYTHONPATH=$(pwd) uv run pytest tests --junit-xml=pytest-results.xml | ||||||
| 
 | 
 | ||||||
|     - name: Upload test results |     - name: Upload test results | ||||||
|       if: always() |       if: always() | ||||||
| @ -34,4 +34,4 @@ jobs: | |||||||
| 
 | 
 | ||||||
|     - name: Print test summary |     - name: Print test summary | ||||||
|       if: always() |       if: always() | ||||||
|       run: uv run pytest tests --tb=short || true |       run: PYTHONPATH=$(pwd) uv run pytest tests --tb=short || true | ||||||
| @ -20,11 +20,3 @@ repos: | |||||||
|       # Compile requirements |       # Compile requirements | ||||||
|       - id: pip-compile |       - id: pip-compile | ||||||
|         args: [ pyproject.toml, -o, requirements.txt ] |         args: [ pyproject.toml, -o, requirements.txt ] | ||||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit |  | ||||||
|     # Ruff version. |  | ||||||
|     rev: v0.11.12 |  | ||||||
|     hooks: |  | ||||||
|       # Run the linter. |  | ||||||
|       - id: ruff |  | ||||||
|       # Run the formatter. |  | ||||||
|       - id: ruff-format |  | ||||||
							
								
								
									
										336
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										336
									
								
								main.py
									
									
									
									
									
								
							| @ -5,6 +5,7 @@ import random | |||||||
| 
 | 
 | ||||||
| from world.world import World, Position | from world.world import World, Position | ||||||
| from world.render_objects import DebugRenderObject, FoodObject | from world.render_objects import DebugRenderObject, FoodObject | ||||||
|  | from world.simulation_interface import Camera | ||||||
| 
 | 
 | ||||||
| # Initialize Pygame | # Initialize Pygame | ||||||
| pygame.init() | pygame.init() | ||||||
| @ -24,106 +25,12 @@ GRID_WIDTH = 20  # Number of cells horizontally | |||||||
| GRID_HEIGHT = 15  # Number of cells vertically | GRID_HEIGHT = 15  # Number of cells vertically | ||||||
| CELL_SIZE = 20  # Size of each cell in pixels | CELL_SIZE = 20  # Size of each cell in pixels | ||||||
| 
 | 
 | ||||||
| DEFAULT_TPS = 20  # Amount of ticks per second for the simulation | DEFAULT_TPS = 20  # Number of ticks per second for the simulation | ||||||
| 
 | FOOD_SPAWNING = True | ||||||
| 
 |  | ||||||
| class Camera: |  | ||||||
|     def __init__(self): |  | ||||||
|         self.x = 0 |  | ||||||
|         self.y = 0 |  | ||||||
|         self.target_x = 0 |  | ||||||
|         self.target_y = 0 |  | ||||||
|         self.zoom = 1.0 |  | ||||||
|         self.target_zoom = 1.0 |  | ||||||
|         self.smoothing = 0.15  # Higher = more responsive, lower = more smooth |  | ||||||
|         self.speed = SPEED |  | ||||||
|         self.zoom_smoothing = 0.10 |  | ||||||
|         self.is_panning = False |  | ||||||
|         self.last_mouse_pos = None |  | ||||||
| 
 |  | ||||||
|     def update(self, keys, deltatime): |  | ||||||
|         # Update target position based on input |  | ||||||
|         if keys[pygame.K_w]: |  | ||||||
|             self.target_y -= self.speed * deltatime / self.zoom |  | ||||||
|         if keys[pygame.K_s]: |  | ||||||
|             self.target_y += self.speed * deltatime / self.zoom |  | ||||||
|         if keys[pygame.K_a]: |  | ||||||
|             self.target_x -= self.speed * deltatime / self.zoom |  | ||||||
|         if keys[pygame.K_d]: |  | ||||||
|             self.target_x += self.speed * deltatime / self.zoom |  | ||||||
|         if keys[pygame.K_r]: |  | ||||||
|             self.target_x = 0 |  | ||||||
|             self.target_y = 0 |  | ||||||
| 
 |  | ||||||
|         # Smooth camera movement with drift |  | ||||||
|         smoothing_factor = 1 - pow( |  | ||||||
|             1 - self.smoothing, deltatime * 60 |  | ||||||
|         )  # Adjust smoothing based on deltatime |  | ||||||
|         self.x += (self.target_x - self.x) * smoothing_factor |  | ||||||
|         self.y += (self.target_y - self.y) * smoothing_factor |  | ||||||
| 
 |  | ||||||
|         # Smooth zoom |  | ||||||
|         zoom_smoothing_factor = 1 - pow(1 - self.zoom_smoothing, deltatime * 60) |  | ||||||
|         self.zoom += (self.target_zoom - self.zoom) * zoom_smoothing_factor |  | ||||||
| 
 |  | ||||||
|     def handle_zoom(self, zoom_delta): |  | ||||||
|         # Zoom in/out with mouse wheel |  | ||||||
|         zoom_factor = 1.1 |  | ||||||
|         if zoom_delta > 0:  # Zoom in |  | ||||||
|             self.target_zoom *= zoom_factor |  | ||||||
|         elif zoom_delta < 0:  # Zoom out |  | ||||||
|             self.target_zoom /= zoom_factor |  | ||||||
| 
 |  | ||||||
|         # Clamp zoom levels |  | ||||||
|         self.target_zoom = max(0.1, min(5.0, self.target_zoom)) |  | ||||||
| 
 |  | ||||||
|     def start_panning(self, mouse_pos): |  | ||||||
|         self.is_panning = True |  | ||||||
|         self.last_mouse_pos = mouse_pos |  | ||||||
| 
 |  | ||||||
|     def stop_panning(self): |  | ||||||
|         self.is_panning = False |  | ||||||
|         self.last_mouse_pos = None |  | ||||||
| 
 |  | ||||||
|     def pan(self, mouse_pos): |  | ||||||
|         if self.is_panning and self.last_mouse_pos: |  | ||||||
|             dx = mouse_pos[0] - self.last_mouse_pos[0] |  | ||||||
|             dy = mouse_pos[1] - self.last_mouse_pos[1] |  | ||||||
|             self.x -= dx / self.zoom |  | ||||||
|             self.y -= dy / self.zoom |  | ||||||
|             self.target_x = self.x  # Sync target position with actual position |  | ||||||
|             self.target_y = self.y |  | ||||||
|             self.last_mouse_pos = mouse_pos |  | ||||||
| 
 |  | ||||||
|     def get_real_coordinates(self, screen_x, screen_y): |  | ||||||
|         # Convert screen coordinates to world coordinates |  | ||||||
|         world_x = (screen_x - SCREEN_WIDTH // 2 + self.x * self.zoom) / self.zoom |  | ||||||
|         world_y = (screen_y - SCREEN_HEIGHT // 2 + self.y * self.zoom) / self.zoom |  | ||||||
| 
 |  | ||||||
|         return world_x, world_y |  | ||||||
| 
 |  | ||||||
|     def is_in_view(self, obj_x, obj_y, margin=0): |  | ||||||
|         half_w = (SCREEN_WIDTH + (RENDER_BUFFER * self.zoom)) / (2 * self.zoom) |  | ||||||
|         half_h = (SCREEN_HEIGHT + (RENDER_BUFFER * self.zoom)) / (2 * self.zoom) |  | ||||||
|         cam_left = self.x - half_w |  | ||||||
|         cam_right = self.x + half_w |  | ||||||
|         cam_top = self.y - half_h |  | ||||||
|         cam_bottom = self.y + half_h |  | ||||||
|         return (cam_left - margin <= obj_x <= cam_right + margin and |  | ||||||
|                 cam_top - margin <= obj_y <= cam_bottom + margin) |  | ||||||
| 
 |  | ||||||
|     def world_to_screen(self, obj_x, obj_y): |  | ||||||
|         screen_x = (obj_x - self.x) * self.zoom + SCREEN_WIDTH // 2 |  | ||||||
|         screen_y = (obj_y - self.y) * self.zoom + SCREEN_HEIGHT // 2 |  | ||||||
|         return int(screen_x), int(screen_y) |  | ||||||
| 
 |  | ||||||
|     def get_relative_size(self, world_size): |  | ||||||
|         # Converts a world size (e.g., radius or width/height) to screen pixels |  | ||||||
|         return int(world_size * self.zoom) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def draw_grid(screen, camera, showing_grid=True): | def draw_grid(screen, camera, showing_grid=True): | ||||||
|     # Fill screen with black |     # Fill the screen with black | ||||||
|     screen.fill(BLACK) |     screen.fill(BLACK) | ||||||
| 
 | 
 | ||||||
|     # Calculate effective cell size with zoom |     # Calculate effective cell size with zoom | ||||||
| @ -200,13 +107,17 @@ def draw_grid(screen, camera, showing_grid=True): | |||||||
|         for start, end in horizontal_lines: |         for start, end in horizontal_lines: | ||||||
|             pygame.draw.line(screen, GRAY, start, end) |             pygame.draw.line(screen, GRAY, start, end) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def main(): | def main(): | ||||||
|     screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), vsync=1) |     screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), vsync=1) | ||||||
|     pygame.display.set_caption("Dynamic Abstraction System Testing") |     pygame.display.set_caption("Dynamic Abstraction System Testing") | ||||||
|     clock = pygame.time.Clock() |     clock = pygame.time.Clock() | ||||||
|     camera = Camera() |     camera = Camera(SCREEN_WIDTH, SCREEN_HEIGHT, RENDER_BUFFER) | ||||||
| 
 | 
 | ||||||
|     is_showing_grid = True  # Flag to control grid visibility |     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) |     font = pygame.font.Font("freesansbold.ttf", 16) | ||||||
| 
 | 
 | ||||||
| @ -215,6 +126,13 @@ def main(): | |||||||
|     last_tps_time = time.perf_counter()  # Tracks the last TPS calculation time |     last_tps_time = time.perf_counter()  # Tracks the last TPS calculation time | ||||||
|     tick_counter = 0  # Counts ticks executed |     tick_counter = 0  # Counts ticks executed | ||||||
|     actual_tps = 0  # Stores the calculated TPS |     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("Controls:") | ||||||
|     print("WASD - Move camera") |     print("WASD - Move camera") | ||||||
| @ -224,13 +142,16 @@ def main(): | |||||||
|     print("ESC or close window - Exit") |     print("ESC or close window - Exit") | ||||||
| 
 | 
 | ||||||
|     # Initialize world |     # Initialize world | ||||||
|     world = World() |     world = World(CELL_SIZE, (CELL_SIZE * GRID_WIDTH, CELL_SIZE * GRID_HEIGHT)) | ||||||
| 
 | 
 | ||||||
|     world.add_object(DebugRenderObject(Position(0,0))) |     world.add_object(DebugRenderObject(Position(x=0, y=0))) | ||||||
|  |     world.add_object(DebugRenderObject(Position(x=20, y=0))) | ||||||
| 
 | 
 | ||||||
|     # setting the seed to 67 >_< |     # sets seed to 67 >_< | ||||||
|     random.seed(67) |     random.seed(67) | ||||||
| 
 | 
 | ||||||
|  |     world.add_object(FoodObject(Position(x=random.randint(-100, 100), y=random.randint(-100, 100)))) | ||||||
|  | 
 | ||||||
|     running = True |     running = True | ||||||
|     while running: |     while running: | ||||||
|         deltatime = clock.get_time() / 1000.0  # Convert milliseconds to seconds |         deltatime = clock.get_time() / 1000.0  # Convert milliseconds to seconds | ||||||
| @ -241,7 +162,10 @@ def main(): | |||||||
|                 running = False |                 running = False | ||||||
|             elif event.type == pygame.KEYDOWN: |             elif event.type == pygame.KEYDOWN: | ||||||
|                 if event.key == pygame.K_ESCAPE: |                 if event.key == pygame.K_ESCAPE: | ||||||
|  |                     if len(selected_objects) == 0: | ||||||
|                         running = False |                         running = False | ||||||
|  |                     selecting = False | ||||||
|  |                     selected_objects = [] | ||||||
|                 if event.key == pygame.K_g: |                 if event.key == pygame.K_g: | ||||||
|                     is_showing_grid = not is_showing_grid |                     is_showing_grid = not is_showing_grid | ||||||
|                 if event.key == pygame.K_UP: |                 if event.key == pygame.K_UP: | ||||||
| @ -250,43 +174,98 @@ def main(): | |||||||
|                 if event.key == pygame.K_DOWN: |                 if event.key == pygame.K_DOWN: | ||||||
|                     if camera.speed > 350: |                     if camera.speed > 350: | ||||||
|                         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 | ||||||
|             elif event.type == pygame.MOUSEWHEEL: |             elif event.type == pygame.MOUSEWHEEL: | ||||||
|                 camera.handle_zoom(event.y) |                 camera.handle_zoom(event.y) | ||||||
|             elif event.type == pygame.MOUSEBUTTONDOWN: |             elif event.type == pygame.MOUSEBUTTONDOWN: | ||||||
|                 if event.button == 2:  # Middle mouse button |                 if event.button == 2:  # Middle mouse button | ||||||
|                     camera.start_panning(event.pos) |                     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: |             elif event.type == pygame.MOUSEBUTTONUP: | ||||||
|                 if event.button == 2:  # Middle mouse button |                 if event.button == 2: | ||||||
|                     camera.stop_panning() |                     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: |             elif event.type == pygame.MOUSEMOTION: | ||||||
|                 camera.pan(event.pos) |                 camera.pan(event.pos) | ||||||
|  |                 if selecting: | ||||||
|  |                     select_end = event.pos | ||||||
| 
 | 
 | ||||||
|         # Get pressed keys for smooth movement |         if not is_paused: | ||||||
|         keys = pygame.key.get_pressed() |  | ||||||
|         camera.update(keys, deltatime) |  | ||||||
| 
 |  | ||||||
|             # Tick logic (runs every tick interval) |             # Tick logic (runs every tick interval) | ||||||
|             current_time = time.perf_counter() |             current_time = time.perf_counter() | ||||||
|             while current_time - last_tick_time >= tick_interval: |             while current_time - last_tick_time >= tick_interval: | ||||||
|                 last_tick_time += tick_interval |                 last_tick_time += tick_interval | ||||||
|                 tick_counter += 1 |                 tick_counter += 1 | ||||||
|             # Add your tick-specific logic here |                 total_ticks += 1 | ||||||
|             print("Tick logic executed") |  | ||||||
|             world.tick_all() |  | ||||||
| 
 | 
 | ||||||
|                 # gets every object in the world and returns amount of FoodObjects |                 # gets every object in the world and returns amount of FoodObjects | ||||||
|                 objects = world.get_objects() |                 objects = world.get_objects() | ||||||
|                 food = len([obj for obj in objects if isinstance(obj, FoodObject)]) |                 food = len([obj for obj in objects if isinstance(obj, FoodObject)]) | ||||||
|             print(f"Food count: {food}") | 
 | ||||||
|             if food < 10: |                 # if food < 10 and FOOD_SPAWNING == True: | ||||||
|                 for i in range(10 - food): |                 #     world.add_object(FoodObject(Position(x=random.randint(-100, 100), y=random.randint(-100, 100)))) | ||||||
|                     world.add_object(FoodObject(Position(random.randint(-200, 200), random.randint(-200, 200)))) | 
 | ||||||
|  |                 # 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() | ||||||
|  |                 ] | ||||||
|  | 
 | ||||||
|  |                 world.tick_all() | ||||||
| 
 | 
 | ||||||
|             # Calculate TPS every second |             # Calculate TPS every second | ||||||
|             if current_time - last_tps_time >= 1.0: |             if current_time - last_tps_time >= 1.0: | ||||||
|                 actual_tps = tick_counter |                 actual_tps = tick_counter | ||||||
|                 tick_counter = 0 |                 tick_counter = 0 | ||||||
|                 last_tps_time += 1.0 |                 last_tps_time += 1.0 | ||||||
|  |         else: | ||||||
|  |             last_tick_time = time.perf_counter() | ||||||
|  |             last_tps_time = time.perf_counter() | ||||||
|  | 
 | ||||||
|  |         # Get pressed keys for smooth movement | ||||||
|  |         keys = pygame.key.get_pressed() | ||||||
|  |         camera.update(keys, deltatime) | ||||||
| 
 | 
 | ||||||
|         # Draw the reference grid |         # Draw the reference grid | ||||||
|         draw_grid(screen, camera, is_showing_grid) |         draw_grid(screen, camera, is_showing_grid) | ||||||
| @ -294,10 +273,53 @@ def main(): | |||||||
|         # Render everything in the world |         # Render everything in the world | ||||||
|         world.render_all(camera, screen) |         world.render_all(camera, screen) | ||||||
| 
 | 
 | ||||||
|  |         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): | ||||||
|  |                     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 | ||||||
|  |                         ) | ||||||
|  | 
 | ||||||
|  |         # 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 |         # Render mouse position as text in top left of screen | ||||||
|         mouse_x, mouse_y = camera.get_real_coordinates(*pygame.mouse.get_pos()) |         mouse_x, mouse_y = camera.get_real_coordinates(*pygame.mouse.get_pos()) | ||||||
|         mouse_text = font.render(f"Mouse: ({mouse_x}, {mouse_y})", True, WHITE) |         mouse_text = font.render(f"Mouse: ({mouse_x:.2f}, {mouse_y:.2f})", True, WHITE) | ||||||
|         text_rect = mouse_text.get_rect() |         text_rect = mouse_text.get_rect() | ||||||
|         text_rect.topleft = (10, 10) |         text_rect.topleft = (10, 10) | ||||||
|         screen.blit(mouse_text, text_rect) |         screen.blit(mouse_text, text_rect) | ||||||
| @ -314,11 +336,87 @@ def main(): | |||||||
|         tps_rect.bottomright = (SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10) |         tps_rect.bottomright = (SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10) | ||||||
|         screen.blit(tps_text, tps_rect) |         screen.blit(tps_text, tps_rect) | ||||||
| 
 | 
 | ||||||
|         # Render camera position and speed in bottom left |         # Render tick count in bottom left | ||||||
|         cam_text = font.render(f"Camera: ({camera.x:.2f}, {camera.y:.2f}), Speed: {camera.speed:.2f}", True, WHITE) |         tick_text = font.render(f"Ticks: {total_ticks}", True, WHITE) | ||||||
|         cam_rect = cam_text.get_rect() |         tick_rect = tick_text.get_rect() | ||||||
|         cam_rect.bottomleft = (10, SCREEN_HEIGHT - 10) |         tick_rect.bottomleft = (10, SCREEN_HEIGHT - 10) | ||||||
|         screen.blit(cam_text, cam_rect) |         screen.blit(tick_text, tick_rect) | ||||||
|  | 
 | ||||||
|  |         if len(selected_objects) >= 1: | ||||||
|  |             i = 0 | ||||||
|  |             for each in selected_objects: | ||||||
|  |                 obj = each | ||||||
|  |                 obj_text = font.render( | ||||||
|  |                     f"Object: {str(obj)}", True, WHITE | ||||||
|  |                 ) | ||||||
|  |                 obj_rect = obj_text.get_rect() | ||||||
|  |                 obj_rect.topleft = (10, 30 + i * 20) | ||||||
|  |                 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"), | ||||||
|  |             ("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) | ||||||
| 
 | 
 | ||||||
|         # Update display |         # Update display | ||||||
|         pygame.display.flip() |         pygame.display.flip() | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ description = "Add your description here" | |||||||
| requires-python = ">=3.11" | requires-python = ">=3.11" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     "pre-commit>=4.2.0", |     "pre-commit>=4.2.0", | ||||||
|  |     "pydantic>=2.11.5", | ||||||
|     "pygame>=2.6.1", |     "pygame>=2.6.1", | ||||||
|     "pytest>=8.3.5", |     "pytest>=8.3.5", | ||||||
| ] | ] | ||||||
|  | |||||||
							
								
								
									
										87
									
								
								tests/test_world.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								tests/test_world.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | |||||||
|  | import pytest | ||||||
|  | from world.world import World, Position, BaseEntity | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DummyEntity(BaseEntity): | ||||||
|  |     def __init__(self, position): | ||||||
|  |         super().__init__(position) | ||||||
|  |         self.ticked = False | ||||||
|  |         self.rendered = False | ||||||
|  | 
 | ||||||
|  |     def tick(self, interactable=None): | ||||||
|  |         self.ticked = True | ||||||
|  |         return self | ||||||
|  | 
 | ||||||
|  |     def render(self, camera, screen): | ||||||
|  |         self.rendered = True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @pytest.fixture | ||||||
|  | def world(): | ||||||
|  |     return World(partition_size=10, world_size=(100, 100)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_add_object_and_get_objects(world): | ||||||
|  |     entity = DummyEntity(Position(x=0, y=0)) | ||||||
|  |     world.add_object(entity) | ||||||
|  |     assert entity in world.get_objects() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_query_objects_within_radius(world): | ||||||
|  |     e1 = DummyEntity(Position(x=0, y=0)) | ||||||
|  |     e2 = DummyEntity(Position(x=5, y=0)) | ||||||
|  |     e3 = DummyEntity(Position(x=20, y=0)) | ||||||
|  |     world.add_object(e1) | ||||||
|  |     world.add_object(e2) | ||||||
|  |     world.add_object(e3) | ||||||
|  |     found = world.query_objects_within_radius(0, 0, 10) | ||||||
|  |     assert e1 in found | ||||||
|  |     assert e2 in found | ||||||
|  |     assert e3 not in found | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_query_objects_in_range(world): | ||||||
|  |     e1 = DummyEntity(Position(x=1, y=1)) | ||||||
|  |     e2 = DummyEntity(Position(x=5, y=5)) | ||||||
|  |     e3 = DummyEntity(Position(x=20, y=20)) | ||||||
|  |     world.add_object(e1) | ||||||
|  |     world.add_object(e2) | ||||||
|  |     world.add_object(e3) | ||||||
|  |     found = world.query_objects_in_range(0, 0, 10, 10) | ||||||
|  |     assert e1 in found | ||||||
|  |     assert e2 in found | ||||||
|  |     assert e3 not in found | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_query_closest_object(world): | ||||||
|  |     e1 = DummyEntity(Position(x=0, y=0)) | ||||||
|  |     e2 = DummyEntity(Position(x=10, y=0)) | ||||||
|  |     world.add_object(e1) | ||||||
|  |     world.add_object(e2) | ||||||
|  |     closest = world.query_closest_object(1, 0) | ||||||
|  |     assert closest == e1 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_tick_all_removes_dead(world): | ||||||
|  |     e1 = DummyEntity(Position(x=0, y=0)) | ||||||
|  |     e2 = DummyEntity(Position(x=10, y=0)) | ||||||
|  |     e2.flags["death"] = True | ||||||
|  |     world.add_object(e1) | ||||||
|  |     world.add_object(e2) | ||||||
|  |     world.tick_all() | ||||||
|  |     objs = world.get_objects() | ||||||
|  |     assert e1 in objs | ||||||
|  |     assert e2 not in objs | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_tick_all_calls_tick(world): | ||||||
|  |     e1 = DummyEntity(Position(x=0, y=0)) | ||||||
|  |     world.add_object(e1) | ||||||
|  |     world.tick_all() | ||||||
|  |     assert e1.ticked | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_add_object_out_of_bounds(world): | ||||||
|  |     entity = DummyEntity(Position(x=1000, y=1000)) | ||||||
|  |     with pytest.raises(ValueError): | ||||||
|  |         world.add_object(entity) | ||||||
							
								
								
									
										112
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										112
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							| @ -2,6 +2,15 @@ version = 1 | |||||||
| revision = 2 | revision = 2 | ||||||
| requires-python = ">=3.11" | requires-python = ">=3.11" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "annotated-types" | ||||||
|  | version = "0.7.0" | ||||||
|  | source = { registry = "https://pypi.org/simple" } | ||||||
|  | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } | ||||||
|  | wheels = [ | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "cfgv" | name = "cfgv" | ||||||
| version = "3.4.0" | version = "3.4.0" | ||||||
| @ -35,6 +44,7 @@ version = "0.1.0" | |||||||
| source = { virtual = "." } | source = { virtual = "." } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "pre-commit" }, |     { name = "pre-commit" }, | ||||||
|  |     { name = "pydantic" }, | ||||||
|     { name = "pygame" }, |     { name = "pygame" }, | ||||||
|     { name = "pytest" }, |     { name = "pytest" }, | ||||||
| ] | ] | ||||||
| @ -47,6 +57,7 @@ dev = [ | |||||||
| [package.metadata] | [package.metadata] | ||||||
| requires-dist = [ | requires-dist = [ | ||||||
|     { name = "pre-commit", specifier = ">=4.2.0" }, |     { name = "pre-commit", specifier = ">=4.2.0" }, | ||||||
|  |     { name = "pydantic", specifier = ">=2.11.5" }, | ||||||
|     { name = "pygame", specifier = ">=2.6.1" }, |     { name = "pygame", specifier = ">=2.6.1" }, | ||||||
|     { name = "pytest", specifier = ">=8.3.5" }, |     { name = "pytest", specifier = ">=8.3.5" }, | ||||||
| ] | ] | ||||||
| @ -133,6 +144,86 @@ wheels = [ | |||||||
|     { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, |     { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "pydantic" | ||||||
|  | version = "2.11.5" | ||||||
|  | source = { registry = "https://pypi.org/simple" } | ||||||
|  | dependencies = [ | ||||||
|  |     { name = "annotated-types" }, | ||||||
|  |     { name = "pydantic-core" }, | ||||||
|  |     { name = "typing-extensions" }, | ||||||
|  |     { name = "typing-inspection" }, | ||||||
|  | ] | ||||||
|  | sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" } | ||||||
|  | wheels = [ | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" }, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "pydantic-core" | ||||||
|  | version = "2.33.2" | ||||||
|  | source = { registry = "https://pypi.org/simple" } | ||||||
|  | dependencies = [ | ||||||
|  |     { name = "typing-extensions" }, | ||||||
|  | ] | ||||||
|  | sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } | ||||||
|  | wheels = [ | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "pygame" | name = "pygame" | ||||||
| version = "2.6.1" | version = "2.6.1" | ||||||
| @ -237,6 +328,27 @@ wheels = [ | |||||||
|     { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" }, |     { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "typing-extensions" | ||||||
|  | version = "4.14.0" | ||||||
|  | source = { registry = "https://pypi.org/simple" } | ||||||
|  | sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } | ||||||
|  | wheels = [ | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "typing-inspection" | ||||||
|  | version = "0.4.1" | ||||||
|  | source = { registry = "https://pypi.org/simple" } | ||||||
|  | dependencies = [ | ||||||
|  |     { name = "typing-extensions" }, | ||||||
|  | ] | ||||||
|  | sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } | ||||||
|  | wheels = [ | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "virtualenv" | name = "virtualenv" | ||||||
| version = "20.31.2" | version = "20.31.2" | ||||||
|  | |||||||
| @ -1,36 +1,173 @@ | |||||||
| from world.world import Position | import random, math | ||||||
|  | from multiprocessing.reduction import duplicate | ||||||
| 
 | 
 | ||||||
|  | from world.world import Position, BaseEntity | ||||||
| import pygame | import pygame | ||||||
|  | from typing import Optional, List, Any | ||||||
| 
 | 
 | ||||||
| # returns desired yellow value for food decay | 
 | ||||||
| def food_decay_yellow(decay): | class DebugRenderObject(BaseEntity): | ||||||
|  |     """ | ||||||
|  |     Debug object that renders as a circle and counts its neighbors. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, position: Position, radius: int = 5) -> None: | ||||||
|  |         """ | ||||||
|  |         Initializes the debug render object. | ||||||
|  | 
 | ||||||
|  |         :param position: The position of the object. | ||||||
|  |         :param radius: The radius of the rendered circle. | ||||||
|  |         """ | ||||||
|  |         super().__init__(position) | ||||||
|  |         self.neighbors: int = 0 | ||||||
|  |         self.radius: int = radius | ||||||
|  |         self.max_visual_width: int = radius * 2 | ||||||
|  |         self.interaction_radius: int = 50 | ||||||
|  |         self.flags: dict[str, bool] = { | ||||||
|  |             "death": False, | ||||||
|  |             "can_interact": True, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     def tick(self, interactable: Optional[List[BaseEntity]] = None) -> "DebugRenderObject": | ||||||
|  |         """ | ||||||
|  |         Updates the object, counting the number of interactable neighbors. | ||||||
|  | 
 | ||||||
|  |         :param interactable: List of nearby entities. | ||||||
|  |         :return: Self. | ||||||
|  |         """ | ||||||
|  |         if interactable is None: | ||||||
|  |             interactable = [] | ||||||
|  |         self.neighbors = len(interactable) | ||||||
|  |         return self | ||||||
|  | 
 | ||||||
|  |     def render(self, camera: Any, screen: Any) -> None: | ||||||
|  |         """ | ||||||
|  |         Renders the debug object as a circle, color intensity based on neighbors. | ||||||
|  | 
 | ||||||
|  |         :param camera: The camera object for coordinate transformation. | ||||||
|  |         :param screen: The Pygame screen surface. | ||||||
|  |         """ | ||||||
|  |         if camera.is_in_view(*self.position.get_position()): | ||||||
|  |             pygame.draw.circle( | ||||||
|  |                 screen, | ||||||
|  |                 (50, 50, min([255, (self.neighbors + 4) * 30])), | ||||||
|  |                 camera.world_to_screen(*self.position.get_position()), | ||||||
|  |                 int(self.radius * camera.zoom), | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |     def __repr__(self) -> str: | ||||||
|  |         """ | ||||||
|  |         Returns a string representation of the object. | ||||||
|  | 
 | ||||||
|  |         :return: String representation. | ||||||
|  |         """ | ||||||
|  |         return f"DebugRenderObject({self.position}, neighbors={self.neighbors})" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def food_decay_yellow(decay: int) -> int: | ||||||
|  |     """ | ||||||
|  |     Returns the yellow color component for food decay visualization. | ||||||
|  | 
 | ||||||
|  |     :param decay: The current decay value (0-255). | ||||||
|  |     :return: The yellow component (0-255). | ||||||
|  |     """ | ||||||
|     if decay < 128: |     if decay < 128: | ||||||
|         return decay |         return decay | ||||||
|     else: |     else: | ||||||
|         return 255 - decay |         return 255 - decay | ||||||
| 
 | 
 | ||||||
| class DebugRenderObject: | def chance_to_grow(decay_rate): | ||||||
|     def __init__(self, position: Position): |     return ((2**(-20*(decay_rate-1)))*12.5)+0.1 | ||||||
|         self.position = position |  | ||||||
| 
 | 
 | ||||||
|     def tick(self): | def chance(percent): | ||||||
|         pass |     return random.random() < percent / 100 | ||||||
| 
 | 
 | ||||||
|     def render(self, camera, screen): | 
 | ||||||
|  | class FoodObject(BaseEntity): | ||||||
|  |     """ | ||||||
|  |     Food object that decays over time and is rendered as a colored circle. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, position: Position) -> None: | ||||||
|  |         """ | ||||||
|  |         Initializes the food object. | ||||||
|  | 
 | ||||||
|  |         :param position: The position of the food. | ||||||
|  |         """ | ||||||
|  |         super().__init__(position) | ||||||
|  |         self.max_visual_width: int = 10 | ||||||
|  |         self.decay: int = 0 | ||||||
|  |         self.decay_rate: int = 1 | ||||||
|  |         self.max_decay = 200 | ||||||
|  |         self.interaction_radius: int = 50 | ||||||
|  |         self.neighbors: int = 0 | ||||||
|  |         self.flags: dict[str, bool] = { | ||||||
|  |             "death": False, | ||||||
|  |             "can_interact": True, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     def tick(self, interactable: Optional[List[BaseEntity]] = None) -> Optional["FoodObject"]: | ||||||
|  |         """ | ||||||
|  |         Updates the food object, increasing decay and flagging for death if decayed. | ||||||
|  | 
 | ||||||
|  |         :param interactable: List of nearby entities (unused). | ||||||
|  |         :return: Self | ||||||
|  |         """ | ||||||
|  |         if interactable is None: | ||||||
|  |             interactable = [] | ||||||
|  | 
 | ||||||
|  |         self.neighbors = len(interactable) | ||||||
|  | 
 | ||||||
|  |         if self.neighbors > 0: | ||||||
|  |             self.decay += self.decay_rate * (1 + (self.neighbors / 10)) | ||||||
|  |         else: | ||||||
|  |             self.decay += self.decay_rate | ||||||
|  | 
 | ||||||
|  |         if self.decay > self.max_decay: | ||||||
|  |             self.decay = self.max_decay | ||||||
|  |             self.flag_for_death() | ||||||
|  | 
 | ||||||
|  |         grow_chance = chance_to_grow(self.decay_rate * (1 + (self.neighbors / 10))) | ||||||
|  | 
 | ||||||
|  |         # print(grow_chance) | ||||||
|  | 
 | ||||||
|  |         if chance(grow_chance): | ||||||
|  |             # print("Growing") | ||||||
|  |             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) | ||||||
|  | 
 | ||||||
|  |             return [self, FoodObject(Position(x=duplicate_x, y=duplicate_y))] | ||||||
|  | 
 | ||||||
|  |         return self | ||||||
|  | 
 | ||||||
|  |     def normalize_decay_to_color(self) -> int: | ||||||
|  |         """ | ||||||
|  |         Normalizes the decay value to a color component. | ||||||
|  | 
 | ||||||
|  |         :return: Normalized decay value (0-255). | ||||||
|  |         """ | ||||||
|  |         return self.decay / self.max_decay * 255 if self.max_decay > 0 else 0 | ||||||
|  | 
 | ||||||
|  |     def render(self, camera: Any, screen: Any) -> None: | ||||||
|  |         """ | ||||||
|  |         Renders the food object as a decaying colored circle. | ||||||
|  | 
 | ||||||
|  |         :param camera: The camera object for coordinate transformation. | ||||||
|  |         :param screen: The Pygame screen surface. | ||||||
|  |         """ | ||||||
|         if camera.is_in_view(*self.position.get_position()): |         if camera.is_in_view(*self.position.get_position()): | ||||||
|             pygame.draw.circle(screen, (255,255,255), camera.world_to_screen(*self.position.get_position()), 15 * camera.zoom) |             pygame.draw.circle( | ||||||
|  |                 screen, | ||||||
|  |                 (255 - self.normalize_decay_to_color(), food_decay_yellow(self.normalize_decay_to_color()), 0), | ||||||
|  |                 camera.world_to_screen(*self.position.get_position()), | ||||||
|  |                 int(5 * camera.zoom) | ||||||
|  |             ) | ||||||
| 
 | 
 | ||||||
| class FoodObject: |     def __repr__(self) -> str: | ||||||
|     def __init__(self, position: Position): |         """ | ||||||
|         self.decay = 0 |         Returns a string representation of the food object. | ||||||
|         self.position = position |  | ||||||
| 
 | 
 | ||||||
|     def tick(self): |         :return: String representation. | ||||||
|         self.decay += 1 |         """ | ||||||
| 
 |         return f"FoodObject({self.position}, decay={self.decay:.0f}, decay_rate={self.decay_rate * (1 + (self.neighbors / 10))})" | ||||||
|         if self.decay > 255: |  | ||||||
|             self.decay = 0 # eventually will raise a destroy flag |  | ||||||
| 
 |  | ||||||
|     def render(self, camera, screen): |  | ||||||
|         if camera.is_in_view(*self.position.get_position()): |  | ||||||
|             pygame.draw.circle(screen, (255-self.decay,food_decay_yellow(self.decay),0), camera.world_to_screen(*self.position.get_position()), 5 * camera.zoom) |  | ||||||
|  | |||||||
							
								
								
									
										183
									
								
								world/simulation_interface.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								world/simulation_interface.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,183 @@ | |||||||
|  | import pygame | ||||||
|  | from typing import Optional, Tuple, Sequence | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Camera: | ||||||
|  |     """ | ||||||
|  |     Camera class for handling world-to-screen transformations, panning, and zooming in a 2D simulation. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def __init__( | ||||||
|  |             self, | ||||||
|  |             screen_width: int, | ||||||
|  |             screen_height: int, | ||||||
|  |             render_buffer: int = 50, | ||||||
|  |     ) -> None: | ||||||
|  |         """ | ||||||
|  |         Initializes the Camera. | ||||||
|  | 
 | ||||||
|  |         :param screen_width: Width of the screen in pixels. | ||||||
|  |         :param screen_height: Height of the screen in pixels. | ||||||
|  |         :param render_buffer: Buffer for rendering objects outside the screen. | ||||||
|  |         """ | ||||||
|  |         self.x: float = 0 | ||||||
|  |         self.y: float = 0 | ||||||
|  |         self.target_x: float = 0 | ||||||
|  |         self.target_y: float = 0 | ||||||
|  |         self.zoom: float = 1.0 | ||||||
|  |         self.target_zoom: float = 1.0 | ||||||
|  |         self.smoothing: float = 0.15  # Higher = more responsive, lower = more smooth | ||||||
|  |         self.speed: float = 700 | ||||||
|  |         self.zoom_smoothing: float = 0.2  # Higher = more responsive, lower = more smooth | ||||||
|  |         self.is_panning: bool = False | ||||||
|  |         self.last_mouse_pos: Optional[Sequence[int]] = None | ||||||
|  |         self.screen_width: int = screen_width | ||||||
|  |         self.screen_height: int = screen_height | ||||||
|  |         self.render_buffer: int = render_buffer | ||||||
|  |         self.min_zoom: float = 50.0  # Maximum zoom level | ||||||
|  |         self.max_zoom: float = 0.01  # Minimum zoom level | ||||||
|  | 
 | ||||||
|  |     def update(self, keys: Sequence[bool], deltatime: float) -> None: | ||||||
|  |         """ | ||||||
|  |         Updates the camera position and zoom based on input and time. | ||||||
|  | 
 | ||||||
|  |         :param keys: Sequence of boolean values representing pressed keys. | ||||||
|  |         :param deltatime: Time elapsed since last update (in seconds). | ||||||
|  |         """ | ||||||
|  |         dx = 0 | ||||||
|  |         dy = 0 | ||||||
|  |         if keys[pygame.K_w]: | ||||||
|  |             dy -= 1 | ||||||
|  |         if keys[pygame.K_s]: | ||||||
|  |             dy += 1 | ||||||
|  |         if keys[pygame.K_a]: | ||||||
|  |             dx -= 1 | ||||||
|  |         if keys[pygame.K_d]: | ||||||
|  |             dx += 1 | ||||||
|  | 
 | ||||||
|  |         length = (dx ** 2 + dy ** 2) ** 0.5 | ||||||
|  |         if length > 0: | ||||||
|  |             dx /= length | ||||||
|  |             dy /= length | ||||||
|  | 
 | ||||||
|  |         self.target_x += dx * self.speed * deltatime / self.zoom | ||||||
|  |         self.target_y += dy * self.speed * deltatime / self.zoom | ||||||
|  | 
 | ||||||
|  |         if keys[pygame.K_r]: | ||||||
|  |             self.target_x = 0 | ||||||
|  |             self.target_y = 0 | ||||||
|  | 
 | ||||||
|  |         smoothing_factor = 1 - pow(1 - self.smoothing, deltatime * 60) | ||||||
|  |         self.x += (self.target_x - self.x) * smoothing_factor | ||||||
|  |         self.y += (self.target_y - self.y) * smoothing_factor | ||||||
|  | 
 | ||||||
|  |         threshold = 0.5 | ||||||
|  |         if abs(self.x - self.target_x) < threshold: | ||||||
|  |             self.x = self.target_x | ||||||
|  |         if abs(self.y - self.target_y) < threshold: | ||||||
|  |             self.y = self.target_y | ||||||
|  | 
 | ||||||
|  |         zoom_smoothing_factor = 1 - pow(1 - self.zoom_smoothing, deltatime * 60) | ||||||
|  |         self.zoom += (self.target_zoom - self.zoom) * zoom_smoothing_factor | ||||||
|  | 
 | ||||||
|  |         zoom_threshold = 0.001 | ||||||
|  |         if abs(self.zoom - self.target_zoom) < zoom_threshold: | ||||||
|  |             self.zoom = self.target_zoom | ||||||
|  | 
 | ||||||
|  |     def handle_zoom(self, zoom_delta: int) -> None: | ||||||
|  |         """ | ||||||
|  |         Adjusts the camera zoom level based on mouse wheel input. | ||||||
|  | 
 | ||||||
|  |         :param zoom_delta: The amount of zoom change (positive for zoom in, negative for zoom out). | ||||||
|  |         """ | ||||||
|  |         zoom_factor = 1.1 | ||||||
|  |         if zoom_delta > 0: | ||||||
|  |             self.target_zoom *= zoom_factor | ||||||
|  |         elif zoom_delta < 0: | ||||||
|  |             self.target_zoom /= zoom_factor | ||||||
|  | 
 | ||||||
|  |         self.target_zoom = max(self.max_zoom, min(self.min_zoom, self.target_zoom)) | ||||||
|  | 
 | ||||||
|  |     def start_panning(self, mouse_pos: Sequence[int]) -> None: | ||||||
|  |         """ | ||||||
|  |         Begins panning the camera. | ||||||
|  | 
 | ||||||
|  |         :param mouse_pos: The current mouse position as a sequence (x, y). | ||||||
|  |         """ | ||||||
|  |         self.is_panning = True | ||||||
|  |         self.last_mouse_pos = mouse_pos | ||||||
|  | 
 | ||||||
|  |     def stop_panning(self) -> None: | ||||||
|  |         """ | ||||||
|  |         Stops panning the camera. | ||||||
|  |         """ | ||||||
|  |         self.is_panning = False | ||||||
|  |         self.last_mouse_pos = None | ||||||
|  | 
 | ||||||
|  |     def pan(self, mouse_pos: Sequence[int]) -> None: | ||||||
|  |         """ | ||||||
|  |         Pans the camera based on mouse movement. | ||||||
|  | 
 | ||||||
|  |         :param mouse_pos: The current mouse position as a sequence (x, y). | ||||||
|  |         """ | ||||||
|  |         if self.is_panning and self.last_mouse_pos: | ||||||
|  |             dx = mouse_pos[0] - self.last_mouse_pos[0] | ||||||
|  |             dy = mouse_pos[1] - self.last_mouse_pos[1] | ||||||
|  |             self.x -= dx / self.zoom | ||||||
|  |             self.y -= dy / self.zoom | ||||||
|  |             self.target_x = self.x | ||||||
|  |             self.target_y = self.y | ||||||
|  |             self.last_mouse_pos = mouse_pos | ||||||
|  | 
 | ||||||
|  |     def get_real_coordinates(self, screen_x: int, screen_y: int) -> Tuple[float, float]: | ||||||
|  |         """ | ||||||
|  |         Converts screen coordinates to world coordinates. | ||||||
|  | 
 | ||||||
|  |         :param screen_x: X coordinate on the screen. | ||||||
|  |         :param screen_y: Y coordinate on the screen. | ||||||
|  |         :return: Tuple of (world_x, world_y). | ||||||
|  |         """ | ||||||
|  |         world_x = (screen_x - self.screen_width // 2 + self.x * self.zoom) / self.zoom | ||||||
|  |         world_y = (screen_y - self.screen_height // 2 + self.y * self.zoom) / self.zoom | ||||||
|  |         return world_x, world_y | ||||||
|  | 
 | ||||||
|  |     def is_in_view(self, obj_x: float, obj_y: float, margin: float = 0) -> bool: | ||||||
|  |         """ | ||||||
|  |         Checks if a world coordinate is within the camera's view. | ||||||
|  | 
 | ||||||
|  |         :param obj_x: X coordinate in world space. | ||||||
|  |         :param obj_y: Y coordinate in world space. | ||||||
|  |         :param margin: Additional margin to expand the view area. | ||||||
|  |         :return: True if the object is in view, False otherwise. | ||||||
|  |         """ | ||||||
|  |         half_w = (self.screen_width + (self.render_buffer * self.zoom)) / (2 * self.zoom) | ||||||
|  |         half_h = (self.screen_height + (self.render_buffer * self.zoom)) / (2 * self.zoom) | ||||||
|  |         cam_left = self.x - half_w | ||||||
|  |         cam_right = self.x + half_w | ||||||
|  |         cam_top = self.y - half_h | ||||||
|  |         cam_bottom = self.y + half_h | ||||||
|  |         return ( | ||||||
|  |                 cam_left - margin <= obj_x <= cam_right + margin | ||||||
|  |                 and cam_top - margin <= obj_y <= cam_bottom + margin | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def world_to_screen(self, obj_x: float, obj_y: float) -> Tuple[int, int]: | ||||||
|  |         """ | ||||||
|  |         Converts world coordinates to screen coordinates. | ||||||
|  | 
 | ||||||
|  |         :param obj_x: X coordinate in world space. | ||||||
|  |         :param obj_y: Y coordinate in world space. | ||||||
|  |         :return: Tuple of (screen_x, screen_y) in pixels. | ||||||
|  |         """ | ||||||
|  |         screen_x = (obj_x - self.x) * self.zoom + self.screen_width // 2 | ||||||
|  |         screen_y = (obj_y - self.y) * self.zoom + self.screen_height // 2 | ||||||
|  |         return int(screen_x), int(screen_y) | ||||||
|  | 
 | ||||||
|  |     def get_relative_size(self, world_size: float) -> int: | ||||||
|  |         """ | ||||||
|  |         Converts a world size (e.g., radius or width/height) to screen pixels. | ||||||
|  | 
 | ||||||
|  |         :param world_size: Size in world units. | ||||||
|  |         :return: Size in screen pixels. | ||||||
|  |         """ | ||||||
|  |         return int(world_size * self.zoom) | ||||||
							
								
								
									
										266
									
								
								world/world.py
									
									
									
									
									
								
							
							
						
						
									
										266
									
								
								world/world.py
									
									
									
									
									
								
							| @ -1,37 +1,255 @@ | |||||||
| class World: | from collections import defaultdict | ||||||
|     def __init__(self): | from abc import ABC, abstractmethod | ||||||
|         self.objects = [] | from typing import List, Dict, Tuple, Optional, Any, TypeVar, Union | ||||||
|         pass | from pydantic import BaseModel, Field | ||||||
| 
 | 
 | ||||||
|     def render_all(self, camera, screen): | T = TypeVar("T", bound="BaseEntity") | ||||||
|         for obj in self.objects: |  | ||||||
|             obj.render(camera, screen) |  | ||||||
| 
 | 
 | ||||||
|     def tick_all(self): |  | ||||||
|         for obj in self.objects: |  | ||||||
|             obj.tick() |  | ||||||
| 
 | 
 | ||||||
|     def add_object(self, new_object): | class Position(BaseModel): | ||||||
|         self.objects.append(new_object) |     """ | ||||||
|  |     Represents a 2D position in the world. | ||||||
|  |     """ | ||||||
|  |     x: int = Field(..., description="X coordinate") | ||||||
|  |     y: int = Field(..., description="Y coordinate") | ||||||
| 
 | 
 | ||||||
|     def get_objects(self): |     def __str__(self) -> str: | ||||||
|         return self.objects |  | ||||||
| 
 |  | ||||||
| class Position: |  | ||||||
|     def __init__(self, x, y): |  | ||||||
|         self.x = x |  | ||||||
|         self.y = y |  | ||||||
| 
 |  | ||||||
|     def __str__(self): |  | ||||||
|         return f"({self.x}, {self.y})" |         return f"({self.x}, {self.y})" | ||||||
| 
 | 
 | ||||||
|     def __repr__(self): |     def __repr__(self) -> str: | ||||||
|         return f"Position({self.x}, {self.y})" |         return f"Position({self.x}, {self.y})" | ||||||
| 
 | 
 | ||||||
|     def set_position(self, x, y): |     def set_position(self, x: int, y: int) -> None: | ||||||
|  |         """ | ||||||
|  |         Sets the position to the given coordinates. | ||||||
|  | 
 | ||||||
|  |         :param x: New X coordinate. | ||||||
|  |         :param y: New Y coordinate. | ||||||
|  |         """ | ||||||
|         self.x = x |         self.x = x | ||||||
|         self.y = y |         self.y = y | ||||||
| 
 | 
 | ||||||
|     def get_position(self): |     def get_position(self) -> Tuple[int, int]: | ||||||
|  |         """ | ||||||
|  |         Returns the current position as a tuple. | ||||||
|  | 
 | ||||||
|  |         :return: Tuple of (x, y). | ||||||
|  |         """ | ||||||
|         return self.x, self.y |         return self.x, self.y | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | class BaseEntity(ABC): | ||||||
|  |     """ | ||||||
|  |     Abstract base class for all entities in the world. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, position: Position) -> None: | ||||||
|  |         """ | ||||||
|  |         Initializes the entity with a position. | ||||||
|  | 
 | ||||||
|  |         :param position: The position of the entity. | ||||||
|  |         """ | ||||||
|  |         self.position: Position = position | ||||||
|  |         self.interaction_radius: int = 0 | ||||||
|  |         self.flags: Dict[str, bool] = { | ||||||
|  |             "death": False, | ||||||
|  |             "can_interact": False, | ||||||
|  |         } | ||||||
|  |         self.world_callbacks: Dict[str, Any] = {} | ||||||
|  |         self.max_visual_width: int = 0 | ||||||
|  | 
 | ||||||
|  |     @abstractmethod | ||||||
|  |     def tick(self, interactable: Optional[List["BaseEntity"]] = None) -> Optional["BaseEntity"]: | ||||||
|  |         """ | ||||||
|  |         Updates the entity for a single tick. | ||||||
|  | 
 | ||||||
|  |         :param interactable: List of entities this entity can interact with. | ||||||
|  |         :return: The updated entity or None if it should be removed. | ||||||
|  |         """ | ||||||
|  |         return self | ||||||
|  | 
 | ||||||
|  |     @abstractmethod | ||||||
|  |     def render(self, camera: Any, screen: Any) -> None: | ||||||
|  |         """ | ||||||
|  |         Renders the entity on the screen. | ||||||
|  | 
 | ||||||
|  |         :param camera: The camera object for coordinate transformation. | ||||||
|  |         :param screen: The Pygame screen surface. | ||||||
|  |         """ | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     def flag_for_death(self) -> None: | ||||||
|  |         """ | ||||||
|  |         Flags the entity for removal from the world. | ||||||
|  |         """ | ||||||
|  |         self.flags["death"] = True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class World: | ||||||
|  |     """ | ||||||
|  |     A world-class that contains and manages all objects in the game using spatial partitioning. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, partition_size: int = 10, world_size: tuple[int, int] = (400, 300)) -> None: | ||||||
|  |         """ | ||||||
|  |         Initializes the world with a partition size. | ||||||
|  | 
 | ||||||
|  |         :param partition_size: The size of each partition cell in the world. | ||||||
|  |         """ | ||||||
|  |         self.partition_size: int = partition_size | ||||||
|  |         self.buffers: List[Dict[Tuple[int, int], List[BaseEntity]]] = [defaultdict(list), defaultdict(list)] | ||||||
|  |         self.world_size: Tuple[int, int] = world_size | ||||||
|  |         self.current_buffer: int = 0 | ||||||
|  | 
 | ||||||
|  |     def _hash_position(self, position: Position) -> Tuple[int, int]: | ||||||
|  |         """ | ||||||
|  |         Hashes a position into a cell based on the partition size. | ||||||
|  | 
 | ||||||
|  |         :param position: A Position object representing the position in the world. | ||||||
|  |         :return: Tuple (cell_x, cell_y) representing the cell coordinates. | ||||||
|  |         """ | ||||||
|  |         # Ensure position is within world bounds, considering a center origin | ||||||
|  |         if position.x < -self.world_size[0] / 2 or position.x >= self.world_size[0] / 2 or position.y < - \ | ||||||
|  |                 self.world_size[1] / 2 or position.y >= self.world_size[1] / 2: | ||||||
|  |             # force position to be within bounds | ||||||
|  |             position.x = max(-self.world_size[0] / 2, min(position.x, self.world_size[0] / 2 - 1)) | ||||||
|  |             position.y = max(-self.world_size[1] / 2, min(position.y, self.world_size[1] / 2 - 1)) | ||||||
|  | 
 | ||||||
|  |         return int(position.x // self.partition_size), int(position.y // self.partition_size) | ||||||
|  | 
 | ||||||
|  |     def render_all(self, camera: Any, screen: Any) -> None: | ||||||
|  |         """ | ||||||
|  |         Renders all objects in the current buffer. | ||||||
|  | 
 | ||||||
|  |         :param camera: The camera object for coordinate transformation. | ||||||
|  |         :param screen: The Pygame screen surface. | ||||||
|  |         """ | ||||||
|  |         for obj_list in self.buffers[self.current_buffer].values(): | ||||||
|  |             for obj in obj_list: | ||||||
|  |                 obj.render(camera, screen) | ||||||
|  | 
 | ||||||
|  |     def tick_all(self) -> None: | ||||||
|  |         """ | ||||||
|  |         Advances all objects in the world by one tick, updating their state and handling interactions. | ||||||
|  |         """ | ||||||
|  |         next_buffer: int = 1 - self.current_buffer | ||||||
|  |         self.buffers[next_buffer].clear() | ||||||
|  | 
 | ||||||
|  |         for obj_list in self.buffers[self.current_buffer].values(): | ||||||
|  |             for obj in obj_list: | ||||||
|  |                 if obj.flags["death"]: | ||||||
|  |                     continue | ||||||
|  |                 if obj.flags["can_interact"]: | ||||||
|  |                     interactable = self.query_objects_within_radius( | ||||||
|  |                         obj.position.x, obj.position.y, obj.interaction_radius | ||||||
|  |                     ) | ||||||
|  |                     interactable.remove(obj) | ||||||
|  |                     new_obj = obj.tick(interactable) | ||||||
|  |                 else: | ||||||
|  |                     new_obj = obj.tick() | ||||||
|  |                 if new_obj is None: | ||||||
|  |                     continue | ||||||
|  | 
 | ||||||
|  |                 # reproduction code | ||||||
|  |                 if isinstance(new_obj, list): | ||||||
|  |                     for item in new_obj: | ||||||
|  |                         if isinstance(item, BaseEntity): | ||||||
|  |                             cell = self._hash_position(item.position) | ||||||
|  |                             self.buffers[next_buffer][cell].append(item) | ||||||
|  |                 else: | ||||||
|  |                     cell = self._hash_position(new_obj.position) | ||||||
|  |                     self.buffers[next_buffer][cell].append(new_obj) | ||||||
|  |         self.current_buffer = next_buffer | ||||||
|  | 
 | ||||||
|  |     def add_object(self, new_object: BaseEntity) -> None: | ||||||
|  |         """ | ||||||
|  |         Adds a new object to the world in the appropriate cell. | ||||||
|  | 
 | ||||||
|  |         :param new_object: The object to add. | ||||||
|  |         """ | ||||||
|  |         cell = self._hash_position(new_object.position) | ||||||
|  |         self.buffers[self.current_buffer][cell].append(new_object) | ||||||
|  | 
 | ||||||
|  |     def query_objects_within_radius(self, x: float, y: float, radius: float) -> List[BaseEntity]: | ||||||
|  |         """ | ||||||
|  |         Returns all objects within a given radius of a point. | ||||||
|  | 
 | ||||||
|  |         :param x: X coordinate of the center. | ||||||
|  |         :param y: Y coordinate of the center. | ||||||
|  |         :param radius: Search radius. | ||||||
|  |         :return: List of objects within the radius. | ||||||
|  |         """ | ||||||
|  |         result: List[BaseEntity] = [] | ||||||
|  |         cell_x, cell_y = int(x // self.partition_size), int(y // self.partition_size) | ||||||
|  |         cells_to_check: List[Tuple[int, int]] = [] | ||||||
|  |         r = int((radius // self.partition_size) + 1) | ||||||
|  |         for dx in range(-r, r + 1): | ||||||
|  |             for dy in range(-r, r + 1): | ||||||
|  |                 cells_to_check.append((cell_x + dx, cell_y + dy)) | ||||||
|  |         for cell in cells_to_check: | ||||||
|  |             for obj in self.buffers[self.current_buffer].get(cell, []): | ||||||
|  |                 obj_x, obj_y = obj.position.get_position() | ||||||
|  |                 dx = obj_x - x | ||||||
|  |                 dy = obj_y - y | ||||||
|  |                 if dx * dx + dy * dy <= radius * radius: | ||||||
|  |                     result.append(obj) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def query_objects_in_range(self, x1: float, y1: float, x2: float, y2: float) -> List[BaseEntity]: | ||||||
|  |         """ | ||||||
|  |         Returns all objects within a rectangular range. | ||||||
|  | 
 | ||||||
|  |         :param x1: Minimum X coordinate. | ||||||
|  |         :param y1: Minimum Y coordinate. | ||||||
|  |         :param x2: Maximum X coordinate. | ||||||
|  |         :param y2: Maximum Y coordinate. | ||||||
|  |         :return: List of objects within the rectangle. | ||||||
|  |         """ | ||||||
|  |         result: List[BaseEntity] = [] | ||||||
|  |         cell_x1, cell_y1 = ( | ||||||
|  |             int(x1 // self.partition_size), | ||||||
|  |             int(y1 // self.partition_size), | ||||||
|  |         ) | ||||||
|  |         cell_x2, cell_y2 = ( | ||||||
|  |             int(x2 // self.partition_size), | ||||||
|  |             int(y2 // self.partition_size), | ||||||
|  |         ) | ||||||
|  |         for cell_x in range(cell_x1, cell_x2 + 1): | ||||||
|  |             for cell_y in range(cell_y1, cell_y2 + 1): | ||||||
|  |                 for obj in self.buffers[self.current_buffer].get((cell_x, cell_y), []): | ||||||
|  |                     obj_x, obj_y = obj.position.get_position() | ||||||
|  |                     if x1 <= obj_x <= x2 and y1 <= obj_y <= y2: | ||||||
|  |                         result.append(obj) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def query_closest_object(self, x: float, y: float) -> Optional[BaseEntity]: | ||||||
|  |         """ | ||||||
|  |         Returns the closest object to a given point. | ||||||
|  | 
 | ||||||
|  |         :param x: X coordinate of the point. | ||||||
|  |         :param y: Y coordinate of the point. | ||||||
|  |         :return: The closest object or None if no objects exist. | ||||||
|  |         """ | ||||||
|  |         closest_obj: Optional[BaseEntity] = None | ||||||
|  |         closest_distance: float = float("inf") | ||||||
|  |         for obj_list in self.buffers[self.current_buffer].values(): | ||||||
|  |             for obj in obj_list: | ||||||
|  |                 obj_x, obj_y = obj.position.get_position() | ||||||
|  |                 dx = obj_x - x | ||||||
|  |                 dy = obj_y - y | ||||||
|  |                 distance = dx * dx + dy * dy | ||||||
|  |                 if distance < closest_distance: | ||||||
|  |                     closest_distance = distance | ||||||
|  |                     closest_obj = obj | ||||||
|  |         return closest_obj | ||||||
|  | 
 | ||||||
|  |     def get_objects(self) -> List[BaseEntity]: | ||||||
|  |         """ | ||||||
|  |         Returns a list of all objects currently in the world. | ||||||
|  | 
 | ||||||
|  |         :return: List of all objects. | ||||||
|  |         """ | ||||||
|  |         all_objects: List[BaseEntity] = [] | ||||||
|  |         for obj_list in self.buffers[self.current_buffer].values(): | ||||||
|  |             all_objects.extend(obj_list) | ||||||
|  |         return all_objects | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user