def test2(self): """ Test analyzing in promotion situations """ board = Board('5k2/PK6/8/8/8/6P1/6P1/8 w - - 1 48') self.analyzerA.setBoardList([board],[]) self.analyzerI.setBoardList([board],[]) self._testLine(self.engineA, self.analyzerA, board, "9. 1833 23 43872584 a8=Q+ Kf7 Qa2+ Kf6 Qd2 Kf5 g4+", ['a8=Q+','Kf7','Qa2+','Kf6','Qd2','Kf5','g4+'], 1833, "9.") self._testLine(self.engineI, self.analyzerI, board.switchColor(), "10. -1883 59 107386433 Kf7 a8=Q Ke6 Qa6+ Ke5 Qd6+ Kf5", ['Kf7','a8=Q','Ke6','Qa6+','Ke5','Qd6+','Kf5'], -1883, "10.")
def test1(self): """ Test analyzing in forced mate situations """ board = Board( 'B1n1n1KR/1r5B/6R1/2b1p1p1/2P1k1P1/1p2P2p/1P2P2P/3N1N2 w - - 0 1') self.analyzerA.setBoardList([board], []) self.analyzerI.setBoardList([board], []) self._testLine(self.engineA, self.analyzerA, board, "1. Mat1 0 1 Bxb7#", ['Bxb7#'], MATE_VALUE, "1.") # Notice, in the opposite situation there is no forced mate. Black can # do Bxe3 or Ne7+, but we just emulate a stupid analyzer not # recognizing this. self._testLine(self.engineI, self.analyzerI, board.switchColor(), "10. -Mat 2 35 64989837 Bd4 Bxb7#", ['Bd4', 'Bxb7#'], -MATE_VALUE, "10.")
def test1(self): """ Test analyzing in forced mate situations """ board = Board('B1n1n1KR/1r5B/6R1/2b1p1p1/2P1k1P1/1p2P2p/1P2P2P/3N1N2 w - - 0 1') self.analyzerA.setBoardList([board],[]) self.analyzerI.setBoardList([board],[]) self._testLine(self.engineA, self.analyzerA, board, "1. Mat1 0 1 Bxb7#", ['Bxb7#'], MATE_VALUE, "1.") # Notice, in the opposite situation there is no forced mate. Black can # do Bxe3 or Ne7+, but we just emulate a stupid analyzer not # recognizing this. self._testLine(self.engineI, self.analyzerI, board.switchColor(), "10. -Mat 2 35 64989837 Bd4 Bxb7#", ['Bd4','Bxb7#'], -MATE_VALUE, "10.")
class CECPEngine(ProtocolEngine): def __init__(self, subprocess, color, protover, md5): ProtocolEngine.__init__(self, subprocess, color, protover, md5) self.features = { "ping": 0, "setboard": 0, "playother": 0, "san": 0, "usermove": 0, "time": 1, "draw": 1, "sigint": 0, "sigterm": 0, "reuse": 0, "analyze": 0, "myname": ', '.join(self.defname), "variants": None, "colors": 1, "ics": 0, "name": 0, "pause": 0, "nps": 0, "debug": 0, "memory": 0, "smp": 0, "egt": '', "option": '', "exclude": 0, "done": None, } self.supported_features = [ "ping", "setboard", "san", "usermove", "time", "draw", "sigint", "analyze", "myname", "variants", "colors", "pause", "done", "egt", "debug", "smp", "memory", "option" ] self.options = {} self.options["Ponder"] = {"name": "Ponder", "type": "check", "default": False} self.name = None self.board = Board(setup=True) # if self.engineIsInNotPlaying == True, engine is in "force" mode, # i.e. not thinking or playing, but still verifying move legality self.engineIsInNotPlaying = False self.engineIsAnalyzing = False self.movenext = False self.waitingForMove = False self.readyForMoveNowCommand = False self.timeHandicap = 1 self.lastping = 0 self.lastpong = 0 self.queue = asyncio.Queue() self.parse_line_task = asyncio.async(self.parseLine(self.engine)) self.died_cid = self.engine.connect("died", lambda e: self.queue.put_nowait("die")) self.invalid_move = None self.optionQueue = [] self.undoQueue = [] self.ready_moves_event = asyncio.Event() self.cids = [ self.connect_after("readyForOptions", self.__onReadyForOptions), self.connect_after("readyForMoves", self.__onReadyForMoves), ] # Starting the game def prestart(self): print("xboard", file=self.engine) if self.protover == 1: # start a new game (CECPv1 engines): print("new", file=self.engine) # we are now ready for options: self.emit("readyForOptions") elif self.protover == 2: # start advanced protocol initialisation: print("protover 2", file=self.engine) # we don't start a new game for CECPv2 here, # we will do it after feature accept/reject is completed. def start(self, event=None): asyncio.async(self.__startBlocking(event)) @asyncio.coroutine def __startBlocking(self, event): if self.protover == 1: self.emit("readyForMoves") return_value = "ready" if self.protover == 2: try: return_value = yield from asyncio.wait_for(self.queue.get(), TIME_OUT_SECOND) if return_value == "not ready": return_value = yield from asyncio.wait_for(self.queue.get(), TIME_OUT_SECOND) # Gaviota sends done=0 after "xboard" and after "protover 2" too if return_value == "not ready": return_value = yield from asyncio.wait_for(self.queue.get(), TIME_OUT_SECOND) self.emit("readyForOptions") self.emit("readyForMoves") except asyncio.TimeoutError: log.warning("Got timeout error", extra={"task": self.defname}) raise PlayerIsDead except: log.warning("Unknown error", extra={"task": self.defname}) raise PlayerIsDead else: if return_value == "die": raise PlayerIsDead assert return_value == "ready" or return_value == "del" if event is not None: event.set() def __onReadyForOptions(self, self_): # We always want post turned on so the Engine Output sidebar can # show those things -Jonas Thiem print("post", file=self.engine) for command in self.optionQueue: print(command, file=self.engine) def __onReadyForMoves(self, self_): if self.mode in (ANALYZING, INVERSE_ANALYZING): # workaround for crafty not sending analysis after it has found a mating line # http://code.google.com/p/pychess/issues/detail?id=515 if "crafty" in self.features["myname"].lower(): print("noise 0", file=self.engine) self.__sendAnalyze(self.mode == INVERSE_ANALYZING) self.ready_moves_event.set() self.readyMoves = True # Ending the game def end(self, status, reason): self.parse_line_task.cancel() if self.engine.handler_is_connected(self.died_cid): self.engine.disconnect(self.died_cid) if self.handler_is_connected(self.analyze_cid): self.disconnect(self.analyze_cid) for cid in self.cids: if self.handler_is_connected(cid): self.disconnect(cid) self.board = None if self.connected: # We currently can't fillout the comment "field" as the repr strings # for reasons and statuses lies in Main.py # Creating Status and Reason class would solve this if status == DRAW: print("result 1/2-1/2 {?}", file=self.engine) elif status == WHITEWON: print("result 1-0 {?}", file=self.engine) elif status == BLACKWON: print("result 0-1 {?}", file=self.engine) else: print("result * {?}", file=self.engine) if reason == WON_ADJUDICATION: self.queue.put_nowait("invalid") # Make sure the engine exits and do some cleaning self.kill(reason) def kill(self, reason): """ Kills the engine, starting with the 'quit' command, then sigterm and eventually sigkill. Returns the exitcode, or if engine have already been killed, returns None """ if self.connected: self.connected = False try: try: print("quit", file=self.engine) self.queue.put_nowait("del") self.engine.terminate() except OSError as err: # No need to raise on a hang up error, as the engine is dead # anyways if err.errno == 32: log.warning("Hung up Error", extra={"task": self.defname}) return err.errno else: raise finally: # Clear the analyzed data, if any self.emit("analyze", []) # Send the player move updates def setBoard(self, board): self.setBoardList([board], []) self.__sendAnalyze(self.mode == INVERSE_ANALYZING) def putMove(self, board1, move, board2): """ Sends the engine the last move made (for spectator engines). @param board1: The current board @param move: The last move made @param board2: The board before the last move was made """ self.setBoardList([board1], []) self.__sendAnalyze(self.mode == INVERSE_ANALYZING) @asyncio.coroutine def makeMove(self, board1, move, board2): """ Gets a move from the engine (for player engines). @param board1: The current board @param move: The last move made @param board2: The board before the last move was made @return: The move the engine decided to make """ log.debug("makeMove: move=%s self.movenext=%s board1=%s board2=%s self.board=%s" % ( move, self.movenext, board1, board2, self.board), extra={"task": self.defname}) assert self.readyMoves if self.board == board1 or not board2 or self.movenext: self.board = board1 self.__tellEngineToPlayCurrentColorAndMakeMove() self.movenext = False else: self.board = board1 self.__usermove(board2, move) if self.engineIsInNotPlaying: self.__tellEngineToPlayCurrentColorAndMakeMove() self.waitingForMove = True self.readyForMoveNowCommand = True # Parse outputs status = yield from self.queue.get() if status == "not ready": log.warning( "Engine seems to be protover=2, but is treated as protover=1", extra={"task": self.defname}) status = yield from self.queue.get() if status == "ready": status = yield from self.queue.get() if status == "invalid": raise InvalidMove if status == "del": raise PlayerIsDead("Killed by foreign forces") if status == "int": raise TurnInterrupt self.waitingForMove = False self.readyForMoveNowCommand = False assert isinstance(status, Move), status return status def updateTime(self, secs, opsecs): if self.features["time"]: print("time %s" % int(secs * 100 * self.timeHandicap), file=self.engine) print("otim %s" % int(opsecs * 100), file=self.engine) # Standard options def setOptionAnalyzing(self, mode): self.mode = mode def setOptionInitialBoard(self, model): def coro(): yield from self.ready_moves_event.wait() # We don't use the optionQueue here, as set board prints a whole lot of # stuff. Instead we just call it. self.setBoardList(model.boards[:], model.moves[:]) asyncio.async(coro()) def setBoardList(self, boards, moves): # Notice: If this method is to be called while playing, the engine will # need 'new' and an arrangement similar to that of 'pause' to avoid # the current thought move to appear if self.mode not in (ANALYZING, INVERSE_ANALYZING): self.__tellEngineToStopPlayingCurrentColor() self.__setBoard(boards[0]) self.board = boards[-1] for board, move in zip(boards[:-1], moves): self.__usermove(board, move) if self.mode in (ANALYZING, INVERSE_ANALYZING): self.board = boards[-1] if self.mode == INVERSE_ANALYZING: self.board = self.board.switchColor() # The called of setBoardList will have to repost/analyze the # analyzer engines at this point. def setOptionVariant(self, variant): if self.features["variants"] is None: log.warning("setOptionVariant: engine doesn't support variants", extra={"task": self.defname}) return if variant in variants.values() and not variant.standard_rules: assert variant.cecp_name in self.features["variants"], \ "%s doesn't support %s variant" % (self, variant.cecp_name) self.optionQueue.append("variant %s" % variant.cecp_name) # Strength system # # Strength Depth Ponder Time handicap # # 1 1 o 1,258% # # 2 2 o 1,584% # # 3 3 o 1.995% # # # # 19 o x 79,43% # # 20 o x o # def setOptionStrength(self, strength, forcePonderOff): self.strength = strength if strength <= 19: self.__setTimeHandicap(0.01 * 10**(strength / 10.)) if strength <= 18: self.__setDepth(strength) # Crafty ofers 100 skill levels if "crafty" in self.features["myname"].lower() and strength <= 19: self.optionQueue.append("skill %s" % strength * 5) self.__setPonder(strength >= 19 and not forcePonderOff) if strength == 20: if "gaviota" in self.features["egt"]: self.optionQueue.append("egtpath gaviota %s" % conf.get( "egtb_path", "")) else: self.optionQueue.append("random") def __setDepth(self, depth): self.optionQueue.append("sd %d" % depth) def __setTimeHandicap(self, timeHandicap): self.timeHandicap = timeHandicap def __setPonder(self, ponder): if ponder: self.optionQueue.append("hard") else: self.optionQueue.append("hard") self.optionQueue.append("easy") def setOptionTime(self, secs, gain, moves): # Notice: In CECP we apply time handicap in updateTime, not in # setOptionTime. minutes = int(secs / 60) secs = int(secs % 60) mins = str(minutes) if secs: mins += ":" + str(secs) self.optionQueue.append("level %s %s %d" % (moves, mins, gain)) # Option handling def setOption(self, key, value): """ Set an option, which will be sent to the engine, after the 'readyForOptions' signal has passed. If you want to know the possible options, you should go to engineDiscoverer or use the getOption, getOptions and hasOption methods, while you are in your 'readyForOptions' signal handler """ if self.readyMoves: log.warning( "Options set after 'readyok' are not sent to the engine", extra={"task": self.defname}) if key == "cores": self.optionQueue.append("cores %s" % value) elif key == "memory": self.optionQueue.append("memory %s" % value) elif key.lower() == "ponder": self.__setPonder(value == 1) else: self.optionQueue.append("option %s=%s" % (key, value)) # Interacting with the player def pause(self): """ Pauses engine using the "pause" command if available. Otherwise put engine in force mode. By the specs the engine shouldn't ponder in force mode, but some of them do so anyways. """ log.debug("pause: self=%s" % self, extra={"task": self.defname}) self.engine.pause() return def resume(self): log.debug("resume: self=%s" % self, extra={"task": self.defname}) self.engine.resume() return def hurry(self): log.debug("hurry: self.waitingForMove=%s self.readyForMoveNowCommand=%s" % ( self.waitingForMove, self.readyForMoveNowCommand), extra={"task": self.defname}) if self.waitingForMove and self.readyForMoveNowCommand: self.__tellEngineToMoveNow() self.readyForMoveNowCommand = False def spectatorUndoMoves(self, moves, gamemodel): log.debug("spectatorUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s" % ( moves, gamemodel.ply, gamemodel.boards[-1], self.board), extra={"task": self.defname}) for i in range(moves): print("undo", file=self.engine) self.board = gamemodel.boards[-1] def playerUndoMoves(self, moves, gamemodel): log.debug("playerUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s" % ( moves, gamemodel.ply, gamemodel.boards[-1], self.board), extra={"task": self.defname}) if gamemodel.curplayer != self and moves % 2 == 1: # Interrupt if we were searching, but should no longer do so self.queue.put_nowait("int") self.__tellEngineToStopPlayingCurrentColor() for i in range(moves): print("undo", file=self.engine) if gamemodel.curplayer == self: self.board = gamemodel.boards[-1] self.__tellEngineToPlayCurrentColorAndMakeMove() else: self.board = None # Offer handling def offer(self, offer): if offer.type == DRAW_OFFER: if self.features["draw"]: print("draw", file=self.engine) else: self.emit("accept", offer) def offerError(self, offer, error): if self.features["draw"]: # We don't keep track if engine draws are offers or accepts. We just # Always assume they are accepts, and if they are not, we get this # error and emit offer instead if offer.type == DRAW_OFFER and error == ACTION_ERROR_NONE_TO_ACCEPT: self.emit("offer", Offer(DRAW_OFFER)) # Internal def __usermove(self, board, move): if self.features["usermove"]: self.engine.write("usermove ") if self.features["san"]: print(toSAN(board, move), file=self.engine) else: castle_notation = CASTLE_KK if board.variant == FISCHERRANDOMCHESS: castle_notation = CASTLE_SAN print( toAN(board, move, short=True, castleNotation=castle_notation), file=self.engine) def __tellEngineToMoveNow(self): if self.features["sigint"]: self.engine.sigint() print("?", file=self.engine) def __tellEngineToStopPlayingCurrentColor(self): print("force", file=self.engine) self.engineIsInNotPlaying = True def __tellEngineToPlayCurrentColorAndMakeMove(self): self.__printColor() print("go", file=self.engine) self.engineIsInNotPlaying = False def __sendAnalyze(self, inverse=False): if inverse and self.board.board.opIsChecked(): # Many engines don't like positions able to take down enemy # king. Therefore we just return the "kill king" move # automaticaly self.emit("analyze", [([toAN( self.board, getMoveKillingKing(self.board))], MATE_VALUE - 1, "")]) return def stop_analyze(): if self.engineIsAnalyzing: print("exit", file=self.engine) # Some engines (crafty, gnuchess) doesn't respond to exit command # we try to force them to stop with an empty board fen print("setboard 8/8/8/8/8/8/8/8 w - - 0 1", file=self.engine) self.engineIsAnalyzing = False print("post", file=self.engine) print("analyze", file=self.engine) self.engineIsAnalyzing = True loop = asyncio.get_event_loop() loop.call_later(conf.get("max_analysis_spin", 3), stop_analyze) def __printColor(self): if self.features["colors"]: # or self.mode == INVERSE_ANALYZING: if self.board.color == WHITE: print("white", file=self.engine) else: print("black", file=self.engine) def __setBoard(self, board): if self.features["setboard"]: self.__tellEngineToStopPlayingCurrentColor() fen = board.asFen(enable_bfen=False) if self.mode == INVERSE_ANALYZING: fen_arr = fen.split() if not self.board.board.opIsChecked(): if fen_arr[1] == "b": fen_arr[1] = "w" else: fen_arr[1] = "b" fen = " ".join(fen_arr) print("setboard %s" % fen, file=self.engine) else: # Kludge to set black to move, avoiding the troublesome and now # deprecated "black" command. - Equal to the one xboard uses self.__tellEngineToStopPlayingCurrentColor() if board.color == BLACK: print("a2a3", file=self.engine) print("edit", file=self.engine) print("#", file=self.engine) for color in WHITE, BLACK: for y_loc, row in enumerate(board.data): for x_loc, piece in row.items(): if not piece or piece.color != color: continue sign = reprSign[piece.sign] cord = repr(Cord(x_loc, y_loc)) print(sign + cord, file=self.engine) print("c", file=self.engine) print(".", file=self.engine) # Parsing @asyncio.coroutine def parseLine(self, proc): while True: line = yield from wait_signal(proc, 'line') if not line: break else: line = line[1] if line[0:1] == "#": # Debug line which we shall ignore as specified in CECPv2 specs continue # log.debug("__parseLine: line=\"%s\"" % line.strip(), extra={"task":self.defname}) parts = whitespaces.split(line.strip()) if parts[0] == "pong": self.lastpong = int(parts[1]) continue # Illegal Move if parts[0].lower().find("illegal") >= 0: log.warning("__parseLine: illegal move: line=\"%s\", board=%s" % ( line.strip(), self.board), extra={"task": self.defname}) if parts[-2] == "sd" and parts[-1].isdigit(): print("depth", parts[-1], file=self.engine) continue # A Move (Perhaps) if self.board: if parts[0] == "move": movestr = parts[1] # Old Variation elif d_plus_dot_expr.match(parts[0]) and parts[1] == "...": movestr = parts[2] else: movestr = False if movestr: self.waitingForMove = False self.readyForMoveNowCommand = False if self.engineIsInNotPlaying: # If engine was set in pause just before the engine sent its # move, we ignore it. However the engine has to know that we # ignored it, and thus we step it one back log.info("__parseLine: Discarding engine's move: %s" % movestr, extra={"task": self.defname}) print("undo", file=self.engine) continue else: try: move = parseAny(self.board, movestr) except ParsingError: self.invalid_move = movestr log.info( "__parseLine: ParsingError engine move: %s %s" % (movestr, self.board), extra={"task": self.defname}) self.end(WHITEWON if self.board.color == BLACK else BLACKWON, WON_ADJUDICATION) continue if validate(self.board, move): self.board = None self.queue.put_nowait(move) continue else: self.invalid_move = movestr log.info( "__parseLine: can't validate engine move: %s %s" % (movestr, self.board), extra={"task": self.defname}) self.end(WHITEWON if self.board.color == BLACK else BLACKWON, WON_ADJUDICATION) continue # Analyzing if self.engineIsInNotPlaying: if parts[:4] == ["0", "0", "0", "0"]: # Crafty doesn't analyze until it is out of book print("book off", file=self.engine) continue match = anare.match(line) if match: depth, score, moves = match.groups() if "mat" in score.lower() or "#" in moves: # Will look either like -Mat 3 or Mat3 scoreval = MATE_VALUE if score.startswith('-'): scoreval = -scoreval else: scoreval = int(score) mvstrs = movere.findall(moves) if mvstrs: self.emit("analyze", [(mvstrs, scoreval, depth.strip())]) continue # Offers draw if parts[0:2] == ["offer", "draw"]: self.emit("accept", Offer(DRAW_OFFER)) continue # Resigns if parts[0] == "resign" or \ (parts[0] == "tellics" and parts[1] == "resign"): # buggy crafty # Previously: if "resign" in parts, # however, this is too generic, since "hint", "bk", # "feature option=.." and possibly other, future CECPv2 # commands can validly contain the word "resign" without this # being an intentional resign offer. self.emit("offer", Offer(RESIGNATION)) continue # if parts[0].lower() == "error": # continue # Tell User Error if parts[0] == "tellusererror": # We don't want to see our stop analyzer hack as an error message if "8/8/8/8/8/8/8/8" in "".join(parts[1:]): continue # Create a non-modal non-blocking message dialog with the error: dlg = Gtk.MessageDialog(parent=None, flags=0, type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.CLOSE, message_format=None) # Use the engine name if already known, otherwise the defname: displayname = self.name if not displayname: displayname = self.defname # Compose the dialog text: dlg.set_markup(GObject.markup_escape_text(_( "The engine %s reports an error:") % displayname) + "\n\n" + GObject.markup_escape_text(" ".join(parts[1:]))) # handle response signal so the "Close" button works: dlg.connect("response", lambda dlg, x: dlg.destroy()) dlg.show_all() continue # Tell Somebody if parts[0][:4] == "tell" and \ parts[0][4:] in ("others", "all", "ics", "icsnoalias"): log.info("Ignoring tell %s: %s" % (parts[0][4:], " ".join(parts[1:]))) continue if "feature" in parts: # Some engines send features after done=1, so we will iterate after done=1 too done1 = False # We skip parts before 'feature', as some engines give us lines like # White (1) : feature setboard=1 analyze...e="GNU Chess 5.07" done=1 parts = parts[parts.index("feature"):] for i, pair in enumerate(parts[1:]): # As "parts" is split with no thoughs on quotes or double quotes # we need to do some extra handling. if pair.find("=") < 0: continue key, value = pair.split("=", 1) if key not in self.features: continue if value.startswith('"') and value.endswith('"'): value = value[1:-1] # If our pair was unfinished, like myname="GNU, we search the # rest of the pairs for a quotating mark. elif value[0] == '"': rest = value[1:] + " " + " ".join(parts[2 + i:]) j = rest.find('"') if j == -1: log.warning("Missing endquotation in %s feature", extra={"task": self.defname}) value = rest else: value = rest[:j] elif value.isdigit(): value = int(value) if key in self.supported_features: print("accepted %s" % key, file=self.engine) else: print("rejected %s" % key, file=self.engine) if key == "done": if value == 1: done1 = True continue elif value == 0: log.info("Adds %d seconds timeout" % TIME_OUT_SECOND, extra={"task": self.defname}) # This'll buy you some more time self.queue.put_nowait("not ready") break if key == "smp" and value == 1: self.options["cores"] = {"name": "cores", "type": "spin", "default": 1, "min": 1, "max": 64} elif key == "memory" and value == 1: self.options["memory"] = {"name": "memory", "type": "spin", "default": 32, "min": 1, "max": 4096} elif key == "option" and key != "done": option = self.__parse_option(value) self.options[option["name"]] = option else: self.features[key] = value if key == "myname" and not self.name: self.setName(value) if done1: # Start a new game before using the engine: # (CECPv2 engines) print("new", file=self.engine) # We are now ready for play: self.emit("readyForOptions") self.emit("readyForMoves") self.queue.put_nowait("ready") # A hack to get better names in protover 1. # Unfortunately it wont work for now, as we don't read any lines from # protover 1 engines. When should we stop? if self.protover == 1: if self.defname[0] in ''.join(parts): basis = self.defname[0] name = ' '.join(itertools.dropwhile( lambda part: basis not in part, parts)) self.features['myname'] = name if not self.name: self.setName(name) def __parse_option(self, option): if " -check " in option: name, value = option.split(" -check ") return {"type": "check", "name": name, "default": bool(int(value))} elif " -spin " in option: name, value = option.split(" -spin ") defv, minv, maxv = value.split() return {"type": "spin", "name": name, "default": int(defv), "min": int(minv), "max": int(maxv)} elif " -slider " in option: name, value = option.split(" -slider ") defv, minv, maxv = value.split() return {"type": "spin", "name": name, "default": int(defv), "min": int(minv), "max": int(maxv)} elif " -string " in option: name, value = option.split(" -string ") return {"type": "text", "name": name, "default": value} elif " -file " in option: name, value = option.split(" -file ") return {"type": "text", "name": name, "default": value} elif " -path " in option: name, value = option.split(" -path ") return {"type": "text", "name": name, "default": value} elif " -combo " in option: name, value = option.split(" -combo ") choices = list(map(str.strip, value.split("///"))) default = "" for choice in choices: if choice.startswith("*"): index = choices.index(choice) default = choice[1:] choices[index] = default break return {"type": "combo", "name": name, "default": default, "choices": choices} elif " -button" in option: pos = option.find(" -button") return {"type": "button", "name": option[:pos]} elif " -save" in option: pos = option.find(" -save") return {"type": "button", "name": option[:pos]} elif " -reset" in option: pos = option.find(" -reset") return {"type": "button", "name": option[:pos]} # Info def canAnalyze(self): assert self.ready, "Still waiting for done=1" return self.features["analyze"] def maxAnalysisLines(self): return 1 def requestMultiPV(self, setting): return 1 def isAnalyzing(self): return self.mode in (ANALYZING, INVERSE_ANALYZING) def __repr__(self): if self.name: return self.name return self.features["myname"]
class UCIEngine(ProtocolEngine): def __init__(self, subprocess, color, protover, md5): ProtocolEngine.__init__(self, subprocess, color, protover, md5) self.ids = {} self.options = {} self.optionsToBeSent = {} self.wtime = 60000 self.btime = 60000 self.incr = 0 self.moves = 0 self.timeHandicap = 1 self.ponderOn = False self.pondermove = None self.ignoreNext = False self.waitingForMove = False self.needBestmove = False self.bestmove_event = asyncio.Event() self.readyForStop = False # keeps track of whether we already sent a 'stop' command self.multipvSetting = 1 # MultiPV option sent to the engine self.multipvExpected = 1 # Number of PVs expected (limited by number of legal moves) self.commands = collections.deque() self.gameBoard = Board( setup=True) # board at the end of all moves played self.board = Board(setup=True) # board to send the engine self.uciPosition = "startpos" self.uciPositionListsMoves = False self.analysis = [None] self.analysis_depth = None self.queue = asyncio.Queue() self.parse_line_task = create_task(self.parseLine(self.engine)) self.died_cid = self.engine.connect( "died", lambda e: self.queue.put_nowait("die")) self.invalid_move = None self.cids = [ self.connect_after("readyForOptions", self.__onReadyForOptions), self.connect_after("readyForMoves", self.__onReadyForMoves), ] # Starting the game def prestart(self): print("uci", file=self.engine) def start(self, event, is_dead): create_task(self.__startBlocking(event, is_dead)) @asyncio.coroutine def __startBlocking(self, event, is_dead): try: return_value = yield from asyncio.wait_for(self.queue.get(), TIME_OUT_SECOND) except asyncio.TimeoutError: log.warning("Got timeout error", extra={"task": self.defname}) is_dead.add(True) except Exception: log.warning("Unknown error", extra={"task": self.defname}) is_dead.add(True) else: if return_value == 'die': is_dead.add(True) assert return_value == "ready" or return_value == "del" if event is not None: event.set() def __onReadyForOptions(self, self_): if self.mode in (ANALYZING, INVERSE_ANALYZING): if self.hasOption("Ponder"): self.setOption('Ponder', False) for option, value in self.optionsToBeSent.items(): if isinstance(value, bool): value = str(value).lower() print("setoption name %s value %s" % (option, str(value)), file=self.engine) print("isready", file=self.engine) def __onReadyForMoves(self, self_): self.readyMoves = True self.queue.put_nowait("ready") self._newGame() if self.isAnalyzing(): self._searchNow() # Ending the game def end(self, status, reason): self.parse_line_task.cancel() if self.engine.handler_is_connected(self.died_cid): self.engine.disconnect(self.died_cid) if self.handler_is_connected(self.analyze_cid): self.disconnect(self.analyze_cid) for cid in self.cids: if self.handler_is_connected(cid): self.disconnect(cid) self.board = None self.gameBoard = None if self.connected: # UCI doens't care about reason, so we just kill if reason == WON_ADJUDICATION: self.queue.put_nowait("invalid") self.kill(reason) def kill(self, reason): """ Kills the engine, starting with the 'stop' and 'quit' commands, then trying sigterm and eventually sigkill. Returns the exitcode, or if engine have already been killed, the method returns None """ if self.connected: self.connected = False try: try: print("stop", file=self.engine) print("quit", file=self.engine) self.queue.put_nowait("del") self.engine.terminate() except OSError as e: # No need to raise on a hang up error, as the engine is dead # anyways if e.errno == 32: log.warning("Hung up Error", extra={"task": self.defname}) return e.errno else: raise finally: # Clear the analyzed data, if any self.emit("analyze", []) # Send the player move updates def _moveToUCI(self, board, move): castle_notation = CASTLE_KK if board.variant == FISCHERRANDOMCHESS: castle_notation = CASTLE_KR return toAN(board, move, short=True, castleNotation=castle_notation) def _recordMove(self, board1, move, board2): if self.gameBoard == board1: return if not board2: if board1.variant == NORMALCHESS and board1.asFen() == FEN_START: self.uciPosition = "startpos" else: self.uciPosition = "fen " + board1.asFen() self.uciPositionListsMoves = False if move: if not self.uciPositionListsMoves: self.uciPosition += " moves" self.uciPositionListsMoves = True self.uciPosition += " " + self._moveToUCI(board2, move) self.board = self.gameBoard = board1 if self.mode == INVERSE_ANALYZING: self.board = self.gameBoard.switchColor() def _recordMoveList(self, model, ply=None): self._recordMove(model.boards[0], None, None) if ply is None: ply = model.ply for board1, move, board2 in zip(model.boards[1:ply + 1], model.moves, model.boards[0:ply]): self._recordMove(board1, move, board2) def setBoard(self, board, search=True): log.debug("setBoardAtPly: board=%s" % board, extra={"task": self.defname}) if not self.readyMoves: return @asyncio.coroutine def coro(): if self.needBestmove: self.bestmove_event.clear() print("stop", file=self.engine) yield from self.bestmove_event.wait() self._recordMove(board, None, None) if search: self._searchNow() create_task(coro()) def putMove(self, board1, move, board2): log.debug("putMove: board1=%s move=%s board2=%s self.board=%s" % (board1, move, board2, self.board), extra={"task": self.defname}) if not self.readyMoves: return @asyncio.coroutine def coro(): if self.needBestmove: self.bestmove_event.clear() print("stop", file=self.engine) yield from self.bestmove_event.wait() self._recordMove(board1, move, board2) if not self.analyzing_paused: self._searchNow() create_task(coro()) @asyncio.coroutine def makeMove(self, board1, move, board2): log.debug( "makeMove: move=%s self.pondermove=%s board1=%s board2=%s self.board=%s" % (move, self.pondermove, board1, board2, self.board), extra={"task": self.defname}) assert self.readyMoves self._recordMove(board1, move, board2) self.waitingForMove = True ponderhit = False if board2 and self.pondermove and move == self.pondermove: ponderhit = True elif board2 and self.pondermove: self.ignoreNext = True print("stop", file=self.engine) self._searchNow(ponderhit=ponderhit) # Parse outputs try: return_queue = yield from self.queue.get() if return_queue == "invalid": raise InvalidMove if return_queue == "del" or return_queue == "die": raise PlayerIsDead if return_queue == "int": self.pondermove = None self.ignoreNext = True self.needBestmove = True self.hurry() raise TurnInterrupt return return_queue finally: self.waitingForMove = False def updateTime(self, secs, opsecs): if self.color == WHITE: self.wtime = int(secs * 1000 * self.timeHandicap) self.btime = int(opsecs * 1000) else: self.btime = int(secs * 1000 * self.timeHandicap) self.wtime = int(opsecs * 1000) # Standard options def setOptionAnalyzing(self, mode): self.mode = mode if self.mode == INVERSE_ANALYZING: self.board = self.gameBoard.switchColor() def setOptionInitialBoard(self, model): log.debug("setOptionInitialBoard: self=%s, model=%s" % (self, model), extra={"task": self.defname}) self._recordMoveList(model) def setOptionVariant(self, variant): if variant == FischerandomBoard: assert self.hasOption("UCI_Chess960") self.setOption("UCI_Chess960", True) elif self.hasOption("UCI_Variant") and not variant.standard_rules: self.setOption("UCI_Variant", variant.cecp_name) def setOptionTime(self, secs, gain, moves): self.wtime = int(max(secs * 1000 * self.timeHandicap, 1)) self.btime = int(max(secs * 1000 * self.timeHandicap, 1)) self.incr = int(gain * 1000 * self.timeHandicap) self.moves = moves def setOptionStrength(self, strength, forcePonderOff): self.strength = strength if self.hasOption('UCI_LimitStrength') and strength <= 18: self.setOption('UCI_LimitStrength', True) if self.hasOption('UCI_Elo'): self.setOption('UCI_Elo', 150 * strength) # Stockfish and anticrux engines offer 20 skill levels if self.hasOption('Skill Level'): self.setOption('Skill Level', strength) if ((not self.hasOption('UCI_Elo')) and (not self.hasOption('Skill Level'))) or strength <= 19: self.timeHandicap = t_hcap = 0.01 * 10**(strength / 10.) self.wtime = int(max(self.wtime * t_hcap, 1)) self.btime = int(max(self.btime * t_hcap, 1)) self.incr = int(self.incr * t_hcap) if self.hasOption('Ponder'): self.setOption('Ponder', strength >= 19 and not forcePonderOff) if self.hasOption('GaviotaTbPath') and strength == 20: self.setOption('GaviotaTbPath', conf.get("egtb_path")) # Interacting with the player def pause(self): log.debug("pause: self=%s" % self, extra={"task": self.defname}) if self.isAnalyzing(): print("stop", file=self.engine) self.readyForStop = False self.analyzing_paused = True else: self.engine.pause() return def resume(self): log.debug("resume: self=%s" % self, extra={"task": self.defname}) if self.isAnalyzing(): self._searchNow() self.analyzing_paused = False else: self.engine.resume() return def hurry(self): log.debug("hurry: self.waitingForMove=%s self.readyForStop=%s" % (self.waitingForMove, self.readyForStop), extra={"task": self.defname}) # sending this more than once per move will crash most engines # so we need to send only the first one, and then ignore every "hurry" request # after that until there is another outstanding "position..go" if self.waitingForMove and self.readyForStop: print("stop", file=self.engine) self.readyForStop = False def playerUndoMoves(self, moves, gamemodel): log.debug("playerUndoMoves: moves=%s \ gamemodel.ply=%s \ gamemodel.boards[-1]=%s \ self.board=%s" % (moves, gamemodel.ply, gamemodel.boards[-1], self.board), extra={"task": self.defname}) self._recordMoveList(gamemodel) if (gamemodel.curplayer != self and moves % 2 == 1) or \ (gamemodel.curplayer == self and moves % 2 == 0): # Interrupt if we were searching but should no longer do so, or # if it is was our move before undo and it is still our move after undo # since we need to send the engine the new FEN in makeMove() log.debug("playerUndoMoves: putting 'int' into self.queue=%s" % self.queue, extra={"task": self.defname}) self.queue.put_nowait("int") def spectatorUndoMoves(self, moves, gamemodel): if self.analyzing_paused: return log.debug("spectatorUndoMoves: moves=%s \ gamemodel.ply=%s \ gamemodel.boards[-1]=%s \ self.board=%s" % (moves, gamemodel.ply, gamemodel.boards[-1], self.board), extra={"task": self.defname}) self._recordMoveList(gamemodel) if self.readyMoves: self._searchNow() # Offer handling def offer(self, offer): if offer.type == DRAW_OFFER: self.emit("decline", offer) else: self.emit("accept", offer) # Option handling def setOption(self, key, value): """ Set an option, which will be sent to the engine, after the 'readyForOptions' signal has passed. If you want to know the possible options, you should go to engineDiscoverer or use the hasOption method while you are in your 'readyForOptions' signal handler """ if self.readyMoves: log.warning( "Options set after 'readyok' are not sent to the engine", extra={"task": self.defname}) self.optionsToBeSent[key] = value self.ponderOn = key == "Ponder" and value is True if key == "MultiPV": self.multipvSetting = int(value) def hasOption(self, key): return key in self.options # Internal def _newGame(self): print("ucinewgame", file=self.engine) def _searchNow(self, ponderhit=False): log.debug( "_searchNow: self.needBestmove=%s ponderhit=%s self.board=%s" % (self.needBestmove, ponderhit, self.board), extra={"task": self.defname}) commands = [] if ponderhit: commands.append("ponderhit") elif self.mode == NORMAL: commands.append("position %s" % self.uciPosition) if self.strength <= 3: commands.append("go depth %d" % self.strength) else: if self.moves > 0: commands.append( "go wtime %d winc %d btime %d binc %d movestogo %s" % (self.wtime, self.incr, self.btime, self.incr, self.moves)) else: commands.append( "go wtime %d winc %d btime %d binc %d" % (self.wtime, self.incr, self.btime, self.incr)) else: print("stop", file=self.engine) if self.mode == INVERSE_ANALYZING: if self.board.board.opIsChecked(): # Many engines don't like positions able to take down enemy # king. Therefore we just return the "kill king" move # automaticaly self.emit("analyze", [(self.board.ply, [ toAN(self.board, getMoveKillingKing(self.board)) ], MATE_VALUE - 1, "1", "")]) return commands.append("position fen %s" % self.board.asFen()) else: commands.append("position %s" % self.uciPosition) if self.analysis_depth is not None: commands.append("go depth %s" % self.analysis_depth) elif conf.get("infinite_analysis"): commands.append("go infinite") else: move_time = int(conf.get("max_analysis_spin")) * 1000 commands.append("go movetime %s" % move_time) if self.hasOption("MultiPV") and self.multipvSetting > 1: self.multipvExpected = min(self.multipvSetting, legalMoveCount(self.board)) else: self.multipvExpected = 1 self.analysis = [None] * self.multipvExpected if self.needBestmove: self.commands.append(commands) log.debug( "_searchNow: self.needBestmove==True, appended to self.commands=%s" % self.commands, extra={"task": self.defname}) else: for command in commands: print(command, file=self.engine) if getStatus(self.board)[1] != WON_MATE: # XXX This looks fishy. self.needBestmove = True self.readyForStop = True def _startPonder(self): uciPos = self.uciPosition if not self.uciPositionListsMoves: uciPos += " moves" print("position", uciPos, self._moveToUCI(self.board, self.pondermove), file=self.engine) print("go ponder wtime", self.wtime, "winc", self.incr, "btime", self.btime, "binc", self.incr, file=self.engine) # Parsing from engine @asyncio.coroutine def parseLine(self, proc): while True: line = yield from wait_signal(proc, 'line') if not line: break else: line = line[1] parts = line.split() if not parts: continue # Initializing if parts[0] == "id": if parts[1] == "name": self.ids[parts[1]] = " ".join(parts[2:]) self.setName(self.ids["name"]) continue if parts[0] == "uciok": self.emit("readyForOptions") continue if parts[0] == "readyok": self.emit("readyForMoves") continue # Options parsing if parts[0] == "option": dic = {} last = 1 varlist = [] for i in range(2, len(parts) + 1): if i == len(parts) or parts[i] in OPTKEYS: key = parts[last] value = " ".join(parts[last + 1:i]) if "type" in dic and dic["type"] in TYPEDIC: value = TYPEDIC[dic["type"]](value) if key == "var": varlist.append(value) elif key == "type" and value == "string": dic[key] = "text" else: dic[key] = value last = i if varlist: dic["choices"] = varlist if "name" in dic: self.options[dic["name"]] = dic continue # A Move if self.mode == NORMAL and parts[0] == "bestmove": self.needBestmove = False self.bestmove_event.set() self.__sendQueuedGo() if self.ignoreNext: log.debug( "__parseLine: line='%s' self.ignoreNext==True, returning" % line.strip(), extra={"task": self.defname}) self.ignoreNext = False self.readyForStop = True continue movestr = parts[1] if not self.waitingForMove: log.warning( "__parseLine: self.waitingForMove==False, ignoring move=%s" % movestr, extra={"task": self.defname}) self.pondermove = None continue self.waitingForMove = False try: move = parseAny(self.board, movestr) except ParsingError: self.invalid_move = movestr log.info( "__parseLine: ParsingError engine move: %s %s" % (movestr, self.board), extra={"task": self.defname}) self.end( WHITEWON if self.board.color == BLACK else BLACKWON, WON_ADJUDICATION) continue if not validate(self.board, move): # This is critical. To avoid game stalls, we need to resign on # behalf of the engine. log.error( "__parseLine: move=%s didn't validate, putting 'del' \ in returnQueue. self.board=%s" % (repr(move), self.board), extra={"task": self.defname}) self.invalid_move = movestr self.end( WHITEWON if self.board.color == BLACK else BLACKWON, WON_ADJUDICATION) continue self._recordMove(self.board.move(move), move, self.board) log.debug("__parseLine: applied move=%s to self.board=%s" % (move, self.board), extra={"task": self.defname}) if self.ponderOn: self.pondermove = None # An engine may send an empty ponder line, simply to clear. if len(parts) == 4: # Engines don't always check for everything in their # ponders. Hence we need to validate. # But in some cases, what they send may not even be # correct AN - specially in the case of promotion. try: pondermove = parseAny(self.board, parts[3]) except ParsingError: pass else: if validate(self.board, pondermove): self.pondermove = pondermove self._startPonder() self.queue.put_nowait(move) log.debug("__parseLine: put move=%s into self.queue=%s" % (move, self.queue), extra={"task": self.defname}) continue # An Analysis if self.mode != NORMAL and parts[0] == "info" and "pv" in parts: multipv = 1 if "multipv" in parts: multipv = int(parts[parts.index("multipv") + 1]) scoretype = parts[parts.index("score") + 1] if scoretype in ('lowerbound', 'upperbound'): score = None else: score = int(parts[parts.index("score") + 2]) if scoretype == 'mate': # print >> self.engine, "stop" if score != 0: sign = score / abs(score) score = sign * (MATE_VALUE - abs(score)) movstrs = parts[parts.index("pv") + 1:] if "depth" in parts: depth = parts[parts.index("depth") + 1] else: depth = "" if "nps" in parts: nps = parts[parts.index("nps") + 1] else: nps = "" if multipv <= len(self.analysis): self.analysis[multipv - 1] = (self.board.ply, movstrs, score, depth, nps) self.emit("analyze", self.analysis) continue # An Analyzer bestmove if self.mode != NORMAL and parts[0] == "bestmove": log.debug( "__parseLine: processing analyzer bestmove='%s'" % line.strip(), extra={"task": self.defname}) self.needBestmove = False self.bestmove_event.set() if parts[1] == "(none)": self.emit("analyze", []) else: self.__sendQueuedGo(sendlast=True) continue # Stockfish complaining it received a 'stop' without a corresponding 'position..go' if line.strip() == "Unknown command: stop": log.debug("__parseLine: processing '%s'" % line.strip(), extra={"task": self.defname}) self.ignoreNext = False self.needBestmove = False self.readyForStop = False self.__sendQueuedGo() continue # * score # * cp <x> # the score from the engine's point of view in centipawns. # * mate <y> # mate in y moves, not plies. # If the engine is getting mated use negative values for y. # * lowerbound # the score is just a lower bound. # * upperbound # the score is just an upper bound. def __sendQueuedGo(self, sendlast=False): """ Sends the next position...go or ponderhit command set which was queued (if any). sendlast -- If True, send the last position-go queued rather than the first, and discard the others (intended for analyzers) """ if len(self.commands) > 0: if sendlast: commands = self.commands.pop() self.commands.clear() else: commands = self.commands.popleft() for command in commands: print(command, file=self.engine) self.needBestmove = True self.readyForStop = True log.debug("__sendQueuedGo: sent queued go=%s" % commands, extra={"task": self.defname}) # Info def getAnalysisLines(self): try: return int(self.optionsToBeSent["MultiPV"]) except (KeyError, ValueError): return 1 # Engine does not support the MultiPV option def minAnalysisLines(self): try: return int(self.options["MultiPV"]["min"]) except (KeyError, ValueError): return 1 # Engine does not support the MultiPV option def maxAnalysisLines(self): try: return int(self.options["MultiPV"]["max"]) except (KeyError, ValueError): return 1 # Engine does not support the MultiPV option def requestMultiPV(self, n): n = min(n, self.maxAnalysisLines()) n = max(n, self.minAnalysisLines()) if n != self.multipvSetting: self.multipvSetting = n print("stop", file=self.engine) print("setoption name MultiPV value", n, file=self.engine) self._searchNow() return n def __repr__(self): if self.name: return self.name if "name" in self.ids: return self.ids["name"] return ', '.join(self.defname)
class CECPEngine(ProtocolEngine): def __init__(self, subprocess, color, protover, md5): ProtocolEngine.__init__(self, subprocess, color, protover, md5) self.features = { "ping": 0, "setboard": 0, "playother": 0, "san": 0, "usermove": 0, "time": 1, "draw": 1, "sigint": 0, "sigterm": 0, "reuse": 0, "analyze": 0, "myname": ', '.join(self.defname), "variants": None, "colors": 1, "ics": 0, "name": 0, "pause": 0, "nps": 0, "debug": 0, "memory": 0, "smp": 0, "egt": '', "option": '', "exclude": 0, "done": None, } self.supported_features = [ "ping", "setboard", "san", "usermove", "time", "draw", "sigint", "analyze", "myname", "variants", "colors", "pause", "done", "egt", "debug", "smp", "memory", "option" ] self.options = {} self.options["Ponder"] = { "name": "Ponder", "type": "check", "default": False } self.name = None self.board = Board(setup=True) # if self.engineIsInNotPlaying == True, engine is in "force" mode, # i.e. not thinking or playing, but still verifying move legality self.engineIsInNotPlaying = False self.engineIsAnalyzing = False self.movenext = False self.waitingForMove = False self.readyForMoveNowCommand = False self.timeHandicap = 1 self.lastping = 0 self.lastpong = 0 self.timeout = None self.returnQueue = Queue() self.engine.connect("line", self.parseLine) self.engine.connect("died", lambda e: self.returnQueue.put("del")) self.invalid_move = None self.funcQueue = Queue() self.optionQueue = [] self.boardLock = RLock() self.undoQueue = [] self.analysis_timer = None self.connect("readyForOptions", self.__onReadyForOptions_before) self.connect_after("readyForOptions", self.__onReadyForOptions) self.connect_after("readyForMoves", self.__onReadyForMoves) #=========================================================================== # Starting the game #=========================================================================== def prestart(self): print("xboard", file=self.engine) if self.protover == 1: # start a new game (CECPv1 engines): print("new", file=self.engine) # we are now ready for options: self.emit("readyForOptions") elif self.protover == 2: # start advanced protocol initialisation: print("protover 2", file=self.engine) # we don't start a new game for CECPv2 here, # we will do it after feature accept/reject is completed. # set timeout for feature accept/reject: self.timeout = time.time() + TIME_OUT_FIRST def start(self): if self.mode in (ANALYZING, INVERSE_ANALYZING): t = Thread(target=self.__startBlocking, name=fident(self.__startBlocking)) t.daemon = True t.start() else: self.__startBlocking() def __startBlocking(self): if self.protover == 1: self.emit("readyForMoves") if self.protover == 2: try: r = self.returnQueue.get(True, max(self.timeout - time.time(), 0)) if r == "not ready": # The engine has sent done=0, and parseLine has added more # time to self.timeout r = self.returnQueue.get( True, max(self.timeout - time.time(), 0)) # Gaviota sends done=0 after "xboard" and after "protover 2" too if r == "not ready": r = self.returnQueue.get( True, max(self.timeout - time.time(), 0)) except Empty: log.warning("Got timeout error", extra={"task": self.defname}) self.emit("readyForOptions") self.emit("readyForMoves") else: if r == 'del': raise PlayerIsDead assert r == "ready" def __onReadyForOptions_before(self, self_): self.readyOptions = True def __onReadyForOptions(self, self_): # This is no longer needed #self.timeout = time.time() # We always want post turned on so the Engine Output sidebar can # show those things -Jonas Thiem print("post", file=self.engine) for command in self.optionQueue: print(command, file=self.engine) def __onReadyForMoves(self, self_): # If we are an analyzer, this signal was already called in a different # thread, so we can safely block it. if self.mode in (ANALYZING, INVERSE_ANALYZING): # workaround for crafty not sending analysis after it has found a mating line # http://code.google.com/p/pychess/issues/detail?id=515 if "crafty" in self.features["myname"].lower(): print("noise 0", file=self.engine) self.__sendAnalyze(self.mode == INVERSE_ANALYZING) self.readyMoves = True semisynced(lambda s: None)(self) #=========================================================================== # Ending the game #=========================================================================== @semisynced def end(self, status, reason): if self.connected: # We currently can't fillout the comment "field" as the repr strings # for reasons and statuses lies in Main.py # Creating Status and Reason class would solve this if status == DRAW: print("result 1/2-1/2 {?}", file=self.engine) elif status == WHITEWON: print("result 1-0 {?}", file=self.engine) elif status == BLACKWON: print("result 0-1 {?}", file=self.engine) else: print("result * {?}", file=self.engine) if reason == WON_ADJUDICATION: self.returnQueue.put("invalid") # Make sure the engine exits and do some cleaning self.kill(reason) def kill(self, reason): """ Kills the engine, starting with the 'quit' command, then sigterm and eventually sigkill. Returns the exitcode, or if engine have already been killed, returns None """ if self.connected: self.connected = False try: try: print("quit", file=self.engine) self.returnQueue.put("del") self.engine.gentleKill() except OSError as e: # No need to raise on a hang up error, as the engine is dead # anyways if e.errno == 32: log.warning("Hung up Error", extra={"task": self.defname}) return e.errno else: raise finally: # Clear the analyzed data, if any self.emit("analyze", []) if self.analysis_timer is not None: self.analysis_timer.cancel() self.analysis_timer.join() #=========================================================================== # Send the player move updates #=========================================================================== def setBoard(self, board): self.setBoardList([board], []) self.__sendAnalyze(self.mode == INVERSE_ANALYZING) @semisynced def putMove(self, board1, move, board2): """ Sends the engine the last move made (for spectator engines). @param board1: The current board @param move: The last move made @param board2: The board before the last move was made """ self.setBoardList([board1], []) self.__sendAnalyze(self.mode == INVERSE_ANALYZING) def makeMove(self, board1, move, board2): """ Gets a move from the engine (for player engines). @param board1: The current board @param move: The last move made @param board2: The board before the last move was made @return: The move the engine decided to make """ log.debug("makeMove: move=%s self.movenext=%s board1=%s board2=%s self.board=%s" % \ (move, self.movenext, board1, board2, self.board), extra={"task":self.defname}) assert self.readyMoves self.boardLock.acquire() try: if self.board == board1 or not board2 or self.movenext: self.board = board1 self.__tellEngineToPlayCurrentColorAndMakeMove() self.movenext = False else: self.board = board1 self.__usermove(board2, move) if self.engineIsInNotPlaying: self.__tellEngineToPlayCurrentColorAndMakeMove() finally: self.boardLock.release() self.waitingForMove = True self.readyForMoveNowCommand = True # Parse outputs r = self.returnQueue.get() if r == "not ready": log.warning( "Engine seems to be protover=2, but is treated as protover=1", extra={"task": self.defname}) r = self.returnQueue.get() if r == "ready": r = self.returnQueue.get() if r == "invalid": raise InvalidMove if r == "del": raise PlayerIsDead("Killed by foreign forces") if r == "int": raise TurnInterrupt self.waitingForMove = False self.readyForMoveNowCommand = False assert isinstance(r, Move), r return r @semisynced def updateTime(self, secs, opsecs): if self.features["time"]: print("time %s" % int(secs * 100 * self.timeHandicap), file=self.engine) print("otim %s" % int(opsecs * 100), file=self.engine) #=========================================================================== # Standard options #=========================================================================== def setOptionAnalyzing(self, mode): self.mode = mode def setOptionInitialBoard(self, model): # We don't use the optionQueue here, as set board prints a whole lot of # stuff. Instead we just call it, and let semisynced handle the rest. self.setBoardList(model.boards[:], model.moves[:]) @semisynced def setBoardList(self, boards, moves): # Notice: If this method is to be called while playing, the engine will # need 'new' and an arrangement similar to that of 'pause' to avoid # the current thought move to appear self.boardLock.acquire() try: if self.mode not in (ANALYZING, INVERSE_ANALYZING): self.__tellEngineToStopPlayingCurrentColor() self.__setBoard(boards[0]) self.board = boards[-1] for board, move in zip(boards[:-1], moves): self.__usermove(board, move) if self.mode in (ANALYZING, INVERSE_ANALYZING): self.board = boards[-1] if self.mode == INVERSE_ANALYZING: self.board = self.board.switchColor() # The called of setBoardList will have to repost/analyze the # analyzer engines at this point. finally: self.boardLock.release() def setOptionVariant(self, variant): if self.features["variants"] is None: log.warning("setOptionVariant: engine doesn't support variants", extra={"task": self.defname}) return if variant in variants.values() and not variant.standard_rules: assert variant.cecp_name in self.features["variants"], \ "%s doesn't support %s variant" % (self, variant.cecp_name) self.optionQueue.append("variant %s" % variant.cecp_name) #==================================================# # Strength system # #==================================================# # Strength Depth Ponder Time handicap # # 1 1 o 1,258% # # 2 2 o 1,584% # # 3 3 o 1.995% # # # # 19 o x 79,43% # # 20 o x o # #==================================================# def setOptionStrength(self, strength, forcePonderOff): self.strength = strength if strength <= 19: self.__setTimeHandicap(0.01 * 10**(strength / 10.)) if strength <= 18: self.__setDepth(strength) # Crafty ofers 100 skill levels if "crafty" in self.features["myname"].lower() and strength <= 19: self.optionQueue.append("skill %s" % strength * 5) self.__setPonder(strength >= 19 and not forcePonderOff) if strength == 20: if "gaviota" in self.features["egt"]: self.optionQueue.append("egtpath gaviota %s" % conf.get("egtb_path", "")) else: self.optionQueue.append("random") def __setDepth(self, depth): self.optionQueue.append("sd %d" % depth) def __setTimeHandicap(self, timeHandicap): self.timeHandicap = timeHandicap def __setPonder(self, ponder): if ponder: self.optionQueue.append("hard") else: self.optionQueue.append("hard") self.optionQueue.append("easy") def setOptionTime(self, secs, gain): # Notice: In CECP we apply time handicap in updateTime, not in # setOptionTime. minutes = int(secs / 60) secs = int(secs % 60) s = str(minutes) if secs: s += ":" + str(secs) self.optionQueue.append("level 0 %s %d" % (s, gain)) #=========================================================================== # Option handling #=========================================================================== def setOption(self, key, value): """ Set an option, which will be sent to the engine, after the 'readyForOptions' signal has passed. If you want to know the possible options, you should go to engineDiscoverer or use the getOption, getOptions and hasOption methods, while you are in your 'readyForOptions' signal handler """ if self.readyMoves: log.warning( "Options set after 'readyok' are not sent to the engine", extra={"task": self.defname}) if key == "cores": self.optionQueue.append("cores %s" % value) elif key == "memory": self.optionQueue.append("memory %s" % value) elif key.lower() == "ponder": self.__setPonder(value == 1) else: self.optionQueue.append("option %s=%s" % (key, value)) #=========================================================================== # Interacting with the player #=========================================================================== @semisynced def pause(self): """ Pauses engine using the "pause" command if available. Otherwise put engine in force mode. By the specs the engine shouldn't ponder in force mode, but some of them do so anyways. """ log.debug("pause: self=%s" % self, extra={"task": self.defname}) self.engine.pause() return if self.mode in (ANALYZING, INVERSE_ANALYZING): return if self.features["pause"]: print("pause", file=self.engine) elif self.board: self.__tellEngineToStopPlayingCurrentColor() self._blockTillMove() @semisynced def resume(self): log.debug("resume: self=%s" % self, extra={"task": self.defname}) self.engine.resume() return if self.mode not in (ANALYZING, INVERSE_ANALYZING): if self.features["pause"]: print("features resume") print("resume", file=self.engine) elif self.board: print("go resume") self.__tellEngineToPlayCurrentColorAndMakeMove() @semisynced def hurry(self): log.debug("hurry: self.waitingForMove=%s self.readyForMoveNowCommand=%s" % \ (self.waitingForMove, self.readyForMoveNowCommand), extra={"task":self.defname}) if self.waitingForMove and self.readyForMoveNowCommand: self.__tellEngineToMoveNow() self.readyForMoveNowCommand = False @semisynced def spectatorUndoMoves(self, moves, gamemodel): log.debug("spectatorUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s" % \ (moves, gamemodel.ply, gamemodel.boards[-1], self.board), extra={"task":self.defname}) for i in range(moves): print("undo", file=self.engine) self.board = gamemodel.boards[-1] @semisynced def playerUndoMoves(self, moves, gamemodel): log.debug("playerUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s" % \ (moves, gamemodel.ply, gamemodel.boards[-1], self.board), extra={"task":self.defname}) if gamemodel.curplayer != self and moves % 2 == 1: # Interrupt if we were searching, but should no longer do so self.returnQueue.put("int") self.__tellEngineToStopPlayingCurrentColor() for i in range(moves): print("undo", file=self.engine) if gamemodel.curplayer == self: self.board = gamemodel.boards[-1] self.__tellEngineToPlayCurrentColorAndMakeMove() else: self.board = None #=========================================================================== # Offer handling #=========================================================================== def offer(self, offer): if offer.type == DRAW_OFFER: if self.features["draw"]: print("draw", file=self.engine) else: self.emit("accept", offer) def offerError(self, offer, error): if self.features["draw"]: # We don't keep track if engine draws are offers or accepts. We just # Always assume they are accepts, and if they are not, we get this # error and emit offer instead if offer.type == DRAW_OFFER and error == ACTION_ERROR_NONE_TO_ACCEPT: self.emit("offer", Offer(DRAW_OFFER)) #=========================================================================== # Internal #=========================================================================== def __usermove(self, board, move): if self.features["usermove"]: self.engine.write("usermove ") if self.features["san"]: print(toSAN(board, move), file=self.engine) else: cn = CASTLE_KK if board.variant == FISCHERRANDOMCHESS: cn = CASTLE_SAN print(toAN(board, move, short=True, castleNotation=cn), file=self.engine) def __tellEngineToMoveNow(self): if self.features["sigint"]: self.engine.sigint() print("?", file=self.engine) def __tellEngineToStopPlayingCurrentColor(self): print("force", file=self.engine) self.engineIsInNotPlaying = True def __tellEngineToPlayCurrentColorAndMakeMove(self): self.__printColor() print("go", file=self.engine) self.engineIsInNotPlaying = False def __sendAnalyze(self, inverse=False): if inverse and self.board.board.opIsChecked(): # Many engines don't like positions able to take down enemy # king. Therefore we just return the "kill king" move # automaticaly self.emit("analyze", [([toAN(self.board, getMoveKillingKing(self.board)) ], MATE_VALUE - 1, "")]) return def stop_analyze(): if self.engineIsAnalyzing: print("exit", file=self.engine) # Some engines (crafty, gnuchess) doesn't respond to exit command # we try to force them to stop with an empty board fen print("setboard 8/8/8/8/8/8/8/8 w - - 0 1", file=self.engine) self.engineIsAnalyzing = False print("post", file=self.engine) print("analyze", file=self.engine) self.engineIsAnalyzing = True if self.analysis_timer is not None: self.analysis_timer.cancel() self.analysis_timer.join() self.analysis_timer = Timer(conf.get("max_analysis_spin", 3), stop_analyze) self.analysis_timer.start() def __printColor(self): if self.features["colors"]: #or self.mode == INVERSE_ANALYZING: if self.board.color == WHITE: print("white", file=self.engine) else: print("black", file=self.engine) def __setBoard(self, board): if self.features["setboard"]: self.__tellEngineToStopPlayingCurrentColor() fen = board.asFen(enable_bfen=False) if self.mode == INVERSE_ANALYZING: fen_arr = fen.split() if not self.board.board.opIsChecked(): if fen_arr[1] == "b": fen_arr[1] = "w" else: fen_arr[1] = "b" fen = " ".join(fen_arr) print("setboard %s" % fen, file=self.engine) else: # Kludge to set black to move, avoiding the troublesome and now # deprecated "black" command. - Equal to the one xboard uses self.__tellEngineToStopPlayingCurrentColor() if board.color == BLACK: print("a2a3", file=self.engine) print("edit", file=self.engine) print("#", file=self.engine) for color in WHITE, BLACK: for y, row in enumerate(board.data): for x, piece in enumerate(row): if not piece or piece.color != color: continue sign = reprSign[piece.sign] cord = repr(Cord(x, y)) print(sign + cord, file=self.engine) print("c", file=self.engine) print(".", file=self.engine) def _blockTillMove(self): saved_state = self.boardLock._release_save() log.debug("_blockTillMove(): acquiring self.movecon lock", extra={"task": self.defname}) self.movecon.acquire() log.debug("_blockTillMove(): self.movecon acquired", extra={"task": self.defname}) try: log.debug("_blockTillMove(): doing self.movecon.wait", extra={"task": self.defname}) self.movecon.wait() finally: log.debug("_blockTillMove(): releasing self.movecon..", extra={"task": self.defname}) self.movecon.release() self.boardLock._acquire_restore(saved_state) #=========================================================================== # Parsing #=========================================================================== def parseLine(self, engine, line): if line[0:1] == "#": # Debug line which we shall ignore as specified in CECPv2 specs return # log.debug("__parseLine: line=\"%s\"" % line.strip(), extra={"task":self.defname}) parts = whitespaces.split(line.strip()) if parts[0] == "pong": self.lastpong = int(parts[1]) return # Illegal Move if parts[0].lower().find("illegal") >= 0: log.warning("__parseLine: illegal move: line=\"%s\", board=%s" \ % (line.strip(), self.board), extra={"task":self.defname}) if parts[-2] == "sd" and parts[-1].isdigit(): print("depth", parts[-1], file=self.engine) return # A Move (Perhaps) if self.board: if parts[0] == "move": movestr = parts[1] # Old Variation elif d_plus_dot_expr.match(parts[0]) and parts[1] == "...": movestr = parts[2] else: movestr = False if movestr: log.debug("__parseLine: acquiring self.boardLock", extra={"task": self.defname}) self.waitingForMove = False self.readyForMoveNowCommand = False self.boardLock.acquire() try: if self.engineIsInNotPlaying: # If engine was set in pause just before the engine sent its # move, we ignore it. However the engine has to know that we # ignored it, and thus we step it one back log.info("__parseLine: Discarding engine's move: %s" % movestr, extra={"task": self.defname}) print("undo", file=self.engine) return else: try: move = parseAny(self.board, movestr) except ParsingError as e: self.invalid_move = movestr log.info( "__parseLine: ParsingError engine move: %s %s" % (movestr, self.board), extra={"task": self.defname}) self.end( WHITEWON if self.board.color == BLACK else BLACKWON, WON_ADJUDICATION) return if validate(self.board, move): self.board = None self.returnQueue.put(move) return else: self.invalid_move = movestr log.info( "__parseLine: can't validate engine move: %s %s" % (movestr, self.board), extra={"task": self.defname}) self.end( WHITEWON if self.board.color == BLACK else BLACKWON, WON_ADJUDICATION) return finally: log.debug("__parseLine(): releasing self.boardLock", extra={"task": self.defname}) self.boardLock.release() self.movecon.acquire() self.movecon.notifyAll() self.movecon.release() # Analyzing if self.engineIsInNotPlaying: if parts[:4] == ["0", "0", "0", "0"]: # Crafty doesn't analyze until it is out of book print("book off", file=self.engine) return match = anare.match(line) if match: depth, score, moves = match.groups() if "mat" in score.lower() or "#" in moves: # Will look either like -Mat 3 or Mat3 scoreval = MATE_VALUE if score.startswith('-'): scoreval = -scoreval else: scoreval = int(score) mvstrs = movere.findall(moves) if mvstrs: self.emit("analyze", [(mvstrs, scoreval, depth.strip())]) return # Offers draw if parts[0:2] == ["offer", "draw"]: self.emit("accept", Offer(DRAW_OFFER)) return # Resigns if parts[0] == "resign" or \ (parts[0] == "tellics" and parts[1] == "resign"): # buggy crafty # Previously: if "resign" in parts, # however, this is too generic, since "hint", "bk", # "feature option=.." and possibly other, future CECPv2 # commands can validly contain the word "resign" without this # being an intentional resign offer. self.emit("offer", Offer(RESIGNATION)) return #if parts[0].lower() == "error": # return #Tell User Error if parts[0] == "tellusererror": # We don't want to see our stop analyzer hack as an error message if "8/8/8/8/8/8/8/8" in "".join(parts[1:]): return # Create a non-modal non-blocking message dialog with the error: dlg = Gtk.MessageDialog(parent=None, flags=0, type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.CLOSE, message_format=None) # Use the engine name if already known, otherwise the defname: displayname = self.name if not displayname: displayname = self.defname # Compose the dialog text: dlg.set_markup( GObject.markup_escape_text( _("The engine %s reports an error:") % displayname) + "\n\n" + GObject.markup_escape_text(" ".join(parts[1:]))) # handle response signal so the "Close" button works: dlg.connect("response", lambda dlg, x: dlg.destroy()) dlg.show_all() return # Tell Somebody if parts[0][:4] == "tell" and \ parts[0][4:] in ("others", "all", "ics", "icsnoalias"): log.info("Ignoring tell %s: %s" % (parts[0][4:], " ".join(parts[1:]))) return if "feature" in parts: # Some engines send features after done=1, so we will iterate after done=1 too done1 = False # We skip parts before 'feature', as some engines give us lines like # White (1) : feature setboard=1 analyze...e="GNU Chess 5.07" done=1 parts = parts[parts.index("feature"):] for i, pair in enumerate(parts[1:]): # As "parts" is split with no thoughs on quotes or double quotes # we need to do some extra handling. if pair.find("=") < 0: continue key, value = pair.split("=", 1) if not key in self.features: continue if value.startswith('"') and value.endswith('"'): value = value[1:-1] # If our pair was unfinished, like myname="GNU, we search the # rest of the pairs for a quotating mark. elif value[0] == '"': rest = value[1:] + " " + " ".join(parts[2 + i:]) j = rest.find('"') if j == -1: log.warning("Missing endquotation in %s feature", extra={"task": self.defname}) value = rest else: value = rest[:j] elif value.isdigit(): value = int(value) if key in self.supported_features: print("accepted %s" % key, file=self.engine) else: print("rejected %s" % key, file=self.engine) if key == "done": if value == 1: done1 = True continue elif value == 0: log.info("Adds %d seconds timeout" % TIME_OUT_SECOND, extra={"task": self.defname}) # This'll buy you some more time self.timeout = time.time() + TIME_OUT_SECOND self.returnQueue.put("not ready") return if key == "smp" and value == 1: self.options["cores"] = { "name": "cores", "type": "spin", "default": 1, "min": 1, "max": 64 } elif key == "memory" and value == 1: self.options["memory"] = { "name": "memory", "type": "spin", "default": 32, "min": 1, "max": 4096 } elif key == "option" and key != "done": option = self.__parse_option(value) self.options[option["name"]] = option else: self.features[key] = value if key == "myname" and not self.name: self.setName(value) if done1: # Start a new game before using the engine: # (CECPv2 engines) print("new", file=self.engine) # We are now ready for play: self.emit("readyForOptions") self.emit("readyForMoves") self.returnQueue.put("ready") # A hack to get better names in protover 1. # Unfortunately it wont work for now, as we don't read any lines from # protover 1 engines. When should we stop? if self.protover == 1: if self.defname[0] in ''.join(parts): basis = self.defname[0] name = ' '.join( itertools.dropwhile(lambda part: basis not in part, parts)) self.features['myname'] = name if not self.name: self.setName(name) def __parse_option(self, option): if " -check " in option: name, value = option.split(" -check ") return {"type": "check", "name": name, "default": bool(int(value))} elif " -spin " in option: name, value = option.split(" -spin ") defv, minv, maxv = value.split() return { "type": "spin", "name": name, "default": int(defv), "min": int(minv), "max": int(maxv) } elif " -slider " in option: name, value = option.split(" -slider ") defv, minv, maxv = value.split() return { "type": "spin", "name": name, "default": int(defv), "min": int(minv), "max": int(maxv) } elif " -string " in option: name, value = option.split(" -string ") return {"type": "text", "name": name, "default": value} elif " -file " in option: name, value = option.split(" -file ") return {"type": "text", "name": name, "default": value} elif " -path " in option: name, value = option.split(" -path ") return {"type": "text", "name": name, "default": value} elif " -combo " in option: name, value = option.split(" -combo ") choices = list(map(str.strip, value.split("///"))) default = "" for choice in choices: if choice.startswith("*"): index = choices.index(choice) default = choice[1:] choices[index] = default break return { "type": "combo", "name": name, "default": default, "choices": choices } elif " -button" in option: pos = option.find(" -button") return {"type": "button", "name": option[:pos]} elif " -save" in option: pos = option.find(" -save") return {"type": "button", "name": option[:pos]} elif " -reset" in option: pos = option.find(" -reset") return {"type": "button", "name": option[:pos]} #=========================================================================== # Info #=========================================================================== def canAnalyze(self): assert self.ready, "Still waiting for done=1" return self.features["analyze"] def maxAnalysisLines(self): return 1 def requestMultiPV(self, setting): return 1 def isAnalyzing(self): return self.mode in (ANALYZING, INVERSE_ANALYZING) def __repr__(self): if self.name: return self.name return self.features["myname"]
class UCIEngine (ProtocolEngine): def __init__ (self, subprocess, color, protover, md5): ProtocolEngine.__init__(self, subprocess, color, protover, md5) self.ids = {} self.options = {} self.optionsToBeSent = {} self.wtime = 60000 self.btime = 60000 self.incr = 0 self.timeHandicap = 1 self.moveLock = RLock() # none of the following variables should be changed or used in a # condition statement without holding the above self.moveLock self.ponderOn = False self.pondermove = None self.ignoreNext = False self.waitingForMove = False self.needBestmove = False self.readyForStop = False # keeps track of whether we already sent a 'stop' command self.multipvSetting = conf.get("multipv", 1) # MultiPV option sent to the engine self.multipvExpected = 1 # Number of PVs expected (limited by number of legal moves) self.commands = collections.deque() self.gameBoard = Board(setup=True) # board at the end of all moves played self.board = Board(setup=True) # board to send the engine self.uciPosition = "startpos" self.uciPositionListsMoves = False self.analysis = [ None ] self.returnQueue = Queue() self.engine.connect("line", self.parseLines) self.engine.connect("died", self.__die) self.connect("readyForOptions", self.__onReadyForOptions_before) self.connect_after("readyForOptions", self.__onReadyForOptions) self.connect_after("readyForMoves", self.__onReadyForMoves) def __die (self, subprocess): self.returnQueue.put("die") #=========================================================================== # Starting the game #=========================================================================== def prestart (self): print("uci", file=self.engine) def start (self): if self.mode in (ANALYZING, INVERSE_ANALYZING): t = Thread(target=self.__startBlocking, name=fident(self.__startBlocking)) t.daemon = True t.start() else: self.__startBlocking() def __startBlocking (self): r = self.returnQueue.get() if r == 'die': raise PlayerIsDead assert r == "ready" or r == 'del' #self.emit("readyForOptions") #self.emit("readyForMoves") def __onReadyForOptions_before (self, self_): self.readyOptions = True def __onReadyForOptions (self, self_): if self.mode in (ANALYZING, INVERSE_ANALYZING): if self.hasOption("Ponder"): self.setOption('Ponder', False) if self.hasOption("MultiPV") and self.multipvSetting > 1: self.setOption('MultiPV', self.multipvSetting) for option, value in self.optionsToBeSent.items(): if isinstance(value, bool): value = str(value).lower() print("setoption name %s value %s" % (option, str(value)), file=self.engine) print("isready", file=self.engine) def __onReadyForMoves (self, self_): self.returnQueue.put("ready") self.readyMoves = True self._newGame() # If we are an analyzer, this signal was already called in a different # thread, so we can safely block it. if self.mode in (ANALYZING, INVERSE_ANALYZING): self._searchNow() #=========================================================================== # Ending the game #=========================================================================== def end (self, status, reason): # UCI doens't care about reason, so we just kill self.kill(reason) def kill (self, reason): """ Kills the engine, starting with the 'stop' and 'quit' commands, then trying sigterm and eventually sigkill. Returns the exitcode, or if engine have already been killed, the method returns None """ if self.connected: self.connected = False try: try: print("stop", file=self.engine) print("quit", file=self.engine) self.returnQueue.put("del") return self.engine.gentleKill() except OSError as e: # No need to raise on a hang up error, as the engine is dead # anyways if e.errno == 32: log.warning("Hung up Error", extra={"task":self.defname}) return e.errno else: raise finally: # Clear the analyzed data, if any self.emit("analyze", []) #=========================================================================== # Send the player move updates #=========================================================================== def _moveToUCI (self, board, move): cn = CASTLE_KK if board.variant == FISCHERRANDOMCHESS: cn = CASTLE_KR return toAN(board, move, short=True, castleNotation=cn) def _recordMove (self, board1, move, board2): if self.gameBoard == board1: return if not board2: if board1.variant == NORMALCHESS and board1.asFen() == FEN_START: self.uciPosition = "startpos" else: self.uciPosition = "fen " + board1.asFen() self.uciPositionListsMoves = False if move: if not self.uciPositionListsMoves: self.uciPosition += " moves" self.uciPositionListsMoves = True self.uciPosition += " " + self._moveToUCI(board2, move) self.board = self.gameBoard = board1 if self.mode == INVERSE_ANALYZING: self.board = self.gameBoard.switchColor() def _recordMoveList (self, model, ply=None): self._recordMove(model.boards[0], None, None) if ply is None: ply = model.ply for board1, move, board2 in zip(model.boards[1:ply+1], model.moves, model.boards[0:ply]): self._recordMove(board1, move, board2) def setBoard (self, board): log.debug("setBoardAtPly: board=%s" % board, extra={"task":self.defname}) self._recordMove(board, None, None) if not self.readyMoves: return self._searchNow() def putMove (self, board1, move, board2): log.debug("putMove: board1=%s move=%s board2=%s self.board=%s" % \ (board1, move, board2, self.board), extra={"task":self.defname}) self._recordMove(board1, move, board2) if not self.readyMoves: return self._searchNow() def makeMove (self, board1, move, board2): log.debug("makeMove: move=%s self.pondermove=%s board1=%s board2=%s self.board=%s" % \ (move, self.pondermove, board1, board2, self.board), extra={"task":self.defname}) assert self.readyMoves with self.moveLock: self._recordMove(board1, move, board2) self.waitingForMove = True ponderhit = False if board2 and self.pondermove and move == self.pondermove: ponderhit = True elif board2 and self.pondermove: self.ignoreNext = True print("stop", file=self.engine) self._searchNow(ponderhit=ponderhit) # Parse outputs try: r = self.returnQueue.get() if r == "del": raise PlayerIsDead if r == "int": with self.moveLock: self.pondermove = None self.ignoreNext = True self.needBestmove = True self.hurry() raise TurnInterrupt return r finally: with self.moveLock: self.waitingForMove = False # empty the queue of any moves received post-undo/TurnInterrupt self.returnQueue.queue.clear() def updateTime (self, secs, opsecs): if self.color == WHITE: self.wtime = int(secs*1000*self.timeHandicap) self.btime = int(opsecs*1000) else: self.btime = int(secs*1000*self.timeHandicap) self.wtime = int(opsecs*1000) #=========================================================================== # Standard options #=========================================================================== def setOptionAnalyzing (self, mode): self.mode = mode if self.mode == INVERSE_ANALYZING: self.board = self.gameBoard.switchColor() def setOptionInitialBoard (self, model): log.debug("setOptionInitialBoard: self=%s, model=%s" % \ (self, model), extra={"task":self.defname}) self._recordMoveList(model) def setOptionVariant (self, variant): if variant == FischerRandomChess: assert self.hasOption("UCI_Chess960") self.setOption("UCI_Chess960", True) elif self.hasOption("UCI_Variant") and not variant.standard_rules: self.setOption("UCI_Variant", variant.cecp_name) def setOptionTime (self, secs, gain): self.wtime = int(max(secs*1000*self.timeHandicap, 1)) self.btime = int(max(secs*1000*self.timeHandicap, 1)) self.incr = int(gain*1000*self.timeHandicap) def setOptionStrength (self, strength, forcePonderOff): self.strength = strength if self.hasOption('UCI_LimitStrength') and strength <= 18: self.setOption('UCI_LimitStrength', True) if self.hasOption('UCI_Elo'): self.setOption('UCI_Elo', 150 * strength) # Stockfish offers 20 skill levels if self.hasOption('Skill Level') and strength <= 19: self.setOption('Skill Level', strength) if ((not self.hasOption('UCI_Elo')) and (not self.hasOption('Skill Level'))) or strength <= 19: self.timeHandicap = th = 0.01 * 10**(strength/10.) self.wtime = int(max(self.wtime*th, 1)) self.btime = int(max(self.btime*th, 1)) self.incr = int(self.incr*th) if self.hasOption('Ponder'): self.setOption('Ponder', strength >= 19 and not forcePonderOff) if self.hasOption('GaviotaTbPath') and strength == 20: self.setOption('GaviotaTbPath', conf.get("egtb_path", "")) #=========================================================================== # Interacting with the player #=========================================================================== def pause (self): log.debug("pause: self=%s" % self, extra={"task":self.defname}) self.engine.pause() return if self.board.color == self.color or \ self.mode != NORMAL or self.pondermove: self.ignoreNext = True print("stop", file=self.engine) def resume (self): log.debug("resume: self=%s" % self, extra={"task":self.defname}) self.engine.resume() return if self.mode == NORMAL: if self.board.color == self.color: self._searchNow() elif self.ponderOn and self.pondermove: self._startPonder() else: self._searchNow() def hurry (self): log.debug("hurry: self.waitingForMove=%s self.readyForStop=%s" % \ (self.waitingForMove, self.readyForStop), extra={"task":self.defname}) # sending this more than once per move will crash most engines # so we need to send only the first one, and then ignore every "hurry" request # after that until there is another outstanding "position..go" with self.moveLock: if self.waitingForMove and self.readyForStop: print("stop", file=self.engine) self.readyForStop = False def playerUndoMoves (self, moves, gamemodel): log.debug("playerUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s" % \ (moves, gamemodel.ply, gamemodel.boards[-1], self.board), extra={"task":self.defname}) self._recordMoveList(gamemodel) if (gamemodel.curplayer != self and moves % 2 == 1) or \ (gamemodel.curplayer == self and moves % 2 == 0): # Interrupt if we were searching but should no longer do so, or # if it is was our move before undo and it is still our move after undo # since we need to send the engine the new FEN in makeMove() log.debug("playerUndoMoves: putting 'int' into self.returnQueue=%s" % \ self.returnQueue.queue, extra={"task":self.defname}) self.returnQueue.put("int") def spectatorUndoMoves (self, moves, gamemodel): log.debug("spectatorUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s" % \ (moves, gamemodel.ply, gamemodel.boards[-1], self.board), extra={"task":self.defname}) self._recordMoveList(gamemodel) if self.readyMoves: self._searchNow() #=========================================================================== # Offer handling #=========================================================================== def offer (self, offer): if offer.type == DRAW_OFFER: self.emit("decline", offer) else: self.emit("accept", offer) #=========================================================================== # Option handling #=========================================================================== def setOption (self, key, value): """ Set an option, which will be sent to the engine, after the 'readyForOptions' signal has passed. If you want to know the possible options, you should go to engineDiscoverer or use the getOption, getOptions and hasOption methods, while you are in your 'readyForOptions' signal handler """ if self.readyMoves: log.warning("Options set after 'readyok' are not sent to the engine", extra={"task":self.defname}) self.optionsToBeSent[key] = value self.ponderOn = key=="Ponder" and value is True def getOption (self, option): assert self.readyOptions if option in self.options: return self.options[option]["default"] return None def getOptions (self): assert self.readyOptions return copy(self.options) def hasOption (self, key): assert self.readyOptions return key in self.options #=========================================================================== # Internal #=========================================================================== def _newGame (self): print("ucinewgame", file=self.engine) def _searchNow (self, ponderhit=False): log.debug("_searchNow: self.needBestmove=%s ponderhit=%s self.board=%s" % \ (self.needBestmove, ponderhit, self.board), extra={"task":self.defname}) with self.moveLock: commands = [] if ponderhit: commands.append("ponderhit") elif self.mode == NORMAL: commands.append("position %s" % self.uciPosition) if self.strength <= 3: commands.append("go depth %d" % self.strength) else: commands.append("go wtime %d winc %d btime %d binc %d" % \ (self.wtime, self.incr, self.btime, self.incr)) else: print("stop", file=self.engine) if self.mode == INVERSE_ANALYZING: if self.board.board.opIsChecked(): # Many engines don't like positions able to take down enemy # king. Therefore we just return the "kill king" move # automaticaly self.emit("analyze", [([getMoveKillingKing(self.board)], MATE_VALUE-1, "")]) return commands.append("position fen %s" % self.board.asFen()) else: commands.append("position %s" % self.uciPosition) #commands.append("go infinite") move_time = int(conf.get("max_analysis_spin", 3))*1000 commands.append("go movetime %s" % move_time) if self.hasOption("MultiPV") and self.multipvSetting > 1: self.multipvExpected = min(self.multipvSetting, legalMoveCount(self.board)) else: self.multipvExpected = 1 self.analysis = [None] * self.multipvExpected if self.needBestmove: self.commands.append(commands) log.debug("_searchNow: self.needBestmove==True, appended to self.commands=%s" % \ self.commands, extra={"task":self.defname}) else: for command in commands: print(command, file=self.engine) if getStatus(self.board)[1] != WON_MATE: # XXX This looks fishy. self.needBestmove = True self.readyForStop = True def _startPonder (self): uciPos = self.uciPosition if not self.uciPositionListsMoves: uciPos += " moves" print("position", uciPos, \ self._moveToUCI(self.board, self.pondermove), file=self.engine) print("go ponder wtime", self.wtime, \ "winc", self.incr, "btime", self.btime, "binc", self.incr, file=self.engine) #=========================================================================== # Parsing from engine #=========================================================================== def parseLines (self, engine, lines): for line in lines: self.__parseLine(line) def __parseLine (self, line): if not self.connected: return parts = line.split() if not parts: return #---------------------------------------------------------- Initializing if parts[0] == "id": self.ids[parts[1]] = " ".join(parts[2:]) if parts[1] == "name": self.setName(self.ids["name"]) return if parts[0] == "uciok": self.emit("readyForOptions") return if parts[0] == "readyok": self.emit("readyForMoves") return #------------------------------------------------------- Options parsing if parts[0] == "option": dic = {} last = 1 varlist = [] for i in range (2, len(parts)+1): if i == len(parts) or parts[i] in OPTKEYS: key = parts[last] value = " ".join(parts[last+1:i]) if "type" in dic and dic["type"] in TYPEDIC: value = TYPEDIC[dic["type"]](value) if key == "var": varlist.append(value) elif key == "type" and value == "string": dic[key] = "text" else: dic[key] = value last = i if varlist: dic["choices"] = varlist self.options[dic["name"]] = dic return #---------------------------------------------------------------- A Move if self.mode == NORMAL and parts[0] == "bestmove": with self.moveLock: self.needBestmove = False self.__sendQueuedGo() if self.ignoreNext: log.debug("__parseLine: line='%s' self.ignoreNext==True, returning" % \ line.strip(), extra={"task":self.defname}) self.ignoreNext = False self.readyForStop = True return if not self.waitingForMove: log.warning("__parseLine: self.waitingForMove==False, ignoring move=%s" % \ parts[1], extra={"task":self.defname}) self.pondermove = None return self.waitingForMove = False try: move = parseAny(self.board, parts[1]) except ParsingError as e: self.end(WHITEWON if self.board.color == BLACK else BLACKWON, WON_ADJUDICATION) return if not validate(self.board, move): # This is critical. To avoid game stalls, we need to resign on # behalf of the engine. log.error("__parseLine: move=%s didn't validate, putting 'del' in returnQueue. self.board=%s" % \ (repr(move), self.board), extra={"task":self.defname}) self.end(WHITEWON if self.board.color == BLACK else BLACKWON, WON_ADJUDICATION) return self._recordMove(self.board.move(move), move, self.board) log.debug("__parseLine: applied move=%s to self.board=%s" % \ (move, self.board), extra={"task":self.defname}) if self.ponderOn: self.pondermove = None # An engine may send an empty ponder line, simply to clear. if len(parts) == 4: # Engines don't always check for everything in their # ponders. Hence we need to validate. # But in some cases, what they send may not even be # correct AN - specially in the case of promotion. try: pondermove = parseAny(self.board, parts[3]) except ParsingError: pass else: if validate(self.board, pondermove): self.pondermove = pondermove self._startPonder() self.returnQueue.put(move) log.debug("__parseLine: put move=%s into self.returnQueue=%s" % \ (move, self.returnQueue.queue), extra={"task":self.defname}) return #----------------------------------------------------------- An Analysis if self.mode != NORMAL and parts[0] == "info" and "pv" in parts: multipv = 1 if "multipv" in parts: multipv = int(parts[parts.index("multipv")+1]) scoretype = parts[parts.index("score")+1] if scoretype in ('lowerbound', 'upperbound'): score = None else: score = int(parts[parts.index("score")+2]) if scoretype == 'mate': # print >> self.engine, "stop" if score != 0: sign = score/abs(score) score = sign*MATE_VALUE movstrs = parts[parts.index("pv")+1:] try: moves = listToMoves (self.board, movstrs, AN, validate=True, ignoreErrors=False) except ParsingError as e: # ParsingErrors may happen when parsing "old" lines from # analyzing engines, which haven't yet noticed their new tasks log.debug("__parseLine: Ignored (%s) from analyzer: ParsingError%s" % \ (' '.join(movstrs),e), extra={"task":self.defname}) return if "depth" in parts: depth = parts[parts.index("depth")+1] else: depth = "" if multipv <= len(self.analysis): self.analysis[multipv - 1] = (moves, score, depth) self.emit("analyze", self.analysis) return #----------------------------------------------- An Analyzer bestmove if self.mode != NORMAL and parts[0] == "bestmove": with self.moveLock: log.debug("__parseLine: processing analyzer bestmove='%s'" % \ line.strip(), extra={"task":self.defname}) self.needBestmove = False self.__sendQueuedGo(sendlast=True) return # Stockfish complaining it received a 'stop' without a corresponding 'position..go' if line.strip() == "Unknown command: stop": with self.moveLock: log.debug("__parseLine: processing '%s'" % line.strip(), extra={"task":self.defname}) self.ignoreNext = False self.needBestmove = False self.readyForStop = False self.__sendQueuedGo() return #* score #* cp <x> # the score from the engine's point of view in centipawns. #* mate <y> # mate in y moves, not plies. # If the engine is getting mated use negative values for y. #* lowerbound # the score is just a lower bound. #* upperbound # the score is just an upper bound. def __sendQueuedGo (self, sendlast=False): """ Sends the next position...go or ponderhit command set which was queued (if any). sendlast -- If True, send the last position-go queued rather than the first, and discard the others (intended for analyzers) """ with self.moveLock: if len(self.commands) > 0: if sendlast: commands = self.commands.pop() self.commands.clear() else: commands = self.commands.popleft() for command in commands: print(command, file=self.engine) self.needBestmove = True self.readyForStop = True log.debug("__sendQueuedGo: sent queued go=%s" % commands, extra={"task":self.defname}) #=========================================================================== # Info #=========================================================================== def maxAnalysisLines (self): try: return int(self.options["MultiPV"]["max"]) except (KeyError, ValueError): return 1 # Engine does not support the MultiPV option def requestMultiPV (self, n): multipvMax = self.maxAnalysisLines() n = min(n, multipvMax) if n != self.multipvSetting: conf.set("multipv", n) with self.moveLock: self.multipvSetting = n print("stop", file=self.engine) print("setoption name MultiPV value", n, file=self.engine) self._searchNow() return n def __repr__ (self): if self.name: return self.name if "name" in self.ids: return self.ids["name"] return ', '.join(self.defname)
class CECPEngine (ProtocolEngine): def __init__ (self, subprocess, color, protover, md5): ProtocolEngine.__init__(self, subprocess, color, protover, md5) self.features = { "ping": 0, "setboard": 0, "playother": 0, "san": 0, "usermove": 0, "time": 1, "draw": 1, "sigint": 0, "sigterm": 0, "reuse": 0, "analyze": 0, "myname": ', '.join(self.defname), "variants": None, "colors": 1, "ics": 0, "name": 0, "pause": 0, "nps": 0, "debug": 0, "memory": 0, "smp": 0, "egt": '', "option": '', } self.supported_features = [ "ping", "setboard", "san", "usermove", "time", "draw", "sigint", "analyze", "myname", "variants", "colors", "pause", "done", "debug", "smp", "memory", "option" ] self.options = {} self.options["Ponder"] = {"name": "Ponder", "type": "check", "default": False} self.name = None self.board = Board(setup=True) # if self.engineIsInNotPlaying == True, engine is in "force" mode, # i.e. not thinking or playing, but still verifying move legality self.engineIsInNotPlaying = False self.movenext = False self.waitingForMove = False self.readyForMoveNowCommand = False self.timeHandicap = 1 self.lastping = 0 self.lastpong = 0 self.timeout = None self.returnQueue = Queue.Queue() self.engine.connect("line", self.parseLines) self.engine.connect("died", lambda e: self.returnQueue.put("del")) self.funcQueue = Queue.Queue() self.optionQueue = [] self.boardLock = RLock() self.undoQueue = [] self.connect("readyForOptions", self.__onReadyForOptions_before) self.connect_after("readyForOptions", self.__onReadyForOptions) self.connect_after("readyForMoves", self.__onReadyForMoves) #=========================================================================== # Starting the game #=========================================================================== def prestart (self): print >> self.engine, "xboard" if self.protover == 1: # start a new game (CECPv1 engines): print >> self.engine, "new" # we are now ready for options: #self.emit("readyForOptions") elif self.protover == 2: # start advanced protocol initialisation: print >> self.engine, "protover 2" # we don't start a new game for CECPv2 here, # we will do it after feature accept/reject is completed. # set timeout for feature accept/reject: self.timeout = time.time() + TIME_OUT_FIRST def start (self): if self.mode in (ANALYZING, INVERSE_ANALYZING): pool.start(self.__startBlocking) else: self.__startBlocking() def __startBlocking (self): if self.protover == 1: #self.emit("readyForMoves") if self.protover == 2: try: r = self.returnQueue.get(True, max(self.timeout-time.time(),0)) if r == "not ready": # The engine has sent done=0, and parseLine has added more # time to self.timeout r = self.returnQueue.get(True, max(self.timeout-time.time(),0)) except Queue.Empty: log.warn("Got timeout error\n", self.defname) #self.emit("readyForOptions") #self.emit("readyForMoves") else: if r == 'del': raise PlayerIsDead assert r == "ready" def __onReadyForOptions_before (self, self_): self.readyOptions = True def __onReadyForOptions (self, self_): # This is no longer needed #self.timeout = time.time() # We always want post turned on so the Engine Output sidebar can # show those things -Jonas Thiem print >> self.engine, "post" for command in self.optionQueue: print >> self.engine, command def __onReadyForMoves (self, self_): # If we are an analyzer, this signal was already called in a different # thread, so we can safely block it. if self.mode in (ANALYZING, INVERSE_ANALYZING): if not self.board: self.board = Board(setup=True) self.__sendAnalyze(self.mode == INVERSE_ANALYZING) self.readyMoves = True semisynced(lambda s:None)(self) #=========================================================================== # Ending the game #=========================================================================== @semisynced def end (self, status, reason): if self.connected: # We currently can't fillout the comment "field" as the repr strings # for reasons and statuses lies in Main.py # Creating Status and Reason class would solve this if status == DRAW: print >> self.engine, "result 1/2-1/2 {?}" elif status == WHITEWON: print >> self.engine, "result 1-0 {?}" elif status == BLACKWON: print >> self.engine, "result 0-1 {?}" else: print >> self.engine, "result * {?}" # Make sure the engine exits and do some cleaning self.kill(reason) def kill (self, reason): """ Kills the engine, starting with the 'quit' command, then sigterm and eventually sigkill. Returns the exitcode, or if engine have already been killed, returns None """ if self.connected: self.connected = False try: try: print >> self.engine, "quit" self.returnQueue.put("del") self.engine.gentleKill() except OSError, e: # No need to raise on a hang up error, as the engine is dead # anyways if e.errno == 32: log.warn("Hung up Error", self.defname) return e.errno else: raise finally: # Clear the analyzed data, if any #self.emit("analyze", []) #=========================================================================== # Send the player move updates #=========================================================================== def setBoard (self, board): self.setBoardList([board], []) @semisynced def putMove (self, board1, move, board2): """ Sends the engine the last move made (for spectator engines). @param board1: The current board @param move: The last move made @param board2: The board before the last move was made """ # If the spactator engine analyzing an older position, let it do if self.board != board2: return self.board = board1 if not board2: self.__tellEngineToPlayCurrentColorAndMakeMove() self.movenext = False return if self.mode == INVERSE_ANALYZING: self.board = self.board.switchColor() self.__printColor() if self.engineIsInNotPlaying: print >> self.engine, "force" self.__usermove(board2, move) if self.mode == INVERSE_ANALYZING: if self.board.board.opIsChecked(): # Many engines don't like positions able to take down enemy # king. Therefore we just return the "kill king" move # automaticaly #self.emit("analyze", [([getMoveKillingKing(self.board)], MATE_VALUE-1)]) return self.__printColor() if self.engineIsInNotPlaying: print >> self.engine, "force" def makeMove (self, board1, move, board2): """ Gets a move from the engine (for player engines). @param board1: The current board @param move: The last move made @param board2: The board before the last move was made @return: The move the engine decided to make """ log.debug("makeMove: move=%s self.movenext=%s board1=%s board2=%s self.board=%s\n" % \ (move, self.movenext, board1, board2, self.board), self.defname) assert self.readyMoves self.boardLock.acquire() try: if self.board == board1 or not board2 or self.movenext: self.board = board1 self.__tellEngineToPlayCurrentColorAndMakeMove() self.movenext = False else: self.board = board1 self.__usermove(board2, move) if self.engineIsInNotPlaying: self.__tellEngineToPlayCurrentColorAndMakeMove() finally: self.boardLock.release() self.waitingForMove = True self.readyForMoveNowCommand = True # Parse outputs r = self.returnQueue.get() if r == "not ready": log.warn("Engine seems to be protover=2, but is treated as protover=1", self.defname) r = self.returnQueue.get() if r == "ready": r = self.returnQueue.get() if r == "del": raise PlayerIsDead, "Killed by foreign forces" if r == "int": raise TurnInterrupt self.waitingForMove = False self.readyForMoveNowCommand = False assert isinstance(r, Move), r return r @semisynced def updateTime (self, secs, opsecs): if self.features["time"]: print >> self.engine, "time", int(secs*100*self.timeHandicap) print >> self.engine, "otim", int(opsecs*100) #=========================================================================== # Standard options #=========================================================================== def setOptionAnalyzing (self, mode): self.mode = mode def setOptionInitialBoard (self, model): # We don't use the optionQueue here, as set board prints a whole lot of # stuff. Instead we just call it, and let semisynced handle the rest. self.setBoardList(model.boards[:], model.moves[:]) @semisynced def setBoardList (self, boards, moves): # Notice: If this method is to be called while playing, the engine will # need 'new' and an arrangement similar to that of 'pause' to avoid # the current thought move to appear self.boardLock.acquire() try: if self.mode == INVERSE_ANALYZING: self.board = self.board.switchColor() self.__printColor() self.__tellEngineToStopPlayingCurrentColor() self.__setBoard(boards[0]) self.board = boards[-1] for board, move in zip(boards[:-1], moves): self.__usermove(board, move) if self.mode in (ANALYZING, INVERSE_ANALYZING): self.board = boards[-1] if self.mode == INVERSE_ANALYZING: self.board = self.board.switchColor() self.__printColor() if self.engineIsInNotPlaying: print >> self.engine, "force" # The called of setBoardList will have to repost/analyze the # analyzer engines at this point. finally: self.boardLock.release() def setOptionVariant (self, variant): if self.features["variants"] is None: log.warn("setOptionVariant: engine doesn't support variants\n", self.defname) return if variant in variants.values() and not variant.standard_rules: assert variant.cecp_name in self.features["variants"], \ "%s doesn't support %s variant" % (self, variant.cecp_name) self.optionQueue.append("variant %s" % variant.cecp_name) #==================================================# # Strength system # #==================================================# # Strength Depth Ponder Time handicap # # 1 1 o 1,258% # # 2 2 o 1,584% # # 3 3 o 1.995% # # # # 19 o x 79,43% # # 20 o x o # #==================================================# def setOptionStrength (self, strength, forcePonderOff): self.strength = strength if strength <= 19: self.__setTimeHandicap(0.01 * 10**(strength/10.)) if strength <= 18: self.__setDepth(strength) # Crafty ofers 100 skill levels if "crafty" in self.features["myname"].lower() and strength <= 19: self.optionQueue.append("skill %s" % strength*5) self.__setPonder(strength >= 19 and not forcePonderOff) if strength == 20: self.optionQueue.append("egtb") else: self.optionQueue.append("random") def __setDepth (self, depth): self.optionQueue.append("sd %d" % depth) def __setTimeHandicap (self, timeHandicap): self.timeHandicap = timeHandicap def __setPonder (self, ponder): if ponder: self.optionQueue.append("hard") else: self.optionQueue.append("hard") self.optionQueue.append("easy") def setOptionTime (self, secs, gain): # Notice: In CECP we apply time handicap in updateTime, not in # setOptionTime. minutes = int(secs / 60) secs = int(secs % 60) s = str(minutes) if secs: s += ":" + str(secs) self.optionQueue.append("level 0 %s %d" % (s, gain)) #=========================================================================== # Option handling #=========================================================================== def setOption (self, key, value): """ Set an option, which will be sent to the engine, after the 'readyForOptions' signal has passed. If you want to know the possible options, you should go to engineDiscoverer or use the getOption, getOptions and hasOption methods, while you are in your 'readyForOptions' signal handler """ if self.readyMoves: log.warn("Options set after 'readyok' are not sent to the engine", self.defname) if key == "cores": self.optionQueue.append("cores %s" % value) elif key == "memory": self.optionQueue.append("memory %s" % value) elif key.lower() == "ponder": self.__setPonder(value==1) else: self.optionQueue.append("option %s=%s" % (key, value)) #=========================================================================== # Interacting with the player #=========================================================================== @semisynced def pause (self): """ Pauses engine using the "pause" command if available. Otherwise put engine in force mode. By the specs the engine shouldn't ponder in force mode, but some of them do so anyways. """ log.debug("pause: self=%s\n" % self, self.defname) self.engine.pause() return if self.mode in (ANALYZING, INVERSE_ANALYZING): return if self.features["pause"]: print >> self.engine, "pause" elif self.board: self.__tellEngineToStopPlayingCurrentColor() self._blockTillMove() @semisynced def resume (self): log.debug("resume: self=%s\n" % self, self.defname) self.engine.resume() return if self.mode not in (ANALYZING, INVERSE_ANALYZING): if self.features["pause"]: print "features resume" print >> self.engine, "resume" elif self.board: print "go resume" self.__tellEngineToPlayCurrentColorAndMakeMove() @semisynced def hurry (self): log.debug("hurry: self.waitingForMove=%s self.readyForMoveNowCommand=%s\n" % \ (self.waitingForMove, self.readyForMoveNowCommand), self.defname) if self.waitingForMove and self.readyForMoveNowCommand: self.__tellEngineToMoveNow() self.readyForMoveNowCommand = False @semisynced def spectatorUndoMoves (self, moves, gamemodel): log.debug("spectatorUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s\n" % \ (moves, gamemodel.ply, gamemodel.boards[-1], self.board), self.defname) if self.mode == INVERSE_ANALYZING: self.board = self.board.switchColor() self.__printColor() if self.engineIsInNotPlaying: print >> self.engine, "force" for i in xrange(moves): print >> self.engine, "undo" self.board = gamemodel.boards[-1] if self.mode == INVERSE_ANALYZING: self.board = self.board.switchColor() self.__printColor() if self.engineIsInNotPlaying: print >> self.engine, "force" @semisynced def playerUndoMoves (self, moves, gamemodel): log.debug("playerUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s\n" % \ (moves, gamemodel.ply, gamemodel.boards[-1], self.board), self.defname) if gamemodel.curplayer != self and moves % 2 == 1: # Interrupt if we were searching, but should no longer do so self.returnQueue.put("int") self.__tellEngineToStopPlayingCurrentColor() if self.board and gamemodel.status in UNFINISHED_STATES: log.debug("playerUndoMoves: self.__tellEngineToMoveNow(), self._blockTillMove()\n") self.__tellEngineToMoveNow() self._blockTillMove() for i in xrange(moves): print >> self.engine, "undo" if gamemodel.curplayer == self: self.board = gamemodel.boards[-1] self.__tellEngineToPlayCurrentColorAndMakeMove() else: self.board = None #=========================================================================== # Offer handling #=========================================================================== def offer (self, offer): if offer.type == DRAW_OFFER: if self.features["draw"]: print >> self.engine, "draw" else: #self.emit("accept", offer) def offerError (self, offer, error): if self.features["draw"]: # We don't keep track if engine draws are offers or accepts. We just # Always assume they are accepts, and if they are not, we get this # error and emit offer instead if offer.type == DRAW_OFFER and error == ACTION_ERROR_NONE_TO_ACCEPT: #self.emit("offer", Offer(DRAW_OFFER)) #=========================================================================== # Internal #=========================================================================== def __usermove (self, board, move): if self.features["usermove"]: self.engine.write("usermove ") if self.features["san"]: print >> self.engine, toSAN(board, move) else: cn = CASTLE_KK if board.variant == FISCHERRANDOMCHESS: cn = CASTLE_SAN print >> self.engine, toAN(board, move, short=True, castleNotation=cn) def __tellEngineToMoveNow (self): if self.features["sigint"]: self.engine.sigint() print >> self.engine, "?" def __tellEngineToStopPlayingCurrentColor (self): print >> self.engine, "force" self.engineIsInNotPlaying = True def __tellEngineToPlayCurrentColorAndMakeMove (self): self.__printColor() print >> self.engine, "go" self.engineIsInNotPlaying = False def __sendAnalyze (self, inverse=False): self.__tellEngineToStopPlayingCurrentColor() if inverse: self.board = self.board.setColor(1-self.color) self.__printColor() if self.engineIsInNotPlaying: print >> self.engine, "force" self.mode = INVERSE_ANALYZING else: self.mode = ANALYZING print >> self.engine, "post" print >> self.engine, "analyze" # workaround for crafty not sending analysis after it has found a mating line # http://code.google.com/p/pychess/issues/detail?id=515 if "crafty" in self.features["myname"].lower(): print >> self.engine, "noise 0" def __printColor (self): if self.features["colors"] or self.mode == INVERSE_ANALYZING: if self.board.color == WHITE: print >> self.engine, "white" else: print >> self.engine, "black" def __setBoard (self, board): if self.features["setboard"]: self.__tellEngineToStopPlayingCurrentColor() fen = board.asFen() if self.mode == INVERSE_ANALYZING: # Some engine doesn't support feature "colors" (f.e: TJchess) # so "black" and "white" command doesn't change the side to move fen_arr = fen.split() if self.board.color == WHITE: if fen_arr[1] == "b": fen_arr[1] = "w" fen = " ".join(fen_arr) else: if fen_arr[1] == "w": fen_arr[1] = "b" fen = " ".join(fen_arr) print >> self.engine, "setboard", fen else: # Kludge to set black to move, avoiding the troublesome and now # deprecated "black" command. - Equal to the one xboard uses self.__tellEngineToStopPlayingCurrentColor() if board.color == BLACK: print >> self.engine, "a2a3" print >> self.engine, "edit" print >> self.engine, "#" for color in WHITE, BLACK: for y, row in enumerate(board.data): for x, piece in enumerate(row): if not piece or piece.color != color: continue sign = reprSign[piece.sign] cord = repr(Cord(x,y)) print >> self.engine, sign+cord print >> self.engine, "c" print >> self.engine, "." def _blockTillMove (self): saved_state = self.boardLock._release_save() log.debug("_blockTillMove(): acquiring self.movecon lock\n", self.defname) self.movecon.acquire() log.debug("_blockTillMove(): self.movecon acquired\n", self.defname) try: log.debug("_blockTillMove(): doing self.movecon.wait\n", self.defname) self.movecon.wait() finally: log.debug("_blockTillMove(): releasing self.movecon..\n", self.defname) self.movecon.release() self.boardLock._acquire_restore(saved_state) #=========================================================================== # Parsing #=========================================================================== def parseLines (self, engine, lines): for line in lines: self.__parseLine(line) def __parseLine (self, line): if line[0:1] == "#": # Debug line which we shall ignore as specified in CECPv2 specs return # log.debug("__parseLine: line=\"%s\"\n" % line.strip(), self.defname) parts = whitespaces.split(line.strip()) if parts[0] == "pong": self.lastpong = int(parts[1]) return # Illegal Move if parts[0].lower().find("illegal") >= 0: log.warn("__parseLine: illegal move: line=\"%s\", board=%s" \ % (line.strip(), self.board), self.defname) if parts[-2] == "sd" and parts[-1].isdigit(): print >> self.engine, "depth", parts[-1] return # A Move (Perhaps) if self.board: if parts[0] == "move": movestr = parts[1] # Old Variation elif d_plus_dot_expr.match(parts[0]) and parts[1] == "...": movestr = parts[2] else: movestr = False if movestr: log.debug("__parseLine: acquiring self.boardLock\n", self.defname) self.waitingForMove = False self.readyForMoveNowCommand = False self.boardLock.acquire() try: if self.engineIsInNotPlaying: # If engine was set in pause just before the engine sent its # move, we ignore it. However the engine has to know that we # ignored it, and thus we step it one back log.info("__parseLine: Discarding engine's move: %s\n" % movestr, self.defname) print >> self.engine, "undo" return else: try: move = parseAny(self.board, movestr) except ParsingError, e: self.end(WHITEWON if self.board.color == BLACK else BLACKWON, WON_ADJUDICATION) return if validate(self.board, move): self.board = None self.returnQueue.put(move) return self.end(WHITEWON if self.board.color == BLACK else BLACKWON, WON_ADJUDICATION) return finally: log.debug("__parseLine(): releasing self.boardLock\n", self.defname) self.boardLock.release() self.movecon.acquire() self.movecon.notifyAll() self.movecon.release() # Analyzing if self.engineIsInNotPlaying: if parts[:4] == ["0","0","0","0"]: # Crafty doesn't analyze until it is out of book print >> self.engine, "book off" return match = anare.match(line) if match: score, moves = match.groups() if "mat" in score.lower() or "#" in moves: # Will look either like -Mat 3 or Mat3 scoreval = MATE_VALUE if score.startswith('-'): scoreval = -scoreval else: scoreval = int(score) mvstrs = movere.findall(moves) try: moves = listToMoves (self.board, mvstrs, type=None, validate=True, ignoreErrors=False) except: # Errors may happen when parsing "old" lines from # analyzing engines, which haven't yet noticed their new tasks log.debug('Ignored an "old" line from analyzer: %s\n' % mvstrs, self.defname) return # Don't emit if we weren't able to parse moves, or if we have a move # to kill the opponent king - as it confuses many engines if moves and not self.board.board.opIsChecked(): #self.emit("analyze", [(moves, scoreval)]) return # Offers draw if parts[0:2] == ["offer", "draw"]: #self.emit("accept", Offer(DRAW_OFFER)) return # Resigns if parts[0] == "resign" or \ (parts[0] == "tellics" and parts[1] == "resign"): # buggy crafty # Previously: if "resign" in parts, # however, this is too generic, since "hint", "bk", # "feature option=.." and possibly other, future CECPv2 # commands can validly contain the word "resign" without this # being an intentional resign offer. #self.emit("offer", Offer(RESIGNATION)) return #if parts[0].lower() == "error": # return #Tell User Error if parts[0] == "tellusererror": # Create a non-modal non-blocking message dialog with the error: dlg = gtk.MessageDialog(parent=None, flags=0, type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE, message_format=None) # Use the engine name if already known, otherwise the defname: displayname = self.name if not displayname: displayname = self.defname # Compose the dialog text: dlg.set_markup(gobject.markup_escape_text(_("The engine %s reports an error:") % displayname) + "\n\n" + gobject.markup_escape_text(" ".join(parts[1:]))) # handle response signal so the "Close" button works: dlg.connect("response", lambda dlg, x: dlg.destroy()) dlg.show_all() return # Tell Somebody if parts[0][:4] == "tell" and \ parts[0][4:] in ("others", "all", "ics", "icsnoalias"): log.info("Ignoring tell %s: %s\n" % (parts[0][4:], " ".join(parts[1:]))) return if "feature" in parts: # Some engines send features after done=1, so we will iterate after done=1 too done1 = False # We skip parts before 'feature', as some engines give us lines like # White (1) : feature setboard=1 analyze...e="GNU Chess 5.07" done=1 parts = parts[parts.index("feature"):] for i, pair in enumerate(parts[1:]): # As "parts" is split with no thoughs on quotes or double quotes # we need to do some extra handling. if pair.find("=") < 0: continue key, value = pair.split("=",1) if value[0] in ('"',"'") and value[-1] in ('"',"'"): value = value[1:-1] # If our pair was unfinished, like myname="GNU, we search the # rest of the pairs for a quotating mark. elif value[0] in ('"',"'"): rest = value[1:] + " " + " ".join(parts[2+i:]) i = rest.find('"') j = rest.find("'") if i + j == -2: log.warn("Missing endquotation in %s feature", self.defname) value = rest elif min(i, j) != -1: value = rest[:min(i, j)] else: l = max(i, j) value = rest[:l] elif value.isdigit(): value = int(value) if key in self.supported_features: print >> self.engine, "accepted %s" % key else: print >> self.engine, "rejected %s" % key if key == "done": if value == 1: done1 = True continue elif value == 0: log.info("Adds %d seconds timeout\n" % TIME_OUT_SECOND, self.defname) # This'll buy you some more time self.timeout = time.time()+TIME_OUT_SECOND self.returnQueue.put("not ready") return if key == "smp" and value == 1: self.options["cores"] = {"name": "cores", "type": "spin", "default": 1, "min": 1, "max": 64} elif key == "memory" and value == 1: self.options["memory"] = {"name": "memory", "type": "spin", "default": 32, "min": 1, "max": 4096} elif key == "option" and key != "done": option = self.__parse_option(value) self.options[option["name"]] = option else: self.features[key] = value if key == "myname" and not self.name: self.setName(value) if done1: # Start a new game before using the engine: # (CECPv2 engines) print >> self.engine, "new" # We are now ready for play: #self.emit("readyForOptions") #self.emit("readyForMoves") self.returnQueue.put("ready") # A hack to get better names in protover 1. # Unfortunately it wont work for now, as we don't read any lines from # protover 1 engines. When should we stop? if self.protover == 1: if self.defname[0] in ''.join(parts): basis = self.defname[0] name = ' '.join(itertools.dropwhile(lambda part: basis not in part, parts)) self.features['myname'] = name if not self.name: self.setName(name) def __parse_option(self, option): if " -check " in option: name, value = option.split(" -check ") return {"type": "check", "name": name, "default": bool(int(value))} elif " -spin " in option: name, value = option.split(" -spin ") defv, minv, maxv = value.split() return {"type": "spin", "name": name, "default": int(defv), "min": int(minv), "max": int(maxv)} elif " -slider " in option: name, value = option.split(" -slider ") defv, minv, maxv = value.split() return {"type": "spin", "name": name, "default": int(defv), "min": int(minv), "max": int(maxv)} elif " -string " in option: name, value = option.split(" -string ") return {"type": "text", "name": name, "default": value} elif " -file " in option: name, value = option.split(" -file ") return {"type": "text", "name": name, "default": value} elif " -path " in option: name, value = option.split(" -path ") return {"type": "text", "name": name, "default": value} elif " -combo " in option: name, value = option.split(" -combo ") return {"type": "combo", "name": name, "default": value} elif " -button" in option: pos = option.find(" -button") return {"type": "button", "name": option[:pos]} elif " -save" in option: pos = option.find(" -save") return {"type": "button", "name": option[:pos]} elif " -reset" in option: pos = option.find(" -reset") return {"type": "button", "name": option[:pos]} #=========================================================================== # Info #=========================================================================== def canAnalyze (self): assert self.ready, "Still waiting for done=1" return self.features["analyze"] def maxAnalysisLines (self): return 1 def requestMultiPV (self, setting): return 1 def isAnalyzing (self): return self.mode in (ANALYZING, INVERSE_ANALYZING) def __repr__ (self): if self.name: return self.name return self.features["myname"]
class UCIEngine (ProtocolEngine): def __init__ (self, subprocess, color, protover): ProtocolEngine.__init__(self, subprocess, color, protover) self.ids = {} self.options = {} self.optionsToBeSent = {} self.wtime = 60000 self.btime = 60000 self.incr = 0 self.timeHandicap = 1 self.moveLock = RLock() # none of the following variables should be changed or used in a # condition statement without holding the above self.moveLock self.pondermove = None self.ignoreNext = False self.waitingForMove = False self.needBestmove = False self.readyForStop = False # keeps track of whether we already sent a 'stop' command self.commands = collections.deque() self.board = None self.uciok = False self.returnQueue = Queue.Queue() self.engine.connect("line", self.parseLines) self.engine.connect("died", self.__die) self.connect("readyForOptions", self.__onReadyForOptions_before) self.connect_after("readyForOptions", self.__onReadyForOptions) self.connect_after("readyForMoves", self.__onReadyForMoves) def __die (self, subprocess): self.returnQueue.put("die") #=========================================================================== # Starting the game #=========================================================================== def prestart (self): print >> self.engine, "uci" def start (self): if self.mode in (ANALYZING, INVERSE_ANALYZING): pool.start(self.__startBlocking) else: self.__startBlocking() def __startBlocking (self): r = self.returnQueue.get() if r == 'die': raise PlayerIsDead assert r == "ready" or r == 'del' #self.emit("readyForOptions") #self.emit("readyForMoves") def __onReadyForOptions_before (self, self_): self.readyOptions = True def __onReadyForOptions (self, self_): if self.mode in (ANALYZING, INVERSE_ANALYZING): if self.hasOption("Ponder"): self.setOption('Ponder', False) for option, value in self.optionsToBeSent.iteritems(): if self.options[option]["default"] != value: self.options[option]["default"] = value if type(value) == bool: value = str(value).lower() print >> self.engine, "setoption name", option, "value", str(value) print >> self.engine, "isready" def __onReadyForMoves (self, self_): self.returnQueue.put("ready") self.readyMoves = True self._newGame() # If we are an analyzer, this signal was already called in a different # thread, so we can safely block it. if self.mode in (ANALYZING, INVERSE_ANALYZING): if not self.board: self.board = Board(setup=True) self.putMove(self.board, None, None) #=========================================================================== # Ending the game #=========================================================================== def end (self, status, reason): # UCI doens't care about reason, so we just kill self.kill(reason) def kill (self, reason): """ Kills the engine, starting with the 'stop' and 'quit' commands, then trying sigterm and eventually sigkill. Returns the exitcode, or if engine have already been killed, the method returns None """ if self.connected: self.connected = False try: try: print >> self.engine, "stop" print >> self.engine, "quit" self.returnQueue.put("del") return self.engine.gentleKill() except OSError, e: # No need to raise on a hang up error, as the engine is dead # anyways if e.errno == 32: log.warn("Hung up Error", self.defname) return e.errno else: raise finally: # Clear the analyzed data, if any self.emit("analyze", [], None) #=========================================================================== # Send the player move updates #=========================================================================== def putMove (self, board1, move, board2): log.debug("putMove: board1=%s move=%s board2=%s self.board=%s\n" % \ (board1, move, board2, self.board), self.defname) if not self.readyMoves: return self.board = board1 if self.mode == INVERSE_ANALYZING: self.board = self.board.switchColor() self._searchNow() def makeMove (self, board1, move, board2): log.debug("makeMove: move=%s self.pondermove=%s board1=%s board2=%s self.board=%s\n" % \ (move, self.pondermove, board1, board2, self.board), self.defname) assert self.readyMoves with self.moveLock: self.board = board1 self.waitingForMove = True ponderhit = False if board2 and self.pondermove and move == self.pondermove: ponderhit = True elif board2 and self.pondermove: self.ignoreNext = True print >> self.engine, "stop" self._searchNow(ponderhit=ponderhit) # Parse outputs try: r = self.returnQueue.get() if r == "del": raise PlayerIsDead if r == "int": with self.moveLock: self.pondermove = None self.ignoreNext = True self.needBestmove = True self.hurry() raise TurnInterrupt return r finally: with self.moveLock: self.waitingForMove = False # empty the queue of any moves received post-undo/TurnInterrupt self.returnQueue.queue.clear() def updateTime (self, secs, opsecs): if self.color == WHITE: self.wtime = int(secs*1000*self.timeHandicap) self.btime = int(opsecs*1000) else: self.btime = int(secs*1000*self.timeHandicap) self.wtime = int(opsecs*1000) #=========================================================================== # Standard options #=========================================================================== def setOptionAnalyzing (self, mode): self.mode = mode def setOptionInitialBoard (self, model): # UCI always sets the position when searching for a new game, but for # getting analyzers ready to analyze at first ply, it is good to have. self.board = model.getBoardAtPly(model.ply) pass def setOptionVariant (self, variant): if variant == FischerRandomChess: assert self.hasOption("UCI_Chess960") self.setOption("UCI_Chess960", True) def setOptionTime (self, secs, gain): self.wtime = int(max(secs*1000*self.timeHandicap, 1)) self.btime = int(max(secs*1000*self.timeHandicap, 1)) self.incr = int(gain*1000*self.timeHandicap) def setOptionStrength (self, strength): self.strength = strength if self.hasOption('UCI_LimitStrength') and strength <= 6: self.setOption('UCI_LimitStrength', True) if self.hasOption('UCI_Elo'): self.setOption('UCI_Elo', 300 * strength + 200) if not self.hasOption('UCI_Elo') or strength == 7: self.timeHandicap = th = 0.01 * 10**(strength/4.) self.wtime = int(max(self.wtime*th, 1)) self.btime = int(max(self.btime*th, 1)) self.incr = int(self.incr*th) if self.hasOption('Ponder'): self.setOption('Ponder', strength >= 7) #=========================================================================== # Interacting with the player #=========================================================================== def pause (self): self.engine.pause() return if self.board and self.board.color == self.color or \ self.mode != NORMAL or self.pondermove: self.ignoreNext = True print >> self.engine, "stop" def resume (self): self.engine.resume() return if self.mode == NORMAL: if self.board and self.board.color == self.color: self._searchNow() elif self.getOption('Ponder') and self.pondermove: self._startPonder() else: self._searchNow() def hurry (self): log.debug("hurry: self.waitingForMove=%s self.readyForStop=%s\n" % \ (self.waitingForMove, self.readyForStop), self.defname) # sending this more than once per move will crash most engines # so we need to send only the first one, and then ignore every "hurry" request # after that until there is another outstanding "position..go" with self.moveLock: if self.waitingForMove and self.readyForStop: print >> self.engine, "stop" self.readyForStop = False def playerUndoMoves (self, moves, gamemodel): log.debug("playerUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s\n" % \ (moves, gamemodel.ply, gamemodel.boards[-1], self.board), self.defname) if (gamemodel.curplayer != self and moves % 2 == 1) or \ (gamemodel.curplayer == self and moves % 2 == 0): # Interrupt if we were searching but should no longer do so, or # if it is was our move before undo and it is still our move after undo # since we need to send the engine the new FEN in makeMove() log.debug("playerUndoMoves: putting 'int' into self.returnQueue=%s\n" % \ self.returnQueue.queue, self.defname) self.returnQueue.put("int") def spectatorUndoMoves (self, moves, gamemodel): log.debug("spectatorUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s\n" % \ (moves, gamemodel.ply, gamemodel.boards[-1], self.board), self.defname) self.putMove(gamemodel.getBoardAtPly(gamemodel.ply), None, None) #=========================================================================== # Offer handling #=========================================================================== def offer (self, offer): if offer.type == DRAW_OFFER: self.emit("decline", offer) else: self.emit("accept", offer) #=========================================================================== # Option handling #=========================================================================== def setOption (self, key, value): """ Set an option, which will be sent to the engine, after the 'readyForOptions' signal has passed. If you want to know the possible options, you should go to engineDiscoverer or use the getOption, getOptions and hasOption methods, while you are in your 'readyForOptions' signal handler """ if self.readyMoves: log.warn("Options set after 'readyok' are not sent to the engine", self.defname) self.optionsToBeSent[key] = value def getOption (self, option): assert self.readyOptions if option in self.options: return self.options[option]["default"] return None def getOptions (self): assert self.readyOptions return copy(self.options) def hasOption (self, key): assert self.readyOptions return key in self.options #=========================================================================== # Internal #=========================================================================== def _newGame (self): print >> self.engine, "ucinewgame" def _searchNow (self, ponderhit=False): log.debug("_searchNow: self.needBestmove=%s ponderhit=%s self.board=%s\n" % \ (self.needBestmove, ponderhit, self.board), self.defname) with self.moveLock: commands = [] if ponderhit: commands.append("ponderhit") elif self.mode == NORMAL: commands.append("position fen %s" % self.board.asFen()) if self.strength <= 3: commands.append("go depth %d" % self.strength) else: commands.append("go wtime %d btime %d winc %d binc %d" % \ (self.wtime, self.btime, self.incr, self.incr)) else: if self.mode == INVERSE_ANALYZING: if self.board.board.opIsChecked(): # Many engines don't like positions able to take down enemy # king. Therefore we just return the "kill king" move # automaticaly self.emit("analyze", [getMoveKillingKing(self.board)], MATE_VALUE-1) return print >> self.engine, "stop" if self.board.asFen() == FEN_START: commands.append("position startpos") else: commands.append("position fen %s" % self.board.asXFen()) commands.append("go infinite") if self.needBestmove: self.commands.append(commands) log.debug("_searchNow: self.needBestmove==True, appended to self.commands=%s\n" % \ self.commands, self.defname) else: for command in commands: print >> self.engine, command if self.board.asFen() != FEN_START and getStatus(self.board)[1] != WON_MATE: self.needBestmove = True self.readyForStop = True def _startPonder (self): print >> self.engine, "position fen", self.board.asXFen(), \ "moves", toAN(self.board, self.pondermove, short=True) print >> self.engine, "go ponder wtime", self.wtime, \ "btime", self.btime, "winc", self.incr, "binc", self.incr #=========================================================================== # Parsing from engine #=========================================================================== def parseLines (self, engine, lines): for line in lines: self.__parseLine(line) def __parseLine (self, line): if not self.connected: return parts = line.split() if not parts: return #---------------------------------------------------------- Initializing if parts[0] == "id": self.ids[parts[1]] = " ".join(parts[2:]) return if parts[0] == "uciok": self.emit("readyForOptions") return if parts[0] == "readyok": self.emit("readyForMoves") return #------------------------------------------------------- Options parsing if parts[0] == "option": dic = {} last = 1 varlist = [] for i in xrange (2, len(parts)+1): if i == len(parts) or parts[i] in OPTKEYS: key = parts[last] value = " ".join(parts[last+1:i]) if "type" in dic and dic["type"] in TYPEDIC: value = TYPEDIC[dic["type"]](value) if key == "var": varlist.append(value) else: dic[key] = value last = i if varlist: dic["vars"] = varlist name = dic["name"] del dic["name"] self.options[name] = dic return #---------------------------------------------------------------- A Move if self.mode == NORMAL and parts[0] == "bestmove": with self.moveLock: self.needBestmove = False self.__sendQueuedGo() if self.ignoreNext: log.debug("__parseLine: line='%s' self.ignoreNext==True, returning\n" % \ line.strip(), self.defname) self.ignoreNext = False self.readyForStop = True return if not self.waitingForMove: log.warn("__parseLine: self.waitingForMove==False, ignoring move=%s\n" % \ parts[1], self.defname) self.pondermove = None return self.waitingForMove = False move = parseAN(self.board, parts[1]) if not validate(self.board, move): # This is critical. To avoid game stalls, we need to resign on # behalf of the engine. log.error("__parseLine: move=%s didn't validate, putting 'del' in returnQueue. self.board=%s\n" % \ (repr(move), self.board), self.defname) self.returnQueue.put('del') return self.board = self.board.move(move) log.debug("__parseLine: applied move=%s to self.board=%s\n" % \ (move, self.board), self.defname) if self.getOption('Ponder'): self.pondermove = None # An engine may send an empty ponder line, simply to clear. if len(parts) == 4 and self.board: # Engines don't always check for everything in their # ponders. Hence we need to validate. # But in some cases, what they send may not even be # correct AN - specially in the case of promotion. try: pondermove = parseAN(self.board, parts[3]) except ParsingError: pass else: if validate(self.board, pondermove): self.pondermove = pondermove self._startPonder() self.returnQueue.put(move) log.debug("__parseLine: put move=%s into self.returnQueue=%s\n" % \ (move, self.returnQueue.queue), self.defname) return #----------------------------------------------------------- An Analysis if self.mode != NORMAL and parts[0] == "info" and "pv" in parts: scoretype = parts[parts.index("score")+1] if scoretype in ('lowerbound', 'upperbound'): score = None else: score = int(parts[parts.index("score")+2]) if scoretype == 'mate': # print >> self.engine, "stop" sign = score/abs(score) score = sign * (MATE_VALUE-abs(score)) movstrs = parts[parts.index("pv")+1:] try: moves = listToMoves (self.board, movstrs, AN, validate=True, ignoreErrors=False) except ParsingError, e: # ParsingErrors may happen when parsing "old" lines from # analyzing engines, which haven't yet noticed their new tasks log.debug("__parseLine: Ignored (%s) from analyzer: ParsingError%s\n" % \ (' '.join(movstrs),e), self.defname) return self.emit("analyze", moves, score) return #----------------------------------------------- An Analyzer bestmove if self.mode != NORMAL and parts[0] == "bestmove": with self.moveLock: log.debug("__parseLine: processing analyzer bestmove='%s'\n" % \ line.strip(), self.defname) self.needBestmove = False self.__sendQueuedGo(sendlast=True) return # Stockfish complaining it received a 'stop' without a corresponding 'position..go' if line.strip() == "Unknown command: stop": with self.moveLock: log.debug("__parseLine: processing '%s'\n" % line.strip(), self.defname) self.ignoreNext = False self.needBestmove = False self.readyForStop = False self.__sendQueuedGo() return
class UCIEngine(ProtocolEngine): def __init__(self, subprocess, color, protover): ProtocolEngine.__init__(self, subprocess, color, protover) self.ids = {} self.options = {} self.optionsToBeSent = {} self.wtime = 60000 self.btime = 60000 self.incr = 0 self.timeHandicap = 1 self.moveLock = RLock() # none of the following variables should be changed or used in a # condition statement without holding the above self.moveLock self.pondermove = None self.ignoreNext = False self.waitingForMove = False self.needBestmove = False self.readyForStop = False # keeps track of whether we already sent a 'stop' command self.commands = collections.deque() self.board = None self.uciok = False self.returnQueue = Queue.Queue() self.engine.connect("line", self.parseLines) self.engine.connect("died", self.__die) self.connect("readyForOptions", self.__onReadyForOptions_before) self.connect_after("readyForOptions", self.__onReadyForOptions) self.connect_after("readyForMoves", self.__onReadyForMoves) def __die(self, subprocess): self.returnQueue.put("die") #=========================================================================== # Starting the game #=========================================================================== def prestart(self): print >> self.engine, "uci" def start(self): if self.mode in (ANALYZING, INVERSE_ANALYZING): pool.start(self.__startBlocking) else: self.__startBlocking() def __startBlocking(self): r = self.returnQueue.get() if r == 'die': raise PlayerIsDead assert r == "ready" or r == 'del' #self.emit("readyForOptions") #self.emit("readyForMoves") def __onReadyForOptions_before(self, self_): self.readyOptions = True def __onReadyForOptions(self, self_): if self.mode in (ANALYZING, INVERSE_ANALYZING): if self.hasOption("Ponder"): self.setOption('Ponder', False) for option, value in self.optionsToBeSent.iteritems(): if self.options[option]["default"] != value: self.options[option]["default"] = value if type(value) == bool: value = str(value).lower() print >> self.engine, "setoption name", option, "value", str( value) print >> self.engine, "isready" def __onReadyForMoves(self, self_): self.returnQueue.put("ready") self.readyMoves = True self._newGame() # If we are an analyzer, this signal was already called in a different # thread, so we can safely block it. if self.mode in (ANALYZING, INVERSE_ANALYZING): if not self.board: self.board = Board(setup=True) self.putMove(self.board, None, None) #=========================================================================== # Ending the game #=========================================================================== def end(self, status, reason): # UCI doens't care about reason, so we just kill self.kill(reason) def kill(self, reason): """ Kills the engine, starting with the 'stop' and 'quit' commands, then trying sigterm and eventually sigkill. Returns the exitcode, or if engine have already been killed, the method returns None """ if self.connected: self.connected = False try: try: print >> self.engine, "stop" print >> self.engine, "quit" self.returnQueue.put("del") return self.engine.gentleKill() except OSError, e: # No need to raise on a hang up error, as the engine is dead # anyways if e.errno == 32: log.warn("Hung up Error", self.defname) return e.errno else: raise finally: # Clear the analyzed data, if any self.emit("analyze", [], None) #=========================================================================== # Send the player move updates #=========================================================================== def putMove(self, board1, move, board2): log.debug("putMove: board1=%s move=%s board2=%s self.board=%s\n" % \ (board1, move, board2, self.board), self.defname) if not self.readyMoves: return self.board = board1 if self.mode == INVERSE_ANALYZING: self.board = self.board.switchColor() self._searchNow() def makeMove(self, board1, move, board2): log.debug("makeMove: move=%s self.pondermove=%s board1=%s board2=%s self.board=%s\n" % \ (move, self.pondermove, board1, board2, self.board), self.defname) assert self.readyMoves with self.moveLock: self.board = board1 self.waitingForMove = True ponderhit = False if board2 and self.pondermove and move == self.pondermove: ponderhit = True elif board2 and self.pondermove: self.ignoreNext = True print >> self.engine, "stop" self._searchNow(ponderhit=ponderhit) # Parse outputs try: r = self.returnQueue.get() if r == "del": raise PlayerIsDead if r == "int": with self.moveLock: self.pondermove = None self.ignoreNext = True self.needBestmove = True self.hurry() raise TurnInterrupt return r finally: with self.moveLock: self.waitingForMove = False # empty the queue of any moves received post-undo/TurnInterrupt self.returnQueue.queue.clear() def updateTime(self, secs, opsecs): if self.color == WHITE: self.wtime = int(secs * 1000 * self.timeHandicap) self.btime = int(opsecs * 1000) else: self.btime = int(secs * 1000 * self.timeHandicap) self.wtime = int(opsecs * 1000) #=========================================================================== # Standard options #=========================================================================== def setOptionAnalyzing(self, mode): self.mode = mode def setOptionInitialBoard(self, model): # UCI always sets the position when searching for a new game, but for # getting analyzers ready to analyze at first ply, it is good to have. self.board = model.getBoardAtPly(model.ply) pass def setOptionVariant(self, variant): if variant == FischerRandomChess: assert self.hasOption("UCI_Chess960") self.setOption("UCI_Chess960", True) def setOptionTime(self, secs, gain): self.wtime = int(max(secs * 1000 * self.timeHandicap, 1)) self.btime = int(max(secs * 1000 * self.timeHandicap, 1)) self.incr = int(gain * 1000 * self.timeHandicap) def setOptionStrength(self, strength): self.strength = strength if self.hasOption('UCI_LimitStrength') and strength <= 6: self.setOption('UCI_LimitStrength', True) if self.hasOption('UCI_Elo'): self.setOption('UCI_Elo', 300 * strength + 200) if not self.hasOption('UCI_Elo') or strength == 7: self.timeHandicap = th = 0.01 * 10**(strength / 4.) self.wtime = int(max(self.wtime * th, 1)) self.btime = int(max(self.btime * th, 1)) self.incr = int(self.incr * th) if self.hasOption('Ponder'): self.setOption('Ponder', strength >= 7) #=========================================================================== # Interacting with the player #=========================================================================== def pause(self): self.engine.pause() return if self.board and self.board.color == self.color or \ self.mode != NORMAL or self.pondermove: self.ignoreNext = True print >> self.engine, "stop" def resume(self): self.engine.resume() return if self.mode == NORMAL: if self.board and self.board.color == self.color: self._searchNow() elif self.getOption('Ponder') and self.pondermove: self._startPonder() else: self._searchNow() def hurry(self): log.debug("hurry: self.waitingForMove=%s self.readyForStop=%s\n" % \ (self.waitingForMove, self.readyForStop), self.defname) # sending this more than once per move will crash most engines # so we need to send only the first one, and then ignore every "hurry" request # after that until there is another outstanding "position..go" with self.moveLock: if self.waitingForMove and self.readyForStop: print >> self.engine, "stop" self.readyForStop = False def playerUndoMoves(self, moves, gamemodel): log.debug("playerUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s\n" % \ (moves, gamemodel.ply, gamemodel.boards[-1], self.board), self.defname) if (gamemodel.curplayer != self and moves % 2 == 1) or \ (gamemodel.curplayer == self and moves % 2 == 0): # Interrupt if we were searching but should no longer do so, or # if it is was our move before undo and it is still our move after undo # since we need to send the engine the new FEN in makeMove() log.debug("playerUndoMoves: putting 'int' into self.returnQueue=%s\n" % \ self.returnQueue.queue, self.defname) self.returnQueue.put("int") def spectatorUndoMoves(self, moves, gamemodel): log.debug("spectatorUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s\n" % \ (moves, gamemodel.ply, gamemodel.boards[-1], self.board), self.defname) self.putMove(gamemodel.getBoardAtPly(gamemodel.ply), None, None) #=========================================================================== # Offer handling #=========================================================================== def offer(self, offer): if offer.type == DRAW_OFFER: self.emit("decline", offer) else: self.emit("accept", offer) #=========================================================================== # Option handling #=========================================================================== def setOption(self, key, value): """ Set an option, which will be sent to the engine, after the 'readyForOptions' signal has passed. If you want to know the possible options, you should go to engineDiscoverer or use the getOption, getOptions and hasOption methods, while you are in your 'readyForOptions' signal handler """ if self.readyMoves: log.warn("Options set after 'readyok' are not sent to the engine", self.defname) self.optionsToBeSent[key] = value def getOption(self, option): assert self.readyOptions if option in self.options: return self.options[option]["default"] return None def getOptions(self): assert self.readyOptions return copy(self.options) def hasOption(self, key): assert self.readyOptions return key in self.options #=========================================================================== # Internal #=========================================================================== def _newGame(self): print >> self.engine, "ucinewgame" def _searchNow(self, ponderhit=False): log.debug("_searchNow: self.needBestmove=%s ponderhit=%s self.board=%s\n" % \ (self.needBestmove, ponderhit, self.board), self.defname) with self.moveLock: commands = [] if ponderhit: commands.append("ponderhit") elif self.mode == NORMAL: commands.append("position fen %s" % self.board.asFen()) if self.strength <= 3: commands.append("go depth %d" % self.strength) else: commands.append("go wtime %d btime %d winc %d binc %d" % \ (self.wtime, self.btime, self.incr, self.incr)) else: if self.mode == INVERSE_ANALYZING: if self.board.board.opIsChecked(): # Many engines don't like positions able to take down enemy # king. Therefore we just return the "kill king" move # automaticaly self.emit("analyze", [getMoveKillingKing(self.board)], MATE_VALUE - 1) return print >> self.engine, "stop" if self.board.asFen() == FEN_START: commands.append("position startpos") else: commands.append("position fen %s" % self.board.asXFen()) commands.append("go infinite") if self.needBestmove: self.commands.append(commands) log.debug("_searchNow: self.needBestmove==True, appended to self.commands=%s\n" % \ self.commands, self.defname) else: for command in commands: print >> self.engine, command if self.board.asFen() != FEN_START and getStatus( self.board)[1] != WON_MATE: self.needBestmove = True self.readyForStop = True def _startPonder(self): print >> self.engine, "position fen", self.board.asXFen(), \ "moves", toAN(self.board, self.pondermove, short=True) print >> self.engine, "go ponder wtime", self.wtime, \ "btime", self.btime, "winc", self.incr, "binc", self.incr #=========================================================================== # Parsing from engine #=========================================================================== def parseLines(self, engine, lines): for line in lines: self.__parseLine(line) def __parseLine(self, line): if not self.connected: return parts = line.split() if not parts: return #---------------------------------------------------------- Initializing if parts[0] == "id": self.ids[parts[1]] = " ".join(parts[2:]) return if parts[0] == "uciok": self.emit("readyForOptions") return if parts[0] == "readyok": self.emit("readyForMoves") return #------------------------------------------------------- Options parsing if parts[0] == "option": dic = {} last = 1 varlist = [] for i in xrange(2, len(parts) + 1): if i == len(parts) or parts[i] in OPTKEYS: key = parts[last] value = " ".join(parts[last + 1:i]) if "type" in dic and dic["type"] in TYPEDIC: value = TYPEDIC[dic["type"]](value) if key == "var": varlist.append(value) else: dic[key] = value last = i if varlist: dic["vars"] = varlist name = dic["name"] del dic["name"] self.options[name] = dic return #---------------------------------------------------------------- A Move if self.mode == NORMAL and parts[0] == "bestmove": with self.moveLock: self.needBestmove = False self.__sendQueuedGo() if self.ignoreNext: log.debug("__parseLine: line='%s' self.ignoreNext==True, returning\n" % \ line.strip(), self.defname) self.ignoreNext = False self.readyForStop = True return if not self.waitingForMove: log.warn("__parseLine: self.waitingForMove==False, ignoring move=%s\n" % \ parts[1], self.defname) self.pondermove = None return self.waitingForMove = False move = parseAN(self.board, parts[1]) if not validate(self.board, move): # This is critical. To avoid game stalls, we need to resign on # behalf of the engine. log.error("__parseLine: move=%s didn't validate, putting 'del' in returnQueue. self.board=%s\n" % \ (repr(move), self.board), self.defname) self.returnQueue.put('del') return self.board = self.board.move(move) log.debug("__parseLine: applied move=%s to self.board=%s\n" % \ (move, self.board), self.defname) if self.getOption('Ponder'): self.pondermove = None # An engine may send an empty ponder line, simply to clear. if len(parts) == 4 and self.board: # Engines don't always check for everything in their # ponders. Hence we need to validate. # But in some cases, what they send may not even be # correct AN - specially in the case of promotion. try: pondermove = parseAN(self.board, parts[3]) except ParsingError: pass else: if validate(self.board, pondermove): self.pondermove = pondermove self._startPonder() self.returnQueue.put(move) log.debug("__parseLine: put move=%s into self.returnQueue=%s\n" % \ (move, self.returnQueue.queue), self.defname) return #----------------------------------------------------------- An Analysis if self.mode != NORMAL and parts[0] == "info" and "pv" in parts: scoretype = parts[parts.index("score") + 1] if scoretype in ('lowerbound', 'upperbound'): score = None else: score = int(parts[parts.index("score") + 2]) if scoretype == 'mate': # print >> self.engine, "stop" sign = score / abs(score) score = sign * (MATE_VALUE - abs(score)) movstrs = parts[parts.index("pv") + 1:] try: moves = listToMoves(self.board, movstrs, AN, validate=True, ignoreErrors=False) except ParsingError, e: # ParsingErrors may happen when parsing "old" lines from # analyzing engines, which haven't yet noticed their new tasks log.debug("__parseLine: Ignored (%s) from analyzer: ParsingError%s\n" % \ (' '.join(movstrs),e), self.defname) return self.emit("analyze", moves, score) return #----------------------------------------------- An Analyzer bestmove if self.mode != NORMAL and parts[0] == "bestmove": with self.moveLock: log.debug("__parseLine: processing analyzer bestmove='%s'\n" % \ line.strip(), self.defname) self.needBestmove = False self.__sendQueuedGo(sendlast=True) return # Stockfish complaining it received a 'stop' without a corresponding 'position..go' if line.strip() == "Unknown command: stop": with self.moveLock: log.debug("__parseLine: processing '%s'\n" % line.strip(), self.defname) self.ignoreNext = False self.needBestmove = False self.readyForStop = False self.__sendQueuedGo() return
class CECPEngine (ProtocolEngine): def __init__ (self, subprocess, color, protover): ProtocolEngine.__init__(self, subprocess, color, protover) self.features = { "ping": 0, "setboard": 0, "playother": 0, "san": 0, "usermove": 0, "time": 1, "draw": 1, "sigint": 0, "sigterm": 0, "reuse": 0, "analyze": 0, "myname": ', '.join(self.defname), "variants": None, "colors": 1, "ics": 0, "name": 0, "pause": 0, "nps": 0, "debug": 0, "memory": 0, "smp": 0, "egt": '', } self.supported_features = [ "ping", "setboard", "san", "usermove", "time", "draw", "sigint", "analyze", "myname", "variants", "colors", "pause", "done" ] self.board = None # if self.engineIsInNotPlaying == True, engine is in "force" mode, # i.e. not thinking or playing, but still verifying move legality self.engineIsInNotPlaying = False self.movenext = False self.waitingForMove = False self.readyForMoveNowCommand = False self.timeHandicap = 1 self.lastping = 0 self.lastpong = 0 self.timeout = None self.returnQueue = Queue.Queue() self.engine.connect("line", self.parseLines) self.engine.connect("died", lambda e: self.returnQueue.put("del")) self.funcQueue = Queue.Queue() self.optionQueue = [] self.boardLock = RLock() self.undoQueue = [] self.connect("readyForOptions", self.__onReadyForOptions_before) self.connect_after("readyForOptions", self.__onReadyForOptions) self.connect_after("readyForMoves", self.__onReadyForMoves) #=========================================================================== # Starting the game #=========================================================================== def prestart (self): print >> self.engine, "xboard" if self.protover == 1: self.emit("readyForOptions") elif self.protover == 2: print >> self.engine, "protover 2" self.timeout = time.time() + TIME_OUT_FIRST def start (self): if self.mode in (ANALYZING, INVERSE_ANALYZING): pool.start(self.__startBlocking) else: self.__startBlocking() def __startBlocking (self): if self.protover == 1: self.emit("readyForMoves") if self.protover == 2: try: r = self.returnQueue.get(True, max(self.timeout-time.time(),0)) if r == "not ready": # The engine has sent done=0, and parseLine has added more # time to self.timeout r = self.returnQueue.get(True, max(self.timeout-time.time(),0)) except Queue.Empty: log.warn("Got timeout error\n", self.defname) self.emit("readyForOptions") self.emit("readyForMoves") else: if r == 'del': raise PlayerIsDead assert r == "ready" def __onReadyForOptions_before (self, self_): self.readyOptions = True def __onReadyForOptions (self, self_): # This is no longer needed #self.timeout = time.time() # Some engines has the 'post' option enabled by default, and posts a lot # of debug information. Generelly this only help to increase the log # file size, and we don't really need it. print >> self.engine, "nopost" for command in self.optionQueue: print >> self.engine, command def __onReadyForMoves (self, self_): # If we are an analyzer, this signal was already called in a different # thread, so we can safely block it. if self.mode in (ANALYZING, INVERSE_ANALYZING): if not self.board: self.board = Board(setup=True) self.__sendAnalyze(self.mode == INVERSE_ANALYZING) self.readyMoves = True semisynced(lambda s:None)(self) #=========================================================================== # Ending the game #=========================================================================== @semisynced def end (self, status, reason): if self.connected: # We currently can't fillout the comment "field" as the repr strings # for reasons and statuses lies in Main.py # Creating Status and Reason class would solve this if status == DRAW: print >> self.engine, "result 1/2-1/2 {?}" elif status == WHITEWON: print >> self.engine, "result 1-0 {?}" elif status == BLACKWON: print >> self.engine, "result 0-1 {?}" else: print >> self.engine, "result * {?}" # Make sure the engine exits and do some cleaning self.kill(reason) def kill (self, reason): """ Kills the engine, starting with the 'quit' command, then sigterm and eventually sigkill. Returns the exitcode, or if engine have already been killed, returns None """ if self.connected: self.connected = False try: try: print >> self.engine, "quit" self.returnQueue.put("del") self.engine.gentleKill() except OSError, e: # No need to raise on a hang up error, as the engine is dead # anyways if e.errno == 32: log.warn("Hung up Error", self.defname) return e.errno else: raise finally: # Clear the analyzed data, if any self.emit("analyze", [], None) #=========================================================================== # Send the player move updates #=========================================================================== @semisynced def putMove (self, board1, move, board2): """ Sends the engine the last move made (for spectator engines). @param board1: The current board @param move: The last move made @param board2: The board before the last move was made """ self.board = board1 if not board2: self.__tellEngineToPlayCurrentColorAndMakeMove() self.movenext = False return if self.mode == INVERSE_ANALYZING: self.board = self.board.switchColor() self.__printColor() if self.engineIsInNotPlaying: print >> self.engine, "force" self.__usermove(board2, move) if self.mode == INVERSE_ANALYZING: if self.board.board.opIsChecked(): # Many engines don't like positions able to take down enemy # king. Therefore we just return the "kill king" move # automaticaly self.emit("analyze", [getMoveKillingKing(self.board)], MATE_VALUE-1) return self.__printColor() if self.engineIsInNotPlaying: print >> self.engine, "force" def makeMove (self, board1, move, board2): """ Gets a move from the engine (for player engines). @param board1: The current board @param move: The last move made @param board2: The board before the last move was made @return: The move the engine decided to make """ log.debug("makeMove: move=%s self.movenext=%s board1=%s board2=%s self.board=%s\n" % \ (move, self.movenext, board1, board2, self.board), self.defname) assert self.readyMoves self.boardLock.acquire() try: if self.board == board1 or not board2 or self.movenext: self.board = board1 self.__tellEngineToPlayCurrentColorAndMakeMove() self.movenext = False else: self.board = board1 self.__usermove(board2, move) if self.engineIsInNotPlaying: self.__tellEngineToPlayCurrentColorAndMakeMove() finally: self.boardLock.release() self.waitingForMove = True self.readyForMoveNowCommand = True # Parse outputs r = self.returnQueue.get() if r == "not ready": log.warn("Engine seems to be protover=2, but is treated as protover=1", self.defname) r = self.returnQueue.get() if r == "ready": r = self.returnQueue.get() if r == "del": raise PlayerIsDead, "Killed by foreign forces" if r == "int": raise TurnInterrupt self.waitingForMove = False self.readyForMoveNowCommand = False assert isinstance(r, Move), r return r @semisynced def updateTime (self, secs, opsecs): if self.features["time"]: print >> self.engine, "time", int(secs*100*self.timeHandicap) print >> self.engine, "otim", int(opsecs*100) #=========================================================================== # Standard options #=========================================================================== def setOptionAnalyzing (self, mode): self.mode = mode def setOptionInitialBoard (self, model): # We don't use the optionQueue here, as set board prints a whole lot of # stuff. Instead we just call it, and let semisynced handle the rest. self.setBoard(model.boards[:], model.moves[:]) @semisynced def setBoard (self, boards, moves): # Notice: If this method is to be called while playing, the engine will # need 'new' and an arrangement simmilar to that of 'pause' to avoid # the current thought move to appear self.boardLock.acquire() try: self.__tellEngineToStopPlayingCurrentColor() if boards[0].asFen() != FEN_START: self.__setBoard(boards[0]) self.board = boards[-1] for board, move in zip(boards[:-1], moves): self.__usermove(board, move) if self.mode in (ANALYZING, INVERSE_ANALYZING): self.board = boards[-1] if self.mode == INVERSE_ANALYZING: self.board = self.board.switchColor() self.__printColor() if self.engineIsInNotPlaying: print >> self.engine, "force" #if self.mode in (ANALYZING, INVERSE_ANALYZING) or \ # gamemodel.boards[-1].color == self.color: # self.board = gamemodel.boards[-1] # if self.mode == ANALYZING: # self.analyze() # elif self.mode == INVERSE_ANALYZING: # self.analyze(inverse=True) # else: # self.movenext = True finally: self.boardLock.release() def setOptionVariant (self, variant): if self.features["variants"] is None: log.warn("setOptionVariant: engine doesn't support variants\n", self.defname) return if variant in variants.values() and not variant.standard_rules: assert variant.cecp_name in self.features["variants"], \ "%s doesn't support %s variant" % (self, variant.cecp_name) self.optionQueue.append("variant %s" % variant.cecp_name) #==================================================# # Strength system # #==================================================# # Strength Depth Ponder Time handicap # # Easy 1 1 o o # # 2 2 o o # # 3 3 o o # # Semi 4 5 o 10,00% # # 5 7 o 20,00% # # 6 9 o 40,00% # # Hard 7 o x 80,00% # # 8 o x o # #==================================================# def setOptionStrength (self, strength): self.strength = strength if 4 <= strength <= 7: self.__setTimeHandicap(0.1 * 2**(strength-4)) if strength <= 3: self.__setDepth(strength) elif strength <= 6: self.__setDepth(5+(strength-4)*2) self.__setPonder(strength >= 7) if strength == 8: self.optionQueue.append("egtb") else: self.optionQueue.append("random") def __setDepth (self, depth): self.optionQueue.append("sd %d" % depth) def __setTimeHandicap (self, timeHandicap): self.timeHandicap = timeHandicap def __setPonder (self, ponder): if ponder: self.optionQueue.append("hard") else: self.optionQueue.append("hard") self.optionQueue.append("easy") def setOptionTime (self, secs, gain): # Notice: In CECP we apply time handicap in updateTime, not in # setOptionTime. minutes = int(secs / 60) secs = int(secs % 60) s = str(minutes) if secs: s += ":" + str(secs) self.optionQueue.append("level 0 %s %d" % (s, gain)) #=========================================================================== # Interacting with the player #=========================================================================== @semisynced def pause (self): """ Pauses engine using the "pause" command if available. Otherwise put engine in force mode. By the specs the engine shouldn't ponder in force mode, but some of them do so anyways. """ self.engine.pause() return if self.mode in (ANALYZING, INVERSE_ANALYZING): return if self.features["pause"]: print >> self.engine, "pause" elif self.board: self.__tellEngineToStopPlayingCurrentColor() self._blockTillMove() @semisynced def resume (self): self.engine.resume() return if self.mode not in (ANALYZING, INVERSE_ANALYZING): if self.features["pause"]: print "features resume" print >> self.engine, "resume" elif self.board: print "go resume" self.__tellEngineToPlayCurrentColorAndMakeMove() @semisynced def hurry (self): log.debug("hurry: self.waitingForMove=%s self.readyForMoveNowCommand=%s\n" % \ (self.waitingForMove, self.readyForMoveNowCommand), self.defname) if self.waitingForMove and self.readyForMoveNowCommand: self.__tellEngineToMoveNow() self.readyForMoveNowCommand = False @semisynced def spectatorUndoMoves (self, moves, gamemodel): log.debug("spectatorUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s\n" % \ (moves, gamemodel.ply, gamemodel.boards[-1], self.board), self.defname) if self.mode == INVERSE_ANALYZING: self.board = self.board.switchColor() self.__printColor() if self.engineIsInNotPlaying: print >> self.engine, "force" for i in xrange(moves): print >> self.engine, "undo" self.board = gamemodel.boards[-1] if self.mode == INVERSE_ANALYZING: self.board = self.board.switchColor() self.__printColor() if self.engineIsInNotPlaying: print >> self.engine, "force" @semisynced def playerUndoMoves (self, moves, gamemodel): log.debug("playerUndoMoves: moves=%s gamemodel.ply=%s gamemodel.boards[-1]=%s self.board=%s\n" % \ (moves, gamemodel.ply, gamemodel.boards[-1], self.board), self.defname) if gamemodel.curplayer != self and moves % 2 == 1: # Interrupt if we were searching, but should no longer do so self.returnQueue.put("int") self.__tellEngineToStopPlayingCurrentColor() if self.board and gamemodel.status in UNFINISHED_STATES: log.debug("playerUndoMoves: self.__tellEngineToMoveNow(), self._blockTillMove()\n") self.__tellEngineToMoveNow() self._blockTillMove() for i in xrange(moves): print >> self.engine, "undo" if gamemodel.curplayer == self: self.board = gamemodel.boards[-1] self.__tellEngineToPlayCurrentColorAndMakeMove() else: self.board = None #=========================================================================== # Offer handling #=========================================================================== def offer (self, offer): if offer.type == DRAW_OFFER: if self.features["draw"]: print >> self.engine, "draw" else: self.emit("accept", offer) def offerError (self, offer, error): if self.features["draw"]: # We don't keep track if engine draws are offers or accepts. We just # Always assume they are accepts, and if they are not, we get this # error and emit offer instead if offer.type == DRAW_OFFER and error == ACTION_ERROR_NONE_TO_ACCEPT: self.emit("offer", Offer(DRAW_OFFER)) #=========================================================================== # Internal #=========================================================================== def __usermove (self, board, move): if self.features["usermove"]: self.engine.write("usermove ") if self.features["san"]: print >> self.engine, toSAN(board, move) else: print >> self.engine, toAN(board, move, short=True) def __tellEngineToMoveNow (self): if self.features["sigint"]: self.engine.sigint() print >> self.engine, "?" def __tellEngineToStopPlayingCurrentColor (self): print >> self.engine, "force" self.engineIsInNotPlaying = True def __tellEngineToPlayCurrentColorAndMakeMove (self): self.__printColor() print >> self.engine, "go" self.engineIsInNotPlaying = False def __sendAnalyze (self, inverse=False): self.__tellEngineToStopPlayingCurrentColor() if inverse: self.board = self.board.setColor(1-self.color) self.__printColor() if self.engineIsInNotPlaying: print >> self.engine, "force" self.mode = INVERSE_ANALYZING else: self.mode = ANALYZING print >> self.engine, "post" print >> self.engine, "analyze" # workaround for crafty not sending analysis after it has found a mating line # http://code.google.com/p/pychess/issues/detail?id=515 if "crafty" in self.features["myname"].lower(): print >> self.engine, "noise 0" def __printColor (self): if self.features["colors"] or self.mode == INVERSE_ANALYZING: if self.board.color == WHITE: print >> self.engine, "white" else: print >> self.engine, "black" def __setBoard (self, board): if self.features["setboard"]: self.__tellEngineToStopPlayingCurrentColor() print >> self.engine, "setboard", board.asFen() else: # Kludge to set black to move, avoiding the troublesome and now # deprecated "black" command. - Equal to the one xboard uses self.__tellEngineToStopPlayingCurrentColor() if board.color == BLACK: print >> self.engine, "a2a3" print >> self.engine, "edit" print >> self.engine, "#" for color in WHITE, BLACK: for y, row in enumerate(board.data): for x, piece in enumerate(row): if not piece or piece.color != color: continue sign = reprSign[piece.sign] cord = repr(Cord(x,y)) print >> self.engine, sign+cord print >> self.engine, "c" print >> self.engine, "." def _blockTillMove (self): saved_state = self.boardLock._release_save() log.debug("_blockTillMove(): acquiring self.movecon lock\n", self.defname) self.movecon.acquire() log.debug("_blockTillMove(): self.movecon acquired\n", self.defname) try: log.debug("_blockTillMove(): doing self.movecon.wait\n", self.defname) self.movecon.wait() finally: log.debug("_blockTillMove(): releasing self.movecon..\n", self.defname) self.movecon.release() self.boardLock._acquire_restore(saved_state) #=========================================================================== # Parsing #=========================================================================== def parseLines (self, engine, lines): for line in lines: self.__parseLine(line) def __parseLine (self, line): # log.debug("__parseLine: line=\"%s\"\n" % line.strip(), self.defname) parts = whitespaces.split(line.strip()) if parts[0] == "pong": self.lastpong = int(parts[1]) return # Illegal Move if parts[0].lower().find("illegal") >= 0: log.warn("__parseLine: illegal move: line=\"%s\", board=%s" \ % (line.strip(), self.board), self.defname) if parts[-2] == "sd" and parts[-1].isdigit(): print >> self.engine, "depth", parts[-1] return # A Move (Perhaps) if self.board: if parts[0] == "move": movestr = parts[1] # Old Variation elif d_plus_dot_expr.match(parts[0]) and parts[1] == "...": movestr = parts[2] else: movestr = False if movestr: log.debug("__parseLine: acquiring self.boardLock\n", self.defname) self.waitingForMove = False self.readyForMoveNowCommand = False self.boardLock.acquire() try: if self.engineIsInNotPlaying: # If engine was set in pause just before the engine sent its # move, we ignore it. However the engine has to know that we # ignored it, and thus we step it one back log.log("__parseLine: Discarding engine's move: %s\n" % movestr, self.defname) print >> self.engine, "undo" return else: try: move = parseAny(self.board, movestr) except ParsingError, e: raise PlayerIsDead, e if validate(self.board, move): self.board = None self.returnQueue.put(move) return raise PlayerIsDead, "Board didn't validate after move" finally: log.debug("__parseLine(): releasing self.boardLock\n", self.defname) self.boardLock.release() self.movecon.acquire() self.movecon.notifyAll() self.movecon.release() # Analyzing if self.engineIsInNotPlaying: if parts[:4] == ["0","0","0","0"]: # Crafty doesn't analyze until it is out of book print >> self.engine, "book off" return match = anare.match(line) if match: score, moves = match.groups() if "mat" in score.lower(): # Will look either like -Mat 3 or Mat3 scoreval = MATE_VALUE - int("".join(c for c in score if c.isdigit())) if score.startswith('-'): scoreval = -scoreval else: scoreval = int(score) mvstrs = movere.findall(moves) try: moves = listToMoves (self.board, mvstrs, type=None, validate=True, ignoreErrors=False) except ParsingError, e: # ParsingErrors may happen when parsing "old" lines from # analyzing engines, which haven't yet noticed their new tasks log.debug("Ignored a line from analyzer: ParsingError%s\n" % e, self.defname) return # Don't emit if we weren't able to parse moves, or if we have a move # to kill the opponent king - as it confuses many engines if moves and not self.board.board.opIsChecked(): self.emit("analyze", moves, scoreval) return # Offers draw if parts[0:2] == ["offer", "draw"]: self.emit("accept", Offer(DRAW_OFFER)) return # Resigns if "resign" in parts: self.emit("offer", Offer(RESIGNATION)) return #if parts[0].lower() == "error": # return #Tell User Error if parts[0] == "tellusererror": log.warn("Ignoring tellusererror: %s\n" % " ".join(parts[1:])) return # Tell Somebody if parts[0][:4] == "tell" and \ parts[0][4:] in ("others", "all", "ics", "icsnoalias"): # Crafty sometimes only resign to ics :S #if parts[1] == "resign": # self.emit("offer", Offer(RESIGNATION)) # log.warn("Interpreted tellics as a wish to resign") #else: log.log("Ignoring tell %s: %s\n" % (parts[0][4:], " ".join(parts[1:]))) return if "feature" in parts: # We skip parts before 'feature', as some engines give us lines like # White (1) : feature setboard=1 analyze...e="GNU Chess 5.07" done=1 parts = parts[parts.index("feature"):] for i, pair in enumerate(parts[1:]): # As "parts" is split with no thoughs on quotes or double quotes # we need to do some extra handling. if pair.find("=") < 0: continue key, value = pair.split("=",1) if value[0] in ('"',"'") and value[-1] in ('"',"'"): value = value[1:-1] # If our pair was unfinished, like myname="GNU, we search the # rest of the pairs for a quotating mark. elif value[0] in ('"',"'"): rest = value[1:] + " " + " ".join(parts[2+i:]) i = rest.find('"') j = rest.find("'") if i + j == -2: log.warn("Missing endquotation in %s feature", self.defname) value = rest elif min(i, j) != -1: value = rest[:min(i, j)] else: l = max(i, j) value = rest[:l] else: # All nonquoted values are ints value = int(value) if key in self.supported_features: print >> self.engine, "accepted %s" % key else: print >> self.engine, "rejected %s" % key if key == "done": if value == 1: self.emit("readyForOptions") self.emit("readyForMoves") self.returnQueue.put("ready") elif value == 0: log.log("Adds %d seconds timeout\n" % TIME_OUT_SECOND, self.defname) # This'll buy you some more time self.timeout = time.time()+TIME_OUT_SECOND self.returnQueue.put("not ready") return self.features[key] = value # A hack to get better names in protover 1. # Unfortunately it wont work for now, as we don't read any lines from # protover 1 engines. When should we stop? if self.protover == 1: if self.defname[0] in ''.join(parts): basis = self.defname[0] name = ' '.join(itertools.dropwhile(lambda part: basis not in part, parts)) self.features['myname'] = name