Skip to content

Commit

Permalink
Merge pull request #103 from Zalk0/refacto_sb
Browse files Browse the repository at this point in the history
Refactoring of the skyblock ranking process
  • Loading branch information
gylfirst authored Nov 25, 2024
2 parents d676eef + 3bea79c commit 07e3421
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 100 deletions.
2 changes: 2 additions & 0 deletions chouette/commands/skyblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ async def spider(self, interaction: discord.Interaction[ChouetteBot]) -> None:
@app_commands.command(name="link")
@app_commands.rename(pseudo="pseudo_mc")
@app_commands.describe(pseudo="Ton pseudo Minecraft", profile="Ton profil Skyblock préféré")
# Cooldown 1 use per 60 seconds
@app_commands.checks.cooldown(rate=1, per=60)
async def link(
self, interaction: discord.Interaction[ChouetteBot], pseudo: str, profile: str | None
):
Expand Down
16 changes: 13 additions & 3 deletions chouette/commands_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async def on_command_error(
bot_perms = ", ".join(error.missing_permissions)
interaction.client.bot_logger.error(
f"{interaction.client.user} is missing {bot_perms} "
f"to do {interaction.command.name} in #{interaction.channel}"
f"to do /{interaction.command.qualified_name} in #{interaction.channel}"
)
if len(error.missing_permissions) == 1:
await interaction.response.send_message(
Expand All @@ -67,7 +67,7 @@ async def on_command_error(
user_perms = ", ".join(error.missing_permissions)
interaction.client.bot_logger.error(
f"{interaction.user} is missing {user_perms} "
f"to do {interaction.command.name} in #{interaction.channel}"
f"to do /{interaction.command.qualified_name} in #{interaction.channel}"
)
if len(error.missing_permissions) == 1:
await interaction.response.send_message(
Expand All @@ -80,9 +80,19 @@ async def on_command_error(
ephemeral=True,
)
return
if isinstance(error, discord.app_commands.CommandOnCooldown):
interaction.client.bot_logger.error(
f"{interaction.user} tried to use /{interaction.command.qualified_name} in "
f"#{interaction.channel.name} but the command is in cooldown for {error.retry_after:.0f}s",
)
await interaction.response.send_message(
f"Vous devez attendre {error.retry_after:.0f} secondes avant de réutiliser cette commande.",
ephemeral=True,
)
return
if isinstance(error, discord.app_commands.CheckFailure):
interaction.client.bot_logger.error(
f"{interaction.user} tried to do {interaction.command.name} "
f"{interaction.user} tried to do /{interaction.command.qualified_name} "
f"in #{interaction.channel}\n{SPACES}{error}"
)
await interaction.response.send_message(
Expand Down
15 changes: 8 additions & 7 deletions chouette/utils/hypixel_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ def experience_to_level(
Calcule le niveau correspondant à une quantité donnée d'expérience cumulative.
Args:
type_xp: Le type d'expérience pour lequel calculer le niveau (compétence, type de slayer, donjon).
Pour `slayer_type`, utilisez l'un des suivants: slayer_zombie, slayer_spider, slayer_web, slayer_vampire.
xp_amount: La quantité d'expérience cumulée.
max_level: Le niveau maximum
`type_xp`: Le type d'expérience pour lequel calculer le niveau (compétence, type de slayer, donjon).
Pour `slayer_type`, utilisez l'un des suivants: `slayer_zombie`, `slayer_spider`, `slayer_wolf`, `slayer_enderman`, `slayer_blaze`,` slayer_vampire`.
`xp_amount`: La quantité d'expérience cumulée.
`max_level`: Le niveau maximum
Returns:
level: Le niveau correspondant à la quantité donnée d'expérience cumulée.
`level`: Le niveau correspondant à la quantité donnée d'expérience cumulée.
`overflow`: L'expérience restante pour atteindre le prochain niveau.
"""
skill_xp_data: list[int] = [
0,
Expand Down Expand Up @@ -187,7 +188,7 @@ def experience_to_level(
elif type_xp == "slayer_spider":
xp_data = slayer_xp_data[1]
# Slayer Wolf/Enderman/Blaze
elif type_xp == "slayer_web":
elif type_xp in ["slayer_wolf", "slayer_enderman", "slayer_blaze"]:
xp_data = slayer_xp_data[2]
# Slayer Vampire
elif type_xp == "slayer_vampire":
Expand All @@ -200,5 +201,5 @@ def experience_to_level(
return max_level, xp_amount - xp
if xp_amount <= xp:
previous_xp = xp_data[level - 1]
return level - 1 + (xp_amount - previous_xp) / (xp - previous_xp), None
return level - 1 + (xp_amount - previous_xp) / (xp - previous_xp), 0
return len(xp_data) - 1, xp_amount - xp_data[-1]
201 changes: 111 additions & 90 deletions chouette/utils/ranking.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import copy
import math
from datetime import date
from itertools import chain

import aiohttp
import discord
Expand Down Expand Up @@ -28,10 +30,23 @@ def format_number(number) -> str:
return str(number)


def format_ranking_message(player: str, value: str, i: int) -> str:
"""Formate le message pour les données du classement de la guilde sur Hypixel Skyblock."""
if i == 0:
message = f"\N{FIRST PLACE MEDAL} **{player}** [{value}]"
elif i == 1:
message = f"\N{SECOND PLACE MEDAL} **{player}** [{value}]"
elif i == 2:
message = f"\N{THIRD PLACE MEDAL} **{player}** [{value}]"
else:
message = f"\N{MEDIUM BLACK CIRCLE} **{player}** [{value}]"
return message


async def update_stats(api_key: str) -> str:
"""Crée le classement de la guilde sur Hypixel Skyblock."""
old_data = await load_skyblock()
new_data = old_data.copy()
new_data = copy.deepcopy(old_data)
msg = "Synchro des données de la guilde sur Hypixel Skyblock pour :"
async with aiohttp.ClientSession() as session:
for uuid in old_data:
Expand All @@ -49,7 +64,7 @@ async def update_stats(api_key: str) -> str:
return msg


def parse_data(data: dict) -> tuple[dict, list]:
def parse_data(data: dict) -> dict:
"""Parse les données de la guilde sur Hypixel Skyblock."""
ranking = {}
skills = [
Expand All @@ -60,7 +75,7 @@ def parse_data(data: dict) -> tuple[dict, list]:
"enchanting",
"taming",
"foraging",
"carpentery",
"carpentry",
"combat",
"dungeoneering",
]
Expand All @@ -78,43 +93,73 @@ def parse_data(data: dict) -> tuple[dict, list]:
if key == "skills":
for skill in skills:
if skill not in ranking:
ranking[skill] = {}
ranking[skill][data[player]["pseudo"]] = value[skills.index(skill)]
ranking[skill] = {"level": {}, "overflow": {}}
if "skill average" not in ranking:
ranking["skill average"] = {}
ranking["skill average"][data[player]["pseudo"]] = []
ranking["skill average"][data[player]["pseudo"]] = None
# Handle 'slayers'
if key == "slayers":
for slayer in slayers:
if slayer not in ranking:
ranking[slayer] = {}
ranking[slayer][data[player]["pseudo"]] = value[slayers.index(slayer)]
ranking[slayer] = {"level": {}, "overflow": {}}
if key == "level_cap":
level_cap[0].append(value[0])
level_cap[1].append(value[1])

# Calculate the level and overflow for each skill and slayer for each player
for player_index, player in enumerate(data):
for skill in chain(skills, slayers):
type_xp = "skill"
category = "skills"
max_level = None
skill_list = skills

if skill == "farming":
max_level = level_cap[0][player_index] + 50
if skill == "taming":
max_level = level_cap[1][player_index] if level_cap[1][player_index] > 50 else 50
if skill in ["fishing", "alchemy", "carpentry", "foraging"]:
max_level = 50
if skill == "dungeoneering":
type_xp = "dungeon"
if skill in slayers:
type_xp = f"slayer_{skill}"
category = "slayers"
skill_list = slayers

level, overflow = experience_to_level(
type_xp, data[player][category][skill_list.index(skill)], max_level
)
ranking[skill]["level"][data[player]["pseudo"]] = level
ranking[skill]["overflow"][data[player]["pseudo"]] = overflow

# Sorting the nested dictionaries by value
sorted_ranking: dict = ranking.copy()
for category in ranking:
if isinstance(ranking[category], dict):
unsorted = ranking[category]
ranking[category] = dict(
sorted(ranking[category].items(), key=lambda item: item[1], reverse=True)
)
# In the case of farming we need to sort the level_cap too
if category == "farming":
level_cap[0] = [
level_cap[0][list(unsorted.keys()).index(key)] for key in ranking[category]
]
if category == "taming":
level_cap[1] = [
level_cap[1][list(unsorted.keys()).index(key)] for key in ranking[category]
]
# Level and Networth
if category in ["level", "networth"]:
sorted_ranking[category] = dict(
sorted(ranking[category].items(), key=lambda item: item[1], reverse=True)
)
# Skills and slayers
if category in chain(skills, slayers):
sorted_ranking[category] = {
"level": dict(
sorted(
ranking[category]["level"].items(),
key=lambda item: (item[1], ranking[category]["overflow"][item[0]]),
reverse=True,
)
),
"overflow": dict(ranking[category]["overflow"].items()),
}
else:
raise ValueError(f"Unknown category while sorting: {category}")
return ranking, level_cap
raise ValueError(f"Unknown category while sorting the ranking: {category}")
return sorted_ranking


def generate_ranking_message(data, category, level_cap):
def generate_ranking_message(data, category) -> list[str]:
skills_list: list[str] = [
"fishing",
"alchemy",
Expand All @@ -123,7 +168,7 @@ def generate_ranking_message(data, category, level_cap):
"enchanting",
"taming",
"foraging",
"carpentery",
"carpentry",
"combat",
"dungeoneering",
]
Expand All @@ -135,76 +180,52 @@ def generate_ranking_message(data, category, level_cap):
"blaze",
"vampire",
]
messages = []
if category == "skill average":
skill_average = {}
for _i, (player, value) in enumerate(data[category].items()):
skill_average[player] = sum(value) / len(value)
skill_average = dict(sorted(skill_average.items(), key=lambda item: item[1], reverse=True))

for i, (player, value) in enumerate(skill_average.items()):
messages: list = []
# Level
if category == "level":
for i, (player, value) in enumerate(data[category].items()):
value = f"{value:.2f}"
if i == 0:
message = f"\N{FIRST PLACE MEDAL} **{player}** [{value}]"
elif i == 1:
message = f"\N{SECOND PLACE MEDAL} **{player}** [{value}]"
elif i == 2:
message = f"\N{THIRD PLACE MEDAL} **{player}** [{value}]"
else:
message = f"\N{MEDIUM BLACK CIRCLE} **{player}** [{value}]"
message = format_ranking_message(player, value, i)
messages.append(message)
return data, messages
for i, (player, value) in enumerate(data[category].items()):
overflow = None
if category not in ("level", "networth", "skill average"):
if category in skills_list:
if category != "dungeoneering":
max_level = 60
# Set max level to 50 for skills (alchemy, carpentery, fishing, foraging)
if category in ("alchemy", "carpentery", "fishing", "foraging"):
max_level = 50
# Set max level to level cap for farming
if category == "farming":
max_level = level_cap[0][i] + 50
if category == "taming":
max_level = max(min(level_cap[1][i], 60), 50)
value, overflow = experience_to_level("skill", value, max_level)
data["skill average"][player].append(math.floor(value))
else:
value, overflow = experience_to_level("dungeon", value)
elif category in slayers_list:
if category == "zombie":
value, overflow = experience_to_level("slayer_zombie", value)
elif category == "spider":
value, overflow = experience_to_level("slayer_spider", value)
elif category == "vampire":
value, overflow = experience_to_level("slayer_vampire", value)
else:
value, overflow = experience_to_level("slayer_web", value)
else:
raise ValueError(f"Unknown category in the ranking: {category}")

# Networth
if category == "networth":
for i, (player, value) in enumerate(data[category].items()):
value = format_number(value)
message = format_ranking_message(player, value, i)
messages.append(message)
# Skills and slayers
if category in chain(skills_list, slayers_list):
for i, (player, value) in enumerate(data[category]["level"].items()):
overflow = data[category]["overflow"][player]
if overflow:
value = f"{value:.0f}"
overflow = math.floor(overflow)
else:
value = f"{value:.2f}"

elif category == "networth":
value = format_number(value)

if i == 0:
message = f"\N{FIRST PLACE MEDAL} **{player}** [{value}]"
elif i == 1:
message = f"\N{SECOND PLACE MEDAL} **{player}** [{value}]"
elif i == 2:
message = f"\N{THIRD PLACE MEDAL} **{player}** [{value}]"
else:
message = f"\N{MEDIUM BLACK CIRCLE} **{player}** [{value}]"
if overflow:
message += f" (*{overflow:,}*)".replace(",", " ")
messages.append(message)
return data, messages
message = format_ranking_message(player, value, i)
if overflow:
message += f" (*{overflow:,}*)".replace(",", " ")
messages.append(message)
# Skill average
skills_avg: list[str] = skills_list.copy()
skills_avg.remove("dungeoneering")
if category == "skill average":
for player in data[category]:
total = []
# Calculate the average of the skills
for skill in skills_avg:
total.append(math.floor(data[skill]["level"][player]))
average = math.fsum(total) / len(total)
data[category][player] = average
# Sort the skill average
data[category] = dict(
sorted(data[category].items(), key=lambda item: item[1], reverse=True)
)
for i, (player, value) in enumerate(data[category].items()):
value = f"{value:.2f}"
message = format_ranking_message(player, value, i)
messages.append(message)
return messages


async def display_ranking(img: str) -> discord.Embed:
Expand All @@ -218,10 +239,10 @@ async def display_ranking(img: str) -> discord.Embed:
color=discord.Colour.from_rgb(0, 170, 255),
)
ranking.set_footer(text="\N{WHITE HEAVY CHECK MARK} Mis à jour le 1er de chaque mois à 8h00")
data, level_cap = parse_data(await load_skyblock())
data = parse_data(await load_skyblock())
for category in data:
if isinstance(data[category], dict):
data, messages = generate_ranking_message(data, category, level_cap)
messages = generate_ranking_message(data, category)
ranking.add_field(
name=f"**[ {category.capitalize()} ]**",
value="\n".join(messages),
Expand Down
9 changes: 9 additions & 0 deletions chouette/utils/skyblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ async def get_hypixel_player(session: ClientSession, api_key: str, uuid: str) ->
async def get_networth(session: ClientSession, pseudo: str, profile_id: str) -> float:
"""Retourne la fortune d'un joueur Skyblock à l'aide de l'API SkyCrypt."""
async with session.get(f"https://sky.shiiyu.moe/api/v2/profile/{pseudo}") as response:
if "Cloudflare" in await response.text():
raise Exception("Fetching networth on SkyCrypt API failed because of Cloudflare")
json: dict = await response.json()
if response.status != 200 and json.get("error") == "Player has no SkyBlock profiles.":
async with session.get(f"https://sky.shiiyu.moe/stats/{pseudo}") as response_error:
Expand Down Expand Up @@ -163,6 +165,13 @@ async def pseudo_to_profile(
return discord[1]
discord = discord[1]
if discord != discord_pseudo:
if discord.lower() == discord_pseudo:
return (
"Vous avez entré le bon pseudo Discord sur Hypixel "
"mais il contient des majuscules !"
)
if not discord.islower():
return "Le pseudo Discord entré sur Hypixel contient des majuscules !"
return "Votre pseudo Discord ne correspond pas à celui entré sur le serveur Hypixel"
client.bot_logger.debug("Les pseudos Discord correspondent")

Expand Down

0 comments on commit 07e3421

Please sign in to comment.