1import asyncio
2import collections
3
4from pychess.compat import create_task
5from pychess.Utils import wait_signal
6from pychess.Utils.Move import parseAny
7from pychess.Utils.Board import Board
8from pychess.Utils.Move import toAN
9from pychess.Utils.logic import validate, getMoveKillingKing, getStatus, legalMoveCount
10from pychess.Utils.const import CASTLE_KK, ANALYZING, WON_ADJUDICATION, FISCHERRANDOMCHESS, \
11    INVERSE_ANALYZING, CASTLE_KR, NORMALCHESS, FEN_START, WHITE, NORMAL, DRAW_OFFER, \
12    WON_MATE, BLACK, BLACKWON, WHITEWON
13from pychess.Utils.lutils.ldata import MATE_VALUE
14from pychess.Utils.lutils.lmove import ParsingError
15from pychess.System import conf
16from pychess.System.Log import log
17from pychess.Variants.fischerandom import FischerandomBoard
18
19from .ProtocolEngine import ProtocolEngine, TIME_OUT_SECOND
20from pychess.Players.Player import PlayerIsDead, TurnInterrupt, InvalidMove
21
22TYPEDIC = {"check": lambda x: x == "true", "spin": int}
23OPTKEYS = ("name", "type", "min", "max", "default", "var")
24
25
26class UCIEngine(ProtocolEngine):
27    def __init__(self, subprocess, color, protover, md5):
28        ProtocolEngine.__init__(self, subprocess, color, protover, md5)
29
30        self.ids = {}
31        self.options = {}
32        self.optionsToBeSent = {}
33
34        self.wtime = 60000
35        self.btime = 60000
36        self.incr = 0
37        self.moves = 0
38        self.timeHandicap = 1
39
40        self.ponderOn = False
41        self.pondermove = None
42        self.ignoreNext = False
43        self.waitingForMove = False
44        self.needBestmove = False
45        self.bestmove_event = asyncio.Event()
46        self.readyForStop = False  # keeps track of whether we already sent a 'stop' command
47        self.multipvSetting = 1  # MultiPV option sent to the engine
48        self.multipvExpected = 1  # Number of PVs expected (limited by number of legal moves)
49        self.commands = collections.deque()
50
51        self.gameBoard = Board(setup=True)  # board at the end of all moves played
52        self.board = Board(setup=True)  # board to send the engine
53        self.uciPosition = "startpos"
54        self.uciPositionListsMoves = False
55        self.analysis = [None]
56        self.analysis_depth = None
57
58        self.queue = asyncio.Queue()
59        self.parse_line_task = create_task(self.parseLine(self.engine))
60        self.died_cid = self.engine.connect("died", lambda e: self.queue.put_nowait("die"))
61        self.invalid_move = None
62
63        self.cids = [
64            self.connect_after("readyForOptions", self.__onReadyForOptions),
65            self.connect_after("readyForMoves", self.__onReadyForMoves),
66        ]
67
68    # Starting the game
69
70    def prestart(self):
71        print("uci", file=self.engine)
72
73    def start(self, event, is_dead):
74        create_task(self.__startBlocking(event, is_dead))
75
76    @asyncio.coroutine
77    def __startBlocking(self, event, is_dead):
78        try:
79            return_value = yield from asyncio.wait_for(self.queue.get(), TIME_OUT_SECOND)
80        except asyncio.TimeoutError:
81            log.warning("Got timeout error", extra={"task": self.defname})
82            is_dead.add(True)
83        except Exception:
84            log.warning("Unknown error", extra={"task": self.defname})
85            is_dead.add(True)
86        else:
87            if return_value == 'die':
88                is_dead.add(True)
89            assert return_value == "ready" or return_value == "del"
90
91        if event is not None:
92            event.set()
93
94    def __onReadyForOptions(self, self_):
95        analyze_mode = self.mode in (ANALYZING, INVERSE_ANALYZING)
96        if analyze_mode:
97            if self.hasOption("Ponder"):
98                self.setOption('Ponder', False)
99            if self.hasOption("UCI_LimitStrength"):
100                self.setOption('UCI_LimitStrength', False)
101        if self.hasOption("UCI_AnalyseMode"):
102            self.setOption("UCI_AnalyseMode", analyze_mode)
103
104        for option, value in self.optionsToBeSent.items():
105            if option == "MultiPV" and not analyze_mode:
106                continue
107            if isinstance(value, bool):
108                value = str(value).lower()
109            print("setoption name %s value %s" % (option, str(value)),
110                  file=self.engine)
111
112        print("isready", file=self.engine)
113
114    def __onReadyForMoves(self, self_):
115        self.readyMoves = True
116        self.queue.put_nowait("ready")
117        self._newGame()
118
119        if self.isAnalyzing():
120            self._searchNow()
121
122    # Ending the game
123
124    def end(self, status, reason):
125        self.parse_line_task.cancel()
126        if self.engine.handler_is_connected(self.died_cid):
127            self.engine.disconnect(self.died_cid)
128        if self.handler_is_connected(self.analyze_cid):
129            self.disconnect(self.analyze_cid)
130        for cid in self.cids:
131            if self.handler_is_connected(cid):
132                self.disconnect(cid)
133        self.board = None
134        self.gameBoard = None
135
136        if self.connected:
137            # UCI doens't care about reason, so we just kill
138            if reason == WON_ADJUDICATION:
139                self.queue.put_nowait("invalid")
140            self.kill(reason)
141
142    def kill(self, reason):
143        """ Kills the engine, starting with the 'stop' and 'quit' commands, then
144            trying sigterm and eventually sigkill.
145            Returns the exitcode, or if engine have already been killed, the
146            method returns None """
147        if self.connected:
148            self.connected = False
149            try:
150                try:
151                    print("stop", file=self.engine)
152                    print("quit", file=self.engine)
153                    self.queue.put_nowait("del")
154                    self.engine.terminate()
155
156                except OSError as e:
157                    # No need to raise on a hang up error, as the engine is dead
158                    # anyways
159                    if e.errno == 32:
160                        log.warning("Hung up Error", extra={"task": self.defname})
161                        return e.errno
162                    else:
163                        raise
164
165            finally:
166                # Clear the analyzed data, if any
167                self.emit("analyze", [])
168
169    # Send the player move updates
170
171    def _moveToUCI(self, board, move):
172        castle_notation = CASTLE_KK
173        if board.variant == FISCHERRANDOMCHESS:
174            castle_notation = CASTLE_KR
175        return toAN(board, move, short=True, castleNotation=castle_notation)
176
177    def _recordMove(self, board1, move, board2):
178        if self.gameBoard == board1:
179            return
180        if not board2:
181            if board1.variant == NORMALCHESS and board1.asFen() == FEN_START:
182                self.uciPosition = "startpos"
183            else:
184                self.uciPosition = "fen " + board1.asFen()
185            self.uciPositionListsMoves = False
186        if move:
187            if not self.uciPositionListsMoves:
188                self.uciPosition += " moves"
189                self.uciPositionListsMoves = True
190            self.uciPosition += " " + self._moveToUCI(board2, move)
191
192        self.board = self.gameBoard = board1
193        if self.mode == INVERSE_ANALYZING:
194            self.board = self.gameBoard.switchColor()
195
196    def _recordMoveList(self, model, ply=None):
197        self._recordMove(model.boards[0], None, None)
198        if ply is None:
199            ply = model.ply
200        for board1, move, board2 in zip(model.boards[1:ply + 1], model.moves,
201                                        model.boards[0:ply]):
202            self._recordMove(board1, move, board2)
203
204    def set_board(self, board):
205        self._recordMove(board, None, None)
206
207    def setBoard(self, board, search=True):
208        log.debug("setBoardAtPly: board=%s" % board,
209                  extra={"task": self.defname})
210        if not self.readyMoves:
211            return
212
213        @asyncio.coroutine
214        def coro():
215            if self.needBestmove:
216                self.bestmove_event.clear()
217                print("stop", file=self.engine)
218                yield from self.bestmove_event.wait()
219
220            self._recordMove(board, None, None)
221            if search:
222                self._searchNow()
223        create_task(coro())
224
225    def putMove(self, board1, move, board2):
226        log.debug("putMove: board1=%s move=%s board2=%s self.board=%s" % (
227            board1, move, board2, self.board), extra={"task": self.defname})
228        if not self.readyMoves:
229            return
230
231        @asyncio.coroutine
232        def coro():
233            if self.needBestmove:
234                self.bestmove_event.clear()
235                print("stop", file=self.engine)
236                yield from self.bestmove_event.wait()
237
238            self._recordMove(board1, move, board2)
239            if not self.analyzing_paused:
240                self._searchNow()
241        create_task(coro())
242
243    @asyncio.coroutine
244    def makeMove(self, board1, move, board2):
245        log.debug("makeMove: move=%s self.pondermove=%s board1=%s board2=%s self.board=%s" % (
246            move, self.pondermove, board1, board2, self.board), extra={"task": self.defname})
247        assert self.readyMoves
248
249        self._recordMove(board1, move, board2)
250        self.waitingForMove = True
251        ponderhit = False
252
253        if board2 and self.pondermove and move == self.pondermove:
254            ponderhit = True
255        elif board2 and self.pondermove:
256            self.ignoreNext = True
257            print("stop", file=self.engine)
258
259        self._searchNow(ponderhit=ponderhit)
260
261        # Parse outputs
262        try:
263            return_queue = yield from self.queue.get()
264            if return_queue == "invalid":
265                raise InvalidMove
266            if return_queue == "del" or return_queue == "die":
267                raise PlayerIsDead
268            if return_queue == "int":
269                self.pondermove = None
270                self.ignoreNext = True
271                self.needBestmove = True
272                self.hurry()
273                raise TurnInterrupt
274            return return_queue
275        finally:
276            self.waitingForMove = False
277
278    def updateTime(self, secs, opsecs):
279        if self.color == WHITE:
280            self.wtime = int(secs * 1000 * self.timeHandicap)
281            self.btime = int(opsecs * 1000)
282        else:
283            self.btime = int(secs * 1000 * self.timeHandicap)
284            self.wtime = int(opsecs * 1000)
285
286    # Standard options
287
288    def setOptionAnalyzing(self, mode):
289        self.mode = mode
290        if self.mode == INVERSE_ANALYZING:
291            self.board = self.gameBoard.switchColor()
292
293    def setOptionInitialBoard(self, model):
294        log.debug("setOptionInitialBoard: self=%s, model=%s" % (
295            self, model), extra={"task": self.defname})
296        self._recordMoveList(model)
297
298    def setOptionVariant(self, variant):
299        if variant == FischerandomBoard:
300            assert self.hasOption("UCI_Chess960")
301            self.setOption("UCI_Chess960", True)
302        elif self.hasOption("UCI_Variant") and not variant.standard_rules:
303            self.setOption("UCI_Variant", variant.cecp_name)
304
305    def setOptionTime(self, secs, gain, moves):
306        self.wtime = int(max(secs * 1000 * self.timeHandicap, 1))
307        self.btime = int(max(secs * 1000 * self.timeHandicap, 1))
308        self.incr = int(gain * 1000 * self.timeHandicap)
309        self.moves = moves
310
311    def setOptionStrength(self, strength, forcePonderOff):
312        self.strength = strength
313
314        # Restriction by embedded ELO evaluation (Stockfish, Arasan, Rybka, CT800, Spike...)
315        if self.hasOption('UCI_LimitStrength') and strength <= 18:
316            self.setOption('UCI_LimitStrength', True)
317            if self.hasOption('UCI_Elo'):
318                try:
319                    minElo = int(self.options["UCI_Elo"]["min"])
320                except Exception:
321                    minElo = 1000
322                try:
323                    maxElo = int(self.options["UCI_Elo"]["max"])
324                except Exception:
325                    maxElo = 2800
326                self.setOption('UCI_Elo', int(minElo + strength * (maxElo - minElo) / 20))
327
328        # Restriction by unofficial option "Skill Level" (Stockfish, anticrux...)
329        if self.hasOption('Skill Level'):
330            self.setOption('Skill Level', strength)
331
332        # Restriction by available time
333        if (not self.hasOption('UCI_Elo') and not self.hasOption('Skill Level')) or strength <= 19:
334            self.timeHandicap = t_hcap = 0.01 * 10 ** (strength / 10.)
335            self.wtime = int(max(self.wtime * t_hcap, 1))
336            self.btime = int(max(self.btime * t_hcap, 1))
337            self.incr = int(self.incr * t_hcap)
338
339        # Amplification with pondering
340        if self.hasOption('Ponder'):
341            self.setOption('Ponder', strength >= 19 and not forcePonderOff)
342
343        # Amplification by endgame database
344        if self.hasOption('GaviotaTbPath') and strength == 20:
345            self.setOption('GaviotaTbPath', conf.get("egtb_path"))
346
347    # Interacting with the player
348
349    def pause(self):
350        log.debug("pause: self=%s" % self, extra={"task": self.defname})
351        if self.isAnalyzing():
352            print("stop", file=self.engine)
353            self.readyForStop = False
354            self.analyzing_paused = True
355        else:
356            self.engine.pause()
357        return
358
359    def resume(self):
360        log.debug("resume: self=%s" % self, extra={"task": self.defname})
361        if self.isAnalyzing():
362            self._searchNow()
363            self.analyzing_paused = False
364        else:
365            self.engine.resume()
366        return
367
368    def hurry(self):
369        log.debug("hurry: self.waitingForMove=%s self.readyForStop=%s" % (
370            self.waitingForMove, self.readyForStop), extra={"task": self.defname})
371        # sending this more than once per move will crash most engines
372        # so we need to send only the first one, and then ignore every "hurry" request
373        # after that until there is another outstanding "position..go"
374        if self.waitingForMove and self.readyForStop:
375            print("stop", file=self.engine)
376            self.readyForStop = False
377
378    def playerUndoMoves(self, moves, gamemodel):
379        log.debug("playerUndoMoves: moves=%s \
380                  gamemodel.ply=%s \
381                  gamemodel.boards[-1]=%s \
382                  self.board=%s" % (moves, gamemodel.ply,
383                                    gamemodel.boards[-1],
384                                    self.board), extra={"task": self.defname})
385
386        self._recordMoveList(gamemodel)
387
388        if (gamemodel.curplayer != self and moves % 2 == 1) or \
389                (gamemodel.curplayer == self and moves % 2 == 0):
390            # Interrupt if we were searching but should no longer do so, or
391            # if it is was our move before undo and it is still our move after undo
392            # since we need to send the engine the new FEN in makeMove()
393            log.debug("playerUndoMoves: putting 'int' into self.queue=%s" %
394                      self.queue, extra={"task": self.defname})
395            self.queue.put_nowait("int")
396
397    def spectatorUndoMoves(self, moves, gamemodel):
398        if self.analyzing_paused:
399            return
400
401        log.debug("spectatorUndoMoves: moves=%s \
402                  gamemodel.ply=%s \
403                  gamemodel.boards[-1]=%s \
404                  self.board=%s" % (moves, gamemodel.ply, gamemodel.boards[-1],
405                                    self.board), extra={"task": self.defname})
406
407        self._recordMoveList(gamemodel)
408
409        if self.readyMoves:
410            self._searchNow()
411
412    # Offer handling
413
414    def offer(self, offer):
415        if offer.type == DRAW_OFFER:
416            self.emit("decline", offer)
417        else:
418            self.emit("accept", offer)
419
420    # Option handling
421
422    def setOption(self, key, value):
423        """ Set an option, which will be sent to the engine, after the
424            'readyForOptions' signal has passed.
425            If you want to know the possible options, you should go to
426            engineDiscoverer or use the hasOption method
427            while you are in your 'readyForOptions' signal handler """
428        if self.readyMoves:
429            log.warning(
430                "Options set after 'readyok' are not sent to the engine",
431                extra={"task": self.defname})
432        self.optionsToBeSent[key] = value
433        self.ponderOn = key == "Ponder" and value is True
434        if key == "MultiPV":
435            self.multipvSetting = int(value)
436
437    def hasOption(self, key):
438        return key in self.options
439
440    # Internal
441
442    def _newGame(self):
443        print("ucinewgame", file=self.engine)
444
445    def _searchNow(self, ponderhit=False):
446        log.debug("_searchNow: self.needBestmove=%s ponderhit=%s self.board=%s" % (
447            self.needBestmove, ponderhit, self.board), extra={"task": self.defname})
448
449        commands = []
450
451        if ponderhit:
452            commands.append("ponderhit")
453
454        elif self.mode == NORMAL:
455            commands.append("position %s" % self.uciPosition)
456            if self.strength <= 3:
457                commands.append("go depth %d" % self.strength)
458            else:
459                if self.moves > 0:
460                    commands.append("go wtime %d winc %d btime %d binc %d movestogo %s" % (
461                                    self.wtime, self.incr, self.btime, self.incr, self.moves))
462                else:
463                    commands.append("go wtime %d winc %d btime %d binc %d" % (
464                                    self.wtime, self.incr, self.btime, self.incr))
465
466        else:
467            print("stop", file=self.engine)
468
469            if self.mode == INVERSE_ANALYZING:
470                if self.board.board.opIsChecked():
471                    # Many engines don't like positions able to take down enemy
472                    # king. Therefore we just return the "kill king" move
473                    # automaticaly
474                    self.emit("analyze", [(self.board.ply, [toAN(
475                        self.board, getMoveKillingKing(self.board))], MATE_VALUE - 1, "1", "")])
476                    return
477                commands.append("position fen %s" % self.board.asFen())
478            else:
479                commands.append("position %s" % self.uciPosition)
480
481            if self.analysis_depth is not None:
482                commands.append("go depth %s" % self.analysis_depth)
483            elif conf.get("infinite_analysis"):
484                commands.append("go infinite")
485            else:
486                move_time = int(conf.get("max_analysis_spin")) * 1000
487                commands.append("go movetime %s" % move_time)
488
489        if self.hasOption("MultiPV") and self.multipvSetting > 1:
490            self.multipvExpected = min(self.multipvSetting,
491                                       legalMoveCount(self.board))
492        else:
493            self.multipvExpected = 1
494        self.analysis = [None] * self.multipvExpected
495
496        if self.needBestmove:
497            self.commands.append(commands)
498            log.debug("_searchNow: self.needBestmove==True, appended to self.commands=%s" %
499                      self.commands, extra={"task": self.defname})
500        else:
501            for command in commands:
502                print(command, file=self.engine)
503            if getStatus(self.board)[
504                    1] != WON_MATE:  # XXX This looks fishy.
505                self.needBestmove = True
506                self.readyForStop = True
507
508    def _startPonder(self):
509        uciPos = self.uciPosition
510        if not self.uciPositionListsMoves:
511            uciPos += " moves"
512        print("position", uciPos, self._moveToUCI(self.board, self.pondermove), file=self.engine)
513        print("go ponder wtime", self.wtime, "winc", self.incr, "btime", self.btime, "binc",
514              self.incr, file=self.engine)
515
516    # Parsing from engine
517
518    @asyncio.coroutine
519    def parseLine(self, proc):
520        while True:
521            line = yield from wait_signal(proc, 'line')
522            if not line:
523                break
524            else:
525                line = line[1]
526                parts = line.split()
527                if not parts:
528                    continue
529                # Initializing
530                if parts[0] == "id":
531                    if parts[1] == "name":
532                        self.ids[parts[1]] = " ".join(parts[2:])
533                        self.setName(self.ids["name"])
534                    continue
535
536                if parts[0] == "uciok":
537                    self.emit("readyForOptions")
538                    continue
539
540                if parts[0] == "readyok":
541                    self.emit("readyForMoves")
542                    continue
543
544                # Options parsing
545                if parts[0] == "option":
546                    dic = {}
547                    last = 1
548                    varlist = []
549                    for i in range(2, len(parts) + 1):
550                        if i == len(parts) or parts[i] in OPTKEYS:
551                            key = parts[last]
552                            value = " ".join(parts[last + 1:i])
553                            if "type" in dic and dic["type"] in TYPEDIC:
554                                value = TYPEDIC[dic["type"]](value)
555
556                            if key == "var":
557                                varlist.append(value)
558                            elif key == "type" and value == "string":
559                                dic[key] = "text"
560                            else:
561                                dic[key] = value
562
563                            last = i
564                    if varlist:
565                        dic["choices"] = varlist
566
567                    if "name" in dic:
568                        self.options[dic["name"]] = dic
569                    continue
570
571                # A Move
572                if self.mode == NORMAL and parts[0] == "bestmove":
573                    self.needBestmove = False
574                    self.bestmove_event.set()
575                    self.__sendQueuedGo()
576
577                    if self.ignoreNext:
578                        log.debug(
579                            "__parseLine: line='%s' self.ignoreNext==True, returning" % line.strip(),
580                            extra={"task": self.defname})
581                        self.ignoreNext = False
582                        self.readyForStop = True
583                        continue
584
585                    movestr = parts[1]
586                    if not self.waitingForMove:
587                        log.warning("__parseLine: self.waitingForMove==False, ignoring move=%s" % movestr,
588                                    extra={"task": self.defname})
589                        self.pondermove = None
590                        continue
591                    self.waitingForMove = False
592
593                    try:
594                        move = parseAny(self.board, movestr)
595                    except ParsingError:
596                        self.invalid_move = movestr
597                        log.info(
598                            "__parseLine: ParsingError engine move: %s %s"
599                            % (movestr, self.board),
600                            extra={"task": self.defname})
601                        self.end(WHITEWON if self.board.color == BLACK else
602                                 BLACKWON, WON_ADJUDICATION)
603                        continue
604
605                    if not validate(self.board, move):
606                        # This is critical. To avoid game stalls, we need to resign on
607                        # behalf of the engine.
608                        log.error("__parseLine: move=%s didn't validate, putting 'del' \
609                                  in returnQueue. self.board=%s" % (
610                            repr(move), self.board), extra={"task": self.defname})
611                        self.invalid_move = movestr
612                        self.end(WHITEWON if self.board.color == BLACK else
613                                 BLACKWON, WON_ADJUDICATION)
614                        continue
615
616                    self._recordMove(self.board.move(move), move, self.board)
617                    log.debug("__parseLine: applied move=%s to self.board=%s" % (
618                        move, self.board), extra={"task": self.defname})
619
620                    if self.ponderOn:
621                        self.pondermove = None
622                        # An engine may send an empty ponder line, simply to clear.
623                        if len(parts) == 4:
624                            # Engines don't always check for everything in their
625                            # ponders. Hence we need to validate.
626                            # But in some cases, what they send may not even be
627                            # correct AN - specially in the case of promotion.
628                            try:
629                                pondermove = parseAny(self.board, parts[3])
630                            except ParsingError:
631                                pass
632                            else:
633                                if validate(self.board, pondermove):
634                                    self.pondermove = pondermove
635                                    self._startPonder()
636
637                    self.queue.put_nowait(move)
638                    log.debug("__parseLine: put move=%s into self.queue=%s" % (
639                        move, self.queue), extra={"task": self.defname})
640                    continue
641
642                # An Analysis
643                if self.mode != NORMAL and parts[0] == "info" and "pv" in parts:
644                    multipv = 1
645                    if "multipv" in parts:
646                        multipv = int(parts[parts.index("multipv") + 1])
647                    scoretype = parts[parts.index("score") + 1]
648                    if scoretype in ('lowerbound', 'upperbound'):
649                        score = None
650                    else:
651                        score = int(parts[parts.index("score") + 2])
652                        if scoretype == 'mate':
653                            #                    print >> self.engine, "stop"
654                            if score != 0:
655                                sign = score / abs(score)
656                                score = sign * (MATE_VALUE - abs(score))
657
658                    movstrs = parts[parts.index("pv") + 1:]
659
660                    if "depth" in parts:
661                        depth = parts[parts.index("depth") + 1]
662                    else:
663                        depth = ""
664
665                    if "nps" in parts:
666                        nps = parts[parts.index("nps") + 1]
667                    else:
668                        nps = ""
669
670                    if multipv <= len(self.analysis):
671                        self.analysis[multipv - 1] = (self.board.ply, movstrs, score, depth, nps)
672                    self.emit("analyze", self.analysis)
673                    continue
674
675                # An Analyzer bestmove
676                if self.mode != NORMAL and parts[0] == "bestmove":
677                    log.debug("__parseLine: processing analyzer bestmove='%s'" % line.strip(),
678                              extra={"task": self.defname})
679                    self.needBestmove = False
680                    self.bestmove_event.set()
681                    if parts[1] == "(none)":
682                        self.emit("analyze", [])
683                    else:
684                        self.__sendQueuedGo(sendlast=True)
685                    continue
686
687                # Stockfish complaining it received a 'stop' without a corresponding 'position..go'
688                if line.strip() == "Unknown command: stop":
689                    log.debug("__parseLine: processing '%s'" % line.strip(),
690                              extra={"task": self.defname})
691                    self.ignoreNext = False
692                    self.needBestmove = False
693                    self.readyForStop = False
694                    self.__sendQueuedGo()
695                    continue
696
697                # * score
698                # * cp <x>
699                #    the score from the engine's point of view in centipawns.
700                # * mate <y>
701                #    mate in y moves, not plies.
702                #    If the engine is getting mated use negative values for y.
703                # * lowerbound
704                #  the score is just a lower bound.
705                # * upperbound
706                #   the score is just an upper bound.
707
708    def __sendQueuedGo(self, sendlast=False):
709        """ Sends the next position...go or ponderhit command set which was queued (if any).
710
711        sendlast -- If True, send the last position-go queued rather than the first,
712        and discard the others (intended for analyzers)
713        """
714        if len(self.commands) > 0:
715            if sendlast:
716                commands = self.commands.pop()
717                self.commands.clear()
718            else:
719                commands = self.commands.popleft()
720
721            for command in commands:
722                print(command, file=self.engine)
723            self.needBestmove = True
724            self.readyForStop = True
725            log.debug("__sendQueuedGo: sent queued go=%s" % commands,
726                      extra={"task": self.defname})
727
728    #    Info
729
730    def getAnalysisLines(self):
731        try:
732            return int(self.optionsToBeSent["MultiPV"])
733        except (KeyError, ValueError):
734            return 1  # Engine does not support the MultiPV option
735
736    def minAnalysisLines(self):
737        try:
738            return int(self.options["MultiPV"]["min"])
739        except (KeyError, ValueError):
740            return 1  # Engine does not support the MultiPV option
741
742    def maxAnalysisLines(self):
743        try:
744            return int(self.options["MultiPV"]["max"])
745        except (KeyError, ValueError):
746            return 1  # Engine does not support the MultiPV option
747
748    def requestMultiPV(self, n):
749        n = min(n, self.maxAnalysisLines())
750        n = max(n, self.minAnalysisLines())
751        if n != self.multipvSetting:
752            self.multipvSetting = n
753            print("stop", file=self.engine)
754            print("setoption name MultiPV value %s" % n, file=self.engine)
755            self._searchNow()
756        return n
757
758    def __repr__(self):
759        if self.name:
760            return self.name
761        if "name" in self.ids:
762            return self.ids["name"]
763        return ', '.join(self.defname)
764