Working base version

This commit is contained in:
LaDfBC 2019-09-08 18:51:44 -05:00
commit e22f67ab17
17 changed files with 548 additions and 0 deletions

12
.idea/ghostballBot.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TestRunnerService">
<option name="projectConfiguration" value="Nosetests" />
<option name="PROJECT_TEST_RUNNER" value="Nosetests" />
</component>
</module>

4
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.6" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ghostballBot.iml" filepath="$PROJECT_DIR$/.idea/ghostballBot.iml" />
</modules>
</component>
</project>

91
.idea/workspace.xml generated Normal file
View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="60bff1c9-5425-4855-bbbe-90297c1dfe49" name="Default Changelist" comment="" />
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Python Script" />
</list>
</option>
</component>
<component name="ProjectId" id="1Ol6SCamPcPPraK1ZGhiktLdkWv" />
<component name="PropertiesComponent">
<property name="settings.editor.selected.configurable" value="preferences.lookFeel" />
</component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/main/database_module/database_classes" />
</key>
</component>
<component name="RunDashboard">
<option name="ruleStates">
<list>
<RuleState>
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
</RuleState>
<RuleState>
<option name="name" value="StatusDashboardGroupingRule" />
</RuleState>
</list>
</option>
</component>
<component name="RunManager">
<configuration name="bot_runner" type="PythonConfigurationType" factoryName="Python" temporary="true">
<module name="ghostballBot" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/src/main/discord_module" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/src/main/discord_module/bot_runner.py" />
<option name="PARAMETERS" value="NjEyODQwNDg5ODI1MzM3Mzc0.XVoPyQ.g8d_P-EV1p8OytraEWmYnsCBB7w /mln/ghostbot.cfg" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<recent_temporary>
<list>
<item itemvalue="Python.bot_runner" />
</list>
</recent_temporary>
</component>
<component name="SvnConfiguration">
<configuration />
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="60bff1c9-5425-4855-bbbe-90297c1dfe49" name="Default Changelist" comment="" />
<created>1564362047285</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1564362047285</updated>
</task>
<servers />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>
<default-breakpoints>
<breakpoint type="python-exception">
<properties notifyOnTerminate="true" exception="BaseException">
<option name="notifyOnTerminate" value="true" />
</properties>
</breakpoint>
</default-breakpoints>
</breakpoint-manager>
</component>
</project>

0
src/__init__.py Normal file
View File

0
src/main/__init__.py Normal file
View File

51
src/main/configs.py Normal file
View File

@ -0,0 +1,51 @@
# Names of Configurations
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DATABASE_USERNAME = 'database_username'
DATABASE_PASSWORD = 'database_password'
DATABASE_HOST = 'database_host'
DATABASE_PORT = 'database_port'
DATABASE_NAME = 'database_name'
SEASON_1_SPREADSHEET_ID = 's1_spreadsheet_id'
SEASON_2_SPREADSHEET_ID = 's2_spreadsheet_id'
PLAYER_SPREADSHEET = 'player_spreadsheet'
'''
Main source for configurations fetched from a startup configuration file. Includes the ability to fetch all, or fetch
one configuration once the file is loaded.
You'll find the names of these configs above as constants that can be used throughout the rest of this repository
'''
class Configs():
configs = {}
def __init__(self, config_file_path):
self.config_file_path = config_file_path
self.__load_configs__()
'''
Fetches a single configuration by the name of that configuration.
Returns None if that configuration does not exist
'''
def get_config_by_name(self, name):
try:
return Configs.configs[name]
except KeyError:
return None
'''
Fetches all configurations and returns them as a dictionary of config_key -> config_value
'''
def get_all_configs(self):
return Configs.configs
'''
Performs the initial load of configurations from a startup configuration file
'''
def __load_configs__(self):
Configs.configs = {}
config_file = open(self.config_file_path, 'r')
for line in config_file:
split_line = line.split('=')
Configs.configs[split_line[0]] = split_line[1].strip('\n')

View File

View File

@ -0,0 +1,31 @@
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):
__tablename__ = 'play'
play_id = Column(String, nullable=False, primary_key=True)
pitch_value = Column(Integer, nullable=True)
swing_value = Column(Integer, nullable=False)
creation_date = Column(Date, nullable=False)
guesses = relationship(lambda : Guess)
class Guess(Base):
__FAKE_VALUE__ = -5000
__tablename__ = 'guess'
member_id = Column(String, nullable=False, primary_key=True)
play_id = Column(UUID, ForeignKey(Play.play_id), nullable=False, primary_key=True)
guessed_number = Column(Integer, nullable=False)
member_name = Column(String, nullable=False)
difference = Column(Integer)
play = relationship("Play", back_populates="guesses")

View File

@ -0,0 +1,85 @@
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'
DIFFERENCE = 'difference'
MEMBER_NAME = 'member_name'
class GuessDAO():
db_string = None
session = None
Session = None
engine = None
def __init__(self):
pass
def insert(self, guess_info):
session = DatabaseSession.session
guess = Guess(
member_id=guess_info[MEMBER_ID],
play_id = guess_info[PLAY_ID],
guessed_number = guess_info[GUESSED_NUMBER],
member_name = guess_info[MEMBER_NAME]
)
existing_guess = self.__convert_all__(session\
.query(Guess)\
.filter(Guess.member_id == guess_info[MEMBER_ID], Guess.play_id == guess_info[PLAY_ID]))
if len(existing_guess) == 0:
session.add(guess)
session.commit()
return True
else:
return False
'''
Converts the database object into a Dictionary, so that the database object is not passed out of the
datastore layer.
'''
def __convert_all__(self, games):
converted_games = []
for game in games:
game_dict = {}
for column in game.__dict__:
game_dict[column] = str(getattr(game, column))
converted_games.append(deepcopy(game_dict))
return converted_games
def set_differences(self, pitch_value, play_id):
session = DatabaseSession.session
games_to_update = self.__convert_all__(session.query(Guess).filter(Guess.play_id == play_id))
for game in games_to_update:
difference = self.calculate_difference(pitch_value, game[GUESSED_NUMBER])
session.query(Guess).filter(Guess.member_id == game[MEMBER_ID], Guess.play_id == game[PLAY_ID]).update({Guess.difference: difference})
session.commit()
def calculate_difference(self, pitch_value, guess_value):
pitched_number = int(pitch_value)
possible_value = abs(int(guess_value) - pitched_number)
if possible_value > 500:
return 1000 - possible_value
else:
return possible_value
def fetch_closest(self, num_to_fetch):
session = DatabaseSession.session
return self.__convert_all__(
session\
.query(Guess)\
.order_by(Guess.difference)\
.limit(num_to_fetch)
)

View File

@ -0,0 +1,79 @@
from copy import deepcopy
from src.main.db_session import DatabaseSession
from src.main.database_module.database_classes.db_classes import Play
PLAY_ID = 'play_id'
PITCH_VALUE = 'pitch_value'
SWING_VALUE = 'swing_value'
CREATION_DATE = 'creation_date'
class PlayDAO():
db_string = None
session = None
Session = None
engine = None
def __init__(self):
pass
def insert(self, play_info):
session = DatabaseSession.session
play = Play(
play_id = play_info[PLAY_ID],
pitch_value = play_info[PITCH_VALUE] if PITCH_VALUE in play_info else None,
swing_value = play_info[SWING_VALUE] if SWING_VALUE in play_info else None,
creation_date = play_info[CREATION_DATE]
)
session.add(play)
session.commit()
def get_play_by_id(self, input_id):
session = DatabaseSession.session
return self.__convert_all__(session.query(Play).filter(Play.play_id == input_id))
'''
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 get_active_play(self):
session = DatabaseSession.session
plays = self.__convert_all__(session.query(Play).filter(Play.pitch_value == None))
if len(plays) > 1:
raise AssertionError("More than one active play! Can't continue!")
elif len(plays) == 0:
return None
else:
return plays[0]
def resolve_play(self, input_pitch, input_swing):
session = DatabaseSession.session
active_id = self.get_active_play()
session\
.query(Play)\
.filter(Play.pitch_value == None)\
.update({Play.pitch_value: input_pitch, Play.swing_value: input_swing})
session.commit()
return active_id
'''
Converts the database object into a Dictionary, so that the database object is not passed out of the
datastore layer.
'''
def __convert_all__(self, plays):
converted_plays = []
for play in plays:
play_dict = {}
for column in play.__dict__:
play_dict[column] = str(getattr(play, column))
converted_plays.append(deepcopy(play_dict))
return converted_plays

26
src/main/db_session.py Normal file
View File

@ -0,0 +1,26 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import sys
sys.path.append('../../../../../src')
from src.main.configs import Configs, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_HOST, DATABASE_NAME
'''
Stores a database session for use throughout the application. Must be initialized at startup before any database calls
are made and AFTER the Configurations are setup.
This shouldn't need to be touched after startup. To use, see the sqlalchemy docs...or just start by calling Session()
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
def __init__(self):
config_map = Configs.configs
db_string = 'postgresql://%s:%s@%s/%s' % \
(config_map[DATABASE_USERNAME], config_map[DATABASE_PASSWORD], config_map[DATABASE_HOST], config_map[DATABASE_NAME])
Session = sessionmaker(create_engine(db_string))
DatabaseSession.session = Session()

View File

View File

@ -0,0 +1,146 @@
import sys
import discord
import uuid
import datetime
from src.main.configs import Configs
from src.main.database_module.guess_dao import GuessDAO, GUESSED_NUMBER, MEMBER_ID, MEMBER_NAME
from src.main.database_module.play_dao import PlayDAO, PLAY_ID, CREATION_DATE
from src.main.db_session import DatabaseSession
from src.main.discord_module.leaderboard_config import LeaderboardConfig
play_dao = None
guess_dao = None
bot = discord.Client()
@bot.event
async def on_ready():
print('Logged in as')
print(bot.user.name)
print(bot.user.id)
print('------')
@bot.event
async def on_message(message):
if message.author == bot.user:
return
content = message.content
'''
Sets up the next set of guesses.
'''
if content.startswith('!ghostball'):
if play_dao.is_active_play():
await bot.send_message(message.channel, "Um, I think 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()}
play_dao.insert(play_object)
await bot.send_message(message.channel, "@everyone, 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():
await bot.send_message(message.channel, "Hey, there's no active play! Start one up first with !ghostball.")
else:
play = play_dao.get_active_play()
guess_object = {PLAY_ID: play['play_id'],
MEMBER_ID: str(message.author.id),
GUESSED_NUMBER: guess_value,
MEMBER_NAME: str(message.author.name)}
if guess_dao.insert(guess_object):
await bot.add_reaction(message, emoji="\N{THUMBS UP SIGN}")
# Closes off the active play to be ready for the next set
if content.startswith('!resolve'):
# try:
pitch_value, swing_value = __parse_resolve_play__(content)
if pitch_value is None or swing_value is None:
await bot.send_message(message.channel, "Hey " + "<@" + str(message.author.id) + ">, I'm not sure what you meant. "
"You need real, numeric, values for this command to work. "
"Use !resolve <pitch number> <swing_number> and try again.")
# Check if we have an active play
if not play_dao.is_active_play():
await bot.send_message(message.channel, "You confused me. There's no active play so I have nothing to close!")
else:
play = play_dao.resolve_play(pitch_value, swing_value)
guess_dao.set_diff erences(pitch_value, play['play_id'])
await bot.send_message(message.channel,
"You got it boss. Closed this play, no further guesses allowed!")
# Likely due to too few parameters but could be any number of things
# except :
# await bot.send_message(message.channel, "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'):
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 bot.send_message(message.channel, string_to_send)
elif leaderboard_config.should_sort_by_best_average():
pass
else:
await bot.send_message(message.channel, "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('!help'):
help_message = __get_help_message__()
recipient = await bot.get_user_info(message.author.id)
await bot.send_message(recipient, help_message)
def __get_help_message__():
# Start message with person who asked for help
help_message = "Hey! I can be instructed to do any number of things! Use the following commands: \n" \
"!guess <NUMBER> --- This will add your guess to the currently active play. " \
"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 <PITCH_NUMBER> <SWING NUMBER>--- Uses the pitch number and real swing number " \
"to figure out who was closest and ends the active play.\n" \
"<HELP MESSAGE NEEDS DOCUMENTATION FOR LEADERBOARD COMMAND>\n"
return help_message
def __parse_leaderboard_message__(message_content):
return LeaderboardConfig(message_content)
def __parse_guess__(message_content):
pieces = message_content.split(' ')
try:
return pieces[1]
except TypeError:
return None
def __parse_resolve_play__(message_content):
pieces = message_content.split(' ')
try:
return pieces[1], pieces[2]
except TypeError:
return None
if __name__ == '__main__':
args = sys.argv
token = args[1]
file_path = args[2]
configs = Configs(file_path)
databaseSession = DatabaseSession()
play_dao = PlayDAO()
guess_dao = GuessDAO()
bot.run(token)

View File

@ -0,0 +1,15 @@
class LeaderboardConfig():
closest = True
def __init__(self, message_content):
pieces = message_content.split(' ')
if len(pieces) == 1:
return
if pieces[1] == 'average':
self.closest = False
def should_sort_by_pure_closest(self):
return self.closest
def should_sort_by_best_average(self):
return not self.closest

0
src/test/__init__.py Normal file
View File