1import re
2import asyncio
3
4from gi.repository import GObject
5
6from pychess.System.Log import log
7from pychess.Savers.pgn import msToClockTimeTag
8
9from pychess.Utils.const import WHITEWON, WON_RESIGN, WON_DISCONNECTION, WON_CALLFLAG, \
10    BLACKWON, WON_MATE, WON_ADJUDICATION, WON_KINGEXPLODE, WON_NOMATERIAL, UNKNOWN_REASON, \
11    DRAW, DRAW_REPETITION, DRAW_BLACKINSUFFICIENTANDWHITETIME, DRAW_WHITEINSUFFICIENTANDBLACKTIME, \
12    DRAW_INSUFFICIENT, DRAW_CALLFLAG, DRAW_AGREE, DRAW_STALEMATE, DRAW_50MOVES, DRAW_LENGTH, \
13    DRAW_ADJUDICATION, ADJOURNED, ADJOURNED_COURTESY_WHITE, ADJOURNED_COURTESY_BLACK, \
14    ADJOURNED_COURTESY, ADJOURNED_AGREEMENT, ADJOURNED_LOST_CONNECTION_WHITE, \
15    ADJOURNED_LOST_CONNECTION_BLACK, ADJOURNED_LOST_CONNECTION, ADJOURNED_SERVER_SHUTDOWN, \
16    ABORTED, ABORTED_AGREEMENT, ABORTED_DISCONNECTION, ABORTED_EARLY, ABORTED_SERVER_SHUTDOWN, \
17    ABORTED_ADJUDICATION, ABORTED_COURTESY, UNKNOWN_STATE, BLACK, WHITE, reprFile, \
18    FISCHERRANDOMCHESS, CRAZYHOUSECHESS, WILDCASTLECHESS, WILDCASTLESHUFFLECHESS, ATOMICCHESS, \
19    LOSERSCHESS, SUICIDECHESS, GIVEAWAYCHESS, reprResult
20
21from pychess.ic import IC_POS_OBSERVING_EXAMINATION, IC_POS_EXAMINATING, GAME_TYPES, IC_STATUS_PLAYING, \
22    BLKCMD_SEEK, BLKCMD_OBSERVE, BLKCMD_MATCH, TYPE_WILD, BLKCMD_SMOVES, BLKCMD_UNOBSERVE, BLKCMD_MOVES, \
23    BLKCMD_FLAG, parseRating
24
25from pychess.ic.FICSObjects import FICSGame, FICSBoard, FICSHistoryGame, \
26    FICSAdjournedGame, FICSJournalGame, FICSPlayer
27
28names = r"(\w+)"
29titles = r"((?:\((?:GM|IM|FM|WGM|WIM|WFM|TM|SR|TD|SR|CA|C|U|D|B|T|\*)\))+)?"
30ratedexp = "(rated|unrated)"
31ratings = r"\(\s*([0-9\ \-\+]{1,4}[P E]?|UNR)\)"
32
33weekdays = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
34months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct",
35          "Nov", "Dec"]
36
37# "Thu Oct 14, 20:36 PDT 2010"
38dates = r"(%s)\s+(%s)\s+(\d+),\s+(\d+):(\d+)\s+([A-Z\?]+)\s+(\d{4})" % \
39    ("|".join(weekdays), "|".join(months))
40
41# "2010-10-14 20:36 UTC"
42datesFatICS = r"(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})\s+(UTC)"
43
44moveListHeader1Str = "%s %s vs. %s %s --- (?:%s|%s)" % (
45    names, ratings, names, ratings, dates, datesFatICS)
46moveListHeader1 = re.compile(moveListHeader1Str)
47moveListHeader2Str = r"%s ([^ ]+) match, initial time: (\d+) minutes, increment: (\d+) seconds\." % \
48    ratedexp
49moveListHeader2 = re.compile(moveListHeader2Str, re.IGNORECASE)
50sanmove = "([a-hx@OoPKQRBN0-8+#=-]{2,7})"
51movetime = r"\((\d:)?(\d{1,2}):(\d\d)(?:\.(\d{1,3}))?\)"
52moveListMoves = re.compile(r"\s*(\d+)\. +(?:%s|\.\.\.) +%s *(?:%s +%s)?" %
53                           (sanmove, movetime, sanmove, movetime))
54
55creating0 = re.compile(
56    r"Creating: %s %s %s %s %s ([^ ]+) (\d+) (\d+)(?: \(adjourned\))?" %
57    (names, ratings, names, ratings, ratedexp))
58creating1 = re.compile(
59    r"{Game (\d+) \(%s vs\. %s\) (?:Creating|Continuing) %s ([^ ]+) match\." %
60    (names, names, ratedexp))
61pr = re.compile(r"<pr> ([\d ]+)")
62sr = re.compile(r"<sr> ([\d ]+)")
63
64fileToEpcord = (("a3", "b3", "c3", "d3", "e3", "f3", "g3", "h3"),
65                ("a6", "b6", "c6", "d6", "e6", "f6", "g6", "h6"))
66
67
68def parse_reason(result, reason, wname=None):
69    """
70    Parse the result value and reason line string for the reason and return
71    the result and reason the game ended.
72
73    result -- The result of the game, if known. It can be "None", but if it
74    is "DRAW", then wname must be supplied
75    """
76    if result in (WHITEWON, BLACKWON):
77        if "resigns" in reason:
78            reason = WON_RESIGN
79        elif "disconnection" in reason:
80            reason = WON_DISCONNECTION
81        elif "time" in reason:
82            reason = WON_CALLFLAG
83        elif "checkmated" in reason:
84            reason = WON_MATE
85        elif "adjudication" in reason:
86            reason = WON_ADJUDICATION
87        elif "exploded" in reason:
88            reason = WON_KINGEXPLODE
89        elif "material" in reason:
90            reason = WON_NOMATERIAL
91        else:
92            reason = UNKNOWN_REASON
93    elif result == DRAW:
94        assert wname is not None
95        if "repetition" in reason:
96            reason = DRAW_REPETITION
97        elif "material" in reason and "time" in reason:
98            if wname + " ran out of time" in reason:
99                reason = DRAW_BLACKINSUFFICIENTANDWHITETIME
100            else:
101                reason = DRAW_WHITEINSUFFICIENTANDBLACKTIME
102        elif "material" in reason:
103            reason = DRAW_INSUFFICIENT
104        elif "time" in reason:
105            reason = DRAW_CALLFLAG
106        elif "agreement" in reason:
107            reason = DRAW_AGREE
108        elif "stalemate" in reason:
109            reason = DRAW_STALEMATE
110        elif "50" in reason:
111            reason = DRAW_50MOVES
112        elif "length" in reason:
113            # FICS has a max game length on 800 moves
114            reason = DRAW_LENGTH
115        elif "adjudication" in reason:
116            reason = DRAW_ADJUDICATION
117        else:
118            reason = UNKNOWN_REASON
119    elif result == ADJOURNED or "adjourned" in reason:
120        result = ADJOURNED
121        if "courtesy" in reason:
122            if wname:
123                if wname in reason:
124                    reason = ADJOURNED_COURTESY_WHITE
125                else:
126                    reason = ADJOURNED_COURTESY_BLACK
127            elif "white" in reason:
128                reason = ADJOURNED_COURTESY_WHITE
129            elif "black" in reason:
130                reason = ADJOURNED_COURTESY_BLACK
131            else:
132                reason = ADJOURNED_COURTESY
133        elif "agreement" in reason:
134            reason = ADJOURNED_AGREEMENT
135        elif "connection" in reason:
136            if "white" in reason:
137                reason = ADJOURNED_LOST_CONNECTION_WHITE
138            elif "black" in reason:
139                reason = ADJOURNED_LOST_CONNECTION_BLACK
140            else:
141                reason = ADJOURNED_LOST_CONNECTION
142        elif "server" in reason:
143            reason = ADJOURNED_SERVER_SHUTDOWN
144        else:
145            reason = UNKNOWN_REASON
146    elif "aborted" in reason:
147        result = ABORTED
148        if "agreement" in reason:
149            reason = ABORTED_AGREEMENT
150        elif "moves" in reason:
151            # lost connection and too few moves; game aborted *
152            reason = ABORTED_DISCONNECTION
153        elif "move" in reason:
154            # Game aborted on move 1 *
155            reason = ABORTED_EARLY
156        elif "shutdown" in reason:
157            reason = ABORTED_SERVER_SHUTDOWN
158        elif "adjudication" in reason:
159            reason = ABORTED_ADJUDICATION
160        else:
161            reason = UNKNOWN_REASON
162    elif "courtesyadjourned" in reason:
163        result = ADJOURNED
164        reason = ADJOURNED_COURTESY
165    elif "courtesyaborted" in reason:
166        result = ABORTED
167        reason = ABORTED_COURTESY
168    else:
169        result = UNKNOWN_STATE
170        reason = UNKNOWN_REASON
171
172    return result, reason
173
174
175class BoardManager(GObject.GObject):
176
177    __gsignals__ = {
178        'playGameCreated': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
179        'obsGameCreated': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
180        'exGameCreated': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
181        'exGameReset': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
182        'gameUndoing': (GObject.SignalFlags.RUN_FIRST, None, (int, int)),
183        'archiveGamePreview': (GObject.SignalFlags.RUN_FIRST, None,
184                               (object, )),
185        'boardSetup': (GObject.SignalFlags.RUN_FIRST, None,
186                       (int, str, str, str)),
187        'timesUpdate': (GObject.SignalFlags.RUN_FIRST, None,
188                        (int, int, int,)),
189        'obsGameEnded': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
190        'curGameEnded': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
191        'obsGameUnobserved': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
192        'madeExamined': (GObject.SignalFlags.RUN_FIRST, None, (int, )),
193        'madeUnExamined': (GObject.SignalFlags.RUN_FIRST, None, (int, )),
194        'gamePaused': (GObject.SignalFlags.RUN_FIRST, None, (int, bool)),
195        'tooManySeeks': (GObject.SignalFlags.RUN_FIRST, None, ()),
196        'nonoWhileExamine': (GObject.SignalFlags.RUN_FIRST, None, ()),
197        'matchDeclined': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
198        'player_on_censor': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
199        'player_on_noplay': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
200        'player_lagged': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
201        'opp_not_out_of_time': (GObject.SignalFlags.RUN_FIRST, None, ()),
202        'req_not_fit_formula': (GObject.SignalFlags.RUN_FIRST, None,
203                                (object, str)),
204    }
205
206    castleSigns = {}
207    queuedStyle12s = {}
208
209    def __init__(self, connection):
210        GObject.GObject.__init__(self)
211        self.connection = connection
212        self.connection.expect_line(self.onStyle12, "<12> (.+)")
213        self.connection.expect_line(self.onWasPrivate,
214                                    r"Sorry, game (\d+) is a private game\.")
215        self.connection.expect_line(self.tooManySeeks,
216                                    "You can only have 3 active seeks.")
217        self.connection.expect_line(
218            self.nonoWhileExamine,
219            "(?:You cannot challenge while you are examining a game.)|" +
220            "(?:You are already examining a game.)")
221        self.connection.expect_line(self.matchDeclined,
222                                    "%s declines the match offer." % names)
223        self.connection.expect_line(self.player_on_censor,
224                                    "%s is censoring you." % names)
225        self.connection.expect_line(self.player_on_noplay,
226                                    "You are on %s's noplay list." % names)
227        self.connection.expect_line(
228            self.player_lagged,
229            r"Game (\d+): %s has lagged for (\d+) seconds\." % names)
230        self.connection.expect_line(
231            self.opp_not_out_of_time,
232            r"Opponent is not out of time, wait for server autoflag\.")
233
234        self.connection.expect_n_lines(
235            self.req_not_fit_formula,
236            "Match request does not fit formula for %s:" % names,
237            "%s's formula: (.+)" % names)
238
239        self.connection.expect_line(
240            self.on_game_remove,
241            r"\{Game (\d+) \(([A-Za-z]+) vs\. ([A-Za-z]+)\) ([A-Za-z']+ .+)\} (\*|1/2-1/2|1-0|0-1)$")
242
243        if self.connection.USCN:
244            self.connection.expect_n_lines(
245                self.onPlayGameCreated,
246                r"Creating: %s %s %s %s %s ([^ ]+) (\d+) (\d+)(?: \(adjourned\))?"
247                % (names, ratings, names, ratings, ratedexp), "",
248                r"{Game (\d+) \(%s vs\. %s\) (?:Creating|Continuing) %s ([^ ]+) match\."
249                % (names, names, ratedexp), "", "<12> (.+)")
250        else:
251            self.connection.expect_n_lines(
252                self.onPlayGameCreated,
253                r"Creating: %s %s %s %s %s ([^ ]+) (\d+) (\d+)(?: \(adjourned\))?"
254                % (names, ratings, names, ratings, ratedexp),
255                r"{Game (\d+) \(%s vs\. %s\) (?:Creating|Continuing) %s ([^ ]+) match\."
256                % (names, names, ratedexp), "", "<12> (.+)")
257
258        # TODO: Trying to precisely match every type of possible response FICS
259        # will throw at us for "Your seek matches..." or "Your seek qualifies
260        # for [player]'s getgame" is error prone and we can never be sure we
261        # even have all of the different types of replies the server will throw
262        # at us. So we should probably make it possible for multi-line
263        # prediction callbacks in VerboseTelnet to put lines the callback isn't
264        # interested in or doesn't handle back onto the input line stack in
265        # VerboseTelnet.TelnetLines
266        self.connection.expect_fromto(
267            self.onMatchingSeekOrGetGame,
268            r"Your seek (?:matches one already posted by %s|qualifies for %s's getgame)\."
269            % (names, names), "(?:<12>|<sn>) (.+)")
270        self.connection.expect_fromto(
271            self.onInterceptedChallenge,
272            r"Your challenge intercepts %s's challenge\." % names, "<12> (.+)")
273
274        if self.connection.USCN:
275            self.connection.expect_n_lines(self.onObserveGameCreated,
276                                           r"You are now observing game \d+\.",
277                                           '', "<12> (.+)")
278        else:
279            self.connection.expect_n_lines(
280                self.onObserveGameCreated, r"You are now observing game \d+\.",
281                r"Game (\d+): %s %s %s %s %s ([\w/]+) (\d+) (\d+)" %
282                (names, ratings, names, ratings, ratedexp), '', "<12> (.+)")
283
284        self.connection.expect_fromto(self.onObserveGameMovesReceived,
285                                      r"Movelist for game (\d+):",
286                                      r"{Still in progress} \*")
287
288        self.connection.expect_fromto(
289            self.onArchiveGameSMovesReceived,
290            moveListHeader1Str,
291            #                                       "\s*{((?:Game courtesyadjourned by (Black|White))|(?:Still in progress)|(?:Game adjourned by mutual agreement)|(?:(White|Black) lost connection; game adjourned)|(?:Game adjourned by ((?:server shutdown)|(?:adjudication)|(?:simul holder))))} \*")
292            r"\s*{.*(?:([Gg]ame.*adjourned.\s*)|(?:Still in progress)|(?:Neither.*)|(?:Game drawn.*)|(?:White.*)|(?:Black.*)).*}\s*(?:(?:1/2-1/2)|(?:1-0)|(?:0-1))?\s*")
293
294        self.connection.expect_line(
295            self.onGamePause, r"Game (\d+): Game clock (paused|resumed)\.")
296        self.connection.expect_line(
297            self.onUnobserveGame,
298            r"Removing game (\d+) from observation list\.")
299
300        self.connection.expect_line(
301            self.made_examined,
302            r"%s has made you an examiner of game (\d+)\." % names)
303
304        self.connection.expect_line(self.made_unexamined,
305                                    r"You are no longer examining game (\d+)\.")
306
307        self.connection.expect_n_lines(
308            self.onExamineGameCreated, r"Starting a game in examine \(scratch\) mode\.",
309            '', "<12> (.+)")
310
311        self.queuedEmits = {}
312        self.gamemodelStartedEvents = {}
313        self.theGameImPlaying = None
314        self.gamesImObserving = {}
315
316        # The ms ivar makes the remaining second fields in style12 use ms
317        self.connection.client.run_command("iset ms 1")
318        # Style12 is a must, when you don't want to parse visualoptimized stuff
319        self.connection.client.run_command("set style 12")
320        # When we observe fischer games, this puts a startpos in the movelist
321        self.connection.client.run_command("iset startpos 1")
322        # movecase ensures that bc3 will never be a bishop move
323        self.connection.client.run_command("iset movecase 1")
324        # don't unobserve games when we start a new game
325        self.connection.client.run_command("set unobserve 3")
326        self.connection.lvm.autoFlagNotify()
327
328        # gameinfo <g1> doesn't really have any interesting info, at least not
329        # until we implement crasyhouse and stuff
330        # self.connection.client.run_command("iset gameinfo 1")
331
332    def start(self):
333        self.connection.games.connect("FICSGameEnded", self.onGameEnd)
334
335    @classmethod
336    def parseStyle12(cls, line, castleSigns=None):
337        # <12> rnbqkb-r pppppppp -----n-- -------- ----P--- -------- PPPPKPPP RNBQ-BNR
338        # B -1 0 0 1 1 0 7 Newton Einstein 1 2 12 39 39 119 122 2 K/e1-e2 (0:06) Ke2 0
339        fields = line.split()
340
341        curcol = fields[8] == "B" and BLACK or WHITE
342        gameno = int(fields[15])
343        relation = int(fields[18])
344        lastmove = fields[28] != "none" and fields[28] or None
345        if lastmove is None:
346            ply = 0
347        else:
348            ply = int(fields[25]) * 2 - (curcol == WHITE and 2 or 1)
349        wname = fields[16]
350        bname = fields[17]
351        wms = int(fields[23])
352        bms = int(fields[24])
353        gain = int(fields[20])
354
355        # Board data
356        fenrows = []
357        for row in fields[:8]:
358            fenrow = []
359            spaceCounter = 0
360            for char in row:
361                if char == "-":
362                    spaceCounter += 1
363                else:
364                    if spaceCounter:
365                        fenrow.append(str(spaceCounter))
366                        spaceCounter = 0
367                    fenrow.append(char)
368            if spaceCounter:
369                fenrow.append(str(spaceCounter))
370            fenrows.append("".join(fenrow))
371
372        fen = "/".join(fenrows)
373        fen += " "
374
375        # Current color
376        fen += fields[8].lower()
377        fen += " "
378
379        # Castling
380        if fields[10:14] == ["0", "0", "0", "0"]:
381            fen += "-"
382        else:
383            if fields[10] == "1":
384                fen += castleSigns[0].upper()
385            if fields[11] == "1":
386                fen += castleSigns[1].upper()
387            if fields[12] == "1":
388                fen += castleSigns[0].lower()
389            if fields[13] == "1":
390                fen += castleSigns[1].lower()
391        fen += " "
392        # 1 0 1 1 when short castling k1 last possibility
393
394        # En passant
395        if fields[9] == "-1":
396            fen += "-"
397        else:
398            fen += fileToEpcord[1 - curcol][int(fields[9])]
399        fen += " "
400
401        # Half move clock
402        fen += str(max(int(fields[14]), 0))
403        fen += " "
404
405        # Standard chess numbering
406        fen += fields[25]
407
408        return gameno, relation, curcol, ply, wname, bname, wms, bms, gain, lastmove, fen
409
410    def onStyle12(self, match):
411        style12 = match.groups()[0]
412        log.debug("onStyle12: %s" % style12)
413        gameno = int(style12.split()[15])
414        if gameno in self.queuedStyle12s:
415            self.queuedStyle12s[gameno].append(style12)
416            return
417
418        try:
419            self.gamemodelStartedEvents[gameno].wait()
420        except KeyError:
421            pass
422
423        if gameno in self.castleSigns:
424            castleSigns = self.castleSigns[gameno]
425        else:
426            castleSigns = ("k", "q")
427        gameno, relation, curcol, ply, wname, bname, wms, bms, gain, lastmove, fen = \
428            self.parseStyle12(style12, castleSigns)
429
430        # examine starts with a <12> line only
431        if lastmove is None and relation == IC_POS_EXAMINATING:
432            pgnHead = [
433                ("Event", "FICS examined game"), ("Site", "freechess.org"),
434                ("White", wname), ("Black", bname), ("Result", "*"),
435                ("SetUp", "1"), ("FEN", fen)
436            ]
437            pgn = "\n".join(['[%s "%s"]' % line for line in pgnHead]) + "\n*\n"
438            wplayer = self.connection.players.get(wname)
439            bplayer = self.connection.players.get(bname)
440
441            if self.connection.examined_game is None:
442                # examine an archived game from GUI
443                if self.connection.archived_examine is not None:
444                    no_smoves = False
445                    game = self.connection.archived_examine
446                    game.gameno = int(gameno)
447                    game.relation = relation
448                    # game.game_type = GAME_TYPES["examined"]
449                    log.debug("Start examine an existing game by %s" % style12,
450                              extra={"task": (self.connection.username, "BM.onStyle12")})
451                else:
452                    # examine from console or got mexamine in observed game
453                    no_smoves = True
454                    game = FICSGame(wplayer,
455                                    bplayer,
456                                    gameno=int(gameno),
457                                    game_type=GAME_TYPES["examined"],
458                                    minutes=0,
459                                    inc=0,
460                                    board=FICSBoard(0,
461                                                    0,
462                                                    pgn=pgn),
463                                    relation=relation)
464                    log.debug("Start new examine game by %s" % style12,
465                              extra={"task": (self.connection.username, "BM.onStyle12")})
466
467                game = self.connection.games.get(game)
468                self.connection.examined_game = game
469
470                # don't start another new game when someone (some human examiner or lecturebot/endgamebot)
471                # changes our relation in an already started game from IC_POS_OBSERVING_EXAMINATION to
472                # IC_POS_EXAMINATING
473                if game.relation == IC_POS_OBSERVING_EXAMINATION:
474                    game.relation = relation  # IC_POS_EXAMINATING
475                    # print("IC_POS_OBSERVING_EXAMINATION --> IC_POS_EXAMINATING")
476                    # before this change server sent an unobserve to us
477                    # and it removed gameno from started events dict
478                    # we have to put it back...
479                    self.gamemodelStartedEvents[game.gameno] = asyncio.Event()
480                    return
481
482            else:
483                # don't start new game in puzzlebot/endgamebot when they just reuse gameno
484                log.debug("emit('boardSetup') with %s %s %s %s" % (gameno, fen, wname, bname),
485                          extra={"task": (self.connection.username, "BM.onStyle12")})
486                self.emit("boardSetup", gameno, fen, wname, bname)
487                return
488
489            game.relation = relation
490            game.board = FICSBoard(0, 0, pgn=pgn)
491            self.gamesImObserving[game] = wms, bms
492
493            # start a new game now or after smoves
494            self.gamemodelStartedEvents[game.gameno] = asyncio.Event()
495            if no_smoves:
496                log.debug("emit('exGameCreated')",
497                          extra={"task": (self.connection.username, "BM.onStyle12")})
498                self.emit("exGameCreated", game)
499                self.gamemodelStartedEvents[game.gameno].wait()
500            else:
501                log.debug("send 'smoves' command",
502                          extra={"task": (self.connection.username, "BM.onStyle12")})
503                if isinstance(game, FICSHistoryGame):
504                    self.connection.client.run_command("smoves %s %s" % (
505                        self.connection.history_owner, game.history_no))
506                elif isinstance(game, FICSJournalGame):
507                    self.connection.client.run_command("smoves %s %%%s" % (
508                        self.connection.journal_owner, game.journal_no))
509                elif isinstance(game, FICSAdjournedGame):
510                    self.connection.client.run_command("smoves %s %s" % (
511                        self.connection.stored_owner, game.opponent.name))
512                self.connection.client.run_command("forward 999")
513        else:
514            if gameno in self.connection.games.games_by_gameno:
515                game = self.connection.games.get_game_by_gameno(gameno)
516                if wms < 0 or bms < 0:
517                    # fics resend latest style12 line again when one player lost on time
518                    return
519                if lastmove is None:
520                    log.debug("emit('boardSetup') with %s %s %s %s" % (gameno, fen, wname, bname),
521                              extra={"task": (self.connection.username, "BM.onStyle12")})
522                    self.emit("boardSetup", gameno, fen, wname, bname)
523                else:
524                    log.debug("put move %s into game.move_queue" % lastmove,
525                              extra={"task": (self.connection.username, "BM.onStyle12")})
526                    game.move_queue.put_nowait((gameno, ply, curcol, lastmove, fen, wname, bname, wms, bms))
527            else:
528                # In some cases (like lost on time) the last move is resent by FICS
529                # but game was already removed from self.connection.games
530                log.debug("Got %s but %s not in connection.games" % (style12, gameno))
531
532    def onExamineGameCreated(self, matchlist):
533        style12 = matchlist[-1].groups()[0]
534        gameno = int(style12.split()[15])
535
536        castleSigns = self.generateCastleSigns(style12, GAME_TYPES["examined"])
537        self.castleSigns[gameno] = castleSigns
538        gameno, relation, curcol, ply, wname, bname, wms, bms, gain, lastmove, fen = \
539            self.parseStyle12(style12, castleSigns)
540
541        pgnHead = [
542            ("Event", "FICS examined game"), ("Site", "freechess.org"),
543            ("White", wname), ("Black", bname), ("Result", "*"),
544            ("SetUp", "1"), ("FEN", fen)
545        ]
546        pgn = "\n".join(['[%s "%s"]' % line for line in pgnHead]) + "\n*\n"
547        wplayer = self.connection.players.get(wname)
548        bplayer = self.connection.players.get(bname)
549
550        game = FICSGame(wplayer,
551                        bplayer,
552                        gameno=int(gameno),
553                        game_type=GAME_TYPES["examined"],
554                        minutes=0,
555                        inc=0,
556                        board=FICSBoard(0,
557                                        0,
558                                        pgn=pgn),
559                        relation=relation)
560        log.debug("Starting a game in examine (scratch) mode.",
561                  extra={"task": (self.connection.username, "BM.onExamineGameCreated")})
562        self.connection.examined_game = game
563        game = self.connection.games.get(game)
564
565        game.relation = relation
566        game.board = FICSBoard(0, 0, pgn=pgn)
567        self.gamesImObserving[game] = wms, bms
568
569        # start a new game now
570        self.gamemodelStartedEvents[game.gameno] = asyncio.Event()
571        log.debug("emit('exGameCreated')",
572                  extra={"task": (self.connection.username, "BM.onExamineGameCreated")})
573        self.emit("exGameCreated", game)
574        self.gamemodelStartedEvents[game.gameno].wait()
575
576    def onGameModelStarted(self, gameno):
577        self.gamemodelStartedEvents[gameno].set()
578
579    def onWasPrivate(self, match):
580        # When observable games were added to the list later than the latest
581        # full send, private information will not be known.
582        gameno = int(match.groups()[0])
583        try:
584            game = self.connection.games.get_game_by_gameno(gameno)
585        except KeyError:
586            return
587        game.private = True
588
589    onWasPrivate.BLKCMD = BLKCMD_OBSERVE
590
591    def tooManySeeks(self, match):
592        self.emit("tooManySeeks")
593
594    tooManySeeks.BLKCMD = BLKCMD_SEEK
595
596    def nonoWhileExamine(self, match):
597        self.emit("nonoWhileExamine")
598
599    nonoWhileExamine.BLKCMD = BLKCMD_SEEK
600
601    def matchDeclined(self, match):
602        decliner, = match.groups()
603        decliner = self.connection.players.get(decliner)
604        self.emit("matchDeclined", decliner)
605
606    @classmethod
607    def generateCastleSigns(cls, style12, game_type):
608        if game_type.variant_type == FISCHERRANDOMCHESS:
609            backrow = style12.split()[0]
610            leftside = backrow.find("r")
611            rightside = backrow.find("r", leftside + 1)
612            return (reprFile[rightside], reprFile[leftside])
613        else:
614            return ("k", "q")
615
616    def onPlayGameCreated(self, matchlist):
617        log.debug(
618            "'%s' '%s' '%s'" %
619            (matchlist[0].string, matchlist[1].string, matchlist[-1].string),
620            extra={"task": (self.connection.username, "BM.onPlayGameCreated")})
621        wname, wrating, bname, brating, rated, match_type, minutes, inc = matchlist[
622            0].groups()
623        item = 2 if self.connection.USCN else 1
624        gameno, wname, bname, rated, match_type = matchlist[item].groups()
625        gameno = int(gameno)
626        wrating = parseRating(wrating)
627        brating = parseRating(brating)
628        rated = rated == "rated"
629        game_type = GAME_TYPES[match_type]
630
631        wplayer = self.connection.players.get(wname)
632        bplayer = self.connection.players.get(bname)
633        for player, rating in ((wplayer, wrating), (bplayer, brating)):
634            if player.ratings[game_type.rating_type] != rating:
635                player.ratings[game_type.rating_type] = rating
636                player.emit("ratings_changed", game_type.rating_type, player)
637
638        style12 = matchlist[-1].groups()[0]
639        castleSigns = self.generateCastleSigns(style12, game_type)
640        self.castleSigns[gameno] = castleSigns
641        gameno, relation, curcol, ply, wname, bname, wms, bms, gain, lastmove, fen = \
642            self.parseStyle12(style12, castleSigns)
643
644        game = FICSGame(wplayer,
645                        bplayer,
646                        gameno=gameno,
647                        rated=rated,
648                        game_type=game_type,
649                        minutes=int(minutes),
650                        inc=int(inc),
651                        board=FICSBoard(wms,
652                                        bms,
653                                        fen=fen))
654
655        game = self.connection.games.get(game)
656
657        for player in (wplayer, bplayer):
658            if player.status != IC_STATUS_PLAYING:
659                player.status = IC_STATUS_PLAYING
660            if player.game != game:
661                player.game = game
662
663        self.theGameImPlaying = game
664        self.gamemodelStartedEvents[gameno] = asyncio.Event()
665        self.connection.client.run_command("follow")
666        self.emit("playGameCreated", game)
667
668    def onMatchingSeekOrGetGame(self, matchlist):
669        if matchlist[-1].string.startswith("<12>"):
670            for line in matchlist[1:-4]:
671                if line.startswith("<sr>"):
672                    self.connection.glm.on_seek_remove(sr.match(line))
673                elif line.startswith("<pr>"):
674                    self.connection.om.onOfferRemove(pr.match(line))
675            self.onPlayGameCreated((creating0.match(matchlist[
676                -4]), creating1.match(matchlist[-3]), matchlist[-1]))
677        else:
678            self.connection.glm.on_seek_add(matchlist[-1])
679
680    onMatchingSeekOrGetGame.BLKCMD = BLKCMD_SEEK
681
682    def onInterceptedChallenge(self, matchlist):
683        self.onMatchingSeekOrGetGame(matchlist)
684
685    onInterceptedChallenge.BLKCMD = BLKCMD_MATCH
686
687    def parseGame(self, matchlist, gameclass, in_progress=False, gameno=None):
688        """
689        Parses the header and movelist for an observed or stored game from its
690        matchlist (an re.match object) into a gameclass (FICSGame or subclass
691        of) object.
692
693        in_progress - should be True for an observed game matchlist, and False
694        for stored/adjourned games
695        """
696        # ################   observed game movelist example:
697        #        Movelist for game 64:
698        #
699        #        Ajido (2281) vs. IMgooeyjim (2068) --- Thu Oct 14, 20:36 PDT 2010
700        #        Rated standard match, initial time: 15 minutes, increment: 3 seconds.
701        #
702        #        Move  Ajido                   IMgooeyjim
703        #        ----  ---------------------   ---------------------
704        #          1.  d4      (0:00.000)      Nf6     (0:00.000)
705        #          2.  c4      (0:04.061)      g6      (0:00.969)
706        #          3.  Nc3     (0:13.280)      Bg7     (0:06.422)
707        #              {Still in progress} *
708        #
709        # #################   stored game example:
710        #        BwanaSlei (1137) vs. mgatto (1336) --- Wed Nov  5, 20:56 PST 2008
711        #        Rated blitz match, initial time: 5 minutes, increment: 0 seconds.
712        #
713        #        Move  BwanaSlei               mgatto
714        #        ----  ---------------------   ---------------------
715        #        1.  e4      (0:00.000)      c5      (0:00.000)
716        #        2.  d4      (0:05.750)      cxd4    (0:03.020)
717        #        ...
718        #        23.  Qxf3    (1:05.500)
719        #             {White lost connection; game adjourned} *
720        #
721        # ################# stored wild/3 game with style12:
722        #        kurushi (1626) vs. mgatto (1627) --- Thu Nov  4, 10:33 PDT 2010
723        #        Rated wild/3 match, initial time: 3 minutes, increment: 0 seconds.
724        #
725        #        <12> nqbrknrn pppppppp -------- -------- -------- -------- PPPPPPPP NQBRKNRN W -1 0 0 0 0 0 17 kurushi mgatto -4 3 0 39 39 169403 45227 1 none (0:00.000) none 0 1 0
726        #
727        #        Move  kurushi                 mgatto
728        #        ----  ---------------------   ---------------------
729        #          1.  Nb3     (0:00.000)      d5      (0:00.000)
730        #          2.  Nhg3    (0:00.386)      e5      (0:03.672)
731        #         ...
732        #         28.  Rxd5    (0:00.412)
733        #              {Black lost connection; game adjourned} *
734        #
735        # #################  stored game movelist following stored game(s):
736        #        Stored games for mgatto:
737        #        C Opponent       On Type          Str  M    ECO Date
738        #        1: W BabyLurking     Y [ br  5   0] 29-13 W27  D37 Fri Nov  5, 04:41 PDT 2010
739        #        2: W gbtami          N [ wr  5   0] 32-34 W14  --- Thu Oct 21, 00:14 PDT 2010
740        #
741        #        mgatto (1233) vs. BabyLurking (1455) --- Fri Nov  5, 04:33 PDT 2010
742        #        Rated blitz match, initial time: 5 minutes, increment: 0 seconds.
743        #
744        #        Move  mgatto             BabyLurking
745        #        ----  ----------------   ----------------
746        #        1.  Nf3     (0:00)     d5      (0:00)
747        #        2.  d4      (0:03)     Nf6     (0:00)
748        #        3.  c4      (0:03)     e6      (0:00)
749        #        {White lost connection; game adjourned} *
750        #
751        # ################## stored game movelist following stored game(s):
752        # ##   Note: A wild stored game in this format won't be parseable into a board because
753        # ##   it doesn't come with a style12 that has the start position, so we warn and return
754        # ##################
755        #        Stored games for mgatto:
756        #        C Opponent       On Type          Str  M    ECO Date
757        #        1: W gbtami          N [ wr  5   0] 32-34 W14  --- Thu Oct 21, 00:14 PDT 2010
758        #
759        #        mgatto (1627) vs. gbtami (1881) --- Thu Oct 21, 00:10 PDT 2010
760        #        Rated wild/fr match, initial time: 5 minutes, increment: 0 seconds.
761        #
762        #        Move  mgatto             gbtami
763        #        ----  ----------------   ----------------
764        #        1.  d4      (0:00)     b6      (0:00)
765        #        2.  b3      (0:06)     d5      (0:03)
766        #        3.  c4      (0:08)     e6      (0:03)
767        #        4.  e3      (0:04)     dxc4    (0:02)
768        #        5.  bxc4    (0:02)     g6      (0:09)
769        #        6.  Nd3     (0:12)     Bg7     (0:02)
770        #        7.  Nc3     (0:10)     Ne7     (0:03)
771        #        8.  Be2     (0:08)     c5      (0:05)
772        #        9.  a4      (0:07)     cxd4    (0:38)
773        #        10.  exd4    (0:06)     Bxd4    (0:03)
774        #        11.  O-O     (0:10)     Qc6     (0:06)
775        #        12.  Bf3     (0:16)     Qxc4    (0:04)
776        #        13.  Bxa8    (0:03)     Rxa8    (0:14)
777        #        {White lost connection; game adjourned} *
778        #
779        # #################   other reasons the game could be stored/adjourned:
780        #        Game courtesyadjourned by (Black|White)
781        #        Still in progress                    # This one must be a FICS bug
782        #        Game adjourned by mutual agreement
783        #        (White|Black) lost connection; game adjourned
784        #        Game adjourned by ((server shutdown)|(adjudication)|(simul holder))
785
786        index = 0
787        if in_progress:
788            gameno = int(matchlist[index].groups()[0])
789            index += 2
790        header1 = matchlist[index] if isinstance(matchlist[index], str) \
791            else matchlist[index].group()
792
793        matches = moveListHeader1.match(header1).groups()
794        wname, wrating, bname, brating = matches[:4]
795        if self.connection.FatICS:
796            year, month, day, hour, minute, timezone = matches[11:]
797        else:
798            weekday, month, day, hour, minute, timezone, year = matches[4:11]
799            month = months.index(month) + 1
800
801        wrating = parseRating(wrating)
802        brating = parseRating(brating)
803        rated, game_type, minutes, increment = \
804            moveListHeader2.match(matchlist[index + 1]).groups()
805        minutes = int(minutes)
806        increment = int(increment)
807        game_type = GAME_TYPES[game_type]
808
809        reason = matchlist[-1].group().lower()
810        if in_progress:
811            result = None
812            result_str = "*"
813        elif "1-0" in reason:
814            result = WHITEWON
815            result_str = "1-0"
816        elif "0-1" in reason:
817            result = BLACKWON
818            result_str = "0-1"
819        elif "1/2-1/2" in reason:
820            result = DRAW
821            result_str = "1/2-1/2"
822        else:
823            result = ADJOURNED
824            result_str = "*"
825        result, reason = parse_reason(result, reason, wname=wname)
826
827        index += 3
828        if matchlist[index].startswith("<12>"):
829            style12 = matchlist[index][5:]
830            castleSigns = self.generateCastleSigns(style12, game_type)
831            gameno, relation, curcol, ply, wname, bname, wms, bms, gain, lastmove, \
832                fen = self.parseStyle12(style12, castleSigns)
833            initialfen = fen
834            movesstart = index + 4
835        else:
836            if game_type.rating_type == TYPE_WILD:
837                # we need a style12 start position to correctly parse a wild/* board
838                log.error("BoardManager.parseGame: no style12 for %s board." %
839                          game_type.fics_name)
840                return None
841            castleSigns = ("k", "q")
842            initialfen = None
843            movesstart = index + 2
844
845        if in_progress:
846            self.castleSigns[gameno] = castleSigns
847
848        moves = {}
849        times = {}
850        wms = bms = minutes * 60 * 1000
851
852        for line in matchlist[movesstart:-1]:
853            if not moveListMoves.match(line):
854                log.error("BoardManager.parseGame: unmatched line: \"%s\"" %
855                          repr(line))
856                raise Exception("BoardManager.parseGame: unmatched line: \"%s\"" % repr(line))
857            moveno, wmove, whour, wmin, wsec, wmsec, bmove, bhour, bmin, bsec, bmsec = \
858                moveListMoves.match(line).groups()
859            whour = 0 if whour is None else int(whour[0])
860            bhour = 0 if bhour is None else int(bhour[0])
861            ply = int(moveno) * 2 - 2
862            if wmove:
863                moves[ply] = wmove
864                wms -= (int(whour) * 60 * 60 * 1000) + (
865                    int(wmin) * 60 * 1000) + (int(wsec) * 1000)
866                if wmsec is not None:
867                    wms -= int(wmsec)
868                else:
869                    wmsec = 0
870                if increment > 0:
871                    wms += (increment * 1000)
872                times[ply] = "%01d:%02d:%02d.%03d" % (int(whour), int(wmin),
873                                                      int(wsec), int(wmsec))
874            if bmove:
875                moves[ply + 1] = bmove
876                bms -= (int(bhour) * 60 * 60 * 1000) + (
877                    int(bmin) * 60 * 1000) + (int(bsec) * 1000)
878                if bmsec is not None:
879                    bms -= int(bmsec)
880                else:
881                    bmsec = 0
882                if increment > 0:
883                    bms += (increment * 1000)
884                times[ply + 1] = "%01d:%02d:%02d.%03d" % (
885                    int(bhour), int(bmin), int(bsec), int(bmsec))
886
887        if in_progress and gameno in self.queuedStyle12s:
888            # Apply queued board updates
889            for style12 in self.queuedStyle12s[gameno]:
890                gameno, relation, curcol, ply, wname, bname, wms, bms, gain, lastmove, fen = \
891                    self.parseStyle12(style12, castleSigns)
892                if lastmove is None:
893                    continue
894                moves[ply - 1] = lastmove
895                # Updated the queuedMoves in case there has been a takeback
896                for moveply in list(moves.keys()):
897                    if moveply > ply - 1:
898                        del moves[moveply]
899            del self.queuedStyle12s[gameno]
900
901        pgnHead = [
902            ("Event", "FICS %s %s game" %
903             (rated.lower(), game_type.fics_name)),
904            ("Site", "freechess.org"),
905            ("White", wname),
906            ("Black", bname),
907            ("TimeControl", "%d+%d" % (minutes * 60, increment)),
908            ("Result", result_str),
909            ("WhiteClock", msToClockTimeTag(wms)),
910            ("BlackClock", msToClockTimeTag(bms)),
911        ]
912        if wrating != 0:
913            pgnHead += [("WhiteElo", wrating)]
914        if brating != 0:
915            pgnHead += [("BlackElo", brating)]
916        if year and month and day and hour and minute:
917            pgnHead += [
918                ("Date", "%04d.%02d.%02d" % (int(year), int(month), int(day))),
919                ("Time", "%02d:%02d:00" % (int(hour), int(minute))),
920            ]
921        if initialfen:
922            pgnHead += [("SetUp", "1"), ("FEN", initialfen)]
923        if game_type.variant_type == FISCHERRANDOMCHESS:
924            pgnHead += [("Variant", "Fischerandom")]
925            # FR is the only variant used in this tag by the PGN generator @
926            # ficsgames.org. They put all the other wild/* stuff only in the
927            # "Event" header.
928        elif game_type.variant_type == CRAZYHOUSECHESS:
929            pgnHead += [("Variant", "Crazyhouse")]
930        elif game_type.variant_type in (WILDCASTLECHESS,
931                                        WILDCASTLESHUFFLECHESS):
932            pgnHead += [("Variant", "Wildcastle")]
933        elif game_type.variant_type == ATOMICCHESS:
934            pgnHead += [("Variant", "Atomic")]
935        elif game_type.variant_type == LOSERSCHESS:
936            pgnHead += [("Variant", "Losers")]
937        elif game_type.variant_type == SUICIDECHESS:
938            pgnHead += [("Variant", "Suicide")]
939        elif game_type.variant_type == GIVEAWAYCHESS:
940            pgnHead += [("Variant", "Giveaway")]
941        pgn = "\n".join(['[%s "%s"]' % line for line in pgnHead]) + "\n"
942
943        moves = sorted(moves.items())
944        for ply, move in moves:
945            if ply % 2 == 0:
946                pgn += "%d. " % (ply // 2 + 1)
947            time = times[ply]
948            pgn += "%s {[%%emt %s]} " % (move, time)
949        pgn += "*\n"
950
951        wplayer = self.connection.players.get(wname)
952        bplayer = self.connection.players.get(bname)
953        for player, rating in ((wplayer, wrating), (bplayer, brating)):
954            if player.ratings[game_type.rating_type] != rating:
955                player.ratings[game_type.rating_type] = rating
956                player.emit("ratings_changed", game_type.rating_type, player)
957        game = gameclass(wplayer,
958                         bplayer,
959                         game_type=game_type,
960                         result=result,
961                         rated=(rated.lower() == "rated"),
962                         minutes=minutes,
963                         inc=increment,
964                         board=FICSBoard(wms,
965                                         bms,
966                                         pgn=pgn))
967
968        if in_progress:
969            game.gameno = gameno
970        else:
971            if gameno is not None:
972                game.gameno = gameno
973            game.reason = reason
974        game = self.connection.games.get(game, emit=False)
975
976        return game
977
978    def on_game_remove(self, match):
979        gameno, wname, bname, comment, result = match.groups()
980        result, reason = parse_reason(
981            reprResult.index(result),
982            comment,
983            wname=wname)
984
985        try:
986            wplayer = self.connection.players.get(wname)
987            wplayer.restore_previous_status()
988            # no status update will be sent by
989            # FICS if the player doesn't become available, so we restore
990            # previous status first (not necessarily true, but the best guess)
991        except KeyError:
992            print("%s not in self.connections.players - creating" % wname)
993            wplayer = FICSPlayer(wname)
994
995        try:
996            bplayer = self.connection.players.get(bname)
997            bplayer.restore_previous_status()
998        except KeyError:
999            print("%s not in self.connections.players - creating" % bname)
1000            bplayer = FICSPlayer(bname)
1001
1002        game = FICSGame(wplayer,
1003                        bplayer,
1004                        gameno=int(gameno),
1005                        result=result,
1006                        reason=reason)
1007        if wplayer.game is not None:
1008            game.rated = wplayer.game.rated
1009        game = self.connection.games.get(game, emit=False)
1010        self.connection.games.game_ended(game)
1011        # Do this last to give anybody connected to the game's signals a chance
1012        # to disconnect from them first
1013        wplayer.game = None
1014        bplayer.game = None
1015
1016    def onObserveGameCreated(self, matchlist):
1017        log.debug("'%s'" % (matchlist[1].string),
1018                  extra={"task": (self.connection.username,
1019                                  "BM.onObserveGameCreated")})
1020        if self.connection.USCN:
1021            # TODO? is this ok?
1022            game_type = GAME_TYPES["blitz"]
1023            castleSigns = ("k", "q")
1024        else:
1025            gameno, wname, wrating, bname, brating, rated, gametype, minutes, inc = matchlist[
1026                1].groups()
1027            wrating = parseRating(wrating)
1028            brating = parseRating(brating)
1029            game_type = GAME_TYPES[gametype]
1030
1031        style12 = matchlist[-1].groups()[0]
1032
1033        castleSigns = self.generateCastleSigns(style12, game_type)
1034        gameno, relation, curcol, ply, wname, bname, wms, bms, gain, lastmove, fen = \
1035            self.parseStyle12(style12, castleSigns)
1036        gameno = int(gameno)
1037        self.castleSigns[gameno] = castleSigns
1038
1039        wplayer = self.connection.players.get(wname)
1040        bplayer = self.connection.players.get(bname)
1041
1042        if relation == IC_POS_OBSERVING_EXAMINATION:
1043            pgnHead = [
1044                ("Event", "FICS %s %s game" % (rated, game_type.fics_name)),
1045                ("Site", "freechess.org"), ("White", wname), ("Black", bname),
1046                ("Result", "*"), ("SetUp", "1"), ("FEN", fen)
1047            ]
1048            pgn = "\n".join(['[%s "%s"]' % line for line in pgnHead]) + "\n*\n"
1049            game = FICSGame(wplayer,
1050                            bplayer,
1051                            gameno=gameno,
1052                            rated=rated == "rated",
1053                            game_type=game_type,
1054                            minutes=int(minutes),
1055                            inc=int(inc),
1056                            board=FICSBoard(wms,
1057                                            bms,
1058                                            pgn=pgn),
1059                            relation=relation)
1060            game = self.connection.games.get(game)
1061
1062            # when puzzlebot reuses same gameno for starting next puzzle
1063            # sometimes no unexamine sent by server, so we have to set None to
1064            # self.connection.examined_game to guide self.onStyle12() a bit...
1065            if self.connection.examined_game is not None and \
1066                    self.connection.examined_game.gameno == gameno:
1067                log.debug("BM.onObserveGameCreated: exGameReset emitted; self.connection.examined_game = %s" % gameno)
1068                self.emit("exGameReset", self.connection.examined_game)
1069                self.connection.examined_game = None
1070
1071            game.relation = relation  # IC_POS_OBSERVING_EXAMINATION
1072            self.gamesImObserving[game] = wms, bms
1073
1074            self.gamemodelStartedEvents[game.gameno] = asyncio.Event()
1075            # puzzlebot sometimes creates next puzzle with same wplayer,bplayer,gameno
1076            game.move_queue = asyncio.Queue()
1077            self.emit("obsGameCreated", game)
1078            self.gamemodelStartedEvents[game.gameno].wait()
1079        else:
1080            game = FICSGame(wplayer,
1081                            bplayer,
1082                            gameno=gameno,
1083                            rated=rated == "rated",
1084                            game_type=game_type,
1085                            minutes=int(minutes),
1086                            inc=int(inc),
1087                            relation=relation)
1088            game = self.connection.games.get(game, emit=False)
1089
1090            if not game.supported:
1091                log.warning("Trying to follow an unsupported type game %s" %
1092                            game.game_type)
1093                return
1094
1095            if game.gameno in self.gamemodelStartedEvents:
1096                log.warning("%s already in gamemodelstartedevents" %
1097                            game.gameno)
1098                return
1099
1100            self.gamesImObserving[game] = wms, bms
1101            self.queuedStyle12s[game.gameno] = []
1102            self.queuedEmits[game.gameno] = []
1103            self.gamemodelStartedEvents[game.gameno] = asyncio.Event()
1104
1105            # FICS doesn't send the move list after 'observe' and 'follow' commands
1106            self.connection.client.run_command("moves %d" % game.gameno)
1107
1108    onObserveGameCreated.BLKCMD = BLKCMD_OBSERVE
1109
1110    def onObserveGameMovesReceived(self, matchlist):
1111        log.debug("'%s'" % (matchlist[0].string),
1112                  extra={"task": (self.connection.username,
1113                                  "BM.onObserveGameMovesReceived")})
1114        game = self.parseGame(matchlist, FICSGame, in_progress=True)
1115        if game.gameno not in self.gamemodelStartedEvents:
1116            return
1117        if game.gameno not in self.queuedEmits:
1118            return
1119        self.emit("obsGameCreated", game)
1120        try:
1121            self.gamemodelStartedEvents[game.gameno].wait()
1122        except KeyError:
1123            pass
1124
1125        for emit in self.queuedEmits[game.gameno]:
1126            emit()
1127        del self.queuedEmits[game.gameno]
1128
1129        wms, bms = self.gamesImObserving[game]
1130        self.emit("timesUpdate", game.gameno, wms, bms)
1131
1132    onObserveGameMovesReceived.BLKCMD = BLKCMD_MOVES
1133
1134    def onArchiveGameSMovesReceived(self, matchlist):
1135        log.debug("'%s'" % (matchlist[0].string),
1136                  extra={"task": (self.connection.username,
1137                                  "BM.onArchiveGameSMovesReceived")})
1138        klass = FICSAdjournedGame if "adjourn" in matchlist[-1].group(
1139        ) else FICSHistoryGame
1140        if self.connection.examined_game is not None:
1141            gameno = self.connection.examined_game.gameno
1142        else:
1143            gameno = None
1144        game = self.parseGame(matchlist,
1145                              klass,
1146                              in_progress=False,
1147                              gameno=gameno)
1148        if game.gameno not in self.gamemodelStartedEvents:
1149            self.emit("archiveGamePreview", game)
1150            return
1151        game.relation = IC_POS_EXAMINATING
1152        game.game_type = GAME_TYPES["examined"]
1153        self.emit("exGameCreated", game)
1154        try:
1155            self.gamemodelStartedEvents[game.gameno].wait()
1156        except KeyError:
1157            pass
1158
1159    onArchiveGameSMovesReceived.BLKCMD = BLKCMD_SMOVES
1160
1161    def onGameEnd(self, games, game):
1162        log.debug("BM.onGameEnd: %s" % game)
1163        if game == self.theGameImPlaying:
1164            if game.gameno in self.gamemodelStartedEvents:
1165                self.gamemodelStartedEvents[game.gameno].wait()
1166            self.emit("curGameEnded", game)
1167            self.theGameImPlaying = None
1168            if game.gameno in self.gamemodelStartedEvents:
1169                del self.gamemodelStartedEvents[game.gameno]
1170
1171        elif game in self.gamesImObserving:
1172            log.debug("BM.onGameEnd: %s: gamesImObserving" % game)
1173            if game.gameno in self.queuedEmits:
1174                log.debug("BM.onGameEnd: %s: queuedEmits" % game)
1175                self.queuedEmits[game.gameno].append(
1176                    lambda: self.emit("obsGameEnded", game))
1177            else:
1178                if game.gameno in self.gamemodelStartedEvents:
1179                    self.gamemodelStartedEvents[game.gameno].wait()
1180                del self.gamesImObserving[game]
1181                self.emit("obsGameEnded", game)
1182
1183    def onGamePause(self, match):
1184        gameno, state = match.groups()
1185        gameno = int(gameno)
1186        if gameno in self.queuedEmits:
1187            self.queuedEmits[gameno].append(
1188                lambda: self.emit("gamePaused", gameno, state == "paused"))
1189        else:
1190            if gameno in self.gamemodelStartedEvents:
1191                self.gamemodelStartedEvents[gameno].wait()
1192            self.emit("gamePaused", gameno, state == "paused")
1193
1194    def onUnobserveGame(self, match):
1195        gameno = int(match.groups()[0])
1196        log.debug("BM.onUnobserveGame: gameno: %s" % gameno)
1197        try:
1198            del self.gamemodelStartedEvents[gameno]
1199            game = self.connection.games.get_game_by_gameno(gameno)
1200        except KeyError:
1201            return
1202        self.emit("obsGameUnobserved", game)
1203        # TODO: delete self.castleSigns[gameno] ?
1204
1205    onUnobserveGame.BLKCMD = BLKCMD_UNOBSERVE
1206
1207    def player_lagged(self, match):
1208        gameno, player, num_seconds = match.groups()
1209        player = self.connection.players.get(player)
1210        self.emit("player_lagged", player)
1211
1212    def opp_not_out_of_time(self, match):
1213        self.emit("opp_not_out_of_time")
1214
1215    opp_not_out_of_time.BLKCMD = BLKCMD_FLAG
1216
1217    def req_not_fit_formula(self, matchlist):
1218        player, formula = matchlist[1].groups()
1219        player = self.connection.players.get(player)
1220        self.emit("req_not_fit_formula", player, formula)
1221
1222    req_not_fit_formula.BLKCMD = BLKCMD_MATCH
1223
1224    def player_on_censor(self, match):
1225        player, = match.groups()
1226        player = self.connection.players.get(player)
1227        self.emit("player_on_censor", player)
1228
1229    player_on_censor.BLKCMD = BLKCMD_MATCH
1230
1231    def player_on_noplay(self, match):
1232        player, = match.groups()
1233        player = self.connection.players.get(player)
1234        self.emit("player_on_noplay", player)
1235
1236    player_on_noplay.BLKCMD = BLKCMD_MATCH
1237
1238    def made_examined(self, match):
1239        """ Changing from observer to examiner """
1240        player, gameno = match.groups()
1241        gameno = int(gameno)
1242        try:
1243            self.connection.games.get_game_by_gameno(gameno)
1244        except KeyError:
1245            return
1246        self.emit("madeExamined", gameno)
1247
1248    def made_unexamined(self, match):
1249        """ You are no longer examine game """
1250        log.debug("BM.made_unexamined(): exGameReset emitted")
1251        self.emit("exGameReset", self.connection.examined_game)
1252        self.connection.examined_game = None
1253        gameno, = match.groups()
1254        gameno = int(gameno)
1255        try:
1256            self.connection.games.get_game_by_gameno(gameno)
1257        except KeyError:
1258            return
1259        self.emit("madeUnExamined", gameno)
1260
1261    ############################################################################
1262    #   Interacting                                                            #
1263    ############################################################################
1264
1265    def isPlaying(self):
1266        return self.theGameImPlaying is not None
1267
1268    def sendMove(self, move):
1269        self.connection.client.run_command(move)
1270
1271    def resign(self):
1272        self.connection.client.run_command("resign")
1273
1274    def callflag(self):
1275        self.connection.client.run_command("flag")
1276
1277    def observe(self, game, player=None):
1278        if game is not None:
1279            self.connection.client.run_command("observe %d" % game.gameno)
1280        elif player is not None:
1281            self.connection.client.run_command("observe %s" % player.name)
1282
1283    def follow(self, player):
1284        self.connection.client.run_command("follow %s" % player.name)
1285
1286    def unexamine(self):
1287        self.connection.client.run_command("unexamine")
1288
1289    def unobserve(self, game):
1290        if game.gameno is not None:
1291            self.connection.client.run_command("unobserve %d" % game.gameno)
1292
1293    def play(self, seekno):
1294        self.connection.client.run_command("play %s" % seekno)
1295
1296    def accept(self, offerno):
1297        self.connection.client.run_command("accept %s" % offerno)
1298
1299    def decline(self, offerno):
1300        self.connection.client.run_command("decline %s" % offerno)
1301
1302
1303if __name__ == "__main__":
1304    from pychess.ic.FICSConnection import Connection
1305    con = Connection("", "", True, "", "")
1306    bm = BoardManager(con)
1307
1308    print(bm._BoardManager__parseStyle12(
1309        "rkbrnqnb pppppppp -------- -------- -------- -------- PPPPPPPP RKBRNQNB W -1 1 1 1 1 0 161 GuestNPFS GuestMZZK -1 2 12 39 39 120 120 1 none (0:00) none 1 0 0",
1310        ("d", "a")))
1311
1312    print(bm._BoardManager__parseStyle12(
1313        "rnbqkbnr pppp-ppp -------- ----p--- ----PP-- -------- PPPP--PP RNBQKBNR B 5 1 1 1 1 0 241 GuestGFFC GuestNXMP -4 2 12 39 39 120000 120000 1 none (0:00.000) none 0 0 0",
1314        ("k", "q")))
1315