I am using the stockfish 3.23 package in python. To get the evaluation of the chess position, I use the following code:

self.stockfish = Stockfish(path="stockfish\\stockfish", depth=18, parameters={"Threads": 2, "Minimum Thinking Time": 1000})
self.stockfish.set_fen_position(fen)
evaluationValue = self.stockfish.get_evaluation()['value']

This works fine. However, I would like stockfish to constantly evaluate the position, and give me the current evaluation when I want, instead of waiting a predetermined amount of time for the result of the evaluation.

Is this possible?

Thank you very much, Joost

2

There are 2 answers

1
eligolf On

I assume one way to solve it would be to make the call in a loop from 1 to maxDepth and then print the results for each depth in the loop.

I am not sure how the Stockfish package works but Stockfish uses some sort of iterative deepening which means that if it searches for depth 18 it will do the loop mentioned above. I just don't know how to print the results from that built in loop with that library, maybe there is some better way of doing it than I proposed.

0
thedemons On

In the stockfish package, The get_evaluation function works by evaluating the top moves in the current position, the score is either the centipawn or mate. While evaluating, stockfish will output top moves at each depth, but the package will wait until the evaluation is done.

I have created a pull request that adds generate_top_moves method which returns a generator that yields top moves in the position at each depth. Here's the idea, you can read more about this in the PR:


class TopMove:
    def __init__(self, line: str) -> None:
        splits = line.split(" ")
        pv_index = splits.index("pv")
        self.move = splits[pv_index + 1]
        self.line = splits[pv_index + 1 :]
        self.depth = int(splits[splits.index("depth") + 1])
        self.seldepth = int(splits[splits.index("seldepth") + 1])

        self.cp = None
        self.mate = None

        try:
            self.cp = int(splits[splits.index("cp") + 1])
        except ValueError:
            self.mate = int(splits[splits.index("mate") + 1])

    def dict(self) -> dict:
        return {
            "move": self.move,
            "depth": self.depth,
            "seldepth": self.seldepth,
            "line": self.line,
            "cp": self.cp,
            "mate": self.mate,
        }

    # compare if this move is better than the other move
    def __gt__(self, other: Stockfish.TopMove) -> bool:

        if other.mate is None:
            # this move is mate and the other is not
            if self.mate is not None:
                # a negative mate value is a losing move
                return self.mate < 0

            # both moves has no mate, compare the depth first than centipawn
            if self.depth == other.depth:
                if self.cp == other.cp:
                    return self.seldepth > other.seldepth
                else:
                    return self.cp > other.cp
            else:
                return self.depth > other.depth

        else:
            # both this move and other move is mate
            if self.mate is not None:
                # both losing move, which takes more moves is better
                # both winning move, which takes less move is better
                if (
                    self.mate < 0
                    and other.mate < 0
                    or self.mate > 0
                    and other.mate > 0
                ):
                    return self.mate < other.mate
                else:
                    # comparing a losing move with a winning move, positive mate score is winning
                    return self.mate > other.mate
            else:
                return other.mate < 0

    # the oposite of __gt__
    def __lt__(self, other: Stockfish.TopMove) -> bool:
        return not self.__gt__(other)

    # equal move, by "move", not by score/evaluation
    def __eq__(self, other: Stockfish.TopMove) -> bool:
        return self.move == other.move

def generate_top_moves(
    self, num_top_moves: int = 5
) -> Generator[List[TopMove], None, None]:
    """Returns a generator that yields top moves in the position at each depth

    Args:
        num_top_moves:
            The number of moves to return info on, assuming there are at least
            those many legal moves.

    Returns:
        A generator that yields top moves in the position at each depth.

        The evaluation could be stopped early by calling Generator.close();
            this however will take some time for stockfish to stop.

        Unlike `get_top_moves` - which returns a list of dict, this will yield
        a list of `Stockfish.TopMove` instead, and the score (cp/mate) is relative
        to which side is playing instead of absolute like `get_top_moves`.

        The score is either `cp` or `mate`; a higher `cp` is better, positive `mate`
        is winning and vice versa.

        If there are no moves in the position, an empty list is returned.
    """

    if num_top_moves <= 0:
        raise ValueError("num_top_moves is not a positive number.")

    old_MultiPV_value = self._parameters["MultiPV"]
    if num_top_moves != self._parameters["MultiPV"]:
        self._set_option("MultiPV", num_top_moves)
        self._parameters.update({"MultiPV": num_top_moves})

    foundBestMove = False

    try:
        self._go()

        top_moves: List[Stockfish.TopMove] = []
        current_depth = 1

        while True:
            line = self._read_line()

            if "multipv" in line and "depth" in line:
                move = Stockfish.TopMove(line)

                # try to find the move in the list, if it exists then update it, else append to the list
                try:
                    idx = top_moves.index(move)

                    # don't update if the new move has a smaller depth than the one in the list
                    if move.depth >= top_moves[idx].depth:
                        top_moves[idx] = move

                except ValueError:
                    top_moves.append(move)

                # yield the top moves once the current depth changed, the current depth might be smaller than the old depth
                if move.depth != current_depth:
                    current_depth = move.depth
                    top_moves.sort(reverse=True)
                    yield top_moves[:num_top_moves]

            elif line.startswith("bestmove"):
                foundBestMove = True
                best_move = line.split(" ")[1]

                
                # no more moves, the game is ended
                if best_move == "(none)":
                    yield []
                else:
                    # sort the list once again
                    top_moves.sort(reverse=True)

                    # if the move at index 0 is not the best move returned by stockfish
                    if best_move != top_moves[0].move:
                        for move in top_moves:
                            if best_move == move.move:
                                top_moves.remove(move)
                                top_moves.insert(0, move)
                                break
                        else:
                            raise ValueError(f"Stockfish returned the best move: {best_move}, but it's not in the list")
                        

                    yield top_moves[:num_top_moves]

                break

    except BaseException as e:
        raise e from e

    finally:
        # stockfish has not returned the best move, but the generator was signaled to close
        if not foundBestMove:
            self._put("stop")
            while not self._read_line().startswith("bestmove"):
                pass

        if old_MultiPV_value != self._parameters["MultiPV"]:
            self._set_option("MultiPV", old_MultiPV_value)
            self._parameters.update({"MultiPV": old_MultiPV_value})

To evaluate the position, you can get the top moves, then the score will be either mate or cp (centipawn) of the best move:

for top_moves in stockfish.generate_top_moves():
    best_move = top_moves[0]
    print(f"Evaluation at depth {best_move.depth}: {best_move.cp}")

The output for the starting position:

Evaluation at depth 2: 141
Evaluation at depth 3: 127
Evaluation at depth 4: 77
Evaluation at depth 5: 70
Evaluation at depth 6: 69
Evaluation at depth 7: 77
Evaluation at depth 8: 77
Evaluation at depth 9: 83
Evaluation at depth 10: 83
Evaluation at depth 11: 63
Evaluation at depth 12: 63
Evaluation at depth 13: 70
Evaluation at depth 14: 56
Evaluation at depth 15: 56
Evaluation at depth 16: 56
Evaluation at depth 17: 56
Evaluation at depth 18: 49
Evaluation at depth 18: 49

With this simple method added, you can do some amazing stuff like this, the evaluation bar on the left is calculated in python: