Merge pull request #115 from Sakimori/indev

push indev branch to live on 1/4/2021
This commit is contained in:
Sakimori 2021-01-04 13:38:11 -05:00 committed by GitHub
commit 27c1a787d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1552 additions and 827 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,35 +1,59 @@
# matteo-the-prestige # matteo-the-prestige
# simsim discord bot # simsim discord bot
blaseball, blaseball, is back! in an unofficial capacity. this is completely unaffiliated with the game band blaseball, blaseball, is back! in an unofficial capacity. this project is completely unaffiliated with the game band.
custom players, 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 (that last one is coming soon™), all set up in discord and watchable at https://simsim.sibr.dev!
we've also got things like custom team creation, easy setup for your teams to play against each other, and player idolization, all powered by this bot and onomancer. 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
accepting pull requests, check the issues for to-dos. accepting pull requests, check the issues for to-dos.
## commands: (everything here is case sensitive, and can be prefixed with either m; or m!) ## commands: (everything here is case sensitive, and can be prefixed with either m; or m!)
### team commands: ### team commands:
#### creation and deletion:
- m;saveteam - m;saveteam
- saves a team to the database allowing it to be used for games. send this command at the top of a list, with entries separated by new lines (shift+enter in discord, or copy+paste from notepad). - 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:
- the first line of the list is your team's name (cannot contain emoji). - the first line of the list is your team's name.
- the second line is your team's icon and slogan, this should begin with an emoji followed by a space, followed by a short slogan. - the second line is the team's icon and slogan, generally this is an emoji followed by a space, followed by a short slogan.
- the third line must be blank.
- the next lines are your batters' names in the order you want them to appear in your lineup, lineups can contain any number of batters between 1 and 12. - the next lines are your batters' names in the order you want them to appear in your lineup, lineups can contain any number of batters between 1 and 12.
- the final line is your pitcher's name. - then another blank line seperating your batters and your pitchers.
- if you did it correctly, you'll get a team embed with a prompt to confirm. hit the 👍 and it'll be saved. - 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)
- 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]
- 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]
- 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]
- 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]
- 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] - m;showteam [name]
- shows information about any saved team. - 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.
- m;showallteams
- shows a paginated list of all teams available for games which can be scrolled through.
- m;searchteams [searchterm] - m;searchteams [searchterm]
- shows a paginated list of all teams whose names contain the given search term. - shows a paginated list of all teams whose names contain the given search term.
- m;deleteteam [teamname] - m;showallteams
- allows you to delete the team with the provided name if you are the owner of it, gives a confirmation first to prevent accidental deletions. if it isn't letting you delete your team, you probably created it before teams having owners was a thing, contact xvi and xie can assign you as the owner. - shows a paginated list of all teams available for games which can be scrolled through.
- m;import
- imports an onomancer collection as a new team. you can use the new onomancer simsim setting to ensure compatibility.
### player commands: ### player commands:
- m;showplayer [name] - m;showplayer [name]
@ -37,18 +61,29 @@ accepting pull requests, check the issues for to-dos.
- m;idolize [name] - m;idolize [name]
- records any name as your idol, mostly for fun. - records any name as your idol, mostly for fun.
- m;showidol - m;showidol
- displays your idol's name and stars. - displays your idol's name and stars in a discord embed.
### game commands: ### game commands:
- m;startgame - m;startgame --day # or -d #
- starts a game with premade teams made using saveteam, use this command at the top of a list followed by each of these in a new line: - starts a game with premade teams made using saveteam. provides a link to the website where you can watch the game.
- the --day/-d is optional, if used it'll force the game to use the #th spot in each team's rotations. if this number is larger than the number of pitchers in one or both of the teams' rotations it'll wrap around. if it is not used pitchers will be chosen randomly from the teams' rotations.
- use this command at the top of a list with entries separated by new lines:
- the away team's name. - the away team's name.
- the home team's name. - the home team's name.
- and finally, optionally, the number of innings, which must be greater than 2 and less than 31. if not included it will default to 9. - optionally, the number of innings, which must be greater than 2 and less than 31. if not included it will default to 9.
- this command has fuzzy search so you don't need to type the full name of the team as long as you give enough to identify the team you're looking for.
- m;randomgame
- starts a 9-inning game between 2 entirely random teams. embrace chaos!
- m;starttournament --rounddelay #
- starts a randomly seeded tournament with up to 64 provided teams, automatically adding byes as necessary. all series have a 5 minute break between games. the current format is: best of 5 until the finals which are best of 7.
- the --rounddelay is optional, if used, # must be between 1 and 120 and it'll set the delay between rounds to be # minutes. if not included it will default to 10.
- use this command at the top of a list with entries separated by new lines:
- the name of the tournament.
- the name of each participating team on its own line.
### other commands: ### other commands:
- m;help [command] - m;help [command]
- shows the instructions from here for given command. if no command is provided, it will instead provide a list of all of the commands that instructions can be provided for. - 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.
- m;credit - m;credit
- shows artist credit for matteo's avatar. - shows artist credit for matteo's avatar.
- m;roman [number] - m;roman [number]
@ -60,3 +95,4 @@ these folks are helping me a *ton* via patreon, and i cannot possibly thank them
- Ale Humano - Ale Humano
- Chris Denmark - Chris Denmark
- Astrid Bek - Astrid Bek
- Kameleon

View File

@ -207,6 +207,21 @@ def save_team(name, team_json_string, user_id):
except: except:
return False return False
def update_team(name, team_json_string):
conn = create_connection()
try:
if conn is not None:
c = conn.cursor()
store_string = "UPDATE teams SET team_json_string = ? WHERE name=?"
c.execute(store_string, (team_json_string, (re.sub('[^A-Za-z0-9 ]+', '', name)))) #this regex removes all non-standard characters
conn.commit()
conn.close()
return True
conn.close()
return False
except:
return False
def get_team(name, owner=False): def get_team(name, owner=False):
conn = create_connection() conn = create_connection()
if conn is not None: if conn is not None:

217
games.py
View File

@ -24,22 +24,13 @@ def config():
return json.load(config_file) return json.load(config_file)
def all_weathers(): def all_weathers():
if not os.path.exists("weather_config.json"): weathers_dic = {
#generate default config #"Supernova" : weather("Supernova", "🌟"),
super_weather_json = jsonpickle.encode(weather("Supernova", "🌟")) "Midnight": weather("Midnight", "🕶"),
mid_weather_json = jsonpickle.encode(weather("Midnight", "🕶")) "Slight Tailwind": weather("Slight Tailwind", "🏌️‍♀️"),
config_dic = { "Heavy Snow": weather("Heavy Snow", "")
"Supernova" : super_weather_json, }
"Midnight": mid_weather_json return weathers_dic
}
with open("weather_config.json", "w") as config_file:
json.dump(config_dic, config_file, indent=4)
with open("weather_config.json") as config_file:
weather_dic = {}
for weather_json in json.load(config_file).values():
this_weather = jsonpickle.decode(weather_json, classes=weather)
weather_dic[this_weather.name] = this_weather
return weather_dic
class appearance_outcomes(Enum): class appearance_outcomes(Enum):
@ -102,10 +93,59 @@ class team(object):
self.name = None self.name = None
self.lineup = [] self.lineup = []
self.lineup_position = 0 self.lineup_position = 0
self.rotation = []
self.pitcher = None self.pitcher = None
self.score = 0 self.score = 0
self.slogan = None self.slogan = None
def find_player(self, name):
for index in range(0,len(self.lineup)):
if self.lineup[index].name == name:
return (self.lineup[index], index, self.lineup)
for index in range(0,len(self.rotation)):
if self.rotation[index].name == name:
return (self.rotation[index], index, self.rotation)
else:
return (None, None, None)
def average_stars(self):
total_stars = 0
for _player in self.lineup:
total_stars += _player.stlats["batting_stars"]
for _player in self.rotation:
total_stars += _player.stlats["pitching_stars"]
return total_stars/(len(self.lineup) + len(self.rotation))
def swap_player(self, name):
this_player, index, roster = self.find_player(name)
if this_player is not None and len(roster) > 1:
if roster == self.lineup:
if self.add_pitcher(this_player):
roster.pop(index)
return True
else:
if self.add_lineup(this_player)[0]:
self.rotation.pop(index)
return True
return False
def delete_player(self, name):
this_player, index, roster = self.find_player(name)
if this_player is not None and len(roster) > 1:
roster.pop(index)
return True
else:
return False
def slide_player(self, name, new_spot):
this_player, index, roster = self.find_player(name)
if this_player is not None and new_spot < len(roster):
roster.pop(index)
roster.insert(new_spot-1, this_player)
return True
else:
return False
def add_lineup(self, new_player): def add_lineup(self, new_player):
if len(self.lineup) < 20: if len(self.lineup) < 20:
self.lineup.append(new_player) self.lineup.append(new_player)
@ -113,34 +153,58 @@ class team(object):
else: else:
return (False, "20 players in the lineup, maximum. We're being really generous here.") return (False, "20 players in the lineup, maximum. We're being really generous here.")
def set_pitcher(self, new_player): def add_pitcher(self, new_player):
self.pitcher = new_player if len(self.rotation) < 8:
return (True,) self.rotation.append(new_player)
return True
else:
return False
def set_pitcher(self, rotation_slot = None, use_lineup = False):
temp_rotation = self.rotation.copy()
if use_lineup:
for batter in self.rotation:
temp_rotation.append(batter)
if rotation_slot is None:
self.pitcher = random.choice(temp_rotation)
else:
self.pitcher = temp_rotation[rotation_slot % len(temp_rotation)]
def is_ready(self): def is_ready(self):
return (len(self.lineup) >= 1 and self.pitcher is not None) try:
return (len(self.lineup) >= 1 and len(self.rotation) > 0)
except AttributeError:
self.rotation = [self.pitcher]
self.pitcher = None
return (len(self.lineup) >= 1 and len(self.rotation) > 0)
def prepare_for_save(self): def prepare_for_save(self):
self.lineup_position = 0 self.lineup_position = 0
self.score = 0 self.score = 0
if self.pitcher is not None and self.pitcher not in self.rotation:
self.rotation.append(self.pitcher)
self.pitcher = None
for this_player in self.lineup: for this_player in self.lineup:
for stat in this_player.game_stats.keys(): for stat in this_player.game_stats.keys():
this_player.game_stats[stat] = 0 this_player.game_stats[stat] = 0
return True for this_player in self.rotation:
for stat in this_player.game_stats.keys():
this_player.game_stats[stat] = 0
return self
def finalize(self): def finalize(self):
if self.is_ready(): if self.is_ready():
self.set_pitcher()
while len(self.lineup) <= 4: while len(self.lineup) <= 4:
self.lineup.append(random.choice(self.lineup)) self.lineup.append(random.choice(self.lineup))
return True return self
else: else:
return False return False
class game(object): class game(object):
def __init__(self, name, team1, team2, length=None): def __init__(self, team1, team2, length=None):
self.name = name
self.over = False self.over = False
self.teams = {"away" : team1, "home" : team2} self.teams = {"away" : team1, "home" : team2}
self.inning = 1 self.inning = 1
@ -160,8 +224,13 @@ class game(object):
def get_batter(self): def get_batter(self):
if self.top_of_inning: if self.top_of_inning:
bat_team = self.teams["away"] bat_team = self.teams["away"]
counter = self.weather.counter_away
else: else:
bat_team = self.teams["home"] bat_team = self.teams["home"]
counter = self.weather.counter_home
if self.weather.name == "Heavy Snow" and counter == bat_team.lineup_position:
return bat_team.pitcher
return bat_team.lineup[bat_team.lineup_position % len(bat_team.lineup)] return bat_team.lineup[bat_team.lineup_position % len(bat_team.lineup)]
def get_pitcher(self): def get_pitcher(self):
@ -422,14 +491,26 @@ class game(object):
def batterup(self): def batterup(self):
scores_to_add = 0 scores_to_add = 0
result = self.at_bat() result = self.at_bat()
self.get_batter()
if self.top_of_inning: if self.top_of_inning:
offense_team = self.teams["away"] offense_team = self.teams["away"]
weather_count = self.weather.counter_away
defense_team = self.teams["home"] defense_team = self.teams["home"]
else: else:
offense_team = self.teams["home"] offense_team = self.teams["home"]
weather_count = self.weather.counter_home
defense_team = self.teams["away"] defense_team = self.teams["away"]
if self.weather.name == "Slight Tailwind" and "mulligan" not in self.last_update[0].keys() and not result["ishit"] and result["text"] != appearance_outcomes.walk:
mulligan_roll_target = -((((self.get_batter().stlats["batting_stars"])-7)/7)**2)+1
if random.random() > mulligan_roll_target:
result["mulligan"] = True
return (result, 0)
if self.weather.name == "Heavy Snow" and weather_count == offense_team.lineup_position and "snow_atbat" not in self.last_update[0].keys():
result["snow_atbat"] = True
result["text"] = f"{offense_team.lineup[offense_team.lineup_position % len(offense_team.lineup)].name}'s hands are too cold! {self.get_batter().name} is forced to bat!"
return (result, 0)
defenders = defense_team.lineup.copy() defenders = defense_team.lineup.copy()
defenders.append(defense_team.pitcher) defenders.append(defense_team.pitcher)
defender = random.choice(defenders) #pitcher can field outs now :3 defender = random.choice(defenders) #pitcher can field outs now :3
@ -519,12 +600,21 @@ class game(object):
for base in self.bases.keys(): for base in self.bases.keys():
self.bases[base] = None self.bases[base] = None
self.outs = 0 self.outs = 0
if self.top_of_inning and self.weather.name == "Heavy Snow" and self.weather.counter_away < self.teams["away"].lineup_position:
self.weather.counter_away = self.pitcher_insert(self.teams["away"])
if not self.top_of_inning: if not self.top_of_inning:
if self.weather.name == "Heavy Snow" and self.weather.counter_home < self.teams["home"].lineup_position:
self.weather.counter_home = self.pitcher_insert(self.teams["home"])
self.inning += 1 self.inning += 1
if self.inning > self.max_innings and self.teams["home"].score != self.teams["away"].score: #game over if self.inning > self.max_innings and self.teams["home"].score != self.teams["away"].score: #game over
self.over = True self.over = True
self.top_of_inning = not self.top_of_inning self.top_of_inning = not self.top_of_inning
def pitcher_insert(self, this_team):
rounds = math.ceil(this_team.lineup_position / len(this_team.lineup))
position = random.randint(0, len(this_team.lineup)-1)
return rounds * len(this_team.lineup) + position
def end_of_game_report(self): def end_of_game_report(self):
return { return {
@ -567,19 +657,9 @@ class game(object):
else: else:
inningtext = "bottom" inningtext = "bottom"
updatestring = f"{self.last_update[0]['batter']} {self.last_update[0]['text'].value} {self.last_update[0]['defender']}{punc}\n" updatestring = "this isn't used but i don't want to break anything"
if self.last_update[1] > 0: return "this isn't used but i don't want to break anything"
updatestring += f"{self.last_update[1]} runs scored!"
return f"""Last update: {updatestring}
Score: {self.teams['away'].score} - {self.teams['home'].score}.
Current inning: {inningtext} of {self.inning}. {self.outs} outs.
Pitcher: {self.get_pitcher().name}
Batter: {self.get_batter().name}
Bases: 3: {str(self.bases[3])} 2: {str(self.bases[2])} 1: {str(self.bases[1])}
"""
else: else:
return f"""Game over! Final score: **{self.teams['away'].score} - {self.teams['home'].score}** return f"""Game over! Final score: **{self.teams['away'].score} - {self.teams['home'].score}**
Last update: {self.last_update[0]['batter']} {self.last_update[0]['text'].value} {self.last_update[0]['defender']}{punc}""" Last update: {self.last_update[0]['batter']} {self.last_update[0]['text'].value} {self.last_update[0]['defender']}{punc}"""
@ -619,20 +699,40 @@ def get_team(name):
try: try:
team_json = jsonpickle.decode(db.get_team(name)[0], keys=True, classes=team) team_json = jsonpickle.decode(db.get_team(name)[0], keys=True, classes=team)
if team_json is not None: if team_json is not None:
if team_json.pitcher is not None: #detects old-format teams, adds pitcher
team_json.rotation.append(team_json.pitcher)
team_json.pitcher = None
update_team(team_json)
return team_json return team_json
return None return None
except AttributeError:
team_json.rotation = []
team_json.rotation.append(team_json.pitcher)
team_json.pitcher = None
update_team(team_json)
return team_json
except: except:
return None return None
def get_team_and_owner(name): def get_team_and_owner(name):
#try: try:
counter, name, team_json_string, timestamp, owner_id = db.get_team(name, owner=True) counter, name, team_json_string, timestamp, owner_id = db.get_team(name, owner=True)
team_json = jsonpickle.decode(team_json_string, keys=True, classes=team) team_json = jsonpickle.decode(team_json_string, keys=True, classes=team)
if team_json is not None: if team_json is not None:
if team_json.pitcher is not None: #detects old-format teams, adds pitcher
team_json.rotation.append(team_json.pitcher)
team_json.pitcher = None
update_team(team_json)
return (team_json, owner_id)
return None
except AttributeError:
team_json.rotation = []
team_json.rotation.append(team_json.pitcher)
team_json.pitcher = None
update_team(team_json)
return (team_json, owner_id) return (team_json, owner_id)
return None except:
#except: return None
#return None
def save_team(this_team, user_id): def save_team(this_team, user_id):
try: try:
@ -643,6 +743,15 @@ def save_team(this_team, user_id):
except: except:
return None return None
def update_team(this_team):
try:
this_team.prepare_for_save()
team_json_string = jsonpickle.encode(this_team, keys=True)
db.update_team(this_team.name, team_json_string)
return True
except:
return None
def get_all_teams(): def get_all_teams():
teams = [] teams = []
for team_pickle in db.get_all_teams(): for team_pickle in db.get_all_teams():
@ -653,8 +762,22 @@ def get_all_teams():
def search_team(search_term): def search_team(search_term):
teams = [] teams = []
for team_pickle in db.search_teams(search_term): for team_pickle in db.search_teams(search_term):
this_team = jsonpickle.decode(team_pickle[0], keys=True, classes=team) team_json = jsonpickle.decode(team_pickle[0], keys=True, classes=team)
teams.append(this_team) try:
if team_json.pitcher is not None:
if len(team_json.rotation) == 0: #detects old-format teams, adds pitcher
team_json.rotation.append(team_json.pitcher)
team_json.pitcher = None
update_team(team_json)
except AttributeError:
team_json.rotation = []
team_json.rotation.append(team_json.pitcher)
team_json.pitcher = None
update_team(team_json)
except:
return None
teams.append(team_json)
return teams return teams
def base_string(base): def base_string(base):
@ -674,6 +797,8 @@ class weather(object):
def __init__(self, new_name, new_emoji): def __init__(self, new_name, new_emoji):
self.name = new_name self.name = new_name
self.emoji = new_emoji self.emoji = new_emoji
self.counter_away = 0
self.counter_home = 0
def __str__(self): def __str__(self):
return f"{self.emoji} {self.name}" return f"{self.emoji} {self.name}"

127
leagues.py Normal file
View File

@ -0,0 +1,127 @@
import time, asyncio, jsonpickle, random, math
from games import team, game
from discord import Embed, Color
import database as db
class league(object):
def __init__(self, name, subleagues_dic):
self.subleagues = {} #key: name, value: [divisions]
self.max_days
self.day = 1
self.name = name
self.subleagues = subleagues_dic
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.bracket = None
self.results = None
self.series_length = series_length
self.finals_length = finals_series_length
self.game_length = max_innings
self.active = False
self.delay = secs_between_games
self.round_delay = secs_between_rounds
self.finals = False
self.id = id
if id is None:
self.id = random.randint(1111,9999)
else:
self.id = id
def build_bracket(self, random_sort = False, by_wins = False):
teams_list = list(self.teams.keys()).copy()
if random_sort:
def sorter(team_in_list):
return random.random()
elif by_wins:
def sorter(team_in_list):
return self.teams[team_in_list]["wins"] #sorts by wins
else: #sort by average stars
def sorter(team_in_list):
return team_in_list.average_stars()
teams_list.sort(key=sorter, reverse=True)
bracket_layers = int(math.ceil(math.log(len(teams_list), 2)))
empty_slots = int(math.pow(2, bracket_layers) - len(teams_list))
for i in range(0, empty_slots):
teams_list.append(None)
previous_bracket_layer = teams_list.copy()
for i in range(0, bracket_layers - 1):
this_layer = []
for pair in range(0, int(len(previous_bracket_layer)/2)):
if pair % 2 == 0: #if even number
this_layer.insert(0+int(pair/2), [previous_bracket_layer.pop(0), previous_bracket_layer.pop(-1)]) #every other pair goes at front of list, moving forward
else:
this_layer.insert(0-int((1+pair)/2), [previous_bracket_layer.pop(int(len(previous_bracket_layer)/2)-1), previous_bracket_layer.pop(int(len(previous_bracket_layer)/2))]) #every other pair goes at end of list, moving backward
previous_bracket_layer = this_layer
self.bracket = bracket(previous_bracket_layer, bracket_layers)
def round_check(self):
if self.bracket.depth == 1:
self.finals = True
return True
else:
return False
class bracket(object):
this_bracket = []
def __init__(self, bracket_list, depth):
self.this_bracket = bracket_list
self.depth = depth
self.bottom_row = []
def get_bottom_row(self):
self.depth = 1
self.bottom_row = []
self.dive(self.this_bracket)
return self.bottom_row
def dive(self, branch):
if not isinstance(branch[0], list): #if it's a pair of games
self.bottom_row.append(branch)
else:
self.depth += 1
return self.dive(branch[0]), self.dive(branch[1])
#def set_winners(self, branch, winners_list):
#new_bracket =
def set_winners_dive(self, winners_list, index = 0, branch = None, parent = None):
if branch is None:
branch = self.this_bracket.copy()
if not isinstance(branch[0], list): #if it's a pair of games
if branch[0].name in winners_list or branch[1] is None:
winner = branch[0]
if parent is not None:
parent[index] = winner
elif branch[1].name in winners_list:
winner = branch[1]
if parent is not None:
parent[index] = winner
else:
self.set_winners_dive(winners_list, index = 0, branch = branch[0], parent = branch)
self.set_winners_dive(winners_list, index = 1, branch = branch[1], parent = branch)
if parent is None:
self.this_bracket = branch
return branch

View File

@ -1,4 +1,4 @@
import asyncio, time, datetime, games, json, threading, jinja2 import asyncio, time, datetime, games, json, threading, jinja2, leagues
from flask import Flask, url_for, Response, render_template, request, jsonify from flask import Flask, url_for, Response, render_template, request, jsonify
from flask_socketio import SocketIO, emit from flask_socketio import SocketIO, emit
@ -9,8 +9,15 @@ socketio = SocketIO(app)
@app.route('/') @app.route('/')
def index(): def index():
if ('league' in request.args):
return render_template("index.html", league=request.args['league'])
return render_template("index.html") return render_template("index.html")
@app.route('/game')
def game_page():
return render_template("game.html")
thread2 = threading.Thread(target=socketio.run,args=(app,'0.0.0.0')) thread2 = threading.Thread(target=socketio.run,args=(app,'0.0.0.0'))
thread2.start() thread2.start()
@ -84,6 +91,19 @@ def update_loop():
state["update_emoji"] = "💎" state["update_emoji"] = "💎"
state["update_text"] = updatestring state["update_text"] = updatestring
elif "mulligan" in this_game.last_update[0].keys():
updatestring = ""
punc = ""
if this_game.last_update[0]["defender"] != "":
punc = ", "
state["update_emoji"] = "🏌️‍♀️"
state["update_text"] = f"{this_game.last_update[0]['batter']} would have gone out, but they took a mulligan!"
elif "snow_atbat" in this_game.last_update[0].keys():
state["update_emoji"] = ""
state["update_text"] = this_game.last_update[0]["text"]
else: else:
updatestring = "" updatestring = ""
punc = "" punc = ""
@ -121,14 +141,16 @@ def update_loop():
state["update_pause"] -= 1 state["update_pause"] -= 1
global data_to_send global data_to_send
template = jinja2.Environment(loader=jinja2.FileSystemLoader('templates')).get_template('game.html')
data_to_send = [] data_to_send = []
template = jinja2.Environment(loader=jinja2.FileSystemLoader('templates')).get_template('game_box.html')
for timestamp in game_states: for timestamp in game_states:
data_to_send.append({ data_to_send.append({
'timestamp' : timestamp, 'timestamp' : timestamp,
'league' : game_states[timestamp]['leagueoruser'] if game_states[timestamp]['is_league'] else '', 'league' : game_states[timestamp]['leagueoruser'] if game_states[timestamp]['is_league'] else '',
'html' : template.render(state=game_states[timestamp]) 'state' : game_states[timestamp],
'html' : template.render(state=game_states[timestamp], timestamp=timestamp)
}) })
socketio.emit("states_update", data_to_send) socketio.emit("states_update", data_to_send)
time.sleep(6) time.sleep(8)

69
static/css/common.css Normal file
View File

@ -0,0 +1,69 @@
@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;
}

232
static/css/game.css Normal file
View File

@ -0,0 +1,232 @@
: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;
justify-self: stretch;
text-align: center;
display: flex;
flex-direction: column;
background:var(--background-main);
border: 4px solid;
border-radius: 4px;
border-color: var(--highlight);
border-top: none;
border-right: none;
border-bottom: none;
height: max-content;
}
.header {
width: 100%;
background-color: var(--background-secondary);
border-top-right-radius: 4px;
height: max-content;
display: flex;
justify-content: space-between
}
.header > div {
margin: 0.3rem 0.5rem;
}
.body {
margin: 0.5rem;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-areas:
"teams teams info" "players players info" "update update update";
grid-template-rows: 5.5rem auto auto;
grid-row-gap: 0.5rem;
grid-column-gap: 0.75rem;
}
.teams {
grid-area: teams;
display: flex;
flex-direction: column;
justify-content: space-around;
min-width: 95%;
max-width: 100%;
width: min-content;
}
.team {
display: flex;
justify-content: space-between;
width: 100%;
margin: 0.25rem 0rem;
}
.team_name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.info {
grid-area: info;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
background: #4f545c;
padding: 0.75rem 0rem;
border-radius: 4px;
}
.batting {
font-size: 10pt;
text-align: left;
height: max-content;
margin: 0.3rem 0.5rem;
}
.leagueoruser {
font-size: 10pt;
text-align: right;
height: max-content;
margin: 0.3rem 0.5rem;
}
.footer {
display: flex;
justify-content: space-between;
}
.outs {
display: flex;
justify-content: space-between;
align-items: center;
width: 60%;
}
.outs_title {
font-weight: bolder;
}
.outs_count {
display: flex;
}
.out {
height: 20px;
}
.team_name, .score {
font-size: 25px
}
.score {
background: var(--background-accent);
width: 40px;
min-width: 40px;
height: 40px;
border-radius: 20px;
margin-left: 10px;
}
.players {
grid-area: players;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
grid-template-rows: auto auto;
grid-column-gap: 0.5rem;
margin-left: 0.3rem;
}
.players > div {
margin: 0.25rem 0rem;
}
.player_type {
text-align: end;
font-weight: bolder;
display: inline-block;
vertical-align: middle;
}
.player_name {
overflow: hidden;
text-overflow: ellipsis;
text-align: start;
white-space: nowrap;
width: 100%;
}
.update {
grid-area: update;
min-height: 3.5rem;
padding: 0rem 0.75rem;
height: 100%;
background: var(--background-secondary);
border-radius: 4px;
align-items: center;
display: flex;
justify-content: start;
}
.update_emoji {
margin-right: 0.75rem;
margin-left: 2px;
}
.update_text {
text-align: start;
}
.field {
display: flex;
justify-content: space-around;
align-items: center;
flex-direction: column;
}
.base {
height: 60px;
}
.base_2 {
margin-bottom: -25%
}
@media only screen and (max-device-width: 800px) {
.batting {
font-size: 15pt;
text-align: left;
height: max-content;
padding: 5px;
}
.leagueoruser {
font-size: 15pt;
text-align: right;
height: max-content;
padding: 5px;
}
.team_name, .score {
font-size: 23px
}
.players {
font-size: 20px;
grid-area: players;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
height: 100%;
padding-right: 50px;
}
.update_emoji, .update_text {
display: inline;
font-size: 20px;
}
}

9
static/css/game_page.css Normal file
View File

@ -0,0 +1,9 @@
#game_container {
margin-top: 50px;
display: flex;
justify-content: space-around;
}
.game {
width: 33%;
}

91
static/css/games_page.css Normal file
View File

@ -0,0 +1,91 @@
.container {
display: grid;
grid-template-columns: repeat(3, minmax(500px, 1fr));
grid-gap: 50px 30px; /*space between rows, then columns*/
align-items: center;
justify-items: center;
grid-auto-flow: row;
}
.container > div {
min-height: 298px;
}
#filters {
display: flex;
justify-content: center;
width: 100%;
align-items: center;
margin-top: 10px;
margin-bottom: 20px;
}
#filters > * {
padding: 0.25rem 0.5rem;
margin: 0rem 0.5rem;
font-size: 16pt;
background: rgba(0,0,0,0);
}
#filters > .filter {
border-radius: 0.5rem;
min-width: 6.25rem;
text-align: center;
border: none;
color: white;
text-decoration: none;
}
#selected_filter {
background: rgb(113, 54, 138);
}
#footer {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 4.5rem;
}
#footer > div {
text-align: center;
font-size: 16pt;
position: relative;
top: 0.25rem;
}
.emptyslot {
border: 2px dashed white;
border-radius: 15px;
align-self: stretch;
justify-self: stretch;
text-align: center;
color: white;
}
@media only screen and (max-device-width: 800px) {
.container {
display: grid;
grid-template-columns: repeat(1, minmax(700px, 90%));
grid-template-rows: 400px;
grid-gap: 50px 30px; /*space between rows, then columns*/
align-items: center;
justify-items: center;
grid-auto-rows: 400px;
grid-auto-flow: row;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
}
.emptyslot {
border: none;
border-radius: 15px;
align-self: stretch;
justify-self: stretch;
text-align: center;
color: white;
flex: 1;
}
}

BIN
static/css/naturalblack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -1,50 +0,0 @@
<div class="header">
<div class="inning"></div>
<div class="weather"></div>
</div>
<div class="body">
<div class="teams">
<div class="team">
<div class="team_name away_name"></div>
<div class="score away_score"></div>
</div>
<div class="team">
<div class="team_name home_name"></div>
<div class="score home_score"></div>
</div>
</div>
<div class="info">
<div class="field">
<img class="base base_2" src=""/>
<div style="display: flex;">
<img class="base base_3" src=""/>
<img class="base base_1" src=""/>
</div>
</div>
<div class="outs">
<div class="outs_title">OUTS</div>
<div class="outs_count">
<img class="out" src=""/>
<img class="out" src=""/>
</div>
</div>
</div>
<div class="players">
<div class="player pitcher">
<div class="player_type">Pitcher:</div>
<div class="player_name pitcher_name"></div>
</div>
<div class="player batter">
<div class="player_type">Batter:</div>
<div class="player_name batter_name"></div>
</div>
</div>
<div class="update">
<div class="update_emoji"></div>
<div class="update_text"></div>
</div>
</div>
<div class="footer">
<div class="batting"></div>
<div class="leagueoruser"></div>
</div>

View File

@ -1,375 +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("prism.png");
}
/* Background pattern from Toptal Subtle Patterns */
div, button {
font-family: 'Alegreya', serif;
color: white;
}
#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;
}
.container {
display: grid;
grid-template-columns: repeat(3, minmax(500px, 1fr));
grid-gap: 50px 30px; /*space between rows, then columns*/
align-items: center;
justify-items: center;
grid-auto-rows: 335px;
grid-auto-flow: row;
}
#header {
width: 100%;
height: 150px;
margin-bottom: 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#header > .page_header {
margin: auto
}
#filters {
display: flex;
justify-content: space-around;
width: max-content;
margin-top: 10px;
}
#filters > * {
padding: 4px 8px;
margin: 0px 8px;
font-size: 16pt;
background: rgba(0,0,0,0);
}
#filters > .filter {
border-radius: 8px;
min-width: 100px;
text-align: center;
border: none;
}
#selected_filter {
background: rgb(113, 54, 138);
}
#footer {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 75px;
}
#footer > div {
text-align: center;
font-size: 20px;
position: relative;
top: 5px;
}
.link{
position: relative;
top: 10px;
}
.h1 {
margin: auto;
width: 45%;
color: white;
font-family: 'Alegreya', serif;
}
.page_header {
color: white;
font-family: 'Goldman', cursive;
}
.emptyslot {
border: 2px dashed white;
border-radius: 15px;
align-self: stretch;
justify-self: stretch;
text-align: center;
color: white;
flex: 1;
}
.game {
font-family: 'Alegreya', serif;
color: white;
align-self: stretch;
justify-self: stretch;
text-align: center;
display: flex;
flex-direction: column;
background: #2f3136; /*discord dark theme background-secondary - the same color as the embeds*/
border: 4px solid;
border-radius: 4px;
border-color: rgb(113, 54, 138); /*matteo purple™*/
border-top: none;
border-right: none;
border-bottom: none;
flex: 1;
}
h2 {
font-family: 'Alegreya', serif;
color: white;
text-align: center;
}
.header {
width: 100%;
background-color: #4f545c; /*discord's background-tertiary*/
border-top-right-radius: 4px;
height: max-content;
}
.inning {
float: left;
margin: 5px;
margin-left: 8px;
}
.weather {
float: right;
margin: 5px;
margin-right: 8px;
}
.body {
margin: 10px;
display: grid;
grid-template-columns: 60% 40%;
grid-template-areas:
"teams info" "players info" "update update";
grid-template-rows: 90px;
grid-row-gap: 8px;
grid-column-gap: 10px;
flex: 1;
}
.teams {
grid-area: teams;
display: flex;
flex-direction: column;
justify-content: space-around;
}
.team {
display: flex;
justify-content: space-between;
width: 100%;
}
.team_name {
overflow: hidden;
white-space: nowrap;
}
.info {
grid-area: info;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
background: #4f545c;
padding-top: 8px;
padding-bottom: 4px;
margin-left: 15%;
margin-right: 10px;
border-radius: 4px;
}
.batting {
font-size: 10pt;
text-align: left;
height: max-content;
padding: 5px;
}
.leagueoruser {
font-size: 10pt;
text-align: right;
height: max-content;
padding: 5px;
}
.footer {
display: flex;
justify-content: space-between;
}
.outs {
display: flex;
justify-content: space-between;
align-items: center;
width: 60%;
}
.outs_title {
font-weight: bolder;
}
.outs_count {
display: flex;
}
.out {
height: 20px;
}
.team_name, .score {
font-size: 25px
}
.score {
background: #4f545c; /*discord's background-accent*/
width: 40px;
min-width: 40px;
height: 40px;
border-radius: 20px;
margin-left: 10px;
}
.players {
grid-area: players;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: start;
height: max-content;
}
.player {
display: flex;
align-items: end;
width: 100%;
flex-direction: column;
}
.player_name {
overflow: hidden;
text-align: start;
white-space: nowrap;
width: 95%;
}
.update {
grid-area: update;
margin-right: 10px;
margin-top: 10px;
min-height: 50px;
}
.player_type {
width: 100%;
text-align: start;
}
.update_emoji, .update_text {
display: inline
}
.field {
display: flex;
justify-content: space-around;
align-items: center;
flex-direction: column;
}
.base {
height: 60px;
}
.base_2 {
margin-bottom: -25%
}
@media only screen and (max-device-width: 800px) {
.container {
display: grid;
grid-template-columns: repeat(1, minmax(700px, 90%));
grid-template-rows: 400px;
grid-gap: 50px 30px; /*space between rows, then columns*/
align-items: center;
justify-items: center;
grid-auto-rows: 400px;
grid-auto-flow: row;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
}
.emptyslot {
border: none;
border-radius: 15px;
align-self: stretch;
justify-self: stretch;
text-align: center;
color: white;
flex: 1;
}
.batting {
font-size: 15pt;
text-align: left;
height: max-content;
padding: 5px;
}
.leagueoruser {
font-size: 15pt;
text-align: right;
height: max-content;
padding: 5px;
}
.team_name, .score {
font-size: 23px
}
.players {
font-size: 20px;
grid-area: players;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
height: 100%;
padding-right: 50px;
}
.update_emoji, .update_text {
display: inline;
font-size: 20px;
}
}

BIN
static/js/.DS_Store vendored Normal file

Binary file not shown.

25
static/js/game_loader.js Normal file
View File

@ -0,0 +1,25 @@
$(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);
});
});

168
static/js/grid_loader.js Normal file
View File

@ -0,0 +1,168 @@
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) {
for (var i = 0; i < 3; i ++) {
insertEmpty(grid);
}
}
if (grid.children[slotnum].className == "emptyslot") {
insertGame(slotnum, game);
break;
};
};
}
};
//remove last rows if not needed
while (grid.children[grid.children.length-1].className == "emptyslot" &&
grid.children[grid.children.length-2].className == "emptyslot" &&
grid.children[grid.children.length-3].className == "emptyslot" &&
grid.children.length > 3) {
for (var i = 0; i < 3; i++) {
grid.removeChild(grid.children[grid.children.length-1]);
}
}
}
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 = league;
$('#filters').append(btn);
return btn;
}
const clearBox = (box) => {
box.className = "emptyslot";
box.timestamp = null;
box.innerHTML = "";
}
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");
}
}

View File

@ -1,123 +0,0 @@
$(document).ready(function (){
var socket = io.connect();
var lastupdate;
var grid = document.getElementById("container");
socket.on('connect', function () {
socket.emit('recieved', { data: 'I\'m connected!' });
});
socket.on("states_update", function (json) { //json is an object containing all game updates
lastupdate = json;
updateGames(json, $('#selected_filter').text());
//get all leagues
leagues = []
for (var game of json) {
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).attr('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
$('#filters').append("<button class='filter'>"+league+"</button>");
}
//add click handlers to each filter
$('#filters .filter').each(function(index) {
$(this).click(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).attr('id', 'selected_filter');
updateGames(lastupdate, $(this).text());
})
})
});
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("");
}
//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)) {
grid.children[slotnum].className = "emptyslot";
grid.children[slotnum].timestamp = null;
grid.children[slotnum].innerHTML = "";
}
}
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) {
for (var i = 0; i < 3; i ++) {
insertEmpty(grid);
}
}
if (grid.children[slotnum].className == "emptyslot") {
insertGame(slotnum, game);
break;
};
};
}
};
//remove last rows if not needed
while (grid.children[grid.children.length-1].className == "emptyslot" &&
grid.children[grid.children.length-2].className == "emptyslot" &&
grid.children[grid.children.length-3].className == "emptyslot" &&
grid.children.length > 3) {
for (var i = 0; i < 3; i++) {
grid.removeChild(grid.children[grid.children.length-1]);
}
}
}
const insertEmpty = (grid) => {
newBox = document.createElement("DIV");
newBox.className = "emptyslot";
grid.appendChild(newBox);
}
const insertGame = (gridboxnum, game) => {
var thisBox = grid.children[gridboxnum];
thisBox.innerHTML = game.html;
thisBox.className = "game";
thisBox.timestamp = game.timestamp;
};
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

31
templates/base.html Normal file
View File

@ -0,0 +1,31 @@
<html lang="en-US">
<head>
<script src="//code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://twemoji.maxcdn.com/v/latest/twemoji.min.js" crossorigin="anonymous"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/3.0.4/socket.io.js"></script>
<script type="text/javascript" async src="https://platform.twitter.com/widgets.js"></script>
<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">
<link rel="stylesheet" href="/static/css/common.css">
{% block head_tags %}{% endblock %}
</head>
<body>
<div id="header">
<div id="link_div">
<a href="https://www.patreon.com/sixteen" class="link" target="_blank" rel="noopener noreferrer">Patreon</a><br />
<a href="https://github.com/Sakimori/matteo-the-prestige" class="link" target="_blank" rel="noopener noreferrer">Github</a><br />
<a href="https://twitter.com/intent/follow?screen_name=SIBR_XVI" class="link" target="_blank" rel="noopener noreferrer">Twitter</a>
</div>
<a href="/" class="page_header"><h2 class="page_header" style="font-size: 50px;">THE SIMMADOME</h2></a>
<h2 class="page_header">Join SIBR on <a href="https://discord.gg/UhAajY2NCW" class="link"><img src="static/discord.png" height="30"></a> to start your own games!</h2>
</div>
{% block body %}{% endblock %}
</body>
</html>

View File

@ -1,57 +1,11 @@
{% macro base(number) -%} {% extends "base.html" %}
{% if state.bases[number] %}/static/img/base_filled.png{% else %}/static/img/base_empty.png{% endif %} {% block head_tags %}
{%- endmacro %} <link rel="stylesheet" href="/static/css/game.css">
{% macro out(number) -%} <link rel="stylesheet" href="/static/css/game_page.css">
{% if number <= state.outs %}/static/img/out_out.png{% else %}/static/img/out_in.png{% endif %} <script type="text/javascript" src="static/js/game_loader.js"></script>
{%- endmacro %} {% endblock %}
{% block body %}
<div class="header"> <div id="game_container">
<div class="inning">Inning: {% if state.display_top_of_inning == true %}🔼{% else %}🔽{% endif %} {{ state.display_inning }}/{{ state.max_innings }}</div> <div class="game"></div>
<div class="weather">{{ state.weather_emoji }} {{ state.weather_text }}</div>
</div>
<div class="body">
<div class="teams">
<div class="team">
<div class="team_name">{{ state.away_name }}</div>
<div class="score">{{ state.away_score }}</div>
</div>
<div class="team">
<div class="team_name">{{ state.home_name }}</div>
<div class="score">{{ state.home_score }}</div>
</div>
</div> </div>
<div class="info"> {% endblock %}
<div class="field">
<img class="base base_2" src="{{ base(2) }}"/>
<div style="display: flex;">
<img class="base base_3" src="{{ base(3) }}"/>
<img class="base base_1" src="{{ 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 pitcher">
<div class="player_type">Pitcher:</div>
<div class="player_name pitcher_name">{{ state.pitcher }}</div>
</div>
<div class="player batter">
<div class="player_type">Batter:</div>
<div class="player_name batter_name">{{ state.batter }}</div>
</div>
</div>
<div class="update">
<div class="update_emoji">{{ state.update_emoji }}</div>
<div class="update_text">{{ state.update_text }}</div>
</div>
</div>
<div class="footer">
<div class="batting">{% if state.display_top_of_inning == true %}{{ state.away_name }}{% else %}{{ state.home_name }}{% endif %} batting.</div>
<div class="leagueoruser">{{ state.leagueoruser }}</div>
</div>

53
templates/game_box.html Normal file
View File

@ -0,0 +1,53 @@
{% 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 }}/{{ state.max_innings }}</div>
<div class="title">{{ state.title }}</div>
<div class="weather">{{ state.weather_emoji }} {{ state.weather_text }}</div>
</div>
<div class="body">
<div class="teams">
<div class="team">
<div class="team_name">{{ state.away_name }}</div>
<div class="score">{{ state.away_score }}</div>
</div>
<div class="team">
<div class="team_name">{{ state.home_name }}</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 }}</div>
<div class="player_type">BATTER</div>
<div class="player_name batter_name">{{ state.batter }}</div>
</div>
<div class="update">
<div class="update_emoji">{{ state.update_emoji }}</div>
<div class="update_text">{{ state.update_text }}</div>
</div>
</div>
<div class="footer">
<div class="batting">{% if state.display_top_of_inning == true %}{{ state.away_name }}{% else %}{{ state.home_name }}{% endif %} batting.</div>
<div class="leagueoruser">{{ state.leagueoruser }} (<a href="/game?timestamp={{ timestamp }}">share</a>)</div>
</div>

View File

@ -1,30 +1,14 @@
<html lang="en-US"> {% extends "base.html" %}
<head> {% block head_tags %}
<script src="//code.jquery.com/jquery-3.5.1.min.js"></script> <link rel="stylesheet" href="/static/css/games_page.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/3.0.4/socket.io.js"></script> <link rel="stylesheet" href="/static/css/game.css">
<script type="text/javascript" async src="https://platform.twitter.com/widgets.js"></script> <script type="text/javascript" src="static/js/grid_loader.js"></script>
<script type="text/javascript" src="static/loader.js"></script> {% endblock %}
<link rel="stylesheet" href="/static/games_page.css"> {% block body %}
<title>⚾ The Simmadome</title> <div id="filters">
<meta property="og:title" content="Watch at the Simmadome" /> <div>Filter:</div>
<meta property="og:description" content="The Simsim: Your players, your teams, your games." /> <button class="filter" {% if not league %}id="selected_filter"{% endif %}>All</button>
<meta name="twitter:card" content="summary"> {% if league %}<button class="filter" id="selected_filter">{{ league }}</button>{% endif %}
<meta name="twitter:site" content="@SIBR_XVI">
</head>
<body>
<div id="header">
<div id="link_div">
<a href="https://www.patreon.com/sixteen" class="link" target="_blank" rel="noopener noreferrer">Patreon</a><br />
<a href="https://github.com/Sakimori/matteo-the-prestige" class="link" target="_blank" rel="noopener noreferrer">Github</a><br />
<a href="https://twitter.com/intent/follow?screen_name=SIBR_XVI" class="link" target="_blank" rel="noopener noreferrer">Twitter</a>
</div>
<h2 class="page_header" style="font-size: 50px;">THE SIMMADOME</h2>
<h2 class="page_header">Join SIBR on <a href="https://discord.gg/UhAajY2NCW" class="link"><img src="static/discord.png" height="30"></a> to start your own games!</h2>
<div id="filters">
<div>Filter:</div>
<button class="filter" id="selected_filter">All</button>
</div>
</div> </div>
<section class="container" id="container"> <section class="container" id="container">
<div class="emptyslot"></div> <div class="emptyslot"></div>
@ -34,5 +18,4 @@
<div id="footer"> <div id="footer">
<div></div> <div></div>
</div> </div>
</body> {% endblock %}
</html>

View File

@ -29,6 +29,9 @@
<Compile Include="debug storage.py"> <Compile Include="debug storage.py">
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>
<Compile Include="leagues.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="main_controller.py"> <Compile Include="main_controller.py">
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>
@ -58,10 +61,6 @@
<Content Include="ids" /> <Content Include="ids" />
<Content Include="matteo.db" /> <Content Include="matteo.db" />
<Content Include="static\discord.png" /> <Content Include="static\discord.png" />
<Content Include="static\game.html" />
<Content Include="static\games_page.css" />
<Content Include="static\loader.js" />
<Content Include="static\prism.png" />
<Content Include="templates\game.html" /> <Content Include="templates\game.html" />
<Content Include="templates\index.html" /> <Content Include="templates\index.html" />
</ItemGroup> </ItemGroup>

View File

@ -1,4 +1,4 @@
import discord, json, math, os, roman, games, asyncio, random, main_controller, threading, time import discord, json, math, os, roman, games, asyncio, random, main_controller, threading, time, urllib, leagues
import database as db import database as db
import onomancer as ono import onomancer as ono
from flask import Flask from flask import Flask
@ -99,7 +99,7 @@ class ShowIdolCommand(Command):
class ShowPlayerCommand(Command): class ShowPlayerCommand(Command):
name = "showplayer" name = "showplayer"
template = "m;showplayer [name]" template = "m;showplayer [name]"
description = "Displays any name's stars in a nice discord embed, there's a limit of 70 characters. That should be *plenty*. Note: if you want to lookup a lot of different players you can do it on onomancer instead of spamming this command a bunch and clogging up discord: https://onomancer.sibr.dev/reflect" description = "Displays any name's stars in a nice discord embed, there's a limit of 70 characters. That should be *plenty*. Note: if you want to lookup a lot of different players you can do it on onomancer here instead of spamming this command a bunch and clogging up discord: <https://onomancer.sibr.dev/reflect>"
async def execute(self, msg, command): async def execute(self, msg, command):
player_name = json.loads(ono.get_stats(command.split(" ",1)[1])) player_name = json.loads(ono.get_stats(command.split(" ",1)[1]))
@ -108,51 +108,47 @@ class ShowPlayerCommand(Command):
class StartGameCommand(Command): class StartGameCommand(Command):
name = "startgame" name = "startgame"
template = "m;startgame [away] [home] [innings]" template = "m;startgame [away] [home] [innings]"
description ="""Starts a game with premade teams made using saveteam, use this command at the top of a list followed by each of these in a new line (shift+enter in discord, or copy+paste from notepad): description ="""Starts a game with premade teams made using saveteam, use this command at the top of a list followed by each of these in a new line (shift+enter in discord, or copy+paste from notepad) (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.):
- the away team's name. - the away team's name.
- the home team's name. - the home team's name.
- and finally, optionally, the number of innings, which must be greater than 2 and less than 31. if not included it will default to 9.""" - and finally, optionally, the number of innings, which must be greater than 2 and less than 31. if not included it will default to 9."""
async def execute(self, msg, command): async def execute(self, msg, command):
league = None league = None
day = None
if config()["game_freeze"]: if config()["game_freeze"]:
await msg.channel.send("Patch incoming. We're not allowing new games right now.") await msg.channel.send("Patch incoming. We're not allowing new games right now.")
return return
if "-l " in command.split("\n")[0]: if "-l " in command.split("\n")[0]:
league = command.split("\n")[0].split("-l ")[1] league = command.split("\n")[0].split("-l ")[1].split("-")[0].strip()
elif "--league " in command.split("\n")[0]: elif "--league " in command.split("\n")[0]:
league = command.split("\n")[0].split("--league ")[1] 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
elif "--day " in command.split("\n")[0]:
day = int(command.split("\n")[0].split("--day ")[1].split("-")[0].strip())-1
except ValueError:
await msg.channel.send("Make sure you put an integer after the -d flag.")
return
innings = None innings = None
try: try:
team_name1 = command.split("\n")[1].strip() team_name1 = command.split("\n")[1].strip()
team1 = games.get_team(team_name1) team1 = get_team_fuzzy_search(team_name1)
if team1 is None:
teams = games.search_team(team_name1.lower())
if len(teams) == 1:
team1 = teams[0]
team_name2 = command.split("\n")[2].strip() team_name2 = command.split("\n")[2].strip()
team2 = games.get_team(team_name2) team2 = get_team_fuzzy_search(team_name2)
if team2 is None:
teams = games.search_team(team_name2.lower())
if len(teams) == 1:
team2 = teams[0]
innings = int(command.split("\n")[3]) innings = int(command.split("\n")[3])
except IndexError: except IndexError:
try: try:
team_name1 = command.split("\n")[1].strip() team_name1 = command.split("\n")[1].strip()
team1 = games.get_team(team_name1) team1 = get_team_fuzzy_search(team_name1)
if team1 is None:
teams = games.search_team(team_name1.lower())
if len(teams) == 1:
team1 = teams[0]
team_name2 = command.split("\n")[2].strip() team_name2 = command.split("\n")[2].strip()
team2 = games.get_team(team_name2) team2 = get_team_fuzzy_search(team_name2)
if team2 is None:
teams = games.search_team(team_name2.lower())
if len(teams) == 1:
team2 = teams[0]
except IndexError: except IndexError:
await msg.channel.send("We need at least three lines: startgame, away team, and home team are required. Optionally, the number of innings can go at the end, if you want a change of pace.") await msg.channel.send("We need at least three lines: startgame, away team, and home team are required. Optionally, the number of innings can go at the end, if you want a change of pace.")
return return
@ -164,12 +160,15 @@ class StartGameCommand(Command):
await msg.channel.send("Anything less than 2 innings isn't even an outing. Try again.") await msg.channel.send("Anything less than 2 innings isn't even an outing. Try again.")
return return
elif innings is not None and innings > 30 and msg.author.id not in config()["owners"]: elif innings is not None and innings > 200 and msg.author.id not in config()["owners"]:
await msg.channel.send("Y'all can't behave, so we've limited games to 30 innings. Ask xvi to start it with more if you really want to.") await msg.channel.send("Y'all can behave, so we've upped the limit on game length to 200 innings.")
return return
if team1 is not None and team2 is not None: if team1 is not None and team2 is not None:
game = games.game(msg.author.name, team1, team2, length=innings) game = games.game(team1.finalize(), team2.finalize(), length=innings)
if day is not None:
game.teams['away'].set_pitcher(rotation_slot = day)
game.teams['home'].set_pitcher(rotation_slot = day)
channel = msg.channel channel = msg.channel
await msg.delete() await msg.delete()
@ -179,6 +178,22 @@ class StartGameCommand(Command):
await msg.channel.send("We can't find one or both of those teams. Check your staging, chief.") await msg.channel.send("We can't find one or both of those teams. Check your staging, chief.")
return return
class StartRandomGameCommand(Command):
name = "randomgame"
template = "m;randomgame"
description = "Starts a 9-inning game between 2 entirely random teams. Embrace chaos!"
async def execute(self, msg, command):
channel = msg.channel
await msg.delete()
await channel.send("Rolling the bones...")
teamslist = games.get_all_teams()
game = games.game(random.choice(teamslist).finalize(), random.choice(teamslist).finalize())
game_task = asyncio.create_task(watch_game(channel, game, user="the winds of chaos"))
await game_task
class SetupGameCommand(Command): class SetupGameCommand(Command):
name = "setupgame" name = "setupgame"
template = "m;setupgame" template = "m;setupgame"
@ -205,13 +220,20 @@ class SetupGameCommand(Command):
class SaveTeamCommand(Command): class SaveTeamCommand(Command):
name = "saveteam" name = "saveteam"
template = "m;saveteam [name] [slogan] [players]" template = """m;saveteam
[name]
[slogan]
[lineup]
[rotation]"""
description = """Saves a team to the database allowing it to be used for games. Send this command at the top of a list, with entries separated by new lines (shift+enter in discord, or copy+paste from notepad). description = """Saves a team to the database allowing it to be used for games. Send this command at the top of a list, with entries separated by new lines (shift+enter in discord, or copy+paste from notepad).
- the first line of the list is your team's name (cannot contain emoji). - the first line of the list is your team's name.
- the second line is your team's icon and slogan, this should begin with an emoji followed by a space, followed by a short slogan. - the second line is the team's icon and slogan, generally this is an emoji followed by a space, followed by a short slogan.
- the third line must be blank.
- the next lines are your batters' names in the order you want them to appear in your lineup, lineups can contain any number of batters between 1 and 12. - the next lines are your batters' names in the order you want them to appear in your lineup, lineups can contain any number of batters between 1 and 12.
- the final line is your pitcher's name. - there must be another blank line between your batters and your pitchers.
if you did it correctly, you'll get a team embed with a prompt to confirm. hit the 👍 and it'll be saved.""" - the final lines are the names of the pitchers in your rotation, rotations can contain any number of pitchers between 1 and 8.
If you did it correctly, you'll get a team embed with a prompt to confirm. hit the 👍 and your team will be saved!"""
async def execute(self, msg, command): async def execute(self, msg, command):
if db.get_team(command.split('\n',1)[1].split("\n")[0]) == None: if db.get_team(command.split('\n',1)[1].split("\n")[0]) == None:
@ -226,7 +248,7 @@ if you did it correctly, you'll get a team embed with a prompt to confirm. hit t
class ImportCommand(Command): class ImportCommand(Command):
name = "import" name = "import"
template = "m;import [onomancer collection URL]" template = "m;import [onomancer collection URL]"
description = "Imports an onomancer collection as a new team. You can use the new onomancer simsim setting to ensure compatibility." description = "Imports an onomancer collection as a new team. You can use the new onomancer simsim setting to ensure compatibility. Similarly to saveteam, you'll get a team embed with a prompt to confirm, hit the 👍 and your team will be saved!"
async def execute(self, msg, command): async def execute(self, msg, command):
team_raw = ono.get_collection(command.strip()) team_raw = ono.get_collection(command.strip())
@ -243,19 +265,15 @@ class ImportCommand(Command):
class ShowTeamCommand(Command): class ShowTeamCommand(Command):
name = "showteam" name = "showteam"
template = "m;showteam [name]" template = "m;showteam [name]"
description = "Shows information about any saved team." description = "Shows the lineup, rotation, and slogan of any saved team in a discord embed with primary stat star ratings for all of the players. This command has fuzzy search so you don't need to type the full name of the team as long as you give enough to identify the team you're looking for."
async def execute(self, msg, command): async def execute(self, msg, command):
team_name = command.strip() team_name = command.strip()
team = games.get_team(team_name) team = get_team_fuzzy_search(team_name)
if team is not None: if team is not None:
await msg.channel.send(embed=build_team_embed(team)) await msg.channel.send(embed=build_team_embed(team))
else: return
teams = games.search_team(team_name.lower()) await msg.channel.send("Can't find that team, boss. Typo?")
if len(teams) == 1:
await msg.channel.send(embed=build_team_embed(teams[0]))
else:
await msg.channel.send("Can't find that team, boss. Typo?")
class ShowAllTeamsCommand(Command): class ShowAllTeamsCommand(Command):
name = "showallteams" name = "showallteams"
@ -287,6 +305,126 @@ class CreditCommand(Command):
async def execute(self, msg, command): async def execute(self, msg, command):
await msg.channel.send("Our avatar was graciously provided to us, with permission, by @HetreaSky on Twitter.") await msg.channel.send("Our avatar was graciously provided to us, with permission, by @HetreaSky on Twitter.")
class SwapPlayerCommand(Command):
name = "swapsection"
template = """m;swapsection
[team name]
[player name]"""
description = "Swaps a player from your lineup to the end of your rotation or your rotation to the end of your lineup. Requires team ownership and exact spelling of team name."
async def execute(self, msg, command):
try:
team_name = command.split("\n")[1].strip()
player_name = command.split("\n")[2].strip()
team, owner_id = games.get_team_and_owner(team_name)
if team is None:
await msg.channel.send("Can't find that team, boss. Typo?")
return
elif owner_id != msg.author.id and msg.author.id not in config()["owners"]:
await msg.channel.send("You're not authorized to mess with this team. Sorry, boss.")
return
elif not team.swap_player(player_name):
await msg.channel.send("Either we can't find that player, you've got no space on the other side, or they're your last member of that side of the roster. Can't field an empty lineup, and we *do* have rules, chief.")
return
else:
await msg.channel.send(embed=build_team_embed(team))
games.update_team(team)
await msg.channel.send("Paperwork signed, stamped, copied, and faxed up to the goddess. Xie's pretty quick with this stuff.")
except IndexError:
await msg.channel.send("Three lines, remember? Command, then team, then name.")
class MovePlayerCommand(Command):
name = "moveplayer"
template = """m;moveplayer
[team name]
[player name]
[new lineup/rotation position number] (indexed with 1 being the top)"""
description = "Moves a player within your lineup or rotation. If you want to instead move a player from your rotation to your lineup or vice versa, use m;swapsection instead. Requires team ownership and exact spelling of team name."
async def execute(self, msg, command):
try:
team_name = command.split("\n")[1].strip()
player_name = command.split("\n")[2].strip()
team, owner_id = games.get_team_and_owner(team_name)
try:
new_pos = int(command.split("\n")[3].strip())
except ValueError:
await msg.channel.send("Hey, quit being cheeky. We're just trying to help. Third line has to be a natural number, boss.")
return
if owner_id != msg.author.id and msg.author.id not in config()["owners"]:
await msg.channel.send("You're not authorized to mess with this team. Sorry, boss.")
return
elif not team.slide_player(player_name, new_pos):
await msg.channel.send("You either gave us a number that was bigger than your current roster, or we couldn't find the player on the team. Try again.")
return
else:
await msg.channel.send(embed=build_team_embed(team))
games.update_team(team)
await msg.channel.send("Paperwork signed, stamped, copied, and faxed up to the goddess. Xie's pretty quick with this stuff.")
except IndexError:
await msg.channel.send("Four lines, remember? Command, then team, then name, and finally, new spot on the lineup or rotation.")
class AddPlayerCommand(Command):
name = "addplayer"
template = """m;addplayer pitcher (or m;addplayer batter)
[team name]
[player name]"""
description = "Adds a new player to the end of your team, either in the lineup or the rotation depending on which version you use. Requires team ownership and exact spelling of team name."
async def execute(self, msg, command):
try:
team_name = command.split("\n")[1].strip()
player_name = command.split("\n")[2].strip()
team, owner_id = games.get_team_and_owner(team_name)
if owner_id != msg.author.id and msg.author.id not in config()["owners"]:
await msg.channel.send("You're not authorized to mess with this team. Sorry, boss.")
return
new_player = games.player(ono.get_stats(player_name))
if "batter" in command.split("\n")[0]:
if not team.add_lineup(new_player)[0]:
await msg.channel.send("Too many batters 🎶")
return
elif "pitcher" in command.split("\n")[0]:
if not team.add_pitcher(new_player):
await msg.channel.send("8 pitchers is quite enough, we think.")
return
await msg.channel.send(embed=build_team_embed(team))
games.update_team(team)
await msg.channel.send("Paperwork signed, stamped, copied, and faxed up to the goddess. Xie's pretty quick with this stuff.")
except IndexError:
await msg.channel.send("Three lines, remember? Command, then team, then name.")
class RemovePlayerCommand(Command):
name = "removeplayer"
template = """m;removeplayer
[team name]
[player name]"""
description = "Removes a player from your team. If there are multiple copies of the same player on a team this will only delete the first one. Requires team ownership and exact spelling of team name."
async def execute(self, msg, command):
try:
team_name = command.split("\n")[1].strip()
player_name = command.split("\n")[2].strip()
team, owner_id = games.get_team_and_owner(team_name)
if owner_id != msg.author.id and msg.author.id not in config()["owners"]:
await msg.channel.send("You're not authorized to mess with this team. Sorry, boss.")
return
if not team.delete_player(player_name):
await msg.channel.send("We've got bad news: that player isn't on your team. The good news is that... that player isn't on your team?")
return
else:
await msg.channel.send(embed=build_team_embed(team))
games.update_team(team)
await msg.channel.send("Paperwork signed, stamped, copied, and faxed up to the goddess. Xie's pretty quick with this stuff.")
except IndexError:
await msg.channel.send("Three lines, remember? Command, then team, then name.")
class HelpCommand(Command): class HelpCommand(Command):
name = "help" name = "help"
template = "m;help [command]" template = "m;help [command]"
@ -310,7 +448,7 @@ class HelpCommand(Command):
class DeleteTeamCommand(Command): class DeleteTeamCommand(Command):
name = "deleteteam" name = "deleteteam"
template = "m;deleteteam [name]" template = "m;deleteteam [name]"
description = "Allows you to delete the team with the provided name if you are the owner of it, Gives a confirmation first to prevent accidental deletions. If it isn't letting you delete your team, you probably created it before teams having owners was a thing, contact xvi and xie can assign you as the owner." description = "Allows you to delete the team with the provided name. You'll get an embed with a confirmation to prevent accidental deletions. Hit the 👍 and your team will be deleted.. Requires team ownership. If you are the owner and the bot is telling you it's not yours, contact xvi and xie can assist."
async def execute(self, msg, command): async def execute(self, msg, command):
team_name = command.strip() team_name = command.strip()
@ -331,7 +469,6 @@ class AssignOwnerCommand(Command):
return user.id in config()["owners"] return user.id in config()["owners"]
async def execute(self, msg, command): async def execute(self, msg, command):
#try:
new_owner = msg.mentions[0] new_owner = msg.mentions[0]
team_name = command.strip().split(new_owner.mention+" ")[1] team_name = command.strip().split(new_owner.mention+" ")[1]
print(team_name) print(team_name)
@ -339,8 +476,43 @@ class AssignOwnerCommand(Command):
await msg.channel.send(f"{team_name} is now owned by {new_owner.display_name}. Don't break it.") await msg.channel.send(f"{team_name} is now owned by {new_owner.display_name}. Don't break it.")
else: else:
await msg.channel.send("We couldn't find that team. Typo?") await msg.channel.send("We couldn't find that team. Typo?")
#except:
#await msg.channel.send("We hit a snag. Tell xvi.") class StartTournamentCommand(Command):
name = "starttournament"
template = """m;starttournament
[tournament name]
[list of teams, each on a new line]"""
description = "Starts a randomly seeded tournament with up to 64 provided teams, automatically adding byes as necessary. All series have a 5 minute break between games and by default there is a 10 minute break between rounds. The current tournament format is:\nBest of 5 until the finals, which are Best of 7."
async def execute(self, msg, command):
to_parse = command.split("\n")[0]
if "--rounddelay " in to_parse:
try:
round_delay = int(to_parse.split("--rounddelay ")[1].split(" ")[0])
except ValueError:
await msg.channel.send("The delay between rounds should be a whole number.")
return
if round_delay < 1 or round_delay > 120:
await msg.channel.send("The delay between rounds has to be between 1 and 120 minutes.")
else:
round_delay = 10
tourney_name = command.split("\n")[1]
list_of_team_names = command.split("\n")[2:]
team_dic = {}
for name in list_of_team_names:
team = get_team_fuzzy_search(name.strip())
if team == None:
await msg.channel.send(f"We couldn't find {name}. Try again?")
return
team_dic[team] = {"wins": 0}
id = random.randint(1111,9999)
tourney = leagues.tournament(tourney_name, team_dic, id=id, secs_between_rounds = round_delay * 60)
tourney.build_bracket(random_sort = True)
await start_tournament_round(msg.channel, tourney)
commands = [ commands = [
@ -353,11 +525,17 @@ commands = [
#SetupGameCommand(), #SetupGameCommand(),
SaveTeamCommand(), SaveTeamCommand(),
ImportCommand(), ImportCommand(),
SwapPlayerCommand(),
MovePlayerCommand(),
AddPlayerCommand(),
RemovePlayerCommand(),
DeleteTeamCommand(), DeleteTeamCommand(),
ShowTeamCommand(), ShowTeamCommand(),
ShowAllTeamsCommand(), ShowAllTeamsCommand(),
SearchTeamsCommand(), SearchTeamsCommand(),
StartGameCommand(), StartGameCommand(),
StartTournamentCommand(),
StartRandomGameCommand(),
CreditCommand(), CreditCommand(),
RomanCommand(), RomanCommand(),
HelpCommand(), HelpCommand(),
@ -365,7 +543,7 @@ commands = [
client = discord.Client() client = discord.Client()
gamesarray = [] gamesarray = []
gamesqueue = [] active_tournaments = []
setupmessages = {} setupmessages = {}
thread1 = threading.Thread(target=main_controller.update_loop) thread1 = threading.Thread(target=main_controller.update_loop)
@ -440,19 +618,6 @@ async def on_message(msg):
except CommandError as ce: except CommandError as ce:
await msg.channel.send(str(ce)) await msg.channel.send(str(ce))
async def start_game(channel):
msg = await channel.send("Play ball!")
await asyncio.sleep(4)
newgame = games.debug_game()
gamesarray.append(newgame)
while not newgame.over:
state = newgame.gamestate_update_full()
if not state.startswith("Game over"):
await msg.edit(content=state)
await asyncio.sleep(3)
await channel.send(state)
gamesarray.pop()
async def setup_game(channel, owner, newgame): async def setup_game(channel, owner, newgame):
newgame.owner = owner newgame.owner = owner
@ -579,17 +744,37 @@ Creator, type `{newgame.name} done` to finalize lineups.""")
await game_task await game_task
async def watch_game(channel, newgame, user = None, league = None): async def watch_game(channel, newgame, user = None, league = None):
blank_emoji = discord.utils.get(client.emojis, id = 790899850295509053) newgame, state_init = prepare_game(newgame)
empty_base = discord.utils.get(client.emojis, id = 790899850395779074)
occupied_base = discord.utils.get(client.emojis, id = 790899850320543745) if league is not None:
out_emoji = discord.utils.get(client.emojis, id = 791578957241778226) discrim_string = league
in_emoji = discord.utils.get(client.emojis, id = 791578957244792832) state_init["is_league"] = True
elif user is not None:
if isinstance(user, str):
discrim_string = f"Started by {user}"
else:
discrim_string = f"Started by {user.name}"
state_init["is_league"] = False
else:
discrim_string = "Unclaimed game."
state_init["is_league"] = False
timestamp = str(time.time() * 1000.0)
ext = "?game="+timestamp
if league is not None:
ext += "&league=" + urllib.parse.quote_plus(league)
await channel.send(f"{newgame.teams['away'].name} vs. {newgame.teams['home'].name}, starting at {config()['simmadome_url']+ext}")
gamesarray.append((newgame, channel, user, timestamp))
main_controller.master_games_dic[timestamp] = (newgame, state_init, discrim_string)
def prepare_game(newgame, league = None, weather_name = None):
if weather_name is None:
weathers = games.all_weathers()
newgame.weather = weathers[random.choice(list(weathers.keys()))]
await asyncio.sleep(1)
weathers = games.all_weathers()
newgame.weather = weathers[random.choice(list(weathers.keys()))]
state_init = { state_init = {
"away_name" : newgame.teams['away'].name, "away_name" : newgame.teams['away'].name,
"home_name" : newgame.teams['home'].name, "home_name" : newgame.teams['home'].name,
@ -603,28 +788,135 @@ async def watch_game(channel, newgame, user = None, league = None):
"end_delay" : 10 "end_delay" : 10
} }
if league is not None: if league is None:
discrim_string = league
state_init["is_league"] = True
elif user is not None:
discrim_string = f"Started by {user.name}"
state_init["is_league"] = False state_init["is_league"] = False
else: else:
discrim_string = "Unclaimed game." state_init["is_league"] = True
state_init["is_league"] = False
await channel.send(f"{newgame.teams['away'].name} vs. {newgame.teams['home'].name}, starting at {config()['simmadome_url']}") if newgame.weather.name == "Heavy Snow":
timestamp = str(time.time() * 1000.0) newgame.weather.counter_away = random.randint(0,len(newgame.teams['away'].lineup)-1)
gamesarray.append((newgame, channel, user, timestamp)) newgame.weather.counter_home = random.randint(0,len(newgame.teams['home'].lineup)-1)
return newgame, state_init
async def start_tournament_round(channel, tourney, seeding = None):
current_games = []
if tourney.bracket is None:
if seeding is None:
tourney.build_bracket(random_sort=True)
games_to_start = tourney.bracket.get_bottom_row()
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)
this_game, state_init = prepare_game(this_game)
state_init["is_league"] = True
state_init["title"] = f"0 - 0"
discrim_string = tourney.name
timestamp = str(time.time() * 1000.0 + random.randint(0,3000))
current_games.append((this_game, timestamp))
main_controller.master_games_dic[timestamp] = (this_game, state_init, discrim_string)
ext = "?league=" + urllib.parse.quote_plus(tourney.name)
if tourney.round_check(): #if finals
await channel.send(f"The {tourney.name} finals are starting now, at {config()['simmadome_url']+ext}")
finals = True
else:
await channel.send(f"{len(current_games)} games started for the {tourney.name} tournament, at {config()['simmadome_url']+ext}")
finals = False
await tourney_round_watcher(channel, tourney, current_games, config()['simmadome_url']+ext, finals)
async def continue_tournament_series(tourney, queue, games_list, wins_in_series):
for oldgame in queue:
away_team = games.get_team(oldgame.teams["away"].name)
home_team = games.get_team(oldgame.teams["home"].name)
this_game = games.game(away_team.finalize(), home_team.finalize(), length = tourney.game_length)
this_game, state_init = prepare_game(this_game)
state_init["is_league"] = True
state_init["title"] = f"{wins_in_series[oldgame.teams['away'].name]} - {wins_in_series[oldgame.teams['home'].name]}"
discrim_string = tourney.name
timestamp = str(time.time() * 1000.0 + random.randint(0,3000))
games_list.append((this_game, timestamp))
main_controller.master_games_dic[timestamp] = (this_game, state_init, discrim_string)
return games_list
async def tourney_round_watcher(channel, tourney, games_list, filter_url, finals = False):
tourney.active = True
active_tournaments.append(tourney)
wins_in_series = {}
winner_list = []
while tourney.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 main_controller.master_games_dic[key][1]["end_delay"] <= 9:
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():
wins_in_series[game.teams["away"].name] = 0
winner_name = game.teams['home'].name if game.teams['home'].score > game.teams['away'].score else game.teams['away'].name
if winner_name in wins_in_series.keys():
wins_in_series[winner_name] += 1
else:
wins_in_series[winner_name] = 1
final_embed = game_over_embed(game)
await channel.send(f"A {tourney.name} game just ended!")
await channel.send(embed=final_embed)
if wins_in_series[winner_name] >= int((tourney.series_length+1)/2) and not finals:
winner_list.append(winner_name)
elif wins_in_series[winner_name] >= int((tourney.finals_length+1)/2):
winner_list.append(winner_name)
else:
queued_games.append(game)
games_list.pop(i)
break
except:
print("something went wrong in tourney_watcher")
await asyncio.sleep(4)
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)
await channel.send(f"{len(queued_games)} games for {tourney.name}, starting at {filter_url}")
games_list = await continue_tournament_series(tourney, queued_games, games_list, wins_in_series)
else:
tourney.active = False
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!")
await channel.send(embed=embed)
active_tournaments.pop(active_tournaments.index(tourney))
return
tourney.bracket.set_winners_dive(winner_list)
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"""
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 start_tournament_round(channel, tourney)
main_controller.master_games_dic[timestamp] = (newgame, state_init, discrim_string)
async def play_from_queue(channel, game, user_mention):
await channel.send(f"{user_mention}, your game's ready.")
game_task = asyncio.create_task(watch_game(channel, game))
await game_task
async def team_delete_confirm(channel, team, owner): async def team_delete_confirm(channel, team, owner):
team_msg = await channel.send(embed=build_team_embed(team)) team_msg = await channel.send(embed=build_team_embed(team))
@ -660,7 +952,10 @@ def build_team_embed(team):
for player in team.lineup: for player in team.lineup:
lineup_string += f"{player.name} {player.star_string('batting_stars')}\n" lineup_string += f"{player.name} {player.star_string('batting_stars')}\n"
embed.add_field(name="Pitcher:", value=f"{team.pitcher.name} {team.pitcher.star_string('pitching_stars')}", inline = False) rotation_string = ""
for player in team.rotation:
rotation_string += f"{player.name} {player.star_string('pitching_stars')}\n"
embed.add_field(name="Rotation:", value=rotation_string, inline = False)
embed.add_field(name="Lineup:", value=lineup_string, inline = False) embed.add_field(name="Lineup:", value=lineup_string, inline = False)
embed.set_footer(text=team.slogan) embed.set_footer(text=team.slogan)
return embed return embed
@ -689,8 +984,8 @@ def team_from_collection(newteam_json):
raise CommandError("We've given you 100 characters for the slogan. Discord puts limits on us and thus, we put limits on you. C'est la vie.") raise CommandError("We've given you 100 characters for the slogan. Discord puts limits on us and thus, we put limits on you. C'est la vie.")
if len(newteam_json["lineup"]) > 20: if len(newteam_json["lineup"]) > 20:
raise CommandError("20 players in the lineup, maximum. We're being really generous here.") raise CommandError("20 players in the lineup, maximum. We're being really generous here.")
if not len(newteam_json["rotation"]) == 1: if not len(newteam_json["rotation"]) > 8:
raise CommandError("One and only one pitcher per team, thanks.") raise CommandError("8 pitchers on the rotation, max. That's a *lot* of pitchers.")
for player in newteam_json["lineup"] + newteam_json["rotation"]: for player in newteam_json["lineup"] + newteam_json["rotation"]:
if len(player["name"]) > 70: if len(player["name"]) > 70:
raise CommandError(f"{player['name']} is too long, chief. 70 or less.") raise CommandError(f"{player['name']} is too long, chief. 70 or less.")
@ -710,14 +1005,22 @@ def team_from_message(command):
roster = command.split("\n",1)[1].split("\n") roster = command.split("\n",1)[1].split("\n")
newteam.name = roster[0] #first line is team name newteam.name = roster[0] #first line is team name
newteam.slogan = roster[1] #second line is slogan newteam.slogan = roster[1] #second line is slogan
for rosternum in range(2,len(roster)-1): if not roster[2].strip() == "":
raise CommandError("The third line should be blank. It wasn't, so just in case, we've not done anything on our end.")
pitchernum = len(roster)-2
for rosternum in range(3,len(roster)-1):
if roster[rosternum] != "": if roster[rosternum] != "":
if len(roster[rosternum]) > 70: if len(roster[rosternum]) > 70:
raise CommandError(f"{roster[rosternum]} is too long, chief. 70 or less.") raise CommandError(f"{roster[rosternum]} is too long, chief. 70 or less.")
newteam.add_lineup(games.player(ono.get_stats(roster[rosternum].rstrip()))) newteam.add_lineup(games.player(ono.get_stats(roster[rosternum].rstrip())))
if len(roster[len(roster)-1]) > 70: else:
raise CommandError(f"{roster[len(roster)-1]} is too long, chief. 70 or less.") pitchernum = rosternum + 1
newteam.set_pitcher(games.player(ono.get_stats(roster[len(roster)-1].rstrip()))) #last line is pitcher name break
for rosternum in range(pitchernum, len(roster)):
if len(roster[rosternum]) > 70:
raise CommandError(f"{roster[len(roster)-1]} is too long, chief. 70 or less.")
newteam.add_pitcher(games.player(ono.get_stats(roster[rosternum].rstrip())))
if len(newteam.name) > 30: if len(newteam.name) > 30:
raise CommandError("Team names have to be less than 30 characters! Try again.") raise CommandError("Team names have to be less than 30 characters! Try again.")
@ -762,7 +1065,10 @@ async def team_pages(msg, all_teams, search_term=None):
embed.set_footer(text = f"Page {page+1} of {page_max}") embed.set_footer(text = f"Page {page+1} of {page_max}")
for i in range(0,25): for i in range(0,25):
try: try:
embed.add_field(name=all_teams[i+25*page].name, value=all_teams[i+25*page].slogan) if all_teams[i+25*page].slogan.strip() != "":
embed.add_field(name=all_teams[i+25*page].name, value=all_teams[i+25*page].slogan)
else:
embed.add_field(name=all_teams[i+25*page].name, value="404: Slogan not found")
except: except:
break break
pages.append(embed) pages.append(embed)
@ -797,36 +1103,64 @@ async def game_watcher():
for i in range(0,len(this_array)): for i in range(0,len(this_array)):
game, channel, user, key = this_array[i] game, channel, user, key = this_array[i]
if game.over and main_controller.master_games_dic[key][1]["end_delay"] <= 9: if game.over and main_controller.master_games_dic[key][1]["end_delay"] <= 9:
title_string = f"{game.teams['away'].name} at {game.teams['home'].name} ended after {game.inning-1} innings" final_embed = game_over_embed(game)
if (game.inning - 1) > game.max_innings: #if extra innings
title_string += f" with {game.inning - (game.max_innings+1)} extra innings."
else:
title_string += "."
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"
if game.victory_lap and winning_team == game.teams['home'].name:
winstring += f"{winning_team} wins with a victory lap!"
elif winning_team == game.teams['home'].name:
winstring += f"{winning_team} wins, shaming {game.teams['away'].name}!"
else:
winstring += f"{winning_team} wins!"
if user is not None: if user is not None:
await channel.send(f"{user.mention}'s game just ended.") await channel.send(f"{user.mention}'s game just ended.")
else: else:
await channel.send("A game started from this channel just ended.") await channel.send("A game started from this channel just ended.")
final_embed = discord.Embed(color=discord.Color.dark_purple(), title=title_string)
final_embed.add_field(name="Final score:", value=winstring)
await channel.send(embed=final_embed) await channel.send(embed=final_embed)
gamesarray.pop(i) gamesarray.pop(i)
break break
except: except:
print("something broke in game_watcher") print("something broke in game_watcher")
await asyncio.sleep(4)
await asyncio.sleep(6) def game_over_embed(game):
title_string = f"{game.teams['away'].name} at {game.teams['home'].name} ended after {game.inning-1} innings"
if (game.inning - 1) > game.max_innings: #if extra innings
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"
if game.victory_lap and winning_team == game.teams['home'].name:
winstring += f"{winning_team} wins with a victory lap!"
elif winning_team == game.teams['home'].name:
winstring += f"{winning_team} wins, shaming {game.teams['away'].name}!"
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)
return embed
def get_team_fuzzy_search(team_name):
team = games.get_team(team_name)
if team is None:
teams = games.search_team(team_name.lower())
if len(teams) == 1:
team = teams[0]
return team
#test_bracket = {
# "Milwaukee Lockpicks" : {"wins": 4, "rd": 0},
# "Madagascar Penguins" : {"wins": 2, "rd": 0},
# "Twin Cities Evening" : {"wins": 1, "rd": 0},
# "Washington State Houses" : {"wins": 9, "rd": 0},
# "Appalachian Underground" : {"wins": 8, "rd": 0},
# "Pacific2 Rams" : {"wins": 3, "rd": 0},
# "New Jersey Radio" : {"wins": 11, "rd": 0},
# "Moline Jolenes" : {"wins": 6, "rd": 0},
# "California Commissioners" : {"wins": 10, "rd": 0},
# "Pigeons Reckoning" : {"wins": 7, "rd": 0},
# "Kernow Technologists" : {"wins": 5, "rd": 0}
# }
#tourney = leagues.tournament("Test Tourney", test_bracket, max_innings=3)
#tourney.build_bracket(by_wins=True)
#tourney.bracket.set_winners_dive(['Twin Cities Evening','Madagascar Penguins', 'Pacific2 Rams'])
#print(tourney.bracket.this_bracket)
client.run(config()["token"]) client.run(config()["token"])