1import asyncio 2import collections 3import datetime 4import random 5import traceback 6from queue import Queue 7from io import StringIO 8 9from gi.repository import GObject 10 11from pychess.compat import create_task 12from pychess.Savers.ChessFile import LoadingError 13from pychess.Players.Player import PlayerIsDead, PassInterrupt, TurnInterrupt, InvalidMove, GameEnded 14from pychess.System import conf 15from pychess.System.protoopen import protoopen, protosave 16from pychess.System.Log import log 17from pychess.Utils.book import getOpenings 18from pychess.Utils.Move import Move 19from pychess.Utils.eco import get_eco 20from pychess.Utils.Offer import Offer 21from pychess.Utils.TimeModel import TimeModel 22from pychess.Utils.DecisionSupportAlgorithm import DecisionSupportAlgorithm 23from pychess.Savers import html, txt 24from pychess.Variants.normal import NormalBoard 25 26from .logic import getStatus, isClaimableDraw, playerHasMatingMaterial 27from .const import WAITING_TO_START, UNKNOWN_REASON, WHITE, ARTIFICIAL, RUNNING, \ 28 FLAG_CALL, BLACK, KILLED, ANALYZING, LOCAL, REMOTE, PAUSED, HURRY_ACTION, \ 29 CHAT_ACTION, RESIGNATION, BLACKWON, WHITEWON, DRAW_CALLFLAG, WON_RESIGN, DRAW, \ 30 WON_CALLFLAG, DRAW_WHITEINSUFFICIENTANDBLACKTIME, DRAW_OFFER, TAKEBACK_OFFER, \ 31 DRAW_BLACKINSUFFICIENTANDWHITETIME, ACTION_ERROR_NOT_OUT_OF_TIME, OFFERS, \ 32 ACTION_ERROR_TOO_LARGE_UNDO, ACTION_ERROR_NONE_TO_WITHDRAW, DRAW_AGREE, \ 33 ACTION_ERROR_NONE_TO_DECLINE, ADJOURN_OFFER, ADJOURNED, ABORT_OFFER, ABORTED, \ 34 ADJOURNED_AGREEMENT, PAUSE_OFFER, RESUME_OFFER, ACTION_ERROR_NONE_TO_ACCEPT, \ 35 ABORTED_AGREEMENT, WHITE_ENGINE_DIED, BLACK_ENGINE_DIED, WON_ADJUDICATION, \ 36 UNDOABLE_STATES, DRAW_REPETITION, UNDOABLE_REASONS, UNFINISHED_STATES, \ 37 DRAW_50MOVES, HINT 38 39 40class GameModel(GObject.GObject): 41 """ GameModel contains all available data on a chessgame. 42 It also has the task of controlling players actions and moves """ 43 44 __gsignals__ = { 45 # game_started is emitted when control is given to the players for the 46 # first time. Notice this is after players.start has been called. 47 "game_started": (GObject.SignalFlags.RUN_FIRST, None, ()), 48 # game_changed is emitted when a move has been made. 49 "game_changed": (GObject.SignalFlags.RUN_FIRST, None, (int, )), 50 # moves_undoig is emitted when a undoMoves call has been accepted, but 51 # before any work has been done to execute it. 52 "moves_undoing": (GObject.SignalFlags.RUN_FIRST, None, (int, )), 53 # moves_undone is emitted after n moves have been undone in the 54 # gamemodel and the players. 55 "moves_undone": (GObject.SignalFlags.RUN_FIRST, None, (int, )), 56 # variation_undoig is emitted when a undo_in_variation call has been started, but 57 # before any work has been done to execute it. 58 "variation_undoing": (GObject.SignalFlags.RUN_FIRST, None, ()), 59 # variation_undone is emitted after 1 move have been undone in the 60 # boardview shown variation 61 "variation_undone": (GObject.SignalFlags.RUN_FIRST, None, ()), 62 # game_unended is emitted if moves have been undone, such that the game 63 # which had previously ended, is now again active. 64 "game_unended": (GObject.SignalFlags.RUN_FIRST, None, ()), 65 # game_loading is emitted if the GameModel is about to load in a chess 66 # game from a file. 67 "game_loading": (GObject.SignalFlags.RUN_FIRST, None, (object, )), 68 # game_loaded is emitted after the chessformat handler has loaded in 69 # all the moves from a file to the game model. 70 "game_loaded": (GObject.SignalFlags.RUN_FIRST, None, (object, )), 71 # game_saved is emitted in the end of model.save() 72 "game_saved": (GObject.SignalFlags.RUN_FIRST, None, (str, )), 73 # game_ended is emitted if the models state has been changed to an 74 # "ended state" 75 "game_ended": (GObject.SignalFlags.RUN_FIRST, None, (int, )), 76 # game_terminated is emitted if the game was terminated. That is all 77 # players and clocks were stopped, and it is no longer possible to 78 # resume the game, even by undo. 79 "game_terminated": (GObject.SignalFlags.RUN_FIRST, None, ()), 80 # game_paused is emitted if the game was successfully paused. 81 "game_paused": (GObject.SignalFlags.RUN_FIRST, None, ()), 82 # game_paused is emitted if the game was successfully resumed from a 83 # pause. 84 "game_resumed": (GObject.SignalFlags.RUN_FIRST, None, ()), 85 # action_error is currently only emitted by ICGameModel, in the case 86 # the "web model" didn't accept the action you were trying to do. 87 "action_error": (GObject.SignalFlags.RUN_FIRST, None, (object, int)), 88 # players_changed is emitted if the players list was changed. 89 "players_changed": (GObject.SignalFlags.RUN_FIRST, None, ()), 90 "analyzer_added": (GObject.SignalFlags.RUN_FIRST, None, (object, str)), 91 "analyzer_removed": (GObject.SignalFlags.RUN_FIRST, None, 92 (object, str)), 93 "analyzer_paused": (GObject.SignalFlags.RUN_FIRST, None, 94 (object, str)), 95 "analyzer_resumed": (GObject.SignalFlags.RUN_FIRST, None, 96 (object, str)), 97 # opening_changed is emitted if the move changed the opening. 98 "opening_changed": (GObject.SignalFlags.RUN_FIRST, None, ()), 99 # variation_added is emitted if a variation was added. 100 "variation_added": (GObject.SignalFlags.RUN_FIRST, None, 101 (object, object)), 102 # variation_extended is emitted if a new move was added to a variation. 103 "variation_extended": (GObject.SignalFlags.RUN_FIRST, None, 104 (object, object)), 105 # scores_changed is emitted if the analyzing scores was changed. 106 "analysis_changed": (GObject.SignalFlags.RUN_FIRST, None, (int, )), 107 # analysis_finished is emitted if the game analyzing finished stepping on all moves. 108 "analysis_finished": (GObject.SignalFlags.RUN_FIRST, None, ()), 109 # FICS games can get kibitz/whisper messages 110 "message_received": (GObject.SignalFlags.RUN_FIRST, None, (str, str)), 111 # FICS games can have observers 112 "observers_received": (GObject.SignalFlags.RUN_FIRST, None, (str, )), 113 } 114 115 def __init__(self, timemodel=None, variant=NormalBoard): 116 GObject.GObject.__init__(self) 117 self.daemon = True 118 self.variant = variant 119 self.boards = [variant(setup=True)] 120 121 self.moves = [] 122 self.scores = {} 123 self.spy_scores = {} 124 self.players = [] 125 126 self.gameno = None 127 self.variations = [self.boards] 128 129 self.terminated = False 130 self.status = WAITING_TO_START 131 self.reason = UNKNOWN_REASON 132 self.curColor = WHITE 133 134 # support algorithm for new players 135 # type apparent : DecisionSupportAlgorithm 136 self.support_algorithm = DecisionSupportAlgorithm() 137 138 if timemodel is None: 139 self.timemodel = TimeModel() 140 else: 141 self.timemodel = timemodel 142 self.timemodel.gamemodel = self 143 144 self.connections = collections.defaultdict(list) # mainly for IC subclasses 145 self.analyzer_cids = {} 146 self.examined = False 147 148 now = datetime.datetime.now() 149 self.tags = collections.defaultdict(str) 150 self.tags["Event"] = _("Local Event") 151 self.tags["Site"] = _("Local Site") 152 self.tags["Date"] = "%04d.%02d.%02d" % (now.year, now.month, now.day) 153 self.tags["Round"] = "1" 154 155 self.endstatus = None 156 self.zero_reached_cid = None 157 158 self.timed = self.timemodel.minutes != 0 or self.timemodel.gain != 0 159 if self.timed: 160 self.zero_reached_cid = self.timemodel.connect('zero_reached', self.zero_reached) 161 if self.timemodel.moves == 0: 162 self.tags["TimeControl"] = "%d%s%d" % (self.timemodel.minutes * 60, "+" if self.timemodel.gain >= 0 else "-", abs(self.timemodel.gain)) 163 else: 164 self.tags["TimeControl"] = "%d/%d" % (self.timemodel.moves, self.timemodel.minutes * 60) 165 # Notice: tags["WhiteClock"] and tags["BlackClock"] are never set 166 # on the gamemodel, but simply written or read during saving/ 167 # loading from pgn. If you want to know the time left for a player, 168 # check the time model. 169 170 # Keeps track of offers, so that accepts can be spotted 171 self.offers = {} 172 173 # True if the game has been changed since last save 174 self.needsSave = False 175 176 # The uri the current game was loaded from, or None if not a loaded game 177 self.uri = None 178 179 # Link to additiona info 180 self.info = None 181 182 self.spectators = {} 183 184 self.undoQueue = Queue() 185 186 # learn_type set by LearnModel.set_learn_data() 187 self.offline_lecture = False 188 self.puzzle_game = False 189 self.lesson_game = False 190 self.end_game = False 191 self.solved = False 192 193 @property 194 def practice_game(self): 195 return self.puzzle_game or self.end_game 196 197 @property 198 def starting_color(self): 199 return BLACK if "FEN" in self.tags and self.tags["FEN"].split()[1] == "b" else WHITE 200 201 @property 202 def orientation(self): 203 if "Orientation" in self.tags: 204 return BLACK if self.tags["Orintation"].lower() == "black" else WHITE 205 else: 206 return self.starting_color 207 208 def zero_reached(self, timemodel, color): 209 if conf.get('autoCallFlag'): 210 if self.status == RUNNING and timemodel.getPlayerTime(color) <= 0: 211 log.info( 212 'Automatically sending flag call on behalf of player %s.' % 213 self.players[1 - color].name) 214 self.players[1 - color].emit("offer", Offer(FLAG_CALL)) 215 216 def __repr__(self): 217 string = "<GameModel at %s" % id(self) 218 string += " (ply=%s" % self.ply 219 if len(self.moves) > 0: 220 string += ", move=%s" % self.moves[-1] 221 string += ", variant=%s" % self.variant.name.encode('utf-8') 222 string += ", status=%s, reason=%s" % (str(self.status), str(self.reason)) 223 string += ", players=%s" % str(self.players) 224 string += ", tags=%s" % str(self.tags) 225 if len(self.boards) > 0: 226 string += "\nboard=%s" % self.boards[-1] 227 return string + ")>" 228 229 @property 230 def display_text(self): 231 if self.variant == NormalBoard and not self.timed: 232 return "[ " + _("Untimed") + " ]" 233 else: 234 text = "[ " 235 if self.variant != NormalBoard: 236 text += self.variant.name + " " 237 if self.timed: 238 text += self.timemodel.display_text + " " 239 return text + "]" 240 241 def setPlayers(self, players): 242 log.debug("GameModel.setPlayers: starting") 243 assert self.status == WAITING_TO_START 244 self.players = players 245 for player in self.players: 246 self.connections[player].append(player.connect("offer", 247 self.offerReceived)) 248 self.connections[player].append(player.connect( 249 "withdraw", self.withdrawReceived)) 250 self.connections[player].append(player.connect( 251 "decline", self.declineReceived)) 252 self.connections[player].append(player.connect( 253 "accept", self.acceptReceived)) 254 self.tags["White"] = str(self.players[WHITE]) 255 self.tags["Black"] = str(self.players[BLACK]) 256 log.debug("GameModel.setPlayers: -> emit players_changed") 257 self.emit("players_changed") 258 log.debug("GameModel.setPlayers: <- emit players_changed") 259 log.debug("GameModel.setPlayers: returning") 260 261 # when the players are set, it is known whether or not there is a bot 262 # we activate the support algorithm if there is one 263 # boolean to know if the game is against a bot 264 # activate support algorithm if that is the case 265 if self.isLocalGame(): 266 self.support_algorithm.set_foe_as_bot() 267 268 def color(self, player): 269 if player is self.players[0]: 270 return WHITE 271 else: 272 return BLACK 273 274 @asyncio.coroutine 275 def start_analyzer(self, analyzer_type, force_engine=None): 276 # Don't start regular analyzers 277 if (self.practice_game or self.lesson_game) and force_engine is None and not self.solved: 278 return 279 280 # prevent starting new analyzers again and again 281 # when fics lecture reuses the same gamemodel 282 if analyzer_type in self.spectators: 283 return 284 285 from pychess.Players.engineNest import init_engine 286 analyzer = yield from init_engine(analyzer_type, self, force_engine=force_engine) 287 if analyzer is None: 288 return 289 290 analyzer.setOptionInitialBoard(self) 291 # Enable to find alternate hint in learn perspective puzzles 292 if force_engine is not None: 293 analyzer.setOption("MultiPV", 3) 294 analyzer.analysis_depth = 20 295 296 self.spectators[analyzer_type] = analyzer 297 self.emit("analyzer_added", analyzer, analyzer_type) 298 self.analyzer_cids[analyzer_type] = analyzer.connect("analyze", self.on_analyze) 299 300 def remove_analyzer(self, analyzer_type): 301 try: 302 analyzer = self.spectators[analyzer_type] 303 except KeyError: 304 return 305 306 analyzer.disconnect(self.analyzer_cids[analyzer_type]) 307 analyzer.end(KILLED, UNKNOWN_REASON) 308 self.emit("analyzer_removed", analyzer, analyzer_type) 309 del self.spectators[analyzer_type] 310 311 def resume_analyzer(self, analyzer_type): 312 try: 313 analyzer = self.spectators[analyzer_type] 314 except KeyError: 315 return 316 317 analyzer.resume() 318 self.emit("analyzer_resumed", analyzer, analyzer_type) 319 320 def pause_analyzer(self, analyzer_type): 321 try: 322 analyzer = self.spectators[analyzer_type] 323 except KeyError: 324 return 325 326 analyzer.pause() 327 self.emit("analyzer_paused", analyzer, analyzer_type) 328 329 @asyncio.coroutine 330 def restart_analyzer(self, analyzer_type): 331 self.remove_analyzer(analyzer_type) 332 yield from self.start_analyzer(analyzer_type) 333 334 def on_analyze(self, analyzer, analysis): 335 def safe_int(p): 336 if p in [None, '']: 337 return 0 338 try: 339 return int(p) 340 except ValueError: 341 return 0 342 343 if analysis and (self.practice_game or self.lesson_game): 344 for i, anal in enumerate(analysis): 345 if anal is not None: 346 ply, pv, score, depth, nps = anal 347 if len(pv) > 0: 348 if ply not in self.hints: 349 self.hints[ply] = [] 350 351 if len(self.hints[ply]) < i + 1: 352 self.hints[ply].append((pv[0], score)) 353 else: 354 self.hints[ply][i] = (pv[0], score) 355 if analysis and analysis[0] is not None: 356 ply, pv, score, depth, nps = analysis[0] 357 if score is not None and depth: 358 if analyzer.mode == ANALYZING: 359 if (ply not in self.scores) or (safe_int(self.scores[ply][2]) <= safe_int(depth)): 360 self.scores[ply] = (pv, score, depth) 361 self.emit("analysis_changed", ply) 362 else: 363 if (ply not in self.spy_scores) or (safe_int(self.spy_scores[ply][2]) <= safe_int(depth)): 364 self.spy_scores[ply] = (pv, score, depth) 365 366 def setOpening(self, ply=None, redetermine=False): 367 if ply is None: 368 ply = self.ply 369 370 opening = None 371 while ply >= self.lowply: 372 opening = get_eco(self.getBoardAtPly(ply).board.hash, exactPosition=True) 373 if opening is None and redetermine: 374 ply = ply - 1 375 else: 376 break 377 378 if opening is not None: 379 self.tags["ECO"] = opening[0] 380 self.tags["Opening"] = opening[1] 381 self.tags["Variation"] = opening[2] 382 else: 383 if redetermine: 384 if 'ECO' in self.tags: 385 del self.tags['ECO'] 386 if 'Opening' in self.tags: 387 del self.tags['Opening'] 388 if 'Variation' in self.tags: 389 del self.tags['Variation'] 390 self.emit("opening_changed") 391 392 # Board stuff 393 394 def _get_ply(self): 395 return self.boards[-1].ply 396 397 ply = property(_get_ply) 398 399 def _get_lowest_ply(self): 400 return self.boards[0].ply 401 402 lowply = property(_get_lowest_ply) 403 404 def _get_curplayer(self): 405 try: 406 return self.players[self.getBoardAtPly(self.ply).color] 407 except IndexError: 408 log.error("%s %s" % 409 (self.players, self.getBoardAtPly(self.ply).color)) 410 raise 411 412 curplayer = property(_get_curplayer) 413 414 def _get_waitingplayer(self): 415 try: 416 return self.players[1 - self.getBoardAtPly(self.ply).color] 417 except IndexError: 418 log.error("%s %s" % 419 (self.players, 1 - self.getBoardAtPly(self.ply).color)) 420 raise 421 422 waitingplayer = property(_get_waitingplayer) 423 424 def _plyToIndex(self, ply): 425 index = ply - self.lowply 426 if index < 0: 427 raise IndexError("%s < %s\n" % (ply, self.lowply)) 428 return index 429 430 def getBoardAtPly(self, ply, variation=0): 431 try: 432 return self.variations[variation][self._plyToIndex(ply)] 433 except IndexError: 434 log.error("%d\t%d\t%d\t%d\t%d" % (self.lowply, ply, self.ply, 435 variation, len(self.variations))) 436 raise 437 438 def getMoveAtPly(self, ply, variation=0): 439 try: 440 return Move(self.variations[variation][self._plyToIndex(ply) + 441 1].board.lastMove) 442 except IndexError: 443 log.error("%d\t%d\t%d\t%d\t%d" % (self.lowply, ply, self.ply, 444 variation, len(self.variations))) 445 raise 446 447 def hasLocalPlayer(self): 448 if self.players[0].__type__ == LOCAL or self.players[ 449 1].__type__ == LOCAL: 450 return True 451 else: 452 return False 453 454 def hasEnginePlayer(self): 455 if self.players[0].__type__ == ARTIFICIAL or self.players[ 456 1].__type__ == ARTIFICIAL: 457 return True 458 else: 459 return False 460 461 def isLocalGame(self): 462 if self.players[0].__type__ != REMOTE and self.players[ 463 1].__type__ != REMOTE: 464 return True 465 else: 466 return False 467 468 def isObservationGame(self): 469 return not self.hasLocalPlayer() 470 471 def isEngine2EngineGame(self): 472 if len(self.players) == 2 and self.players[0].__type__ == ARTIFICIAL and self.players[1].__type__ == ARTIFICIAL: 473 return True 474 else: 475 return False 476 477 def isPlayingICSGame(self): 478 if self.players and self.status in (WAITING_TO_START, PAUSED, RUNNING): 479 if (self.players[0].__type__ == LOCAL and self.players[1].__type__ == REMOTE) or \ 480 (self.players[1].__type__ == LOCAL and self.players[0].__type__ == REMOTE) or \ 481 ((self.offline_lecture or self.practice_game or self.lesson_game) and not self.solved) or \ 482 (self.players[1].__type__ == REMOTE and self.players[0].__type__ == REMOTE and 483 self.examined and ( 484 self.players[0].name == "puzzlebot" or self.players[1].name == "puzzlebot") or 485 self.players[0].name == "endgamebot" or self.players[1].name == "endgamebot"): 486 return True 487 return False 488 489 def isLoadedGame(self): 490 return self.gameno is not None 491 492 # Offer management 493 494 def offerReceived(self, player, offer): 495 log.debug("GameModel.offerReceived: offerer=%s %s" % 496 (repr(player), offer)) 497 if player == self.players[WHITE]: 498 opPlayer = self.players[BLACK] 499 elif player == self.players[BLACK]: 500 opPlayer = self.players[WHITE] 501 else: 502 # Player comments echoed to opponent if the player started a conversation 503 # with you prior to observing a game the player is in #1113 504 return 505 506 if offer.type == HURRY_ACTION: 507 opPlayer.hurry() 508 509 elif offer.type == CHAT_ACTION: 510 # print("GameModel.offerreceived(player, offer)", player.name, offer.param) 511 opPlayer.putMessage(offer.param) 512 513 elif offer.type == RESIGNATION: 514 if player == self.players[WHITE]: 515 self.end(BLACKWON, WON_RESIGN) 516 else: 517 self.end(WHITEWON, WON_RESIGN) 518 519 elif offer.type == FLAG_CALL: 520 assert self.timed 521 if self.timemodel.getPlayerTime(1 - player.color) <= 0: 522 if self.timemodel.getPlayerTime(player.color) <= 0: 523 self.end(DRAW, DRAW_CALLFLAG) 524 elif not playerHasMatingMaterial(self.boards[-1], 525 player.color): 526 if player.color == WHITE: 527 self.end(DRAW, DRAW_WHITEINSUFFICIENTANDBLACKTIME) 528 else: 529 self.end(DRAW, DRAW_BLACKINSUFFICIENTANDWHITETIME) 530 else: 531 if player == self.players[WHITE]: 532 self.end(WHITEWON, WON_CALLFLAG) 533 else: 534 self.end(BLACKWON, WON_CALLFLAG) 535 else: 536 player.offerError(offer, ACTION_ERROR_NOT_OUT_OF_TIME) 537 538 elif offer.type == DRAW_OFFER and isClaimableDraw(self.boards[-1]): 539 reason = getStatus(self.boards[-1])[1] 540 self.end(DRAW, reason) 541 542 elif offer.type == TAKEBACK_OFFER and offer.param < self.lowply: 543 player.offerError(offer, ACTION_ERROR_TOO_LARGE_UNDO) 544 545 elif offer.type in OFFERS: 546 if offer not in self.offers: 547 log.debug("GameModel.offerReceived: doing %s.offer(%s)" % ( 548 repr(opPlayer), offer)) 549 self.offers[offer] = player 550 opPlayer.offer(offer) 551 # If we updated an older offer, we want to delete the old one 552 keys = self.offers.keys() 553 for offer_ in keys: 554 if offer.type == offer_.type and offer != offer_: 555 del self.offers[offer_] 556 557 def withdrawReceived(self, player, offer): 558 log.debug("GameModel.withdrawReceived: withdrawer=%s %s" % ( 559 repr(player), offer)) 560 if player == self.players[WHITE]: 561 opPlayer = self.players[BLACK] 562 else: 563 opPlayer = self.players[WHITE] 564 565 if offer in self.offers and self.offers[offer] == player: 566 del self.offers[offer] 567 opPlayer.offerWithdrawn(offer) 568 else: 569 player.offerError(offer, ACTION_ERROR_NONE_TO_WITHDRAW) 570 571 def declineReceived(self, player, offer): 572 log.debug("GameModel.declineReceived: decliner=%s %s" % ( 573 repr(player), offer)) 574 if player == self.players[WHITE]: 575 opPlayer = self.players[BLACK] 576 else: 577 opPlayer = self.players[WHITE] 578 579 if offer in self.offers and self.offers[offer] == opPlayer: 580 del self.offers[offer] 581 log.debug("GameModel.declineReceived: declining %s" % offer) 582 opPlayer.offerDeclined(offer) 583 else: 584 player.offerError(offer, ACTION_ERROR_NONE_TO_DECLINE) 585 586 def acceptReceived(self, player, offer): 587 log.debug("GameModel.acceptReceived: accepter=%s %s" % ( 588 repr(player), offer)) 589 if player == self.players[WHITE]: 590 opPlayer = self.players[BLACK] 591 else: 592 opPlayer = self.players[WHITE] 593 594 if offer in self.offers and self.offers[offer] == opPlayer: 595 if offer.type == DRAW_OFFER: 596 self.end(DRAW, DRAW_AGREE) 597 elif offer.type == TAKEBACK_OFFER: 598 log.debug("GameModel.acceptReceived: undoMoves(%s)" % offer.param) 599 self.undoMoves(offer.param) 600 elif offer.type == ADJOURN_OFFER: 601 self.end(ADJOURNED, ADJOURNED_AGREEMENT) 602 elif offer.type == ABORT_OFFER: 603 self.end(ABORTED, ABORTED_AGREEMENT) 604 elif offer.type == PAUSE_OFFER: 605 self.pause() 606 elif offer.type == RESUME_OFFER: 607 self.resume() 608 del self.offers[offer] 609 else: 610 player.offerError(offer, ACTION_ERROR_NONE_TO_ACCEPT) 611 612 # Data stuff 613 614 def loadAndStart(self, uri, loader, gameno, position, first_time=True): 615 if first_time: 616 assert self.status == WAITING_TO_START 617 618 uriIsFile = not isinstance(uri, str) 619 if not uriIsFile: 620 chessfile = loader.load(protoopen(uri)) 621 else: 622 chessfile = loader.load(uri) 623 624 self.gameno = gameno 625 self.emit("game_loading", uri) 626 try: 627 chessfile.loadToModel(gameno, -1, self) 628 # Postpone error raising to make games loadable to the point of the 629 # error 630 except LoadingError as e: 631 error = e 632 else: 633 error = None 634 if self.players: 635 self.players[WHITE].setName(self.tags["White"]) 636 self.players[BLACK].setName(self.tags["Black"]) 637 self.emit("game_loaded", uri) 638 639 self.needsSave = False 640 if not uriIsFile: 641 self.uri = uri 642 else: 643 self.uri = None 644 645 # Even if the game "starts ended", the players should still be moved 646 # to the last position, so analysis is correct, and a possible "undo" 647 # will work as expected. 648 for spectator in self.spectators.values(): 649 spectator.setOptionInitialBoard(self) 650 for player in self.players: 651 player.setOptionInitialBoard(self) 652 if self.timed: 653 self.timemodel.setMovingColor(self.boards[-1].color) 654 655 if first_time: 656 if self.status == RUNNING: 657 if self.timed: 658 self.timemodel.start() 659 660 # Store end status from Result tag 661 if self.status in (DRAW, WHITEWON, BLACKWON): 662 self.endstatus = self.status 663 self.status = WAITING_TO_START 664 self.start() 665 666 if error: 667 raise error 668 669 def save(self, uri, saver, append, position=None, flip=False): 670 if saver in (html, txt): 671 fileobj = open(uri, "a" if append else "w", encoding="utf-8", newline="") 672 self.uri = uri 673 elif isinstance(uri, str): 674 fileobj = protosave(uri, append) 675 self.uri = uri 676 else: 677 fileobj = uri 678 self.uri = None 679 with fileobj: 680 saver.save(fileobj, self, position, flip) 681 self.needsSave = False 682 self.emit("game_saved", uri) 683 684 def get_book_move(self): 685 openings = getOpenings(self.boards[-1].board) 686 openings.sort(key=lambda t: t[1], reverse=True) 687 if not openings: 688 return None 689 690 total_weights = 0 691 for move, weight, learn in openings: 692 total_weights += weight 693 694 if total_weights < 1: 695 return None 696 697 choice = random.randint(0, total_weights - 1) 698 699 current_sum = 0 700 for move, weight, learn in openings: 701 current_sum += weight 702 if current_sum > choice: 703 return Move(move) 704 705 # Run stuff 706 707 def start(self): 708 @asyncio.coroutine 709 def coro(): 710 log.debug("GameModel.run: Starting. self=%s" % self) 711 # Avoid racecondition when self.start is called while we are in 712 # self.end 713 if self.status != WAITING_TO_START: 714 return 715 716 if not self.isLocalGame(): 717 self.timemodel.handle_gain = False 718 719 self.status = RUNNING 720 721 for player in self.players + list(self.spectators.values()): 722 event = asyncio.Event() 723 is_dead = set() 724 player.start(event, is_dead) 725 726 yield from event.wait() 727 728 if is_dead: 729 if player in self.players[WHITE]: 730 self.kill(WHITE_ENGINE_DIED) 731 break 732 elif player in self.players[BLACK]: 733 self.kill(BLACK_ENGINE_DIED) 734 break 735 736 log.debug("GameModel.run: emitting 'game_started' self=%s" % self) 737 self.emit("game_started") 738 739 # Let GameModel end() itself on games started with loadAndStart() 740 if not self.lesson_game: 741 self.checkStatus() 742 743 if self.isEngine2EngineGame() and self.timed: 744 self.timemodel.start() 745 self.timemodel.started = True 746 747 self.curColor = self.boards[-1].color 748 749 book_depth_max = conf.get("book_depth_max") 750 751 while self.status in (PAUSED, RUNNING, DRAW, WHITEWON, BLACKWON): 752 curPlayer = self.players[self.curColor] 753 754 if self.timed: 755 log.debug("GameModel.run: id=%s, players=%s, self.ply=%s: updating %s's time" % ( 756 id(self), str(self.players), str(self.ply), str(curPlayer))) 757 curPlayer.updateTime( 758 self.timemodel.getPlayerTime(self.curColor), 759 self.timemodel.getPlayerTime(1 - self.curColor)) 760 try: 761 log.debug("GameModel.run: id=%s, players=%s, self.ply=%s: calling %s.makeMove()" % ( 762 id(self), str(self.players), self.ply, str(curPlayer))) 763 764 move = None 765 # if the current player is a bot 766 if curPlayer.__type__ == ARTIFICIAL and book_depth_max > 0 and self.ply <= book_depth_max: 767 move = self.get_book_move() 768 log.debug("GameModel.run: id=%s, players=%s, self.ply=%s: got move=%s from book" % ( 769 id(self), str(self.players), self.ply, move)) 770 if move is not None: 771 curPlayer.set_board(self.boards[-1].move(move)) 772 # if the current player is not a bot 773 if move is None: 774 775 if self.ply > self.lowply: 776 move = yield from curPlayer.makeMove(self.boards[-1], self.moves[-1], self.boards[-2]) 777 else: 778 move = yield from curPlayer.makeMove(self.boards[-1], None, None) 779 log.debug("GameModel.run: id=%s, players=%s, self.ply=%s: got move=%s from %s" % ( 780 id(self), str(self.players), self.ply, move, str(curPlayer))) 781 except PlayerIsDead as e: 782 if self.status in (WAITING_TO_START, PAUSED, RUNNING): 783 stringio = StringIO() 784 traceback.print_exc(file=stringio) 785 error = stringio.getvalue() 786 log.error( 787 "GameModel.run: A Player died: player=%s error=%s\n%s" 788 % (curPlayer, error, e)) 789 if self.curColor == WHITE: 790 self.kill(WHITE_ENGINE_DIED) 791 else: 792 self.kill(BLACK_ENGINE_DIED) 793 break 794 except InvalidMove as e: 795 stringio = StringIO() 796 traceback.print_exc(file=stringio) 797 error = stringio.getvalue() 798 log.error( 799 "GameModel.run: InvalidMove by player=%s error=%s\n%s" 800 % (curPlayer, error, e)) 801 if self.curColor == WHITE: 802 self.end(BLACKWON, WON_ADJUDICATION) 803 else: 804 self.end(WHITEWON, WON_ADJUDICATION) 805 break 806 except PassInterrupt: 807 log.debug("GameModel.run: id=%s, players=%s, self.ply=%s: PassInterrupt" % ( 808 id(self), str(self.players), self.ply)) 809 continue 810 except TurnInterrupt: 811 log.debug("GameModel.run: id=%s, players=%s, self.ply=%s: TurnInterrupt" % ( 812 id(self), str(self.players), self.ply)) 813 self.curColor = self.boards[-1].color 814 continue 815 except GameEnded: 816 log.debug("GameModel.run: got GameEnded exception") 817 break 818 819 assert isinstance(move, Move), "%s" % repr(move) 820 log.debug("GameModel.run: id=%s, players=%s, self.ply=%s: applying move=%s" % ( 821 id(self), str(self.players), self.ply, str(move))) 822 self.needsSave = True 823 newBoard = self.boards[-1].move(move) 824 newBoard.board.prev = self.boards[-1].board 825 826 # newBoard.printPieces() 827 # Variation on next move can exist from the hint panel... 828 if self.boards[-1].board.next is not None: 829 newBoard.board.children = self.boards[ 830 -1].board.next.children 831 832 self.boards = self.variations[0] 833 self.boards[-1].board.next = newBoard.board 834 self.boards.append(newBoard) 835 self.moves.append(move) 836 837 if self.timed: 838 self.timemodel.tap() 839 840 if not self.terminated: 841 self.emit("game_changed", self.ply) 842 843 for spectator in self.spectators.values(): 844 if spectator.board == self.boards[-2]: 845 spectator.putMove(self.boards[-1], self.moves[-1], 846 self.boards[-2]) 847 848 if self.puzzle_game and len(self.moves) % 2 == 1: 849 status, reason = getStatus(self.boards[-1]) 850 self.failed_playing_best = self.check_failed_playing_best(status) 851 if self.failed_playing_best: 852 # print("failed_playing_best() == True -> yield from asyncio.sleep(1.5) ") 853 # It may happen that analysis had no time to fill hints with best moves 854 # so we give him another chance with some additional time to think on it 855 self.spectators[HINT].setBoard(self.boards[-2]) 856 # TODO: wait for an event (analyzer PV reaching 18 ply) 857 # instead of hard coded sleep time 858 yield from asyncio.sleep(1.5) 859 self.failed_playing_best = self.check_failed_playing_best(status) 860 861 self.checkStatus() 862 863 self.setOpening() 864 865 self.curColor = 1 - self.curColor 866 867 self.checkStatus() 868 869 create_task(coro()) 870 871 def checkStatus(self): 872 """ Updates self.status so it fits with what getStatus(boards[-1]) 873 would return. That is, if the game is e.g. check mated this will 874 call mode.end(), or if moves have been undone from an otherwise 875 ended position, this will call __resume and emit game_unended. """ 876 log.debug("GameModel.checkStatus:") 877 878 # call flag by engine 879 if self.isEngine2EngineGame() and self.status in UNDOABLE_STATES: 880 return 881 882 status, reason = getStatus(self.boards[-1]) 883 884 if self.practice_game and (len(self.moves) % 2 == 1 or status in UNDOABLE_STATES): 885 self.check_goal(status, reason) 886 887 if self.endstatus is not None: 888 self.end(self.endstatus, reason) 889 return 890 891 if status != RUNNING and self.status in (WAITING_TO_START, PAUSED, 892 RUNNING): 893 if status == DRAW and reason in (DRAW_REPETITION, DRAW_50MOVES): 894 if self.isEngine2EngineGame(): 895 self.end(status, reason) 896 return 897 else: 898 self.end(status, reason) 899 return 900 901 if status != self.status and self.status in UNDOABLE_STATES \ 902 and self.reason in UNDOABLE_REASONS: 903 self.__resume() 904 self.status = status 905 self.reason = UNKNOWN_REASON 906 self.emit("game_unended") 907 908 def __pause(self): 909 log.debug("GameModel.__pause: %s" % self) 910 if self.isEngine2EngineGame(): 911 for player in self.players: 912 player.end(self.status, self.reason) 913 if self.timed: 914 self.timemodel.end() 915 else: 916 for player in self.players: 917 player.pause() 918 if self.timed: 919 self.timemodel.pause() 920 921 def pause(self): 922 """ Players will raise NotImplementedError if they doesn't support 923 pause. Spectators will be ignored. """ 924 925 self.__pause() 926 self.status = PAUSED 927 self.emit("game_paused") 928 929 def __resume(self): 930 for player in self.players: 931 player.resume() 932 if self.timed: 933 self.timemodel.resume() 934 self.emit("game_resumed") 935 936 def resume(self): 937 self.status = RUNNING 938 self.__resume() 939 940 def end(self, status, reason): 941 if self.status not in UNFINISHED_STATES: 942 log.info( 943 "GameModel.end: Can't end a game that's already ended: %s %s" % 944 (status, reason)) 945 return 946 if self.status not in (WAITING_TO_START, PAUSED, RUNNING): 947 self.needsSave = True 948 949 log.debug("GameModel.end: players=%s, self.ply=%s: Ending a game with status %d for reason %d" % ( 950 repr(self.players), str(self.ply), status, reason)) 951 self.status = status 952 self.reason = reason 953 954 self.emit("game_ended", reason) 955 956 self.__pause() 957 958 def kill(self, reason): 959 log.debug("GameModel.kill: players=%s, self.ply=%s: Killing a game for reason %d\n%s" % ( 960 repr(self.players), str(self.ply), reason, "".join( 961 traceback.format_list(traceback.extract_stack())).strip())) 962 963 self.status = KILLED 964 self.reason = reason 965 966 for player in self.players: 967 player.end(self.status, reason) 968 969 for spectator in self.spectators.values(): 970 spectator.end(self.status, reason) 971 972 if self.timed: 973 self.timemodel.end() 974 975 self.emit("game_ended", reason) 976 977 def terminate(self): 978 log.debug("GameModel.terminate: %s" % self) 979 self.terminated = True 980 981 if self.status != KILLED: 982 for player in self.players: 983 player.end(self.status, self.reason) 984 985 analyzer_types = list(self.spectators.keys()) 986 for analyzer_type in analyzer_types: 987 self.remove_analyzer(analyzer_type) 988 989 if self.timed: 990 log.debug("GameModel.terminate: -> timemodel.end()") 991 self.timemodel.end() 992 log.debug("GameModel.terminate: <- timemodel.end() %s" % 993 repr(self.timemodel)) 994 if self.zero_reached_cid is not None: 995 self.timemodel.disconnect(self.zero_reached_cid) 996 997 # ICGameModel may did this if game was a FICS game 998 if self.connections is not None: 999 for player in self.players: 1000 for cid in self.connections[player]: 1001 player.disconnect(cid) 1002 self.connections = {} 1003 1004 self.timemodel.gamemodel = None 1005 self.players = [] 1006 self.emit("game_terminated") 1007 1008 # Other stuff 1009 1010 def undoMoves(self, moves): 1011 """ Undo and remove moves number of moves from the game history from 1012 the GameModel, players, and any spectators """ 1013 if self.ply < 1 or moves < 1: 1014 return 1015 if self.ply - moves < 0: 1016 # There is no way in the current threaded/asynchronous design 1017 # for the GUI to know that the number of moves it requests to takeback 1018 # will still be valid once the undo is actually processed. So, until 1019 # we either add some locking or get a synchronous design, we quietly 1020 # "fix" the takeback request rather than cause AssertionError or IndexError 1021 moves = 1 1022 1023 log.debug("GameModel.undoMoves: players=%s, self.ply=%s, moves=%s, board=%s" % ( 1024 repr(self.players), self.ply, moves, self.boards[-1])) 1025 self.emit("moves_undoing", moves) 1026 self.needsSave = True 1027 1028 self.boards = self.variations[0] 1029 del self.boards[-moves:] 1030 del self.moves[-moves:] 1031 self.boards[-1].board.next = None 1032 1033 for player in self.players: 1034 player.playerUndoMoves(moves, self) 1035 for spectator in self.spectators.values(): 1036 spectator.spectatorUndoMoves(moves, self) 1037 1038 log.debug("GameModel.undoMoves: undoing timemodel") 1039 if self.timed: 1040 self.timemodel.undoMoves(moves) 1041 1042 self.checkStatus() 1043 self.setOpening(redetermine=True) 1044 1045 self.emit("moves_undone", moves) 1046 1047 def isChanged(self): 1048 if self.ply == 0: 1049 return False 1050 if self.needsSave: 1051 return True 1052 # what was this for? 1053 # if not self.uri or not isWriteable(self.uri): 1054 # return True 1055 return False 1056 1057 def add_variation(self, board, moves, comment="", score="", emit=True): 1058 if board.board.next is None: 1059 # If we are in the latest played board, and want to add a variation 1060 # we have to add the latest move first 1061 if board.board.lastMove is None or board.board.prev is None: 1062 return 1063 moves = [Move(board.board.lastMove)] + moves 1064 board = board.board.prev.pieceBoard 1065 1066 board0 = board 1067 board = board0.clone() 1068 board.board.prev = None 1069 1070 # this prevents annotation panel node searches to find this instead of board0 1071 board.board.hash = -1 1072 1073 if comment: 1074 board.board.children.append(comment) 1075 1076 variation = [board] 1077 1078 for move in moves: 1079 new = board.move(move) 1080 if len(variation) == 1: 1081 new.board.prev = board0.board 1082 variation[0].board.next = new.board 1083 else: 1084 new.board.prev = board.board 1085 board.board.next = new.board 1086 variation.append(new) 1087 board = new 1088 1089 board0.board.next.children.append( 1090 [vboard.board for vboard in variation]) 1091 if score: 1092 variation[-1].board.children.append(score) 1093 1094 head = None 1095 for vari in self.variations: 1096 if board0 in vari: 1097 head = vari 1098 break 1099 1100 variation[0] = board0 1101 self.variations.append(head[:board0.ply - self.lowply] + variation) 1102 self.needsSave = True 1103 if emit: 1104 self.emit("variation_added", board0.board.next.children[-1], board0.board.next) 1105 return self.variations[-1] 1106 1107 def add_move2variation(self, board, move, variationIdx): 1108 new = board.move(move) 1109 new.board.prev = board.board 1110 board.board.next = new.board 1111 1112 # Find the variation (low level lboard list) to append 1113 cur_board = board.board 1114 vari = None 1115 while cur_board.prev is not None: 1116 for child in cur_board.prev.next.children: 1117 if isinstance(child, list) and cur_board in child: 1118 vari = child 1119 break 1120 if vari is None: 1121 cur_board = cur_board.prev 1122 else: 1123 break 1124 vari.append(new.board) 1125 1126 self.variations[variationIdx].append(new) 1127 self.needsSave = True 1128 self.emit("variation_extended", board.board, new.board) 1129 1130 def remove_variation(self, board, parent): 1131 """ board must be an lboard object of the first Board object of a variation Board(!) list """ 1132 # Remove the variation (list of lboards) containing board from parent's children list 1133 for child in parent.children: 1134 if isinstance(child, list) and board in child: 1135 parent.children.remove(child) 1136 break 1137 1138 # Remove all variations from gamemodel's variations list which contains this board 1139 for vari in self.variations[1:]: 1140 if board.pieceBoard in vari: 1141 self.variations.remove(vari) 1142 1143 # remove null_board if variation was added on last played move 1144 if not parent.fen_was_applied: 1145 parent.prev.next = None 1146 1147 self.needsSave = True 1148 1149 def undo_in_variation(self, board): 1150 """ board must be the latest Board object of a variation board list """ 1151 assert board.board.next is None and len(board.board.children) == 0 1152 self.emit("variation_undoing") 1153 1154 for vari in self.variations[1:]: 1155 if board in vari: 1156 break 1157 1158 board = board.board 1159 parent = board.prev.next 1160 1161 # If this is a one move only variation we have to remove the whole variation 1162 # if it's a longer one, just remove the latest move from it 1163 first_vari_moves = [child[1] for child in parent.children if not isinstance(child, str)] 1164 if board in first_vari_moves: 1165 self.remove_variation(board, parent) 1166 else: 1167 board.prev.next = None 1168 del vari[-1] 1169 1170 self.needsSave = True 1171 self.emit("variation_undone") 1172