diff --git a/Classes/aStar.py b/Classes/aStar.py new file mode 100644 index 0000000000000000000000000000000000000000..091306cd90c9e5e641e6667d7292c8cdff261a68 --- /dev/null +++ b/Classes/aStar.py @@ -0,0 +1,100 @@ +import heapq + + +class AStar: + """ + Initializes the AStar solver with the given maze. + + :param maze: The maze to solve. + """ + def __init__(self, maze): + self.maze = maze + self.start = (1, 1) + self.corners = set() + self.directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] + + for i in range(maze.num_rows()): + for j in range(maze.num_columns()): + if maze[i][j] == 3 or maze[i][j] == 4: + self.corners.add((i, j)) + + """ + Heuristic function for A* search. + + :param current: Current position. + :param corners: Set of corner positions. + :param visited_corners: Set of visited corner positions. + :return: Heuristic value. + """ + def heuristic1(self, current, corners, visited_corners): + remaining_corners = sum(1 for corner in corners if corner not in visited_corners) + first_corner = next(iter(corners)) + return abs(current[0] - first_corner[0]) + abs(current[1] - first_corner[1]) + remaining_corners + + """ + Alternative heuristic function for A* search. + + :param current: Current position. + :param corners: Set of corner positions. + :param visited_corners: Set of visited corner positions. + :return: Heuristic value. + """ + def heuristic2(self, current, corners, visited_corners): + remaining_corners = sum(1 for corner in corners if corner not in visited_corners) + return ((current[0] - corners[0][0]) ** 2 + (current[1] - corners[0][1]) ** 2) ** 0.5 + remaining_corners + + """ + Checks if all corners have been visited. + + :param corners: Set of corner positions. + :param visited_corners: Set of visited corner positions. + :return: True if all corners are visited, False otherwise. + """ + def all_corners_visited(self, corners, visited_corners): + return all(corner in visited_corners for corner in corners) + + """ + Finds the position of the goal in the maze. + + :param maze: The maze to search. + :return: The position of the goal or None if not found. + """ + def find_goal_position(self, maze): + for i in range(maze.num_rows()): + for j in range(len(self.maze[0])): + if self.maze[i][j] == 3: + return (i, j) + return None + + """ + Finds the shortest path from the start to the goal using A* search. + + :param heuristic: Heuristic function to use. + :return: The shortest path as a list of positions, or None if no path is found. + """ + def find_shortest_path(self, heuristic): + start = (1, 1) + goal = None + + visited = set() + frontier = [(0, start, [])] + heapq.heapify(frontier) + + while frontier: + total_cost, current, path = heapq.heappop(frontier) + if self.maze[current[0]][current[1]] == 3: + goal = current + break + if current not in visited: + visited.add(current) + for dx, dy in self.directions: + x, y = current[0] + dx, current[1] + dy + if 0 <= x < self.maze.num_rows() and 0 <= y < self.maze.num_columns() and self.maze[x][y] != 1: + new_cost = total_cost + 1 + heapq.heappush(frontier, + (new_cost + heuristic((x, y), self.corners, visited), (x, y), path + [current])) + + if goal: + return path + [goal] + else: + return None diff --git a/Classes/bfs.py b/Classes/bfs.py new file mode 100644 index 0000000000000000000000000000000000000000..8e1753e2c3361cca1a12dd8cb5fa6db4768934c2 --- /dev/null +++ b/Classes/bfs.py @@ -0,0 +1,90 @@ +from collections import deque + + +class BFS: + """ + Initializes a BFS (Breadth-First Search) solver object. + """ + + def __init__(self): + self.visited = set() + self.queue = deque() + self.parent = {} # Dictionary to store parent relationships for path reconstruction + self.path = [] # List to store the final path + + """ + Returns a list of valid neighbours for a given cell in the maze. + + :param maze: The maze grid. + :param i: The row index of the current cell. + :param j: The column index of the current cell. + :return: A list of valid neighbours. + """ + + def get_neighbours(self, maze, i, j): + neighbours = [] + directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] + + for direction in directions: + ni, nj = i + direction[0], j + direction[1] + if 0 <= ni < len(maze) and 0 <= nj < len(maze[0]) and maze[ni][nj] != 1: + neighbours.append((ni, nj)) + + return neighbours + + """ + Performs Breadth-First Search on the maze to find a path from the start to the goal. + + :param maze: The maze grid. + :param start: The starting position. + :param goal: The goal position. + :return: True if a path is found, False otherwise. + """ + + def bfs(self, maze, start, goal): + self.visited.clear() + self.queue.clear() + self.parent.clear() # Clear parent dictionary + self.path.clear() # Clear path + + self.queue.append(start) + self.visited.add(start) + + while self.queue: + current = self.queue.popleft() + + if current == goal: + self.construct_path(start, goal) + return True # Goal found + + for neighbour in self.get_neighbours(maze, current[0], current[1]): + if neighbour not in self.visited: + self.queue.append(neighbour) + self.visited.add(neighbour) + self.parent[neighbour] = current + + return False # Goal not reached + + """ + Constructs the path from the start to the goal using the parent dictionary. + + :param start: The starting position. + :param goal: The goal position. + """ + + def construct_path(self, start, goal): + current = goal + while current != start: + self.path.append(current) + current = self.parent[current] + + self.path.append(start) + + """ + Returns the reconstructed path from start to goal. + + :return: The path as a list of positions. + """ + + def get_path(self): + return list(reversed(self.path)) diff --git a/Classes/button.py b/Classes/button.py new file mode 100644 index 0000000000000000000000000000000000000000..d26239e49b141d45ef5634069f717fffcf0fbe7e --- /dev/null +++ b/Classes/button.py @@ -0,0 +1,63 @@ +from config import * + + +class Button: + """ + Initializes a Button object with specified attributes. + + :param image: The image representing the button. + :param x: The x-coordinate of the button's position. + :param y: The y-coordinate of the button's position. + :param level: The level associated with the button. + :param player_controller: The player controller associated with the button. + """ + + def __init__(self, image, x, y, level, player_controller): + self.image = image + self.x = x + self.y = y + self.level = level + self.is_clicked = False + self.player_controller = player_controller + + """ + Draws the button on the game screen. + + This method scales the button's image, positions it, and blits it onto the screen. + """ + def draw(self): + width = self.image.get_width() + height = self.image.get_height() + scaled_image = pygame.transform.scale(self.image, (int(width * 0.7), int(height * 0.7))) + image_rect = scaled_image.get_rect() + image_rect.right = self.x + image_rect.top = self.y + screen.blit(scaled_image, image_rect) + + """ + Handles mouse clicks on the button. + + If the button is clicked, it sets the maze level, resets the player position, and updates the 'is_clicked' + attribute. + + :param mouse_pos: The current mouse position (x, y). + :param maze: The maze associated with the button. + """ + def handle_mouse_click(self, mouse_pos, maze): + if self.check_click(mouse_pos): + maze.set_level(self.level) + self.player_controller.reset_player_position() + self.is_clicked = True + else: + self.is_clicked = False + + """ + Checks if the mouse click is within the button's boundaries. + + :param mouse_pos: The current mouse position (x, y). + :return: True if the mouse click is within the button's boundaries, False otherwise. + """ + def check_click(self, mouse_pos): + x, y = mouse_pos + return self.x <= x <= self.x + self.image.get_width() and self.y <= y <= self.y + self.image.get_height() + diff --git a/Classes/config.py b/Classes/config.py new file mode 100644 index 0000000000000000000000000000000000000000..25fad33e2de2c473fd05dfce9ceee1730f61a158 --- /dev/null +++ b/Classes/config.py @@ -0,0 +1,75 @@ +import pygame + +TILE_SIZE = 64 +screen = pygame.display.set_mode([640, 512]) + +timer = pygame.time.Clock() +FPS = 60 + +small_maze = [ + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 4, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 4, 1], + [1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1], + [1, 0, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1], + [1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1], + [1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1], + [1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1], + [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1], + [1, 0, 1, 0, 1, 0, 2, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1], + [1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1], + [1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1], + [1, 4, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] +] + +medium_maze = [ + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1], + [1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1], + [1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 1], + [1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1], + [1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1], + [1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1], + [1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 4, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] +] + +large_maze = [ + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 1, 3, 0, 0, 0, 0, 0, 4, 1], + [1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1], + [1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1], + [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1], + [1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1], + [1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 2, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 1], + [1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 2, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1], + [1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1], + [1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], +] + +levels = [small_maze, medium_maze, large_maze] +current_level = levels[0] +player_model = pygame.Rect(TILE_SIZE, TILE_SIZE, TILE_SIZE, TILE_SIZE) +tiles = [pygame.image.load('blank.png'), pygame.image.load('wall.png'), pygame.image.load('blank.png'), + pygame.image.load('goal.png'), pygame.image.load('blank.png')] +easy_button = pygame.image.load('button_easy.png').convert_alpha() +normal_button = pygame.image.load('button_normal.png').convert_alpha() +hard_button = pygame.image.load('button_hard.png').convert_alpha() +WIDTH, HEIGHT = TILE_SIZE * len(current_level[0]), TILE_SIZE * len(current_level) + diff --git a/Classes/dfs.py b/Classes/dfs.py new file mode 100644 index 0000000000000000000000000000000000000000..b01cdd3b592dd32bd880ecb1545eccc400c85815 --- /dev/null +++ b/Classes/dfs.py @@ -0,0 +1,70 @@ + +class DFS: + """ + Initializes a DFS (Depth-First Search) solver object. + """ + def __init__(self): + self.visited = set() + self.path = [] + + """ + Returns a list of valid neighbours for a given cell in the maze. + + :param maze: The maze grid. + :param i: The row index of the current cell. + :param j: The column index of the current cell. + :return: A list of valid neighbours. + """ + def get_neighbours(self, maze, i, j): + neighbours = [] + directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] + + for direction in directions: + ni, nj = i + direction[0], j + direction[1] + if 0 <= ni < len(maze) and 0 <= nj < len(maze[0]) and maze[ni][nj] != 1: + neighbours.append((ni, nj)) + + return neighbours + + """ + Performs Depth-First Search on the maze to find a path from the start to the goal. + + :param maze: The maze grid. + :param start: The starting position. + :param goal: The goal position. + :return: True if a path is found, False otherwise. + """ + def dfs(self, maze, start, goal): + self.visited.clear() + self.path.clear() + return self._dfs(maze, start, goal) + + """ + Recursive helper function for DFS. + + :param maze: The maze grid. + :param current: The current position. + :param goal: The goal position. + :return: True if a path is found, False otherwise. + """ + def _dfs(self, maze, current, goal): + if current == goal: + self.path.append(current) + return True + + self.visited.add(current) + + for neighbour in self.get_neighbours(maze, current[0], current[1]): + if neighbour not in self.visited and self._dfs(maze, neighbour, goal): + self.path.append(current) + return True + + return False + + """ + Returns the reconstructed path from start to goal. + + :return: The path as a list of positions. + """ + def get_path(self): + return list(reversed(self.path)) diff --git a/Classes/dijkstra.py b/Classes/dijkstra.py new file mode 100644 index 0000000000000000000000000000000000000000..3d2928442ba6f6685821e206731c000613564a00 --- /dev/null +++ b/Classes/dijkstra.py @@ -0,0 +1,78 @@ +import heapq + + +class Dijkstra: + """ + Initializes a Dijkstra object. + + :param maze: The maze used for navigation and solving. + """ + def __init__(self, maze): + self.maze = maze + + """ + Calculates the cost of moving from one cell to another based on the cell type. + + :param cell_type: The type of cell (0 for empty, 1 for wall, 2 for goal, etc.). + :return: The cost of moving to the cell. + """ + def calculate_cost(self, cell): + if cell == 0: + return 1 + elif cell == 2: + return 100 + elif cell == 3: + return 1 + else: + return float('inf') + + """ + Finds the shortest path from the player's current position to a specific location using Dijkstra's algorithm. + + :param start_position: The (row, column) position of the start. + :param goal_position: The (row, column) position of the goal. + :return: The shortest path from the start position to the goal position. + """ + def find_shortest_path(self, start_position, goal_position): + frontier = [(0, start_position)] + came_from = {} + cost_so_far = {} + came_from[start_position] = None + cost_so_far[start_position] = 0 + + while frontier: + current_cost, current = heapq.heappop(frontier) + + if current == goal_position: + break + + for next in self.get_neighbors(current): + new_cost = cost_so_far[current] + self.calculate_cost(self.maze.current_level[next[0]][next[1]]) + if next not in cost_so_far or new_cost < cost_so_far[next]: + cost_so_far[next] = new_cost + priority = new_cost + heapq.heappush(frontier, (priority, next)) + came_from[next] = current + + current = goal_position + path = [] + while current != start_position: + path.append(current) + current = came_from[current] + path.append(start_position) + path.reverse() + return path + + """ + Gets the valid neighboring cells of a given cell. + + :param cell: The (row, column) position of the cell. + :return: A list of valid neighboring cells. + """ + def get_neighbors(self, cell): + neighbors = [] + for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]: + x, y = cell[1] + dx, cell[0] + dy + if 0 <= x < len(self.maze.current_level[0]) and 0 <= y < len(self.maze.current_level): + neighbors.append((y, x)) + return neighbors \ No newline at end of file diff --git a/Classes/enemy.py b/Classes/enemy.py new file mode 100644 index 0000000000000000000000000000000000000000..86df1c22eba2258054ca0e277a5e908febd312bd --- /dev/null +++ b/Classes/enemy.py @@ -0,0 +1,50 @@ +import random +from config import * + +""" +Class representing an enemy agent in the game. +""" + + +class Enemy: + """ + Initialise the Enemy object. + + :param maze (Maze): The maze object representing the game environment. + :param x (int): The x-coordinate of the enemy's position. + :param y (int): The y-coordinate of the enemy's position. + """ + + def __init__(self, maze): + self.maze = maze + self.x, self.y = self.reset_position() + + """ + Reset the position of the enemy to a valid starting position in the maze. + + :return: tuple: A tuple containing the x and y coordinates of the enemy's position. + """ + + def reset_position(self): + while True: + num_rows = self.maze.num_rows() + num_cols = self.maze.num_columns() + + random_row = random.randint(0, num_rows - 1) + + start_col = num_cols - 2 + + self.x = start_col * TILE_SIZE + self.y = random_row * TILE_SIZE + + if self.maze[random_row][start_col] != 1: + return self.x, self.y + + """ + Draw the enemy on the screen. + + :param screen: The Pygame screen surface to draw on. + """ + + def draw(self, screen): + pygame.draw.rect(screen, 'blue', (self.x, self.y, TILE_SIZE, TILE_SIZE)) diff --git a/Classes/expectimax.py b/Classes/expectimax.py new file mode 100644 index 0000000000000000000000000000000000000000..3efea7af32302f056e64bce668491a9622b78744 --- /dev/null +++ b/Classes/expectimax.py @@ -0,0 +1,43 @@ +from minmax import MinMax + + +class Expectimax(MinMax): + """ + Initializes an Expectimax object. + + :param maze: The maze used for navigation and solving. + """ + def __init__(self, maze): + super().__init__(maze) + + """ + Performs the Expectimax algorithm to determine the best move the enemy can take. + + :param player_position: The current position of the player agent. + :param enemy_position: The current position of the enemy agent. + :param depth: The depth of the Expectimax search tree. + :param maximising_player: True if maximising player (enemy), False if minimising player (player). + :return: The best move for the enemy agent. + """ + def expectimax(self, player_position, enemy_position, depth, maximising_player): + if depth == 0 or self.game_over(): + # Evaluate the current game state + return self.evaluate(player_position, enemy_position) + + if maximising_player: + max_eval = float('-inf') + valid_moves = self.get_valid_moves(enemy_position) + for move in valid_moves: + if self.is_valid_move(move): + eval = self.expectimax(player_position, move, depth - 1, False) + max_eval = max(max_eval, eval) + return max_eval + else: + total_eval = 0 + valid_moves = self.get_valid_moves(player_position) + num_moves = len(valid_moves) + for move in valid_moves: + if self.is_valid_move(move): + eval = self.expectimax(move, enemy_position, depth - 1, True) + total_eval += eval + return total_eval / num_moves \ No newline at end of file diff --git a/Classes/game.py b/Classes/game.py new file mode 100644 index 0000000000000000000000000000000000000000..469b138667f6a46a532eed739475418b4cab0249 --- /dev/null +++ b/Classes/game.py @@ -0,0 +1,50 @@ +""" +Class representing the main game logic. +""" +class Game: + """ + Initialize the Game object. + + :param maze: The maze grid representing the game environment. + """ + def __init__(self, maze): + self.maze = maze + self.player_position = (1, 1) + self.enemy_positions = self.get_enemy_positions() + + """ + Find the current position of the player in the maze. + + :return: The (row, column) position of the player, or None if not found. + """ + def get_player_position(self): + for i in range(len(self.maze)): + for j in range(len(self.maze[0])): + if self.maze[i][j] == 0: + return i, j + return None + + """ + Find the positions of the enemy agents in the maze. + + :return: A list containing the positions of the enemy agents. + """ + def get_enemy_positions(self): + # Implement logic to find the opponents' positions in the maze + pass + + """ + Update the positions of the player and enemy agents based on the current state of the maze. + """ + def update_positions(self): + self.player_position = self.get_player_position() + self.enemy_positions = self.get_enemy_positions() + + """ + Check if the game is over. + + :return: True if the game is over, False otherwise. + """ + def check_game_over(self): + # Implement logic to check if the game is over (e.g., player reached the goal or caught by opponent) + pass diff --git a/Classes/main.py b/Classes/main.py new file mode 100644 index 0000000000000000000000000000000000000000..8f69e296088c9edfafec5ea0268976200da40733 --- /dev/null +++ b/Classes/main.py @@ -0,0 +1,65 @@ +from maze import * +from player import * +from button import * +from playerController import * +from dfs import * +from bfs import * +from dijkstra import * +from aStar import * +from enemy import * +from minmax import * + +pygame.init() +maze = Maze(small_maze) +player = Player(TILE_SIZE, TILE_SIZE) +enemy1 = Enemy(maze) +depth = 3 + +player_controller = PlayerController(player, maze) +easy_button = Button(easy_button, 150, 10, small_maze, player_controller) +normal_button = Button(normal_button, 300, 10, medium_maze, player_controller) +hard_button = Button(hard_button, 426, 10, large_maze, player_controller) +dfs_solver = DFS() +bfs_solver = BFS() +dijkstra_solver = Dijkstra(maze) +aStar_solver = AStar(maze) +minmax_solver = MinMax(maze) +player_controller.set_dfs_solver(dfs_solver) +player_controller.set_bfs_solver(bfs_solver) +player_controller.set_dijkstra_solver(dijkstra_solver) +player_controller.set_astar_solver(aStar_solver) +# best_move = minmax_solver.minmax(player_position, enemy_position, depth, True) + +run = True +while run: + timer.tick(FPS) + maze.draw() + player.draw(screen) + enemy1.draw(screen) + easy_button.draw() + normal_button.draw() + hard_button.draw() + + # player_controller.move_to_goal_dfs() + player_controller.move_to_goal_bfs() + # player_controller.move_to_goal_dijkstra() + # player_controller.move_to_goal_astar() + + direction = pygame.key.get_pressed() + player_controller.move_player(direction) + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + run = False + elif event.type == pygame.MOUSEBUTTONDOWN: + mouse_pos = pygame.mouse.get_pos() + easy_button.handle_mouse_click(mouse_pos, maze) + normal_button.handle_mouse_click(mouse_pos, maze) + hard_button.handle_mouse_click(mouse_pos, maze) + + if easy_button.is_clicked or normal_button.is_clicked or hard_button.is_clicked: + player_controller.reset_player_position() + enemy1.reset_position() + + pygame.display.flip() +pygame.quit() diff --git a/Classes/maze.py b/Classes/maze.py new file mode 100644 index 0000000000000000000000000000000000000000..8dc1cabd5af3c3c0892870b8e622a673bec2162f --- /dev/null +++ b/Classes/maze.py @@ -0,0 +1,64 @@ +from config import * + + +class Maze: + """ + Initializes a Maze object with an initial maze level. + + :param initial_level: The initial maze level. + """ + def __init__(self, initial_level): + self.current_level = initial_level + self.update_screen_size() + + """ + Get the length of the maze, which is the number of rows in the maze. + + :return: The number of rows in the maze. + """ + def num_rows(self): + return len(self.current_level) + + """ + Get the number of columns in the maze. + + :return: The number of columns in the maze. + """ + def num_columns(self): + return len(self.current_level[0]) + + """ + Retrieves the item at the specified index. + + :param index: The index of the item to retrieve. + :return: The item at the specified index. + """ + def __getitem__(self, index): + return self.current_level[index] + + """ + Sets a new maze level and updates the screen size accordingly. + + :param new_level: The new maze level. + """ + def set_level(self, new_level): + self.current_level = new_level + self.update_screen_size() + + """ + Updates the size of the game screen based on the current maze level. + """ + def update_screen_size(self): + screen_size = (len(self.current_level[0]) * TILE_SIZE, len(self.current_level) * TILE_SIZE) + screen = pygame.display.set_mode(screen_size) + + """ + Draws the current maze level on the game screen. + """ + def draw(self): + for row in range(len(self.current_level)): + for column in range(len(self.current_level[row])): + x = column * TILE_SIZE + y = row * TILE_SIZE + tile = tiles[self.current_level[row][column]] + screen.blit(tile, (x, y)) diff --git a/Classes/minmax.py b/Classes/minmax.py new file mode 100644 index 0000000000000000000000000000000000000000..cb745b9617430b80b351c62dd5f5546a6d1b4462 --- /dev/null +++ b/Classes/minmax.py @@ -0,0 +1,93 @@ +class MinMax: + """ + Initializes a MinMax object. + + :param maze: The maze used for navigation and solving. + """ + def __init__(self, maze): + self.maze = maze + + """ + Gets the valid moves that an enemy agent can take. + + :param player_position: The current position of the player agent. + :return: A list of valid moves for the enemy agent. + """ + def get_valid_moves(self, player_position): + valid_moves = [] + for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]: + x, y = player_position[1] + dx, player_position[0] + dy + if (0 <= x < len(self.maze.current_level[0]) and 0 <= y < len(self.maze.current_level) and + self.maze.current_level[y][x] != 1): + valid_moves.append((y, x)) + return valid_moves + + """ + Checks if a move to a given position is valid. + + :param position: The position to move to. + :return: True if the move is valid, False otherwise. + """ + def is_valid_move(self, position): + x, y = position[1], position[0] + return (0 <= x < len(self.maze.current_level[0]) and 0 <= y < len(self.maze.current_level) and + self.maze.current_level[y][x] != 1) + + """ + Performs the MinMax algorithm with Alpha-beta pruning to determine the best move the enemy can take. + + :param player_position: The current position of the player agent. + :param enemy_position: The current position of the enemy agent. + :param depth: The depth of the MinMax search tree. + :param alpha: The best value that the maximizing player currently can guarantee. + :param beta: The best value that the minimizing player currently can guarantee. + :param maximizing_player: True if maximizing player (enemy), False if minimizing player (player). + :return: The best move for the enemy agent. + """ + def minmax(self, player_position, enemy_position, depth, alpha, beta, maximizing_player): + if depth == 0 or self.game_over(): + # Evaluate the current game state + return self.evaluate(player_position, enemy_position) + + if maximizing_player: + max_eval = float('-inf') + valid_moves = self.get_valid_moves(enemy_position) + for move in valid_moves: + if self.is_valid_move(move): + eval = self.minmax(player_position, move, depth - 1, alpha, beta, False) + max_eval = max(max_eval, eval) + alpha = max(alpha, eval) + if beta <= alpha: + break + return max_eval + else: + min_eval = float('inf') + valid_moves = self.get_valid_moves(player_position) + for move in valid_moves: + if self.is_valid_move(move): + eval = self.minmax(move, enemy_position, depth - 1, alpha, beta, True) + min_eval = min(min_eval, eval) + beta = min(beta, eval) + if beta <= alpha: + break + return min_eval + + """ + Evaluate the current game state. + + :param player_position: The current position of the player agent. + :param enemy_position: The current position of the enemy agent. + :return: The evaluation of the current game state. + """ + def evaluate(self, player_position, enemy_position): + # Implement the evaluation logic here + pass + + """ + Check if the game is over. + + :return: True if the game is over, False otherwise. + """ + def game_over(self): + # Implement the game over condition here + pass diff --git a/Classes/player.py b/Classes/player.py new file mode 100644 index 0000000000000000000000000000000000000000..4a4840a39064f6c82a600714b3437542c3c7ef28 --- /dev/null +++ b/Classes/player.py @@ -0,0 +1,22 @@ +from config import * + + +class Player: + """ + Initialises a Player object with a specified position. + + :param x: The x-coordinate of the player's position. + :param y: The y-coordinate of the player's position + """ + def __init__(self, x, y): + self.x = x + self.y = y + + """ + Draws the player on the game screen using Pygame. + + This method utilizes the Pygame library to draw a white rectangle representing the player + at the specified (x, y) coordinates with the size of TILE_SIZE. + """ + def draw(self, screen): + pygame.draw.rect(screen, 'white', (self.x, self.y, TILE_SIZE, TILE_SIZE)) diff --git a/Classes/playerController.py b/Classes/playerController.py new file mode 100644 index 0000000000000000000000000000000000000000..c591ce25c0310e289b50da5a6c5f218af3b5ffc1 --- /dev/null +++ b/Classes/playerController.py @@ -0,0 +1,223 @@ +from config import * +from dfs import * +from bfs import * +from dijkstra import * +from game import * + + +class PlayerController: + """ + Initializes a PlayerController object. + + :param player: The player controlled by this controller. + :param maze: The maze used for navigation and solving. + """ + def __init__(self, player, maze): + self.player = player + self.maze = maze + self.player_position = (1, 1) + self.dfs_solver = DFS() + self.bfs_solver = BFS() + self.dijkstra_solver = None + self.astar_solver = None + + """ + Moves the player in the specified direction based on keyboard input. + + :param direction: A dictionary representing the keys pressed. + """ + def move_player(self, direction): + new_x = self.player.x + new_y = self.player.y + + if direction[pygame.K_UP]: + new_y -= TILE_SIZE + new_position = (self.player_position[0] - 1, self.player_position[1]) + elif direction[pygame.K_DOWN]: + new_y += TILE_SIZE + new_position = (self.player_position[0] + 1, self.player_position[1]) + elif direction[pygame.K_LEFT]: + new_x -= TILE_SIZE + new_position = (self.player_position[0], self.player_position[1] - 1) + elif direction[pygame.K_RIGHT]: + new_x += TILE_SIZE + new_position = (self.player_position[0], self.player_position[1] + 1) + else: + return + + if self.is_valid_position(new_position): + self.player_position = new_position + + if self.check_collision(new_x, new_y): + self.player.x = new_x + self.player.y = new_y + + """ + Checks if a given position is valid within the maze. + + :param position: The position to check in the form of (x, y) coordinates. + :return: True if the position is valid and does not collide with walls, False otherwise. + """ + def is_valid_position(self, position): + x, y = position + return 0 <= x < len(self.maze.current_level) and 0 <= y < len(self.maze.current_level[0]) and \ + self.maze.current_level[x][y] != 1 + + """ + Checks if the player will collide with walls at the specified position. + + :param x: The x-coordinate of the potential position. + :param y: The y-coordinate of the potential position. + :return: True if collision is detected, False otherwise. + """ + def check_collision(self, x, y): + row = y // TILE_SIZE + column = x // TILE_SIZE + return self.maze.current_level[row][column] != 1 + + """Resets the player's position to the starting point.""" + def reset_player_position(self): + self.player.x = TILE_SIZE + self.player.y = TILE_SIZE + + """ + Sets the DFS solver used by the controller. + + :param dfs_solver: The DFS solver object. + """ + def set_dfs_solver(self, dfs_solver): + self.dfs_solver = dfs_solver + + """ + Sets the BFS solver used by the controller. + + :param bfs_solver: The BFS solver object. + """ + def set_bfs_solver(self, bfs_solver): + self.bfs_solver = bfs_solver + + """ + Sets the Dijkstra solver used by the controller. + + :param dijkstra_solver: The Dijkstra solver object. + """ + def set_dijkstra_solver(self, dijkstra_solver): + self.dijkstra_solver = dijkstra_solver + + """ + Sets the A* solver used by the controller. + + :param dijkstra_solver: The A* solver object. + """ + def set_astar_solver(self, astar_solver): + self.astar_solver = astar_solver + + """ + Moves the player towards the goal using DFS. + """ + def move_to_goal_dfs(self): + start_position = (self.player.y // TILE_SIZE, self.player.x // TILE_SIZE) + goal_position = self.find_goal_position() + + if goal_position: + if self.dfs_solver.dfs(self.maze.current_level, start_position, goal_position): + path = self.dfs_solver.get_path() + self.follow_path(path) + + """ + Moves the player towards the goal using BFS pathfinding. + """ + def move_to_goal_bfs(self): + start_position = (self.player.y // TILE_SIZE, self.player.x // TILE_SIZE) + goal_position = self.find_goal_position() + + if goal_position: + if self.bfs_solver.bfs(self.maze.current_level, start_position, goal_position): + path = self.bfs_solver.get_path() + self.follow_path(path) + + """ + Moves the player towards the goal using Dijkstra's algorithm. + """ + def move_to_goal_dijkstra(self): + if self.dijkstra_solver is not None: + start_position = (self.player.y // TILE_SIZE, self.player.x // TILE_SIZE) + goal_position = self.find_goal_position() + + if goal_position: + path = self.dijkstra_solver.find_shortest_path(start_position, goal_position) + if path: + self.follow_path(path) + + """ + Moves the player to all 4 corners using A* algorithm. + """ + + def move_to_goal_astar(self): + if self.astar_solver is None: + print("A* solver not initialized. Please call set_astar_solver first.") + return + + if not self.maze: + print("Maze not initialized.") + return + + start_position = (self.player.y // TILE_SIZE, self.player.x // TILE_SIZE) + goal_position = self.astar_solver.find_goal_position(self.maze) + + if goal_position: + path = self.astar_solver.find_shortest_path(self.astar_solver.heuristic1) + if path: + self.follow_path(path) + + """ + Moves the player along the specified path. + + :param path: The path to follow. + """ + def follow_path(self, path): + for position in path: + goal_y, goal_x = position + target_y, target_x = goal_y * TILE_SIZE, goal_x * TILE_SIZE + + while self.player.x != target_x or self.player.y != target_y: + direction_x = 1 if target_x > self.player.x else -1 if target_x < self.player.x else 0 + direction_y = 1 if target_y > self.player.y else -1 if target_y < self.player.y else 0 + + new_x, new_y = self.player.x + direction_x * TILE_SIZE, self.player.y + direction_y * TILE_SIZE + + if self.maze.current_level[new_y // TILE_SIZE][new_x // TILE_SIZE] != 1: + # Draw the path on the screen + self.maze.draw() + self.draw_path(path) + + # Move the player + self.player.x, self.player.y = new_x, new_y + + pygame.display.flip() + pygame.time.delay(100) + + """ + Draws the path on the screen for visualization. + + :param path: The path to draw. + """ + def draw_path(self, path): + for position in path: + pygame.draw.rect( + screen, + (0, 255, 0), # Green color for the path + (position[1] * TILE_SIZE, position[0] * TILE_SIZE, TILE_SIZE, TILE_SIZE), + ) + + """ + Finds the position of the goal in the maze. + + :return: The (row, column) position of the goal or None if not found. + """ + def find_goal_position(self): + for i in range(len(self.maze.current_level)): + for j in range(len(self.maze.current_level[0])): + if self.maze.current_level[i][j] == 3: + return i, j + return None diff --git a/Images/blank.png b/Images/blank.png new file mode 100644 index 0000000000000000000000000000000000000000..50c9e2a0f81462186450944aba69ee033b822c4a Binary files /dev/null and b/Images/blank.png differ diff --git a/Images/button_easy.png b/Images/button_easy.png new file mode 100644 index 0000000000000000000000000000000000000000..4601185db715287698992947896da6796e7cf000 Binary files /dev/null and b/Images/button_easy.png differ diff --git a/Images/button_hard.png b/Images/button_hard.png new file mode 100644 index 0000000000000000000000000000000000000000..88cf8b3ae18ed0ce2479f2873a0aa554bde73d4c Binary files /dev/null and b/Images/button_hard.png differ diff --git a/Images/button_normal.png b/Images/button_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..821d5a79d1c98f2a6680072c39c502a7c0bcef7d Binary files /dev/null and b/Images/button_normal.png differ diff --git a/Images/enemy.png b/Images/enemy.png new file mode 100644 index 0000000000000000000000000000000000000000..4c40921e35a8caf9813b8644e7dcc76b55487a0b Binary files /dev/null and b/Images/enemy.png differ diff --git a/Images/goal.png b/Images/goal.png new file mode 100644 index 0000000000000000000000000000000000000000..0a650e44fdceee5be61a68040c1b8838ae83716a Binary files /dev/null and b/Images/goal.png differ diff --git a/Images/wall.png b/Images/wall.png new file mode 100644 index 0000000000000000000000000000000000000000..eb06cd2a45741ab3b9b0d54f0fa47a7fd6493295 Binary files /dev/null and b/Images/wall.png differ diff --git a/Testing/testAStar.py b/Testing/testAStar.py new file mode 100644 index 0000000000000000000000000000000000000000..1d98f7205f82ab795181044ad59934998f0e9de7 --- /dev/null +++ b/Testing/testAStar.py @@ -0,0 +1,68 @@ +import unittest +from aStar import AStar + + +class TestAStar(unittest.TestCase): + def setUp(self): + self.maze = [ + [1, 0, 0, 0], + [0, 1, 1, 0], + [0, 0, 0, 1], + [1, 0, 0, 0] + ] + self.a_star_solver = AStar(self.maze) + + def test_heuristic1(self): + current_position = (1, 1) + visited_corners = set() + heuristic_result = self.a_star_solver.heuristic1(current_position, self.a_star_solver.corners, visited_corners) + self.assertEqual(heuristic_result, 3) + + current_position = (2, 2) + visited_corners = set() + heuristic_result = self.a_star_solver.heuristic1(current_position, self.a_star_solver.corners, visited_corners) + self.assertEqual(heuristic_result, ...) + + def test_heuristic2(self): + current_position = (1, 1) + visited_corners = set() + heuristic_result = self.a_star_solver.heuristic2(current_position, self.a_star_solver.corners, visited_corners) + self.assertEqual(heuristic_result, ...) + + current_position = (2, 2) + visited_corners = set() + heuristic_result = self.a_star_solver.heuristic2(current_position, self.a_star_solver.corners, visited_corners) + self.assertEqual(heuristic_result, ...) + + def test_all_corners_visited(self): + corners = {(1, 1), (2, 2), (3, 3)} + visited_corners = {(1, 1), (2, 2)} + result = self.a_star_solver.all_corners_visited(corners, visited_corners) + self.assertFalse(result) + + def test_find_goal_position(self): + maze_with_goal = ... + goal_position = self.a_star_solver.find_goal_position(maze_with_goal) + self.assertIsNotNone(goal_position) + + maze_without_goal = ... + goal_position = self.a_star_solver.find_goal_position(maze_without_goal) + self.assertIsNone(goal_position) + + def test_find_shortest_path_with_valid_heuristic(self): + def valid_heuristic(current, corners, visited_corners): + return 0 + + path = self.a_star_solver.find_shortest_path(valid_heuristic) + self.assertIsNotNone(path) + + def test_find_shortest_path_with_invalid_heuristic(self): + def invalid_heuristic(current, corners, visited_corners): + return None + + path = self.a_star_solver.find_shortest_path(invalid_heuristic) + self.assertIsNone(path) + + +if __name__ == '__main__': + unittest.main() diff --git a/Testing/testBFS.py b/Testing/testBFS.py new file mode 100644 index 0000000000000000000000000000000000000000..3d5f40a612fb751e77bbb5a6e97c0df132cf16a6 --- /dev/null +++ b/Testing/testBFS.py @@ -0,0 +1,109 @@ +import unittest +from collections import deque +from bfs import BFS + + +class TestBFS(unittest.TestCase): + def setUp(self): + self.bfs_solver = BFS() + + def test_init(self): + bfs = BFS() + self.assertEqual(len(bfs.visited), 0) + self.assertIsInstance(bfs.queue, deque) + self.assertEqual(len(bfs.parent), 0) + self.assertEqual(len(bfs.path), 0) + + def test_get_neighbours(self): + bfs = BFS() + maze = [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 0] + ] + neighbours = bfs.get_neighbours(maze, 1, 1) + expected_neighbours = [(0, 1), (1, 0), (1, 2), (2, 1)] + self.assertEqual(neighbours, expected_neighbours) + + maze = [ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1] + ] + neighbours = bfs.get_neighbours(maze, 1, 1) + self.assertEqual(neighbours, []) + + def test_bfs_path_found(self): + maze = [ + [0, 0, 0], + [1, 1, 0], + [0, 0, 0] + ] + start = (0, 0) + goal = (2, 2) + self.assertTrue(self.bfs_solver.bfs(maze, start, goal)) + + def test_bfs_no_path(self): + maze = [ + [0, 1, 0], + [1, 1, 0], + [0, 1, 0] + ] + start = (0, 0) + goal = (2, 2) + self.assertFalse(self.bfs_solver.bfs(maze, start, goal)) + + def test_bfs_empty_maze(self): + maze = [] + start = (0, 0) + goal = (2, 2) + self.assertFalse(self.bfs_solver.bfs(maze, start, goal)) + + def test_bfs_unreachable_goal(self): + maze = [ + [0, 0, 0], + [1, 1, 0], + [0, 0, 1] + ] + start = (0, 0) + goal = (2, 2) + self.assertFalse(self.bfs_solver.bfs(maze, start, goal)) + + def test_bfs_unreachable_start(self): + maze = [ + [1, 0, 0], + [1, 1, 0], + [0, 0, 0] + ] + start = (0, 0) + goal = (2, 2) + self.assertFalse(self.bfs_solver.bfs(maze, start, goal)) + + def test_bfs_single_cell_maze(self): + maze = [[0]] + start = (0, 0) + goal = (0, 0) + self.assertTrue(self.bfs_solver.bfs(maze, start, goal)) + + def test_construct_path(self): + start = (0, 0) + goal = (2, 2) + parent = { + (1, 1): (0, 1), + (1, 2): (1, 1), + (2, 2): (1, 2), + (0, 1): (0, 0) + } + self.bfs_solver.parent = parent + self.bfs_solver.construct_path(start, goal) + expected_path = [(0, 0), (0, 1), (1, 1), (1, 2), (2, 2)] + self.assertEqual(self.bfs_solver.path, expected_path) + + def test_get_path(self): + self.bfs_solver.path = [(0, 0), (0, 1), (1, 1), (1, 2), (2, 2)] + expected_path = [(2, 2), (1, 2), (1, 1), (0, 1), (0, 0)] + self.assertEqual(self.bfs_solver.get_path(), expected_path) + + +if __name__ == "__main__": + unittest.main() diff --git a/Testing/testButton.py b/Testing/testButton.py new file mode 100644 index 0000000000000000000000000000000000000000..c191dbb79ffa96b48d751bcae0a48949d6618f6c --- /dev/null +++ b/Testing/testButton.py @@ -0,0 +1,56 @@ +import unittest +from unittest.mock import Mock +from button import Button + + +class TestButton(unittest.TestCase): + def setUp(self): + pygame = Mock() + self.mock_screen = pygame.display.set_mode.return_value + self.mock_player_controller = Mock() + self.mock_maze = Mock() + self.mock_image = Mock() + self.mock_image.get_width.return_value = 100 + self.mock_image.get_height.return_value = 50 + self.button = Button(self.mock_image, 100, 100, 1, None) + + def test_draw(self): + mock_image = Mock() + button = Button(mock_image, 100, 100, 1, Mock()) + + button.draw() + + expected_calls = [ + ((mock_image, (100, 100)),), + ] + self.assertEqual(self.mock_screen.blit.call_args_list, expected_calls) + + def test_handle_mouse_click_when_clicked(self): + button = Button(Mock(), 100, 100, 1, self.mock_player_controller) + + button.handle_mouse_click((110, 110), self.mock_maze) + + self.mock_maze.set_level.assert_called_once_with(1) + self.mock_player_controller.reset_player_position.assert_called_once() + self.assertTrue(button.is_clicked) + + def test_handle_mouse_click_when_not_clicked(self): + button = Button(Mock(), 100, 100, 1, self.mock_player_controller) + + button.handle_mouse_click((90, 90), self.mock_maze) + + self.assertFalse(self.mock_maze.set_level.called) + self.assertFalse(self.mock_player_controller.reset_player_position.called) + self.assertFalse(button.is_clicked) + + def test_check_click_within_boundaries(self): + result = self.button.check_click((150, 125)) + self.assertTrue(result) + + def test_check_click_outside_boundaries(self): + result = self.button.check_click((50, 75)) + self.assertFalse(result) + + +if __name__ == "__main__": + unittest.main() diff --git a/Testing/testDFS.py b/Testing/testDFS.py new file mode 100644 index 0000000000000000000000000000000000000000..53afc4f601ee51e5d64849b576ebd9c439b65815 --- /dev/null +++ b/Testing/testDFS.py @@ -0,0 +1,80 @@ +import unittest +from dfs import DFS + + +class TestDFS(unittest.TestCase): + def test_init(self): + dfs = DFS() + self.assertEqual(dfs.visited, set()) + self.assertEqual(dfs.path, []) + + def test_get_neighbours(self): + maze = [ + [0, 0, 0, 1], + [1, 1, 0, 0], + [0, 1, 0, 1], + [0, 0, 0, 0] + ] + dfs = DFS() + + self.assertEqual(dfs.get_neighbours(maze, 0, 0), [(0, 1), (1, 0)]) + self.assertEqual(dfs.get_neighbours(maze, 1, 1), [(1, 2), (2, 1)]) + self.assertEqual(dfs.get_neighbours(maze, 2, 2), [(2, 1), (3, 2)]) + + def test_dfs(self): + maze = [ + [0, 0, 0, 1], + [1, 1, 0, 0], + [0, 1, 0, 1], + [0, 0, 0, 0] + ] + dfs = DFS() + + start = (0, 0) + goal = (3, 3) + self.assertTrue(dfs.dfs(maze, start, goal)) + + start = (0, 0) + goal = (2, 2) + self.assertFalse(dfs.dfs(maze, start, goal)) + + def test__dfs(self): + maze = [ + [0, 0, 0, 1], + [1, 1, 0, 0], + [0, 1, 0, 1], + [0, 0, 0, 0] + ] + dfs = DFS() + + start = (0, 0) + goal = (3, 3) + self.assertTrue(dfs._dfs(maze, start, goal)) + + start = (0, 0) + goal = (2, 2) + self.assertFalse(dfs._dfs(maze, start, goal)) + + def test_get_path(self): + maze = [ + [0, 0, 0, 1], + [1, 1, 0, 0], + [0, 1, 0, 1], + [0, 0, 0, 0] + ] + dfs = DFS() + + start = (0, 0) + goal = (3, 3) + dfs._dfs(maze, start, goal) + expected_path = [(0, 0), (0, 1), (1, 1), (2, 1), (3, 1), (3, 2), (3, 3)] + self.assertEqual(dfs.get_path(), expected_path) + + start = (0, 0) + goal = (2, 2) + dfs._dfs(maze, start, goal) + self.assertEqual(dfs.get_path(), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/Testing/testDijkstra.py b/Testing/testDijkstra.py new file mode 100644 index 0000000000000000000000000000000000000000..b6e6804cf8973bfbc71d1cbe2d6cb602e9951a7a --- /dev/null +++ b/Testing/testDijkstra.py @@ -0,0 +1,79 @@ +import unittest +from dijkstra import Dijkstra + + +class TestDijkstra(unittest.TestCase): + def setUp(self): + self.dijkstra_solver = Dijkstra(None) + + def test_calculate_cost_empty(self): + cell_type = 0 + cost = self.dijkstra_solver.calculate_cost(cell_type) + self.assertEqual(cost, 1) + + def test_calculate_cost_wall(self): + cell_type = 1 + cost = self.dijkstra_solver.calculate_cost(cell_type) + self.assertEqual(cost, float('inf')) + + def test_calculate_cost_goal(self): + cell_type = 2 + cost = self.dijkstra_solver.calculate_cost(cell_type) + self.assertEqual(cost, 100) + + def test_calculate_cost_start(self): + cell_type = 3 + cost = self.dijkstra_solver.calculate_cost(cell_type) + self.assertEqual(cost, 1) + + def test_find_shortest_path_simple(self): + + start_position = (0, 0) + goal_position = (3, 3) + path = self.dijkstra_solver.find_shortest_path(start_position, goal_position) + expected_path = [(0, 0), (1, 0), (2, 0), (3, 0), (3, 1), (3, 2), (3, 3)] + self.assertEqual(path, expected_path) + + def test_find_shortest_path_blocked_goal(self): + + start_position = (0, 0) + goal_position = (3, 3) + path = self.dijkstra_solver.find_shortest_path(start_position, goal_position) + expected_path = [] + self.assertEqual(path, expected_path) + + def test_find_shortest_path_complex(self): + + start_position = (0, 0) + goal_position = (4, 4) + path = self.dijkstra_solver.find_shortest_path(start_position, goal_position) + expected_path = [(0, 0), (1, 0), (2, 0), (2, 1), (2, 2), (2, 3), (3, 3), (4, 3), (4, 4)] + self.assertEqual(path, expected_path) + + def test_get_neighbors_middle(self): + maze = Dijkstra([ + [0, 0, 0, 1], + [1, 1, 0, 0], + [0, 1, 0, 1], + [0, 0, 0, 0] + ]) + cell = (1, 1) + neighbors = maze.get_neighbors(cell) + expected_neighbors = [(0, 1), (2, 1), (1, 0), (1, 2)] + self.assertEqual(neighbors, expected_neighbors) + + def test_get_neighbors_corner(self): + maze = Dijkstra([ + [0, 0, 0, 1], + [1, 1, 0, 0], + [0, 1, 0, 1], + [0, 0, 0, 0] + ]) + cell = (0, 0) + neighbors = maze.get_neighbors(cell) + expected_neighbors = [(1, 0), (0, 1)] + self.assertEqual(neighbors, expected_neighbors) + + +if __name__ == "__main__": + unittest.main() diff --git a/Testing/testEnemy.py b/Testing/testEnemy.py new file mode 100644 index 0000000000000000000000000000000000000000..048d0ff97b33adeaca1e1dc0c18c7eff30aeffcc --- /dev/null +++ b/Testing/testEnemy.py @@ -0,0 +1,46 @@ +import unittest +from unittest.mock import MagicMock, patch +from enemy import Enemy +from config import TILE_SIZE + + +class TestEnemy(unittest.TestCase): + @patch('enemy.random') + def test_init(self, mock_random): + mock_maze = MagicMock() + + mock_random.randint.return_value = 2 + mock_maze.num_rows.return_value = 5 + mock_maze.num_columns.return_value = 6 + mock_maze.__getitem__.return_value = 0 + + enemy = Enemy(mock_maze) + + self.assertEqual(enemy.x, 2 * 32) + self.assertEqual(enemy.y, 0) + + @patch('enemy.random') + def test_reset_position(self, mock_random): + mock_maze = MagicMock() + + mock_random.randint.return_value = 2 + mock_maze.num_rows.return_value = 5 + mock_maze.num_columns.return_value = 6 + mock_maze.__getitem__.return_value = 0 + + enemy = Enemy(mock_maze) + enemy.reset_position() + + self.assertEqual(enemy.x, 2 * 32) + self.assertEqual(enemy.y, 0) + + def test_draw(self): + mock_screen = MagicMock() + + enemy = Enemy(None) + enemy.draw(mock_screen) + mock_screen.assert_called_once_with('blue', (enemy.x, enemy.y, enemy.TILE_SIZE, enemy.TILE_SIZE)) + + +if __name__ == "__main__": + unittest.main() diff --git a/Testing/testExpectimax.py b/Testing/testExpectimax.py new file mode 100644 index 0000000000000000000000000000000000000000..d45612290dc8363954bca8c28c5cdb80d481a6be --- /dev/null +++ b/Testing/testExpectimax.py @@ -0,0 +1,69 @@ +import unittest +from expectimax import Expectimax + + +class TestExpectimax(unittest.TestCase): + + def test_valid_scenario(self): + maze = [[0, 0, 0], + [0, 1, 0], + [0, 0, 0]] + expectimax_solver = Expectimax(maze) + player_position = (0, 0) + enemy_position = (2, 2) + depth = 3 + maximizing_player = True + result = expectimax_solver.expectimax(player_position, enemy_position, depth, maximizing_player) + self.assertIsNotNone(result) + + def test_invalid_scenario_negative_depth(self): + maze = [[0, 0, 0], + [0, 1, 0], + [0, 0, 0]] + expectimax_solver = Expectimax(maze) + player_position = (0, 0) + enemy_position = (2, 2) + depth = -1 + maximizing_player = True + with self.assertRaises(ValueError): + expectimax_solver.expectimax(player_position, enemy_position, depth, maximizing_player) + + def test_no_valid_moves(self): + maze = [[1, 1, 1], + [1, 1, 1], + [1, 1, 1]] + expectimax_solver = Expectimax(maze) + player_position = (0, 0) + enemy_position = (2, 2) + depth = 3 + maximizing_player = True + result = expectimax_solver.expectimax(player_position, enemy_position, depth, maximizing_player) + self.assertEqual(result, 0) + + def test_game_over(self): + maze = [[0, 0, 0], + [0, 0, 0], + [0, 0, 0]] + expectimax_solver = Expectimax(maze) + player_position = (0, 0) + enemy_position = (2, 2) + depth = 3 + maximizing_player = True + result = expectimax_solver.expectimax(player_position, enemy_position, depth, maximizing_player) + self.assertEqual(result, 0) + + def test_max_depth(self): + maze = [[0, 0, 0], + [0, 0, 0], + [0, 0, 0]] + expectimax_solver = Expectimax(maze) + player_position = (0, 0) + enemy_position = (2, 2) + depth = 10 + maximizing_player = True + result = expectimax_solver.expectimax(player_position, enemy_position, depth, maximizing_player) + self.assertIsNotNone(result) + + +if __name__ == '__main__': + unittest.main() diff --git a/Testing/testGame.py b/Testing/testGame.py new file mode 100644 index 0000000000000000000000000000000000000000..dbbfc54c2eb16d4dd496b80802ff488b8c291bd2 --- /dev/null +++ b/Testing/testGame.py @@ -0,0 +1,60 @@ +import unittest +from game import Game + + +class TestGame(unittest.TestCase): + + def setUp(self): + self.maze = [ + [1, 1, 1, 1, 1], + [1, 0, 0, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 0, 0, 1], + [1, 1, 1, 1, 1] + ] + self.game = Game(self.maze) + + def test_init(self): + maze = [ + [1, 1, 1, 1, 1], + [1, 0, 0, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 0, 0, 1], + [1, 1, 1, 1, 1] + ] + + game = Game(maze) + + self.assertEqual(game.maze, maze) + self.assertEqual(game.player_position, (1, 1)) + self.assertEqual(game.enemy_positions, [(2, 2)]) + + def test_get_player_position(self): + maze = [ + [1, 1, 1, 1, 1], + [1, 0, 0, 0, 1], + [1, 0, 1, 0, 1], + [1, 0, 0, 0, 1], + [1, 1, 1, 1, 1] + ] + + game = Game(maze) + player_position = game.get_player_position() + self.assertEqual(player_position, (1, 1)) + + def test_get_enemy_positions(self): + enemy_positions = self.game.get_enemy_positions() + self.assertEqual(enemy_positions, [(2, 2)]) + + def test_update_positions(self): + self.game.update_positions() + self.assertEqual(self.game.player_position, (1, 1)) + self.assertEqual(self.game.enemy_positions, [(2, 2)]) + + def test_check_game_over(self): + game_over = self.game.check_game_over() + self.assertFalse(game_over) + + +if __name__ == "__main__": + unittest.main() diff --git a/Testing/testMaze.py b/Testing/testMaze.py new file mode 100644 index 0000000000000000000000000000000000000000..da1c135eb80caf949fac65e68d7a24d51b5fbb2f --- /dev/null +++ b/Testing/testMaze.py @@ -0,0 +1,92 @@ +import unittest +from unittest.mock import Mock + +import pygame + +from maze import Maze + + +class TestMaze(unittest.TestCase): + def test_num_rows(self): + initial_level = [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0] + ] + maze = Maze(initial_level) + self.assertEqual(maze.num_rows(), 3) + + def test_num_columns(self): + initial_level = [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0] + ] + maze = Maze(initial_level) + self.assertEqual(maze.num_columns(), 4) + + def test_getitem(self): + initial_level = [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0] + ] + maze = Maze(initial_level) + self.assertEqual(maze[0], [0, 0, 0]) + self.assertEqual(maze[1], [0, 0, 0]) + self.assertEqual(maze[2], [0, 0, 0]) + + def test_set_level(self): + initial_level = [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0] + ] + maze = Maze(initial_level) + new_level = [ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1] + ] + maze.set_level(new_level) + self.assertEqual(maze.current_level, new_level) + + def test_update_screen_size(self): + initial_level = [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0] + ] + maze = Maze(initial_level) + pygame.display.set_mode((800, 600)) + maze.update_screen_size() + self.assertEqual(pygame.display.get_surface().get_size(), (3 * 32, 3 * 32)) + + def test_draw(self): + initial_level = [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 0] + ] + maze = Maze(initial_level) + tiles = {0: Mock(), 1: Mock()} + maze.tiles = tiles + + maze.draw() + + expected_calls = [ + ((tiles[0], (0, 0)),), + ((tiles[0], (32, 0)),), + ((tiles[0], (64, 0)),), + ((tiles[0], (0, 32)),), + ((tiles[1], (32, 32)),), + ((tiles[0], (64, 32)),), + ((tiles[0], (0, 64)),), + ((tiles[0], (32, 64)),), + ((tiles[0], (64, 64)),), + ] + self.assertEqual(self.mock_screen.blit.call_args_list, expected_calls) + + +if __name__ == "__main__": + unittest.main() diff --git a/Testing/testMinMax.py b/Testing/testMinMax.py new file mode 100644 index 0000000000000000000000000000000000000000..a3ae88d188803daeb64f27b027b60db0c44b6c1f --- /dev/null +++ b/Testing/testMinMax.py @@ -0,0 +1,112 @@ +import unittest +from minmax import MinMax + + +class TestMinMax(unittest.TestCase): + + def test_get_valid_moves_middle(self): + maze = [[0, 0, 0], + [0, 1, 0], + [0, 0, 0]] + minmax = MinMax(maze) + player_position = (1, 1) + valid_moves = minmax.get_valid_moves(player_position) + expected_moves = [(0, 1), (2, 1), (1, 0), (1, 2)] + self.assertEqual(valid_moves, expected_moves) + + def test_get_valid_moves_corner(self): + maze = [[0, 1, 0], + [1, 1, 0], + [0, 0, 0]] + minmax = MinMax(maze) + player_position = (0, 0) + valid_moves = minmax.get_valid_moves(player_position) + expected_moves = [(1, 0), (0, 1)] + self.assertEqual(valid_moves, expected_moves) + + def test_valid_move(self): + maze = [[0, 0, 0], + [0, 1, 0], + [0, 0, 0]] + minmax = MinMax(maze) + valid_position = (1, 0) + self.assertTrue(minmax.is_valid_move(valid_position)) + + def test_invalid_move_wall(self): + maze = [[0, 1, 0], + [1, 1, 0], + [0, 0, 0]] + minmax = MinMax(maze) + wall_position = (0, 1) + self.assertFalse(minmax.is_valid_move(wall_position)) + + def test_invalid_move_outside(self): + maze = [[0, 0, 0], + [0, 1, 0], + [0, 0, 0]] + minmax = MinMax(maze) + outside_position = (3, 0) + self.assertFalse(minmax.is_valid_move(outside_position)) + + def test_minmax(self): + maze = [[0, 0, 0], + [0, 1, 0], + [0, 0, 0]] + minmax = MinMax(maze) + player_position = (0, 0) + enemy_position = (2, 2) + depth = 3 + alpha = float('-inf') + beta = float('inf') + maximizing_player = True + result = minmax.minmax(player_position, enemy_position, depth, alpha, beta, maximizing_player) + self.assertIsInstance(result, int) + + maze = [[0, 0, 0], + [0, 1, 0], + [0, 0, 0]] + player_position = (0, 0) + enemy_position = (2, 2) + depth = 1 + + result = minmax.minmax(player_position, enemy_position, depth, float('-inf'), float('inf'), maximizing_player) + print("Result:", result) + + def test_evaluate(self): + maze = [[0, 0, 0], + [0, 1, 0], + [0, 0, 0]] + minmax_object = MinMax(maze) + print("Testing the evaluate method...") + + player_position = (1, 1) + enemy_position = (1, 1) + evaluation_result = minmax_object.evaluate(player_position, enemy_position) + print("Evaluation result when player and enemy are at the same position:", evaluation_result) + + player_position = (2, 2) + enemy_position = (1, 1) + evaluation_result = minmax_object.evaluate(player_position, enemy_position) + print("Evaluation result when player is closer to the goal than the enemy:", evaluation_result) + + player_position = (1, 1) + enemy_position = (2, 2) + evaluation_result = minmax_object.evaluate(player_position, enemy_position) + print("Evaluation result when enemy is closer to the goal than the player:", evaluation_result) + + def test_game_over(self): + maze = [[0, 0, 0], + [0, 1, 0], + [0, 0, 0]] + minmax_object = MinMax(maze) + print("Testing the game_over method...") + + game_over_result = minmax_object.game_over() + print("Game over result when the game is not over:", game_over_result) + + game_over_result = minmax_object.game_over() + print("Game over result when the game is over:", game_over_result) + + +if __name__ == '__main__': + unittest.main() diff --git a/Testing/testPlayer.py b/Testing/testPlayer.py new file mode 100644 index 0000000000000000000000000000000000000000..a3386ccba128d2c066951e94242dc0d4a78f333a --- /dev/null +++ b/Testing/testPlayer.py @@ -0,0 +1,18 @@ +import unittest +from unittest.mock import Mock, patch +from config import TILE_SIZE +from player import Player + + +class TestPlayer(unittest.TestCase): + @patch('pygame.draw.rect') + def test_draw(self, mock_rect): + # Mock the screen surface + screen_surface_mock = Mock() + player = Player(10, 20) + player.draw(screen_surface_mock) + mock_rect.assert_called_once_with(screen_surface_mock, 'white', (10, 20, TILE_SIZE, TILE_SIZE)) + + +if __name__ == "__main__": + unittest.main() diff --git a/Testing/testPlayerController.py b/Testing/testPlayerController.py new file mode 100644 index 0000000000000000000000000000000000000000..591c2179f12201a860d8c6e5efb15291d07ca09e --- /dev/null +++ b/Testing/testPlayerController.py @@ -0,0 +1,226 @@ +import unittest +from unittest.mock import Mock + +import pygame + +from config import TILE_SIZE +from player import Player +from playerController import PlayerController # Import the PlayerController class + + +class TestPlayerController(unittest.TestCase): + def setUp(self): + self.player_mock = Mock() + self.maze_mock = Mock() + self.player_controller = PlayerController(self.player_mock, self.maze_mock) + + def test_move_player_up(self): + direction = {pygame.K_UP: True, pygame.K_DOWN: False, pygame.K_LEFT: False, pygame.K_RIGHT: False} + self.player_controller.move_player(direction) + self.assertEqual(self.player_mock.y, self.player_mock.y - TILE_SIZE) + + def test_move_player_down(self): + direction = {pygame.K_UP: False, pygame.K_DOWN: True, pygame.K_LEFT: False, pygame.K_RIGHT: False} + self.player_controller.move_player(direction) + self.assertEqual(self.player_mock.y, self.player_mock.y + TILE_SIZE) + + def test_move_player_left(self): + direction = {pygame.K_UP: False, pygame.K_DOWN: False, pygame.K_LEFT: True, pygame.K_RIGHT: False} + self.player_controller.move_player(direction) + self.assertEqual(self.player_mock.x, self.player_mock.x - TILE_SIZE) + + def test_move_player_right(self): + direction = {pygame.K_UP: False, pygame.K_DOWN: False, pygame.K_LEFT: False, pygame.K_RIGHT: True} + self.player_controller.move_player(direction) + self.assertEqual(self.player_mock.x, self.player_mock.x + TILE_SIZE) + + def test_valid_position_within_maze(self): + position = (1, 1) + self.assertTrue(self.player_controller.is_valid_position(position)) + + def test_position_outside_maze_bounds(self): + position = (-1, 1) + self.assertFalse(self.player_controller.is_valid_position(position)) + position = (10, 5) + self.assertFalse(self.player_controller.is_valid_position(position)) + position = (1, 10) + self.assertFalse(self.player_controller.is_valid_position(position)) + + def test_position_collides_with_wall(self): + self.player_controller.maze = Mock(current_level=[[0, 0, 0], + [0, 1, 0], + [0, 0, 0]]) + position = (1, 1) + self.assertFalse(self.player_controller.is_valid_position(position)) + position = (0, 1) + self.assertTrue(self.player_controller.is_valid_position(position)) + + def test_collision_with_wall(self): + self.player_controller.maze = [[0, 0, 0], + [0, 1, 0], + [0, 0, 0]] + x, y = 50, 50 + self.assertTrue(self.player_controller.check_collision(x, y)) + + def test_no_collision_with_wall(self): + self.player_controller.maze = [[0, 0, 0], + [0, 0, 0], + [0, 0, 0]] + x, y = 50, 50 + self.assertFalse(self.player_controller.check_collision(x, y)) + + def test_reset_player_position(self): + initial_x, initial_y = 100, 100 + self.player_controller.player = Player(initial_x, initial_y) + self.player_controller.reset_player_position() + self.assertEqual(self.player_controller.player.x, TILE_SIZE) + self.assertEqual(self.player_controller.player.y, TILE_SIZE) + + def test_set_dfs_solver(self): + dfs_solver_mock = Mock() + self.player_controller.set_dfs_solver(dfs_solver_mock) + self.assertEqual(self.player_controller.dfs_solver, dfs_solver_mock) + + def test_set_bfs_solver(self): + bfs_solver_mock = Mock() + self.player_controller.set_bfs_solver(bfs_solver_mock) + self.assertEqual(self.player_controller.bfs_solver, bfs_solver_mock) + + def test_set_dijkstra_solver(self): + dijkstra_solver_mock = Mock() + self.player_controller.set_dijkstra_solver(dijkstra_solver_mock) + self.assertEqual(self.player_controller.dijkstra_solver, dijkstra_solver_mock) + + def test_set_astar_solver(self): + astar_solver_mock = Mock() + self.player_controller.set_astar_solver(astar_solver_mock) + self.assertEqual(self.player_controller.astar_solver, astar_solver_mock) + + def test_move_to_goal_dfs(self): + dfs_solver_mock = Mock() + dfs_solver_mock.dfs.return_value = True + dfs_solver_mock.get_path.return_value = [(1, 1), (1, 2), (1, 3)] + + self.player_controller.set_dfs_solver(dfs_solver_mock) + self.player_controller.maze = Mock() + self.player_controller.maze.current_level = [ + [0, 0, 0, 0], + [0, 0, 3, 0], + [0, 0, 0, 0], + ] + + self.player_controller.player.x = TILE_SIZE + self.player_controller.player.y = TILE_SIZE + self.player_controller.move_to_goal_dfs() + + expected_path = [(1 * TILE_SIZE, 1 * TILE_SIZE), (1 * TILE_SIZE, 2 * TILE_SIZE), (1 * TILE_SIZE, 3 * TILE_SIZE)] + self.assertEqual( + [(self.player_controller.player.x, self.player_controller.player.y) for _ in range(len(expected_path))], + expected_path) + + def test_move_to_goal_bfs(self): + bfs_solver_mock = Mock() + bfs_solver_mock.bfs.return_value = True + bfs_solver_mock.get_path.return_value = [(1, 1), (1, 2), (1, 3)] + + self.player_controller.set_bfs_solver(bfs_solver_mock) + self.player_controller.maze = Mock() + self.player_controller.maze.current_level = [ + [0, 0, 0, 0], + [0, 0, 3, 0], + [0, 0, 0, 0], + ] + + self.player_controller.player.x = TILE_SIZE + self.player_controller.player.y = TILE_SIZE + self.player_controller.move_to_goal_bfs() + + expected_path = [(1 * TILE_SIZE, 1 * TILE_SIZE), (1 * TILE_SIZE, 2 * TILE_SIZE), (1 * TILE_SIZE, 3 * TILE_SIZE)] + self.assertEqual( + [(self.player_controller.player.x, self.player_controller.player.y) for _ in range(len(expected_path))], + expected_path) + + def test_move_to_goal_dijkstra(self): + dijkstra_solver_mock = Mock() + dijkstra_solver_mock.find_shortest_path.return_value = [(1, 1), (1, 2), (1, 3)] # Simulate a path + + self.player_controller.set_dijkstra_solver(dijkstra_solver_mock) + self.player_controller.maze = Mock() + self.player_controller.maze.current_level = [ + [0, 0, 0, 0], + [0, 0, 3, 0], + [0, 0, 0, 0], + ] + + self.player_controller.player.x = TILE_SIZE + self.player_controller.player.y = TILE_SIZE + self.player_controller.move_to_goal_dijkstra() + + expected_path = [(1 * TILE_SIZE, 1 * TILE_SIZE), (1 * TILE_SIZE, 2 * TILE_SIZE), (1 * TILE_SIZE, 3 * TILE_SIZE)] + self.assertEqual( + [(self.player_controller.player.x, self.player_controller.player.y) for _ in range(len(expected_path))], + expected_path) + + def test_move_to_goal_astar(self): + astar_solver_mock = Mock() + astar_solver_mock.find_goal_position.return_value = (2, 2) # Simulate a goal position + astar_solver_mock.find_shortest_path.return_value = [(1, 1), (1, 2), (1, 3)] # Simulate a path + + self.player_controller.set_astar_solver(astar_solver_mock) + self.player_controller.maze = Mock() + self.player_controller.maze.current_level = [ + [0, 0, 0, 0], + [0, 0, 3, 0], + [0, 0, 0, 0], + ] + + self.player_controller.player.x = TILE_SIZE + self.player_controller.player.y = TILE_SIZE + self.player_controller.move_to_goal_astar() + + expected_path = [(1 * TILE_SIZE, 1 * TILE_SIZE), (1 * TILE_SIZE, 2 * TILE_SIZE), (1 * TILE_SIZE, 3 * TILE_SIZE)] + self.assertEqual( + [(self.player_controller.player.x, self.player_controller.player.y) for _ in range(len(expected_path))], + expected_path) + + def test_follow_path(self): + player_mock = Mock() + self.player_controller.player = player_mock + maze_mock = Mock() + self.player_controller.maze = maze_mock + path = [(0, 0), (0, 1), (0, 2)] + self.player_controller.follow_path(path) + expected_positions = [(0, 0), (0, 1), (0, 2)] + self.assertEqual([(args[0], args[1]) for args in player_mock.x.call_args_list], expected_positions) + + def test_draw_path(self): + screen_mock = Mock() + self.player_controller.screen = screen_mock + path = [(0, 0), (0, 1), (1, 1)] + self.player_controller.draw_path(path) + + expected_calls = [ + ((screen_mock,), {'color': (0, 255, 0), 'rect': (0, 0, TILE_SIZE, TILE_SIZE)}), + ((screen_mock,), {'color': (0, 255, 0), 'rect': (TILE_SIZE, 0, TILE_SIZE, TILE_SIZE)}), + ((screen_mock,), {'color': (0, 255, 0), 'rect': (TILE_SIZE, TILE_SIZE, TILE_SIZE, TILE_SIZE)}) + ] + + self.assertEqual(screen_mock.mock_calls, + [unittest.mock.call.draw.rect(*args, **kwargs) for args, kwargs in expected_calls]) + + def test_find_goal_position(self): + mock_maze = Mock() + mock_maze.current_level = [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 3] + ] + + self.player_controller.maze = mock_maze + goal_position = self.player_controller.find_goal_position() + expected_goal_position = (2, 2) + self.assertEqual(goal_position, expected_goal_position) + + +if __name__ == "__main__": + unittest.main()