mmolbbot/cogs/team.py

401 lines
16 KiB
Python

import hashlib
import json
from pathlib import Path
from datetime import datetime, timedelta, timezone
import requests
import re
import asyncio
import nextcord
import itertools
from nextcord.ext import commands, application_checks, tasks
from nextcord import TextInputStyle, IntegrationType
teams_dict = {}
teams_list = []
def get_all_teams():
data = requests.get("https://freecashe.ws/api/teams").json()
teams_dict.clear()
teams_list.clear()
for index in data["items"]:
teams_list.append(f"{index["location"]} {index["name"]}")
teams_dict.update({f"{index["location"]} {index["name"]}": index["team_id"]})
print(teams_dict)
print(teams_list)
#all of this code is by evilscientist3, full credit to them
HTTP_CACHE_DIR = Path("http_cache")
statdict = {
"BA":(0.4, 0.35, 0.3, 0.25, 0.2, 0.15),
"OBP":(0.475, 0.425, 0.375, 0.325, 0.275, 0.225),
"SLG":(0.55, 0.52, 0.46, 0.4, 0.36, 0.33),
"OPS":(1.0, 0.9, 0.8, 0.7, 0.6, 0.5),
"ERA":(2.75, 3.25, 3.75, 4.75, 5.5, 6.5),
"WHIP":(1, 1.1, 1.25, 1.4, 1.5, 1.6)
}
# whether ascending is GOOD (True) or BAD (False) for figure of merit
NGUdict = {
"BA":True,
"OBP":True,
"SLG":True,
"OPS":True,
"ERA":False,
"WHIP":False
}
# sig figs for each stat
sfdict = {
"BA":3,
"OBP":3,
"SLG":3,
"OPS":3,
"ERA":2,
"WHIP":2
}
def eval_stat(stat, val):
# modifier for if descending is better. in this case logic is reversed; most easily accounted for by multiplying bounds and values by -1
# why bother keeping descending values' bounds positive, you ask? for human readability and ease of modification, I say
mod = 1 if NGUdict[stat] else -1
# set of ANSI colour codes. best to worst, one more than bounds
colours = [32, 72, 36, 30, 33, 31, 31]
# multiply bounds to account for descending
bounds = [mod*i for i in statdict[stat]]
mval = mod*val
# flag for colour being set, as an "else" condition to the entire loop
cset = 0
# main loop - checks quality in descending order
for i, j in enumerate(bounds):
if mval > j:
colour = colours[i]
cset = 1
break
# check if value is below all bounds
if cset == 0:
colour = colours[-1]
# return stat string in correct colour
return f"\033[38;5;{colour}m{val:.{sfdict[stat]}f}\033[0m"
def stable_str_hash(in_val: str) -> str:
return hex(int(hashlib.md5(in_val.encode("utf-8")).hexdigest(), 16))[2:]
def get_json(url: str) -> dict:
HTTP_CACHE_DIR.mkdir(exist_ok=True)
cache = {}
cache_file_path = HTTP_CACHE_DIR / f"{stable_str_hash(url)}.json"
try:
with open(cache_file_path) as cache_file:
cache = json.load(cache_file)
except FileNotFoundError:
pass
# Return from cache if the cache entry is less than 5 minutes old
now = datetime.now(timezone.utc)
if (
url in cache and
"__archived_at" in cache[url] and
cache[url]["__archived_at"] > (now - timedelta(minutes=5)).isoformat()
):
return cache[url]
data = requests.get(url).json()
cache[url] = data
cache[url]["__archived_at"] = now.isoformat()
with open(cache_file_path, "w") as cache_file:
json.dump(cache, cache_file)
return data
def teamstats(MY_TEAM_ID):
finalstr = ""
team_obj = get_json(f"https://mmolb.com/api/team/{MY_TEAM_ID}")
for player in team_obj["Players"]:
if player['FirstName'] == 'Empty' and player["LastName"] == "Slot":
continue
player_obj = get_json(f"https://mmolb.com/api/player/{player['PlayerID']}")
try:
# I'm pretty sure IDs are lexicographically ordered, so we want the
# maximum value to get stats for the latest season
stats_obj = player_obj["Stats"][max(player_obj["Stats"].keys())]
except ValueError:
finalstr += (f"No stats for {player["FirstName"]} {player["LastName"]} \n")
continue
singles = stats_obj.get("singles", 0)
doubles = stats_obj.get("doubles", 0)
triples = stats_obj.get("triples", 0)
home_runs = stats_obj.get("home_runs", 0)
hits = singles + doubles + triples + home_runs
bb = stats_obj.get("walked", 0)
hbp = stats_obj.get("hit_by_pitch", 0)
earned_runs = stats_obj.get("earned_runs", 0)
hits_allowed = stats_obj.get("hits_allowed", 0)
walks = stats_obj.get("walks", 0)
try:
ab = stats_obj["at_bats"]
except KeyError:
ba_str = None
else:
ba = hits / ab
ba_str = f"BA: {eval_stat('BA', ba)} ({ab} AB)"
try:
pa = stats_obj["plate_appearances"]
ab = stats_obj["at_bats"]
except KeyError:
ops_str = None
else:
obp = (hits + bb + hbp) / pa
slg = (singles + 2 * doubles + 3 * triples + 4 * home_runs) / ab
ops = obp + slg
pa_str = dot_format(pa)
ops_str = f"OBP: {eval_stat('OBP', obp)}, SLG: {eval_stat('SLG', slg)}, OPS: {eval_stat('OPS', ops)} ({pa_str} PA)"
try:
ip = stats_obj["outs"] / 3
except KeyError:
era_str = None
else:
era = 9 * earned_runs / ip
whip = (walks + hits_allowed) / ip
ip_str = dot_format(ip)
era_str = f"ERA {eval_stat('ERA', era)} WHIP {eval_stat('WHIP', whip)} ({ip_str} IP)"
stats_str = ", ".join(s for s in [ba_str, ops_str, era_str] if s is not None)
if stats_str:
finalstr += (f"{player["Position"]} {player["FirstName"]} {player["LastName"]} ")
finalstr += "" * (30 - (len(player["Position"]) + len(player["FirstName"]) + len(player["LastName"])))
finalstr += f"{stats_str}\n"
return (finalstr if finalstr != "" else "There are no players on this team")
def dot_format(in_val: float) -> str:
ip_whole = int(in_val)
ip_remainder = int((in_val - ip_whole) / 3 * 10)
if ip_remainder == 0:
ip_str = f"{ip_whole}"
else:
ip_str = f"{ip_whole}.{ip_remainder}"
return ip_str
#-------
class TeamView(nextcord.ui.View):
def __init__(self):
super().__init__(timeout=None)
@nextcord.ui.button(
label="Players", style=nextcord.ButtonStyle.green, custom_id="team:players"
)
async def getplayersbutton(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
await interaction.response.defer(ephemeral=True)
loop = asyncio.get_event_loop()
ogmsg = interaction.message.embeds
embed = ogmsg[0]
stats = await loop.run_in_executor(None, teamstats,embed.footer.text)
splistats = [stats[i:i+1900] for i in range(0, len(stats), 1900)] #TODO make better as in don't do cutoff
for i in splistats:
await interaction.followup.send(f"```ansi\n{i}```",ephemeral=True)
@nextcord.ui.button(
label="Game History", style=nextcord.ButtonStyle.green, custom_id="team:gamehistory"
)
async def gamehistorybutton(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
await interaction.response.defer()
ogmsg = interaction.message.embeds
embed = ogmsg[0]
teamid = embed.footer.text
data = requests.get(f"https://mmolb.com/api/team/{teamid}").json()
history = requests.get(f"https://freecashe.ws/api/games?team={teamid}&season=2").json()["items"]
color = tuple(int(data["Color"][i:i+2], 16) for i in (0, 2, 4))
embed = nextcord.Embed(title=f"Last ten games for the {data["Location"]} {data["Name"]} {data["Emoji"]}", colour = nextcord.Color.from_rgb(color[0], color[1], color[2]))
embed.set_footer(text=teamid)
for index in reversed(list(itertools.islice(history, (len(history)-10 if len(history)-10 > 0 else 0) , len(history)))):
if index["away_team_id"] != teamid:
awayteamid = index["away_team_id"]
otherscore = index["last_update"]["away_score"]
ourscore = index["last_update"]["home_score"]
else:
awayteamid = index["home_team_id"]
otherscore = index["last_update"]["home_score"]
ourscore = index["last_update"]["away_score"]
tempdata = requests.get(f"https://mmolb.com/api/team/{awayteamid}").json()
embed.add_field(name=f"vs. {tempdata["Location"]} {tempdata["Name"]} {tempdata["Emoji"]} ({ourscore} - {otherscore})", value=f"[watch](<https://mmolb.com/watch/{index['game_id']}>)", inline=False)
await interaction.followup.send(embed=embed,ephemeral=True)
class team(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.updateallteams.start()
def cog_unload(self):
self.updateallteams.cancel()
@nextcord.slash_command(
name="team",
description="Get information about a team",
integration_types=[
IntegrationType.user_install,
IntegrationType.guild_install,
],
contexts=[
nextcord.InteractionContextType.guild,
nextcord.InteractionContextType.bot_dm,
nextcord.InteractionContextType.private_channel,
],
force_global=True,
)
async def greaterteam(self, interaction: nextcord.Interaction, team: str = nextcord.SlashOption(
name = "team",
description = "The team",
)):
if team not in teams_dict:
await interaction.response.send_message("Invalid Team!", ephemeral=True)
return
await interaction.response.defer()
teamid = teams_dict[team]
data = requests.get(f"https://mmolb.com/api/team/{teamid}").json()
rankdata = requests.get(f"https://mmolb.com/api/league-top-teams/{data["League"]}").json()["teams"]
ranking = 0
for i in rankdata:
ranking += 1
if i["_id"] == teamid:
break
color = tuple(int(data["Color"][i:i+2], 16) for i in (0, 2, 4))
embed = nextcord.Embed(title=f"{data["Location"]} {data["Name"]} {data["Emoji"]}", description=f"{data["Motto"]}", colour = nextcord.Color.from_rgb(color[0], color[1], color[2]))
embed.add_field(name="League", value=f"{dict((v,k) for k,v in self.bot.leagues_dict.items())[data["League"]]}", inline=True)
embed.add_field(name="Wins", value=f"{data["Record"]["Regular Season"]["Wins"]}", inline=True)
embed.add_field(name="Losses", value=f"{data["Record"]["Regular Season"]["Losses"]}", inline=True)
embed.add_field(name="Run Differential", value=f"{data["Record"]["Regular Season"]["RunDifferential"]}", inline=True)
embed.add_field(name="Rank", value=f"{ranking}", inline=True)
embed.add_field(name="Augments", value=f"{data["Augments"]}", inline=True)
embed.add_field(name="Championships", value=f"{data["Championships"]}", inline=True)
embed.set_footer(text=teamid)
await interaction.edit_original_message(embed=embed,view=TeamView())
@greaterteam.on_autocomplete("team")
async def greaterteamac(self, interaction: nextcord.Interaction, team: str):
if not team:
thanksdiscord = teams_list[:20]
await interaction.response.send_autocomplete(thanksdiscord)
return
closestteam = [name for name in teams_list if name.lower().startswith(team.lower())]
thanksdiscord = closestteam[:20]
await interaction.response.send_autocomplete(thanksdiscord)
@nextcord.slash_command(
name="gamehistory",
description="Get the last 10 games played by a team",
integration_types=[
IntegrationType.user_install,
IntegrationType.guild_install,
],
contexts=[
nextcord.InteractionContextType.guild,
nextcord.InteractionContextType.bot_dm,
nextcord.InteractionContextType.private_channel,
],
force_global=True,
)
async def gamehistory(self, interaction: nextcord.Interaction, team: str = nextcord.SlashOption(
name = "team",
description = "The team"
)):
if team not in teams_dict:
await interaction.response.send_message("Invalid Team!", ephemeral=True)
return
await interaction.response.defer()
teamid = teams_dict[team]
data = requests.get(f"https://mmolb.com/api/team/{teamid}").json()
history = requests.get(f"https://freecashe.ws/api/games?team={teamid}&season=2").json()["items"]
color = tuple(int(data["Color"][i:i+2], 16) for i in (0, 2, 4))
embed = nextcord.Embed(title=f"Last ten games for the {data["Location"]} {data["Name"]} {data["Emoji"]}", colour = nextcord.Color.from_rgb(color[0], color[1], color[2]))
embed.set_footer(text=teamid)
for index in reversed(list(itertools.islice(history, (len(history)-10 if len(history)-10 > 0 else 0) , len(history)))):
if index["away_team_id"] != teamid:
awayteamid = index["away_team_id"]
otherscore = index["last_update"]["away_score"]
ourscore = index["last_update"]["home_score"]
else:
awayteamid = index["home_team_id"]
otherscore = index["last_update"]["home_score"]
ourscore = index["last_update"]["away_score"]
tempdata = requests.get(f"https://mmolb.com/api/team/{awayteamid}").json()
embed.add_field(name=f"vs. {tempdata["Location"]} {tempdata["Name"]} {tempdata["Emoji"]} ({ourscore} - {otherscore})", value=f"[watch](<https://mmolb.com/watch/{index['game_id']}>)", inline=False)
await interaction.edit_original_message(embed=embed)
@gamehistory.on_autocomplete("team")
async def gamehistoryac(self, interaction: nextcord.Interaction, team: str):
if not team:
print("we're here")
thanksdiscord = teams_list[:20]
await interaction.response.send_autocomplete(thanksdiscord)
return
closestteam = [name for name in teams_list if name.lower().startswith(team.lower())]
thanksdiscord = closestteam[:20]
await interaction.response.send_autocomplete(thanksdiscord)
@nextcord.slash_command(
name="players",
description="Get a team's player statistics",
integration_types=[
IntegrationType.user_install,
IntegrationType.guild_install,
],
contexts=[
nextcord.InteractionContextType.guild,
nextcord.InteractionContextType.bot_dm,
nextcord.InteractionContextType.private_channel,
],
force_global=True,
)
async def teamstats(self, interaction: nextcord.Interaction, team: str):
if team not in teams_dict:
await interaction.response.send_message("Invalid Team!", ephemeral=True)
return
await interaction.response.defer()
teamid = teams_dict[team]
loop = asyncio.get_event_loop()
stats = await loop.run_in_executor(None, teamstats,teamid)
splistats = [stats[i:i+1900] for i in range(0, len(stats), 1900)] #TODO make better as in don't do cutoff
await interaction.edit_original_message(content=f"```ansi\n{splistats[0]}```")
splistats.pop(0)
for i in splistats:
await interaction.followup.send(f"```ansi\n{i}```")
@teamstats.on_autocomplete("team")
async def teamstatsac(self, interaction: nextcord.Interaction, team: str):
if not team:
print("we're here")
thanksdiscord = teams_list[:20]
await interaction.response.send_autocomplete(thanksdiscord)
return
closestteam = [name for name in teams_list if name.lower().startswith(team.lower())]
thanksdiscord = closestteam[:20]
await interaction.response.send_autocomplete(thanksdiscord)
@tasks.loop(hours=1)
async def updateallteams(self):
print("Updating teams autocomplete")
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, get_all_teams)
self.bot.teams_list = teams_list
self.bot.teams_dict = teams_dict
def setup(bot: commands.Bot):
bot.add_cog(team(bot))