Exemplo n.º 1
0
    def perform_action(self, state: GameState, verbose=True):

        value, selected_move, confidence, selected_child_idx = super(
        ).perform_action(state)

        # apply the selected mve on the current board state in order to create a lookup table for future board states
        state.apply_move(selected_move)

        # select the q value for the child which leads to the best calculated line
        value = self.root_node.q[selected_child_idx]

        # select the next node
        node = self.root_node.child_nodes[selected_child_idx]

        # store the reference links for all possible child future child to the node lookup table
        for idx, mv in enumerate(state.get_legal_moves()):
            state_future = deepcopy(state)
            state_future.apply_move(mv)

            # store the current child node with it's board fen as the hash-key if the child node has already been expanded
            if node is not None and idx < node.nb_direct_child_nodes and node.child_nodes[
                    idx] is not None:
                self.node_lookup[
                    state_future.get_board_fen()] = node.child_nodes[idx]

        return value, selected_move, confidence, selected_child_idx
Exemplo n.º 2
0
    def _run_single_playout(self,
                            state: GameState,
                            parent_node: Node,
                            depth=1,
                            mv_list=[]):  #, pipe_id):
        """
        This function works recursively until a terminal node is reached

        :param state: Current game-state for the evaluation. This state differs between the treads
        :param parent_node: Current parent-node of the selected node. In the first  expansion this is the root node.
        :param depth: Current depth for the evaluation. Depth is increased by 1 for every recusive call
        :param mv_list: List of moves which have been taken in the current path. For each selected child node this list
                        is expanded by one move recursively.
        :return: -value: The inverse value prediction of the current board state. The flipping by -1 each turn is needed
                        because the point of view changes each half-move
                depth: Current depth reach by this evaluation
                mv_list: List of moves which have been selected
        """

        # select a legal move on the chess board
        node, move, child_idx = self._select_node(parent_node)

        if move is None:
            raise Exception(
                "Illegal tree setup. A 'None' move was selected which souldn't be possible"
            )

        # update the visit counts to this node
        # temporarily reduce the attraction of this node by applying a virtual loss /
        # the effect of virtual loss will be undone if the playout is over
        parent_node.apply_virtual_loss_to_child(child_idx, self.virtual_loss)

        # apply the selected move on the board
        state.apply_move(move)

        # append the selected move to the move list
        mv_list.append(move)

        if node is None:

            # get the board-fen which is used as an identifier for the board positions in the look-up table
            board_fen = state.get_board_fen()

            # check if the addressed fen exist in the look-up table
            if board_fen in self.node_lookup:
                # get the node from the look-up list
                node = self.node_lookup[board_fen]

                with parent_node.lock:
                    # setup a new connection from the parent to the child
                    parent_node.child_nodes[child_idx] = node

                # get the prior value from the leaf node which has already been expanded
                #value = node.v

                # get the value from the leaf node (the current function is called recursively)
                value, depth, mv_list = self._run_single_playout(
                    state, node, depth + 1, mv_list)

            else:
                # expand and evaluate the new board state (the node wasn't found in the look-up table)
                # its value will be backpropagated through the tree and flipped after every layer

                # receive a free available pipe
                my_pipe = self.my_pipe_endings.pop()
                my_pipe.send(state.get_state_planes())
                # this pipe waits for the predictions of the network inference service
                [value, policy_vec] = my_pipe.recv()
                # put the used pipe back into the list
                self.my_pipe_endings.append(my_pipe)

                # initialize is_leaf by default to false
                is_leaf = False

                # check if the current player has won the game
                # (we don't need to check for is_lost() because the game is already over
                #  if the current player checkmated his opponent)
                if state.is_won() is True:
                    value = -1
                    is_leaf = True
                    legal_moves = []
                    p_vec_small = None

                # check if you can claim a draw - its assumed that the draw is always claimed
                elif state.is_draw() is True:
                    value = 0
                    is_leaf = True
                    legal_moves = []
                    p_vec_small = None
                else:
                    # get the current legal move of its board state
                    legal_moves = list(state.get_legal_moves())
                    if len(legal_moves) < 1:
                        raise Exception(
                            'No legal move is available for state: %s' % state)

                    # extract a sparse policy vector with normalized probabilities
                    try:
                        p_vec_small = get_probs_of_move_list(
                            policy_vec,
                            legal_moves,
                            is_white_to_move=state.is_white_to_move(),
                            normalize=True)

                    except KeyError:
                        raise Exception('Key Error for state: %s' % state)

                # convert all legal moves to a string if the option check_mate_in_one was enabled
                if self.check_mate_in_one is True:
                    str_legal_moves = str(state.get_legal_moves())
                else:
                    str_legal_moves = ''

                # create a new node
                new_node = Node(value, p_vec_small, legal_moves,
                                str_legal_moves, is_leaf)

                #if is_leaf is False:
                # test of adding dirichlet noise to a new node
                #    new_node.apply_dirichlet_noise_to_prior_policy(epsilon=self.dirichlet_epsilon/4, alpha=self.dirichlet_alpha)

                # include a reference to the new node in the look-up table
                self.node_lookup[board_fen] = new_node

                with parent_node.lock:
                    # add the new node to its parent
                    parent_node.child_nodes[child_idx] = new_node

                # check if the new node has a mate_in_one connection (if yes overwrite the network prediction)
                if new_node.mate_child_idx is not None:
                    value = 1

        # check if we have reached a leaf node
        elif node.is_leaf is True:
            value = node.v
            # receive a free available pipe
            my_pipe = self.my_pipe_endings.pop()
            my_pipe.send(state.get_state_planes())
            # this pipe waits for the predictions of the network inference service
            [_, _] = my_pipe.recv()
            # put the used pipe back into the list
            self.my_pipe_endings.append(my_pipe)

        else:
            # get the value from the leaf node (the current function is called recursively)
            value, depth, mv_list = self._run_single_playout(
                state, node, depth + 1, mv_list)

        # revert the virtual loss and apply the predicted value by the network to the node
        parent_node.revert_virtual_loss_and_update(child_idx,
                                                   self.virtual_loss, -value)

        # we invert the value prediction for the parent of the above node layer because the player's turn is flipped every turn
        return -value, depth, mv_list
Exemplo n.º 3
0
    def _run_single_playout(self,
                            state: GameState,
                            parent_node: Node,
                            pipe_id=0,
                            depth=1,
                            chosen_nodes=[]):
        """
        This function works recursively until a leaf or terminal node is reached.
        It ends by backpropagating the value of the new expanded node or by propagating the value of a terminal state.

        :param state_: Current game-state for the evaluation. This state differs between the treads
        :param parent_node: Current parent-node of the selected node. In the first  expansion this is the root node.
        :param depth: Current depth for the evaluation. Depth is increased by 1 for every recusive call
        :param chosen_nodes: List of moves which have been taken in the current path. For each selected child node this list
                        is expanded by one move recursively.
        :param chosen_nodes: List of all nodes that this thread has explored with respect to the root node
        :return: -value: The inverse value prediction of the current board state. The flipping by -1 each turn is needed
                        because the point of view changes each half-move
                depth: Current depth reach by this evaluation
                mv_list: List of moves which have been selected
        """

        # select a legal move on the chess board
        node, move, child_idx = self._select_node(parent_node)

        if move is None:
            raise Exception(
                "Illegal tree setup. A 'None' move was selected which souldn't be possible"
            )

        # update the visit counts to this node
        # temporarily reduce the attraction of this node by applying a virtual loss /
        # the effect of virtual loss will be undone if the playout is over
        parent_node.apply_virtual_loss_to_child(child_idx, self.virtual_loss)

        if depth == 1:
            state = GameState(deepcopy(state.get_pythonchess_board()))

        # apply the selected move on the board
        state.apply_move(move)

        # append the selected move to the move list
        # append the chosen child idx to the chosen_nodes list
        chosen_nodes.append(child_idx)

        if node is None:

            # get the transposition-key which is used as an identifier for the board positions in the look-up table
            transposition_key = state.get_transposition_key()

            # check if the addressed fen exist in the look-up table
            # note: It's important to use also the halfmove-counter here, otherwise the system can create an infinite
            # feed-back-loop
            key = (transposition_key, state.get_halfmove_counter())

            # expand and evaluate the new board state (the node wasn't found in the look-up table)
            # its value will be backpropagated through the tree and flipped after every layer
            # receive a free available pipe
            my_pipe = self.my_pipe_endings[pipe_id]

            if self.send_batches is True:
                my_pipe.send(state.get_state_planes())
                # this pipe waits for the predictions of the network inference service
                [value, policy_vec] = my_pipe.recv()
            else:
                state_planes = state.get_state_planes()
                self.batch_state_planes[pipe_id] = state_planes

                my_pipe.send(pipe_id)

                result_channel = my_pipe.recv()

                value = np.array(self.batch_value_results[result_channel])
                policy_vec = np.array(
                    self.batch_policy_results[result_channel])

            # initialize is_leaf by default to false
            is_leaf = False

            # check if the current player has won the game
            # (we don't need to check for is_lost() because the game is already over
            #  if the current player checkmated his opponent)
            is_won = False
            is_check = False

            if state.is_check() is True:
                is_check = True
                if state.is_won() is True:
                    is_won = True

            if is_won is True:
                value = -1
                is_leaf = True
                legal_moves = []
                p_vec_small = None
                # establish a mate in one connection in order to stop exploring different alternatives
                parent_node.mate_child_idx = child_idx

            # get the value from the leaf node (the current function is called recursively)
            # check if you can claim a draw - its assumed that the draw is always claimed
            elif self.can_claim_threefold_repetition(transposition_key, chosen_nodes) or \
                    state.get_pythonchess_board().can_claim_fifty_moves() is True:
                value = 0
                is_leaf = True
                legal_moves = []
                p_vec_small = None
            else:
                # get the current legal move of its board state
                legal_moves = state.get_legal_moves()

                if len(legal_moves) < 1:
                    raise Exception(
                        'No legal move is available for state: %s' % state)

                # extract a sparse policy vector with normalized probabilities
                try:
                    p_vec_small = get_probs_of_move_list(
                        policy_vec,
                        legal_moves,
                        is_white_to_move=state.is_white_to_move(),
                        normalize=True)

                except KeyError:
                    raise Exception('Key Error for state: %s' % state)

            # convert all legal moves to a string if the option check_mate_in_one was enabled
            if self.check_mate_in_one is True:
                str_legal_moves = str(state.get_legal_moves())
            else:
                str_legal_moves = ''

            # clip the visit nodes for all nodes in the search tree except the director opp. move
            clip_low_visit = self.use_pruning and depth != 1

            # create a new node
            new_node = Node(value, p_vec_small, legal_moves, str_legal_moves,
                            is_leaf, transposition_key, clip_low_visit)

            if depth == 1:

                # disable uncertain moves from being visited by giving them a very bad score
                if is_leaf is False:
                    if self.root_node_prior_policy[
                            child_idx] < 1e-3 and value * -1 < self.root_node.v:
                        with parent_node.lock:
                            value = 99

                if value < 0:  # and state.are_pocket_empty(): #and pipe_id == 0:
                    # test of adding dirichlet noise to a new node
                    new_node.apply_dirichlet_noise_to_prior_policy(
                        epsilon=self.dirichlet_epsilon * .02,
                        alpha=self.dirichlet_alpha)

            if self.use_pruning is False:
                # include a reference to the new node in the look-up table
                self.node_lookup[key] = new_node

            with parent_node.lock:
                # add the new node to its parent
                parent_node.child_nodes[child_idx] = new_node

        # check if we have reached a leaf node
        elif node.is_leaf is True:
            value = node.v

        else:
            # get the value from the leaf node (the current function is called recursively)
            value, depth, chosen_nodes = self._run_single_playout(
                state, node, pipe_id, depth + 1, chosen_nodes)

        # revert the virtual loss and apply the predicted value by the network to the node
        parent_node.revert_virtual_loss_and_update(child_idx,
                                                   self.virtual_loss, -value)

        # we invert the value prediction for the parent of the above node layer because the player's turn is flipped every turn
        return -value, depth, chosen_nodes
Exemplo n.º 4
0
    def evaluate_board_state(self, state: GameState):
        """
        Analyzes the current board state. This is the main method which get called by the uci interface or analysis
        request.

        :param state_in: Actual game state to evaluate for the MCTS
        :return:
        """

        # store the time at which the search started
        self.t_start_eval = time()

        # check if the net prediction service has already been started
        if self.net_pred_services[0].running is False:
            # start the prediction daemon thread
            for net_pred_service in self.net_pred_services:
                net_pred_service.start()

        # receive a list of all possible legal move in the current board position
        legal_moves = state.get_legal_moves()

        # consistency check
        if len(legal_moves) == 0:
            raise Exception(
                'The given board state has no legal move available')

        # check first if the the current tree can be reused
        key = (state.get_transposition_key(), state.get_halfmove_counter)

        if self.use_pruning is False and key in self.node_lookup:
            self.root_node = self.node_lookup[key]
            logging.debug(
                'Reuse the search tree. Number of nodes in search tree: %d',
                self.root_node.nb_total_expanded_child_nodes)
            self.total_nodes_pre_search = deepcopy(self.root_node.n_sum)

            # reset potential good nodes for the root
            self.root_node.q[self.root_node.q < 1.1] = 0

        else:
            logging.debug("Starting a brand new search tree...")
            self.root_node = None
            self.total_nodes_pre_search = 0

        # check for fast way out
        if len(legal_moves) == 1:

            # if there's only a single legal move you only must go 1 depth
            max_depth_reached = 1

            if self.root_node is None:
                # conduct all necessary steps for fastest way out
                self._expand_root_node_single_move(state, legal_moves)
        else:

            if self.root_node is None:
                # run a single expansion on the root node
                self._expand_root_node_multiple_moves(state, legal_moves)

            # conduct the mcts-search based on the given settings
            max_depth_reached = self._run_mcts_search(state)

            t_elapsed = time() - self.t_start_eval
            print('info string move overhead is %dms' %
                  (t_elapsed * 1000 - self.movetime_ms))

        # receive the policy vector based on the MCTS search
        p_vec_small = self.root_node.get_mcts_policy(self.q_value_weight)

        if self.use_pruning is False:
            # store the current root in the lookup table
            self.node_lookup[key] = self.root_node

        # select the q-value according to the mcts best child value
        best_child_idx = p_vec_small.argmax()
        value = self.root_node.q[best_child_idx]

        lst_best_moves, _ = self.get_calculated_line()
        str_moves = self._mv_list_to_str(lst_best_moves)

        # show the best calculated line
        node_searched = int(self.root_node.n_sum - self.total_nodes_pre_search)
        # In uci the depth is given using half-moves notation also called plies
        time_e = time() - self.t_start_eval

        if len(legal_moves) != len(p_vec_small):
            raise Exception(
                'Legal move list %s with length %s is uncompatible to policy vector %s'
                ' with shape %s for board state %s and nodes legal move list: %s'
                % (legal_moves, len(legal_moves), p_vec_small,
                   p_vec_small.shape, state, self.root_node.legal_moves))

        # define the remaining return variables
        cp = value_to_centipawn(value)
        depth = max_depth_reached
        nodes = node_searched
        time_elapsed_s = time_e * 1000
        nps = node_searched / time_e
        pv = str_moves

        return value, legal_moves, p_vec_small, cp, depth, nodes, time_elapsed_s, nps, pv