Beispiel #1
0
    def playother(self, async_callback=None):
        """
        Set the engine to play the side whose turn it is NOT to move.

        :return: Nothing
        """
        if not self.features.supports("playother"):
            raise EngineStateException(
                "engine does not support the 'playother' feature")

        with self.state_changed:
            if not self.idle:
                raise EngineStateException(
                    "playother command while engine is busy")

        self.in_force = False

        def command():
            with self.semaphore:
                self.send_line("playother")

                if self.terminated.is_set():
                    raise EngineTerminatedException

        return self._queue_command(command, async_callback)
    def ponderhit(self, *, async_callback=None):
        """
        May be sent if the expected ponder move has been played.

        The engine should continue searching, but should switch from pondering
        to normal search.

        :return: Nothing.

        :raises: :exc:`~chess.uci.EngineStateException` if the engine is not
            currently searching in ponder mode.
        """
        with self.state_changed:
            if self.idle:
                raise EngineStateException("ponderhit but not searching")
            if not self.pondering:
                raise EngineStateException("ponderhit but not pondering")

            self.pondering = False
            self.state_changed.notify_all()

        def command():
            self.search_started.wait()
            with self.semaphore:
                self.send_line("ponderhit")

        return self._queue_command(command, async_callback)
Beispiel #3
0
    def cores(self, num, async_callback=None):
        """
        Set the maximum number of processors the engine is allowed to use.
        That is, the number of search threads for an SMP engine.

        :param num: The number of processors

        :return: Nothing
        """
        if not self.features.supports("smp"):
            raise EngineStateException(
                "engine does not support the 'smp' feature")

        with self.state_changed:
            if not self.idle:
                raise EngineStateException(
                    "cores command while engine is busy")

        def command():
            with self.semaphore:
                self.send_line("cores " + str(num))

                if self.terminated.is_set():
                    raise EngineTerminatedException

        return self._queue_command(command, async_callback)
Beispiel #4
0
    def memory(self, amount, async_callback=None):
        """
        Set the maximum memory of the engines hash/pawn/bitbase/etc tables.

        :param amount: Maximum amount of memory to use in MegaBytes

        :return: Nothing
        """
        if not self.features.supports("memory"):
            raise EngineStateException(
                "engine does not support the 'memory' feature")

        with self.state_changed:
            if not self.idle:
                raise EngineStateException(
                    "memory command while engine is busy")

        def command():
            with self.semaphore:
                self.send_line("memory " + str(amount))

                if self.terminated.is_set():
                    raise EngineTerminatedException

        return self._queue_command(command, async_callback)
Beispiel #5
0
    def on_line_received(self, buf):
        LOGGER.debug("%s >> %s", self.process, buf)

        if buf.startswith("feature"):
            return self._feature(buf[8:])
        elif buf.startswith("Illegal"):
            split_buf = buf.split()
            illegal_move = split_buf[-1]
            exception_msg = "Engine received an illegal move: {}".format(illegal_move)
            if len(split_buf) == 4:
                reason = split_buf[2][1:-2]
                exception_msg = " ".join([exception_msg, reason])
            raise EngineStateException(exception_msg)
        elif buf.startswith("Error"):
            err_msg = buf.split()[1][1:-2]
            raise EngineStateException("Engine produced an error: {}".format(err_msg))
        elif buf.startswith("#"):
            return

        command_and_args = buf.split()
        if not command_and_args:
            return

        if len(command_and_args) == 1:
            if command_and_args[0] == "resign":
                return self._resign()
        elif len(command_and_args) == 2:
            if command_and_args[0] == "pong":
                return self._pong(command_and_args[1])
            elif command_and_args[0] == "move":
                return self._move(command_and_args[1])
            elif command_and_args[0] == "offer" and command_and_args[1] == "draw":
                return self._offer_draw()
        elif len(command_and_args) >= 5:
            return self._post(buf)
Beispiel #6
0
    def setboard(self, board, async_callback=None):
        """
        Set up a given board position.

        :param board: A *chess.Board*.

        :return: Nothing

        :raises: :exc:`~chess.engine.EngineStateException` if the engine is still
            calculating.
        """
        with self.state_changed:
            if not self.idle:
                raise EngineStateException(
                    "setboard command while engine is busy")

        # Setboard should be sent after force.
        self.force()

        builder = []
        builder.append("setboard")
        builder.append(board.fen())

        self.board = board.copy(stack=False)

        def command():
            with self.semaphore:
                self.send_line(" ".join(builder))

                if self.terminated.is_set():
                    raise EngineTerminatedException()

        return self._queue_command(command, async_callback)
Beispiel #7
0
    def new(self, async_callback=None):
        """
        Reset the board to the standard chess starting position.
        Set White on move.
        Leave force mode and set the engine to play Black.
        Associate the engines clock with Black and the opponent's clock
        with White.
        Reset clocks and time controls to the start of a new game.
        Use wall clock for time measurement.
        Stop clocks.
        Do not ponder on this move, even if pondering is on.
        Remove any search depth limit previously set by the sd command.

        :return: Nothing
        """
        with self.state_changed:
            if not self.idle:
                raise EngineStateException("new command while engine is busy")

        def command():
            with self.semaphore:
                self.send_line("new")

                if self.terminated.is_set():
                    raise EngineTerminatedException()

        return self._queue_command(command, async_callback)
Beispiel #8
0
    def go(self, async_callback=None):
        """
        Set engine to move on the current side to play.
        Start calculating on the current position.

        :return: the best move according to the engine.

        :raises: :exc:`~chess.engine.EngineStateException` if the engine is
            already calculating.
        """
        with self.state_changed:
            if not self.idle:
                raise EngineStateException(
                    "go command while engine is already busy")

            self.idle = False
            self.search_started.clear()
            self.move_received.clear()
            self.state_changed.notify_all()

        for post_handler in self.post_handlers:
            post_handler.on_go()

        def command():
            with self.semaphore:
                self.send_line("go")
                self.search_started.set()

            self.move_received.wait()

            with self.state_changed:
                self.idle = True
                self.state_changed.notify_all()

            if self.terminated.is_set():
                raise EngineTerminatedException()

            if self.auto_force:
                self.force()

            try:
                self.board.push_uci(str(self.move))
            except ValueError:
                try:
                    self.board.push_san(str(self.move))
                except ValueError:
                    LOGGER.exception("exception parsing move")

            return self.move

        return self._queue_command(command, async_callback)
Beispiel #9
0
    def time(self, time, async_callback=None):
        """
        Synchronize the engines clock with the total amount of time left.

        :param time: The total time left in centiseconds

        :return: Nothing
        """
        if not self.features.supports("time"):
            raise EngineStateException(
                "engine does not support the 'time' feature")

        with self.state_changed:
            if not self.idle:
                raise EngineStateException("time command while engine is busy")

        def command():
            with self.semaphore:
                self.send_line("time " + str(time))

                if self.terminated.is_set():
                    raise EngineTerminatedException

        return self._queue_command(command, async_callback)
Beispiel #10
0
    def level(self,
              movestogo=0,
              minutes=5,
              seconds=None,
              inc=0,
              async_callback=None):
        """
        Set the time controls for the game.

        :param movestogo: The number of moves to be played before the time
            control repeats.
            Defaults to 0 (in order to play the whole game in the given time control).
        :param minutes: The number of minutes for the whole game or until
            *movestogo* moves are played. In addition to seconds.
            Defaults to 5.
        :param seconds: The number of seconds for the whole game or until
            *movestogo* moves are played. In addition to minutes.
            Defaults to 0.
        :param inc: The amount of increment(in seconds) to be provided
            after each move is played.
            Defaults to 0.

        :return: Nothing
        """
        with self.state_changed:
            if not self.idle:
                raise EngineStateException(
                    "level command while engine is busy")

        builder = []
        builder.append("level")
        builder.append(str(int(movestogo)))
        builder.append(str(int(minutes)))

        if seconds is not None:
            builder.append(":")
            builder.append(str(int(seconds)))

        builder.append(str(int(inc)))

        def command():
            with self.semaphore:
                self.send_line(" ".join(builder))

                if self.terminated.is_set():
                    raise EngineTerminatedException

        return self._queue_command(command, async_callback)
    def send_variant(self, variant, *, async_callback=None):
        """
        Optionally sent to the engine immediately after 'new' for games that use
        other than FIDE rules. Sets the engine to play mentioned variant.
        Only used with variants that the engine announced it could play in the
        'feature variants="variant,variant,..."' command at startup.

        :param variant: The variant name to play.

        :return: Nothing.
        """
        self._assert_not_busy("variant")

        if variant not in self.supported_variants:
            raise EngineStateException("Engine does not support %s variant" % variant)

        command = self.command("variant %s" % variant)
        return self._queue_command(command, async_callback)
    def egtpath(self, egt_type, egt_path, *, async_callback=None):
        """
        Tells the engine to use the *egt_type* endgame tablebases at *egt_path*.

        The engine must have this type specified in the *feature egt*. For example,
        the engine may have *feature egt=syzygy*, then it is legal to call
        *egtpath("syzygy", "<path-to-syzygy>")*.

        :param egt_type: The type of EGT pointed to (syzygy, gaviota, etc.).
        :param egt_path: The path to the desired EGT.

        :return: Nothing.
        """
        if egt_type not in self.features.get("egt"):
            raise EngineStateException("engine does not support the '{}' egt".format(egt_type))

        builder = ["egtpath", egt_type, egt_path]
        command = self.command(" ".join(builder))
        return self._queue_command(command, async_callback)
Beispiel #13
0
    def st(self, time, async_callback=None):
        """
        Set maximum time the engine is to search for.

        :param time: Time to search for in seconds

        :return: Nothing
        """
        with self.state_changed:
            if not self.idle:
                raise EngineStateException("st command while engine is busy")

        def command():
            with self.semaphore:
                self.send_line("st " + str(time))

                if self.terminated.is_set():
                    raise EngineTerminatedException

        return self._queue_command(command, async_callback)
Beispiel #14
0
    def sd(self, depth, async_callback=None):
        """
        Set maximum depth the engine is to search for.

        :param depth: Depth to search for

        :return: Nothing
        """
        with self.state_changed:
            if not self.idle:
                raise EngineStateException("sd command while engine is busy")

        def command():
            with self.semaphore:
                self.send_line("sd " + str(depth))

                if self.terminated.is_set():
                    raise EngineTerminatedException

        return self._queue_command(command, async_callback)
 def _assert_not_busy(self, cmd):
     with self.state_changed:
         if not self.idle:
             raise EngineStateException("{} command while engine is busy".format(cmd))
 def _assert_supports_feature(self, feature_name):
     if not self.features.supports(feature_name):
         raise EngineStateException("engine does not support the '{}' feature"
                                    .format(feature_name))
    def usermove(self, move, *, async_callback=None):
        """
        Tells the XBoard engine to make a move on its internal
        board.

        If *auto_force* is set to ``True``, the engine will not start
        thinking about its next move immediately after.

        :param move: The move to play in XBoard notation.

        :return: Nothing.
        """
        builder = []
        if self.features.supports("usermove"):
            builder.append("usermove")

        if self.draw_handler:
            self.draw_handler.clear_offer()
            self.engine_offered_draw = False

        if self.auto_force:
            self.force()
        elif not self.in_force:
            with self.state_changed:
                if not self.idle:
                    raise EngineStateException("usermove command while engine is already busy")

                self.idle = False
                self.search_started.clear()
                self.move_received.clear()
                self.state_changed.notify_all()

            for post_handler in self.post_handlers:
                post_handler.on_go()

        try_move(self.board, str(move))

        builder.append(str(move))

        def command():
            move_str = " ".join(builder)
            self.ponder_move = None
            if self.in_force:
                with self.semaphore:
                    self.send_line(move_str)

                if self.terminated.is_set():
                    raise EngineTerminatedException()
            else:
                with self.semaphore:
                    self.send_line(move_str)
                    self.search_started.set()

                self.move_received.wait()

                with self.state_changed:
                    self.idle = True
                    self.state_changed.notify_all()

                if self.terminated.is_set():
                    raise EngineTerminatedException()

                try_move(self.board, str(self.move))

                return self.move

        return self._queue_command(command, async_callback)
    def go(self,
           *,
           searchmoves=None,
           ponder=False,
           wtime=None,
           btime=None,
           winc=None,
           binc=None,
           movestogo=None,
           depth=None,
           nodes=None,
           mate=None,
           movetime=None,
           infinite=False,
           async_callback=None):
        """
        Start calculating on the current position.

        All parameters are optional, but there should be at least one of
        *depth*, *nodes*, *mate*, *infinite* or some time control settings,
        so that the engine knows how long to calculate.

        Note that when using *infinite* or *ponder*, the engine will not stop
        until it is told to.

        :param searchmoves: Restrict search to moves in this list.
        :param ponder: Bool to enable pondering mode. The engine will not stop
            pondering in the background until a *stop* command is received.
        :param wtime: Integer of milliseconds White has left on the clock.
        :param btime: Integer of milliseconds Black has left on the clock.
        :param winc: Integer of white Fisher increment.
        :param binc: Integer of black Fisher increment.
        :param movestogo: Number of moves to the next time control. If this is
            not set, but wtime or btime are, then it is sudden death.
        :param depth: Search *depth* ply only.
        :param nodes: Search so many *nodes* only.
        :param mate: Search for a mate in *mate* moves.
        :param movetime: Integer. Search exactly *movetime* milliseconds.
        :param infinite: Search in the background until a *stop* command is
            received.

        :return: A tuple of two elements. The first is the best move according
            to the engine. The second is the ponder move. This is the reply
            as sent by the engine. Either of the elements may be ``None``.

        :raises: :exc:`~chess.uci.EngineStateException` if the engine is
            already calculating.
        """
        with self.state_changed:
            if not self.idle:
                raise EngineStateException(
                    "go command while engine is already busy")

            self.idle = False
            self.search_started.clear()
            self.bestmove_received.clear()
            self.pondering = ponder
            self.state_changed.notify_all()

        for info_handler in self.info_handlers:
            info_handler.on_go()

        builder = ["go"]

        if ponder:
            builder.append("ponder")

        if wtime is not None:
            builder.append("wtime")
            builder.append(str(int(wtime)))

        if btime is not None:
            builder.append("btime")
            builder.append(str(int(btime)))

        if winc is not None:
            builder.append("winc")
            builder.append(str(int(winc)))

        if binc is not None:
            builder.append("binc")
            builder.append(str(int(binc)))

        if movestogo is not None and movestogo > 0:
            builder.append("movestogo")
            builder.append(str(int(movestogo)))

        if depth is not None:
            builder.append("depth")
            builder.append(str(int(depth)))

        if nodes is not None:
            builder.append("nodes")
            builder.append(str(int(nodes)))

        if mate is not None:
            builder.append("mate")
            builder.append(str(int(mate)))

        if movetime is not None:
            builder.append("movetime")
            builder.append(str(int(movetime)))

        if infinite:
            builder.append("infinite")

        if searchmoves:
            builder.append("searchmoves")
            for move in searchmoves:
                builder.append(self.board.uci(move))

        def command():
            with self.semaphore:
                self.send_line(" ".join(builder))
                self.search_started.set()

            self.bestmove_received.wait()

            with self.state_changed:
                self.idle = True
                self.state_changed.notify_all()

            if self.terminated.is_set():
                raise EngineTerminatedException()

            return BestMove(self.bestmove, self.ponder)

        return self._queue_command(command, async_callback)
    def position(self, board, *, async_callback=None):
        """
        Set up a given position.

        Rather than sending just the final FEN, the initial FEN and all moves
        leading up to the position will be sent. This will allow the engine
        to use the move history (for example to detect repetitions).

        If the position is from a new game, it is recommended to use the
        *ucinewgame* command before the *position* command.

        :param board: A *chess.Board*.

        :return: Nothing

        :raises: :exc:`~chess.uci.EngineStateException` if the engine is still
            calculating.
        """
        # Raise if this is called while the engine is still calculating.
        with self.state_changed:
            if not self.idle:
                raise EngineStateException(
                    "position command while engine is busy")

        # Set UCI_Variant and UCI_Chess960.
        options = {}

        uci_variant = type(board).uci_variant
        if uci_variant != (self.uci_variant or "chess"):
            if self.uci_variant is None:
                LOGGER.warning(
                    "engine may not support UCI_Variant or has not been initialized with 'uci' command"
                )
            options["UCI_Variant"] = type(board).uci_variant

        if bool(self.uci_chess960) != board.chess960:
            if self.uci_chess960 is None:
                LOGGER.warning(
                    "engine may not support UCI_Chess960 or has not been initialized with 'uci' command"
                )
            options["UCI_Chess960"] = board.chess960

        option_lines = self._setoption(options)

        # Send starting position.
        builder = ["position"]
        root = board.root()
        fen = root.fen()
        if uci_variant == "chess" and fen == chess.STARTING_FEN:
            builder.append("startpos")
        else:
            builder.append("fen")
            builder.append(root.shredder_fen() if self.uci_chess960 else fen)

        # Send moves.
        if board.move_stack:
            builder.append("moves")
            builder.extend(move.uci() for move in board.move_stack)

        self.board = board.copy(stack=False)

        def command():
            with self.semaphore:
                for option_line in option_lines:
                    self.send_line(option_line)

                self.send_line(" ".join(builder))

                if self.terminated.is_set():
                    raise EngineTerminatedException()

        return self._queue_command(command, async_callback)
Beispiel #20
0
    def position(self, board, async_callback=None):
        """
        Set up a given position.

        Instead of just the final FEN, the initial FEN and all moves leading
        up to the position will be sent, so that the engine can detect
        repetitions.

        If the position is from a new game, it is recommended to use the
        *ucinewgame* command before the *position* command.

        :param board: A *chess.Board*.

        :return: Nothing

        :raises: :exc:`~chess.uci.EngineStateException` if the engine is still
            calculating.
        """
        # Check UCI_Variant
        uci_variant = type(board).uci_variant
        if uci_variant == "chess" and self.uci_variant is None:
            pass
        elif uci_variant != self.uci_variant:
            LOGGER.error("current UCI_Variant (%s) does not match position (%s)", self.uci_variant, uci_variant)

        # Raise if this is called while the engine is still calculating.
        with self.state_changed:
            if not self.idle:
                raise EngineStateException("position command while engine is busy")

        builder = []
        builder.append("position")

        # Take back moves to obtain the FEN at the latest pawn move or
        # capture. Later giving the moves explicitly allows for transposition
        # detection.
        switchyard = collections.deque()
        while board.move_stack:
            move = board.pop()
            switchyard.append(move)

            if board.is_irreversible(move):
                break

        # Validate castling rights.
        if not self.uci_chess960 and board.chess960:
            if board.has_chess960_castling_rights():
                LOGGER.error("not in UCI_Chess960 mode but position has non-standard castling rights")

                # Just send the final FEN without transpositions in hopes
                # that this will work.
                while switchyard:
                    board.push(switchyard.pop())

        # Send starting position.
        if uci_variant == "chess" and board.fen() == chess.STARTING_FEN:
            builder.append("startpos")
        else:
            builder.append("fen")
            builder.append(board.shredder_fen() if self.uci_chess960 else board.fen())

        # Send moves.
        if switchyard:
            builder.append("moves")

            while switchyard:
                move = switchyard.pop()
                builder.append(board.uci(move, chess960=self.uci_chess960))
                board.push(move)

        self.board = board.copy(stack=False)

        def command():
            with self.semaphore:
                self.send_line(" ".join(builder))

                if self.terminated.is_set():
                    raise EngineTerminatedException()

        return self._queue_command(command, async_callback)
Beispiel #21
0
    def usermove(self, move, async_callback=None):
        """
        Tell the XBoard engine to make a move on it's internal
        board.
        If auto_force is set to True, the engine will not start
        thinking about it's next move immediately after.

        :param move: The move to play in XBoard notation.

        :return: Nothing
        """
        builder = []
        if self.features.supports("usermove"):
            builder.append("usermove")

        if self.auto_force:
            self.force()
        elif not self.in_force:
            with self.state_changed:
                if not self.idle:
                    raise EngineStateException(
                        "usermove command while engine is already busy")

                self.idle = False
                self.search_started.clear()
                self.move_received.clear()
                self.state_changed.notify_all()

            for post_handler in self.post_handlers:
                post_handler.on_go()

        try:
            self.board.push_uci(str(move))
        except ValueError:
            try:
                self.board.push_san(str(move))
            except ValueError:
                LOGGER.exception("exception parsing move")

        builder.append(str(move))

        def command():
            # Use the join(builder) once we parse usermove=1 feature
            move_str = " ".join(builder)
            if self.in_force:
                with self.semaphore:
                    self.send_line(move_str)

                if self.terminated.is_set():
                    raise EngineTerminatedException()
            else:
                with self.semaphore:
                    self.send_line(move_str)
                    self.search_started.set()

                self.move_received.wait()

                with self.state_changed:
                    self.idle = True
                    self.state_changed.notify_all()

                if self.terminated.is_set():
                    raise EngineTerminatedException()

                try:
                    self.board.push_uci(str(self.move))
                except ValueError:
                    try:
                        self.board.push_san(str(self.move))
                    except ValueError:
                        LOGGER.exception("exception parsing move")

                return self.move

        return self._queue_command(command, async_callback)