예제 #1
0
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)
예제 #2
0
 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()
예제 #6
0
    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
예제 #7
0
 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
예제 #8
0
 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()
예제 #10
0
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))