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 test_fen_default(self): for variant in VARIANTS: chess960 = variant.endswith("960") variant_name = variant[:-3] if chess960 else variant board = FairyBoard(variant_name, chess960=chess960) fen = board.initial_fen valid, sanitized = sanitize_fen(variant_name, fen, chess960) self.assertTrue(valid)
def test_encode_decode(self): for idx, variant in enumerate(VARIANTS): print(idx, variant) variant = variant.rstrip("960") FEN = sf.start_fen(variant) # fill the pockets with possible pieces for empty_pocket in ("[]", "[-]"): if empty_pocket in FEN: pocket = "".join([ i for i in set(FEN.split()[0]) if i in string.ascii_letters and i not in "Kk" ]) parts = FEN.split(empty_pocket) FEN = "%s[%s]%s" % (parts[0], pocket, parts[1]) board = FairyBoard(variant, initial_fen=FEN) moves = board.legal_moves() saved_restored = decode_moves(encode_moves(moves, variant), variant) self.assertEqual(saved_restored, moves)
async def index(request): """ Create home html. """ users = request.app["users"] games = request.app["games"] db = request.app["db"] # Who made the request? session = await aiohttp_session.get_session(request) session_user = session.get("user_name") session["last_visit"] = datetime.now().isoformat() session["guest"] = True if session_user is not None: log.info("+++ Existing user %s connected.", session_user) doc = None try: doc = await db.user.find_one({"_id": session_user}) except Exception: log.error("Failed to get user %s from mongodb!", session_user) if doc is not None: session["guest"] = False if not doc.get("enabled", True): log.info("Closed account %s tried to connect.", session_user) session.invalidate() return web.HTTPFound("/") if session_user in users: user = users[session_user] else: if session_user.startswith("Anon-"): session.invalidate() return web.HTTPFound(request.rel_url) log.debug("New lichess user %s joined.", session_user) title = session["title"] if "title" in session else "" perfs = {variant: DEFAULT_PERF for variant in VARIANTS} user = User(request.app, username=session_user, anon=session["guest"], title=title, perfs=perfs) users[user.username] = user else: user = User(request.app, anon=True) log.info("+++ New guest user %s connected.", user.username) users[user.username] = user session["user_name"] = user.username lang = session.get("lang", "en") get_template = request.app["jinja"][lang].get_template view = "lobby" gameId = request.match_info.get("gameId") ply = request.rel_url.query.get("ply") tournamentId = request.match_info.get("tournamentId") if request.path == "/about": view = "about" elif request.path == "/faq": view = "faq" elif request.path == "/stats": view = "stats" elif request.path.startswith("/news"): view = "news" elif request.path.startswith("/variants"): view = "variants" elif request.path == "/players": view = "players" elif request.path == "/allplayers": view = "allplayers" elif request.path == "/games": view = "games" elif request.path == "/patron": view = "patron" elif request.path == "/patron/thanks": view = "thanks" elif request.path == "/level8win": view = "level8win" elif request.path == "/tv": view = "tv" gameId = await tv_game(db, request.app) elif request.path.startswith("/editor"): view = "editor" elif request.path.startswith("/analysis"): view = "analysis" elif request.path.startswith("/embed"): view = "embed" elif request.path == "/paste": view = "paste" elif request.path.endswith("/shields"): view = "shields" elif request.path.endswith("/winners"): view = "winners" elif request.path.startswith("/tournaments"): view = "tournaments" if user.username in ADMINS: if request.path.endswith("/new"): view = "arena-new" elif request.path.endswith("/edit"): view = "arena-new" tournament = await load_tournament(request.app, tournamentId) if tournament is None or tournament.status != T_CREATED: view = "tournaments" elif request.path.endswith("/arena"): data = await request.post() await create_or_update_tournament(request.app, user.username, data) elif request.path.startswith("/tournament"): view = "tournament" tournament = await load_tournament(request.app, tournamentId) if tournament is None: return web.HTTPFound("/") if user.username in ADMINS and tournament.status == T_CREATED: if request.path.endswith("/edit"): data = await request.post() await create_or_update_tournament(request.app, user.username, data, tournament=tournament) elif request.path.endswith("/cancel"): await tournament.abort() return web.HTTPFound("/tournaments") if request.path.endswith("/pause") and user in tournament.players: await tournament.pause(user) profileId = request.match_info.get("profileId") if profileId is not None and profileId not in users: await asyncio.sleep(3) raise web.HTTPNotFound() variant = request.match_info.get("variant") if (variant is not None) and ((variant not in VARIANTS) and variant != "terminology"): log.debug("Invalid variant %s in request", variant) raise web.HTTPNotFound() fen = request.rel_url.query.get("fen") rated = None if (fen is not None) and "//" in fen: log.debug("Invelid FEN %s in request", fen) raise web.HTTPNotFound() if profileId is not None: view = "profile" if request.path[-3:] == "/tv": view = "tv" # TODO: tv for variants gameId = await tv_game_user(db, users, profileId) elif request.path[-7:] == "/import": rated = IMPORTED elif request.path[-6:] == "/rated": rated = RATED elif "/challenge" in request.path: view = "lobby" if user.anon: return web.HTTPFound("/") # Do we have gameId in request url? if (gameId is not None) and gameId != "variants": if view not in ("tv", "analysis", "embed"): view = "round" invites = request.app["invites"] if (gameId not in games) and (gameId in invites): seek_id = invites[gameId].id seek = request.app["seeks"][seek_id] if request.path.startswith("/invite/accept/"): player = request.match_info.get("player") seek_status = await join_seek(request.app, user, seek_id, gameId, join_as=player) if seek_status["type"] == "seek_joined": view = "invite" inviter = "wait" elif seek_status["type"] == "seek_occupied": view = "invite" inviter = "occupied" elif seek_status["type"] == "seek_yourself": view = "invite" inviter = "yourself" elif seek_status["type"] == "new_game": try: # Put response data to sse subscribers queue channels = request.app["invite_channels"] for queue in channels: await queue.put(json.dumps({"gameId": gameId})) # return games[game_id] except ConnectionResetError: pass else: view = "invite" inviter = seek.creator.username if user.username != seek.creator.username else "" if view != "invite": game = await load_game(request.app, gameId) if game is None: raise web.HTTPNotFound() if (ply is not None) and (view != "embed"): view = "analysis" if user.username != game.wplayer.username and user.username != game.bplayer.username: game.spectators.add(user) if game.tournamentId is not None: tournament_name = await get_tournament_name( request.app, game.tournamentId) if view in ("profile", "level8win"): if (profileId in users) and not users[profileId].enabled: template = get_template("closed.html") else: template = get_template("profile.html") elif view == "players": template = get_template("players.html") elif view == "shields": template = get_template("shields.html") elif view == "winners": template = get_template("winners.html") elif view == "allplayers": template = get_template("allplayers.html") elif view == "tournaments": template = get_template("tournaments.html") elif view == "arena-new": template = get_template("arena-new.html") elif view == "news": template = get_template("news.html") elif view == "variants": template = get_template("variants.html") elif view == "patron": template = get_template("patron.html") elif view == "faq": template = get_template("FAQ.html") elif view == "analysis": template = get_template("analysis.html") elif view == "embed": template = get_template("embed.html") else: template = get_template("index.html") render = { "js": "/static/pychess-variants.js%s%s" % (BR_EXTENSION, SOURCE_VERSION), "dev": DEV, "app_name": "PyChess", "languages": LANGUAGES, "lang": lang, "title": view.capitalize(), "view": view, "asseturl": STATIC_ROOT, "view_css": ("round" if view == "tv" else view) + ".css", "home": URI, "user": user.username if session["guest"] else "", "anon": user.anon, "username": user.username, "guest": session["guest"], "profile": profileId if profileId is not None else "", "variant": variant if variant is not None else "", "fen": fen.replace(".", "+").replace("_", " ") if fen is not None else "", "variants": VARIANTS, "variant_display_name": variant_display_name, "tournamentdirector": user.username in TOURNAMENT_DIRECTORS, } if view in ("profile", "level8win"): if view == "level8win": profileId = "Fairy-Stockfish" render["trophies"] = [] else: hs = request.app["highscore"] render["trophies"] = [(v, "top10") for v in hs if profileId in hs[v].keys()[:10]] for i, (v, kind) in enumerate(render["trophies"]): if hs[v].peekitem(0)[0] == profileId: render["trophies"][i] = (v, "top1") render["trophies"] = sorted(render["trophies"], key=lambda x: x[1]) shield_owners = request.app["shield_owners"] render["trophies"] += [(v, "shield") for v in shield_owners if shield_owners[v] == profileId] if profileId in CUSTOM_TROPHY_OWNERS: v, kind = CUSTOM_TROPHY_OWNERS[profileId] if v in VARIANTS: render["trophies"].append((v, kind)) render["title"] = "Profile • " + profileId render["icons"] = VARIANT_ICONS render["cup"] = TROPHIES if profileId not in users or users[profileId].perfs is None: render["ratings"] = {} else: render["ratings"] = { k: ("%s%s" % (int(round(v["gl"]["r"], 0)), "?" if v["gl"]["d"] > PROVISIONAL_PHI else ""), v["nb"]) for (k, v) in sorted(users[profileId].perfs.items(), key=lambda x: x[1]["nb"], reverse=True) } if variant is not None: render["variant"] = variant render["profile_title"] = users[ profileId].title if profileId in users else "" render["rated"] = rated elif view == "players": online_users = [ u for u in users.values() if u.username == user.username or (u.online and not u.anon) ] anon_online = sum((1 for u in users.values() if u.anon and u.online)) render["icons"] = VARIANT_ICONS render["users"] = users render["online_users"] = online_users render["anon_online"] = anon_online # render["offline_users"] = offline_users hs = request.app["highscore"] render["highscore"] = { variant: dict(hs[variant].items()[:10]) for variant in hs } elif view in ("shields", "winners"): wi = await get_winners(request.app, shield=(view == "shields")) render["view_css"] = "players.css" render["users"] = users render["icons"] = VARIANT_ICONS render["winners"] = wi elif view == "allplayers": allusers = [u for u in users.values() if not u.anon] render["allusers"] = allusers elif view == "tournaments": render["icons"] = VARIANT_ICONS render["pairing_system_name"] = pairing_system_name render["time_control_str"] = time_control_str render["tables"] = await get_latest_tournaments(request.app) render["admin"] = user.username in ADMINS if (gameId is not None) and gameId != "variants": if view == "invite": render["gameid"] = gameId render["variant"] = seek.variant render["chess960"] = seek.chess960 render["rated"] = seek.rated render["base"] = seek.base render["inc"] = seek.inc render["byo"] = seek.byoyomi_period render["inviter"] = inviter render["seekempty"] = seek.player1 is None and seek.player2 is None else: render["gameid"] = gameId render["variant"] = game.variant render["wplayer"] = game.wplayer.username render["wtitle"] = game.wplayer.title render["wrating"] = game.wrating render["wrdiff"] = game.wrdiff render["chess960"] = game.chess960 render["rated"] = game.rated render["level"] = game.level render["bplayer"] = game.bplayer.username render["btitle"] = game.bplayer.title render["brating"] = game.brating render["brdiff"] = game.brdiff render["fen"] = game.board.fen render["base"] = game.base render["inc"] = game.inc render["byo"] = game.byoyomi_period render["result"] = game.result render["status"] = game.status render["date"] = game.date.isoformat() render["title"] = game.browser_title render["ply"] = ply if ply is not None else game.board.ply - 1 if game.tournamentId is not None: render["tournamentid"] = game.tournamentId render["tournamentname"] = tournament_name render["wberserk"] = game.wberserk render["bberserk"] = game.bberserk if tournamentId is not None: render["tournamentid"] = tournamentId render["tournamentname"] = tournament.name render["description"] = tournament.description render["variant"] = tournament.variant render["chess960"] = tournament.chess960 render["rated"] = tournament.rated render["base"] = tournament.base render["inc"] = tournament.inc render["byo"] = tournament.byoyomi_period render["fen"] = tournament.fen render["before_start"] = tournament.before_start render["minutes"] = tournament.minutes render["date"] = tournament.starts_at render["rounds"] = tournament.rounds render["frequency"] = tournament.frequency render["status"] = tournament.status if view == "level8win": render["level"] = 8 render["profile"] = "Fairy-Stockfish" elif view == "variants": render["icons"] = VARIANT_ICONS render["groups"] = VARIANT_GROUPS # variant None indicates intro.md if lang in ("es", "hu", "it", "pt", "fr"): locale = ".%s" % lang else: locale = "" if variant == "terminology": render["variant"] = "docs/terminology%s.html" % locale else: render["variant"] = "docs/" + ("terminology" if variant is None else variant) + "%s.html" % locale elif view == "news": news_item = request.match_info.get("news_item") if (news_item is None) or (news_item not in NEWS): news_item = list(NEWS.keys())[0] news_item = news_item.replace("_", " ") render["news"] = NEWS render["news_item"] = "news/%s.html" % news_item elif view == "faq": # TODO: make it translatable similar to above variant pages render["faq"] = "docs/faq.html" elif view == "editor" or (view == "analysis" and gameId is None): if fen is None: fen = FairyBoard(variant).start_fen(variant) else: fen = fen.replace(".", "+").replace("_", " ") render["variant"] = variant render["fen"] = fen elif view == "arena-new": render["edit"] = tournamentId is not None if tournamentId is None: render["rated"] = True try: text = await template.render_async(render) except Exception: return web.HTTPFound("/") # log.debug("Response: %s" % text) response = web.Response(text=html_minify(text), content_type="text/html") parts = urlparse(URI) response.set_cookie("user", session["user_name"], domain=parts.hostname, secure=parts.scheme == "https", samesite="Lax", max_age=None if user.anon else MAX_AGE) return response
def create_board(self, variant, initial_fen, chess960, count_started): return FairyBoard(variant, initial_fen, chess960, count_started)
async def index(request): """ Create home html. """ users = request.app["users"] games = request.app["games"] db = request.app["db"] # Who made the request? session = await aiohttp_session.get_session(request) session_user = session.get("user_name") session["last_visit"] = datetime.now().isoformat() session["guest"] = True if session_user is not None: log.info("+++ Existing user %s connected." % session_user) doc = None try: doc = await db.user.find_one({"_id": session_user}) except Exception: log.error("Failed to get user %s from mongodb!" % session_user) if doc is not None: session["guest"] = False if not doc.get("enabled", True): log.info("Closed account %s tried to connect." % session_user) session.invalidate() raise web.HTTPFound("/") if session_user in users: user = users[session_user] else: if session_user.startswith("Anon-"): session.invalidate() raise web.HTTPFound("/") log.debug("New lichess user %s joined." % session_user) title = session["title"] if "title" in session else "" perfs = {variant: DEFAULT_PERF for variant in VARIANTS} user = User(request.app, username=session_user, anon=session["guest"], title=title, perfs=perfs) users[user.username] = user user.ping_counter = 0 else: user = User(request.app, anon=True) log.info("+++ New guest user %s connected." % user.username) users[user.username] = user session["user_name"] = user.username lang = session.get("lang", "en") get_template = request.app["jinja"][lang].get_template view = "lobby" gameId = request.match_info.get("gameId") if request.path == "/about": view = "about" elif request.path.startswith("/variant"): view = "variant" elif request.path == "/players": view = "players" elif request.path == "/allplayers": view = "allplayers" elif request.path == "/games": view = "games" elif request.path == "/patron": view = "patron" elif request.path == "/patron/thanks": view = "thanks" elif request.path == "/level8win": view = "level8win" elif request.path == "/tv": view = "tv" gameId = await tv_game(db, request.app) elif request.path.startswith("/editor"): view = "editor" profileId = request.match_info.get("profileId") variant = request.match_info.get("variant") fen = request.rel_url.query.get("fen") if (fen is not None) and "//" in fen: return web.Response(status=404) if profileId is not None: view = "profile" if request.path[-3:] == "/tv": view = "tv" # TODO: tv for variants gameId = await tv_game_user(db, users, profileId) elif "/challenge" in request.path: view = "lobby" if user.anon: raise web.HTTPFound("/") # Do we have gameId in request url? if gameId is not None: if view != "tv": view = "round" game = await load_game(request.app, gameId) if game is None: log.debug("Requested game %s not in app['games']" % gameId) template = get_template("404.html") return web.Response(text=html_minify(template.render({"home": URI})), content_type="text/html") games[gameId] = game if game.status > STARTED: view = "analysis" if user.username != game.wplayer.username and user.username != game.bplayer.username: game.spectators.add(user) if view == "profile" or view == "level8win": if (profileId in users) and not users[profileId].enabled: template = get_template("closed.html") else: template = get_template("profile.html") elif view == "players": template = get_template("players.html") elif view == "allplayers": template = get_template("allplayers.html") elif view == "variant": template = get_template("variant.html") elif view == "patron": template = get_template("patron.html") else: template = get_template("index.html") render = { "app_name": "PyChess", "languages": LANGUAGES, "lang": lang, "title": view.capitalize(), "view": view, "home": URI, "user": user.username if session["guest"] else "", "anon": user.anon, "username": user.username, "country": session["country"] if "country" in session else "", "guest": session["guest"], "profile": profileId if profileId is not None else "", "variant": variant if variant is not None else "", "fen": fen.replace(".", "+").replace("_", " ") if fen is not None else "", } if view == "profile" or view == "level8win": if view == "level8win": profileId = "Fairy-Stockfish" render["title"] = "Profile • " + profileId render["icons"] = VARIANT_ICONS if profileId not in users or users[profileId].perfs is None: render["ratings"] = {} else: render["ratings"] = { k: ("%s%s" % (int(round(v["gl"]["r"], 0)), "?" if v["gl"]["d"] > PROVISIONAL_PHI else ""), v["nb"]) for (k, v) in sorted(users[profileId].perfs.items(), key=lambda x: x[1]["nb"], reverse=True) } if variant is not None: render["variant"] = variant render["profile_title"] = users[ profileId].title if profileId in users else "" if view == "players": online_users = [ u for u in users.values() if u.online(user.username) and not u.anon ] # offline_users = (u for u in users.values() if not u.online(user.username) and not u.anon) anon_online = sum( (1 for u in users.values() if u.anon and u.online(user.username))) render["icons"] = VARIANT_ICONS render["users"] = users render["online_users"] = online_users render["anon_online"] = anon_online # render["offline_users"] = offline_users render["highscore"] = request.app["highscore"] elif view == "allplayers": allusers = [u for u in users.values() if not u.anon] render["allusers"] = allusers if gameId is not None: render["gameid"] = gameId render["variant"] = game.variant render["wplayer"] = game.wplayer.username render["wtitle"] = game.wplayer.title render["wrating"] = game.wrating render["wrdiff"] = game.wrdiff render["chess960"] = game.chess960 render["rated"] = game.rated render["level"] = game.level render["bplayer"] = game.bplayer.username render["btitle"] = game.bplayer.title render["brating"] = game.brating render["brdiff"] = game.brdiff render["fen"] = game.board.fen render["base"] = game.base render["inc"] = game.inc render["byo"] = game.byoyomi_period render["result"] = game.result render["status"] = game.status render["date"] = game.date.isoformat() render[ "title"] = game.wplayer.username + ' vs ' + game.bplayer.username if view == "level8win": render["level"] = 8 render["profile"] = "Fairy-Stockfish" elif view == "variant": render["variants"] = VARIANTS render["icons"] = VARIANT_ICONS locale = ".%s" % lang if (variant is None) and lang in ("hu", ) else "" if variant == "terminology": render["variant"] = "terminology%s.html" % locale else: render["variant"] = ("intro" if variant is None else variant) + "%s.html" % locale elif view == "editor": if fen is None: fen = FairyBoard(variant).start_fen(variant) render["variant"] = variant render["fen"] = fen try: text = template.render(render) except Exception: raise web.HTTPFound("/") # log.debug("Response: %s" % text) response = web.Response(text=html_minify(text), content_type="text/html") hostname = urlparse(URI).hostname response.set_cookie("user", session["user_name"], domain=hostname, secure="." not in hostname, max_age=None if user.anon else MAX_AGE) return 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
def create_board(self, variant, initial_fen, chess960): return FairyBoard(variant, initial_fen, chess960)
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()
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, }
async def index(request): """ Create home html. """ users = request.app["users"] games = request.app["games"] db = request.app["db"] # Who made the request? session = await aiohttp_session.get_session(request) session_user = session.get("user_name") session["last_visit"] = datetime.now().isoformat() session["guest"] = True if session_user is not None: log.info("+++ Existing user %s connected.", session_user) doc = None try: doc = await db.user.find_one({"_id": session_user}) except Exception: log.error("Failed to get user %s from mongodb!", session_user) if doc is not None: session["guest"] = False if not doc.get("enabled", True): log.info("Closed account %s tried to connect.", session_user) session.invalidate() return web.HTTPFound("/") if session_user in users: user = users[session_user] else: if session_user.startswith("Anon-"): session.invalidate() return web.HTTPFound(request.rel_url) log.debug("New lichess user %s joined.", session_user) title = session["title"] if "title" in session else "" perfs = {variant: DEFAULT_PERF for variant in VARIANTS} user = User(request.app, username=session_user, anon=session["guest"], title=title, perfs=perfs) users[user.username] = user else: user = User(request.app, anon=True) log.info("+++ New guest user %s connected.", user.username) users[user.username] = user session["user_name"] = user.username lang = session.get("lang", "en") get_template = request.app["jinja"][lang].get_template view = "lobby" gameId = request.match_info.get("gameId") ply = request.rel_url.query.get("ply") if request.path == "/about": view = "about" elif request.path == "/faq": view = "faq" elif request.path == "/stats": view = "stats" elif request.path.startswith("/news"): view = "news" elif request.path.startswith("/variant"): view = "variant" elif request.path == "/players": view = "players" elif request.path == "/allplayers": view = "allplayers" elif request.path == "/games": view = "games" elif request.path == "/patron": view = "patron" elif request.path == "/patron/thanks": view = "thanks" elif request.path == "/level8win": view = "level8win" elif request.path == "/tv": view = "tv" gameId = await tv_game(db, request.app) elif request.path.startswith("/editor"): view = "editor" elif request.path.startswith("/analysis"): view = "analysis" elif request.path.startswith("/embed"): view = "embed" elif request.path == "/paste": view = "paste" profileId = request.match_info.get("profileId") variant = request.match_info.get("variant") if (variant is not None) and ((variant not in VARIANTS) and variant != "terminology"): log.debug("Invalid variant %s in request", variant) return web.Response(status=404) fen = request.rel_url.query.get("fen") rated = None if (fen is not None) and "//" in fen: log.debug("Invelid FEN %s in request", fen) return web.Response(status=404) if profileId is not None: view = "profile" if request.path[-3:] == "/tv": view = "tv" # TODO: tv for variants gameId = await tv_game_user(db, users, profileId) elif request.path[-7:] == "/import": rated = IMPORTED elif request.path[-6:] == "/rated": rated = RATED elif "/challenge" in request.path: view = "lobby" if user.anon: return web.HTTPFound("/") # Do we have gameId in request url? if gameId is not None: if view not in ("tv", "analysis", "embed"): view = "round" invites = request.app["invites"] if (gameId not in games) and (gameId in invites): if not request.path.startswith("/invite/accept/"): seek_id = invites[gameId].id seek = request.app["seeks"][seek_id] view = "invite" inviter = seek.user.username if user.username != seek.user.username else "" if view != "invite": game = await load_game(request.app, gameId, user) if game is None: log.debug("Requested game %s not in app['games']", gameId) template = get_template("404.html") text = await template.render_async({"home": URI}) return web.Response(text=html_minify(text), content_type="text/html") if (ply is not None) and (view != "embed"): view = "analysis" if user.username != game.wplayer.username and user.username != game.bplayer.username: game.spectators.add(user) if view in ("profile", "level8win"): if (profileId in users) and not users[profileId].enabled: template = get_template("closed.html") else: template = get_template("profile.html") elif view == "players": template = get_template("players.html") elif view == "allplayers": template = get_template("allplayers.html") elif view == "news": template = get_template("news.html") elif view == "variant": template = get_template("variant.html") elif view == "patron": template = get_template("patron.html") elif view == "faq": template = get_template("FAQ.html") elif view == "analysis": template = get_template("analysis.html") elif view == "embed": template = get_template("embed.html") else: template = get_template("index.html") render = { "app_name": "PyChess", "languages": LANGUAGES, "lang": lang, "title": view.capitalize(), "view": view, "asseturl": STATIC_ROOT, "view_css": ("round" if view == "tv" else view) + ".css", "home": URI, "user": user.username if session["guest"] else "", "anon": user.anon, "username": user.username, "country": session["country"] if "country" in session else "", "guest": session["guest"], "profile": profileId if profileId is not None else "", "variant": variant if variant is not None else "", "fen": fen.replace(".", "+").replace("_", " ") if fen is not None else "", "variants": VARIANTS, } if view in ("profile", "level8win"): if view == "level8win": profileId = "Fairy-Stockfish" render["title"] = "Profile • " + profileId render["icons"] = VARIANT_ICONS if profileId not in users or users[profileId].perfs is None: render["ratings"] = {} else: render["ratings"] = { k: ("%s%s" % (int(round(v["gl"]["r"], 0)), "?" if v["gl"]["d"] > PROVISIONAL_PHI else ""), v["nb"]) for (k, v) in sorted(users[profileId].perfs.items(), key=lambda x: x[1]["nb"], reverse=True) } if variant is not None: render["variant"] = variant render["profile_title"] = users[ profileId].title if profileId in users else "" render["rated"] = rated render["variant_display_name"] = variant_display_name if view == "players": online_users = [ u for u in users.values() if u.username == user.username or (u.online and not u.anon) ] anon_online = sum((1 for u in users.values() if u.anon and u.online)) render["icons"] = VARIANT_ICONS render["users"] = users render["online_users"] = online_users render["anon_online"] = anon_online # render["offline_users"] = offline_users hs = request.app["highscore"] render["highscore"] = { variant: dict(hs[variant].items()[:10]) for variant in hs } render["variant_display_name"] = variant_display_name elif view == "allplayers": allusers = [u for u in users.values() if not u.anon] render["allusers"] = allusers if gameId is not None: if view == "invite": render["gameid"] = gameId render["variant"] = seek.variant render["chess960"] = seek.chess960 render["rated"] = seek.rated render["base"] = seek.base render["inc"] = seek.inc render["byo"] = seek.byoyomi_period render["inviter"] = inviter else: render["gameid"] = gameId render["variant"] = game.variant render["wplayer"] = game.wplayer.username render["wtitle"] = game.wplayer.title render["wrating"] = game.wrating render["wrdiff"] = game.wrdiff render["chess960"] = game.chess960 render["rated"] = game.rated render["level"] = game.level render["bplayer"] = game.bplayer.username render["btitle"] = game.bplayer.title render["brating"] = game.brating render["brdiff"] = game.brdiff render["fen"] = game.board.fen render["base"] = game.base render["inc"] = game.inc render["byo"] = game.byoyomi_period render["result"] = game.result render["status"] = game.status render["date"] = game.date.isoformat() render[ "title"] = game.wplayer.username + ' vs ' + game.bplayer.username if ply is not None: render["ply"] = ply if view == "level8win": render["level"] = 8 render["profile"] = "Fairy-Stockfish" elif view == "variant": render["icons"] = VARIANT_ICONS # variant None indicates intro.md if lang in ("hu", "it", "pt", "fr"): locale = ".%s" % lang elif lang == "zh": # Only intro.md locale = ".%s" % lang if variant in (None, ) else "" else: locale = "" if variant == "terminology": render["variant"] = "docs/terminology%s.html" % locale else: render["variant"] = "docs/" + ("intro" if variant is None else variant) + "%s.html" % locale render["variant_display_name"] = variant_display_name elif view == "news": news_item = request.match_info.get("news_item") if (news_item is None) or (news_item not in NEWS): news_item = NEWS[0] news_item = news_item.replace("_", " ") render["news"] = NEWS render["news_item"] = "news/%s.html" % news_item elif view == "faq": # TODO: make it translatable similar to above variant pages render["faq"] = "docs/faq.html" elif view == "editor" or (view == "analysis" and gameId is None): if fen is None: fen = FairyBoard(variant).start_fen(variant) else: fen = fen.replace(".", "+").replace("_", " ") render["variant"] = variant render["fen"] = fen try: text = await template.render_async(render) except Exception: return web.HTTPFound("/") # log.debug("Response: %s" % text) response = web.Response(text=html_minify(text), content_type="text/html") parts = urlparse(URI) response.set_cookie("user", session["user_name"], domain=parts.hostname, secure=parts.scheme == "https", samesite="Lax", max_age=None if user.anon else MAX_AGE) return response