From f1d1402fb93418c40bd4ff452b81ba56a17546e3 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sun, 30 Mar 2025 21:00:23 +0200 Subject: [PATCH 01/38] Move the inventory_gui.py file to the new src/sprites/gui directory. --- src/{ => sprites/gui}/inventory_gui.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) rename src/{ => sprites/gui}/inventory_gui.py (93%) diff --git a/src/inventory_gui.py b/src/sprites/gui/inventory_gui.py similarity index 93% rename from src/inventory_gui.py rename to src/sprites/gui/inventory_gui.py index 4231309..3421512 100644 --- a/src/inventory_gui.py +++ b/src/sprites/gui/inventory_gui.py @@ -1,8 +1,7 @@ -from typing import Dict, Tuple - import pygame # type: ignore from src.inventory import Inventory +from src.settings import TILE_SIZE class InventoryGUI: @@ -19,9 +18,10 @@ def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: self.max_visible_items = 10 self.item_height = 60 - # Load sprite sheet and extract the icons (Testing purposes) + # Load a sprite sheet and extract the icons (Testing purposes) # To be replaced when: - # 1) Spritesheet has been decide. 2) A 'Buy', 'Found' or 'Add' in-game feature has been implemented + # 1) Sprite sheet has been decided. + # 2) A 'Buy', 'Found' or 'Add' in-game feature has been implemented self.sprite_sheet = pygame.image.load("images/tilesets/Treasure+.png").convert_alpha() self.icons = { "Gold Coin": self.extract_icon(0, 0), @@ -67,12 +67,12 @@ def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: "Glowing Crystal": self.extract_icon(16, 176), } - # Button dimmentions + # Button dimensions self.button_width = 100 self.button_height = 50 # Initialize button actions - self.button_actions: Dict[str, Tuple[pygame.Rect, pygame.Rect]] = {} + self.button_actions: dict[str, tuple[pygame.Rect, pygame.Rect]] = {} # Action messages self.message = "" @@ -86,12 +86,13 @@ def handle_events(self, event): max_offset = max(0, len(self.inventory.get_items()) - self.max_visible_items) self.scroll_offset = min(self.scroll_offset, max_offset) - def extract_icon(self, x, y, size=16): + def extract_icon(self, x, y, size=TILE_SIZE): """Extract a single icon from the sprite sheet.""" return self.sprite_sheet.subsurface((x, y, size, size)) - def draw_buttons(self, x: int, y: int, item: str) -> Tuple[pygame.Rect, pygame.Rect]: + def draw_buttons(self, x: int, y: int, item: str) -> tuple[pygame.Rect, pygame.Rect]: """Draw Use and Discard buttons for a specific item.""" + use_button = pygame.Rect(x, y, self.button_width, self.button_height) discard_button = pygame.Rect(x + self.button_width + 10, y, self.button_width, self.button_height) @@ -108,6 +109,7 @@ def draw_buttons(self, x: int, y: int, item: str) -> Tuple[pygame.Rect, pygame.R def draw(self): """Draw the inventory overlay.""" + self.screen.fill((0, 0, 0)) # Solid Black background # Reset button actions @@ -138,11 +140,11 @@ def draw(self): self.button_actions[item] = (use_button, discard_button) y_offset += 60 # Move down for the next item - # Draw hint + # Draw a hint hint_text = self.font.render("Press 'I' to close inventory", True, (200, 200, 200)) # Light gray text self.screen.blit(hint_text, (50, self.screen.get_height() - 60)) - # Display action message above the hint + # Display an action message above the hint if self.message and pygame.time.get_ticks() < self.message_end_time: # Render the message text message_text = self.font.render(self.message, True, (255, 255, 0)) # Yellow @@ -171,6 +173,7 @@ def draw(self): def handle_mouse_click(self, mouse_pos) -> None: """Handle mouse clicks on buttons.""" + for item, (use_button, discard_button) in self.button_actions.items(): if use_button.collidepoint(mouse_pos): self.message = self.inventory.use_item(item) # `self.message` stores strings From bd1a64a1359ebf0b4f9125b60999db69cfbe3182 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sun, 30 Mar 2025 21:23:31 +0200 Subject: [PATCH 02/38] Add typehints to inventory_gui.py --- src/sprites/gui/inventory_gui.py | 56 +++++++++++++++++--------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/sprites/gui/inventory_gui.py b/src/sprites/gui/inventory_gui.py index 3421512..dc611d4 100644 --- a/src/sprites/gui/inventory_gui.py +++ b/src/sprites/gui/inventory_gui.py @@ -8,22 +8,22 @@ class InventoryGUI: """Graphical User Interface to display the player's inventory.""" def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: - self.screen = screen - self.inventory = inventory - self.font = pygame.font.Font(None, 36) - self.running = False + self.screen: pygame.Surface = screen + self.inventory: Inventory = inventory + self.font: pygame.font.Font = pygame.font.Font(None, 36) + self.running: bool = False # Scrolling inventory - self.scroll_offset = 0 - self.max_visible_items = 10 - self.item_height = 60 + self.scroll_offset: int = 0 + self.max_visible_items: int = 10 + self.item_height: int = 60 # Load a sprite sheet and extract the icons (Testing purposes) # To be replaced when: # 1) Sprite sheet has been decided. # 2) A 'Buy', 'Found' or 'Add' in-game feature has been implemented self.sprite_sheet = pygame.image.load("images/tilesets/Treasure+.png").convert_alpha() - self.icons = { + self.icons: dict[str, pygame.Surface] = { "Gold Coin": self.extract_icon(0, 0), "Silver Coin": self.extract_icon(16, 0), "Coin Stack (1)": self.extract_icon(32, 0), @@ -68,46 +68,48 @@ def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: } # Button dimensions - self.button_width = 100 - self.button_height = 50 + self.button_width: int = 100 + self.button_height: int = 50 # Initialize button actions self.button_actions: dict[str, tuple[pygame.Rect, pygame.Rect]] = {} # Action messages - self.message = "" - self.message_end_time = 0 # Time to display the message + self.message: str = "" + self.message_end_time: int = 0 # Time to display the message - def handle_events(self, event): + def handle_events(self, event: pygame.event.Event) -> None: """Handle events like keypress or mouse wheel.""" + if event.type == pygame.MOUSEWHEEL: # Adjust scroll offset self.scroll_offset = max(0, self.scroll_offset - event.y) max_offset = max(0, len(self.inventory.get_items()) - self.max_visible_items) self.scroll_offset = min(self.scroll_offset, max_offset) - def extract_icon(self, x, y, size=TILE_SIZE): + def extract_icon(self, x: int, y: int, size: int = TILE_SIZE) -> pygame.Surface: """Extract a single icon from the sprite sheet.""" + return self.sprite_sheet.subsurface((x, y, size, size)) def draw_buttons(self, x: int, y: int, item: str) -> tuple[pygame.Rect, pygame.Rect]: """Draw Use and Discard buttons for a specific item.""" - use_button = pygame.Rect(x, y, self.button_width, self.button_height) - discard_button = pygame.Rect(x + self.button_width + 10, y, self.button_width, self.button_height) + use_button: pygame.Rect = pygame.Rect(x, y, self.button_width, self.button_height) + discard_button: pygame.Rect = pygame.Rect(x + self.button_width + 10, y, self.button_width, self.button_height) pygame.draw.rect(self.screen, (0, 255, 0), use_button) # Green pygame.draw.rect(self.screen, (150, 75, 0), discard_button) # Brown - use_text = self.font.render("Use", True, (0, 0, 0)) # Black - discard_text = self.font.render("Discard", True, (0, 0, 0)) + use_text: pygame.Surface = self.font.render("Use", True, (0, 0, 0)) # Black + discard_text: pygame.Surface = self.font.render("Discard", True, (0, 0, 0)) self.screen.blit(use_text, (x + 10, y + 10)) self.screen.blit(discard_text, (x + self.button_width + 20, y + 10)) return use_button, discard_button - def draw(self): + def draw(self) -> None: """Draw the inventory overlay.""" self.screen.fill((0, 0, 0)) # Solid Black background @@ -116,9 +118,9 @@ def draw(self): self.button_actions = {} # Draw the inventory items - items = list(self.inventory.get_items().items()) - visible_items = items[self.scroll_offset : self.scroll_offset + self.max_visible_items] - y_offset = 50 # Start below the title + items: list = list(self.inventory.get_items().items()) + visible_items: list = items[self.scroll_offset : self.scroll_offset + self.max_visible_items] + y_offset: int = 50 # Start below the title for item, quantity in visible_items: # Draw icon @@ -153,10 +155,10 @@ def draw(self): text_width, text_height = message_text.get_size() # Message background - message_bg_x = 40 - message_bg_y = self.screen.get_height() - 120 - message_bg_width = text_width + 20 # Add padding - message_bg_height = text_height + 10 # Add padding + message_bg_x: int = 40 + message_bg_y: int = self.screen.get_height() - 120 + message_bg_width: int = text_width + 20 # Add padding + message_bg_height: int = text_height + 10 # Add padding # Draw background rectangle for the message pygame.draw.rect( @@ -171,7 +173,7 @@ def draw(self): (message_bg_x + 10, message_bg_y + 5), # Position text with padding ) - def handle_mouse_click(self, mouse_pos) -> None: + def handle_mouse_click(self, mouse_pos: tuple[int, int]) -> None: """Handle mouse clicks on buttons.""" for item, (use_button, discard_button) in self.button_actions.items(): From cfb92069deabfd0c3268cb92e34d9cd1de54350f Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Tue, 1 Apr 2025 00:17:23 +0200 Subject: [PATCH 03/38] Add grid manager to the game_running.py state --- src/states/game_running.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/states/game_running.py b/src/states/game_running.py index 35e7788..f728a35 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -14,6 +14,7 @@ from src.sprites.base import BaseSprite from src.sprites.camera.player import PlayerCamera from src.sprites.entities.player import Player +from src.sprites.tiles.grid_manager import GridManager from src.states.base_state import BaseState from src.states.paused import Paused from src.states.shop_state import ShowShop, WindowShop @@ -39,8 +40,13 @@ def __init__(self, game_state_manager) -> None: self.player_inventory = Inventory() self.load_inventory_from_json("data/inventory.json") + # Camera group self.all_sprites = PlayerCamera() + # Render the grid + self.grid_manager = GridManager(self.all_sprites.display_surface, TILE_SIZE) + self.show_grid: bool = True + # The start positions will be one of the 4 islands in the corners of the board self.setup(player_start_pos="top_left_island") @@ -145,20 +151,28 @@ def update(self, events) -> None: dt = self.clock.tick() / 1000 self.all_sprites.update(dt) + # Handle player movement and grid snapping + self.player.update(dt, grid=self.grid_manager) + # get events like keypress or mouse clicks for event in events: if event.type == pygame.KEYDOWN: if event.key == pygame.K_i: # Toggle inventory with "I" key self.game_state_manager.enter_state(Paused(self.game_state_manager, self.player_inventory)) - if collide and event.key == pygame.K_e: + elif collide and event.key == pygame.K_e: self.game_state_manager.enter_state( WindowShop(self.game_state_manager, self.player, self.shop, self.player_inventory) ) + elif event.key == pygame.K_g: # Toggle grid with "G" key + self.show_grid = not self.show_grid def render(self, screen) -> None: """draw sprites to the canvas""" screen.fill("#000000") - self.all_sprites.draw(self.player.rect.center) + self.all_sprites.draw( + self.player.rect.center, + show_grid=self.show_grid + ) # self.welcome_message = self.font.render("Press 'E' to interact!", True, (100, 100, 100)) # point = self.shop.rect From 06f3f012c78b0ab8f1bb9caa121da333c2f6a4f5 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Tue, 1 Apr 2025 00:18:41 +0200 Subject: [PATCH 04/38] create grid manager class that handles the grid overlay over the game --- src/sprites/tiles/grid_manager.py | 68 +++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/sprites/tiles/grid_manager.py diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py new file mode 100644 index 0000000..8af4079 --- /dev/null +++ b/src/sprites/tiles/grid_manager.py @@ -0,0 +1,68 @@ +from abc import ABC + +import pygame +from pygame import Surface + +from src.settings import TILE_SIZE + +class GridManager: + """Handles grid rendering and interaction.""" + + def __init__( + self, + display_surface: pygame.Surface, + tile_size: int = TILE_SIZE, + grid_color: str = "grey", + hover_color: str = "azure4" + ): + self.display_surface: pygame.Surface = display_surface + self.tile_size: int = tile_size + self.grid_color: str = grid_color + self.hover_color: str = hover_color + self.overlay_alpha: int = 50 # Transparency level (0-255) + self.block_size: int = 64 + self.coordinates: dict[tuple[int, int], tuple[int, int]] = {} + + def draw(self, mouse_pos: tuple[int, int], valid_moves: set[tuple[int, int]] = None) -> None: + + overlay: Surface = pygame.Surface(self.display_surface.get_size(), pygame.SRCALPHA) + overlay.fill((0, 0, 0, 0)) # Fully transparent background + + font = pygame.font.SysFont("Arial", 12) + + for x in range(0, self.display_surface.get_width(), self.block_size): + for y in range(0, self.display_surface.get_height(), self.block_size): + rect = pygame.Rect(x, y, self.block_size, self.block_size) + + # Highlight valid tiles + if valid_moves and (x, y) in valid_moves: + pygame.draw.rect(overlay, (0, 255, 0, 100), rect) + elif rect.collidepoint(mouse_pos): + pygame.draw.rect(overlay, (0, 0, 255, 100), rect) + + # Draw grid lines + pygame.draw.rect(self.display_surface, self.grid_color, rect, 1) + self.coordinates[(x, y)] = (x // self.block_size, y // self.block_size) + + # Render the x, y integers + text = font.render(f"({self.coordinates[(x, y)][0]}, {self.coordinates[(x, y)][1]})", True, (0, 0, 0)) + text_rect = text.get_rect(center=rect.center) + self.display_surface.blit(text, text_rect) + + # Blit the transparent overlay onto the display surface + self.display_surface.blit(overlay, (0, 0)) + + def get_tile_coordinates(self, mouse_pos: tuple[int, int], player: object) -> tuple[int, int]: + x, y = mouse_pos + player_x, player_y = player.position + max_distance: int = 6 + + # Calculate the distance from the player's position to the mouse position + distance = ((x - player_x) ** 2 + (y - player_y) ** 2) ** 0.5 + + return (x // self.block_size * self.block_size, y // self.block_size * self.block_size) if distance > max_distance else None + + def get_coordinates(self, x, y): + """Returns the coordinates of a tile at position (x, y).""" + + return self.coordinates.get((x, y)) \ No newline at end of file From a26adf7fdcf8aac52cf4eb479eae1c8f7273dd79 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Tue, 1 Apr 2025 00:20:59 +0200 Subject: [PATCH 05/38] Edit the player class to handle a simple implementation of the grid --- src/sprites/entities/player.py | 155 ++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 70 deletions(-) diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py index d6933fc..5202946 100644 --- a/src/sprites/entities/player.py +++ b/src/sprites/entities/player.py @@ -1,13 +1,17 @@ import pygame -from pygame import Surface, Vector2 +from pygame import Surface, Vector2, FRect from pygame.sprite import Group from src.inventory import Inventory -from src.settings import TILE_SIZE +from src.settings import TILE_SIZE, SCREEN_WIDTH, SCREEN_HEIGHT from src.sprites.base import BaseSprite class Player(BaseSprite): + """Handles player interaction with the grid, using instant tile movement.""" + rect: FRect + + def __init__( self, pos: tuple[int, int], @@ -30,9 +34,10 @@ def __init__( self.frames = frames self.frame_index: float = 0.0 - # Ghost preview - self.player_preview = first_frame.copy() - self.player_preview.set_alpha(128) + self.position = pos + self.selected: bool = False + self.valid_moves: list = [] # Stores valid moves around the player + self.prev_tile = None # Inventory system self.inventory = Inventory() @@ -40,78 +45,88 @@ def __init__( # Input handling self.mouse_have_been_pressed: bool = False - def input(self) -> None: - """move the player and show a ghost to preview the move""" - - # Reset direction - self.direction = Vector2(0, 0) - - # Get mouse position - mouse_pos = pygame.mouse.get_pos() - - # get the relative pos of the player from the mouse - # to know on which axis the player will move - delta_x = abs(self.rect.centerx - mouse_pos[0]) - delta_y = abs(self.rect.centery - mouse_pos[1]) + def get_adjacent_tiles(self, grid, blocked_tiles=[]): + """Calculate and return all valid adjacent (neighbor) tiles for the player.""" + + if blocked_tiles is None: + blocked_tiles = [] + x, y = self.rect.topleft + tile_size = grid.block_size + directions = [ + (0, -tile_size), (0, tile_size), # Up, Down + (-tile_size, 0), (tile_size, 0) # Left, Right + ] + self.valid_moves = [ + (x + dx, y + dy) + for dx, dy in directions + if 0 <= x + dx < SCREEN_WIDTH + and 0 <= y + dy < SCREEN_HEIGHT + and (x + dx, y + dy) not in blocked_tiles + ] + + + def input(self, grid) -> None: + """Handle player movement using instant tile-based logic""" - # # move the ghost on the x-axis - # self.player_preview_rect = self.rect.copy() - # if delta_x > delta_y: - # if delta_x < (TILE_SIZE / 2): - # # don't move the ghost if the mouse is on the player hit box - # self.player_preview_rect.x = self.rect.x - # elif mouse_pos[0] > self.rect.centerx: - # # go right - # self.player_preview_rect.x = self.rect.x + TILE_SIZE - # else: - # # go left - # self.player_preview_rect.x = self.rect.x - TILE_SIZE - # # move the ghost on the y-axis - # else: - # if delta_y < (TILE_SIZE / 2): - # # don't move if the mouse is on the player hit box - # self.player_preview_rect.y = self.rect.y - # elif mouse_pos[1] > self.rect.centery: - # # go down - # self.player_preview_rect.y = self.rect.y + TILE_SIZE - # else: - # # go up - # self.player_preview_rect.y = self.rect.y - TILE_SIZE - - # Handle mouse input for movement if not pygame.mouse.get_pressed()[0]: self.mouse_have_been_pressed = False return - if self.mouse_have_been_pressed: - return - self.mouse_have_been_pressed = True - - # Move on the x-axis or y-axis - if delta_x > delta_y: - if delta_x >= (TILE_SIZE / 2): - if mouse_pos[0] > self.rect.centerx: - self.direction.x = 1 - # if delta_x >= (TILE_SIZE / 2): - # self.direction.x = 1 if mouse_pos[0] > self.rect.centerx else -1 - else: - self.direction.x = -1 - # if delta_y >= (TILE_SIZE / 2): - # self.direction.y = 1 if mouse_pos[1] > self.rect.centery else -1 - else: - if delta_y >= (TILE_SIZE / 2): - if mouse_pos[1] > self.rect.centery: - self.direction.y = 1 - else: - self.direction.y = -1 - - # Update position - self.rect.x += self.direction.x * TILE_SIZE - self.rect.y += self.direction.y * TILE_SIZE + # Get the tile coordinates from the grid + mouse_pos = pygame.mouse.get_pos() + tile_x, tile_y = grid.get_tile_coordinates(mouse_pos, self) + + if (tile_x, tile_y) and (tile_x, tile_y) in self.valid_moves: + self.rect.topleft = (tile_x, tile_y) # Instantly snap to the tile + self.prev_tile = (tile_x, tile_y) # Update previous tile + + # # Reset direction + # self.direction = Vector2(0, 0) + # + # # Get mouse position + # mouse_pos = pygame.mouse.get_pos() + # + # # get the relative pos of the player from the mouse + # # to know on which axis the player will move + # delta_x = abs(self.rect.centerx - mouse_pos[0]) + # delta_y = abs(self.rect.centery - mouse_pos[1]) + # + # # Handle mouse input for movement + # if not pygame.mouse.get_pressed()[0]: + # self.mouse_have_been_pressed = False + # return + # if self.mouse_have_been_pressed: + # return + # + # self.mouse_have_been_pressed = True + # + # # Move on the x-axis or y-axis + # if delta_x > delta_y: + # if delta_x >= (TILE_SIZE / 2): + # if mouse_pos[0] > self.rect.centerx: + # self.direction.x = 1 + # # if delta_x >= (TILE_SIZE / 2): + # # self.direction.x = 1 if mouse_pos[0] > self.rect.centerx else -1 + # else: + # self.direction.x = -1 + # # if delta_y >= (TILE_SIZE / 2): + # # self.direction.y = 1 if mouse_pos[1] > self.rect.centery else -1 + # else: + # if delta_y >= (TILE_SIZE / 2): + # if mouse_pos[1] > self.rect.centery: + # self.direction.y = 1 + # else: + # self.direction.y = -1 + # + # # Update position + # self.rect.x += self.direction.x * TILE_SIZE + # self.rect.y += self.direction.y * TILE_SIZE # return None - def update(self, dt: float) -> None: + def update(self, dt: float, grid=None) -> None: """Update the player's position and state.""" - self.input() + if grid: + self.get_adjacent_tiles(grid) # Update valid moves + self.input(grid) # Handle input self.animate(dt) From 99fabbe3b95976ed853d160692c4f71f01a436cd Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Tue, 1 Apr 2025 00:23:04 +0200 Subject: [PATCH 06/38] Edit the PlayerCamera class to make the grid workable in the camera position --- src/sprites/camera/player.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index 2c6ab44..22c9317 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -1,7 +1,8 @@ import pygame # ignore -from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS +from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS, TILE_SIZE from src.sprites.camera.group import AllSprites +from src.sprites.tiles.grid_manager import GridManager class PlayerCamera(AllSprites): @@ -19,7 +20,18 @@ class PlayerCamera(AllSprites): scale (float): The scaling factor for rendering sprites. """ - def draw(self, player_center): + def __init__(self): + super().__init__() + + self.display_surface = pygame.display.get_surface() + if not self.display_surface: + raise ValueError("Display surface is not initialized") + + self.offset = pygame.math.Vector2() + self.scale = 2.0 + self.grid = GridManager(self.display_surface, tile_size=TILE_SIZE) + + def draw(self, player_center, show_grid = False): # Calculate offsets self.offset.x = -(player_center[0] * self.scale - SCREEN_WIDTH / 2) self.offset.y = -(player_center[1] * self.scale - SCREEN_HEIGHT / 2) @@ -59,3 +71,7 @@ def draw(self, player_center): if self.display_surface is None: raise ValueError("self.display_surface cannot be None") self.display_surface.blit(scaled_image, scaled_rect.topleft) + + # Draw transparent grid overlay if toggled + if show_grid: + self.grid.draw(pygame.mouse.get_pos()) \ No newline at end of file From 31008cdc1ad613b6dfbeebbe42950b0add1a3258 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Tue, 1 Apr 2025 00:24:01 +0200 Subject: [PATCH 07/38] testgrid functionality for movement --- src/testgrid.py | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/testgrid.py diff --git a/src/testgrid.py b/src/testgrid.py new file mode 100644 index 0000000..8617e8e --- /dev/null +++ b/src/testgrid.py @@ -0,0 +1,50 @@ +class Player(Entity): + """Move tile-by-tile on a grid""" + + def __init__(self, pos, frames, groups): + super().__init__(pos, frames, groups) + + self.grid_pos = pygame.math.Vector2( + pos[0] // TILE_SIZE, pos[1] // TILE_SIZE + ) # Store player's position in grid coordinates + self.target_grid_pos = self.grid_pos.copy() # Target position on the grid + self.rect.topleft = (self.grid_pos.x * TILE_SIZE, self.grid_pos.y * TILE_SIZE) # Align rect with the grid + self.mouse_have_been_pressed = False + + def snap_to_grid(self): + """Ensure the player's rect is perfectly aligned with the grid""" + self.rect.topleft = (self.grid_pos.x * TILE_SIZE, self.grid_pos.y * TILE_SIZE) + + def input(self): + """Handle mouse input to move the player on the grid""" + if not pygame.mouse.get_pressed()[0]: + self.mouse_have_been_pressed = False + return + + if self.mouse_have_been_pressed: + return + + self.mouse_have_been_pressed = True + + # Get mouse position and convert it to grid coordinates + mouse_pos = pygame.mouse.get_pos() + mouse_grid_pos = pygame.math.Vector2( + mouse_pos[0] // TILE_SIZE, mouse_pos[1] // TILE_SIZE + ) + + # Check if the mouse click is on an adjacent tile + delta = mouse_grid_pos - self.grid_pos + if delta.length() == 1: # Ensure it's exactly one tile away + self.target_grid_pos = mouse_grid_pos + + def update(self): + """Update the player's position""" + if self.grid_pos != self.target_grid_pos: + # Move the player one step closer to the target position + delta = self.target_grid_pos - self.grid_pos + if delta.x != 0: + self.grid_pos.x += delta.x / abs(delta.x) # Move horizontally + elif delta.y != 0: + self.grid_pos.y += delta.y / abs(delta.y) # Move vertically + + self.snap_to_grid() From 069685364c3c344ebceb93aaa0f096284edce2db Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Tue, 1 Apr 2025 00:24:11 +0200 Subject: [PATCH 08/38] Revert "Edit the PlayerCamera class to make the grid workable in the camera position" This reverts commit 99fabbe3b95976ed853d160692c4f71f01a436cd. --- src/sprites/camera/player.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index 22c9317..2c6ab44 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -1,8 +1,7 @@ import pygame # ignore -from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS, TILE_SIZE +from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS from src.sprites.camera.group import AllSprites -from src.sprites.tiles.grid_manager import GridManager class PlayerCamera(AllSprites): @@ -20,18 +19,7 @@ class PlayerCamera(AllSprites): scale (float): The scaling factor for rendering sprites. """ - def __init__(self): - super().__init__() - - self.display_surface = pygame.display.get_surface() - if not self.display_surface: - raise ValueError("Display surface is not initialized") - - self.offset = pygame.math.Vector2() - self.scale = 2.0 - self.grid = GridManager(self.display_surface, tile_size=TILE_SIZE) - - def draw(self, player_center, show_grid = False): + def draw(self, player_center): # Calculate offsets self.offset.x = -(player_center[0] * self.scale - SCREEN_WIDTH / 2) self.offset.y = -(player_center[1] * self.scale - SCREEN_HEIGHT / 2) @@ -71,7 +59,3 @@ def draw(self, player_center, show_grid = False): if self.display_surface is None: raise ValueError("self.display_surface cannot be None") self.display_surface.blit(scaled_image, scaled_rect.topleft) - - # Draw transparent grid overlay if toggled - if show_grid: - self.grid.draw(pygame.mouse.get_pos()) \ No newline at end of file From 3fc7749e1af86dc1dfe1ddc05166e9c49f0997a9 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Tue, 1 Apr 2025 00:26:19 +0200 Subject: [PATCH 09/38] Edit the PlayerCamera to handle the grid showing when moving over the grid. --- src/sprites/camera/player.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index 2c6ab44..22c9317 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -1,7 +1,8 @@ import pygame # ignore -from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS +from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS, TILE_SIZE from src.sprites.camera.group import AllSprites +from src.sprites.tiles.grid_manager import GridManager class PlayerCamera(AllSprites): @@ -19,7 +20,18 @@ class PlayerCamera(AllSprites): scale (float): The scaling factor for rendering sprites. """ - def draw(self, player_center): + def __init__(self): + super().__init__() + + self.display_surface = pygame.display.get_surface() + if not self.display_surface: + raise ValueError("Display surface is not initialized") + + self.offset = pygame.math.Vector2() + self.scale = 2.0 + self.grid = GridManager(self.display_surface, tile_size=TILE_SIZE) + + def draw(self, player_center, show_grid = False): # Calculate offsets self.offset.x = -(player_center[0] * self.scale - SCREEN_WIDTH / 2) self.offset.y = -(player_center[1] * self.scale - SCREEN_HEIGHT / 2) @@ -59,3 +71,7 @@ def draw(self, player_center): if self.display_surface is None: raise ValueError("self.display_surface cannot be None") self.display_surface.blit(scaled_image, scaled_rect.topleft) + + # Draw transparent grid overlay if toggled + if show_grid: + self.grid.draw(pygame.mouse.get_pos()) \ No newline at end of file From 1fdabe3fe4f57647a04c314614480c332a37664a Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Tue, 1 Apr 2025 00:28:34 +0200 Subject: [PATCH 10/38] Revert "Edit the PlayerCamera to handle the grid showing when moving over the grid." This reverts commit 3fc7749e1af86dc1dfe1ddc05166e9c49f0997a9. --- src/sprites/camera/player.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index 22c9317..2c6ab44 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -1,8 +1,7 @@ import pygame # ignore -from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS, TILE_SIZE +from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS from src.sprites.camera.group import AllSprites -from src.sprites.tiles.grid_manager import GridManager class PlayerCamera(AllSprites): @@ -20,18 +19,7 @@ class PlayerCamera(AllSprites): scale (float): The scaling factor for rendering sprites. """ - def __init__(self): - super().__init__() - - self.display_surface = pygame.display.get_surface() - if not self.display_surface: - raise ValueError("Display surface is not initialized") - - self.offset = pygame.math.Vector2() - self.scale = 2.0 - self.grid = GridManager(self.display_surface, tile_size=TILE_SIZE) - - def draw(self, player_center, show_grid = False): + def draw(self, player_center): # Calculate offsets self.offset.x = -(player_center[0] * self.scale - SCREEN_WIDTH / 2) self.offset.y = -(player_center[1] * self.scale - SCREEN_HEIGHT / 2) @@ -71,7 +59,3 @@ def draw(self, player_center, show_grid = False): if self.display_surface is None: raise ValueError("self.display_surface cannot be None") self.display_surface.blit(scaled_image, scaled_rect.topleft) - - # Draw transparent grid overlay if toggled - if show_grid: - self.grid.draw(pygame.mouse.get_pos()) \ No newline at end of file From c0315fb9ae64871c54d0d9efd2ce13b059562280 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Tue, 1 Apr 2025 00:30:29 +0200 Subject: [PATCH 11/38] Add logic from the grid to the player camera that if the player moves over the grid, that the camera moves with it. --- src/sprites/camera/player.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index 2c6ab44..22c9317 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -1,7 +1,8 @@ import pygame # ignore -from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS +from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS, TILE_SIZE from src.sprites.camera.group import AllSprites +from src.sprites.tiles.grid_manager import GridManager class PlayerCamera(AllSprites): @@ -19,7 +20,18 @@ class PlayerCamera(AllSprites): scale (float): The scaling factor for rendering sprites. """ - def draw(self, player_center): + def __init__(self): + super().__init__() + + self.display_surface = pygame.display.get_surface() + if not self.display_surface: + raise ValueError("Display surface is not initialized") + + self.offset = pygame.math.Vector2() + self.scale = 2.0 + self.grid = GridManager(self.display_surface, tile_size=TILE_SIZE) + + def draw(self, player_center, show_grid = False): # Calculate offsets self.offset.x = -(player_center[0] * self.scale - SCREEN_WIDTH / 2) self.offset.y = -(player_center[1] * self.scale - SCREEN_HEIGHT / 2) @@ -59,3 +71,7 @@ def draw(self, player_center): if self.display_surface is None: raise ValueError("self.display_surface cannot be None") self.display_surface.blit(scaled_image, scaled_rect.topleft) + + # Draw transparent grid overlay if toggled + if show_grid: + self.grid.draw(pygame.mouse.get_pos()) \ No newline at end of file From 426032af619061700d84c9f17cd6de9397ea9744 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Tue, 1 Apr 2025 00:31:34 +0200 Subject: [PATCH 12/38] Revert "Add logic from the grid to the player camera that if the player moves over the grid, that the camera moves with it." This reverts commit c0315fb9ae64871c54d0d9efd2ce13b059562280. --- src/sprites/camera/player.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index 22c9317..2c6ab44 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -1,8 +1,7 @@ import pygame # ignore -from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS, TILE_SIZE +from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS from src.sprites.camera.group import AllSprites -from src.sprites.tiles.grid_manager import GridManager class PlayerCamera(AllSprites): @@ -20,18 +19,7 @@ class PlayerCamera(AllSprites): scale (float): The scaling factor for rendering sprites. """ - def __init__(self): - super().__init__() - - self.display_surface = pygame.display.get_surface() - if not self.display_surface: - raise ValueError("Display surface is not initialized") - - self.offset = pygame.math.Vector2() - self.scale = 2.0 - self.grid = GridManager(self.display_surface, tile_size=TILE_SIZE) - - def draw(self, player_center, show_grid = False): + def draw(self, player_center): # Calculate offsets self.offset.x = -(player_center[0] * self.scale - SCREEN_WIDTH / 2) self.offset.y = -(player_center[1] * self.scale - SCREEN_HEIGHT / 2) @@ -71,7 +59,3 @@ def draw(self, player_center, show_grid = False): if self.display_surface is None: raise ValueError("self.display_surface cannot be None") self.display_surface.blit(scaled_image, scaled_rect.topleft) - - # Draw transparent grid overlay if toggled - if show_grid: - self.grid.draw(pygame.mouse.get_pos()) \ No newline at end of file From 747fa012ab77af65f19b50cefee2b9e0a5b33302 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Tue, 1 Apr 2025 00:32:16 +0200 Subject: [PATCH 13/38] Add logic from the grid to the player camera that if the player moves over the grid, that the camera moves with it. --- src/sprites/camera/player.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index 2c6ab44..22c9317 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -1,7 +1,8 @@ import pygame # ignore -from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS +from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS, TILE_SIZE from src.sprites.camera.group import AllSprites +from src.sprites.tiles.grid_manager import GridManager class PlayerCamera(AllSprites): @@ -19,7 +20,18 @@ class PlayerCamera(AllSprites): scale (float): The scaling factor for rendering sprites. """ - def draw(self, player_center): + def __init__(self): + super().__init__() + + self.display_surface = pygame.display.get_surface() + if not self.display_surface: + raise ValueError("Display surface is not initialized") + + self.offset = pygame.math.Vector2() + self.scale = 2.0 + self.grid = GridManager(self.display_surface, tile_size=TILE_SIZE) + + def draw(self, player_center, show_grid = False): # Calculate offsets self.offset.x = -(player_center[0] * self.scale - SCREEN_WIDTH / 2) self.offset.y = -(player_center[1] * self.scale - SCREEN_HEIGHT / 2) @@ -59,3 +71,7 @@ def draw(self, player_center): if self.display_surface is None: raise ValueError("self.display_surface cannot be None") self.display_surface.blit(scaled_image, scaled_rect.topleft) + + # Draw transparent grid overlay if toggled + if show_grid: + self.grid.draw(pygame.mouse.get_pos()) \ No newline at end of file From c431746858b8806735593587dbd78abe6bd93a40 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Tue, 1 Apr 2025 00:33:04 +0200 Subject: [PATCH 14/38] Uncomment old movement system for now. --- src/sprites/entities/player.py | 96 +++++++++++++++++----------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py index 5202946..42e7acb 100644 --- a/src/sprites/entities/player.py +++ b/src/sprites/entities/player.py @@ -68,59 +68,59 @@ def get_adjacent_tiles(self, grid, blocked_tiles=[]): def input(self, grid) -> None: """Handle player movement using instant tile-based logic""" - if not pygame.mouse.get_pressed()[0]: - self.mouse_have_been_pressed = False - return - - # Get the tile coordinates from the grid - mouse_pos = pygame.mouse.get_pos() - tile_x, tile_y = grid.get_tile_coordinates(mouse_pos, self) - - if (tile_x, tile_y) and (tile_x, tile_y) in self.valid_moves: - self.rect.topleft = (tile_x, tile_y) # Instantly snap to the tile - self.prev_tile = (tile_x, tile_y) # Update previous tile - - # # Reset direction - # self.direction = Vector2(0, 0) - # - # # Get mouse position - # mouse_pos = pygame.mouse.get_pos() - # - # # get the relative pos of the player from the mouse - # # to know on which axis the player will move - # delta_x = abs(self.rect.centerx - mouse_pos[0]) - # delta_y = abs(self.rect.centery - mouse_pos[1]) - # - # # Handle mouse input for movement # if not pygame.mouse.get_pressed()[0]: # self.mouse_have_been_pressed = False # return - # if self.mouse_have_been_pressed: - # return # - # self.mouse_have_been_pressed = True - # - # # Move on the x-axis or y-axis - # if delta_x > delta_y: - # if delta_x >= (TILE_SIZE / 2): - # if mouse_pos[0] > self.rect.centerx: - # self.direction.x = 1 - # # if delta_x >= (TILE_SIZE / 2): - # # self.direction.x = 1 if mouse_pos[0] > self.rect.centerx else -1 - # else: - # self.direction.x = -1 - # # if delta_y >= (TILE_SIZE / 2): - # # self.direction.y = 1 if mouse_pos[1] > self.rect.centery else -1 - # else: - # if delta_y >= (TILE_SIZE / 2): - # if mouse_pos[1] > self.rect.centery: - # self.direction.y = 1 - # else: - # self.direction.y = -1 + # # Get the tile coordinates from the grid + # mouse_pos = pygame.mouse.get_pos() + # tile_x, tile_y = grid.get_tile_coordinates(mouse_pos, self) # - # # Update position - # self.rect.x += self.direction.x * TILE_SIZE - # self.rect.y += self.direction.y * TILE_SIZE + # if (tile_x, tile_y) and (tile_x, tile_y) in self.valid_moves: + # self.rect.topleft = (tile_x, tile_y) # Instantly snap to the tile + # self.prev_tile = (tile_x, tile_y) # Update previous tile + + # Reset direction + self.direction = Vector2(0, 0) + + # Get mouse position + mouse_pos = pygame.mouse.get_pos() + + # get the relative pos of the player from the mouse + # to know on which axis the player will move + delta_x = abs(self.rect.centerx - mouse_pos[0]) + delta_y = abs(self.rect.centery - mouse_pos[1]) + + # Handle mouse input for movement + if not pygame.mouse.get_pressed()[0]: + self.mouse_have_been_pressed = False + return + if self.mouse_have_been_pressed: + return + + self.mouse_have_been_pressed = True + + # Move on the x-axis or y-axis + if delta_x > delta_y: + if delta_x >= (TILE_SIZE / 2): + if mouse_pos[0] > self.rect.centerx: + self.direction.x = 1 + # if delta_x >= (TILE_SIZE / 2): + # self.direction.x = 1 if mouse_pos[0] > self.rect.centerx else -1 + else: + self.direction.x = -1 + # if delta_y >= (TILE_SIZE / 2): + # self.direction.y = 1 if mouse_pos[1] > self.rect.centery else -1 + else: + if delta_y >= (TILE_SIZE / 2): + if mouse_pos[1] > self.rect.centery: + self.direction.y = 1 + else: + self.direction.y = -1 + + # Update position + self.rect.x += self.direction.x * TILE_SIZE + self.rect.y += self.direction.y * TILE_SIZE # return None From 0fe87514bdcbbcc1a46906cd701053da73be52af Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sat, 17 May 2025 22:29:17 +0200 Subject: [PATCH 15/38] Fixed a bug that showed a copy of the player in the starting position after moving to different tiles. --- src/sprites/camera/player.py | 18 ++++++++---------- src/states/game_running.py | 22 ++++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index 22c9317..a8e5060 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -21,15 +21,13 @@ class PlayerCamera(AllSprites): """ def __init__(self): - super().__init__() - - self.display_surface = pygame.display.get_surface() - if not self.display_surface: - raise ValueError("Display surface is not initialized") - - self.offset = pygame.math.Vector2() - self.scale = 2.0 - self.grid = GridManager(self.display_surface, tile_size=TILE_SIZE) + super().__init__() + self.display_surface = pygame.display.get_surface() + if not self.display_surface: + raise ValueError("Display surface is not initialized") + self.offset = pygame.math.Vector2() + self.scale = 2.0 + self.grid = GridManager(self.display_surface, tile_size=TILE_SIZE) def draw(self, player_center, show_grid = False): # Calculate offsets @@ -72,6 +70,6 @@ def draw(self, player_center, show_grid = False): raise ValueError("self.display_surface cannot be None") self.display_surface.blit(scaled_image, scaled_rect.topleft) - # Draw transparent grid overlay if toggled + # Draw a transparent grid overlay if toggled if show_grid: self.grid.draw(pygame.mouse.get_pos()) \ No newline at end of file diff --git a/src/states/game_running.py b/src/states/game_running.py index f728a35..ca76817 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -43,8 +43,11 @@ def __init__(self, game_state_manager) -> None: # Camera group self.all_sprites = PlayerCamera() + # Load the TMX map + self.tmx_map = {"map": load_pygame(os.path.join(".", "data", "new_maps", "100x100_map.tmx"))} + # Render the grid - self.grid_manager = GridManager(self.all_sprites.display_surface, TILE_SIZE) + self.grid_manager = GridManager(self.tmx_map["map"], TILE_SIZE) self.show_grid: bool = True # The start positions will be one of the 4 islands in the corners of the board @@ -58,7 +61,6 @@ def setup(self, player_start_pos: str) -> None: """ set up the map and player from the tiled file """ - self.tmx_map = {"map": load_pygame(os.path.join(".", "data", "new_maps", "100x100_map.tmx"))} self.world_frames = { "water": import_folder(".", "images", "tilesets", "temporary_water"), @@ -111,14 +113,14 @@ def setup(self, player_start_pos: str) -> None: z=WORLD_LAYERS["bg"], ) - # Enitites - for obj in self.tmx_map["map"].get_layer_by_name("Ships"): - if obj.name == "Player" and obj.properties["pos"] == player_start_pos: - self.player = Player( - pos=(obj.x, obj.y), - frames=self.world_frames["ships"]["player_test_ship"], - groups=(self.all_sprites,), - ) + # Enitites + for obj in self.tmx_map["map"].get_layer_by_name("Ships"): + if obj.name == "Player" and obj.properties["pos"] == player_start_pos: + self.player = Player( + pos=(obj.x, obj.y), + frames=self.world_frames["ships"]["player_test_ship"], + groups=(self.all_sprites,), + ) # Coast for obj in self.tmx_map["map"].get_layer_by_name("Coast"): From 634c8bf73cd9e851da2aa83a715530b07024ff97 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sun, 18 May 2025 00:57:23 +0200 Subject: [PATCH 16/38] - Add pathfinding to requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d1593ad..24ba017 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pygame-ce>=2.5.0 -pytmx>=3.32 \ No newline at end of file +pytmx>=3.32 +pathfinding~=1.0.17 \ No newline at end of file From 9f798b3993855e6ec89e3a7c1908e32686b51ec6 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sun, 18 May 2025 00:57:56 +0200 Subject: [PATCH 17/38] - Implement pathfinding to GridManager class --- src/sprites/camera/player.py | 6 +- src/sprites/entities/player.py | 73 ++++--------- src/sprites/tiles/grid_manager.py | 172 +++++++++++++++++++++--------- src/states/game_running.py | 30 ++++-- 4 files changed, 168 insertions(+), 113 deletions(-) diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index a8e5060..335ca91 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -20,14 +20,16 @@ class PlayerCamera(AllSprites): scale (float): The scaling factor for rendering sprites. """ - def __init__(self): + def __init__(self, tmx_map, player_start_pos): super().__init__() self.display_surface = pygame.display.get_surface() if not self.display_surface: raise ValueError("Display surface is not initialized") + self.offset = pygame.math.Vector2() self.scale = 2.0 - self.grid = GridManager(self.display_surface, tile_size=TILE_SIZE) + self.grid = GridManager(tmx_map, tile_size=TILE_SIZE) + self.player_start_pos = player_start_pos def draw(self, player_center, show_grid = False): # Calculate offsets diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py index 42e7acb..2f7acf8 100644 --- a/src/sprites/entities/player.py +++ b/src/sprites/entities/player.py @@ -11,7 +11,6 @@ class Player(BaseSprite): """Handles player interaction with the grid, using instant tile movement.""" rect: FRect - def __init__( self, pos: tuple[int, int], @@ -25,10 +24,10 @@ def __init__( :param groups: Sprite groups the player belongs to. """ - # Use the first frame as the base surface - first_frame = frames[0] if isinstance(frames, (list, tuple)) and frames else Surface((TILE_SIZE, TILE_SIZE)) - first_frame.fill("red") - super().__init__(pos=pos, surf=first_frame, groups=groups) + # Initialize the player sprite + player_square = frames[0] if isinstance(frames, (list, tuple)) and frames else Surface((TILE_SIZE, TILE_SIZE)) + player_square.fill("red") + super().__init__(pos=pos, surf=player_square, groups=groups) # Animation frames self.frames = frames @@ -36,7 +35,7 @@ def __init__( self.position = pos self.selected: bool = False - self.valid_moves: list = [] # Stores valid moves around the player + self.valid_moves: list = [] # Stores validly move around the player self.prev_tile = None # Inventory system @@ -45,16 +44,20 @@ def __init__( # Input handling self.mouse_have_been_pressed: bool = False - def get_adjacent_tiles(self, grid, blocked_tiles=[]): + def get_adjacent_tiles(self, grid, blocked_tiles=None): """Calculate and return all valid adjacent (neighbor) tiles for the player.""" + if blocked_tiles is None: + blocked_tiles = [] if blocked_tiles is None: blocked_tiles = [] x, y = self.rect.topleft - tile_size = grid.block_size + tile_size = grid.tile_size directions = [ - (0, -tile_size), (0, tile_size), # Up, Down - (-tile_size, 0), (tile_size, 0) # Left, Right + (0, -tile_size), (0, tile_size), # Up, Down + (-tile_size, 0), (tile_size, 0), # Left, Right + (-tile_size, -tile_size), (tile_size, -tile_size), # Diagonal: Top-left, Top-right + (-tile_size, tile_size), (tile_size, tile_size) # Diagonal: Bottom-left, Bottom-right ] self.valid_moves = [ (x + dx, y + dy) @@ -68,30 +71,12 @@ def get_adjacent_tiles(self, grid, blocked_tiles=[]): def input(self, grid) -> None: """Handle player movement using instant tile-based logic""" - # if not pygame.mouse.get_pressed()[0]: - # self.mouse_have_been_pressed = False - # return - # - # # Get the tile coordinates from the grid - # mouse_pos = pygame.mouse.get_pos() - # tile_x, tile_y = grid.get_tile_coordinates(mouse_pos, self) - # - # if (tile_x, tile_y) and (tile_x, tile_y) in self.valid_moves: - # self.rect.topleft = (tile_x, tile_y) # Instantly snap to the tile - # self.prev_tile = (tile_x, tile_y) # Update previous tile - # Reset direction self.direction = Vector2(0, 0) # Get mouse position mouse_pos = pygame.mouse.get_pos() - # get the relative pos of the player from the mouse - # to know on which axis the player will move - delta_x = abs(self.rect.centerx - mouse_pos[0]) - delta_y = abs(self.rect.centery - mouse_pos[1]) - - # Handle mouse input for movement if not pygame.mouse.get_pressed()[0]: self.mouse_have_been_pressed = False return @@ -100,29 +85,17 @@ def input(self, grid) -> None: self.mouse_have_been_pressed = True - # Move on the x-axis or y-axis - if delta_x > delta_y: - if delta_x >= (TILE_SIZE / 2): - if mouse_pos[0] > self.rect.centerx: - self.direction.x = 1 - # if delta_x >= (TILE_SIZE / 2): - # self.direction.x = 1 if mouse_pos[0] > self.rect.centerx else -1 - else: - self.direction.x = -1 - # if delta_y >= (TILE_SIZE / 2): - # self.direction.y = 1 if mouse_pos[1] > self.rect.centery else -1 - else: - if delta_y >= (TILE_SIZE / 2): - if mouse_pos[1] > self.rect.centery: - self.direction.y = 1 - else: - self.direction.y = -1 - - # Update position - self.rect.x += self.direction.x * TILE_SIZE - self.rect.y += self.direction.y * TILE_SIZE + # Calculate the tile coordinates from the grid + tile_x, tile_y = grid.get_tile_coordinates(mouse_pos, self) - # return None + # Check if the clicked tile is a valid move + if (tile_x, tile_y) in self.valid_moves: + # Move the player to the clicked tile + self.rect.topleft = (tile_x, tile_y) + self.prev_tile = (tile_x, tile_y) # Update previous tile + else: + # If not valid, reset the direction + self.direction = Vector2(0, 0) def update(self, dt: float, grid=None) -> None: """Update the player's position and state.""" diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index 8af4079..93ab1f4 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -1,68 +1,140 @@ -from abc import ABC - import pygame from pygame import Surface +import pytmx +from pathfinding.core.grid import Grid +from pathfinding.finder.a_star import AStarFinder +from pathfinding.core.diagonal_movement import DiagonalMovement from src.settings import TILE_SIZE class GridManager: - """Handles grid rendering and interaction.""" - def __init__( - self, - display_surface: pygame.Surface, - tile_size: int = TILE_SIZE, - grid_color: str = "grey", - hover_color: str = "azure4" - ): - self.display_surface: pygame.Surface = display_surface - self.tile_size: int = tile_size - self.grid_color: str = grid_color - self.hover_color: str = hover_color - self.overlay_alpha: int = 50 # Transparency level (0-255) - self.block_size: int = 64 - self.coordinates: dict[tuple[int, int], tuple[int, int]] = {} + def __init__(self, tmx_map: pytmx.TiledMap, tile_size: int = TILE_SIZE): + if tmx_map is None: + raise ValueError("tmx_map cannot be None") + + self.tmx_map = tmx_map + self.tile_size = tile_size + self.width = tmx_map.width # Number of tiles wide + self.height = tmx_map.height # Number of tiles high + self.grid_matrix = self.create_grid_matrix() + self.grid = Grid(matrix=self.grid_matrix) + + # The display surface should be provided by the calling context, e.g., PlayerCamera + self.display_surface: Surface = pygame.display.get_surface() - def draw(self, mouse_pos: tuple[int, int], valid_moves: set[tuple[int, int]] = None) -> None: - - overlay: Surface = pygame.Surface(self.display_surface.get_size(), pygame.SRCALPHA) - overlay.fill((0, 0, 0, 0)) # Fully transparent background - - font = pygame.font.SysFont("Arial", 12) - - for x in range(0, self.display_surface.get_width(), self.block_size): - for y in range(0, self.display_surface.get_height(), self.block_size): - rect = pygame.Rect(x, y, self.block_size, self.block_size) - - # Highlight valid tiles - if valid_moves and (x, y) in valid_moves: - pygame.draw.rect(overlay, (0, 255, 0, 100), rect) - elif rect.collidepoint(mouse_pos): - pygame.draw.rect(overlay, (0, 0, 255, 100), rect) + def create_grid_matrix(self) -> list[list[int]]: + """ + Create a grid matrix from the Tiled map. + Each tile is represented as 0 (walkable) or 1 (non-walkable). + """ + matrix = [[0 for _ in range(self.width)] for _ in range(self.height)] + for layer in self.tmx_map.visible_layers: + if isinstance(layer, pytmx.TiledTileLayer): + if layer.name == 'Sea': + for x, y, gid in layer: + matrix[y][x] = 0 # Walkable + elif layer.name == 'Islands': + for x, y, gid in layer: + matrix[y][x] = 1 # Non-walkable + return matrix - # Draw grid lines - pygame.draw.rect(self.display_surface, self.grid_color, rect, 1) - self.coordinates[(x, y)] = (x // self.block_size, y // self.block_size) + def find_path(self, start: tuple[int, int], end: tuple[int, int]) -> list[list[int]]: + """ + Find a path from start to end using A* algorithm. + """ + start_node = self.grid.node(start[0], start[1]) + end_node = self.grid.node(end[0], end[1]) - # Render the x, y integers - text = font.render(f"({self.coordinates[(x, y)][0]}, {self.coordinates[(x, y)][1]})", True, (0, 0, 0)) - text_rect = text.get_rect(center=rect.center) - self.display_surface.blit(text, text_rect) + finder = AStarFinder(diagonal_movement=DiagonalMovement.always) + path, _ = finder.find_path(start_node, end_node, self.grid) - # Blit the transparent overlay onto the display surface - self.display_surface.blit(overlay, (0, 0)) + return [[node.x, node.y] for node in path] def get_tile_coordinates(self, mouse_pos: tuple[int, int], player: object) -> tuple[int, int]: + """ + Get the tile coordinates based on mouse position. + This is used to determine where the player can move. + """ x, y = mouse_pos - player_x, player_y = player.position - max_distance: int = 6 - # Calculate the distance from the player's position to the mouse position - distance = ((x - player_x) ** 2 + (y - player_y) ** 2) ** 0.5 + return x // self.tile_size * self.tile_size, y // self.tile_size * self.tile_size - return (x // self.block_size * self.block_size, y // self.block_size * self.block_size) if distance > max_distance else None + def draw(self, mouse_pos: tuple[int, int]): + """ + Draw the grid on the screen. + Highlight the tile under the mouse cursor. + """ + for y in range(self.height): + for x in range(self.width): + rect = pygame.Rect(x * self.tile_size, y * self.tile_size, self.tile_size, self.tile_size) + pygame.draw.rect(self.display_surface, (0, 255, 0, 50), rect, 1) # Draw grid lines - def get_coordinates(self, x, y): - """Returns the coordinates of a tile at position (x, y).""" + start = (mouse_pos[0] // self.tile_size, mouse_pos[1] // self.tile_size) + end = (self.width - 1, self.height - 1) + path = self.find_path(start, end) + for x, y in path: + rect = pygame.Rect(x * self.tile_size, y * self.tile_size, self.tile_size, self.tile_size) + pygame.draw.rect(self.display_surface, (0, 255, 0, 50), rect, 2) - return self.coordinates.get((x, y)) \ No newline at end of file +# class GridManager: +# """Handles grid rendering and interaction.""" +# +# def __init__( +# self, +# display_surface: pygame.Surface, +# tile_size: int = TILE_SIZE, +# grid_color: str = "grey", +# hover_color: str = "azure4" +# ): +# self.display_surface: pygame.Surface = display_surface +# self.tile_size: int = tile_size +# self.grid_color: str = grid_color +# self.hover_color: str = hover_color +# self.overlay_alpha: int = 50 # Transparency level (0-255) +# self.block_size: int = 64 +# self.coordinates: dict[tuple[int, int], tuple[int, int]] = {} +# +# def draw(self, mouse_pos: tuple[int, int], valid_moves: set[tuple[int, int]] = None) -> None: +# +# overlay: Surface = pygame.Surface(self.display_surface.get_size(), pygame.SRCALPHA) +# overlay.fill((0, 0, 0, 0)) # Fully transparent background +# +# font = pygame.font.SysFont("Arial", 12) +# +# for x in range(0, self.display_surface.get_width(), self.block_size): +# for y in range(0, self.display_surface.get_height(), self.block_size): +# rect = pygame.Rect(x, y, self.block_size, self.block_size) +# +# # Highlight valid tiles +# if valid_moves and (x, y) in valid_moves: +# pygame.draw.rect(overlay, (0, 255, 0, 100), rect) +# elif rect.collidepoint(mouse_pos): +# pygame.draw.rect(overlay, (0, 0, 255, 100), rect) +# +# # Draw grid lines +# pygame.draw.rect(self.display_surface, self.grid_color, rect, 1) +# self.coordinates[(x, y)] = (x // self.block_size, y // self.block_size) +# +# # Render the x, y integers +# text = font.render(f"({self.coordinates[(x, y)][0]}, {self.coordinates[(x, y)][1]})", True, (0, 0, 0)) +# text_rect = text.get_rect(center=rect.center) +# self.display_surface.blit(text, text_rect) +# +# # Blit the transparent overlay onto the display surface +# self.display_surface.blit(overlay, (0, 0)) +# +# def get_tile_coordinates(self, mouse_pos: tuple[int, int], player: object) -> tuple[int, int]: +# x, y = mouse_pos +# player_x, player_y = player.position +# max_distance: int = 6 +# +# # Calculate the distance from the player's position to the mouse position +# distance = ((x - player_x) ** 2 + (y - player_y) ** 2) ** 0.5 +# +# return (x // self.block_size * self.block_size, y // self.block_size * self.block_size) if distance > max_distance else None +# +# def get_coordinates(self, x, y): +# """Returns the coordinates of a tile at position (x, y).""" +# +# return self.coordinates.get((x, y)) \ No newline at end of file diff --git a/src/states/game_running.py b/src/states/game_running.py index ca76817..b4bdf9e 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -40,14 +40,11 @@ def __init__(self, game_state_manager) -> None: self.player_inventory = Inventory() self.load_inventory_from_json("data/inventory.json") - # Camera group - self.all_sprites = PlayerCamera() - - # Load the TMX map - self.tmx_map = {"map": load_pygame(os.path.join(".", "data", "new_maps", "100x100_map.tmx"))} + # Initialize sprite groups + self.all_sprites = pygame.sprite.Group() # Render the grid - self.grid_manager = GridManager(self.tmx_map["map"], TILE_SIZE) + self.grid_manager = None # Initialize grid_manager as None self.show_grid: bool = True # The start positions will be one of the 4 islands in the corners of the board @@ -58,9 +55,13 @@ def __init__(self, game_state_manager) -> None: self.in_shop = False def setup(self, player_start_pos: str) -> None: - """ - set up the map and player from the tiled file - """ + # Load the TMX map and make it an attribute of the class + self.tmx_map = {"map": load_pygame(os.path.join(".", "data", "new_maps", "100x100_map.tmx"))} + if not self.tmx_map: + raise ValueError("Failed to load the TMX map") + + # Initialize the grid manager + self.grid_manager = GridManager(self.tmx_map["map"], TILE_SIZE) self.world_frames = { "water": import_folder(".", "images", "tilesets", "temporary_water"), @@ -69,7 +70,7 @@ def setup(self, player_start_pos: str) -> None: } # Initialize self.player to None by default - # self.player = None + self.player = None # Sea for x, y, surface in self.tmx_map["map"].get_layer_by_name("Sea").tiles(): @@ -113,7 +114,7 @@ def setup(self, player_start_pos: str) -> None: z=WORLD_LAYERS["bg"], ) - # Enitites + # Entities for obj in self.tmx_map["map"].get_layer_by_name("Ships"): if obj.name == "Player" and obj.properties["pos"] == player_start_pos: self.player = Player( @@ -133,6 +134,13 @@ def setup(self, player_start_pos: str) -> None: z=WORLD_LAYERS["bg"], ) + # Create a new PlayerCamera with all the sprites + sprites = list(self.all_sprites) # Get all sprites from the temporary group + self.all_sprites = PlayerCamera(self.tmx_map["map"], self.player.rect.topleft) + # Add all sprites to the new camera group + for sprite in sprites: + self.all_sprites.add(sprite) + def load_inventory_from_json(self, file_path: str): """Load initial inventory items from JSON file.""" try: From 137f6a8e385f743a1346a598671b816675b7c23b Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sun, 18 May 2025 16:51:10 +0200 Subject: [PATCH 18/38] - Properly transform the mouse position from screen coordinates to world coordinates - Draw the grid and pathfinding visualization correctly aligned with the camera view - Scale the grid and path visualization according to the camera zoom level --- src/sprites/camera/player.py | 4 ++- src/sprites/tiles/grid_manager.py | 50 ++++++++++++++++++++++++++----- src/states/game_running.py | 8 +++++ 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index 335ca91..c2c3b54 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -74,4 +74,6 @@ def draw(self, player_center, show_grid = False): # Draw a transparent grid overlay if toggled if show_grid: - self.grid.draw(pygame.mouse.get_pos()) \ No newline at end of file + self.grid.draw(pygame.mouse.get_pos(), + camera_offset=self.offset, + camera_scale=self.scale) diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index 93ab1f4..b4ae662 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -60,22 +60,58 @@ def get_tile_coordinates(self, mouse_pos: tuple[int, int], player: object) -> tu return x // self.tile_size * self.tile_size, y // self.tile_size * self.tile_size - def draw(self, mouse_pos: tuple[int, int]): + def draw(self, mouse_pos: tuple[int, int], camera_offset=None, camera_scale=None) -> None: """ Draw the grid on the screen. Highlight the tile under the mouse cursor. + + Args: + mouse_pos (tuple[int, int]): The current mouse position. + camera_offset (tuple[int, int], optional): The camera offset from PlayerCamera (Vector2) + camera_scale (float, optional): The camera scale from PlayerCamera (float) """ + + if camera_offset is None: + camera_offset = pygame.math.Vector2() + if camera_scale is None: + camera_scale = 1.0 + + # Convert screen mouse position to grid coordinates + word_mouse_x = (mouse_pos[0] - camera_offset.x) / camera_scale + word_mouse_y = (mouse_pos[1] - camera_offset.y) / camera_scale + + # Draw grid lines for y in range(self.height): for x in range(self.width): - rect = pygame.Rect(x * self.tile_size, y * self.tile_size, self.tile_size, self.tile_size) + # Calculate world position + world_x = x * self.tile_size + world_y = y * self.tile_size + + # Convert to screen coordinates + screen_x = world_x * camera_scale + camera_offset.x + screen_y = world_y * camera_scale + camera_offset.y + + rect = pygame.Rect(screen_x, screen_y, + self.tile_size * camera_scale, + self.tile_size * camera_scale) pygame.draw.rect(self.display_surface, (0, 255, 0, 50), rect, 1) # Draw grid lines - start = (mouse_pos[0] // self.tile_size, mouse_pos[1] // self.tile_size) + # Calculate pathfinding start position using world coordinates + start = ((int(word_mouse_x) // self.tile_size), + (int(word_mouse_y) // self.tile_size)) end = (self.width - 1, self.height - 1) - path = self.find_path(start, end) - for x, y in path: - rect = pygame.Rect(x * self.tile_size, y * self.tile_size, self.tile_size, self.tile_size) - pygame.draw.rect(self.display_surface, (0, 255, 0, 50), rect, 2) + + # Only calculate a path if the start position is within bounds + if 0 <= start[0] < self.width and 0 <= start[1] < self.height: + path = self.find_path(start, end) + for x, y in path: + # Convert path coordinates to screen position + screen_x = x * self.tile_size * camera_scale + camera_offset.x + screen_y = y * self.tile_size * camera_scale + camera_offset.y + rect = pygame.Rect(screen_x, screen_y, + self.tile_size * camera_scale, + self.tile_size * camera_scale) + pygame.draw.rect(self.display_surface, (255, 0, 0, 50), rect, 2) # Draw path tiles # class GridManager: # """Handles grid rendering and interaction.""" diff --git a/src/states/game_running.py b/src/states/game_running.py index b4bdf9e..86d0b29 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -184,6 +184,14 @@ def render(self, screen) -> None: show_grid=self.show_grid ) + # Pass the player's position to the draw method + # if self.player: + # self.grid_manager.draw( + # player_pos=self.player.rect.topleft, + # camera_offset=self.all_sprites.offset, + # camera_scale=self.all_sprites.scale, + # ) + # self.welcome_message = self.font.render("Press 'E' to interact!", True, (100, 100, 100)) # point = self.shop.rect # collide = self.player.rect.colliderect(point) From a5c4f882346083c6179516f620dea033de33210c Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sun, 18 May 2025 17:34:51 +0200 Subject: [PATCH 19/38] - Pass Player Position and Mouse Position: The draw method now passes the player's position (player_center) and the mouse position (mouse_pos) to the grid.draw method. This ensures that the pathfinding algorithm starts from the player's position and updates dynamically based on the mouse click position. - Grid Rendering: The grid.draw method is called with the player's position and the mouse position, allowing the GridManager class to calculate and render the path from the player's position to the mouse click position. --- src/sprites/camera/player.py | 9 ++++++--- src/sprites/entities/player.py | 3 +-- src/sprites/tiles/grid_manager.py | 24 +++++++++++++++-------- src/states/game_running.py | 32 ++++++++----------------------- 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index c2c3b54..c624a7f 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -74,6 +74,9 @@ def draw(self, player_center, show_grid = False): # Draw a transparent grid overlay if toggled if show_grid: - self.grid.draw(pygame.mouse.get_pos(), - camera_offset=self.offset, - camera_scale=self.scale) + mouse_pos = pygame.mouse.get_pos() + self.grid.draw( + player_pos=player_center, + mouse_pos=mouse_pos, + camera_offset=self.offset, + camera_scale=self.scale) diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py index 2f7acf8..4758c59 100644 --- a/src/sprites/entities/player.py +++ b/src/sprites/entities/player.py @@ -49,8 +49,7 @@ def get_adjacent_tiles(self, grid, blocked_tiles=None): if blocked_tiles is None: blocked_tiles = [] - if blocked_tiles is None: - blocked_tiles = [] + x, y = self.rect.topleft tile_size = grid.tile_size directions = [ diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index b4ae662..17aaefe 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -60,12 +60,13 @@ def get_tile_coordinates(self, mouse_pos: tuple[int, int], player: object) -> tu return x // self.tile_size * self.tile_size, y // self.tile_size * self.tile_size - def draw(self, mouse_pos: tuple[int, int], camera_offset=None, camera_scale=None) -> None: + def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_offset=None, camera_scale=None) -> None: """ Draw the grid on the screen. Highlight the tile under the mouse cursor. Args: + player_pos (tuple[int, int]): The current player position. mouse_pos (tuple[int, int]): The current mouse position. camera_offset (tuple[int, int], optional): The camera offset from PlayerCamera (Vector2) camera_scale (float, optional): The camera scale from PlayerCamera (float) @@ -77,8 +78,16 @@ def draw(self, mouse_pos: tuple[int, int], camera_offset=None, camera_scale=None camera_scale = 1.0 # Convert screen mouse position to grid coordinates - word_mouse_x = (mouse_pos[0] - camera_offset.x) / camera_scale - word_mouse_y = (mouse_pos[1] - camera_offset.y) / camera_scale + # word_mouse_x = (mouse_pos[0] - camera_offset.x) / camera_scale + # word_mouse_y = (mouse_pos[1] - camera_offset.y) / camera_scale + + # Convert player position to grid coordinates + player_grid_x = int(player_pos[0] // self.tile_size) + player_grid_y = int(player_pos[1] // self.tile_size) + + # Convert mouse position to world coordinates + mouse_grid_x = int(mouse_pos[0] // self.tile_size) + mouse_grid_y = int(mouse_pos[1] // self.tile_size) # Draw grid lines for y in range(self.height): @@ -94,18 +103,17 @@ def draw(self, mouse_pos: tuple[int, int], camera_offset=None, camera_scale=None rect = pygame.Rect(screen_x, screen_y, self.tile_size * camera_scale, self.tile_size * camera_scale) - pygame.draw.rect(self.display_surface, (0, 255, 0, 50), rect, 1) # Draw grid lines + # pygame.draw.rect(self.display_surface, (0, 255, 0, 50), rect, 1) # Draw grid lines # Calculate pathfinding start position using world coordinates - start = ((int(word_mouse_x) // self.tile_size), - (int(word_mouse_y) // self.tile_size)) - end = (self.width - 1, self.height - 1) + start = (player_grid_x, player_grid_y) + end = (mouse_grid_x, mouse_grid_y) # Only calculate a path if the start position is within bounds if 0 <= start[0] < self.width and 0 <= start[1] < self.height: path = self.find_path(start, end) for x, y in path: - # Convert path coordinates to screen position + # Convert path coordinates to the screen position screen_x = x * self.tile_size * camera_scale + camera_offset.x screen_y = y * self.tile_size * camera_scale + camera_offset.y rect = pygame.Rect(screen_x, screen_y, diff --git a/src/states/game_running.py b/src/states/game_running.py index 86d0b29..388ada4 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -185,29 +185,13 @@ def render(self, screen) -> None: ) # Pass the player's position to the draw method - # if self.player: - # self.grid_manager.draw( - # player_pos=self.player.rect.topleft, - # camera_offset=self.all_sprites.offset, - # camera_scale=self.all_sprites.scale, - # ) - - # self.welcome_message = self.font.render("Press 'E' to interact!", True, (100, 100, 100)) - # point = self.shop.rect - # collide = self.player.rect.colliderect(point) - # if collide: - # screen.blit(self.welcome_message, (155, 155)) - - # keys = pygame.key.get_pressed() - # if collide and keys[pygame.K_e]: - # self.in_shop = True - - # if self.in_shop: - # self.shop_window.fill((0, 0, 0)) - # screen.blit(self.shop_window, (260, 40)) - - # if keys[pygame.K_q]: - # self.in_shop = False - # print("Exiting shop") + if self.player: + mouse_pos = pygame.mouse.get_pos() + self.grid_manager.draw( + player_pos=self.player.rect.topleft, + mouse_pos=mouse_pos, + camera_offset=self.all_sprites.offset, + camera_scale=self.all_sprites.scale, + ) pygame.display.update() From e0578238390b85ee1d35bc6f146777599aee3291 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 19 May 2025 13:59:31 +0200 Subject: [PATCH 20/38] - Fixed player position start on integer position instead of a floating position - Removed old code test_grid.py since it was old code made with different use cases. - Removed deprecated file tile.py, no longer in use, was made to use as a base abstract class to edit tiles, but is no longer needed. - Added an option to see where your next position will be on with the click of the mouse key. - Grid is now always shown for the use case of debugging functionality. --- src/sprites/base.py | 2 +- src/sprites/camera/player.py | 47 ++++++++++++++++++----------- src/sprites/entities/player.py | 2 ++ src/sprites/tiles/grid_manager.py | 18 +++++++++-- src/sprites/tiles/tile.py | 45 ---------------------------- src/states/game_running.py | 12 ++++++-- src/testgrid.py | 50 ------------------------------- 7 files changed, 58 insertions(+), 118 deletions(-) delete mode 100644 src/sprites/tiles/tile.py delete mode 100644 src/testgrid.py diff --git a/src/sprites/base.py b/src/sprites/base.py index c6b866f..a2f8a12 100644 --- a/src/sprites/base.py +++ b/src/sprites/base.py @@ -45,7 +45,7 @@ def __init__( raise ValueError("The `surf` parameter must be a valid pygame.Surface.") self.image = surf - self.rect: FRect = self.image.get_frect(topleft=pos) + self.rect = self.image.get_rect(topleft=pos) self.z = z self.frames = frames or [surf] diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index c624a7f..c598091 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -1,5 +1,6 @@ import pygame # ignore + from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS, TILE_SIZE from src.sprites.camera.group import AllSprites from src.sprites.tiles.grid_manager import GridManager @@ -21,21 +22,42 @@ class PlayerCamera(AllSprites): """ def __init__(self, tmx_map, player_start_pos): - super().__init__() - self.display_surface = pygame.display.get_surface() - if not self.display_surface: - raise ValueError("Display surface is not initialized") + super().__init__() + self.display_surface = pygame.display.get_surface() + if not self.display_surface: + raise ValueError("Display surface is not initialized") + + self.offset = pygame.math.Vector2() + self.scale = 2.0 + self.grid = GridManager(tmx_map, tile_size=TILE_SIZE) + self.player_start_pos = player_start_pos + + # camera offset + self.half_width = self.display_surface.get_size()[0] / 2 + self.half_height = self.display_surface.get_size()[1] / 2 - self.offset = pygame.math.Vector2() - self.scale = 2.0 - self.grid = GridManager(tmx_map, tile_size=TILE_SIZE) - self.player_start_pos = player_start_pos + # camera speed + self.keyboard_speed = 5 + self.mouse_speed = 0.2 + + # camera zoom + self.zoom_scale = 1 + self.internal_surf_size = (2500, 2500) + self.internal_surf = pygame.Surface(self.internal_surf_size, pygame.SRCALPHA) + self.internal_rect = self.internal_surf.get_rect(center=(self.half_width, self.half_height)) + self.internal_size_vector = pygame.math.Vector2(self.internal_surf_size) + self.internal_offset = pygame.math.Vector2() + self.internal_offset.x = self.internal_surf_size[0] / 2 - self.half_width + self.internal_offset.y = self.internal_surf_size[1] / 2 - self.half_height def draw(self, player_center, show_grid = False): # Calculate offsets self.offset.x = -(player_center[0] * self.scale - SCREEN_WIDTH / 2) self.offset.y = -(player_center[1] * self.scale - SCREEN_HEIGHT / 2) + # print(f"Player Center: {player_center}") + # print(f"Camera Offset: {self.offset}") + # Separate sprites into layers background_sprites = [sprite for sprite in self if sprite.z < WORLD_LAYERS["main"]] main_sprites = [sprite for sprite in self if sprite.z == WORLD_LAYERS["main"]] @@ -71,12 +93,3 @@ def draw(self, player_center, show_grid = False): if self.display_surface is None: raise ValueError("self.display_surface cannot be None") self.display_surface.blit(scaled_image, scaled_rect.topleft) - - # Draw a transparent grid overlay if toggled - if show_grid: - mouse_pos = pygame.mouse.get_pos() - self.grid.draw( - player_pos=player_center, - mouse_pos=mouse_pos, - camera_offset=self.offset, - camera_scale=self.scale) diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py index 4758c59..2da95b1 100644 --- a/src/sprites/entities/player.py +++ b/src/sprites/entities/player.py @@ -87,6 +87,8 @@ def input(self, grid) -> None: # Calculate the tile coordinates from the grid tile_x, tile_y = grid.get_tile_coordinates(mouse_pos, self) + print(f"Mouse Position: {mouse_pos}, Tile Coordinates: {(tile_x, tile_y)}") + # Check if the clicked tile is a valid move if (tile_x, tile_y) in self.valid_moves: # Move the player to the clicked tile diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index 17aaefe..be699f9 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -34,7 +34,7 @@ def create_grid_matrix(self) -> list[list[int]]: if layer.name == 'Sea': for x, y, gid in layer: matrix[y][x] = 0 # Walkable - elif layer.name == 'Islands': + elif layer.name == 'Islands' or layer.name == 'Shallow Sea': for x, y, gid in layer: matrix[y][x] = 1 # Non-walkable return matrix @@ -89,7 +89,9 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o mouse_grid_x = int(mouse_pos[0] // self.tile_size) mouse_grid_y = int(mouse_pos[1] // self.tile_size) - # Draw grid lines + # Initialize a font object + font = pygame.font.SysFont(None, 12) # You can adjust the font size as needed + for y in range(self.height): for x in range(self.width): # Calculate world position @@ -103,7 +105,17 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o rect = pygame.Rect(screen_x, screen_y, self.tile_size * camera_scale, self.tile_size * camera_scale) - # pygame.draw.rect(self.display_surface, (0, 255, 0, 50), rect, 1) # Draw grid lines + pygame.draw.rect(self.display_surface, (0, 255, 0, 50), rect, 1) # Draw grid lines + + # Render the x and y coordinates as text + text = font.render(f"{x}, {y}", True, (255, 255, 255)) # White text + + # Calculate the position to draw the text (center of the tile) + text_rect = text.get_rect(center=(screen_x + self.tile_size * camera_scale / 2, + screen_y + self.tile_size * camera_scale / 2)) + + # Draw the text on the screen + self.display_surface.blit(text, text_rect) # Calculate pathfinding start position using world coordinates start = (player_grid_x, player_grid_y) diff --git a/src/sprites/tiles/tile.py b/src/sprites/tiles/tile.py deleted file mode 100644 index 1421869..0000000 --- a/src/sprites/tiles/tile.py +++ /dev/null @@ -1,45 +0,0 @@ -import pygame - -from src.sprites.base import BaseSprite - - -class Tile(BaseSprite): - """Handle tiles for the map""" - - def __init__( - self, - *groups: pygame.sprite.Group, - pos: tuple[float, float], - surf: pygame.Surface, - name: str | None = None, - ) -> None: - r""" - Initialize a Tile instance. - - Args: - *groups (pygame.sprite.Group): One or more sprite groups that this tile will belong to. - pos (Tuple[float, float]): The top-left position where the tile will be placed on the screen. - surf (pygame.Surface): The surface (image) that will be drawn for this tile. - name (Optional[str]): An optional name for the tile. - - Note: - There is a known typing error related to missing type parameters for the generic type 'Group' in pygame. - You may see warnings like: - - src\\base.py:107: error: Missing type parameters for generic type 'Group' [type-arg] - - src\\GUI\\game_manager.py:45: error: Missing type parameters for generic type 'SpriteGroup' [type-arg] - - These errors can be ignored for now, as they are related to type annotations in pygame's codebase. - """ - super().__init__(*groups) - - self.pos = pos - self.surf = surf - self.name = name - - self.image: pygame.Surface = self.surf - self.rect: pygame.FRect = self.image.get_frect(topleft=self.pos) - - def draw(self, display_surface: pygame.Surface, offset: tuple[float, float] = (0, 0)) -> None: - """Could be useful for a camera?""" - offset_rect = self.rect.move(offset) - display_surface.blit(self.image, offset_rect) diff --git a/src/states/game_running.py b/src/states/game_running.py index 388ada4..77decb7 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -70,7 +70,7 @@ def setup(self, player_start_pos: str) -> None: } # Initialize self.player to None by default - self.player = None + # self.player = None # Sea for x, y, surface in self.tmx_map["map"].get_layer_by_name("Sea").tiles(): @@ -117,11 +117,14 @@ def setup(self, player_start_pos: str) -> None: # Entities for obj in self.tmx_map["map"].get_layer_by_name("Ships"): if obj.name == "Player" and obj.properties["pos"] == player_start_pos: + grid_x = int(obj.x / TILE_SIZE) * TILE_SIZE + grid_y = int(obj.y / TILE_SIZE) * TILE_SIZE self.player = Player( - pos=(obj.x, obj.y), + pos=(grid_x, grid_y), frames=self.world_frames["ships"]["player_test_ship"], groups=(self.all_sprites,), ) + # print(f"Player Position: {self.player.rect.topleft}") # Coast for obj in self.tmx_map["map"].get_layer_by_name("Coast"): @@ -194,4 +197,9 @@ def render(self, screen) -> None: camera_scale=self.all_sprites.scale, ) + # Draw the tile coordinates on the screen + tile_x, tile_y = self.grid_manager.get_tile_coordinates(mouse_pos, self.player) + tile_pos = (tile_x, tile_y) + pygame.draw.circle(screen, (0, 255, 0), tile_pos, 5) # Green circle at tile coordinates + pygame.display.update() diff --git a/src/testgrid.py b/src/testgrid.py deleted file mode 100644 index 8617e8e..0000000 --- a/src/testgrid.py +++ /dev/null @@ -1,50 +0,0 @@ -class Player(Entity): - """Move tile-by-tile on a grid""" - - def __init__(self, pos, frames, groups): - super().__init__(pos, frames, groups) - - self.grid_pos = pygame.math.Vector2( - pos[0] // TILE_SIZE, pos[1] // TILE_SIZE - ) # Store player's position in grid coordinates - self.target_grid_pos = self.grid_pos.copy() # Target position on the grid - self.rect.topleft = (self.grid_pos.x * TILE_SIZE, self.grid_pos.y * TILE_SIZE) # Align rect with the grid - self.mouse_have_been_pressed = False - - def snap_to_grid(self): - """Ensure the player's rect is perfectly aligned with the grid""" - self.rect.topleft = (self.grid_pos.x * TILE_SIZE, self.grid_pos.y * TILE_SIZE) - - def input(self): - """Handle mouse input to move the player on the grid""" - if not pygame.mouse.get_pressed()[0]: - self.mouse_have_been_pressed = False - return - - if self.mouse_have_been_pressed: - return - - self.mouse_have_been_pressed = True - - # Get mouse position and convert it to grid coordinates - mouse_pos = pygame.mouse.get_pos() - mouse_grid_pos = pygame.math.Vector2( - mouse_pos[0] // TILE_SIZE, mouse_pos[1] // TILE_SIZE - ) - - # Check if the mouse click is on an adjacent tile - delta = mouse_grid_pos - self.grid_pos - if delta.length() == 1: # Ensure it's exactly one tile away - self.target_grid_pos = mouse_grid_pos - - def update(self): - """Update the player's position""" - if self.grid_pos != self.target_grid_pos: - # Move the player one step closer to the target position - delta = self.target_grid_pos - self.grid_pos - if delta.x != 0: - self.grid_pos.x += delta.x / abs(delta.x) # Move horizontally - elif delta.y != 0: - self.grid_pos.y += delta.y / abs(delta.y) # Move vertically - - self.snap_to_grid() From 88fbc5f1c67e28ddd8660beb69101a0b46e38b76 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 19 May 2025 14:53:37 +0200 Subject: [PATCH 21/38] Optimize grid rendering and add FPS display. - Simplified grid text rendering by caching text surfaces and optimized visible grid calculation based on camera bounds. - Added dynamic FPS display to the game window title. Minor cleanup and improved grid snapping logic in game state initialization. --- src/game_manager.py | 5 +- src/sprites/tiles/grid_manager.py | 101 +++++++----------------------- src/states/game_running.py | 1 + 3 files changed, 28 insertions(+), 79 deletions(-) diff --git a/src/game_manager.py b/src/game_manager.py index f766588..2e46961 100644 --- a/src/game_manager.py +++ b/src/game_manager.py @@ -29,7 +29,7 @@ def __init__(self) -> None: # init pygame pygame.init() self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) - pygame.display.set_caption("PyCeas") + # pygame.display.set_caption("PyCeas") self.clock = pygame.Clock() self.running = True @@ -83,4 +83,5 @@ def run(self) -> None: self.states_stack[-1].render(self.screen) # magic value, use 'a' FPS const in settings or delta time - self.clock.tick(FPS) + self.clock.tick() + pygame.display.set_caption(f"{self.clock.get_fps():.2f} FPS") diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index be699f9..218d097 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -23,6 +23,14 @@ def __init__(self, tmx_map: pytmx.TiledMap, tile_size: int = TILE_SIZE): # The display surface should be provided by the calling context, e.g., PlayerCamera self.display_surface: Surface = pygame.display.get_surface() + self.font = pygame.font.SysFont(None, 12) + + self.coordinate_surfaces = {} + for y in range(self.height): + for x in range(self.width): + text_surface = self.font.render(f"{x}, {y}", True, (255, 255, 255)) + self.coordinate_surfaces[(x, y)] = text_surface.convert_alpha() + def create_grid_matrix(self) -> list[list[int]]: """ Create a grid matrix from the Tiled map. @@ -58,7 +66,7 @@ def get_tile_coordinates(self, mouse_pos: tuple[int, int], player: object) -> tu """ x, y = mouse_pos - return x // self.tile_size * self.tile_size, y // self.tile_size * self.tile_size + return int(x // self.tile_size) * self.tile_size, int(y // self.tile_size) * self.tile_size def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_offset=None, camera_scale=None) -> None: """ @@ -77,10 +85,6 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o if camera_scale is None: camera_scale = 1.0 - # Convert screen mouse position to grid coordinates - # word_mouse_x = (mouse_pos[0] - camera_offset.x) / camera_scale - # word_mouse_y = (mouse_pos[1] - camera_offset.y) / camera_scale - # Convert player position to grid coordinates player_grid_x = int(player_pos[0] // self.tile_size) player_grid_y = int(player_pos[1] // self.tile_size) @@ -89,11 +93,16 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o mouse_grid_x = int(mouse_pos[0] // self.tile_size) mouse_grid_y = int(mouse_pos[1] // self.tile_size) - # Initialize a font object - font = pygame.font.SysFont(None, 12) # You can adjust the font size as needed + # Calculate the visible area based on camera offset and scale + visible_start_x = max(0, int(-camera_offset.x // self.tile_size * camera_scale)) + visible_start_y = max(0, int(-camera_offset.y // self.tile_size * camera_scale)) + visible_end_x = min(self.width, + int((self.display_surface.get_width() - camera_offset.x) // self.tile_size * camera_scale)) + visible_end_y = min(self.height, + int((self.display_surface.get_height() - camera_offset.y) // self.tile_size * camera_scale)) - for y in range(self.height): - for x in range(self.width): + for y in range(visible_start_y, visible_end_y): + for x in range(visible_start_x, visible_end_x): # Calculate world position world_x = x * self.tile_size world_y = y * self.tile_size @@ -107,21 +116,21 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o self.tile_size * camera_scale) pygame.draw.rect(self.display_surface, (0, 255, 0, 50), rect, 1) # Draw grid lines - # Render the x and y coordinates as text - text = font.render(f"{x}, {y}", True, (255, 255, 255)) # White text + # Calculate the position to draw the text (center of the tile) + text_surface = self.coordinate_surfaces[(x, y)] # Calculate the position to draw the text (center of the tile) - text_rect = text.get_rect(center=(screen_x + self.tile_size * camera_scale / 2, + text_rect = text_surface.get_rect(center=(screen_x + self.tile_size * camera_scale / 2, screen_y + self.tile_size * camera_scale / 2)) # Draw the text on the screen - self.display_surface.blit(text, text_rect) + self.display_surface.blit(text_surface, text_rect) # Calculate pathfinding start position using world coordinates start = (player_grid_x, player_grid_y) end = (mouse_grid_x, mouse_grid_y) - # Only calculate a path if the start position is within bounds + # Path finding and drawing a path if 0 <= start[0] < self.width and 0 <= start[1] < self.height: path = self.find_path(start, end) for x, y in path: @@ -131,66 +140,4 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o rect = pygame.Rect(screen_x, screen_y, self.tile_size * camera_scale, self.tile_size * camera_scale) - pygame.draw.rect(self.display_surface, (255, 0, 0, 50), rect, 2) # Draw path tiles - -# class GridManager: -# """Handles grid rendering and interaction.""" -# -# def __init__( -# self, -# display_surface: pygame.Surface, -# tile_size: int = TILE_SIZE, -# grid_color: str = "grey", -# hover_color: str = "azure4" -# ): -# self.display_surface: pygame.Surface = display_surface -# self.tile_size: int = tile_size -# self.grid_color: str = grid_color -# self.hover_color: str = hover_color -# self.overlay_alpha: int = 50 # Transparency level (0-255) -# self.block_size: int = 64 -# self.coordinates: dict[tuple[int, int], tuple[int, int]] = {} -# -# def draw(self, mouse_pos: tuple[int, int], valid_moves: set[tuple[int, int]] = None) -> None: -# -# overlay: Surface = pygame.Surface(self.display_surface.get_size(), pygame.SRCALPHA) -# overlay.fill((0, 0, 0, 0)) # Fully transparent background -# -# font = pygame.font.SysFont("Arial", 12) -# -# for x in range(0, self.display_surface.get_width(), self.block_size): -# for y in range(0, self.display_surface.get_height(), self.block_size): -# rect = pygame.Rect(x, y, self.block_size, self.block_size) -# -# # Highlight valid tiles -# if valid_moves and (x, y) in valid_moves: -# pygame.draw.rect(overlay, (0, 255, 0, 100), rect) -# elif rect.collidepoint(mouse_pos): -# pygame.draw.rect(overlay, (0, 0, 255, 100), rect) -# -# # Draw grid lines -# pygame.draw.rect(self.display_surface, self.grid_color, rect, 1) -# self.coordinates[(x, y)] = (x // self.block_size, y // self.block_size) -# -# # Render the x, y integers -# text = font.render(f"({self.coordinates[(x, y)][0]}, {self.coordinates[(x, y)][1]})", True, (0, 0, 0)) -# text_rect = text.get_rect(center=rect.center) -# self.display_surface.blit(text, text_rect) -# -# # Blit the transparent overlay onto the display surface -# self.display_surface.blit(overlay, (0, 0)) -# -# def get_tile_coordinates(self, mouse_pos: tuple[int, int], player: object) -> tuple[int, int]: -# x, y = mouse_pos -# player_x, player_y = player.position -# max_distance: int = 6 -# -# # Calculate the distance from the player's position to the mouse position -# distance = ((x - player_x) ** 2 + (y - player_y) ** 2) ** 0.5 -# -# return (x // self.block_size * self.block_size, y // self.block_size * self.block_size) if distance > max_distance else None -# -# def get_coordinates(self, x, y): -# """Returns the coordinates of a tile at position (x, y).""" -# -# return self.coordinates.get((x, y)) \ No newline at end of file + pygame.draw.rect(self.display_surface, (255, 0, 0, 50), rect, 2) # Draw path tiles \ No newline at end of file diff --git a/src/states/game_running.py b/src/states/game_running.py index 77decb7..fec621e 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -117,6 +117,7 @@ def setup(self, player_start_pos: str) -> None: # Entities for obj in self.tmx_map["map"].get_layer_by_name("Ships"): if obj.name == "Player" and obj.properties["pos"] == player_start_pos: + # Cast the player position to int and snap to the grid grid_x = int(obj.x / TILE_SIZE) * TILE_SIZE grid_y = int(obj.y / TILE_SIZE) * TILE_SIZE self.player = Player( From 4d5bd6683e6ce5c86d26f1fe5d9694f2fb50434b Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 19 May 2025 21:44:32 +0200 Subject: [PATCH 22/38] Clamp mouse position and pathfinding coordinates to grid boundaries --- src/sprites/tiles/grid_manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index 218d097..b385698 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -89,9 +89,9 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o player_grid_x = int(player_pos[0] // self.tile_size) player_grid_y = int(player_pos[1] // self.tile_size) - # Convert mouse position to world coordinates - mouse_grid_x = int(mouse_pos[0] // self.tile_size) - mouse_grid_y = int(mouse_pos[1] // self.tile_size) + # Clamp mouse position to the grid boundaries + mouse_grid_x = max(0, min(self.width - 1, int(mouse_pos[0] // self.tile_size))) + mouse_grid_y = max(0, min(self.height - 1, int(mouse_pos[1] // self.tile_size))) # Calculate the visible area based on camera offset and scale visible_start_x = max(0, int(-camera_offset.x // self.tile_size * camera_scale)) @@ -126,7 +126,7 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o # Draw the text on the screen self.display_surface.blit(text_surface, text_rect) - # Calculate pathfinding start position using world coordinates + # Clamp pathfinding start and end points to the grid boundaries start = (player_grid_x, player_grid_y) end = (mouse_grid_x, mouse_grid_y) From 332f014228749096b5c620a8b7c8e0892d282cd6 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Tue, 20 May 2025 00:19:28 +0200 Subject: [PATCH 23/38] Implement pathfinding caching and update player movement logic --- src/sprites/entities/player.py | 35 ++++++++++++++++--------------- src/sprites/tiles/grid_manager.py | 23 +++++++++++++------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py index 2da95b1..65302e5 100644 --- a/src/sprites/entities/player.py +++ b/src/sprites/entities/player.py @@ -37,6 +37,7 @@ def __init__( self.selected: bool = False self.valid_moves: list = [] # Stores validly move around the player self.prev_tile = None + self.path = [] # Stores the path to the destination tile # Inventory system self.inventory = Inventory() @@ -44,7 +45,7 @@ def __init__( # Input handling self.mouse_have_been_pressed: bool = False - def get_adjacent_tiles(self, grid, blocked_tiles=None): + def get_neighbor_tiles(self, grid, blocked_tiles=None): """Calculate and return all valid adjacent (neighbor) tiles for the player.""" if blocked_tiles is None: @@ -70,9 +71,6 @@ def get_adjacent_tiles(self, grid, blocked_tiles=None): def input(self, grid) -> None: """Handle player movement using instant tile-based logic""" - # Reset direction - self.direction = Vector2(0, 0) - # Get mouse position mouse_pos = pygame.mouse.get_pos() @@ -86,21 +84,24 @@ def input(self, grid) -> None: # Calculate the tile coordinates from the grid tile_x, tile_y = grid.get_tile_coordinates(mouse_pos, self) - - print(f"Mouse Position: {mouse_pos}, Tile Coordinates: {(tile_x, tile_y)}") - - # Check if the clicked tile is a valid move - if (tile_x, tile_y) in self.valid_moves: - # Move the player to the clicked tile - self.rect.topleft = (tile_x, tile_y) - self.prev_tile = (tile_x, tile_y) # Update previous tile - else: - # If not valid, reset the direction - self.direction = Vector2(0, 0) + player_tile = (self.rect.x // grid.tile_size, self.rect.y // grid.tile_size) + target_tile = (tile_x // grid.tile_size, tile_y // grid.tile_size) + + # Find a path using A* algorithm + path = grid.find_path(player_tile, target_tile) + if path and len(path) > 1: + # Move to the next tile in the path + # self.path = path[1:] (will automatically find the fastest path) + next_tile = path[1] + self.rect.topleft = (next_tile[0] * grid.tile_size, next_tile[1] * grid.tile_size) + self.prev_tile = player_tile def update(self, dt: float, grid=None) -> None: """Update the player's position and state.""" if grid: - self.get_adjacent_tiles(grid) # Update valid moves + self.get_neighbor_tiles(grid) # Update valid moves self.input(grid) # Handle input - self.animate(dt) + if self.path: + next_tile = self.path.pop(0) + self.rect.topleft = (next_tile[0] * grid.tile_size, next_tile[1] * grid.tile_size) + # self.animate(dt) diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index b385698..29ad704 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -19,13 +19,14 @@ def __init__(self, tmx_map: pytmx.TiledMap, tile_size: int = TILE_SIZE): self.height = tmx_map.height # Number of tiles high self.grid_matrix = self.create_grid_matrix() self.grid = Grid(matrix=self.grid_matrix) + self._cached_start = None + self._cached_end = None + self._cached_path = [] - # The display surface should be provided by the calling context, e.g., PlayerCamera self.display_surface: Surface = pygame.display.get_surface() - self.font = pygame.font.SysFont(None, 12) - self.coordinate_surfaces = {} + for y in range(self.height): for x in range(self.width): text_surface = self.font.render(f"{x}, {y}", True, (255, 255, 255)) @@ -51,17 +52,25 @@ def find_path(self, start: tuple[int, int], end: tuple[int, int]) -> list[list[i """ Find a path from start to end using A* algorithm. """ + if start == self._cached_start and end == self._cached_end: + return self._cached_path + + self.grid.cleanup() # Reset the grid state + start_node = self.grid.node(start[0], start[1]) end_node = self.grid.node(end[0], end[1]) finder = AStarFinder(diagonal_movement=DiagonalMovement.always) path, _ = finder.find_path(start_node, end_node, self.grid) - return [[node.x, node.y] for node in path] + self._cached_start = start + self._cached_end = end + self._cached_path = [[node.x, node.y] for node in path] + return self._cached_path def get_tile_coordinates(self, mouse_pos: tuple[int, int], player: object) -> tuple[int, int]: """ - Get the tile coordinates based on mouse position. + Get the tile indices (x, y) based on mouse position. This is used to determine where the player can move. """ x, y = mouse_pos @@ -114,7 +123,7 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o rect = pygame.Rect(screen_x, screen_y, self.tile_size * camera_scale, self.tile_size * camera_scale) - pygame.draw.rect(self.display_surface, (0, 255, 0, 50), rect, 1) # Draw grid lines + pygame.draw.rect(self.display_surface, ("dark grey"), rect, 1) # Draw grid lines # Calculate the position to draw the text (center of the tile) text_surface = self.coordinate_surfaces[(x, y)] @@ -140,4 +149,4 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o rect = pygame.Rect(screen_x, screen_y, self.tile_size * camera_scale, self.tile_size * camera_scale) - pygame.draw.rect(self.display_surface, (255, 0, 0, 50), rect, 2) # Draw path tiles \ No newline at end of file + pygame.draw.rect(self.display_surface, ("green"), rect, 2) # Draw path tiles From c0b12b95bb7214f410993692a7eddfe848583590 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Wed, 21 May 2025 14:46:32 +0200 Subject: [PATCH 24/38] Refactor player movement and grid rendering: use numpy for grid matrix and add visible radius to grid draw method --- requirements.txt | 3 ++- src/sprites/entities/player.py | 12 ++++++------ src/sprites/tiles/grid_manager.py | 27 ++++++++++++++------------- src/states/game_running.py | 18 ++++++++++-------- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/requirements.txt b/requirements.txt index 24ba017..460e7c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pygame-ce>=2.5.0 pytmx>=3.32 -pathfinding~=1.0.17 \ No newline at end of file +pathfinding~=1.0.17 +numpy~=2.2.6 \ No newline at end of file diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py index 65302e5..a03720f 100644 --- a/src/sprites/entities/player.py +++ b/src/sprites/entities/player.py @@ -91,17 +91,17 @@ def input(self, grid) -> None: path = grid.find_path(player_tile, target_tile) if path and len(path) > 1: # Move to the next tile in the path - # self.path = path[1:] (will automatically find the fastest path) - next_tile = path[1] - self.rect.topleft = (next_tile[0] * grid.tile_size, next_tile[1] * grid.tile_size) - self.prev_tile = player_tile + self.path = path[1:] + # next_tile = path[1] + # self.rect.topleft = (next_tile[0] * grid.tile_size, next_tile[1] * grid.tile_size) + # self.prev_tile = player_tile def update(self, dt: float, grid=None) -> None: """Update the player's position and state.""" if grid: - self.get_neighbor_tiles(grid) # Update valid moves + # self.get_neighbor_tiles(grid) # Update valid moves self.input(grid) # Handle input if self.path: next_tile = self.path.pop(0) self.rect.topleft = (next_tile[0] * grid.tile_size, next_tile[1] * grid.tile_size) - # self.animate(dt) + self.animate(dt) diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index 29ad704..2d6f124 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -1,6 +1,7 @@ import pygame from pygame import Surface import pytmx +import numpy as np from pathfinding.core.grid import Grid from pathfinding.finder.a_star import AStarFinder from pathfinding.core.diagonal_movement import DiagonalMovement @@ -32,20 +33,20 @@ def __init__(self, tmx_map: pytmx.TiledMap, tile_size: int = TILE_SIZE): text_surface = self.font.render(f"{x}, {y}", True, (255, 255, 255)) self.coordinate_surfaces[(x, y)] = text_surface.convert_alpha() - def create_grid_matrix(self) -> list[list[int]]: + def create_grid_matrix(self) -> np.ndarray: """ Create a grid matrix from the Tiled map. Each tile is represented as 0 (walkable) or 1 (non-walkable). """ - matrix = [[0 for _ in range(self.width)] for _ in range(self.height)] + matrix = np.zeros((self.height, self.width), dtype=int) # Initialize with zeros (walkable) for layer in self.tmx_map.visible_layers: if isinstance(layer, pytmx.TiledTileLayer): if layer.name == 'Sea': for x, y, gid in layer: - matrix[y][x] = 0 # Walkable + matrix[y, x] = 0 # Walkable elif layer.name == 'Islands' or layer.name == 'Shallow Sea': for x, y, gid in layer: - matrix[y][x] = 1 # Non-walkable + matrix[y, x] = 1 # Non-walkable return matrix def find_path(self, start: tuple[int, int], end: tuple[int, int]) -> list[list[int]]: @@ -77,7 +78,8 @@ def get_tile_coordinates(self, mouse_pos: tuple[int, int], player: object) -> tu return int(x // self.tile_size) * self.tile_size, int(y // self.tile_size) * self.tile_size - def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_offset=None, camera_scale=None) -> None: + def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_offset=None, camera_scale=None, + visible_radius: int = 5) -> None: """ Draw the grid on the screen. Highlight the tile under the mouse cursor. @@ -87,6 +89,7 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o mouse_pos (tuple[int, int]): The current mouse position. camera_offset (tuple[int, int], optional): The camera offset from PlayerCamera (Vector2) camera_scale (float, optional): The camera scale from PlayerCamera (float) + visible_radius (int, optional): The radius of the visible area around the player. """ if camera_offset is None: @@ -103,12 +106,10 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o mouse_grid_y = max(0, min(self.height - 1, int(mouse_pos[1] // self.tile_size))) # Calculate the visible area based on camera offset and scale - visible_start_x = max(0, int(-camera_offset.x // self.tile_size * camera_scale)) - visible_start_y = max(0, int(-camera_offset.y // self.tile_size * camera_scale)) - visible_end_x = min(self.width, - int((self.display_surface.get_width() - camera_offset.x) // self.tile_size * camera_scale)) - visible_end_y = min(self.height, - int((self.display_surface.get_height() - camera_offset.y) // self.tile_size * camera_scale)) + visible_start_x = max(0, player_grid_x - visible_radius) + visible_start_y = max(0, player_grid_y - visible_radius) + visible_end_x = min(self.width, player_grid_x + visible_radius + 1) + visible_end_y = min(self.height, player_grid_y + visible_radius + 1) for y in range(visible_start_y, visible_end_y): for x in range(visible_start_x, visible_end_x): @@ -123,7 +124,7 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o rect = pygame.Rect(screen_x, screen_y, self.tile_size * camera_scale, self.tile_size * camera_scale) - pygame.draw.rect(self.display_surface, ("dark grey"), rect, 1) # Draw grid lines + pygame.draw.rect(self.display_surface, "dark grey", rect, 1) # Draw grid lines # Calculate the position to draw the text (center of the tile) text_surface = self.coordinate_surfaces[(x, y)] @@ -149,4 +150,4 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o rect = pygame.Rect(screen_x, screen_y, self.tile_size * camera_scale, self.tile_size * camera_scale) - pygame.draw.rect(self.display_surface, ("green"), rect, 2) # Draw path tiles + pygame.draw.rect(self.display_surface, "green", rect, 2) # Draw path tiles diff --git a/src/states/game_running.py b/src/states/game_running.py index fec621e..f13fd71 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -173,12 +173,12 @@ def update(self, events) -> None: if event.type == pygame.KEYDOWN: if event.key == pygame.K_i: # Toggle inventory with "I" key self.game_state_manager.enter_state(Paused(self.game_state_manager, self.player_inventory)) + elif event.key == pygame.K_g: # Toggle grid with "G" key + self.show_grid = not self.show_grid elif collide and event.key == pygame.K_e: self.game_state_manager.enter_state( WindowShop(self.game_state_manager, self.player, self.shop, self.player_inventory) ) - elif event.key == pygame.K_g: # Toggle grid with "G" key - self.show_grid = not self.show_grid def render(self, screen) -> None: """draw sprites to the canvas""" @@ -191,12 +191,14 @@ def render(self, screen) -> None: # Pass the player's position to the draw method if self.player: mouse_pos = pygame.mouse.get_pos() - self.grid_manager.draw( - player_pos=self.player.rect.topleft, - mouse_pos=mouse_pos, - camera_offset=self.all_sprites.offset, - camera_scale=self.all_sprites.scale, - ) + if self.show_grid: + self.grid_manager.draw( + player_pos=self.player.rect.topleft, + mouse_pos=mouse_pos, + camera_offset=self.all_sprites.offset, + camera_scale=self.all_sprites.scale, + visible_radius=5, + ) # Draw the tile coordinates on the screen tile_x, tile_y = self.grid_manager.get_tile_coordinates(mouse_pos, self.player) From b064fdb87269b3c5c20098def5e40e0523709014 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Wed, 21 May 2025 17:30:52 +0200 Subject: [PATCH 25/38] Fix player input and grid coordinate calculations: incorporate camera offset and scale in movement logic --- src/sprites/entities/player.py | 17 ++++--------- src/sprites/tiles/grid_manager.py | 42 +++++++++++++++++++++++-------- src/states/game_running.py | 25 +++++++++++++----- 3 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py index a03720f..d490de7 100644 --- a/src/sprites/entities/player.py +++ b/src/sprites/entities/player.py @@ -67,40 +67,33 @@ def get_neighbor_tiles(self, grid, blocked_tiles=None): and (x + dx, y + dy) not in blocked_tiles ] - - def input(self, grid) -> None: + def input(self, grid, camera_offset=None, camera_scale=None) -> None: """Handle player movement using instant tile-based logic""" # Get mouse position mouse_pos = pygame.mouse.get_pos() - if not pygame.mouse.get_pressed()[0]: self.mouse_have_been_pressed = False return if self.mouse_have_been_pressed: return - self.mouse_have_been_pressed = True - # Calculate the tile coordinates from the grid - tile_x, tile_y = grid.get_tile_coordinates(mouse_pos, self) + # Calculate the tile coordinates from the grid with camera offset and scale player_tile = (self.rect.x // grid.tile_size, self.rect.y // grid.tile_size) - target_tile = (tile_x // grid.tile_size, tile_y // grid.tile_size) + target_tile = grid.get_tile_coordinates(mouse_pos, camera_offset, camera_scale) # Find a path using A* algorithm path = grid.find_path(player_tile, target_tile) if path and len(path) > 1: # Move to the next tile in the path self.path = path[1:] - # next_tile = path[1] - # self.rect.topleft = (next_tile[0] * grid.tile_size, next_tile[1] * grid.tile_size) - # self.prev_tile = player_tile - def update(self, dt: float, grid=None) -> None: + def update(self, dt: float, grid=None, camera_offset=None, camera_scale=None) -> None: """Update the player's position and state.""" if grid: # self.get_neighbor_tiles(grid) # Update valid moves - self.input(grid) # Handle input + self.input(grid, camera_offset, camera_scale) # Handle input with camera offset and scale if self.path: next_tile = self.path.pop(0) self.rect.topleft = (next_tile[0] * grid.tile_size, next_tile[1] * grid.tile_size) diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index 2d6f124..0b18aa6 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -69,14 +69,30 @@ def find_path(self, start: tuple[int, int], end: tuple[int, int]) -> list[list[i self._cached_path = [[node.x, node.y] for node in path] return self._cached_path - def get_tile_coordinates(self, mouse_pos: tuple[int, int], player: object) -> tuple[int, int]: + def get_tile_coordinates(self, mouse_pos: tuple[int, int], camera_offset: pygame.math.Vector2 = None, + camera_scale: float = None) -> tuple[int, int]: """ Get the tile indices (x, y) based on mouse position. This is used to determine where the player can move. """ - x, y = mouse_pos + # if camera_offset is None: + # camera_offset = pygame.math.Vector2() # Initialize to zero vector if None + # if camera_scale is None: + # camera_scale = 2.0 - return int(x // self.tile_size) * self.tile_size, int(y // self.tile_size) * self.tile_size + # Reverse the camera transformations to get world coordinates + world_x = (mouse_pos[0] - camera_offset.x) / camera_scale + world_y = (mouse_pos[1] - camera_offset.y) / camera_scale + + # Convert world coordinates to grid coordinates + grid_x = int(world_x // self.tile_size) + grid_y = int(world_y // self.tile_size) + + # Clamp grid coordinates to the grid boundaries + grid_x = max(0, min(self.width - 1, grid_x)) + grid_y = max(0, min(self.height - 1, grid_y)) + + return grid_x, grid_y def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_offset=None, camera_scale=None, visible_radius: int = 5) -> None: @@ -94,16 +110,15 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o if camera_offset is None: camera_offset = pygame.math.Vector2() - if camera_scale is None: - camera_scale = 1.0 + # if camera_scale is None: + # camera_scale = 2.0 # Convert player position to grid coordinates player_grid_x = int(player_pos[0] // self.tile_size) player_grid_y = int(player_pos[1] // self.tile_size) - # Clamp mouse position to the grid boundaries - mouse_grid_x = max(0, min(self.width - 1, int(mouse_pos[0] // self.tile_size))) - mouse_grid_y = max(0, min(self.height - 1, int(mouse_pos[1] // self.tile_size))) + # Get mouse grid coordinates with camera offset and scale + mouse_grid_x, mouse_grid_y = self.get_tile_coordinates(mouse_pos, camera_offset, camera_scale) # Calculate the visible area based on camera offset and scale visible_start_x = max(0, player_grid_x - visible_radius) @@ -131,7 +146,7 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o # Calculate the position to draw the text (center of the tile) text_rect = text_surface.get_rect(center=(screen_x + self.tile_size * camera_scale / 2, - screen_y + self.tile_size * camera_scale / 2)) + screen_y + self.tile_size * camera_scale / 2)) # Draw the text on the screen self.display_surface.blit(text_surface, text_rect) @@ -148,6 +163,11 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o screen_x = x * self.tile_size * camera_scale + camera_offset.x screen_y = y * self.tile_size * camera_scale + camera_offset.y rect = pygame.Rect(screen_x, screen_y, - self.tile_size * camera_scale, - self.tile_size * camera_scale) + self.tile_size * camera_scale, + self.tile_size * camera_scale) pygame.draw.rect(self.display_surface, "green", rect, 2) # Draw path tiles + + # Draw the green dot at the mouse grid coordinates + dot_x = mouse_grid_x * self.tile_size * camera_scale + camera_offset.x + dot_y = mouse_grid_y * self.tile_size * camera_scale + camera_offset.y + pygame.draw.circle(self.display_surface, (0, 255, 0), (dot_x, dot_y), 5) # Green circle at tile coordinates diff --git a/src/states/game_running.py b/src/states/game_running.py index f13fd71..5a8e27d 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -166,7 +166,11 @@ def update(self, events) -> None: self.all_sprites.update(dt) # Handle player movement and grid snapping - self.player.update(dt, grid=self.grid_manager) + self.player.update(dt, + grid=self.grid_manager, + camera_offset=self.all_sprites.offset, + camera_scale=self.all_sprites.scale + ) # get events like keypress or mouse clicks for event in events: @@ -181,7 +185,7 @@ def update(self, events) -> None: ) def render(self, screen) -> None: - """draw sprites to the canvas""" + """Draw sprites to the canvas.""" screen.fill("#000000") self.all_sprites.draw( self.player.rect.center, @@ -200,9 +204,18 @@ def render(self, screen) -> None: visible_radius=5, ) - # Draw the tile coordinates on the screen - tile_x, tile_y = self.grid_manager.get_tile_coordinates(mouse_pos, self.player) - tile_pos = (tile_x, tile_y) - pygame.draw.circle(screen, (0, 255, 0), tile_pos, 5) # Green circle at tile coordinates + # Get tile coordinates with camera offset and scale + tile_x, tile_y = self.grid_manager.get_tile_coordinates( + mouse_pos, + self.all_sprites.offset, + self.all_sprites.scale + ) + + # Convert grid coordinates to screen coordinates + dot_x = tile_x * TILE_SIZE * self.all_sprites.scale + self.all_sprites.offset.x + dot_y = tile_y * TILE_SIZE * self.all_sprites.scale + self.all_sprites.offset.y + + # Draw the green dot at the screen coordinates + pygame.draw.circle(screen, (0, 255, 0), (dot_x, dot_y), 5) # Green circle at tile coordinates pygame.display.update() From 559f1824229cff18d26295aa6d7cb8fe649dd620 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Wed, 21 May 2025 18:15:14 +0200 Subject: [PATCH 26/38] Refactor player and grid methods: comment out unused code and update function signatures to include camera parameters --- src/sprites/entities/player.py | 57 ++++++++++++++++--------------- src/sprites/tiles/grid_manager.py | 23 +++++++------ src/states/game_running.py | 2 +- 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py index d490de7..50efc2e 100644 --- a/src/sprites/entities/player.py +++ b/src/sprites/entities/player.py @@ -34,9 +34,9 @@ def __init__( self.frame_index: float = 0.0 self.position = pos - self.selected: bool = False - self.valid_moves: list = [] # Stores validly move around the player - self.prev_tile = None + # self.selected: bool = False + # self.valid_moves: list = [] # Stores validly move around the player + # self.prev_tile = None self.path = [] # Stores the path to the destination tile # Inventory system @@ -45,29 +45,28 @@ def __init__( # Input handling self.mouse_have_been_pressed: bool = False - def get_neighbor_tiles(self, grid, blocked_tiles=None): - """Calculate and return all valid adjacent (neighbor) tiles for the player.""" - - if blocked_tiles is None: - blocked_tiles = [] - - x, y = self.rect.topleft - tile_size = grid.tile_size - directions = [ - (0, -tile_size), (0, tile_size), # Up, Down - (-tile_size, 0), (tile_size, 0), # Left, Right - (-tile_size, -tile_size), (tile_size, -tile_size), # Diagonal: Top-left, Top-right - (-tile_size, tile_size), (tile_size, tile_size) # Diagonal: Bottom-left, Bottom-right - ] - self.valid_moves = [ - (x + dx, y + dy) - for dx, dy in directions - if 0 <= x + dx < SCREEN_WIDTH - and 0 <= y + dy < SCREEN_HEIGHT - and (x + dx, y + dy) not in blocked_tiles - ] - - def input(self, grid, camera_offset=None, camera_scale=None) -> None: + # def get_neighbor_tiles(self, grid, blocked_tiles=None): + # """Calculate and return all valid adjacent (neighbor) tiles for the player.""" + # if blocked_tiles is None: + # blocked_tiles = [] + # + # x, y = self.rect.topleft + # tile_size = grid.tile_size + # directions = [ + # (0, -tile_size), (0, tile_size), # Up, Down + # (-tile_size, 0), (tile_size, 0), # Left, Right + # (-tile_size, -tile_size), (tile_size, -tile_size), # Diagonal: Top-left, Top-right + # (-tile_size, tile_size), (tile_size, tile_size) # Diagonal: Bottom-left, Bottom-right + # ] + # self.valid_moves = [ + # (x + dx, y + dy) + # for dx, dy in directions + # if 0 <= x + dx < SCREEN_WIDTH + # and 0 <= y + dy < SCREEN_HEIGHT + # and (x + dx, y + dy) not in blocked_tiles + # ] + + def input(self, grid, camera_offset: pygame.math.Vector2 = None, camera_scale: float = None) -> None: """Handle player movement using instant tile-based logic""" # Get mouse position @@ -89,10 +88,12 @@ def input(self, grid, camera_offset=None, camera_scale=None) -> None: # Move to the next tile in the path self.path = path[1:] - def update(self, dt: float, grid=None, camera_offset=None, camera_scale=None) -> None: + def update(self, dt: float, grid=None, camera_offset: pygame.math.Vector2 = None, + camera_scale: float = None) -> None: """Update the player's position and state.""" if grid: - # self.get_neighbor_tiles(grid) # Update valid moves + # this method is not used, could be useful when implementing a player switching system + # self.get_neighbor_tiles(grid) self.input(grid, camera_offset, camera_scale) # Handle input with camera offset and scale if self.path: next_tile = self.path.pop(0) diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index 0b18aa6..b65b705 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -52,6 +52,10 @@ def create_grid_matrix(self) -> np.ndarray: def find_path(self, start: tuple[int, int], end: tuple[int, int]) -> list[list[int]]: """ Find a path from start to end using A* algorithm. + + Args: + start (tuple[int, int]): The starting tile coordinates (x, y). + end (tuple[int, int]): The ending tile coordinates (x, y). """ if start == self._cached_start and end == self._cached_end: return self._cached_path @@ -74,11 +78,12 @@ def get_tile_coordinates(self, mouse_pos: tuple[int, int], camera_offset: pygame """ Get the tile indices (x, y) based on mouse position. This is used to determine where the player can move. + + Args: + mouse_pos (tuple[int, int]): The current mouse position. + camera_offset (tuple[int, int], optional): The camera offset from PlayerCamera (Vector2) + camera_scale (float, optional): The camera scale from PlayerCamera (float) """ - # if camera_offset is None: - # camera_offset = pygame.math.Vector2() # Initialize to zero vector if None - # if camera_scale is None: - # camera_scale = 2.0 # Reverse the camera transformations to get world coordinates world_x = (mouse_pos[0] - camera_offset.x) / camera_scale @@ -94,8 +99,9 @@ def get_tile_coordinates(self, mouse_pos: tuple[int, int], camera_offset: pygame return grid_x, grid_y - def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_offset=None, camera_scale=None, - visible_radius: int = 5) -> None: + def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_offset: pygame.math.Vector2 = None, + camera_scale: float = None, + visible_radius: int = None) -> None: """ Draw the grid on the screen. Highlight the tile under the mouse cursor. @@ -108,11 +114,6 @@ def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_o visible_radius (int, optional): The radius of the visible area around the player. """ - if camera_offset is None: - camera_offset = pygame.math.Vector2() - # if camera_scale is None: - # camera_scale = 2.0 - # Convert player position to grid coordinates player_grid_x = int(player_pos[0] // self.tile_size) player_grid_y = int(player_pos[1] // self.tile_size) diff --git a/src/states/game_running.py b/src/states/game_running.py index 5a8e27d..8cf0f66 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -120,12 +120,12 @@ def setup(self, player_start_pos: str) -> None: # Cast the player position to int and snap to the grid grid_x = int(obj.x / TILE_SIZE) * TILE_SIZE grid_y = int(obj.y / TILE_SIZE) * TILE_SIZE + # print(f"Player Position: ({grid_x, grid_y})") self.player = Player( pos=(grid_x, grid_y), frames=self.world_frames["ships"]["player_test_ship"], groups=(self.all_sprites,), ) - # print(f"Player Position: {self.player.rect.topleft}") # Coast for obj in self.tmx_map["map"].get_layer_by_name("Coast"): From fc318945538666a8e65e9d92928bcaef7e1800bf Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Thu, 22 May 2025 00:04:56 +0200 Subject: [PATCH 27/38] Minor refactor player and grid methods: comment out unused camera parameters and adjust draw method signature --- src/settings.py | 2 +- src/sprites/camera/player.py | 24 ++++++++++++------------ src/sprites/tiles/grid_manager.py | 5 ++++- src/states/game_running.py | 3 --- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/settings.py b/src/settings.py index 34f9b4f..bcca3d4 100644 --- a/src/settings.py +++ b/src/settings.py @@ -9,7 +9,7 @@ ANIMATION_SPEED = 4 WORLD_LAYERS = {"water": 0, "bg": 1, "main": 2, "top": 3} -FPS = 60 +FPS = 30 # For some imports like pygame.freetype, Mypy can't infer the type of this attribute, so we suppress the error. diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index c598091..e6cd191 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -33,22 +33,22 @@ def __init__(self, tmx_map, player_start_pos): self.player_start_pos = player_start_pos # camera offset - self.half_width = self.display_surface.get_size()[0] / 2 - self.half_height = self.display_surface.get_size()[1] / 2 + # self.half_width = self.display_surface.get_size()[0] / 2 + # self.half_height = self.display_surface.get_size()[1] / 2 # camera speed - self.keyboard_speed = 5 - self.mouse_speed = 0.2 + # self.keyboard_speed = 5 + # self.mouse_speed = 0.2 # camera zoom - self.zoom_scale = 1 - self.internal_surf_size = (2500, 2500) - self.internal_surf = pygame.Surface(self.internal_surf_size, pygame.SRCALPHA) - self.internal_rect = self.internal_surf.get_rect(center=(self.half_width, self.half_height)) - self.internal_size_vector = pygame.math.Vector2(self.internal_surf_size) - self.internal_offset = pygame.math.Vector2() - self.internal_offset.x = self.internal_surf_size[0] / 2 - self.half_width - self.internal_offset.y = self.internal_surf_size[1] / 2 - self.half_height + # self.zoom_scale = 1 + # self.internal_surf_size = (2500, 2500) + # self.internal_surf = pygame.Surface(self.internal_surf_size, pygame.SRCALPHA) + # self.internal_rect = self.internal_surf.get_rect(center=(self.half_width, self.half_height)) + # self.internal_size_vector = pygame.math.Vector2(self.internal_surf_size) + # self.internal_offset = pygame.math.Vector2() + # self.internal_offset.x = self.internal_surf_size[0] / 2 - self.half_width + # self.internal_offset.y = self.internal_surf_size[1] / 2 - self.half_height def draw(self, player_center, show_grid = False): # Calculate offsets diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index b65b705..cfdd088 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -99,7 +99,10 @@ def get_tile_coordinates(self, mouse_pos: tuple[int, int], camera_offset: pygame return grid_x, grid_y - def draw(self, player_pos: tuple[int, int], mouse_pos: tuple[int, int], camera_offset: pygame.math.Vector2 = None, + def draw(self, + player_pos: tuple[int, int], + mouse_pos: tuple[int, int], + camera_offset: pygame.math.Vector2 = None, camera_scale: float = None, visible_radius: int = None) -> None: """ diff --git a/src/states/game_running.py b/src/states/game_running.py index 8cf0f66..ea3cf50 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -69,9 +69,6 @@ def setup(self, player_start_pos: str) -> None: "ships": all_character_import(".", "images", "tilesets", "ships"), } - # Initialize self.player to None by default - # self.player = None - # Sea for x, y, surface in self.tmx_map["map"].get_layer_by_name("Sea").tiles(): BaseSprite( From 49a441a3919a706d4797798f8d0ee947e9992eb1 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sat, 24 May 2025 16:36:13 +0200 Subject: [PATCH 28/38] Refactor grid rendering logic into smaller private methods. Simplified and modularized the grid rendering code by introducing private helper methods for better readability and maintainability. These methods handle coordinate conversions, grid drawing, pathfinding visualization, and mouse indicator rendering. --- src/sprites/tiles/grid_manager.py | 140 +++++++++++++++++++++++------- 1 file changed, 108 insertions(+), 32 deletions(-) diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index cfdd088..3bdd3f9 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -26,12 +26,15 @@ def __init__(self, tmx_map: pytmx.TiledMap, tile_size: int = TILE_SIZE): self.display_surface: Surface = pygame.display.get_surface() self.font = pygame.font.SysFont(None, 12) - self.coordinate_surfaces = {} + self.coordinate_surfaces = self._preload_coordinates_surfaces() + def _preload_coordinates_surfaces(self): + coordinate_surfaces = {} for y in range(self.height): for x in range(self.width): text_surface = self.font.render(f"{x}, {y}", True, (255, 255, 255)) - self.coordinate_surfaces[(x, y)] = text_surface.convert_alpha() + coordinate_surfaces[(x, y)] = text_surface.convert_alpha() + return coordinate_surfaces def create_grid_matrix(self) -> np.ndarray: """ @@ -84,19 +87,28 @@ def get_tile_coordinates(self, mouse_pos: tuple[int, int], camera_offset: pygame camera_offset (tuple[int, int], optional): The camera offset from PlayerCamera (Vector2) camera_scale (float, optional): The camera scale from PlayerCamera (float) """ - - # Reverse the camera transformations to get world coordinates + world_x, world_y = self._convert_mouse_to_world(mouse_pos, camera_offset, camera_scale) + grid_x, grid_y = self._convert_world_to_grid(world_x, world_y) + return self._clamp_grid_coordinates(grid_x, grid_y) + + @staticmethod + def _convert_mouse_to_world(mouse_pos: tuple[int, int], camera_offset: pygame.math.Vector2, + camera_scale: float) -> tuple[float, float]: + # Adjust the mouse position to world coordinates by reversing the camera's position and scale world_x = (mouse_pos[0] - camera_offset.x) / camera_scale world_y = (mouse_pos[1] - camera_offset.y) / camera_scale + return world_x, world_y + def _convert_world_to_grid(self, world_x: float, world_y: float) -> tuple[int, int]: # Convert world coordinates to grid coordinates grid_x = int(world_x // self.tile_size) grid_y = int(world_y // self.tile_size) + return grid_x, grid_y + def _clamp_grid_coordinates(self, grid_x: int, grid_y: int) -> tuple[int, int]: # Clamp grid coordinates to the grid boundaries grid_x = max(0, min(self.width - 1, grid_x)) grid_y = max(0, min(self.height - 1, grid_y)) - return grid_x, grid_y def draw(self, @@ -116,62 +128,126 @@ def draw(self, camera_scale (float, optional): The camera scale from PlayerCamera (float) visible_radius (int, optional): The radius of the visible area around the player. """ + player_grid_x, player_grid_y = self._convert_world_to_grid(*player_pos) + mouse_grid_x, mouse_grid_y = self.get_tile_coordinates(mouse_pos, camera_offset, camera_scale) - # Convert player position to grid coordinates - player_grid_x = int(player_pos[0] // self.tile_size) - player_grid_y = int(player_pos[1] // self.tile_size) + visible_start_x, visible_start_y, visible_end_x, visible_end_y = self._calculate_visible_area( + player_grid_x, player_grid_y, visible_radius) - # Get mouse grid coordinates with camera offset and scale - mouse_grid_x, mouse_grid_y = self.get_tile_coordinates(mouse_pos, camera_offset, camera_scale) + self._draw_grid_lines(visible_start_x, visible_start_y, visible_end_x, visible_end_y, + camera_offset, camera_scale) + + self._draw_path(player_grid_x, player_grid_y, mouse_grid_x, mouse_grid_y, + camera_offset, camera_scale) + + self._draw_mouse_indicator(mouse_grid_x, mouse_grid_y, camera_offset, camera_scale) - # Calculate the visible area based on camera offset and scale + def _calculate_visible_area(self, player_grid_x: int, player_grid_y: int, + visible_radius: int) -> tuple[int, int, int, int]: visible_start_x = max(0, player_grid_x - visible_radius) visible_start_y = max(0, player_grid_y - visible_radius) visible_end_x = min(self.width, player_grid_x + visible_radius + 1) visible_end_y = min(self.height, player_grid_y + visible_radius + 1) + return visible_start_x, visible_start_y, visible_end_x, visible_end_y + def _draw_grid_lines(self, visible_start_x: int, visible_start_y: int, visible_end_x: int, visible_end_y: int, + camera_offset: pygame.math.Vector2, camera_scale: float) -> None: for y in range(visible_start_y, visible_end_y): for x in range(visible_start_x, visible_end_x): - # Calculate world position - world_x = x * self.tile_size - world_y = y * self.tile_size - - # Convert to screen coordinates - screen_x = world_x * camera_scale + camera_offset.x - screen_y = world_y * camera_scale + camera_offset.y - + screen_x, screen_y = self._convert_to_screen_coordinates(x, y, camera_offset, camera_scale) rect = pygame.Rect(screen_x, screen_y, self.tile_size * camera_scale, self.tile_size * camera_scale) pygame.draw.rect(self.display_surface, "dark grey", rect, 1) # Draw grid lines - # Calculate the position to draw the text (center of the tile) text_surface = self.coordinate_surfaces[(x, y)] - - # Calculate the position to draw the text (center of the tile) text_rect = text_surface.get_rect(center=(screen_x + self.tile_size * camera_scale / 2, screen_y + self.tile_size * camera_scale / 2)) - - # Draw the text on the screen self.display_surface.blit(text_surface, text_rect) - # Clamp pathfinding start and end points to the grid boundaries - start = (player_grid_x, player_grid_y) - end = (mouse_grid_x, mouse_grid_y) + def _convert_to_screen_coordinates(self, x: int, y: int, camera_offset: pygame.math.Vector2, + camera_scale: float) -> tuple[float, float]: + world_x = x * self.tile_size + world_y = y * self.tile_size + screen_x = world_x * camera_scale + camera_offset.x + screen_y = world_y * camera_scale + camera_offset.y + return screen_x, screen_y + + def _draw_path(self, start_x: int, start_y: int, end_x: int, end_y: int, + camera_offset: pygame.math.Vector2, camera_scale: float) -> None: + start = (start_x, start_y) + end = (end_x, end_y) - # Path finding and drawing a path if 0 <= start[0] < self.width and 0 <= start[1] < self.height: path = self.find_path(start, end) for x, y in path: - # Convert path coordinates to the screen position - screen_x = x * self.tile_size * camera_scale + camera_offset.x - screen_y = y * self.tile_size * camera_scale + camera_offset.y + screen_x, screen_y = self._convert_to_screen_coordinates(x, y, camera_offset, camera_scale) rect = pygame.Rect(screen_x, screen_y, self.tile_size * camera_scale, self.tile_size * camera_scale) pygame.draw.rect(self.display_surface, "green", rect, 2) # Draw path tiles - # Draw the green dot at the mouse grid coordinates + def _draw_mouse_indicator(self, mouse_grid_x: int, mouse_grid_y: int, + camera_offset: pygame.math.Vector2, camera_scale: float) -> None: dot_x = mouse_grid_x * self.tile_size * camera_scale + camera_offset.x dot_y = mouse_grid_y * self.tile_size * camera_scale + camera_offset.y pygame.draw.circle(self.display_surface, (0, 255, 0), (dot_x, dot_y), 5) # Green circle at tile coordinates + + # # Convert player position to grid coordinates + # player_grid_x = int(player_pos[0] // self.tile_size) + # player_grid_y = int(player_pos[1] // self.tile_size) + # + # # Get mouse grid coordinates with camera offset and scale + # mouse_grid_x, mouse_grid_y = self.get_tile_coordinates(mouse_pos, camera_offset, camera_scale) + # + # # Calculate the visible area based on camera offset and scale + # visible_start_x = max(0, player_grid_x - visible_radius) + # visible_start_y = max(0, player_grid_y - visible_radius) + # visible_end_x = min(self.width, player_grid_x + visible_radius + 1) + # visible_end_y = min(self.height, player_grid_y + visible_radius + 1) + # + # for y in range(visible_start_y, visible_end_y): + # for x in range(visible_start_x, visible_end_x): + # # Calculate world position + # world_x = x * self.tile_size + # world_y = y * self.tile_size + # + # # Convert to screen coordinates + # screen_x = world_x * camera_scale + camera_offset.x + # screen_y = world_y * camera_scale + camera_offset.y + # + # rect = pygame.Rect(screen_x, screen_y, + # self.tile_size * camera_scale, + # self.tile_size * camera_scale) + # pygame.draw.rect(self.display_surface, "dark grey", rect, 1) # Draw grid lines + # + # # Calculate the position to draw the text (center of the tile) + # text_surface = self.coordinate_surfaces[(x, y)] + # + # # Calculate the position to draw the text (center of the tile) + # text_rect = text_surface.get_rect(center=(screen_x + self.tile_size * camera_scale / 2, + # screen_y + self.tile_size * camera_scale / 2)) + # + # # Draw the text on the screen + # self.display_surface.blit(text_surface, text_rect) + # + # # Clamp pathfinding start and end points to the grid boundaries + # start = (player_grid_x, player_grid_y) + # end = (mouse_grid_x, mouse_grid_y) + # + # # Path finding and drawing a path + # if 0 <= start[0] < self.width and 0 <= start[1] < self.height: + # path = self.find_path(start, end) + # for x, y in path: + # # Convert path coordinates to the screen position + # screen_x = x * self.tile_size * camera_scale + camera_offset.x + # screen_y = y * self.tile_size * camera_scale + camera_offset.y + # rect = pygame.Rect(screen_x, screen_y, + # self.tile_size * camera_scale, + # self.tile_size * camera_scale) + # pygame.draw.rect(self.display_surface, "green", rect, 2) # Draw path tiles + # + # # Draw the green dot at the mouse grid coordinates + # dot_x = mouse_grid_x * self.tile_size * camera_scale + camera_offset.x + # dot_y = mouse_grid_y * self.tile_size * camera_scale + camera_offset.y + # pygame.draw.circle(self.display_surface, (0, 255, 0), (dot_x, dot_y), 5) # Green circle at tile coordinates From 25e5c1aa270dc68e90d1c4984ce08b2ad136cae6 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sat, 24 May 2025 22:08:38 +0200 Subject: [PATCH 29/38] Refactor PlayerCamera import and remove obsolete code. Renamed the import path for `PlayerCamera` to reflect the updated file name and removed outdated commented code in the player camera class to improve code clarity and maintainability. No functional changes were made. --- .../camera/{player.py => player_camera.py} | 18 ------------------ src/states/game_running.py | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) rename src/sprites/camera/{player.py => player_camera.py} (79%) diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player_camera.py similarity index 79% rename from src/sprites/camera/player.py rename to src/sprites/camera/player_camera.py index e6cd191..baa5e61 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player_camera.py @@ -32,24 +32,6 @@ def __init__(self, tmx_map, player_start_pos): self.grid = GridManager(tmx_map, tile_size=TILE_SIZE) self.player_start_pos = player_start_pos - # camera offset - # self.half_width = self.display_surface.get_size()[0] / 2 - # self.half_height = self.display_surface.get_size()[1] / 2 - - # camera speed - # self.keyboard_speed = 5 - # self.mouse_speed = 0.2 - - # camera zoom - # self.zoom_scale = 1 - # self.internal_surf_size = (2500, 2500) - # self.internal_surf = pygame.Surface(self.internal_surf_size, pygame.SRCALPHA) - # self.internal_rect = self.internal_surf.get_rect(center=(self.half_width, self.half_height)) - # self.internal_size_vector = pygame.math.Vector2(self.internal_surf_size) - # self.internal_offset = pygame.math.Vector2() - # self.internal_offset.x = self.internal_surf_size[0] / 2 - self.half_width - # self.internal_offset.y = self.internal_surf_size[1] / 2 - self.half_height - def draw(self, player_center, show_grid = False): # Calculate offsets self.offset.x = -(player_center[0] * self.scale - SCREEN_WIDTH / 2) diff --git a/src/states/game_running.py b/src/states/game_running.py index ea3cf50..33a5b41 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -12,7 +12,7 @@ from src.settings import TILE_SIZE, WORLD_LAYERS from src.sprites.animations import AnimatedSprites from src.sprites.base import BaseSprite -from src.sprites.camera.player import PlayerCamera +from src.sprites.camera.player_camera import PlayerCamera from src.sprites.entities.player import Player from src.sprites.tiles.grid_manager import GridManager from src.states.base_state import BaseState From 397c64cb12ac6b44ce73ff36e985b408510c5996 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sat, 24 May 2025 22:42:28 +0200 Subject: [PATCH 30/38] Add PathFinder class and refactor pathfinding logic Introduced a new `PathFinder` class to encapsulate A* pathfinding functionality. Refactored `grid_manager.py` to use `PathFinder`, improving code structure and removing redundancy. Removed unused pathfinding-related code from `player.py` and streamlined logic for calculating paths. --- src/sprites/entities/player.py | 5 +-- src/sprites/tiles/grid_manager.py | 63 ++++++++++++++++--------------- src/sprites/tiles/pathfinding.py | 40 ++++++++++++++++++++ 3 files changed, 74 insertions(+), 34 deletions(-) create mode 100644 src/sprites/tiles/pathfinding.py diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py index 50efc2e..7d19a64 100644 --- a/src/sprites/entities/player.py +++ b/src/sprites/entities/player.py @@ -8,7 +8,7 @@ class Player(BaseSprite): - """Handles player interaction with the grid, using instant tile movement.""" + """Handles player interaction with the grid, moving with the pathfinding.""" rect: FRect def __init__( @@ -34,9 +34,6 @@ def __init__( self.frame_index: float = 0.0 self.position = pos - # self.selected: bool = False - # self.valid_moves: list = [] # Stores validly move around the player - # self.prev_tile = None self.path = [] # Stores the path to the destination tile # Inventory system diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index 3bdd3f9..3587093 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -2,9 +2,7 @@ from pygame import Surface import pytmx import numpy as np -from pathfinding.core.grid import Grid -from pathfinding.finder.a_star import AStarFinder -from pathfinding.core.diagonal_movement import DiagonalMovement +from src.sprites.tiles.pathfinding import PathFinder from src.settings import TILE_SIZE @@ -19,10 +17,11 @@ def __init__(self, tmx_map: pytmx.TiledMap, tile_size: int = TILE_SIZE): self.width = tmx_map.width # Number of tiles wide self.height = tmx_map.height # Number of tiles high self.grid_matrix = self.create_grid_matrix() - self.grid = Grid(matrix=self.grid_matrix) - self._cached_start = None - self._cached_end = None - self._cached_path = [] + self.path_finder = PathFinder(self.grid_matrix) + # self.grid = Grid(matrix=self.grid_matrix) + # self._cached_start = None + # self._cached_end = None + # self._cached_path = [] self.display_surface: Surface = pygame.display.get_surface() self.font = pygame.font.SysFont(None, 12) @@ -52,29 +51,33 @@ def create_grid_matrix(self) -> np.ndarray: matrix[y, x] = 1 # Non-walkable return matrix + # Not the best way to do this, but it works for now def find_path(self, start: tuple[int, int], end: tuple[int, int]) -> list[list[int]]: - """ - Find a path from start to end using A* algorithm. - - Args: - start (tuple[int, int]): The starting tile coordinates (x, y). - end (tuple[int, int]): The ending tile coordinates (x, y). - """ - if start == self._cached_start and end == self._cached_end: - return self._cached_path - - self.grid.cleanup() # Reset the grid state - - start_node = self.grid.node(start[0], start[1]) - end_node = self.grid.node(end[0], end[1]) - - finder = AStarFinder(diagonal_movement=DiagonalMovement.always) - path, _ = finder.find_path(start_node, end_node, self.grid) - - self._cached_start = start - self._cached_end = end - self._cached_path = [[node.x, node.y] for node in path] - return self._cached_path + return self.path_finder.find_path(start, end) + + # def find_path(self, start: tuple[int, int], end: tuple[int, int]) -> list[list[int]]: + # """ + # Find a path from start to end using A* algorithm. + # + # Args: + # start (tuple[int, int]): The starting tile coordinates (x, y). + # end (tuple[int, int]): The ending tile coordinates (x, y). + # """ + # if start == self._cached_start and end == self._cached_end: + # return self._cached_path + # + # self.grid.cleanup() # Reset the grid state + # + # start_node = self.grid.node(start[0], start[1]) + # end_node = self.grid.node(end[0], end[1]) + # + # finder = AStarFinder(diagonal_movement=DiagonalMovement.always) + # path, _ = finder.find_path(start_node, end_node, self.grid) + # + # self._cached_start = start + # self._cached_end = end + # self._cached_path = [[node.x, node.y] for node in path] + # return self._cached_path def get_tile_coordinates(self, mouse_pos: tuple[int, int], camera_offset: pygame.math.Vector2 = None, camera_scale: float = None) -> tuple[int, int]: @@ -179,7 +182,7 @@ def _draw_path(self, start_x: int, start_y: int, end_x: int, end_y: int, end = (end_x, end_y) if 0 <= start[0] < self.width and 0 <= start[1] < self.height: - path = self.find_path(start, end) + path = self.path_finder.find_path(start, end) for x, y in path: screen_x, screen_y = self._convert_to_screen_coordinates(x, y, camera_offset, camera_scale) rect = pygame.Rect(screen_x, screen_y, diff --git a/src/sprites/tiles/pathfinding.py b/src/sprites/tiles/pathfinding.py new file mode 100644 index 0000000..c58b50a --- /dev/null +++ b/src/sprites/tiles/pathfinding.py @@ -0,0 +1,40 @@ +from pathfinding.core.grid import Grid +from pathfinding.finder.a_star import AStarFinder +from pathfinding.core.diagonal_movement import DiagonalMovement + + +class PathFinder: + def __init__(self, grid_matrix): + """ + Initialize the PathFinder with a grid matrix. + + :param grid_matrix: A 2D list representing the grid where 0 is walkable and 1 is blocked. + """ + self.grid = Grid(matrix=grid_matrix) + self._cached_start = None + self._cached_end = None + self._cached_path = None + + def find_path(self, start: tuple[int, int], end: tuple[int, int]) -> list[list[int]]: + """ + Find a path from start to end using A* algorithm. + + Args: + start (tuple[int, int]): The starting tile coordinates (x, y). + end (tuple[int, int]): The ending tile coordinates (x, y). + """ + if start == self._cached_start and end == self._cached_end: + return self._cached_path + + self.grid.cleanup() # Reset the grid state + + start_node = self.grid.node(start[0], start[1]) + end_node = self.grid.node(end[0], end[1]) + + finder = AStarFinder(diagonal_movement=DiagonalMovement.always) + path, _ = finder.find_path(start_node, end_node, self.grid) + + self._cached_start = start + self._cached_end = end + self._cached_path = [[node.x, node.y] for node in path] + return self._cached_path From b06a9891560cb6b9b87d199061445e0bdb84fb70 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sat, 24 May 2025 23:25:42 +0200 Subject: [PATCH 31/38] Refactor pathfinding logic: improved pathfinding for the use case to properly test the logic of pathfinding. Also removed old code from grid_manager.py related pathfinding.py logic. --- src/sprites/tiles/grid_manager.py | 28 ----------- src/sprites/tiles/pathfinding.py | 77 +++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index 3587093..01c4316 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -18,10 +18,6 @@ def __init__(self, tmx_map: pytmx.TiledMap, tile_size: int = TILE_SIZE): self.height = tmx_map.height # Number of tiles high self.grid_matrix = self.create_grid_matrix() self.path_finder = PathFinder(self.grid_matrix) - # self.grid = Grid(matrix=self.grid_matrix) - # self._cached_start = None - # self._cached_end = None - # self._cached_path = [] self.display_surface: Surface = pygame.display.get_surface() self.font = pygame.font.SysFont(None, 12) @@ -55,30 +51,6 @@ def create_grid_matrix(self) -> np.ndarray: def find_path(self, start: tuple[int, int], end: tuple[int, int]) -> list[list[int]]: return self.path_finder.find_path(start, end) - # def find_path(self, start: tuple[int, int], end: tuple[int, int]) -> list[list[int]]: - # """ - # Find a path from start to end using A* algorithm. - # - # Args: - # start (tuple[int, int]): The starting tile coordinates (x, y). - # end (tuple[int, int]): The ending tile coordinates (x, y). - # """ - # if start == self._cached_start and end == self._cached_end: - # return self._cached_path - # - # self.grid.cleanup() # Reset the grid state - # - # start_node = self.grid.node(start[0], start[1]) - # end_node = self.grid.node(end[0], end[1]) - # - # finder = AStarFinder(diagonal_movement=DiagonalMovement.always) - # path, _ = finder.find_path(start_node, end_node, self.grid) - # - # self._cached_start = start - # self._cached_end = end - # self._cached_path = [[node.x, node.y] for node in path] - # return self._cached_path - def get_tile_coordinates(self, mouse_pos: tuple[int, int], camera_offset: pygame.math.Vector2 = None, camera_scale: float = None) -> tuple[int, int]: """ diff --git a/src/sprites/tiles/pathfinding.py b/src/sprites/tiles/pathfinding.py index c58b50a..1d833d6 100644 --- a/src/sprites/tiles/pathfinding.py +++ b/src/sprites/tiles/pathfinding.py @@ -1,9 +1,53 @@ +from dataclasses import dataclass + from pathfinding.core.grid import Grid from pathfinding.finder.a_star import AStarFinder from pathfinding.core.diagonal_movement import DiagonalMovement +@dataclass(frozen=True) +class Coordinate: + """A simple data class to represent a coordinate in the grid.""" + x: int + y: int + + +class PathCache: + def __init__(self) -> None: + """ + Storing the start and end coordinates along with the path to avoid recalculating paths in a cached manner. + """ + self.start: Coordinate | None = None + self.end: Coordinate | None = None + self.path: list[Coordinate] | None = None + + def get_cached_path(self, start: Coordinate, end: Coordinate) -> list[Coordinate] | None: + """ + Retrieve the cached path if the start and end coordinates match. + + :param start: The starting coordinate. + :param end: The ending coordinate. + :return: The cached path or None if not found. + """ + if self.start == start and self.end == end: + return self.path + return None + + def update_cache(self, start: Coordinate, end: Coordinate, path: list[Coordinate]) -> None: + """ + Update the cache with a new path. + + :param start: The starting coordinate. + :param end: The ending coordinate. + :param path: The path to cache. + """ + self.start = start + self.end = end + self.path = path + class PathFinder: + MOVEMENT_TYPE = DiagonalMovement.always + def __init__(self, grid_matrix): """ Initialize the PathFinder with a grid matrix. @@ -11,9 +55,7 @@ def __init__(self, grid_matrix): :param grid_matrix: A 2D list representing the grid where 0 is walkable and 1 is blocked. """ self.grid = Grid(matrix=grid_matrix) - self._cached_start = None - self._cached_end = None - self._cached_path = None + self._cache = PathCache() def find_path(self, start: tuple[int, int], end: tuple[int, int]) -> list[list[int]]: """ @@ -23,18 +65,27 @@ def find_path(self, start: tuple[int, int], end: tuple[int, int]) -> list[list[i start (tuple[int, int]): The starting tile coordinates (x, y). end (tuple[int, int]): The ending tile coordinates (x, y). """ - if start == self._cached_start and end == self._cached_end: - return self._cached_path + + start_coord = Coordinate(start[0], start[1]) + end_coord = Coordinate(end[0], end[1]) + + cached_path = self._cache.get_cached_path(start_coord, end_coord) + if cached_path: + return [[coord.x, coord.y] for coord in cached_path] self.grid.cleanup() # Reset the grid state + path = self._calculate_path(start_coord, end_coord) - start_node = self.grid.node(start[0], start[1]) - end_node = self.grid.node(end[0], end[1]) + path_coordinates = [Coordinate(node.x, node.y) for node in path] + self._cache.update_cache(start_coord, end_coord, path_coordinates) - finder = AStarFinder(diagonal_movement=DiagonalMovement.always) - path, _ = finder.find_path(start_node, end_node, self.grid) + return [[coord.x, coord.y] for coord in path_coordinates] - self._cached_start = start - self._cached_end = end - self._cached_path = [[node.x, node.y] for node in path] - return self._cached_path + def _calculate_path(self, start: Coordinate, end: Coordinate) -> list: + """Calculate the path using A* algorithm.""" + + start_node = self.grid.node(start.x, start.y) + end_node = self.grid.node(end.x, end.y) + finder = AStarFinder(diagonal_movement=self.MOVEMENT_TYPE) + path, _ = finder.find_path(start_node, end_node, self.grid) + return path From 4b9cd8086e70554fcc4e256e0488cf807f9d970c Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 26 May 2025 14:33:34 +0200 Subject: [PATCH 32/38] Refactor imports and improve type hinting across multiple files --- src/game_manager.py | 2 +- src/sprites/animations.py | 4 +- src/sprites/base.py | 4 +- src/sprites/camera/group.py | 4 +- src/sprites/camera/player_camera.py | 14 +++- src/sprites/entities/player.py | 20 +++-- src/sprites/gui/inventory_gui.py | 1 - src/sprites/tiles/grid_manager.py | 117 ++++++++++++++++++---------- src/sprites/tiles/pathfinding.py | 8 +- src/states/game_running.py | 79 ++++++++++--------- 10 files changed, 152 insertions(+), 101 deletions(-) diff --git a/src/game_manager.py b/src/game_manager.py index 2e46961..c8b6904 100644 --- a/src/game_manager.py +++ b/src/game_manager.py @@ -7,7 +7,7 @@ import pygame # type: ignore -from src.settings import FPS, SCREEN_HEIGHT, SCREEN_WIDTH +from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH # import base state for typehint from src.states.base_state import BaseState diff --git a/src/sprites/animations.py b/src/sprites/animations.py index f6c7509..3d154c7 100644 --- a/src/sprites/animations.py +++ b/src/sprites/animations.py @@ -1,5 +1,5 @@ -import pygame -from pygame.sprite import Group +import pygame # type: ignore +from pygame.sprite import Group # type: ignore from src.settings import ANIMATION_SPEED, WORLD_LAYERS from src.sprites.base import BaseSprite diff --git a/src/sprites/base.py b/src/sprites/base.py index a2f8a12..5e1131c 100644 --- a/src/sprites/base.py +++ b/src/sprites/base.py @@ -2,8 +2,8 @@ from abc import ABC -from pygame import FRect, Surface, Vector2 -from pygame.sprite import Group, Sprite +from pygame import Surface, Vector2 # type: ignore +from pygame.sprite import Group, Sprite # type: ignore from src.settings import ANIMATION_SPEED, WORLD_LAYERS diff --git a/src/sprites/camera/group.py b/src/sprites/camera/group.py index eec8377..4fcb453 100644 --- a/src/sprites/camera/group.py +++ b/src/sprites/camera/group.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod -import pygame -from pygame.sprite import Group +import pygame # type: ignore +from pygame.sprite import Group # type: ignore # class CameraGroup(pygame.sprite.Group, ABC): diff --git a/src/sprites/camera/player_camera.py b/src/sprites/camera/player_camera.py index baa5e61..30dcb2a 100644 --- a/src/sprites/camera/player_camera.py +++ b/src/sprites/camera/player_camera.py @@ -1,7 +1,6 @@ import pygame # ignore - -from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS, TILE_SIZE +from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, TILE_SIZE, WORLD_LAYERS from src.sprites.camera.group import AllSprites from src.sprites.tiles.grid_manager import GridManager @@ -21,18 +20,25 @@ class PlayerCamera(AllSprites): scale (float): The scaling factor for rendering sprites. """ - def __init__(self, tmx_map, player_start_pos): + def __init__(self, tmx_map=None, player_start_pos=None): super().__init__() self.display_surface = pygame.display.get_surface() if not self.display_surface: raise ValueError("Display surface is not initialized") + if tmx_map is None: + raise ValueError("TMX map cannot be None") + if player_start_pos is None: + raise ValueError("Player start position cannot be None") + + self.player_start_pos = player_start_pos + self.tmx_map = tmx_map self.offset = pygame.math.Vector2() self.scale = 2.0 self.grid = GridManager(tmx_map, tile_size=TILE_SIZE) self.player_start_pos = player_start_pos - def draw(self, player_center, show_grid = False): + def draw(self, player_center, show_grid=False): # Calculate offsets self.offset.x = -(player_center[0] * self.scale - SCREEN_WIDTH / 2) self.offset.y = -(player_center[1] * self.scale - SCREEN_HEIGHT / 2) diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py index 7d19a64..0052be0 100644 --- a/src/sprites/entities/player.py +++ b/src/sprites/entities/player.py @@ -1,14 +1,15 @@ -import pygame -from pygame import Surface, Vector2, FRect -from pygame.sprite import Group +import pygame # type: ignore +from pygame import FRect, Surface # type: ignore +from pygame.sprite import Group # type: ignore from src.inventory import Inventory -from src.settings import TILE_SIZE, SCREEN_WIDTH, SCREEN_HEIGHT +from src.settings import TILE_SIZE from src.sprites.base import BaseSprite class Player(BaseSprite): """Handles player interaction with the grid, moving with the pathfinding.""" + rect: FRect def __init__( @@ -34,7 +35,8 @@ def __init__( self.frame_index: float = 0.0 self.position = pos - self.path = [] # Stores the path to the destination tile + # In your __init__ method + self.path: list[tuple[int, int]] = [] # Stores the path to the destination tile # Inventory system self.inventory = Inventory() @@ -63,7 +65,7 @@ def __init__( # and (x + dx, y + dy) not in blocked_tiles # ] - def input(self, grid, camera_offset: pygame.math.Vector2 = None, camera_scale: float = None) -> None: + def input(self, grid, camera_offset: pygame.math.Vector2 | None = None, camera_scale: float | None = None) -> None: """Handle player movement using instant tile-based logic""" # Get mouse position @@ -85,8 +87,10 @@ def input(self, grid, camera_offset: pygame.math.Vector2 = None, camera_scale: f # Move to the next tile in the path self.path = path[1:] - def update(self, dt: float, grid=None, camera_offset: pygame.math.Vector2 = None, - camera_scale: float = None) -> None: + def update( + self, dt: float, grid=None, camera_offset: pygame.math.Vector2 | None = None, + camera_scale: float | None = None + ) -> None: """Update the player's position and state.""" if grid: # this method is not used, could be useful when implementing a player switching system diff --git a/src/sprites/gui/inventory_gui.py b/src/sprites/gui/inventory_gui.py index b929768..47e8bcc 100644 --- a/src/sprites/gui/inventory_gui.py +++ b/src/sprites/gui/inventory_gui.py @@ -117,7 +117,6 @@ def draw(self) -> None: # Reset button actions self.button_actions: dict[str, tuple[pygame.Rect, pygame.Rect]] = {} - # Draw the inventory items items: list = list(self.inventory.get_items().items()) visible_items: list = items[self.scroll_offset : self.scroll_offset + self.max_visible_items] diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index 01c4316..0ded48b 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -1,13 +1,13 @@ -import pygame -from pygame import Surface -import pytmx import numpy as np -from src.sprites.tiles.pathfinding import PathFinder +import pygame # type: ignore +import pytmx +from pygame import Surface # type: ignore from src.settings import TILE_SIZE +from src.sprites.tiles.pathfinding import PathFinder -class GridManager: +class GridManager: def __init__(self, tmx_map: pytmx.TiledMap, tile_size: int = TILE_SIZE): if tmx_map is None: raise ValueError("tmx_map cannot be None") @@ -19,7 +19,7 @@ def __init__(self, tmx_map: pytmx.TiledMap, tile_size: int = TILE_SIZE): self.grid_matrix = self.create_grid_matrix() self.path_finder = PathFinder(self.grid_matrix) - self.display_surface: Surface = pygame.display.get_surface() + self.display_surface: Surface | None = pygame.display.get_surface() self.font = pygame.font.SysFont(None, 12) self.coordinate_surfaces = self._preload_coordinates_surfaces() @@ -39,10 +39,10 @@ def create_grid_matrix(self) -> np.ndarray: matrix = np.zeros((self.height, self.width), dtype=int) # Initialize with zeros (walkable) for layer in self.tmx_map.visible_layers: if isinstance(layer, pytmx.TiledTileLayer): - if layer.name == 'Sea': + if layer.name == "Sea": for x, y, gid in layer: matrix[y, x] = 0 # Walkable - elif layer.name == 'Islands' or layer.name == 'Shallow Sea': + elif layer.name == "Islands" or layer.name == "Shallow Sea": for x, y, gid in layer: matrix[y, x] = 1 # Non-walkable return matrix @@ -51,8 +51,12 @@ def create_grid_matrix(self) -> np.ndarray: def find_path(self, start: tuple[int, int], end: tuple[int, int]) -> list[list[int]]: return self.path_finder.find_path(start, end) - def get_tile_coordinates(self, mouse_pos: tuple[int, int], camera_offset: pygame.math.Vector2 = None, - camera_scale: float = None) -> tuple[int, int]: + def get_tile_coordinates( + self, + mouse_pos: tuple[int, int], + camera_offset: pygame.math.Vector2 | None = None, + camera_scale: float | None = None, + ) -> tuple[int, int]: """ Get the tile indices (x, y) based on mouse position. This is used to determine where the player can move. @@ -62,13 +66,18 @@ def get_tile_coordinates(self, mouse_pos: tuple[int, int], camera_offset: pygame camera_offset (tuple[int, int], optional): The camera offset from PlayerCamera (Vector2) camera_scale (float, optional): The camera scale from PlayerCamera (float) """ + if camera_offset is None: + camera_offset = pygame.math.Vector2() + if camera_scale is None: + camera_scale = 1.0 world_x, world_y = self._convert_mouse_to_world(mouse_pos, camera_offset, camera_scale) grid_x, grid_y = self._convert_world_to_grid(world_x, world_y) return self._clamp_grid_coordinates(grid_x, grid_y) @staticmethod - def _convert_mouse_to_world(mouse_pos: tuple[int, int], camera_offset: pygame.math.Vector2, - camera_scale: float) -> tuple[float, float]: + def _convert_mouse_to_world( + mouse_pos: tuple[int, int], camera_offset: pygame.math.Vector2, camera_scale: float + ) -> tuple[float, float]: # Adjust the mouse position to world coordinates by reversing the camera's position and scale world_x = (mouse_pos[0] - camera_offset.x) / camera_scale world_y = (mouse_pos[1] - camera_offset.y) / camera_scale @@ -86,12 +95,14 @@ def _clamp_grid_coordinates(self, grid_x: int, grid_y: int) -> tuple[int, int]: grid_y = max(0, min(self.height - 1, grid_y)) return grid_x, grid_y - def draw(self, - player_pos: tuple[int, int], - mouse_pos: tuple[int, int], - camera_offset: pygame.math.Vector2 = None, - camera_scale: float = None, - visible_radius: int = None) -> None: + def draw( + self, + player_pos: tuple[int, int], + mouse_pos: tuple[int, int], + camera_offset: pygame.math.Vector2 | None = None, + camera_scale: float | None = None, + visible_radius: int | None = None, + ) -> None: """ Draw the grid on the screen. Highlight the tile under the mouse cursor. @@ -103,67 +114,91 @@ def draw(self, camera_scale (float, optional): The camera scale from PlayerCamera (float) visible_radius (int, optional): The radius of the visible area around the player. """ + if camera_offset is None: + camera_offset = pygame.math.Vector2() + if camera_scale is None: + camera_scale = 1.0 + if visible_radius is None: + visible_radius = 5 player_grid_x, player_grid_y = self._convert_world_to_grid(*player_pos) mouse_grid_x, mouse_grid_y = self.get_tile_coordinates(mouse_pos, camera_offset, camera_scale) visible_start_x, visible_start_y, visible_end_x, visible_end_y = self._calculate_visible_area( - player_grid_x, player_grid_y, visible_radius) + player_grid_x, player_grid_y, visible_radius + ) - self._draw_grid_lines(visible_start_x, visible_start_y, visible_end_x, visible_end_y, - camera_offset, camera_scale) + self._draw_grid_lines( + visible_start_x, visible_start_y, visible_end_x, visible_end_y, camera_offset, camera_scale + ) - self._draw_path(player_grid_x, player_grid_y, mouse_grid_x, mouse_grid_y, - camera_offset, camera_scale) + self._draw_path(player_grid_x, player_grid_y, mouse_grid_x, mouse_grid_y, camera_offset, camera_scale) self._draw_mouse_indicator(mouse_grid_x, mouse_grid_y, camera_offset, camera_scale) - def _calculate_visible_area(self, player_grid_x: int, player_grid_y: int, - visible_radius: int) -> tuple[int, int, int, int]: + def _calculate_visible_area( + self, player_grid_x: int, player_grid_y: int, visible_radius: int + ) -> tuple[int, int, int, int]: visible_start_x = max(0, player_grid_x - visible_radius) visible_start_y = max(0, player_grid_y - visible_radius) visible_end_x = min(self.width, player_grid_x + visible_radius + 1) visible_end_y = min(self.height, player_grid_y + visible_radius + 1) return visible_start_x, visible_start_y, visible_end_x, visible_end_y - def _draw_grid_lines(self, visible_start_x: int, visible_start_y: int, visible_end_x: int, visible_end_y: int, - camera_offset: pygame.math.Vector2, camera_scale: float) -> None: + def _draw_grid_lines( + self, + visible_start_x: int, + visible_start_y: int, + visible_end_x: int, + visible_end_y: int, + camera_offset: pygame.math.Vector2, + camera_scale: float, + ) -> None: for y in range(visible_start_y, visible_end_y): for x in range(visible_start_x, visible_end_x): + assert self.display_surface is not None, "Display surface must be initialized" screen_x, screen_y = self._convert_to_screen_coordinates(x, y, camera_offset, camera_scale) - rect = pygame.Rect(screen_x, screen_y, - self.tile_size * camera_scale, - self.tile_size * camera_scale) + rect = pygame.Rect(screen_x, screen_y, self.tile_size * camera_scale, self.tile_size * camera_scale) pygame.draw.rect(self.display_surface, "dark grey", rect, 1) # Draw grid lines text_surface = self.coordinate_surfaces[(x, y)] - text_rect = text_surface.get_rect(center=(screen_x + self.tile_size * camera_scale / 2, - screen_y + self.tile_size * camera_scale / 2)) + text_rect = text_surface.get_rect( + center=(screen_x + self.tile_size * camera_scale / 2, screen_y + self.tile_size * camera_scale / 2) + ) self.display_surface.blit(text_surface, text_rect) - def _convert_to_screen_coordinates(self, x: int, y: int, camera_offset: pygame.math.Vector2, - camera_scale: float) -> tuple[float, float]: + def _convert_to_screen_coordinates( + self, x: int, y: int, camera_offset: pygame.math.Vector2, camera_scale: float + ) -> tuple[float, float]: world_x = x * self.tile_size world_y = y * self.tile_size screen_x = world_x * camera_scale + camera_offset.x screen_y = world_y * camera_scale + camera_offset.y return screen_x, screen_y - def _draw_path(self, start_x: int, start_y: int, end_x: int, end_y: int, - camera_offset: pygame.math.Vector2, camera_scale: float) -> None: + def _draw_path( + self, + start_x: int, + start_y: int, + end_x: int, + end_y: int, + camera_offset: pygame.math.Vector2, + camera_scale: float, + ) -> None: start = (start_x, start_y) end = (end_x, end_y) if 0 <= start[0] < self.width and 0 <= start[1] < self.height: path = self.path_finder.find_path(start, end) for x, y in path: + assert self.display_surface is not None, "Display surface must be initialized" screen_x, screen_y = self._convert_to_screen_coordinates(x, y, camera_offset, camera_scale) - rect = pygame.Rect(screen_x, screen_y, - self.tile_size * camera_scale, - self.tile_size * camera_scale) + rect = pygame.Rect(screen_x, screen_y, self.tile_size * camera_scale, self.tile_size * camera_scale) pygame.draw.rect(self.display_surface, "green", rect, 2) # Draw path tiles - def _draw_mouse_indicator(self, mouse_grid_x: int, mouse_grid_y: int, - camera_offset: pygame.math.Vector2, camera_scale: float) -> None: + def _draw_mouse_indicator( + self, mouse_grid_x: int, mouse_grid_y: int, camera_offset: pygame.math.Vector2, camera_scale: float + ) -> None: + assert self.display_surface is not None, "Display surface must be initialized" dot_x = mouse_grid_x * self.tile_size * camera_scale + camera_offset.x dot_y = mouse_grid_y * self.tile_size * camera_scale + camera_offset.y pygame.draw.circle(self.display_surface, (0, 255, 0), (dot_x, dot_y), 5) # Green circle at tile coordinates diff --git a/src/sprites/tiles/pathfinding.py b/src/sprites/tiles/pathfinding.py index 1d833d6..d705676 100644 --- a/src/sprites/tiles/pathfinding.py +++ b/src/sprites/tiles/pathfinding.py @@ -1,13 +1,14 @@ from dataclasses import dataclass -from pathfinding.core.grid import Grid -from pathfinding.finder.a_star import AStarFinder -from pathfinding.core.diagonal_movement import DiagonalMovement +from pathfinding.core.diagonal_movement import DiagonalMovement # noqa: F401 +from pathfinding.core.grid import Grid # noqa: F401 +from pathfinding.finder.a_star import AStarFinder # noqa: F401 @dataclass(frozen=True) class Coordinate: """A simple data class to represent a coordinate in the grid.""" + x: int y: int @@ -45,6 +46,7 @@ def update_cache(self, start: Coordinate, end: Coordinate, path: list[Coordinate self.end = end self.path = path + class PathFinder: MOVEMENT_TYPE = DiagonalMovement.always diff --git a/src/states/game_running.py b/src/states/game_running.py index 33a5b41..d8b9dd9 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -40,21 +40,30 @@ def __init__(self, game_state_manager) -> None: self.player_inventory = Inventory() self.load_inventory_from_json("data/inventory.json") - # Initialize sprite groups - self.all_sprites = pygame.sprite.Group() - # Render the grid - self.grid_manager = None # Initialize grid_manager as None + self.grid_manager: GridManager | None = None # Initialize grid_manager as None self.show_grid: bool = True + sprite_group: pygame.sprite.Group = pygame.sprite.Group() # Initialize sprite group + self.all_sprites: PlayerCamera # Initialize all_sprites as PlayerCamera + # The start positions will be one of the 4 islands in the corners of the board - self.setup(player_start_pos="top_left_island") + self.setup(player_start_pos="top_left_island", sprite_group=sprite_group) + + # Create the player camera and add all sprites to it + sprites = list(sprite_group) + self.all_sprites = PlayerCamera(self.tmx_map["map"], self.player.rect.topleft) + for sprite in sprites: + self.all_sprites.add(sprite) self.font = pygame.font.Font(None, 36) self.shop_window = pygame.Surface((800, 600)) self.in_shop = False - def setup(self, player_start_pos: str) -> None: + def setup(self, player_start_pos: str, sprite_group=None) -> None: + if sprite_group is None: + sprite_group = pygame.sprite.Group() + # Load the TMX map and make it an attribute of the class self.tmx_map = {"map": load_pygame(os.path.join(".", "data", "new_maps", "100x100_map.tmx"))} if not self.tmx_map: @@ -74,7 +83,7 @@ def setup(self, player_start_pos: str) -> None: BaseSprite( pos=(x * TILE_SIZE, y * TILE_SIZE), surf=surface, - groups=(self.all_sprites,), + groups=(sprite_group,), z=WORLD_LAYERS["bg"], ) @@ -85,20 +94,18 @@ def setup(self, player_start_pos: str) -> None: AnimatedSprites( pos=(x, y), frames=self.world_frames["water"], - groups=(self.all_sprites,), + groups=(sprite_group,), z=WORLD_LAYERS["water"], ) # Shallow water for x, y, surface in self.tmx_map["map"].get_layer_by_name("Shallow Sea").tiles(): - BaseSprite( - pos=(x * TILE_SIZE, y * TILE_SIZE), surf=surface, groups=(self.all_sprites,), z=WORLD_LAYERS["bg"] - ) + BaseSprite(pos=(x * TILE_SIZE, y * TILE_SIZE), surf=surface, groups=(sprite_group,), z=WORLD_LAYERS["bg"]) # Buildings for x, y, surface in self.tmx_map["map"].get_layer_by_name("Shop").tiles(): self.shop = ShowShop( - pos=(x * TILE_SIZE, y * TILE_SIZE), surface=surface, groups=(self.all_sprites,), z=WORLD_LAYERS["main"] + pos=(x * TILE_SIZE, y * TILE_SIZE), surface=surface, groups=(sprite_group,), z=WORLD_LAYERS["main"] ) # Islands @@ -107,7 +114,7 @@ def setup(self, player_start_pos: str) -> None: BaseSprite( pos=(x * TILE_SIZE, y * TILE_SIZE), surf=surface, - groups=(self.all_sprites,), + groups=(sprite_group,), z=WORLD_LAYERS["bg"], ) @@ -121,7 +128,7 @@ def setup(self, player_start_pos: str) -> None: self.player = Player( pos=(grid_x, grid_y), frames=self.world_frames["ships"]["player_test_ship"], - groups=(self.all_sprites,), + groups=(sprite_group,), ) # Coast @@ -131,17 +138,10 @@ def setup(self, player_start_pos: str) -> None: AnimatedSprites( pos=(obj.x, obj.y), frames=self.world_frames["coast"][terrain][side], - groups=(self.all_sprites,), + groups=(sprite_group,), z=WORLD_LAYERS["bg"], ) - # Create a new PlayerCamera with all the sprites - sprites = list(self.all_sprites) # Get all sprites from the temporary group - self.all_sprites = PlayerCamera(self.tmx_map["map"], self.player.rect.topleft) - # Add all sprites to the new camera group - for sprite in sprites: - self.all_sprites.add(sprite) - def load_inventory_from_json(self, file_path: str): """Load initial inventory items from JSON file.""" try: @@ -158,16 +158,25 @@ def update(self, events) -> None: update each sprites and handle events """ - collide = self.player.rect.colliderect(self.shop.rect) if self.player else False + collide: bool = ( + self.player is not None + and self.shop is not None + and isinstance(self.player.rect, (pygame.Rect, pygame.FRect)) + and isinstance(self.shop.rect, (pygame.Rect, pygame.FRect)) + and self.player.rect.colliderect(self.shop.rect) + ) dt = self.clock.tick() / 1000 self.all_sprites.update(dt) # Handle player movement and grid snapping - self.player.update(dt, - grid=self.grid_manager, - camera_offset=self.all_sprites.offset, - camera_scale=self.all_sprites.scale - ) + if isinstance(self.all_sprites, PlayerCamera): + camera_offset = self.all_sprites.offset + scale = self.all_sprites.scale + else: + camera_offset = pygame.math.Vector2() + scale = 1.0 + # scale = getattr(self.all_sprites, "scale", 1.0) + self.player.update(dt, grid=self.grid_manager, camera_offset=camera_offset, camera_scale=scale) # get events like keypress or mouse clicks for event in events: @@ -184,17 +193,15 @@ def update(self, events) -> None: def render(self, screen) -> None: """Draw sprites to the canvas.""" screen.fill("#000000") - self.all_sprites.draw( - self.player.rect.center, - show_grid=self.show_grid - ) + if isinstance(self.all_sprites, PlayerCamera): + self.all_sprites.draw(self.player.rect.center, show_grid=self.show_grid) # Pass the player's position to the draw method - if self.player: + if self.player and self.grid_manager is not None: mouse_pos = pygame.mouse.get_pos() if self.show_grid: self.grid_manager.draw( - player_pos=self.player.rect.topleft, + player_pos=(int(self.player.rect.topleft[0]), int(self.player.rect.topleft[1])), mouse_pos=mouse_pos, camera_offset=self.all_sprites.offset, camera_scale=self.all_sprites.scale, @@ -203,9 +210,7 @@ def render(self, screen) -> None: # Get tile coordinates with camera offset and scale tile_x, tile_y = self.grid_manager.get_tile_coordinates( - mouse_pos, - self.all_sprites.offset, - self.all_sprites.scale + mouse_pos, self.all_sprites.offset, self.all_sprites.scale ) # Convert grid coordinates to screen coordinates From 28d794abe58e828384a989f151a1b403cf8341f2 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 26 May 2025 17:04:17 +0200 Subject: [PATCH 33/38] Refactor indentation and replace assertions with exceptions. Adjust method parameter indentation for consistency and readability. Replaced `assert` statements with exceptions to standardize error handling and improve robustness. --- src/sprites/tiles/grid_manager.py | 9 ++++++--- src/states/game_running.py | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index 0ded48b..e6d4f89 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -155,7 +155,8 @@ def _draw_grid_lines( ) -> None: for y in range(visible_start_y, visible_end_y): for x in range(visible_start_x, visible_end_x): - assert self.display_surface is not None, "Display surface must be initialized" + if self.display_surface is None: + raise RuntimeError("Display surface must be initialized") screen_x, screen_y = self._convert_to_screen_coordinates(x, y, camera_offset, camera_scale) rect = pygame.Rect(screen_x, screen_y, self.tile_size * camera_scale, self.tile_size * camera_scale) pygame.draw.rect(self.display_surface, "dark grey", rect, 1) # Draw grid lines @@ -190,7 +191,8 @@ def _draw_path( if 0 <= start[0] < self.width and 0 <= start[1] < self.height: path = self.path_finder.find_path(start, end) for x, y in path: - assert self.display_surface is not None, "Display surface must be initialized" + if self.display_surface is None: + raise RuntimeError("Display surface must be initialized") screen_x, screen_y = self._convert_to_screen_coordinates(x, y, camera_offset, camera_scale) rect = pygame.Rect(screen_x, screen_y, self.tile_size * camera_scale, self.tile_size * camera_scale) pygame.draw.rect(self.display_surface, "green", rect, 2) # Draw path tiles @@ -198,7 +200,8 @@ def _draw_path( def _draw_mouse_indicator( self, mouse_grid_x: int, mouse_grid_y: int, camera_offset: pygame.math.Vector2, camera_scale: float ) -> None: - assert self.display_surface is not None, "Display surface must be initialized" + if self.display_surface is None: + raise RuntimeError("Display surface must be initialized") dot_x = mouse_grid_x * self.tile_size * camera_scale + camera_offset.x dot_y = mouse_grid_y * self.tile_size * camera_scale + camera_offset.y pygame.draw.circle(self.display_surface, (0, 255, 0), (dot_x, dot_y), 5) # Green circle at tile coordinates diff --git a/src/states/game_running.py b/src/states/game_running.py index d8b9dd9..11889dd 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -175,7 +175,6 @@ def update(self, events) -> None: else: camera_offset = pygame.math.Vector2() scale = 1.0 - # scale = getattr(self.all_sprites, "scale", 1.0) self.player.update(dt, grid=self.grid_manager, camera_offset=camera_offset, camera_scale=scale) # get events like keypress or mouse clicks From a3795f50c90997b89cf7aeb50fb54aa703dea538 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Wed, 28 May 2025 13:54:52 +0200 Subject: [PATCH 34/38] chore(unit tests): Add unit test and refactor GridManager Tests the grid_manager calculations with unit tests BREAKING CHANGE: Refactor the GridManager to support initialization with either a tmx_map or a grid_matrix, improving flexibility and testability. --- src/sprites/tiles/grid_manager.py | 20 +++--- tests/test_grid_manager.py | 115 ++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 tests/test_grid_manager.py diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index e6d4f89..c36f6c9 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -8,15 +8,19 @@ class GridManager: - def __init__(self, tmx_map: pytmx.TiledMap, tile_size: int = TILE_SIZE): - if tmx_map is None: - raise ValueError("tmx_map cannot be None") - - self.tmx_map = tmx_map + def __init__(self, tmx_map: pytmx.TiledMap = None, tile_size: int = TILE_SIZE, grid_matrix: np.ndarray = None): + if grid_matrix is not None: + self.grid_matrix = grid_matrix + self.height, self.width = grid_matrix.shape + self.tmx_map = None + else: + if tmx_map is None: + raise ValueError("Either tmx_map or grid_matrix must be provided") + self.tmx_map = tmx_map + self.width = tmx_map.width # Number of tiles wide + self.height = tmx_map.height # Number of tiles high + self.grid_matrix = self.create_grid_matrix() self.tile_size = tile_size - self.width = tmx_map.width # Number of tiles wide - self.height = tmx_map.height # Number of tiles high - self.grid_matrix = self.create_grid_matrix() self.path_finder = PathFinder(self.grid_matrix) self.display_surface: Surface | None = pygame.display.get_surface() diff --git a/tests/test_grid_manager.py b/tests/test_grid_manager.py new file mode 100644 index 0000000..d1097ec --- /dev/null +++ b/tests/test_grid_manager.py @@ -0,0 +1,115 @@ +import os +import sys + +# Add the project root to sys.path to allow imports to work when running tests directly with `python`. +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +import pytest +from hypothesis import given, strategies as st +import numpy as np +import pygame +from src.sprites.tiles.grid_manager import GridManager + + +@pytest.fixture(scope="session", autouse=True) +def pygame_init(): + """Initialize Pygame for the test session.""" + pygame.init() + pygame.font.init() + pygame.display.set_mode((800, 600)) + pygame.display.set_caption("Grid Manager Test") + yield + pygame.quit() + + +@pytest.fixture +def grid_manager(): + grid = np.zeros((10, 10), dtype=int) # 10x10 walkable grid + return GridManager(grid_matrix=grid, tile_size=64) + + +# --- Tests --- +def test_grid_matrix_shape(grid_manager): + assert grid_manager.grid_matrix.shape == (10, 10) + + +def test_grid_matrix_walkable(grid_manager): + # All tiles should be walkable (0) + assert np.all(grid_manager.grid_matrix == 0) + + +@given( + mouse_x=st.integers(min_value=0, max_value=99), + mouse_y=st.integers(min_value=0, max_value=99), + offset_x=st.integers(min_value=-50, max_value=50), + offset_y=st.integers(min_value=-50, max_value=50), + scale=st.floats(min_value=0.5, max_value=2.0) +) +def test_get_tile_coordinates_hypothesis(mouse_x, mouse_y, offset_x, offset_y, scale): + grid = np.zeros((10, 10), dtype=int) # 10x10 walkable grid + grid_manager = GridManager(grid_matrix=grid, tile_size=10) + mouse_pos = (mouse_x, mouse_y) + camera_offset = pygame.math.Vector2(offset_x, offset_y) + camera_scale = scale + x, y = grid_manager.get_tile_coordinates(mouse_pos, camera_offset, camera_scale) + assert 0 <= x < grid_manager.width + assert 0 <= y < grid_manager.height + + +def test_convert_mouse_to_world(grid_manager): + mouse_pos = (10, 20) + camera_offset = pygame.math.Vector2(2, 3) + camera_scale = 2.0 + world_x, world_y = grid_manager._convert_mouse_to_world(mouse_pos, camera_offset, camera_scale) + assert world_x == (10 - 2) / 2.0 + assert world_y == (20 - 3) / 2.0 + + +@pytest.mark.parametrize("tile_size, world_x, world_y, expected_x, expected_y", [ + (1, 5.7, 8.2, 5, 8), + (16, 5.7, 8.2, 0, 0), + (32, 5.7, 8.2, 0, 0), + (64, 5.7, 8.2, 0, 0), + (16, 20.0, 33.0, 1, 2), + (64, 128.0, 192.0, 2, 3), +]) +def test_convert_world_to_grid(tile_size, world_x, world_y, expected_x, expected_y): + grid = np.zeros((10, 10), dtype=int) + grid_manager = GridManager(grid_matrix=grid, tile_size=tile_size) + grid_x, grid_y = grid_manager._convert_world_to_grid(world_x, world_y) + assert grid_x == expected_x + assert grid_y == expected_y + + +def test_clamp_grid_coordinates(grid_manager): + # In bounds + assert grid_manager._clamp_grid_coordinates(5, 5) == (5, 5) + # Out of bounds + assert grid_manager._clamp_grid_coordinates(-1, 0) == (0, 0) + assert grid_manager._clamp_grid_coordinates(0, -1) == (0, 0) + assert grid_manager._clamp_grid_coordinates(20, 5) == (9, 5) + assert grid_manager._clamp_grid_coordinates(5, 20) == (5, 9) + + +def test_manual_inspect_grid_manager(grid_manager): + """Creates a pygame window to see the grid manager visually.""" + import time + print("Manual inspection: Close the window to exit.") + running = True + player_pos = (5, 5) # Example player position + mouse_pos = (0, 0) # Example mouse position + camera_offset = pygame.math.Vector2() + camera_scale = 1.0 + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + grid_manager.draw( + player_pos=player_pos, + mouse_pos=mouse_pos, + camera_offset=camera_offset, + camera_scale=camera_scale, + visible_radius=5 + ) + pygame.display.flip() + time.sleep(0.01) From c12df4b56329eea1ad7ff71f98d780d7b7d1abe5 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Wed, 28 May 2025 14:04:48 +0200 Subject: [PATCH 35/38] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit f183c958704087f82141148b88c17ec77e935bb4 Merge: ef6692f 68b393c Author: Davidek523 Date: Wed May 28 11:07:36 2025 +0200 Refactor unittest framework (#118) # Pull Request Template ## Type of Pull Request Only check one box per pull request. - [x] πŸ› οΈ Code - Changes to the codebase. - [ ] πŸ”„ Revert - Reverts a previously merged commit or PR. - [ ] πŸ“„ Documentation - Updates to documentation files. - [ ] 🎡 Audio - Changes to audio files. - [ ] 🎨 Asset - Updates to design or visual assets. ## Summary This PR ... refactors the `unit test` framework back to `pytest`, as specified in `requirements_dev.txt`. This change is useful as it simplified the creation of unit tests easier and adds additional support showing errors during testing. The update is also needed, to integrate the [Hypothesis](https://hypothesis.readthedocs.io/en/latest/index.html) library, which employs property based testing. Useful for testing edge cases you haven't thought of. Given the ongoing movement rework, associated with issues #103 & #104 which involved libraries such as NumPy and Pathfinding, it could be very useful. The unit test for the inventory was made by @AnSiChen, I integrated the `get_message` module in the unit test as well, this change could be better in the future if the inventory gets an overhaul, with mock testing for example, or with the use of Hypothesis to handle specific edge cases. ## Related Issues This PR issue(s) --- ## βœ… Checklist: - [x] Tested the changes locally. - [x] Verified that no breaking changes are introduced. - [ ] Updated documentation (if applicable). ## 🏷️ Labels: `type: ...`, `area: ...`, `game-...`, ... commit 68b393c328e305f57ffcbc06b5e90565a876ab81 Author: ultimateownsz Date: Mon May 26 00:42:51 2025 +0200 fix ruff import check A long error in the terminal when you ran ruff, has now been fixed: `include I` keyword was not found in terminal ruff check --select I --fix . commit dac598d0fb7cacbc71d907791aadd4e7b2439a4b Author: ultimateownsz Date: Thu May 22 22:41:04 2025 +0200 fix ruff issues fix unit test assert commit c910ef1e1fbe5afe1b2be376caa721cfc7734472 Author: ultimateownsz Date: Thu May 22 22:31:10 2025 +0200 Create .gitattributes Developers on different OSes won’t accidentally introduce line ending changes, keeping the repository history clean. commit 2955dc5e50e2a7019d5c6b3f9f668f7c4773c0ae Author: ultimateownsz Date: Thu May 22 22:24:45 2025 +0200 Refactor inventory unit tests Refactor inventory tests: migrate from unit test to `pytest` and update test structure Co-Authored-By: Anthony Em <139152052+AnSiChen@users.noreply.github.com> --- .gitattributes | 2 + README.md | 4 +- pyproject.toml | 9 ++ tests/test_inventory.py | 211 ++++++++++++++++++++++------------------ 4 files changed, 129 insertions(+), 97 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..eba1110 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto \ No newline at end of file diff --git a/README.md b/README.md index 968ab0b..7cecec3 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ We chose to use [Ruff](https://docs.astral.sh/ruff/) to automatically lint and f > > > ```sh -> ruff format . && ruff check --include I --fix . # this formats code and sort imports +> ruff format . && ruff check --select I --fix . # this formats code and sort imports > ruff check . # run linting and perform fixes > mypy main.py > ``` @@ -193,7 +193,7 @@ We chose to use [Ruff](https://docs.astral.sh/ruff/) to automatically lint and f > > ```powershell > ruff format .; -> ruff check --include I --fix .; +> ruff check --select I --fix .; > ruff check .; > mypy main.py > ``` diff --git a/pyproject.toml b/pyproject.toml index 063326e..ece25c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,3 +12,12 @@ explicit_package_bases = true ["mypy-pygame.*"] ignore_missing_imports = true + +[tool.pytest.ini_options] +minversion = "6.0" +# -ra: Show extra test summary info for all tests. +# -q: Quiet mode, which reduces the verbosity of the output. +addopts = "-ra -q" +testpaths = [ + "tests", +] diff --git a/tests/test_inventory.py b/tests/test_inventory.py index 7fc7afb..bac7eda 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -4,100 +4,121 @@ # Add the project root to sys.path to allow imports to work when running tests directly with `python`. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -import unittest - -from src.inventory import Chest, Inventory, Quest +import pytest -class TestInventory(unittest.TestCase): - def setUp(self): - """Set up a new Inventory object before each test.""" - self.inventory = Inventory() - - # Test add_item - def test_add_item_new(self): - """Test adding a new item.""" - result = self.inventory.add_item("Sword", 1) - self.assertEqual(self.inventory.items, {"Sword": 1}) - self.assertEqual(result, "Successfully added 1 Sword(s) to your inventory.") - - def test_add_item_existing(self): - """Test adding to an existing item.""" - self.inventory.add_item("Potion", 1) - result = self.inventory.add_item("Potion", 2) - self.assertEqual(self.inventory.items, {"Potion": 3}) - self.assertEqual(result, "Successfully added 2 Potion(s) to your inventory.") - - # Test remove_item - def test_remove_item_success(self): - """Test successfully removing an item.""" - self.inventory.add_item("Potion", 3) - result = self.inventory.remove_item("Potion", 2) - self.assertEqual(self.inventory.items, {"Potion": 1}) - self.assertEqual(result, "Successfully removed 2 Potion(s) from your inventory.") - - def test_remove_item_fail(self): - """Test failing to remove an item not in inventory or insufficient quantity.""" - result = self.inventory.remove_item("Sword", 1) - self.assertEqual(self.inventory.items, {}) - self.assertEqual(result, "Cannot remove 1 Sword(s), insufficient quantity.") - - # Test use_item - def test_use_item_success(self): - """Test using an item.""" - self.inventory.add_item("Potion", 1) - result = self.inventory.use_item("Potion") - self.assertEqual(self.inventory.items, {}) - self.assertEqual(result, "You used Potion.") - - def test_use_item_fail(self): - """Test failing to use an item.""" - result = self.inventory.use_item("Potion") - self.assertEqual(self.inventory.items, {}) - self.assertEqual(result, "You dont' have Potion in your inventory.") - - # Test add_chest - def test_add_chest(self): - """Test adding a chest.""" - chest = Chest("Gold Chest") - self.inventory.add_chest(chest) - self.assertEqual(len(self.inventory.chests), 1) - self.assertEqual(self.inventory.chests[0].name, "Gold Chest") - - # Test add_quest - def test_add_quest(self): - """Test adding a quest.""" - quest = Quest() - self.inventory.add_quest(quest) - self.assertEqual(len(self.inventory.quests), 1) - self.assertFalse(self.inventory.quests[0].completed) - - # Test get_items - def test_get_items(self): - """Test getting a copy of items.""" - self.inventory.add_item("Sword", 1) - items = self.inventory.get_items() - self.assertEqual(items, {"Sword": 1}) - self.assertIsNot(items, self.inventory.items) # Copy of items - - # Test get_chests - def test_get_chests(self): - """Test getting a copy of chests.""" - chest = Chest("Gold Chest") - self.inventory.add_chest(chest) - chests = self.inventory.get_chests() - self.assertEqual(len(chests), 1) - self.assertIsNot(chests, self.inventory.chests) # Copy of items - - # Test get_quests - def test_get_quests(self): - """Test getting a copy of quests.""" - quest = Quest() - self.inventory.add_quest(quest) - quests = self.inventory.get_quests() - self.assertEqual(len(quests), 1) - self.assertIsNot(quests, self.inventory.quests) # Copy of items - - -if __name__ == "__main__": - unittest.main() +from src.inventory import Chest, Inventory, Quest +from src.utils.messaging import get_message + +# Old way of writing tests using unittest +# class TestInventory(unittest.TestCase): +# def setUp(self): +# """Set up a new Inventory object before each test.""" +# self.inventory = Inventory() + + +@pytest.fixture +def inventory(): + """Set up a new Inventory object before each test.""" + return Inventory() + + +# Test add_item +def test_add_item_new(inventory): + """Test adding a new item.""" + result = inventory.add_item("Sword", 1) + expected = get_message("inventory", "add_success", item="Sword", quantity=1) + assert inventory.items == {"Sword": 1} + assert result == expected + + +def test_add_item_existing(inventory): + """Test adding to an existing item.""" + result = inventory.add_item("Potion", 2) + expected = get_message("inventory", "add_success", item="Potion", quantity=2) + assert inventory.items == {"Potion": 2} + assert result == expected + + +# Test remove_item +def test_remove_item_success(inventory): + """Test successfully removing an item.""" + inventory.add_item("Potion", 3) + result = inventory.remove_item("Potion", 2) + expected = get_message("inventory", "remove_success", item="Potion", quantity=2) + assert inventory.items == {"Potion": 1} + assert result == expected + + +def test_remove_item_fail(inventory): + """Test failing to remove an item not in inventory or insufficient quantity.""" + result = inventory.remove_item("Sword", 1) + expected = get_message("inventory", "remove_fail", item="Sword", quantity=1) + assert inventory.items == {} + assert result == expected + + +# Test use_item +def test_use_item_success(inventory): + """Test using an item.""" + inventory.add_item("Potion", 1) + result = inventory.use_item("Potion") + expected = get_message("inventory", "use_success", item="Potion") + assert inventory.items == {} + assert result == expected + + +def test_use_item_fail(inventory): + """Test failing to use an item.""" + result = inventory.use_item("Potion") + expected = get_message("inventory", "use_fail", item="Potion") + assert inventory.items == {} + assert result == expected + + +# Test add_chest +def test_add_chest(inventory): + """Test adding a chest.""" + chest = Chest("Gold Chest") + inventory.add_chest(chest) + assert len(inventory.chests) == 1 + assert inventory.chests[0].name == "Gold Chest" + + +# Test add_quest +def test_add_quest(inventory): + """Test adding a quest.""" + quest = Quest() + inventory.add_quest(quest) + assert len(inventory.quests) == 1 + assert not inventory.quests[0].completed + + +# Test get_items +def test_get_items(inventory): + """Test getting a copy of items.""" + inventory.add_item("Sword", 1) + items = inventory.get_items() + assert items == {"Sword": 1} + assert items is not inventory.items # Copy of items + + +# Test get_chests +def test_get_chests(inventory): + """Test getting a copy of chests.""" + chest = Chest("Gold Chest") + inventory.add_chest(chest) + chests = inventory.get_chests() + assert len(chests) == 1 + assert chests[0].name == "Gold Chest" + assert chests is not inventory.chests # Copy of items + + +# Test get_quests +def test_get_quests(inventory): + """Test getting a copy of quests.""" + quest = Quest() + inventory.add_quest(quest) + quests = inventory.get_quests() + assert len(quests) == 1 + assert quests is not inventory.quests # Copy of items From 21fbd2973b09d2a485ed756f36bc2649b7e6ad3e Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Thu, 29 May 2025 00:42:35 +0200 Subject: [PATCH 36/38] docs(add guides): Add documentation movement rework This commit introduces three new documentation guides: - Coordinate Conversions Guide - Camera Guide - Grid Manager Guide. - PathfindingAStar Guide These guides cover essential concepts like coordinate systems, camera offsets/scales, grid rendering, and interaction handling, providing detailed examples and use cases for game development. proper conversion processes, controls, and edge case handling are also documented. --- docs/Guides/CameraGuide.md | 53 ++++++++++++++ docs/Guides/CoordinateConversionsGuide.md | 80 +++++++++++++++++++++ docs/Guides/GridManagerGuide.md | 85 +++++++++++++++++++++++ docs/Guides/PathfindingAStar.md | 71 +++++++++++++++++++ 4 files changed, 289 insertions(+) create mode 100644 docs/Guides/CameraGuide.md create mode 100644 docs/Guides/CoordinateConversionsGuide.md create mode 100644 docs/Guides/GridManagerGuide.md create mode 100644 docs/Guides/PathfindingAStar.md diff --git a/docs/Guides/CameraGuide.md b/docs/Guides/CameraGuide.md new file mode 100644 index 0000000..f7db1e1 --- /dev/null +++ b/docs/Guides/CameraGuide.md @@ -0,0 +1,53 @@ +# Camera Guide + +The word `world` refers in this context to the `pytmx` map, which is the game world where the player interacts with +tiles. + +## Overview + +This document explains how to handle camera offset and scale in a game development context. Understanding these concepts +is crucial for accurate rendering and interaction with the game world. + +## Camera Offset + +- **Purpose**: The camera offset shifts the visible area of the game world to center the player or a specific point of + interest on the screen. +- **Effect**: The player stays centered or the view follows the player. +- **Conversion**: When converting screen (mouse) coordinates to world coordinates, the camera offset must be subtracted + from the mouse position, it's necessary to adjust for the camera offset to get the correct position in the game world. + +## Camera Scale (Zoom) + +- **Purpose**:The camera scale changes the size of the tiles on the screen. +- **Effect**: This affects how the game world is displayed and how the player interacts with it. +- **Conversion**: The scale factor is used to convert between pixel coordinates and grid coordinates. +- To convert between screen and world coordinates, you must divide by the camera scale. + +## Importance + +Properly accounting for camera offset and scale is essential for: + +- Ensuring that the game world is rendered correctly on the screen. +- Allowing accurate interaction with the game world, such as clicking on tiles or objects. +- Maintaining consistency between the visual representation and the underlying game logic. +- Making sure that mouse clicks correspond to the correct positions in the game world. + +## In Code + +```python +# Constants +TILE_SIZE = 16 # Define the size of each tile in pixels +camera_offset_x, camera_offset_y = 100, 50 # Example camera offset values +camera_scale = 1.5 # Example camera scale value + +# Mouse position +mouse_x, mouse_y = 320, 240 # Example mouse coordinates + +# Convert mouse position to world coordinates +world_x = (mouse_x - camera_offset_x) / camera_scale +world_y = (mouse_y - camera_offset_y) / camera_scale + +# Convert world position to grid (tile) coordinates +grid_x = int(world_x // TILE_SIZE) +grid_y = int(world_y // TILE_SIZE) +``` \ No newline at end of file diff --git a/docs/Guides/CoordinateConversionsGuide.md b/docs/Guides/CoordinateConversionsGuide.md new file mode 100644 index 0000000..8cd99b7 --- /dev/null +++ b/docs/Guides/CoordinateConversionsGuide.md @@ -0,0 +1,80 @@ +# Coordinate Conversions Guide + +## Overview + +This document explains how to convert between grid (tile) coordinates and pixel coordinates in the context of game +development. These conversions are essential for translating game logic positions to screen positions and vice versa. + +### Description of the Grid coordinates + +The grid coordinates are a system used to represent the position of tiles on the game board. Each tile is represented by +a pair of coordinates, which can be converted to pixel coordinates for rendering on the screen. + +## Coordinate System + +### Grid (Tile) Coordinates + +- **Description**: Grid coordinates represent the position of a tile on the game board +- **Notation**: The x and y-axis as `(tile_x, tile_y)`, where `tile_x` is the column and `tile_y` is the row. +- **Example**: `(16, 10)` refers to the tile at column 16 and row 10. + +### Pixel Coordinates + +- **Description**: Pixel coordinates represent the exact position on the screen, measured in pixels, +- **Notation**: Denoted as `(pixel_x, pixel_y)`. +- **Example**: `(256, 160)` refers to the pixel at x=256 and y=160. + +## Conversion Between Coordinate System + +```py + +# Constants +TILE_SIZE: int = 16 + +# From grid to pixel conversion +tile_x, tile_y: int = 16, 10 +pixel_x: int = tile_x * TILE_SIZE # 256 +pixel_y: int = tile_y * TILE_SIZE # 160 + +# From pixel to grid conversion +grid_x: int = pixel_x // TILE_SIZE # 16 +grid_y: int = pixel_y // TILE_SIZE # 10 +``` + +## Conversion in mathematical notation + +For grid (position) to pixel conversion: + +$$ +\begin{align*} +\text{pixel}_x &= \text{tile}_x \times \text{TILE_SIZE} \\ +\text{pixel}_y &= \text{tile}_y \times \text{TILE_SIZE} +\end{align*} +$$ + +Given $\text{TILE_SIZE} = 16$, $\text{tile_x} = 16$, and $\text{tile_y} = 10$ + +$$ +\begin{align*} +\text{256}_x &= \text{16}_x \times \text{16} \\ +\text{160}_y &= \text{10}_y \times \text{16} +\end{align*} +$$ + +For pixel (position) to grid conversion: + +$$ +\begin{align*} +\text{grid}_x &= \left\lfloor \frac{\text{pixel}_x}{\text{TILE_SIZE}} \right\rfloor \\ +\text{grid}_y &= \left\lfloor \frac{\text{pixel}_y}{\text{TILE_SIZE}} \right\rfloor +\end{align*} +$$ + +Given $\text{TILE_SIZE} = 16$, $\text{tile_x} = 256$, and $\text{tile_y} = 160$ + +$$ +\begin{align*} +\text{16}_x &= \left\lfloor \frac{\text{256}_x}{\text{16}} \right\rfloor \\ +\text{10}_y &= \left\lfloor \frac{\text{160}_y}{\text{16}} \right\rfloor +\end{align*} +$$ \ No newline at end of file diff --git a/docs/Guides/GridManagerGuide.md b/docs/Guides/GridManagerGuide.md new file mode 100644 index 0000000..7fc83a2 --- /dev/null +++ b/docs/Guides/GridManagerGuide.md @@ -0,0 +1,85 @@ +# Grid Manager Guide + +The word `world` refers in this context to the `pytmx` map, which is the game world where the player interacts with +tiles. + +### Key Features + +- **Grid Drawing**: The grid is drawn on the screen, allowing players to see the layout of the game world. +- **Pathfinding Visualization**: The system can visualize the pathfinding process, showing the route taken by the AI or + player. +- **Edge Case Handling**: The system handles edge cases, such as clicks outside the `pytmx` remain withing the bounds of + the `pytmx map`. +- **Mouse Indicator**: A mouse indicator is rendered to show the current tile under the mouse cursor, helping players + understand where they are clicking. +- **Coordinate Conversion**: The system handles the conversion between grid coordinates and pixel coordinates, ensuring + accurate rendering and interaction with the game world. +- **Customizable Grid Size**: The grid size can be adjusted to fit different game worlds, allowing for flexibility in + design. + +## Controls Documentation + +### Controls Summary + +- **Keyboard**: Press `G` to toggle the grid on and off. +- **Mouse**: When the grid is enabled, clicking on a tile will show its pixel coordinates. + Without it, you can still click on the tile, but it will not show the pixel coordinates and the grid will not be + drawn. + +## Coordinate Systems + +### Mouse Coordinates Summary + +- **Description**: Represents the position of the mouse cursor on the screen. +- **Notation**: Denoted as `(mouse_x, mouse_y)`. + +### World Coordinates Summary + +- **Description**: Represents the position in the game world, accounting for camera offset and scale. +- **Notation**: Denoted as `(world_x, world_y)`. + +### Grid Coordinates Summary + +- **Description**: Represents the position of a tile in the grid, based on the world coordinates. +- **Notation**: Denoted as `(grid_x, grid_y)`. + +### Screen (Pixel) Coordinates Summary + +- **Description**: Represents the position on the screen in pixels. +- **Notation**: Denoted as `(screen_x, screen_y)`. + +## Conversion Processes + +### Mouse to World Coordinates + +- **Description**: Converts mouse coordinates to world coordinates by adjusting for camera offset and scale. + +**Mathematical Notation**: + +$$ +\begin{align*} +\text{world\_x} &= \frac{\text{mouse\_x} - \text{camera\_offset.x}}{\text{camera\_scale}} \\ +\text{world\_y} &= \frac{\text{mouse\_y} - \text{camera\_offset.y}}{\text{camera\_scale}} +\end{align*} +$$ + +**Code Example**: + +```python +def _convert_mouse_to_world(mouse_pos, camera_offset, camera_scale): + world_x = (mouse_pos[0] - camera_offset.x) / camera_scale + world_y = (mouse_pos[1] - camera_offset.y) / camera_scale + return world_x, world_y +``` + +## Running Tests + +Tests in the project use `pytest`. To run the tests: +`pytest tests/test_grid_manager.py` + +## No Collisions Implemented Yet + +- With the rework, you can still move over objects, like 'Islands,' this is intentional and needs to be changed later on + with enhancements. +- The ` 0` is the tile that is walkable, and the `1` is the tile that is not walkable. This is a placeholder for future + collision detection implementation. \ No newline at end of file diff --git a/docs/Guides/PathfindingAStar.md b/docs/Guides/PathfindingAStar.md new file mode 100644 index 0000000..37a353b --- /dev/null +++ b/docs/Guides/PathfindingAStar.md @@ -0,0 +1,71 @@ +# Pathfinding and Movement System + +## Overview + +This document describes the pathfinding and movement system used in the game. The system utilizes the A* algorithm to +find optimal paths on a grid, allowing characters or objects to navigate from a starting point to an endpoint while +avoiding obstacles. + +## Components + +### Coordinate Dataclass + +- **Purpose**: Represents a coordinate in the grid. +- **Attributes**: + - `x`: The x-coordinate on the grid. + - `y`: The y-coordinate on the grid. + +### PathCache Class + +- **Purpose**: Caches paths to avoid recalculating them, improving performance. +- **Methods**: + - `get_cached_path(start, end)`: Retrieves a cached path if the start and end coordinates match. + - `update_cache(start, end, path)`: Updates the cache with a new path. + +### PathFinder Class + +- **Purpose**: Finds paths on a grid using the A* algorithm. +- **Attributes**: + - `grid`: A grid representing the game world, where `0` is walkable and `1` is blocked. + - `_cache`: An instance of `PathCache` to store and retrieve paths. +- **Methods**: + - `find_path(start, end)`: Finds a path from the start to the end coordinates. + - `_calculate_path(start, end)`: Uses the A* algorithm to calculate the path. + +## Pathfinding Algorithm + +### A* Algorithm + +- **Description**: A* is a popular pathfinding algorithm that efficiently finds the shortest path between two points on + a grid. It uses a heuristic to estimate the cost of reaching the goal from each node, which helps in prioritizing the + exploration of promising paths. +- **Heuristic**: The algorithm uses the Euclidean distance or Manhattan distance as a heuristic to estimate the cost. +- **Diagonal Movement**: The algorithm supports diagonal movement, which can be enabled or disabled based on the game's + requirements. + +### Movement Types + +- **DiagonalMovement.always**: Allows movement in all eight possible directions (horizontal, vertical, and diagonal). +- **DiagonalMovement.never**: Restricts movement to four directions (horizontal and vertical only). +- **DiagonalMovement.only_when_no_obstacle**: Allows diagonal movement only when there are no obstacles. +- **DiagonalMovement.if_at_most_one_obstacle**: Allows diagonal movement if there is at most one obstacle. + +## Usage + +### Finding a Path + +To find a path from a starting point to an endpoint, use the `find_path` method of the `PathFinder` class: + +```python +grid_matrix = [ + [0, 0, 0, 0], + [1, 1, 0, 1], + [0, 0, 0, 0], + [0, 0, 0, 0] +] + +path_finder = PathFinder(grid_matrix) +start = (0, 0) +end = (3, 3) +path = path_finder.find_path(start, end) +``` \ No newline at end of file From ad01b75128699dd2984818fcc26256b253b46d63 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Thu, 29 May 2025 00:56:35 +0200 Subject: [PATCH 37/38] chore(ruff/mypy): Fix ruff and mypy issues This fixes some imports, mypy issues. BREAKING CHANGE: Mypy can complain for some reasons, For Python 3.10+ you need to do this like seen below: # PEP 604 syntax, requires Python 3.10+ or `from __future__ import annotations` def good(x: int | None = None): ... https://github.com/hauntsaninja/no_implicit_optional --- src/sprites/tiles/grid_manager.py | 65 +++---------------------------- tests/test_grid_manager.py | 30 ++++++++------ 2 files changed, 23 insertions(+), 72 deletions(-) diff --git a/src/sprites/tiles/grid_manager.py b/src/sprites/tiles/grid_manager.py index c36f6c9..a27cac8 100644 --- a/src/sprites/tiles/grid_manager.py +++ b/src/sprites/tiles/grid_manager.py @@ -8,7 +8,9 @@ class GridManager: - def __init__(self, tmx_map: pytmx.TiledMap = None, tile_size: int = TILE_SIZE, grid_matrix: np.ndarray = None): + def __init__( + self, tmx_map: pytmx.TiledMap = None, tile_size: int = TILE_SIZE, grid_matrix: np.ndarray | None = None + ): if grid_matrix is not None: self.grid_matrix = grid_matrix self.height, self.width = grid_matrix.shape @@ -40,6 +42,8 @@ def create_grid_matrix(self) -> np.ndarray: Create a grid matrix from the Tiled map. Each tile is represented as 0 (walkable) or 1 (non-walkable). """ + if self.tmx_map is None: + raise ValueError("TMX map must be None when creating grid matrix") matrix = np.zeros((self.height, self.width), dtype=int) # Initialize with zeros (walkable) for layer in self.tmx_map.visible_layers: if isinstance(layer, pytmx.TiledTileLayer): @@ -209,62 +213,3 @@ def _draw_mouse_indicator( dot_x = mouse_grid_x * self.tile_size * camera_scale + camera_offset.x dot_y = mouse_grid_y * self.tile_size * camera_scale + camera_offset.y pygame.draw.circle(self.display_surface, (0, 255, 0), (dot_x, dot_y), 5) # Green circle at tile coordinates - - # # Convert player position to grid coordinates - # player_grid_x = int(player_pos[0] // self.tile_size) - # player_grid_y = int(player_pos[1] // self.tile_size) - # - # # Get mouse grid coordinates with camera offset and scale - # mouse_grid_x, mouse_grid_y = self.get_tile_coordinates(mouse_pos, camera_offset, camera_scale) - # - # # Calculate the visible area based on camera offset and scale - # visible_start_x = max(0, player_grid_x - visible_radius) - # visible_start_y = max(0, player_grid_y - visible_radius) - # visible_end_x = min(self.width, player_grid_x + visible_radius + 1) - # visible_end_y = min(self.height, player_grid_y + visible_radius + 1) - # - # for y in range(visible_start_y, visible_end_y): - # for x in range(visible_start_x, visible_end_x): - # # Calculate world position - # world_x = x * self.tile_size - # world_y = y * self.tile_size - # - # # Convert to screen coordinates - # screen_x = world_x * camera_scale + camera_offset.x - # screen_y = world_y * camera_scale + camera_offset.y - # - # rect = pygame.Rect(screen_x, screen_y, - # self.tile_size * camera_scale, - # self.tile_size * camera_scale) - # pygame.draw.rect(self.display_surface, "dark grey", rect, 1) # Draw grid lines - # - # # Calculate the position to draw the text (center of the tile) - # text_surface = self.coordinate_surfaces[(x, y)] - # - # # Calculate the position to draw the text (center of the tile) - # text_rect = text_surface.get_rect(center=(screen_x + self.tile_size * camera_scale / 2, - # screen_y + self.tile_size * camera_scale / 2)) - # - # # Draw the text on the screen - # self.display_surface.blit(text_surface, text_rect) - # - # # Clamp pathfinding start and end points to the grid boundaries - # start = (player_grid_x, player_grid_y) - # end = (mouse_grid_x, mouse_grid_y) - # - # # Path finding and drawing a path - # if 0 <= start[0] < self.width and 0 <= start[1] < self.height: - # path = self.find_path(start, end) - # for x, y in path: - # # Convert path coordinates to the screen position - # screen_x = x * self.tile_size * camera_scale + camera_offset.x - # screen_y = y * self.tile_size * camera_scale + camera_offset.y - # rect = pygame.Rect(screen_x, screen_y, - # self.tile_size * camera_scale, - # self.tile_size * camera_scale) - # pygame.draw.rect(self.display_surface, "green", rect, 2) # Draw path tiles - # - # # Draw the green dot at the mouse grid coordinates - # dot_x = mouse_grid_x * self.tile_size * camera_scale + camera_offset.x - # dot_y = mouse_grid_y * self.tile_size * camera_scale + camera_offset.y - # pygame.draw.circle(self.display_surface, (0, 255, 0), (dot_x, dot_y), 5) # Green circle at tile coordinates diff --git a/tests/test_grid_manager.py b/tests/test_grid_manager.py index d1097ec..e7dfd7e 100644 --- a/tests/test_grid_manager.py +++ b/tests/test_grid_manager.py @@ -4,10 +4,12 @@ # Add the project root to sys.path to allow imports to work when running tests directly with `python`. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -import pytest -from hypothesis import given, strategies as st import numpy as np import pygame +import pytest +from hypothesis import given +from hypothesis import strategies as st + from src.sprites.tiles.grid_manager import GridManager @@ -43,7 +45,7 @@ def test_grid_matrix_walkable(grid_manager): mouse_y=st.integers(min_value=0, max_value=99), offset_x=st.integers(min_value=-50, max_value=50), offset_y=st.integers(min_value=-50, max_value=50), - scale=st.floats(min_value=0.5, max_value=2.0) + scale=st.floats(min_value=0.5, max_value=2.0), ) def test_get_tile_coordinates_hypothesis(mouse_x, mouse_y, offset_x, offset_y, scale): grid = np.zeros((10, 10), dtype=int) # 10x10 walkable grid @@ -65,14 +67,17 @@ def test_convert_mouse_to_world(grid_manager): assert world_y == (20 - 3) / 2.0 -@pytest.mark.parametrize("tile_size, world_x, world_y, expected_x, expected_y", [ - (1, 5.7, 8.2, 5, 8), - (16, 5.7, 8.2, 0, 0), - (32, 5.7, 8.2, 0, 0), - (64, 5.7, 8.2, 0, 0), - (16, 20.0, 33.0, 1, 2), - (64, 128.0, 192.0, 2, 3), -]) +@pytest.mark.parametrize( + "tile_size, world_x, world_y, expected_x, expected_y", + [ + (1, 5.7, 8.2, 5, 8), + (16, 5.7, 8.2, 0, 0), + (32, 5.7, 8.2, 0, 0), + (64, 5.7, 8.2, 0, 0), + (16, 20.0, 33.0, 1, 2), + (64, 128.0, 192.0, 2, 3), + ], +) def test_convert_world_to_grid(tile_size, world_x, world_y, expected_x, expected_y): grid = np.zeros((10, 10), dtype=int) grid_manager = GridManager(grid_matrix=grid, tile_size=tile_size) @@ -94,6 +99,7 @@ def test_clamp_grid_coordinates(grid_manager): def test_manual_inspect_grid_manager(grid_manager): """Creates a pygame window to see the grid manager visually.""" import time + print("Manual inspection: Close the window to exit.") running = True player_pos = (5, 5) # Example player position @@ -109,7 +115,7 @@ def test_manual_inspect_grid_manager(grid_manager): mouse_pos=mouse_pos, camera_offset=camera_offset, camera_scale=camera_scale, - visible_radius=5 + visible_radius=5, ) pygame.display.flip() time.sleep(0.01) From 89d57d219e600cb1f7e55790ea045f889e6d301e Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Thu, 29 May 2025 01:30:11 +0200 Subject: [PATCH 38/38] chore(add hypothesis dep): Add property based testing This change adds the Hypothesis library to `requirements_dev.txt` to support property-based testing. Existing dependencies remain unchanged, ensuring compatibility and functionality. --- requirements_dev.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 6317e4c..3038c5f 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,4 @@ ruff>=0.5.3 mypy>=1.11.1 -pytest>=7.4.4 \ No newline at end of file +pytest>=7.4.4 +hypothesis>=6.80.0 \ No newline at end of file