Merge remote-tracking branch 'upstream/master'
6
.dockerignore
Normal file
|
@ -0,0 +1,6 @@
|
|||
venv/
|
||||
matteo_env/
|
||||
__pycache__/
|
||||
simmadome/node_modules
|
||||
data/
|
||||
.git/
|
5
.gitignore
vendored
|
@ -344,11 +344,16 @@ config.json
|
|||
games_config.json
|
||||
weather_config.json
|
||||
ids
|
||||
data/
|
||||
|
||||
# database
|
||||
matteo.db
|
||||
matteo.db-wal
|
||||
matteo.db-shm
|
||||
/data/leagues/*
|
||||
/matteo_env/Lib/site-packages/flask_socketio/__init__.py
|
||||
Pipfile
|
||||
*_bak
|
||||
env
|
||||
/data/leagues
|
||||
/simmadome/build
|
||||
|
|
19
Dockerfile
|
@ -1,9 +1,20 @@
|
|||
FROM python:3.8
|
||||
EXPOSE 5000
|
||||
|
||||
# - Build stage 1: frontend (simmadome/ directory)
|
||||
FROM node:alpine AS frontend
|
||||
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
|
||||
COPY . ./
|
||||
COPY --from=frontend /app/build/ simmadome/build/
|
||||
|
||||
CMD ["python", "the_prestige.py"]
|
||||
|
|
1111
LICENSE.md
13
Makefile
Normal file
|
@ -0,0 +1,13 @@
|
|||
SOURCES = $(wildcard ./simmadome/src/*) $(wildcard ./simmadome/public/*)
|
||||
OUTPUTS = $(wildcard ./simmadome/build/*)
|
||||
|
||||
.PHONY: run frontend
|
||||
|
||||
run: $(OUTPUTS)
|
||||
python3 the_prestige.py
|
||||
|
||||
frontend:
|
||||
(cd simmadome && npm run build)
|
||||
|
||||
$(OUTPUTS): $(SOURCES)
|
||||
(cd simmadome && npm run build)
|
72
README.md
|
@ -14,7 +14,7 @@ This fork changes small things about the bot to better fit in in the FredChat di
|
|||
|
||||
blaseball, blaseball, is back! in an unofficial capacity. this project is completely unaffiliated with the game band.
|
||||
|
||||
we have custom players (generated by onomancer), custom teams, custom leagues (that last one is coming soon™), all set up in discord and watchable at https://simsim.sibr.dev!
|
||||
we have custom players (generated by onomancer), custom teams, custom leagues, all set up in discord and watchable at https://simsim.sibr.dev!
|
||||
|
||||
if you would like to add matteo to your server to be able to set up teams and games, you can do so with this link: https://discord.com/api/oauth2/authorize?client_id=789956166796574740&permissions=388160&scope=bot
|
||||
|
||||
|
@ -23,7 +23,6 @@ accepting pull requests, check the issues for to-dos.
|
|||
|
||||
## commands: (everything here is case sensitive, and can be prefixed with either m; or m!)
|
||||
### team commands:
|
||||
|
||||
#### creation and deletion:
|
||||
- m;saveteam
|
||||
- saves a team to the database allowing it to be used for games. use this command at the top of a list with entries separated by new lines:
|
||||
|
@ -34,30 +33,28 @@ accepting pull requests, check the issues for to-dos.
|
|||
- then another blank line seperating your batters and your pitchers.
|
||||
- the final lines are the names of the pitchers in your rotation, rotations can contain any number of pitchers between 1 and 8.
|
||||
- if you did it correctly, you'll get a team embed with a prompt to confirm. hit the 👍 and your team will be saved!
|
||||
- m;deleteteam [teamname] (requires team ownership)
|
||||
- m;deleteteam [teamname] \(requires team ownership)
|
||||
- allows you to delete the team with the provided name. you'll get an embed with a confirmation to prevent accidental deletions. hit the 👍 and your team will be deleted.
|
||||
- m;import
|
||||
- imports an onomancer collection as a new team. you can use the new onomancer simsim setting to ensure compatibility. similarly to saveteam, you'll get a team embed with a prompt to confirm, hit the 👍 and your team will be saved!
|
||||
|
||||
#### editing (all of these commands require ownership and exact spelling of the team name):
|
||||
- m;addplayer batter/pitcher [team name] [player name]
|
||||
- m;addplayer batter/pitcher [team name] \[player name]
|
||||
- adds a new player to the end of your team, either in the lineup or the rotation depending on which version you use. use addplayer batter or addplayer pitcher at the top of a list with entries separated by new lines:
|
||||
- the name of the team you want to add the player to.
|
||||
- the name of the player you want to add to the team.
|
||||
- m;moveplayer [team name] [player name] [new lineup/rotation position number]
|
||||
- m;moveplayer [team name] \[player name] [new lineup/rotation position number]
|
||||
- moves a player within your lineup or rotation. if you want to instead move a player from your rotation to your lineup or vice versa, use m;swapsection instead. use this command at the top of a list with entries separated by new lines:
|
||||
- the name of the team you want to move the player on.
|
||||
- the name of the player you want to move.
|
||||
- the position you want to move them too, indexed with 1 being the first position of the lineup or rotation. all players below the specified position in the lineup or rotation will be pushed down.
|
||||
- m;swapsection [team name] [player name]
|
||||
- m;swapsection [team name] \[player name]
|
||||
- swaps a player from your lineup to the end of your rotation or your rotation to the end of your lineup. use this command at the top of a list with entries separated by new lines:
|
||||
- the name of the team you want to swap the player on.
|
||||
- the name of the player you want to swap.
|
||||
- m;removeplayer [team name] [player name]
|
||||
- m;removeplayer [team name] \[player name]
|
||||
- removes a player from your team. if there are multiple copies of the same player on a team this will only delete the first one. use this command at the top of a list with entries separated by new lines:
|
||||
- the name of the team you want to remove the player from.
|
||||
- the name of the player you want to remove.
|
||||
|
||||
#### viewing and searching:
|
||||
- m;showteam [name]
|
||||
- shows the lineup, rotation, and slogan of any saved team in a discord embed with primary stat star ratings for all of the players. this command has fuzzy search so you don't need to type the full name of the team as long as you give enough to identify the team you're looking for.
|
||||
|
@ -66,14 +63,6 @@ accepting pull requests, check the issues for to-dos.
|
|||
- m;showallteams
|
||||
- shows a paginated list of all teams available for games which can be scrolled through.
|
||||
|
||||
### player commands:
|
||||
- m;showplayer [name]
|
||||
- displays any name's stars, there's a limit of 70 characters. that should be *plenty*. note: if you want to lookup a lot of different players you can do it on onomancer instead of spamming this command a bunch and clogging up discord: https://onomancer.sibr.dev/reflect
|
||||
- m;idolize [name]
|
||||
- records any name as your idol, mostly for fun.
|
||||
- m;showidol
|
||||
- displays your idol's name and stars in a discord embed.
|
||||
|
||||
### game commands:
|
||||
- m;startgame --day # or -d #
|
||||
- starts a game with premade teams made using saveteam. provides a link to the website where you can watch the game.
|
||||
|
@ -92,6 +81,50 @@ accepting pull requests, check the issues for to-dos.
|
|||
- the name of the tournament.
|
||||
- the name of each participating team on its own line.
|
||||
|
||||
### draft commands
|
||||
- m;startdraft
|
||||
- starts a draft with an arbitrary number of participants. use this command at the top of a list with entries separated by new lines:
|
||||
- for each participant's entry you need three lines:
|
||||
- their discord @
|
||||
- their team name
|
||||
- their team slogan
|
||||
- post this with all three of these things for all participants and the draft will begin.
|
||||
- the draft will begin once all participants have given a 👍 and will proceed in the order that participants were entered. each participant will select 12 hitters and 1 pitcher from a pool of 20 random players which will refresh automatically when it becomes small.
|
||||
- m;draft [name]
|
||||
- use this on your turn during a draft to pick your player.
|
||||
- you can also just use a 'd' instead of the full command.
|
||||
|
||||
### league commands
|
||||
- all of these commands are for leagues that have already been started. to start a league, click the 'create a league' button on the website and fill out the info for your league there, then use the m;claimleague command in discord to set yourself as the owner.
|
||||
- commissioner commands (all of these except for m;claimleague require ownership of the specified league):
|
||||
- m;claimleague [leaguename]
|
||||
- sets yourself as the owner of an unclaimed league created on the website. make sure to do this as soon as possible since if someone does this before you, you will not have access to the league.
|
||||
- m;addleagueowner [leaguename]
|
||||
- use this command at the top of a list of @mentions, with entries separated by new lines, of people you want to have owner powers in your league.
|
||||
- m;startleague [leaguename] --queue #/-q # --noautopostseason
|
||||
- send this command with the number of games per hour you want on the next line, minimum 1 (one game every hour), maximum 12 (one game every 5 minutes, uses spillover rules).
|
||||
- starts the playing of league games at the pace specified, by default will play the entire season and the postseason unless an owner pauses the league with the m;pauseleague command.
|
||||
- if you use the --queue #/-q # flag, the league will only play # series' at a time before automatically pausing until you use this command again.
|
||||
- if you use the --noautopostseason flag, instead of starting automatically, the league will pause at the end of the regular season and not start the postseason until you use this command again.
|
||||
- m;pauseleague [leaguename]
|
||||
- pauses the specified league after the current series finishes until the league is started again with m;startleague.
|
||||
- general commands (all of these can be used by anyone):
|
||||
- m;leaguestandings [leaguename] --season #/-s #
|
||||
- displays the current standings for the specified league.
|
||||
- by default this will display the standings for the current season but if the --season #/-s # flag is set it will instead display the standings for the #th season instead for viewing historical standings.
|
||||
- m;leaguewildcard [leaguename]
|
||||
- displays the wild card standings for the specified league. if the league doesn't have wild cards, it will instead tell you that.
|
||||
- m;leagueschedule [leaguename]
|
||||
- displays the upcoming schedule for the specified league. shows the current series and the next three series after that for every team.
|
||||
|
||||
### player commands:
|
||||
- m;showplayer [name]
|
||||
- displays any name's stars, there's a limit of 70 characters. that should be *plenty*. note: if you want to lookup a lot of different players you can do it on onomancer instead of spamming this command a bunch and clogging up discord: https://onomancer.sibr.dev/reflect
|
||||
- m;idolize [name]
|
||||
- records any name as your idol, mostly for fun.
|
||||
- m;showidol
|
||||
- displays your idol's name and stars in a discord embed.
|
||||
|
||||
### other commands:
|
||||
- m;help [command]
|
||||
- shows instructions for a given command. if no command is provided, it will instead provide a list of all of the commands that instructions can be provided for.
|
||||
|
@ -109,3 +142,8 @@ these folks are helping me a *ton* via patreon, and i cannot possibly thank them
|
|||
- Kameleon
|
||||
- Ryan Littleton
|
||||
- Evie Diver
|
||||
- iliana etaoin
|
||||
|
||||
## Attribution
|
||||
|
||||
Twemoji is copyright 2020 Twitter, Inc and other contributors; code licensed under [the MIT License](http://opensource.org/licenses/MIT), graphics licensed under [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/)
|
||||
|
|
44
games.py
|
@ -58,7 +58,6 @@ def all_weathers():
|
|||
weather_dic[this_weather.name] = this_weather
|
||||
return weather_dic
|
||||
|
||||
|
||||
class appearance_outcomes(Enum):
|
||||
strikeoutlooking = "strikes out looking."
|
||||
strikeoutswinging = "strikes out swinging."
|
||||
|
@ -195,7 +194,7 @@ class team(object):
|
|||
if rotation_slot is None:
|
||||
self.pitcher = random.choice(temp_rotation)
|
||||
else:
|
||||
self.pitcher = temp_rotation[rotation_slot % len(temp_rotation)]
|
||||
self.pitcher = temp_rotation[(rotation_slot-1) % len(temp_rotation)]
|
||||
|
||||
def is_ready(self):
|
||||
try:
|
||||
|
@ -221,7 +220,8 @@ class team(object):
|
|||
|
||||
def finalize(self):
|
||||
if self.is_ready():
|
||||
self.set_pitcher()
|
||||
if self.pitcher == None:
|
||||
self.set_pitcher()
|
||||
while len(self.lineup) <= 4:
|
||||
self.lineup.append(random.choice(self.lineup))
|
||||
return self
|
||||
|
@ -291,6 +291,13 @@ class game(object):
|
|||
hitnum = random.gauss(2*math.erf(bat_stat/4)-1,3)
|
||||
crow = False
|
||||
|
||||
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 self.weather.name == "Crowstorm":
|
||||
randomchance = random.gauss(0,1)
|
||||
|
@ -401,7 +408,7 @@ class game(object):
|
|||
outcome["ishit"] = True
|
||||
if hitnum < 1:
|
||||
outcome["text"] = appearance_outcomes.single
|
||||
elif hitnum < 2.85:
|
||||
elif hitnum < 2.85 or "error" in outcome.keys():
|
||||
outcome["text"] = appearance_outcomes.double
|
||||
elif hitnum < 3.1:
|
||||
outcome["text"] = appearance_outcomes.triple
|
||||
|
@ -454,6 +461,7 @@ class game(object):
|
|||
run_roll = run_roll * .9 #stealing third is harder
|
||||
if run_roll < 1:
|
||||
outcome["steals"].append(f"{baserunner} was caught stealing {base_string(start_base+1)} base by {defender}!")
|
||||
self.get_pitcher().game_stats["outs_pitched"] += 1
|
||||
self.outs += 1
|
||||
else:
|
||||
outcome["steals"].append(f"{baserunner} steals {base_string(start_base+1)} base!")
|
||||
|
@ -473,6 +481,11 @@ class game(object):
|
|||
if base is not None:
|
||||
runs += 1
|
||||
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
|
||||
|
||||
elif "advance" in outcome.keys():
|
||||
|
@ -622,6 +635,10 @@ class game(object):
|
|||
elif result["text"] == appearance_outcomes.homerun or result["text"] == appearance_outcomes.grandslam:
|
||||
self.get_batter().game_stats["total_bases"] += 4
|
||||
self.get_batter().game_stats["home_runs"] += 1
|
||||
if self.weather.name == "Thinned Veil":
|
||||
result["veil"] = True
|
||||
|
||||
|
||||
|
||||
scores_to_add += self.baserunner_check(defender, result)
|
||||
|
||||
|
@ -759,6 +776,10 @@ class game(object):
|
|||
return "Game not started."
|
||||
|
||||
def add_stats(self):
|
||||
players = self.get_stats()
|
||||
db.add_stats(players)
|
||||
|
||||
def get_stats(self):
|
||||
players = []
|
||||
for this_player in self.teams["away"].lineup:
|
||||
players.append((this_player.name, this_player.game_stats))
|
||||
|
@ -766,7 +787,20 @@ class game(object):
|
|||
players.append((this_player.name, this_player.game_stats))
|
||||
players.append((self.teams["home"].pitcher.name, self.teams["home"].pitcher.game_stats))
|
||||
players.append((self.teams["away"].pitcher.name, self.teams["away"].pitcher.game_stats))
|
||||
db.add_stats(players)
|
||||
return players
|
||||
|
||||
def get_team_specific_stats(self):
|
||||
players = {
|
||||
self.teams["away"].name : [],
|
||||
self.teams["home"].name : []
|
||||
}
|
||||
for this_player in self.teams["away"].lineup:
|
||||
players[self.teams["away"].name].append((this_player.name, this_player.game_stats))
|
||||
for this_player in self.teams["home"].lineup:
|
||||
players[self.teams["home"].name].append((this_player.name, this_player.game_stats))
|
||||
players[self.teams["home"].name].append((self.teams["home"].pitcher.name, self.teams["home"].pitcher.game_stats))
|
||||
players[self.teams["away"].name].append((self.teams["away"].pitcher.name, self.teams["away"].pitcher.game_stats))
|
||||
return players
|
||||
|
||||
|
||||
|
||||
|
|
202
league_storage.py
Normal file
|
@ -0,0 +1,202 @@
|
|||
import os, json, re, jsonpickle
|
||||
import sqlite3 as sql
|
||||
|
||||
data_dir = "data"
|
||||
league_dir = "leagues"
|
||||
|
||||
def create_connection(league_name):
|
||||
#create connection, create db if doesn't exist
|
||||
conn = None
|
||||
try:
|
||||
if not os.path.exists(os.path.join(data_dir, league_dir, league_name)):
|
||||
|
||||
os.makedirs(os.path.join(data_dir, league_dir, league_name))
|
||||
conn = sql.connect(os.path.join(data_dir, league_dir, league_name, f"{league_name}.db"))
|
||||
|
||||
# enable write-ahead log for performance and resilience
|
||||
conn.execute('pragma journal_mode=wal')
|
||||
|
||||
return conn
|
||||
except:
|
||||
print("oops, db connection no work")
|
||||
return conn
|
||||
|
||||
def create_season_connection(league_name, season_num):
|
||||
#create connection, create db if doesn't exist
|
||||
conn = None
|
||||
try:
|
||||
if not os.path.exists(os.path.join(data_dir, league_dir, league_name)):
|
||||
|
||||
os.makedirs(os.path.join(data_dir, league_dir, league_name))
|
||||
conn = sql.connect(os.path.join(data_dir, league_dir, league_name, season_num, f"{league_name}.db"))
|
||||
|
||||
# enable write-ahead log for performance and resilience
|
||||
conn.execute('pragma journal_mode=wal')
|
||||
|
||||
return conn
|
||||
except:
|
||||
print("oops, db connection no work")
|
||||
return conn
|
||||
|
||||
def state(league_name):
|
||||
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")) as state_file:
|
||||
return json.load(state_file)
|
||||
|
||||
def init_league_db(league):
|
||||
conn = create_connection(league.name)
|
||||
|
||||
player_stats_table_check_string = """ CREATE TABLE IF NOT EXISTS stats (
|
||||
counter integer PRIMARY KEY,
|
||||
id text,
|
||||
name text,
|
||||
team_name text,
|
||||
outs_pitched integer DEFAULT 0,
|
||||
walks_allowed integer DEFAULT 0,
|
||||
hits_allowed integer DEFAULT 0,
|
||||
strikeouts_given integer DEFAULT 0,
|
||||
runs_allowed integer DEFAULT 0,
|
||||
plate_appearances integer DEFAULT 0,
|
||||
walks_taken integer DEFAULT 0,
|
||||
sacrifices integer DEFAULT 0,
|
||||
hits integer DEFAULT 0,
|
||||
home_runs integer DEFAULT 0,
|
||||
total_bases integer DEFAULT 0,
|
||||
rbis integer DEFAULT 0,
|
||||
strikeouts_taken integer DEFAULT 0
|
||||
);"""
|
||||
|
||||
teams_table_check_string = """ CREATE TABLE IF NOT EXISTS teams (
|
||||
counter integer PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
wins integer DEFAULT 0,
|
||||
losses integer DEFAULT 0,
|
||||
run_diff integer DEFAULT 0
|
||||
); """
|
||||
|
||||
if conn is not None:
|
||||
c = conn.cursor()
|
||||
c.execute(player_stats_table_check_string)
|
||||
c.execute(teams_table_check_string)
|
||||
|
||||
for team in league.teams_in_league():
|
||||
c.execute("INSERT INTO teams (name) VALUES (?)", (team.name,))
|
||||
|
||||
player_string = "INSERT INTO stats (name, team_name) VALUES (?,?)"
|
||||
for batter in team.lineup:
|
||||
c.execute(player_string, (batter.name, team.name))
|
||||
for pitcher in team.rotation:
|
||||
c.execute(player_string, (pitcher.name, team.name))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def save_league(league):
|
||||
if league_exists(league.name):
|
||||
state_dic = {
|
||||
"season" : league.season,
|
||||
"day" : league.day,
|
||||
"constraints" : league.constraints,
|
||||
"schedule" : league.schedule,
|
||||
"game_length" : league.game_length,
|
||||
"series_length" : league.series_length,
|
||||
"games_per_hour" : league.games_per_hour,
|
||||
"owner" : league.owner,
|
||||
"champion" : league.champion,
|
||||
"historic" : league.historic
|
||||
}
|
||||
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)
|
||||
|
||||
def add_stats(league_name, player_game_stats_list):
|
||||
conn = create_connection(league_name)
|
||||
if conn is not None:
|
||||
c=conn.cursor()
|
||||
for team_name in player_game_stats_list.keys():
|
||||
for (name, player_stats_dic) in player_game_stats_list[team_name]:
|
||||
c.execute("SELECT * FROM stats WHERE name=? AND team_name=?",(name, team_name))
|
||||
this_player = c.fetchone()
|
||||
if this_player is not None:
|
||||
for stat in player_stats_dic.keys():
|
||||
c.execute(f"SELECT {stat} FROM stats WHERE name=? AND team_name=?",(name, team_name))
|
||||
old_value = int(c.fetchone()[0])
|
||||
c.execute(f"UPDATE stats SET {stat} = ? WHERE name=? AND team_name=?",(player_stats_dic[stat]+old_value, name, team_name))
|
||||
else:
|
||||
c.execute("INSERT INTO stats(name) VALUES (?)",(name,))
|
||||
for stat in player_stats_dic.keys():
|
||||
c.execute(f"UPDATE stats SET {stat} = ? WHERE name=? AND team_name=?",(player_stats_dic[stat], name, team_name))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def update_standings(league_name, update_dic):
|
||||
if league_exists(league_name):
|
||||
conn = create_connection(league_name)
|
||||
if conn is not None:
|
||||
c = conn.cursor()
|
||||
|
||||
for team_name in update_dic.keys():
|
||||
for stat_type in update_dic[team_name].keys(): #wins, losses, run_diff
|
||||
c.execute(f"SELECT {stat_type} FROM teams WHERE name = ?", (team_name,))
|
||||
old_value = int(c.fetchone()[0])
|
||||
c.execute(f"UPDATE teams SET {stat_type} = ? WHERE name = ?", (update_dic[team_name][stat_type]+old_value, team_name))
|
||||
conn.commit()
|
||||
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 season_save(league):
|
||||
if league_exists(league.name):
|
||||
seasons = 1
|
||||
with os.scandir(os.path.join(data_dir, league_dir, league.name)) as folder:
|
||||
for item in folder:
|
||||
if "." not in item.name:
|
||||
seasons += 1
|
||||
new_dir = os.path.join(data_dir, league_dir, league.name, str(seasons))
|
||||
os.makedirs(new_dir)
|
||||
with os.scandir(os.path.join(data_dir, league_dir, league.name)) as folder:
|
||||
for item in folder:
|
||||
if "." in item.name:
|
||||
os.rename(os.path.join(data_dir, league_dir, league.name, item.name), os.path.join(new_dir, item.name))
|
||||
|
||||
def get_past_standings(league_name, season_num):
|
||||
if league_exists(league_name):
|
||||
with os.scandir(os.path.join(data_dir, league_dir, league_name)) as folder:
|
||||
for item in folder:
|
||||
if item.name == str(season_num):
|
||||
conn = create_season_connection(league_name, str(item.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 get_past_champion(league_name, season_num):
|
||||
if league_exists(league_name):
|
||||
with os.scandir(os.path.join(data_dir, league_dir, league_name)) as folder:
|
||||
for item in folder:
|
||||
if item.name == str(season_num):
|
||||
with open(os.path.join(data_dir, league_dir, league_name, item.name, f"{league_name}.state")) as state_file:
|
||||
state_dic = json.load(state_file)
|
||||
return state_dic["champion"]
|
||||
|
||||
def league_exists(league_name):
|
||||
with os.scandir(os.path.join(data_dir, league_dir)) as folder:
|
||||
for subfolder in folder:
|
||||
if league_name == subfolder.name:
|
||||
with os.scandir(subfolder.path) as league_folder:
|
||||
for item in league_folder:
|
||||
if item.name == f"{league_name}.db":
|
||||
return True
|
||||
return False
|
423
leagues.py
|
@ -1,28 +1,377 @@
|
|||
import time, asyncio, jsonpickle, random, math
|
||||
import time, asyncio, json, jsonpickle, random, math, os
|
||||
import league_storage as league_db
|
||||
from itertools import chain
|
||||
from copy import deepcopy
|
||||
from games import team, game
|
||||
from discord import Embed, Color
|
||||
import database as db
|
||||
|
||||
data_dir = "data"
|
||||
league_dir = "leagues"
|
||||
|
||||
|
||||
|
||||
|
||||
class league(object):
|
||||
def __init__(self, name, subleagues_dic):
|
||||
self.subleagues = {} #key: name, value: [divisions]
|
||||
self.max_days
|
||||
self.day = 1
|
||||
class league_structure(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.subleagues = subleagues_dic
|
||||
self.historic = False
|
||||
self.owner = None
|
||||
self.season = 1
|
||||
self.autoplay = -1
|
||||
self.champion = None
|
||||
|
||||
def setup(self, league_dic, division_games = 1, inter_division_games = 1, inter_league_games = 1, games_per_hour = 2):
|
||||
self.league = league_dic # { subleague name : { division name : [team object] } }
|
||||
self.constraints = {
|
||||
"division_games" : division_games,
|
||||
"inter_div_games" : inter_division_games,
|
||||
"inter_league_games" : inter_league_games,
|
||||
"division_leaders" : 0,
|
||||
"wild_cards" : 0
|
||||
}
|
||||
self.day = 1
|
||||
self.schedule = {}
|
||||
self.series_length = 3 #can be changed
|
||||
self.game_length = None
|
||||
self.active = False
|
||||
self.games_per_hour = games_per_hour
|
||||
|
||||
def season_reset(self):
|
||||
self.season += 1
|
||||
self.day = 1
|
||||
self.champion = None
|
||||
self.schedule = {}
|
||||
self.generate_schedule()
|
||||
save_league(self)
|
||||
|
||||
def add_stats_from_game(self, players_dic):
|
||||
league_db.add_stats(self.name, players_dic)
|
||||
|
||||
def update_standings(self, results_dic):
|
||||
league_db.update_standings(self.name, results_dic)
|
||||
|
||||
|
||||
def last_series_check(self):
|
||||
return str(math.ceil((self.day)/self.series_length) + 1) not in self.schedule.keys()
|
||||
|
||||
def day_to_series_num(self, day):
|
||||
return math.ceil((self.day)/self.series_length)
|
||||
|
||||
def tiebreaker_required(self):
|
||||
standings = {}
|
||||
matchups = []
|
||||
tournaments = []
|
||||
for team_name, wins, losses, run_diff in league_db.get_standings(self.name):
|
||||
standings[team_name] = {"wins" : wins, "losses" : losses, "run_diff" : run_diff}
|
||||
|
||||
for subleague in iter(self.league.keys()):
|
||||
team_dic = {}
|
||||
subleague_array = []
|
||||
wildcard_leaders = []
|
||||
for division in iter(self.league[subleague].keys()):
|
||||
division_standings = []
|
||||
division_standings += self.division_standings(self.league[subleague][division], standings)
|
||||
division_leaders = division_standings[:self.constraints["division_leaders"]]
|
||||
for division_team, wins, losses, diff, gb in division_standings[self.constraints["division_leaders"]:]:
|
||||
if division_team.name != division_leaders[-1][0].name and standings[division_team.name]["wins"] == standings[division_leaders[-1][0].name]["wins"]:
|
||||
matchups.append((division_team, division_standings[self.constraints["division_leaders"]-1][0], f"{division} Tiebreaker"))
|
||||
|
||||
this_div_wildcard = [this_team for this_team, wins, losses, diff, gb in self.division_standings(self.league[subleague][division], standings)[self.constraints["division_leaders"]:]]
|
||||
subleague_array += this_div_wildcard
|
||||
if self.constraints["wild_cards"] > 0:
|
||||
wildcard_standings = self.division_standings(subleague_array, standings)
|
||||
wildcard_leaders = wildcard_standings[:self.constraints["wild_cards"]]
|
||||
for wildcard_team, wins, losses, diff, gb in wildcard_standings[self.constraints["wild_cards"]:]:
|
||||
if wildcard_team.name != wildcard_leaders[-1][0].name and standings[wildcard_team.name]["wins"] == standings[wildcard_leaders[-1][0].name]["wins"]:
|
||||
matchups.append((wildcard_team, wildcard_standings[self.constraints["wild_cards"]-1][0], f"{subleague} Wildcard Tiebreaker"))
|
||||
|
||||
for team_a, team_b, type in matchups:
|
||||
tourney = tournament(f"{self.name} {type}",{team_a : {"wins" : 1}, team_b : {"wins" : 0}}, finals_series_length=1, secs_between_games=int(3600/self.games_per_hour), secs_between_rounds=int(7200/self.games_per_hour))
|
||||
tourney.build_bracket(by_wins = True)
|
||||
tourney.league = self
|
||||
tournaments.append(tourney)
|
||||
return tournaments
|
||||
|
||||
def find_team(self, team_name):
|
||||
for subleague in iter(self.league.keys()):
|
||||
for division in iter(self.league[subleague].keys()):
|
||||
if team_name in self.league[subleague][division]:
|
||||
return (subleague, division)
|
||||
|
||||
def teams_in_league(self):
|
||||
teams = []
|
||||
for division in self.league.values():
|
||||
for teams_list in division.values():
|
||||
teams += teams_list
|
||||
return teams
|
||||
|
||||
def teams_in_subleague(self, subleague_name):
|
||||
teams = []
|
||||
if subleague_name in self.league.keys():
|
||||
for division_list in self.league[subleague_name].values():
|
||||
teams += division_list
|
||||
return teams
|
||||
else:
|
||||
print("League not found.")
|
||||
return None
|
||||
|
||||
def teams_in_division(self, subleague_name, division_name):
|
||||
if subleague_name in self.league.keys() and division_name in self.league[subleague_name].keys():
|
||||
return self.league[subleague_name][division_name]
|
||||
else:
|
||||
print("Division in that league not found.")
|
||||
return None
|
||||
|
||||
def make_matchups(self):
|
||||
matchups = []
|
||||
batch_subleagues = [] #each sub-array is all teams in each subleague
|
||||
subleague_max = 1
|
||||
league = deepcopy(self.league)
|
||||
for subleague in league.keys():
|
||||
teams = deepcopy(self.teams_in_subleague(subleague))
|
||||
if subleague_max < len(teams):
|
||||
subleague_max = len(teams)
|
||||
batch_subleagues.append(teams)
|
||||
|
||||
for subleague in batch_subleagues:
|
||||
while len(subleague) < subleague_max:
|
||||
subleague.append("OFF")
|
||||
|
||||
for i in range(0, self.constraints["inter_league_games"]): #generates inter-league matchups
|
||||
unmatched_indices = [i for i in range(0, len(batch_subleagues))]
|
||||
for subleague_index in range(0, len(batch_subleagues)):
|
||||
if subleague_index in unmatched_indices:
|
||||
unmatched_indices.pop(unmatched_indices.index(subleague_index))
|
||||
match_with_index = random.choice(unmatched_indices)
|
||||
unmatched_indices.pop(unmatched_indices.index(match_with_index))
|
||||
league_a = batch_subleagues[subleague_index].copy()
|
||||
league_b = batch_subleagues[match_with_index].copy()
|
||||
random.shuffle(league_a)
|
||||
random.shuffle(league_b)
|
||||
a_home = True
|
||||
for team_a, team_b in zip(league_a, league_b):
|
||||
if a_home:
|
||||
matchups.append([team_b.name, team_a.name])
|
||||
else:
|
||||
matchups.append([team_a.name, team_b.name])
|
||||
a_home = not a_home
|
||||
|
||||
for i in range(0, self.constraints["inter_div_games"]): #inter-division matchups
|
||||
for subleague in league.keys():
|
||||
division_max = 1
|
||||
divisions = []
|
||||
for div in league[subleague].keys():
|
||||
if division_max < len(league[subleague][div]):
|
||||
divison_max = len(league[subleague][div])
|
||||
divisions.append(deepcopy(league[subleague][div]))
|
||||
|
||||
last_div = None
|
||||
if len(divisions) % 2 != 0:
|
||||
if division_max % 2 != 0:
|
||||
divisions.append(["OFF" for i in range(0, division_max)])
|
||||
else:
|
||||
last_div = divisions.pop
|
||||
|
||||
divs_a = list(chain(divisions[int(len(divisions)/2):]))[0]
|
||||
if last_div is not None:
|
||||
divs_a.extend(last_div[int(len(last_div)/2):])
|
||||
random.shuffle(divs_a)
|
||||
|
||||
divs_b = list(chain(divisions[:int(len(divisions)/2)]))[0]
|
||||
if last_div is not None:
|
||||
divs_a.extend(last_div[:int(len(last_div)/2)])
|
||||
random.shuffle(divs_b)
|
||||
|
||||
a_home = True
|
||||
for team_a, team_b in zip(divs_a, divs_b):
|
||||
if a_home:
|
||||
matchups.append([team_b.name, team_a.name])
|
||||
else:
|
||||
matchups.append([team_a.name, team_b.name])
|
||||
a_home = not a_home
|
||||
|
||||
|
||||
for subleague in league.keys():
|
||||
for division in league[subleague].values(): #generate round-robin matchups
|
||||
if len(division) % 2 != 0:
|
||||
division.append("OFF")
|
||||
|
||||
for i in range(0, len(division)-1):
|
||||
teams_a = division[int(len(division)/2):]
|
||||
teams_b = division[:int(len(division)/2)]
|
||||
teams_b.reverse()
|
||||
|
||||
for team_a, team_b in zip(teams_a, teams_b):
|
||||
if team_a != "OFF" and team_b != "OFF":
|
||||
for j in range(0, self.constraints["division_games"]):
|
||||
if i % 2 == 0:
|
||||
matchups.append([team_b.name, team_a.name])
|
||||
else:
|
||||
matchups.append([team_a.name, team_b.name])
|
||||
|
||||
division.insert(1, division.pop())
|
||||
return matchups
|
||||
|
||||
def generate_schedule(self):
|
||||
matchups = self.make_matchups()
|
||||
random.shuffle(matchups)
|
||||
for game in matchups:
|
||||
scheduled = False
|
||||
day = 1
|
||||
while not scheduled:
|
||||
found = False
|
||||
if str(day) in self.schedule.keys():
|
||||
for game_on_day in self.schedule[str(day)]:
|
||||
for team in game:
|
||||
if team in game_on_day:
|
||||
found = True
|
||||
if not found:
|
||||
self.schedule[str(day)].append(game)
|
||||
scheduled = True
|
||||
else:
|
||||
self.schedule[str(day)] = [game]
|
||||
scheduled = True
|
||||
day += 1
|
||||
|
||||
def division_standings(self, division, standings):
|
||||
def sorter(team_in_list):
|
||||
if team_in_list[2] == 0 and team_in_list[1] == 0:
|
||||
return (0, team_in_list[3])
|
||||
return (team_in_list[1]/(team_in_list[1]+team_in_list[2]), team_in_list[3])
|
||||
|
||||
teams = division.copy()
|
||||
|
||||
for index in range(0, len(teams)):
|
||||
this_team = teams[index]
|
||||
teams[index] = [this_team, standings[teams[index].name]["wins"], standings[teams[index].name]["losses"], standings[teams[index].name]["run_diff"], 0]
|
||||
|
||||
teams.sort(key=sorter, reverse=True)
|
||||
return teams
|
||||
|
||||
def past_standings(self, season_num):
|
||||
this_embed = Embed(color=Color.purple(), title=self.name)
|
||||
standings = {}
|
||||
for team_name, wins, losses, run_diff in league_db.get_past_standings(self.name, season_num):
|
||||
standings[team_name] = {"wins" : wins, "losses" : losses, "run_diff" : run_diff}
|
||||
|
||||
this_embed.add_field(name=league_db.get_past_champion(self.name, season_num), value=f"Season {season_num} champions", inline = False)
|
||||
|
||||
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"Season {season_num} Final Standings")
|
||||
return this_embed
|
||||
|
||||
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=f"{self.name} Season {self.season}")
|
||||
standings = {}
|
||||
for team_name, wins, losses, run_diff in league_db.get_standings(self.name):
|
||||
standings[team_name] = {"wins" : wins, "losses" : losses, "run_diff" : run_diff}
|
||||
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 division(object):
|
||||
def __init__(self):
|
||||
self.teams = {} #key: team object, value: {wins; rd (run diff)}
|
||||
|
||||
class tournament(object):
|
||||
def __init__(self, name, team_dic, series_length = 5, finals_series_length = 7, max_innings = 9, id = None, secs_between_games = 300, secs_between_rounds = 600):
|
||||
self.name = name
|
||||
self.teams = team_dic #same format as division, wins/losses will be used for seeding later
|
||||
self.teams = team_dic #key: team object, value: wins
|
||||
self.bracket = None
|
||||
self.results = None
|
||||
self.series_length = series_length
|
||||
|
@ -33,6 +382,9 @@ class tournament(object):
|
|||
self.round_delay = secs_between_rounds
|
||||
self.finals = False
|
||||
self.id = id
|
||||
self.league = None
|
||||
self.winner = None
|
||||
self.day = None
|
||||
|
||||
if id is None:
|
||||
self.id = random.randint(1111,9999)
|
||||
|
@ -40,7 +392,7 @@ class tournament(object):
|
|||
self.id = id
|
||||
|
||||
|
||||
def build_bracket(self, random_sort = False, by_wins = False):
|
||||
def build_bracket(self, random_sort = False, by_wins = False, manual = False):
|
||||
teams_list = list(self.teams.keys()).copy()
|
||||
|
||||
if random_sort:
|
||||
|
@ -54,8 +406,9 @@ class tournament(object):
|
|||
else: #sort by average stars
|
||||
def sorter(team_in_list):
|
||||
return team_in_list.average_stars()
|
||||
|
||||
teams_list.sort(key=sorter, reverse=True)
|
||||
|
||||
if not manual:
|
||||
teams_list.sort(key=sorter, reverse=True)
|
||||
|
||||
|
||||
bracket_layers = int(math.ceil(math.log(len(teams_list), 2)))
|
||||
|
@ -121,4 +474,36 @@ class bracket(object):
|
|||
|
||||
if parent is None:
|
||||
self.this_bracket = branch
|
||||
return branch
|
||||
return branch
|
||||
|
||||
def save_league(this_league):
|
||||
if not league_db.league_exists(this_league.name):
|
||||
league_db.init_league_db(this_league)
|
||||
with open(os.path.join(data_dir, league_dir, this_league.name, f"{this_league.name}.league"), "w") as league_file:
|
||||
league_json_string = jsonpickle.encode(this_league.league, keys=True)
|
||||
json.dump(league_json_string, league_file, indent=4)
|
||||
league_db.save_league(this_league)
|
||||
|
||||
def load_league_file(league_name):
|
||||
if league_db.league_exists(league_name):
|
||||
state = league_db.state(league_name)
|
||||
this_league = league_structure(league_name)
|
||||
with open(os.path.join(data_dir, league_dir, league_name, f"{this_league.name}.league")) as league_file:
|
||||
this_league.league = jsonpickle.decode(json.load(league_file), keys=True, classes=team)
|
||||
with open(os.path.join(data_dir, league_dir, league_name, f"{this_league.name}.state")) as state_file:
|
||||
state_dic = json.load(state_file)
|
||||
|
||||
this_league.day = state_dic["day"]
|
||||
this_league.schedule = state_dic["schedule"]
|
||||
this_league.constraints = state_dic["constraints"]
|
||||
this_league.game_length = state_dic["game_length"]
|
||||
this_league.series_length = state_dic["series_length"]
|
||||
this_league.owner = state_dic["owner"]
|
||||
this_league.games_per_hour = state_dic["games_per_hour"]
|
||||
this_league.historic = state_dic["historic"]
|
||||
this_league.season = state_dic["season"]
|
||||
try:
|
||||
this_league.champion = state_dic["champion"]
|
||||
except:
|
||||
this_league.champion = None
|
||||
return this_league
|
|
@ -1,37 +1,127 @@
|
|||
import asyncio, time, datetime, games, json, threading, jinja2, leagues
|
||||
from flask import Flask, url_for, Response, render_template, request, jsonify
|
||||
import asyncio, time, datetime, games, json, threading, jinja2, leagues, os, leagues
|
||||
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
|
||||
import database as db
|
||||
|
||||
app = Flask("the-prestige")
|
||||
app = Flask("the-prestige", static_folder='simmadome/build')
|
||||
app.config['SECRET KEY'] = 'dev'
|
||||
#app.config['SERVER_NAME'] = '0.0.0.0:5000'
|
||||
socketio = SocketIO(app)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
if ('league' in request.args):
|
||||
return render_template("index.html", league=request.args['league'])
|
||||
return render_template("index.html")
|
||||
# Serve React App
|
||||
@app.route('/', defaults={'path': ''})
|
||||
@app.route('/<path:path>')
|
||||
def serve(path):
|
||||
if path != "" and os.path.exists(app.static_folder + '/' + path):
|
||||
return send_from_directory(app.static_folder, path)
|
||||
else:
|
||||
return send_from_directory(app.static_folder, 'index.html')
|
||||
|
||||
@app.route('/game')
|
||||
def game_page():
|
||||
return render_template("game.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
|
||||
|
||||
MAX_SUBLEAGUE_DIVISION_TOTAL = 22;
|
||||
MAX_TEAMS_PER_DIVISION = 12;
|
||||
|
||||
@app.route('/api/leagues', methods=['POST'])
|
||||
def create_league():
|
||||
config = json.loads(request.data)
|
||||
|
||||
if league_exists(config['name']):
|
||||
return jsonify({'status':'err_league_exists'}), 400
|
||||
|
||||
num_subleagues = len(config['structure']['subleagues'])
|
||||
if num_subleagues < 1 or num_subleagues % 2 != 0:
|
||||
return jsonify({'status':'err_invalid_subleague_count'}), 400
|
||||
|
||||
num_divisions = len(config['structure']['subleagues'][0]['divisions'])
|
||||
if num_subleagues * (num_divisions + 1) > MAX_SUBLEAGUE_DIVISION_TOTAL:
|
||||
return jsonify({'status':'err_invalid_subleague_division_total'}), 400
|
||||
|
||||
league_dic = {}
|
||||
err_teams = []
|
||||
for subleague in config['structure']['subleagues']:
|
||||
if subleague['name'] in league_dic:
|
||||
return jsonify({'status':'err_duplicate_name', 'cause':subleague['name']})
|
||||
|
||||
subleague_dic = {}
|
||||
for division in subleague['divisions']:
|
||||
if division['name'] in subleague_dic:
|
||||
return jsonify({'status':'err_duplicate_name', 'cause':f"{subleague['name']}/{division['name']}"}), 400
|
||||
elif len(division['teams']) > MAX_TEAMS_PER_DIVISION:
|
||||
return jsonify({'status':'err_too_many_teams', 'cause':f"{subleague['name']}/{division['name']}"})
|
||||
|
||||
teams = []
|
||||
for team_name in division['teams']:
|
||||
team = games.get_team(team_name)
|
||||
if team is None:
|
||||
err_teams.append(team_name)
|
||||
else:
|
||||
teams.append(team)
|
||||
subleague_dic[division['name']] = teams
|
||||
league_dic[subleague['name']] = subleague_dic
|
||||
|
||||
if len(err_teams) > 0:
|
||||
return jsonify({'status':'err_no_such_team', 'cause': err_teams}), 400
|
||||
|
||||
for (key, min_val) in [
|
||||
('division_series', 1),
|
||||
('inter_division_series', 1),
|
||||
('inter_league_series', 1)
|
||||
]:
|
||||
if config[key] < min_val:
|
||||
return jsonify({'status':'err_invalid_optiion_value', 'cause':key}), 400
|
||||
|
||||
new_league = league_structure(config['name'])
|
||||
new_league.setup(
|
||||
league_dic,
|
||||
division_games=config['division_series'], # need to add a check that makes sure these values are ok
|
||||
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 jsonify({'status':'success_league_created'})
|
||||
|
||||
|
||||
|
||||
### SOCKETS
|
||||
|
||||
thread2 = threading.Thread(target=socketio.run,args=(app,'0.0.0.0'))
|
||||
thread2.start()
|
||||
|
||||
master_games_dic = {} #key timestamp : (game game, {} state)
|
||||
data_to_send = []
|
||||
game_states = []
|
||||
|
||||
@socketio.on("recieved")
|
||||
def handle_new_conn(data):
|
||||
socketio.emit("states_update", data_to_send, room=request.sid)
|
||||
socketio.emit("states_update", game_states, room=request.sid)
|
||||
|
||||
def update_loop():
|
||||
global game_states
|
||||
while True:
|
||||
game_states = {}
|
||||
game_states = []
|
||||
game_ids = iter(master_games_dic.copy().keys())
|
||||
for game_id in game_ids:
|
||||
this_game, state, discrim_string = master_games_dic[game_id]
|
||||
|
@ -111,6 +201,8 @@ def update_loop():
|
|||
if this_game.last_update[0]["defender"] != "":
|
||||
punc = ". "
|
||||
|
||||
|
||||
|
||||
if "fc_out" in this_game.last_update[0].keys():
|
||||
name, base_string = this_game.last_update[0]['fc_out']
|
||||
updatestring = f"{this_game.last_update[0]['batter']} {this_game.last_update[0]['text'].value.format(name, base_string)} {this_game.last_update[0]['defender']}{punc}"
|
||||
|
@ -122,11 +214,18 @@ def update_loop():
|
|||
state["update_emoji"] = "🏏"
|
||||
state["update_text"] = updatestring
|
||||
|
||||
if "veil" in this_game.last_update[0].keys():
|
||||
state["update_emoji"] = "🌌"
|
||||
state["update_text"] += f" {this_game.last_update[0]['batter']}'s will manifests on {games.base_string(this_game.last_update[1])} base."
|
||||
elif "error" in this_game.last_update[0].keys():
|
||||
state["update_emoji"] = "👻"
|
||||
state["update_text"] = f"{this_game.last_update[0]['batter']}'s hit goes ethereal, and {this_game.last_update[0]['defender']} can't catch it! {this_game.last_update[0]['batter']} reaches base safely."
|
||||
|
||||
state["bases"] = this_game.named_bases()
|
||||
|
||||
state["top_of_inning"] = this_game.top_of_inning
|
||||
|
||||
game_states[game_id] = state
|
||||
game_states.append([game_id, state])
|
||||
|
||||
if state["update_pause"] <= 1 and state["start_delay"] < 0:
|
||||
if this_game.over:
|
||||
|
@ -142,17 +241,5 @@ def update_loop():
|
|||
|
||||
state["update_pause"] -= 1
|
||||
|
||||
global data_to_send
|
||||
data_to_send = []
|
||||
template = jinja2.Environment(loader=jinja2.FileSystemLoader('templates')).get_template('game_box.html')
|
||||
|
||||
for id in game_states:
|
||||
data_to_send.append({
|
||||
'timestamp' : id,
|
||||
'league' : game_states[id]['leagueoruser'] if game_states[id]['is_league'] else '',
|
||||
'state' : game_states[id],
|
||||
'html' : template.render(state=game_states[id], timestamp=id)
|
||||
})
|
||||
|
||||
socketio.emit("states_update", data_to_send)
|
||||
socketio.emit("states_update", game_states)
|
||||
time.sleep(8)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import requests, json, urllib
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.packages.urllib3.util.retry import Retry
|
||||
from urllib3.util.retry import Retry
|
||||
import database as db
|
||||
|
||||
|
||||
|
|
1
simmadome/.eslintcache
Normal file
|
@ -0,0 +1 @@
|
|||
[{"/Users/elijah/Documents/Projects/matteo/simmadome/src/index.tsx":"1","/Users/elijah/Documents/Projects/matteo/simmadome/src/reportWebVitals.ts":"2","/Users/elijah/Documents/Projects/matteo/simmadome/src/GamesPage.tsx":"3","/Users/elijah/Documents/Projects/matteo/simmadome/src/GamePage.tsx":"4","/Users/elijah/Documents/Projects/matteo/simmadome/src/CreateLeague.tsx":"5","/Users/elijah/Documents/Projects/matteo/simmadome/src/GamesUtil.tsx":"6","/Users/elijah/Documents/Projects/matteo/simmadome/src/util.tsx":"7","/Users/elijah/Documents/Projects/matteo/simmadome/src/Game.tsx":"8"},{"size":2368,"mtime":1610663769654,"results":"9","hashOfConfig":"10"},{"size":425,"mtime":1610566206674,"results":"11","hashOfConfig":"10"},{"size":4725,"mtime":1610664926203,"results":"12","hashOfConfig":"10"},{"size":1836,"mtime":1610677519051,"results":"13","hashOfConfig":"10"},{"size":18825,"mtime":1610778204901,"results":"14","hashOfConfig":"10"},{"size":1116,"mtime":1610677473305,"results":"15","hashOfConfig":"10"},{"size":961,"mtime":1610694553519,"results":"16","hashOfConfig":"10"},{"size":3089,"mtime":1610572714752,"results":"17","hashOfConfig":"10"},{"filePath":"18","messages":"19","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1bvn6qu",{"filePath":"20","messages":"21","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"22","messages":"23","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"24","messages":"25","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"26","messages":"27","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"28","messages":"29","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"30","messages":"31","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"32","messages":"33","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/elijah/Documents/Projects/matteo/simmadome/src/index.tsx",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/reportWebVitals.ts",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/GamesPage.tsx",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/GamePage.tsx",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/CreateLeague.tsx",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/GamesUtil.tsx",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/util.tsx",[],"/Users/elijah/Documents/Projects/matteo/simmadome/src/Game.tsx",[]]
|
24
simmadome/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.eslintcache
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
46
simmadome/README.md
Normal file
|
@ -0,0 +1,46 @@
|
|||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
16792
simmadome/package-lock.json
generated
Normal file
56
simmadome/package.json
Normal file
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"name": "simmadome",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"proxy": "http://localhost:5000",
|
||||
"dependencies": {
|
||||
"jquery": "^3.5.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.1",
|
||||
"socket.io-client": "^3.0.5",
|
||||
"twemoji": "^13.0.1",
|
||||
"typescript": "^4.1.3",
|
||||
"web-vitals": "^0.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.8",
|
||||
"@testing-library/react": "^11.2.2",
|
||||
"@testing-library/user-event": "^12.6.0",
|
||||
"@types/jest": "^26.0.19",
|
||||
"@types/jquery": "^3.5.5",
|
||||
"@types/node": "^12.19.12",
|
||||
"@types/react": "^16.14.2",
|
||||
"@types/react-dom": "^16.9.10",
|
||||
"@types/react-router": "^5.1.10",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/socket.io-client": "^1.4.34",
|
||||
"@types/twemoji": "^12.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
30
simmadome/public/index.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<title>⚾ The Simmadome</title>
|
||||
|
||||
<meta property="og:title" content="Watch at the Simmadome" />
|
||||
<meta property="og:description" content="The Simsim: Your players, your teams, your games." />
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:site" content="@SIBR_XVI">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
3
simmadome/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
325
simmadome/src/CreateLeague.css
Normal file
|
@ -0,0 +1,325 @@
|
|||
.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.25rem;
|
||||
}
|
||||
|
||||
.cl_structure_err {
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
|
||||
.cl_structure_err_div {
|
||||
margin-top: -0.25rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.cl_structure_err_team {
|
||||
margin-top: -0.5rem;
|
||||
margin-bottom: 0;
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.cl_team_name_err {
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
.cl_structure_err_teams {
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
.cl_subleague_add_filler, .cl_division_add_filler {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
/* 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, .cl_subleague_add_filler {
|
||||
position: relative;
|
||||
top: 1.5rem;
|
||||
}
|
||||
|
||||
.cl_division_add, .cl_division_add_filler {
|
||||
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;
|
||||
}
|
527
simmadome/src/CreateLeague.tsx
Normal file
|
@ -0,0 +1,527 @@
|
|||
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';
|
||||
|
||||
// CONSTS
|
||||
|
||||
const MAX_SUBLEAGUE_DIVISION_TOTAL = 22;
|
||||
const MAX_TEAMS_PER_DIVISION = 12;
|
||||
|
||||
// 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 [deletedTeams, setDeletedTeams] = useState<string[]>([]);
|
||||
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} deletedTeams={deletedTeams} 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) {
|
||||
let err = JSON.parse(req.response);
|
||||
switch (err.status) {
|
||||
case 'err_league_exists':
|
||||
setNameExists(true);
|
||||
break;
|
||||
case 'err_no_such_team':
|
||||
setDeletedTeams(err.cause);
|
||||
break;
|
||||
}
|
||||
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, si) =>
|
||||
subleague.name !== "" &&
|
||||
!structure.subleagues.slice(0, si).some(val => val.name === subleague.name) &&
|
||||
subleague.divisions.every((division, di) =>
|
||||
division.name !== "" &&
|
||||
division.teams.length >= 2 &&
|
||||
division.teams.length <= MAX_TEAMS_PER_DIVISION &&
|
||||
!subleague.divisions.slice(0, di).some(val => val.name === division.name)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function validNumber(value: string, min = 1) {
|
||||
return !isNaN(Number(value)) && Number(value) >= min;
|
||||
}
|
||||
|
||||
// LEAGUE STRUCUTRE
|
||||
|
||||
function LeagueStructre(props: {state: LeagueStructureState, dispatch: React.Dispatch<StructureReducerActions>, deletedTeams: string[], showError: boolean}) {
|
||||
let nSubleagues = props.state.subleagues.length;
|
||||
let nDivisions = props.state.subleagues[0].divisions.length;
|
||||
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} deletedTeams={props.deletedTeams} showError={props.showError}/>
|
||||
</div>
|
||||
{ (nSubleagues+1) * (nDivisions+1) < MAX_SUBLEAGUE_DIVISION_TOTAL ?
|
||||
<button className="cl_subleague_add" onClick={e => props.dispatch({type: 'add_subleague'})}>➕</button> :
|
||||
<div className="cl_subleague_add_filler"/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="cl_structure_err">{props.state.subleagues.length % 2 !== 0 && props.showError ? "Must have an even number of subleagues." : ""}</div>
|
||||
{ nSubleagues * (nDivisions+2) < MAX_SUBLEAGUE_DIVISION_TOTAL ?
|
||||
<button className="cl_division_add" onClick={e => props.dispatch({type: 'add_divisions'})}>➕</button>:
|
||||
<div className="cl_division_add_filler"/>
|
||||
}
|
||||
</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) => {
|
||||
let err =
|
||||
subleague.name === "" ?
|
||||
"A name is required." :
|
||||
props.subleagues.slice(0, i).some(val => val.name === subleague.name) ?
|
||||
"Each subleague must have a different name." :
|
||||
"";
|
||||
|
||||
return (
|
||||
<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">{props.showError ? err : ""}</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>, deletedTeams: string[], 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))
|
||||
}
|
||||
isDuplicate={subleague.divisions.slice(0, di).some(val => val.name === subleague.divisions[di].name)}
|
||||
deletedTeams={props.deletedTeams} showError={props.showError} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</>);
|
||||
}
|
||||
|
||||
function Division(props: {
|
||||
state: DivisionState,
|
||||
dispatch: (action: DistributiveOmit<StructureReducerActions, 'subleague_index'|'division_index'>) => void,
|
||||
isDuplicate: boolean,
|
||||
deletedTeams: string[],
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
let divisionErr =
|
||||
props.state.name === "" ?
|
||||
"A name is required." :
|
||||
props.isDuplicate ?
|
||||
"Each division in a subleague must have a different name." :
|
||||
""
|
||||
|
||||
let teamsErr = props.state.teams.length < 2 ? "Must have at least 2 teams." : "";
|
||||
|
||||
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.showError ? divisionErr : ""}</div>
|
||||
</div>
|
||||
{props.state.teams.map((team, i) => {
|
||||
let showDeleted = props.showError && props.deletedTeams.includes(team.name)
|
||||
return (<>
|
||||
<div className="cl_team" key={team.id}>
|
||||
<div className={"cl_team_name" + (showDeleted ? " cl_team_name_err" : "")}>{team.name}</div>
|
||||
<button className="cl_team_delete" onClick={e => props.dispatch({type:'remove_team', name: team.name})}>➖</button>
|
||||
</div>
|
||||
<div className="cl_structure_err cl_structure_err_team">{showDeleted ? "This team was deleted" : ""}</div>
|
||||
</>)
|
||||
})}
|
||||
{ props.state.teams.length < MAX_TEAMS_PER_DIVISION ? <>
|
||||
<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>):
|
||||
null
|
||||
}</> :
|
||||
null
|
||||
}
|
||||
|
||||
<div className="cl_structure_err cl_structure_err_teams">{props.showError ? teamsErr : ""}</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;
|
|
@ -1,13 +1,4 @@
|
|||
|
||||
:root {
|
||||
--background-main: #2f3136; /*discord dark theme background-secondary - the same color as the embeds*/
|
||||
--background-secondary: #4f545c; /*discord's background-tertiary*/
|
||||
--background-accent: #4f545c; /*discord's background-accent*/
|
||||
--highlight: rgb(113, 54, 138); /*matteo purple™*/
|
||||
}
|
||||
|
||||
.game {
|
||||
align-self: stretch;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -125,7 +116,7 @@
|
|||
}
|
||||
|
||||
.score {
|
||||
background: var(--background-accent);
|
||||
background: var(--background-secondary);
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
|
@ -170,7 +161,7 @@
|
|||
border-radius: 4px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.update_emoji {
|
||||
|
@ -193,7 +184,7 @@
|
|||
height: 60px;
|
||||
}
|
||||
|
||||
.base_2 {
|
||||
.field > .base {
|
||||
margin-bottom: -25%
|
||||
}
|
||||
|
85
simmadome/src/Game.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { GameState } from './GamesUtil';
|
||||
import twemoji from 'twemoji';
|
||||
import React, { useRef, useLayoutEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './Game.css';
|
||||
import base_filled from './img/base_filled.png';
|
||||
import base_empty from './img/base_empty.png';
|
||||
import out_filled from './img/out_out.png';
|
||||
import out_empty from './img/out_in.png';
|
||||
|
||||
function Game(props: {gameId: string, state : GameState}) {
|
||||
let self: React.MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||
useLayoutEffect(() => {
|
||||
if (self.current) {
|
||||
twemoji.parse(self.current);
|
||||
}
|
||||
})
|
||||
|
||||
let state = props.state;
|
||||
return (
|
||||
<div className="game" ref={self}>
|
||||
<div className="header">
|
||||
<div className="inning">Inning: <span>{state.display_top_of_inning ? "🔼" : "🔽"}</span> {state.display_inning}/{state.max_innings}</div>
|
||||
<div className="title">{state.title}</div>
|
||||
<div className="weather">{state.weather_emoji} {state.weather_text}</div>
|
||||
</div>
|
||||
<div className="body">
|
||||
<div className="teams">
|
||||
<Team name={state.away_name} score={state.away_score}/>
|
||||
<Team name={state.home_name} score={state.home_score}/>
|
||||
</div>
|
||||
<div className="info">
|
||||
<div className="field">
|
||||
<Base name={state.bases[2]} />
|
||||
<div style={{display: "flex"}}>
|
||||
<Base name={state.bases[3]} />
|
||||
<Base name={state.bases[1]} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="outs">
|
||||
<div className="outs_title">OUTS</div>
|
||||
<div className="outs_count">
|
||||
{[1, 2].map((out) => <Out thisOut={out} totalOuts={state.outs} key={out} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="players">
|
||||
<div className="player_type">PITCHER</div>
|
||||
<div className="player_name">{state.pitcher}</div>
|
||||
<div className="player_type">BATTER</div>
|
||||
<div className="player_name batter_name">{state.batter}</div>
|
||||
</div>
|
||||
<div className="update">
|
||||
<div className="update_emoji">{state.update_emoji}</div>
|
||||
<div className="update_text">{state.update_text}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<div className="batting">{state.display_top_of_inning ? state.away_name : state.home_name} batting.</div>
|
||||
<div className="leagueoruser">{state.leagueoruser} (<Link to={"/game/" + props.gameId}>share</Link>)</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Team(props: {name: string, score: number}) {
|
||||
return (
|
||||
<div className="team">
|
||||
<div className="team_name">{ props.name }</div>
|
||||
<div className="score">{ props.score }</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Base(props: {name: string | null}) {
|
||||
return (
|
||||
<img className="base" alt={ props.name ?? "" } src={ props.name ? base_filled : base_empty }/>
|
||||
);
|
||||
}
|
||||
|
||||
function Out(props: {thisOut: number, totalOuts: number}) {
|
||||
return <img className="out" alt="" src={props.thisOut <= props.totalOuts ? out_filled : out_empty}/>;
|
||||
}
|
||||
|
||||
export default Game;
|
37
simmadome/src/GamePage.css
Normal file
|
@ -0,0 +1,37 @@
|
|||
#game_container {
|
||||
margin-top: 3rem;
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
62
simmadome/src/GamePage.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React, {useState, useRef, useLayoutEffect} from 'react';
|
||||
import twemoji from 'twemoji';
|
||||
import ReactRouter from 'react-router';
|
||||
import {GameState, useListener} from './GamesUtil';
|
||||
import './GamePage.css';
|
||||
import Game from './Game';
|
||||
import {getUID} from './util';
|
||||
|
||||
function GamePage(props: ReactRouter.RouteComponentProps<{id: string}>) {
|
||||
let [game, setGame] = useState<[string, GameState]|undefined>(undefined);
|
||||
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>
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="game_container">
|
||||
<Game gameId={game[0]} state={game[1]}/>
|
||||
{ history.current.length > 0 ?
|
||||
<GameHistory history={history.current}/> :
|
||||
null
|
||||
}
|
||||
</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;
|
|
@ -5,10 +5,16 @@
|
|||
grid-auto-flow: row;
|
||||
}
|
||||
|
||||
.slot_container {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.emptyslot, .game {
|
||||
min-height: 18.75rem;
|
||||
justify-self: center;
|
||||
max-width: 44rem;
|
||||
min-width: 32rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#filters {
|
||||
|
@ -58,8 +64,6 @@
|
|||
.emptyslot {
|
||||
border: 2px dashed white;
|
||||
border-radius: 15px;
|
||||
align-self: stretch;
|
||||
justify-self: stretch;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
@ -72,9 +76,4 @@
|
|||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.emptyslot {
|
||||
border: none;
|
||||
min-height: 0px;
|
||||
}
|
||||
}
|
142
simmadome/src/GamesPage.tsx
Normal file
|
@ -0,0 +1,142 @@
|
|||
import React, {useState, useRef, useEffect, useLayoutEffect} from 'react';
|
||||
import {GameState, GameList, useListener} from './GamesUtil';
|
||||
import {Link} from 'react-router-dom';
|
||||
import './GamesPage.css';
|
||||
import Game from './Game';
|
||||
|
||||
function GamesPage() {
|
||||
let [search, setSearch] = useState(window.location.search);
|
||||
useEffect(() => {
|
||||
setSearch(window.location.search);
|
||||
//eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [window.location.search])
|
||||
|
||||
let searchparams = new URLSearchParams(search);
|
||||
let filter = searchparams.get('league') ?? ""
|
||||
|
||||
let [games, setGames] = useState<[string, GameState][]>([]);
|
||||
useListener(setGames);
|
||||
|
||||
let filters = useRef(filter !== "" ? [filter] : []);
|
||||
games.forEach((game) => { if (game[1].is_league && !filters.current.includes(game[1].leagueoruser)) { filters.current.push(game[1].leagueoruser) }});
|
||||
filters.current = filters.current.filter((f) => games.find((game) => game && game[1].is_league && game[1].leagueoruser === f) || f === filter);
|
||||
|
||||
let gameList = useRef<(string | null)[]>([]);
|
||||
let filterGames = games.filter((game, i) => filter === "" || game[1].leagueoruser === filter);
|
||||
updateList(gameList.current, filterGames, searchparams.get('game'));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Filters filterList={filters.current} selectedFilter={filter} />
|
||||
<Grid gameList={gameList.current.map((val) => val !== null ? filterGames.find((game) => game[0] === val) as [string, GameState] : null )}/>
|
||||
<Footer has_games={filterGames.length > 0}/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// adds and removes games from list to keep it up to date, without relocating games already in place
|
||||
function updateList(gameList: (string | null)[], games: [string, GameState][], firstGame: string | null) {
|
||||
// insert firstGame into first slot, if necessary
|
||||
if (firstGame !== null && games.find((game) => game[0] === firstGame)) {
|
||||
if (gameList.includes(firstGame)) {
|
||||
gameList[gameList.indexOf(firstGame)] = null;
|
||||
}
|
||||
gameList[0] = firstGame;
|
||||
}
|
||||
|
||||
//remove games no longer present
|
||||
for (let i = 0; i < gameList.length; i ++) {
|
||||
if (gameList[i] !== null && games.findIndex((val) => val[0] === gameList[i]) < 0) {
|
||||
gameList[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// add games not present
|
||||
for (let game of games) {
|
||||
if (!gameList.find((val) => val !== null && val === game[0])) {
|
||||
let firstEmpty = gameList.indexOf(null);
|
||||
if (firstEmpty < 0) {
|
||||
gameList.push(game[0])
|
||||
} else {
|
||||
gameList[firstEmpty] = game[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//remove trailing empty cells
|
||||
while (gameList[gameList.length-1] === null) {
|
||||
gameList.pop();
|
||||
}
|
||||
}
|
||||
|
||||
function Filters (props: {filterList: string[], selectedFilter: string}) {
|
||||
function Filter(innerprops: {title: string, filter:string} ) {
|
||||
let search = new URLSearchParams();
|
||||
search.append('league', innerprops.filter);
|
||||
|
||||
return (
|
||||
<Link to={innerprops.filter !== "" ? "/?" + search.toString() : "/"} className="filter" id={innerprops.filter === props.selectedFilter ? "selected_filter" : ""}>
|
||||
{innerprops.title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="filters">
|
||||
<div>Filter:</div>
|
||||
<Filter title="All" filter="" key="" />
|
||||
{props.filterList.map((filter: string) =>
|
||||
<Filter title={filter} filter={filter} key={filter} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Grid(props: { gameList: GameList }) {
|
||||
let self: React.RefObject<HTMLElement> = useRef(null);
|
||||
let [numcols, setNumcols] = useState(3);
|
||||
let newList = [...props.gameList];
|
||||
|
||||
while (newList.length === 0 || newList.length % numcols !== 0) {
|
||||
newList.push(null);
|
||||
}
|
||||
|
||||
function getCols() {
|
||||
if (self.current !== null) {
|
||||
//this is a hack, but there's weirdly no "real" way to get the number of columns
|
||||
return window.getComputedStyle(self.current).getPropertyValue('grid-template-columns').split(' ').length;
|
||||
} else {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
//set num cols after page loads, then add listener to update if window resizes
|
||||
useLayoutEffect(() => {
|
||||
setNumcols(getCols());
|
||||
window.addEventListener('resize', (event) => {
|
||||
setNumcols(getCols());
|
||||
})
|
||||
}, [])
|
||||
|
||||
let emptyKey = 0;
|
||||
return (
|
||||
<section className="container" id="container" ref={self}>
|
||||
{newList.map((game) => (
|
||||
<div className="slot_container" key={game ? game[0] : emptyKey++}>
|
||||
{game ? <Game gameId={game[0]} state={game[1]}/> : <div className="emptyslot"/>}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Footer(props: { has_games: boolean }) {
|
||||
let text = props.has_games ? "" : "No games right now. Why not head over to Discord and start one?";
|
||||
return (
|
||||
<div id="footer">
|
||||
<div>{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GamesPage;
|
42
simmadome/src/GamesUtil.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import {useLayoutEffect} from 'react';
|
||||
import io from 'socket.io-client';
|
||||
|
||||
interface GameState {
|
||||
bases: (string | null)[];
|
||||
outs: number;
|
||||
display_top_of_inning: boolean
|
||||
display_inning: number
|
||||
max_innings: number
|
||||
title: string
|
||||
weather_emoji: string
|
||||
weather_text: string
|
||||
away_name: string
|
||||
away_score: number
|
||||
home_name: string
|
||||
home_score: number
|
||||
pitcher: string
|
||||
batter: string
|
||||
update_emoji: string
|
||||
update_text: string
|
||||
is_league: boolean
|
||||
leagueoruser: string
|
||||
start_delay: number
|
||||
end_delay: number
|
||||
}
|
||||
|
||||
type GameList = ([id: string, game: GameState] | null)[];
|
||||
|
||||
|
||||
// connects to the given url (or host if none) and waits for state updates
|
||||
const useListener = (onUpdate: (update: [string, GameState][]) => void, url: string | null = null) => {
|
||||
useLayoutEffect(() => {
|
||||
let socket = url ? io(url) : io();
|
||||
socket.on('connect', () => socket.emit('recieved', {}));
|
||||
socket.on('states_update', onUpdate);
|
||||
return () => {socket.disconnect()};
|
||||
//eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [url])
|
||||
}
|
||||
|
||||
export { useListener };
|
||||
export type { GameState, GameList };
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
BIN
simmadome/src/img/github.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 256 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
BIN
simmadome/src/img/patreon.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
simmadome/src/img/twitter.png
Executable file
After Width: | Height: | Size: 8.7 KiB |
111
simmadome/src/index.css
Normal file
|
@ -0,0 +1,111 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Alegreya&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Goldman:wght@700&display=swap');
|
||||
|
||||
:root {
|
||||
--background-main: #2f3136;
|
||||
--background-secondary: #4f545c;
|
||||
--background-tertiary: #202225;
|
||||
--background-accent: #40444b;
|
||||
--highlight: rgb(113, 54, 138); /*matteo purple™*/
|
||||
--accent-red: #f04747;
|
||||
--accent-green: rgb(67, 181, 129);
|
||||
}
|
||||
|
||||
body {
|
||||
background-image: url("img/naturalblack.png");
|
||||
}
|
||||
/* Background pattern from Toptal Subtle Patterns */
|
||||
|
||||
* {
|
||||
font-family: 'Alegreya', serif;
|
||||
color: white;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link{
|
||||
position: relative;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.h1 {
|
||||
margin: auto;
|
||||
width: 45%;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page_header {
|
||||
color: white;
|
||||
font-family: 'Goldman', cursive;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#header {
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#header .page_header {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#link_div {
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 2rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.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;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:link, a:visited {
|
||||
color: lightblue;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#utility_links {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 2rem;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
margin: 0 .05em 0 .1em;
|
||||
vertical-align: -0.1em;
|
||||
}
|
57
simmadome/src/index.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
|
||||
import './index.css';
|
||||
import GamesPage from './GamesPage';
|
||||
import GamePage from './GamePage';
|
||||
import CreateLeague from './CreateLeague';
|
||||
import discordlogo from "./img/discord.png";
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import patreonLogo from './img/patreon.png';
|
||||
import githubLogo from './img/github.png';
|
||||
import twitterLogo from './img/twitter.png';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Router>
|
||||
<Header />
|
||||
<Switch>
|
||||
<Route path="/game/:id" component={GamePage}/>
|
||||
<Route path="/create_league" component={CreateLeague} />
|
||||
<Route path="/" component={GamesPage}/>
|
||||
</Switch>
|
||||
</Router>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<div id="header">
|
||||
<div id="link_div">
|
||||
<a href="https://www.patreon.com/sixteen" className="patreon_link" target="_blank" rel="noopener noreferrer">
|
||||
<div className="patreon_container">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
1
simmadome/src/react-app-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="react-scripts" />
|
15
simmadome/src/reportWebVitals.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
5
simmadome/src/setupTests.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
34
simmadome/src/util.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
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};
|
26
simmadome/tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
|
@ -6,4 +6,4 @@ SELECT name,
|
|||
ROUND(strikeouts_given*27.0/(outs_pitched*1.0),3) as kper9,
|
||||
ROUND(strikeouts_given*1.0/walks_allowed*1.0,3) as kperbb
|
||||
FROM stats WHERE outs_pitched > 150
|
||||
ORDER BY bbper9 ASC;
|
||||
ORDER BY era ASC;
|
BIN
static/.DS_Store
vendored
|
@ -1,69 +0,0 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Alegreya&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Goldman:wght@700&display=swap');
|
||||
body {
|
||||
background-image: url("naturalblack.png");
|
||||
}
|
||||
/* Background pattern from Toptal Subtle Patterns */
|
||||
|
||||
div, button, h1, h2, a {
|
||||
font-family: 'Alegreya', serif;
|
||||
color: white;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link{
|
||||
position: relative;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.h1 {
|
||||
margin: auto;
|
||||
width: 45%;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page_header {
|
||||
color: white;
|
||||
font-family: 'Goldman', cursive;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#header {
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#header .page_header {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#link_div {
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 30px;
|
||||
}
|
||||
|
||||
#link_div > a {
|
||||
background-color: transparent;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#link_div > a:link, #link_div > a:visited {
|
||||
color: lightblue;
|
||||
}
|
||||
|
||||
#link_div > a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
height: 14px;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
#game_container {
|
||||
margin-top: 3rem;
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
Before Width: | Height: | Size: 9.5 KiB |
BIN
static/js/.DS_Store
vendored
|
@ -1,25 +0,0 @@
|
|||
$(document).ready(function (){
|
||||
var socket = io.connect();
|
||||
|
||||
socket.on('connect', function () {
|
||||
console.log("connected")
|
||||
socket.emit('recieved', {});
|
||||
});
|
||||
|
||||
socket.on("states_update", function (json) { //json is an object containing all game updates
|
||||
var searchparams = new URLSearchParams(window.location.search);
|
||||
var exists = false;
|
||||
for (game of json) {
|
||||
if (searchparams.get('timestamp') == game.timestamp) {
|
||||
$('.game').html(game.html);
|
||||
exists = true;
|
||||
}
|
||||
}
|
||||
if (!exists) {
|
||||
$('game').remove()
|
||||
$('#game_container').text("The game you're looking for either doesn't exist or has already ended.")
|
||||
}
|
||||
|
||||
twemoji.parse(document.body);
|
||||
});
|
||||
});
|
|
@ -1,187 +0,0 @@
|
|||
var socket = io.connect();
|
||||
var lastupdate;
|
||||
var grid;
|
||||
|
||||
$(document).ready(function (){
|
||||
grid = document.getElementById("container")
|
||||
|
||||
socket.on('connect', function () {
|
||||
socket.emit('recieved', {});
|
||||
});
|
||||
|
||||
socket.on("states_update", function (json) { //json is an object containing all game updates
|
||||
lastupdate = json;
|
||||
updateGames(json, $('#selected_filter').text());
|
||||
updateLeagues(json);
|
||||
});
|
||||
});
|
||||
|
||||
const updateGames = (json, filter) => {
|
||||
|
||||
filterjson = [];
|
||||
for (var game of json) {
|
||||
if (game.league == filter || filter == "All") {
|
||||
filterjson.push(game);
|
||||
}
|
||||
}
|
||||
|
||||
if (filterjson.length == 0) {
|
||||
$('#footer div').html("No games right now. Why not head over to Discord and start one?");
|
||||
} else {
|
||||
$('#footer div').html("");
|
||||
}
|
||||
|
||||
var searchparams = new URLSearchParams(window.location.search);
|
||||
if (searchparams.has('game') && filterjson.some(x => x.timestamp == searchparams.get('game')) && grid.children[0].timestamp != searchparams.get('game')) {
|
||||
var game = filterjson.find(x => x.timestamp == searchparams.get('game'))
|
||||
var oldbox = Array.prototype.slice.call(grid.children).find(x => x.timestamp == game.timestamp)
|
||||
if (oldbox) { clearBox(oldbox) }
|
||||
insertGame(0, game)
|
||||
}
|
||||
|
||||
//replace games that have ended with empty slots
|
||||
for (var slotnum = 0; slotnum < grid.children.length; slotnum++) {
|
||||
if (grid.children[slotnum].className == "game" && !filterjson.some((x) => x.timestamp == grid.children[slotnum].timestamp)) {
|
||||
clearBox(grid.children[slotnum])
|
||||
}
|
||||
}
|
||||
|
||||
for (var game of filterjson) {
|
||||
//updates game in list
|
||||
for (var slotnum = 0; slotnum < grid.children.length; slotnum++) {
|
||||
if (grid.children[slotnum].timestamp == game.timestamp) {
|
||||
insertGame(slotnum, game);
|
||||
};
|
||||
};
|
||||
|
||||
//adds game to list if not there already
|
||||
if (!Array.prototype.slice.call(grid.children).some(x => x.timestamp == game.timestamp)) {
|
||||
for (var slotnum = 0; true; slotnum++) { //this is really a while loop but shh don't tell anyone
|
||||
if (slotnum >= grid.children.length) {
|
||||
insertEmpty(grid);
|
||||
}
|
||||
if (grid.children[slotnum].className == "emptyslot") {
|
||||
insertGame(slotnum, game);
|
||||
break;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
fillgrid(grid)
|
||||
};
|
||||
}
|
||||
|
||||
const insertEmpty = (grid) => {
|
||||
var newBox = document.createElement("DIV");
|
||||
newBox.className = "emptyslot";
|
||||
grid.appendChild(newBox);
|
||||
}
|
||||
|
||||
const insertGame = (gridboxnum, game) => {
|
||||
var thisBox = grid.children[gridboxnum];
|
||||
thisBox.className = "game";
|
||||
thisBox.timestamp = game.timestamp;
|
||||
thisBox.innerHTML = game.html;
|
||||
twemoji.parse(thisBox);
|
||||
};
|
||||
|
||||
const insertLeague = (league) => {
|
||||
var btn = document.createElement("BUTTON");
|
||||
btn.className = "filter";
|
||||
btn.innerHTML = escapeHtml(league);
|
||||
$('#filters').append(btn);
|
||||
return btn;
|
||||
}
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
const clearBox = (box) => {
|
||||
box.className = "emptyslot";
|
||||
box.timestamp = null;
|
||||
box.innerHTML = "";
|
||||
}
|
||||
|
||||
const fillgrid = (grid) => {
|
||||
var gridwidth = window.getComputedStyle(grid).getPropertyValue('grid-template-columns').split(" ").length //hack to get number of grid columns
|
||||
|
||||
// add cells to fill last row
|
||||
while (grid.children.length % gridwidth != 0) {
|
||||
insertEmpty(grid)
|
||||
}
|
||||
|
||||
//remove last rows if not needed
|
||||
while (grid.children.length > gridwidth && Array.prototype.slice.call(grid.children).slice(grid.children.length - gridwidth).every( x => x.className == 'emptyslot')) {
|
||||
for (var i = 0; i < gridwidth; i++) {
|
||||
grid.removeChild(grid.children[grid.children.length-1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateLeagues = (games) => {
|
||||
//get all leagues
|
||||
var leagues = []
|
||||
for (var game of games) {
|
||||
if (game.league != "" && !leagues.includes(game.league)) {
|
||||
leagues.push(game.league)
|
||||
}
|
||||
}
|
||||
|
||||
//remove leagues no longer present
|
||||
$('#filters .filter').each(function(index) {
|
||||
if (!leagues.includes($(this).text())) {
|
||||
if (this.id != 'selected_filter' && $(this).text() != "All") { //don't remove the currently selected filter or the "all" filter
|
||||
$(this).remove();
|
||||
}
|
||||
} else {
|
||||
leagues.splice(leagues.indexOf($(this).text()), 1);
|
||||
}
|
||||
})
|
||||
|
||||
// add leagues not already present
|
||||
for (var league of leagues) { // we removed the entries that are already there in the loop above
|
||||
insertLeague(league)
|
||||
}
|
||||
|
||||
//add click handlers to each filter
|
||||
$('#filters .filter').each(function(index) {
|
||||
this.onclick = function() {
|
||||
if ($('#filters #selected_filter').text() == 'All') {
|
||||
updateGames([], ""); // clear grid when switching off of All, to make games collapse to top
|
||||
}
|
||||
$('#filters #selected_filter').attr('id', '');
|
||||
this.id = 'selected_filter';
|
||||
|
||||
var search = new URLSearchParams();
|
||||
search.append('league', this.textContent);
|
||||
history.pushState({}, "", "/" + (this.textContent != 'All' ? "?" + search.toString() : ""));
|
||||
updateGames(lastupdate, this.textContent);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
window.onpopstate = function(e) {
|
||||
var searchparams = new URLSearchParams(window.location.search);
|
||||
updateLeagues(lastupdate);
|
||||
$('#filters #selected_filter').attr('id', '');
|
||||
if (searchparams.has('league')) {
|
||||
var filter_found = false
|
||||
$('#filters .filter').each(function(i) { if (this.textContent == searchparams.get('league')) { this.id = 'selected_filter'; filter_found = true }});
|
||||
if (!filter_found) { insertLeague(searchparams.get('league')).id = 'selected_filter' }
|
||||
|
||||
updateGames(lastupdate, searchparams.get('league'));
|
||||
} else {
|
||||
$('#filters .filter').each(function(i) { if (this.textContent == 'All') { this.id = 'selected_filter' }})
|
||||
updateGames(lastupdate, "All");
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', function(e) {
|
||||
fillgrid(grid)
|
||||
})
|
BIN
templates/.DS_Store
vendored
|
@ -1,11 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block head_tags %}
|
||||
<link rel="stylesheet" href="/static/css/game.css">
|
||||
<link rel="stylesheet" href="/static/css/game_page.css">
|
||||
<script type="text/javascript" src="static/js/game_loader.js"></script>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<div id="game_container">
|
||||
<div class="game"></div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,53 +0,0 @@
|
|||
{% macro base(number) -%}
|
||||
src={% if state.bases[number] %}"/static/img/base_filled.png" alt="{{state.bases[number]}}"{% else %}"/static/img/base_empty.png"{% endif %}
|
||||
{%- endmacro %}
|
||||
{% macro out(number) -%}
|
||||
{% if number <= state.outs %}/static/img/out_out.png{% else %}/static/img/out_in.png{% endif %}
|
||||
{%- endmacro %}
|
||||
<div class="header">
|
||||
<div class="inning">Inning: {% if state.display_top_of_inning == true %}🔼{% else %}🔽{% endif %} {{ state.display_inning | escape }}/{{ state.max_innings | escape }}</div>
|
||||
<div class="title">{{ state.title | escape }}</div>
|
||||
<div class="weather">{{ state.weather_emoji | escape }} {{ state.weather_text | escape }}</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="teams">
|
||||
<div class="team">
|
||||
<div class="team_name">{{ state.away_name | escape }}</div>
|
||||
<div class="score">{{ state.away_score }}</div>
|
||||
</div>
|
||||
<div class="team">
|
||||
<div class="team_name">{{ state.home_name | escape }}</div>
|
||||
<div class="score">{{ state.home_score }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="field">
|
||||
<img class="base base_2" {{ base(2) }}/>
|
||||
<div style="display: flex;">
|
||||
<img class="base base_3" {{ base(3) }}/>
|
||||
<img class="base base_1" {{ base(1) }}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="outs">
|
||||
<div class="outs_title">OUTS</div>
|
||||
<div class="outs_count">
|
||||
<img class="out" src="{{ out(1) }}"/>
|
||||
<img class="out" src="{{ out(2) }}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="players">
|
||||
<div class="player_type">PITCHER</div>
|
||||
<div class="player_name pitcher_name">{{ state.pitcher | escape }}</div>
|
||||
<div class="player_type">BATTER</div>
|
||||
<div class="player_name batter_name">{{ state.batter | escape }}</div>
|
||||
</div>
|
||||
<div class="update">
|
||||
<div class="update_emoji">{{ state.update_emoji | escape }}</div>
|
||||
<div class="update_text">{{ state.update_text | escape }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="batting">{% if state.display_top_of_inning == true %}{{ state.away_name | escape }}{% else %}{{ state.home_name | escape }}{% endif %} batting.</div>
|
||||
<div class="leagueoruser">{{ state.leagueoruser | escape }} (<a href="/game?timestamp={{ timestamp }}">share</a>)</div>
|
||||
</div>
|
|
@ -1,21 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block head_tags %}
|
||||
<link rel="stylesheet" href="/static/css/games_page.css">
|
||||
<link rel="stylesheet" href="/static/css/game.css">
|
||||
<script type="text/javascript" src="static/js/grid_loader.js"></script>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<div id="filters">
|
||||
<div>Filter:</div>
|
||||
<button class="filter" {% if not league %}id="selected_filter"{% endif %}>All</button>
|
||||
{% if league %}<button class="filter" id="selected_filter">{{ league }}</button>{% endif %}
|
||||
</div>
|
||||
<section class="container" id="container">
|
||||
<div class="emptyslot"></div>
|
||||
<div class="emptyslot"></div>
|
||||
<div class="emptyslot"></div>
|
||||
</section>
|
||||
<div id="footer">
|
||||
<div></div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -32,6 +32,9 @@
|
|||
<Compile Include="leagues.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="league_storage.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="main_controller.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
|
|
674
the_prestige.py
|
@ -1,7 +1,7 @@
|
|||
import discord, json, math, os, roman, games, asyncio, random, main_controller, threading, time, urllib, leagues
|
||||
import discord, json, math, os, roman, games, asyncio, random, main_controller, threading, time, urllib, leagues, datetime
|
||||
import database as db
|
||||
import onomancer as ono
|
||||
import random
|
||||
from league_storage import league_exists, season_save
|
||||
from the_draft import Draft, DRAFT_ROUNDS
|
||||
from flask import Flask
|
||||
from uuid import uuid4
|
||||
|
@ -78,7 +78,7 @@ class IdolizeCommand(Command):
|
|||
else:
|
||||
meme = False
|
||||
|
||||
player_name = discord.utils.escape_mentions(command)
|
||||
player_name = discord.utils.escape_mentions(command.strip())
|
||||
if len(player_name) >= 70:
|
||||
await msg.channel.send("That name is too long. Please keep it below 70 characters, for my sake and yours.")
|
||||
return
|
||||
|
@ -138,9 +138,9 @@ class StartGameCommand(Command):
|
|||
league = command.split("\n")[0].split("--league ")[1].split("-")[0].strip()
|
||||
try:
|
||||
if "-d " in command.split("\n")[0]:
|
||||
day = int(command.split("\n")[0].split("-d ")[1].split("-")[0].strip())-1
|
||||
day = int(command.split("\n")[0].split("-d ")[1].split("-")[0].strip())
|
||||
elif "--day " in command.split("\n")[0]:
|
||||
day = int(command.split("\n")[0].split("--day ")[1].split("-")[0].strip())-1
|
||||
day = int(command.split("\n")[0].split("--day ")[1].split("-")[0].strip())
|
||||
except ValueError:
|
||||
await msg.channel.send("Make sure you put an integer after the -d flag.")
|
||||
return
|
||||
|
@ -196,7 +196,7 @@ class StartRandomGameCommand(Command):
|
|||
|
||||
channel = msg.channel
|
||||
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()
|
||||
|
||||
game = games.game(random.choice(teamslist).finalize(), random.choice(teamslist).finalize())
|
||||
|
@ -564,7 +564,12 @@ class StartTournamentCommand(Command):
|
|||
if team == None:
|
||||
await msg.channel.send(f"We couldn't find {name}. Try again?")
|
||||
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
|
||||
await msg.delete()
|
||||
|
@ -598,8 +603,8 @@ class DraftPlayerCommand(Command):
|
|||
|
||||
class StartDraftCommand(Command):
|
||||
name = "startdraft"
|
||||
template = "m;startdraft [mention] [teamname] [slogan]"
|
||||
description = """Starts a draft with an arbitrary number of participants. Send this command at the top of the list with each mention, teamname, and slogan on a new line (shift+enter in discord).
|
||||
template = "m;startdraft\n[mention]\n[teamname]\n[slogan]"
|
||||
description = """Starts a draft with an arbitrary number of participants. Send this command at the top of the list with each mention, teamname, and slogan on their own lines (shift+enter in discord).
|
||||
- The draft will proceed in the order that participants were entered.
|
||||
- 20 players will be available for draft at a time, and the pool will refresh automatically when it becomes small.
|
||||
- Each participant will be asked to draft 12 hitters then finally one pitcher.
|
||||
|
@ -611,7 +616,7 @@ class StartDraftCommand(Command):
|
|||
draft = Draft.make_draft()
|
||||
mentions = {f'<@!{m.id}>' for m in msg.mentions}
|
||||
content = msg.content.split('\n')[1:] # drop command out of message
|
||||
if len(content) % 3:
|
||||
if not content or len(content) % 3:
|
||||
await msg.channel.send('Invalid list')
|
||||
raise ValueError('Invalid length')
|
||||
|
||||
|
@ -727,6 +732,231 @@ class StartDraftCommand(Command):
|
|||
raise SlowDraftError('Too slow')
|
||||
return draft_message
|
||||
|
||||
class DebugLeagueStart(Command):
|
||||
name = "startdebugleague"
|
||||
|
||||
async def execute(self, msg, command):
|
||||
if not league_exists("test2"):
|
||||
league = leagues.league_structure("test2")
|
||||
league.setup({
|
||||
"nL" : {
|
||||
"nL west" : [get_team_fuzzy_search("lockpicks"), get_team_fuzzy_search("liches")],
|
||||
"nL east" : [get_team_fuzzy_search("bethesda soft"), get_team_fuzzy_search("traverse city")]
|
||||
},
|
||||
"aL" : {
|
||||
"aL west" : [get_team_fuzzy_search("deep space"), get_team_fuzzy_search("phoenix")],
|
||||
"aL east" : [get_team_fuzzy_search("cheyenne mountain"), get_team_fuzzy_search("tarot dragons")]
|
||||
}
|
||||
}, division_games=6, inter_division_games=3, inter_league_games=3, games_per_hour = 12)
|
||||
league.generate_schedule()
|
||||
leagues.save_league(league)
|
||||
|
||||
class DebugLeagueDisplay(Command):
|
||||
name = "displaydebugleague"
|
||||
|
||||
async def execute(self, msg, command):
|
||||
if league_exists("Midseries"):
|
||||
league = leagues.load_league_file("Midseries")
|
||||
league.champion = "Butts"
|
||||
leagues.save_league(league)
|
||||
season_save(league)
|
||||
league.season_reset()
|
||||
|
||||
await msg.channel.send(embed=league.past_standings(1))
|
||||
|
||||
|
||||
class StartLeagueCommand(Command):
|
||||
name = "startleague"
|
||||
template = "m;startleague [league name]\n[games per hour]"
|
||||
description = """Optional flags for the first line: `--queue X` or `-q X` to play X number of series before stopping; `--noautopostseason` will pause the league before starting postseason.
|
||||
Plays a league with a given name, provided that league has been saved on the website. The games per hour sets how often the games will start. (e.g. GPH 2 will start games at X:00 and X:30)"""
|
||||
|
||||
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
|
||||
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.split("\n")[1].strip())
|
||||
if gph < 1 or gph > 12:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
await msg.channel.send("Chief, we need a games per hour number between 1 and 12. We think that's reasonable.")
|
||||
return
|
||||
|
||||
if league_exists(league_name):
|
||||
league = leagues.load_league_file(league_name)
|
||||
if "--noautopostseason" in command:
|
||||
autoplay = int(list(league.schedule.keys())[-1]) - league.day_to_series_num(league.day) + 1
|
||||
|
||||
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"] or league.owner is None:
|
||||
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. Use `--season X` or `-s X` to get standings from season X of that league."
|
||||
|
||||
async def execute(self, msg, command):
|
||||
if league_exists(command.split("-")[0].strip()):
|
||||
league = leagues.load_league_file(command.split("-")[0].strip())
|
||||
|
||||
try:
|
||||
if "--season " in command:
|
||||
season_num = int(command.split("--season ")[1])
|
||||
await msg.channel.send(embed=league.past_standings(season_num))
|
||||
elif "-s " in command:
|
||||
season_num = int(command.split("-s ")[1])
|
||||
await msg.channel.send(embed=league.past_standings(season_num))
|
||||
else:
|
||||
await msg.channel.send(embed=league.standings_embed())
|
||||
except ValueError:
|
||||
await msg.channel.send("Give us a proper number, boss.")
|
||||
except TypeError:
|
||||
await msg.channel.send("That season hasn't been played yet, chief.")
|
||||
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.name} commissioner is doing a great job. That's you, by the way.")
|
||||
return
|
||||
else:
|
||||
await msg.channel.send("That league has already been claimed!")
|
||||
else:
|
||||
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 (league.owner is not None and msg.author.id in config()["owners"]):
|
||||
for user in msg.mentions:
|
||||
if user.id not in league.owner:
|
||||
league.owner.append(user.id)
|
||||
leagues.save_league(league)
|
||||
await msg.channel.send(f"The new {league.name} front office is now up and running.")
|
||||
return
|
||||
else:
|
||||
await msg.channel.send(f"That league isn't yours, boss.")
|
||||
return
|
||||
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 = [
|
||||
IntroduceCommand(),
|
||||
|
@ -747,19 +977,29 @@ commands = [
|
|||
ShowAllTeamsCommand(),
|
||||
SearchTeamsCommand(),
|
||||
StartGameCommand(),
|
||||
StartTournamentCommand(),
|
||||
StartRandomGameCommand(),
|
||||
StartTournamentCommand(),
|
||||
LeagueClaimCommand(),
|
||||
LeagueAddOwnersCommand(),
|
||||
StartLeagueCommand(),
|
||||
LeaguePauseCommand(),
|
||||
LeagueDisplayCommand(),
|
||||
LeagueWildcardCommand(),
|
||||
LeagueScheduleCommand(),
|
||||
CreditCommand(),
|
||||
RomanCommand(),
|
||||
HelpCommand(),
|
||||
StartDraftCommand(),
|
||||
DraftPlayerCommand(),
|
||||
DebugLeagueStart(),
|
||||
DebugLeagueDisplay(),
|
||||
ShowHistoryCommand(),
|
||||
]
|
||||
|
||||
client = discord.Client()
|
||||
gamesarray = []
|
||||
active_tournaments = []
|
||||
active_leagues = []
|
||||
setupmessages = {}
|
||||
|
||||
thread1 = threading.Thread(target=main_controller.update_loop)
|
||||
|
@ -794,6 +1034,7 @@ async def on_ready():
|
|||
print(f"logged in as {client.user} with token {config()['token']}")
|
||||
await client.change_presence(activity=discord.Game(name="Use m;help"))
|
||||
|
||||
|
||||
@client.event
|
||||
async def on_reaction_add(reaction, user):
|
||||
if reaction.message in setupmessages.keys():
|
||||
|
@ -1028,7 +1269,16 @@ async def start_tournament_round(channel, tourney, seeding = None):
|
|||
|
||||
for pair in games_to_start:
|
||||
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)
|
||||
|
||||
state_init["is_league"] = True
|
||||
|
@ -1059,6 +1309,14 @@ async def continue_tournament_series(tourney, queue, games_list, wins_in_series)
|
|||
for oldgame in queue:
|
||||
away_team = games.get_team(oldgame.teams["away"].name)
|
||||
home_team = games.get_team(oldgame.teams["home"].name)
|
||||
|
||||
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, state_init = prepare_game(this_game)
|
||||
|
||||
|
@ -1090,7 +1348,7 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals
|
|||
try:
|
||||
for i in range(0, len(games_list)):
|
||||
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():
|
||||
wins_in_series[game.teams["home"].name] = 0
|
||||
if game.teams['away'].name not in wins_in_series.keys():
|
||||
|
@ -1104,6 +1362,7 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals
|
|||
wins_in_series[winner_name] = 1
|
||||
|
||||
final_embed = game_over_embed(game)
|
||||
final_embed.add_field(name="Series score:", value=f"{wins_in_series[game.teams['away'].name]} - {wins_in_series[game.teams['home'].name]}")
|
||||
await channel.send(f"A {tourney.name} game just ended!")
|
||||
await channel.send(embed=final_embed)
|
||||
if wins_in_series[winner_name] >= int((tourney.series_length+1)/2) and not finals:
|
||||
|
@ -1118,11 +1377,36 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals
|
|||
except:
|
||||
print("something went wrong in tourney_watcher")
|
||||
await asyncio.sleep(4)
|
||||
|
||||
if tourney.league is not None:
|
||||
tourney.day += 1
|
||||
|
||||
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}")
|
||||
games_list = await continue_tournament_series(tourney, queued_games, games_list, wins_in_series)
|
||||
else:
|
||||
|
@ -1130,7 +1414,10 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, 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!")
|
||||
if tourney.day > tourney.league.day:
|
||||
tourney.league.day = tourney.day
|
||||
await channel.send(embed=embed)
|
||||
tourney.winner = get_team_fuzzy_search(winner_list[0])
|
||||
active_tournaments.pop(active_tournaments.index(tourney))
|
||||
return
|
||||
|
||||
|
@ -1139,11 +1426,37 @@ async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals
|
|||
winners_string = ""
|
||||
for game in tourney.bracket.get_bottom_row():
|
||||
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.
|
||||
Advancing teams:
|
||||
{winners_string}""")
|
||||
await asyncio.sleep(tourney.round_delay)
|
||||
await asyncio.sleep(tourney.round_delay)
|
||||
await start_tournament_round(channel, tourney)
|
||||
|
||||
|
||||
|
@ -1428,7 +1741,7 @@ async def game_watcher():
|
|||
this_array = gamesarray.copy()
|
||||
for i in range(0,len(this_array)):
|
||||
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)
|
||||
if isinstance(user, str):
|
||||
await channel.send(f"A game started by {user} just ended.")
|
||||
|
@ -1449,7 +1762,6 @@ def game_over_embed(game):
|
|||
title_string += f" with {game.inning - (game.max_innings+1)} extra innings.\n"
|
||||
else:
|
||||
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
|
||||
winstring = f"{game.teams['away'].score} to {game.teams['home'].score}\n"
|
||||
|
@ -1457,6 +1769,16 @@ def game_over_embed(game):
|
|||
winstring += f"{winning_team} wins with a victory lap!"
|
||||
elif winning_team == game.teams['home'].name:
|
||||
winstring += f"{winning_team} wins, shaming {game.teams['away'].name}!"
|
||||
else:
|
||||
winstring += f"{winning_team} wins!"
|
||||
|
||||
embed = discord.Embed(color=discord.Color.dark_purple(), title=title_string)
|
||||
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
|
||||
|
||||
def get_team_fuzzy_search(team_name):
|
||||
team = games.get_team(team_name)
|
||||
if team is None:
|
||||
|
@ -1465,4 +1787,316 @@ def get_team_fuzzy_search(team_name):
|
|||
team = teams[0]
|
||||
return team
|
||||
|
||||
async def start_league_day(channel, league, partial = False):
|
||||
current_games = []
|
||||
|
||||
games_to_start = league.schedule[str(league.day_to_series_num(league.day))]
|
||||
if league.game_length is None:
|
||||
game_length = games.config()["default_length"]
|
||||
else:
|
||||
game_length = league.game_length
|
||||
|
||||
for pair in games_to_start:
|
||||
if pair[0] is not None and pair[1] is not None:
|
||||
away = get_team_fuzzy_search(pair[0])
|
||||
away.set_pitcher(rotation_slot=league.day)
|
||||
home = get_team_fuzzy_search(pair[1])
|
||||
home.set_pitcher(rotation_slot=league.day)
|
||||
|
||||
this_game = games.game(away.finalize(), home.finalize(), length = game_length)
|
||||
this_game, state_init = prepare_game(this_game)
|
||||
|
||||
state_init["is_league"] = True
|
||||
if not partial:
|
||||
series_string = "Series score:"
|
||||
state_init["title"] = f"{series_string} 0 - 0"
|
||||
else:
|
||||
state_init["title"] = "Interrupted series!"
|
||||
discrim_string = league.name
|
||||
|
||||
id = str(uuid4())
|
||||
current_games.append((this_game, id))
|
||||
main_controller.master_games_dic[id] = (this_game, state_init, discrim_string)
|
||||
|
||||
ext = "?league=" + urllib.parse.quote_plus(league.name)
|
||||
|
||||
if league.last_series_check(): #if finals
|
||||
await channel.send(f"The final series of the {league.name} regular season is starting now, at {config()['simmadome_url']+ext}")
|
||||
last = True
|
||||
|
||||
else:
|
||||
await channel.send(f"The day {league.day} series of the {league.name} is starting now, at {config()['simmadome_url']+ext}")
|
||||
last = False
|
||||
|
||||
if partial:
|
||||
missed_games = (league.day % league.series_length) - 1
|
||||
if missed_games == -1:
|
||||
missed_games = 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, last = False, missed = 0):
|
||||
league.active = True
|
||||
league.autoplay -= 1
|
||||
if league not in active_leagues:
|
||||
active_leagues.append(league)
|
||||
series_results = {}
|
||||
|
||||
while league.active:
|
||||
queued_games = []
|
||||
while len(games_list) > 0:
|
||||
try:
|
||||
for i in range(0, len(games_list)):
|
||||
game, key = games_list[i]
|
||||
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():
|
||||
series_results[game.teams["home"].name] = {}
|
||||
series_results[game.teams["home"].name]["wins"] = 0
|
||||
series_results[game.teams["home"].name]["losses"] = 0
|
||||
series_results[game.teams["home"].name]["run_diff"] = 0
|
||||
if game.teams['away'].name not in series_results.keys():
|
||||
series_results[game.teams["away"].name] = {}
|
||||
series_results[game.teams["away"].name]["wins"] = 0
|
||||
series_results[game.teams["away"].name]["losses"] = 0
|
||||
series_results[game.teams["away"].name]["run_diff"] = 0
|
||||
|
||||
winner_name = game.teams['home'].name if game.teams['home'].score > game.teams['away'].score else game.teams['away'].name
|
||||
loser_name = game.teams['away'].name if game.teams['home'].score > game.teams['away'].score else game.teams['home'].name
|
||||
rd = int(math.fabs(game.teams['home'].score - game.teams['away'].score))
|
||||
|
||||
series_results[winner_name]["wins"] += 1
|
||||
series_results[winner_name]["run_diff"] += rd
|
||||
|
||||
winner_dic = {"wins" : 1, "run_diff" : rd}
|
||||
|
||||
series_results[loser_name]["losses"] += 1
|
||||
series_results[loser_name]["run_diff"] -= rd
|
||||
|
||||
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.add_field(name="Day:", value=league.day)
|
||||
final_embed.add_field(name="Series score:", value=f"{series_results[game.teams['away'].name]['wins']} - {series_results[game.teams['home'].name]['wins']}")
|
||||
await channel.send(f"A {league.name} game just ended!")
|
||||
await channel.send(embed=final_embed)
|
||||
if series_results[winner_name]["wins"] + series_results[winner_name]["losses"] + missed < league.series_length:
|
||||
queued_games.append(game)
|
||||
games_list.pop(i)
|
||||
break
|
||||
except:
|
||||
print("something went wrong in league_day_watcher")
|
||||
await asyncio.sleep(2)
|
||||
league.day += 1
|
||||
|
||||
if len(queued_games) > 0:
|
||||
|
||||
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
|
||||
|
||||
leagues.save_league(league)
|
||||
await channel.send(embed=league.standings_embed())
|
||||
await channel.send(f"The day {league.day} games for the {league.name} will start in {math.ceil(wait_seconds/60)} minutes.")
|
||||
leagues.save_league(league)
|
||||
await asyncio.sleep(wait_seconds)
|
||||
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, missed)
|
||||
else:
|
||||
league.active = False
|
||||
|
||||
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))
|
||||
return
|
||||
|
||||
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))
|
||||
|
||||
next_start = (now + delta).replace(second=0, microsecond=0)
|
||||
wait_seconds = (next_start - now).seconds
|
||||
await channel.send(f"This {league.name} season is now over! The postseason (with any necessary tiebreakers) will be starting in {math.ceil(wait_seconds/60)} minutes.")
|
||||
await asyncio.sleep(wait_seconds)
|
||||
await league_postseason(channel, league)
|
||||
|
||||
#need to reset league to new season here
|
||||
|
||||
return
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
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 asyncio.sleep(wait_seconds)
|
||||
|
||||
await start_league_day(channel, league)
|
||||
|
||||
async def continue_league_series(league, queue, games_list, series_results, missed):
|
||||
for oldgame in queue:
|
||||
away_team = games.get_team(oldgame.teams["away"].name)
|
||||
away_team.set_pitcher(rotation_slot=league.day)
|
||||
home_team = games.get_team(oldgame.teams["home"].name)
|
||||
home_team.set_pitcher(rotation_slot=league.day)
|
||||
this_game = games.game(away_team.finalize(), home_team.finalize(), length = league.game_length)
|
||||
this_game, state_init = prepare_game(this_game)
|
||||
|
||||
state_init["is_league"] = True
|
||||
series_string = f"Series score:"
|
||||
|
||||
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
|
||||
|
||||
id = str(uuid4())
|
||||
games_list.append((this_game, id))
|
||||
main_controller.master_games_dic[id] = (this_game, state_init, discrim_string)
|
||||
|
||||
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.champion = world_series.winner.name
|
||||
leagues.save_league(league)
|
||||
season_save(league)
|
||||
league.season_reset()
|
||||
|
||||
|
||||
|
||||
client.run(config()["token"])
|
||||
|
|