class MonteCarloGameManager: """ CLass is responsible for performing UCT algorithm moves and keeping information about game state and settings. """ def __init__(self, game_state: BaseGameState, settings: MonteCarloSettings): self.current_state = game_state self.tree = MonteCarloTree(self.current_state) self.settings = settings self.first_move = True self.previous_move_calculated = None self.chosen_node = None self.iteration_performed = CustomEvent() def notify_move_performed(self, move: BaseGameMove): """ Updates tree's nodes after player's move. It this is not the first move, it firstly updates the information after last algorithm's move to keep consistency. Args: move: BaseGameMove object Returns: None """ if self.first_move: self.first_move = False return else: self.tree.perform_move_on_root(self.previous_move_calculated) self.tree.perform_move_on_root(move) self.previous_move_calculated = None def perform_previous_move(self): """ Updates tree's nodes after last algorithm's move. Returns: None """ self.tree.perform_move_on_root(self.previous_move_calculated) self.previous_move_calculated = None def calculate_next_move(self): """ Calculates algorithm's move with UCT algorithm. Information about chosen move is stored after execution. Returns: calculated move, BaseGameMove object """ mcts = MonteCarloTreeSearch(self.tree, self.settings) mcts.iteration_performed += self._handle_iteration_performed move, state, best_node = mcts.calculate_next_move() self.chosen_node = best_node self.previous_move_calculated = move return move def _handle_iteration_performed(self, sender, earg): self.iteration_performed.fire(self, earg)
def __init__(self, game_state: BaseGameState, settings: MonteCarloSettings): self.current_state = game_state self.tree = MonteCarloTree(self.current_state) self.settings = settings self.first_move = True self.previous_move_calculated = None self.chosen_node = None self.iteration_performed = CustomEvent()
def __init__(self, canvas: GameCanvas, game_mode: GameMode, start_state: BaseGameState, settings: MonteCarloSettings, game: Game): self.canvas = canvas self.game_mode = game_mode self.mc_manager = MonteCarloGameManager(start_state, settings) self.on_update_tree = CustomEvent() self.game = game self.canvas.player_move_performed += self._handle_player_move_performed self.on_update_tree += self._handle_machine_move_performed
class MonteCarloWindowManager: """ Class responsible for managing visualization window information. """ def __init__(self, canvas: GameCanvas, game_mode: GameMode, start_state: BaseGameState, settings: MonteCarloSettings, game: Game): self.canvas = canvas self.game_mode = game_mode self.mc_manager = MonteCarloGameManager(start_state, settings) self.on_update_tree = CustomEvent() self.game = game self.canvas.player_move_performed += self._handle_player_move_performed self.on_update_tree += self._handle_machine_move_performed def perform_algorithm_move(self): """ Calculates next PC's move and performs it. It notifies other methods to update information in window, such as: - game status label - chosen node info. It also informs whether the game is still in progress. If not, player cannot click and needs to start over. Returns: None """ alg_move = self.mc_manager.calculate_next_move() self.canvas.perform_algorithm_move(alg_move) if self.game == Game.Chess: phase = ChessState.cast_chess_phase_to_abstract_phase( self.canvas.chess_manager.board.game_status) elif self.game == Game.Mancala: phase = self.canvas.board.phase move_info = {"phase": phase, "node": self.mc_manager.chosen_node} self.on_update_tree.fire(self, earg=move_info) if self.game_mode == GameMode.PC_VS_PC: self.mc_manager.perform_previous_move() def _handle_player_move_performed(self, sender, move_info): if self.game_mode == GameMode.PLAYER_VS_PC and move_info[ "phase"] == GamePhase.IN_PROGRESS: self.mc_manager.notify_move_performed(move_info["move"]) QApplication.setOverrideCursor(Qt.WaitCursor) self.perform_algorithm_move() QApplication.restoreOverrideCursor() elif move_info["phase"] != GamePhase.IN_PROGRESS: self.canvas.set_player_can_click(False) self.canvas.game_ended = True def _handle_machine_move_performed(self, sender, move_info): if move_info["phase"] != GamePhase.IN_PROGRESS: self.canvas.set_player_can_click(False) self.canvas.game_ended = True
def __init__(self): """ Initialize chess-game logic class. Set white color to start. """ self.possible_moves = [] self.check = False self.current_player_color = Color.WHITE self.figures = ChessFiguresCollection(Chessboard.create_figures()) self.game_status = GameStatus.IN_PROGRESS self.past_moves = [] self.notify_tile_marked = CustomEvent()
def __init__(self, parent: QMainWindow, manager: MonteCarloWindowManager, main_layout: QGridLayout): super(GameWindow, self).__init__(parent) self.parent = parent self.manager = manager self.main_widget = QWidget() self._create_game_layout() main_layout.addWidget(self.game_widget, 1, 0, alignment=QtCore.Qt.AlignTop) self.main_widget.setLayout(main_layout) self.setCentralWidget(self.main_widget) self.on_close_request = CustomEvent() self.manager.canvas.player_move_performed += self.change_game_status_label self.manager.on_update_tree += self.change_game_status_label
def __init__(self): super().__init__() self.WIDTH = 600 self.HEIGHT = 600 self.player_move_performed = CustomEvent() self.setMinimumSize(self.WIDTH, self.HEIGHT) self.setMaximumSize(self.WIDTH, self.HEIGHT) self.player_can_click = True self.game_ended = False
def _setup_widget(self): self.native.setMinimumWidth(600) self.native.setMinimumHeight(600) self.native.wheelEvent = self.handle_wheel_event self.native.mousePressEvent = self.handle_mouse_click_event self.native.mouseMoveEvent = self.handle_mouse_move_event self.native.mouseReleaseEvent = self.handle_mouse_release_event self.on_node_clicked = CustomEvent() set_viewport(0, 0, self.physical_size[0], self.physical_size[1]) set_state(clear_color=(160 / 255, 160 / 255, 160 / 255, 1), depth_test=False, blend=True, blend_func=("src_alpha", "one_minus_src_alpha"))
class Chessboard: """ Class is responsible for chess logic. """ def __init__(self): """ Initialize chess-game logic class. Set white color to start. """ self.possible_moves = [] self.check = False self.current_player_color = Color.WHITE self.figures = ChessFiguresCollection(Chessboard.create_figures()) self.game_status = GameStatus.IN_PROGRESS self.past_moves = [] self.notify_tile_marked = CustomEvent() @staticmethod def create_figures(): """ Arranges chess figures in their native positions. Returns: list of chess figures """ base_figures = [ Rook(Color.WHITE, (0, 0)), Knight(Color.WHITE, (0, 1)), Bishop(Color.WHITE, (0, 2)), Queen(Color.WHITE, (0, 3)), King(Color.WHITE, (0, 4)), Bishop(Color.WHITE, (0, 5)), Knight(Color.WHITE, (0, 6)), Rook(Color.WHITE, (0, 7)), Rook(Color.BLACK, (7, 0)), Knight(Color.BLACK, (7, 1)), Bishop(Color.BLACK, (7, 2)), Queen(Color.BLACK, (7, 3)), King(Color.BLACK, (7, 4)), Bishop(Color.BLACK, (7, 5)), Knight(Color.BLACK, (7, 6)), Rook(Color.BLACK, (7, 7)) ] for i in range(8): base_figures.append(Pawn(Color.WHITE, (1, i))) base_figures.append(Pawn(Color.BLACK, (6, i))) return base_figures def deep_copy(self): """ Creates a deep copy. Returns: deep copy """ rc = Chessboard() rc.current_player_color = self.current_player_color rc.check = self.check rc.game_status = self.game_status rc.possible_moves = copy.deepcopy(self.possible_moves) rc.figures = copy.deepcopy(self.figures) rc.past_moves = copy.deepcopy(self.past_moves) return rc def check_for_check(self, color_that_causes_check): """ Determines if player with given color threatens the opponent with check. The function updates check mask of the king if this color. Args: color_that_causes_check: color of the king to check if threatened Returns: bool value telling whether there is a check """ king = self.figures.get_king(color_that_causes_check) king.update_check_mask(self.figures) self.check = king.check_mask[king.position] def do_move(self, move, selected_tile): """ Does chess move and changes positions of the involved figures. Determines which move type is this. Updates king's position. Args: move: ChessMove class object selected_tile: tile selected to move to Returns: None """ figure_moved = self.figures.get_figure_at(selected_tile) if move.move_type == MoveType.NORMAL: from chess.chess_utils import do_normal_move do_normal_move(self, move, figure_moved) elif move.move_type == MoveType.PAWN_DOUBLE_MOVE: from chess.chess_utils import do_pawn_double_move do_pawn_double_move(self, move, figure_moved) elif move.move_type == MoveType.EN_PASSANT: from chess.chess_utils import do_en_passant_move do_en_passant_move(self, move, figure_moved) elif move.move_type == MoveType.PROMOTION: from chess.chess_utils import do_promotion do_promotion(self, move, figure_moved) elif move.move_type == MoveType.CASTLE_SHORT or move.move_type == MoveType.CASTLE_LONG: from chess.chess_utils import do_castling do_castling(self, move, figure_moved) if figure_moved.figure_type == FigureType.KING: self.figures.set_king_reference(figure_moved) def add_past_move(self, position, figures_count_before_move, old_position): """ Adds move to the 'historical moves' list. Args: position: position we make move on figures_count_before_move: to determine whether the move was a capture old_position: position we make move from Returns: None """ figure = self.figures.get_figure_at(position) from chess.chess_utils import PastMove self.past_moves.append( PastMove( position, self.check, figure, len(self.figures.figures_list) < figures_count_before_move, old_position)) def perform_legal_move(self, move): """ Main function that does a move and updates game status afterwards. It does move, checks for king-check, adds move to the list of past moves, switches current moving player and updates game status. It also clears 'effects' on pawns that could be captured en passant. Args: move: ChessMove object Returns: None """ Pawn.clear_en_passant_capture_ability_for_one_team( self.figures, self.current_player_color) figures_count_before_move = len(self.figures.figures_list) self.do_move(move, move.position_from) self.check_for_check(self.get_opposite_color()) if self.check: king_pos = self.figures.get_king_position( self.get_opposite_color()) self.notify_tile_marked.fire(self, earg=TileMarkArgs( king_pos, TileMarkType.CHECKED)) self.add_past_move(move.position_to, figures_count_before_move, move.position_from) self.notify_tile_marked.fire(self, earg=TileMarkArgs(move.position_from, TileMarkType.MOVED)) self.notify_tile_marked.fire(self, earg=TileMarkArgs(move.position_to, TileMarkType.MOVED)) self.switch_current_player() self.update_game_status() def update_game_status(self): """ Determins if game is still in progress or it has ended (with checkmate, stalemate, draw, etc.) Returns: None """ from chess.chess_utils import is_there_any_possible_move move_is_possible = is_there_any_possible_move(self) if not move_is_possible: if self.check: self.game_status = \ GameStatus.CHECKMATE_WHITE if self.current_player_color == Color.BLACK else GameStatus.CHECKMATE_BLACK else: self.game_status = GameStatus.STALEMATE else: from chess.chess_utils import is_there_a_draw is_there_a_draw(self) def get_opposite_color(self): """ Returns: Opponent's color """ return Color.WHITE if self.current_player_color == Color.BLACK else Color.BLACK def switch_current_player(self): """ Sets opponent as the current moving player. """ self.current_player_color = self.get_opposite_color()
class GameWindow(QMainWindow): """ Class responsible for game window management. It provides 'Start over' button that resets the window. """ def __init__(self, parent: QMainWindow, manager: MonteCarloWindowManager, main_layout: QGridLayout): super(GameWindow, self).__init__(parent) self.parent = parent self.manager = manager self.main_widget = QWidget() self._create_game_layout() main_layout.addWidget(self.game_widget, 1, 0, alignment=QtCore.Qt.AlignTop) self.main_widget.setLayout(main_layout) self.setCentralWidget(self.main_widget) self.on_close_request = CustomEvent() self.manager.canvas.player_move_performed += self.change_game_status_label self.manager.on_update_tree += self.change_game_status_label def _create_game_layout(self): self.game_layout = QGridLayout() # self.game_layout.setContentsMargins(0, 20, 20, 20) self.game_layout.setSpacing(20) self.game_widget = QWidget() self.game_widget.setLayout(self.game_layout) self.start_over_button = get_button("Start over") self.start_over_button.clicked.connect(self._handle_start_over_button) self.game_status_label = get_non_resizable_label("Game in progress") self.game_layout.addWidget(self.manager.canvas, 0, 0) self.game_layout.addWidget(self.game_status_label, 1, 0, alignment=QtCore.Qt.AlignCenter) self.game_layout.addWidget(self.start_over_button, 2, 0, alignment=QtCore.Qt.AlignCenter) def _handle_start_over_button(self): answer = show_dialog("Do you want to restart the game?") if answer == QMessageBox.Ok: game_window_properties = { "game": self.manager.game, "game_mode": self.manager.game_mode, "settings": self.manager.mc_manager.settings, "display_settings": None } self.on_close_request.fire(self, earg=game_window_properties) self.close() def update_game_status_label(self, game_status): """ Changes game status label depending on the status given" Player 1 WINS/Player 2 WINS/DRAW. Args: game_status: GamePhase enum object Returns: None """ if game_status == GamePhase.IN_PROGRESS or self.game_status_label.text( ) != "Game in progress": return else: self.game_status_label.setFont(LARGE_FONT_BOLD) if game_status == GamePhase.PLAYER1_WON: label_text = "Player 1 WINS" elif game_status == GamePhase.PLAYER2_WON: label_text = "Player 2 WINS" elif game_status == GamePhase.DRAW: label_text = "DRAW" self.game_status_label.setText(label_text) def change_game_status_label(self, sender, move_info): """ Changes label with information about game status - in progress or finished Args: sender: info about object sending the notification move_info: dictionary with information about move, e.g. game phase it caused Returns: None """ self.update_game_status_label(move_info['phase']) def showEvent(self, event): """ Overrides base class. Shows window and centers it in relation to parent window. Args: event: QShowEvent, information about window-showing event Returns: None """ super().showEvent(event) amend_window_position_on_screen(self)
def __init__(self, tree: MonteCarloTree, settings: MonteCarloSettings): self.tree = tree self.settings = settings self.iteration_performed = CustomEvent() self.iterations = 0
class MonteCarloTreeSearch: """ Class responsible for executing four steps of the Monte Carlo Tree Search method in an iterative way. """ def __init__(self, tree: MonteCarloTree, settings: MonteCarloSettings): self.tree = tree self.settings = settings self.iteration_performed = CustomEvent() self.iterations = 0 def calculate_next_move(self) -> (BaseGameMove, BaseGameState): """ Depending on the user settings, function calculates the best move for a computer using UCT algorithm.\ It is calculated by limiting maximum iterations number or by the given time limit. The calculation process covers 4 phases: selection, expansion, simulation and backpropagation. Returns: tuple of (BaseGameMove, BaseGameState, MonteCarloNode) of the chosen move """ if self.settings.limit_iterations: return self._calculate_next_move_iterations_limited() else: return self._calculate_next_move_time_limited() def _calculate_next_move_iterations_limited(self): """ Calculates the best move for a computer using UCT algorithm for a given number of iterations. After the calculation an event that signalizes the end of iteration is triggered. Returns: tuple of (BaseGameMove, BaseGameState, MonteCarloNode) of the chosen move """ while self.iterations < self.settings.max_iterations: self._perform_iteration() self.iteration_performed.fire( self, self.iterations / self.settings.max_iterations) return self._select_result_node() def _calculate_next_move_time_limited(self): """ Calculates the best move for a computer using UCT algorithm for a given amount of time. After the calculation an event that signalizes the end of iteration is triggered. When the time is over during calculation, the last iteration is calculated to the end. Returns: tuple of (BaseGameMove, BaseGameState, MonteCarloNode) of the chosen move """ start_time = time.time() elapsed_time_ms = 0 max_time = self.settings.get_internal_time() progress_fraction = 0 while elapsed_time_ms < max_time: self._perform_iteration() elapsed_time_ms = (time.time() - start_time) * 1000 progress_fraction = elapsed_time_ms / max_time self.iteration_performed.fire(self, progress_fraction) if progress_fraction != 1: self.iteration_performed.fire(self, 1) return self._select_result_node() def _perform_iteration(self): """ Performs single UCT algorithm iteration. Execution consists of four steps: selection, expansion, simulation and backpropagation. Returns: None """ QApplication.processEvents() promising_node = self._selection(self.tree.root) self._expansion(promising_node) if promising_node.has_children(): leaf_to_explore = NodeUtils.get_random_child(promising_node) else: leaf_to_explore = promising_node simulation_result = self._simulation(leaf_to_explore) self._backpropagation(leaf_to_explore, simulation_result) self.iterations += 1 def _select_result_node(self): """ Selects the best UCT node and retrieves game state of that node. The turn is switched afterwards in the resulting game state. Returns: tuple of (BaseGameMove, BaseGameState, MonteCarloNode) of the chosen move """ best_child = NodeUtils.get_child_with_max_score(self.tree.root) result_game_state = self.tree.retrieve_node_game_state(best_child) result_game_state.switch_current_player() result_move = best_child.move return result_move, result_game_state, best_child def _selection(self, node): """ Executes 1st stage of MCTS. Starts from root R and selects successive child nodes until a leaf node L is reached. Args: node: node from which to start selection Returns: UCT-best leaf node """ tmp_node = node while tmp_node.has_children() != 0: tmp_node = self._find_best_child_with_uct(tmp_node) return tmp_node def _expansion(self, node): """ Executes 2nd stage of MCTS. Unless L ends the game, creates one (or more) child nodes and chooses node C from one of them. Args: node: node from which to start expanding Returns: None """ node_state = self.tree.retrieve_node_game_state(node) possible_moves = node_state.get_all_possible_moves() for move in possible_moves: node.add_child_by_move(move[0], state_desc=move[1]) def _simulation(self, leaf) -> MonteCarloSimulationResult: """ Executes 3rd stage of MCTS. Complete a random playout from node C. Args: leaf: leaf from which to process a random playout Returns: None """ leaf_state = self.tree.retrieve_node_game_state(leaf) tmp_state = leaf_state.deep_copy() tmp_phase = leaf_state.phase moves_counter = 0 while tmp_phase == Enums.GamePhase.IN_PROGRESS: tmp_state.perform_random_move() tmp_phase = tmp_state.phase moves_counter += 1 if self.settings.limit_moves and moves_counter >= self.settings.max_moves_per_iteration: break return MonteCarloSimulationResult(tmp_state) def _backpropagation(self, leaf, simulation_result: MonteCarloSimulationResult): """ Executes 4th stage of MCTS. Uses the result of the playout to update information in the nodes on the path from C to R. Args: leaf: leaf from which to start backpropagating simulation_result: result of random simulation simulated from Returns: None """ leaf_state = self.tree.retrieve_node_game_state(leaf) leaf_player = leaf_state.current_player if simulation_result.phase == Enums.get_player_win(leaf_player): reward = 1 elif simulation_result.phase == Enums.GamePhase.DRAW: reward = 0.5 else: reward = simulation_result.get_reward(leaf_player) tmp_node = leaf while tmp_node != self.tree.root: tmp_node.details.mark_visit() tmp_current_player = tmp_node.move.player if leaf_player == tmp_current_player: tmp_node.details.add_score(reward) tmp_node = tmp_node.parent self.tree.root.details.mark_visit() def _find_best_child_with_uct(self, node): """ Calculates UCT value for children of a given node, with the formula: uct_value = (win_score / visits) + 1.41 * sqrt(log(parent_visit) / visits) and returns the most profitable one. Args: node: MonteCarloNode object Returns: MonteCarloNode node with the best UCT calculated value """ def uct_value(n, p_visit, exp_par): visits = n.details.visits_count win_score = n.details.win_score if visits == 0: return 10000000 # TODO: won't 2 be enough? else: uct_val = (win_score / visits) + exp_par * sqrt(log(p_visit) / visits) return uct_val parent_visit = node.details.visits_count return max(node.children, key=lambda n: uct_value(n, parent_visit, self.settings. exploration_parameter))