diff --git a/algorithms/bfs.py b/algorithms/bfs.py new file mode 100644 index 0000000..8dd8a7b --- /dev/null +++ b/algorithms/bfs.py @@ -0,0 +1,129 @@ +from __future__ import annotations +from typing import List + +# temporary goal for testing +from common.constants import ACTION, Direction, ROWS, COLUMNS + +GOAL = (9, 9) + + +class State: + def __init__(self, row, column, direction): + self.row = row + self.column = column + self.direction = direction + + +class Node: + def __init__(self, state, parent=None, action=None): + self.state = state + self.parent = parent + self.action = action + + +def goal_test(state: State): + if (state.row, state.column) == GOAL: + return True + return False + + +def get_successors(state: State, map): + successors = list() + + state_left = State(state.row, state.column, state.direction.right()) + successors.append((ACTION.get("rotate_left"), state_left)) + + state_right = State(state.row, state.column, state.direction.left()) + successors.append((ACTION.get("rotate_right"), state_right)) + + target = go(state.row, state.column, state.direction) + + if is_valid_move(map, target[0], target[1]): + state_go = State(target[0], target[1], state.direction) + successors.append((ACTION.get("go"), state_go)) + + return successors + + +def graphsearch(initial_state: State, map, fringe: List[Node] = None, explored: List[Node] = None, + tox: int = None, toy: int = None): + # fringe and explored initialization + global GOAL + if fringe is None: + fringe = list() + if explored is None: + explored = list() + if tox is not None and toy is not None: + GOAL = (tox, toy) + explored_states = set() + fringe_states = set() + + # root Node + fringe.append(Node(initial_state)) + fringe_states.add((initial_state.row, initial_state.column, initial_state.direction)) + + while True: + # fringe empty -> solution not found + if not any(fringe): + print("Brak rozwiazania") + return [] + + # get first element from fringe + element = fringe.pop(0) + fringe_states.remove((element.state.row, element.state.column, element.state.direction)) + + # if solution was found, prepare and return actions sequence + if goal_test(element.state): + actions_sequence = [element.action] + parent = element.parent + + while parent is not None: + # root's action will be None, don't add it + if parent.action is not None: + actions_sequence.append(parent.action) + parent = parent.parent + + actions_sequence.reverse() + return actions_sequence + + # add current node to explored (prevents infinite cycles) + explored.append(element) + explored_states.add((element.state.row, element.state.column, element.state.direction)) + + # loop through every possible next action + for successor in get_successors(element.state, map): + + # make sure not to fall into a cycle + successor_state = (successor[1].row, successor[1].column, successor[1].direction) + if successor_state not in fringe_states and \ + successor_state not in explored_states: + # create new Node and add it at the end of fringe + new_node = Node(state=successor[1], + parent=element, + action=successor[0]) + fringe.append(new_node) + fringe_states.add((new_node.state.row, new_node.state.column, new_node.state.direction)) + + +# TEMPORARY METHOD +def go(row, column, direction): + target = tuple() + + if direction == Direction.RIGHT: + target = row, column + 1 + elif direction == Direction.LEFT: + target = row, column - 1 + elif direction == Direction.UP: + target = row - 1, column + elif direction == Direction.DOWN: + target = row + 1, column + + return target + + +def is_valid_move(map, target_row, target_column): + # TODO: check collisions with objects + if 0 <= target_row < ROWS and 0 <= target_column < COLUMNS: + return True + + return False diff --git a/common/constants.py b/common/constants.py index 0924d6d..9772ff8 100644 --- a/common/constants.py +++ b/common/constants.py @@ -1,7 +1,10 @@ +from enum import Enum + GAME_TITLE = 'WMICraft' WINDOW_HEIGHT = 800 WINDOW_WIDTH = 1360 FPS_COUNT = 60 +TURN_INTERVAL = 1000 GRID_CELL_PADDING = 5 GRID_CELL_SIZE = 36 @@ -19,9 +22,7 @@ RIGHT_KNIGHTS_SPAWN_FIRST_ROW = 6 RIGHT_KNIGHTS_SPAWN_FIRST_COL = 20 CASTLE_SPAWN_WIDTH = 6 -# CASTLE_SPAWN_WIDTH = 0 CASTLE_SPAWN_HEIGHT = 5 -# CASTLE_SPAWN_HEIGHT = 0 CASTLE_SPAWN_FIRST_ROW = 7 CASTLE_SPAWN_FIRST_COL = 9 @@ -38,3 +39,25 @@ TILES = [ 'water.png', 'grass_with_tree.jpg', ] + + +class Direction(Enum): + UP = 0 + RIGHT = 1 + DOWN = 2 + LEFT = 3 + + def left(self): + v = (self.value + 1) % 4 + return Direction(v) + + def right(self): + v = (self.value - 1) % 4 + return Direction(v) + + +ACTION = { + "rotate_left": -1, + "rotate_right": 1, + "go": 0, +} diff --git a/common/helpers.py b/common/helpers.py index 63238b6..b3d51c5 100644 --- a/common/helpers.py +++ b/common/helpers.py @@ -1,4 +1,5 @@ import pygame +from common.constants import GRID_CELL_PADDING, GRID_CELL_SIZE, COLUMNS, ROWS def draw_text(text, color, surface, x, y, text_size=30, is_bold=False): @@ -10,3 +11,20 @@ def draw_text(text, color, surface, x, y, text_size=30, is_bold=False): textrect = textobj.get_rect() textrect.topleft = (x, y) surface.blit(textobj, textrect) + + +def print_numbers(): + display_surface = pygame.display.get_surface() + font = pygame.font.SysFont('Arial', 16) + + for row_index in range(ROWS): + for col_index in range(COLUMNS): + x = (GRID_CELL_PADDING + GRID_CELL_SIZE) * col_index + GRID_CELL_PADDING + 7 + y = (GRID_CELL_PADDING + GRID_CELL_SIZE) * row_index + GRID_CELL_PADDING + 16 + display_surface.blit(font.render(f'[{col_index}, {row_index}]', True, (255, 0, 0)), (x, y)) + pygame.display.update() + + +# parse array index to screen x or y coordinate +def parse_cord(cord): + return (GRID_CELL_PADDING + GRID_CELL_SIZE) * cord + GRID_CELL_PADDING + 7 diff --git a/helpers/debug.py b/helpers/debug.py deleted file mode 100644 index c5c9bee..0000000 --- a/helpers/debug.py +++ /dev/null @@ -1,15 +0,0 @@ -import pygame.display - -from common.constants import GRID_CELL_PADDING, GRID_CELL_SIZE, COLUMNS, ROWS - - -def print_numbers(): - display_surface = pygame.display.get_surface() - font = pygame.font.SysFont('Arial', 16) - - for row_index in range(ROWS): - for col_index in range(COLUMNS): - x = (GRID_CELL_PADDING + GRID_CELL_SIZE) * col_index + GRID_CELL_PADDING + 7 - y = (GRID_CELL_PADDING + GRID_CELL_SIZE) * row_index + GRID_CELL_PADDING + 16 - display_surface.blit(font.render(f'[{col_index}, {row_index}]', True, (255, 0, 0)), (x, y)) - pygame.display.update() diff --git a/logic/game.py b/logic/game.py index 35cb1b9..b81673e 100644 --- a/logic/game.py +++ b/logic/game.py @@ -3,7 +3,8 @@ import sys import pygame from common.constants import * -from helpers.debug import print_numbers +from common.helpers import print_numbers +from logic.knights_queue import KnightsQueue from logic.level import Level from ui.logs import Logs from ui.screens.credits import Credits @@ -37,6 +38,13 @@ class Game: stats = Stats() logs = Logs() + # setup clock for rounds + NEXT_TURN = pygame.USEREVENT + 1 + pygame.time.set_timer(NEXT_TURN, TURN_INTERVAL) + + # create level + self.level.create_map() + print_numbers_flag = False running = True while running: @@ -51,11 +59,13 @@ class Game: running = False if event.key == 110: # clicked n letter on keyboard print_numbers_flag = not print_numbers_flag + if event.type == NEXT_TURN: # is called every 't' milliseconds + self.level.handle_turn() stats.draw(self.screen) logs.draw(self.screen) - self.level.run() + self.level.update() if print_numbers_flag: print_numbers() diff --git a/logic/level.py b/logic/level.py index 1ec13d3..75a1e3e 100644 --- a/logic/level.py +++ b/logic/level.py @@ -2,7 +2,9 @@ import random import pygame +from algorithms.bfs import graphsearch, State from common.constants import * +from logic.knights_queue import KnightsQueue from logic.spawner import Spawner from models.castle import Castle from models.knight import Knight @@ -17,18 +19,29 @@ class Level: # sprite group setup self.sprites = pygame.sprite.Group() - self.create_map() + self.map = [[' ' for x in range(COLUMNS)] for y in range(ROWS)] + + self.list_knights_blue = [] + self.list_knights_red = [] + self.list_monsters = [] + self.list_castles = [] + + self.knights_queue = None + + def create_map(self): + self.generate_map() + self.setup_base_tiles() + self.setup_objects() + self.knights_queue = KnightsQueue(self.list_knights_blue, self.list_knights_red) def generate_map(self): - map = [[' ' for x in range(COLUMNS)] for y in range(ROWS)] - - spawner = Spawner(map) + 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_in_area(['k' for x in range(4)], LEFT_KNIGHTS_SPAWN_FIRST_ROW, LEFT_KNIGHTS_SPAWN_FIRST_COL, + spawner.spawn_in_area(['k_b' for x in range(4)], LEFT_KNIGHTS_SPAWN_FIRST_ROW, LEFT_KNIGHTS_SPAWN_FIRST_COL, KNIGHTS_SPAWN_WIDTH, KNIGHTS_SPAWN_HEIGHT) - spawner.spawn_in_area(['k' for x in range(4)], RIGHT_KNIGHTS_SPAWN_FIRST_ROW, RIGHT_KNIGHTS_SPAWN_FIRST_COL, + spawner.spawn_in_area(['k_r' for x 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, @@ -36,50 +49,87 @@ class Level: spawner.spawn_where_possible(['m' for x in range(NBR_OF_MONSTERS)]) - return map - - def create_map(self): - map = self.generate_map() - + def setup_base_tiles(self): textures = [] for texture_path in TILES: converted_texture = pygame.image.load('resources/textures/' + texture_path).convert_alpha() converted_texture = pygame.transform.scale(converted_texture, (40, 40)) textures.append((texture_path, converted_texture)) - castle_count = 0 # TODO: find some smarter method to print castle - - for row_index, row in enumerate(map): - print(row) + for row_index, row in enumerate(self.map): for col_index, col in enumerate(row): - x = (GRID_CELL_PADDING + GRID_CELL_SIZE) * col_index + GRID_CELL_PADDING + 7 - y = (GRID_CELL_PADDING + GRID_CELL_SIZE) * row_index + GRID_CELL_PADDING + 7 # add base tiles, e.g. water, tree, grass if col == "w": texture_index = 5 texture_surface = textures[texture_index][1] - Tile((x, y), texture_surface, self.sprites, 'w') + Tile((row_index, col_index), texture_surface, self.sprites, 'w') elif col == "t": texture_index = 6 texture_surface = textures[texture_index][1] - Tile((x, y), texture_surface, self.sprites, 't') + Tile((row_index, col_index), texture_surface, self.sprites, 't') else: texture_index = random.randint(0, 4) texture_surface = textures[texture_index][1] - Tile((x, y), texture_surface, self.sprites) + Tile((row_index, col_index), texture_surface, self.sprites) + + def setup_objects(self): + castle_count = 0 # TODO: find some smarter method to print castle + + for row_index, row in enumerate(self.map): + print(row) + for col_index, col in enumerate(row): # add objects, e.g. knights, monsters, castle - if col == "k": - map[row_index][col_index] = Knight((x, y), self.sprites) + if col == "k_b": + knight = Knight((row_index, col_index), self.sprites, "blue") + self.map[row_index][col_index] = knight + self.list_knights_blue.append(knight) + elif col == "k_r": + knight = Knight((row_index, col_index), self.sprites, "red") + self.map[row_index][col_index] = knight + self.list_knights_red.append(knight) elif col == "m": - map[row_index][col_index] = Monster((x, y), self.sprites) + monster = Monster((row_index, col_index), self.sprites) + self.map[row_index][col_index] = monster + self.list_monsters.append(monster) elif col == "c": castle_count += 1 if castle_count == 4: - map[row_index][col_index] = Castle((x, y), self.sprites) + castle = Castle((row_index, col_index), self.sprites) + self.map[row_index][col_index] = castle + self.list_castles.append(castle) - def run(self): + def handle_turn(self): + print("next turn") + 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_x, knight_pos_y, current_knight.direction) + action_list = graphsearch(state, self.map) + print(action_list) + + if len(action_list) == 0: + return + + if action_list[0] == ACTION.get("rotate_left"): + current_knight.rotate_left() + elif action_list[0] == ACTION.get("rotate_right"): + current_knight.rotate_right() + elif action_list[0] == ACTION.get("go"): + current_knight.step_forward() + self.map[knight_pos_x][knight_pos_y] = ' ' + + if current_knight.direction.name == 'UP': + self.map[knight_pos_x][knight_pos_y - 1] = current_knight + elif current_knight.direction.name == 'RIGHT': + self.map[knight_pos_x + 1][knight_pos_y] = current_knight + elif current_knight.direction.name == 'DOWN': + self.map[knight_pos_x][knight_pos_y + 1] = current_knight + elif current_knight.direction.name == 'LEFT': + self.map[knight_pos_x - 1][knight_pos_y] = current_knight + + def update(self): bg_width = (GRID_CELL_PADDING + GRID_CELL_SIZE) * COLUMNS + BORDER_WIDTH bg_height = (GRID_CELL_PADDING + GRID_CELL_SIZE) * ROWS + BORDER_WIDTH pygame.draw.rect(self.screen, (255, 255, 255), pygame.Rect(5, 5, bg_width, bg_height), 0, BORDER_RADIUS) diff --git a/models/castle.py b/models/castle.py index ca75946..2f51d32 100644 --- a/models/castle.py +++ b/models/castle.py @@ -1,10 +1,13 @@ import pygame.image +from common.helpers import parse_cord + class Castle(pygame.sprite.Sprite): def __init__(self, position, group): super().__init__(group) self.image = pygame.image.load("./resources/textures/castle.png").convert_alpha() self.image = pygame.transform.scale(self.image, (78, 78)) - self.rect = self.image.get_rect(center=position) + position_in_px = (parse_cord(position[1]), parse_cord(position[0])) + self.rect = self.image.get_rect(center=position_in_px) self.health = 80 diff --git a/models/knight.py b/models/knight.py index 6e8a18a..b2c9cba 100644 --- a/models/knight.py +++ b/models/knight.py @@ -1,7 +1,8 @@ import pygame.image import random -from common.constants import GRID_CELL_SIZE +from common.constants import GRID_CELL_SIZE, Direction +from common.helpers import parse_cord def load_knight_textures(): @@ -17,32 +18,37 @@ def load_knight_textures(): class Knight(pygame.sprite.Sprite): - def __init__(self, position, group): + def __init__(self, position, group, team): super().__init__(group) - self.direction = 2 + self.direction = Direction.DOWN self.states = load_knight_textures() - self.image = self.states[self.direction] - self.rect = self.image.get_rect(topleft=position) + self.image = self.states[self.direction.value] + self.position = position + position_in_px = (parse_cord(position[1]), parse_cord(position[0])) + self.rect = self.image.get_rect(topleft=position_in_px) + self.team = team self.health = random.randint(7, 12) self.attack = random.randint(4, 7) self.defense = random.randint(1, 4) self.points = 1 - # direction arg = -1 to rotate left - # direction arg = 1 to rotate right - def rotate(self, direction): - self.direction = (self.direction + direction) % 4 - self.image = self.states[self.direction] + def rotate_left(self): + self.direction = self.direction.right() + self.image = self.states[self.direction.value] + + def rotate_right(self): + self.direction = self.direction.left() + self.image = self.states[self.direction.value] def step_forward(self): - if self.direction == 0: - self.rect.y = self.rect.y - GRID_CELL_SIZE - elif self.direction == 1: - self.rect.x = self.rect.x + GRID_CELL_SIZE - elif self.direction == 2: - self.rect.y = self.rect.y + GRID_CELL_SIZE - else: - self.rect.x = self.rect.x - GRID_CELL_SIZE + if self.direction.name == 'UP': + self.rect.y = self.rect.y - GRID_CELL_SIZE - 5 + elif self.direction.name == 'RIGHT': + self.rect.x = self.rect.x + GRID_CELL_SIZE + 5 + elif self.direction.name == 'DOWN': + self.rect.y = self.rect.y + GRID_CELL_SIZE + 5 + elif self.direction.name == 'LEFT': + self.rect.x = self.rect.x - GRID_CELL_SIZE - 5 diff --git a/models/monster.py b/models/monster.py index 7109dff..7a97973 100644 --- a/models/monster.py +++ b/models/monster.py @@ -1,6 +1,8 @@ import pygame.image import random +from common.helpers import parse_cord + monster_images = [ pygame.image.load("./resources/textures/dragon2.png"), pygame.image.load("./resources/textures/birb.png"), @@ -14,7 +16,8 @@ class Monster(pygame.sprite.Sprite): super().__init__(group) self.image = random.choice(monster_images) self.image = pygame.transform.scale(self.image, (40, 40)) - self.rect = self.image.get_rect(topleft=position) + position_in_px = (parse_cord(position[1]), parse_cord(position[0])) + self.rect = self.image.get_rect(topleft=position_in_px) self.health = random.randrange(15, 25) self.attack = random.randrange(2, 10) diff --git a/models/tile.py b/models/tile.py index e4dc283..a28456e 100644 --- a/models/tile.py +++ b/models/tile.py @@ -1,9 +1,12 @@ import pygame +from common.helpers import parse_cord + class Tile(pygame.sprite.Sprite): def __init__(self, position, image, group, tile_type=' '): super().__init__(group) self.image = image - self.rect = self.image.get_rect(topleft=position) + position_in_px = (parse_cord(position[1]), parse_cord(position[0])) + self.rect = self.image.get_rect(topleft=position_in_px) self.tile_type = tile_type \ No newline at end of file