Example #1
0
 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.")
Example #2
0
 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.")
Example #3
0
    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.")
Example #4
0
 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.")
Example #5
0
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"]
Example #6
0
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)
Example #7
0
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"]
Example #8
0
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)
Example #9
0
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
Example #11
0
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