mmolbbot/cogs/team.py
2025-04-24 22:59:30 -04:00

305 lines
12 KiB
Python

import hashlib
import json
from pathlib import Path
from datetime import datetime, timedelta, timezone
import requests
import re
import asyncio
import nextcord
from nextcord.ext import commands, application_checks
from nextcord import TextInputStyle, IntegrationType
#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"]:
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
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.fields[0].value)
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)
class team(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
@nextcord.slash_command(
name="greaterteam",
description="Get information about a greater leauge 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, teamid: str = nextcord.SlashOption(
name = "team",
description = "The greater leauge team",
choices = {
"Seattle Shine" : "6805db0cac48194de3cd40a2",
"Chicago Seers": "6805db0cac48194de3cd40b5",
"Atlanta Tree Frogs": "6805db0cac48194de3cd40ee",
"Baltimore Lady Beetles": "6805db0cac48194de3cd407c",
"St. Louis Archers": "6805db0cac48194de3cd400a",
"Anaheim Angles": "6805db0cac48194de3cd401d",
"Miami Merfolk": "6805db0cac48194de3cd4101",
"Washington Baseball Team": "6805db0cac48194de3cd3ff7",
"Dallas Instruments": "6805db0cac48194de3cd4114",
"Roswell Weather Balloons": "6805db0cac48194de3cd40c8",
"Toronto Northern Lights": "6805db0cac48194de3cd4043",
"Kansas City Stormchasers": "6805db0cac48194de3cd4069",
"Philadelphia Phantasms": "6805db0cac48194de3cd40db",
"Durhamshire Badgers": "6805db0cac48194de3cd4056",
"Boston Street Sweepers": "6805db0cac48194de3cd408f",
"Brooklyn Scooter Dodgers": "6805db0cac48194de3cd4030"
}
)):
await interaction.response.defer()
data = requests.get(f"https://mmolb.com/api/team/{teamid}").json()
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="Team ID", value=f"{teamid}", inline=False)
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="Augments", value=f"{data["Augments"]}", inline=True)
embed.add_field(name="Champtionships", value=f"{data["Championships"]}", inline=True)
await interaction.edit_original_message(embed=embed,view=TeamView())
@nextcord.slash_command(
name="lesserteam",
description="Get information about a lesser leauge 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 lesserteam(self, interaction: nextcord.Interaction, teamid: str = nextcord.SlashOption(
name = "team",
description = "The lesser leauge team"
)):
await interaction.response.defer()
data = requests.get(f"https://mmolb.com/api/team/{teamid}").json()
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="Team ID", value=f"{teamid}", inline=False)
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="Augments", value=f"{data["Augments"]}", inline=True)
embed.add_field(name="Champtionships", value=f"{data["Championships"]}", inline=True)
await interaction.edit_original_message(embed=embed,view=TeamView())
@nextcord.slash_command(
name="teamstats",
description="Get a teams stats",
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, teamid: str):
await interaction.response.defer()
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}```")
def setup(bot: commands.Bot):
bot.add_cog(team(bot))