457 lines
21 KiB
Python
457 lines
21 KiB
Python
import time, asyncio, json, jsonpickle, random, math, os
|
|
import league_storage as league_db
|
|
from itertools import chain
|
|
from games import team, game
|
|
from discord import Embed, Color
|
|
|
|
data_dir = "data"
|
|
league_dir = "leagues"
|
|
|
|
class league_structure(object):
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.historic = False
|
|
self.owner = None
|
|
self.season = 1
|
|
self.autoplay = -1
|
|
|
|
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] } }
|
|
self.constraints = {
|
|
"division_games" : division_games,
|
|
"inter_div_games" : inter_division_games,
|
|
"inter_league_games" : inter_league_games,
|
|
"division_leaders" : 0,
|
|
"wild_cards" : 0
|
|
}
|
|
self.day = 1
|
|
self.schedule = {}
|
|
self.series_length = 3 #can be changed
|
|
self.game_length = None
|
|
self.active = False
|
|
self.games_per_hour = games_per_hour
|
|
|
|
def add_stats_from_game(self, players_dic):
|
|
league_db.add_stats(self.name, players_dic)
|
|
|
|
def update_standings(self, results_dic):
|
|
league_db.update_standings(self.name, results_dic)
|
|
|
|
|
|
def last_series_check(self):
|
|
return str(math.ceil((self.day)/self.series_length) + 1) not in self.schedule.keys()
|
|
|
|
def day_to_series_num(self, day):
|
|
return math.ceil((self.day)/self.series_length)
|
|
|
|
def tiebreaker_required(self):
|
|
standings = {}
|
|
matchups = []
|
|
tournaments = []
|
|
for team_name, wins, losses, run_diff in league_db.get_standings(self.name):
|
|
standings[team_name] = {"wins" : wins, "losses" : losses, "run_diff" : run_diff}
|
|
|
|
for subleague in iter(self.league.keys()):
|
|
team_dic = {}
|
|
subleague_array = []
|
|
wildcard_leaders = []
|
|
for division in iter(self.league[subleague].keys()):
|
|
division_standings = []
|
|
division_standings += self.division_standings(self.league[subleague][division], standings)
|
|
division_leaders = division_standings[:self.constraints["division_leaders"]]
|
|
for division_team, wins, losses, diff, gb in division_standings[self.constraints["division_leaders"]:]:
|
|
if division_team.name != division_leaders[-1][0].name and standings[division_team.name]["wins"] == standings[division_leaders[-1][0].name]["wins"]:
|
|
matchups.append((division_team, division_standings[self.constraints["division_leaders"]-1][0], f"{division} Tiebreaker"))
|
|
|
|
this_div_wildcard = [this_team for this_team, wins, losses, diff, gb in self.division_standings(self.league[subleague][division], standings)[self.constraints["division_leaders"]:]]
|
|
subleague_array += this_div_wildcard
|
|
if self.constraints["wild_cards"] > 0:
|
|
wildcard_standings = self.division_standings(subleague_array, standings)
|
|
wildcard_leaders = wildcard_standings[:self.constraints["wild_cards"]]
|
|
for wildcard_team, wins, losses, diff, gb in wildcard_standings[self.constraints["wild_cards"]:]:
|
|
if wildcard_team.name != wildcard_leaders[-1][0].name and standings[wildcard_team.name]["wins"] == standings[wildcard_leaders[-1][0].name]["wins"]:
|
|
matchups.append((wildcard_team, wildcard_standings[self.constraints["wild_cards"]-1][0], f"{subleague} Wildcard Tiebreaker"))
|
|
|
|
for team_a, team_b, type in matchups:
|
|
tourney = tournament(f"{self.name} {type}",{team_a : {"wins" : 1}, team_b : {"wins" : 0}}, finals_series_length=1, secs_between_games=int(3600/self.games_per_hour), secs_between_rounds=int(7200/self.games_per_hour))
|
|
tourney.build_bracket(by_wins = True)
|
|
tourney.league = self
|
|
tournaments.append(tourney)
|
|
return tournaments
|
|
|
|
def find_team(self, team_name):
|
|
for subleague in iter(self.league.keys()):
|
|
for division in iter(self.league[subleague].keys()):
|
|
if team_name in self.league[subleague][division]:
|
|
return (subleague, division)
|
|
|
|
def teams_in_league(self):
|
|
teams = []
|
|
for division in self.league.values():
|
|
for teams_list in division.values():
|
|
teams += teams_list
|
|
return teams
|
|
|
|
def teams_in_subleague(self, subleague_name):
|
|
teams = []
|
|
if subleague_name in self.league.keys():
|
|
for division_list in self.league[subleague_name].values():
|
|
teams += division_list
|
|
return teams
|
|
else:
|
|
print("League not found.")
|
|
return None
|
|
|
|
def teams_in_division(self, subleague_name, division_name):
|
|
if subleague_name in self.league.keys() and division_name in self.league[subleague_name].keys():
|
|
return self.league[subleague_name][division_name]
|
|
else:
|
|
print("Division in that league not found.")
|
|
return None
|
|
|
|
def make_matchups(self):
|
|
matchups = []
|
|
batch_subleagues = [] #each sub-array is all teams in each subleague
|
|
subleague_max = 1
|
|
for subleague in self.league.keys():
|
|
teams = self.teams_in_subleague(subleague)
|
|
if subleague_max < len(teams):
|
|
subleague_max = len(teams)
|
|
batch_subleagues.append(teams)
|
|
|
|
for subleague in batch_subleagues:
|
|
while len(subleague) < subleague_max:
|
|
subleague.append("OFF")
|
|
|
|
for i in range(0, self.constraints["inter_league_games"]): #generates inter-league matchups
|
|
unmatched_indices = [i for i in range(0, len(batch_subleagues))]
|
|
for subleague_index in range(0, len(batch_subleagues)):
|
|
if subleague_index in unmatched_indices:
|
|
unmatched_indices.pop(unmatched_indices.index(subleague_index))
|
|
match_with_index = random.choice(unmatched_indices)
|
|
unmatched_indices.pop(unmatched_indices.index(match_with_index))
|
|
league_a = batch_subleagues[subleague_index].copy()
|
|
league_b = batch_subleagues[match_with_index].copy()
|
|
random.shuffle(league_a)
|
|
random.shuffle(league_b)
|
|
a_home = True
|
|
for team_a, team_b in zip(league_a, league_b):
|
|
if a_home:
|
|
matchups.append([team_b.name, team_a.name])
|
|
else:
|
|
matchups.append([team_a.name, team_b.name])
|
|
a_home != a_home
|
|
|
|
for i in range(0, self.constraints["inter_div_games"]): #inter-division matchups
|
|
for subleague in self.league.keys():
|
|
division_max = 1
|
|
divisions = []
|
|
for div in self.league[subleague].keys():
|
|
if division_max < len(self.league[subleague][div]):
|
|
divison_max = len(self.league[subleague][div])
|
|
divisions.append(self.league[subleague][div])
|
|
|
|
last_div = None
|
|
if len(divisions) % 2 != 0:
|
|
if division_max % 2 != 0:
|
|
divisions.append(["OFF" for i in range(0, division_max)])
|
|
else:
|
|
last_div = divisions.pop
|
|
|
|
divs_a = list(chain(divisions[int(len(divisions)/2):]))[0]
|
|
if last_div is not None:
|
|
divs_a.extend(last_div[int(len(last_div)/2):])
|
|
random.shuffle(divs_a)
|
|
|
|
divs_b = list(chain(divisions[:int(len(divisions)/2)]))[0]
|
|
if last_div is not None:
|
|
divs_a.extend(last_div[:int(len(last_div)/2)])
|
|
random.shuffle(divs_b)
|
|
|
|
a_home = True
|
|
for team_a, team_b in zip(divs_a, divs_b):
|
|
if a_home:
|
|
matchups.append([team_b.name, team_a.name])
|
|
else:
|
|
matchups.append([team_a.name, team_b.name])
|
|
a_home != a_home
|
|
|
|
|
|
for subleague in self.league.keys():
|
|
for division in self.league[subleague].values(): #generate round-robin matchups
|
|
if len(division) % 2 != 0:
|
|
division.append("OFF")
|
|
|
|
for i in range(0, len(division)-1):
|
|
teams_a = division[int(len(division)/2):]
|
|
teams_b = division[:int(len(division)/2)]
|
|
teams_b.reverse()
|
|
|
|
for team_a, team_b in zip(teams_a, teams_b):
|
|
for j in range(0, self.constraints["division_games"]):
|
|
if i % 2 == 0:
|
|
matchups.append([team_b.name, team_a.name])
|
|
else:
|
|
matchups.append([team_a.name, team_b.name])
|
|
|
|
division.insert(1, division.pop())
|
|
return matchups
|
|
|
|
def generate_schedule(self):
|
|
matchups = self.make_matchups()
|
|
random.shuffle(matchups)
|
|
for game in matchups:
|
|
scheduled = False
|
|
day = 1
|
|
while not scheduled:
|
|
found = False
|
|
if str(day) in self.schedule.keys():
|
|
for game_on_day in self.schedule[str(day)]:
|
|
for team in game:
|
|
if team in game_on_day:
|
|
found = True
|
|
if not found:
|
|
self.schedule[str(day)].append(game)
|
|
scheduled = True
|
|
else:
|
|
self.schedule[str(day)] = [game]
|
|
scheduled = True
|
|
day += 1
|
|
|
|
def division_standings(self, division, standings):
|
|
def sorter(team_in_list):
|
|
if team_in_list[2] == 0 and team_in_list[1] == 0:
|
|
return (0, team_in_list[3])
|
|
return (team_in_list[1]/(team_in_list[1]+team_in_list[2]), team_in_list[3])
|
|
|
|
teams = division.copy()
|
|
|
|
for index in range(0, len(teams)):
|
|
this_team = teams[index]
|
|
teams[index] = [this_team, standings[teams[index].name]["wins"], standings[teams[index].name]["losses"], standings[teams[index].name]["run_diff"], 0]
|
|
|
|
teams.sort(key=sorter, reverse=True)
|
|
return teams
|
|
|
|
|
|
def standings_embed(self):
|
|
this_embed = Embed(color=Color.purple(), title=self.name)
|
|
standings = {}
|
|
for team_name, wins, losses, run_diff in league_db.get_standings(self.name):
|
|
standings[team_name] = {"wins" : wins, "losses" : losses, "run_diff" : run_diff}
|
|
for subleague in iter(self.league.keys()):
|
|
this_embed.add_field(name="Subleague:", value=f"**{subleague}**", inline = False)
|
|
for division in iter(self.league[subleague].keys()):
|
|
this_embed.add_field(name="Division:", value=f"**{division}**", inline = False)
|
|
teams = self.division_standings(self.league[subleague][division], standings)
|
|
|
|
for index in range(0, len(teams)):
|
|
if index == self.constraints["division_leaders"] - 1:
|
|
teams[index][4] = "-"
|
|
else:
|
|
games_behind = ((teams[self.constraints["division_leaders"] - 1][1] - teams[index][1]) + (teams[index][2] - teams[self.constraints["division_leaders"] - 1][2]))/2
|
|
teams[index][4] = games_behind
|
|
|
|
for this_team in teams:
|
|
if this_team[2] != 0 or this_team[1] != 0:
|
|
this_embed.add_field(name=this_team[0].name, value=f"{this_team[1]} - {this_team[2]} WR: {round(this_team[1]/(this_team[1]+this_team[2]), 3)} GB: {this_team[4]}", inline = False)
|
|
else:
|
|
this_embed.add_field(name=this_team[0].name, value=f"{this_team[1]} - {this_team[2]} WR: - GB: {this_team[4]}", inline = False)
|
|
|
|
this_embed.set_footer(text=f"Standings as of day {self.day-1}")
|
|
return this_embed
|
|
|
|
def wildcard_embed(self):
|
|
this_embed = Embed(color=Color.purple(), title=f"{self.name} Wildcard Race")
|
|
standings = {}
|
|
for team_name, wins, losses, run_diff in league_db.get_standings(self.name):
|
|
standings[team_name] = {"wins" : wins, "losses" : losses, "run_diff" : run_diff}
|
|
for subleague in iter(self.league.keys()):
|
|
this_embed.add_field(name="Subleague:", value=f"**{subleague}**", inline = False)
|
|
subleague_array = []
|
|
for division in iter(self.league[subleague].keys()):
|
|
this_div = [this_team for this_team, wins, losses, diff, gb in self.division_standings(self.league[subleague][division], standings)[self.constraints["division_leaders"]:]]
|
|
subleague_array += this_div
|
|
|
|
teams = self.division_standings(subleague_array, standings)
|
|
|
|
for index in range(0, len(teams)):
|
|
if index == self.constraints["wild_cards"] - 1:
|
|
teams[index][4] = "-"
|
|
else:
|
|
games_behind = ((teams[self.constraints["wild_cards"] - 1][1] - teams[index][1]) + (teams[index][2] - teams[self.constraints["wild_cards"] - 1][2]))/2
|
|
teams[index][4] = games_behind
|
|
|
|
for this_team in teams:
|
|
if this_team[2] != 0 or this_team[1] != 0:
|
|
this_embed.add_field(name=this_team[0].name, value=f"{this_team[1]} - {this_team[2]} WR: {round(this_team[1]/(this_team[1]+this_team[2]), 3)} GB: {this_team[4]}", inline = False)
|
|
else:
|
|
this_embed.add_field(name=this_team[0].name, value=f"{this_team[1]} - {this_team[2]} WR: - GB: {this_team[4]}", inline = False)
|
|
|
|
this_embed.set_footer(text=f"Wildcard standings as of day {self.day-1}")
|
|
return this_embed
|
|
|
|
def champ_series(self):
|
|
tournaments = []
|
|
standings = {}
|
|
|
|
for team_name, wins, losses, run_diff in league_db.get_standings(self.name):
|
|
standings[team_name] = {"wins" : wins, "losses" : losses, "run_diff" : run_diff}
|
|
|
|
for subleague in iter(self.league.keys()):
|
|
team_dic = {}
|
|
division_leaders = []
|
|
subleague_array = []
|
|
wildcard_leaders = []
|
|
for division in iter(self.league[subleague].keys()):
|
|
division_leaders += self.division_standings(self.league[subleague][division], standings)[:self.constraints["division_leaders"]]
|
|
this_div_wildcard = [this_team for this_team, wins, losses, diff, gb in self.division_standings(self.league[subleague][division], standings)[self.constraints["division_leaders"]:]]
|
|
subleague_array += this_div_wildcard
|
|
if self.constraints["wild_cards"] > 0:
|
|
wildcard_leaders = self.division_standings(subleague_array, standings)[:self.constraints["wild_cards"]]
|
|
|
|
for this_team, wins, losses, diff, gb in division_leaders + wildcard_leaders:
|
|
team_dic[this_team] = {"wins" : wins}
|
|
|
|
subleague_tournament = tournament(f"{self.name} {subleague} Championship Series", team_dic, secs_between_games=int(3600/self.games_per_hour), secs_between_rounds=int(7200/self.games_per_hour))
|
|
subleague_tournament.build_bracket(by_wins = True)
|
|
subleague_tournament.league = self
|
|
tournaments.append(subleague_tournament)
|
|
|
|
return tournaments
|
|
|
|
|
|
class tournament(object):
|
|
def __init__(self, name, team_dic, series_length = 5, finals_series_length = 7, max_innings = 9, id = None, secs_between_games = 300, secs_between_rounds = 600):
|
|
self.name = name
|
|
self.teams = team_dic #key: team object, value: wins
|
|
self.bracket = None
|
|
self.results = None
|
|
self.series_length = series_length
|
|
self.finals_length = finals_series_length
|
|
self.game_length = max_innings
|
|
self.active = False
|
|
self.delay = secs_between_games
|
|
self.round_delay = secs_between_rounds
|
|
self.finals = False
|
|
self.id = id
|
|
self.league = None
|
|
self.winner = None
|
|
|
|
if id is None:
|
|
self.id = random.randint(1111,9999)
|
|
else:
|
|
self.id = id
|
|
|
|
|
|
def build_bracket(self, random_sort = False, by_wins = False, manual = False):
|
|
teams_list = list(self.teams.keys()).copy()
|
|
|
|
if random_sort:
|
|
def sorter(team_in_list):
|
|
return random.random()
|
|
|
|
elif by_wins:
|
|
def sorter(team_in_list):
|
|
return self.teams[team_in_list]["wins"] #sorts by wins
|
|
|
|
else: #sort by average stars
|
|
def sorter(team_in_list):
|
|
return team_in_list.average_stars()
|
|
|
|
if not manual:
|
|
teams_list.sort(key=sorter, reverse=True)
|
|
|
|
|
|
bracket_layers = int(math.ceil(math.log(len(teams_list), 2)))
|
|
empty_slots = int(math.pow(2, bracket_layers) - len(teams_list))
|
|
|
|
for i in range(0, empty_slots):
|
|
teams_list.append(None)
|
|
|
|
previous_bracket_layer = teams_list.copy()
|
|
for i in range(0, bracket_layers - 1):
|
|
this_layer = []
|
|
for pair in range(0, int(len(previous_bracket_layer)/2)):
|
|
if pair % 2 == 0: #if even number
|
|
this_layer.insert(0+int(pair/2), [previous_bracket_layer.pop(0), previous_bracket_layer.pop(-1)]) #every other pair goes at front of list, moving forward
|
|
else:
|
|
this_layer.insert(0-int((1+pair)/2), [previous_bracket_layer.pop(int(len(previous_bracket_layer)/2)-1), previous_bracket_layer.pop(int(len(previous_bracket_layer)/2))]) #every other pair goes at end of list, moving backward
|
|
previous_bracket_layer = this_layer
|
|
self.bracket = bracket(previous_bracket_layer, bracket_layers)
|
|
|
|
def round_check(self):
|
|
if self.bracket.depth == 1:
|
|
self.finals = True
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
class bracket(object):
|
|
this_bracket = []
|
|
|
|
def __init__(self, bracket_list, depth):
|
|
self.this_bracket = bracket_list
|
|
self.depth = depth
|
|
self.bottom_row = []
|
|
|
|
def get_bottom_row(self):
|
|
self.depth = 1
|
|
self.bottom_row = []
|
|
self.dive(self.this_bracket)
|
|
return self.bottom_row
|
|
|
|
def dive(self, branch):
|
|
if not isinstance(branch[0], list): #if it's a pair of games
|
|
self.bottom_row.append(branch)
|
|
else:
|
|
self.depth += 1
|
|
return self.dive(branch[0]), self.dive(branch[1])
|
|
|
|
def set_winners_dive(self, winners_list, index = 0, branch = None, parent = None):
|
|
if branch is None:
|
|
branch = self.this_bracket.copy()
|
|
if not isinstance(branch[0], list): #if it's a pair of games
|
|
if branch[0].name in winners_list or branch[1] is None:
|
|
winner = branch[0]
|
|
if parent is not None:
|
|
parent[index] = winner
|
|
elif branch[1].name in winners_list:
|
|
winner = branch[1]
|
|
if parent is not None:
|
|
parent[index] = winner
|
|
else:
|
|
self.set_winners_dive(winners_list, index = 0, branch = branch[0], parent = branch)
|
|
self.set_winners_dive(winners_list, index = 1, branch = branch[1], parent = branch)
|
|
|
|
if parent is None:
|
|
self.this_bracket = branch
|
|
return branch
|
|
|
|
def save_league(this_league):
|
|
if not league_db.league_exists(this_league.name):
|
|
league_db.init_league_db(this_league)
|
|
with open(os.path.join(data_dir, league_dir, this_league.name, f"{this_league.name}.league"), "w") as league_file:
|
|
league_json_string = jsonpickle.encode(this_league.league, keys=True)
|
|
json.dump(league_json_string, league_file, indent=4)
|
|
league_db.save_league(this_league)
|
|
|
|
def load_league_file(league_name):
|
|
if league_db.league_exists(league_name):
|
|
state = league_db.state(league_name)
|
|
this_league = league_structure(league_name)
|
|
with open(os.path.join(data_dir, league_dir, league_name, f"{this_league.name}.league")) as league_file:
|
|
this_league.league = jsonpickle.decode(json.load(league_file), keys=True, classes=team)
|
|
with open(os.path.join(data_dir, league_dir, league_name, f"{this_league.name}.state")) as state_file:
|
|
state_dic = json.load(state_file)
|
|
|
|
this_league.day = state_dic["day"]
|
|
this_league.schedule = state_dic["schedule"]
|
|
this_league.constraints = state_dic["constraints"]
|
|
this_league.game_length = state_dic["game_length"]
|
|
this_league.series_length = state_dic["series_length"]
|
|
this_league.owner = state_dic["owner"]
|
|
this_league.games_per_hour = state_dic["games_per_hour"]
|
|
this_league.historic = state_dic["historic"]
|
|
this_league.season = state_dic["season"]
|
|
return this_league |