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)
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
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)
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
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)
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)
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)
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)
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)
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
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
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
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)
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
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)
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
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)
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)
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)
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)
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
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
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
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)
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)
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)
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)
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)
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)
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