from random import choice, randint import project_constants as const from assets.display_assets import blit_graphics from algorithms.search import a_star from algorithms.learn.genetic_algorithm import ga_roulette, helpers from minefield import Minefield from objects.mine_models.time_mine import TimeMine from ui.ui_components_manager import UiComponentsManager from ui.text_box import TextBox from ui.button import Button from ui.input_box import InputBox class Game: def __init__(self, turn=0, minefield=None, window=None): # loading game settings self.window = window if window is not None else const.SCREEN # setting in game data self.turn = turn self.turn_on_which_last_action_was_taken = turn self.goal = (0, 0) # assigning minefield and agent to variables self.minefield = minefield if minefield is not None else Minefield(const.MAP_RANDOM_10x10) self.initial_minefield = self.minefield.__copy__() self.agent = self.minefield.agent self.agent_action = None self.millisecond_timer = 0 self.delta_time = 0 self.action_timer = 0 self.action_delta_time = 0 self.genetic_sequence = None self.genetics_done = False # declaring and initializing gui components # ui_component managers self.in_game_gui_components_manager = UiComponentsManager() self.game_over_gui_components_manager = UiComponentsManager() # in game gui self.textbox_points = TextBox((0, 0), (0, 0)) self.input_box_row = InputBox((0, 0), (0, 0)) self.input_box_column = InputBox((0, 0), (0, 0)) self.button_auto = Button((0, 0), (0, 0)) self.button_random = Button((0, 0), (0, 0)) self.button_ok = Button((0, 0), (0, 0)) self.button_genetic_algorithm = Button((0, 0), (0, 0)) self.input_box_generations = InputBox((0, 0), (0, 0)) # game over screen self.text_box_game_over = TextBox((0, 0), (0, 0)) self.button_try_again = Button((0, 0), (0, 0)) self.button_close = Button((0, 0), (0, 0)) # draws minefield and agent instances def draw_minefield(self): blit_graphics(self.minefield) self.agent.update_and_draw(self.window, self.action_delta_time, self.minefield) # draws menu def run_in_game_menu_overlay(self, mouse_position, events): # drawing and updating all gui components self.in_game_gui_components_manager.run_all(self.window, mouse_position, events) self.in_game_gui_components_manager.switch_selected_objects_with_arrow_keys(mouse_position, events) # setting random goal if random button is clicked if self.button_random.is_clicked(mouse_position, events): self.goal = (randint(0, 9), randint(0, 9)) self.input_box_row.set_texts(user_input=str(self.goal[0])) self.input_box_column.set_texts(user_input=str(self.goal[1])) # if any input box is empty don't allow player to run game self.button_ok.set_flags(is_active=not (self.input_box_row.empty() or self.input_box_column.empty())) # updating goal if input boxes are filled and highlighting chosen tile destination if not (self.input_box_row.empty() or self.input_box_column.empty()): # updating goal self.goal = (min(9, int(self.input_box_row.get_input())), min(9, int(self.input_box_column.get_input()))) # highlighting chosen tile destination self.window.blit(const.HIGHLIGHT, const.get_tile_coordinates(self.goal)) # makes agent take next action def agent_take_next_action(self, action_sequence): # give agent an action if there is one self.agent.take_action(self.agent_action) # check what the next action is going to be self.agent_action = action_sequence.pop(0) # start animating agents next action self.agent.animate(self.agent_action) # updates in game time and timers def update_time(self, number_of_seconds_since_last_tick): self.millisecond_timer += number_of_seconds_since_last_tick / 1000 self.action_timer = self.millisecond_timer / const.TURN_INTERVAL self.delta_time = number_of_seconds_since_last_tick / 1000 self.action_delta_time = self.delta_time / const.TURN_INTERVAL # updates number of turns in game def update_turns(self): # if one turns time passed: updating number of turns if self.millisecond_timer >= const.TURN_INTERVAL: self.turn += 1 self.minefield.next_turn() self.textbox_points.text = str(self.minefield.points) # resetting timer self.millisecond_timer %= const.TURN_INTERVAL def revert_minefield_turn(self): self.minefield.turn -= 1 self.minefield.points -= 1 # returns turn number def get_turn_number(self): return self.turn # draws a random mine to disarm (in auto mode) def set_random_mine_as_target(self): if any(self.minefield.get_active_mines()): self.goal = choice(self.minefield.get_active_mines()).position # display new destination self.input_box_row.set_texts(user_input=str(self.goal[0])) self.input_box_column.set_texts(user_input=str(self.goal[1])) # prevents highlighting input_box_row, # couldn't find any better solution w/o major Game class changes self.input_box_row.set_is_selected(False) return True else: return False def run_genetics(self, generations=10): print("Starting genetics algorithm...") genetics_minefield = Minefield(const.MAP_RANDOM_10x10) sequence = \ ga_roulette.genetic_algorithm(minefield=genetics_minefield, population=helpers.get_mines_coords(self.minefield), pop_size=100, elite_size=20, mutation_rate=0.01, generations=generations) self.genetic_sequence = sequence self.genetics_done = True def set_next_genetic_target(self): if any(self.genetic_sequence): self.goal = self.genetic_sequence.pop(0) # display new destination self.input_box_row.set_texts(user_input=str(self.goal[0])) self.input_box_column.set_texts(user_input=str(self.goal[1])) # prevents highlighting input_box_row, # couldn't find any better solution w/o major Game class changes self.input_box_row.set_is_selected(False) return True else: return False # gets action sequence for agent def get_action_sequence(self, target_type: str = "tile"): return a_star.graphsearch( initial_state=a_star.State( row=self.agent.row, column=self.agent.column, direction=self.agent.direction ), minefield=self.minefield, target_type=target_type, tox=self.goal[0], toy=self.goal[1] ) # returns a mine on a given position def get_mine(self, position): row, column = position return self.minefield.matrix[row][column].mine def explosion(self, position): # show explosion on position self.minefield.points += const.EXPLOSION_PENALTY # initializes attributes before game loop begins def initialize_before_game_loop(self): self.agent_action = None self.millisecond_timer = 0 self.action_timer = 0 self.delta_time = 0 self.action_delta_time = 0 for component in self.in_game_gui_components_manager.selectable_ui_components: component.set_flags(is_active=False) # gives agent last action def agent_take_last_action(self): self.agent.take_action(self.agent_action) # cleans up after game loop ends def cleanup_after_game_loop(self): self.agent.update_and_draw(self.window, self.action_delta_time, self.minefield) self.agent.reset_actions() for comp in self.in_game_gui_components_manager.selectable_ui_components: comp.set_flags(is_active=True) if not any([comp.is_selected for comp in self.in_game_gui_components_manager.selectable_ui_components]): self.input_box_row.set_is_selected(True) # returns True if agents should take an action in this turn, or False otherwise def agent_should_take_next_action(self, action_sequence): number_of_turns_since_last_action = self.turn - self.turn_on_which_last_action_was_taken agent_action_cost = 1 if self.agent_action != const.Action.GO else self._get_next_tiles_entering_cost() if number_of_turns_since_last_action >= agent_action_cost and any(action_sequence): self.turn_on_which_last_action_was_taken = self.turn return True return False # returns True if agent made all actions he was instructed to do, or False otherwise def agent_made_all_actions(self, action_sequence): number_of_turns_since_last_action = self.turn - self.turn_on_which_last_action_was_taken agent_action_cost = 1 if self.agent_action != const.Action.GO else self._get_next_tiles_entering_cost() if number_of_turns_since_last_action >= agent_action_cost and not any(action_sequence): self.turn_on_which_last_action_was_taken = self.turn return True return False # runs game over screen def run_game_over_screen(self, mouse_position, events): self.window.fill((255, 255, 255)) self.game_over_gui_components_manager.run_all(self.window, mouse_position, events) # resetting minefield to the original state if self.button_try_again.is_clicked(mouse_position, events): del self.minefield del self.agent self.minefield = self.initial_minefield.__copy__() self.agent = self.initial_minefield.agent # initializes old ui components and assigns them to attributes def initialize_gui_components(self): # calculating in game gui coordinates gui_width = const.V_SIDE_MENU_WIDTH ib_height, bt_height = const.V_INPUT_BOX_HEIGHT, const.V_BUTTON_HEIGHT gui_x = const.V_TILE_AREA_WIDTH + 2 * const.V_SCREEN_PADDING + const.V_NUMBER_PADDING gui_y = const.SCREEN.get_height() / 2 - (2 * ib_height + 3 * bt_height + 50) / 2 # creating in game gui components self.textbox_points = TextBox( position=(gui_x, gui_y - 134), dimensions=(gui_width, ib_height - 10), text="0", box_color=(172, 220, 172) ) self.input_box_row = InputBox( position=(gui_x, gui_y - 50), dimensions=(gui_width, ib_height), text="row", box_color=(100, 200, 100), bottom_strip_color=(120, 220, 120), inner_box_color=(120, 220, 120), outline_color=(80, 180, 80), outline_additional_pixel=True, valid_input_characters="1234567890", input_centered=True, clear_input_on_click=True ) self.input_box_column = InputBox( position=(gui_x, gui_y + ib_height - 40), dimensions=(gui_width, ib_height), text="column", box_color=(100, 200, 100), bottom_strip_color=(120, 220, 120), inner_box_color=(120, 220, 120), outline_color=(80, 180, 80), outline_additional_pixel=True, valid_input_characters="1234567890", input_centered=True, clear_input_on_click=True ) self.button_auto = Button( position=(gui_x, gui_y + 2 * ib_height - 30), dimensions=(gui_width, bt_height), text="auto", box_color=(100, 200, 100), outline_color=(80, 180, 80), outline_additional_pixel=True ) self.button_random = Button( position=(gui_x, gui_y + 2 * ib_height + 1 * bt_height - 20), dimensions=(gui_width, bt_height), text="random", box_color=(100, 200, 100), outline_color=(80, 180, 80), outline_additional_pixel=True ) self.button_ok = Button( position=(gui_x, gui_y + 2 * ib_height + 2 * bt_height - 10), dimensions=(gui_width, bt_height), text="ok", box_color=(100, 200, 100), outline_color=(80, 180, 80), outline_additional_pixel=True ) self.button_genetic_algorithm = Button( position=(gui_x, gui_y + 2 * ib_height + 4 * bt_height + 10), dimensions=(gui_width, bt_height), text="genetic", box_color=(100, 200, 100), outline_color=(80, 180, 80), outline_additional_pixel=True ) self.input_box_generations = InputBox( position=(gui_x, gui_y + 2 * ib_height + 5 * bt_height + 20), dimensions=(gui_width, ib_height), text="iters", user_input="10", box_color=(100, 200, 100), bottom_strip_color=(120, 220, 120), inner_box_color=(120, 220, 120), outline_color=(80, 180, 80), outline_additional_pixel=True, valid_input_characters="1234567890", input_centered=True, clear_input_on_click=True ) gui_list = [ self.textbox_points, self.input_box_row, self.input_box_column, self.button_auto, self.button_genetic_algorithm, self.input_box_generations, self.button_random, self.button_ok ] self.in_game_gui_components_manager = UiComponentsManager(gui_list) # creating game over gui components self.text_box_game_over = TextBox(position=(150, 200), dimensions=(500, 100), text="Game Over") self.button_try_again = Button(position=(350, 350), dimensions=(100, 50), text="Try again") self.button_close = Button(position=(350, 450), dimensions=(100, 50), text="Close") gui_list = [self.text_box_game_over, self.button_try_again, self.button_close] self.game_over_gui_components_manager = UiComponentsManager(gui_list, select_first_item=False) # returns true if there is a time mine that went out of time def time_mine_exploded(self): # auxiliary function that checks if a tile contains a time mine which timer went to 0 for i in range(const.V_GRID_HOR_TILES): for j in range(const.V_GRID_VER_TILES): mine = self.minefield.matrix[i][j].mine if mine is not None and isinstance(mine, TimeMine) and mine.timer <= 0: return True return False # sets all in game components is_active flags to a given value def set_is_active_flag_for_all_in_game_gui_components(self, is_active): for component in self.in_game_gui_components_manager.selectable_ui_components: component.set_flags(is_active=is_active) # returns cost of entering tile in front of the agent def _get_next_tiles_entering_cost(self): row, column = self._get_next_agent_position() return self.minefield.matrix[row][column].cost.value # returns position in front of agent def _get_next_agent_position(self): # heading either up or down if self.agent_action == const.Action.GO and self.agent.direction.value % 2 == 0: return max(0, min(9, self.agent.row + self.agent.direction.value - 1)), self.agent.column # heading either left or right else: return self.agent.row, max(0, min(9, self.agent.column - self.agent.direction.value + 2)) def get_input_generations(self): return self.input_box_generations.get_input()