diff --git a/database.py b/database.py index 69afb31..5a63ff8 100644 --- a/database.py +++ b/database.py @@ -457,6 +457,15 @@ def set_obl_rival(base_team, rival): conn.commit() conn.close() +def clear_obl(): + conn = create_connection() + if conn is not None: + c=conn.cursor() + + c.execute("DELETE FROM one_big_league") + conn.commit() + conn.close() + def list_to_newline_string(list): string = "" for element in list: diff --git a/games.py b/games.py index 84dae73..222900d 100644 --- a/games.py +++ b/games.py @@ -1,7 +1,6 @@ -import json, random, os, math, jsonpickle +import json, random, os, math, jsonpickle, weather import database as db -import weather -from gametext import base_string, appearance_outcomes +from gametext import base_string, appearance_outcomes, game_strings_base data_dir = "data" games_config_file = os.path.join(data_dir, "games_config.json") @@ -82,17 +81,17 @@ class team(object): def find_player(self, name): for index in range(0,len(self.lineup)): - if self.lineup[index].name == name: + if self.lineup[index].name.replace(" ", " ") == name: return (self.lineup[index], index, self.lineup) for index in range(0,len(self.rotation)): - if self.rotation[index].name == name: + if self.rotation[index].name.replace(" ", " ") == name: return (self.rotation[index], index, self.rotation) 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: + if roster[s_index].name.replace(" ", " ") == name: return (roster[s_index], s_index) def average_stars(self): @@ -223,6 +222,7 @@ class game(object): self.max_innings = config()["default_length"] self.bases = {1 : None, 2 : None, 3 : None} self.weather = weather.Weather(self) + self.voice = None self.current_batter = None def occupied_bases(self): @@ -266,6 +266,7 @@ class game(object): defender = random.choice(defender_list) #make pitchers field outcome["batter"] = batter + outcome["pitcher"] = pitcher outcome["defender"] = "" player_rolls = {} @@ -285,18 +286,18 @@ class game(object): outcome["ishit"] = False fc_flag = False if roll["hitnum"] < -1.5: - outcome["text"] = random.choice([appearance_outcomes.strikeoutlooking, appearance_outcomes.strikeoutswinging]) + outcome["outcome"] = random.choice([appearance_outcomes.strikeoutlooking, appearance_outcomes.strikeoutswinging]) elif roll["hitnum"] < 1: - outcome["text"] = appearance_outcomes.groundout + outcome["outcome"] = appearance_outcomes.groundout outcome["defender"] = defender elif roll["hitnum"] < 4: - outcome["text"] = appearance_outcomes.flyout + outcome["outcome"] = appearance_outcomes.flyout outcome["defender"] = defender else: - outcome["text"] = appearance_outcomes.walk + outcome["outcome"] = appearance_outcomes.walk if self.bases[1] is not None and roll["hitnum"] < -2 and self.outs != 2: - outcome["text"] = appearance_outcomes.doubleplay + outcome["outcome"] = appearance_outcomes.doubleplay outcome["defender"] = "" #for base in self.bases.values(): @@ -313,7 +314,7 @@ class game(object): if self.outs < 2 and len(runners) > 1: #fielder's choice replaces not great groundouts if any forceouts are present def_stat = random_star_gen("defense_stars", defender) if -1.5 <= roll["hitnum"] and roll["hitnum"] < -0.5: #poorly hit groundouts - outcome["text"] = appearance_outcomes.fielderschoice + outcome["outcome"] = appearance_outcomes.fielderschoice outcome["defender"] = "" if 2.5 <= roll["hitnum"] and self.outs < 2: #well hit flyouts can lead to sacrifice flies/advanced runners @@ -322,16 +323,16 @@ class game(object): else: outcome["ishit"] = True if roll["hitnum"] < 1: - outcome["text"] = appearance_outcomes.single + outcome["outcome"] = appearance_outcomes.single elif roll["hitnum"] < 2.85 or "error" in outcome.keys(): - outcome["text"] = appearance_outcomes.double + outcome["outcome"] = appearance_outcomes.double elif roll["hitnum"] < 3.1: - outcome["text"] = appearance_outcomes.triple + outcome["outcome"] = appearance_outcomes.triple else: if self.bases[1] is not None and self.bases[2] is not None and self.bases[3] is not None: - outcome["text"] = appearance_outcomes.grandslam + outcome["outcome"] = appearance_outcomes.grandslam else: - outcome["text"] = appearance_outcomes.homerun + outcome["outcome"] = appearance_outcomes.homerun return outcome def thievery_attempts(self): #returns either false or "at-bat" outcome @@ -366,7 +367,6 @@ class game(object): defense_team = self.teams["away"] outcome = {} - outcome["steals"] = [] for baserunner, start_base in attempts: defender = random.choice(defense_team.lineup) #excludes pitcher @@ -376,14 +376,16 @@ class game(object): if start_base == 2: run_roll = run_roll * .9 #stealing third is harder if run_roll < 1: - outcome["steals"].append(f"{baserunner} was caught stealing {base_string(start_base+1)} base by {defender}!") + successful = False self.get_pitcher().game_stats["outs_pitched"] += 1 self.outs += 1 else: - outcome["steals"].append(f"{baserunner} steals {base_string(start_base+1)} base!") + successful = True self.bases[start_base+1] = baserunner self.bases[start_base] = None + self.voice.stealing(outcome, baserunner.name, base_string(start_base+1), defender.name, successful) + if self.outs >= 3: self.flip_inning() @@ -391,7 +393,7 @@ class game(object): def baserunner_check(self, defender, outcome): def_stat = random_star_gen("defense_stars", defender) - if outcome["text"] == appearance_outcomes.homerun or outcome["text"] == appearance_outcomes.grandslam: + if outcome["outcome"] == appearance_outcomes.homerun or outcome["outcome"] == appearance_outcomes.grandslam: runs = 1 for base in self.bases.values(): if base is not None: @@ -407,7 +409,7 @@ class game(object): elif "advance" in outcome.keys(): runs = 0 if self.bases[3] is not None: - outcome["text"] = appearance_outcomes.sacrifice + outcome["outcome"] = appearance_outcomes.sacrifice self.get_batter().game_stats["sacrifices"] += 1 self.bases[3] = None runs = 1 @@ -418,10 +420,12 @@ class game(object): self.bases[2] = None return runs - elif outcome["text"] == appearance_outcomes.fielderschoice: + elif outcome["outcome"] == appearance_outcomes.fielderschoice: furthest_base, runner = outcome["runners"].pop() #get furthest baserunner self.bases[furthest_base] = None outcome["fc_out"] = (runner.name, base_string(furthest_base+1)) #runner thrown out + outcome["runner"] = runner.name + outcome["base"] = furthest_base+1 for index in range(0,len(outcome["runners"])): base, this_runner = outcome["runners"].pop() self.bases[base+1] = this_runner #includes batter, at base 0 @@ -430,20 +434,20 @@ class game(object): return 1 return 0 - elif outcome["text"] == appearance_outcomes.groundout or outcome["text"] == appearance_outcomes.doubleplay: + elif outcome["outcome"] == appearance_outcomes.groundout or outcome["outcome"] == appearance_outcomes.doubleplay: runs = 0 if self.bases[3] is not None: runs += 1 self.bases[3] = None if self.bases[2] is not None: run_roll = random.gauss(2*math.erf((random_star_gen("baserunning_stars", self.bases[2])-def_stat)/4)-1,3) - if run_roll > 1.5 or outcome["text"] == appearance_outcomes.doubleplay: #double play gives them time to run, guaranteed + if run_roll > 1.5 or outcome["outcome"] == appearance_outcomes.doubleplay: #double play gives them time to run, guaranteed self.bases[3] = self.bases[2] self.bases[2] = None if self.bases[1] is not None: #double plays set this to None before this call run_roll = random.gauss(2*math.erf((random_star_gen("baserunning_stars", self.bases[1])-def_stat)/4)-1,3) if run_roll < 2 or self.bases[2] is not None: #if runner can't make it or if baserunner blocking on second, convert to fielder's choice - outcome["text"] == appearance_outcomes.fielderschoice + outcome["outcome"] == appearance_outcomes.fielderschoice runners = [(0,self.get_batter())] for base in range(1,4): if self.bases[base] == None: @@ -458,7 +462,7 @@ class game(object): elif outcome["ishit"]: runs = 0 - if outcome["text"] == appearance_outcomes.single: + if outcome["outcome"] == appearance_outcomes.single: if self.bases[3] is not None: runs += 1 self.bases[3] = None @@ -483,7 +487,7 @@ class game(object): self.bases[1] = self.get_batter() return runs - elif outcome["text"] == appearance_outcomes.double: + elif outcome["outcome"] == appearance_outcomes.double: runs = 0 if self.bases[3] is not None: runs += 1 @@ -503,7 +507,7 @@ class game(object): return runs - elif outcome["text"] == appearance_outcomes.triple: + elif outcome["outcome"] == appearance_outcomes.triple: runs = 0 for basenum in self.bases.keys(): if self.bases[basenum] is not None: @@ -515,45 +519,75 @@ class game(object): def batterup(self): scores_to_add = 0 - result = self.at_bat() - self.weather.activate(self, result) # possibly modify result in-place - - if "text_only" in result: - return (result, 0) + if "twopart" not in self.last_update[0]: + result = self.at_bat() - if self.top_of_inning: - offense_team = self.teams["away"] - defense_team = self.teams["home"] + if self.top_of_inning: + offense_team = self.teams["away"] + defense_team = self.teams["home"] + else: + offense_team = self.teams["home"] + defense_team = self.teams["away"] + + + defenders = defense_team.lineup.copy() + defenders.append(defense_team.pitcher) + defender = random.choice(defenders) #pitcher can field outs now :3 + result["defender"] = defender + result["defense_team"] = defense_team + result["offense_team"] = offense_team + + if "advance" in result.keys() and self.bases[3] is not None: + result["outcome"] = appearance_outcomes.sacrifice + result["runner"] = self.bases[3].name + + text_list = getattr(self.voice, result["outcome"].name) + voice_index = random.randrange(0, len(text_list)) + result["voiceindex"] = voice_index else: - offense_team = self.teams["home"] - defense_team = self.teams["away"] + result = {} + self.voice.activate(self.last_update[0], result, self) - defenders = defense_team.lineup.copy() - defenders.append(defense_team.pitcher) - defender = random.choice(defenders) #pitcher can field outs now :3 + if "twopart" not in result: + self.weather.activate(self, result) # possibly modify result in-place + + if "text_only" in result: + return (result, 0) + + if "twopart" in result: + if self.voice.post_format != []: + format_list = [] + for extra_format in self.voice.post_format: + if extra_format == "base": + format_list.append(base_string(result["base"])) + elif extra_format == "runner": + format_list.append(result["runner"]) + self.voice.post_format = [] + result["displaytext"] = result["displaytext"].format(*format_list) + return (result, 0) if result["ishit"]: #if batter gets a hit: self.get_batter().game_stats["hits"] += 1 self.get_pitcher().game_stats["hits_allowed"] += 1 - if result["text"] == appearance_outcomes.single: + if result["outcome"] == appearance_outcomes.single: self.get_batter().game_stats["total_bases"] += 1 - elif result["text"] == appearance_outcomes.double: + elif result["outcome"] == appearance_outcomes.double: self.get_batter().game_stats["total_bases"] += 2 - elif result["text"] == appearance_outcomes.triple: + elif result["outcome"] == appearance_outcomes.triple: self.get_batter().game_stats["total_bases"] += 3 - elif result["text"] == appearance_outcomes.homerun or result["text"] == appearance_outcomes.grandslam: + elif result["outcome"] == appearance_outcomes.homerun or result["outcome"] == appearance_outcomes.grandslam: self.get_batter().game_stats["total_bases"] += 4 self.get_batter().game_stats["home_runs"] += 1 - scores_to_add += self.baserunner_check(defender, result) + scores_to_add += self.baserunner_check(result["defender"], result) else: #batter did not get a hit - if result["text"] == appearance_outcomes.walk: + if result["outcome"] == appearance_outcomes.walk: walkers = [(0,self.get_batter())] for base in range(1,4): if self.bases[base] == None: @@ -570,29 +604,30 @@ class game(object): self.get_batter().game_stats["walks_taken"] += 1 self.get_pitcher().game_stats["walks_allowed"] += 1 - elif result["text"] == appearance_outcomes.doubleplay: + elif result["outcome"] == appearance_outcomes.doubleplay: self.get_pitcher().game_stats["outs_pitched"] += 2 self.outs += 2 self.bases[1] = None if self.outs < 3: - scores_to_add += self.baserunner_check(defender, result) + scores_to_add += self.baserunner_check(result["defender"], result) self.get_batter().game_stats["rbis"] -= scores_to_add #remove the fake rbi from the player in advance - elif result["text"] == appearance_outcomes.fielderschoice or result["text"] == appearance_outcomes.groundout: + elif result["outcome"] == appearance_outcomes.fielderschoice or result["outcome"] == appearance_outcomes.groundout: self.get_pitcher().game_stats["outs_pitched"] += 1 self.outs += 1 if self.outs < 3: - scores_to_add += self.baserunner_check(defender, result) + scores_to_add += self.baserunner_check(result["defender"], result) - elif "advance" in result.keys(): + elif "advance" in result.keys() or result["outcome"] == appearance_outcomes.sacrifice: self.get_pitcher().game_stats["outs_pitched"] += 1 self.outs += 1 if self.outs < 3: if self.bases[3] is not None: + result["runner"] = self.bases[3].name self.get_batter().game_stats["sacrifices"] += 1 - scores_to_add += self.baserunner_check(defender, result) + scores_to_add += self.baserunner_check(result["defender"], result) - elif result["text"] == appearance_outcomes.strikeoutlooking or result["text"] == appearance_outcomes.strikeoutswinging: + elif result["outcome"] == appearance_outcomes.strikeoutlooking or result["outcome"] == appearance_outcomes.strikeoutswinging: self.get_pitcher().game_stats["outs_pitched"] += 1 self.outs += 1 self.get_batter().game_stats["strikeouts_taken"] += 1 @@ -604,13 +639,24 @@ class game(object): self.get_batter().game_stats["plate_appearances"] += 1 + if self.voice.post_format != []: + format_list = [] + for extra_format in self.voice.post_format: + if extra_format == "base": + format_list.append(base_string(result["base"])) + elif extra_format == "runner": + format_list.append(result["runner"]) + self.voice.post_format = [] + result["displaytext"] = result["displaytext"].format(*format_list) + + if self.outs < 3: - offense_team.score += scores_to_add #only add points if inning isn't over + result["offense_team"].score += scores_to_add #only add points if inning isn't over else: scores_to_add = 0 self.get_batter().game_stats["rbis"] += scores_to_add self.get_pitcher().game_stats["runs_allowed"] += scores_to_add - offense_team.lineup_position += 1 #put next batter up + result["offense_team"].lineup_position += 1 #put next batter up self.choose_next_batter() if self.outs >= 3: self.flip_inning() @@ -666,7 +712,7 @@ class game(object): def gamestate_update_full(self): self.play_has_begun = True attempts = self.thievery_attempts() - if attempts == False: + if attempts == False or "twopart" in self.last_update[0]: self.last_update = self.batterup() else: self.last_update = attempts diff --git a/gametext.py b/gametext.py index d7ec310..082f9ef 100644 --- a/gametext.py +++ b/gametext.py @@ -1,4 +1,5 @@ from enum import Enum +from random import randrange class appearance_outcomes(Enum): strikeoutlooking = "strikes out looking." @@ -15,6 +16,375 @@ class appearance_outcomes(Enum): homerun = "hits a dinger!" grandslam = "hits a grand slam!" +class game_strings_base(object): + def __init__(self): + self.intro_counter = 2 + self.post_format = [] + + default_format = ("defender",) + + intro_formats = [] + intro = [("⚡","Automated Baseball Caster V16.38, now online."),("⚡", "Play ball!")] + + strikeoutlooking = ["strikes out looking."] + strikeoutswinging = ["strikes out swinging."] + groundout = ["grounds out to {}."] + flyout = ["flies out to {}."] + fielderschoice = ["reaches on fielder's choice. {} is out at {} base."] + doubleplay = ["grounds into a double play!"] + sacrifice = ["hits a sacrifice fly towards {}."] + walk = ["draws a walk."] + single = ["hits a single!"] + double = ["hits a double!"] + triple = ["hits a triple!"] + homerun = ["hits a dinger!"] + grandslam = ["hits a grand slam!"] + + steal_caught = ["{} was caught stealing {} base by {}!"] + steal_success = ["{} steals {} base!"] + + twoparts = [] + + diff_formats = {fielderschoice[0]: ("defender", "base_string"), + steal_success[0]: ("runner", "base_string"), + steal_caught[0]: ("runner", "base_string", "defender")} + no_formats = strikeoutlooking + strikeoutswinging + doubleplay + walk + single + double + triple + homerun + grandslam + + def activate(self, lastupdate, currentupdate, game): + if "twopart" in lastupdate: + for key, value in lastupdate.items(): + if key != "twopart": + currentupdate[key] = value + currentupdate["displaytext"] = self.format_gamestring(getattr(self, currentupdate["outcome"].name)[currentupdate["voiceindex"]][1], currentupdate) + + elif "outcome" in currentupdate: + if self.check_for_twopart(getattr(self, currentupdate["outcome"].name)[currentupdate["voiceindex"]]): + currentupdate.update({ + "twopart": True, + "displaytext": f"{currentupdate['batter']} {self.format_gamestring(getattr(self, currentupdate['outcome'].name)[currentupdate['voiceindex']][0], currentupdate)}" + }) + else: + currentupdate["displaytext"] = f"{currentupdate['batter']} {self.format_gamestring(getattr(self, currentupdate['outcome'].name)[currentupdate['voiceindex']], currentupdate)}" + + def check_for_twopart(self, gamestring): + return gamestring in self.twoparts + + def format_gamestring(self, gamestring, update): + if gamestring in self.no_formats: + return gamestring + elif gamestring in self.diff_formats: + return gamestring.format(*self.parse_formats(self.diff_formats[gamestring], update)) + else: + return gamestring.format(*self.parse_formats(self.default_format, update)) + + def parse_formats(self, format_tuple, update): + out_list = [] + for string in format_tuple: + if string == "defender": + out_list.append(update['defender'].name) + elif string == "base_string": + self.post_format.append("base") + out_list.append("{}") + elif string == "batter": + out_list.append(update['batter'].name) + elif string == "pitcher": + out_list.append(update['pitcher'].name) + elif string == "fc_out" or string == "runner": + self.post_format.append("runner") + out_list.append("{}") + elif string == "defense_team": + out_list.append(update['defense_team'].name) + elif string == "offense_team": + out_list.append(update['offense_team'].name) + return tuple(out_list) + + def activate_weather(self, lastupdate, currentupdate, game): + pass + + def stealing(self, currentupdate, runner, base_string, defender, is_successful): + if is_successful: + index = randrange(0, len(self.steal_success)) + text = self.steal_success[index] + else: + index = randrange(0, len(self.steal_caught)) + text = self.steal_caught[index] + + if text not in self.no_formats: + format_list = [] + for format in self.diff_formats[text]: + if format == "runner": + format_list.append(runner) + elif format == "base_string": + format_list.append(base_string) + elif format == "defender": + format_list.append(defender) + + currentupdate["steals"] = [text.format(*format_list)] + +class TheGoddesses(game_strings_base): + + def __init__(self): + self.intro_counter = 4 + self.post_format = [] + + intro = [("💜", "This game is now blessed 💜"), ("🏳️‍⚧️","I'm Sakimori,"), ("🌺", "and i'm xvi! the sim16 goddesses are live and on-site, bringing you today's game."), ("🎆", "It's time to play ball!!")] + + strikeoutlooking = ["watches a slider barely catch the outside corner. Hang up a ꓘ!", + "looks at a fastball paint the inside of the plate, and strikes out looking.", + "can't believe it as the ump calls strike 3 on a pitch that could have gone either way!", + "freezes up as a curveball catches the bottom edge of the zone. strike iii!"] + + strikeoutswinging = ["swings at a changeup in the dirt for strike 3!", + "can't catch up to the fastball, and misses strike iii.", + "just misses the cutter and strikes out with a huge swing."] + + groundout = ["hits a sharp grounder to {}! they throw to first and get the out easily.", + ("tries to sneak a grounder between third base and the shortstop, but it's stopped by {} with a dive! They throw to first from their knees...", "and force the groundout!"), + "hits a routine ground ball to {} and is put out at first.", + ("pulls a swinging bunt to {}, who rushes forward and throws to first...", "in time! {} is out!")] + + flyout = [("shoots a line drive over {}'s head...", "They leap up and catch it! {} is out!!"), + "is out on a routine fly ball to {}.", + ("hits a high fly ball deep to center! this one looks like a dinger...", "{} jumps up and steals it! {} is out!"), + "lines a ball to first, and it's caught by {} for the easy out.", + ("hits a shallow fly to short center field! this might get down for a base hit...", "{} dives forward and catches it! We love to see it, 16.")] + + fielderschoice = ["hits a soft grounder to shortstop, and {} forces {} out the short way. {} reaches on fielder's choice this time.", + "sharply grounds to third, and the throw over to {} forces {} out with a fielder's choice."] + + doubleplay = ["grounds to {}. the throw to second makes it in time, as does the throw to first! {} turn the double play!", + ("hits a grounder tailor-made for a double play right to {}.", "Two quick throws, and {} do indeed turn two!")] + + sacrifice = [("hits a deep fly ball to right field, and {} looks ready to tag up...", "They beat the throw by a mile!"), + "sends a fly ball to deep center field, and {} comfortably tags up after the catch."] + + walk = ["watches a changeup miss low, and takes first base for free.", + "is given a walk after a slider misses the zone for ball iv.", + ("takes a close pitch that just misses inside, and is awarded a base on balls.", "saki. did you really just call it that? in [current year]?"), + "is walked on iv pitches.", + "jumps away from the plate as ball 4 misses far inside, just avoiding the hit-by-pitch and taking first on a walk."] + + single = [("tries to sneak a grounder between third base and the shortstop, but it's stopped by {} with a dive! They throw to first from their knees...", "but they beat the throw! {} is safe at first with a hit."), + ("shoots a line drive over {}'s head...", "They leap up but can't quite reach it! {} is safe at first with a single!!"), + ("pulls a swinging bunt to {}, who rushes forward and throws to first...", "{} tags the bag just before the throw, and are rewarded with an infield single!"), + "hits a soft line drive over the infield, and makes it to first as it lands in the grass for a single.", + "shoots a comebacker right over second base and pulls into first safely, racking up a base hit.", + "hits a hard grounder into the right-side gap for a base hit."] + + double = [("hits a shallow fly to short center field! this might get down for a base hit...", "{} dives for it, but can't quite get there! {} makes it around to second before the ball gets chased down. Good effort though!"), + "hits a fly ball into the outfield gap and pulls into second with a stand-up double.", + ("hits a high fly ball deep to center! this one looks like a dinger...", "hell. it bounces off the wall, and that's a double."), + "shoots a line drive right down the third base line, just getting past {} for a double!"] + + triple = ["hits a fly ball down the first base line! it lands fair, and gives {} enough time to round the bases all the way to third!!", + "stretches a line drive to the outfield gap from a double to a triple with a dive into third, just beating the throw!"] + + homerun = [("hits a high fly ball deep to center! this one looks like a dinger...", "{} jumps up for the steal but can't get to it! Rack up another dinger for {}!"), + ("hits a no-doubter over the right field wall!","Artemis won't be happy about that one 😅"), + "smacks a dinger to right field and takes a free trip around the basepaths.", + "hits a fly ball to deep left field, and it barely gets over the wall for a dinger!"] + + grandslam = ["hits a fly ball to deep left field, and it barely gets over the wall for a GRAND SLAM!!!", + ("hits a high fly ball deep to center! this one looks like a dinger...", "{} jumps up for the steal but can't get to it! {} gets a grand slam!!!")] + + steal_success = ["{} takes {} with ease on a low changeup!", + "{} steals {} after a close throw from {} is just a moment too late!", + "{} tries to catch {} stealing, but a high throw means they pull off the steal at {} just in time."] + + steal_caught = ["{} catches {} trying to steal {} with a laser throw!", + "{} tries to steal {}, but they can't beat the throw and get caught.", + "{} gets caught stealing easily with that throw from {}!"] + + diff_formats = {groundout[3][1] : ("batter",), + flyout[0][1]: ("batter",), flyout[2][1]: ("defender", "batter"), + fielderschoice[0]: ("defender", "fc_out", "batter"), fielderschoice[1]: ("base_string", "fc_out"), + doubleplay[0]: ("defender", "defense_team"), doubleplay[1][1]: ("defense_team",), + sacrifice[0][0]: ("runner",), sacrifice[1]: ("runner",), + single[0][1]: ("batter",), single[1][1]: ("batter",), single[2][1]: ("batter",), + double[0][1]: ("defender", "batter"), + triple[0]: ("batter",), + homerun[0][1]: ("defender", "batter"), + grandslam[1][1]: ("defender", "batter"), + steal_success[0]: ("runner", "base_string"), steal_success[1]: ("runner", "base_string", "defender"), steal_success[2]: ("defender", "runner", "base_string"), + steal_caught[0]: ("defender", "runner", "base_string"), steal_caught[1]: ("runner", "base_string"), steal_caught[2]: ("runner", "defender")} + + no_formats = strikeoutlooking + strikeoutswinging + walk + single[3:] + [flyout[4][0], sacrifice[0][1], + double[0][0], double[1], double[2][0], double[2][1], triple[1], + homerun[0][0], homerun[1][0], homerun[1][1], homerun[2:], + grandslam[0], grandslam[1][0]] + + twoparts = [groundout[1], groundout[3], flyout[0], flyout[2], flyout[4], walk[2], doubleplay[1], single[0], single[1], single[2], sacrifice[0], double[0], double[2], homerun[0], homerun[1], grandslam[1]] + +class TheNewGuy(game_strings_base): + def __init__(self): + self.intro_counter = 4 + self.post_format = [] + + intro = [("👋","Hey, folks! First day, great to be here."),("👋", "Never played Baseball, or, uh, seen it, but I’m really excited to learn along with you."),("👋", "Here we go! Uh, how’s it go again…"),("👋", "Play baseball!")] + + strikeoutlooking = ["watches the ball go right past ‘em. Think you’re supposed to hit it. And they’re out.", + "waits for the bat to start moving on its own, I guess? It doesn’t, so… they’re out.", + "doesn’t even try to go for the ball, apparently, and they’re out."] + + strikeoutswinging = ["swings, but the ball is already past them! They were off by a mile! They’re out.", + "swings three times, but never actually hits the ball. Out!", + "swings three times, and it looks like they’re headed home! I-in a bad way. As in they’re out."] + + groundout = ["hits the ball! It rolls over to {}, who’s throwing it to first, and… yep. That’s an out, I guess.", + "hits the ball! Nice!! And there they go, running towards first! Wait, since when did {} have the ball— and, whoops, yeah, they’re out at first.", + "hits the ball right onto the ground. It rolls over to {}, who nabs it and tosses it to first, and it looks like {}’s out.", + "hits the ball over to the right side of the field. And it looks like {}’s sending it right over to first, where {}’s out."] + + flyout = [("knocks the ball real hard right over {}.","They leap up and catch it! {} is out!!"), + "hits the ball super high up in the air! But it drops right into the hands of {}.", + ("absolutely SMASHES that ball! It’s gone!","Woah, hold on, nevermind. It looks like {} jumped up and grabbed it!"), + "bops the ball up in the air and over to {}. Worth a go, I guess?", + ("winds up a real heavy hit, and the ball goes really fast!", "Not fast enough to faze {}, though, who does a spectacular leap over and nabs it out of the air.")] + + fielderschoice = ["whacks the ball over to the left side of the baseball triangle, where {} sends {} back to their pit. {}’s on base, though!", + ("hits the ball onto the ground, where it rolls over to the second-and-a-half-base guard.","That guard puts a stop to {}’s running, but they forgot about {}!")] + + doubleplay = [("hits the ball over to {}, who picks it up and stops that runner at second.", "And they’re not done! They toss it to first and get {} out, too. Yikes."), + "hits the ball to {}, who gets that runner from first out before tossing it right back at first, and… wow. That went as poorly as it could’ve. They’re both out."] + + sacrifice = [("does a little mini-hit, bumping the ball onto the field. {}’s got it, and, yeah, they’re out at first.", "Wait, when did {} score? Oh, was that— that was clever! Very nice."), + "POUNDS the ball right up into the air! It lands right in the hands of {}, but {} takes the opportunity to stroll over to the one that gives points!"] + + walk = ["watches the ball go right past them four times, and… gets to advance? Okay!", + ("just stands there. The umpire’s calling it a ball, which… they’re all balls, aren’t they? It’s baseball, not—", "Oh, uh, right. They’re on first now. I guess."), + "hangs out for four throws, gets bored, and walks over to first. Didn’t know you can do that, but good for them!", + "gets smacked with the ball! Yikes! They’re limping over to first, so… we’re… we’re just going to keep going, then. Alright!"] + + single = [("knocks the ball real hard right over {}.", "And get away with it, too, making it to first long before the ball does!"), + "hits the ball too hard for anyone to nab it and runs right over to first.", + "barely brushes up against the ball with the bat, and it lands pretty close to them. A bunch of folks scramble for it, but not before {} makes it to first!"] + + double = ["hits the ball super hard, and while everyone’s scrambling for it they’re off to first! No, second!!", + "whacks the ball and gets out of there, making it to first and then second before anyone on the field can get the ball near them."] + + triple = ["obliterates the ball and gets moving! They’re at first! No, second! No, they’re at third! And... Oh, that’s it, they’re done. Okay.", + "hits a three-base baseball whack! They try to go a little extra, but get scared into running back to third. Works."] + + homerun = ["whacks the ball out of the park!! The crowd’s going wild as they run all the way around the baseball triangle and back to zeroth base!", + ("absolutely SMASHES that ball! It’s gone!", "And so are they, as they run all the bases and head right back home!"), + "hits a home run! Ooh, I know this one!"] + + grandslam = ["hits the ball and chases all their friends home with them!", + "whacks the ball onto the ground reeeally far away. {} gets to it eventually, but not in time to stop ANYONE from making it home!!", + "hits a quadruple home run!"] + + steal_success = ["runs to the next base too early!! You can do that??", + "is cheating!! They just ran to the next base and nobody even hit the ball!", + "gets bored of waiting and takes off, narrowly making it to the next base!"] + + steal_caught = ["tries to run to the next base too early, and gets caught cheating!", + "sees if they can get away with dashing over to the next base. They can’t, turns out.", + "tries running to the next base, but {}’s ready for them. Out!"] + + no_formats = strikeoutlooking + strikeoutswinging + walk + double + triple + steal_success + steal_caught[:2] + [flyout[2][0], flyout[4][0], fielderschoice[1][0], single[0][1], single[1], walk[1][0], walk[1][1], + homerun[0], homerun[1][0], homerun[1][1], homerun[2], grandslam[0], grandslam[2]] + + diff_formats = {groundout[2]: ("defender", "batter"), groundout[3]: ("defender", "batter"), + flyout[0][1]: ("batter",), + fielderschoice[0]: ("defender", "fc_out", "batter"), fielderschoice[1][1]: ("fc_out", "batter"), + doubleplay[0][1]: ("batter",), + sacrifice[0][1]: ("runner",), sacrifice[1]: ("defender", "runner"), + single[2]: ("batter",), + steal_caught[2]: ("defender",)} + + twoparts = [flyout[0], flyout[2], flyout[4], fielderschoice[1], doubleplay[0], sacrifice[0], walk[1], single[0], homerun[1]] + +class TrespassOracle(game_strings_base): + def __init__(self): + self.intro_counter = 1 + self.post_format = [] + + intro = [("👁‍🗨", "Trespass Oracle here for Channel 16, bringing you this full game live and ad-free thanks to the generous supporters on Patreon.")] + + strikeoutlooking = ["punches out on that one. Strike 3!", + "stands by like a couch on the side of the road for that ball. Strike 3!", + "gets caught looking there. Can't take your eyes off the pitcher for a second."] + + strikeoutswinging = ["whiffs on that last ball and they're outta here! Strike 3!", + "gets nothing but air on that ball. Strike 3!", + "squares up for a bunt, two strikes-- and it goes foul! It's gonna be a long walk back to the dugout for {}."] + + groundout = ["rolls out the red carpet to {} on that ball. Easy out.", + "hits it into shallow right field there, where {} scoops the ball up and throws it to first. {} is outta here!", + "jumps on that pitch and makes a run for it, but {} throws them out at first!"] + + flyout = ["hits a fly ball deep into centre field, and {} catches it!", + ("hits a high fly into right field--", "{} dives-- and gets it! {} is out!"), + "hits the ball deep into left field-- but {} says it's not gonna be that easy, and robs them of a home run!", + ("hits a high fly into right field--", "but {} dives into the stands to catch it! I've seen it, and I still don't believe what I just witnessed!")] + + fielderschoice = ["hits it hard on the ground-- tough play. {} reaches on fielder's choice!", + "hits that one shallow, and {}'s made their choice: {} is outta here, but {} reaches!"] + + doubleplay = ["grounds out into the double play! We really do hate to see it.", + "rolls out the red carpet to {}, and they turn the double play. They're making it look easy out there!", + "hits deep, but {} throws it into the double play there! Just a well-oiled defensive machine on the field tonight."] + + sacrifice = [("hits that one deep, and {} tags up--", "They're safe! Shook hands with danger on that one!"), + "bunts on that one, letting {} come home on the sacrifice!", + "hits a sacrifice fly! {} tags up to keep this rally alive!"] + + walk = ["draws a walk on that one. {} must not have wanted to risk it.", + "makes the walk to first.", + "gets hit by a beanball-- that one looks like it smarts! They're taking the long way round to First.", + "draws the walk there. Sometimes you've just gotta take what you can get."] + + single = ["hits that one shallow, and just makes it before {} throws to first!", + "hits the ball deep, with {} running for it-- but {} dives to first in the knick of time! This league needs instant replay!", + "hits that one deep, but {} is on it like a hawk-- Don't get cocky, kid! {} puts on the brakes, safe at first with time to spare!"] + + double = ["hits it deep and makes it to second with time to spare. The most dangerous base!", + "knocks that one into left field, swiftly rounds first-- and ends up safe at second!", + "nails the double and stops safe at second! You're halfway home, kid!"] + + triple = ["puts an exclamation point on that one! Just enough time for {} to make it to third!", + "hits a high fly on that one-- {} runs for it but it hits the grass! {} makes it to third in the knick of time!", + "absolutely nails that one-- and safe at third! We love to see it!!", + "hits that one with authority and makes it to third! But they're still a long way from home!"] + + homerun = ["hits one deep into left field-- No doubt about it, this one's a Home Run!", + "hits a high fly deep into right field-- and it's GONE! Home Run!", + "sends that one right into the stands!! HOME RUN! Wow, they are unbelievable!"] + + grandslam = ["with the GRAND SLAM! AND THIS IS, INDEED, THE GREATEST NIGHT IN THE HISTORY OF OUR SPORT.", + "with a high fly into right field there-- and it's GONE! That's a GRAND SLAM!", + "with the GRAND SLAM! And if you had any dobuts about them, that should put them to rest right there!"] + + steal_caught = ["{} was caught stealing {} base by {}!"] + steal_success = ["{} steals {} base!"] + + no_formats = strikeoutlooking + strikeoutswinging[:2] + walk[1:] + double + triple[2:] + homerun + grandslam + [flyout[1][0] + flyout[3][0] + doubleplay[0] + sacrifice[0][1]] + + diff_formats = {strikeoutswinging[2]: ("batter",), + groundout[1]: ("defender", "batter"), + flyout[1][1]: ("defender", "batter"), + fielderschoice[0]: ("batter",), fielderschoice[1]: ("defender", "runner", "batter"), + sacrifice[0][0]: ("runner",), sacrifice[1]: ("runner",), sacrifice[2]: ("runner",), + walk[0]: ("pitcher",), + single[1]: ("defender", "batter"), single[2]: ("defender", "batter"), + triple[0]: ("batter",), triple[1]: ("defender", "batter"), + steal_success[0]: ("runner", "base_string"), + steal_caught[0]: ("runner", "base_string", "defender")} + + twoparts = [flyout[1], flyout[3], sacrifice[0]] + + + +def all_voices(): + return {"default": game_strings_base, + "The Goddesses": TheGoddesses, + "The New Guy": TheNewGuy, + "Trespass Oracle": TrespassOracle} + +def weighted_voices(): #these are the ones accessible to random games + return [game_strings_base, TheGoddesses, TheNewGuy], [6, 4, 2] + + def base_string(base): if base == 1: return "first" diff --git a/league_storage.py b/league_storage.py index 443aead..af8ef4a 100644 --- a/league_storage.py +++ b/league_storage.py @@ -146,6 +146,7 @@ def save_league(league): state_dic = { "season" : league.season, "day" : league.day, + "subs" : league.subbed_channels, "last_weather_event" : league.last_weather_event_day, "constraints" : league.constraints, "game_length" : league.game_length, diff --git a/leagues.py b/leagues.py index 5a97517..dbb9fc0 100644 --- a/leagues.py +++ b/leagues.py @@ -5,6 +5,7 @@ from itertools import chain from copy import deepcopy from games import team, game from discord import Embed, Color +from uuid import uuid4 data_dir = "data" league_dir = "leagues" @@ -21,6 +22,8 @@ class league_structure(object): self.weather_override = None #set to a weather for league-wide weather effects self.last_weather_event_day = 0 self.weather_event_duration = 0 + self.postseason = True + self.subbed_channels = [] def setup(self, league_dic, division_games = 1, inter_division_games = 1, inter_league_games = 1, games_per_hour = 2): self.league = league_dic # { subleague name : { division name : [team object] } } @@ -514,7 +517,7 @@ class tournament(object): self.day = None if id is None: - self.id = random.randint(1111,9999) + self.id = str(uuid4()) else: self.id = id @@ -651,4 +654,8 @@ def load_league_file(league_name): this_league.last_weather_event_day = state_dic["last_weather_event"] except: this_league.last_weather_event_day = 0 + try: + this_league.subbed_channels = state_dic["subs"] + except: + this_league.subbed_channels = [] return this_league \ No newline at end of file diff --git a/main_controller.py b/main_controller.py index ec58a3a..95d4f82 100644 --- a/main_controller.py +++ b/main_controller.py @@ -1,4 +1,4 @@ -import asyncio, time, datetime, games, json, threading, jinja2, leagues, os, leagues, gametext +import asyncio, time, datetime, games, json, threading, jinja2, leagues, os, leagues, gametext, logging from leagues import league_structure from league_storage import league_exists from flask import Flask, url_for, Response, render_template, request, jsonify, send_from_directory, abort @@ -10,6 +10,10 @@ app.config['SECRET KEY'] = 'dev' #url = "sakimori.space:5000" #app.config['SERVER_NAME'] = url socketio = SocketIO(app) +socket_thread = None +log = logging.getLogger('werkzeug') +log.disabled = True +app.logger.disabled = True # Serve React App @app.route('/', defaults={'path': ''}) @@ -90,11 +94,11 @@ def create_league(): for (key, min_val) in [ ('division_series', 1), - ('inter_division_series', 1), - ('inter_league_series', 1) + ('inter_division_series', 0), + ('inter_league_series', 0) ]: if config[key] < min_val: - return jsonify({'status':'err_invalid_optiion_value', 'cause':key}), 400 + return jsonify({'status':'err_invalid_option_value', 'cause':key}), 400 new_league = league_structure(config['name']) new_league.setup( @@ -126,7 +130,10 @@ def handle_new_conn(data): def update_loop(): global game_states + global socket_thread while True: + if socket_thread is not None: + socket_thread.join() game_states = [] game_ids = iter(master_games_dic.copy().keys()) for game_id in game_ids: @@ -142,8 +149,13 @@ def update_loop(): state["home_score"] = this_game.teams["home"].score #update_pause = 0 #victory_lap = False if not this_game.play_has_begun: #weather_emoji - state["update_emoji"] = "🎆" #weather_text - state["update_text"] = "Play ball!" #they also need a timestamp + if state["start_delay"] - this_game.voice.intro_counter > -1: + state["update_emoji"] = "🪑" #weather_text + state["update_text"] = "The broadcast booth is being prepared..." #they also need a timestamp + else: + state["update_emoji"] = this_game.voice.intro[-this_game.voice.intro_counter][0] + state["update_text"] = this_game.voice.intro[-this_game.voice.intro_counter][1] + this_game.voice.intro_counter -= 1 state["start_delay"] -= 1 state["display_top_of_inning"] = state["top_of_inning"] @@ -168,9 +180,9 @@ def update_loop(): if this_game.victory_lap and winning_team == this_game.teams['home'].name: state["update_text"] = f"{winning_team} wins with a victory lap!" elif winning_team == this_game.teams['home'].name: - state["update_text"] = f"{winning_team} wins, shaming {this_game.teams['away'].name}!" + state["update_text"] = f"{winning_team} wins with a partial victory lap!" else: - state["update_text"] = f"{winning_team} wins!" + state["update_text"] = f"{winning_team} wins on the road!" state["pitcher"] = "-" state["batter"] = "-" @@ -189,11 +201,13 @@ def update_loop(): elif state["update_pause"] != 1 and this_game.play_has_begun: - if "weather_message" in this_game.last_update[0].keys(): + if "twopart" in this_game.last_update[0].keys(): + state["update_emoji"] = "💬" + elif "weather_message" in this_game.last_update[0].keys(): state["update_emoji"] = this_game.weather.emoji elif "ishit" in this_game.last_update[0].keys() and this_game.last_update[0]["ishit"]: state["update_emoji"] = "🏏" - elif "text" in this_game.last_update[0].keys() and this_game.last_update[0]["text"] == gametext.appearance_outcomes.walk: + elif "outcome" in this_game.last_update[0].keys() and this_game.last_update[0]["outcome"] == gametext.appearance_outcomes.walk: state["update_emoji"] = "👟" else: state["update_emoji"] = "🗞" @@ -209,23 +223,16 @@ def update_loop(): elif "text_only" in this_game.last_update[0].keys(): #this handles many weather messages state["update_text"] = this_game.last_update[0]["text"] else: - updatestring = "" - punc = "" - if this_game.last_update[0]["defender"] != "": - punc = ". " + updatestring = this_game.last_update[0]["displaytext"] - if "fc_out" in this_game.last_update[0].keys(): - name, out_at_base_string = this_game.last_update[0]['fc_out'] - updatestring = f"{this_game.last_update[0]['batter']} {this_game.last_update[0]['text'].value.format(name, out_at_base_string)} {this_game.last_update[0]['defender']}{punc}" - else: - updatestring = f"{this_game.last_update[0]['batter']} {this_game.last_update[0]['text'].value} {this_game.last_update[0]['defender']}{punc}" if this_game.last_update[1] > 0: - updatestring += f"{this_game.last_update[1]} runs scored!" + updatestring += f" {this_game.last_update[1]} runs scored!" - state["update_text"] = updatestring + state["update_text"] = f"{updatestring}" - this_game.weather.modify_atbat_message(this_game, state) + if "twopart" not in this_game.last_update[0].keys(): + this_game.weather.modify_atbat_message(this_game, state) state["bases"] = this_game.named_bases() @@ -246,5 +253,7 @@ def update_loop(): state["update_pause"] -= 1 - socketio.emit("states_update", game_states) + socket_thread = threading.Thread(target=socketio.emit, args=("states_update", game_states)) + socket_thread.start() + #socketio.emit("states_update", game_states) time.sleep(8) \ No newline at end of file diff --git a/the_draft.py b/the_draft.py index cfb4cbc..15504c9 100644 --- a/the_draft.py +++ b/the_draft.py @@ -5,10 +5,6 @@ import uuid import onomancer -DRAFT_SIZE = 20 -REFRESH_DRAFT_SIZE = 4 # fewer players remaining than this and the list refreshes -DRAFT_ROUNDS = 13 - Participant = namedtuple('Participant', ['handle', 'team']) BOOKMARK = Participant(handle="bookmark", team=None) # keep track of start/end of draft round @@ -20,15 +16,19 @@ class Draft: """ @classmethod - def make_draft(cls): - draft = cls() + def make_draft(cls, teamsize, draftsize, minsize, pitchers): + draft = cls(teamsize, draftsize, minsize, pitchers) return draft - def __init__(self): + def __init__(self, teamsize, draftsize, minsize, pitchers): + self.DRAFT_SIZE = draftsize + self.REFRESH_DRAFT_SIZE = minsize # fewer players remaining than this and the list refreshes + self.DRAFT_ROUNDS = teamsize + self.pitchers = pitchers self._id = str(uuid.uuid4())[:6] self._participants = [] self._active_participant = BOOKMARK # draft mutex - self._players = onomancer.get_names(limit=DRAFT_SIZE) + self._players = onomancer.get_names(limit=self.DRAFT_SIZE) self._round = 0 @property @@ -68,7 +68,7 @@ class Draft: self.advance_draft() def refresh_players(self): - self._players = onomancer.get_names(limit=DRAFT_SIZE) + self._players = onomancer.get_names(limit=self.DRAFT_SIZE) def advance_draft(self): """ @@ -104,12 +104,12 @@ class Draft: raise ValueError(f'Player `{player_name}` not in draft list') del self._players[player['name']] - if len(self._players) <= REFRESH_DRAFT_SIZE: + if len(self._players) <= self.REFRESH_DRAFT_SIZE: self.refresh_players() - if self._round < DRAFT_ROUNDS: + if self._round <= self.DRAFT_ROUNDS - self.pitchers: self._active_participant.team.add_lineup(games.player(json.dumps(player))) - elif self._round == DRAFT_ROUNDS: + else: self._active_participant.team.add_pitcher(games.player(json.dumps(player))) self.advance_draft() diff --git a/the_prestige.py b/the_prestige.py index 114c08b..39e306e 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -1,8 +1,8 @@ -import discord, json, math, os, roman, games, asyncio, random, main_controller, threading, time, urllib, leagues, datetime +import discord, json, math, os, roman, games, asyncio, random, main_controller, threading, time, urllib, leagues, datetime, gametext import database as db import onomancer as ono from league_storage import league_exists, season_save, season_restart -from the_draft import Draft, DRAFT_ROUNDS +from the_draft import Draft from flask import Flask from uuid import uuid4 import weather @@ -14,7 +14,7 @@ class Command: def isauthorized(self, user): return True - async def execute(self, msg, command): + async def execute(self, msg, command, flags): return class DraftError(Exception): @@ -34,7 +34,7 @@ class IntroduceCommand(Command): def isauthorized(self, user): return user.id in config()["owners"] - async def execute(self, msg, command): + async def execute(self, msg, command, flags): text = """**Your name, favorite team, and pronouns**: Matteo Prestige, CHST, they/them ***only.*** There's more than one of us up here, after all. **What are you majoring in (wrong answers only)**: Economics. **Your favorite and least favorite beverage, without specifying which**: Vanilla milkshakes, chocolate milkshakes. @@ -53,7 +53,7 @@ class CountActiveGamesCommand(Command): def isauthorized(self, user): return user.id in config()["owners"] - async def execute(self, msg, command): + async def execute(self, msg, command, flags): await msg.channel.send(f"There's {len(gamesarray)} active games right now, boss.") class RomanCommand(Command): @@ -61,18 +61,18 @@ class RomanCommand(Command): template = "m;roman [number]" description = "Converts any natural number less than 4,000,000 into roman numerals. This one is just for fun." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): try: await msg.channel.send(roman.roman_convert(command)) except ValueError: - await msg.channel.send(f"\"{command}\" isn't an integer in Arabic numerals.") + raise CommandError(f"\"{command}\" isn't an integer in Arabic numerals.") class IdolizeCommand(Command): name = "idolize" template = "m;idolize [name]" description = "Records any name as your idol, mostly for fun. There's a limit of 70 characters. That should be *plenty*." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): if (command.startswith("meme")): meme = True command = command.split(" ",1)[1] @@ -81,8 +81,7 @@ class IdolizeCommand(Command): player_name = discord.utils.escape_mentions(command.strip()) if len(player_name) >= 70: - await msg.channel.send("That name is too long. Please keep it below 70 characters, for my sake and yours.") - return + raise CommandError("That name is too long. Please keep it below 70 characters, for my sake and yours.") try: player_json = ono.get_stats(player_name) db.designate_player(msg.author, json.loads(player_json)) @@ -92,28 +91,28 @@ class IdolizeCommand(Command): await msg.channel.send(f"{player_name} is now {msg.author.display_name}'s idol.") await msg.channel.send(f"Reply if {player_name} is your idol also.") except: - await msg.channel.send("Something went wrong. Tell xvi.") + raise CommandError("Something went wrong. Tell xvi.") class ShowIdolCommand(Command): name = "showidol" template = "m;showidol" description = "Displays your idol's name and stars in a nice discord embed." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): try: player_json = db.get_user_player(msg.author) embed=build_star_embed(player_json) embed.set_footer(text=msg.author.display_name) await msg.channel.send(embed=embed) except: - await msg.channel.send("We can't find your idol. Looked everywhere, too.") + raise CommandError("We can't find your idol. Looked everywhere, too.") class ShowPlayerCommand(Command): name = "showplayer" template = "m;showplayer [name]" description = "Displays any name's stars in a nice discord embed, 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 here instead of spamming this command a bunch and clogging up discord: " - async def execute(self, msg, command): + async def execute(self, msg, command, flags): player_name = json.loads(ono.get_stats(command.split(" ",1)[1])) await msg.channel.send(embed=build_star_embed(player_name)) @@ -126,89 +125,82 @@ class StartGameCommand(Command): - and finally, optionally, the number of innings, which must be greater than 2 and less than 201. if not included it will default to 9. - this command has fuzzy search so you don't need to type the full name of the team as long as you give enough to identify the team you're looking for.""" - async def execute(self, msg, command): + async def execute(self, msg, command, flags): league = None day = None - if config()["game_freeze"]: - await msg.channel.send("Patch incoming. We're not allowing new games right now.") - return - - if "-l " in command.split("\n")[0]: - league = command.split("\n")[0].split("-l ")[1].split("-")[0].strip() - elif "--league " in command.split("\n")[0]: - league = command.split("\n")[0].split("--league ")[1].split("-")[0].strip() - try: - if "-d " in command.split("\n")[0]: - day = int(command.split("\n")[0].split("-d ")[1].split("-")[0].strip()) - elif "--day " in command.split("\n")[0]: - day = int(command.split("\n")[0].split("--day ")[1].split("-")[0].strip()) - except ValueError: - await msg.channel.send("Make sure you put an integer after the -d flag.") - return - weather_name = None - if "-w " in command.split("\n")[0]: - weather_name = command.split("\n")[0].split("-w ")[1].split("-")[0].strip() - elif "--weather " in command.split("\n")[0]: - weather_name = command.split("\n")[0].split("--weather ")[1].split("-")[0].strip() - innings = None + voice = None + + if config()["game_freeze"]: + raise CommandError("Patch incoming. We're not allowing new games right now.") + + for flag in flags: + if flag[0] == "l": + league = flag[1] + elif flag[0] == "d": + try: + day = int(flag[1]) + except: + raise CommandError("Make sure you put an integer after the -d flag.") + elif flag[0] == "w": + weather_name = flag[1] + if weather_name not in weather.all_weathers(): + raise CommandError("We can't find that weather, chief. Try again.") + elif flag[0] == "v": + if flag[1] in gametext.all_voices(): + voice = gametext.all_voices()[flag[1]] + else: + raise CommandError("We can't find that broadcaster.") + else: + raise CommandError("One or more of those flags wasn't right. Try and fix that for us and we'll see about sorting you out.") + try: team_name1 = command.split("\n")[1].strip() team1 = get_team_fuzzy_search(team_name1) team_name2 = command.split("\n")[2].strip() - team2 = get_team_fuzzy_search(team_name2) - + team2 = get_team_fuzzy_search(team_name2) + except IndexError: + raise CommandError("We need at least three lines: startgame, away team, and home team are required. Optionally, the number of innings can go at the end, if you want a change of pace.") + try: innings = int(command.split("\n")[3]) except IndexError: - try: - team_name1 = command.split("\n")[1].strip() - team1 = get_team_fuzzy_search(team_name1) - - team_name2 = command.split("\n")[2].strip() - team2 = get_team_fuzzy_search(team_name2) - except IndexError: - await msg.channel.send("We need at least three lines: startgame, away team, and home team are required. Optionally, the number of innings can go at the end, if you want a change of pace.") - return - except: - await msg.channel.send("Something about that command tripped us up. Either we couldn't find a team, or you gave us a bad number of innings.") - return + pass + except ValueError: + raise CommandError("That number of innings isn't even an integer, chief. We can't do fractional innings, nor do we want to.") if innings is not None and innings < 2: - await msg.channel.send("Anything less than 2 innings isn't even an outing. Try again.") - return + raise CommandError("Anything less than 2 innings isn't even an outing. Try again.") elif innings is not None and innings > 200 and msg.author.id not in config()["owners"]: - await msg.channel.send("Y'all can behave, so we've upped the limit on game length to 200 innings.") - return + raise CommandError("Y'all can behave, so we've upped the limit on game length to 200 innings.") if team1 is not None and team2 is not None: game = games.game(team1.finalize(), team2.finalize(), length=innings) if day is not None: game.teams['away'].set_pitcher(rotation_slot = day) game.teams['home'].set_pitcher(rotation_slot = day) + if voice is not None: + game.voice = voice() channel = msg.channel - if weather_name is not None and weather_name in weather.all_weathers().keys(): - game.weather = weather.all_weathers()[weather_name](game) - + if weather_name is not None: + game.weather = weather.all_weathers()[weather_name](game) game_task = asyncio.create_task(watch_game(channel, game, user=msg.author, league=league)) await game_task else: - await msg.channel.send("We can't find one or both of those teams. Check your staging, chief.") - return + raise CommandError("We can't find one or both of those teams. Check your staging, chief.") class StartRandomGameCommand(Command): name = "randomgame" template = "m;randomgame" description = "Starts a 9-inning game between 2 entirely random teams. Embrace chaos!" - async def execute(self, msg, command): + async def execute(self, msg, command, flags): if config()["game_freeze"]: - await msg.channel.send("Patch incoming. We're not allowing new games right now.") - return + raise CommandError("Patch incoming. We're not allowing new games right now.") channel = msg.channel await channel.send("Rolling the bones... This might take a while.") @@ -222,24 +214,19 @@ class StartRandomGameCommand(Command): class SetupGameCommand(Command): name = "setupgame" template = "m;setupgame" - description = "Begins setting up a 3-inning pickup game. Pitchers, lineups, and team names are given during the setup process by anyone able to type in that channel. Idols are easily signed up via emoji during the process. The game will start automatically after setup." + description = "Begins setting up a 5-inning pickup game. Pitchers, lineups, and team names are given during the setup process by anyone able to type in that channel. Idols are easily signed up via emoji during the process. The game will start automatically after setup." - async def execute(self, msg, command): - if len(gamesarray) > 45: - await msg.channel.send("We're running 45 games and we doubt Discord will be happy with any more. These edit requests don't come cheap.") - return - elif config()["game_freeze"]: - await msg.channel.send("Patch incoming. We're not allowing new games right now.") - return + async def execute(self, msg, command, flags): + if config()["game_freeze"]: + raise CommandError("Patch incoming. We're not allowing new games right now.") for game in gamesarray: if game.name == msg.author.name: - await msg.channel.send("You've already got a game in progress! Wait a tick, boss.") - return + raise CommandError("You've already got a game in progress! Wait a tick, boss.") try: inningmax = int(command) except: - inningmax = 3 + inningmax = 5 game_task = asyncio.create_task(setup_game(msg.channel, msg.author, games.game(msg.author.name, games.team(), games.team(), length=inningmax))) await game_task @@ -260,7 +247,7 @@ class SaveTeamCommand(Command): - 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!""" - async def execute(self, msg, command): + async def execute(self, msg, command, flags): if db.get_team(command.split('\n',1)[1].split("\n")[0]) == None: await msg.channel.send(f"Fetching players...") team = team_from_message(command) @@ -268,14 +255,14 @@ If you did it correctly, you'll get a team embed with a prompt to confirm. hit t await save_task else: name = command.split('\n',1)[1].split('\n')[0] - await msg.channel.send(f"{name} already exists. Try a new name, maybe?") + raise CommandError(f"{name} already exists. Try a new name, maybe?") class ImportCommand(Command): name = "import" template = "m;import [onomancer collection URL]" description = "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!" - async def execute(self, msg, command): + async def execute(self, msg, command, flags): team_raw = ono.get_collection(command.strip()) if not team_raw == None: team_json = json.loads(team_raw) @@ -283,29 +270,29 @@ class ImportCommand(Command): team = team_from_collection(team_json) await asyncio.create_task(save_team_confirm(msg, team)) else: - await msg.channel.send(f"{team_json['fullName']} already exists. Try a new name, maybe?") + raise CommandError(f"{team_json['fullName']} already exists. Try a new name, maybe?") else: - await msg.channel.send("Something went pear-shaped while we were looking for that collection. You certain it's a valid onomancer URL?") + raise CommandError("Something went pear-shaped while we were looking for that collection. You certain it's a valid onomancer URL?") class ShowTeamCommand(Command): name = "showteam" template = "m;showteam [name]" description = "Shows the lineup, rotation, and slogan of any saved team in a discord embed with primary stat star ratings for all of the players. This command has fuzzy search so you don't need to type the full name of the team as long as you give enough to identify the team you're looking for." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): team_name = command.strip() team = get_team_fuzzy_search(team_name) if team is not None: await msg.channel.send(embed=build_team_embed(team)) return - await msg.channel.send("Can't find that team, boss. Typo?") + raise CommandError("Can't find that team, boss. Typo?") class ShowAllTeamsCommand(Command): name = "showallteams" template = "m;showallteams" description = "Shows a paginated list of all teams available for games which can be scrolled through." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): list_task = asyncio.create_task(team_pages(msg, games.get_all_teams())) await list_task @@ -314,11 +301,10 @@ class SearchTeamsCommand(Command): template = "m;searchteams [searchterm]" description = "Shows a paginated list of all teams whose names contain the given search term." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): search_term = command.strip() if len(search_term) > 30: - await msg.channel.send("Team names can't even be that long, chief. Try something shorter.") - return + raise CommandError("Team names can't even be that long, chief. Try something shorter.") list_task = asyncio.create_task(team_pages(msg, games.search_team(search_term), search_term=search_term)) await list_task @@ -327,7 +313,7 @@ class CreditCommand(Command): template = "m;credit" description = "Shows artist credit for matteo's avatar." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): await msg.channel.send("Our avatar was graciously provided to us, with permission, by @HetreaSky on Twitter.") class SwapPlayerCommand(Command): @@ -337,26 +323,23 @@ class SwapPlayerCommand(Command): [player name]""" description = "Swaps a player from your lineup to the end of your rotation or your rotation to the end of your lineup. Requires team ownership and exact spelling of team name." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): try: team_name = command.split("\n")[1].strip() player_name = command.split("\n")[2].strip() team, owner_id = games.get_team_and_owner(team_name) if team is None: - await msg.channel.send("Can't find that team, boss. Typo?") - return + raise CommandError("Can't find that team, boss. Typo?") elif 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 + raise CommandError("You're not authorized to mess with this team. Sorry, boss.") elif not team.swap_player(player_name): - await msg.channel.send("Either we can't find that player, you've got no space on the other side, or they're your last member of that side of the roster. Can't field an empty lineup, and we *do* have rules, chief.") - return + raise CommandError("Either we can't find that player, you've got no space on the other side, or they're your last member of that side of the roster. Can't field an empty lineup, and we *do* have rules, chief.") 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.") except IndexError: - await msg.channel.send("Three lines, remember? Command, then team, then name.") + raise CommandError("Three lines, remember? Command, then team, then name.") class MovePlayerCommand(Command): name = "moveplayer" @@ -366,7 +349,7 @@ class MovePlayerCommand(Command): [new lineup/rotation position number] (indexed with 1 being the top)""" description = "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. Requires team ownership and exact spelling of team name." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): try: team_name = command.split("\n")[1].strip() player_name = command.split("\n")[2].strip() @@ -374,15 +357,12 @@ class MovePlayerCommand(Command): try: new_pos = int(command.split("\n")[3].strip()) except ValueError: - await msg.channel.send("Hey, quit being cheeky. We're just trying to help. Third line has to be a natural number, boss.") - return + raise CommandError("Hey, quit being cheeky. We're just trying to help. Third line has to be a natural number, boss.") 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 + raise CommandError("You're not authorized to mess with this team. Sorry, boss.") else: - 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 team.find_player(player_name)[2] is None or len(team.find_player(player_name)[2]) < new_pos: + raise CommandError("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.") if "batter" in command.split("\n")[0].lower(): roster = team.lineup @@ -396,11 +376,10 @@ class MovePlayerCommand(Command): 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 + raise CommandError("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.") except IndexError: - await msg.channel.send("Four lines, remember? Command, then team, then name, and finally, new spot on the lineup or rotation.") + raise CommandError("Four lines, remember? Command, then team, then name, and finally, new spot on the lineup or rotation.") class AddPlayerCommand(Command): name = "addplayer" @@ -409,37 +388,32 @@ class AddPlayerCommand(Command): [player name]""" description = "Adds a new player to the end of your team, either in the lineup or the rotation depending on which version you use. Requires team ownership and exact spelling of team name." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): try: team_name = command.split("\n")[1].strip() player_name = command.split("\n")[2].strip() if len(player_name) > 70: - await msg.channel.send("70 characters per player, boss. Quit being sneaky.") - return + raise CommandError("70 characters per player, boss. Quit being sneaky.") team, owner_id = games.get_team_and_owner(team_name) 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 + raise CommandError("You're not authorized to mess with this team. Sorry, boss.") new_player = games.player(ono.get_stats(player_name)) if "batter" in command.split("\n")[0].lower(): if not team.add_lineup(new_player)[0]: - await msg.channel.send("Too many batters 🎶") - return + raise CommandError("Too many batters 🎶") elif "pitcher" in command.split("\n")[0].lower(): if not team.add_pitcher(new_player): - await msg.channel.send("8 pitchers is quite enough, we think.") - return + raise CommandError("8 pitchers is quite enough, we think.") else: - await msg.channel.send("You have to tell us if you want a pitcher or a batter, boss. Just say so in the first line, with the command.") - return + raise CommandError("You have to tell us if you want a pitcher or a batter, boss. Just say so in the first line, with the command.") 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.") except IndexError: - await msg.channel.send("Three lines, remember? Command, then team, then name.") + raise CommandError("Three lines, remember? Command, then team, then name.") class RemovePlayerCommand(Command): name = "removeplayer" @@ -448,25 +422,23 @@ class RemovePlayerCommand(Command): [player name]""" description = "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. Requires team ownership and exact spelling of team name." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): try: team_name = command.split("\n")[1].strip() player_name = command.split("\n")[2].strip() team, owner_id = games.get_team_and_owner(team_name) 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 + raise CommandError("You're not authorized to mess with this team. Sorry, boss.") if not team.delete_player(player_name): - await msg.channel.send("We've got bad news: that player isn't on your team. The good news is that... that player isn't on your team?") - return + raise CommandError("We've got bad news: that player isn't on your team. The good news is that... that player isn't on your team?") 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.") except IndexError: - await msg.channel.send("Three lines, remember? Command, then team, then name.") + raise CommandError("Three lines, remember? Command, then team, then name.") class ReplacePlayerCommand(Command): name = "replaceplayer" @@ -476,25 +448,22 @@ class ReplacePlayerCommand(Command): [player name to **add**]""" description = "Replaces a player on your team. If there are multiple copies of the same player on a team this will only replace the first one. Requires team ownership and exact spelling of team name." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): try: team_name = command.split("\n")[1].strip() remove_name = command.split("\n")[2].strip() add_name = command.split("\n")[3].strip() if len(add_name) > 70: - await msg.channel.send("70 characters per player, boss. Quit being sneaky.") - return + raise CommandError("70 characters per player, boss. Quit being sneaky.") team, owner_id = games.get_team_and_owner(team_name) 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 + raise CommandError("You're not authorized to mess with this team. Sorry, boss.") old_player, old_pos, old_list = team.find_player(remove_name) new_player = games.player(ono.get_stats(add_name)) if old_player is None: - await msg.channel.send("We've got bad news: that player isn't on your team. The good news is that... that player isn't on your team?") - return + raise CommandError("We've got bad news: that player isn't on your team. The good news is that... that player isn't on your team?") else: if old_list == team.lineup: @@ -509,14 +478,14 @@ class ReplacePlayerCommand(Command): 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.") except IndexError: - await msg.channel.send("Four lines, remember? Command, then team, then the two names.") + raise CommandError("Four lines, remember? Command, then team, then the two names.") class HelpCommand(Command): name = "help" template = "m;help [command]" description = "Shows the instructions from the readme for a given command. If no command is provided, we will instead provide a list of all of the commands that instructions can be provided for." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): query = command.strip() if query == "": text = "Here's everything we know how to do; use `m;help [command]` for more info:" @@ -536,12 +505,11 @@ class DeleteTeamCommand(Command): template = "m;deleteteam [name]" description = "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.. Requires team ownership. If you are the owner and the bot is telling you it's not yours, contact xvi and xie can assist." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): team_name = command.strip() team, owner_id = games.get_team_and_owner(team_name) if owner_id != msg.author.id and msg.author.id not in config()["owners"]: #returns if person is not owner and not bot mod - await msg.channel.send("That team ain't yours, chief. If you think that's not right, bug xvi about deleting it for you.") - return + raise CommandError("That team ain't yours, chief. If you think that's not right, bug xvi about deleting it for you.") elif team is not None: delete_task = asyncio.create_task(team_delete_confirm(msg.channel, team, msg.author)) await delete_task @@ -554,14 +522,14 @@ class AssignOwnerCommand(Command): def isauthorized(self, user): return user.id in config()["owners"] - async def execute(self, msg, command): + async def execute(self, msg, command, flags): if self.isauthorized(msg.author): new_owner = msg.mentions[0] team_name = command.strip().split("> ",1)[1] if db.assign_owner(team_name, new_owner.id): await msg.channel.send(f"{team_name} is now owned by {new_owner.display_name}. Don't break it.") else: - await msg.channel.send("We couldn't find that team. Typo?") + raise CommandError("We couldn't find that team. Typo?") class StartTournamentCommand(Command): name = "starttournament" @@ -570,57 +538,54 @@ class StartTournamentCommand(Command): [list of teams, each on a new line]""" description = "Starts a randomly seeded tournament with the provided teams, automatically adding byes as necessary. All series have a 5 minute break between games and by default there is a 10 minute break between rounds. The current tournament format is:\nBest of 5 until the finals, which are Best of 7." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): + round_delay = 10 + series_length = 5 + finals_series_length = 7 + rand_seed = True + pre_seeded = False + if config()["game_freeze"]: - await msg.channel.send("Patch incoming. We're not allowing new games right now.") - return + raise CommandError("Patch incoming. We're not allowing new games right now.") - to_parse = command.split("\n")[0] - - if "--rounddelay " in to_parse: - try: - round_delay = int(to_parse.split("--rounddelay ")[1].split("-")[0].strip()) - except ValueError: - await msg.channel.send("The delay between rounds should be a whole number.") - return - if round_delay < 1 or round_delay > 120: - await msg.channel.send("The delay between rounds has to bebetween 1 and 120 minutes.") - return - else: - round_delay = 10 - - if "--bestof " in command.split("\n")[0]: - try: - series_length = int(to_parse.split("--bestof ")[1].split("-")[0].strip()) - if series_length % 2 == 0 or series_length < 0: - raise ValueError - except ValueError: - await msg.channel.send("Series length has to be an odd positive integer.") - return - if msg.author.id not in config()["owners"] and series_length > 21: - await msg.channel.send("That's too long, boss. We have to run patches *some* time.") - return - else: - series_length = 5 - - if "--finalsbestof " in command.split("\n")[0]: - try: - finals_series_length = int(to_parse.split("--finalsbestof ")[1].split("-")[0].strip()) - if finals_series_length % 2 == 0 or finals_series_length < 0: - raise ValueError - except ValueError: - await msg.channel.send("Finals series length has to be an odd positive integer.") - return - if msg.author.id not in config()["owners"] and finals_series_length > 21: - await msg.channel.send("That's too long, boss. We have to run patches *some* time.") - return - else: - finals_series_length = 7 - - rand_seed = not "--seeding stars" in command.split("\n")[0] - - - + for flag in flags: + if flag[0] == "r": #rounddelay + try: + round_delay = int(flag[1]) + except ValueError: + raise CommandError("The delay between rounds should be a whole number.") + if round_delay < 1 or round_delay > 120: + raise CommandError("The delay between rounds has to bebetween 1 and 120 minutes.") + elif flag[0] == "b": #bestof + try: + series_length = int(flag[1]) + if series_length % 2 == 0 or series_length < 0: + raise ValueError + except ValueError: + raise CommandError("Series length has to be an odd positive integer.") + if msg.author.id not in config()["owners"] and series_length > 21: + raise CommandError("That's too long, boss. We have to run patches *some* time.") + elif flag[0] == "f": #pay respects (finalsbestof) + try: + finals_series_length = int(flag[1]) + if finals_series_length % 2 == 0 or finals_series_length < 0: + raise ValueError + except ValueError: + raise CommandError("Finals series length has to be an odd positive integer.") + if msg.author.id not in config()["owners"] and finals_series_length > 21: + raise CommandError("That's too long, boss. We have to run patches *some* time.") + if flag[0] == "s": #seeding + if flag[1] == "stars": + rand_seed = False + elif flag[1] == "given": + rand_seed = False + pre_seeded = True + elif flag[1] == "random": + pass + else: + raise CommandError("Valid seeding types are: 'random' (default), 'stars', and 'given'.") + else: + raise CommandError("One or more of those flags wasn't right. Try and fix that for us and we'll see about sorting you out.") tourney_name = command.split("\n")[1] list_of_team_names = command.split("\n")[2:] @@ -628,8 +593,7 @@ class StartTournamentCommand(Command): for name in list_of_team_names: team = get_team_fuzzy_search(name.strip()) if team == None: - await msg.channel.send(f"We couldn't find {name}. Try again?") - return + raise CommandError(f"We couldn't find {name}. Try again?") add = True for extant_team in team_dic.keys(): if extant_team.name == team.name: @@ -638,18 +602,14 @@ class StartTournamentCommand(Command): team_dic[team] = {"wins": 0} channel = msg.channel - await msg.delete() if len(team_dic) < 2: await msg.channel.send("One team does not a tournament make.") return - id = random.randint(1111,9999) - - tourney = leagues.tournament(tourney_name, team_dic, series_length = series_length, finals_series_length = finals_series_length, id=id, secs_between_rounds = round_delay * 60) + tourney = leagues.tournament(tourney_name, team_dic, series_length = series_length, finals_series_length = finals_series_length, secs_between_rounds = round_delay * 60) tourney.build_bracket(random_sort = rand_seed) - - + await start_tournament_round(channel, tourney) @@ -658,7 +618,7 @@ class DraftPlayerCommand(Command): template = "m;draft [playername]" description = "On your turn during a draft, use this command to pick your player." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): """ This is a no-op definition. `StartDraftCommand` handles the orchestration directly, this is just here to provide a help entry and so the command dispatcher recognizes it @@ -678,8 +638,35 @@ class StartDraftCommand(Command): - use the command `d`, `draft`, or `m;draft` on your turn to draft someone """ - async def execute(self, msg, command): - draft = Draft.make_draft() + async def execute(self, msg, command, flags): + teamsize = 13 + draftsize = 20 + minsize = 4 + pitchers = 3 + + for flag in flags: + try: + if flag[0] == "t": + teamsize = int(flag[1]) + elif flag[0] == "d": + draftsize = int(flag[1]) + elif flag[0] == "r": #refreshsize, default 4 + minsize = int(flag[1]) + elif flag[0] == "p": + pitchers = int(flag[1]) + else: + raise CommandError(f"We don't recognize that {flag[0]} flag.") + except ValueError: + raise CommandError(f"Your {flag[0]} flag isn't a valid integer, boss.") + + if teamsize-pitchers > 20 or pitchers > 8: + raise CommandError("You can't fit that many players on a team, chief. Slow your roll.") + if teamsize < 3 or pitchers < 1 or draftsize < 10 or minsize < 2: + raise CommandError("One of those numbers is too low. Draft size has to be at least 10, the rest should be obvious.") + if drafsize > 40: + raise CommandError("40 players is the max. We're not too confident about pushing for more.") + + draft = Draft.make_draft(teamsize, draftsize, minsize, pitchers) mentions = {f'<@!{m.id}>' for m in msg.mentions} content = msg.content.split('\n')[1:] # drop command out of message if not content or len(content) % 3: @@ -708,23 +695,30 @@ class StartDraftCommand(Command): draft.start_draft() footer = f"The draft class of {random.randint(2007, 2075)}" - while draft.round <= DRAFT_ROUNDS: - message_prefix = f'Round {draft.round}/{DRAFT_ROUNDS}:' - if draft.round == DRAFT_ROUNDS: + while draft.round <= draft.DRAFT_ROUNDS: + message_prefix = f'Round {draft.round}/{draft.DRAFT_ROUNDS}:' + if draft.round == draft.DRAFT_ROUNDS: body = random.choice([ f"Now just choose a pitcher and we can finish off this paperwork for you, {draft.active_drafter}", f"Pick a pitcher, {draft.active_drafter}, and we can all go home happy. 'Cept your players. They'll have to play baseball.", f"Almost done, {draft.active_drafter}. Pick your pitcher.", ]) message = f"⚾️ {message_prefix} {body}" - else: + elif draft.round <= draft.DRAFT_ROUNDS - draft.pitchers: body = random.choice([ - f"Choose a batter, {draft.active_drafter}", + f"Choose a batter, {draft.active_drafter}.", f"{draft.active_drafter}, your turn. Pick one.", - f"Pick one to fill your next lineup slot, {draft.active_drafter}", + f"Pick one to fill your next lineup slot, {draft.active_drafter}.", f"Alright, {draft.active_drafter}, choose a batter.", ]) message = f"🏏 {message_prefix} {body}" + else: + body = random.choice([ + f"Warning: Pitcher Zone. Enter if you dare, {draft.active_drafter}.", + f"Time to pitch a picker, {draft.active_drafter}.\nWait, that doesn't sound right.", + f"Choose a yeeter, {draft.active_drafter}.\nDid we use that word right?", + f"Choose a pitcher, {draft.active_drafter}."]) + message = f"⚾️ {message_prefix} {body}" await msg.channel.send( message, embed=build_draft_embed(draft.get_draftees(), footer=footer), @@ -806,57 +800,57 @@ class StartLeagueCommand(Command): 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): + async def execute(self, msg, command, flags): + autoplay = -1 + autopost = False + nopost = False + if config()["game_freeze"]: - await msg.channel.send("Patch incoming. We're not allowing new games right now.") - return + raise CommandError("Patch incoming. We're not allowing new games right now.") league_name = command.split("-")[0].split("\n")[0].strip() - autoplay = None - - try: - if "--queue " in command: - autoplay = int(command.split("--queue ")[1].split("\n")[0]) - elif "-q " in command: - autoplay = int(command.split("-q ")[1].split("\n")[0]) - if autoplay is not None and autoplay <= 0: - raise ValueError - elif autoplay is None: - autoplay = -1 - except ValueError: - await msg.channel.send("Sorry boss, the queue flag needs a natural number. Any whole number over 0 will do just fine.") - return - except IndexError: - await msg.channel.send("We need a games per hour number in the second line.") - return - - + for flag in flags: + if flag[0] == "q": + try: + autoplay = int(flag[1]) + if autoplay <= 0: + raise ValueError + except ValueError: + raise CommandError("Sorry boss, the queue flag needs a natural number. Any whole number over 0 will do just fine.") + elif flag[0] == "n": #noautopostseason + await msg.channel.send("Automatic postseason is now disabled by default! No need for this flag in the future. --autopostseason (or -a) will *enable* autopostseason, should you want it.") + elif flag[0] == "a": #autopostseason + await msg.channel.send("We'll automatically start postseason for you, when we get there.") + autopost = True + elif flag[0] == "s": #skippostseason + await msg.channel.send("We'll **skip postseason** for you! Make sure you wanted to do this.") + autopost = True + nopost = True + else: + raise CommandError("One or more of those flags wasn't right. Try and fix that for us and we'll see about sorting you out.") try: gph = int(command.split("\n")[1].strip()) if gph < 1 or gph > 12: raise ValueError except ValueError: - await msg.channel.send("Chief, we need a games per hour number between 1 and 12. We think that's reasonable.") - return + raise CommandError("Chief, we need a games per hour number between 1 and 12. We think that's reasonable.") except IndexError: - await msg.channel.send("We need a games per hour number in the second line.") - return + raise CommandError("We need a games per hour number in the second line.") if league_exists(league_name): league = leagues.load_league_file(league_name) - if "--noautopostseason" in command: - await msg.channel.send("Automatic postseason disabled.") - autoplay = int(list(league.schedule.keys())[-1]) - league.day_to_series_num(league.day) + 1 + if autoplay == -1 and not autopost: + autoplay = int(list(league.schedule.keys())[-1]) - league.day_to_series_num(league.day) + 1 + if nopost: + league.postseason = False if league.historic: - await msg.channel.send("That league is done and dusted, chief. Sorry.") - return + raise CommandError("That league is done and dusted, chief. Sorry.") for active_league in active_leagues: if active_league.name == league.name: - await msg.channel.send("That league is already running, boss. Patience is a virtue, you know.") - return + raise CommandError("That league is already running, boss. Patience is a virtue, you know.") if (league.owner is not None and msg.author.id in league.owner) or msg.author.id in config()["owners"] or league.owner is None: league.autoplay = autoplay league.games_per_hour = gph @@ -867,69 +861,86 @@ Not every team will play every series, due to how the scheduling algorithm is co else: await start_league_day(msg.channel, league, partial = True) else: - await msg.channel.send("You don't have permission to manage that league.") - return + raise CommandError("You don't have permission to manage that league.") else: - await msg.channel.send("Couldn't find that league, boss. Did you save it on the website?") + raise CommandError("Couldn't find that league, boss. Did you save it on the website?") + +class LeagueSubscribeCommand(Command): + name = "leaguesub" + template = "m;leaguesub [league name]" + description = "Posts all league feed events to this channel, in addition to the channel the league was started in. Run again to unsubscribe." + + async def execute(self, msg, command, flags): + league_name = command.strip() + if league_exists(league_name): + league = leagues.load_league_file(league_name) + if msg.channel.id in league.subbed_channels: + league.subbed_channels.pop(league.subbed_channels.index(msg.channel.id)) + await msg.channel.send("You're off the mailing list, boss. We promise.") + else: + league.subbed_channels.append(msg.channel.id) + await msg.channel.send(f"Thanks for signing up to the {league_name} newsletter.") + leagues.save_league(league) + else: + raise CommandError("That league doesn't exist, boss.") class LeagueDisplayCommand(Command): name = "leaguestandings" - template = "m;leaguestandings [league name]" + template = "m;leaguestandings\n[league name]" description = "Displays the current standings for the given league. Use `--season X` or `-s X` to get standings from season X of that league." - async def execute(self, msg, command): - if league_exists(command.split("-")[0].strip()): - league = leagues.load_league_file(command.split("-")[0].strip()) - + async def execute(self, msg, command, flags): + if league_exists(command.split("\n")[1].strip()): try: - if "--season " in command: - season_num = int(command.split("--season ")[1]) - await msg.channel.send(embed=league.past_standings(season_num)) - elif "-s " in command: - season_num = int(command.split("-s ")[1]) - await msg.channel.send(embed=league.past_standings(season_num)) - else: - await msg.channel.send(embed=league.standings_embed()) - 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.") + league = leagues.load_league_file(command.split("\n")[1].strip()) + except IndexError: + raise CommandError("League name goes on the second line now, boss.") + + for flag in flags: + if flag[0] == "s": + try: + season_num = int(flag[1]) + await msg.channel.send(embed=league.past_standings(season_num)) + return + except ValueError: + raise CommandError("Give us a proper number, boss.") + except TypeError: + raise CommandError("That season hasn't been played yet, chief.") + + await msg.channel.send(embed=league.standings_embed()) else: - await msg.channel.send("Can't find that league, boss.") + raise CommandError("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): + async def execute(self, msg, command, flags): 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 + raise CommandError("Nobody's played enough games to get meaningful stats in that category yet, chief. Try again after the next game or two.") if stat_embed is None: - await msg.channel.send("We don't know what that stat is, chief.") - return + raise CommandError("We don't know what that stat is, chief.") 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 + raise CommandError("Nobody's played enough games to get meaningful stats in that category yet, chief. Try again after the next game or two.") - await msg.channel.send("Can't find that league, boss.") + raise CommandError("Can't find that league, boss.") class LeagueDivisionDisplayCommand(Command): name = "divisionstandings" template = "m;divisionstandings [league name]\n[division name]" description = "Displays the current standings for the given division in the given league." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): 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() @@ -939,39 +950,35 @@ class LeagueDivisionDisplayCommand(Command): 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 - + raise CommandError("Chief, that division doesn't exist in that league.") 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.") + except: + raise CommandError("Something went wrong, boss. Check your staging.") else: - await msg.channel.send("Can't find that league, boss.") + raise CommandError("Can't find that league, boss.") class LeagueWildcardCommand(Command): name = "leaguewildcard" template = "m;leaguewildcard [league name]" description = "Displays the current wildcard race for the given league, if the league has wildcard slots." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): if league_exists(command.strip()): league = leagues.load_league_file(command.strip()) if league.constraints["wild_cards"] > 0: await msg.channel.send(embed=league.wildcard_embed()) else: - await msg.channel.send("That league doesn't have wildcards, boss.") + raise CommandError("That league doesn't have wildcards, boss.") else: - await msg.channel.send("Can't find that league, boss.") + raise CommandError("Can't find that league, boss.") class LeaguePauseCommand(Command): name = "pauseleague" template = "m;pauseleague [league name]" description = "Tells a currently running league to stop running after the current series." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): league_name = command.strip() for active_league in active_leagues: if active_league.name == league_name: @@ -980,16 +987,15 @@ class LeaguePauseCommand(Command): await msg.channel.send(f"Loud and clear, chief. {league_name} will stop after this series is over.") return else: - await msg.channel.send("You don't have permission to manage that league.") - return - await msg.channel.send("That league either doesn't exist or isn't running.") + raise CommandError("You don't have permission to manage that league.") + raise CommandError("That league either doesn't exist or isn't running.") class LeagueClaimCommand(Command): name = "claimleague" template = "m;claimleague [league name]" description = "Claims an unclaimed league. Do this as soon as possible after creating the league, or it will remain unclaimed." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): league_name = command.strip() if league_exists(league_name): league = leagues.load_league_file(league_name) @@ -999,16 +1005,16 @@ class LeagueClaimCommand(Command): await msg.channel.send(f"The {league.name} commissioner is doing a great job. That's you, by the way.") return else: - await msg.channel.send("That league has already been claimed!") + raise CommandError("That league has already been claimed!") else: - await msg.channel.send("Can't find that league, boss.") + raise CommandError("Can't find that league, boss.") class LeagueAddOwnersCommand(Command): name = "addleagueowner" template = "m;addleagueowner [league name]\n[user mentions]" description = "Adds additional owners to a league." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): league_name = command.split("\n")[0].strip() if league_exists(league_name): league = leagues.load_league_file(league_name) @@ -1020,17 +1026,16 @@ class LeagueAddOwnersCommand(Command): await msg.channel.send(f"The new {league.name} front office is now up and running.") return else: - await msg.channel.send(f"That league isn't yours, boss.") - return + raise CommandError(f"That league isn't yours, boss.") else: - await msg.channel.send("Can't find that league, boss.") + raise CommandError("Can't find that league, boss.") class LeagueScheduleCommand(Command): name = "leagueschedule" template = "m;leagueschedule [league name]" description = "Sends an embed with the given league's schedule for the next 4 series." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): league_name = command.split("\n")[0].strip() if league_exists(league_name): league = leagues.load_league_file(league_name) @@ -1076,16 +1081,16 @@ class LeagueScheduleCommand(Command): sched_embed.add_field(name=embed_title, 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.") + raise CommandError("That league's already finished with this season, boss.") else: - await msg.channel.send("We can't find that league. Typo?") + raise CommandError("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): + async def execute(self, msg, command, flags): league_name = command.split("\n")[0].strip() team_name = command.split("\n")[1].strip() team = get_team_fuzzy_search(team_name) @@ -1094,8 +1099,7 @@ class LeagueTeamScheduleCommand(Command): 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 + raise CommandError("Can't find that team in that league, chief.") 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()) @@ -1116,16 +1120,16 @@ class LeagueTeamScheduleCommand(Command): 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.") + raise CommandError("That league's already finished with this season, boss.") else: - await msg.channel.send("We can't find that league. Typo?") + raise CommandError("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): + async def execute(self, msg, command, flags): league_name = command.split("\n")[0].strip() if league_exists(league_name): league = leagues.load_league_file(league_name) @@ -1138,10 +1142,9 @@ class LeagueRegenerateScheduleCommand(Command): 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 + raise CommandError("That league isn't yours, boss.") else: - await msg.channel.send("We can't find that league. Typo?") + raise CommandError("We can't find that league. Yay?") class LeagueForceStopCommand(Command): name = "leagueforcestop" @@ -1151,21 +1154,21 @@ class LeagueForceStopCommand(Command): def isauthorized(self, user): return user.id in config()["owners"] - async def execute(self, msg, command): + async def execute(self, msg, command, flags): 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?") + raise CommandError("That league either doesn't exist or isn't in the active list. So, huzzah?") -class LeagueSwapTeamCommand(Command): - name = "leagueswapteam" - template = "m;leagueswapteam [league name]\n[team to remove]\n[team to add]" +class LeagueReplaceTeamCommand(Command): + name = "leaguereplaceteam" + template = "m;leaguereplaceteam [league name]\n[team to remove]\n[team to add]" description = "Adds a team to a league, removing the old one in the process. Can only be executed by a league owner, and only before the start of a new season." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): league_name = command.split("\n")[0].strip() if league_exists(league_name): league = leagues.load_league_file(league_name) @@ -1177,24 +1180,20 @@ class LeagueSwapTeamCommand(Command): team_del = get_team_fuzzy_search(command.split("\n")[1].strip()) team_add = get_team_fuzzy_search(command.split("\n")[2].strip()) except IndexError: - await msg.channel.send("Three lines, boss. Make sure you give us the team to remove, then the team to add.") - return + raise CommandError("Three lines, boss. Make sure you give us the team to remove, then the team to add.") if team_add.name == team_del.name: - await msg.channel.send("Quit being cheeky. The teams have to be different.") - return + raise CommandError("Quit being cheeky. The teams have to be different.") if team_del is None or team_add is None: - await msg.channel.send("We couldn't find one or both of those teams, boss. Try again.") - return + raise CommandError("We couldn't find one or both of those teams, boss. Try again.") + subleague, division = league.find_team(team_del) if subleague is None or division is None: - await msg.channel.send("That first team isn't in that league, chief. So, that's good, right?") - return + raise CommandError("That first team isn't in that league, chief. So, that's good, right?") if league.find_team(team_add)[0] is not None: - await msg.channel.send("That second team is already in that league, chief. No doubles.") - return + raise CommandError("That second team is already in that league, chief. No doubles.") for index in range(0, len(league.league[subleague][division])): if league.league[subleague][division][index].name == team_del.name: @@ -1206,32 +1205,80 @@ class LeagueSwapTeamCommand(Command): await msg.channel.send(embed=league.standings_embed()) 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("That league isn't yours, chief.") + raise CommandError("That league isn't yours, chief.") else: - await msg.channel.send("We can't find that league.") + raise CommandError("We can't find that league.") -class LeagueRenameCommand(Command): - name = "leaguerename" - template = "m;leaguerename [league name]\n[old conference/division name]\n[new conference/division name]" - description = "Changes the name of an existing conference or division. Can only be executed by a league owner, and only before the start of a new season." - - async def execute(self, msg, command): +class LeagueSwapTeamCommand(Command): + name = "leagueswapteams" + template = "m;leagueswapteams [league name]\n[team a]\n[team b]" + description = "Swaps two teams in any divisions or conferences of your league." + + async def execute(self, msg, command, flags): league_name = command.split("\n")[0].strip() if league_exists(league_name): league = leagues.load_league_file(league_name) if league.day != 1: await msg.channel.send("That league hasn't finished its current season yet, chief. Either reset it, or be patient.") return + 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"]): + try: + team_a = get_team_fuzzy_search(command.split("\n")[1].strip()) + team_b = get_team_fuzzy_search(command.split("\n")[2].strip()) + except IndexError: + raise CommandError("Three lines, boss. Make sure you give us the team to remove, then the team to add.") + if team_a.name == team_b.name: + raise CommandError("Quit being cheeky. The teams have to be different.") + + if team_a is None or team_b is None: + raise CommandError("We couldn't find one or both of those teams, boss. Try again.") + + a_subleague, a_division = league.find_team(team_a) + b_subleague, b_division = league.find_team(team_b) + + if a_subleague is None or b_subleague is None: + raise CommandError("One of those teams isn't in the league. Try leaguereplaceteam instead.") + + for index in range(0, len(league.league[a_subleague][a_division])): + if league.league[a_subleague][a_division][index].name == team_a.name: + a_index = index + for index in range(0, len(league.league[b_subleague][b_division])): + if league.league[b_subleague][b_division][index].name == team_b.name: + b_index = index + + league.league[a_subleague][a_division][a_index] = team_b + league.league[b_subleague][b_division][b_index] = team_a + league.schedule = {} + league.generate_schedule() + leagues.save_league_as_new(league) + await msg.channel.send(embed=league.standings_embed()) + await msg.channel.send("Paperwork signed, stamped, copied, and faxed up to the goddess. Xie's pretty quick with this stuff.") + else: + raise CommandError("That league isn't yours, chief.") + else: + raise CommandError("We can't find that league.") + + +class LeagueRenameCommand(Command): + name = "leaguerename" + template = "m;leaguerename [league name]\n[old conference/division name]\n[new conference/division name]" + description = "Changes the name of an existing conference or division. Can only be executed by a league owner, and only before the start of a new season." + + async def execute(self, msg, command, flags): + league_name = command.split("\n")[0].strip() + if league_exists(league_name): + league = leagues.load_league_file(league_name) + if league.day != 1: + raise CommandError("That league hasn't finished its current season yet, chief. Either reset it, or be patient.") 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"]): try: old_name = command.split("\n")[1].strip() new_name = command.split("\n")[2].strip() except IndexError: - await msg.channel.send("Three lines, boss. Make sure you give us the old name, then the new name, on their own lines.") - return + raise CommandError("Three lines, boss. Make sure you give us the old name, then the new name, on their own lines.") if old_name == new_name: - await msg.channel.send("Quit being cheeky. They have to be different names, clearly.") + raise CommandError("Quit being cheeky. They have to be different names, clearly.") found = False @@ -1244,7 +1291,7 @@ class LeagueRenameCommand(Command): found = True break if found: - await msg.channel.send(f"{new_name} is already present in that league, chief. They have to be different.") + raise CommandError(f"{new_name} is already present in that league, chief. They have to be different.") found = False for subleague in league.league.keys(): @@ -1258,22 +1305,21 @@ class LeagueRenameCommand(Command): found = True break if not found: - await msg.channel.send(f"We couldn't find {old_name} anywhere in that league, boss.") - return + raise CommandError(f"We couldn't find {old_name} anywhere in that league, boss.") leagues.save_league_as_new(league) await msg.channel.send(embed=league.standings_embed()) 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("That league isn't yours, chief.") + raise CommandError("That league isn't yours, chief.") else: - await msg.channel.send("We can't find that league.") + raise CommandError("We can't find that league.") class OBLExplainCommand(Command): name = "oblhelp" template = "m;oblhelp" description = "Explains the One Big League!" - async def execute(self, msg, command): + async def execute(self, msg, command, flags): await msg.channel.send("""The One Big League, or OBL, is an asynchronous league that includes every team in the simsim's database. To participate, just use the m;oblteam command with your team of choice. **No signup is required!** This will give you a list of five opponents; playing against one of them and winning nets you a point, and will refresh the list with five new opponents. **Losing results in no penalty!** Each meta-season will last for a few weeks, after which the leaderboards are reset to start the race again! Look out for the people wrestling emoji, which indicates the potential for a :people_wrestling:Wrassle Match:people_wrestling:, where both teams are on each others' lists and both have the opportunity to score a point. Team rankings and points can also be viewed in the m;oblteam command, and the overall OBL leaderboard can be checked with the m;oblstandings command. Best of luck!! @@ -1284,7 +1330,7 @@ class OBLLeaderboardCommand(Command): template = "m;oblstandings" description = "Displays the 15 teams with the most OBL points in this meta-season." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): leaders_list = db.obl_leaderboards()[:15] leaders = [] rank = 1 @@ -1302,11 +1348,10 @@ class OBLTeamCommand(Command): template = "m;oblteam [team name]" description = "Displays a team's rank, current OBL points, and current opponent selection." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): team = get_team_fuzzy_search(command.strip()) if team is None: - await msg.channel.send("Sorry boss, we can't find that team.") - return + raise CommandError("Sorry boss, we can't find that team.") rival_team = None points, beaten_teams_list, opponents_string, rank, rival_name = db.get_obl_stats(team, full=True) @@ -1340,36 +1385,32 @@ class OBLSetRivalCommand(Command): template = "m;oblrival\n[your team name]\n[rival team name]" description = "Sets your team's OBL rival. Can be changed at any time. Requires ownership." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): try: team_i = get_team_fuzzy_search(command.split("\n")[1].strip()) team_r = get_team_fuzzy_search(command.split("\n")[2].strip()) except IndexError: - await msg.channel.send("You didn't give us enough lines. Command on the top, your team in the middle, and your rival at the bottom.") - return + raise CommandError("You didn't give us enough lines. Command on the top, your team in the middle, and your rival at the bottom.") team, owner_id = games.get_team_and_owner(team_i.name) if team is None or team_r is None: - await msg.channel.send("Can't find one of those teams, boss. Typo?") - return + raise CommandError("Can't find one of those teams, boss. Typo?") elif 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 + raise CommandError("You're not authorized to mess with this team. Sorry, boss.") try: db.set_obl_rival(team, team_r) await msg.channel.send("One pair of mortal enemies, coming right up. Unless you're more of the 'enemies to lovers' type. We can manage that too, don't worry.") except: - await msg.channel.send("Hm. We don't think that team has tried to do anything in the One Big League yet, so you'll have to wait for consent. Get them to check their bounty board.") + raise CommandError("Hm. We don't think that team has tried to do anything in the One Big League yet, so you'll have to wait for consent. Get them to check their bounty board.") class OBLConqueredCommand(Command): name = "oblwins" template = "m;oblwins [team name]" description = "Displays all teams that a given team has won points off of." - async def execute(self, msg, command): + async def execute(self, msg, command, flags): team = get_team_fuzzy_search(command.strip()) if team is None: - await msg.channel.send("Sorry boss, we can't find that team.") - return + raise CommandError("Sorry boss, we can't find that team.") points, teams, oppTeams, rank, rivalName = db.get_obl_stats(team, full=True) pages = [] @@ -1414,14 +1455,37 @@ class OBLConqueredCommand(Command): except asyncio.TimeoutError: return +class OBLResetCommand(Command): + name = "oblreset" + template = "m;oblreset" + description = "NUKES THE OBL BOARD. BE CAREFUL." + + def isauthorized(self, user): + return user.id in config()["owners"] + + async def execute(self, msg, command, flags): + if self.isauthorized(msg.author): + db.clear_obl() + await msg.channel.send("💣") + +class TeamsInfoCommand(Command): + name = "teamcount" + template = "m;teamcount" + description = "Prints a readout of how many teams exist in the sim." + + async def execute(self, msg, command, flags): + await msg.channel.send("Let us flip through our records for a second...") + await msg.channel.send(f"We've got {len(games.get_all_teams())} teams! Thanks for asking.") + commands = [ IntroduceCommand(), CountActiveGamesCommand(), + TeamsInfoCommand(), AssignOwnerCommand(), IdolizeCommand(), ShowIdolCommand(), ShowPlayerCommand(), - #SetupGameCommand(), + SetupGameCommand(), SaveTeamCommand(), ImportCommand(), SwapPlayerCommand(), @@ -1441,9 +1505,11 @@ commands = [ OBLSetRivalCommand(), OBLConqueredCommand(), OBLLeaderboardCommand(), + OBLResetCommand(), LeagueClaimCommand(), LeagueAddOwnersCommand(), StartLeagueCommand(), + LeagueSubscribeCommand(), LeaguePauseCommand(), LeagueDisplayCommand(), LeagueLeadersCommand(), @@ -1453,6 +1519,7 @@ commands = [ LeagueTeamScheduleCommand(), LeagueRegenerateScheduleCommand(), LeagueSwapTeamCommand(), + LeagueReplaceTeamCommand(), LeagueRenameCommand(), LeagueForceStopCommand(), CreditCommand(), @@ -1543,7 +1610,17 @@ async def on_message(msg): else: try: comm = next(c for c in commands if command.startswith(c.name)) - await comm.execute(msg, command[len(comm.name):]) + send_text = command[len(comm.name):] + first_line = send_text.split("\n")[0] + flags = [] + if "-" in first_line: + check = first_line.split("-")[1:] + for flag in [_ for _ in check if _ != ""]: + try: + flags.append((flag.split(" ")[0][0].lower(), flag.split(" ",1)[1].strip())) + except IndexError: + flags.append((flag.split(" ")[0][0].lower(), None)) + await comm.execute(msg, send_text, flags) except StopIteration: await msg.channel.send("Can't find that command, boss; try checking the list with `m;help`.") except CommandError as ce: @@ -1706,6 +1783,9 @@ def prepare_game(newgame, league = None, weather_name = None): weathers = weather.all_weathers() newgame.weather = weathers[random.choice(list(weathers.keys()))](newgame) + if newgame.voice is None: + newgame.voice = random.choices(gametext.weighted_voices()[0], weights=gametext.weighted_voices()[1]) + state_init = { "away_name" : newgame.teams['away'].name, "home_name" : newgame.teams['home'].name, @@ -1715,7 +1795,7 @@ def prepare_game(newgame, league = None, weather_name = None): "victory_lap" : False, "weather_emoji" : newgame.weather.emoji, "weather_text" : newgame.weather.name, - "start_delay" : 3, + "start_delay" : 5, "end_delay" : 9 } @@ -1763,19 +1843,25 @@ async def start_tournament_round(channel, tourney, seeding = None): ext = "?league=" + urllib.parse.quote_plus(tourney.name) - if tourney.round_check(): #if finals - await channel.send(f"The {tourney.name} finals are starting now, at {config()['simmadome_url']+ext}") + if tourney.round_check(): #if finals + if tourney.league is not None: + await league_subscriber_update(tourney.league, channel, f"The {tourney.name} finals are starting now, at {config()['simmadome_url']+ext}") + else: + await channel.send(f"The {tourney.name} finals are starting now, at {config()['simmadome_url']+ext}") finals = True - else: - await channel.send(f"{len(current_games)} games started for the {tourney.name} tournament, at {config()['simmadome_url']+ext}") + else: + if tourney.league is not None: + await league_subscriber_update(tourney.league, channel, f"{len(current_games)} games started for the {tourney.name} tournament, at {config()['simmadome_url']+ext}") + else: + await channel.send(f"{len(current_games)} games started for the {tourney.name} tournament, at {config()['simmadome_url']+ext}") finals = False await tourney_round_watcher(channel, tourney, current_games, config()['simmadome_url']+ext, finals) async def continue_tournament_series(tourney, queue, games_list, wins_in_series): for oldgame in queue: - away_team = games.get_team(oldgame.teams["away"].name) - home_team = games.get_team(oldgame.teams["home"].name) + away_team = games.get_team(oldgame.teams["home"].name) + home_team = games.get_team(oldgame.teams["away"].name) if tourney.league is not None: if tourney.day is None: @@ -1794,7 +1880,7 @@ async def continue_tournament_series(tourney, queue, games_list, wins_in_series) else: series_string = f"Best of {tourney.series_length}:" - state_init["title"] = f"{series_string} {wins_in_series[oldgame.teams['away'].name]} - {wins_in_series[oldgame.teams['home'].name]}" + state_init["title"] = f"{series_string} {wins_in_series[away_team.name]} - {wins_in_series[home_team.name]}" discrim_string = tourney.name @@ -1830,8 +1916,12 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals final_embed = game_over_embed(game) final_embed.add_field(name="Series score:", value=f"{wins_in_series[game.teams['away'].name]} - {wins_in_series[game.teams['home'].name]}") - await channel.send(f"A {tourney.name} game just ended!") - await channel.send(embed=final_embed) + if tourney.league is not None: + await league_subscriber_update(tourney.league, channel, f"A {tourney.name} game just ended!") + await league_subscriber_update(tourney.league, channel, final_embed) + else: + await channel.send(f"A {tourney.name} game just ended!") + await channel.send(embed=final_embed) if wins_in_series[winner_name] >= int((tourney.series_length+1)/2) and not finals: winner_list.append(winner_name) elif wins_in_series[winner_name] >= int((tourney.finals_length+1)/2): @@ -1868,13 +1958,22 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals delta = datetime.timedelta(minutes= (60 - now.minute)) next_start = (now + delta).replace(second=0, microsecond=0) - wait_seconds = (next_start - now).seconds - await channel.send(f"The next batch of games for the {tourney.name} will start in {math.ceil(wait_seconds/60)} minutes.") + wait_seconds = (next_start - now).seconds + if tourney.league is not None: + await league_subscriber_update(tourney.league, channel, f"The next batch of games for the {tourney.name} will start in {math.ceil(wait_seconds/60)} minutes.") + else: + await channel.send(f"The next batch of games for the {tourney.name} will start in {math.ceil(wait_seconds/60)} minutes.") await asyncio.sleep(wait_seconds) else: - await channel.send(f"The next batch of games for {tourney.name} will start in {int(tourney.delay/60)} minutes.") + if tourney.league is not None: + await league_subscriber_update(tourney.league, channel, f"The next batch of games for {tourney.name} will start in {int(tourney.delay/60)} minutes.") + else: + await channel.send(f"The next batch of games for {tourney.name} will start in {int(tourney.delay/60)} minutes.") await asyncio.sleep(tourney.delay) - await channel.send(f"{len(queued_games)} games for {tourney.name}, starting at {filter_url}") + if tourney.league is not None: + await league_subscriber_update(tourney.league, channel, f"{len(queued_games)} games for {tourney.name}, starting at {filter_url}") + else: + await channel.send(f"{len(queued_games)} games for {tourney.name}, starting at {filter_url}") games_list = await continue_tournament_series(tourney, queued_games, games_list, wins_in_series) else: tourney.active = False @@ -1883,7 +1982,10 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals embed = discord.Embed(color = discord.Color.dark_purple(), title = f"{winner_list[0]} win the {tourney.name} finals!") if tourney.league is not None and tourney.day > tourney.league.day: tourney.league.day = tourney.day - await channel.send(embed=embed) + if tourney.league is not None: + await league_subscriber_update(tourney.league, channel, embed) + else: + await channel.send(embed=embed) tourney.winner = get_team_fuzzy_search(winner_list[0]) active_tournaments.pop(active_tournaments.index(tourney)) return @@ -1914,7 +2016,7 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals next_start = (now + delta).replace(second=0, microsecond=0) wait_seconds = (next_start - now).seconds - await channel.send(f"""This round of games for the {tourney.name} is now complete! The next round will start in {math.ceil(wait_seconds/60)} minutes. + await league_subscriber_update(tourney.league, channel, f"""This round of games for the {tourney.name} is now complete! The next round will start in {math.ceil(wait_seconds/60)} minutes. Advancing teams: {winners_string}""") await asyncio.sleep(wait_seconds) @@ -2153,9 +2255,9 @@ def game_over_embed(game): if game.victory_lap and winning_team == game.teams['home'].name: winstring += f"{winning_team} wins with a victory lap!" elif winning_team == game.teams['home'].name: - winstring += f"{winning_team} wins, shaming {game.teams['away'].name}!" + winstring += f"{winning_team} wins with a partial victory lap!" else: - winstring += f"{winning_team} wins!" + winstring += f"{winning_team} wins on the road!" embed = discord.Embed(color=discord.Color.dark_purple(), title=title_string) embed.add_field(name="Final score:", value=winstring, inline=False) @@ -2209,16 +2311,16 @@ async def start_league_day(channel, league, partial = False): ext = "?league=" + urllib.parse.quote_plus(league.name) if weather_check_result == 2: - await channel.send(f"The entire league is struck by a {league.weather_override.emoji} {league.weather_override.name}! The games must go on.") + await league_subscriber_update(league, channel, f"The entire league is struck by a {league.weather_override.emoji} {league.weather_override.name}! The games must go on.") elif weather_check_result == 1: - await channel.send(f"The {league.weather_override.emoji} {league.weather_override.name} continues to afflict the league.") + await league_subscriber_update(league, channel, f"The {league.weather_override.emoji} {league.weather_override.name} continues to afflict the league.") if league.last_series_check(): #if finals - await channel.send(f"The final series of the {league.name} regular season is starting now, at {config()['simmadome_url']+ext}") + await league_subscriber_update(league, channel, f"The final series of the {league.name} regular season is starting now, at {config()['simmadome_url']+ext}") last = True else: - await channel.send(f"The day {league.day} series of the {league.name} is starting now, at {config()['simmadome_url']+ext}") + await league_subscriber_update(league, channel, f"The day {league.day} series of the {league.name} is starting now, at {config()['simmadome_url']+ext}") last = False if partial: @@ -2275,8 +2377,8 @@ async def league_day_watcher(channel, league, games_list, filter_url, last = Fal final_embed = game_over_embed(game) final_embed.add_field(name="Day:", value=league.day) final_embed.add_field(name="Series score:", value=f"{series_results[game.teams['away'].name]['wins']} - {series_results[game.teams['home'].name]['wins']}") - await channel.send(f"A {league.name} game just ended!") - await channel.send(embed=final_embed) + await league_subscriber_update(league, channel, f"A {league.name} game just ended!") + await league_subscriber_update(league, channel, final_embed) if series_results[winner_name]["wins"] + series_results[winner_name]["losses"] + missed < league.series_length: queued_games.append(game) games_list.pop(i) @@ -2311,21 +2413,21 @@ async def league_day_watcher(channel, league, games_list, filter_url, last = Fal leagues.save_league(league) active_standings[league] = await channel.send(embed=league.standings_embed()) - await channel.send(f"The day {league.day} games for the {league.name} will start in {math.ceil(wait_seconds/60)} minutes.") + await league_subscriber_update(league, channel, f"The day {league.day} games for the {league.name} will start in {math.ceil(wait_seconds/60)} minutes.") weather_check_result = league.weather_event_check() if weather_check_result == 2: - await channel.send(f"The entire league is struck by a {league.weather_override.emoji} {league.weather_override.name}! The games must go on.") + await league_subscriber_update(league, channel, f"The entire league is struck by a {league.weather_override.emoji} {league.weather_override.name}! The games must go on.") elif weather_check_result == 1: - await channel.send(f"The {league.weather_override.emoji} {league.weather_override.name} continues to afflict the league.") + await league_subscriber_update(league, channel, f"The {league.weather_override.emoji} {league.weather_override.name} continues to afflict the league.") await asyncio.sleep(wait_seconds) - await channel.send(f"A {league.name} series is continuing now at {filter_url}") + await league_subscriber_update(league, channel, f"A {league.name} series is continuing now at {filter_url}") games_list = await continue_league_series(league, queued_games, games_list, series_results, missed) else: league.active = False if league.autoplay == 0 or config()["game_freeze"]: #if number of series to autoplay has been reached - active_standings[league] = await channel.send(embed=league.standings_embed()) - await channel.send(f"The {league.name} is no longer autoplaying.") + active_standings[league] = await league_subscriber_update(league, channel, league.standings_embed()) + await league_subscriber_update(league, channel, f"The {league.name} is no longer autoplaying.") if config()["game_freeze"]: await channel.send("Patch incoming.") leagues.save_league(league) @@ -2352,7 +2454,7 @@ async def league_day_watcher(channel, league, games_list, filter_url, last = Fal next_start = (now + delta).replace(second=0, microsecond=0) wait_seconds = (next_start - now).seconds - await channel.send(f"This {league.name} season is now over! The postseason (with any necessary tiebreakers) will be starting in {math.ceil(wait_seconds/60)} minutes.") + await league_subscriber_update(league, channel, f"This {league.name} season is now over! The postseason (with any necessary tiebreakers) will be starting in {math.ceil(wait_seconds/60)} minutes. (unless you skipped it, that is.)") await asyncio.sleep(wait_seconds) await league_postseason(channel, league) @@ -2388,11 +2490,8 @@ async def league_day_watcher(channel, league, games_list, filter_url, last = Fal wait_seconds = (next_start - now).seconds leagues.save_league(league) - if league in active_standings.keys(): - await active_standings[league].unpin() - active_standings[league] = await channel.send(embed=league.standings_embed()) - active_standings[league].pin() - await channel.send(f"""This {league.name} series is now complete! The next series will be starting in {int(wait_seconds/60)} minutes.""") + active_standings[league] = await league_subscriber_update(league, channel, league.standings_embed()) + await league_subscriber_update(league, channel, f"""This {league.name} series is now complete! The next series will be starting in {int(wait_seconds/60)} minutes.""") await asyncio.sleep(wait_seconds) await start_league_day(channel, league) @@ -2428,14 +2527,49 @@ async def league_postseason(channel, league): embed.set_footer(text="Final Standings") await channel.send(embed=embed) + if league.postseason: - tiebreakers = league.tiebreaker_required() - if tiebreakers != []: - await channel.send("Tiebreakers required!") - await asyncio.gather(*[start_tournament_round(channel, tourney) for tourney in tiebreakers]) - for tourney in tiebreakers: - league.update_standings({tourney.winner.name : {"wins" : 1}}) - leagues.save_league(league) + tiebreakers = league.tiebreaker_required() + if tiebreakers != []: + await channel.send("Tiebreakers required!") + await asyncio.gather(*[start_tournament_round(channel, tourney) for tourney in tiebreakers]) + for tourney in tiebreakers: + league.update_standings({tourney.winner.name : {"wins" : 1}}) + leagues.save_league(league) + now = datetime.datetime.now() + + validminutes = [int((60 * div)/league.games_per_hour) for div in range(0,league.games_per_hour)] + for i in range(0, len(validminutes)): + if now.minute > validminutes[i]: + if i <= len(validminutes)-3: + if validminutes[i+1] == now.minute: + delta = datetime.timedelta(minutes= (validminutes[i+2] - now.minute)) + else: + delta = datetime.timedelta(minutes= (validminutes[i+1] - now.minute)) + elif i <= len(validminutes)-2: + if validminutes[i+1] == now.minute: + delta = datetime.timedelta(minutes= (60 - now.minute)) + else: + delta = datetime.timedelta(minutes= (validminutes[i+1] - now.minute)) + else: + delta = datetime.timedelta(minutes= (60 - now.minute)) + + next_start = (now + delta).replace(second=0, microsecond=0) + wait_seconds = (next_start - now).seconds + await league_subscriber_update(league, channel, f"Tiebreakers complete! Postseason starting in {math.ceil(wait_seconds/60)} minutes.") + await asyncio.sleep(wait_seconds) + + + tourneys = league.champ_series() + await asyncio.gather(*[start_tournament_round(channel, tourney) for tourney in tourneys]) + champs = {} + for tourney in tourneys: + for team in tourney.teams.keys(): + if team.name == tourney.winner.name: + champs[tourney.winner] = {"wins" : tourney.teams[team]["wins"]} + world_series = leagues.tournament(f"{league.name} Championship Series", champs, series_length=7, secs_between_games=int(3600/league.games_per_hour), secs_between_rounds=int(7200/league.games_per_hour)) + world_series.build_bracket(by_wins = True) + world_series.league = league now = datetime.datetime.now() validminutes = [int((60 * div)/league.games_per_hour) for div in range(0,league.games_per_hour)] @@ -2456,46 +2590,22 @@ async def league_postseason(channel, league): next_start = (now + delta).replace(second=0, microsecond=0) wait_seconds = (next_start - now).seconds - await channel.send(f"Tiebreakers complete! Postseason starting in {math.ceil(wait_seconds/60)} minutes.") + await league_subscriber_update(league, channel, f"The {league.name} Championship Series is starting in {math.ceil(wait_seconds/60)} minutes!") await asyncio.sleep(wait_seconds) - + await start_tournament_round(channel, world_series) + league.champion = world_series.winner.name - tourneys = league.champ_series() - await asyncio.gather(*[start_tournament_round(channel, tourney) for tourney in tourneys]) - champs = {} - for tourney in tourneys: - for team in tourney.teams.keys(): - if team.name == tourney.winner.name: - champs[tourney.winner] = {"wins" : tourney.teams[team]["wins"]} - world_series = leagues.tournament(f"{league.name} Championship Series", champs, series_length=7, secs_between_games=int(3600/league.games_per_hour), secs_between_rounds=int(7200/league.games_per_hour)) - world_series.build_bracket(by_wins = True) - world_series.league = league - now = datetime.datetime.now() - - validminutes = [int((60 * div)/league.games_per_hour) for div in range(0,league.games_per_hour)] - for i in range(0, len(validminutes)): - if now.minute > validminutes[i]: - if i <= len(validminutes)-3: - if validminutes[i+1] == now.minute: - delta = datetime.timedelta(minutes= (validminutes[i+2] - now.minute)) - else: - delta = datetime.timedelta(minutes= (validminutes[i+1] - now.minute)) - elif i <= len(validminutes)-2: - if validminutes[i+1] == now.minute: - delta = datetime.timedelta(minutes= (60 - now.minute)) - else: - delta = datetime.timedelta(minutes= (validminutes[i+1] - now.minute)) - else: - delta = datetime.timedelta(minutes= (60 - now.minute)) - - next_start = (now + delta).replace(second=0, microsecond=0) - wait_seconds = (next_start - now).seconds - await channel.send(f"The {league.name} Championship Series is starting in {math.ceil(wait_seconds/60)} minutes!") - await asyncio.sleep(wait_seconds) - await start_tournament_round(channel, world_series) - league.champion = world_series.winner.name leagues.save_league(league) season_save(league) league.season_reset() +async def league_subscriber_update(league, start_channel, message): + channel_list = list(filter(lambda chan : chan.id in league.subbed_channels, client.get_all_channels())) + channel_list.append(start_channel) + for channel in channel_list: + if isinstance(message, discord.Embed): + await channel.send(embed=message) + else: + await channel.send(message) + client.run(config()["token"]) \ No newline at end of file diff --git a/weather.py b/weather.py index 29b8945..e94d9e5 100644 --- a/weather.py +++ b/weather.py @@ -1,5 +1,5 @@ import random, math, roman -from gametext import appearance_outcomes, base_string +from gametext import appearance_outcomes, game_strings_base, base_string class Weather: name = "Sunny" @@ -72,7 +72,7 @@ class SlightTailwind(Weather): def activate(self, game, result): - if "mulligan" not in game.last_update[0].keys() and not result["ishit"] and result["text"] != appearance_outcomes.walk: + if "mulligan" not in game.last_update[0].keys() and not result["ishit"] and result["outcome"] != appearance_outcomes.walk: mulligan_roll_target = -((((game.get_batter().stlats["batting_stars"])-5)/6)**2)+1 if random.random() > mulligan_roll_target and game.get_batter().stlats["batting_stars"] <= 5: result.clear() @@ -90,7 +90,7 @@ class Starlight(Weather): def activate(self, game, result): - if (result["text"] == appearance_outcomes.homerun or result["text"] == appearance_outcomes.grandslam): + if (result["outcome"] == appearance_outcomes.homerun or result["outcome"] == appearance_outcomes.grandslam): result["weather_message"] = True dinger_roll = random.random() if "dragon" in game.get_batter().name.lower(): @@ -190,7 +190,7 @@ class ThinnedVeil(Weather): def activate(self, game, result): if result["ishit"]: - if result["text"] == appearance_outcomes.homerun or result["text"] == appearance_outcomes.grandslam: + if result["outcome"] == appearance_outcomes.homerun or result["outcome"] == appearance_outcomes.grandslam: result["veil"] = True def modify_atbat_message(self, game, state): @@ -420,12 +420,47 @@ class Downpour(Weather): def modify_top_of_inning_message(self, game, state): state["update_emoji"] = self.emoji - state["update_text"] = "The gods are not yet pleased. Play continues through the storm." + if game.teams["away"].score >= self.target: #if the away team has met the target + if game.teams["home"].score == game.teams["away"].score: #if the teams are tied + state["update_text"] = "The gods demand a victor. Play on." + else: + state["update_text"] = f"The gods are pleased, but demand more from {game.teams['home'].name}. Take the field." + else: + state["update_text"] = "The gods are not yet pleased. Play continues through the storm." def modify_game_end_message(self, game, state): state["update_emoji"] = self.emoji state["update_text"] = f"{self.target} runs are reached, pleasing the gods. The storm clears." - + +class SummerMist(Weather): + name = "Summer Mist" + emoji = "🌁" + duration_range = [1,3] + substances = ["yellow mustard", "cat fur", "dread", "caramel", "nacho cheese", "mud", "dirt", "justice", "a green goo", "water, probably", "antimatter", "something not of this world", "live ferrets", "snow", "leaves", + "yarn", "seaweed", "sawdust", "stardust", "code fragments", "milk", "lizards", "a large tarp", "feathers"] + + def __init__(self, game): + self.missing_players = {game.teams["home"].name: None, game.teams["away"].name: None} + self.text = "" + + def activate(self, game, result): + if result["outcome"] in [appearance_outcomes.flyout, appearance_outcomes.groundout, appearance_outcomes.sacrifice]: + roll = random.random() + if roll < .3: #get lost + result["mist"] = True + self.text = f" {result['batter'].name} gets lost in the mist on the way back to the dugout." + if self.missing_players[result["offense_team"].name] is not None: + self.text += f" {self.missing_players[result['offense_team'].name].name} wanders back, covered in {random.choice(self.substances)}!" + result["offense_team"].lineup[result["offense_team"].lineup_position % len(result["offense_team"].lineup)] = self.missing_players[result["offense_team"].name] + else: + result["offense_team"].lineup.pop(result["offense_team"].lineup_position % len(result["offense_team"].lineup)) + self.missing_players[result["offense_team"].name] = result["batter"] + + def modify_atbat_message(self, game, state): + if "mist" in game.last_update[0]: + state["update_emoji"] = self.emoji + state["update_text"] += self.text + self.text = "" def all_weathers(): weathers_dic = { @@ -442,31 +477,33 @@ def all_weathers(): "Meteor Shower" : MeteorShower, "Hurricane" : Hurricane, "Tornado" : Tornado, - "Torrential Downpour" : Downpour + "Torrential Downpour" : Downpour, + "Summer Mist" : SummerMist } return weathers_dic class WeatherChains(): - light = [SlightTailwind, Twilight, Breezy, Drizzle] #basic starting points for weather, good comfortable spots to return to + light = [SlightTailwind, Twilight, Breezy, Drizzle, SummerMist] #basic starting points for weather, good comfortable spots to return to magic = [Twilight, ThinnedVeil, MeteorShower, Starlight] #weathers involving breaking the fabric of spacetime sudden = [Tornado, Hurricane, Twilight, Starlight, Midnight, Downpour] #weathers that always happen and leave over 1-3 games disaster = [Hurricane, Tornado, Downpour, Blizzard] #storms - aftermath = [Midnight, Starlight, MeteorShower] #calm epilogues + aftermath = [Midnight, Starlight, MeteorShower, SummerMist] #calm epilogues dictionary = { #Supernova : (magic + sudden + disaster, None), supernova happens leaguewide and shouldn't need a chain, but here just in case - Midnight : ([SlightTailwind, Breezy, Drizzle, Starlight, MeteorShower, HeatWave],[2,2,2,4,4,1]), + Midnight : ([SlightTailwind, Breezy, Drizzle, Starlight, MeteorShower, HeatWave, SummerMist],[2,2,2,4,4,1,2]), SlightTailwind : ([Breezy, Drizzle, Tornado], [3,3,1]), Blizzard : ([Midnight, Starlight, MeteorShower, Twilight, Downpour], [2,2,2,2,4]), - Twilight : ([ThinnedVeil, Midnight, MeteorShower, SlightTailwind], [2,4,2,1]), + Twilight : ([ThinnedVeil, Midnight, MeteorShower, SlightTailwind, SummerMist], [2,4,2,1,2]), ThinnedVeil : (light, None), - HeatWave : ([Tornado, Hurricane, SlightTailwind, Breezy],[4,4,1,1]), + HeatWave : ([Tornado, Hurricane, SlightTailwind, Breezy, SummerMist],[4,4,1,1,2]), Drizzle : ([Hurricane, Downpour, Blizzard],[2,2,1]), Breezy : ([Drizzle, HeatWave, Blizzard, Tornado], [3,3,1,1]), Starlight : ([SlightTailwind, Twilight, Breezy, Drizzle, ThinnedVeil, HeatWave], None), MeteorShower : ([Starlight, ThinnedVeil, HeatWave], None), Hurricane : ([Midnight, Starlight, MeteorShower, Twilight, Downpour], [2,2,2,2,4]), Tornado : ([Midnight, Starlight, MeteorShower, Twilight, Downpour],[2,2,2,2,4]), + SummerMist : ([Drizzle, Breezy, Hurricane, Downpour],[2, 1, 1, 1]), Downpour : (aftermath, None) }