diff --git a/README.md b/README.md index 44744e5..a36951a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,16 @@ if you would like to add matteo to your server to be able to set up teams and ga accepting pull requests, check the issues for to-dos. +## FAQ (this FAQ is a work in progress and will be expanded over time): +- Q: Why have the teams in my league played an uneven amount of games/why are some teams not scheduled for games for some weeks? + A: Scheduling algorithms are really hard and the way xvi chose to do it involves some teams having byes for some series of the season, it'll even out by the end and every team will play the same number of games. + +- Q: My league stopped playing randomly and I don't know why, what should I do? + A: There were probably server issues or a patch went out, use the startleague command again and things should resume from where they left off. + +- Q: What should I do if my question isn't answered by this FAQ, this readme, or the help text for the commands, or I find a bug? + A: Please submit your issue or bug to this form and Artemis will pass it along if it's something we can do anything about. https://forms.gle/PjbpfT46yuMDGca46 + ## commands: (everything here is case sensitive, and can be prefixed with either m; or m!) ### team commands: @@ -19,28 +29,35 @@ accepting pull requests, check the issues for to-dos. - the second line is the team's icon and slogan, generally this is an emoji followed by a space, followed by a short slogan. - the third line must be blank. - the next lines are your batters' names in the order you want them to appear in your lineup, lineups can contain any number of batters between 1 and 12. - - then another blank line seperating your batters and your pitchers. + - then another blank line separating your batters and your pitchers. - the final lines are the names of the pitchers in your rotation, rotations can contain any number of pitchers between 1 and 8. - if you did it correctly, you'll get a team embed with a prompt to confirm. hit the 👍 and your team will be saved! -- m;deleteteam [teamname] \(requires team ownership) +- m;deleteteam [teamname] (requires team ownership) - allows you to delete the team with the provided name. you'll get an embed with a confirmation to prevent accidental deletions. hit the 👍 and your team will be deleted. - m;import - imports an onomancer collection as a new team. you can use the new onomancer simsim setting to ensure compatibility. similarly to saveteam, you'll get a team embed with a prompt to confirm, hit the 👍 and your team will be saved! #### editing (all of these commands require ownership and exact spelling of the team name): -- m;addplayer batter/pitcher [team name] \[player name] +- m;replaceplayer [team name] [player to remove] [player to add] + - replaces a player on your team with a new player. if there are multiple copies of the same player on a team this will only replace the first one. use this command at the top of a list with entries separated by new lines: + - the name of the team you want to replace the player on. + - the name of the player you want to remove from the team. + - the name of the player you want to replace them with. +- m;addplayer batter/pitcher [team name] [player name] - adds a new player to the end of your team, either in the lineup or the rotation depending on which version you use. use addplayer batter or addplayer pitcher at the top of a list with entries separated by new lines: - the name of the team you want to add the player to. - the name of the player you want to add to the team. -- m;moveplayer [team name] \[player name] [new lineup/rotation position number] - - moves a player within your lineup or rotation. if you want to instead move a player from your rotation to your lineup or vice versa, use m;swapsection instead. use this command at the top of a list with entries separated by new lines: +- m;moveplayer (batter/pitcher) [team name] [player name] [new lineup/rotation position number] + - moves a player within your lineup or rotation. if you want to instead move a player from your rotation to your lineup or vice versa, use m;swapsection instead. + - you can optionally specify batter or pitcher if you have a player in both your rotation and lineup and you want to move a specific one. you don't need to include it if you only have one copy of the player on your team. + - use this command at the top of a list with entries separated by new lines: - the name of the team you want to move the player on. - the name of the player you want to move. - the position you want to move them too, indexed with 1 being the first position of the lineup or rotation. all players below the specified position in the lineup or rotation will be pushed down. -- m;swapsection [team name] \[player name] +- m;swapsection [team name] [player name] - swaps a player from your lineup to the end of your rotation or your rotation to the end of your lineup. use this command at the top of a list with entries separated by new lines: - the name of the team you want to swap the player on. - the name of the player you want to swap. -- m;removeplayer [team name] \[player name] +- m;removeplayer [team name] [player name] - removes a player from your team. if there are multiple copies of the same player on a team this will only delete the first one. use this command at the top of a list with entries separated by new lines: - the name of the team you want to remove the player from. - the name of the player you want to remove. @@ -86,26 +103,34 @@ accepting pull requests, check the issues for to-dos. ### league commands - all of these commands are for leagues that have already been started. to start a league, click the 'create a league' button on the website and fill out the info for your league there, then use the m;claimleague command in discord to set yourself as the owner. - commissioner commands (all of these except for m;claimleague require ownership of the specified league): - - m;claimleague [leaguename] + - m;claimleague [league name] - sets yourself as the owner of an unclaimed league created on the website. make sure to do this as soon as possible since if someone does this before you, you will not have access to the league. - - m;addleagueowner [leaguename] + - m;addleagueowner [league name] - use this command at the top of a list of @mentions, with entries separated by new lines, of people you want to have owner powers in your league. - - m;startleague [leaguename] --queue #/-q # --noautopostseason + - m;startleague [league name] --queue #/-q # --noautopostseason - send this command with the number of games per hour you want on the next line, minimum 1 (one game every hour), maximum 12 (one game every 5 minutes, uses spillover rules). - starts the playing of league games at the pace specified, by default will play the entire season and the postseason unless an owner pauses the league with the m;pauseleague command. - - if you use the --queue #/-q # flag, the league will only play # series' at a time before automatically pausing until you use this command again. + - if you use the --queue #/-q # flag, the league will only play # series' at a time before automatically pausing until you use this command again, by default it will play the entire season unless stopped. - if you use the --noautopostseason flag, instead of starting automatically, the league will pause at the end of the regular season and not start the postseason until you use this command again. - - m;pauseleague [leaguename] + - m;pauseleague [league name] - pauses the specified league after the current series finishes until the league is started again with m;startleague. + - m;leagueseasonreset [league name] + - completely scraps the given league's current season, resetting everything to day 1 of the current season. make sure to use m;startleague again to restart the season afterwards. - general commands (all of these can be used by anyone): - - m;leaguestandings [leaguename] --season #/-s # + - m;leaguestandings [league name] --season #/-s # - displays the current standings for the specified league. - by default this will display the standings for the current season but if the --season #/-s # flag is set it will instead display the standings for the #th season instead for viewing historical standings. - - m;leaguewildcard [leaguename] + - m;leaguewildcard [league name] - displays the wild card standings for the specified league. if the league doesn't have wild cards, it will instead tell you that. - - m;leagueschedule [leaguename] + - m;leagueschedule [league name] - displays the upcoming schedule for the specified league. shows the current series and the next three series after that for every team. - + - m;teamchedule [league name] [team name] + - displays the upcoming schedule for the specified team within the specified league. shows the current series and the next six series after that for the given team. + - m;leagueleaders [league name] [stat] + - displays a league's leaders in the given stat. + - the currently available starts are: + - for batters: avg (batting average), slg (slugging percentage), obp (on-base percentage), ops (on-base plus slugging). + - for pitchers era (earned run average), whip (walks and hits per innings pitched), kper9 (strikeouts per 9 innings), bbper9 (walks per 9 innings), kperbb (strikeout to walk ratio). ### player commands: - m;showplayer [name] - displays any name's stars, there's a limit of 70 characters. that should be *plenty*. note: if you want to lookup a lot of different players you can do it on onomancer instead of spamming this command a bunch and clogging up discord: https://onomancer.sibr.dev/reflect @@ -132,6 +157,7 @@ these folks are helping me a *ton* via patreon, and i cannot possibly thank them - Ryan Littleton - Evie Diver - iliana etaoin +- yooori ## Attribution diff --git a/games.py b/games.py index 69d1d16..f12b787 100644 --- a/games.py +++ b/games.py @@ -32,10 +32,12 @@ def all_weathers(): weathers_dic = { #"Supernova" : weather("Supernova", "🌟"), #"Midnight": weather("Midnight", "🕶"), - "Slight Tailwind": weather("Slight Tailwind", "🏌️‍♀️"), + #"Slight Tailwind": weather("Slight Tailwind", "🏌️‍♀️"), "Heavy Snow": weather("Heavy Snow", "❄"), "Twilight" : weather("Twilight", "👻"), - "Thinned Veil" : weather("Thinned Veil", "🌌") + "Thinned Veil" : weather("Thinned Veil", "🌌"), + "Heat Wave" : weather("Heat Wave", "🌄"), + "Drizzle" : weather("Drizzle", "🌧") } return weathers_dic @@ -114,6 +116,11 @@ class team(object): else: return (None, None, None) + def find_player_spec(self, name, roster): + for s_index in range(0,len(roster)): + if roster[s_index].name == name: + return (roster[s_index], s_index) + def average_stars(self): total_stars = 0 for _player in self.lineup: @@ -151,6 +158,21 @@ class team(object): return True else: return False + + def slide_player_spec(self, this_player_name, new_spot, roster): + index = None + for s_index in range(0,len(roster)): + if roster[s_index].name == this_player_name: + index = s_index + this_player = roster[s_index] + if index is None: + return False + elif new_spot <= len(roster): + roster.pop(index) + roster.insert(new_spot-1, this_player) + return True + else: + return False def add_lineup(self, new_player): if len(self.lineup) < 20: @@ -169,7 +191,7 @@ class team(object): def set_pitcher(self, rotation_slot = None, use_lineup = False): temp_rotation = self.rotation.copy() if use_lineup: - for batter in self.rotation: + for batter in self.lineup: temp_rotation.append(batter) if rotation_slot is None: self.pitcher = random.choice(temp_rotation) @@ -616,6 +638,25 @@ class game(object): offense_team.lineup_position += 1 #put next batter up if self.outs >= 3: self.flip_inning() + if self.weather.name == "Heat Wave": + if self.top_of_inning: + self.weather.home_pitcher = self.get_pitcher() + if self.inning >= self.weather.counter_home: + self.weather.counter_home = self.weather.counter_home - (self.weather.counter_home % 5) + 5 + random.randint(1,4) #rounds down to last 5, adds up to next 5. then adds a random number 2<=x<=5 to determine next pitcher + tries = 0 + while self.get_pitcher() == self.weather.home_pitcher and tries < 3: + self.teams["home"].set_pitcher(use_lineup = True) + tries += 1 + + + else: + self.weather.away_pitcher = self.get_pitcher() + if self.inning >= self.weather.counter_away: + self.weather.counter_away = self.weather.counter_away - (self.weather.counter_away % 5) + 5 + random.randint(1,4) + tries = 0 + while self.get_pitcher() == self.weather.away_pitcher and tries < 3: + self.teams["away"].set_pitcher(use_lineup = True) + tries += 1 return (result, scores_to_add) #returns ab information and scores @@ -635,6 +676,12 @@ class game(object): self.over = True self.top_of_inning = not self.top_of_inning + if self.weather.name == "Drizzle": + if self.top_of_inning: + self.bases[2] = self.teams["away"].lineup[(self.teams["away"].lineup_position-1) % len(self.teams["away"].lineup)] + else: + self.bases[2] = self.teams["home"].lineup[(self.teams["home"].lineup_position-1) % len(self.teams["home"].lineup)] + def pitcher_insert(self, this_team): rounds = math.ceil(this_team.lineup_position / len(this_team.lineup)) position = random.randint(0, len(this_team.lineup)-1) @@ -837,7 +884,7 @@ class weather(object): def __init__(self, new_name, new_emoji): self.name = new_name - self.emoji = new_emoji + self.emoji = new_emoji + "\uFE00" self.counter_away = 0 self.counter_home = 0 diff --git a/league_storage.py b/league_storage.py index 183003e..2b36a6f 100644 --- a/league_storage.py +++ b/league_storage.py @@ -3,6 +3,7 @@ import sqlite3 as sql data_dir = "data" league_dir = "leagues" +statements_file = os.path.join(data_dir, "sql_statements.xvi") def create_connection(league_name): #create connection, create db if doesn't exist @@ -21,6 +22,45 @@ def create_connection(league_name): print("oops, db connection no work") return conn +def statements(): + if not os.path.exists(os.path.dirname(statements_file)): + os.makedirs(os.path.dirname(statements_file)) + if not os.path.exists(statements_file): + #generate default statements: bat_base and pitch_base to be appended with a relevant ORDER BY statement + config_dic = { + "bat_base" : """SELECT name, team_name, + plate_appearances - (walks_taken + sacrifices) as ABs, + ROUND(hits*1.0 / (plate_appearances - (walks_taken + sacrifices)*1.0),3) as BA, + ROUND(total_bases*1.0 / (plate_appearances - (walks_taken + sacrifices)*1.0),3) as SLG, + ROUND((walks_taken + hits)*1.0/plate_appearances*1.0,3) as OBP, + ROUND((walks_taken + hits)*1.0/plate_appearances*1.0,3) + ROUND(total_bases*1.0 / (plate_appearances - (walks_taken + sacrifices)*1.0),3) as OPS +FROM stats WHERE plate_appearances > 8""", + "avg" : ["ORDER BY BA DESC;", "bat_base"], + "slg" : ["ORDER BY SLG DESC;", "bat_base"], + "obp" : ["ORDER BY OBP DESC;", "bat_base"], + "ops" : ["ORDER BY OPS DESC;", "bat_base"], + "pitch_base" : """SELECT name, team_name, + ROUND(((outs_pitched*1.0)/3.0),1) as IP, + ROUND(runs_allowed*27.0/(outs_pitched*1.0),3) as ERA, + ROUND((walks_allowed+hits_allowed)*3.0/(outs_pitched*1.0),3) as WHIP, + ROUND(walks_allowed*27.0/(outs_pitched*1.0),3) as BBper9, + ROUND(strikeouts_given*27.0/(outs_pitched*1.0),3) as Kper9, + ROUND(strikeouts_given*1.0/walks_allowed*1.0,3) as KperBB +FROM stats WHERE outs_pitched > 20 +""", + "era" : ["ORDER BY ERA ASC;", "pitch_base"], + "whip" : ["ORDER BY WHIP ASC;", "pitch_base"], + "kper9" : ["ORDER BY Kper9 DESC;", "pitch_base"], + "bbper9" : ["ORDER BY BBper9 ASC;", "pitch_base"], + "kperbb" : ["ORDER BY KperBB DESC;", "pitch_base"] + } + with open(statements_file, "w") as config_file: + json.dump(config_dic, config_file, indent=4) + return config_dic + else: + with open(statements_file) as config_file: + return json.load(config_file) + def create_season_connection(league_name, season_num): #create connection, create db if doesn't exist conn = None @@ -129,6 +169,19 @@ def add_stats(league_name, player_game_stats_list): conn.commit() conn.close() +def get_stats(league_name, stat, is_batter=True): + conn = create_connection(league_name) + stats = None + if conn is not None: + conn.row_factory = sql.Row + c=conn.cursor() + + if stat in statements().keys(): + c.execute(statements()[statements()[stat][1]]+"\n"+statements()[stat][0]) + stats = c.fetchall() + conn.close() + return stats + def update_standings(league_name, update_dic): if league_exists(league_name): conn = create_connection(league_name) @@ -168,6 +221,13 @@ def season_save(league): if "." in item.name: os.rename(os.path.join(data_dir, league_dir, league.name, item.name), os.path.join(new_dir, item.name)) +def season_restart(league): + if league_exists(league.name): + with os.scandir(os.path.join(data_dir, league_dir, league.name)) as folder: + for item in folder: + if "." in item.name: + os.remove(os.path.join(data_dir, league_dir, league.name, item.name)) + def get_past_standings(league_name, season_num): if league_exists(league_name): with os.scandir(os.path.join(data_dir, league_dir, league_name)) as folder: diff --git a/leagues.py b/leagues.py index 5e85f61..7970c0d 100644 --- a/leagues.py +++ b/leagues.py @@ -102,6 +102,14 @@ class league_structure(object): teams += teams_list return teams + def team_names_in_league(self): + teams = [] + for division in self.league.values(): + for teams_list in division.values(): + for team in teams_list: + teams.append(team.name) + return teams + def teams_in_subleague(self, subleague_name): teams = [] if subleague_name in self.league.keys(): @@ -156,32 +164,39 @@ class league_structure(object): for i in range(0, self.constraints["inter_div_games"]): #inter-division matchups extra_teams = [] for subleague in league.keys(): - division_max = 1 divisions = [] for div in league[subleague].keys(): - if division_max < len(league[subleague][div]): - divison_max = len(league[subleague][div]) divisions.append(deepcopy(league[subleague][div])) + #Check if there's an odd number of divisions last_div = None if len(divisions) % 2 != 0: last_div = divisions.pop() - - divs_a = list(chain(divisions[int(len(divisions)/2):]))[0] - if last_div is not None: - divs_a.extend(last_div[int(len(last_div)/2):]) - random.shuffle(divs_a) - - divs_b = list(chain(divisions[:int(len(divisions)/2)]))[0] - if last_div is not None: - divs_a.extend(last_div[:int(len(last_div)/2)]) - random.shuffle(divs_b) - if len(divs_a) % 2 != 0: - extra_teams.append(divs_a.pop()) - if len(divs_b) % 2 != 0: + #Get teams from half of the divisions + divs_a = list(chain(divisions[int(len(divisions)/2):]))[0] + if last_div is not None: #If there's an extra division, take half of those teams too + divs_a.extend(last_div[int(len(last_div)/2):]) + + #Get teams from the other half of the divisions + divs_b = list(chain(divisions[:int(len(divisions)/2)]))[0] + if last_div is not None: #If there's an extra division, take the rest of those teams too + divs_b.extend(last_div[:int(len(last_div)/2)]) + + #Ensure both groups have the same number of teams + #Uness logic above changes, divs_a will always be one longer than divs_b or they'll be the same + if len(divs_a) > len(divs_b): + divs_b.append(divs_a.pop()) + + #Now we shuffle the groups + random.shuffle(divs_a) + random.shuffle(divs_b) + + #If there are an odd number of teams overall, then we need to remember the extra team for later + if len(divs_a) < len(divs_b): extra_teams.append(divs_b.pop()) + #Match up teams from each group a_home = True for team_a, team_b in zip(divs_a, divs_b): if a_home: @@ -190,10 +205,11 @@ class league_structure(object): matchups.append([team_a.name, team_b.name]) a_home = not a_home + #Pair up any extra teams if extra_teams != []: if len(extra_teams) % 2 == 0: for index in range(0, int(len(extra_teams)/2)): - matchups.append(extra_teams[index], extra_teams[index+1]) + matchups.append([extra_teams[index].name, extra_teams[index+1].name]) for subleague in league.keys(): @@ -315,6 +331,30 @@ class league_structure(object): this_embed.set_footer(text=f"Standings as of day {self.day-1} / {self.season_length()}") return this_embed + def standings_embed_div(self, division, div_name): + this_embed = Embed(color=Color.purple(), title=f"{self.name} Season {self.season}") + standings = {} + for team_name, wins, losses, run_diff in league_db.get_standings(self.name): + standings[team_name] = {"wins" : wins, "losses" : losses, "run_diff" : run_diff} + teams = self.division_standings(division, standings) + + for index in range(0, len(teams)): + if index == self.constraints["division_leaders"] - 1: + teams[index][4] = "-" + else: + games_behind = ((teams[self.constraints["division_leaders"] - 1][1] - teams[index][1]) + (teams[index][2] - teams[self.constraints["division_leaders"] - 1][2]))/2 + teams[index][4] = games_behind + teams_string = "" + for this_team in teams: + if this_team[2] != 0 or this_team[1] != 0: + teams_string += f"**{this_team[0].name}\n**{this_team[1]} - {this_team[2]} WR: {round(this_team[1]/(this_team[1]+this_team[2]), 3)} GB: {this_team[4]}\n\n" + else: + teams_string += f"**{this_team[0].name}\n**{this_team[1]} - {this_team[2]} WR: - GB: {this_team[4]}\n\n" + + this_embed.add_field(name=f"{div_name} Division:", value=teams_string, inline = False) + this_embed.set_footer(text=f"Standings as of day {self.day-1} / {self.season_length()}") + return this_embed + def wildcard_embed(self): this_embed = Embed(color=Color.purple(), title=f"{self.name} Wildcard Race") standings = {} @@ -375,6 +415,22 @@ class league_structure(object): return tournaments + def stat_embed(self, stat_name): + this_embed = Embed(color=Color.purple(), title=f"{self.name} Season {self.season} {stat_name} Leaders") + stats = league_db.get_stats(self.name, stat_name.lower()) + if stats is None: + return None + else: + stat_names = list(stats[0].keys())[2:] + for index in range(0, min(10,len(stats))): + this_row = list(stats[index]) + player_name = this_row.pop(0) + content_string = f"**{this_row.pop(0)}**\n" + for stat_index in range(0, len(this_row)): + content_string += f"**{stat_names[stat_index]}**: {str(this_row[stat_index])}; " + this_embed.add_field(name=player_name, value=content_string, inline=False) + return this_embed + class tournament(object): def __init__(self, name, team_dic, series_length = 5, finals_series_length = 7, max_innings = 9, id = None, secs_between_games = 300, secs_between_rounds = 600): diff --git a/main_controller.py b/main_controller.py index 0ecfbef..176c28f 100644 --- a/main_controller.py +++ b/main_controller.py @@ -172,11 +172,24 @@ def update_loop(): state["batter"] = "-" elif this_game.top_of_inning: state["update_text"] = f"Top of {this_game.inning}. {this_game.teams['away'].name} batting!" + if this_game.weather.name == "Drizzle": + state["update_emoji"] = "🌧" + state["update_text"] += f' Due to inclement weather, {this_game.teams["away"].lineup[(this_game.teams["away"].lineup_position-1) % len(this_game.teams["away"].lineup)].name} is placed on second base.' + elif this_game.weather.name == "Heat Wave" and hasattr(this_game.weather, "home_pitcher") and this_game.weather.home_pitcher.name != state["pitcher"]: + state["update_emoji"] = "🌄" + state["update_text"] += f' {this_game.weather.home_pitcher} is exhausted from the heat. {state["pitcher"]} is forced to pitch!' + else: if this_game.inning >= this_game.max_innings: if this_game.teams["home"].score > this_game.teams["away"].score: this_game.victory_lap = True state["update_text"] = f"Bottom of {this_game.inning}. {this_game.teams['home'].name} batting!" + if this_game.weather.name == "Drizzle": + state["update_emoji"] = "🌧" + state["update_text"] += f' Due to inclement weather, {this_game.teams["home"].lineup[(this_game.teams["home"].lineup_position-1) % len(this_game.teams["home"].lineup)].name} is placed on second base.' + elif this_game.weather.name == "Heat Wave" and hasattr(this_game.weather, "away_pitcher") and this_game.weather.away_pitcher.name != state["pitcher"]: + state["update_emoji"] = "🌄" + state["update_text"] += f' {this_game.weather.away_pitcher} is exhausted from the heat. {state["pitcher"]} is forced to pitch!' elif state["update_pause"] != 1 and test_string != "Game not started.": if "steals" in this_game.last_update[0].keys(): @@ -225,6 +238,8 @@ def update_loop(): elif "error" in this_game.last_update[0].keys(): state["update_emoji"] = "👻" state["update_text"] = f"{this_game.last_update[0]['batter']}'s hit goes ethereal, and {this_game.last_update[0]['defender']} can't catch it! {this_game.last_update[0]['batter']} reaches base safely." + if this_game.last_update[1] > 0: + updatestring += f"{this_game.last_update[1]} runs scored!" state["bases"] = this_game.named_bases() diff --git a/the_prestige.py b/the_prestige.py index c91853c..1003b1a 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -1,7 +1,7 @@ import discord, json, math, os, roman, games, asyncio, random, main_controller, threading, time, urllib, leagues, datetime import database as db import onomancer as ono -from league_storage import league_exists, season_save +from league_storage import league_exists, season_save, season_restart from the_draft import Draft, DRAFT_ROUNDS from flask import Flask from uuid import uuid4 @@ -370,13 +370,26 @@ class MovePlayerCommand(Command): if owner_id != msg.author.id and msg.author.id not in config()["owners"]: await msg.channel.send("You're not authorized to mess with this team. Sorry, boss.") return - elif not team.slide_player(player_name, new_pos): - await msg.channel.send("You either gave us a number that was bigger than your current roster, or we couldn't find the player on the team. Try again.") - return else: - await msg.channel.send(embed=build_team_embed(team)) - games.update_team(team) - await msg.channel.send("Paperwork signed, stamped, copied, and faxed up to the goddess. Xie's pretty quick with this stuff.") + if team.find_player(player_name)[2] is None or len(team.find_player(player_name)[2]) <= new_pos: + await msg.channel.send("You either gave us a number that was bigger than your current roster, or we couldn't find the player on the team. Try again.") + return + + if "batter" in command.split("\n")[0].lower(): + roster = team.lineup + elif "pitcher" in command.split("\n")[0].lower(): + roster = team.rotation + else: + roster = None + + if (roster is not None and team.slide_player_spec(player_name, new_pos, roster)) or (roster is None and team.slide_player(player_name, new_pos)): + await msg.channel.send(embed=build_team_embed(team)) + games.update_team(team) + await msg.channel.send("Paperwork signed, stamped, copied, and faxed up to the goddess. Xie's pretty quick with this stuff.") + else: + await msg.channel.send("You either gave us a number that was bigger than your current roster, or we couldn't find the player on the team. Try again.") + return + except IndexError: await msg.channel.send("Four lines, remember? Command, then team, then name, and finally, new spot on the lineup or rotation.") @@ -769,44 +782,13 @@ class StartDraftCommand(Command): raise SlowDraftError('Too slow') return draft_message -class DebugLeagueStart(Command): - name = "startdebugleague" - - async def execute(self, msg, command): - if not league_exists("test2"): - league = leagues.league_structure("test2") - league.setup({ - "nL" : { - "nL west" : [get_team_fuzzy_search("lockpicks"), get_team_fuzzy_search("liches")], - "nL east" : [get_team_fuzzy_search("bethesda soft"), get_team_fuzzy_search("traverse city")] - }, - "aL" : { - "aL west" : [get_team_fuzzy_search("deep space"), get_team_fuzzy_search("phoenix")], - "aL east" : [get_team_fuzzy_search("cheyenne mountain"), get_team_fuzzy_search("tarot dragons")] - } - }, division_games=6, inter_division_games=3, inter_league_games=3, games_per_hour = 12) - league.generate_schedule() - leagues.save_league(league) - -class DebugLeagueDisplay(Command): - name = "displaydebugleague" - - async def execute(self, msg, command): - if league_exists("Midseries"): - league = leagues.load_league_file("Midseries") - league.champion = "Butts" - leagues.save_league(league) - season_save(league) - league.season_reset() - - await msg.channel.send(embed=league.past_standings(1)) - class StartLeagueCommand(Command): name = "startleague" template = "m;startleague [league name]\n[games per hour]" description = """Optional flags for the first line: `--queue X` or `-q X` to play X number of series before stopping; `--noautopostseason` will pause the league before starting postseason. -Plays a league with a given name, provided that league has been saved on the website. The games per hour sets how often the games will start. (e.g. GPH 2 will start games at X:00 and X:30)""" +Starts games from a league with a given name, provided that league has been saved on the website and has been claimed using claimleague. The games per hour sets how often the games will start (e.g. GPH 2 will start games at X:00 and X:30). By default it will play the entire season followed by the postseason and then stop but this can be customized using the flags. +Not every team will play every series, due to how the scheduling algorithm is coded but it will all even out by the end.""" async def execute(self, msg, command): if config()["game_freeze"]: @@ -842,6 +824,9 @@ Plays a league with a given name, provided that league has been saved on the web except ValueError: await msg.channel.send("Chief, we need a games per hour number between 1 and 12. We think that's reasonable.") return + except IndexError: + await msg.channel.send("We need a games per hour number in the second line.") + return if league_exists(league_name): league = leagues.load_league_file(league_name) @@ -896,6 +881,60 @@ class LeagueDisplayCommand(Command): else: await msg.channel.send("Can't find that league, boss.") +class LeagueLeadersCommand(Command): + name = "leagueleaders" + template = "m;leagueleaders [league name]\n[stat name/abbreviation]" + description = "Displays a league's leaders in the given stat. A list of the allowed stats can be found on the github readme." + + async def execute(self, msg, command): + if league_exists(command.split("\n")[0].strip()): + league = leagues.load_league_file(command.split("\n")[0].strip()) + stat_name = command.split("\n")[1].strip() + try: + stat_embed = league.stat_embed(stat_name) + except IndexError: + await msg.channel.send("Nobody's played enough games to get meaningful stats in that category yet, chief. Try again after the next game or two.") + return + + if stat_embed is None: + await msg.channel.send("We don't know what that stat is, chief.") + return + try: + await msg.channel.send(embed=stat_embed) + return + except: + await msg.channel.send("Nobody's played enough games to get meaningful stats in that category yet, chief. Try again after the next game or two.") + return + + await msg.channel.send("Can't find that league, boss.") + +class LeagueDivisionDisplayCommand(Command): + name = "divisionstandings" + template = "m;divisionstandings [league name]\n[team name]" + description = "Displays the current standings for the given division in the given league." + + async def execute(self, msg, command): + if league_exists(command.split("\n")[0].strip()): + league = leagues.load_league_file(command.split("\n")[0].strip()) + division_name = command.split("\n")[1].strip() + division = None + for subleague in iter(league.league.keys()): + for div in iter(league.league[subleague].keys()): + if div == division_name: + division = league.league[subleague][div] + if division is None: + await msg.channel.send("Chief, that division doesn't exist in that league.") + return + + try: + await msg.channel.send(embed=league.standings_embed_div(division, division_name)) + except ValueError: + await msg.channel.send("Give us a proper number, boss.") + #except TypeError: + #await msg.channel.send("That season hasn't been played yet, chief.") + else: + await msg.channel.send("Can't find that league, boss.") + class LeagueWildcardCommand(Command): name = "leaguewildcard" template = "m;leaguewildcard [league name]" @@ -914,7 +953,7 @@ class LeagueWildcardCommand(Command): class LeaguePauseCommand(Command): name = "pauseleague" template = "m;pauseleague [league name]" - description = "Tells a currently running league to stop running automatically after the current series." + description = "Tells a currently running league to stop running after the current series." async def execute(self, msg, command): league_name = command.strip() @@ -976,18 +1015,60 @@ class LeagueScheduleCommand(Command): description = "Sends an embed with the given league's schedule for the next 4 series." async def execute(self, msg, command): - league_name = command.strip() + league_name = command.split("\n")[0].strip() if league_exists(league_name): league = leagues.load_league_file(league_name) current_series = league.day_to_series_num(league.day) if str(current_series+1) in league.schedule.keys(): - sched_embed = discord.Embed(title=f"{league.name}'s Schedule:") + sched_embed = discord.Embed(title=f"{league.name}'s Schedule:", color=discord.Color.magenta()) days = [0,1,2,3] for day in days: if str(current_series+day) in league.schedule.keys(): schedule_text = "" + teams = league.team_names_in_league() for game in league.schedule[str(current_series+day)]: schedule_text += f"**{game[0]}** @ **{game[1]}**\n" + teams.pop(teams.index(game[0])) + teams.pop(teams.index(game[1])) + if len(teams) > 0: + schedule_text += "Resting:\n" + for team in teams: + schedule_text += f"**{team}**\n" + sched_embed.add_field(name=f"Days {((current_series+day-1)*league.series_length) + 1} - {(current_series+day)*(league.series_length)}", value=schedule_text, inline = False) + await msg.channel.send(embed=sched_embed) + else: + await msg.channel.send("That league's already finished with this season, boss.") + else: + await msg.channel.send("We can't find that league. Typo?") + +class LeagueTeamScheduleCommand(Command): + name = "teamschedule" + template = "m;teamschedule [league name]\n[team name]" + description = "Sends an embed with the given team's schedule in the given league for the next 7 series." + + async def execute(self, msg, command): + league_name = command.split("\n")[0].strip() + team_name = command.split("\n")[1].strip() + team = get_team_fuzzy_search(team_name) + if league_exists(league_name): + league = leagues.load_league_file(league_name) + current_series = league.day_to_series_num(league.day) + + if team.name not in league.team_names_in_league(): + await msg.channel.send("Can't find that team in that league, chief.") + return + + if str(current_series+1) in league.schedule.keys(): + sched_embed = discord.Embed(title=f"{team.name}'s Schedule for the {league.name}:", color=discord.Color.purple()) + days = [0,1,2,3,4,5,6] + for day in days: + if str(current_series+day) in league.schedule.keys(): + schedule_text = "" + for game in league.schedule[str(current_series+day)]: + if team.name in game: + schedule_text += f"**{game[0]}** @ **{game[1]}**" + if schedule_text == "": + schedule_text += "Resting" sched_embed.add_field(name=f"Days {((current_series+day-1)*league.series_length) + 1} - {(current_series+day)*(league.series_length)}", value=schedule_text, inline = False) await msg.channel.send(embed=sched_embed) else: @@ -995,6 +1076,48 @@ class LeagueScheduleCommand(Command): else: await msg.channel.send("We can't find that league. Typo?") +class LeagueRegenerateScheduleCommand(Command): + name = "leagueseasonreset" + template = "m;leagueseasonreset [league name]" + description = "Completely scraps the given league's current season, resetting everything to day 1 of the current season. Requires ownership." + + async def execute(self, msg, command): + league_name = command.split("\n")[0].strip() + if league_exists(league_name): + league = leagues.load_league_file(league_name) + if (league.owner is not None and msg.author.id in league.owner) or (league.owner is not None and msg.author.id in config()["owners"]): + await msg.channel.send("You got it, boss. Give us two seconds and a bucket of white-out.") + season_restart(league) + league.season -= 1 + league.season_reset() + await asyncio.sleep(1) + await msg.channel.send("Done and dusted. Go ahead and start the league again whenever you want.") + return + else: + await msg.channel.send("That league isn't yours, boss.") + return + else: + await msg.channel.send("We can't find that league. Typo?") + +class LeagueForceStopCommand(Command): + name = "leagueforcestop" + template = "m;leagueforcestop [league name]" + description = "Halts a league and removes it from the list of currently running leagues. To be used in the case of crashed loops." + + def isauthorized(self, user): + return user.id in config()["owners"] + + async def execute(self, msg, command): + league_name = command.split("\n")[0].strip() + for index in range(0,len(active_leagues)): + if active_leagues[index].name == league_name: + active_leagues.pop(index) + await msg.channel.send("League halted, boss. We hope you did that on purpose.") + return + await msg.channel.send("That league either doesn't exist or isn't in the active list. So, huzzah?") + + + commands = [ IntroduceCommand(), @@ -1023,15 +1146,18 @@ commands = [ StartLeagueCommand(), LeaguePauseCommand(), LeagueDisplayCommand(), + LeagueLeadersCommand(), + LeagueDivisionDisplayCommand(), LeagueWildcardCommand(), LeagueScheduleCommand(), + LeagueTeamScheduleCommand(), + LeagueRegenerateScheduleCommand(), + LeagueForceStopCommand(), CreditCommand(), RomanCommand(), HelpCommand(), StartDraftCommand(), - DraftPlayerCommand(), - DebugLeagueStart(), - DebugLeagueDisplay() + DraftPlayerCommand() ] client = discord.Client() @@ -1094,7 +1220,7 @@ async def on_reaction_add(reaction, user): @client.event async def on_message(msg): - if msg.author == client.user: + if msg.author == client.user or not msg.webhook_id is None: return command_b = False @@ -1294,6 +1420,9 @@ def prepare_game(newgame, league = None, weather_name = None): if newgame.weather.name == "Heavy Snow": newgame.weather.counter_away = random.randint(0,len(newgame.teams['away'].lineup)-1) newgame.weather.counter_home = random.randint(0,len(newgame.teams['home'].lineup)-1) + elif newgame.weather.name == "Heat Wave": + newgame.weather.counter_away = random.randint(2,4) + newgame.weather.counter_home = random.randint(2,4) return newgame, state_init async def start_tournament_round(channel, tourney, seeding = None): @@ -1547,6 +1676,7 @@ def build_team_embed(team): rotation_string += f"{player.name} {player.star_string('pitching_stars')}\n" embed.add_field(name="Rotation:", value=rotation_string, inline = False) embed.add_field(name="Lineup:", value=lineup_string, inline = False) + embed.add_field(name="█a██:", value=str(abs(hash(team.name)) % (10 ** 4))) embed.set_footer(text=team.slogan) return embed