From f592bb8234a8bc3cc05f05770c89ad8204cc4ff4 Mon Sep 17 00:00:00 2001 From: LaDfBC Date: Sun, 20 Dec 2020 10:11:09 -0600 Subject: [PATCH] Adding points check and fixing some minor other issues --- .gitignore | 1 + .../database_classes/db_classes.py | 4 +- src/main/database_module/guess_dao.py | 24 +++- src/main/database_module/play_dao.py | 45 ++++-- src/main/db_session.py | 19 ++- src/main/discord_module/bot_runner.py | 134 ++++++++++++------ src/main/services/__init__.py | 0 src/main/services/points_service.py | 45 ++++++ 8 files changed, 203 insertions(+), 69 deletions(-) create mode 100644 src/main/services/__init__.py create mode 100644 src/main/services/points_service.py diff --git a/.gitignore b/.gitignore index 1c2d52b..48fd780 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .idea/* +__pycache__/ diff --git a/src/main/database_module/database_classes/db_classes.py b/src/main/database_module/database_classes/db_classes.py index c8110dc..83f3371 100644 --- a/src/main/database_module/database_classes/db_classes.py +++ b/src/main/database_module/database_classes/db_classes.py @@ -1,11 +1,8 @@ from sqlalchemy import Column, String, Integer, ForeignKey, Date from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship -from src.main.db_session import DatabaseSession - Base = declarative_base() class Play(Base): @@ -14,6 +11,7 @@ class Play(Base): play_id = Column(String, nullable=False, primary_key=True) pitch_value = Column(Integer, nullable=True) creation_date = Column(Date, nullable=False) + server_id = Column(String, nullable=False) guesses = relationship(lambda : Guess) diff --git a/src/main/database_module/guess_dao.py b/src/main/database_module/guess_dao.py index a586451..0797605 100644 --- a/src/main/database_module/guess_dao.py +++ b/src/main/database_module/guess_dao.py @@ -3,6 +3,7 @@ from copy import deepcopy from src.main.database_module.database_classes.db_classes import Guess from src.main.db_session import DatabaseSession + MEMBER_ID = 'member_id' PLAY_ID = 'play_id' GUESSED_NUMBER = 'guessed_number' @@ -15,11 +16,13 @@ class GuessDAO(): Session = None engine = None + _database_session = None + def __init__(self): - pass + self._database_session = DatabaseSession() def insert(self, guess_info): - session = DatabaseSession.session + session = self._database_session.get_or_create_session() guess = Guess( member_id=guess_info[MEMBER_ID], @@ -55,7 +58,7 @@ class GuessDAO(): return converted_games def set_differences(self, pitch_value, play_id): - session = DatabaseSession.session + session = self._database_session.get_or_create_session() games_to_update = self.__convert_all__(session.query(Guess).filter(Guess.play_id == play_id)) for game in games_to_update: @@ -74,7 +77,7 @@ class GuessDAO(): return possible_value def fetch_closest(self, num_to_fetch): - session = DatabaseSession.session + session = self._database_session.get_or_create_session() return self.__convert_all__( session\ @@ -83,8 +86,19 @@ class GuessDAO(): .limit(num_to_fetch) ) + def refresh(self): + self._database_session.__create_new_session__() # I know, I know. It's fine. + + def get_all_guesses_for_plays(self, play_ids): + session = self._database_session.get_or_create_session() + return self.__convert_all__( + session + .query(Guess) + .filter(Guess.play_id.in_(play_ids)) + ) + def get_closest_on_play(self, play): - session = DatabaseSession.session + session = self._database_session.get_or_create_session() # TODO: Make this a MAX query for ties converted_guesses = self.__convert_all__( diff --git a/src/main/database_module/play_dao.py b/src/main/database_module/play_dao.py index 7105c8d..f035a87 100644 --- a/src/main/database_module/play_dao.py +++ b/src/main/database_module/play_dao.py @@ -1,46 +1,62 @@ from copy import deepcopy +from sqlalchemy.sql.expression import and_ from src.main.db_session import DatabaseSession from src.main.database_module.database_classes.db_classes import Play +import datetime + PLAY_ID = 'play_id' PITCH_VALUE = 'pitch_value' CREATION_DATE = 'creation_date' +SERVER_ID = 'server_id' class PlayDAO(): db_string = None session = None Session = None engine = None + _database_session = None def __init__(self): - pass + self._database_session = DatabaseSession() def insert(self, play_info): - session = DatabaseSession.session + session = self._database_session.get_or_create_session() play = Play( play_id = play_info[PLAY_ID], pitch_value = play_info[PITCH_VALUE] if PITCH_VALUE in play_info else None, - creation_date = play_info[CREATION_DATE] + creation_date = play_info[CREATION_DATE], + server_id = play_info[SERVER_ID] ) session.add(play) session.commit() def get_play_by_id(self, input_id): - session = DatabaseSession.session + session = self._database_session.get_or_create_session() return self.__convert_all__(session.query(Play).filter(Play.play_id == input_id)) + def get_all_plays_after(self, timestamp, input_server_id): + session = self._database_session.get_or_create_session() + return self.__convert_all__(session.query(Play).filter(and_(Play.server_id == str(input_server_id), Play.creation_date > timestamp))) + + def get_all_plays_on_server(self, input_server_id, earliest_timestamp): + session = self._database_session.get_or_create_session() + converted_datetime = datetime.datetime.fromtimestamp(earliest_timestamp / 1000.0) + + return self.__convert_all__(session.query(Play).filter(and_(Play.server_id == str(input_server_id), Play.creation_date > converted_datetime))) + ''' Checks to see if there is a play that is currently active or not ''' - def is_active_play(self): - return self.get_active_play() != None + def is_active_play(self, server_id): + return self.get_active_play(server_id) != None - def get_active_play(self): - session = DatabaseSession.session - plays = self.__convert_all__(session.query(Play).filter(Play.pitch_value == None)) + def get_active_play(self, input_server_id): + session = self._database_session.get_or_create_session() + plays = self.__convert_all__(session.query(Play).filter(and_(Play.pitch_value == None, Play.server_id == str(input_server_id)))) if len(plays) > 1: raise AssertionError("More than one active play! Can't continue!") @@ -49,18 +65,21 @@ class PlayDAO(): else: return plays[0] - def resolve_play(self, input_pitch): - session = DatabaseSession.session - active_id = self.get_active_play() + def resolve_play(self, input_pitch, input_server_id): + session = self._database_session.get_or_create_session() + active_id = self.get_active_play(input_server_id) session\ .query(Play)\ - .filter(Play.pitch_value == None)\ + .filter(and_(Play.pitch_value == None, Play.server_id == str(input_server_id)))\ .update({Play.pitch_value: input_pitch}) session.commit() return active_id + def refresh(self): + self._database_session.__create_new_session__() # I know, I know. It's fine. + ''' Converts the database object into a Dictionary, so that the database object is not passed out of the datastore layer. diff --git a/src/main/db_session.py b/src/main/db_session.py index bd4c597..53b5192 100644 --- a/src/main/db_session.py +++ b/src/main/db_session.py @@ -17,13 +17,28 @@ and then use it to handle the necessary CRUD operations. You should NOT instantiate this in any method except the main application runner ''' class DatabaseSession(): - session = None + _session = None def __init__(self): + self.__create_new_session__() + + def __create_new_session__(self): + if self._session is not None: + self._session.close() + config_map = Configs.configs db_string = self._pgsql_conn_string_(config_map) Session = sessionmaker(create_engine(db_string)) - DatabaseSession.session = Session() + self._session = Session() + + return self._session + + def get_or_create_session(self): + try: + self._session.connection() + return self._session + except: # The linter can scream all it wants, this makes sense. If it's this broke, we want a new one anyway. + return self.__create_new_session__() # Look, this kinda sucks. But it's for fun and friends and I'm doing it quick and dirty. def _pgsql_conn_string_(self, config_map): diff --git a/src/main/discord_module/bot_runner.py b/src/main/discord_module/bot_runner.py index 5d35283..41f594e 100644 --- a/src/main/discord_module/bot_runner.py +++ b/src/main/discord_module/bot_runner.py @@ -1,17 +1,22 @@ import sys import discord +from discord.utils import get + import uuid import datetime +import dateparser from src.main.configs import Configs from src.main.database_module.guess_dao import GuessDAO, GUESSED_NUMBER, MEMBER_ID, MEMBER_NAME, DIFFERENCE -from src.main.database_module.play_dao import PlayDAO, PLAY_ID, CREATION_DATE +from src.main.services.points_service import PointsService +from src.main.database_module.play_dao import PlayDAO, PLAY_ID, CREATION_DATE, SERVER_ID from src.main.db_session import DatabaseSession from src.main.discord_module.leaderboard_config import LeaderboardConfig play_dao = None guess_dao = None +points_service = PointsService() bot = discord.Client() @@ -29,26 +34,28 @@ async def on_message(message): return content = message.content + server_id = message.guild.id ''' Sets up the next set of guesses. ''' if content.startswith('!ghostball'): - if play_dao.is_active_play(): + if play_dao.is_active_play(server_id): await message.channel.send("There's already an active play. Could you close that one first, please?") else: - play_object = {PLAY_ID: uuid.uuid4(), CREATION_DATE: datetime.datetime.now()} + generated_play_id = uuid.uuid4() + play_object = {PLAY_ID: generated_play_id, CREATION_DATE: datetime.datetime.now(), SERVER_ID: server_id} play_dao.insert(play_object) - await message.channel.send("@flappyball, pitch is in! Send me your guesses with a !guess command.") + await message.channel.send("@flappy ball, pitch is in! Send me your guesses with a !guess command.") if content.startswith("!guess"): guess_value = __parse_guess__(content) - if not play_dao.is_active_play(): + if not play_dao.is_active_play(server_id): await message.channel.send("Hey, there's no active play! Start one up first with !ghostball.") else: - play = play_dao.get_active_play() + play = play_dao.get_active_play(server_id) guess_object = {PLAY_ID: play['play_id'], MEMBER_ID: str(message.author.id), GUESSED_NUMBER: guess_value, @@ -60,52 +67,62 @@ async def on_message(message): # Closes off the active play to be ready for the next set if content.startswith('!resolve'): # try: - pitch_value = __parse_resolve_play__(content) - if pitch_value is None: + args, has_batter = __parse_resolve_play__(content) + if args is None: await message.channel.send("Hey " + "<@" + str(message.author.id) + ">, I'm not sure what you meant. " "You need real, numeric, values for this command to work. " - "Use !resolve and try again.") + "Use !resolve " + " and try again.") # Check if we have an active play - if not play_dao.is_active_play(): + if not play_dao.is_active_play(server_id): await message.channel.send("You confused me. There's no active play so I have nothing to close!") else: - play = play_dao.resolve_play(pitch_value) + if has_batter: + referenced_member_id = args[1][3:-1] + play = play_dao.get_active_play(server_id) + guess_object = {PLAY_ID: play['play_id'], + MEMBER_ID: str(referenced_member_id), + GUESSED_NUMBER: args[2], + MEMBER_NAME: bot.get_user(int(referenced_member_id)).name} + + guess_dao.insert(guess_object) + + pitch_value = args[0] + play = play_dao.resolve_play(pitch_value, server_id) guess_dao.set_differences(pitch_value, play['play_id']) - closest_guess = guess_dao.get_closest_on_play(play['play_id']) + guesses = points_service.fetch_sorted_guesses_by_play(guess_dao, play['play_id']) - await message.channel.send( - "Closed this play! " + "<@" + str(closest_guess[MEMBER_ID]) + - "> was the closest with a guess of " + closest_guess[GUESSED_NUMBER] + - " resulting in a difference of " + closest_guess[DIFFERENCE] + ".") + response_message = "Closed this play! Here are the results:\n" + response_message += "PLAYER --- DIFFERENCE --- POINTS GAINED\n" + for guess in guesses: + response_message += guess[1] + " --- " + str(guess[2]) + " --- " + str(guess[3]) + "\n" - # Likely due to too few parameters but could be any number of things - # except : - # await message.channel.send( "Hey " + "<@" + str(message.author.id) + ">, you confused me with that message. " - # "To close an active pitch, the proper command is !resolve . " - # "Use that format and try again, ok?") + response_message += "\nCongrats to <@" + str(guesses[0][0]) + "> for being the closest! \n" + response_message += "And tell <@" + str(guesses[-1][0]) + "> they suck." - if content.startswith('!leaderboard'): - leaderboard_config = __parse_leaderboard_message__(content) - - if leaderboard_config.should_sort_by_pure_closest(): - values = guess_dao.fetch_closest(10) - - string_to_send = '' - for i, value in enumerate(values): - string_to_send += str(i + 1) + ': ' + value['member_name'] + ', ' + value['difference'] + '\n' - - await message.channel.send(string_to_send) - - elif leaderboard_config.should_sort_by_best_average(): - pass - else: - await message.channel.send( - "I don't understand that leaderboard command, sorry! I know it's a little confusing, so send me" - " a !help message if you want the full rundown for how to make this work!") + await message.channel.send(response_message) if content.startswith("!points"): - pass #TODO + try: + timestamp = __parse_points_message__(content) + except: + await message.channel.send("You gave me a timestamp that was so bad, the best date handling library in the" + " world of software couldn't figure out what you meant. That's...impressive. Now" + " fix your shit and try again.") + return + + points_by_user = points_service.fetch_points(timestamp, server_id, play_dao, guess_dao) + response = "Here are the top guessers by points as per your request..." + for user in points_by_user: + response += "\n" + str(user[1]) + " : " + str(user[2]) + + await message.channel.send(response) + + # Refresh Postgres connection + if content.startswith('!restart'): + play_dao.refresh() + guess_dao.refresh() if content.startswith('!help'): help_message = __get_help_message__() @@ -120,9 +137,14 @@ def __get_help_message__(): "I will give you a thumbs up if everything worked!\n" \ "!ghostball --- Starts a new play. I'll let you know if this didn't work for some reason!\n" \ "!help --- You just asked for this. If you ask for it again, I'll repeat myself.\n" \ - "!resolve --- Uses the pitch number and real swing number " \ - "to figure out who was closest and ends the active play.\n" \ - "\n" + "!resolve --- " \ + "Uses the pitch number and real swing number to figure out who was closest and ends the active play." \ + "If you include the batter and their swing number, they will get credit for how well they did!\n" \ + "!points Fetches all plays since your requested time, or the beginning of the unvierse " \ + "if none given. Will currently always dump all players - top X coming soon...\n" \ + "!restart --- If the bot looks broken, this will take a shot at fixing it. It won't answer your commands " \ + "for about 3 seconds after you do this! BE CAREFUL! ONLY USE IN AN EMERGENCY!\n" \ + "\n" return help_message @@ -131,6 +153,20 @@ def __parse_leaderboard_message__(message_content): return LeaderboardConfig(message_content) +def __parse_points_message__(message_content): + pieces = message_content.split(' ') + + if len(pieces) > 1: + try: + timestamp = dateparser.parse(pieces[1]) + except: + raise RuntimeError("Unable to parse timestamp!") + else: + timestamp = dateparser.parse("1970-01-01") + + return timestamp + + def __parse_guess__(message_content): pieces = message_content.split(' ') try: @@ -140,11 +176,17 @@ def __parse_guess__(message_content): def __parse_resolve_play__(message_content): - pieces = message_content.split(' ') + pieces = message_content.split() try: - return pieces[1] + if len(pieces) == 2: + return [pieces[1]], False + elif len(pieces) == 4: + return [pieces[1], pieces[2], pieces[3]], True + else: + print("Illegal resolution command") + return None, None except TypeError: - return None + return None, None if __name__ == '__main__': diff --git a/src/main/services/__init__.py b/src/main/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/main/services/points_service.py b/src/main/services/points_service.py new file mode 100644 index 0000000..9fee8c9 --- /dev/null +++ b/src/main/services/points_service.py @@ -0,0 +1,45 @@ + + +class PointsService(): + _point_table = [(5,25),(25, 75), (75, 50), (150, 25)] + + def fetch_points(self, timestamp, server_id, play_dao, guess_dao): + plays = play_dao.get_all_plays_after(timestamp, server_id) + all_guesses = guess_dao.get_all_guesses_for_plays(x['play_id'] for x in plays) + + # Build a dictionary of each member and their total points + totals_by_player = {} + for guess in all_guesses: + if guess['member_id'] in totals_by_player: + totals_by_player[guess['member_id']]['points'] += self.__get_points_for_diff__(guess['difference']) + else: + totals_by_player[guess['member_id']] = {} + totals_by_player[guess['member_id']]['points'] = self.__get_points_for_diff__(guess['difference']) + totals_by_player[guess['member_id']]['member_name'] = guess['member_name'] + + # And now pull those numbers out into a list and sort them + sorted_players = [] + for player in totals_by_player: + sorted_players.append([player, + totals_by_player[player]['member_name'], + totals_by_player[player]['points']]) + + sorted_players.sort(key=lambda x: x[2], reverse=True) + return sorted_players + + def fetch_sorted_guesses_by_play(self, guess_dao, play_id): + all_guesses = guess_dao.get_all_guesses_for_plays([play_id]) + player_list = [] + for guess in all_guesses: + player_list.append([guess['member_id'], guess['member_name'], int(guess['difference']), self.__get_points_for_diff__(guess['difference'])]) + + player_list.sort(key=lambda x: x[2]) + return player_list + + # Iterates through the point table, which we assume is sorted, and gets the points + def __get_points_for_diff__(self, diff): + for i in range(0, len(self._point_table)): + if int(diff) < self._point_table[i][0]: + return self._point_table[i][1] + + return 0