From b68d606c8740739744816c016a6b16d4ea2936b1 Mon Sep 17 00:00:00 2001 From: 99oblivius <99dark.oblivion99@gmail.com> Date: Fri, 26 Jul 2024 00:26:55 +0000 Subject: [PATCH] better leaderboard with multiple messages and better ranking --- src/cogs/settings/cog.py | 23 ++------ src/matches/match.py | 19 +++--- src/utils/database.py | 47 ++++++++++++++- src/utils/statistics.py | 124 +++++++++++++++++++++++++++++---------- 4 files changed, 152 insertions(+), 61 deletions(-) diff --git a/src/cogs/settings/cog.py b/src/cogs/settings/cog.py index 1a9e8c5..fe5c34b 100644 --- a/src/cogs/settings/cog.py +++ b/src/cogs/settings/cog.py @@ -27,7 +27,7 @@ from config import * from utils.logger import Logger as log from utils.models import BotRegions, BotSettings, MMBotRanks, Platform, MMBotUsers, MMBotUserSummaryStats -from utils.statistics import create_leaderboard_embed +from utils.statistics import update_leaderboard from views.register import RegistryButtonView @@ -237,24 +237,9 @@ async def set_ranks(self, interaction: nextcord.Interaction, @settings.subcommand(name="set_leaderboard", description="Set the channel and leaderboard message") async def set_leaderboard(self, interaction: nextcord.Interaction): - settings = await self.bot.store.get_settings(interaction.guild.id) - channel = interaction.guild.get_channel(settings.leaderboard_channel) - if channel: - try: - old_message = await channel.fetch_message(settings.leaderboard_message) - await old_message.delete() - except nextcord.NotFound: pass - - data = await self.bot.store.get_leaderboard(interaction.guild.id) - ranks = await self.bot.store.get_ranks(interaction.guild.id) - previous_data = await self.bot.store.get_last_mmr_for_users(interaction.guild.id) - embed = create_leaderboard_embed(interaction.guild, data, previous_data, ranks) - msg = await interaction.channel.send(embed=embed) - await self.bot.store.update(BotSettings, - guild_id=interaction.guild.id, - leaderboard_channel=interaction.channel.id, - leaderboard_message=msg.id) - await interaction.response.send_message( + await interaction.response.defer(ephemeral=True) + await update_leaderboard(self.bot.store, interaction.guild) + await interaction.followup.send( f"Match Making Leaderboard set", ephemeral=True) @nextcord.slash_command(name="change_mmr", description="Change a member's Match Making Rating", guild_ids=[GUILD_ID]) diff --git a/src/matches/match.py b/src/matches/match.py index d5c5c91..80e4f2b 100644 --- a/src/matches/match.py +++ b/src/matches/match.py @@ -41,7 +41,7 @@ from utils.logger import Logger as log, VariableLog from utils.models import * from utils.utils import format_duration, format_mm_attendance, generate_score_image -from utils.statistics import create_leaderboard_embed +from utils.statistics import update_leaderboard from views.match.accept import AcceptView from views.match.banning import BanView, ChosenBansView from views.match.map_pick import ChosenMapView, MapPickView @@ -908,20 +908,21 @@ def server_score(server_region): await self.bot.rcon_manager.comp_mode(serveraddr, state=False, retry_attempts=1) embed = nextcord.Embed(title="The match will terminate in 10 seconds", color=VALORS_THEME1) - channel = guild.get_channel(settings.leaderboard_channel) try: await self.match_thread.send(embed=embed) except AttributeError: pass + # channel = guild.get_channel(settings.leaderboard_channel) - guild = self.bot.get_guild(self.guild_id) - message = await channel.fetch_message(settings.leaderboard_message) - ranks = await self.bot.store.get_ranks(guild.id) + # guild = self.bot.get_guild(self.guild_id) + # message = await channel.fetch_message(settings.leaderboard_message) + # ranks = await self.bot.store.get_ranks(guild.id) - data = await self.bot.store.get_leaderboard(guild.id) - previous_data = await self.bot.store.get_last_mmr_for_users(guild.id) - embed = create_leaderboard_embed(guild, data, previous_data, ranks) - asyncio.create_task(message.edit(embed=embed)) + # data = await self.bot.store.get_leaderboard(guild.id) + # previous_data = await self.bot.store.get_last_mmr_for_users(guild.id) + # embeds = create_leaderboard_embeds(guild, data, previous_data, ranks) + # asyncio.create_task(message.edit(embeds=embeds)) + asyncio.create_task(update_leaderboard(self.bot.store, guild)) await asyncio.sleep(10) # match_thread try: diff --git a/src/utils/database.py b/src/utils/database.py index 877aae3..7487657 100644 --- a/src/utils/database.py +++ b/src/utils/database.py @@ -402,7 +402,52 @@ async def get_last_mmr_for_users(self, guild_id: int) -> Dict[int, int]: .where(MMBotUserMatchStats.guild_id == guild_id)) result = await session.execute(query) - return {row.user_id: row.mmr_before for row in result} + return { row.user_id: row.mmr_before for row in result } + + async def get_leaderboard_with_previous_mmr(self, guild_id: int) -> List[Dict[str, Any]]: + async with self._session_maker() as session: + subquery = ( + select( + MMBotUserMatchStats.user_id, + func.max(MMBotUserMatchStats.match_id).label('latest_match_id')) + .where( + MMBotUserMatchStats.guild_id == guild_id, + MMBotUserMatchStats.abandoned == False) + .group_by(MMBotUserMatchStats.user_id) + .subquery()) + + query = ( + select( + MMBotUserSummaryStats, + MMBotUserMatchStats.mmr_before.label('previous_mmr')) + .join( + subquery, + (MMBotUserSummaryStats.user_id == subquery.c.user_id)) + .join( + MMBotUserMatchStats, + (MMBotUserMatchStats.user_id == subquery.c.user_id) & + (MMBotUserMatchStats.match_id == subquery.c.latest_match_id)) + .where( + MMBotUserSummaryStats.guild_id == guild_id, + MMBotUserSummaryStats.games > 0) + .order_by(desc(MMBotUserSummaryStats.mmr))) + + result = await session.execute(query) + return [ + { + "user_id": row.MMBotUserSummaryStats.user_id, + "mmr": row.MMBotUserSummaryStats.mmr, + "previous_mmr": row.previous_mmr, + "games": row.MMBotUserSummaryStats.games, + "wins": row.MMBotUserSummaryStats.wins, + "win_rate": row.MMBotUserSummaryStats.wins / row.MMBotUserSummaryStats.games if row.MMBotUserSummaryStats.games > 0 else 0, + "avg_kills": row.MMBotUserSummaryStats.total_kills / row.MMBotUserSummaryStats.games if row.MMBotUserSummaryStats.games > 0 else 0, + "avg_deaths": row.MMBotUserSummaryStats.total_deaths / row.MMBotUserSummaryStats.games if row.MMBotUserSummaryStats.games > 0 else 0, + "avg_assists": row.MMBotUserSummaryStats.total_assists / row.MMBotUserSummaryStats.games if row.MMBotUserSummaryStats.games > 0 else 0, + "avg_score": row.MMBotUserSummaryStats.total_score / row.MMBotUserSummaryStats.games if row.MMBotUserSummaryStats.games > 0 else 0, + } + for row in result + ] async def get_user_pick_preferences(self, guild_id: int, user_id: int) -> Dict[str, Dict[str, int]]: async with self._session_maker() as session: diff --git a/src/utils/statistics.py b/src/utils/statistics.py index 56f04e7..0c95013 100644 --- a/src/utils/statistics.py +++ b/src/utils/statistics.py @@ -23,7 +23,7 @@ from concurrent.futures import ThreadPoolExecutor import nextcord -from nextcord import Embed, Guild, User, Member +from nextcord import Embed, Guild, User, Member, TextChannel, Interaction import pandas as pd import numpy as np from scipy.fft import fft, ifft @@ -31,7 +31,7 @@ from plotly.subplots import make_subplots from config import VALORS_THEME1, VALORS_THEME1_1, VALORS_THEME1_2, VALORS_THEME2, REGION_TIMEZONES -from utils.models import MMBotRanks, MMBotUserMatchStats +from utils.models import MMBotRanks, MMBotUserMatchStats, BotSettings from utils.utils import get_rank_color, get_rank_role async def create_graph_async(loop, graph_type, match_stats, ranks=None, preferences=None, play_periods=None, user_region=None): @@ -406,30 +406,42 @@ def create_stats_embed(guild: Guild, user: User | Member, leaderboard_data, summ return embed -def create_leaderboard_embed(guild: Guild, leaderboard_data: List[Dict[str, Any]], last_mmr: Dict[int, int], ranks: List[MMBotRanks]) -> Embed: - field_count = 0 - +async def create_leaderboard_embed(guild: Guild, leaderboard_data: List[Dict[str, Any]], ranks: List[MMBotRanks], start_rank: int) -> Tuple[Embed, int]: valid_scores = [player['avg_score'] for player in leaderboard_data if guild.get_member(player['user_id'])] avg_score = sum(valid_scores) / len(valid_scores) if valid_scores else 0 - embed = Embed(title="Match Making Leaderboard", description=f"{len(valid_scores)} ranking players\nK/D/A and Score are mean averages") + + embed = Embed() + if start_rank == 1: + embed.title = "Match Making Leaderboard" + embed.set_footer(text="K/D/A and Score are mean averages") - ranked_mmr = ((n, user_id) for n, (user_id, _) in enumerate(sorted(last_mmr.items(), key=lambda x: x[1], reverse=True), 1)) - previous_positions = { user_id: n for n, user_id in ranked_mmr if guild.get_member(user_id) } + field_content = "" + players_added = 0 - ranking_position = 0 - for player in leaderboard_data: - if field_count > 25: break + previous_positions = { player['user_id']: i + 1 for i, player in enumerate(sorted(leaderboard_data, key=lambda x: x['previous_mmr'] or 0, reverse=True)) } + + for ranking_position in range(start_rank, start_rank + 50): + if ranking_position > len(leaderboard_data): + break + player = leaderboard_data[ranking_position - 1] member = guild.get_member(player['user_id']) - if member is None: continue - - ranking_position += 1 + if member is None: + continue + + players_added += 1 previous_position = previous_positions.get(player['user_id'], None) - if previous_position is None: rank_change = "\u001b[35m·" - elif ranking_position < previous_position: rank_change = "\u001b[32m↑" - elif ranking_position > previous_position: rank_change = "\u001b[31m↓" - else: rank_change = "\u001b[36m|" + if previous_position is None: + rank_change = "\u001b[35m·" + else: + position_change = previous_position - ranking_position + if position_change > 0: + rank_change = f"\u001b[32m↑" + elif position_change < 0: + rank_change = f"\u001b[31m↓" + else: + rank_change = f"\u001b[36m|" name = member.display_name[:11] + '…' if len(member.display_name) > 12 else member.display_name name = name.ljust(12) @@ -441,32 +453,80 @@ def create_leaderboard_embed(guild: Guild, leaderboard_data: List[Dict[str, Any] a = floor(player['avg_assists']) score = floor(player['avg_score']) - # Color coding rank_color = get_rank_color(guild, float(mmr), ranks) win_rate_color = "\u001b[32m" if win_rate > 60 else "\u001b[31m" if win_rate < 40 else "\u001b[0m" score_color = "\u001b[32m" if score > avg_score else "\u001b[31m" kda = f"{k}/{d}/{a}".rjust(8) - if k < d: - kda_formatted = kda.replace(str(d), f"\u001b[31m{d}\u001b[0m", 1) - else: - kda_formatted = kda + kda_formatted = kda.replace(str(d), f"\u001b[31m{d}\u001b[0m", 1) if k < d else kda row = f"{ranking_position:3} {rank_change}\u001b[0m {name} | {rank_color}{mmr}\u001b[0m | {games} | {win_rate_color}{win_rate:3}%\u001b[0m | {kda_formatted} | {score_color}{score:2}\u001b[0m\n" - if ranking_position % 5 == 1: - if ranking_position == 1: + if players_added % 5 == 1: + if players_added == 1: header = " R | Player | MMR | G | W% | K/D/A | S " field_content = f"```ansi\n\u001b[1m{header}\u001b[0m\n{'─' * len(header)}\n{row}" else: - field_content += "```" - embed.add_field(name="\u200b", value=field_content, inline=False) + embed.add_field(name="\u200b", value=field_content + "```", inline=False) field_content = f"```ansi\n{row}" - field_count += 1 else: field_content += row + if field_content: - field_content += "```" - embed.add_field(name="\u200b", value=field_content, inline=False) - - return embed \ No newline at end of file + embed.add_field(name="\u200b", value=field_content + "```", inline=False) + + return embed, ranking_position + 1 + +async def update_leaderboard(store, guild: Guild): + settings = await store.get_settings(guild.id) + channel = guild.get_channel(settings.leaderboard_channel) + if not isinstance(channel, TextChannel): + return + + data = await store.get_leaderboard_with_previous_mmr(guild.id) + ranks = await store.get_ranks(guild.id) + + # Remove players who have left the server + data = [player for player in data if guild.get_member(player['user_id'])] + + total_players = len(data) + + header_embed = Embed(title="Match Making Leaderboard") + header_embed.add_field(name="Total Players", value=str(total_players), inline=True) + header_embed.set_footer(text="K/D/A and Score are mean averages") + + try: + header_message = await channel.fetch_message(settings.leaderboard_message) + await header_message.edit(content=None, embed=header_embed) + except: + header_message = await channel.send(embed=header_embed) + await store.update(BotSettings, + guild_id=guild.id, + leaderboard_channel=channel.id, + leaderboard_message=header_message.id) + + existing_messages = [] + async for message in channel.history(after=header_message, limit=None): + if message.author == guild.me: + existing_messages.append(message) + else: + await message.delete() + existing_messages.sort(key=lambda m: m.created_at) + + start_rank = 1 + for i in range((len(data) - 1) // 50 + 1): + embed, next_start_rank = await create_leaderboard_embed(guild, data, ranks, start_rank) + if i < len(existing_messages): + try: + await existing_messages[i].edit(embed=embed) + except: + await channel.send(embed=embed) + else: + await channel.send(embed=embed) + start_rank = next_start_rank + + for message in existing_messages[((len(data) - 1) // 50 + 1):]: + try: + await message.delete() + except: + pass \ No newline at end of file