Adding points check and fixing some minor other issues

This commit is contained in:
LaDfBC 2020-12-20 10:11:09 -06:00
parent caa0819d91
commit f592bb8234
8 changed files with 203 additions and 69 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
.idea/* .idea/*
__pycache__/

View File

@ -1,11 +1,8 @@
from sqlalchemy import Column, String, Integer, ForeignKey, Date from sqlalchemy import Column, String, Integer, ForeignKey, Date
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from src.main.db_session import DatabaseSession
Base = declarative_base() Base = declarative_base()
class Play(Base): class Play(Base):
@ -14,6 +11,7 @@ class Play(Base):
play_id = Column(String, nullable=False, primary_key=True) play_id = Column(String, nullable=False, primary_key=True)
pitch_value = Column(Integer, nullable=True) pitch_value = Column(Integer, nullable=True)
creation_date = Column(Date, nullable=False) creation_date = Column(Date, nullable=False)
server_id = Column(String, nullable=False)
guesses = relationship(lambda : Guess) guesses = relationship(lambda : Guess)

View File

@ -3,6 +3,7 @@ from copy import deepcopy
from src.main.database_module.database_classes.db_classes import Guess from src.main.database_module.database_classes.db_classes import Guess
from src.main.db_session import DatabaseSession from src.main.db_session import DatabaseSession
MEMBER_ID = 'member_id' MEMBER_ID = 'member_id'
PLAY_ID = 'play_id' PLAY_ID = 'play_id'
GUESSED_NUMBER = 'guessed_number' GUESSED_NUMBER = 'guessed_number'
@ -15,11 +16,13 @@ class GuessDAO():
Session = None Session = None
engine = None engine = None
_database_session = None
def __init__(self): def __init__(self):
pass self._database_session = DatabaseSession()
def insert(self, guess_info): def insert(self, guess_info):
session = DatabaseSession.session session = self._database_session.get_or_create_session()
guess = Guess( guess = Guess(
member_id=guess_info[MEMBER_ID], member_id=guess_info[MEMBER_ID],
@ -55,7 +58,7 @@ class GuessDAO():
return converted_games return converted_games
def set_differences(self, pitch_value, play_id): 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)) games_to_update = self.__convert_all__(session.query(Guess).filter(Guess.play_id == play_id))
for game in games_to_update: for game in games_to_update:
@ -74,7 +77,7 @@ class GuessDAO():
return possible_value return possible_value
def fetch_closest(self, num_to_fetch): def fetch_closest(self, num_to_fetch):
session = DatabaseSession.session session = self._database_session.get_or_create_session()
return self.__convert_all__( return self.__convert_all__(
session\ session\
@ -83,8 +86,19 @@ class GuessDAO():
.limit(num_to_fetch) .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): 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 # TODO: Make this a MAX query for ties
converted_guesses = self.__convert_all__( converted_guesses = self.__convert_all__(

View File

@ -1,46 +1,62 @@
from copy import deepcopy from copy import deepcopy
from sqlalchemy.sql.expression import and_
from src.main.db_session import DatabaseSession from src.main.db_session import DatabaseSession
from src.main.database_module.database_classes.db_classes import Play from src.main.database_module.database_classes.db_classes import Play
import datetime
PLAY_ID = 'play_id' PLAY_ID = 'play_id'
PITCH_VALUE = 'pitch_value' PITCH_VALUE = 'pitch_value'
CREATION_DATE = 'creation_date' CREATION_DATE = 'creation_date'
SERVER_ID = 'server_id'
class PlayDAO(): class PlayDAO():
db_string = None db_string = None
session = None session = None
Session = None Session = None
engine = None engine = None
_database_session = None
def __init__(self): def __init__(self):
pass self._database_session = DatabaseSession()
def insert(self, play_info): def insert(self, play_info):
session = DatabaseSession.session session = self._database_session.get_or_create_session()
play = Play( play = Play(
play_id = play_info[PLAY_ID], play_id = play_info[PLAY_ID],
pitch_value = play_info[PITCH_VALUE] if PITCH_VALUE in play_info else None, 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.add(play)
session.commit() session.commit()
def get_play_by_id(self, input_id): 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)) 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 Checks to see if there is a play that is currently active or not
''' '''
def is_active_play(self): def is_active_play(self, server_id):
return self.get_active_play() != None return self.get_active_play(server_id) != None
def get_active_play(self): def get_active_play(self, input_server_id):
session = DatabaseSession.session session = self._database_session.get_or_create_session()
plays = self.__convert_all__(session.query(Play).filter(Play.pitch_value == None)) plays = self.__convert_all__(session.query(Play).filter(and_(Play.pitch_value == None, Play.server_id == str(input_server_id))))
if len(plays) > 1: if len(plays) > 1:
raise AssertionError("More than one active play! Can't continue!") raise AssertionError("More than one active play! Can't continue!")
@ -49,18 +65,21 @@ class PlayDAO():
else: else:
return plays[0] return plays[0]
def resolve_play(self, input_pitch): def resolve_play(self, input_pitch, input_server_id):
session = DatabaseSession.session session = self._database_session.get_or_create_session()
active_id = self.get_active_play() active_id = self.get_active_play(input_server_id)
session\ session\
.query(Play)\ .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}) .update({Play.pitch_value: input_pitch})
session.commit() session.commit()
return active_id 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 Converts the database object into a Dictionary, so that the database object is not passed out of the
datastore layer. datastore layer.

View File

@ -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 You should NOT instantiate this in any method except the main application runner
''' '''
class DatabaseSession(): class DatabaseSession():
session = None _session = None
def __init__(self): 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 config_map = Configs.configs
db_string = self._pgsql_conn_string_(config_map) db_string = self._pgsql_conn_string_(config_map)
Session = sessionmaker(create_engine(db_string)) 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. # 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): def _pgsql_conn_string_(self, config_map):

View File

@ -1,17 +1,22 @@
import sys import sys
import discord import discord
from discord.utils import get
import uuid import uuid
import datetime import datetime
import dateparser
from src.main.configs import Configs 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.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.db_session import DatabaseSession
from src.main.discord_module.leaderboard_config import LeaderboardConfig from src.main.discord_module.leaderboard_config import LeaderboardConfig
play_dao = None play_dao = None
guess_dao = None guess_dao = None
points_service = PointsService()
bot = discord.Client() bot = discord.Client()
@ -29,15 +34,17 @@ async def on_message(message):
return return
content = message.content content = message.content
server_id = message.guild.id
''' '''
Sets up the next set of guesses. Sets up the next set of guesses.
''' '''
if content.startswith('!ghostball'): 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?") await message.channel.send("There's already an active play. Could you close that one first, please?")
else: 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) play_dao.insert(play_object)
await message.channel.send("@flappy ball, 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.")
@ -45,10 +52,10 @@ async def on_message(message):
if content.startswith("!guess"): if content.startswith("!guess"):
guess_value = __parse_guess__(content) 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.") await message.channel.send("Hey, there's no active play! Start one up first with !ghostball.")
else: else:
play = play_dao.get_active_play() play = play_dao.get_active_play(server_id)
guess_object = {PLAY_ID: play['play_id'], guess_object = {PLAY_ID: play['play_id'],
MEMBER_ID: str(message.author.id), MEMBER_ID: str(message.author.id),
GUESSED_NUMBER: guess_value, 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 # Closes off the active play to be ready for the next set
if content.startswith('!resolve'): if content.startswith('!resolve'):
# try: # try:
pitch_value = __parse_resolve_play__(content) args, has_batter = __parse_resolve_play__(content)
if pitch_value is None: if args is None:
await message.channel.send("Hey " + "<@" + str(message.author.id) + ">, I'm not sure what you meant. " 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. " "You need real, numeric, values for this command to work. "
"Use !resolve <pitch number> and try again.") "Use !resolve <pitch number> <optional batter> <optional swing number>"
" and try again.")
# Check if we have an active play # 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!") await message.channel.send("You confused me. There's no active play so I have nothing to close!")
else: 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']) 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( response_message = "Closed this play! Here are the results:\n"
"Closed this play! " + "<@" + str(closest_guess[MEMBER_ID]) + response_message += "PLAYER --- DIFFERENCE --- POINTS GAINED\n"
"> was the closest with a guess of " + closest_guess[GUESSED_NUMBER] + for guess in guesses:
" resulting in a difference of " + closest_guess[DIFFERENCE] + ".") response_message += guess[1] + " --- " + str(guess[2]) + " --- " + str(guess[3]) + "\n"
# Likely due to too few parameters but could be any number of things response_message += "\nCongrats to <@" + str(guesses[0][0]) + "> for being the closest! \n"
# except : response_message += "And tell <@" + str(guesses[-1][0]) + "> they suck."
# 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 <pitch number> <swing_number>. "
# "Use that format and try again, ok?")
if content.startswith('!leaderboard'): await message.channel.send(response_message)
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!")
if content.startswith("!points"): 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'): if content.startswith('!help'):
help_message = __get_help_message__() help_message = __get_help_message__()
@ -120,9 +137,14 @@ def __get_help_message__():
"I will give you a thumbs up if everything worked!\n" \ "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" \ "!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" \ "!help --- You just asked for this. If you ask for it again, I'll repeat myself.\n" \
"!resolve <PITCH_NUMBER> --- Uses the pitch number and real swing number " \ "!resolve <PITCH_NUMBER> <OPTIONAL --- BATTER by @-mention> <OPTIONAL - ACTUAL SWING NUMBER> --- " \
"to figure out who was closest and ends the active play.\n" \ "Uses the pitch number and real swing number to figure out who was closest and ends the active play." \
"<HELP MESSAGE NEEDS DOCUMENTATION FOR LEADERBOARD COMMAND! PING KALI IF YOU'RE ANGRY!>\n" "If you include the batter and their swing number, they will get credit for how well they did!\n" \
"!points <OPTIONAL --- TIMESTAMP> 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" \
"<PING KALI IF YOU'RE CONFUSED, ANGRY, OR WANT TO GEEK OUT ABOUT BRAVELY DEFAULT!>\n"
return help_message return help_message
@ -131,6 +153,20 @@ def __parse_leaderboard_message__(message_content):
return LeaderboardConfig(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): def __parse_guess__(message_content):
pieces = message_content.split(' ') pieces = message_content.split(' ')
try: try:
@ -140,11 +176,17 @@ def __parse_guess__(message_content):
def __parse_resolve_play__(message_content): def __parse_resolve_play__(message_content):
pieces = message_content.split(' ') pieces = message_content.split()
try: 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: except TypeError:
return None return None, None
if __name__ == '__main__': if __name__ == '__main__':

View File

View File

@ -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