Esempio n. 1
0
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)
Esempio n. 2
0
    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)
Esempio n. 3
0
    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)
Esempio n. 4
0
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
Esempio n. 5
0
 def create_board(self, variant, initial_fen, chess960, count_started):
     return FairyBoard(variant, initial_fen, chess960, count_started)
Esempio n. 6
0
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
Esempio n. 7
0
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
Esempio n. 8
0
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
Esempio n. 9
0
 def create_board(self, variant, initial_fen, chess960):
     return FairyBoard(variant, initial_fen, chess960)
Esempio n. 10
0
    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()
Esempio n. 11
0
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,
        }
Esempio n. 12
0
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