Esempio n. 1
0
    async def rating(self, ctx, *args: str):
        """Plots Codeforces rating graph for the handles provided."""
        (zoom,), args = cf_common.filter_flags(args, ['+zoom'])
        handles = args or ('!' + str(ctx.author),)
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.rating(handle=handle) for handle in handles]

        if not any(resp):
            handles_str = ', '.join(f'`{handle}`' for handle in handles)
            if len(handles) == 1:
                message = f'User {handles_str} is not rated'
            else:
                message = f'None of the given users {handles_str} are rated'
            raise GraphCogError(message)

        plt.clf()
        _plot_rating(resp)
        current_ratings = [rating_changes[-1].newRating if rating_changes else 'Unrated' for rating_changes in resp]
        labels = [f'\N{ZERO WIDTH SPACE}{handle} ({rating})' for handle, rating in zip(handles, current_ratings)]
        plt.legend(labels, loc='upper left')

        if not zoom:
            min_rating = 1100
            max_rating = 1800
            for rating_changes in resp:
                for rating in rating_changes:
                    min_rating = min(min_rating, rating.newRating)
                    max_rating = max(max_rating, rating.newRating)
            plt.ylim(min_rating - 100, max_rating + 200)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='Rating graph on Codeforces')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Esempio n. 2
0
    def _make_rankup_embed(guild, contest, change_by_handle):
        """Make an embed containing a list of rank changes and top rating increases for the members
        of this guild.
        """
        user_id_handle_pairs = cf_common.user_db.get_handles_for_guild(guild.id)
        member_handle_pairs = [(guild.get_member(int(user_id)), handle)
                               for user_id, handle in user_id_handle_pairs]
        def ispurg(member):
            # TODO: temporary code, todo properly later
            return any(role.name == 'Purgatory' for role in member.roles)

        member_change_pairs = [(member, change_by_handle[handle])
                               for member, handle in member_handle_pairs
                               if member is not None and handle in change_by_handle and not ispurg(member)]
        if not member_change_pairs:
            raise HandleCogError(f'Contest `{contest.id} | {contest.name}` was not rated for any '
                                 'member of this server.')

        member_change_pairs.sort(key=lambda pair: pair[1].newRating, reverse=True)
        rank_to_role = {role.name: role for role in guild.roles}

        def rating_to_displayable_rank(rating):
            rank = cf.rating2rank(rating).title
            role = rank_to_role.get(rank)
            return role.mention if role else rank

        rank_changes_str = []
        for member, change in member_change_pairs:
            cache = cf_common.cache2.rating_changes_cache
            if (change.oldRating == 1500
                    and len(cache.get_rating_changes_for_handle(change.handle)) == 1):
                # If this is the user's first rated contest.
                old_role = 'Unrated'
            else:
                old_role = rating_to_displayable_rank(change.oldRating)
            new_role = rating_to_displayable_rank(change.newRating)
            if new_role != old_role:
                rank_change_str = (f'{member.mention} [{change.handle}]({cf.PROFILE_BASE_URL}{change.handle}): {old_role} '
                                   f'\N{LONG RIGHTWARDS ARROW} {new_role}')
                rank_changes_str.append(rank_change_str)

        member_change_pairs.sort(key=lambda pair: pair[1].newRating - pair[1].oldRating,
                                 reverse=True)
        top_increases_str = []
        for member, change in member_change_pairs[:_TOP_DELTAS_COUNT]:
            delta = change.newRating - change.oldRating
            if delta <= 0:
                break
            increase_str = (f'{member.mention} [{change.handle}]({cf.PROFILE_BASE_URL}{change.handle}): {change.oldRating} '
                            f'\N{HORIZONTAL BAR} **{delta:+}** \N{LONG RIGHTWARDS ARROW} '
                            f'{change.newRating}')
            top_increases_str.append(increase_str)

        desc = '\n'.join(rank_changes_str) or 'No rank changes'
        embed = discord_common.cf_color_embed(title=contest.name, url=contest.url, description=desc)
        embed.set_author(name='Rank updates')
        embed.add_field(name='Top rating increases',
                        value='\n'.join(top_increases_str) or 'Nobody got a positive delta :(',
                        inline=False)
        return embed
Esempio n. 3
0
    async def scatter(self, ctx, *args):
        """Plot Codeforces rating overlaid on a scatter plot of problems solved.
        Also plots a running average of ratings of problems solved in practice."""
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        handle, bin_size, point_size = None, 10, 3
        for arg in args:
            if arg[0:2] == 'b=':
                bin_size = int(arg[2:])
            elif arg[0:2] == 's=':
                point_size = int(arg[2:])
            else:
                if handle:
                    raise GraphCogError('Only one handle allowed.')
                handle = arg

        if bin_size < 1 or point_size < 1 or point_size > 100:
            raise GraphCogError('Invalid parameters')

        handle = handle or '!' + str(ctx.author)
        handle, = await cf_common.resolve_handles(ctx, self.converter, (handle,))
        rating_resp = [await cf.user.rating(handle=handle)]
        rating_resp = [filt.filter_rating_changes(rating_changes) for rating_changes in rating_resp]
        submissions = filt.filter_subs(await cf.user.status(handle=handle))

        def extract_time_and_rating(submissions):
            return [(dt.datetime.fromtimestamp(sub.creationTimeSeconds), sub.problem.rating)
                    for sub in submissions]

        if not any(submissions):
            raise GraphCogError(f'No submissions for user `{handle}`')

        solved_by_type = _classify_submissions(submissions)
        regular = extract_time_and_rating(solved_by_type['CONTESTANT'] +
                                          solved_by_type['OUT_OF_COMPETITION'])
        practice = extract_time_and_rating(solved_by_type['PRACTICE'])
        virtual = extract_time_and_rating(solved_by_type['VIRTUAL'])

        plt.clf()
        _plot_scatter(regular, practice, virtual, point_size)
        labels = []
        if practice:
            labels.append('Practice')
        if regular:
            labels.append('Regular')
        if virtual:
            labels.append('Virtual')
        plt.legend(labels, loc='upper left')
        _plot_average(practice, bin_size)
        _plot_rating(rating_resp, mark='')

        # zoom
        ymin, ymax = plt.gca().get_ylim()
        plt.ylim(max(ymin, filt.rlo - 100), min(ymax, filt.rhi + 100))

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title=f'Rating vs solved problem rating for {handle}')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Esempio n. 4
0
 def _make_contest_embed_for_ranklist(ranklist):
     contest = ranklist.contest
     assert contest.phase != 'BEFORE', f'Contest {contest.id} has not started.'
     embed = discord_common.cf_color_embed(title=contest.name,
                                           url=contest.url)
     phase = contest.phase.capitalize().replace('_', ' ')
     embed.add_field(name='Phase', value=phase)
     if ranklist.is_rated:
         embed.add_field(name='Deltas', value=ranklist.deltas_status)
     now = time.time()
     en = '\N{EN SPACE}'
     if contest.phase == 'CODING':
         elapsed = cf_common.pretty_time_format(now -
                                                contest.startTimeSeconds,
                                                shorten=True)
         remaining = cf_common.pretty_time_format(contest.end_time - now,
                                                  shorten=True)
         msg = f'{elapsed} elapsed{en}|{en}{remaining} remaining'
         embed.add_field(name='Tick tock', value=msg, inline=False)
     else:
         start = _contest_start_time_format(contest, dt.timezone.utc)
         duration = _contest_duration_format(contest)
         since = cf_common.pretty_time_format(now - contest.end_time,
                                              only_most_significant=True)
         msg = f'{start}{en}|{en}{duration}{en}|{en}Ended {since} ago'
         embed.add_field(name='When', value=msg, inline=False)
     return embed
Esempio n. 5
0
    async def howgud(self, ctx, *members: discord.Member):
        members = members or (ctx.author, )
        if len(members) > 5:
            raise GraphCogError('Please specify at most 5 gudgitters.')

        # shift the [-300, 300] gitgud range to center the text
        hist_bins = list(range(-300 - 50, 300 + 50 + 1, 100))
        deltas = [[x[0] for x in cf_common.user_db.howgud(member.id)]
                  for member in members]
        labels = [
            gc.StrWrap(f'{member.display_name}: {len(delta)}')
            for member, delta in zip(members, deltas)
        ]

        plt.clf()
        plt.margins(x=0)
        plt.hist(deltas, bins=hist_bins, label=labels, rwidth=1)
        plt.xlabel('Problem delta')
        plt.ylabel('Number solved')
        plt.legend(prop=self.fontprop)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='Histogram of gudgitting')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Esempio n. 6
0
    async def stalk(self, ctx, *args):
        """Print problems solved by user sorted by time (default) or rating.
        All submission types are included by default (practice, contest, etc.)
        """
        (hardest,), args = cf_common.filter_flags(args, ['+hardest'])
        filt = cf_common.SubFilter(False)
        args = filt.parse(args)
        handles = args or ('!' + str(ctx.author),)
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        submissions = [await cf.user.status(handle=handle) for handle in handles]
        submissions = [sub for subs in submissions for sub in subs]
        submissions = filt.filter(submissions)

        if not submissions:
            raise CodeforcesCogError('Submissions not found within the search parameters')

        if hardest:
            submissions.sort(key=lambda sub: (sub.problem.rating or 0, sub.creationTimeSeconds), reverse=True)
        else:
            submissions.sort(key=lambda sub: sub.creationTimeSeconds, reverse=True)

        msg = '\n'.join(
            f'[{sub.problem.name}]({sub.problem.url})\N{EN SPACE}'
            f'[{sub.problem.rating if sub.problem.rating else "?"}]\N{EN SPACE}'
            f'({cf_common.days_ago(sub.creationTimeSeconds)})'
            for sub in submissions[:10]
        )
        title = '{} solved problems by `{}`'.format('Hardest' if hardest else 'Recently',
                                                    '`, `'.join(handles))
        embed = discord_common.cf_color_embed(title=title, description=msg)
        await ctx.send(embed=embed)
Esempio n. 7
0
    async def hist(self, ctx, *args: str):
        """Shows the histogram of problems solved over time on Codeforces for the handles provided."""
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        handles = args or ('!' + str(ctx.author), )
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.status(handle=handle) for handle in handles]
        all_solved_subs = [
            filt.filter_subs(submissions) for submissions in resp
        ]

        if not any(all_solved_subs):
            raise GraphCogError(
                f'There are no problems within the specified parameters.')

        plt.clf()
        plt.xlabel('Time')
        plt.ylabel('Number solved')
        if len(handles) == 1:
            handle, solved_by_type = handles[0], _classify_submissions(
                all_solved_subs[0])
            all_times = [[
                dt.datetime.fromtimestamp(sub.creationTimeSeconds)
                for sub in solved_by_type[sub_type]
            ] for sub_type in filt.types]

            nice_names = nice_sub_type(filt.types)
            labels = [
                name.format(len(times))
                for name, times in zip(nice_names, all_times)
            ]
            plt.hist(all_times, stacked=True, label=labels, bins=34)

            total = sum(map(len, all_times))
            plt.legend(title=f'{handle}: {total}',
                       title_fontsize=plt.rcParams['legend.fontsize'])
        else:
            all_times = [[
                dt.datetime.fromtimestamp(sub.creationTimeSeconds)
                for sub in solved_subs
            ] for solved_subs in all_solved_subs]

            # NOTE: matplotlib ignores labels that begin with _
            # https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.legend
            # Add zero-width space to work around this
            labels = [
                gc.StrWrap(f'{handle}: {len(times)}')
                for handle, times in zip(handles, all_times)
            ]

            plt.hist(all_times)
            plt.legend(labels)

        plt.gcf().autofmt_xdate()
        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title='Histogram of number of solved problems over time')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Esempio n. 8
0
    async def upsolve(self, ctx, choice: int = -1):
        """Request an unsolved problem from a contest you participated in
        delta  | -300 | -200 | -100 |  0  | +100 | +200 | +300
        points |   2  |   3  |   5  |  8  |  12  |  17  |  23
        """
        await self._validate_gitgud_status(ctx,delta=None)
        handle, = await cf_common.resolve_handles(ctx, self.converter, ('!' + str(ctx.author),))
        user = cf_common.user_db.fetch_cf_user(handle)
        rating = round(user.effective_rating, -2)
        resp = await cf.user.rating(handle=handle)
        contests = {change.contestId for change in resp}
        submissions = await cf.user.status(handle=handle)
        solved = {sub.problem.name for sub in submissions if sub.verdict == 'OK'}
        problems = [prob for prob in cf_common.cache2.problem_cache.problems
                    if prob.name not in solved and prob.contestId in contests
                    and abs(rating - prob.rating) <= 300]

        if not problems:
            await ctx.send('Problems not found within the search parameters')
            return

        problems.sort(key=lambda problem: cf_common.cache2.contest_cache.get_contest(
            problem.contestId).startTimeSeconds, reverse=True)

        if choice > 0 and choice <= len(problems):
            problem = problems[choice - 1]
            await self._gitgud(ctx, handle, problem, problem.rating - rating)
        else:
            msg = '\n'.join(f'{i + 1}: [{prob.name}]({prob.url}) [{prob.rating}]'
                            for i, prob in enumerate(problems[:5]))
            title = f'Select a problem to upsolve (1-{len(problems)}):'
            embed = discord_common.cf_color_embed(title=title, description=msg)
            await ctx.send(embed=embed)
Esempio n. 9
0
    async def curve(self, ctx, *args: str):
        """Plots the count of problems solved over time on Codeforces for the handles provided."""
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        handles = args or ('!' + str(ctx.author),)
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.status(handle=handle) for handle in handles]
        all_solved_subs = [filt.filter_subs(submissions) for submissions in resp]

        if not any(all_solved_subs):
            raise GraphCogError(f'There are no problems within the specified parameters.')

        plt.clf()
        plt.xlabel('Time')
        plt.ylabel('Cumulative solve count')

        all_times = [[dt.datetime.fromtimestamp(sub.creationTimeSeconds) for sub in solved_subs]
                     for solved_subs in all_solved_subs]
        for times in all_times:
            cumulative_solve_count = list(range(1, len(times)+1)) + [len(times)]
            timestretched = times + [min(dt.datetime.now(), dt.datetime.fromtimestamp(filt.dhi))]
            plt.plot(timestretched, cumulative_solve_count)

        labels = [gc.StrWrap(f'{handle}: {len(times)}')
                  for handle, times in zip(handles, all_times)]

        plt.legend(labels)

        plt.gcf().autofmt_xdate()
        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='Curve of number of solved problems over time')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Esempio n. 10
0
 def _make_contest_embed_for_ranklist(ranklist):
     contest = ranklist.contest
     assert (contest.phase !=
             "BEFORE"), f"Contest {contest.id} has not started."
     embed = discord_common.cf_color_embed(title=contest.name,
                                           url=contest.url)
     phase = contest.phase.capitalize().replace("_", " ")
     embed.add_field(name="Phase", value=phase)
     if ranklist.is_rated:
         embed.add_field(name="Deltas", value=ranklist.deltas_status)
     now = time.time()
     en = "\N{EN SPACE}"
     if contest.phase == "CODING":
         elapsed = cf_common.pretty_time_format(now -
                                                contest.startTimeSeconds,
                                                shorten=True)
         remaining = cf_common.pretty_time_format(contest.end_time - now,
                                                  shorten=True)
         msg = f"{elapsed} elapsed{en}|{en}{remaining} remaining"
         embed.add_field(name="Tick tock", value=msg, inline=False)
     else:
         start = _contest_start_time_format(contest, dt.timezone.utc)
         duration = _contest_duration_format(contest)
         since = cf_common.pretty_time_format(now - contest.end_time,
                                              only_most_significant=True)
         msg = f"{start}{en}|{en}{duration}{en}|{en}Ended {since} ago"
         embed.add_field(name="When", value=msg, inline=False)
     return embed
Esempio n. 11
0
 def make_page(chunk, pi, num):
     title = f'Select a problem to upsolve (1-{num}):'
     msg = '\n'.join(
         make_line(10 * pi + i, prob)
         for i, prob in enumerate(chunk))
     embed = discord_common.cf_color_embed(description=msg)
     return title, embed
Esempio n. 12
0
def complete_duel(duelid, guild_id, win_status, winner, loser, finish_time,
                  score, dtype):
    winner_r = cf_common.user_db.get_duel_rating(winner.id)
    loser_r = cf_common.user_db.get_duel_rating(loser.id)
    delta = round(elo_delta(winner_r, loser_r, score))
    rc = cf_common.user_db.complete_duel(duelid, win_status, finish_time,
                                         winner.id, loser.id, delta, dtype)
    if rc == 0:
        raise DuelCogError("Hey! No cheating!")

    if dtype == DuelType.UNOFFICIAL:
        return None

    winner_cf = get_cf_user(winner.id, guild_id)
    loser_cf = get_cf_user(loser.id, guild_id)
    desc = f"Rating change after **[{winner_cf.handle}]({winner_cf.url})** vs **[{loser_cf.handle}]({loser_cf.url})**:"
    embed = discord_common.cf_color_embed(description=desc)
    embed.add_field(
        name=f"{winner.display_name}",
        value=f"{winner_r} -> {winner_r + delta}",
        inline=False,
    )
    embed.add_field(
        name=f"{loser.display_name}",
        value=f"{loser_r} -> {loser_r - delta}",
        inline=False,
    )
    return embed
Esempio n. 13
0
    async def extreme(self, ctx, handle: str = None):
        """Plots pairs of lowest rated unsolved problem and highest rated solved problem for every
        contest that was rated for the given user.
        """
        handle = handle or '!' + str(ctx.author)
        handle, = await cf_common.resolve_handles(ctx, self.converter, [handle])
        ratingchanges = await cf.user.rating(handle=handle)
        if not ratingchanges:
            raise GraphCogError(f'User {handle} is not rated')

        contest_ids = [change.contestId for change in ratingchanges]
        subs_by_contest_id = {contest_id: [] for contest_id in contest_ids}
        for sub in await cf.user.status(handle=handle):
            if sub.contestId in subs_by_contest_id:
                subs_by_contest_id[sub.contestId].append(sub)

        packed_contest_subs_problemset = [
            (cf_common.cache2.contest_cache.get_contest(contest_id),
             cf_common.cache2.problemset_cache.get_problemset(contest_id),
             subs_by_contest_id[contest_id])
            for contest_id in contest_ids
        ]

        rating = max(ratingchanges, key=lambda change: change.ratingUpdateTimeSeconds).newRating
        _plot_extreme(handle, rating, packed_contest_subs_problemset)

        discord_file = _get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='Codeforces extremes graph')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Esempio n. 14
0
    def _make_vc_rating_changes_embed(guild, contest_id, change_by_handle):
        """Make an embed containing a list of rank changes and rating changes for ratedvc participants.
        """
        contest = cf_common.cache2.contest_cache.get_contest(contest_id)
        user_id_handle_pairs = cf_common.user_db.get_handles_for_guild(
            guild.id)
        member_handle_pairs = [(guild.get_member(int(user_id)), handle)
                               for user_id, handle in user_id_handle_pairs]
        member_change_pairs = [
            (member, change_by_handle[handle])
            for member, handle in member_handle_pairs
            if member is not None and handle in change_by_handle
        ]

        member_change_pairs.sort(key=lambda pair: pair[1].newRating,
                                 reverse=True)
        rank_to_role = {role.name: role for role in guild.roles}

        def rating_to_displayable_rank(rating):
            rank = cf.rating2rank(rating).title
            role = rank_to_role.get(rank)
            return role.mention if role else rank

        rank_changes_str = []
        for member, change in member_change_pairs:
            if len(cf_common.user_db.get_vc_rating_history(member.id)) == 1:
                # If this is the user's first rated contest.
                old_role = 'Unrated'
            else:
                old_role = rating_to_displayable_rank(change.oldRating)
            new_role = rating_to_displayable_rank(change.newRating)
            if new_role != old_role:
                rank_change_str = (
                    f'{member.mention} [{discord.utils.escape_markdown(change.handle)}]({cf.PROFILE_BASE_URL}{change.handle}): {old_role} '
                    f'\N{LONG RIGHTWARDS ARROW} {new_role}')
                rank_changes_str.append(rank_change_str)

        member_change_pairs.sort(
            key=lambda pair: pair[1].newRating - pair[1].oldRating,
            reverse=True)
        rating_changes_str = []
        for member, change in member_change_pairs:
            delta = change.newRating - change.oldRating
            rating_change_str = (
                f'{member.mention} [{discord.utils.escape_markdown(change.handle)}]({cf.PROFILE_BASE_URL}{change.handle}): {change.oldRating} '
                f'\N{HORIZONTAL BAR} **{delta:+}** \N{LONG RIGHTWARDS ARROW} '
                f'{change.newRating}')
            rating_changes_str.append(rating_change_str)

        desc = '\n'.join(rank_changes_str) or 'No rank changes'
        embed = discord_common.cf_color_embed(title=contest.name,
                                              url=contest.url,
                                              description=desc)
        embed.set_author(name='VC Results')
        embed.add_field(name='Rating Changes',
                        value='\n'.join(rating_changes_str)
                        or 'No rating changes',
                        inline=False)
        return embed
Esempio n. 15
0
    async def _rating_hist(self, ctx, ratings, mode, binsize, title):
        if mode not in ('log', 'normal'):
            raise GraphCogError('Mode should be either `log` or `normal`')

        ratings = [r for r in ratings if r >= 0]
        assert ratings, 'Cannot histogram plot empty list of ratings'

        assert 100 % binsize == 0  # because bins is semi-hardcoded
        bins = 39*100//binsize

        colors = []
        low, high = 0, binsize * bins
        for rank in cf.RATED_RANKS:
            for r in range(max(rank.low, low), min(rank.high, high), binsize):
                colors.append('#' + '%06x' % rank.color_embed)
        assert len(colors) == bins, f'Expected {bins} colors, got {len(colors)}'

        height = [0] * bins
        for r in ratings:
            height[r // binsize] += 1

        csum = 0
        cent = [0]
        users = sum(height)
        for h in height:
            csum += h
            cent.append(round(100 * csum / users))

        x = [k * binsize for k in range(bins)]
        label = [f'{r} ({c})' for r, c in zip(x, cent)]

        l, r = 0, bins-1
        while not height[l]:
            l += 1
        while not height[r]:
            r -= 1
        x = x[l:r+1]
        cent = cent[l:r+1]
        label = label[l:r+1]
        colors = colors[l:r+1]
        height = height[l:r+1]

        plt.clf()
        fig = plt.figure(figsize=(15, 5))

        plt.xticks(rotation=45)
        plt.xlim(l * binsize - binsize//2, r * binsize + binsize//2)
        plt.bar(x, height, binsize*0.9, color=colors, linewidth=0,
                tick_label=label, log=(mode == 'log'))
        plt.xlabel('Rating')
        plt.ylabel('Number of users')

        discord_file = gc.get_current_figure_as_file()
        plt.close(fig)

        embed = discord_common.cf_color_embed(title=title)
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Esempio n. 16
0
 def _make_contest_pages(contests, title):
     pages = []
     chunks = paginator.chunkify(contests, _CONTESTS_PER_PAGE)
     for chunk in chunks:
         embed = discord_common.cf_color_embed()
         for name, value in _get_embed_fields_from_contests(chunk):
             embed.add_field(name=name, value=value, inline=False)
         pages.append((title, embed))
     return pages
Esempio n. 17
0
    async def rating(self, ctx, *args: str):
        """Plots Codeforces rating graph for the handles provided."""

        (zoom, peak), args = cf_common.filter_flags(args, ['+zoom' , '+peak'])
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        handles = args or ('!' + str(ctx.author),)
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.rating(handle=handle) for handle in handles]
        resp = [filt.filter_rating_changes(rating_changes) for rating_changes in resp]

        if not any(resp):
            handles_str = ', '.join(f'`{handle}`' for handle in handles)
            if len(handles) == 1:
                message = f'User {handles_str} is not rated'
            else:
                message = f'None of the given users {handles_str} are rated'
            raise GraphCogError(message)

        def max_prefix(user):
            max_rate = 0
            res = []
            for data in user:
                old_rating = data.oldRating
                if old_rating == 0:
                    old_rating = 1500
                if data.newRating - old_rating >= 0 and data.newRating >= max_rate:
                    max_rate = data.newRating
                    res.append(data)
            return(res)

        if peak:
            resp = [max_prefix(user) for user in resp]

        plt.clf()
        plt.axes().set_prop_cycle(gc.rating_color_cycler)
        _plot_rating(resp)
        current_ratings = [rating_changes[-1].newRating if rating_changes else 'Unrated' for rating_changes in resp]
        labels = [gc.StrWrap(f'{handle} ({rating})') for handle, rating in zip(handles, current_ratings)]
        plt.legend(labels, loc='upper left')

        if not zoom:
            min_rating = 1100
            max_rating = 1800
            for rating_changes in resp:
                for rating in rating_changes:
                    min_rating = min(min_rating, rating.newRating)
                    max_rating = max(max_rating, rating.newRating)
            plt.ylim(min_rating - 100, max_rating + 200)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='Rating graph on Codeforces')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Esempio n. 18
0
    async def ratedvc(self, ctx, contest_id: int, *members: discord.Member):
        ratedvc_channel_id = cf_common.user_db.get_rated_vc_channel(
            ctx.guild.id)
        if not ratedvc_channel_id or ctx.channel.id != ratedvc_channel_id:
            raise ContestCogError(
                'You must use this command in ratedvc channel.')
        if not members:
            raise ContestCogError('Missing members')
        contest = cf_common.cache2.contest_cache.get_contest(contest_id)
        try:
            (await
             cf.contest.ratingChanges(contest_id=contest_id
                                      ))[_MIN_RATED_CONTESTANTS_FOR_RATED_VC -
                                         1]
        except (cf.RatingChangesUnavailableError, IndexError):
            error = (
                f'`{contest.name}` was not rated for at least {_MIN_RATED_CONTESTANTS_FOR_RATED_VC} contestants'
                ' or the ratings changes are not published yet.')
            raise ContestCogError(error)

        ongoing_vc_member_ids = _get_ongoing_vc_participants()
        this_vc_member_ids = {str(member.id) for member in members}
        intersection = this_vc_member_ids & ongoing_vc_member_ids
        if intersection:
            busy_members = ", ".join([
                ctx.guild.get_member(int(member_id)).mention
                for member_id in intersection
            ])
            error = f'{busy_members} are registered in ongoing ratedvcs.'
            raise ContestCogError(error)

        handles = cf_common.members_to_handles(members, ctx.guild.id)
        visited_contests = await cf_common.get_visited_contests(handles)
        if contest_id in visited_contests:
            raise ContestCogError(
                f'Some of the handles: {", ".join(handles)} have submissions in the contest'
            )
        start_time = time.time()
        finish_time = start_time + contest.durationSeconds + _RATED_VC_EXTRA_TIME
        cf_common.user_db.create_rated_vc(contest_id, start_time, finish_time,
                                          ctx.guild.id,
                                          [member.id for member in members])
        title = f'Starting {contest.name} for:'
        msg = "\n".join(
            f'[{discord.utils.escape_markdown(handle)}]({cf.PROFILE_BASE_URL}{handle})'
            for handle in handles)
        embed = discord_common.cf_color_embed(title=title,
                                              description=msg,
                                              url=contest.url)
        await ctx.send(embed=embed)
        embed = discord_common.embed_alert(
            f'You have {int(finish_time - start_time) // 60} minutes to complete the vc!'
        )
        embed.set_footer(text='GL & HF')
        await ctx.send(embed=embed)
Esempio n. 19
0
    async def solved(self, ctx, *args: str):
        """Shows a histogram of problems solved on Codeforces for the handles provided.
        e.g. ;plot solved meooow +contest +virtual +outof +dp"""
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        handles = args or ('!' + str(ctx.author),)
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.status(handle=handle) for handle in handles]
        all_solved_subs = [filt.filter_subs(
            submissions) for submissions in resp]

        if not any(all_solved_subs):
            raise GraphCogError(f'There are no problems within the specified parameters.')

        plt.clf()
        plt.xlabel('Problem rating')
        plt.ylabel('Number solved')
        if len(handles) == 1:
            # Display solved problem separately by type for a single user.
            handle, solved_by_type = handles[0], _classify_submissions(
                all_solved_subs[0])
            all_ratings = [[sub.problem.rating for sub in solved_by_type[sub_type]]
                           for sub_type in filt.types]

            nice_names = nice_sub_type(filt.types)
            labels = [name.format(len(ratings))
                      for name, ratings in zip(nice_names, all_ratings)]

            step = 100
            # shift the range to center the text
            hist_bins = list(range(filt.rlo - step // 2,
                                   filt.rhi + step // 2 + 1, step))
            plt.hist(all_ratings, stacked=True, bins=hist_bins, label=labels)
            total = sum(map(len, all_ratings))
            plt.legend(title=f'{handle}: {total}', title_fontsize=plt.rcParams['legend.fontsize'],
                       loc='upper right')

        else:
            all_ratings = [[sub.problem.rating for sub in solved_subs]
                           for solved_subs in all_solved_subs]
            labels = [gc.StrWrap(f'{handle}: {len(ratings)}')
                      for handle, ratings in zip(handles, all_ratings)]

            step = 200
            hist_bins = list(range(filt.rlo - step // 2,
                                   filt.rhi + step // 2 + 1, step))
            plt.hist(all_ratings, bins=hist_bins)
            plt.legend(labels, loc='upper right')

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title='Histogram of problems solved on Codeforces')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Esempio n. 20
0
    async def performance(self, ctx, *args: str):
        """Plots Codeforces performance graph for the handles provided."""

        (zoom,
         peak), args = cf_common.filter_flags(args,
                                              ['+zoom', '+asdfgdsafefsdve'])
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        handles = args or ('!' + str(ctx.author), )
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.rating(handle=handle) for handle in handles]
        # extract last rating before corrections
        current_ratings = [
            rating_changes[-1].newRating if rating_changes else 'Unrated'
            for rating_changes in resp
        ]
        resp = cf.user.correct_rating_changes(resp=resp)
        resp = [
            filt.filter_rating_changes(rating_changes)
            for rating_changes in resp
        ]

        if not any(resp):
            handles_str = ', '.join(f'`{handle}`' for handle in handles)
            if len(handles) == 1:
                message = f'User {handles_str} is not rated'
            else:
                message = f'None of the given users {handles_str} are rated'
            raise GraphCogError(message)

        plt.clf()
        plt.axes().set_prop_cycle(gc.rating_color_cycler)
        _plot_perf(resp)
        labels = [
            gc.StrWrap(f'{handle} ({rating})')
            for handle, rating in zip(handles, current_ratings)
        ]
        plt.legend(labels, loc='upper left')

        if not zoom:
            min_rating = 1100
            max_rating = 1800
            for rating_changes in resp:
                for rating in rating_changes:
                    min_rating = min(min_rating, rating.oldRating)
                    max_rating = max(max_rating, rating.oldRating)
            plt.ylim(min_rating - 100, max_rating + 200)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title='Performance graph on Codeforces')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Esempio n. 21
0
 def _make_contest_embed_for_vc_ranklist(ranklist, vc_start_time=None, vc_end_time=None):
     contest = ranklist.contest
     embed = discord_common.cf_color_embed(title=contest.name, url=contest.url)
     embed.set_author(name='VC Standings')
     now = time.time()
     if vc_start_time and vc_end_time:
         en = '\N{EN SPACE}'
         elapsed = cf_common.pretty_time_format(now - vc_start_time, shorten=True)
         remaining = cf_common.pretty_time_format(max(0,vc_end_time - now), shorten=True)
         msg = f'{elapsed} elapsed{en}|{en}{remaining} remaining'
         embed.add_field(name='Tick tock', value=msg, inline=False)
     return embed
Esempio n. 22
0
        def make_page(chunk, page_num):
            style = table.Style('{:>}  {:<}  {:<}')
            t = table.Table(style)
            t += table.Header('#', 'Name', 'Handle')
            t += table.Line()
            for index, (member, handle) in enumerate(chunk):
                t += table.Data(_PER_PAGE * page_num + index,
                                f'{member.display_name}', handle)

            table_str = f'```\n{t}\n```'
            embed = discord_common.cf_color_embed(description=table_str)
            return 'List of contestants', embed
Esempio n. 23
0
        def make_page(chunk, page_num):
            style = table.Style('{:>}  {:<}  {:<}  {:<}')
            t = table.Table(style)
            t += table.Header('#', 'Name', 'Handle', 'Rating')
            t += table.Line()
            for index, (member, handle, rating) in enumerate(chunk):
                rating_str = f'{rating} ({rating2rank(rating).title_abbr})'
                t += table.Data(_PER_PAGE * page_num + index, f'{member.display_name}', handle, rating_str)

            table_str = f'```\n{t}\n```'
            embed = discord_common.cf_color_embed(description=table_str)
            return 'List of duelists', embed
Esempio n. 24
0
    async def extreme(self, ctx, *args: str):
        """Plots Codeforces lowest unsolved problem/highest solved on codeforces graph for the handle provided."""
        handles = args or ('!' + str(ctx.author), )
        if len(handles) > 1:
            raise GraphCogError('Too many users, try one at a time.')

        handles = await cf_common.resolve_handles(ctx, self.converter, handles)

        resp = await cf.user.rating(handle=handles[0])

        if not resp:
            raise GraphCogError('This user is not rated.')

        contests = [
            cf_common.cache2.contest_cache.get_contest(change.contestId)
            for change in resp
        ]
        contests.sort(key=lambda c: c.id)
        contest_ids = set(c.id for c in contests)
        user_status = [
            submission
            for submission in await cf.user.status(handle=handles[0])
            if submission.contestId in contest_ids
        ]
        user_status.sort(key=lambda sub: sub.contestId)

        statuses = []
        for submission in user_status:
            if not statuses or statuses[-1][
                    0].contestId != submission.contestId:
                statuses.append([])
            statuses[-1].append(submission)

        problemsets = [
            problems for problems in [
                cf_common.cache2.contest_cache.get_problems(contest_id=c.id)
                for c in contests
            ]
        ]

        plt.clf()
        user, = await cf.user.info(handles=handles)
        _plot_extreme(user, resp, statuses, problemsets)
        current_rating = resp[-1].newRating
        labels = [f'\N{ZERO WIDTH SPACE}{handles[0]} ({current_rating})']

        discord_file = _get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title='Extremes graph on Codeforces')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Esempio n. 25
0
    async def vcperformance(self, ctx, *members: discord.Member):
        """Plots VC performance for at most 5 users."""
        members = members or (ctx.author, )
        if len(members) > 5:
            raise ContestCogError('Cannot plot more than 5 VCers at once.')
        plot_data = defaultdict(list)

        min_rating = 1100
        max_rating = 1800

        for member in members:
            rating_history = cf_common.user_db.get_vc_rating_history(member.id)
            if not rating_history:
                raise ContestCogError(f'{member.mention} has no vc history.')
            ratingbefore = 1500
            for vc_id, rating in rating_history:
                vc = cf_common.user_db.get_rated_vc(vc_id)
                perf = ratingbefore + (rating - ratingbefore)*4
                date = dt.datetime.fromtimestamp(vc.finish_time)
                plot_data[member.display_name].append((date, perf))
                min_rating = min(min_rating, perf)
                max_rating = max(max_rating, perf)
                ratingbefore = rating

        plt.clf()
        # plot at least from mid gray to mid purple
        for rating_data in plot_data.values():
            x, y = zip(*rating_data)
            plt.plot(x, y,
                     linestyle='-',
                     marker='o',
                     markersize=4,
                     markerfacecolor='white',
                     markeredgewidth=0.5)

        gc.plot_rating_bg(cf.RATED_RANKS)
        plt.gcf().autofmt_xdate()

        plt.ylim(min_rating - 100, max_rating + 200)
        labels = [
            gc.StrWrap('{} ({})'.format(
                member_display_name,
                ratingbefore))
            for member_display_name, rating_data in plot_data.items()
        ]
        plt.legend(labels, loc='upper left', prop=gc.fontprop)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(title='VC performance graph')
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Esempio n. 26
0
    async def mashup(self, ctx, *args):
        """Create a mashup contest using problems within +-100 of average rating of handles provided.
        Add tags with "+" before them.
        Ban tags with "~" before them.
        """
        handles = [arg for arg in args if arg[0] not in '+~']
        tags = cf_common.parse_tags(args, prefix='+')
        bantags = cf_common.parse_tags(args, prefix='~')

        handles = handles or ('!' + str(ctx.author), )
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.status(handle=handle) for handle in handles]
        submissions = [sub for user in resp for sub in user]
        solved = {sub.problem.name for sub in submissions}
        info = await cf.user.info(handles=handles)
        rating = int(
            round(
                sum(user.effective_rating for user in info) / len(handles),
                -2))
        problems = [
            prob for prob in cf_common.cache2.problem_cache.problems
            if abs(prob.rating -
                   rating) <= 100 and prob.name not in solved and not any(
                       cf_common.is_contest_writer(prob.contestId, handle)
                       for handle in handles)
            and not cf_common.is_nonstandard_problem(prob) and
            prob.matches_all_tags(tags) and not prob.matches_any_tag(bantags)
        ]

        if len(problems) < 4:
            raise CodeforcesCogError(
                'Problems not found within the search parameters')

        problems.sort(key=lambda problem: cf_common.cache2.contest_cache.
                      get_contest(problem.contestId).startTimeSeconds)

        choices = []
        for i in range(4):
            k = max(random.randrange(len(problems) - i) for _ in range(2))
            for c in choices:
                if k >= c:
                    k += 1
            choices.append(k)
            choices.sort()

        problems = reversed([problems[k] for k in choices])
        msg = '\n'.join(f'{"ABCD"[i]}: [{p.name}]({p.url}) [{p.rating}]'
                        for i, p in enumerate(problems))
        str_handles = '`, `'.join(handles)
        embed = discord_common.cf_color_embed(description=msg)
        await ctx.send(f'Mashup contest for `{str_handles}`', embed=embed)
Esempio n. 27
0
async def _send_reminder_at(channel, role, contests, before_secs, send_time):
    delay = send_time - time.time()
    if delay <= 0:
        return
    await asyncio.sleep(delay)
    values = _secs_to_days_hrs_mins_secs(before_secs)
    labels = 'days hrs mins secs'.split()
    before_str = ' '.join(f'{value} {label}'
                          for label, value in zip(labels, values) if value > 0)
    desc = f'About to start in {before_str}'
    embed = discord_common.cf_color_embed(description=desc)
    for name, value in _get_embed_fields_from_contests(contests):
        embed.add_field(name=name, value=value)
    await channel.send(role.mention, embed=embed)
Esempio n. 28
0
    async def rating(self, ctx, *args: str):
        """Plots Codeforces rating graph for the handles provided."""

        (zoom, ), args = cf_common.filter_flags(args, ["+zoom"])
        filt = cf_common.SubFilter()
        args = filt.parse(args)
        args = _mention_to_handle(args, ctx)
        handles = args or ("!" + str(ctx.author), )
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.rating(handle=handle) for handle in handles]
        resp = [
            filt.filter_rating_changes(rating_changes)
            for rating_changes in resp
        ]

        if not any(resp):
            handles_str = ", ".join(f"`{handle}`" for handle in handles)
            if len(handles) == 1:
                message = f"User {handles_str} is not rated"
            else:
                message = f"None of the given users {handles_str} are rated"
            raise GraphCogError(message)

        plt.clf()
        _plot_rating(resp)
        current_ratings = [
            rating_changes[-1].newRating if rating_changes else "Unrated"
            for rating_changes in resp
        ]
        labels = [
            gc.StrWrap(f"{handle} ({rating})")
            for handle, rating in zip(handles, current_ratings)
        ]
        plt.legend(labels, loc="upper left")

        if not zoom:
            min_rating = 1100
            max_rating = 1800
            for rating_changes in resp:
                for rating in rating_changes:
                    min_rating = min(min_rating, rating.newRating)
                    max_rating = max(max_rating, rating.newRating)
            plt.ylim(min_rating - 100, max_rating + 200)

        discord_file = gc.get_current_figure_as_file()
        embed = discord_common.cf_color_embed(
            title="Rating graph on Codeforces")
        discord_common.attach_image(embed, discord_file)
        discord_common.set_author_footer(embed, ctx.author)
        await ctx.send(embed=embed, file=discord_file)
Esempio n. 29
0
    async def mashup(self, ctx, *args):
        """Create a mashup contest using problems within +-100 of average rating of handles provided.
        Add tags with "+" before them.
        """
        handles = [arg for arg in args if arg[0] != "+"]
        tags = [arg[1:] for arg in args if arg[0] == "+" and len(arg) > 1]

        handles = handles or ("!" + str(ctx.author), )
        handles = await cf_common.resolve_handles(ctx, self.converter, handles)
        resp = [await cf.user.status(handle=handle) for handle in handles]
        submissions = [sub for user in resp for sub in user]
        solved = {sub.problem.name for sub in submissions}
        info = await cf.user.info(handles=handles)
        rating = int(
            round(
                sum(user.effective_rating for user in info) / len(handles),
                -2))
        problems = [
            prob for prob in cf_common.cache2.problem_cache.problems
            if abs(prob.rating -
                   rating) <= 100 and prob.name not in solved and not any(
                       cf_common.is_contest_writer(prob.contestId, handle)
                       for handle in handles)
            and not cf_common.is_nonstandard_problem(prob)
        ]
        if tags:
            problems = [prob for prob in problems if prob.tag_matches(tags)]

        if len(problems) < 4:
            await ctx.send("Problems not found within the search parameters")
            return

        problems = self._get_problems(problems)

        choices = []
        for i in range(4):
            k = max(random.randrange(len(problems) - i) for _ in range(2))
            for c in choices:
                if k >= c:
                    k += 1
            choices.append(k)
            choices.sort()

        problems = reversed([problems[k] for k in choices])
        msg = "\n".join(f'{"ABCD"[i]}: [{p.name}]({p.url}) [{p.rating}]'
                        for i, p in enumerate(problems))
        str_handles = "`, `".join(handles)
        embed = discord_common.cf_color_embed(description=msg)
        await ctx.send(f"Mashup contest for `{str_handles}`", embed=embed)
Esempio n. 30
0
        def make_page(chunk, page_num):
            style = table.Style("{:>}  {:<}  {:<}")
            t = table.Table(style)
            t += table.Header("#", "Name", "Handle")
            t += table.Line()
            for index, (member, handle) in enumerate(chunk):
                t += table.Data(
                    _PER_PAGE * page_num + index,
                    f"{member.display_name}",
                    handle,
                )

            table_str = f"```\n{t}\n```"
            embed = discord_common.cf_color_embed(description=table_str)
            return "List of contestants", embed