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