mirror of
https://github.com/insertapp/mmolbbot.git
synced 2025-07-01 14:27:03 +00:00
305 lines
12 KiB
Python
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)) |