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