From 47b5788a09e37c246ebc640c7c1223c3b78e7294 Mon Sep 17 00:00:00 2001 From: Astrid Date: Wed, 6 Jan 2021 11:53:51 +0100 Subject: [PATCH 01/22] Store bot data in a data/ directory --- database.py | 3 ++- games.py | 11 ++++++++--- the_prestige.py | 10 +++++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/database.py b/database.py index a53da43..edccb3b 100644 --- a/database.py +++ b/database.py @@ -2,12 +2,13 @@ import os, json, datetime, re import sqlite3 as sql +data_dir = "data" def create_connection(): #create connection, create db if doesn't exist conn = None try: - conn = sql.connect("matteo.db") + conn = sql.connect(os.path.join(data_dir, "matteo.db")) return conn except: print("oops, db connection no work") diff --git a/games.py b/games.py index bc7a716..8ca356d 100644 --- a/games.py +++ b/games.py @@ -2,8 +2,13 @@ import json, random, os, math, jsonpickle from enum import Enum import database as db +data_dir = "data" +games_config_file = os.path.join(data_dir, "games_config.json") + def config(): - if not os.path.exists("games_config.json"): + if not os.path.exists(os.path.dirname(games_config_file)): + os.makedirs(os.path.dirname(games_config_file)) + if not os.path.exists(games_config_file): #generate default config config_dic = { "default_length" : 3, @@ -16,11 +21,11 @@ def config(): "stolen_base_chance_mod" : 1, "stolen_base_success_mod" : 1 } - with open("games_config.json", "w") as config_file: + with open(games_config_file, "w") as config_file: json.dump(config_dic, config_file, indent=4) return config_dic else: - with open("games_config.json") as config_file: + with open(games_config_file) as config_file: return json.load(config_file) def all_weathers(): diff --git a/the_prestige.py b/the_prestige.py index a324c19..87a1959 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -4,6 +4,8 @@ import onomancer as ono from flask import Flask from uuid import uuid4 +data_dir = "data" +config_filename = os.path.join(data_dir, "config.json") class Command: def isauthorized(self, user): @@ -605,7 +607,9 @@ thread1 = threading.Thread(target=main_controller.update_loop) thread1.start() def config(): - if not os.path.exists("config.json"): + if not os.path.exists(os.path.dirname(config_filename)): + os.makedirs(os.path.dirname(config_filename)) + if not os.path.exists(config_filename): #generate default config config_dic = { "token" : "", @@ -617,12 +621,12 @@ def config(): "soulscream channel id" : 0, "game_freeze" : 0 } - with open("config.json", "w") as config_file: + with open(config_filename, "w") as config_file: json.dump(config_dic, config_file, indent=4) print("please fill in bot token and any bot admin discord ids to the new config.json file!") quit() else: - with open("config.json") as config_file: + with open(config_filename) as config_file: return json.load(config_file) @client.event From e44ce70a1c0cb4a042e52d9ed6d1539b83feb436 Mon Sep 17 00:00:00 2001 From: Astrid Date: Wed, 6 Jan 2021 12:03:52 +0100 Subject: [PATCH 02/22] Use sqlite3 write-ahead log --- database.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/database.py b/database.py index edccb3b..d10a056 100644 --- a/database.py +++ b/database.py @@ -9,6 +9,10 @@ def create_connection(): conn = None try: conn = sql.connect(os.path.join(data_dir, "matteo.db")) + + # enable write-ahead log for performance and resilience + conn.execute('pragma journal_mode=wal') + return conn except: print("oops, db connection no work") From 1d3ac175e31ec334dadb9427dc0f2219748d3d3e Mon Sep 17 00:00:00 2001 From: Astrid Date: Wed, 6 Jan 2021 12:04:44 +0100 Subject: [PATCH 03/22] Add WAL files to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1722461..72644a6 100644 --- a/.gitignore +++ b/.gitignore @@ -347,4 +347,6 @@ ids # database matteo.db +matteo.db-wal +matteo.db-shm /matteo_env/Lib/site-packages/flask_socketio/__init__.py From f60da215e34b6870b612f57fa152f21728401929 Mon Sep 17 00:00:00 2001 From: Sakimori Date: Wed, 6 Jan 2021 19:57:53 -0500 Subject: [PATCH 04/22] added new supporter --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 04105fa..c770661 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,4 @@ these folks are helping me a *ton* via patreon, and i cannot possibly thank them - Chris Denmark - Astrid Bek - Kameleon +- Ryan Littleton From 2718f79c19b842ff09f5b76141d6090c853286f0 Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 3 Jan 2021 15:19:48 -0800 Subject: [PATCH 05/22] Draft controller class --- .gitignore | 2 + onomancer.py | 18 +++++++ the_draft.py | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 the_draft.py diff --git a/.gitignore b/.gitignore index 72644a6..6b7e8e2 100644 --- a/.gitignore +++ b/.gitignore @@ -350,3 +350,5 @@ matteo.db matteo.db-wal matteo.db-shm /matteo_env/Lib/site-packages/flask_socketio/__init__.py + +env diff --git a/onomancer.py b/onomancer.py index f270a48..2031ec9 100644 --- a/onomancer.py +++ b/onomancer.py @@ -7,6 +7,7 @@ import database as db onomancer_url = "https://onomancer.sibr.dev/api/" name_stats_hook = "getOrGenerateStats?name=" collection_hook = "getCollection?token=" +names_hook = "getNames" def get_stats(name): player = db.get_stats(name) @@ -36,3 +37,20 @@ def get_collection(collection_url): db.cache_stats(player['name'], json.dumps(player)) return json.dumps(response.json()) + + +def get_names(limit=20, threshold=1): + """ + Get `limit` random players that have at least `threshold` upvotes. + Returns dictionary keyed by player name of stats. + """ + response = requests.get( + onomancer_url + names_hook, + params={ + 'limit': limit, + 'threshold': threshold, + 'with_stats': 1, + 'random': 1, + }, + ) + return {p['name']: p for p in response.json()} diff --git a/the_draft.py b/the_draft.py new file mode 100644 index 0000000..94fba62 --- /dev/null +++ b/the_draft.py @@ -0,0 +1,145 @@ +from collections import namedtuple +import games +import uuid + +import onomancer + +DRAFT_SIZE = 20 +REFRESH_DRAFT_SIZE = 4 # fewer players remaining than this and the list refreshes +DRAFT_ROUNDS = 13 + +Participant = namedtuple('Participant', ['handle', 'team']) +BOOKMARK = Participant(handle="bookmark", team=None) # keep track of start/end of draft round + + +class Draft: + """ + Represents a draft party with n participants constructing their team from a pool + of names. + """ + + _ongoing_drafts = {} + + @classmethod + def make_draft(cls): + draft = cls() + cls._ongoing_drafts[draft._id] = draft + return draft + + def __init__(self): + self._id = str(uuid.uuid4())[:6] + self._participants = [] + self._active_participant = BOOKMARK # draft mutex + self._players = onomancer.get_names(limit=DRAFT_SIZE) + self._round = 0 + + @property + def round(self): + """ + Current draft round. 1 indexed. + """ + return self._round + + @property + def active_drafter(self): + """ + Handle of whomever is currently up to draft. + """ + return self._active_participant.handle + + def add_participant(self, handle, team_name, slogan): + """ + A participant is someone participating in this draft. Initializes an empty team for them + in memory. + + `handle`: discord @ handle, for ownership and identification + """ + team = games.team() + team.name = team_name + team.slogan = slogan + self._participants.append(Participant(handle=handle, team=team)) + + def start_draft(self): + """ + Call after adding all participants and confirming they're good to go. + """ + self.advance_draft() + + def refresh_players(self): + self._players = onomancer.get_names(limit=DRAFT_SIZE) + + def advance_draft(self): + """ + The participant list is treated as a circular queue with the head being popped off + to act as the draftign mutex. + """ + if self._active_participant == BOOKMARK: + self._round += 1 + self._participants.append(self._active_participant) + self._active_participant = self._participants.pop(0) + + def get_draftees(self): + return list(self._players.keys()) + + def draft_player(self, handle, player_name): + """ + `handle` is the participant's discord handle. + """ + if self._active_participant.handle != handle: + raise ValueError('Invalid drafter') + + player = self._players.get(player_name) + if not player: + # might be some whitespace shenanigans + for name, stats in self._players.items(): + if name.replace('\xa0', ' ') == player_name: + player = stats + break + else: + # still not found + raise ValueError('Player not in draft list') + del self._players[player['name']] + + if len(self._players) <= REFRESH_DRAFT_SIZE: + self.refresh_players() + + if self._round < DRAFT_ROUNDS: + self._active_participant.team.add_lineup(player['name']) + elif self._round == DRAFT_ROUNDS: + self._active_participant.team.set_pitcher(player['name']) + + self.advance_draft() + if self._active_participant == BOOKMARK: + self.advance_draft() + + return player + + def get_teams(self): + teams = {} + teams[self._active_participant.handle] = self._active_participant.team + for participant in self._participants: + teams[participant.handle] = participant.team + return teams + + +if __name__ == '__main__': + # extremely robust testing OC do not steal + # DRAFT_ROUNDS = 3 + draft = Draft.make_draft() + draft.add_participant('@bluh', 'Bluhstein Bluhs', 'bluh bluh bluh') + draft.add_participant('@what', 'Barcelona IDK', 'huh') + draft.start_draft() + + while draft.round <= DRAFT_ROUNDS: + print(draft.get_draftees()) + cmd = input(f'{draft.round} {draft.active_drafter}:') + drafter, player = cmd.split(' ', 1) + try: + draft.draft_player(drafter, player) + except ValueError as e: + print(e) + print(draft.get_teams()) + print(draft.get_teams()['@bluh'].lineup) + print(draft.get_teams()['@bluh'].pitcher) + print(draft.get_teams()['@what'].lineup) + print(draft.get_teams()['@what'].pitcher) From e22a1b3345aa12612b3b263ee8ffffed5c29a284 Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 3 Jan 2021 15:30:00 -0800 Subject: [PATCH 06/22] filter bookmark out of teams --- the_draft.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/the_draft.py b/the_draft.py index 94fba62..aa50892 100644 --- a/the_draft.py +++ b/the_draft.py @@ -116,15 +116,17 @@ class Draft: def get_teams(self): teams = {} - teams[self._active_participant.handle] = self._active_participant.team + if self._active_participant != BOOKMARK: + teams[self._active_participant.handle] = self._active_participant.team for participant in self._participants: - teams[participant.handle] = participant.team + if participant != BOOKMARK: + teams[participant.handle] = participant.team return teams if __name__ == '__main__': # extremely robust testing OC do not steal - # DRAFT_ROUNDS = 3 + DRAFT_ROUNDS = 3 draft = Draft.make_draft() draft.add_participant('@bluh', 'Bluhstein Bluhs', 'bluh bluh bluh') draft.add_participant('@what', 'Barcelona IDK', 'huh') From 996d35ec06852883886673cafe0c0c2e0d0c09b8 Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 3 Jan 2021 15:41:36 -0800 Subject: [PATCH 07/22] more whitepace handling --- the_draft.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/the_draft.py b/the_draft.py index aa50892..bab2b05 100644 --- a/the_draft.py +++ b/the_draft.py @@ -88,11 +88,13 @@ class Draft: if self._active_participant.handle != handle: raise ValueError('Invalid drafter') + player_name = player_name.strip() + player = self._players.get(player_name) if not player: # might be some whitespace shenanigans for name, stats in self._players.items(): - if name.replace('\xa0', ' ') == player_name: + if name.replace('\xa0', ' ').strip() == player_name: player = stats break else: From 5bc2c520ea42c19abe03769947882bb9622f9e3b Mon Sep 17 00:00:00 2001 From: joe Date: Mon, 4 Jan 2021 15:52:34 -0800 Subject: [PATCH 08/22] discord commands --- games.py | 2 +- the_draft.py | 18 ++++--- the_prestige.py | 133 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 8 deletions(-) diff --git a/games.py b/games.py index 8ca356d..b89d970 100644 --- a/games.py +++ b/games.py @@ -806,4 +806,4 @@ class weather(object): self.counter_home = 0 def __str__(self): - return f"{self.emoji} {self.name}" \ No newline at end of file + return f"{self.emoji} {self.name}" diff --git a/the_draft.py b/the_draft.py index bab2b05..4b424d3 100644 --- a/the_draft.py +++ b/the_draft.py @@ -1,5 +1,6 @@ from collections import namedtuple import games +import json import uuid import onomancer @@ -18,12 +19,9 @@ class Draft: of names. """ - _ongoing_drafts = {} - @classmethod def make_draft(cls): draft = cls() - cls._ongoing_drafts[draft._id] = draft return draft def __init__(self): @@ -86,7 +84,7 @@ class Draft: `handle` is the participant's discord handle. """ if self._active_participant.handle != handle: - raise ValueError('Invalid drafter') + raise ValueError(f'{self._active_participant.handle} is drafting, not you') player_name = player_name.strip() @@ -99,16 +97,16 @@ class Draft: break else: # still not found - raise ValueError('Player not in draft list') + raise ValueError(f'Player `{player_name}` not in draft list') del self._players[player['name']] if len(self._players) <= REFRESH_DRAFT_SIZE: self.refresh_players() if self._round < DRAFT_ROUNDS: - self._active_participant.team.add_lineup(player['name']) + self._active_participant.team.add_lineup(games.player(json.dumps(player))) elif self._round == DRAFT_ROUNDS: - self._active_participant.team.set_pitcher(player['name']) + self._active_participant.team.set_pitcher(games.player(json.dumps(player))) self.advance_draft() if self._active_participant == BOOKMARK: @@ -125,6 +123,12 @@ class Draft: teams[participant.handle] = participant.team return teams + def finish_draft(self): + for handle, team in self.get_teams().items(): + success = games.save_team(team, int(handle[3:-1])) + if not success: + raise Exception(f'Error saving team for {handle}') + if __name__ == '__main__': # extremely robust testing OC do not steal diff --git a/the_prestige.py b/the_prestige.py index 87a1959..6f80414 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -1,6 +1,8 @@ import discord, json, math, os, roman, games, asyncio, random, main_controller, threading, time, urllib, leagues import database as db import onomancer as ono +import random +from the_draft import Draft, DRAFT_ROUNDS from flask import Flask from uuid import uuid4 @@ -14,6 +16,12 @@ class Command: async def execute(self, msg, command): return +class DraftError(Exception): + pass + +class SlowDraftError(DraftError): + pass + class CommandError(Exception): pass @@ -572,6 +580,119 @@ class StartTournamentCommand(Command): await start_tournament_round(channel, tourney) +class DraftPlayerCommand(Command): + name = "draft" + template = "m;draft [playername]" + description = "On your turn during a draft, use this command to pick your player." + + async def execute(self, msg, command): + """ + This is a no-op definition. `StartDraftCommand` handles the orchestration directly, + this is just here to provide a help entry and so the command dispatcher recognizes it + as valid. + """ + pass + + +class StartDraftCommand(Command): + name = "startdraft" + template = "m;startdraft [mention] [teamname] [slogan]" + description = """Starts a draft with an arbitrary number of participants. Send this command at the +top of the list with each mention, teamname, and slogan on a new line (shift+enter in discord). + - The draft will proceed in the order that participants were entered. + - 20 players will be available for draft at a time, and the pool will refresh automatically when it becomes small. + - Each participant will be asked to draft 12 hitters then finally one pitcher. + - The draft will start only once every participant has given a 👍 to begin. + - use the command `m;draft` on your turn to draft someone + """ + + async def execute(self, msg, command): + draft = Draft.make_draft() + mentions = {f'<@!{m.id}>' for m in msg.mentions} + content = msg.content.split('\n')[1:] # drop command out of message + if len(content) % 3: + await msg.channel.send('Invalid list') + raise ValueError('Invalid length') + + for i in range(0, len(content), 3): + handle = content[i].strip() + team_name = content[i + 1].strip() + if games.get_team(team_name): + await msg.channel.send(f'Sorry {handle}, {team_name} already exists') + raise ValueError('Existing team') + slogan = content[i + 2].strip() + draft.add_participant(handle, team_name, slogan) + + success = await self.wait_start(msg.channel, mentions) + if not success: + return + + draft.start_draft() + while draft.round <= DRAFT_ROUNDS: + choosing = 'pitcher' if draft.round == DRAFT_ROUNDS else 'hitter' + await msg.channel.send( + f'Round {draft.round}/{DRAFT_ROUNDS}: {draft.active_drafter}, choose a {choosing}.', + embed=build_draft_embed(draft.get_draftees()), + ) + try: + draft_message = await self.wait_draft(msg.channel, draft) + draft.draft_player(f'<@!{draft_message.author.id}>', draft_message.content.split(' ', 1)[1]) + except SlowDraftError: + player = random.choice(draft.get_draftees()) + await msg.channel.send(f"I'm not waiting forever. You get {player}. Next") + draft.draft_player(draft.active_drafter, player) + except ValueError as e: + await msg.channel.send(str(e)) + + for handle, team in draft.get_teams().items(): + await msg.channel.send(f'{handle} behold your team', embed=build_team_embed(team)) + try: + draft.finish_draft() + except Exception as e: + await msg.channel.send(str(e)) + + async def wait_start(self, channel, mentions): + start_msg = await channel.send('Are we good to go? ' + ' '.join(mentions)) + await start_msg.add_reaction("👍") + await start_msg.add_reaction("👎") + + def react_check(react, user): + return f'<@!{user.id}>' in mentions and react.message == start_msg + + while True: + try: + react, _ = await client.wait_for('reaction_add', timeout=60.0, check=react_check) + if react.emoji == "👎": + await channel.send("Got it, stopping the draft") + return False + if react.emoji == "👍": + reactors = set() + async for user in react.users(): + reactors.add(f'<@!{user.id}>') + if reactors.intersection(mentions) == mentions: + return True + except asyncio.TimeoutError: + await channel.send("Y'all aren't ready.") + return False + return False + + async def wait_draft(self, channel, draft): + + def check(m): + if m.channel != channel: + return False + for prefix in config()['prefix']: + if m.content.startswith(prefix + 'draft'): + return True + return False + + try: + draft_message = await client.wait_for('message', timeout=120.0, check=check) + except asyncio.TimeoutError: + raise SlowDraftError('Too slow') + return draft_message + + commands = [ IntroduceCommand(), CountActiveGamesCommand(), @@ -596,6 +717,8 @@ commands = [ CreditCommand(), RomanCommand(), HelpCommand(), + StartDraftCommand(), + DraftPlayerCommand(), ] client = discord.Client() @@ -1015,6 +1138,16 @@ async def team_delete_confirm(channel, team, owner): return +def build_draft_embed(names): + embed = discord.Embed(color=discord.Color.purple(), title="The Draft") + column_size = 7 + for i in range(0, len(names), column_size): + draft = '\n'.join(names[i:i + column_size]) + embed.add_field(name="-", value=draft, inline=True) + embed.set_footer(text="You must choose") + return embed + + def build_team_embed(team): embed = discord.Embed(color=discord.Color.purple(), title=team.name) lineup_string = "" From 56a1b06d28b223076465db75b828309b5ac72b8f Mon Sep 17 00:00:00 2001 From: joe Date: Mon, 4 Jan 2021 15:54:28 -0800 Subject: [PATCH 09/22] rebase latest master --- the_prestige.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/the_prestige.py b/the_prestige.py index 6f80414..654614d 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -597,7 +597,7 @@ class DraftPlayerCommand(Command): class StartDraftCommand(Command): name = "startdraft" template = "m;startdraft [mention] [teamname] [slogan]" - description = """Starts a draft with an arbitrary number of participants. Send this command at the + description = """Starts a draft with an arbitrary number of participants. Send this command at the top of the list with each mention, teamname, and slogan on a new line (shift+enter in discord). - The draft will proceed in the order that participants were entered. - 20 players will be available for draft at a time, and the pool will refresh automatically when it becomes small. @@ -636,7 +636,7 @@ top of the list with each mention, teamname, and slogan on a new line (shift+ent ) try: draft_message = await self.wait_draft(msg.channel, draft) - draft.draft_player(f'<@!{draft_message.author.id}>', draft_message.content.split(' ', 1)[1]) + draft.draft_player(f'<@!{draft_message.author.id}>', draft_message.content.split(' ', 1)[1]) except SlowDraftError: player = random.choice(draft.get_draftees()) await msg.channel.send(f"I'm not waiting forever. You get {player}. Next") @@ -1349,4 +1349,22 @@ def get_team_fuzzy_search(team_name): 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}, +# "Pigeon’s 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"]) From ba70ac4f313c5cc85b18071ac4ac791f4fe3fd22 Mon Sep 17 00:00:00 2001 From: joe Date: Mon, 4 Jan 2021 19:19:12 -0800 Subject: [PATCH 10/22] some fixes --- onomancer.py | 30 ++++++++++++++++++++++++++---- the_prestige.py | 15 ++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/onomancer.py b/onomancer.py index 2031ec9..278d128 100644 --- a/onomancer.py +++ b/onomancer.py @@ -1,6 +1,8 @@ #interfaces with onomancer import requests, json, urllib +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry import database as db @@ -9,13 +11,27 @@ name_stats_hook = "getOrGenerateStats?name=" collection_hook = "getCollection?token=" names_hook = "getNames" + +def _retry_session(retries=3, backoff=0.3, status=(500, 501, 502, 503, 504)): + session = requests.Session() + retry = Retry( + total=retries, + read=retries, + connect=retries, + backoff_factor=backoff, + status_forcelist=status, + ) + adapter = HTTPAdapter(max_retries=retry) + session.mount('https://', adapter) + return session + def get_stats(name): player = db.get_stats(name) if player is not None: return player #returns json_string #yell at onomancer if not in cache or too old - response = requests.get(onomancer_url + name_stats_hook + urllib.parse.quote_plus(name)) + response = _retry_session().get(onomancer_url + name_stats_hook + urllib.parse.quote_plus(name)) if response.status_code == 200: stats = json.dumps(response.json()) db.cache_stats(name, stats) @@ -31,7 +47,7 @@ def get_scream(username): return scream def get_collection(collection_url): - response = requests.get(onomancer_url + collection_hook + urllib.parse.quote(collection_url)) + response = _retry_session().get(onomancer_url + collection_hook + urllib.parse.quote(collection_url)) if response.status_code == 200: for player in response.json()['lineup'] + response.json()['rotation']: db.cache_stats(player['name'], json.dumps(player)) @@ -44,7 +60,7 @@ def get_names(limit=20, threshold=1): Get `limit` random players that have at least `threshold` upvotes. Returns dictionary keyed by player name of stats. """ - response = requests.get( + response = _retry_session().get( onomancer_url + names_hook, params={ 'limit': limit, @@ -53,4 +69,10 @@ def get_names(limit=20, threshold=1): 'random': 1, }, ) - return {p['name']: p for p in response.json()} + response.raise_for_status() + res = {} + for stats in response.json(): + name = stats['name'] + db.cache_stats(name, json.dumps(stats)) + res[name] = stats + return res diff --git a/the_prestige.py b/the_prestige.py index 654614d..5f0e424 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -603,7 +603,7 @@ top of the list with each mention, teamname, and slogan on a new line (shift+ent - 20 players will be available for draft at a time, and the pool will refresh automatically when it becomes small. - Each participant will be asked to draft 12 hitters then finally one pitcher. - The draft will start only once every participant has given a 👍 to begin. - - use the command `m;draft` on your turn to draft someone + - use the command `d`, `draft`, or `m;draft` on your turn to draft someone """ async def execute(self, msg, command): @@ -615,11 +615,18 @@ top of the list with each mention, teamname, and slogan on a new line (shift+ent raise ValueError('Invalid length') for i in range(0, len(content), 3): - handle = content[i].strip() + handle_token = content[i].strip() + for mention in mentions: + if mention in handle_token: + handle = mention + break + else: + await msg.channel.send(f"I don't recognize {handle_token}") + return team_name = content[i + 1].strip() if games.get_team(team_name): await msg.channel.send(f'Sorry {handle}, {team_name} already exists') - raise ValueError('Existing team') + return slogan = content[i + 2].strip() draft.add_participant(handle, team_name, slogan) @@ -681,6 +688,8 @@ top of the list with each mention, teamname, and slogan on a new line (shift+ent def check(m): if m.channel != channel: return False + if m.content.startswith('d') or m.content.startswith('draft'): + return True for prefix in config()['prefix']: if m.content.startswith(prefix + 'draft'): return True From b17d3a30425918223bf3398f6c9f53e42651d2ad Mon Sep 17 00:00:00 2001 From: joe Date: Mon, 4 Jan 2021 20:32:42 -0800 Subject: [PATCH 11/22] pitcher fix --- the_draft.py | 6 +++++- the_prestige.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/the_draft.py b/the_draft.py index 4b424d3..aec71c4 100644 --- a/the_draft.py +++ b/the_draft.py @@ -45,6 +45,10 @@ class Draft: """ return self._active_participant.handle + @property + def active_drafting_team(self): + return self._active_participant.team.name + def add_participant(self, handle, team_name, slogan): """ A participant is someone participating in this draft. Initializes an empty team for them @@ -106,7 +110,7 @@ class Draft: if self._round < DRAFT_ROUNDS: self._active_participant.team.add_lineup(games.player(json.dumps(player))) elif self._round == DRAFT_ROUNDS: - self._active_participant.team.set_pitcher(games.player(json.dumps(player))) + self._active_participant.team.add_pitcher(games.player(json.dumps(player))) self.advance_draft() if self._active_participant == BOOKMARK: diff --git a/the_prestige.py b/the_prestige.py index 5f0e424..9635345 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -636,9 +636,9 @@ top of the list with each mention, teamname, and slogan on a new line (shift+ent draft.start_draft() while draft.round <= DRAFT_ROUNDS: - choosing = 'pitcher' if draft.round == DRAFT_ROUNDS else 'hitter' + choosing = '⚾️pitcher⚾️' if draft.round == DRAFT_ROUNDS else '🏏hitter🏏' await msg.channel.send( - f'Round {draft.round}/{DRAFT_ROUNDS}: {draft.active_drafter}, choose a {choosing}.', + f'Round {draft.round}/{DRAFT_ROUNDS}: {draft.active_drafter}, choose a {choosing} for {draft.active_drafting_team}.', embed=build_draft_embed(draft.get_draftees()), ) try: From b76c7de8bc9ef1615413053f350fbd40a0073592 Mon Sep 17 00:00:00 2001 From: joe Date: Mon, 4 Jan 2021 21:41:51 -0800 Subject: [PATCH 12/22] text tweaks --- the_draft.py | 8 ++++---- the_prestige.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/the_draft.py b/the_draft.py index aec71c4..8795a1b 100644 --- a/the_draft.py +++ b/the_draft.py @@ -119,16 +119,16 @@ class Draft: return player def get_teams(self): - teams = {} + teams = [] if self._active_participant != BOOKMARK: - teams[self._active_participant.handle] = self._active_participant.team + teams.append((self._active_participant.handle, self._active_participant.team)) for participant in self._participants: if participant != BOOKMARK: - teams[participant.handle] = participant.team + teams.append((self._active_participant.handle, self._active_participant.team)) return teams def finish_draft(self): - for handle, team in self.get_teams().items(): + for handle, team in self.get_teams(): success = games.save_team(team, int(handle[3:-1])) if not success: raise Exception(f'Error saving team for {handle}') diff --git a/the_prestige.py b/the_prestige.py index 9635345..4572be3 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -639,7 +639,7 @@ top of the list with each mention, teamname, and slogan on a new line (shift+ent choosing = '⚾️pitcher⚾️' if draft.round == DRAFT_ROUNDS else '🏏hitter🏏' await msg.channel.send( f'Round {draft.round}/{DRAFT_ROUNDS}: {draft.active_drafter}, choose a {choosing} for {draft.active_drafting_team}.', - embed=build_draft_embed(draft.get_draftees()), + embed=build_draft_embed(draft.get_draftees(), footer=f"{choosing[0]}You must choose"), ) try: draft_message = await self.wait_draft(msg.channel, draft) @@ -651,7 +651,7 @@ top of the list with each mention, teamname, and slogan on a new line (shift+ent except ValueError as e: await msg.channel.send(str(e)) - for handle, team in draft.get_teams().items(): + for handle, team in draft.get_teams(): await msg.channel.send(f'{handle} behold your team', embed=build_team_embed(team)) try: draft.finish_draft() @@ -1147,13 +1147,13 @@ async def team_delete_confirm(channel, team, owner): return -def build_draft_embed(names): - embed = discord.Embed(color=discord.Color.purple(), title="The Draft") +def build_draft_embed(names, title="The Draft", footer="You must choose"): + embed = discord.Embed(color=discord.Color.purple(), title=title) column_size = 7 for i in range(0, len(names), column_size): draft = '\n'.join(names[i:i + column_size]) embed.add_field(name="-", value=draft, inline=True) - embed.set_footer(text="You must choose") + embed.set_footer(text=footer) return embed From c65d81a5e6cdbfaf82b261db4b9ec625f7a0a2f7 Mon Sep 17 00:00:00 2001 From: joe Date: Mon, 4 Jan 2021 22:10:06 -0800 Subject: [PATCH 13/22] fix help text --- the_prestige.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/the_prestige.py b/the_prestige.py index 4572be3..3a57660 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -597,8 +597,7 @@ class DraftPlayerCommand(Command): class StartDraftCommand(Command): name = "startdraft" template = "m;startdraft [mention] [teamname] [slogan]" - description = """Starts a draft with an arbitrary number of participants. Send this command at the -top of the list with each mention, teamname, and slogan on a new line (shift+enter in discord). + description = """Starts a draft with an arbitrary number of participants. Send this command at the top of the list with each mention, teamname, and slogan on a new line (shift+enter in discord). - The draft will proceed in the order that participants were entered. - 20 players will be available for draft at a time, and the pool will refresh automatically when it becomes small. - Each participant will be asked to draft 12 hitters then finally one pitcher. From c15f500058c290211aca84c7a8a45bd8e5903a0a Mon Sep 17 00:00:00 2001 From: joe Date: Mon, 4 Jan 2021 23:38:53 -0800 Subject: [PATCH 14/22] Final tweaks --- the_draft.py | 10 +++------- the_prestige.py | 43 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/the_draft.py b/the_draft.py index 8795a1b..cfb4cbc 100644 --- a/the_draft.py +++ b/the_draft.py @@ -96,7 +96,7 @@ class Draft: if not player: # might be some whitespace shenanigans for name, stats in self._players.items(): - if name.replace('\xa0', ' ').strip() == player_name: + if name.replace('\xa0', ' ').strip().lower() == player_name.lower(): player = stats break else: @@ -124,7 +124,7 @@ class Draft: teams.append((self._active_participant.handle, self._active_participant.team)) for participant in self._participants: if participant != BOOKMARK: - teams.append((self._active_participant.handle, self._active_participant.team)) + teams.append((participant.handle, participant.team)) return teams def finish_draft(self): @@ -136,7 +136,7 @@ class Draft: if __name__ == '__main__': # extremely robust testing OC do not steal - DRAFT_ROUNDS = 3 + DRAFT_ROUNDS = 2 draft = Draft.make_draft() draft.add_participant('@bluh', 'Bluhstein Bluhs', 'bluh bluh bluh') draft.add_participant('@what', 'Barcelona IDK', 'huh') @@ -151,7 +151,3 @@ if __name__ == '__main__': except ValueError as e: print(e) print(draft.get_teams()) - print(draft.get_teams()['@bluh'].lineup) - print(draft.get_teams()['@bluh'].pitcher) - print(draft.get_teams()['@what'].lineup) - print(draft.get_teams()['@what'].pitcher) diff --git a/the_prestige.py b/the_prestige.py index 3a57660..c17c26a 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -634,31 +634,56 @@ class StartDraftCommand(Command): return draft.start_draft() + footer = f"The draft class of {random.randint(2007, 2075)}" while draft.round <= DRAFT_ROUNDS: - choosing = '⚾️pitcher⚾️' if draft.round == DRAFT_ROUNDS else '🏏hitter🏏' + message_prefix = f'Round {draft.round}/{DRAFT_ROUNDS}:' + if draft.round == DRAFT_ROUNDS: + body = random.choice([ + f"Now just choose a pitcher and we can finish off this paperwork for you, {draft.active_drafter}", + f"Pick a pitcher, {draft.active_drafter}, and we can all go home happy. 'Cept your players. They'll have to play bllaseball.", + f"Almost done, {draft.active_drafter}. Pick your pitcher.", + ]) + message = f"⚾️ {message_prefix} {body}" + else: + body = random.choice([ + f"Choose a batter, {draft.active_drafter}", + f"{draft.active_drafter}, your turn. Pick one.", + f"Pick one to fill your next lineup slot, {draft.active_drafter}", + f"Alright, {draft.active_drafter}, choose a batter.", + ]) + message = f"🏏 {message_prefix} {body}" await msg.channel.send( - f'Round {draft.round}/{DRAFT_ROUNDS}: {draft.active_drafter}, choose a {choosing} for {draft.active_drafting_team}.', - embed=build_draft_embed(draft.get_draftees(), footer=f"{choosing[0]}You must choose"), + message, + embed=build_draft_embed(draft.get_draftees(), footer=footer), ) try: draft_message = await self.wait_draft(msg.channel, draft) draft.draft_player(f'<@!{draft_message.author.id}>', draft_message.content.split(' ', 1)[1]) except SlowDraftError: player = random.choice(draft.get_draftees()) - await msg.channel.send(f"I'm not waiting forever. You get {player}. Next") + await msg.channel.send(f"I'm not waiting forever. You get {player}. Next.") draft.draft_player(draft.active_drafter, player) except ValueError as e: await msg.channel.send(str(e)) + except IndexError: + await msg.channel.send("Quit the funny business.") for handle, team in draft.get_teams(): - await msg.channel.send(f'{handle} behold your team', embed=build_team_embed(team)) + await msg.channel.send( + random.choice([ + f"Done and dusted, {handle}. Here's your squad.", + f"Behold the {team.name}, {handle}. Flawless, we think.", + f"Oh, huh. Interesting stat distribution. Good luck, {handle}.", + ]), + embed=build_team_embed(team), + ) try: draft.finish_draft() except Exception as e: await msg.channel.send(str(e)) async def wait_start(self, channel, mentions): - start_msg = await channel.send('Are we good to go? ' + ' '.join(mentions)) + start_msg = await channel.send("Sound off, folks. 👍 if you're good to go " + " ".join(mentions)) await start_msg.add_reaction("👍") await start_msg.add_reaction("👎") @@ -669,7 +694,7 @@ class StartDraftCommand(Command): try: react, _ = await client.wait_for('reaction_add', timeout=60.0, check=react_check) if react.emoji == "👎": - await channel.send("Got it, stopping the draft") + await channel.send("We dragged out the photocopier for this! Fine, putting it back.") return False if react.emoji == "👍": reactors = set() @@ -687,10 +712,10 @@ class StartDraftCommand(Command): def check(m): if m.channel != channel: return False - if m.content.startswith('d') or m.content.startswith('draft'): + if m.content.startswith('d ') or m.content.startswith('draft '): return True for prefix in config()['prefix']: - if m.content.startswith(prefix + 'draft'): + if m.content.startswith(prefix + 'draft '): return True return False From 97546d7be5006a267fba7727b2abe34209a0d6c3 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 6 Jan 2021 17:45:39 -0800 Subject: [PATCH 15/22] erroneous merge --- the_prestige.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/the_prestige.py b/the_prestige.py index c17c26a..fa0206d 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -1382,22 +1382,4 @@ def get_team_fuzzy_search(team_name): 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}, -# "Pigeon’s 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"]) From 43d6b08961af35ede707b0c2905ecf8a38bd9482 Mon Sep 17 00:00:00 2001 From: Sakimori Date: Thu, 7 Jan 2021 19:27:12 -0500 Subject: [PATCH 16/22] changed "home run" to "dinger" --- games.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games.py b/games.py index b89d970..f246be8 100644 --- a/games.py +++ b/games.py @@ -50,7 +50,7 @@ class appearance_outcomes(Enum): single = "hits a single!" double = "hits a double!" triple = "hits a triple!" - homerun = "hits a home run!" + homerun = "hits a dinger!" grandslam = "hits a grand slam!" From 65546bd239f4ae7461818f0ac77bde38f0578fb5 Mon Sep 17 00:00:00 2001 From: Sakimori Date: Thu, 7 Jan 2021 20:01:53 -0500 Subject: [PATCH 17/22] added another patreon sub --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c770661..33d9fef 100644 --- a/README.md +++ b/README.md @@ -97,3 +97,4 @@ these folks are helping me a *ton* via patreon, and i cannot possibly thank them - Astrid Bek - Kameleon - Ryan Littleton +- Evie Diver From e6a1100b5ea622667da1adfbb1762dd8bbca1d74 Mon Sep 17 00:00:00 2001 From: Sakimori Date: Thu, 7 Jan 2021 22:37:04 -0500 Subject: [PATCH 18/22] fixed tailwind only applying to batters with more than 5 stars --- games.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games.py b/games.py index f246be8..8ac5014 100644 --- a/games.py +++ b/games.py @@ -507,7 +507,7 @@ class game(object): if self.weather.name == "Slight Tailwind" and "mulligan" not in self.last_update[0].keys() and not result["ishit"] and result["text"] != appearance_outcomes.walk: mulligan_roll_target = -((((self.get_batter().stlats["batting_stars"])-5)/6)**2)+1 - if random.random() > mulligan_roll_target and self.get_batter().stlats["batting_stars"] >= 5: + if random.random() > mulligan_roll_target and self.get_batter().stlats["batting_stars"] <= 5: result["mulligan"] = True return (result, 0) From d34efcad4b18fe12db123af7dcc4ac84ca3f41db Mon Sep 17 00:00:00 2001 From: Genderdruid Date: Fri, 8 Jan 2021 10:10:01 -0800 Subject: [PATCH 19/22] a few minor text updates --- README.md | 4 ++-- the_prestige.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 33d9fef..a670c83 100644 --- a/README.md +++ b/README.md @@ -70,12 +70,12 @@ accepting pull requests, check the issues for to-dos. - use this command at the top of a list with entries separated by new lines: - the away team's name. - the home team's name. - - 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 201. 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. + - starts a randomly seeded tournament with the 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. diff --git a/the_prestige.py b/the_prestige.py index fa0206d..2f2365c 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -119,10 +119,11 @@ class ShowPlayerCommand(Command): class StartGameCommand(Command): name = "startgame" 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) (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.): + 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): - the away 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 201. 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.)""" async def execute(self, msg, command): league = None @@ -500,7 +501,7 @@ class StartTournamentCommand(Command): 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." + description = "Starts a randomly seeded tournament with the 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): if config()["game_freeze"]: From ad6cc90e821736b82f1f82cd91370416d8b51971 Mon Sep 17 00:00:00 2001 From: Genderdruid Date: Fri, 8 Jan 2021 10:15:50 -0800 Subject: [PATCH 20/22] Update the_prestige.py --- the_prestige.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/the_prestige.py b/the_prestige.py index 2f2365c..ddb0267 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -123,7 +123,7 @@ class StartGameCommand(Command): - the away team's name. - the home team's name. - and finally, optionally, the number of innings, which must be greater than 2 and less than 201. 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.)""" + - 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): league = None From 0a1f1a9e731cf5407209e3321e8266a722ef72b2 Mon Sep 17 00:00:00 2001 From: Sakimori Date: Fri, 8 Jan 2021 13:55:52 -0500 Subject: [PATCH 21/22] added connection closing statements to most database.py functions --- database.py | 17 +++++++++++++++-- the_prestige.py | 1 - 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/database.py b/database.py index d10a056..1dd1c90 100644 --- a/database.py +++ b/database.py @@ -186,9 +186,13 @@ def get_user_player_conn(conn, user): except TypeError: return False else: - print(conn) + conn.close() + return False except: - print(conn) + conn.close() + return False +conn.close() +return False def get_user_player(user): conn = create_connection() @@ -211,6 +215,8 @@ def save_team(name, team_json_string, user_id): return False except: return False + conn.close() + return False def update_team(name, team_json_string): conn = create_connection() @@ -225,7 +231,10 @@ def update_team(name, team_json_string): conn.close() return False except: + conn.close() return False + conn.close() + return False def get_team(name, owner=False): conn = create_connection() @@ -258,6 +267,8 @@ def delete_team(team): except: conn.close() return False + conn.close() + return False def assign_owner(team_name, owner_id): conn = create_connection() @@ -271,6 +282,8 @@ def assign_owner(team_name, owner_id): except: conn.close() return False + conn.close() + return False def get_all_teams(): conn = create_connection() diff --git a/the_prestige.py b/the_prestige.py index fa0206d..5a70a76 100644 --- a/the_prestige.py +++ b/the_prestige.py @@ -489,7 +489,6 @@ class AssignOwnerCommand(Command): async def execute(self, msg, command): new_owner = msg.mentions[0] team_name = command.strip().split(new_owner.mention+" ")[1] - print(team_name) if db.assign_owner(team_name, new_owner.id): await msg.channel.send(f"{team_name} is now owned by {new_owner.display_name}. Don't break it.") else: From 7e3b1d353f4755cf3ab6851b9f57a8f9e7f70027 Mon Sep 17 00:00:00 2001 From: Sakimori Date: Fri, 8 Jan 2021 13:56:36 -0500 Subject: [PATCH 22/22] fixed an error oops --- database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database.py b/database.py index 1dd1c90..6e89b73 100644 --- a/database.py +++ b/database.py @@ -191,8 +191,8 @@ def get_user_player_conn(conn, user): except: conn.close() return False -conn.close() -return False + conn.close() + return False def get_user_player(user): conn = create_connection()