diff --git a/algorithms/a_star.py b/algorithms/a_star.py new file mode 100644 index 0000000..1b8db00 --- /dev/null +++ b/algorithms/a_star.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +import heapq +from dataclasses import dataclass, field +from typing import Tuple, Optional, List + +from common.constants import ROWS, COLUMNS + +EMPTY_FIELDS = ['s', 'g', ' '] +LEFT = 'LEFT' +RIGHT = 'RIGHT' +UP = 'UP' +DOWN = 'DOWN' + +TURN_LEFT = 'TURN_LEFT' +TURN_RIGHT = 'TURN_RIGHT' +FORWARD = 'FORWARD' + +directions = { + LEFT: (0, -1), + RIGHT: (0, 1), + UP: (-1, 0), + DOWN: (1, 0) +} + + +@dataclass +class State: + position: Tuple[int, int] + direction: str + + def __eq__(self, other: State) -> bool: + return other.position == self.position and self.direction == other.direction + + def __lt__(self, state): + return self.position < state.position + + def __hash__(self) -> int: + return hash(self.position) + + +@dataclass +class Node: + state: State + parent: Optional[Node] + action: Optional[str] + grid: List[List[str]] + cost: int = field(init=False) + depth: int = field(init=False) + + def __lt__(self, node) -> None: + return self.state < node.state + + def __post_init__(self) -> None: + if self.grid[self.state.position[0]][self.state.position[1]] == 'g': + self.cost = 1 if not self.parent else self.parent.cost + 1 + else: + self.cost = 2 if not self.parent else self.parent.cost + 2 + self.depth = 0 if not self.parent else self.parent.depth + 1 + + def __hash__(self) -> int: + return hash(self.state) + + +def expand(node: Node, grid: List[List[str]]) -> List[Node]: + return [child_node(node=node, action=action, grid=grid) for action in actions(node.state, grid)] + + +def child_node(node: Node, action: str, grid: List[List[str]]) -> Node: + next_state = result(state=node.state, action=action) + return Node(state=next_state, parent=node, action=action, grid=grid) + + +def next_position(current_position: Tuple[int, int], direction: str) -> Tuple[int, int]: + next_row, next_col = directions[direction] + row, col = current_position + return next_row + row, next_col + col + + +def valid_move(position: Tuple[int, int], grid: List[List[str]]) -> bool: + row, col = position + return grid[row][col] in EMPTY_FIELDS + + +def actions(state: State, grid: List[List[str]]) -> List[str]: + possible_actions = [FORWARD, TURN_LEFT, TURN_RIGHT] + row, col = state.position + direction = state.direction + + if direction == UP and row == 0: + remove_forward(possible_actions) + if direction == DOWN and row == ROWS - 1: + remove_forward(possible_actions) + if direction == LEFT and col == 0: + remove_forward(possible_actions) + if direction == RIGHT and col == COLUMNS - 1: + remove_forward(possible_actions) + + if FORWARD in possible_actions and not valid_move(next_position(state.position, direction), grid): + remove_forward(possible_actions) + + return possible_actions + + +def remove_forward(possible_actions: List[str]) -> None: + if FORWARD in possible_actions: + possible_actions.remove(FORWARD) + + +def result(state: State, action: str) -> State: + next_state = State(state.position, state.direction) + + if state.direction == UP: + if action == TURN_LEFT: + next_state.direction = LEFT + elif action == TURN_RIGHT: + next_state.direction = RIGHT + elif action == FORWARD: + next_state.position = next_position(state.position, UP) + + elif state.direction == DOWN: + if action == TURN_LEFT: + next_state.direction = RIGHT + elif action == TURN_RIGHT: + next_state.direction = LEFT + elif action == FORWARD: + next_state.position = next_position(state.position, DOWN) + + elif state.direction == LEFT: + if action == TURN_LEFT: + next_state.direction = DOWN + elif action == TURN_RIGHT: + next_state.direction = UP + elif action == FORWARD: + next_state.position = next_position(state.position, LEFT) + + elif state.direction == RIGHT: + if action == TURN_LEFT: + next_state.direction = UP + elif action == TURN_RIGHT: + next_state.direction = DOWN + elif action == FORWARD: + next_state.position = next_position(state.position, RIGHT) + + return next_state + + +def goal_test(state: State, goal_list: List[Tuple[int, int]]) -> bool: + return state.position in goal_list + + +def h(state: State, goal: Tuple[int, int]) -> int: + """heuristics that calculates Manhattan distance between current position and goal""" + x1, y1 = state.position + x2, y2 = goal + return abs(x1 - x2) + abs(y1 - y2) + + +def f(current_node: Node, goal: Tuple[int, int]) -> int: + """f(n) = g(n) + h(n), g stands for current cost, h for heuristics""" + return current_node.cost + h(state=current_node.state, goal=goal) + + +def get_path_from_start(node: Node) -> List[str]: + path = [node.action] + + while node.parent is not None: + node = node.parent + if node.action: + path.append(node.action) + + path.reverse() + return path + + +def a_star(state: State, grid: List[List[str]], goals: List[Tuple[int, int]]) -> List[str]: + node = Node(state=state, parent=None, action=None, grid=grid) + + frontier = list() + heapq.heappush(frontier, (f(node, goals[0]), node)) + explored = set() + + while frontier: + r, node = heapq.heappop(frontier) + + if goal_test(node.state, goals): + return get_path_from_start(node) + + explored.add(node.state) + + for child in expand(node, grid): + p = f(child, goals[0]) + if child.state not in explored and (p, child) not in frontier: + heapq.heappush(frontier, (p, child)) + elif (r, child) in frontier and r > p: + heapq.heappush(frontier, (p, child)) + + return [] diff --git a/algorithms/bfs.py b/algorithms/bfs.py index e900faa..b2ed3d3 100644 --- a/algorithms/bfs.py +++ b/algorithms/bfs.py @@ -114,7 +114,7 @@ def go(row, column, direction): def is_valid_move(map, target_row, target_column): - if 0 <= target_row < ROWS and 0 <= target_column < COLUMNS and map[target_row][target_column] == ' ': + if 0 <= target_row < ROWS and 0 <= target_column < COLUMNS and map[target_row][target_column] in ['g', 's', ' ']: return True return False diff --git a/common/constants.py b/common/constants.py index 7e96fd0..32ed1ee 100644 --- a/common/constants.py +++ b/common/constants.py @@ -4,7 +4,7 @@ GAME_TITLE = 'WMICraft' WINDOW_HEIGHT = 800 WINDOW_WIDTH = 1360 FPS_COUNT = 60 -TURN_INTERVAL = 1000 +TURN_INTERVAL = 300 GRID_CELL_PADDING = 5 GRID_CELL_SIZE = 36 @@ -29,6 +29,7 @@ CASTLE_SPAWN_FIRST_COL = 9 NBR_OF_WATER = 16 NBR_OF_TREES = 20 NBR_OF_MONSTERS = 2 +NBR_OF_SANDS = 35 TILES = [ 'grass1.png', diff --git a/logic/game.py b/logic/game.py index ec40e2f..2643f1b 100644 --- a/logic/game.py +++ b/logic/game.py @@ -19,12 +19,13 @@ class Game: pygame.display.set_icon(pygame.image.load('./resources/icons/sword.png')) self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) + self.logs = Logs(self.screen) self.clock = pygame.time.Clock() self.bg = pygame.image.load("./resources/textures/bg.jpg") self.screens = {'credits': Credits(self.screen, self.clock), 'options': Options(self.screen, self.clock)} - self.level = Level(self.screen) + self.level = Level(self.screen, self.logs) def main_menu(self): menu = MainMenu(self.screen, self.clock, self.bg, @@ -34,8 +35,7 @@ class Game: menu.display_screen() def game(self): - stats = Stats(self.screen) - logs = Logs() + stats = Stats() # setup clock for rounds NEXT_TURN = pygame.USEREVENT + 1 @@ -63,8 +63,8 @@ class Game: if event.type == NEXT_TURN: # is called every 'TURN_INTERVAL' milliseconds self.level.handle_turn() - stats.draw() - logs.draw(self.screen) + stats.draw(self.screen) + self.logs.draw() self.level.update() diff --git a/logic/level.py b/logic/level.py index 4aba1a0..7179f32 100644 --- a/logic/level.py +++ b/logic/level.py @@ -2,7 +2,7 @@ import random import pygame -from algorithms.bfs import graphsearch, State +from algorithms.a_star import a_star, State, TURN_RIGHT, TURN_LEFT, FORWARD, UP, DOWN, LEFT, RIGHT from common.constants import * from common.helpers import castle_neighbors from logic.knights_queue import KnightsQueue @@ -14,13 +14,13 @@ from models.tile import Tile class Level: - def __init__(self, screen): + def __init__(self, screen, logs): self.screen = screen - + self.logs = logs # sprite group setup self.sprites = pygame.sprite.Group() - self.map = [[' ' for x in range(COLUMNS)] for y in range(ROWS)] + self.map = [['g' for _ in range(COLUMNS)] for y in range(ROWS)] self.list_knights_blue = [] self.list_knights_red = [] @@ -37,18 +37,19 @@ class Level: def generate_map(self): spawner = Spawner(self.map) - spawner.spawn_where_possible(['w' for x in range(NBR_OF_WATER)]) - spawner.spawn_where_possible(['t' for x in range(NBR_OF_TREES)]) + spawner.spawn_where_possible(['w' for _ in range(NBR_OF_WATER)]) + spawner.spawn_where_possible(['t' for _ in range(NBR_OF_TREES)]) + spawner.spawn_where_possible(['s' for _ in range(NBR_OF_SANDS)]) - spawner.spawn_in_area(['k_b' for x in range(4)], LEFT_KNIGHTS_SPAWN_FIRST_ROW, LEFT_KNIGHTS_SPAWN_FIRST_COL, + spawner.spawn_in_area(['k_b' for _ in range(4)], LEFT_KNIGHTS_SPAWN_FIRST_ROW, LEFT_KNIGHTS_SPAWN_FIRST_COL, KNIGHTS_SPAWN_WIDTH, KNIGHTS_SPAWN_HEIGHT) - spawner.spawn_in_area(['k_r' for x in range(4)], RIGHT_KNIGHTS_SPAWN_FIRST_ROW, RIGHT_KNIGHTS_SPAWN_FIRST_COL, + spawner.spawn_in_area(['k_r' for _ in range(4)], RIGHT_KNIGHTS_SPAWN_FIRST_ROW, RIGHT_KNIGHTS_SPAWN_FIRST_COL, KNIGHTS_SPAWN_WIDTH, KNIGHTS_SPAWN_HEIGHT) spawner.spawn_in_area(['c'], CASTLE_SPAWN_FIRST_ROW, CASTLE_SPAWN_FIRST_COL, CASTLE_SPAWN_WIDTH, CASTLE_SPAWN_HEIGHT, 2) - spawner.spawn_where_possible(['m' for x in range(NBR_OF_MONSTERS)]) + spawner.spawn_where_possible(['m' for _ in range(NBR_OF_MONSTERS)]) def setup_base_tiles(self): textures = [] @@ -69,8 +70,12 @@ class Level: texture_index = 6 texture_surface = textures[texture_index][1] Tile((col_index, row_index), texture_surface, self.sprites, 't') + elif col == "s": + texture_index = 4 + texture_surface = textures[texture_index][1] + Tile((col_index, row_index), texture_surface, self.sprites) else: - texture_index = random.randint(0, 4) + texture_index = random.randint(0, 3) texture_surface = textures[texture_index][1] Tile((col_index, row_index), texture_surface, self.sprites) @@ -106,34 +111,41 @@ class Level: current_knight = self.knights_queue.dequeue_knight() knight_pos_x = current_knight.position[0] knight_pos_y = current_knight.position[1] - state = State(knight_pos_y, knight_pos_x, current_knight.direction) castle_cords = (self.list_castles[0].position[0], self.list_castles[0].position[1]) goal_list = castle_neighbors(self.map, castle_cords[0], castle_cords[1]) # list of castle neighbors - action_list = graphsearch(state, self.map, goal_list) + + state = State((knight_pos_y, knight_pos_x), current_knight.direction.name) + action_list = a_star(state, self.map, goal_list) print(action_list) if len(action_list) == 0: return next_action = action_list.pop(0) - if next_action == ACTION.get("rotate_left"): + if next_action == TURN_LEFT: + self.logs.enqueue_log(f'AI {current_knight.team}: Obrót w lewo.') current_knight.rotate_left() - elif next_action == ACTION.get("rotate_right"): + elif next_action == TURN_RIGHT: + self.logs.enqueue_log(f'AI {current_knight.team}: Obrót w prawo.') current_knight.rotate_right() - elif next_action == ACTION.get("go"): + elif next_action == FORWARD: current_knight.step_forward() - self.map[knight_pos_y][knight_pos_x] = ' ' + self.map[knight_pos_y][knight_pos_x] = 'g' # update knight on map - if current_knight.direction.name == 'UP': - self.map[knight_pos_y - 1][knight_pos_x] = current_knight - elif current_knight.direction.name == 'RIGHT': - self.map[knight_pos_y][knight_pos_x + 1] = current_knight - elif current_knight.direction.name == 'DOWN': - self.map[knight_pos_y + 1][knight_pos_x] = current_knight - elif current_knight.direction.name == 'LEFT': - self.map[knight_pos_y][knight_pos_x - 1] = current_knight + if current_knight.direction.name == UP: + self.logs.enqueue_log(f'AI {current_knight.team}: Ruch do góry.') + self.map[knight_pos_y - 1][knight_pos_x] = current_knight.team_alias() + elif current_knight.direction.name == RIGHT: + self.logs.enqueue_log(f'AI {current_knight.team}: Ruch w prawo.') + self.map[knight_pos_y][knight_pos_x + 1] = current_knight.team_alias() + elif current_knight.direction.name == DOWN: + self.logs.enqueue_log(f'AI {current_knight.team}: Ruch w dół.') + self.map[knight_pos_y + 1][knight_pos_x] = current_knight.team_alias() + elif current_knight.direction.name == LEFT: + self.logs.enqueue_log(f'AI {current_knight.team}: Ruch w lewo.') + self.map[knight_pos_y][knight_pos_x - 1] = current_knight.team_alias() def update(self): bg_width = (GRID_CELL_PADDING + GRID_CELL_SIZE) * COLUMNS + BORDER_WIDTH diff --git a/logic/spawner.py b/logic/spawner.py index 67431e4..1e91795 100644 --- a/logic/spawner.py +++ b/logic/spawner.py @@ -8,7 +8,7 @@ class Spawner: self.map = map def __is_free_field(self, field): - return field == ' ' + return field in ['g', 's', ' '] def spawn_in_area(self, objects: list, spawn_area_pos_row=0, spawn_area_pos_column=0, spawn_area_width=0, spawn_area_height=0, size=1): @@ -17,17 +17,17 @@ class Spawner: while spawned_objects_count != len(objects): x = random.randint(0, spawn_area_height) + spawn_area_pos_row y = random.randint(0, spawn_area_width) + spawn_area_pos_column - if x < ROWS-1 and y < COLUMNS-1 and self.__is_free_field(self.map[x][y]): + if x < ROWS - 1 and y < COLUMNS - 1 and self.__is_free_field(self.map[x][y]): for i in range(size): for j in range(size): - self.map[x-i][y-j] = objects[spawned_objects_count] + self.map[x - i][y - j] = objects[spawned_objects_count] spawned_objects_count += 1 def spawn_where_possible(self, objects: list): spawned_objects_count = 0 while spawned_objects_count != len(objects): - x = random.randint(0, ROWS-1) - y = random.randint(0, COLUMNS-1) + x = random.randint(0, ROWS - 1) + y = random.randint(0, COLUMNS - 1) if self.__is_free_field(self.map[x][y]): self.map[x][y] = objects[spawned_objects_count] spawned_objects_count += 1 diff --git a/models/knight.py b/models/knight.py index 85c166c..6855328 100644 --- a/models/knight.py +++ b/models/knight.py @@ -1,6 +1,7 @@ -import pygame.image import random +import pygame.image + from common.constants import GRID_CELL_SIZE, Direction from common.helpers import parse_cord from logic.health_bar import HealthBar @@ -59,3 +60,6 @@ class Knight(pygame.sprite.Sprite): elif self.direction.name == 'LEFT': self.position = (self.position[0] - 1, self.position[1]) self.rect.x = self.rect.x - GRID_CELL_SIZE - 5 + + def team_alias(self) -> str: + return "k_b" if self.team == "blue" else "k_r" diff --git a/ui/logs.py b/ui/logs.py index 166264e..bbd0e8a 100644 --- a/ui/logs.py +++ b/ui/logs.py @@ -1,3 +1,5 @@ +from queue import Queue + import pygame from common.colors import FONT_DARK, ORANGE, WHITE @@ -6,20 +8,31 @@ from common.helpers import draw_text class Logs: - def __init__(self): - self.grid = [] + def __init__(self, screen): + self.log_queue = Queue(maxsize=7) + self.screen = screen - def draw(self, screen): + def draw(self): x = (GRID_CELL_PADDING + GRID_CELL_SIZE) * COLUMNS + BORDER_WIDTH + 15 y = 470 # background - pygame.draw.rect(screen, WHITE, pygame.Rect(x, y, 340, 323), 0, BORDER_RADIUS) + pygame.draw.rect(self.screen, WHITE, pygame.Rect(x, y, 340, 323), 0, BORDER_RADIUS) # title - draw_text('LOGS', FONT_DARK, screen, x + 120, y + 10, 36) - pygame.draw.rect(screen, ORANGE, pygame.Rect(x, y + 65, 340, 3)) + draw_text('LOGS', FONT_DARK, self.screen, x + 120, y + 10, 36) + pygame.draw.rect(self.screen, ORANGE, pygame.Rect(x, y + 65, 340, 3)) # texts - draw_text('AI Blue: Zniszczyła fortecę (4, 8).', FONT_DARK, screen, x + 35, y + 90, 16) - draw_text('AI Red: Zniszczyła fortecę (12, 5).', FONT_DARK, screen, x + 35, y + 120, 16) + next_y = y + 90 + i = 0 + start = len(self.log_queue.queue) - 1 + for idx in range(start, -1, -1): + draw_text(self.log_queue.queue[idx], FONT_DARK, self.screen, x + 35, next_y + i * 30, 16) + i = i + 1 + + def enqueue_log(self, text): + if self.log_queue.full(): + self.log_queue.get() + self.log_queue.put(text) + self.draw()