async def analysis_move(app, user, game, move, fen, ply): invalid_move = False board = FairyBoard(game.variant, fen, game.chess960) try: # san = board.get_san(move) lastmove = move board.push(move) dests, promotions = get_dests(board) check = board.is_checked() except Exception: invalid_move = True log.exception("!!! analysis_move() exception occured") if invalid_move: analysis_board_response = game.get_board(full=True) else: analysis_board_response = { "type": "analysis_board", "gameId": game.id, "fen": board.fen, "ply": ply, "lastMove": lastmove, "dests": dests, "promo": promotions, "check": check, } ws = user.game_sockets[game.id] await ws.send_json(analysis_board_response)
def sanitize_fen(variant, initial_fen, chess960): # Initial_fen needs validation to prevent segfaulting in pyffish sanitized_fen = initial_fen start_fen = sf.start_fen(variant) # self.board.start_fen(self.variant) start = start_fen.split() init = initial_fen.split() # Cut off tail if len(init) > 6: init = init[:6] sanitized_fen = " ".join(init) # We need starting color invalid0 = len(init) < 2 # Only piece types listed in variant start position can be used later if variant == "makruk" or variant == "cambodian": non_piece = "~+0123456789[]fF" else: non_piece = "~+0123456789[]" invalid1 = any((c not in start[0] + non_piece for c in init[0])) # Required number of rows invalid2 = start[0].count("/") != init[0].count("/") # Accept zh FEN in lichess format (they use / instead if [] for pockets) if invalid2 and variant == "crazyhouse": if (init[0].count("/") == 8) and ("[" not in init[0]) and ("]" not in init[0]): k = init[0].rfind("/") init[0] = init[0][:k] + "[" + init[0][k + 1:] + "]" sanitized_fen = " ".join(init) invalid2 = False # Allowed starting colors invalid3 = len(init) > 1 and init[1] not in "bw" # Castling rights (and piece virginity) check invalid4 = False if variant == "seirawan" or variant == "shouse": invalid4 = len(init) > 2 and any((c not in "KQABCDEFGHkqabcdefgh-" for c in init[2])) elif chess960: if all((c in "KQkq-" for c in init[2])): chess960 = False else: invalid4 = len(init) > 2 and any((c not in "ABCDEFGHIJabcdefghij-" for c in init[2])) elif variant[-5:] != "shogi": invalid4 = len(init) > 2 and any((c not in start[2] + "-" for c in init[2])) # Castling right need rooks and king placed in starting square if not invalid4: rows = init[0].split("/") backRankB = rows[1] if (variant == 'shako') else rows[0] backRankW = rows[-2] if (variant == 'shako') else rows[-1] rookPosQ = 1 if (variant == 'shako') else 0 rookPosK = -2 if (variant == 'shako') else -1 if ("q" in init[2] and backRankB[rookPosQ] != 'r') or \ ("k" in init[2] and backRankB[rookPosK] != 'r') or \ ("Q" in init[2] and backRankW[rookPosQ] != 'R') or \ ("K" in init[2] and backRankW[rookPosK] != 'R'): invalid4 = True # Number of kings invalid5 = init[0].count("k") != 1 or init[0].count("K") != 1 # Opp king already in check curr_color = init[1] opp_color = "w" if curr_color == "b" else "b" init[1] = init[1].replace(curr_color, opp_color) board = FairyBoard(variant, " ".join(init), chess960) invalid6 = board.is_checked() if invalid0 or invalid1 or invalid2 or invalid3 or invalid4 or invalid5 or invalid6: print(invalid0, invalid1, invalid2, invalid3, invalid4, invalid5, invalid6) sanitized_fen = start_fen return False, start_fen else: return True, sanitized_fen
def sanitize_fen(variant, initial_fen, chess960): # Prevent this particular one to fail on our general sastling check if variant == "capablanca" and initial_fen == CONSERVATIVE_CAPA_FEN: return True, initial_fen # Initial_fen needs validation to prevent segfaulting in pyffish sanitized_fen = initial_fen start_fen = sf.start_fen(variant) # self.board.start_fen(self.variant) start = start_fen.split() init = initial_fen.split() # Cut off tail if len(init) > 6: init = init[:6] sanitized_fen = " ".join(init) # We need starting color invalid0 = len(init) < 2 # Only piece types listed in variant start position can be used later if variant == "dobutsu": non_piece = "~+0123456789[]hH-" elif variant == "orda": non_piece = "~+0123456789[]qH-" else: non_piece = "~+0123456789[]-" invalid1 = any((c not in start[0] + non_piece for c in init[0])) # Required number of rows invalid2 = start[0].count("/") != init[0].count("/") # Accept zh FEN in lichess format (they use / instead if [] for pockets) if invalid2 and variant == "crazyhouse": if (init[0].count("/") == 8) and ("[" not in init[0]) and ("]" not in init[0]): k = init[0].rfind("/") init[0] = init[0][:k] + "[" + init[0][k + 1:] + "]" sanitized_fen = " ".join(init) invalid2 = False # Allowed starting colors invalid3 = len(init) > 1 and init[1] not in "bw" # Castling rights (and piece virginity) check invalid4 = False if len(init) > 2: if variant in ("seirawan", "shouse"): invalid4 = any((c not in "KQABCDEFGHkqabcdefgh-" for c in init[2])) elif chess960: if all((c in "KQkq-" for c in init[2])): chess960 = False else: invalid4 = any((c not in "ABCDEFGHIJabcdefghij-" for c in init[2])) elif variant[-5:] != "shogi" and variant not in ("dobutsu", "gorogoro", "gorogoroplus"): invalid4 = any((c not in start[2] + "-" for c in init[2])) # Castling right need rooks and king placed in starting square if (not invalid2) and (not invalid4) and not (chess960 and (variant in ("seirawan", "shouse"))): rows = init[0].split("/") backRankB = rows[1] if (variant == 'shako') else rows[0] backRankW = rows[-2] if (variant == 'shako') else rows[-1] # cut off pockets k = backRankW.rfind("[") if k > 0: backRankW = backRankW[:k] rookPosQ = 1 if (variant == 'shako') else 0 rookPosK = -2 if (variant == 'shako') else -1 if ("q" in init[2] and backRankB[rookPosQ] != 'r') or \ ("k" in init[2] and backRankB[rookPosK] != 'r') or \ ("Q" in init[2] and backRankW[rookPosQ] != 'R') or \ ("K" in init[2] and backRankW[rookPosK] != 'R'): invalid4 = True # Number of kings bking = "l" if variant == "dobutsu" else "k" wking = "L" if variant == "dobutsu" else "K" invalid5 = init[0].count(bking) != 1 or init[0].count(wking) != 1 # Opp king already in check curr_color = init[1] opp_color = "w" if curr_color == "b" else "b" init[1] = init[1].replace(curr_color, opp_color) board = FairyBoard(variant, " ".join(init), chess960) invalid6 = board.is_checked() if invalid0 or invalid1 or invalid2 or invalid3 or invalid4 or invalid5 or invalid6: print(invalid0, invalid1, invalid2, invalid3, invalid4, invalid5, invalid6) sanitized_fen = start_fen return False, start_fen return True, sanitized_fen
class Game: def __init__(self, app, gameId, variant, initial_fen, wplayer, bplayer, base=1, inc=0, byoyomi_period=0, level=0, rated=CASUAL, chess960=False, create=True, tournamentId=None): self.app = app self.db = app["db"] if "db" in app else None self.users = app["users"] self.games = app["games"] self.highscore = app["highscore"] self.db_crosstable = app["crosstable"] self.saved = False self.remove_task = None self.variant = variant self.initial_fen = initial_fen self.wplayer = wplayer self.bplayer = bplayer self.rated = rated self.base = base self.inc = inc self.level = level if level is not None else 0 self.tournamentId = tournamentId self.chess960 = chess960 self.create = create self.imported_by = "" self.berserk_time = self.base * 1000 * 30 self.browser_title = "%s • %s vs %s" % ( variant_display_name(self.variant + ("960" if self.chess960 else "")).title(), self.wplayer.username, self.bplayer.username) # rating info self.white_rating = wplayer.get_rating(variant, chess960) self.wrating = "%s%s" % self.white_rating.rating_prov self.wrdiff = 0 self.black_rating = bplayer.get_rating(variant, chess960) self.brating = "%s%s" % self.black_rating.rating_prov self.brdiff = 0 # crosstable info self.need_crosstable_save = False self.bot_game = self.bplayer.bot or self.wplayer.bot if self.bot_game or self.wplayer.anon or self.bplayer.anon: self.crosstable = "" else: if self.wplayer.username < self.bplayer.username: self.s1player = self.wplayer.username self.s2player = self.bplayer.username else: self.s1player = self.bplayer.username self.s2player = self.wplayer.username self.ct_id = self.s1player + "/" + self.s2player self.crosstable = self.db_crosstable.get(self.ct_id, { "_id": self.ct_id, "s1": 0, "s2": 0, "r": [] }) self.spectators = set() self.draw_offers = set() self.rematch_offers = set() self.messages = collections.deque([], MAX_CHAT_LINES) self.date = datetime.now(timezone.utc) self.ply_clocks = [{ "black": (base * 1000 * 60) + 0 if base > 0 else inc * 1000, "white": (base * 1000 * 60) + 0 if base > 0 else inc * 1000, "movetime": 0 }] self.dests = {} self.promotions = [] self.lastmove = None self.check = False self.status = CREATED self.result = "*" self.last_server_clock = monotonic() self.id = gameId # Makruk manual counting use_manual_counting = self.variant in ("makruk", "makpong", "cambodian") self.manual_count = use_manual_counting and not self.bot_game self.manual_count_toggled = [] # Calculate the start of manual counting count_started = 0 if self.manual_count: count_started = -1 if self.initial_fen: parts = self.initial_fen.split() board_state = parts[0] side_to_move = parts[1] counting_limit = int( parts[3]) if len(parts) >= 4 and parts[3].isdigit() else 0 counting_ply = int(parts[4]) if len(parts) >= 5 else 0 move_number = int(parts[5]) if len(parts) >= 6 else 0 white_pieces = sum(1 for c in board_state if c.isupper()) black_pieces = sum(1 for c in board_state if c.islower()) if counting_limit > 0 and counting_ply > 0: if white_pieces <= 1 or black_pieces <= 1: # Disable manual count if either side is already down to lone king count_started = 0 self.manual_count = False else: last_ply = 2 * move_number - (2 if side_to_move == 'w' else 1) count_started = last_ply - counting_ply + 1 if count_started < 1: # Move number is too small for the current count count_started = 0 self.manual_count = False else: counting_player = self.bplayer if counting_ply % 2 == 0 else self.wplayer self.draw_offers.add(counting_player.username) disabled_fen = "" if self.chess960 and self.initial_fen and self.create: if self.wplayer.fen960_as_white == self.initial_fen: disabled_fen = self.initial_fen self.initial_fen = "" self.board = FairyBoard(self.variant, self.initial_fen, self.chess960, count_started, disabled_fen) # Janggi setup needed when player is not BOT if self.variant == "janggi": if self.initial_fen: self.bsetup = False self.wsetup = False else: self.bsetup = not self.bplayer.bot self.wsetup = not self.wplayer.bot if self.bplayer.bot: self.board.janggi_setup("b") self.overtime = False self.byoyomi = byoyomi_period > 0 self.byoyomi_period = byoyomi_period # Remaining byoyomi periods by players self.byoyomi_periods = { "white": byoyomi_period, "black": byoyomi_period } # On page refresh we have to add extra byoyomi times gained by current player to report correct clock time # We adjust this in "byoyomi" messages in wsr.py self.byo_correction = 0 self.initial_fen = self.board.initial_fen self.wplayer.fen960_as_white = self.initial_fen self.random_mover = self.wplayer.username == "Random-Mover" or self.bplayer.username == "Random-Mover" self.random_move = "" self.set_dests() if self.board.move_stack: self.check = self.board.is_checked() self.steps = [{ "fen": self.initial_fen if self.initial_fen else self.board.initial_fen, "san": None, "turnColor": "black" if self.board.color == BLACK else "white", "check": self.check }] self.stopwatch = Clock(self) if not self.bplayer.bot: self.bplayer.game_in_progress = self.id if not self.wplayer.bot: self.wplayer.game_in_progress = self.id self.wberserk = False self.bberserk = False self.move_lock = asyncio.Lock() def berserk(self, color): if color == "white" and not self.wberserk: self.wberserk = True self.ply_clocks[0]["white"] = self.berserk_time elif color == "black" and not self.bberserk: self.bberserk = True self.ply_clocks[0]["black"] = self.berserk_time async def play_move(self, move, clocks=None, ply=None): self.stopwatch.stop() self.byo_correction = 0 if self.status > STARTED: return if self.status == CREATED: self.status = STARTED self.app["g_cnt"][0] += 1 response = {"type": "g_cnt", "cnt": self.app["g_cnt"][0]} await lobby_broadcast(self.app["lobbysockets"], response) cur_player = self.bplayer if self.board.color == BLACK else self.wplayer opp_player = self.wplayer if self.board.color == BLACK else self.bplayer # Move cancels draw offer response = reject_draw(self, opp_player.username) if response is not None: await round_broadcast(self, self.app["users"], response, full=True) cur_time = monotonic() # BOT players doesn't send times used for moves if self.bot_game: movetime = int(round((cur_time - self.last_server_clock) * 1000)) # print(self.board.ply, move, movetime) if clocks is None: clocks = { "white": self.ply_clocks[-1]["white"], "black": self.ply_clocks[-1]["black"], "movetime": movetime } if cur_player.bot and self.board.ply >= 2: cur_color = "black" if self.board.color == BLACK else "white" if self.byoyomi: if self.overtime: clocks[cur_color] = self.inc * 1000 else: clocks[cur_color] = max( 0, self.clocks[cur_color] - movetime) else: clocks[cur_color] = max( 0, self.clocks[cur_color] - movetime + (self.inc * 1000)) if clocks[cur_color] == 0: if self.byoyomi and self.byoyomi_periods[cur_color] > 0: self.overtime = True clocks[cur_color] = self.inc * 1000 self.byoyomi_periods[cur_color] -= 1 else: w, b = self.board.insufficient_material() if (w and b) or (cur_color == "black" and w) or (cur_color == "white" and b): result = "1/2-1/2" else: result = "1-0" if self.board.color == BLACK else "0-1" self.update_status(FLAG, result) print(self.result, "flag") await self.save_game() else: if (ply is not None ) and ply <= 2 and self.tournamentId is not None: # Just in case for move and berserk messages race if self.wberserk: clocks["white"] = self.berserk_time if self.bberserk: clocks["black"] = self.berserk_time self.last_server_clock = cur_time if self.status <= STARTED: try: san = self.board.get_san(move) self.lastmove = move self.board.push(move) self.ply_clocks.append(clocks) self.set_dests() self.update_status() # Stop manual counting when the king is bared if self.board.count_started != 0: board_state = self.board.fen.split()[0] white_pieces = sum(1 for c in board_state if c.isupper()) black_pieces = sum(1 for c in board_state if c.islower()) if white_pieces <= 1 or black_pieces <= 1: if self.board.count_started > 0: self.stop_manual_count() self.board.count_started = 0 if self.status > STARTED: await self.save_game() self.steps.append({ "fen": self.board.fen, "move": move, "san": san, "turnColor": "black" if self.board.color == BLACK else "white", "check": self.check }) self.stopwatch.restart() except Exception: log.exception("ERROR: Exception in game %s play_move() %s", self.id, move) result = "1-0" if self.board.color == BLACK else "0-1" self.update_status(INVALIDMOVE, result) await self.save_game() # TODO: this causes random game abort if False: # not self.bot_game: # print("--------------ply-", ply) # print(self.board.color, clocks, self.ply_clocks) opp_color = self.steps[-1]["turnColor"] if clocks[opp_color] < self.ply_clocks[ ply - 1][opp_color] and self.status <= STARTED: self.update_status(ABORTED) await self.save_game(with_clocks=True) async def save_game(self, with_clocks=False): if self.saved: return self.saved = True if self.rated == IMPORTED: log.exception("Save IMPORTED game %s ???", self.id) return self.stopwatch.clock_task.cancel() try: await self.stopwatch.clock_task except asyncio.CancelledError: pass if self.board.ply > 0: self.app["g_cnt"][0] -= 1 response = {"type": "g_cnt", "cnt": self.app["g_cnt"][0]} await lobby_broadcast(self.app["lobbysockets"], response) async def remove(keep_time): # Keep it in our games dict a little to let players get the last board # not to mention that BOT players want to abort games after 20 sec inactivity await asyncio.sleep(keep_time) try: del self.games[self.id] except KeyError: log.info("Failed to del %s from games", self.id) if self.bot_game: try: if self.wplayer.bot: del self.wplayer.game_queues[self.id] if self.bplayer.bot: del self.bplayer.game_queues[self.id] except KeyError: log.info("Failed to del %s from game_queues", self.id) self.remove_task = asyncio.create_task(remove(KEEP_TIME)) if self.board.ply < 3 and (self.db is not None) and (self.tournamentId is None): result = await self.db.game.delete_one({"_id": self.id}) log.debug("Removed too short game %s from db. Deleted %s game.", self.id, result.deleted_count) else: if self.result != "*": if self.rated == RATED: await self.update_ratings() if (not self.bot_game) and (not self.wplayer.anon) and ( not self.bplayer.anon): await self.save_crosstable() if self.tournamentId is not None: try: await self.app["tournaments"][self.tournamentId ].game_update(self) except Exception: log.exception("Exception in tournament game_update()") # self.print_game() new_data = { "d": self.date, "f": self.board.fen, "s": self.status, "r": R2C[self.result], 'm': encode_moves( map(grand2zero, self.board.move_stack) if self.variant in GRANDS else self.board.move_stack, self.variant) } if self.rated == RATED and self.result != "*": new_data["p0"] = self.p0 new_data["p1"] = self.p1 # Janggi game starts with a prelude phase to set up horses and elephants, so # initial FEN may be different compared to one we used when db game document was created if self.variant == "janggi": new_data["if"] = self.board.initial_fen if with_clocks: new_data["clocks"] = self.ply_clocks if self.tournamentId is not None: new_data["wb"] = self.wberserk new_data["bb"] = self.bberserk if self.manual_count: if self.board.count_started > 0: self.manual_count_toggled.append( (self.board.count_started, self.board.ply + 1)) new_data["mct"] = self.manual_count_toggled if self.db is not None: await self.db.game.find_one_and_update({"_id": self.id}, {"$set": new_data}) def set_crosstable(self): if self.bot_game or self.wplayer.anon or self.bplayer.anon or self.board.ply < 3 or self.result == "*": return if len(self.crosstable["r"] ) > 0 and self.crosstable["r"][-1].startswith(self.id): log.info("Crosstable was already updated with %s result", self.id) return if self.result == "1/2-1/2": s1 = s2 = 5 tail = "=" elif (self.result == "1-0" and self.s1player == self.wplayer.username ) or (self.result == "0-1" and self.s1player == self.bplayer.username): s1 = 10 s2 = 0 tail = "+" else: s1 = 0 s2 = 10 tail = "-" self.crosstable["s1"] += s1 self.crosstable["s2"] += s2 self.crosstable["r"].append("%s%s" % (self.id, tail)) self.crosstable["r"] = self.crosstable["r"][-20:] new_data = { "_id": self.ct_id, "s1": self.crosstable["s1"], "s2": self.crosstable["s2"], "r": self.crosstable["r"], } self.db_crosstable[self.ct_id] = new_data self.need_crosstable_save = True async def save_crosstable(self): if not self.need_crosstable_save: log.info("Crosstable update for %s was already saved to mongodb", self.id) return new_data = { "s1": self.crosstable["s1"], "s2": self.crosstable["s2"], "r": self.crosstable["r"], } try: await self.db.crosstable.find_one_and_update({"_id": self.ct_id}, {"$set": new_data}, upsert=True) except Exception: if self.db is not None: log.error("Failed to save new crosstable to mongodb!") self.need_crosstable_save = False def get_highscore(self, variant, chess960): len_hs = len(self.highscore[variant + ("960" if chess960 else "")]) if len_hs > 0: return (self.highscore[variant + ("960" if chess960 else "")].peekitem()[1], len_hs) return (0, 0) async def set_highscore(self, variant, chess960, value): self.highscore[variant + ("960" if chess960 else "")].update(value) # We have to preserve previous top 10! # See test_win_and_in_then_lost_and_out() in test.py # if len(self.highscore[variant + ("960" if chess960 else "")]) > MAX_HIGH_SCORE: # self.highscore[variant + ("960" if chess960 else "")].popitem() new_data = { "scores": dict(self.highscore[variant + ("960" if chess960 else "")].items()[:10]) } try: await self.db.highscore.find_one_and_update( {"_id": variant + ("960" if chess960 else "")}, {"$set": new_data}, upsert=True) except Exception: if self.db is not None: log.error("Failed to save new highscore to mongodb!") async def update_ratings(self): if self.result == '1-0': (white_score, black_score) = (1.0, 0.0) elif self.result == '1/2-1/2': (white_score, black_score) = (0.5, 0.5) elif self.result == '0-1': (white_score, black_score) = (0.0, 1.0) else: raise RuntimeError('game.result: unexpected result code') wr = gl2.rate(self.white_rating, [(white_score, self.black_rating)]) br = gl2.rate(self.black_rating, [(black_score, self.white_rating)]) # print("ratings after updated:", wr, br) await self.wplayer.set_rating(self.variant, self.chess960, wr) await self.bplayer.set_rating(self.variant, self.chess960, br) self.wrdiff = int(round(wr.mu - self.white_rating.mu, 0)) self.p0 = {"e": self.wrating, "d": self.wrdiff} self.brdiff = int(round(br.mu - self.black_rating.mu, 0)) self.p1 = {"e": self.brating, "d": self.brdiff} w_nb = self.wplayer.perfs[self.variant + ("960" if self.chess960 else "")]["nb"] if w_nb >= HIGHSCORE_MIN_GAMES: await self.set_highscore( self.variant, self.chess960, {self.wplayer.username: int(round(wr.mu, 0))}) b_nb = self.bplayer.perfs[self.variant + ("960" if self.chess960 else "")]["nb"] if b_nb >= HIGHSCORE_MIN_GAMES: await self.set_highscore( self.variant, self.chess960, {self.bplayer.username: int(round(br.mu, 0))}) def update_status(self, status=None, result=None): if self.status > STARTED: return def result_string_from_value(color, game_result_value): if game_result_value < 0: return "1-0" if color == BLACK else "0-1" if game_result_value > 0: return "0-1" if color == BLACK else "1-0" return "1/2-1/2" if status is not None: self.status = status if result is not None: self.result = result self.set_crosstable() if not self.bplayer.bot: self.bplayer.game_in_progress = None if not self.wplayer.bot: self.wplayer.game_in_progress = None return if self.board.move_stack: self.check = self.board.is_checked() w, b = self.board.insufficient_material() if w and b: # print("1/2 by board.insufficient_material()") self.status = DRAW self.result = "1/2-1/2" if not self.dests: game_result_value = self.board.game_result() self.result = result_string_from_value(self.board.color, game_result_value) if self.board.is_immediate_game_end()[0]: self.status = VARIANTEND # print(self.result, "variant end") elif self.check: self.status = MATE if self.variant == 'atomic' and game_result_value == 0: # If Fairy game_result() is 0 it is not mate but stalemate self.status = STALEMATE # Draw if the checkmating player is the one counting if self.board.count_started > 0: counting_side = 'b' if self.board.count_started % 2 == 0 else 'w' if self.result == ("1-0" if counting_side == 'w' else "0-1"): self.status = DRAW self.result = "1/2-1/2" # Pawn drop mate # TODO: remove this when https://github.com/ianfab/Fairy-Stockfish/issues/48 resolves if self.board.move_stack[-1][0:2] == "P@" and self.variant in ( "shogi", "minishogi", "gorogoro", "gorogoroplus"): self.status = INVALIDMOVE # print(self.result, "checkmate") else: self.status = STALEMATE # print(self.result, "stalemate") elif self.variant in ('makruk', 'makpong', 'cambodian', 'sittuyin', 'asean'): parts = self.board.fen.split() if parts[3].isdigit(): counting_limit = int(parts[3]) counting_ply = int(parts[4]) if counting_ply > counting_limit: self.status = DRAW self.result = "1/2-1/2" # print(self.result, "counting limit reached") else: # end the game by 50 move rule and repetition automatically # for non-draw results and bot games is_game_end, game_result_value = self.board.is_optional_game_end() if is_game_end and (game_result_value != 0 or (self.wplayer.bot or self.bplayer.bot)): self.result = result_string_from_value(self.board.color, game_result_value) self.status = CLAIM if game_result_value != 0 else DRAW # print(self.result, "claim") if self.board.ply > MAX_PLY: self.status = DRAW self.result = "1/2-1/2" # print(self.result, "Ply %s reached" % MAX_PLY) if self.status > STARTED: self.set_crosstable() if not self.bplayer.bot: self.bplayer.game_in_progress = None if not self.wplayer.bot: self.wplayer.game_in_progress = None def set_dests(self): dests = {} promotions = [] moves = self.board.legal_moves() # print("self.board.legal_moves()", moves) if self.random_mover: self.random_move = random.choice(moves) if moves else "" # print("RM: %s" % self.random_move) for move in moves: # chessgroundx key uses ":" for tenth rank if self.variant in GRANDS: move = move.replace("10", ":") source, dest = move[0:2], move[2:4] if source in dests: dests[source].append(dest) else: dests[source] = [dest] if not move[-1].isdigit(): if not (self.variant in ("seirawan", "shouse") and (move[1] == '1' or move[1] == '8')): promotions.append(move) if self.variant in ("kyotoshogi", "chennis") and move[0] == "+": promotions.append(move) self.dests = dests self.promotions = promotions def print_game(self): print(self.pgn) print(self.board.print_pos()) # print(self.board.move_stack) # print("---CLOCKS---") # for ply, clocks in enumerate(self.ply_clocks): # print(ply, self.board.move_stack[ply - 1] if ply > 0 else "", self.ply_clocks[ply]["movetime"], self.ply_clocks[ply]["black"], self.ply_clocks[ply]["white"]) # print(self.result) @property def pgn(self): try: mlist = sf.get_san_moves(self.variant, self.initial_fen, self.board.move_stack, self.chess960, sf.NOTATION_SAN) except Exception: log.exception("ERROR: Exception in game %s pgn()", self.id) mlist = self.board.move_stack moves = " ".join( (move if ind % 2 == 1 else "%s. %s" % (((ind + 1) // 2) + 1, move) for ind, move in enumerate(mlist))) no_setup = self.initial_fen == self.board.start_fen( "chess") and not self.chess960 # Use lichess format for crazyhouse games to support easy import setup_fen = self.initial_fen if self.variant != "crazyhouse" else self.initial_fen.replace( "[]", "") tc = "-" if self.base + self.inc == 0 else "%s+%s" % (int( self.base * 60), self.inc) return '[Event "{}"]\n[Site "{}"]\n[Date "{}"]\n[Round "-"]\n[White "{}"]\n[Black "{}"]\n[Result "{}"]\n[TimeControl "{}"]\n[WhiteElo "{}"]\n[BlackElo "{}"]\n[Variant "{}"]\n{fen}{setup}\n{} {}\n'.format( "PyChess " + ("rated" if self.rated == RATED else "casual" if self.rated == CASUAL else "imported") + " game", URI + "/" + self.id, self.date.strftime("%Y.%m.%d"), self.wplayer.username, self.bplayer.username, self.result, tc, self.wrating, self.brating, self.variant.capitalize() if not self.chess960 else VARIANT_960_TO_PGN[self.variant], moves, self.result, fen="" if no_setup else '[FEN "%s"]\n' % setup_fen, setup="" if no_setup else '[SetUp "1"]\n') @property def uci_usi(self): if self.variant[-5:] == "shogi": mirror = mirror9 if self.variant == "shogi" else mirror5 return "position sfen %s moves %s" % ( self.board.initial_sfen, " ".join( map(uci2usi, map(mirror, self.board.move_stack)))) return "position fen %s moves %s" % (self.board.initial_fen, " ".join( self.board.move_stack)) @property def clocks(self): return self.ply_clocks[-1] @property def is_claimable_draw(self): return self.board.is_claimable_draw() @property def spectator_list(self): return spectators(self) def analysis_start(self, username): return '{"type": "analysisStart", "username": "******", "game": {"id": "%s", "skill_level": "%s", "chess960": "%s"}}\n' % ( username, self.id, self.level, self.chess960) @property def game_start(self): return '{"type": "gameStart", "game": {"id": "%s", "skill_level": "%s", "chess960": "%s"}}\n' % ( self.id, self.level, self.chess960) @property def game_end(self): return '{"type": "gameEnd", "game": {"id": "%s"}}\n' % self.id @property def game_full(self): return '{"type": "gameFull", "id": "%s", "variant": {"name": "%s"}, "white": {"name": "%s"}, "black": {"name": "%s"}, "initialFen": "%s", "state": %s}\n' % ( self.id, self.variant, self.wplayer.username, self.bplayer.username, self.initial_fen, self.game_state[:-1]) @property def game_state(self): clocks = self.clocks return '{"type": "gameState", "moves": "%s", "wtime": %s, "btime": %s, "winc": %s, "binc": %s}\n' % ( " ".join(self.board.move_stack), clocks["white"], clocks["black"], self.inc, self.inc) async def abort(self): self.update_status(ABORTED) await self.save_game() return { "type": "gameEnd", "status": self.status, "result": "Game aborted.", "gameId": self.id, "pgn": self.pgn } async def game_ended(self, user, reason): """ Abort, resign, flag, abandone """ if self.result == "*": if reason == "abort": result = "*" elif self.variant == "janggi" and self.wsetup and reason == "flag": # In Janggi game the second player (red) failed to do the setup phase in time result = "1-0" else: if reason == "flag": w, b = self.board.insufficient_material() if (w and b) or (self.board.color == BLACK and w) or (self.board.color == WHITE and b): result = "1/2-1/2" else: result = "0-1" if user.username == self.wplayer.username else "1-0" else: result = "0-1" if user.username == self.wplayer.username else "1-0" self.update_status(LOSERS[reason], result) await self.save_game() return { "type": "gameEnd", "status": self.status, "result": self.result, "gameId": self.id, "pgn": self.pgn, "ct": self.crosstable, "rdiffs": { "brdiff": self.brdiff, "wrdiff": self.wrdiff } if self.status > STARTED and self.rated == RATED else "" } def start_manual_count(self): if self.manual_count: cur_player = self.bplayer if self.board.color == BLACK else self.wplayer opp_player = self.wplayer if self.board.color == BLACK else self.bplayer self.draw_offers.discard(opp_player.username) self.draw_offers.add(cur_player.username) self.board.count_started = self.board.ply + 1 def stop_manual_count(self): if self.manual_count: cur_player = self.bplayer if self.board.color == BLACK else self.wplayer opp_player = self.wplayer if self.board.color == BLACK else self.bplayer self.draw_offers.discard(cur_player.username) self.draw_offers.discard(opp_player.username) self.manual_count_toggled.append( (self.board.count_started, self.board.ply + 1)) self.board.count_started = -1 def get_board(self, full=False): if full: steps = self.steps # To not touch self.ply_clocks we are creating deep copy from clocks clocks = { "black": self.clocks["black"], "white": self.clocks["white"] } if self.status == STARTED and self.board.ply >= 2: # We have to adjust current player latest saved clock time # unless he will get free extra time on browser page refresh # (also needed for spectators entering to see correct clock times) cur_time = monotonic() elapsed = int(round( (cur_time - self.last_server_clock) * 1000)) cur_color = "black" if self.board.color == BLACK else "white" clocks[cur_color] = max( 0, clocks[cur_color] + self.byo_correction - elapsed) crosstable = self.crosstable else: clocks = self.clocks steps = (self.steps[-1], ) crosstable = self.crosstable if self.status > STARTED else "" if self.byoyomi: byoyomi_periods = (self.byoyomi_periods["white"], self.byoyomi_periods["black"]) else: byoyomi_periods = "" return { "type": "board", "gameId": self.id, "status": self.status, "result": self.result, "fen": self.board.fen, "lastMove": self.lastmove, "steps": steps, "dests": self.dests, "promo": self.promotions, "check": self.check, "ply": self.board.ply, "clocks": { "black": clocks["black"], "white": clocks["white"] }, "byo": byoyomi_periods, "pgn": self.pgn if self.status > STARTED else "", "rdiffs": { "brdiff": self.brdiff, "wrdiff": self.wrdiff } if self.status > STARTED and self.rated == RATED else "", "uci_usi": self.uci_usi if self.status > STARTED else "", "rm": self.random_move if self.status <= STARTED else "", "ct": crosstable, "berserk": { "w": self.wberserk, "b": self.bberserk }, "by": self.imported_by, } def game_json(self, player): color = "w" if self.wplayer == player else "b" opp_player = self.bplayer if color == "w" else self.wplayer opp_rating = self.black_rating if color == "w" else self.white_rating opp_rating, prov = opp_rating.rating_prov return { "gameId": self.id, "title": opp_player.title, "name": opp_player.username, "rating": opp_rating, "prov": prov, "color": color, "result": self.result, }