Merge branch 'leagues'

This commit is contained in:
Genderdruid 2021-01-15 13:17:55 -08:00
commit 68a48b9441
25 changed files with 1752 additions and 112 deletions

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
venv/
matteo_env/
__pycache__/
simmadome/node_modules
data/
.git/

2
.gitignore vendored
View File

@ -344,6 +344,7 @@ config.json
games_config.json games_config.json
weather_config.json weather_config.json
ids ids
data/
# database # database
matteo.db matteo.db
@ -351,5 +352,6 @@ matteo.db-wal
matteo.db-shm matteo.db-shm
/data/leagues/* /data/leagues/*
/matteo_env/Lib/site-packages/flask_socketio/__init__.py /matteo_env/Lib/site-packages/flask_socketio/__init__.py
Pipfile
env env

View File

@ -1,9 +1,20 @@
FROM python:3.8 # - Build stage 1: frontend (simmadome/ directory)
EXPOSE 5000 FROM node:alpine AS frontend
WORKDIR /app WORKDIR /app
COPY . ./ COPY simmadome/package.json simmadome/package-lock.json ./
RUN npm install
COPY simmadome/ ./
RUN npm run build
# - Build stage 2: backend (Python)
FROM python:3.8
EXPOSE 5000
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
COPY . ./
COPY --from=frontend /app/build/ simmadome/build/
CMD ["python", "the_prestige.py"] CMD ["python", "the_prestige.py"]

View File

@ -31,13 +31,14 @@ def config():
def all_weathers(): def all_weathers():
weathers_dic = { weathers_dic = {
#"Supernova" : weather("Supernova", "🌟"), #"Supernova" : weather("Supernova", "🌟"),
"Midnight": weather("Midnight", "🕶"), #"Midnight": weather("Midnight", "🕶"),
"Slight Tailwind": weather("Slight Tailwind", "🏌️‍♀️"), "Slight Tailwind": weather("Slight Tailwind", "🏌️‍♀️"),
"Heavy Snow": weather("Heavy Snow", "") "Heavy Snow": weather("Heavy Snow", ""),
"Twilight" : weather("Twilight", "👻"),
"Thinned Veil" : weather("Thinned Veil", "🌌")
} }
return weathers_dic return weathers_dic
class appearance_outcomes(Enum): class appearance_outcomes(Enum):
strikeoutlooking = "strikes out looking." strikeoutlooking = "strikes out looking."
strikeoutswinging = "strikes out swinging." strikeoutswinging = "strikes out swinging."
@ -173,7 +174,7 @@ class team(object):
if rotation_slot is None: if rotation_slot is None:
self.pitcher = random.choice(temp_rotation) self.pitcher = random.choice(temp_rotation)
else: else:
self.pitcher = temp_rotation[rotation_slot % len(temp_rotation)] self.pitcher = temp_rotation[(rotation_slot-1) % len(temp_rotation)]
def is_ready(self): def is_ready(self):
try: try:
@ -269,6 +270,13 @@ class game(object):
pb_system_stat = (random.gauss(1*math.erf((bat_stat - pitch_stat)*1.5)-1.8,2.2)) 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) hitnum = random.gauss(2*math.erf(bat_stat/4)-1,3)
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
if pb_system_stat <= 0: if pb_system_stat <= 0:
@ -313,7 +321,7 @@ class game(object):
outcome["ishit"] = True outcome["ishit"] = True
if hitnum < 1: if hitnum < 1:
outcome["text"] = appearance_outcomes.single outcome["text"] = appearance_outcomes.single
elif hitnum < 2.85: elif hitnum < 2.85 or "error" in outcome.keys():
outcome["text"] = appearance_outcomes.double outcome["text"] = appearance_outcomes.double
elif hitnum < 3.1: elif hitnum < 3.1:
outcome["text"] = appearance_outcomes.triple outcome["text"] = appearance_outcomes.triple
@ -384,6 +392,11 @@ class game(object):
if base is not None: if base is not None:
runs += 1 runs += 1
self.bases = {1 : None, 2 : None, 3 : None} self.bases = {1 : None, 2 : None, 3 : None}
if "veil" in outcome.keys():
if runs < 4:
self.bases[runs] = self.get_batter()
else:
runs += 1
return runs return runs
elif "advance" in outcome.keys(): elif "advance" in outcome.keys():
@ -535,6 +548,10 @@ class game(object):
elif result["text"] == appearance_outcomes.homerun or result["text"] == appearance_outcomes.grandslam: 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["total_bases"] += 4
self.get_batter().game_stats["home_runs"] += 1 self.get_batter().game_stats["home_runs"] += 1
if self.weather.name == "Thinned Veil":
result["veil"] = True
scores_to_add += self.baserunner_check(defender, result) scores_to_add += self.baserunner_check(defender, result)

View File

@ -64,6 +64,7 @@ def init_league_db(league):
c.execute(teams_table_check_string) c.execute(teams_table_check_string)
for team in league.teams_in_league(): for team in league.teams_in_league():
print(team)
c.execute("INSERT INTO teams (name) VALUES (?)", (team.name,)) c.execute("INSERT INTO teams (name) VALUES (?)", (team.name,))
player_string = "INSERT INTO stats (name, team_name) VALUES (?,?)" player_string = "INSERT INTO stats (name, team_name) VALUES (?,?)"
@ -72,22 +73,26 @@ def init_league_db(league):
for pitcher in team.rotation: for pitcher in team.rotation:
c.execute(player_string, (pitcher.name, team.name)) c.execute(player_string, (pitcher.name, team.name))
conn.commit()
conn.close()
def save_league(league):
if league_exists(league.name):
state_dic = { state_dic = {
"season" : league.season,
"day" : league.day, "day" : league.day,
"constraints" : league.constraints,
"schedule" : league.schedule, "schedule" : league.schedule,
"game_length" : league.game_length, "game_length" : league.game_length,
"series_length" : league.series_length, "series_length" : league.series_length,
"games_per_hour" : league.games_per_hour, "games_per_hour" : league.games_per_hour,
"historic" : False "owner" : league.owner,
"champions" : league.champions,
"historic" : league.historic
} }
if not os.path.exists(os.path.dirname(os.path.join(data_dir, league_dir, league.name, f"{league.name}.state"))):
os.makedirs(os.path.dirname(os.path.join(data_dir, league_dir, league.name, f"{league.name}.state")))
with open(os.path.join(data_dir, league_dir, league.name, f"{league.name}.state"), "w") as state_file: with open(os.path.join(data_dir, league_dir, league.name, f"{league.name}.state"), "w") as state_file:
json.dump(state_dic, state_file, indent=4) json.dump(state_dic, state_file, indent=4)
conn.commit()
conn.close()
def add_stats(league_name, player_game_stats_list): def add_stats(league_name, player_game_stats_list):
conn = create_connection(league_name) conn = create_connection(league_name)
if conn is not None: if conn is not None:
@ -122,11 +127,22 @@ def update_standings(league_name, update_dic):
conn.commit() conn.commit()
conn.close() conn.close()
def get_standings(league_name):
if league_exists(league_name):
conn = create_connection(league_name)
if conn is not None:
c = conn.cursor()
c.execute("SELECT name, wins, losses, run_diff FROM teams",)
standings_array = c.fetchall()
conn.close()
return standings_array
def league_exists(league_name): def league_exists(league_name):
with os.scandir(os.path.join(data_dir, league_dir)) as folder: with os.scandir(os.path.join(data_dir, league_dir)) as folder:
for subfolder in folder: for subfolder in folder:
if league_name in subfolder.name: if league_name in subfolder.name:
return not state(league_name)["historic"] return True
return False return False

View File

@ -10,13 +10,20 @@ league_dir = "leagues"
class league_structure(object): class league_structure(object):
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
self.historic = False
self.owner = None
self.season = 1
self.autoplay = -1
self.champions = {}
def setup(self, league_dic, division_games = 1, inter_division_games = 1, inter_league_games = 1, games_per_hour = 2): def setup(self, league_dic, division_games = 1, inter_division_games = 1, inter_league_games = 1, games_per_hour = 2):
self.league = league_dic #key: subleague, value: {division : team_name} self.league = league_dic # { subleague name : { division name : [team object] } }
self.constraints = { self.constraints = {
"division_games" : division_games, "division_games" : division_games,
"inter_div_games" : inter_division_games, "inter_div_games" : inter_division_games,
"inter_league_games" : inter_league_games "inter_league_games" : inter_league_games,
"division_leaders" : 0,
"wild_cards" : 0
} }
self.day = 1 self.day = 1
self.schedule = {} self.schedule = {}
@ -33,11 +40,46 @@ class league_structure(object):
def last_series_check(self): def last_series_check(self):
return str(math.ceil((self.day)/self.series_length) + 1) in self.schedule.keys() return str(math.ceil((self.day)/self.series_length) + 1) not in self.schedule.keys()
def day_to_series_num(self, day): def day_to_series_num(self, day):
return math.ceil((self.day)/self.series_length) 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): def find_team(self, team_name):
for subleague in iter(self.league.keys()): for subleague in iter(self.league.keys()):
for division in iter(self.league[subleague].keys()): for division in iter(self.league[subleague].keys()):
@ -164,19 +206,126 @@ class league_structure(object):
day = 1 day = 1
while not scheduled: while not scheduled:
found = False found = False
if day in self.schedule.keys(): if str(day) in self.schedule.keys():
for game_on_day in self.schedule[day]: for game_on_day in self.schedule[str(day)]:
for team in game: for team in game:
if team in game_on_day: if team in game_on_day:
found = True found = True
if not found: if not found:
self.schedule[day].append(game) self.schedule[str(day)].append(game)
scheduled = True scheduled = True
else: else:
self.schedule[day] = [game] self.schedule[str(day)] = [game]
scheduled = True scheduled = True
day += 1 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 season_length(self):
return int(list(self.schedule.keys())[-1]) * self.series_length
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()):
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
teams_string = ""
for this_team in teams:
if this_team[2] != 0 or this_team[1] != 0:
teams_string += f"**{this_team[0].name}\n**{this_team[1]} - {this_team[2]} WR: {round(this_team[1]/(this_team[1]+this_team[2]), 3)} GB: {this_team[4]}\n\n"
else:
teams_string += f"**{this_team[0].name}\n**{this_team[1]} - {this_team[2]} WR: - GB: {this_team[4]}\n\n"
this_embed.add_field(name=f"{division} Division:", value=teams_string, inline = False)
this_embed.set_footer(text=f"Standings as of day {self.day-1} / {self.season_length()}")
return this_embed
def wildcard_embed(self):
this_embed = Embed(color=Color.purple(), title=f"{self.name} Wildcard Race")
standings = {}
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()):
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)
teams_string = ""
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:
teams_string += f"**{this_team[0].name}\n**{this_team[1]} - {this_team[2]} WR: {round(this_team[1]/(this_team[1]+this_team[2]), 3)} GB: {this_team[4]}\n\n"
else:
teams_string += f"**{this_team[0].name}\n**{this_team[1]} - {this_team[2]} WR: - GB: {this_team[4]}\n\n"
this_embed.add_field(name=f"{subleague} League:", value=teams_string, 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} Subleague Series", team_dic, series_length=3, finals_series_length=5, 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): 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): 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.name = name
@ -191,6 +340,9 @@ class tournament(object):
self.round_delay = secs_between_rounds self.round_delay = secs_between_rounds
self.finals = False self.finals = False
self.id = id self.id = id
self.league = None
self.winner = None
self.day = None
if id is None: if id is None:
self.id = random.randint(1111,9999) self.id = random.randint(1111,9999)
@ -288,7 +440,7 @@ def save_league(this_league):
with open(os.path.join(data_dir, league_dir, this_league.name, f"{this_league.name}.league"), "w") as league_file: 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) league_json_string = jsonpickle.encode(this_league.league, keys=True)
json.dump(league_json_string, league_file, indent=4) json.dump(league_json_string, league_file, indent=4)
return True league_db.save_league(this_league)
def load_league_file(league_name): def load_league_file(league_name):
if league_db.league_exists(league_name): if league_db.league_exists(league_name):
@ -298,4 +450,18 @@ def load_league_file(league_name):
this_league.league = jsonpickle.decode(json.load(league_file), keys=True, classes=team) 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: 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) 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"]
try:
this_league.champions = state_dic["champions"]
except:
this_league.champions = {}
return this_league return this_league

View File

@ -1,6 +1,9 @@
import asyncio, time, datetime, games, json, threading, jinja2, leagues, os import asyncio, time, datetime, games, json, threading, jinja2, leagues, os, leagues
from flask import Flask, url_for, Response, render_template, request, jsonify, send_from_directory 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
from flask_socketio import SocketIO, emit from flask_socketio import SocketIO, emit
import database as db
app = Flask("the-prestige", static_folder='simmadome/build') app = Flask("the-prestige", static_folder='simmadome/build')
app.config['SECRET KEY'] = 'dev' app.config['SECRET KEY'] = 'dev'
@ -16,6 +19,59 @@ def serve(path):
else: else:
return send_from_directory(app.static_folder, 'index.html') return send_from_directory(app.static_folder, 'index.html')
### API
@app.route('/api/teams/search')
def search_teams():
query = request.args.get('query')
page_len = int(request.args.get('page_len'))
page_num = int(request.args.get('page_num'))
if query is None:
abort(400, "A query term is required")
result = db.search_teams(query)
if page_len is not None: #pagination should probably be done in the sqlite query but this will do for now
if page_num is None:
abort(400, "A page_len argument must be accompanied by a page_num argument")
result = result[page_num*page_len : (page_num + 1)*page_len]
return jsonify([json.loads(x[0])['name'] for x in result]) #currently all we need is the name but that can change
@app.route('/api/leagues', methods=['POST'])
def create_league():
config = json.loads(request.data)
if (league_exists(config['name'])):
abort(400, "A league by that name already exists")
print(config)
league_dic = {
subleague['name'] : {
division['name'] : [games.get_team(team_name) for team_name in division['teams']]
for division in subleague['divisions']
}
for subleague in config['structure']['subleagues']
}
new_league = league_structure(config['name'])
new_league.setup(
league_dic,
division_games=config['division_series'],
inter_division_games=config['inter_division_series'],
inter_league_games=config['inter_league_series'],
)
new_league.constraints["division_leaders"] = config["top_postseason"]
new_league.constraints["wild_cards"] = config["wildcards"]
new_league.generate_schedule()
leagues.save_league(new_league)
return "League created successfully"
### SOCKETS
thread2 = threading.Thread(target=socketio.run,args=(app,'0.0.0.0')) thread2 = threading.Thread(target=socketio.run,args=(app,'0.0.0.0'))
thread2.start() thread2.start()
@ -110,6 +166,8 @@ def update_loop():
if this_game.last_update[0]["defender"] != "": if this_game.last_update[0]["defender"] != "":
punc = ". " punc = ". "
if "fc_out" in this_game.last_update[0].keys(): if "fc_out" in this_game.last_update[0].keys():
name, base_string = this_game.last_update[0]['fc_out'] 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}" 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}"
@ -121,6 +179,13 @@ def update_loop():
state["update_emoji"] = "🏏" state["update_emoji"] = "🏏"
state["update_text"] = updatestring 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."
state["bases"] = this_game.named_bases() state["bases"] = this_game.named_bases()
state["top_of_inning"] = this_game.top_of_inning state["top_of_inning"] = this_game.top_of_inning

View File

@ -2276,6 +2276,15 @@
"pretty-format": "^26.0.0" "pretty-format": "^26.0.0"
} }
}, },
"@types/jquery": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.5.tgz",
"integrity": "sha512-6RXU9Xzpc6vxNrS6FPPapN1SxSHgQ336WC6Jj/N8q30OiaBZ00l1GBgeP7usjVZPivSkGUfL1z/WW6TX989M+w==",
"dev": true,
"requires": {
"@types/sizzle": "*"
}
},
"@types/json-schema": { "@types/json-schema": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
@ -2370,6 +2379,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/sizzle": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
"dev": true
},
"@types/socket.io-client": { "@types/socket.io-client": {
"version": "1.4.34", "version": "1.4.34",
"resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.34.tgz", "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.34.tgz",
@ -9529,6 +9544,11 @@
} }
} }
}, },
"jquery": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
},
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@ -4,6 +4,7 @@
"private": true, "private": true,
"proxy": "http://localhost:5000", "proxy": "http://localhost:5000",
"dependencies": { "dependencies": {
"jquery": "^3.5.1",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-router": "^5.2.0", "react-router": "^5.2.0",
@ -19,6 +20,7 @@
"@testing-library/react": "^11.2.2", "@testing-library/react": "^11.2.2",
"@testing-library/user-event": "^12.6.0", "@testing-library/user-event": "^12.6.0",
"@types/jest": "^26.0.19", "@types/jest": "^26.0.19",
"@types/jquery": "^3.5.5",
"@types/node": "^12.19.12", "@types/node": "^12.19.12",
"@types/react": "^16.14.2", "@types/react": "^16.14.2",
"@types/react-dom": "^16.9.10", "@types/react-dom": "^16.9.10",

View File

@ -0,0 +1,310 @@
.cl_table_header > .cl_subleague_bg {
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
padding-top: 1rem;
}
.cl_league_structure_table .cl_table_row:last-child .cl_subleague_bg {
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
padding-bottom: 1rem;
}
.cl_league_structure_table .cl_table_row:last-child .cl_division_delete {
margin-bottom: 0.5rem;
}
input {
border: none;
border-radius: 1rem;
height: 2rem;
padding-left: 1rem;
background: var(--background-secondary);
font-size: 14pt;
}
input:focus {
outline: none;
}
input[type="number"] {
-webkit-appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
.cl_league_main {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 3rem;
}
.cl_league_name {
margin-top: 1rem;
}
.cl_league_options, .cl_league_structure, .cl_confirm_box {
display: flex;
background: var(--background-tertiary);
flex-direction: column;
max-width: 100%;
border-radius: 1rem;
padding-top: 1.5rem;
}
.cl_confirm_box {
min-width: 55rem;
padding: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 20pt;
}
.cl_league_options {
align-items: center;
}
.cl_league_structure, .cl_subleague_add_align {
display: flex;
align-items: center;
justify-content: center;
width: min-content;
}
.cl_league_structure {
margin-top: 1rem;
}
.cl_league_structure_table {
display: table;
margin-right: 1rem;
}
.cl_headers, .cl_table_row {
display: table-row;
height: min-content;
}
.cl_table_header, .cl_delete_filler, .cl_delete_box, .cl_division_cell {
display: table-cell;
height:100%;
}
.cl_delete_box {
vertical-align: middle;
}
.cl_league_structure_scrollbox {
max-width: 100%;
overflow-y: scroll;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.cl_league_structure_scrollbox::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.cl_league_structure_scrollbox {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.cl_subleague_add_align{
margin-left: 1.5rem;
padding-right: 1.5rem;
}
.cl_subleague_header {
display: flex;
width:100%;
align-items: center;
justify-content: space-between;
}
.cl_subleague_bg {
background: var(--background-main);
padding: 0.5rem 1rem;
margin: 0rem 0.5rem;
min-width: 22rem;
height: 100%;
box-sizing: border-box;
}
.cl_subleague_name {
flex-grow: 1;
margin-right: 0.5rem;
}
.cl_division_name_box {
width: 100%;
}
.cl_division_name {
margin-bottom: 0.5rem;
width: 95%;
}
.cl_newteam_name {
width: 95%;
}
.cl_division {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem;
border-radius: 0.5rem;
background: var(--background-accent);
}
.cl_team, .cl_team_add {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin: 0.4rem 0rem;
}
.cl_team_name {
font-size: 14pt;
padding: 0 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
}
.cl_team_add {
display: flex;
flex-direction: column;
margin-bottom: 0rem;
}
.cl_search_list {
width: 95%;
margin-top: 0.6rem;
padding: 0.5rem;
background: var(--background-tertiary);
border-radius: 0.5rem;
}
.cl_search_result {
padding: 0.2rem 0.4rem;
border-radius: 0.5rem;
}
.cl_search_result:hover {
background: var(--background-main);
}
.cl_league_options {
padding: 1rem;
margin-top: 1.5rem;
width: 55rem;
box-sizing: border-box;
}
.cl_option_main {
display: flex;
flex-direction: row;
justify-content: space-around;
width: 100%;
}
.cl_option_submit_box {
display: flex;
flex-direction: column;
align-items: center;
}
.cl_option_box {
margin: 1rem;
width: max-content;
}
.cl_option_label, .cl_option_err, .cl_structure_err {
margin: 0.25rem;
}
.cl_option_err, .cl_structure_err {
color: var(--accent-red);
}
.cl_option_err {
min-height: 1.5rem;
margin-bottom: -0.5rem;
margin-top: 0.5rem;
}
.cl_structure_err {
margin-bottom: -0.5rem;
}
.cl_structure_err_div {
margin-top: -0.5rem;
margin-bottom: 0;
}
.cl_structure_err_teams {
width: 98%;
}
/* button styles */
button > .emoji {
margin: 0;
width: 1rem;
height: 1rem;
}
.cl_subleague_delete, .cl_team_delete, .cl_division_delete, .cl_subleague_add, .cl_division_add {
padding: 0;
width: 2rem;
height: 2rem;
border: none;
border-radius: 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.cl_subleague_delete, .cl_team_delete, .cl_division_delete {
background: var(--accent-red);
}
.cl_subleague_add, .cl_division_add {
background: var(--accent-green);
}
.cl_subleague_add {
position: relative;
top: 1.6rem;
}
.cl_division_add {
margin-top: 1.25rem;
margin-bottom: 1.25rem;
}
.cl_delete_filler {
min-width: 3rem;
}
.cl_option_submit {
padding: 1rem 2rem;
height: 2rem;
border: none;
border-radius: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
background: var(--accent-green);
font-size: 14pt;
}

View File

@ -0,0 +1,463 @@
import React, {useState, useRef, useLayoutEffect, useReducer} from 'react';
import {removeIndex, replaceIndex, append, arrayOf, shallowClone, getUID, DistributiveOmit} from './util';
import './CreateLeague.css';
import twemoji from 'twemoji';
// STATE CLASSES
class LeagueStructureState {
subleagues: SubleagueState[]
constructor(subleagues: SubleagueState[] = []) {
this.subleagues = subleagues;
}
}
class SubleagueState {
name: string
divisions: DivisionState[]
id: string|number
constructor(divisions: DivisionState[] = []) {
this.name = "";
this.divisions = divisions;
this.id = getUID();
}
}
class DivisionState {
name: string
teams: TeamState[]
id: string|number
constructor() {
this.name = "";
this.teams = [];
this.id = getUID();
}
}
class TeamState {
name: string
id: string|number
constructor(name: string = "") {
this.name = name;
this.id = getUID();
}
}
// STRUCTURE REDUCER
type StructureReducerActions =
{type: 'remove_subleague', subleague_index: number} |
{type: 'add_subleague'} |
{type: 'rename_subleague', subleague_index: number, name: string} |
{type: 'remove_divisions', division_index: number} |
{type: 'add_divisions'} |
{type: 'rename_division', subleague_index: number, division_index: number, name: string} |
{type: 'remove_team', subleague_index: number, division_index: number, name:string} |
{type: 'add_team', subleague_index:number, division_index:number, name:string}
function leagueStructureReducer(state: LeagueStructureState, action: StructureReducerActions): LeagueStructureState {
switch (action.type) {
case 'remove_subleague':
return {subleagues: removeIndex(state.subleagues, action.subleague_index)};
case 'add_subleague':
return {subleagues: append(state.subleagues, new SubleagueState(
arrayOf(state.subleagues[0].divisions.length, i =>
new DivisionState()
)
))}
case 'rename_subleague':
return replaceSubleague(state, action.subleague_index, subleague => {
let nSubleague = shallowClone(subleague);
nSubleague.name = action.name;
return nSubleague;
});
case 'remove_divisions':
return {subleagues: state.subleagues.map(subleague => {
let nSubleague = shallowClone(subleague);
nSubleague.divisions = removeIndex(subleague.divisions, action.division_index)
return nSubleague;
})};
case 'add_divisions':
return {subleagues: state.subleagues.map(subleague => {
let nSubleague = shallowClone(subleague);
nSubleague.divisions = append(subleague.divisions, new DivisionState())
return nSubleague;
})};
case 'rename_division':
return replaceDivision(state, action.subleague_index, action.division_index, division => {
let nDivision = shallowClone(division);
nDivision.name = action.name;
return nDivision;
});
case 'remove_team':
return replaceDivision(state, action.subleague_index, action.division_index, division => {
let nDivision = shallowClone(division);
nDivision.teams = removeIndex(division.teams, division.teams.findIndex(val => val.name === action.name));
return nDivision;
});
case 'add_team':
return replaceDivision(state, action.subleague_index, action.division_index, division => {
let nDivision = shallowClone(division);
nDivision.teams = append(division.teams, new TeamState(action.name));
return nDivision;
});
}
}
function replaceSubleague(state: LeagueStructureState, si: number, func: (val: SubleagueState) => SubleagueState) {
return {subleagues: replaceIndex(state.subleagues, si, func(state.subleagues[si]))}
}
function replaceDivision(state: LeagueStructureState, si: number, di: number, func:(val: DivisionState) => DivisionState) {
return replaceSubleague(state, si, subleague => {
let nSubleague = shallowClone(subleague);
nSubleague.divisions = replaceIndex(subleague.divisions, di, func(subleague.divisions[di]));
return nSubleague;
});
}
// OPTIONS REDUCER
class LeagueOptionsState {
games_series = "3"
intra_division_series = "8"
inter_division_series = "16"
inter_league_series = "8"
top_postseason = "1"
wildcards = "0"
}
type OptionsReducerActions =
{type: 'set_games_series', value: string} |
{type: 'set_intra_division_series', value: string} |
{type: 'set_inter_division_series', value: string} |
{type: 'set_inter_league_series', value: string} |
{type: 'set_top_postseason', value: string} |
{type: 'set_wildcards', value: string}
function LeagueOptionsReducer(state: LeagueOptionsState, action: OptionsReducerActions) {
let newState = shallowClone(state);
switch (action.type) {
case 'set_games_series':
newState.games_series = action.value;
break;
case 'set_intra_division_series':
newState.intra_division_series = action.value;
break;
case 'set_inter_division_series':
newState.inter_division_series = action.value;
break;
case 'set_inter_league_series':
newState.inter_league_series = action.value;
break;
case 'set_top_postseason':
newState.top_postseason = action.value;
break;
case 'set_wildcards':
newState.wildcards = action.value;
break;
}
return newState
}
// CREATE LEAGUE
let initLeagueStructure = {
subleagues: [0, 1].map((val) =>
new SubleagueState([0, 1].map((val) =>
new DivisionState()
))
)
};
function CreateLeague() {
let [name, setName] = useState("");
let [showError, setShowError] = useState(false);
let [nameExists, setNameExists] = useState(false);
let [createSuccess, setCreateSuccess] = useState(false);
let [structure, structureDispatch] = useReducer(leagueStructureReducer, initLeagueStructure);
let [options, optionsDispatch] = useReducer(LeagueOptionsReducer, new LeagueOptionsState());
let self = useRef<HTMLDivElement | null>(null)
useLayoutEffect(() => {
if (self.current) {
twemoji.parse(self.current)
}
})
if (createSuccess) {
return(
<div className="cl_league_main" ref={self}>
<div className="cl_confirm_box">
League created succesfully!
</div>
</div>
);
}
return (
<div className="cl_league_main" ref={self}>
<input type="text" className="cl_league_name" placeholder="League Name" value={name} onChange={(e) => {
setName(e.target.value);
setNameExists(false);
}}/>
<div className="cl_structure_err">{
name === "" && showError ? "A name is required." :
nameExists && showError ? "A league by that name already exists" :
""
}</div>
<LeagueStructre state={structure} dispatch={structureDispatch} showError={showError}/>
<div className="cl_league_options">
<LeagueOptions state={options} dispatch={optionsDispatch} showError={showError}/>
<div className="cl_option_submit_box">
<button className="cl_option_submit" onClick={e => {
if (!validRequest(name, structure, options)) {
setShowError(true);
} else {
let req = new XMLHttpRequest();
let data = makeRequest(name, structure, options);
req.open("POST", "/api/leagues", true);
req.setRequestHeader("Content-type", "application/json");
req.onreadystatechange = () => {
if(req.readyState === 4) {
if (req.status === 200) {
setCreateSuccess(true);
}
if (req.status === 400) {
setNameExists(true);
setShowError(true);
}
}
}
req.send(data);
}
}}>Submit</button>
<div className="cl_option_err">{
!validRequest(name, structure, options) && showError ?
"Cannot create league. Some information is missing or invalid." : ""
}</div>
</div>
</div>
</div>
);
}
function makeRequest(name:string, structure: LeagueStructureState, options:LeagueOptionsState) {
return JSON.stringify({
name: name,
structure: {
subleagues: structure.subleagues.map(subleague => ({
name: subleague.name,
divisions: subleague.divisions.map(division => ({
name: division.name,
teams: division.teams.map(team => team.name)
}))
}))
},
games_per_series: Number(options.games_series),
division_series: Number(options.intra_division_series),
inter_division_series: Number(options.inter_division_series),
inter_league_series: Number(options.inter_league_series),
top_postseason: Number(options.top_postseason),
wildcards: Number(options.wildcards)
});
}
function validRequest(name:string, structure: LeagueStructureState, options:LeagueOptionsState) {
return (
name !== "" &&
validNumber(options.games_series) &&
validNumber(options.intra_division_series) &&
validNumber(options.inter_division_series) &&
validNumber(options.inter_league_series) &&
validNumber(options.top_postseason) &&
validNumber(options.wildcards, 0) &&
structure.subleagues.length % 2 === 0 &&
structure.subleagues.every(subleague =>
subleague.name !== "" &&
subleague.divisions.every(division =>
division.name !== "" &&
division.teams.length >= 2
)
)
)
}
function validNumber(value: string, min = 1) {
return !isNaN(Number(value)) && Number(value) >= min
}
// LEAGUE STRUCUTRE
function LeagueStructre(props: {state: LeagueStructureState, dispatch: React.Dispatch<StructureReducerActions>, showError: boolean}) {
return (
<div className="cl_league_structure">
<div className="cl_league_structure_scrollbox">
<div className="cl_subleague_add_align">
<div className="cl_league_structure_table">
<SubleagueHeaders subleagues={props.state.subleagues} dispatch={props.dispatch} showError={props.showError}/>
<Divisions subleagues={props.state.subleagues} dispatch={props.dispatch} showError={props.showError}/>
</div>
<button className="cl_subleague_add" onClick={e => props.dispatch({type: 'add_subleague'})}></button>
</div>
</div>
<div className="cl_structure_err">{props.state.subleagues.length % 2 !== 0 && props.showError ? "Must have an even number of subleagues." : ""}</div>
<button className="cl_division_add" onClick={e => props.dispatch({type: 'add_divisions'})}></button>
</div>
);
}
function SubleagueHeaders(props: {subleagues: SubleagueState[], dispatch: React.Dispatch<StructureReducerActions>, showError:boolean}) {
return (
<div className="cl_headers">
<div key="filler" className="cl_delete_filler"/>
{props.subleagues.map((subleague, i) => (
<div key={subleague.id} className="cl_table_header">
<div className="cl_subleague_bg">
<SubleageHeader state={subleague} canDelete={props.subleagues.length > 1} dispatch={action =>
props.dispatch(Object.assign({subleague_index: i}, action))
}/>
<div className="cl_structure_err">{subleague.name === "" && props.showError ? "A name is required." : ""}</div>
</div>
</div>
))}
</div>
);
}
function SubleageHeader(props: {state: SubleagueState, canDelete: boolean, dispatch:(action: DistributiveOmit<StructureReducerActions, 'subleague_index'>) => void}) {
return (
<div className="cl_subleague_header">
<input type="text" className="cl_subleague_name" placeholder="Subleague Name" value={props.state.name} onChange={e =>
props.dispatch({type: 'rename_subleague', name: e.target.value})
}/>
{props.canDelete ? <button className="cl_subleague_delete" onClick={e => props.dispatch({type: 'remove_subleague'})}></button> : null}
</div>
);
}
function Divisions(props: {subleagues: SubleagueState[], dispatch: React.Dispatch<StructureReducerActions>, showError: boolean}) {
return (<>
{props.subleagues[0].divisions.map((val, di) => (
<div key={val.id} className="cl_table_row">
<div key="delete" className="cl_delete_box">
{props.subleagues[0].divisions.length > 1 ?
<button className="cl_division_delete" onClick={e => props.dispatch({type: 'remove_divisions', division_index: di})}></button> :
null
}
</div>
{props.subleagues.map((subleague, si) => (
<div key={subleague.id} className="cl_division_cell">
<div className="cl_subleague_bg">
<Division state={subleague.divisions[di]} dispatch={action =>
props.dispatch(Object.assign({subleague_index: si, division_index: di}, action))
} showError={props.showError}/>
</div>
</div>
))}
</div>
))}
</>);
}
function Division(props: {state: DivisionState, dispatch:(action: DistributiveOmit<StructureReducerActions, 'subleague_index'|'division_index'>) => void, showError:boolean}) {
let [newName, setNewName] = useState("");
let [searchResults, setSearchResults] = useState<string[]>([]);
let newNameInput = useRef<HTMLInputElement>(null);
let resultList = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (resultList.current) {
twemoji.parse(resultList.current)
}
})
return (
<div className="cl_division">
<div className="cl_division_name_box">
<input type="text" className="cl_division_name" placeholder="Division Name" key="input" value={props.state.name} onChange={e =>
props.dispatch({type: 'rename_division', name: e.target.value})
}/>
<div className="cl_structure_err cl_structure_err_div">{props.state.name === "" && props.showError ? "A name is required." : ""}</div>
</div>
{props.state.teams.map((team, i) => (
<div className="cl_team" key={team.id}>
<div className="cl_team_name">{team.name}</div>
<button className="cl_team_delete" onClick={e => props.dispatch({type:'remove_team', name: team.name})}></button>
</div>
))}
<div className="cl_team_add">
<input type="text" className="cl_newteam_name" placeholder="Add team..." value={newName} ref={newNameInput}
onChange={e => {
let params = new URLSearchParams({query: e.target.value, page_len: '5', page_num: '0'});
fetch("/api/teams/search?" + params.toString())
.then(response => response.json())
.then(data => setSearchResults(data));
setNewName(e.target.value);
}}/>
</div>
{searchResults.length > 0 && newName.length > 0 ?
(<div className="cl_search_list" ref={resultList}>
{searchResults.map(result =>
<div className="cl_search_result" key={result} onClick={e => {
props.dispatch({type:'add_team', name: result});
setNewName("");
if (newNameInput.current) {
newNameInput.current.focus();
}
}}>{result}</div>
)}
</div>):
<div/>
}
<div className="cl_structure_err cl_structure_err_teams">{props.state.teams.length < 2 && props.showError ? "Must have at least 2 teams." : ""}</div>
</div>
);
}
// LEAGUE OPTIONS
function LeagueOptions(props: {state: LeagueOptionsState, dispatch: React.Dispatch<OptionsReducerActions>, showError: boolean}) {
return (
<div className="cl_option_main">
<div className="cl_option_column">
<NumberInput title="Number of games per series" value={props.state.games_series} setValue={(value: string) =>
props.dispatch({type: 'set_games_series', value: value})} showError={props.showError}/>
<NumberInput title="Number of teams from top of division to postseason" value={props.state.top_postseason} setValue={(value: string) =>
props.dispatch({type: 'set_top_postseason', value: value})} showError={props.showError}/>
<NumberInput title="Number of wildcards" value={props.state.wildcards} minValue={0} setValue={(value: string) =>
props.dispatch({type: 'set_wildcards', value: value})} showError={props.showError}/>
</div>
<div className="cl_option_column">
<NumberInput title="Number of series with each division opponent" value={props.state.intra_division_series} setValue={(value: string) =>
props.dispatch({type: 'set_intra_division_series', value: value})} showError={props.showError}/>
<NumberInput title="Number of inter-divisional series" value={props.state.inter_division_series} setValue={(value: string) =>
props.dispatch({type: 'set_inter_division_series', value: value})} showError={props.showError}/>
<NumberInput title="Number of inter-league series" value={props.state.inter_league_series} setValue={(value: string) =>
props.dispatch({type: 'set_inter_league_series', value: value})} showError={props.showError}/>
</div>
</div>
);
}
function NumberInput(props: {title: string, value: string, setValue: (newVal: string) => void, showError: boolean, minValue?:number}) {
let minValue = 1;
if (props.minValue !== undefined) {
minValue = props.minValue
}
return (
<div className="cl_option_box">
<div className="cl_option_label">{props.title}</div>
<input className="cl_option_input" type="number" min={minValue} value={props.value} onChange={e => props.setValue(e.target.value)}/>
<div className="cl_option_err">{(!isNaN(Number(props.value)) || Number(props.value) < minValue) && props.showError ? "Must be a number greater than "+minValue : ""}</div>
</div>
);
}
export default CreateLeague;

View File

@ -1,5 +1,4 @@
.game { .game {
align-self: stretch;
text-align: center; text-align: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -3,5 +3,35 @@
margin-left: 1rem; margin-left: 1rem;
margin-right: 1rem; margin-right: 1rem;
display: flex; display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around; justify-content: space-around;
} }
.history_box {
width: 100%;
margin-top: 3rem;
padding: 1rem;
padding-top: 0.5rem;
background: var(--background-main);
border-radius: 0.25rem;
width: 100%;
min-width: 32rem;
max-width: 44rem;
box-sizing: border-box;
border: 4px solid;
border-radius: 4px;
border-color: var(--highlight);
border-top: none;
border-right: none;
border-bottom: none;
}
.history_title {
font-size: 14pt;
}
.history_update {
height: 4rem;
margin: 0.5rem;
}

View File

@ -1,22 +1,62 @@
import React, {useState} from 'react'; import React, {useState, useRef, useLayoutEffect} from 'react';
import twemoji from 'twemoji';
import ReactRouter from 'react-router'; import ReactRouter from 'react-router';
import {GameState, useListener} from './GamesUtil'; import {GameState, useListener} from './GamesUtil';
import './GamePage.css'; import './GamePage.css';
import Game from './Game'; import Game from './Game';
import {getUID} from './util';
function GamePage(props: ReactRouter.RouteComponentProps<{id: string}>) { function GamePage(props: ReactRouter.RouteComponentProps<{id: string}>) {
let [games, setGames] = useState<[string, GameState][]>([]); let [game, setGame] = useState<[string, GameState]|undefined>(undefined);
useListener((newGames) => setGames(newGames)); let history = useRef<[number, string, string][]>([]);
useListener((newGames) => {
let newGame = newGames.find((gamePair) => gamePair[0] === props.match.params.id);
setGame(newGame);
console.log(newGame);
if (newGame !== undefined && newGame[1].start_delay < 0 && newGame[1].end_delay > 8) {
history.current.unshift([getUID(), newGame[1].update_emoji, newGame[1].update_text]);
if (history.current.length > 8) {
history.current.pop();
}
}
});
if (game === undefined) {
return <div id="game_container">The game you're looking for either doesn't exist or has already ended.</div>
}
let game = games.find((game) => game[0] === props.match.params.id)
return ( return (
<div id="game_container"> <div id="game_container">
{ game ? <Game gameId={game[0]} state={game[1]}/>
<Game gameId={game[0]} state={game[1]}/> : { history.current.length > 0 ?
"The game you're looking for either doesn't exist or has already ended." <GameHistory history={history.current}/> :
null
} }
</div> </div>
); );
} }
function GameHistory(props: {history: [number, string, string][]}) {
let self = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (self.current) {
twemoji.parse(self.current);
}
})
return (
<div className="history_box" ref={self}>
<div className="history_title">History</div>
{props.history.map((update) => (
<div className="update history_update" key={update[0]}>
<div className="update_emoji">{update[1]}</div>
<div className="update_text">{update[2]}</div>
</div>
))}
</div>
);
}
export default GamePage; export default GamePage;

View File

@ -76,9 +76,4 @@
left: 50%; left: 50%;
transform: translate(-50%, 0); transform: translate(-50%, 0);
} }
.emptyslot {
border: none;
min-height: 0px;
}
} }

View File

@ -8,6 +8,7 @@ function GamesPage() {
let [search, setSearch] = useState(window.location.search); let [search, setSearch] = useState(window.location.search);
useEffect(() => { useEffect(() => {
setSearch(window.location.search); setSearch(window.location.search);
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [window.location.search]) }, [window.location.search])
let searchparams = new URLSearchParams(search); let searchparams = new URLSearchParams(search);

View File

@ -20,6 +20,8 @@ interface GameState {
update_text: string update_text: string
is_league: boolean is_league: boolean
leagueoruser: string leagueoruser: string
start_delay: number
end_delay: number
} }
type GameList = ([id: string, game: GameState] | null)[]; type GameList = ([id: string, game: GameState] | null)[];
@ -32,6 +34,7 @@ const useListener = (onUpdate: (update: [string, GameState][]) => void, url: str
socket.on('connect', () => socket.emit('recieved', {})); socket.on('connect', () => socket.emit('recieved', {}));
socket.on('states_update', onUpdate); socket.on('states_update', onUpdate);
return () => {socket.disconnect()}; return () => {socket.disconnect()};
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]) }, [url])
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
simmadome/src/img/twitter.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -58,23 +58,51 @@ h2 {
#link_div { #link_div {
text-align: right; text-align: right;
position: absolute; position: absolute;
top: 0px; top: 1rem;
right: 30px; right: 2rem;
display: flex;
} }
#link_div > a { .github_logo, .twitter_logo, .patreon_container {
height: 2rem;
width: 2rem;
margin-left: 0.75rem;
}
.patreon_container {
border-radius: 1rem;
background: #FF424D;
}
.patreon_logo {
box-sizing: border-box;
padding: 0.35rem;
height: 2rem;
width: 2rem;
position: relative;
left: 0.1rem;
bottom: 0.05rem;
}
a {
background-color: transparent; background-color: transparent;
text-decoration: underline; text-decoration: underline;
} }
#link_div > a:link, #link_div > a:visited { a:link, a:visited {
color: lightblue; color: lightblue;
} }
#link_div > a:hover { a:hover {
color: white; color: white;
} }
#utility_links {
position: absolute;
top: 1rem;
left: 2rem;
}
img.emoji { img.emoji {
height: 1em; height: 1em;
width: 1em; width: 1em;

View File

@ -1,11 +1,15 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
import './index.css'; import './index.css';
import GamesPage from './GamesPage'; import GamesPage from './GamesPage';
import GamePage from './GamePage'; import GamePage from './GamePage';
import CreateLeague from './CreateLeague';
import discordlogo from "./img/discord.png"; import discordlogo from "./img/discord.png";
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import patreonLogo from './img/patreon.png';
import githubLogo from './img/github.png';
import twitterLogo from './img/twitter.png';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
@ -13,6 +17,7 @@ ReactDOM.render(
<Header /> <Header />
<Switch> <Switch>
<Route path="/game/:id" component={GamePage}/> <Route path="/game/:id" component={GamePage}/>
<Route path="/create_league" component={CreateLeague} />
<Route path="/" component={GamesPage}/> <Route path="/" component={GamesPage}/>
</Switch> </Switch>
</Router> </Router>
@ -20,13 +25,25 @@ ReactDOM.render(
document.getElementById('root') document.getElementById('root')
); );
function Header() { function Header() {
return ( return (
<div id="header"> <div id="header">
<div id="link_div"> <div id="link_div">
<a href="https://www.patreon.com/sixteen" className="link" target="_blank" rel="noopener noreferrer">Patreon</a><br /> <a href="https://www.patreon.com/sixteen" className="patreon_link" target="_blank" rel="noopener noreferrer">
<a href="https://github.com/Sakimori/matteo-the-prestige" className="link" target="_blank" rel="noopener noreferrer">Github</a><br /> <div className="patreon_container">
<a href="https://twitter.com/intent/follow?screen_name=SIBR_XVI" className="link" target="_blank" rel="noopener noreferrer">Twitter</a> <img className="patreon_logo" src={patreonLogo} alt="Patreon"/>
</div>
</a>
<a href="https://github.com/Sakimori/matteo-the-prestige" className="github_link" target="_blank" rel="noopener noreferrer">
<img className="github_logo" src={githubLogo} alt="Github"/>
</a>
<a href="https://twitter.com/intent/follow?screen_name=SIBR_XVI" className="twitter_link" target="_blank" rel="noopener noreferrer">
<img className="twitter_logo" src={twitterLogo} alt="Twitter"/>
</a>
</div>
<div id="utility_links">
<Link to="/create_league">Create a League</Link>
</div> </div>
<a href="/" className="page_header"><h2 className="page_header" style={{fontSize:"50px"} as React.CSSProperties}>THE SIMMADOME</h2></a> <a href="/" className="page_header"><h2 className="page_header" style={{fontSize:"50px"} as React.CSSProperties}>THE SIMMADOME</h2></a>
<h2 className="page_header">Join SIBR on <a href="https://discord.gg/UhAajY2NCW" className="link"><img src={discordlogo} alt="" height="30"/></a> to start your own games!</h2> <h2 className="page_header">Join SIBR on <a href="https://discord.gg/UhAajY2NCW" className="link"><img src={discordlogo} alt="" height="30"/></a> to start your own games!</h2>

37
simmadome/src/util.tsx Normal file
View File

@ -0,0 +1,37 @@
import {useRef} from 'react';
function removeIndex(arr: any[], index: number) {
return arr.slice(0, index).concat(arr.slice(index+1));
}
function replaceIndex<T>(arr: T[], index: number, val: T) {
return arr.slice(0, index).concat([val]).concat(arr.slice(index+1));
}
function append<T>(arr: T[], val: T) {
return arr.concat([val]);
}
function arrayOf<T>(length: number, func: (i: number) => T): T[] {
var out: T[] = [];
for (var i = 0; i < length; i++) {
out.push(func(i));
}
return out;
}
function shallowClone<T>(obj: T): T {
return Object.assign({}, obj);
}
let getUID = function() { // does NOT generate UUIDs. Meant to create list keys ONLY
let id = 0;
return function() { return id++ }
}()
type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
//type DistributivePick<T, K extends keyof T> = T extends any ? Pick<T, K> : never;
export {removeIndex, replaceIndex, append, arrayOf, shallowClone, getUID};
export type {DistributiveOmit};

View File

@ -6,4 +6,4 @@ SELECT name,
ROUND(strikeouts_given*27.0/(outs_pitched*1.0),3) as kper9, ROUND(strikeouts_given*27.0/(outs_pitched*1.0),3) as kper9,
ROUND(strikeouts_given*1.0/walks_allowed*1.0,3) as kperbb ROUND(strikeouts_given*1.0/walks_allowed*1.0,3) as kperbb
FROM stats WHERE outs_pitched > 150 FROM stats WHERE outs_pitched > 150
ORDER BY bbper9 ASC; ORDER BY era ASC;

View File

@ -1,6 +1,7 @@
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
import database as db import database as db
import onomancer as ono import onomancer as ono
from league_storage import league_exists
from the_draft import Draft, DRAFT_ROUNDS from the_draft import Draft, DRAFT_ROUNDS
from flask import Flask from flask import Flask
from uuid import uuid4 from uuid import uuid4
@ -137,9 +138,9 @@ class StartGameCommand(Command):
league = command.split("\n")[0].split("--league ")[1].split("-")[0].strip() league = command.split("\n")[0].split("--league ")[1].split("-")[0].strip()
try: try:
if "-d " in command.split("\n")[0]: if "-d " in command.split("\n")[0]:
day = int(command.split("\n")[0].split("-d ")[1].split("-")[0].strip())-1 day = int(command.split("\n")[0].split("-d ")[1].split("-")[0].strip())
elif "--day " in command.split("\n")[0]: elif "--day " in command.split("\n")[0]:
day = int(command.split("\n")[0].split("--day ")[1].split("-")[0].strip())-1 day = int(command.split("\n")[0].split("--day ")[1].split("-")[0].strip())
except ValueError: except ValueError:
await msg.channel.send("Make sure you put an integer after the -d flag.") await msg.channel.send("Make sure you put an integer after the -d flag.")
return return
@ -201,7 +202,7 @@ class StartRandomGameCommand(Command):
channel = msg.channel channel = msg.channel
await msg.delete() await msg.delete()
await channel.send("Rolling the bones...") await channel.send("Rolling the bones... This might take a while.")
teamslist = games.get_all_teams() teamslist = games.get_all_teams()
game = games.game(random.choice(teamslist).finalize(), random.choice(teamslist).finalize()) game = games.game(random.choice(teamslist).finalize(), random.choice(teamslist).finalize())
@ -561,7 +562,12 @@ class StartTournamentCommand(Command):
if team == None: if team == None:
await msg.channel.send(f"We couldn't find {name}. Try again?") await msg.channel.send(f"We couldn't find {name}. Try again?")
return return
team_dic[team] = {"wins": 0} add = True
for extant_team in team_dic.keys():
if extant_team.name == team.name:
add = False
if add:
team_dic[team] = {"wins": 0}
channel = msg.channel channel = msg.channel
await msg.delete() await msg.delete()
@ -724,26 +730,204 @@ class StartDraftCommand(Command):
raise SlowDraftError('Too slow') raise SlowDraftError('Too slow')
return draft_message return draft_message
class DebugLeague(Command): class DebugLeagueStart(Command):
name = "league" name = "startdebugleague"
async def execute(self, msg, command): async def execute(self, msg, command):
league = leagues.league_structure("test2") if not league_exists("test2"):
league.setup({ league = leagues.league_structure("test2")
"nL" : { league.setup({
"nL west" : [get_team_fuzzy_search("lockpicks"), get_team_fuzzy_search("liches")], "nL" : {
"nL east" : [get_team_fuzzy_search("bethesda soft"), get_team_fuzzy_search("traverse city")] "nL west" : [get_team_fuzzy_search("lockpicks"), get_team_fuzzy_search("liches")],
}, "nL east" : [get_team_fuzzy_search("bethesda soft"), get_team_fuzzy_search("traverse city")]
"aL" : { },
"aL west" : [get_team_fuzzy_search("deep space"), get_team_fuzzy_search("phoenix")], "aL" : {
"aL east" : [get_team_fuzzy_search("cheyenne mountain"), get_team_fuzzy_search("tarot dragons")] "aL west" : [get_team_fuzzy_search("deep space"), get_team_fuzzy_search("phoenix")],
} "aL east" : [get_team_fuzzy_search("cheyenne mountain"), get_team_fuzzy_search("tarot dragons")]
}, division_games=6, inter_division_games=3, inter_league_games=3, games_per_hour = 12) }
league.generate_schedule() }, division_games=6, inter_division_games=3, inter_league_games=3, games_per_hour = 12)
leagues.save_league(league) league.generate_schedule()
await start_league_day(msg.channel, league, autoplay = 1) leagues.save_league(league)
class DebugLeagueDisplay(Command):
name = "displaydebugleague"
async def execute(self, msg, command):
if league_exists("test2"):
league = leagues.load_league_file("test2")
await msg.channel.send(embed=league.standings_embed())
class StartLeagueCommand(Command):
name = "startleague"
template = "m;startleague [league name]\n[games per hour]"
description = """Optional flag for the first line: `--queue X` or `-q X` to play X number of series before stopping.
Plays a league with a given name, provided that league has been saved on the website. The games per hour sets how often the games will start. (e.g. GPH 2 will start games at X:00 and X:30)"""
async def execute(self, msg, command):
if config()["game_freeze"]:
await msg.channel.send("Patch incoming. We're not allowing new games right now.")
return
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
command = command.split("\n")[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
try:
gph = int(command.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
if league_exists(league_name):
league = leagues.load_league_file(league_name)
if league.historic:
await msg.channel.send("That league is done and dusted, chief. Sorry.")
return
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
if (league.owner is not None and msg.author.id in league.owner) or msg.author.id in config()["owners"]:
league.autoplay = autoplay
league.games_per_hour = gph
if str(league.day_to_series_num(league.day)) not in league.schedule.keys():
await league_postseason(msg.channel, league)
elif league.day % league.series_length == 1:
await start_league_day(msg.channel, league)
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
else:
await msg.channel.send("Couldn't find that league, boss. Did you save it on the website?")
class LeagueDisplayCommand(Command):
name = "leaguestandings"
template = "m;leaguestandings [league name]"
description = "Displays the current standings for the given league."
async def execute(self, msg, command):
if league_exists(command.strip()):
league = leagues.load_league_file(command.strip())
await msg.channel.send(embed=league.standings_embed())
else:
await msg.channel.send("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):
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.")
else:
await msg.channel.send("Can't find that league, boss.")
class LeaguePauseCommand(Command):
name = "pauseleague"
template = "m;pauseleague [league name]"
descripton = "Tells a currently running league to stop running automatically after the current series."
async def execute(self, msg, command):
league_name = command.strip()
for active_league in active_leagues:
if active_league.name == league_name:
if (active_league.owner is not None and msg.author.id in active_league.owner) or msg.author.id in config()["owners"]:
active_league.autoplay = 0
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.")
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):
league_name = command.strip()
if league_exists(league_name):
league = leagues.load_league_file(league_name)
if league.owner is None:
league.owner = [msg.author.id]
leagues.save_league(league)
await msg.channel.send(f"The {league} commissioner is doing a great job. That's you, by the way.")
else:
await msg.channel.send("That league has already been claimed!")
await msg.channel.send("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):
league_name = command.split("\n")[0].strip()
if league_exists(league_name):
league = leagues.load_league_file(league_name)
if league.owner is not None and (msg.author.id in league.owner or msg.author.id in config()["owners"]):
for user in msg.mentions:
if user.id not in league.owner:
league.owner.append(user.id)
await msg.channel.send(f"The new {league} front office is now up and running.")
return
else:
await msg.channel.send(f"That league hasn't been claimed yet. Try m;claimleague first.")
return
else:
await msg.channel.send("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):
league_name = command.strip()
if league_exists(league_name):
league = leagues.load_league_file(league_name)
current_series = league.day_to_series_num(league.day)
if str(current_series+1) in league.schedule.keys():
sched_embed = discord.Embed(title=f"{league.name}'s Schedule:")
days = [0,1,2,3]
for day in days:
if str(current_series+day) in league.schedule.keys():
schedule_text = ""
for game in league.schedule[str(current_series+day)]:
schedule_text += f"**{game[0]}** @ **{game[1]}**\n"
sched_embed.add_field(name=f"Days {((current_series+day-1)*league.series_length) + 1} - {(current_series+day)*(league.series_length)}", value=schedule_text, inline = False)
await msg.channel.send(embed=sched_embed)
else:
await msg.channel.send("That league's already finished with this season, boss.")
else:
await msg.channel.send("We can't find that league. Typo?")
commands = [ commands = [
@ -765,14 +949,22 @@ commands = [
ShowAllTeamsCommand(), ShowAllTeamsCommand(),
SearchTeamsCommand(), SearchTeamsCommand(),
StartGameCommand(), StartGameCommand(),
StartTournamentCommand(),
StartRandomGameCommand(), StartRandomGameCommand(),
StartTournamentCommand(),
LeagueClaimCommand(),
LeagueAddOwnersCommand(),
StartLeagueCommand(),
LeaguePauseCommand(),
LeagueDisplayCommand(),
LeagueWildcardCommand(),
LeagueScheduleCommand(),
CreditCommand(), CreditCommand(),
RomanCommand(), RomanCommand(),
HelpCommand(), HelpCommand(),
StartDraftCommand(), StartDraftCommand(),
DraftPlayerCommand(), DraftPlayerCommand(),
DebugLeague() DebugLeagueStart(),
DebugLeagueDisplay()
] ]
client = discord.Client() client = discord.Client()
@ -1046,7 +1238,16 @@ async def start_tournament_round(channel, tourney, seeding = None):
for pair in games_to_start: for pair in games_to_start:
if pair[0] is not None and pair[1] is not None: if pair[0] is not None and pair[1] is not None:
this_game = games.game(pair[0].prepare_for_save().finalize(), pair[1].prepare_for_save().finalize(), length = tourney.game_length) team_a = get_team_fuzzy_search(pair[0].name)
team_b = get_team_fuzzy_search(pair[1].name)
if tourney.league is not None:
if tourney.day is None:
tourney.day = tourney.league.day
team_a.set_pitcher(rotation_slot = tourney.day)
team_b.set_pitcher(rotation_slot = tourney.day)
this_game = games.game(team_a.finalize(), team_b.finalize(), length = tourney.game_length)
this_game, state_init = prepare_game(this_game) this_game, state_init = prepare_game(this_game)
state_init["is_league"] = True state_init["is_league"] = True
@ -1077,6 +1278,14 @@ async def continue_tournament_series(tourney, queue, games_list, wins_in_series)
for oldgame in queue: for oldgame in queue:
away_team = games.get_team(oldgame.teams["away"].name) away_team = games.get_team(oldgame.teams["away"].name)
home_team = games.get_team(oldgame.teams["home"].name) home_team = games.get_team(oldgame.teams["home"].name)
if tourney.league is not None:
if tourney.day is None:
tourney.day = tourney.league.day
away_team.set_pitcher(rotation_slot = tourney.day)
home_team.set_pitcher(rotation_slot = tourney.day)
this_game = games.game(away_team.finalize(), home_team.finalize(), length = tourney.game_length) this_game = games.game(away_team.finalize(), home_team.finalize(), length = tourney.game_length)
this_game, state_init = prepare_game(this_game) this_game, state_init = prepare_game(this_game)
@ -1108,7 +1317,7 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals
try: try:
for i in range(0, len(games_list)): for i in range(0, len(games_list)):
game, key = games_list[i] game, key = games_list[i]
if game.over and main_controller.master_games_dic[key][1]["end_delay"] <= 8: if game.over and ((key in main_controller.master_games_dic.keys() and main_controller.master_games_dic[key][1]["end_delay"] <= 8) or not key in main_controller.master_games_dic.keys()):
if game.teams['home'].name not in wins_in_series.keys(): if game.teams['home'].name not in wins_in_series.keys():
wins_in_series[game.teams["home"].name] = 0 wins_in_series[game.teams["home"].name] = 0
if game.teams['away'].name not in wins_in_series.keys(): if game.teams['away'].name not in wins_in_series.keys():
@ -1122,6 +1331,7 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals
wins_in_series[winner_name] = 1 wins_in_series[winner_name] = 1
final_embed = game_over_embed(game) 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(f"A {tourney.name} game just ended!")
await channel.send(embed=final_embed) await channel.send(embed=final_embed)
if wins_in_series[winner_name] >= int((tourney.series_length+1)/2) and not finals: if wins_in_series[winner_name] >= int((tourney.series_length+1)/2) and not finals:
@ -1136,11 +1346,36 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals
except: except:
print("something went wrong in tourney_watcher") print("something went wrong in tourney_watcher")
await asyncio.sleep(4) await asyncio.sleep(4)
if tourney.league is not None:
tourney.day += 1
if len(queued_games) > 0: if len(queued_games) > 0:
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) if tourney.league is not None:
now = datetime.datetime.now()
validminutes = [int((60 * div)/tourney.league.games_per_hour) for div in range(0,tourney.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 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.")
await asyncio.sleep(tourney.delay)
await channel.send(f"{len(queued_games)} games for {tourney.name}, starting at {filter_url}") 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) games_list = await continue_tournament_series(tourney, queued_games, games_list, wins_in_series)
else: else:
@ -1148,7 +1383,10 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals
if finals: #if this last round was finals if finals: #if this last round was finals
embed = discord.Embed(color = discord.Color.dark_purple(), title = f"{winner_list[0]} win the {tourney.name} finals!") embed = discord.Embed(color = discord.Color.dark_purple(), title = f"{winner_list[0]} win the {tourney.name} finals!")
if tourney.day > tourney.league.day:
tourney.league.day = tourney.day
await channel.send(embed=embed) await channel.send(embed=embed)
tourney.winner = get_team_fuzzy_search(winner_list[0])
active_tournaments.pop(active_tournaments.index(tourney)) active_tournaments.pop(active_tournaments.index(tourney))
return return
@ -1157,11 +1395,37 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals
winners_string = "" winners_string = ""
for game in tourney.bracket.get_bottom_row(): for game in tourney.bracket.get_bottom_row():
winners_string += f"{game[0].name}\n{game[1].name}\n" winners_string += f"{game[0].name}\n{game[1].name}\n"
await channel.send(f"""
if tourney.league is not None:
now = datetime.datetime.now()
validminutes = [int((60 * div)/tourney.league.games_per_hour) for div in range(0,tourney.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"""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)
else:
await channel.send(f"""
This round of games for {tourney.name} is now complete! The next round will be starting in {int(tourney.round_delay/60)} minutes. This round of games for {tourney.name} is now complete! The next round will be starting in {int(tourney.round_delay/60)} minutes.
Advancing teams: Advancing teams:
{winners_string}""") {winners_string}""")
await asyncio.sleep(tourney.round_delay) await asyncio.sleep(tourney.round_delay)
await start_tournament_round(channel, tourney) await start_tournament_round(channel, tourney)
@ -1361,7 +1625,7 @@ async def game_watcher():
this_array = gamesarray.copy() this_array = gamesarray.copy()
for i in range(0,len(this_array)): for i in range(0,len(this_array)):
game, channel, user, key = this_array[i] game, channel, user, key = this_array[i]
if game.over and main_controller.master_games_dic[key][1]["end_delay"] <= 8: if game.over and ((key in main_controller.master_games_dic.keys() and main_controller.master_games_dic[key][1]["end_delay"] <= 8) or not key in main_controller.master_games_dic.keys()):
final_embed = game_over_embed(game) final_embed = game_over_embed(game)
if isinstance(user, str): if isinstance(user, str):
await channel.send(f"A game started by {user} just ended.") await channel.send(f"A game started by {user} just ended.")
@ -1382,7 +1646,6 @@ def game_over_embed(game):
title_string += f" with {game.inning - (game.max_innings+1)} extra innings.\n" title_string += f" with {game.inning - (game.max_innings+1)} extra innings.\n"
else: else:
title_string += ".\n" title_string += ".\n"
title_string += game.weather.emoji + game.weather.name
winning_team = game.teams['home'].name if game.teams['home'].score > game.teams['away'].score else game.teams['away'].name winning_team = game.teams['home'].name if game.teams['home'].score > game.teams['away'].score else game.teams['away'].name
winstring = f"{game.teams['away'].score} to {game.teams['home'].score}\n" winstring = f"{game.teams['away'].score} to {game.teams['home'].score}\n"
@ -1394,7 +1657,10 @@ def game_over_embed(game):
winstring += f"{winning_team} wins!" winstring += f"{winning_team} wins!"
embed = discord.Embed(color=discord.Color.dark_purple(), title=title_string) embed = discord.Embed(color=discord.Color.dark_purple(), title=title_string)
embed.add_field(name="Final score:", value=winstring) embed.add_field(name="Final score:", value=winstring, inline=False)
embed.add_field(name=f"{game.teams['away'].name} pitcher:", value=game.teams['away'].pitcher.name)
embed.add_field(name=f"{game.teams['home'].name} pitcher:", value=game.teams['home'].pitcher.name)
embed.set_footer(text=game.weather.emoji + game.weather.name)
return embed return embed
def get_team_fuzzy_search(team_name): def get_team_fuzzy_search(team_name):
@ -1405,10 +1671,10 @@ def get_team_fuzzy_search(team_name):
team = teams[0] team = teams[0]
return team return team
async def start_league_day(channel, league, autoplay = 1): async def start_league_day(channel, league, partial = False):
current_games = [] current_games = []
games_to_start = league.schedule[league.day_to_series_num(league.day)] games_to_start = league.schedule[str(league.day_to_series_num(league.day))]
if league.game_length is None: if league.game_length is None:
game_length = games.config()["default_length"] game_length = games.config()["default_length"]
else: else:
@ -1417,15 +1683,19 @@ async def start_league_day(channel, league, autoplay = 1):
for pair in games_to_start: for pair in games_to_start:
if pair[0] is not None and pair[1] is not None: if pair[0] is not None and pair[1] is not None:
away = get_team_fuzzy_search(pair[0]) away = get_team_fuzzy_search(pair[0])
away.set_pitcher(rotation_slot=league.day-1) away.set_pitcher(rotation_slot=league.day)
home = get_team_fuzzy_search(pair[1]) home = get_team_fuzzy_search(pair[1])
home.set_pitcher(rotation_slot=league.day)
this_game = games.game(away.prepare_for_save().finalize(), home.prepare_for_save().finalize(), length = game_length) this_game = games.game(away.finalize(), home.finalize(), length = game_length)
this_game, state_init = prepare_game(this_game) this_game, state_init = prepare_game(this_game)
state_init["is_league"] = True state_init["is_league"] = True
series_string = f"Series score:" if not partial:
state_init["title"] = f"{series_string} 0 - 0" series_string = "Series score:"
state_init["title"] = f"{series_string} 0 - 0"
else:
state_init["title"] = "Interrupted series!"
discrim_string = league.name discrim_string = league.name
id = str(uuid4()) id = str(uuid4())
@ -1435,20 +1705,25 @@ async def start_league_day(channel, league, autoplay = 1):
ext = "?league=" + urllib.parse.quote_plus(league.name) ext = "?league=" + urllib.parse.quote_plus(league.name)
if league.last_series_check(): #if finals if league.last_series_check(): #if finals
await channel.send(f"The final series of the {league.name} is starting now, at {config()['simmadome_url']+ext}") await channel.send(f"The final series of the {league.name} regular season is starting now, at {config()['simmadome_url']+ext}")
last = True last = True
else: else:
await channel.send(f"The next series of the {league.name} is starting now, at {config()['simmadome_url']+ext}") await channel.send(f"The day {league.day} series of the {league.name} is starting now, at {config()['simmadome_url']+ext}")
last = False last = False
await league_day_watcher(channel, league, current_games, config()['simmadome_url']+ext, autoplay, last) if partial:
missed_games = (league.day % league.series_length) - 1
await league_day_watcher(channel, league, current_games, config()['simmadome_url']+ext, last, missed = missed_games)
else:
await league_day_watcher(channel, league, current_games, config()['simmadome_url']+ext, last)
async def league_day_watcher(channel, league, games_list, filter_url, autoplay, last = False): async def league_day_watcher(channel, league, games_list, filter_url, last = False, missed = 0):
league.active = True league.active = True
autoplay -= 1 league.autoplay -= 1
active_leagues.append(league) if league not in active_leagues:
active_leagues.append(league)
series_results = {} series_results = {}
while league.active: while league.active:
@ -1457,7 +1732,7 @@ async def league_day_watcher(channel, league, games_list, filter_url, autoplay,
try: try:
for i in range(0, len(games_list)): for i in range(0, len(games_list)):
game, key = games_list[i] game, key = games_list[i]
if game.over and main_controller.master_games_dic[key][1]["end_delay"] <= 8: if game.over and ((key in main_controller.master_games_dic.keys() and main_controller.master_games_dic[key][1]["end_delay"] <= 8) or not key in main_controller.master_games_dic.keys()):
if game.teams['home'].name not in series_results.keys(): if game.teams['home'].name not in series_results.keys():
series_results[game.teams["home"].name] = {} series_results[game.teams["home"].name] = {}
series_results[game.teams["home"].name]["wins"] = 0 series_results[game.teams["home"].name]["wins"] = 0
@ -1475,21 +1750,29 @@ async def league_day_watcher(channel, league, games_list, filter_url, autoplay,
series_results[winner_name]["wins"] += 1 series_results[winner_name]["wins"] += 1
series_results[winner_name]["run_diff"] += rd series_results[winner_name]["run_diff"] += rd
winner_dic = {"wins" : 1, "run_diff" : rd}
series_results[loser_name]["losses"] += 1 series_results[loser_name]["losses"] += 1
series_results[loser_name]["run_diff"] -= rd series_results[loser_name]["run_diff"] -= rd
league.add_stats_from_game(game.get_team_specific_stats()) loser_dic = {"losses" : 1, "run_diff" : -rd}
league.add_stats_from_game(game.get_team_specific_stats())
league.update_standings({winner_name : winner_dic, loser_name : loser_dic})
leagues.save_league(league)
final_embed = game_over_embed(game) 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(f"A {league.name} game just ended!")
await channel.send(embed=final_embed) await channel.send(embed=final_embed)
if series_results[winner_name]["wins"] + series_results[winner_name]["losses"] < league.series_length: if series_results[winner_name]["wins"] + series_results[winner_name]["losses"] + missed < league.series_length:
queued_games.append(game) queued_games.append(game)
games_list.pop(i) games_list.pop(i)
break break
except: except:
print("something went wrong in league_day_watcher") print("something went wrong in league_day_watcher")
await asyncio.sleep(1) await asyncio.sleep(2)
league.day += 1 league.day += 1
if len(queued_games) > 0: if len(queued_games) > 0:
@ -1499,49 +1782,103 @@ async def league_day_watcher(channel, league, games_list, filter_url, autoplay,
validminutes = [int((60 * div)/league.games_per_hour) for div in range(0,league.games_per_hour)] validminutes = [int((60 * div)/league.games_per_hour) for div in range(0,league.games_per_hour)]
for i in range(0, len(validminutes)): for i in range(0, len(validminutes)):
if now.minute > validminutes[i]: if now.minute > validminutes[i]:
if i < len(validminutes)-1: if i <= len(validminutes)-3:
delta = datetime.timedelta(minutes= (validminutes[i+1] - now.minute)) 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: else:
delta = datetime.timedelta(minutes= (60 - now.minute)) delta = datetime.timedelta(minutes= (60 - now.minute))
next_start = (now + delta).replace(microsecond=0) next_start = (now + delta).replace(second=0, microsecond=0)
wait_seconds = (next_start - now).seconds wait_seconds = (next_start - now).seconds
leagues.save_league(league)
await channel.send(f"The next batch of games for the {league.name} will start in {int(wait_seconds/60)} minutes.") 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.")
leagues.save_league(league)
await asyncio.sleep(wait_seconds) await asyncio.sleep(wait_seconds)
await channel.send(f"A {league.name} series is continuing now at {filter_url}") await channel.send(f"A {league.name} series is continuing now at {filter_url}")
games_list = await continue_league_series(league, queued_games, games_list, series_results) games_list = await continue_league_series(league, queued_games, games_list, series_results, missed)
else: else:
league.active = False league.active = False
league.update_standings(series_results) if last: #if last game of the season
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))
if last or autoplay <= 0: #if this series was the last of the season OR number of series to autoplay has been reached next_start = (now + delta).replace(second=0, microsecond=0)
#needs some kind of notification that it's over here 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 asyncio.sleep(wait_seconds)
await league_postseason(channel, league)
#need to reset league to new season here
return
if league.autoplay == 0 or config()["game_freeze"]: #if number of series to autoplay has been reached
await channel.send(embed=league.standings_embed())
await channel.send(f"The {league.name} is no longer autoplaying.")
if config()["game_freeze"]:
await channel.send("Patch incoming.")
leagues.save_league(league)
active_leagues.pop(active_leagues.index(league)) active_leagues.pop(active_leagues.index(league))
return return
now = datetime.datetime.now() now = datetime.datetime.now()
validminutes = [int((60 * div)/league.games_per_hour) for div in range(0,league.games_per_hour)] validminutes = [int((60 * div)/league.games_per_hour) for div in range(0,league.games_per_hour)]
for i in range(0, len(validminutes)): for i in range(0, len(validminutes)):
if now.minute > validminutes[i]: if now.minute > validminutes[i]:
if i < len(validminutes)-1: if i <= len(validminutes)-3:
delta = datetime.timedelta(minutes= (validminutes[i+1] - now.minute)) 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: else:
delta = datetime.timedelta(minutes= (60 - now.minute)) delta = datetime.timedelta(minutes= (60 - now.minute))
next_start = (now + delta).replace(microsecond=0) next_start = (now + delta).replace(second=0, microsecond=0)
wait_seconds = (next_start - now).seconds wait_seconds = (next_start - now).seconds
leagues.save_league(league)
await channel.send(embed=league.standings_embed())
await channel.send(f"""This {league.name} series is now complete! The next series will be starting in {int(wait_seconds/60)} minutes.""") await channel.send(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 asyncio.sleep(wait_seconds)
await start_league_day(channel, league, autoplay) await start_league_day(channel, league)
async def continue_league_series(league, queue, games_list, series_results): async def continue_league_series(league, queue, games_list, series_results, missed):
for oldgame in queue: for oldgame in queue:
away_team = games.get_team(oldgame.teams["away"].name) away_team = games.get_team(oldgame.teams["away"].name)
away_team.set_pitcher(rotation_slot=league.day) away_team.set_pitcher(rotation_slot=league.day)
@ -1552,7 +1889,12 @@ async def continue_league_series(league, queue, games_list, series_results):
state_init["is_league"] = True state_init["is_league"] = True
series_string = f"Series score:" series_string = f"Series score:"
state_init["title"] = f"{series_string} {series_results[away_team.name]['wins']} - {series_results[home_team.name]['wins']}"
if missed <= 0:
series_string = "Series score:"
state_init["title"] = f"{series_string} {series_results[away_team.name]['wins']} - {series_results[home_team.name]['wins']}"
else:
state_init["title"] = "Interrupted series!"
discrim_string = league.name discrim_string = league.name
id = str(uuid4()) id = str(uuid4())
@ -1561,8 +1903,78 @@ async def continue_league_series(league, queue, games_list, series_results):
return games_list return games_list
async def league_postseason(channel, league):
embed = league.standings_embed()
embed.set_footer(text="Final Standings")
await channel.send(embed=embed)
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 channel.send(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)]
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.champions[str(league.season)] = world_series.winner.name
leagues.save_league(league)