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)) command_and_args = buf.split() if not command_and_args: return if len(command_and_args) == 1: if command_and_args[0] == "#": pass elif 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)
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
def ucinewgame(self, async_callback=None): """ Tell the engine that the next search will be from a different game. This can be a new game the engine should play or if the engine should analyse a position from a different game. Using this command is recommended, but not required. :return: Nothing """ # Warn if this is called while the engine is still calculating. with self.state_changed: if not self.idle: LOGGER.warning("ucinewgame while engine is busy") def command(): with self.semaphore: with self.readyok_received: self.send_line("ucinewgame") self.send_line("isready") self.readyok_received.wait() if self.terminated.is_set(): raise EngineTerminatedException() return self._queue_command(command, async_callback)
def on_line_received(self, buf): LOGGER.debug("%s >> %s", self.process, buf) command_and_args = buf.split(None, 1) if not command_and_args: return if len(command_and_args) >= 1: if command_and_args[0] == "uciok": return self._uciok() elif command_and_args[0] == "readyok": return self._readyok() if len(command_and_args) >= 2: if command_and_args[0] == "id": return self._id(command_and_args[1]) elif command_and_args[0] == "bestmove": return self._bestmove(command_and_args[1]) elif command_and_args[0] == "copyprotection": return self._copyprotection(command_and_args[1]) elif command_and_args[0] == "registration": return self._registration(command_and_args[1]) elif command_and_args[0] == "info": return self._info(command_and_args[1]) elif command_and_args[0] == "option": return self._option(command_and_args[1])
def _bestmove(self, arg): tokens = arg.split(None, 2) self.bestmove = None if tokens[0] != "(none)": try: self.bestmove = self.board.parse_uci(tokens[0]) except ValueError: LOGGER.exception("exception parsing bestmove") self.ponder = None if self.bestmove is not None and len(tokens) >= 3 and tokens[1] == "ponder" and tokens[2] != "(none)": # The ponder move must be legal after the bestmove. Generally, we # trust the engine on this. But we still have to convert # non-UCI_Chess960 castling moves. try: self.ponder = chess.Move.from_uci(tokens[2]) if self.ponder.from_square in [chess.E1, chess.E8] and self.ponder.to_square in [chess.C1, chess.C8, chess.G1, chess.G8]: # Make a copy of the board to avoid race conditions. board = self.board.copy(stack=False) board.push(self.bestmove) self.ponder = board.parse_uci(tokens[2]) except ValueError: LOGGER.exception("exception parsing bestmove ponder") self.ponder = None self.bestmove_received.set() for info_handler in self.info_handlers: info_handler.on_bestmove(self.bestmove, self.ponder)
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() if self.move in DUMMY_RESPONSES: return self.move 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
def handle_move_token(token, fn): try: move = chess.Move.from_uci(token) except ValueError: LOGGER.exception("exception parsing move token") return for info_handler in self.info_handlers: fn(info_handler, move)
def handle_integer_token(token, fn): try: intval = int(token) except ValueError: LOGGER.exception("exception parsing integer token") return for info_handler in self.info_handlers: fn(info_handler, intval)
def _pong(self, pong_arg): try: pong_num = int(pong_arg) except ValueError: LOGGER.exception("exception parsing pong") if self.ping_num == pong_num: self.pong.set() with self.pong_received: self.pong_received.notify_all()
def set_feature(self, key, value): if key == "egt": for egt_type in value.split(","): self._features["egt"].append(egt_type) else: try: value = int(value) except ValueError: pass try: self._features[key] = value except KeyError: LOGGER.exception("exception looking up feature")
def _move(self, arg): self.move = None try: self.move = self.board.parse_uci(arg) except ValueError: try: self.move = self.board.parse_san(arg) except ValueError: LOGGER.exception("exception parsing move") self.move_received.set() if self.draw_handler: self.draw_handler.clear_offer() self.engine_offered_draw = False for post_handler in self.post_handlers: post_handler.on_move(self.move)
def _post(self, arg): if not self.post_handlers: return # Notify post handlers of start. for post_handler in self.post_handlers: post_handler.pre_info() def handle_integer_token(token, fn): try: intval = int(token) except ValueError: LOGGER.exception("exception parsing integer token") return for post_handler in self.post_handlers: fn(post_handler, intval) pv = [] board = self.board.copy(stack=False) tokens = arg.split() # Order: <score> <depth> <time> <nodes> <pv>. handle_integer_token(tokens[0], lambda handler, val: handler.depth(val)) handle_integer_token(tokens[1], lambda handler, val: handler.score(val)) handle_integer_token(tokens[2], lambda handler, val: handler.time(val)) handle_integer_token(tokens[3], lambda handler, val: handler.nodes(val)) for token in tokens[4:]: # Ignore move number. For example, 1. Nf3 Nf6 -> Nf3 Nf6. if '.' in token or '<' in token: continue try: pv.append(board.push_uci(token)) except ValueError: try: pv.append(board.push_san(token)) except ValueError: LOGGER.exception("exception parsing pv") if pv is not None: for post_handler in self.post_handlers: post_handler.pv(pv) # Notify post handlers of end. for post_handler in self.post_handlers: post_handler.post_info()
def _registration(self, arg): LOGGER.error("engine registration not supported")
def _copyprotection(self, arg): LOGGER.error("engine copyprotection not supported")
def _info(self, arg): if not self.info_handlers: return # Notify info handlers of start. for info_handler in self.info_handlers: info_handler.pre_info(arg) # Initialize parser state. board = None pv = None score_kind = None score_cp = None score_mate = None score_lowerbound = False score_upperbound = False refutation_move = None refuted_by = [] currline_cpunr = None currline_moves = [] string = [] def end_of_parameter(): # Parameters with variable length can only be handled when the # next parameter starts or at the end of the line. if pv is not None: for info_handler in self.info_handlers: info_handler.pv(pv) if score_cp is not None or score_mate is not None: for info_handler in self.info_handlers: info_handler.score(score_cp, score_mate, score_lowerbound, score_upperbound) if refutation_move is not None: if refuted_by: for info_handler in self.info_handlers: info_handler.refutation(refutation_move, refuted_by) else: for info_handler in self.info_handlers: info_handler.refutation(refutation_move, None) if currline_cpunr is not None: for info_handler in self.info_handlers: info_handler.currline(currline_cpunr, currline_moves) def handle_integer_token(token, fn): try: intval = int(token) except ValueError: LOGGER.exception("exception parsing integer token") return for info_handler in self.info_handlers: fn(info_handler, intval) def handle_move_token(token, fn): try: move = chess.Move.from_uci(token) except ValueError: LOGGER.exception("exception parsing move token") return for info_handler in self.info_handlers: fn(info_handler, move) # Find multipv parameter first. if "multipv" in arg: current_parameter = None for token in arg.split(" "): if token == "string": break if current_parameter == "multipv": handle_integer_token(token, lambda handler, val: handler.multipv(val)) current_parameter = token # Parse all other parameters. current_parameter = None for token in arg.split(" "): if current_parameter == "string": string.append(token) elif not token: # Ignore extra spaces. Those can not be directly discarded, # because they may occur in the string parameter. pass elif token in ["depth", "seldepth", "time", "nodes", "pv", "multipv", "score", "currmove", "currmovenumber", "hashfull", "nps", "tbhits", "cpuload", "refutation", "currline", "string"]: end_of_parameter() current_parameter = token pv = None score_kind = None score_mate = None score_cp = None score_lowerbound = False score_upperbound = False refutation_move = None refuted_by = [] currline_cpunr = None currline_moves = [] if current_parameter == "pv": pv = [] if current_parameter in ["refutation", "pv", "currline"]: board = self.board.copy(stack=False) elif current_parameter == "depth": handle_integer_token(token, lambda handler, val: handler.depth(val)) elif current_parameter == "seldepth": handle_integer_token(token, lambda handler, val: handler.seldepth(val)) elif current_parameter == "time": handle_integer_token(token, lambda handler, val: handler.time(val)) elif current_parameter == "nodes": handle_integer_token(token, lambda handler, val: handler.nodes(val)) elif current_parameter == "pv": try: pv.append(board.push_uci(token)) except ValueError: LOGGER.exception("exception parsing pv") elif current_parameter == "multipv": # Ignore multipv. It was already parsed before anything else. pass elif current_parameter == "score": if token in ["cp", "mate"]: score_kind = token elif token == "lowerbound": score_lowerbound = True elif token == "upperbound": score_upperbound = True elif score_kind == "cp": try: score_cp = int(token) except ValueError: LOGGER.exception("exception parsing score cp value") elif score_kind == "mate": try: score_mate = int(token) except ValueError: LOGGER.exception("exception parsing score mate value") elif current_parameter == "currmove": handle_move_token(token, lambda handler, val: handler.currmove(val)) elif current_parameter == "currmovenumber": handle_integer_token(token, lambda handler, val: handler.currmovenumber(val)) elif current_parameter == "hashfull": handle_integer_token(token, lambda handler, val: handler.hashfull(val)) elif current_parameter == "nps": handle_integer_token(token, lambda handler, val: handler.nps(val)) elif current_parameter == "tbhits": handle_integer_token(token, lambda handler, val: handler.tbhits(val)) elif current_parameter == "cpuload": handle_integer_token(token, lambda handler, val: handler.cpuload(val)) elif current_parameter == "refutation": try: if refutation_move is None: refutation_move = board.push_uci(token) else: refuted_by.append(board.push_uci(token)) except ValueError: LOGGER.exception("exception parsing refutation") elif current_parameter == "currline": try: if currline_cpunr is None: currline_cpunr = int(token) else: currline_moves.append(board.push_uci(token)) except ValueError: LOGGER.exception("exception parsing currline") end_of_parameter() if string: for info_handler in self.info_handlers: info_handler.string(" ".join(string)) # Notify info handlers of end. for info_handler in self.info_handlers: info_handler.post_info()
def _option(self, arg): current_parameter = None name = [] type = [] default = [] min = None max = None current_var = None var = [] for token in arg.split(" "): if token == "name" and not name: current_parameter = "name" elif token == "type" and not type: current_parameter = "type" elif token == "default" and not default: current_parameter = "default" elif token == "min" and min is None: current_parameter = "min" elif token == "max" and max is None: current_parameter = "max" elif token == "var": current_parameter = "var" if current_var is not None: var.append(" ".join(current_var)) current_var = [] elif current_parameter == "name": name.append(token) elif current_parameter == "type": type.append(token) elif current_parameter == "default": default.append(token) elif current_parameter == "var": current_var.append(token) elif current_parameter == "min": try: min = int(token) except ValueError: LOGGER.exception("exception parsing option min") elif current_parameter == "max": try: max = int(token) except ValueError: LOGGER.exception("exception parsing option max") if current_var is not None: var.append(" ".join(current_var)) type = " ".join(type) default = " ".join(default) if type == "check": if default == "true": default = True elif default == "false": default = False else: default = None elif type == "spin": try: default = int(default) except ValueError: LOGGER.exception("exception parsing option spin default") default = None option = Option(" ".join(name), type, default, min, max, var) self.options[option.name] = option
def send_line(self, line): LOGGER.debug("%s << %s", self.process, line) return self.process.send_line(line)
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: 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)
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 startposition. 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)
def get_option(self, key): try: return self._features["option"][key] except KeyError: LOGGER.exception("exception looking up option")
def set_option(self, key, value): try: self._features["option"][key] = value except KeyError: LOGGER.exception("exception looking up option")
def get(self, key): try: return self._features[key] except KeyError: LOGGER.exception("exception looking up feature")
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)