diff --git a/database.py b/database.py index 6e89b73..8e5df7e 100644 --- a/database.py +++ b/database.py @@ -1,6 +1,7 @@ #handles the database interactions import os, json, datetime, re import sqlite3 as sql +from random import sample data_dir = "data" @@ -71,6 +72,15 @@ def initialcheck(): owner_id integer ); """ + one_big_league_check_string = """ CREATE TABLE IF NOT EXISTS one_big_league ( + counter integer PRIMARY KEY, + team_name text NOT NULL, + teams_beaten_list text, + current_opponent_pool text, + obl_points int DEFAULT 0, + rival_name text + );""" + if conn is not None: c = conn.cursor() c.execute(soulscream_table_check_string) @@ -78,6 +88,7 @@ def initialcheck(): c.execute(player_table_check_string) c.execute(player_stats_table_check_string) c.execute(teams_table_check_string) + c.execute(one_big_league_check_string) conn.commit() conn.close() @@ -297,6 +308,26 @@ def get_all_teams(): conn.close() return None +def get_all_team_names(): + conn = create_connection() + if conn is not None: + c = conn.cursor() + c.execute("SELECT name FROM teams") + team_names = c.fetchall() + team_names_out = [name for (name,) in team_names] + conn.close() + return team_names_out + conn.close() + return None + +def get_filtered_teams(filter_list): + teams_list = get_all_team_names() + out_list = [] + for team in teams_list: + if team not in filter_list: + out_list.append(team) + return out_list + def search_teams(search_string): conn = create_connection() if conn is not None: @@ -327,3 +358,97 @@ def add_stats(player_game_stats_list): c.execute(f"UPDATE stats SET {stat} = ? WHERE name=?",(player_stats_dic[stat],name)) conn.commit() conn.close() + + +def add_team_obl(team): + conn = create_connection() + if conn is not None: + c=conn.cursor() + opponents = sample(get_filtered_teams([team.name]), 5) + c.execute("INSERT INTO one_big_league(team_name, current_opponent_pool) VALUES (?, ?)", (team.name, list_to_newline_string(opponents))) + + conn.commit() + conn.close() + +def save_obl_results(winning_team, losing_team): + conn = create_connection() + if conn is not None: + c=conn.cursor() + c.execute("SELECT teams_beaten_list, current_opponent_pool, obl_points FROM one_big_league WHERE team_name = ?", (winning_team.name,)) + try: + beaten_string, opponents_string, obl_points = c.fetchone() + except TypeError: #add team to OBL + add_team_obl(winning_team) + add_team_obl(losing_team) + else: + beaten_teams = newline_string_to_list(beaten_string) + opponent_teams = newline_string_to_list(opponents_string) + if losing_team.name in opponent_teams: + beaten_teams.append(losing_team.name) + opponent_teams = sample(get_filtered_teams([winning_team.name]), 5) + obl_points += 1 + + c.execute("UPDATE one_big_league SET teams_beaten_list = ?, current_opponent_pool = ?, obl_points = ? WHERE team_name = ?", (list_to_newline_string(beaten_teams), list_to_newline_string(opponent_teams), obl_points, winning_team.name)) + + conn.commit() + conn.close() + return + +def get_obl_stats(team, full = False): + conn = create_connection() + if conn is not None: + c=conn.cursor() + opponents_string = None + while opponents_string is None: + c.execute("SELECT teams_beaten_list, current_opponent_pool, rival_name FROM one_big_league WHERE team_name = ?", (team.name,)) + try: + beaten_string, opponents_string, rival_name = c.fetchone() + except TypeError: #add team to OBL + add_team_obl(team) + + beaten_teams = newline_string_to_list(beaten_string) + opponent_teams = opponents_string + obl_points = len(beaten_teams) + + teams_list = [name for name, points in obl_leaderboards()] + rank = teams_list.index(team.name) + 1 + if not full: + return (obl_points, opponent_teams, rank) + else: + return (obl_points, beaten_teams, opponent_teams, rank, rival_name) + conn.close() + return (None, None) + +def obl_leaderboards(): + conn = create_connection() + if conn is not None: + c=conn.cursor() + c.execute("SELECT team_name, obl_points FROM one_big_league ORDER BY obl_points DESC") + teams_list = c.fetchall() + + return teams_list #element (team_name, obl_points) + conn.close() + return False + +def set_obl_rival(base_team, rival): + conn = create_connection() + if conn is not None: + c=conn.cursor() + + c.execute("UPDATE one_big_league SET rival_name = ? WHERE team_name = ?", (rival.name, base_team.name)) + conn.commit() + conn.close() + +def list_to_newline_string(list): + string = "" + for element in list: + if string != "": + string += "\n" + string += element + return string + +def newline_string_to_list(string): + if string is not None and string != "": + return string.split("\n") + else: + return [] \ No newline at end of file diff --git a/games.py b/games.py index ed39dae..283edf3 100644 --- a/games.py +++ b/games.py @@ -1,6 +1,7 @@ import json, random, os, math, jsonpickle -from enum import Enum import database as db +import weather +from gametext import base_string, appearance_outcomes data_dir = "data" games_config_file = os.path.join(data_dir, "games_config.json") @@ -28,34 +29,6 @@ def config(): with open(games_config_file) as config_file: return json.load(config_file) -def all_weathers(): - weathers_dic = { - "Supernova" : weather("Supernova", "🌟"), - "Midnight": weather("Midnight", "🕶"), - "Slight Tailwind": weather("Slight Tailwind", "🏌️‍♀️"), - "Heavy Snow": weather("Heavy Snow", "❄"), - "Twilight" : weather("Twilight", "👻"), - "Thinned Veil" : weather("Thinned Veil", "🌌"), - "Heat Wave" : weather("Heat Wave", "🌄"), - "Drizzle" : weather("Drizzle", "🌧") - } - return weathers_dic - -class appearance_outcomes(Enum): - 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." #requires .format(player, base_string) - 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!" - class player(object): def __init__(self, json_string): @@ -240,27 +213,30 @@ class game(object): self.outs = 0 self.top_of_inning = True self.last_update = ({},0) #this is a ({outcome}, runs) tuple + self.play_has_begun = False self.owner = None - self.ready = False self.victory_lap = False if length is not None: self.max_innings = length else: self.max_innings = config()["default_length"] self.bases = {1 : None, 2 : None, 3 : None} - self.weather = weather("Sunny","🌞") + self.weather = weather.Weather(self) + self.current_batter = None - def get_batter(self): + def choose_next_batter(self): if self.top_of_inning: bat_team = self.teams["away"] - counter = self.weather.counter_away else: bat_team = self.teams["home"] - counter = self.weather.counter_home - if self.weather.name == "Heavy Snow" and counter == bat_team.lineup_position: - return bat_team.pitcher - return bat_team.lineup[bat_team.lineup_position % len(bat_team.lineup)] + self.current_batter = bat_team.lineup[bat_team.lineup_position % len(bat_team.lineup)] + self.weather.on_choose_next_batter(self) + + def get_batter(self): + if self.current_batter == None: + self.choose_next_batter() + return self.current_batter def get_pitcher(self): if self.top_of_inning: @@ -284,38 +260,34 @@ class game(object): outcome["batter"] = batter outcome["defender"] = "" - bat_stat = random_star_gen("batting_stars", batter) - pitch_stat = random_star_gen("pitching_stars", pitcher) - if weather.name == "Supernova": - pitch_stat = pitch_stat * 0.9 + player_rolls = {} + player_rolls["bat_stat"] = random_star_gen("batting_stars", batter) + player_rolls["pitch_stat"] = random_star_gen("pitching_stars", pitcher) - pb_system_stat = (random.gauss(1*math.erf((bat_stat - pitch_stat)*1.5)-1.8,2.2)) - hitnum = random.gauss(2*math.erf(bat_stat/4)-1,3) + self.weather.modify_atbat_stats(player_rolls) - if self.weather.name == "Twilight": - error_line = - (math.log(defender.stlats["defense_stars"] + 1)/50) + 1 - error_roll = random.random() - if error_roll > error_line: - outcome["error"] = True - outcome["defender"] = defender - pb_system_stat = 0.1 + roll = {} + roll["pb_system_stat"] = (random.gauss(1*math.erf((player_rolls["bat_stat"] - player_rolls["pitch_stat"])*1.5)-1.8,2.2)) + roll["hitnum"] = random.gauss(2*math.erf(player_rolls["bat_stat"]/4)-1,3) + + self.weather.modify_atbat_roll(outcome, roll, defender) - if pb_system_stat <= 0: + if roll["pb_system_stat"] <= 0: outcome["ishit"] = False fc_flag = False - if hitnum < -1.5: + if roll["hitnum"] < -1.5: outcome["text"] = random.choice([appearance_outcomes.strikeoutlooking, appearance_outcomes.strikeoutswinging]) - elif hitnum < 1: + elif roll["hitnum"] < 1: outcome["text"] = appearance_outcomes.groundout outcome["defender"] = defender - elif hitnum < 4: + elif roll["hitnum"] < 4: outcome["text"] = appearance_outcomes.flyout outcome["defender"] = defender else: outcome["text"] = appearance_outcomes.walk - if self.bases[1] is not None and hitnum < -2 and self.outs != 2: + if self.bases[1] is not None and roll["hitnum"] < -2 and self.outs != 2: outcome["text"] = appearance_outcomes.doubleplay outcome["defender"] = "" @@ -332,20 +304,20 @@ 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 <= hitnum and hitnum < -0.5: #poorly hit groundouts + if -1.5 <= roll["hitnum"] and roll["hitnum"] < -0.5: #poorly hit groundouts outcome["text"] = appearance_outcomes.fielderschoice outcome["defender"] = "" - if 2.5 <= hitnum and self.outs < 2: #well hit flyouts can lead to sacrifice flies/advanced runners + if 2.5 <= roll["hitnum"] and self.outs < 2: #well hit flyouts can lead to sacrifice flies/advanced runners if self.bases[2] is not None or self.bases[3] is not None: outcome["advance"] = True else: outcome["ishit"] = True - if hitnum < 1: + if roll["hitnum"] < 1: outcome["text"] = appearance_outcomes.single - elif hitnum < 2.85 or "error" in outcome.keys(): + elif roll["hitnum"] < 2.85 or "error" in outcome.keys(): outcome["text"] = appearance_outcomes.double - elif hitnum < 3.1: + elif roll["hitnum"] < 3.1: outcome["text"] = 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: @@ -362,13 +334,16 @@ class game(object): if self.bases[base+1] is None: #if there's somewhere to go thieves.append((self.bases[base], base)) for baserunner, start_base in thieves: - run_stars = random_star_gen("baserunning_stars", baserunner)*config()["stolen_base_chance_mod"] - if self.weather.name == "Midnight": - run_stars = run_stars*2 - def_stars = random_star_gen("defense_stars", self.get_pitcher()) - if run_stars >= (def_stars - 1.5): #if baserunner isn't worse than pitcher + stats = { + "run_stars": random_star_gen("baserunning_stars", baserunner)*config()["stolen_base_chance_mod"], + "def_stars": random_star_gen("defense_stars", self.get_pitcher()) + } + + self.weather.modify_steal_stats(stats) + + if stats["run_stars"] >= (stats["def_stars"] - 1.5): #if baserunner isn't worse than pitcher roll = random.random() - if roll >= (-(((run_stars+1)/14)**2)+1): #plug it into desmos or something, you'll see + if roll >= (-(((stats["run_stars"]+1)/14)**2)+1): #plug it into desmos or something, you'll see attempts.append((baserunner, start_base)) if len(attempts) == 0: @@ -533,25 +508,19 @@ 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 self.top_of_inning: offense_team = self.teams["away"] - weather_count = self.weather.counter_away defense_team = self.teams["home"] else: offense_team = self.teams["home"] - weather_count = self.weather.counter_home defense_team = self.teams["away"] - if self.weather.name == "Slight Tailwind" and "mulligan" not in self.last_update[0].keys() and not result["ishit"] and result["text"] != appearance_outcomes.walk: - mulligan_roll_target = -((((self.get_batter().stlats["batting_stars"])-5)/6)**2)+1 - if random.random() > mulligan_roll_target and self.get_batter().stlats["batting_stars"] <= 5: - result["mulligan"] = True - return (result, 0) - - if self.weather.name == "Heavy Snow" and weather_count == offense_team.lineup_position and "snow_atbat" not in self.last_update[0].keys(): - result["snow_atbat"] = True - result["text"] = f"{offense_team.lineup[offense_team.lineup_position % len(offense_team.lineup)].name}'s hands are too cold! {self.get_batter().name} is forced to bat!" - return (result, 0) defenders = defense_team.lineup.copy() defenders.append(defense_team.pitcher) @@ -570,8 +539,6 @@ class game(object): elif result["text"] == appearance_outcomes.homerun or result["text"] == appearance_outcomes.grandslam: self.get_batter().game_stats["total_bases"] += 4 self.get_batter().game_stats["home_runs"] += 1 - if self.weather.name == "Thinned Veil": - result["veil"] = True @@ -636,27 +603,10 @@ class game(object): 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 + self.choose_next_batter() if self.outs >= 3: self.flip_inning() - if self.weather.name == "Heat Wave": - if self.top_of_inning: - self.weather.home_pitcher = self.get_pitcher() - if self.inning >= self.weather.counter_home: - self.weather.counter_home = self.weather.counter_home - (self.weather.counter_home % 5) + 5 + random.randint(1,4) #rounds down to last 5, adds up to next 5. then adds a random number 2<=x<=5 to determine next pitcher - tries = 0 - while self.get_pitcher() == self.weather.home_pitcher and tries < 3: - self.teams["home"].set_pitcher(use_lineup = True) - tries += 1 - - - else: - self.weather.away_pitcher = self.get_pitcher() - if self.inning >= self.weather.counter_away: - self.weather.counter_away = self.weather.counter_away - (self.weather.counter_away % 5) + 5 + random.randint(1,4) - tries = 0 - while self.get_pitcher() == self.weather.away_pitcher and tries < 3: - self.teams["away"].set_pitcher(use_lineup = True) - tries += 1 + return (result, scores_to_add) #returns ab information and scores @@ -665,27 +615,19 @@ class game(object): for base in self.bases.keys(): self.bases[base] = None self.outs = 0 - if self.top_of_inning and self.weather.name == "Heavy Snow" and self.weather.counter_away < self.teams["away"].lineup_position: - self.weather.counter_away = self.pitcher_insert(self.teams["away"]) - if not self.top_of_inning: - if self.weather.name == "Heavy Snow" and self.weather.counter_home < self.teams["home"].lineup_position: - self.weather.counter_home = self.pitcher_insert(self.teams["home"]) + self.top_of_inning = not self.top_of_inning + + self.weather.on_flip_inning(self) + + self.choose_next_batter() + + if self.top_of_inning: self.inning += 1 if self.inning > self.max_innings and self.teams["home"].score != self.teams["away"].score: #game over self.over = True - self.top_of_inning = not self.top_of_inning + db.save_obl_results(self.teams["home"] if self.teams["home"].score > self.teams["away"].score else self.teams["away"], self.teams["home"] if self.teams["home"].score < self.teams["away"].score else self.teams["away"]) - if self.weather.name == "Drizzle": - if self.top_of_inning: - self.bases[2] = self.teams["away"].lineup[(self.teams["away"].lineup_position-1) % len(self.teams["away"].lineup)] - else: - self.bases[2] = self.teams["home"].lineup[(self.teams["home"].lineup_position-1) % len(self.teams["home"].lineup)] - - def pitcher_insert(self, this_team): - rounds = math.ceil(this_team.lineup_position / len(this_team.lineup)) - position = random.randint(0, len(this_team.lineup)-1) - return rounds * len(this_team.lineup) + position def end_of_game_report(self): return { @@ -707,6 +649,7 @@ class game(object): def gamestate_update_full(self): + self.play_has_begun = True attempts = self.thievery_attempts() if attempts == False: self.last_update = self.batterup() @@ -715,29 +658,10 @@ class game(object): return self.gamestate_display_full() def gamestate_display_full(self): - if "steals" in self.last_update[0].keys(): + if not self.over: return "Still in progress." else: - try: - punc = "" - if self.last_update[0]["defender"] != "": - punc = "." - if not self.over: - if self.top_of_inning: - inningtext = "top" - else: - inningtext = "bottom" - - updatestring = "this isn't used but i don't want to break anything" - - return "this isn't used but i don't want to break anything" - else: - return f"""Game over! Final score: **{self.teams['away'].score} - {self.teams['home'].score}** - Last update: {self.last_update[0]['batter']} {self.last_update[0]['text'].value} {self.last_update[0]['defender']}{punc}""" - except TypeError: - return "Game not started." - except KeyError: - return "Game not started." + return f"""Game over! Final score: **{self.teams['away'].score} - {self.teams['home'].score}**""" def add_stats(self): players = self.get_stats() @@ -868,25 +792,10 @@ def search_team(search_term): teams.append(team_json) return teams -def base_string(base): - if base == 1: - return "first" - elif base == 2: - return "second" - elif base == 3: - return "third" - elif base == 4: - return "None" - -class weather(object): - name = "Sunny" - emoji = "🌞" - - def __init__(self, new_name, new_emoji): - self.name = new_name - self.emoji = new_emoji - self.counter_away = 0 - self.counter_home = 0 - - def __str__(self): - return f"{self.emoji} {self.name}" +def get_filtered_teams(teams_to_remove): + teams = [] + for team_pickle in db.get_all_teams(): + this_team = jsonpickle.decode(team_pickle[0], keys=True, classes=team) + if this_team.name not in teams_to_remove: + teams.append(this_team) + return teams \ No newline at end of file diff --git a/gametext.py b/gametext.py new file mode 100644 index 0000000..d7ec310 --- /dev/null +++ b/gametext.py @@ -0,0 +1,27 @@ +from enum import Enum + +class appearance_outcomes(Enum): + 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." #requires .format(player, base_string) + 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!" + +def base_string(base): + if base == 1: + return "first" + elif base == 2: + return "second" + elif base == 3: + return "third" + elif base == 4: + return "None" + diff --git a/main_controller.py b/main_controller.py index 176c28f..ca693a2 100644 --- a/main_controller.py +++ b/main_controller.py @@ -140,7 +140,7 @@ def update_loop(): state["away_score"] = this_game.teams["away"].score #top_of_inning = True state["home_score"] = this_game.teams["home"].score #update_pause = 0 #victory_lap = False - if test_string == "Game not started.": #weather_emoji + if not this_game.play_has_begun: #weather_emoji state["update_emoji"] = "🍿" #weather_text state["update_text"] = "Play blall!" #they also need a timestamp state["start_delay"] -= 1 @@ -156,9 +156,9 @@ def update_loop(): state["display_inning"] -= 1 state["display_top_of_inning"] = False - if state["update_pause"] == 1: + if state["update_pause"] == 1: #generate the top of the inning message before displaying the at bat result state["update_emoji"] = "🍿" - if this_game.over: + if this_game.over: # game over message state["display_inning"] -= 1 state["display_top_of_inning"] = False winning_team = this_game.teams['home'].name if this_game.teams['home'].score > this_game.teams['away'].score else this_game.teams['away'].name @@ -170,28 +170,23 @@ def update_loop(): state["update_text"] = f"{winning_team} wins!" state["pitcher"] = "-" state["batter"] = "-" - elif this_game.top_of_inning: - state["update_text"] = f"Top of {this_game.inning}. {this_game.teams['away'].name} batting!" - if this_game.weather.name == "Drizzle": - state["update_emoji"] = "🌧" - state["update_text"] += f' Due to inclement weather, {this_game.teams["away"].lineup[(this_game.teams["away"].lineup_position-1) % len(this_game.teams["away"].lineup)].name} is placed on second base.' - elif this_game.weather.name == "Heat Wave" and hasattr(this_game.weather, "home_pitcher") and this_game.weather.home_pitcher.name != state["pitcher"]: - state["update_emoji"] = "🌄" - state["update_text"] += f' {this_game.weather.home_pitcher} is exhausted from the heat. {state["pitcher"]} is forced to pitch!' - else: - if this_game.inning >= this_game.max_innings: - if this_game.teams["home"].score > this_game.teams["away"].score: - this_game.victory_lap = True - state["update_text"] = f"Bottom of {this_game.inning}. {this_game.teams['home'].name} batting!" - if this_game.weather.name == "Drizzle": - state["update_emoji"] = "🌧" - state["update_text"] += f' Due to inclement weather, {this_game.teams["home"].lineup[(this_game.teams["home"].lineup_position-1) % len(this_game.teams["home"].lineup)].name} is placed on second base.' - elif this_game.weather.name == "Heat Wave" and hasattr(this_game.weather, "away_pitcher") and this_game.weather.away_pitcher.name != state["pitcher"]: - state["update_emoji"] = "🌄" - state["update_text"] += f' {this_game.weather.away_pitcher} is exhausted from the heat. {state["pitcher"]} is forced to pitch!' + if this_game.top_of_inning: + state["update_text"] = f"Top of {this_game.inning}. {this_game.teams['away'].name} batting!" + else: + if this_game.inning >= this_game.max_innings: + if this_game.teams["home"].score > this_game.teams["away"].score: + this_game.victory_lap = True + state["update_text"] = f"Bottom of {this_game.inning}. {this_game.teams['home'].name} batting!" + + this_game.weather.modify_top_of_inning_message(this_game, state) + + + elif state["update_pause"] != 1 and this_game.play_has_begun: + + if "weather_message" in this_game.last_update[0].keys(): + state["update_emoji"] = this_game.weather.emoji - elif state["update_pause"] != 1 and test_string != "Game not started.": if "steals" in this_game.last_update[0].keys(): updatestring = "" for attempt in this_game.last_update[0]["steals"]: @@ -200,30 +195,17 @@ def update_loop(): state["update_emoji"] = "💎" state["update_text"] = updatestring - elif "mulligan" in this_game.last_update[0].keys(): - updatestring = "" - punc = "" - if this_game.last_update[0]["defender"] != "": - punc = ", " - - state["update_emoji"] = "🏌️‍♀️" - state["update_text"] = f"{this_game.last_update[0]['batter']} would have gone out, but they took a mulligan!" - - elif "snow_atbat" in this_game.last_update[0].keys(): - state["update_emoji"] = "❄" + 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 = ". " - - if "fc_out" in this_game.last_update[0].keys(): - name, 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, base_string)} {this_game.last_update[0]['defender']}{punc}" + 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: @@ -231,15 +213,8 @@ def update_loop(): state["update_emoji"] = "🏏" state["update_text"] = updatestring - - if "veil" in this_game.last_update[0].keys(): - state["update_emoji"] = "🌌" - state["update_text"] += f" {this_game.last_update[0]['batter']}'s will manifests on {games.base_string(this_game.last_update[1])} base." - elif "error" in this_game.last_update[0].keys(): - state["update_emoji"] = "👻" - state["update_text"] = f"{this_game.last_update[0]['batter']}'s hit goes ethereal, and {this_game.last_update[0]['defender']} can't catch it! {this_game.last_update[0]['batter']} reaches base safely." - if this_game.last_update[1] > 0: - updatestring += f"{this_game.last_update[1]} runs scored!" + + this_game.weather.modify_atbat_message(this_game, state) state["bases"] = this_game.named_bases() @@ -261,4 +236,4 @@ def update_loop(): state["update_pause"] -= 1 socketio.emit("states_update", game_states) - time.sleep(8) \ No newline at end of file + time.sleep(8) diff --git a/the-prestige.pyproj b/the-prestige.pyproj index 090ed69..7ad6158 100644 --- a/the-prestige.pyproj +++ b/the-prestige.pyproj @@ -29,6 +29,7 @@ Code + Code @@ -46,6 +47,7 @@ + diff --git a/the_prestige.py b/the_prestige.py index 201aa8e..e8a3626 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -5,6 +5,7 @@ from league_storage import league_exists, season_save, season_restart from the_draft import Draft, DRAFT_ROUNDS from flask import Flask from uuid import uuid4 +import weather data_dir = "data" config_filename = os.path.join(data_dir, "config.json") @@ -189,8 +190,8 @@ class StartGameCommand(Command): game.teams['home'].set_pitcher(rotation_slot = day) channel = msg.channel - if weather_name is not None and weather_name in games.all_weathers().keys(): - game.weather = games.all_weathers()[weather_name] + if weather_name is not None and weather_name in weather.all_weathers().keys(): + game.weather = weather.all_weathers()[weather_name](game) game_task = asyncio.create_task(watch_game(channel, game, user=msg.author, league=league)) @@ -1232,6 +1233,147 @@ class LeagueRenameCommand(Command): else: await msg.channel.send("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): + 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 oblteam command, and the overall OBL leaderboard can be checked with the m;oblstandings command. Best of luck!! +""") + +class OBLLeaderboardCommand(Command): + name = "oblstandings" + template = "m;oblstandings" + description = "Displays the 15 teams with the most OBL points in this meta-season." + + async def execute(self, msg, command): + leaders_list = db.obl_leaderboards()[:15] + leaders = {} + rank = 1 + for team, points in leaders_list: + leaders[team] = {"rank" : rank, "points" : points} + rank += 1 + + embed = discord.Embed(color=discord.Color.red(), title="The One Big League") + for team in leaders.keys(): + embed.add_field(name=f"{leaders[team]['rank']}. {team}", value=f"{leaders[team]['points']} points" , inline = False) + await msg.channel.send(embed=embed) + +class OBLTeamCommand(Command): + name = "oblteam" + template = "m;oblteam [team name]" + description = "Displays a team's rank, current OBL points, and current opponent selection." + + async def execute(self, msg, command): + team = get_team_fuzzy_search(command.strip()) + if team is None: + await msg.channel.send("Sorry boss, we can't find that team.") + return + + rival_team = None + points, beaten_teams_list, opponents_string, rank, rival_name = db.get_obl_stats(team, full=True) + opponents_list = db.newline_string_to_list(opponents_string) + for index in range(0, len(opponents_list)): + oppteam = get_team_fuzzy_search(opponents_list[index]) + opplist = db.get_obl_stats(oppteam)[1] + if team.name in opplist: + opponents_list[index] = opponents_list[index] + " 🤼" + if rival_name == opponents_list[index]: + opponents_list[index] = opponents_list[index] + " 😈" + if rival_name is not None: + rival_team = games.get_team(rival_name) + opponents_string = db.list_to_newline_string(opponents_list) + + embed = discord.Embed(color=discord.Color.red(), title=f"{team.name} in the One Big League") + embed.add_field(name="OBL Points", value=points) + embed.add_field(name="Rank", value=rank) + embed.add_field(name="Bounty Board", value=opponents_string, inline=False) + if rival_team is not None: + r_points, r_beaten_teams_list, r_opponents_string, r_rank, r_rival_name = db.get_obl_stats(rival_team, full=True) + embed.add_field(name="Rival", value=f"**{rival_team.name}**: Rank {r_rank}\n{rival_team.slogan}\nPoints: {r_points}") + if r_rival_name == team.name: + embed.set_footer(text="🔥") + else: + embed.set_footer(text="Set a rival with m;oblrival!") + await msg.channel.send(embed=embed) + +class OBLSetRivalCommand(Command): + name = "oblrival" + 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): + team_i = get_team_fuzzy_search(command.split("\n")[1].strip()) + team_r = get_team_fuzzy_search(command.split("\n")[2].strip()) + 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 + 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 + #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.") + +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): + team = get_team_fuzzy_search(command.strip()) + if team is None: + await msg.channel.send("Sorry boss, we can't find that team.") + return + + points, teams, oppTeams, rank, rivalName = db.get_obl_stats(team, full=True) + pages = [] + page_max = math.ceil(len(teams)/25) + + title_text = f"Rank {rank}: {team.name}" + + for page in range(0,page_max): + embed = discord.Embed(color=discord.Color.red(), title=title_text) + embed.set_footer(text = f"{points} OBL Points") + for i in range(0,25): + try: + thisteam = games.get_team(teams[i+25*page]) + if thisteam.slogan.strip() != "": + embed.add_field(name=thisteam.name, value=thisteam.slogan) + else: + embed.add_field(name=thisteam.name, value="404: Slogan not found") + except: + break + pages.append(embed) + + teams_list = await msg.channel.send(embed=pages[0]) + current_page = 0 + + if page_max > 1: + await teams_list.add_reaction("◀") + await teams_list.add_reaction("▶") + + def react_check(react, user): + return user == msg.author and react.message == teams_list + + while True: + try: + react, user = await client.wait_for('reaction_add', timeout=60.0, check=react_check) + if react.emoji == "◀" and current_page > 0: + current_page -= 1 + await react.remove(user) + elif react.emoji == "▶" and current_page < (page_max-1): + current_page += 1 + await react.remove(user) + await teams_list.edit(embed=pages[current_page]) + except asyncio.TimeoutError: + return commands = [ IntroduceCommand(), @@ -1255,6 +1397,11 @@ commands = [ StartGameCommand(), StartRandomGameCommand(), StartTournamentCommand(), + OBLExplainCommand(), + OBLTeamCommand(), + OBLSetRivalCommand(), + OBLConqueredCommand(), + OBLLeaderboardCommand(), LeagueClaimCommand(), LeagueAddOwnersCommand(), StartLeagueCommand(), @@ -1516,9 +1663,9 @@ async def watch_game(channel, newgame, user = None, league = None): main_controller.master_games_dic[id] = (newgame, state_init, discrim_string) def prepare_game(newgame, league = None, weather_name = None): - if weather_name is None and newgame.weather.name == "Sunny": #if no weather name supplied and the game's weather is default, pick random weather - weathers = games.all_weathers() - newgame.weather = weathers[random.choice(list(weathers.keys()))] + if weather_name is None and newgame.weather.name == "Sunny": + weathers = weather.all_weathers() + newgame.weather = weathers[random.choice(list(weathers.keys()))](newgame) state_init = { "away_name" : newgame.teams['away'].name, @@ -1538,12 +1685,6 @@ def prepare_game(newgame, league = None, weather_name = None): else: state_init["is_league"] = True - if newgame.weather.name == "Heavy Snow": - newgame.weather.counter_away = random.randint(0,len(newgame.teams['away'].lineup)-1) - newgame.weather.counter_home = random.randint(0,len(newgame.teams['home'].lineup)-1) - elif newgame.weather.name == "Heat Wave": - newgame.weather.counter_away = random.randint(2,4) - newgame.weather.counter_home = random.randint(2,4) return newgame, state_init async def start_tournament_round(channel, tourney, seeding = None): diff --git a/weather.py b/weather.py new file mode 100644 index 0000000..8996a19 --- /dev/null +++ b/weather.py @@ -0,0 +1,341 @@ +import random +import math +from gametext import appearance_outcomes, base_string + +class Weather: + def __init__(self, game): + self.name = "Sunny" + self.emoji = "🌞" + + def __str__(self): + return f"{self.emoji} {self.name}" + + def modify_atbat_stats(self, player_rolls): + # Activates before batting + pass + + def modify_steal_stats(self, roll): + pass + + def modify_atbat_roll(self, outcome, roll, defender): + # activates after batter roll + pass + + def activate(self, game, result): + # activates after the batter calculation. modify result, or just return another thing + pass + + def on_choose_next_batter(self, game): + pass + + def on_flip_inning(self, game): + pass + + def modify_top_of_inning_message(self, game, state): + pass + + def modify_atbat_message(self, game, state): + pass + + +class Supernova(Weather): + def __init__(self, game): + self.name = "Supernova" + self.emoji = "🌟" + + def modify_atbat_stats(self, roll): + roll["pitch_stat"] *= 0.9 + +class Midnight(Weather): + def __init__(self, game): + self.name = "Midnight" + self.emoji = "🕶" + + def modify_steal_stats(self, roll): + roll["run_stars"] *= 2 + +class SlightTailwind(Weather): + def __init__(self, game): + self.name = "Slight Tailwind" + self.emoji = "🏌️‍♀️" + + def activate(self, game, result): + if game.top_of_inning: + offense_team = game.teams["away"] + defense_team = game.teams["home"] + else: + offense_team = game.teams["home"] + defense_team = game.teams["away"] + + if "mulligan" not in game.last_update[0].keys() and not result["ishit"] and result["text"] != 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() + result.update({ + "text": f"{game.get_batter()} would have gone out, but they took a mulligan!", + "text_only": True, + "weather_message": True, + }) + +class HeavySnow(Weather): + def __init__(self, game): + self.name = "Heavy Snow" + self.emoji = "❄" + self.counter_away = random.randint(0,len(game.teams['away'].lineup)-1) + self.counter_home = random.randint(0,len(game.teams['home'].lineup)-1) + + self.swapped_batter_data = None + + def activate(self, game, result): + if self.swapped_batter_data: + original, sub = self.swapped_batter_data + self.swapped_batter_data = None + result.clear() + result.update({ + "snow_atbat": True, + "text": f"{original.name}'s hands are too cold! {sub.name} is forced to bat!", + "text_only": True, + "weather_message": True, + }) + + def on_flip_inning(self, game): + if game.top_of_inning and self.counter_away < game.teams["away"].lineup_position: + self.counter_away = self.pitcher_insert_index(game.teams["away"]) + + if not game.top_of_inning and self.counter_home < game.teams["home"].lineup_position: + self.counter_home = self.pitcher_insert_index(game.teams["home"]) + + def pitcher_insert_index(self, this_team): + rounds = math.ceil(this_team.lineup_position / len(this_team.lineup)) + position = random.randint(0, len(this_team.lineup)-1) + return rounds * len(this_team.lineup) + position + + def on_choose_next_batter(self, game): + if game.top_of_inning: + bat_team = game.teams["away"] + counter = self.counter_away + else: + bat_team = game.teams["home"] + counter = self.counter_home + + if bat_team.lineup_position == counter: + self.swapped_batter_data = (game.current_batter, bat_team.pitcher) # store this to generate the message during activate() + game.current_batter = bat_team.pitcher + +class Twilight(Weather): + def __init__(self,game): + self.name = "Twilight" + self.emoji = "👻" + + def modify_atbat_roll(self, outcome, roll, defender): + error_line = - (math.log(defender.stlats["defense_stars"] + 1)/50) + 1 + error_roll = random.random() + if error_roll > error_line: + outcome["error"] = True + outcome["weather_message"] = True + outcome["defender"] = defender + roll["pb_system_stat"] = 0.1 + + def modify_atbat_message(self, this_game, state): + result = this_game.last_update[0] + if "error" in result.keys(): + state["update_text"] = f"{result['batter']}'s hit goes ethereal, and {result['defender']} can't catch it! {result['batter']} reaches base safely." + if this_game.last_update[1] > 0: + state["update_text"] += f" {this_game.last_update[1]} runs scored!" + +class ThinnedVeil(Weather): + def __init__(self,game): + self.name = "Thinned Veil" + self.emoji = "🌌" + + def activate(self, game, result): + if result["ishit"]: + if result["text"] == appearance_outcomes.homerun or result["text"] == appearance_outcomes.grandslam: + result["veil"] = True + + def modify_atbat_message(self, game, state): + if "veil" in game.last_update[0].keys(): + state["update_emoji"] = self.emoji + state["update_text"] += f" {game.last_update[0]['batter']}'s will manifests on {base_string(game.last_update[1])} base." + +class HeatWave(Weather): + def __init__(self,game): + self.name = "Heat Wave" + self.emoji = "🌄" + + self.counter_away = random.randint(2,4) + self.counter_home = random.randint(2,4) + + self.swapped_pitcher_data = None + + def on_flip_inning(self, game): + original_pitcher = game.get_pitcher() + if game.top_of_inning: + bat_team = game.teams["home"] + counter = self.counter_home + else: + bat_team = game.teams["away"] + counter = self.counter_away + + if game.inning == counter: + if game.top_of_inning: + self.counter_home = self.counter_home - (self.counter_home % 5) + 5 + random.randint(1,4) #rounds down to last 5, adds up to next 5. then adds a random number 2<=x<=5 to determine next pitcher + else: + self.counter_away = self.counter_away - (self.counter_away % 5) + 5 + random.randint(1,4) + + #swap, accounting for teams where where someone's both batter and pitcher + tries = 0 + while game.get_pitcher() == original_pitcher and tries < 3: + bat_team.set_pitcher(use_lineup = True) + tries += 1 + if game.get_pitcher() != original_pitcher: + self.swapped_pitcher_data = (original_pitcher, game.get_pitcher()) + + def modify_top_of_inning_message(self, game, state): + if self.swapped_pitcher_data: + original, sub = self.swapped_pitcher_data + self.swapped_pitcher_data = None + state["update_emoji"] = self.emoji + state["update_text"] += f' {original} is exhausted from the heat. {sub} is forced to pitch!' + + + +class Drizzle(Weather): + def __init__(self,game): + self.name = "Drizzle" + self.emoji = "🌧" + + def on_flip_inning(self, game): + if game.top_of_inning: + next_team = "away" + else: + next_team = "home" + + lineup = game.teams[next_team].lineup + game.bases[2] = lineup[(game.teams[next_team].lineup_position-1) % len(lineup)] + + def modify_top_of_inning_message(self, game, state): + if game.top_of_inning: + next_team = "away" + else: + next_team = "home" + + placed_player = game.teams[next_team].lineup[(game.teams[next_team].lineup_position-1) % len(game.teams[next_team].lineup)] + + state["update_emoji"] = self.emoji + state["update_text"] += f' Due to inclement weather, {placed_player.name} is placed on second base.' + + +class Sun2(Weather): + def __init__(self, game): + self.name = "Sun 2" + + + def activate(self, game): + for teamtype in game.teams: + team = game.teams[teamtype] + if team.score >= 10: + team.score -= 10 + # no win counting yet :( + result.clear() + result.update({ + "text": "The {} collect 10! Sun 2 smiles.".format(team.name), + "text_only": True, + "weather_message": True + }) + +class Breezy(Weather): + def __init__(self, game): + self.name = "Breezy" + self.emoji = "🎐" + self.activation_chance = 0.05 + + def activate(self, game, result): + if random.random() < self.activation_chance: + teamtype = random.choice(["away","home"]) + team = game.teams[teamtype] + player = random.choice(team.lineup) + old_player_name = player.name + if ' ' in player.name: + names = player.name.split(" ") + first_first_letter = names[0][0] + last_first_letter = names[-1][0] + names[0] = last_first_letter + names[0][1:] + names[-1] = first_first_letter + names[-1][1:] + player.name = ' '.join(names) + else: + #name is one word, so turn 'bartholemew' into 'martholebew' + first_letter = player.name[0] + last_letter = player.name[-1] + player.name = last_letter + player.name[1:-1] + last_letter + + book_adjectives = ["action-packed", "historical", "friendly", "rude", "mystery", "thriller", "horror", "sci-fi", "fantasy", "spooky","romantic"] + book_types = ["novel","novella","poem","anthology","fan fiction","tablet","carving", "autobiography"] + book = "{} {}".format(random.choice(book_adjectives),random.choice(book_types)) + + result.clear() + result.update({ + "text": "{} stopped to read a {} and became Literate! {} is now {}!".format(old_player_name, book, old_player_name, player.name), + "text_only": True, + "weather_message": True + }) + + +class Feedback(Weather): + def __init__(self, game): + self.name = "Feedback" + self.emoji = "🎤" + self.activation_chance = 0.02 + self.swap_batter_vs_pitcher_chance = 0.8 + + def activate(self, game, result): + if random.random() < self.activation_chance: + # feedback time + player1 = None + player2 = None + team1 = game.teams["home"] + team2 = game.teams["away"] + if random.random() < self.swap_batter_vs_pitcher_chance: + # swapping batters + # theoretically this could swap players already on base :( + team1 = game.teams["home"] + team2 = game.teams["away"] + homePlayerIndex = random.randint(0,len(team1.lineup)-1) + awayPlayerIndex = random.randint(0,len(team2.lineup)-1) + + player1 = team1.lineup[homePlayerIndex] + player2 = team2.lineup[awayPlayerIndex] + + team1.lineup[homePlayerIndex] = player2 + team2.lineup[awayPlayerIndex] = player1 + else: + # swapping pitchers + player1 = team1.pitcher + player2 = team2.pitcher + + team1.pitcher = player2 + team2.pitcher = player1 + + result.clear() + result.update({ + "text": "{} and {} switched teams in the feedback!".format(player1.name,player2.name), + "text_only": True, + "weather_message": True, + }) + +def all_weathers(): + weathers_dic = { + "Supernova" : Supernova, + "Midnight": Midnight, + "Slight Tailwind": SlightTailwind, + "Heavy Snow": HeavySnow, + "Twilight" : Twilight, + "Thinned Veil" : ThinnedVeil, + "Heat Wave" : HeatWave, + "Drizzle" : Drizzle, +# "Sun 2": Sun2, + "Feedback": Feedback, + "Breezy": Breezy, + } + return weathers_dic +