Merge remote-tracking branch 'upstream/indev' into react

This commit is contained in:
Elijah Steres 2021-01-08 00:44:31 -05:00
commit 2cad62eda4
8 changed files with 392 additions and 16 deletions

4
.gitignore vendored
View File

@ -347,4 +347,8 @@ ids
# database # database
matteo.db matteo.db
matteo.db-wal
matteo.db-shm
/matteo_env/Lib/site-packages/flask_socketio/__init__.py /matteo_env/Lib/site-packages/flask_socketio/__init__.py
env

View File

@ -96,7 +96,9 @@ these folks are helping me a *ton* via patreon, and i cannot possibly thank them
- Chris Denmark - Chris Denmark
- Astrid Bek - Astrid Bek
- Kameleon - Kameleon
- Ryan Littleton
- Evie Diver
## Attribution ## Attribution
Twemoji is copyright 2020 Twitter, Inc and other contributors; code licensed under [the MIT License](http://opensource.org/licenses/MIT), graphics licensed under [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/) Twemoji is copyright 2020 Twitter, Inc and other contributors; code licensed under [the MIT License](http://opensource.org/licenses/MIT), graphics licensed under [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/)

View File

@ -2,12 +2,17 @@
import os, json, datetime, re import os, json, datetime, re
import sqlite3 as sql import sqlite3 as sql
data_dir = "data"
def create_connection(): def create_connection():
#create connection, create db if doesn't exist #create connection, create db if doesn't exist
conn = None conn = None
try: try:
conn = sql.connect("matteo.db") 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 return conn
except: except:
print("oops, db connection no work") print("oops, db connection no work")

View File

@ -2,8 +2,13 @@ import json, random, os, math, jsonpickle
from enum import Enum from enum import Enum
import database as db import database as db
data_dir = "data"
games_config_file = os.path.join(data_dir, "games_config.json")
def config(): 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 #generate default config
config_dic = { config_dic = {
"default_length" : 3, "default_length" : 3,
@ -16,11 +21,11 @@ def config():
"stolen_base_chance_mod" : 1, "stolen_base_chance_mod" : 1,
"stolen_base_success_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) json.dump(config_dic, config_file, indent=4)
return config_dic return config_dic
else: else:
with open("games_config.json") as config_file: with open(games_config_file) as config_file:
return json.load(config_file) return json.load(config_file)
def all_weathers(): def all_weathers():
@ -45,7 +50,7 @@ class appearance_outcomes(Enum):
single = "hits a single!" single = "hits a single!"
double = "hits a double!" double = "hits a double!"
triple = "hits a triple!" triple = "hits a triple!"
homerun = "hits a home run!" homerun = "hits a dinger!"
grandslam = "hits a grand slam!" grandslam = "hits a grand slam!"
@ -502,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: 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 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 result["mulligan"] = True
return (result, 0) return (result, 0)
@ -801,4 +806,4 @@ class weather(object):
self.counter_home = 0 self.counter_home = 0
def __str__(self): def __str__(self):
return f"{self.emoji} {self.name}" return f"{self.emoji} {self.name}"

View File

@ -103,9 +103,6 @@ class bracket(object):
self.depth += 1 self.depth += 1
return self.dive(branch[0]), self.dive(branch[1]) return self.dive(branch[0]), self.dive(branch[1])
#def set_winners(self, branch, winners_list):
#new_bracket =
def set_winners_dive(self, winners_list, index = 0, branch = None, parent = None): def set_winners_dive(self, winners_list, index = 0, branch = None, parent = None):
if branch is None: if branch is None:
branch = self.this_bracket.copy() branch = self.this_bracket.copy()

View File

@ -1,12 +1,29 @@
#interfaces with onomancer #interfaces with onomancer
import requests, json, urllib import requests, json, urllib
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
import database as db import database as db
onomancer_url = "https://onomancer.sibr.dev/api/" onomancer_url = "https://onomancer.sibr.dev/api/"
name_stats_hook = "getOrGenerateStats?name=" name_stats_hook = "getOrGenerateStats?name="
collection_hook = "getCollection?token=" 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): def get_stats(name):
player = db.get_stats(name) player = db.get_stats(name)
@ -14,7 +31,7 @@ def get_stats(name):
return player #returns json_string return player #returns json_string
#yell at onomancer if not in cache or too old #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: if response.status_code == 200:
stats = json.dumps(response.json()) stats = json.dumps(response.json())
db.cache_stats(name, stats) db.cache_stats(name, stats)
@ -30,9 +47,32 @@ def get_scream(username):
return scream return scream
def get_collection(collection_url): 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: if response.status_code == 200:
for player in response.json()['lineup'] + response.json()['rotation']: for player in response.json()['lineup'] + response.json()['rotation']:
db.cache_stats(player['name'], json.dumps(player)) db.cache_stats(player['name'], json.dumps(player))
return json.dumps(response.json()) 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 = _retry_session().get(
onomancer_url + names_hook,
params={
'limit': limit,
'threshold': threshold,
'with_stats': 1,
'random': 1,
},
)
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

153
the_draft.py Normal file
View File

@ -0,0 +1,153 @@
from collections import namedtuple
import games
import json
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.
"""
@classmethod
def make_draft(cls):
draft = cls()
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
@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
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(f'{self._active_participant.handle} is drafting, not you')
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', ' ').strip().lower() == player_name.lower():
player = stats
break
else:
# still not found
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(games.player(json.dumps(player)))
elif self._round == DRAFT_ROUNDS:
self._active_participant.team.add_pitcher(games.player(json.dumps(player)))
self.advance_draft()
if self._active_participant == BOOKMARK:
self.advance_draft()
return player
def get_teams(self):
teams = []
if self._active_participant != BOOKMARK:
teams.append((self._active_participant.handle, self._active_participant.team))
for participant in self._participants:
if participant != BOOKMARK:
teams.append((participant.handle, participant.team))
return teams
def finish_draft(self):
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}')
if __name__ == '__main__':
# extremely robust testing OC do not steal
DRAFT_ROUNDS = 2
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())

View File

@ -1,9 +1,13 @@
import discord, json, math, os, roman, games, asyncio, random, main_controller, threading, time, urllib, leagues import discord, json, math, os, roman, games, asyncio, random, main_controller, threading, time, urllib, leagues
import database as db import database as db
import onomancer as ono import onomancer as ono
import random
from the_draft import Draft, DRAFT_ROUNDS
from flask import Flask from flask import Flask
from uuid import uuid4 from uuid import uuid4
data_dir = "data"
config_filename = os.path.join(data_dir, "config.json")
class Command: class Command:
def isauthorized(self, user): def isauthorized(self, user):
@ -12,6 +16,12 @@ class Command:
async def execute(self, msg, command): async def execute(self, msg, command):
return return
class DraftError(Exception):
pass
class SlowDraftError(DraftError):
pass
class CommandError(Exception): class CommandError(Exception):
pass pass
@ -570,6 +580,152 @@ class StartTournamentCommand(Command):
await start_tournament_round(channel, tourney) 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 `d`, `draft`, or `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_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')
return
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()
footer = f"The draft class of {random.randint(2007, 2075)}"
while draft.round <= DRAFT_ROUNDS:
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(
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.")
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(
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("Sound off, folks. 👍 if you're 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("We dragged out the photocopier for this! Fine, putting it back.")
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
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
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 = [ commands = [
IntroduceCommand(), IntroduceCommand(),
CountActiveGamesCommand(), CountActiveGamesCommand(),
@ -594,6 +750,8 @@ commands = [
CreditCommand(), CreditCommand(),
RomanCommand(), RomanCommand(),
HelpCommand(), HelpCommand(),
StartDraftCommand(),
DraftPlayerCommand(),
] ]
client = discord.Client() client = discord.Client()
@ -605,7 +763,9 @@ thread1 = threading.Thread(target=main_controller.update_loop)
thread1.start() thread1.start()
def config(): 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 #generate default config
config_dic = { config_dic = {
"token" : "", "token" : "",
@ -617,12 +777,12 @@ def config():
"soulscream channel id" : 0, "soulscream channel id" : 0,
"game_freeze" : 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) 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!") print("please fill in bot token and any bot admin discord ids to the new config.json file!")
quit() quit()
else: else:
with open("config.json") as config_file: with open(config_filename) as config_file:
return json.load(config_file) return json.load(config_file)
@client.event @client.event
@ -1011,6 +1171,16 @@ async def team_delete_confirm(channel, team, owner):
return return
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=footer)
return embed
def build_team_embed(team): def build_team_embed(team):
embed = discord.Embed(color=discord.Color.purple(), title=team.name) embed = discord.Embed(color=discord.Color.purple(), title=team.name)
lineup_string = "" lineup_string = ""