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