Catchup commit
Includes discord_ui refactor, testing overhaul, addition of
This commit is contained in:
parent
dd23d3657a
commit
3debfd6e82
@ -32,5 +32,9 @@ README.md
|
||||
**/venv
|
||||
**/tests
|
||||
**/storage
|
||||
**/htmlcov
|
||||
*_legacy.py
|
||||
pytest.ini
|
||||
pytest.ini
|
||||
CLAUDE.md
|
||||
**.db
|
||||
**/.claude
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -132,4 +132,8 @@ dmypy.json
|
||||
.idea/
|
||||
storage*
|
||||
*compose.yml
|
||||
|
||||
CLAUDE**
|
||||
**.db
|
||||
**/htmlcov
|
||||
.vscode/**
|
||||
.claude/**
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -3,5 +3,8 @@
|
||||
"."
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.pytestEnv": {
|
||||
"DOCKER_HOST": "unix:///home/cal/.docker/desktop/docker.sock"
|
||||
}
|
||||
}
|
||||
13
api_calls.py
13
api_calls.py
@ -9,10 +9,7 @@ import os
|
||||
from exceptions import DatabaseError
|
||||
|
||||
AUTH_TOKEN = {'Authorization': f'Bearer {os.environ.get("API_TOKEN")}'}
|
||||
try:
|
||||
ENV_DATABASE = os.environ.get('DATABASE').lower()
|
||||
except Exception as e:
|
||||
ENV_DATABASE = 'dev'
|
||||
ENV_DATABASE = os.getenv("DATABASE", "dev").lower()
|
||||
DB_URL = 'https://pd.manticorum.com/api' if 'prod' in ENV_DATABASE else 'https://pddev.manticorum.com/api'
|
||||
master_debug = True
|
||||
PLAYER_CACHE = {}
|
||||
@ -26,7 +23,7 @@ def param_char(other_params):
|
||||
return '?'
|
||||
|
||||
|
||||
def get_req_url(endpoint: str, api_ver: int = 2, object_id: int = None, params: list = None):
|
||||
def get_req_url(endpoint: str, api_ver: int = 2, object_id: Optional[int] = None, params: Optional[list] = None):
|
||||
req_url = f'{DB_URL}/v{api_ver}/{endpoint}{"/" if object_id is not None else ""}{object_id if object_id is not None else ""}'
|
||||
|
||||
if params:
|
||||
@ -55,7 +52,7 @@ def log_return_value(log_string: str):
|
||||
# logger.debug(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}\n')
|
||||
|
||||
|
||||
async def db_get(endpoint: str, api_ver: int = 2, object_id: int = None, params: list = None, none_okay: bool = True, timeout: int = 3):
|
||||
async def db_get(endpoint: str, api_ver: int = 2, object_id: Optional[int] = None, params: Optional[list] = None, none_okay: bool = True, timeout: int = 3):
|
||||
req_url = get_req_url(endpoint, api_ver=api_ver, object_id=object_id, params=params)
|
||||
log_string = f'db_get - get: {endpoint} id: {object_id} params: {params}'
|
||||
logger.info(log_string) if master_debug else logger.debug(log_string)
|
||||
@ -93,7 +90,7 @@ async def db_patch(endpoint: str, object_id: int, params: list, api_ver: int = 2
|
||||
raise DatabaseError(e)
|
||||
|
||||
|
||||
async def db_post(endpoint: str, api_ver: int = 2, payload: dict = None, timeout: int = 3):
|
||||
async def db_post(endpoint: str, api_ver: int = 2, payload: Optional[dict] = None, timeout: int = 3):
|
||||
req_url = get_req_url(endpoint, api_ver=api_ver)
|
||||
log_string = f'db_post - post: {endpoint} payload: {payload}\ntype: {type(payload)}'
|
||||
logger.info(log_string) if master_debug else logger.debug(log_string)
|
||||
@ -110,7 +107,7 @@ async def db_post(endpoint: str, api_ver: int = 2, payload: dict = None, timeout
|
||||
raise DatabaseError(e)
|
||||
|
||||
|
||||
async def db_put(endpoint: str, api_ver: int = 2, payload: dict = None, timeout: int = 3):
|
||||
async def db_put(endpoint: str, api_ver: int = 2, payload: Optional[dict] = None, timeout: int = 3):
|
||||
req_url = get_req_url(endpoint, api_ver=api_ver)
|
||||
log_string = f'post:\n{endpoint} payload: {payload}\ntype: {type(payload)}'
|
||||
logger.info(log_string) if master_debug else logger.debug(log_string)
|
||||
|
||||
@ -34,7 +34,7 @@ class Admins(commands.Cog):
|
||||
# Check for Paper Sluggers event
|
||||
e_query = await db_get('events', params=[('name', 'Paper Sluggers')])
|
||||
if e_query is None:
|
||||
this_event = db_post(
|
||||
this_event = await db_post(
|
||||
'events',
|
||||
payload={
|
||||
"name": "Paper Sluggers",
|
||||
@ -51,16 +51,17 @@ class Admins(commands.Cog):
|
||||
|
||||
# Check for Game Rewards
|
||||
gr_query = await db_get('gamerewards', params=[('name', 'MVP Pack')])
|
||||
if gr_query['count'] == 0:
|
||||
mv_pack = db_post(
|
||||
'gamerewards',
|
||||
payload={
|
||||
'name': 'MVP',
|
||||
'pack_type_id': 5
|
||||
}
|
||||
)
|
||||
else:
|
||||
mv_pack = gr_query['gamerewards'][0]
|
||||
if gr_query:
|
||||
if gr_query['count'] == 0:
|
||||
mv_pack = db_post(
|
||||
'gamerewards',
|
||||
payload={
|
||||
'name': 'MVP',
|
||||
'pack_type_id': 5
|
||||
}
|
||||
)
|
||||
else:
|
||||
mv_pack = gr_query['gamerewards'][0]
|
||||
|
||||
gr_query = await db_get('gamerewards', params=[('name', 'All-Star Pack')])
|
||||
if gr_query['count'] == 0:
|
||||
|
||||
346
constants.py
Normal file
346
constants.py
Normal file
@ -0,0 +1,346 @@
|
||||
"""
|
||||
Paper Dynasty Discord App Constants
|
||||
|
||||
This module contains all the configuration constants, static data structures,
|
||||
and lookup tables used throughout the application.
|
||||
"""
|
||||
import discord
|
||||
from typing import Literal
|
||||
|
||||
# Season Configuration
|
||||
SBA_SEASON = 11
|
||||
PD_SEASON = 9
|
||||
ranked_cardsets = [20, 21, 22, 17, 18, 19]
|
||||
LIVE_CARDSET_ID = 24
|
||||
LIVE_PROMO_CARDSET_ID = 25
|
||||
MAX_CARDSET_ID = 30
|
||||
|
||||
# Cardset Configuration
|
||||
CARDSETS = {
|
||||
'Ranked': {
|
||||
'primary': ranked_cardsets,
|
||||
'human': ranked_cardsets
|
||||
},
|
||||
'Minor League': {
|
||||
'primary': [20, 8], # 1998, Mario
|
||||
'secondary': [6], # 2013
|
||||
'human': [x for x in range(1, MAX_CARDSET_ID)]
|
||||
},
|
||||
'Major League': {
|
||||
'primary': [20, 21, 17, 18, 12, 6, 7, 8], # 1998, 1998 Promos, 2024, 24 Promos, 2008, 2013, 2012, Mario
|
||||
'secondary': [5, 3], # 2019, 2022
|
||||
'human': ranked_cardsets
|
||||
},
|
||||
'Hall of Fame': {
|
||||
'primary': [x for x in range(1, MAX_CARDSET_ID)],
|
||||
'secondary': [],
|
||||
'human': ranked_cardsets
|
||||
},
|
||||
'Flashback': {
|
||||
'primary': [5, 1, 3, 9, 8], # 2019, 2021, 2022, 2023, Mario
|
||||
'secondary': [13, 5], # 2018, 2019
|
||||
'human': [5, 1, 3, 9, 8] # 2019, 2021, 2022, 2023
|
||||
},
|
||||
'gauntlet-3': {
|
||||
'primary': [13], # 2018
|
||||
'secondary': [5, 11, 9], # 2019, 2016, 2023
|
||||
'human': [x for x in range(1, MAX_CARDSET_ID)]
|
||||
},
|
||||
'gauntlet-4': {
|
||||
'primary': [3, 6, 16], # 2022, 2013, Backyard Baseball
|
||||
'secondary': [4, 9], # 2022 Promos, 2023
|
||||
'human': [3, 4, 6, 9, 15, 16]
|
||||
},
|
||||
'gauntlet-5': {
|
||||
'primary': [17, 8], # 2024, Mario
|
||||
'secondary': [13], # 2018
|
||||
'human': [x for x in range(1, MAX_CARDSET_ID)]
|
||||
},
|
||||
'gauntlet-6': {
|
||||
'primary': [20, 8], # 1998, Mario
|
||||
'secondary': [12], # 2008
|
||||
'human': [x for x in range(1, MAX_CARDSET_ID)]
|
||||
},
|
||||
'gauntlet-7': {
|
||||
'primary': [5, 23], # 2019, Brilliant Stars
|
||||
'secondary': [1], # 2021
|
||||
'human': [x for x in range(1, MAX_CARDSET_ID)]
|
||||
}
|
||||
}
|
||||
|
||||
# Application Configuration
|
||||
SBA_COLOR = 'a6ce39'
|
||||
PD_PLAYERS = 'Paper Dynasty Players'
|
||||
SBA_PLAYERS_ROLE_NAME = f'Season {SBA_SEASON} Players'
|
||||
PD_PLAYERS_ROLE_NAME = f'Paper Dynasty Players'
|
||||
|
||||
# External URLs and Resources
|
||||
PD_IMAGE_BUCKET = 'https://paper-dynasty.s3.us-east-1.amazonaws.com/static-images'
|
||||
PKMN_REF_URL = 'https://pkmncards.com/card/'
|
||||
|
||||
# Google Sheets Configuration
|
||||
RATINGS_BATTER_FORMULA = '=IMPORTRANGE("1zDmlOw94gTzOAjqOpNdDZsg0O6rxNWkL4-XT6-iL2IE","guide_Batters!A1:CD")'
|
||||
RATINGS_PITCHER_FORMULA = '=IMPORTRANGE("1zDmlOw94gTzOAjqOpNdDZsg0O6rxNWkL4-XT6-iL2IE","guide_Pitchers!A1:BQ")'
|
||||
RATINGS_SHEET_KEY = '1zDmlOw94gTzOAjqOpNdDZsg0O6rxNWkL4-XT6-iL2IE'
|
||||
|
||||
# MLB Teams Lookup
|
||||
ALL_MLB_TEAMS = {
|
||||
'Arizona Diamondbacks': ['ARI', 'Diamondbacks'],
|
||||
'Atlanta Braves': ['ATL', 'MLN', 'Braves'],
|
||||
'Baltimore Orioles': ['BAL', 'Orioles'],
|
||||
'Boston Red Sox': ['BOS', 'Red Sox'],
|
||||
'Chicago Cubs': ['CHC', 'Cubs'],
|
||||
'Chicago White Sox': ['CHW', 'White Sox'],
|
||||
'Cincinnati Reds': ['CIN', 'Reds'],
|
||||
'Cleveland Guardians': ['CLE', 'Guardians'],
|
||||
'Colorado Rockies': ['COL', 'Rockies'],
|
||||
'Detroit Tigers': ['DET', 'Tigers'],
|
||||
'Houston Astros': ['HOU', 'Astros'],
|
||||
'Kansas City Royals': ['KCR', 'Royals'],
|
||||
'Los Angeles Angels': ['LAA', 'CAL', 'Angels'],
|
||||
'Los Angeles Dodgers': ['LAD', 'Dodgers'],
|
||||
'Miami Marlins': ['MIA', 'Marlins'],
|
||||
'Milwaukee Brewers': ['MIL', 'MKE', 'Brewers'],
|
||||
'Minnesota Twins': ['MIN', 'Twins'],
|
||||
'New York Mets': ['NYM', 'Mets'],
|
||||
'New York Yankees': ['NYY', 'Yankees'],
|
||||
'Oakland Athletics': ['OAK', 'Athletics'],
|
||||
'Philadelphia Phillies': ['PHI', 'Phillies'],
|
||||
'Pittsburgh Pirates': ['PIT', 'Pirates'],
|
||||
'San Diego Padres': ['SDP', 'Padres'],
|
||||
'Seattle Mariners': ['SEA', 'Mariners'],
|
||||
'San Francisco Giants': ['SFG', 'Giants'],
|
||||
'St Louis Cardinals': ['STL', 'Cardinals'],
|
||||
'Tampa Bay Rays': ['TBR', 'Rays'],
|
||||
'Texas Rangers': ['TEX', 'Senators', 'Rangers'],
|
||||
'Toronto Blue Jays': ['TOR', 'Jays'],
|
||||
'Washington Nationals': ['WSN', 'WAS', 'Nationals'],
|
||||
}
|
||||
|
||||
# Image URLs
|
||||
IMAGES = {
|
||||
'logo': f'{PD_IMAGE_BUCKET}/sba-logo.png',
|
||||
'mvp-hype': f'{PD_IMAGE_BUCKET}/mvp.png',
|
||||
'pack-sta': f'{PD_IMAGE_BUCKET}/pack-standard.png',
|
||||
'pack-pre': f'{PD_IMAGE_BUCKET}/pack-premium.png',
|
||||
'pack-mar': f'{PD_IMAGE_BUCKET}/mario-gauntlet.png',
|
||||
'pack-pkmnbs': f'{PD_IMAGE_BUCKET}/pokemon-brilliantstars.jpg',
|
||||
'mvp': {
|
||||
'Arizona Diamondbacks': f'{PD_IMAGE_BUCKET}/mvp/arizona-diamondbacks.gif',
|
||||
'Atlanta Braves': f'{PD_IMAGE_BUCKET}/mvp/atlanta-braves.gif',
|
||||
'Baltimore Orioles': f'{PD_IMAGE_BUCKET}/mvp/baltimore-orioles.gif',
|
||||
'Boston Red Sox': f'{PD_IMAGE_BUCKET}/mvp/boston-red-sox.gif',
|
||||
'Chicago Cubs': f'{PD_IMAGE_BUCKET}/mvp/chicago-cubs.gif',
|
||||
'Chicago White Sox': f'{PD_IMAGE_BUCKET}/mvp/chicago-white-sox.gif',
|
||||
'Cincinnati Reds': f'{PD_IMAGE_BUCKET}/mvp/cincinnati-reds.gif',
|
||||
'Cleveland Indians': f'{PD_IMAGE_BUCKET}/mvp/cleveland-guardians.gif',
|
||||
'Cleveland Guardians': f'{PD_IMAGE_BUCKET}/mvp/cleveland-guardians.gif',
|
||||
'Colorado Rockies': f'{PD_IMAGE_BUCKET}/mvp/colorado-rockies.gif',
|
||||
'Detroit Tigers': f'{PD_IMAGE_BUCKET}/mvp/detroit-tigers.gif',
|
||||
'Houston Astros': f'{PD_IMAGE_BUCKET}/mvp/houston-astros.gif',
|
||||
'Kansas City Royals': f'{PD_IMAGE_BUCKET}/mvp/kansas-city-royals.gif',
|
||||
'Los Angeles Angels': f'{PD_IMAGE_BUCKET}/mvp/los-angeles-angels.gif',
|
||||
'Los Angeles Dodgers': f'{PD_IMAGE_BUCKET}/mvp/los-angeles-dodgers.gif',
|
||||
'Miami Marlins': f'{PD_IMAGE_BUCKET}/mvp/miami-marlins.gif',
|
||||
'Milwaukee Brewers': f'{PD_IMAGE_BUCKET}/mvp/milwaukee-brewers.gif',
|
||||
'Minnesota Twins': f'{PD_IMAGE_BUCKET}/mvp/minnesota-twins.gif',
|
||||
'New York Mets': f'{PD_IMAGE_BUCKET}/mvp/new-york-mets.gif',
|
||||
'New York Yankees': f'{PD_IMAGE_BUCKET}/mvp/new-york-yankees.gif',
|
||||
'Oakland Athletics': f'{PD_IMAGE_BUCKET}/mvp/oakland-athletics.gif',
|
||||
'Philadelphia Phillies': f'{PD_IMAGE_BUCKET}/mvp/philadelphia-phillies.gif',
|
||||
'Pittsburgh Pirates': f'{PD_IMAGE_BUCKET}/mvp/pittsburgh-pirates.gif',
|
||||
'San Diego Padres': f'{PD_IMAGE_BUCKET}/mvp/san-diego-padres.gif',
|
||||
'Seattle Mariners': f'{PD_IMAGE_BUCKET}/mvp/seattle-mariners.gif',
|
||||
'San Francisco Giants': f'{PD_IMAGE_BUCKET}/mvp/san-francisco-giants.gif',
|
||||
'St Louis Cardinals': f'{PD_IMAGE_BUCKET}/mvp/st-louis-cardinals.gif',
|
||||
'St. Louis Cardinals': f'{PD_IMAGE_BUCKET}/mvp/st-louis-cardinals.gif',
|
||||
'Tampa Bay Rays': f'{PD_IMAGE_BUCKET}/mvp/tampa-bay-rays.gif',
|
||||
'Texas Rangers': f'{PD_IMAGE_BUCKET}/mvp/texas-rangers.gif',
|
||||
'Toronto Blue Jays': f'{PD_IMAGE_BUCKET}/mvp/toronto-blue-jays.gif',
|
||||
'Washington Nationals': f'{PD_IMAGE_BUCKET}/mvp/washington-nationals.gif',
|
||||
'Junior All Stars': f'{PD_IMAGE_BUCKET}/mvp.png',
|
||||
'Mario Super Sluggers': f'{PD_IMAGE_BUCKET}/mvp.png',
|
||||
'Pokemon League': f'{PD_IMAGE_BUCKET}/masterball.jpg'
|
||||
},
|
||||
'gauntlets': f'{PD_IMAGE_BUCKET}/gauntlets.png'
|
||||
}
|
||||
|
||||
# Game Mechanics Charts
|
||||
INFIELD_X_CHART = {
|
||||
'si1': {
|
||||
'rp': 'No runner on first: Batter is safe at first and no one covers second. Batter to second, runners only '
|
||||
'advance 1 base.\nRunner on first: batter singles, runners advance 1 base.',
|
||||
'e1': 'Single and Error, batter to second, runners advance 2 bases.',
|
||||
'e2': 'Single and Error, batter to third, all runners score.',
|
||||
'no': 'Single, runners advance 1 base.'
|
||||
},
|
||||
'po': {
|
||||
'rp': 'The batters hits a popup. None of the fielders take charge on the play and the ball drops in the '
|
||||
'infield for a SI1! All runners advance 1 base.',
|
||||
'e1': 'The catcher drops a popup for an error. All runners advance 1 base.',
|
||||
'e2': 'The catcher grabs a squib in front of the plate and throws it into right field. The batter goes to '
|
||||
'second and all runners score.',
|
||||
'no': 'The batter pops out to the catcher.'
|
||||
},
|
||||
'fo': {
|
||||
'rp': 'Batter swings and misses, but is awarded first base on a catcher interference call! One base error, '
|
||||
'baserunners advance only if forced.',
|
||||
'e1': 'The catcher drops a foul popup for an error. Batter rolls AB again.',
|
||||
'e2': 'The catcher drops a foul popup for an error. Batter rolls AB again.',
|
||||
'no': 'Runner(s) on base: make a passed ball check. If no passed ball, batter pops out to the catcher. If a '
|
||||
'passed ball occurs, batter roll his AB again.\nNo runners: batter pops out to the catcher'
|
||||
},
|
||||
'g1': {
|
||||
'rp': 'Runner on first, <2 outs: runner on first breaks up the double play, gbB\n'
|
||||
'Else: gbA',
|
||||
'e1': 'Error, batter to first, runners advance 1 base.',
|
||||
'e2': 'Error, batter to second, runners advance 2 bases.',
|
||||
'no': 'Consult Groundball Chart: `!gbA`'
|
||||
},
|
||||
'g2': {
|
||||
'rp': 'Runner(s) on base: fielder makes bad throw for lead runner but batter is out at first for a gbC\n'
|
||||
'No runners: gbB',
|
||||
'e1': 'Error, batter to first, runners advance 1 base.',
|
||||
'e2': 'Error, batter to second, runners advance 2 bases.',
|
||||
'no': 'Consult Groundball Chart: `!gbB`'
|
||||
},
|
||||
'g3': {
|
||||
'rp': 'Runner(s) on base: fielder checks the runner before throwing to first and allows a SI*\n'
|
||||
'No runners: gbC',
|
||||
'e1': 'Error, batter to first, runners advance 1 base.',
|
||||
'e2': 'Error, batter to second, runners advance 2 bases.',
|
||||
'no': 'Consult Groundball Chart: `!gbC`'
|
||||
},
|
||||
'spd': {
|
||||
'rp': 'Catcher throws to first and hits the batter-runner in the back, SI1',
|
||||
'e1': 'Error, batter to first, runners advance 1 base.',
|
||||
'e2': 'Error, batter to second, runners advance 2 bases.',
|
||||
'no': 'Speed check, Batter\'s safe range = Running; if safe, SI*; if out, gbC'
|
||||
},
|
||||
}
|
||||
|
||||
OUTFIELD_X_CHART = {
|
||||
'si2': {
|
||||
'rp': 'Batter singles, baserunners advance 2 bases. As the batter rounds first, the fielder throws behind him '
|
||||
'and catches him off the bag for an out!',
|
||||
'e1': 'Single and error, batter to second, runners advance 2 bases.',
|
||||
'e2': 'Single and error, batter to third, all runners score.',
|
||||
'e3': 'Single and error, batter to third, all runners score',
|
||||
'no': 'Single, all runners advance 2 bases.'
|
||||
},
|
||||
'do2': {
|
||||
'rp': 'Batter doubles and runners advance three bases, but batter-runner is caught between second and third! '
|
||||
'He is tagged out in the rundown.',
|
||||
'e1': 'Double and error, batter to third, all runners score.',
|
||||
'e2': 'Double and error, batter to third, all runners score.',
|
||||
'e3': 'Double and error, batter and all runners score. Little league home run!',
|
||||
'no': 'Double, all runners advance 2 bases.'
|
||||
},
|
||||
'do3': {
|
||||
'rp': 'Batter doubles and runners advance three bases, but batter-runner is caught between second and third! '
|
||||
'He is tagged out in the rundown.',
|
||||
'e1': 'Double and error, batter to third, all runners score.',
|
||||
'e2': 'Double and error, batter and all runners score. Little league home run!',
|
||||
'e3': 'Double and error, batter and all runners score. Little league home run!',
|
||||
'no': 'Double, all runners score.'
|
||||
},
|
||||
'tr3': {
|
||||
'rp': 'Batter hits a ball into the gap and the outfielders collide trying to make the play! The ball rolls to '
|
||||
'the wall and the batter trots home with an inside-the-park home run!',
|
||||
'e1': 'Triple and error, batter and all runners score. Little league home run!',
|
||||
'e2': 'Triple and error, batter and all runners score. Little league home run!',
|
||||
'e3': 'Triple and error, batter and all runners score. Little league home run!',
|
||||
'no': 'Triple, all runners score.'
|
||||
},
|
||||
'f1': {
|
||||
'rp': 'The outfielder races back and makes a diving catch and collides with the wall! In the time he takes to '
|
||||
'recuperate, all baserunners tag-up and advance 2 bases.',
|
||||
'e1': '1 base error, runners advance 1 base.',
|
||||
'e2': '2 base error, runners advance 2 bases.',
|
||||
'e3': '3 base error, batter to third, all runners score.',
|
||||
'no': 'Flyball A'
|
||||
},
|
||||
'f2': {
|
||||
'rp': 'The outfielder catches the flyball for an out. If there is a runner on third, he tags-up and scores. '
|
||||
'The play is appealed and the umps rule that the runner left early and is out on the appeal!',
|
||||
'e1': '1 base error, runners advance 1 base.',
|
||||
'e2': '2 base error, runners advance 2 bases.',
|
||||
'e3': '3 base error, batter to third, all runners score.',
|
||||
'no': 'Flyball B'
|
||||
},
|
||||
'f3': {
|
||||
'rp': 'The outfielder makes a running catch in the gap! The lead runner lost track of the ball and was '
|
||||
'advancing - he cannot return in time and is doubled off by the outfielder.',
|
||||
'e1': '1 base error, runners advance 1 base.',
|
||||
'e2': '2 base error, runners advance 2 bases.',
|
||||
'e3': '3 base error, batter to third, all runners score.',
|
||||
'no': 'Flyball C'
|
||||
}
|
||||
}
|
||||
|
||||
# Player Rarity Values
|
||||
RARITY = {
|
||||
'HoF': 8,
|
||||
'MVP': 5,
|
||||
'All-Star': 3,
|
||||
'Starter': 2,
|
||||
'Reserve': 1,
|
||||
'Replacement': 0
|
||||
}
|
||||
|
||||
# Discord UI Options
|
||||
SELECT_CARDSET_OPTIONS = [
|
||||
discord.SelectOption(label='2025 Live', value='24'),
|
||||
discord.SelectOption(label='2025 Promos', value='25'),
|
||||
discord.SelectOption(label='1998 Season', value='20'),
|
||||
discord.SelectOption(label='1998 Promos', value='21'),
|
||||
discord.SelectOption(label='2024 Season', value='17'),
|
||||
discord.SelectOption(label='2024 Promos', value='18'),
|
||||
discord.SelectOption(label='2023 Season', value='9'),
|
||||
discord.SelectOption(label='2023 Promos', value='10'),
|
||||
discord.SelectOption(label='2022 Season', value='3'),
|
||||
discord.SelectOption(label='2022 Promos', value='4'),
|
||||
discord.SelectOption(label='2021 Season', value='1'),
|
||||
discord.SelectOption(label='2019 Season', value='5'),
|
||||
discord.SelectOption(label='2018 Season', value='13'),
|
||||
discord.SelectOption(label='2018 Promos', value='14'),
|
||||
discord.SelectOption(label='2016 Season', value='11'),
|
||||
discord.SelectOption(label='2013 Season', value='6'),
|
||||
discord.SelectOption(label='2012 Season', value='7')
|
||||
]
|
||||
|
||||
# Type Definitions
|
||||
ACTIVE_EVENT_LITERAL = Literal['2025 Season']
|
||||
DEFENSE_LITERAL = Literal['Pitcher', 'Catcher', 'First Base', 'Second Base', 'Third Base', 'Shortstop', 'Left Field', 'Center Field', 'Right Field']
|
||||
DEFENSE_NO_PITCHER_LITERAL = Literal['Catcher', 'First Base', 'Second Base', 'Third Base', 'Shortstop', 'Left Field', 'Center Field', 'Right Field']
|
||||
|
||||
# Color Definitions
|
||||
COLORS = {
|
||||
'sba': int('a6ce39', 16),
|
||||
'yellow': int('FFEA00', 16),
|
||||
'red': int('C70039', 16),
|
||||
'white': int('FFFFFF', 16)
|
||||
}
|
||||
|
||||
# Bot Response Content
|
||||
INSULTS = [
|
||||
'Ugh, who even are you?',
|
||||
'Ugh, who even are you? Go away.',
|
||||
'Ugh, who even are you? Leave me alone.',
|
||||
'I will call the fucking cops!',
|
||||
'I will call the fucking cops! Go away.',
|
||||
'I will call the fucking cops! Leave me alone',
|
||||
'Please don\'t talk to me',
|
||||
'Don\'t talk to me.',
|
||||
'Eww, don\'t talk to me.',
|
||||
'Get away from me.',
|
||||
'Get away from me, creep.',
|
||||
'Get away from me, loser.',
|
||||
'Get away from me, pedobear.',
|
||||
'Why are you even here?',
|
||||
'Why are you even here? Get lost.',
|
||||
'Why are you even here? Scram.',
|
||||
'Why are you even here? No one knows who you are.',
|
||||
'HEY, DON\'T TOUCH ME!',
|
||||
'Hey, don\'t touch me!'
|
||||
]
|
||||
23
discord_ui/__init__.py
Normal file
23
discord_ui/__init__.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""
|
||||
Discord UI Components
|
||||
|
||||
This package contains all Discord UI classes and components used throughout the application.
|
||||
"""
|
||||
|
||||
from .confirmations import Question, Confirm, ButtonOptions
|
||||
from .pagination import Pagination
|
||||
from .selectors import (
|
||||
SelectChoicePackTeam, SelectOpenPack, SelectPaperdexCardset,
|
||||
SelectPaperdexTeam, SelectBuyPacksCardset, SelectBuyPacksTeam,
|
||||
SelectUpdatePlayerTeam, SelectView
|
||||
)
|
||||
from .dropdowns import Dropdown, DropdownView
|
||||
|
||||
__all__ = [
|
||||
'Question', 'Confirm', 'ButtonOptions',
|
||||
'Pagination',
|
||||
'SelectChoicePackTeam', 'SelectOpenPack', 'SelectPaperdexCardset',
|
||||
'SelectPaperdexTeam', 'SelectBuyPacksCardset', 'SelectBuyPacksTeam',
|
||||
'SelectUpdatePlayerTeam', 'SelectView',
|
||||
'Dropdown', 'DropdownView'
|
||||
]
|
||||
188
discord_ui/confirmations.py
Normal file
188
discord_ui/confirmations.py
Normal file
@ -0,0 +1,188 @@
|
||||
"""
|
||||
Discord confirmation and interaction UI components.
|
||||
|
||||
Contains Question, Confirm, and ButtonOptions classes for user interactions.
|
||||
"""
|
||||
import asyncio
|
||||
import discord
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class Question:
|
||||
def __init__(self, bot, channel, prompt, qtype, timeout, embed=None):
|
||||
"""
|
||||
Version 0.4
|
||||
|
||||
:param bot, discord bot object
|
||||
:param channel, discord Channel object
|
||||
:param prompt, string, prompt message to post
|
||||
:param qtype, string, 'yesno', 'int', 'float', 'text', or 'url'
|
||||
:param timeout, float, time to wait for a response
|
||||
"""
|
||||
if not prompt and not embed:
|
||||
raise TypeError('prompt and embed may not both be None')
|
||||
|
||||
self.bot = bot
|
||||
self.channel = channel
|
||||
self.prompt = prompt
|
||||
self.qtype = qtype
|
||||
self.timeout = timeout
|
||||
self.embed = embed
|
||||
|
||||
async def ask(self, responders: list):
|
||||
"""
|
||||
Params: responder, list of discord User objects
|
||||
Returns: True/False if confirm question; full response otherwise
|
||||
"""
|
||||
yes = ['yes', 'y', 'ye', 'yee', 'yerp', 'yep', 'yeet', 'yip', 'yup', 'yarp', 'si']
|
||||
no = ['no', 'n', 'nope', 'nah', 'nyet', 'nein']
|
||||
|
||||
if type(responders) is not list:
|
||||
raise TypeError('Responders must be a list of Members')
|
||||
|
||||
def yesno(mes):
|
||||
return mes.channel == self.channel and mes.author in responders and mes.content.lower() in yes + no
|
||||
|
||||
def text(mes):
|
||||
return mes.channel == self.channel and mes.author in responders
|
||||
|
||||
await self.channel.send(content=self.prompt, embed=self.embed)
|
||||
|
||||
try:
|
||||
resp = await self.bot.wait_for(
|
||||
'message',
|
||||
timeout=self.timeout,
|
||||
check=yesno if self.qtype == 'yesno' else text
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
return None
|
||||
except Exception as e:
|
||||
await self.channel.send(f'Yuck, do you know what this means?\n\n{e}')
|
||||
return None
|
||||
|
||||
if self.qtype == 'yesno':
|
||||
if resp.content.lower() in yes:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
elif self.qtype == 'int':
|
||||
return int(resp.content)
|
||||
elif self.qtype == 'float':
|
||||
return float(resp.content)
|
||||
elif self.qtype == 'url':
|
||||
if resp.content[:7] == 'http://' or resp.content[:8] == 'https://':
|
||||
return resp.content
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return resp.content
|
||||
|
||||
|
||||
class Confirm(discord.ui.View):
|
||||
def __init__(self, responders: list, timeout: float = 300.0, label_type: Literal['yes', 'confirm'] = None):
|
||||
super().__init__(timeout=timeout)
|
||||
if not isinstance(responders, list):
|
||||
raise TypeError('responders must be a list')
|
||||
self.value = None
|
||||
self.responders = responders
|
||||
if label_type == 'yes':
|
||||
self.confirm.label = 'Yes'
|
||||
self.cancel.label = 'No'
|
||||
|
||||
# When the confirm button is pressed, set the inner value to `True` and
|
||||
# stop the View from listening to more input.
|
||||
# We also send the user an ephemeral message that we're confirming their choice.
|
||||
@discord.ui.button(label='Confirm', style=discord.ButtonStyle.green)
|
||||
async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user not in self.responders:
|
||||
return
|
||||
|
||||
self.value = True
|
||||
self.clear_items()
|
||||
self.stop()
|
||||
|
||||
# This one is similar to the confirmation button except sets the inner value to `False`
|
||||
@discord.ui.button(label='Cancel', style=discord.ButtonStyle.grey)
|
||||
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user not in self.responders:
|
||||
return
|
||||
|
||||
self.value = False
|
||||
self.clear_items()
|
||||
self.stop()
|
||||
|
||||
|
||||
class ButtonOptions(discord.ui.View):
|
||||
def __init__(self, responders: list, timeout: float = 300.0, labels=None):
|
||||
super().__init__(timeout=timeout)
|
||||
if not isinstance(responders, list):
|
||||
raise TypeError('responders must be a list')
|
||||
self.value = None
|
||||
self.responders = responders
|
||||
self.options = labels
|
||||
for count, x in enumerate(labels):
|
||||
if count == 0:
|
||||
self.option1.label = x
|
||||
if x is None or x.lower() == 'na' or x == 'N/A':
|
||||
self.remove_item(self.option1)
|
||||
if count == 1:
|
||||
self.option2.label = x
|
||||
if x is None or x.lower() == 'na' or x == 'N/A':
|
||||
self.remove_item(self.option2)
|
||||
if count == 2:
|
||||
self.option3.label = x
|
||||
if x is None or x.lower() == 'na' or x == 'N/A':
|
||||
self.remove_item(self.option3)
|
||||
if count == 3:
|
||||
self.option4.label = x
|
||||
if x is None or x.lower() == 'na' or x == 'N/A':
|
||||
self.remove_item(self.option4)
|
||||
if count == 4:
|
||||
self.option5.label = x
|
||||
if x is None or x.lower() == 'na' or x == 'N/A':
|
||||
self.remove_item(self.option5)
|
||||
|
||||
@discord.ui.button(label='Option 1', style=discord.ButtonStyle.primary)
|
||||
async def option1(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user not in self.responders:
|
||||
return
|
||||
|
||||
self.value = self.options[0]
|
||||
self.clear_items()
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label='Option 2', style=discord.ButtonStyle.primary)
|
||||
async def option2(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user not in self.responders:
|
||||
return
|
||||
|
||||
self.value = self.options[1]
|
||||
self.clear_items()
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label='Option 3', style=discord.ButtonStyle.primary)
|
||||
async def option3(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user not in self.responders:
|
||||
return
|
||||
|
||||
self.value = self.options[2]
|
||||
self.clear_items()
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label='Option 4', style=discord.ButtonStyle.primary)
|
||||
async def option4(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user not in self.responders:
|
||||
return
|
||||
|
||||
self.value = self.options[3]
|
||||
self.clear_items()
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label='Option 5', style=discord.ButtonStyle.primary)
|
||||
async def option5(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user not in self.responders:
|
||||
return
|
||||
|
||||
self.value = self.options[4]
|
||||
self.clear_items()
|
||||
self.stop()
|
||||
52
discord_ui/dropdowns.py
Normal file
52
discord_ui/dropdowns.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""
|
||||
Discord dropdown UI components.
|
||||
|
||||
Contains generic Dropdown and DropdownView classes for custom selections.
|
||||
"""
|
||||
import logging
|
||||
import discord
|
||||
|
||||
logger = logging.getLogger('discord_app')
|
||||
|
||||
|
||||
class Dropdown(discord.ui.Select):
|
||||
def __init__(self, option_list: list, placeholder: str = 'Make your selection', min_values: int = 1,
|
||||
max_values: int = 1, callback=None):
|
||||
# Set the options that will be presented inside the dropdown
|
||||
# options = [
|
||||
# discord.SelectOption(label='Red', description='Your favourite colour is red', emoji='🟥'),
|
||||
# discord.SelectOption(label='Green', description='Your favourite colour is green', emoji='🟩'),
|
||||
# discord.SelectOption(label='Blue', description='Your favourite colour is blue', emoji='🟦'),
|
||||
# ]
|
||||
|
||||
# The placeholder is what will be shown when no option is chosen
|
||||
# The min and max values indicate we can only pick one of the three options
|
||||
# The options parameter defines the dropdown options. We defined this above
|
||||
|
||||
# If a default option is set on any SelectOption, the View will not process if only the default is
|
||||
# selected by the user
|
||||
self.custom_callback = callback
|
||||
super().__init__(
|
||||
placeholder=placeholder,
|
||||
min_values=min_values,
|
||||
max_values=max_values,
|
||||
options=option_list
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
# Use the interaction object to send a response message containing
|
||||
# the user's favourite colour or choice. The self object refers to the
|
||||
# Select object, and the values attribute gets a list of the user's
|
||||
# selected options. We only want the first one.
|
||||
# await interaction.response.send_message(f'Your favourite colour is {self.values[0]}')
|
||||
logger.info(f'Dropdown callback: {self.custom_callback}')
|
||||
await self.custom_callback(interaction, self.values)
|
||||
|
||||
|
||||
class DropdownView(discord.ui.View):
|
||||
def __init__(self, dropdown_objects: list[Dropdown], timeout: float = 300.0):
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
# self.add_item(Dropdown())
|
||||
for x in dropdown_objects:
|
||||
self.add_item(x)
|
||||
49
discord_ui/pagination.py
Normal file
49
discord_ui/pagination.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""
|
||||
Discord pagination UI component.
|
||||
|
||||
Contains the Pagination class for navigating through multiple pages of content.
|
||||
"""
|
||||
import logging
|
||||
import discord
|
||||
|
||||
logger = logging.getLogger('discord_app')
|
||||
|
||||
|
||||
class Pagination(discord.ui.View):
|
||||
def __init__(self, responders: list, timeout: float = 300.0):
|
||||
super().__init__(timeout=timeout)
|
||||
if not isinstance(responders, list):
|
||||
raise TypeError('responders must be a list')
|
||||
|
||||
self.value = None
|
||||
self.responders = responders
|
||||
|
||||
@discord.ui.button(label='⏮️', style=discord.ButtonStyle.blurple)
|
||||
async def left_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user not in self.responders:
|
||||
logger.info(f'{interaction.user} is not in {self.responders}')
|
||||
return
|
||||
|
||||
self.value = 'left'
|
||||
await interaction.response.defer()
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label='❌️', style=discord.ButtonStyle.secondary)
|
||||
async def cancel_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user not in self.responders:
|
||||
logger.info(f'{interaction.user} is not in {self.responders}')
|
||||
return
|
||||
|
||||
self.value = 'cancel'
|
||||
await interaction.response.defer()
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label='⏭️', style=discord.ButtonStyle.blurple)
|
||||
async def right_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user not in self.responders:
|
||||
logger.info(f'{interaction.user} is not in {self.responders}')
|
||||
return
|
||||
|
||||
self.value = 'right'
|
||||
await interaction.response.defer()
|
||||
self.stop()
|
||||
488
discord_ui/selectors.py
Normal file
488
discord_ui/selectors.py
Normal file
@ -0,0 +1,488 @@
|
||||
"""
|
||||
Discord Select UI components.
|
||||
|
||||
Contains all Select classes for various team, cardset, and pack selections.
|
||||
"""
|
||||
import logging
|
||||
import discord
|
||||
from typing import Literal, Optional
|
||||
from constants import ALL_MLB_TEAMS, IMAGES
|
||||
|
||||
logger = logging.getLogger('discord_app')
|
||||
|
||||
# Team name to ID mappings
|
||||
AL_TEAM_IDS = {
|
||||
'Baltimore Orioles': 3,
|
||||
'Boston Red Sox': 4,
|
||||
'Chicago White Sox': 6,
|
||||
'Cleveland Guardians': 8,
|
||||
'Detroit Tigers': 10,
|
||||
'Houston Astros': 11,
|
||||
'Kansas City Royals': 12,
|
||||
'Los Angeles Angels': 13,
|
||||
'Minnesota Twins': 17,
|
||||
'New York Yankees': 19,
|
||||
'Oakland Athletics': 20,
|
||||
'Seattle Mariners': 24,
|
||||
'Tampa Bay Rays': 27,
|
||||
'Texas Rangers': 28,
|
||||
'Toronto Blue Jays': 29
|
||||
}
|
||||
|
||||
NL_TEAM_IDS = {
|
||||
'Arizona Diamondbacks': 1,
|
||||
'Atlanta Braves': 2,
|
||||
'Chicago Cubs': 5,
|
||||
'Cincinnati Reds': 7,
|
||||
'Colorado Rockies': 9,
|
||||
'Los Angeles Dodgers': 14,
|
||||
'Miami Marlins': 15,
|
||||
'Milwaukee Brewers': 16,
|
||||
'New York Mets': 18,
|
||||
'Philadelphia Phillies': 21,
|
||||
'Pittsburgh Pirates': 22,
|
||||
'San Diego Padres': 23,
|
||||
'San Francisco Giants': 25,
|
||||
'St Louis Cardinals': 26, # Note: constants has 'St Louis Cardinals' not 'St. Louis Cardinals'
|
||||
'Washington Nationals': 30
|
||||
}
|
||||
|
||||
# Get AL teams from constants
|
||||
AL_TEAMS = [team for team in ALL_MLB_TEAMS.keys() if team in AL_TEAM_IDS]
|
||||
NL_TEAMS = [team for team in ALL_MLB_TEAMS.keys() if team in NL_TEAM_IDS or team == 'St Louis Cardinals']
|
||||
|
||||
# Cardset mappings
|
||||
CARDSET_LABELS_TO_IDS = {
|
||||
'2022 Season': 3,
|
||||
'2022 Promos': 4,
|
||||
'2021 Season': 1,
|
||||
'2019 Season': 5,
|
||||
'2013 Season': 6,
|
||||
'2012 Season': 7,
|
||||
'Mario Super Sluggers': 8,
|
||||
'2023 Season': 9,
|
||||
'2016 Season': 11,
|
||||
'2008 Season': 12,
|
||||
'2018 Season': 13,
|
||||
'2024 Season': 17,
|
||||
'2024 Promos': 18,
|
||||
'1998 Season': 20,
|
||||
'2025 Live': 24,
|
||||
'Pokemon - Brilliant Stars': 23
|
||||
}
|
||||
|
||||
|
||||
def _get_team_id(team_name: str, league: Literal['AL', 'NL']) -> int:
|
||||
"""Get team ID from team name and league."""
|
||||
if league == 'AL':
|
||||
return AL_TEAM_IDS.get(team_name)
|
||||
else:
|
||||
# Handle the St. Louis Cardinals special case
|
||||
if team_name == 'St. Louis Cardinals':
|
||||
return NL_TEAM_IDS.get('St Louis Cardinals')
|
||||
return NL_TEAM_IDS.get(team_name)
|
||||
|
||||
|
||||
class SelectChoicePackTeam(discord.ui.Select):
|
||||
def __init__(self, which: Literal['AL', 'NL'], team, cardset_id: Optional[int] = None):
|
||||
self.which = which
|
||||
self.owner_team = team
|
||||
self.cardset_id = cardset_id
|
||||
|
||||
if which == 'AL':
|
||||
options = [discord.SelectOption(label=team) for team in AL_TEAMS]
|
||||
else:
|
||||
# Handle St. Louis Cardinals display name
|
||||
options = [discord.SelectOption(label='St. Louis Cardinals' if team == 'St Louis Cardinals' else team)
|
||||
for team in NL_TEAMS]
|
||||
|
||||
super().__init__(placeholder=f'Select an {which} team', options=options)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
# Import here to avoid circular imports
|
||||
from api_calls import db_get, db_patch
|
||||
from helpers import open_choice_pack
|
||||
|
||||
team_id = _get_team_id(self.values[0], self.which)
|
||||
if team_id is None:
|
||||
raise ValueError(f'Unknown team: {self.values[0]}')
|
||||
|
||||
await interaction.response.edit_message(content=f'You selected the **{self.values[0]}**', view=None)
|
||||
# Get the selected packs
|
||||
params = [
|
||||
('pack_type_id', 8), ('team_id', self.owner_team['id']), ('opened', False), ('limit', 1),
|
||||
('exact_match', True)
|
||||
]
|
||||
if self.cardset_id is not None:
|
||||
params.append(('pack_cardset_id', self.cardset_id))
|
||||
p_query = await db_get('packs', params=params)
|
||||
if p_query['count'] == 0:
|
||||
logger.error(f'open-packs - no packs found with params: {params}')
|
||||
raise ValueError(f'Unable to open packs')
|
||||
|
||||
this_pack = await db_patch('packs', object_id=p_query['packs'][0]['id'], params=[('pack_team_id', team_id)])
|
||||
|
||||
await open_choice_pack(this_pack, self.owner_team, interaction, self.cardset_id)
|
||||
|
||||
|
||||
class SelectOpenPack(discord.ui.Select):
|
||||
def __init__(self, options: list, team: dict):
|
||||
self.owner_team = team
|
||||
super().__init__(placeholder='Select a Pack Type', options=options)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
# Import here to avoid circular imports
|
||||
from api_calls import db_get
|
||||
from helpers import open_st_pr_packs, open_choice_pack
|
||||
|
||||
logger.info(f'SelectPackChoice - selection: {self.values[0]}')
|
||||
pack_vals = self.values[0].split('-')
|
||||
logger.info(f'pack_vals: {pack_vals}')
|
||||
|
||||
# Get the selected packs
|
||||
params = [('team_id', self.owner_team['id']), ('opened', False), ('limit', 5), ('exact_match', True)]
|
||||
|
||||
open_type = 'standard'
|
||||
if 'Standard' in pack_vals:
|
||||
open_type = 'standard'
|
||||
params.append(('pack_type_id', 1))
|
||||
elif 'Premium' in pack_vals:
|
||||
open_type = 'standard'
|
||||
params.append(('pack_type_id', 3))
|
||||
elif 'Daily' in pack_vals:
|
||||
params.append(('pack_type_id', 4))
|
||||
elif 'Promo Choice' in pack_vals:
|
||||
open_type = 'choice'
|
||||
params.append(('pack_type_id', 9))
|
||||
elif 'MVP' in pack_vals:
|
||||
open_type = 'choice'
|
||||
params.append(('pack_type_id', 5))
|
||||
elif 'All Star' in pack_vals:
|
||||
open_type = 'choice'
|
||||
params.append(('pack_type_id', 6))
|
||||
elif 'Mario' in pack_vals:
|
||||
open_type = 'choice'
|
||||
params.append(('pack_type_id', 7))
|
||||
elif 'Team Choice' in pack_vals:
|
||||
open_type = 'choice'
|
||||
params.append(('pack_type_id', 8))
|
||||
else:
|
||||
raise KeyError(f'Cannot identify pack details: {pack_vals}')
|
||||
|
||||
# If team isn't already set on team choice pack, make team pack selection now
|
||||
await interaction.response.edit_message(view=None)
|
||||
|
||||
cardset_id = None
|
||||
if 'Team Choice' in pack_vals and 'Cardset' in pack_vals:
|
||||
# cardset_id = pack_vals[2]
|
||||
cardset_index = pack_vals.index('Cardset')
|
||||
cardset_id = pack_vals[cardset_index + 1]
|
||||
params.append(('pack_cardset_id', cardset_id))
|
||||
if 'Team' not in pack_vals:
|
||||
view = SelectView(
|
||||
[SelectChoicePackTeam('AL', self.owner_team, cardset_id),
|
||||
SelectChoicePackTeam('NL', self.owner_team, cardset_id)],
|
||||
timeout=30
|
||||
)
|
||||
await interaction.channel.send(
|
||||
content=None,
|
||||
view=view
|
||||
)
|
||||
return
|
||||
|
||||
params.append(('pack_team_id', pack_vals[pack_vals.index('Team') + 1]))
|
||||
else:
|
||||
if 'Team' in pack_vals:
|
||||
params.append(('pack_team_id', pack_vals[pack_vals.index('Team') + 1]))
|
||||
if 'Cardset' in pack_vals:
|
||||
cardset_id = pack_vals[pack_vals.index('Cardset') + 1]
|
||||
params.append(('pack_cardset_id', cardset_id))
|
||||
|
||||
p_query = await db_get('packs', params=params)
|
||||
if p_query['count'] == 0:
|
||||
logger.error(f'open-packs - no packs found with params: {params}')
|
||||
raise ValueError(f'Unable to open packs')
|
||||
|
||||
# Open the packs
|
||||
if open_type == 'standard':
|
||||
await open_st_pr_packs(p_query['packs'], self.owner_team, interaction)
|
||||
elif open_type == 'choice':
|
||||
await open_choice_pack(p_query['packs'][0], self.owner_team, interaction, cardset_id)
|
||||
|
||||
|
||||
class SelectPaperdexCardset(discord.ui.Select):
|
||||
def __init__(self):
|
||||
options = [
|
||||
discord.SelectOption(label='2025 Live'),
|
||||
discord.SelectOption(label='1998 Season'),
|
||||
discord.SelectOption(label='2024 Season'),
|
||||
discord.SelectOption(label='2023 Season'),
|
||||
discord.SelectOption(label='2022 Season'),
|
||||
discord.SelectOption(label='2022 Promos'),
|
||||
discord.SelectOption(label='2021 Season'),
|
||||
discord.SelectOption(label='2019 Season'),
|
||||
discord.SelectOption(label='2018 Season'),
|
||||
discord.SelectOption(label='2016 Season'),
|
||||
discord.SelectOption(label='2013 Season'),
|
||||
discord.SelectOption(label='2012 Season'),
|
||||
discord.SelectOption(label='2008 Season'),
|
||||
discord.SelectOption(label='Mario Super Sluggers')
|
||||
]
|
||||
super().__init__(placeholder='Select a Cardset', options=options)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
# Import here to avoid circular imports
|
||||
from api_calls import db_get
|
||||
from helpers import get_team_by_owner, paperdex_cardset_embed, embed_pagination
|
||||
|
||||
logger.info(f'SelectPaperdexCardset - selection: {self.values[0]}')
|
||||
cardset_id = CARDSET_LABELS_TO_IDS.get(self.values[0])
|
||||
if cardset_id is None:
|
||||
raise ValueError(f'Unknown cardset: {self.values[0]}')
|
||||
|
||||
c_query = await db_get('cardsets', object_id=cardset_id, none_okay=False)
|
||||
await interaction.response.edit_message(content=f'Okay, sifting through your cards...', view=None)
|
||||
|
||||
cardset_embeds = await paperdex_cardset_embed(
|
||||
team=await get_team_by_owner(interaction.user.id),
|
||||
this_cardset=c_query
|
||||
)
|
||||
await embed_pagination(cardset_embeds, interaction.channel, interaction.user)
|
||||
|
||||
|
||||
class SelectPaperdexTeam(discord.ui.Select):
|
||||
def __init__(self, which: Literal['AL', 'NL']):
|
||||
self.which = which
|
||||
|
||||
if which == 'AL':
|
||||
options = [discord.SelectOption(label=team) for team in AL_TEAMS]
|
||||
else:
|
||||
# Handle St. Louis Cardinals display name
|
||||
options = [discord.SelectOption(label='St. Louis Cardinals' if team == 'St Louis Cardinals' else team)
|
||||
for team in NL_TEAMS]
|
||||
|
||||
super().__init__(placeholder=f'Select an {which} team', options=options)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
# Import here to avoid circular imports
|
||||
from api_calls import db_get
|
||||
from helpers import get_team_by_owner, paperdex_team_embed, embed_pagination
|
||||
|
||||
team_id = _get_team_id(self.values[0], self.which)
|
||||
if team_id is None:
|
||||
raise ValueError(f'Unknown team: {self.values[0]}')
|
||||
|
||||
t_query = await db_get('teams', object_id=team_id, none_okay=False)
|
||||
await interaction.response.edit_message(content=f'Okay, sifting through your cards...', view=None)
|
||||
|
||||
team_embeds = await paperdex_team_embed(team=await get_team_by_owner(interaction.user.id), mlb_team=t_query)
|
||||
await embed_pagination(team_embeds, interaction.channel, interaction.user)
|
||||
|
||||
|
||||
class SelectBuyPacksCardset(discord.ui.Select):
|
||||
def __init__(self, team: dict, quantity: int, pack_type_id: int, pack_embed: discord.Embed, cost: int):
|
||||
options = [
|
||||
discord.SelectOption(label='2025 Live'),
|
||||
discord.SelectOption(label='1998 Season'),
|
||||
discord.SelectOption(label='Pokemon - Brilliant Stars'),
|
||||
discord.SelectOption(label='2024 Season'),
|
||||
discord.SelectOption(label='2023 Season'),
|
||||
discord.SelectOption(label='2022 Season'),
|
||||
discord.SelectOption(label='2021 Season'),
|
||||
discord.SelectOption(label='2019 Season'),
|
||||
discord.SelectOption(label='2018 Season'),
|
||||
discord.SelectOption(label='2016 Season'),
|
||||
discord.SelectOption(label='2013 Season'),
|
||||
discord.SelectOption(label='2012 Season'),
|
||||
discord.SelectOption(label='2008 Season')
|
||||
]
|
||||
self.team = team
|
||||
self.quantity = quantity
|
||||
self.pack_type_id = pack_type_id
|
||||
self.pack_embed = pack_embed
|
||||
self.cost = cost
|
||||
super().__init__(placeholder='Select a Cardset', options=options)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
# Import here to avoid circular imports
|
||||
from api_calls import db_post
|
||||
from discord_ui.confirmations import Confirm
|
||||
|
||||
logger.info(f'SelectBuyPacksCardset - selection: {self.values[0]}')
|
||||
cardset_id = CARDSET_LABELS_TO_IDS.get(self.values[0])
|
||||
if cardset_id is None:
|
||||
raise ValueError(f'Unknown cardset: {self.values[0]}')
|
||||
|
||||
if self.values[0] == 'Pokemon - Brilliant Stars':
|
||||
self.pack_embed.set_image(url=IMAGES['pack-pkmnbs'])
|
||||
|
||||
self.pack_embed.description = f'{self.pack_embed.description} - {self.values[0]}'
|
||||
view = Confirm(responders=[interaction.user], timeout=30)
|
||||
await interaction.response.edit_message(
|
||||
content=None,
|
||||
embed=self.pack_embed,
|
||||
view=None
|
||||
)
|
||||
question = await interaction.channel.send(
|
||||
content=f'Your Wallet: {self.team["wallet"]}₼\n'
|
||||
f'Pack{"s" if self.quantity > 1 else ""} Price: {self.cost}₼\n'
|
||||
f'After Purchase: {self.team["wallet"] - self.cost}₼\n\n'
|
||||
f'Would you like to make this purchase?',
|
||||
view=view
|
||||
)
|
||||
await view.wait()
|
||||
|
||||
if not view.value:
|
||||
await question.edit(
|
||||
content='Saving that money. Smart.',
|
||||
view=None
|
||||
)
|
||||
return
|
||||
|
||||
p_model = {
|
||||
'team_id': self.team['id'],
|
||||
'pack_type_id': self.pack_type_id,
|
||||
'pack_cardset_id': cardset_id
|
||||
}
|
||||
await db_post('packs', payload={'packs': [p_model for x in range(self.quantity)]})
|
||||
await db_post(f'teams/{self.team["id"]}/money/-{self.cost}')
|
||||
|
||||
await question.edit(
|
||||
content=f'{"They are" if self.quantity > 1 else "It is"} all yours! Go rip \'em with `/open-packs`',
|
||||
view=None
|
||||
)
|
||||
|
||||
|
||||
class SelectBuyPacksTeam(discord.ui.Select):
|
||||
def __init__(
|
||||
self, which: Literal['AL', 'NL'], team: dict, quantity: int, pack_type_id: int, pack_embed: discord.Embed,
|
||||
cost: int):
|
||||
self.which = which
|
||||
self.team = team
|
||||
self.quantity = quantity
|
||||
self.pack_type_id = pack_type_id
|
||||
self.pack_embed = pack_embed
|
||||
self.cost = cost
|
||||
|
||||
if which == 'AL':
|
||||
options = [discord.SelectOption(label=team) for team in AL_TEAMS]
|
||||
else:
|
||||
# Handle St. Louis Cardinals display name
|
||||
options = [discord.SelectOption(label='St. Louis Cardinals' if team == 'St Louis Cardinals' else team)
|
||||
for team in NL_TEAMS]
|
||||
|
||||
super().__init__(placeholder=f'Select an {which} team', options=options)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
# Import here to avoid circular imports
|
||||
from api_calls import db_post
|
||||
from discord_ui.confirmations import Confirm
|
||||
|
||||
team_id = _get_team_id(self.values[0], self.which)
|
||||
if team_id is None:
|
||||
raise ValueError(f'Unknown team: {self.values[0]}')
|
||||
|
||||
self.pack_embed.description = f'{self.pack_embed.description} - {self.values[0]}'
|
||||
view = Confirm(responders=[interaction.user], timeout=30)
|
||||
await interaction.response.edit_message(
|
||||
content=None,
|
||||
embed=self.pack_embed,
|
||||
view=None
|
||||
)
|
||||
question = await interaction.channel.send(
|
||||
content=f'Your Wallet: {self.team["wallet"]}₼\n'
|
||||
f'Pack{"s" if self.quantity > 1 else ""} Price: {self.cost}₼\n'
|
||||
f'After Purchase: {self.team["wallet"] - self.cost}₼\n\n'
|
||||
f'Would you like to make this purchase?',
|
||||
view=view
|
||||
)
|
||||
await view.wait()
|
||||
|
||||
if not view.value:
|
||||
await question.edit(
|
||||
content='Saving that money. Smart.',
|
||||
view=None
|
||||
)
|
||||
return
|
||||
|
||||
p_model = {
|
||||
'team_id': self.team['id'],
|
||||
'pack_type_id': self.pack_type_id,
|
||||
'pack_team_id': team_id
|
||||
}
|
||||
await db_post('packs', payload={'packs': [p_model for x in range(self.quantity)]})
|
||||
await db_post(f'teams/{self.team["id"]}/money/-{self.cost}')
|
||||
|
||||
await question.edit(
|
||||
content=f'{"They are" if self.quantity > 1 else "It is"} all yours! Go rip \'em with `/open-packs`',
|
||||
view=None
|
||||
)
|
||||
|
||||
|
||||
class SelectUpdatePlayerTeam(discord.ui.Select):
|
||||
def __init__(self, which: Literal['AL', 'NL'], player: dict, reporting_team: dict, bot):
|
||||
self.bot = bot
|
||||
self.which = which
|
||||
self.player = player
|
||||
self.reporting_team = reporting_team
|
||||
|
||||
if which == 'AL':
|
||||
options = [discord.SelectOption(label=team) for team in AL_TEAMS]
|
||||
else:
|
||||
# Handle St. Louis Cardinals display name
|
||||
options = [discord.SelectOption(label='St. Louis Cardinals' if team == 'St Louis Cardinals' else team)
|
||||
for team in NL_TEAMS]
|
||||
|
||||
super().__init__(placeholder=f'Select an {which} team', options=options)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
# Import here to avoid circular imports
|
||||
from api_calls import db_patch, db_post
|
||||
from discord_ui.confirmations import Confirm
|
||||
from helpers import player_desc, send_to_channel
|
||||
|
||||
if self.values[0] == self.player['franchise'] or self.values[0] == self.player['mlbclub']:
|
||||
await interaction.response.send_message(
|
||||
content=f'Thank you for the help, but it looks like somebody beat you to it! '
|
||||
f'**{player_desc(self.player)}** is already assigned to the **{self.player["mlbclub"]}**.'
|
||||
)
|
||||
return
|
||||
|
||||
view = Confirm(responders=[interaction.user], timeout=15)
|
||||
await interaction.response.edit_message(
|
||||
content=f'Should I update **{player_desc(self.player)}**\'s team to the **{self.values[0]}**?',
|
||||
view=None
|
||||
)
|
||||
question = await interaction.channel.send(
|
||||
content=None,
|
||||
view=view
|
||||
)
|
||||
await view.wait()
|
||||
|
||||
if not view.value:
|
||||
await question.edit(
|
||||
content='That didnt\'t sound right to me, either. Let\'s not touch that.',
|
||||
view=None
|
||||
)
|
||||
return
|
||||
else:
|
||||
await question.delete()
|
||||
|
||||
await db_patch('players', object_id=self.player['player_id'], params=[
|
||||
('mlbclub', self.values[0]), ('franchise', self.values[0])
|
||||
])
|
||||
await db_post(f'teams/{self.reporting_team["id"]}/money/25')
|
||||
await send_to_channel(
|
||||
self.bot, 'pd-news-ticker',
|
||||
content=f'{interaction.user.name} just updated **{player_desc(self.player)}**\'s team to the '
|
||||
f'**{self.values[0]}**'
|
||||
)
|
||||
await interaction.channel.send(f'All done!')
|
||||
|
||||
|
||||
class SelectView(discord.ui.View):
|
||||
def __init__(self, select_objects: list[discord.ui.Select], timeout: float = 300.0):
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
for x in select_objects:
|
||||
self.add_item(x)
|
||||
231
discord_utils.py
Normal file
231
discord_utils.py
Normal file
@ -0,0 +1,231 @@
|
||||
"""
|
||||
Discord Utilities
|
||||
|
||||
This module contains Discord helper functions for channels, roles, embeds,
|
||||
and other Discord-specific operations.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from constants import SBA_COLOR, PD_SEASON, IMAGES
|
||||
|
||||
logger = logging.getLogger('discord_app')
|
||||
|
||||
|
||||
async def send_to_bothole(ctx, content, embed):
|
||||
"""Send a message to the pd-bot-hole channel."""
|
||||
await discord.utils.get(ctx.guild.text_channels, name='pd-bot-hole') \
|
||||
.send(content=content, embed=embed)
|
||||
|
||||
|
||||
async def send_to_news(ctx, content, embed):
|
||||
"""Send a message to the pd-news-ticker channel."""
|
||||
await discord.utils.get(ctx.guild.text_channels, name='pd-news-ticker') \
|
||||
.send(content=content, embed=embed)
|
||||
|
||||
|
||||
async def typing_pause(ctx, seconds=1):
|
||||
"""Show typing indicator for specified seconds."""
|
||||
async with ctx.typing():
|
||||
await asyncio.sleep(seconds)
|
||||
|
||||
|
||||
async def pause_then_type(ctx, message):
|
||||
"""Show typing indicator based on message length, then send message."""
|
||||
async with ctx.typing():
|
||||
await asyncio.sleep(len(message) / 100)
|
||||
await ctx.send(message)
|
||||
|
||||
|
||||
async def check_if_pdhole(ctx):
|
||||
"""Check if the current channel is pd-bot-hole."""
|
||||
if ctx.message.channel.name != 'pd-bot-hole':
|
||||
await ctx.send('Slide on down to my bot-hole for running commands.')
|
||||
await ctx.message.add_reaction('❌')
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def bad_channel(ctx):
|
||||
"""Check if current channel is in the list of bad channels for commands."""
|
||||
bad_channels = ['paper-dynasty-chat', 'pd-news-ticker']
|
||||
if ctx.message.channel.name in bad_channels:
|
||||
await ctx.message.add_reaction('❌')
|
||||
bot_hole = discord.utils.get(
|
||||
ctx.guild.text_channels,
|
||||
name=f'pd-bot-hole'
|
||||
)
|
||||
await ctx.send(f'Slide on down to the {bot_hole.mention} ;)')
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_channel(ctx, name) -> Optional[discord.TextChannel]:
|
||||
"""Get a text channel by name."""
|
||||
channel = discord.utils.get(
|
||||
ctx.guild.text_channels,
|
||||
name=name
|
||||
)
|
||||
if channel:
|
||||
return channel
|
||||
return None
|
||||
|
||||
|
||||
async def get_emoji(ctx, name, return_empty=True):
|
||||
"""Get an emoji by name, with fallback options."""
|
||||
try:
|
||||
emoji = await commands.converter.EmojiConverter().convert(ctx, name)
|
||||
except:
|
||||
if return_empty:
|
||||
emoji = ''
|
||||
else:
|
||||
return name
|
||||
return emoji
|
||||
|
||||
|
||||
async def react_and_reply(ctx, reaction, message):
|
||||
"""Add a reaction to the message and send a reply."""
|
||||
await ctx.message.add_reaction(reaction)
|
||||
await ctx.send(message)
|
||||
|
||||
|
||||
async def send_to_channel(bot, channel_name, content=None, embed=None):
|
||||
"""Send a message to a specific channel by name or ID."""
|
||||
guild = bot.get_guild(int(os.environ.get('GUILD_ID')))
|
||||
if not guild:
|
||||
logger.error('Cannot send to channel - bot not logged in')
|
||||
return
|
||||
|
||||
this_channel = discord.utils.get(guild.text_channels, name=channel_name)
|
||||
|
||||
if not this_channel:
|
||||
this_channel = discord.utils.get(guild.text_channels, id=channel_name)
|
||||
if not this_channel:
|
||||
raise NameError(f'**{channel_name}** channel not found')
|
||||
|
||||
return await this_channel.send(content=content, embed=embed)
|
||||
|
||||
|
||||
async def get_or_create_role(ctx, role_name, mentionable=True):
|
||||
"""Get an existing role or create it if it doesn't exist."""
|
||||
this_role = discord.utils.get(ctx.guild.roles, name=role_name)
|
||||
|
||||
if not this_role:
|
||||
this_role = await ctx.guild.create_role(name=role_name, mentionable=mentionable)
|
||||
|
||||
return this_role
|
||||
|
||||
|
||||
def get_special_embed(special):
|
||||
"""Create an embed for a special item."""
|
||||
embed = discord.Embed(title=f'{special.name} - Special #{special.get_id()}',
|
||||
color=discord.Color.random(),
|
||||
description=f'{special.short_desc}')
|
||||
embed.add_field(name='Description', value=f'{special.long_desc}', inline=False)
|
||||
if special.thumbnail.lower() != 'none':
|
||||
embed.set_thumbnail(url=f'{special.thumbnail}')
|
||||
if special.url.lower() != 'none':
|
||||
embed.set_image(url=f'{special.url}')
|
||||
|
||||
return embed
|
||||
|
||||
|
||||
def get_random_embed(title, thumb=None):
|
||||
"""Create a basic embed with random color."""
|
||||
embed = discord.Embed(title=title, color=discord.Color.random())
|
||||
if thumb:
|
||||
embed.set_thumbnail(url=thumb)
|
||||
|
||||
return embed
|
||||
|
||||
|
||||
def get_team_embed(title, team=None, thumbnail: bool = True):
|
||||
"""Create a team-branded embed."""
|
||||
if team:
|
||||
embed = discord.Embed(
|
||||
title=title,
|
||||
color=int(team["color"], 16) if team["color"] else int(SBA_COLOR, 16)
|
||||
)
|
||||
embed.set_footer(text=f'Paper Dynasty Season {team["season"]}', icon_url=IMAGES['logo'])
|
||||
if thumbnail:
|
||||
embed.set_thumbnail(url=team["logo"] if team["logo"] else IMAGES['logo'])
|
||||
else:
|
||||
embed = discord.Embed(
|
||||
title=title,
|
||||
color=int(SBA_COLOR, 16)
|
||||
)
|
||||
embed.set_footer(text=f'Paper Dynasty Season {PD_SEASON}', icon_url=IMAGES['logo'])
|
||||
if thumbnail:
|
||||
embed.set_thumbnail(url=IMAGES['logo'])
|
||||
|
||||
return embed
|
||||
|
||||
|
||||
async def create_channel_old(
|
||||
ctx, channel_name: str, category_name: str, everyone_send=False, everyone_read=True, allowed_members=None,
|
||||
allowed_roles=None):
|
||||
"""Create a text channel with specified permissions (legacy version)."""
|
||||
this_category = discord.utils.get(ctx.guild.categories, name=category_name)
|
||||
if not this_category:
|
||||
raise ValueError(f'I couldn\'t find a category named **{category_name}**')
|
||||
|
||||
overwrites = {
|
||||
ctx.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True),
|
||||
ctx.guild.default_role: discord.PermissionOverwrite(read_messages=everyone_read, send_messages=everyone_send)
|
||||
}
|
||||
if allowed_members:
|
||||
if isinstance(allowed_members, list):
|
||||
for member in allowed_members:
|
||||
overwrites[member] = discord.PermissionOverwrite(read_messages=True, send_messages=True)
|
||||
if allowed_roles:
|
||||
if isinstance(allowed_roles, list):
|
||||
for role in allowed_roles:
|
||||
overwrites[role] = discord.PermissionOverwrite(read_messages=True, send_messages=True)
|
||||
|
||||
this_channel = await ctx.guild.create_text_channel(
|
||||
channel_name,
|
||||
overwrites=overwrites,
|
||||
category=this_category
|
||||
)
|
||||
|
||||
logger.info(f'Creating channel ({channel_name}) in ({category_name})')
|
||||
|
||||
return this_channel
|
||||
|
||||
|
||||
async def create_channel(
|
||||
ctx, channel_name: str, category_name: str, everyone_send=False, everyone_read=True,
|
||||
read_send_members: list = None, read_send_roles: list = None, read_only_roles: list = None):
|
||||
"""Create a text channel with specified permissions."""
|
||||
this_category = discord.utils.get(ctx.guild.categories, name=category_name)
|
||||
if not this_category:
|
||||
raise ValueError(f'I couldn\'t find a category named **{category_name}**')
|
||||
|
||||
overwrites = {
|
||||
ctx.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True),
|
||||
ctx.guild.default_role: discord.PermissionOverwrite(read_messages=everyone_read, send_messages=everyone_send)
|
||||
}
|
||||
if read_send_members:
|
||||
for member in read_send_members:
|
||||
overwrites[member] = discord.PermissionOverwrite(read_messages=True, send_messages=True)
|
||||
if read_send_roles:
|
||||
for role in read_send_roles:
|
||||
overwrites[role] = discord.PermissionOverwrite(read_messages=True, send_messages=True)
|
||||
if read_only_roles:
|
||||
for role in read_only_roles:
|
||||
overwrites[role] = discord.PermissionOverwrite(read_messages=True, send_messages=False)
|
||||
|
||||
this_channel = await ctx.guild.create_text_channel(
|
||||
channel_name,
|
||||
overwrites=overwrites,
|
||||
category=this_category
|
||||
)
|
||||
|
||||
logger.info(f'Creating channel ({channel_name}) in ({category_name})')
|
||||
|
||||
return this_channel
|
||||
@ -15,7 +15,7 @@ def log_errors(func):
|
||||
except Exception as e:
|
||||
logger.error(func.__name__)
|
||||
log_exception(e)
|
||||
return result
|
||||
return result # type: ignore
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
22
gauntlets.py
22
gauntlets.py
@ -243,27 +243,27 @@ async def get_opponent(session: Session, this_team, this_event, this_run) -> Tea
|
||||
raise KeyError(f'Hmm...I do not know who you should be playing right now.')
|
||||
elif this_event['id'] == 8:
|
||||
if gp == 0:
|
||||
teams = [6, 13]
|
||||
teams = [6, 9] # White Sox and Rockies
|
||||
elif gp == 1:
|
||||
teams = [11, 12, 22, 28]
|
||||
teams = [22, 30, 20, 2] # Pirates, Nats, A's, Braves
|
||||
elif gp == 2:
|
||||
teams = [12, 27, 29]
|
||||
teams = [3, 15, 12] # Orioles, Marlins, Royals
|
||||
elif gp == 3:
|
||||
teams = [16, 18, 25]
|
||||
teams = [17, 8, 13] # Twins, Guardians, Angels
|
||||
elif gp == 4:
|
||||
teams = [10, 18, 26]
|
||||
teams = [13, 1, 28] # Angels, Dbacks, Rangers
|
||||
elif gp == 5:
|
||||
teams = [8, 24]
|
||||
teams = [26, 27] # Cardinals, Rays
|
||||
elif gp == 6:
|
||||
teams = [1, 3, 8]
|
||||
teams = [7, 25, 4] # Reds, Giants, Red Sox
|
||||
elif gp == 7:
|
||||
teams = [4, 21, 23, 30]
|
||||
teams = [24, 23, 19] # Mariners, Padres, Yankees
|
||||
elif gp == 8:
|
||||
teams = [2, 20]
|
||||
teams = [18, 21, 11] # Mets, Phillies, Astros
|
||||
elif gp == 9:
|
||||
teams = [13, 14]
|
||||
teams = [14, 29, 16] # Dodgers, Blue Jays, Brewers
|
||||
elif gp == 10:
|
||||
teams = [19, 5]
|
||||
teams = [5, 10] # Cubs, Tigers
|
||||
else:
|
||||
raise KeyError(f'Hmm...I do not know who you should be playing right now.')
|
||||
t_id = teams[random.randint(0, len(teams)-1)]
|
||||
|
||||
1785
helpers.py
1785
helpers.py
File diff suppressed because it is too large
Load Diff
@ -604,21 +604,29 @@ async def get_all_positions(session: Session, this_card: Card, skip_cache: bool
|
||||
logger.info(f'we found a cached position rating: {position} / created: {position.created}')
|
||||
tdelta = datetime.datetime.now() - position.created
|
||||
logger.debug(f'tdelta: {tdelta}')
|
||||
if tdelta.total_seconds() >= CACHE_LIMIT:
|
||||
if tdelta.total_seconds() >= CACHE_LIMIT or datetime.datetime.now().day < 5:
|
||||
session.delete(position)
|
||||
session.commit()
|
||||
should_repull = True
|
||||
|
||||
if not should_repull:
|
||||
if not should_repull and len(all_pos) > 0:
|
||||
logger.info(f'Returning {len(all_pos)}')
|
||||
return len(all_pos)
|
||||
|
||||
p_query = await db_get('cardpositions', params=[('player_id', this_card.player.id)])
|
||||
|
||||
if p_query['count'] == 0:
|
||||
if not p_query or p_query['count'] == 0:
|
||||
logger.info(f'No positions received, returning 0')
|
||||
return 0
|
||||
|
||||
old_pos = session.exec(select(PositionRating).where(PositionRating.player_id == this_card.player_id)).all()
|
||||
|
||||
for position in old_pos:
|
||||
logger.info(f'Deleting orphaned position rating: {position}')
|
||||
session.delete(position)
|
||||
|
||||
session.commit()
|
||||
|
||||
def cache_pos(json_data: dict) -> PositionRating:
|
||||
if 'id' in json_data:
|
||||
del json_data['id']
|
||||
@ -716,7 +724,11 @@ async def get_or_create_ai_card(session: Session, player: Player, team: Team, sk
|
||||
logger.info(f'gameplay_models - get_or_create_ai_card: creating {player.description} {player.name} card for {team.abbrev}')
|
||||
|
||||
if dev_mode:
|
||||
this_card = Card(player=player, team=team)
|
||||
# Find next available ID since Card model has autoincrement=False
|
||||
from sqlmodel import func
|
||||
max_id = session.exec(select(func.max(Card.id))).one()
|
||||
next_id = (max_id or 0) + 1
|
||||
this_card = Card(id=next_id, player=player, team=team)
|
||||
session.add(this_card)
|
||||
session.commit()
|
||||
session.refresh(this_card)
|
||||
|
||||
@ -2,3 +2,5 @@
|
||||
asyncio_mode = auto
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning
|
||||
env =
|
||||
DOCKER_HOST = unix:///home/cal/.docker/desktop/docker.sock
|
||||
|
||||
219
random_content.py
Normal file
219
random_content.py
Normal file
@ -0,0 +1,219 @@
|
||||
"""
|
||||
Random Content Generators
|
||||
|
||||
This module contains all the random content generation functions including
|
||||
GIFs, phrases, codenames, and other content used for bot interactions.
|
||||
"""
|
||||
import random
|
||||
import logging
|
||||
import requests
|
||||
from constants import INSULTS
|
||||
|
||||
logger = logging.getLogger('discord_app')
|
||||
|
||||
|
||||
def random_conf_gif():
|
||||
"""Returns a random confirmation GIF URL."""
|
||||
conf_gifs = [
|
||||
'https://tenor.com/view/boom-annakendrick-pitchperfect-pitchperfect2-micdrop-gif-5143507',
|
||||
'https://tenor.com/view/boom-annakendrick-pitchperfect-pitchperfect2-micdrop-gif-5143507',
|
||||
'https://tenor.com/view/boom-annakendrick-pitchperfect-pitchperfect2-micdrop-gif-5143507',
|
||||
'https://tenor.com/view/explosion-boom-iron-man-gif-14282225',
|
||||
'https://tenor.com/view/betty-white-dab-consider-it-done-gif-11972415',
|
||||
'https://tenor.com/view/done-and-done-spongebob-finished-just-did-it-gif-10843280',
|
||||
'https://tenor.com/view/thumbs-up-okay-ok-well-done-gif-13840394',
|
||||
'https://tenor.com/view/tinkerbell-peter-pan-all-done-gif-15003723',
|
||||
'https://tenor.com/view/done-and-done-ron-swanson-gotchu-gif-10843254',
|
||||
'https://tenor.com/view/sponge-bob-thumbs-up-ok-smile-gif-12038157',
|
||||
'https://tenor.com/view/thumbs-up-cool-okay-bye-gif-8633196',
|
||||
'https://i0.wp.com/media1.giphy.com/media/iwvuPyfi7z14I/giphy.gif',
|
||||
'https://media1.tenor.com/images/859a2d3b201fbacec13904242976b9e0/tenor.gif',
|
||||
'https://tenor.com/bc1OJ.gif',
|
||||
'https://tenor.com/1EmF.gif',
|
||||
'https://tenor.com/ZYCh.gif',
|
||||
'https://tenor.com/patd.gif',
|
||||
'https://tenor.com/u6mU.gif',
|
||||
'https://tenor.com/x2sa.gif',
|
||||
'https://tenor.com/bAVeS.gif',
|
||||
'https://tenor.com/bxOcj.gif',
|
||||
'https://tenor.com/ETJ7.gif',
|
||||
'https://tenor.com/bpH3g.gif',
|
||||
'https://tenor.com/biF9q.gif',
|
||||
'https://tenor.com/OySS.gif',
|
||||
'https://tenor.com/bvVFv.gif',
|
||||
'https://tenor.com/bFeqA.gif'
|
||||
]
|
||||
return conf_gifs[random.randint(0, len(conf_gifs) - 1)]
|
||||
|
||||
|
||||
def random_no_gif():
|
||||
"""Returns a random 'no' reaction GIF URL."""
|
||||
no_gifs = [
|
||||
'https://tenor.com/view/youre-not-my-dad-dean-jensen-ackles-supernatural-you-arent-my-dad-gif-19503399',
|
||||
'https://tenor.com/view/youre-not-my-dad-kid-gif-8300190',
|
||||
'https://tenor.com/view/youre-not-my-supervisor-youre-not-my-boss-gif-12971403',
|
||||
'https://tenor.com/view/dont-tell-me-what-to-do-gif-4951202'
|
||||
]
|
||||
return no_gifs[random.randint(0, len(no_gifs) - 1)]
|
||||
|
||||
|
||||
def random_salute_gif():
|
||||
"""Returns a random salute GIF URL."""
|
||||
salute_gifs = [
|
||||
'https://media.giphy.com/media/fSAyceY3BCgtiQGnJs/giphy.gif',
|
||||
'https://media.giphy.com/media/bsWDUSFUmJCOk/giphy.gif',
|
||||
'https://media.giphy.com/media/hStvd5LiWCFzYNyxR4/giphy.gif',
|
||||
'https://media.giphy.com/media/RhSR5xXDsXJ7jbnrRW/giphy.gif',
|
||||
'https://media.giphy.com/media/lNQvrlPdbmZUU2wlh9/giphy.gif',
|
||||
'https://gfycat.com/skeletaldependableandeancat',
|
||||
'https://i.gifer.com/5EJk.gif',
|
||||
'https://tenor.com/baJUV.gif',
|
||||
'https://tenor.com/bdnQH.gif',
|
||||
'https://tenor.com/bikQU.gif',
|
||||
'https://i.pinimg.com/originals/04/36/bf/0436bfc9861b4b57ffffda82d3adad6e.gif',
|
||||
'https://media.giphy.com/media/6RtOG4Q7v34kw/giphy.gif',
|
||||
'https://keyassets-p2.timeincuk.net/wp/prod/wp-content/uploads/sites/42/2017/04/anigif_'
|
||||
'enhanced-946-1433453114-7.gif',
|
||||
'https://keyassets-p2.timeincuk.net/wp/prod/wp-content/uploads/sites/42/2017/04/100c5d677cc28ea3f15'
|
||||
'4c70d641f655b_meme-crying-gif-crying-gif-meme_620-340.gif',
|
||||
'https://media.giphy.com/media/fnKd6rCHaZoGdzLjjA/giphy.gif',
|
||||
'https://media.giphy.com/media/47D5jmVc4f7ylygXYD/giphy.gif',
|
||||
'https://media.giphy.com/media/I4wGMXoi2kMDe/giphy.gif',
|
||||
]
|
||||
return salute_gifs[random.randint(0, len(salute_gifs) - 1)]
|
||||
|
||||
|
||||
def random_conf_word():
|
||||
"""Returns a random confirmation word."""
|
||||
conf_words = [
|
||||
'dope',
|
||||
'cool',
|
||||
'got it',
|
||||
'noice',
|
||||
'ok',
|
||||
'lit',
|
||||
]
|
||||
return conf_words[random.randint(0, len(conf_words) - 1)]
|
||||
|
||||
|
||||
def random_codename():
|
||||
"""Returns a random codename from a large list of options."""
|
||||
all_names = [
|
||||
'Shong', 'DerekSux', 'JoeSux', 'CalSux', 'Friend', 'Andrea', 'Ent', 'Lindved', 'Camp', 'Idyll', 'Elaphus',
|
||||
'Turki', 'Shrimp', 'Primary', 'Anglica', 'Shail', 'Blanket', 'Baffled', 'Deer', 'Thisted', 'Brisk', 'Shy',
|
||||
'Table', 'Jorts', 'Renati', 'Gisky', 'Prig', 'Bathtub', 'Gallery', 'Mavas', 'Chird', 'Oxyura', 'Mydal', 'Brown',
|
||||
'Vasen', 'Worthy', 'Bivver', 'Cirlus', 'Self', 'Len', 'Sharp', 'Dart', 'Crepis', 'Ferina', 'Curl', 'Lancome',
|
||||
'Stuff', 'Glove', 'Consist', 'Smig', 'Egg', 'Pleat', 'Picture', 'Spin', 'Ridgty', 'Ickled', 'Abashed', 'Haul',
|
||||
'Cordage', 'Chivery', 'Stointy', 'Baa', 'Here', 'Ulmi', 'Tour', 'Tribe', 'Crunch', 'Used', 'Pigface', 'Audit',
|
||||
'Written', 'Once', 'Fickle', 'Drugged', 'Swarm', 'Blimber', 'Torso', 'Retusa', 'Hockey', 'Pusty', 'Sallow',
|
||||
'Next', 'Mansion', 'Glass', 'Screen', 'Josiah', 'Bonkey', 'Stuff', 'Sane', 'Blooded', 'Gnat', 'Liparis',
|
||||
'Ocean', 'Sway', 'Roband', 'Still', 'Ribba', 'Biryani', 'Halibut', 'Flyn', 'Until', 'Depend', 'Intel',
|
||||
'Affinis', 'Chef', 'Trounce', 'Crawl', 'Grab', 'Eggs', 'Malfroy', 'Sitta', 'Cretin', 'May', 'Smithii',
|
||||
'Saffron', 'Crummy', 'Powered', 'Rail', 'Trait', 'Koiled', 'Bronze', 'Quickly', 'Vikis', 'Trift', 'Jubilar',
|
||||
'Deft', 'Juncus', 'Sodding', 'Distant', 'Poecile', 'Pipe', 'Sell', 'Inops', 'Peusi', 'Sparrow', 'Yams',
|
||||
'Kidneys', 'Artery', 'Vuffin', 'Boink', 'Bos', 'Notable', 'Alba', 'Spurge', 'Ruby', 'Cilia', 'Pellow', 'Nox',
|
||||
'Woozy', 'Semvik', 'Tyda', 'Season', 'Lychnis', 'Ibestad', 'Bagge', 'Marked', 'Browdie', 'Fisher', 'Tilly',
|
||||
'Troll', 'Gypsy', 'Thisted', 'Flirt', 'Stop', 'Radiate', 'Poop', 'Plenty', 'Jeff', 'Magpie', 'Roof', 'Ent',
|
||||
'Dumbo', 'Pride', 'Weights', 'Winted', 'Dolden', 'Meotica', 'Yikes', 'Teeny', 'Fizz', 'Eide', 'Foetida',
|
||||
'Crash', 'Mann', 'Salong', 'Cetti', 'Balloon', 'Petite', 'Find', 'Sputter', 'Patula', 'Upstage', 'Aurora',
|
||||
'Dadson', 'Drate', 'Heidal', 'Robin', 'Auditor', 'Ithil', 'Warmen', 'Pat', 'Muppet', '007', 'Advantage',
|
||||
'Alert', 'Backhander', 'Badass', 'Blade', 'Blaze', 'Blockade', 'Blockbuster', 'Boxer', 'Brimstone', 'Broadway',
|
||||
'Buccaneer', 'Champion', 'Cliffhanger', 'Coachman', 'Comet', 'Commander', 'Courier', 'Cowboy', 'Crawler',
|
||||
'Crossroads', 'DeepSpace', 'Desperado', 'Double-Decker', 'Echelon', 'Edge', 'Encore', 'EnRoute', 'Escape',
|
||||
'Eureka', 'Evangelist', 'Excursion', 'Explorer', 'Fantastic', 'Firefight', 'Foray', 'Forge', 'Freeway',
|
||||
'Frontier', 'FunMachine', 'Galaxy', 'GameOver', 'Genesis', 'Hacker', 'Hawkeye', 'Haybailer', 'Haystack',
|
||||
'Hexagon', 'Hitman', 'Hustler', 'Iceberg', 'Impossible', 'Impulse', 'Invader', 'Inventor', 'IronWolf',
|
||||
'Jackrabbit', 'Juniper', 'Keyhole', 'Lancelot', 'Liftoff', 'MadHatter', 'Magnum', 'Majestic', 'Merlin',
|
||||
'Multiplier', 'Netiquette', 'Nomad', 'Octagon', 'Offense', 'OliveBranch', 'OlympicTorch', 'Omega', 'Onyx',
|
||||
'Orbit', 'OuterSpace', 'Outlaw', 'Patron', 'Patriot', 'Pegasus', 'Pentagon', 'Pilgrim', 'Pinball', 'Pinnacle',
|
||||
'Pipeline', 'Pirate', 'Portal', 'Predator', 'Prism', 'RagingBull', 'Ragtime', 'Reunion', 'Ricochet',
|
||||
'Roadrunner', 'Rockstar', 'RobinHood', 'Rover', 'Runabout', 'Sapphire', 'Scrappy', 'Seige', 'Shadow',
|
||||
'Shakedown', 'Shockwave', 'Shooter', 'Showdown', 'SixPack', 'SlamDunk', 'Slasher', 'Sledgehammer', 'Spirit',
|
||||
'Spotlight', 'Starlight', 'Steamroller', 'Stride', 'Sunrise', 'Superhuman', 'Supernova', 'SuperBowl', 'Sunset',
|
||||
'Sweetheart', 'TopHand', 'Touchdown', 'Tour', 'Trailblazer', 'Transit', 'Trekker', 'Trio', 'TriplePlay',
|
||||
'TripleThreat', 'Universe', 'Unstoppable', 'Utopia', 'Vicinity', 'Vector', 'Vigilance', 'Vigilante', 'Vista',
|
||||
'Visage', 'Vis-à-vis', 'VIP', 'Volcano', 'Volley', 'Whizzler', 'Wingman', 'Badger', 'BlackCat', 'Bobcat',
|
||||
'Caracal', 'Cheetah', 'Cougar', 'Jaguar', 'Leopard', 'Lion', 'Lynx', 'MountainLion', 'Ocelot', 'Panther',
|
||||
'Puma', 'Siamese', 'Serval', 'Tiger', 'Wolverine', 'Abispa', 'Andrena', 'BlackWidow', 'Cataglyphis',
|
||||
'Centipede', 'Cephalotes', 'Formica', 'Hornet', 'Jellyfish', 'Scorpion', 'Tarantula', 'Yellowjacket', 'Wasp',
|
||||
'Apollo', 'Ares', 'Artemis', 'Athena', 'Hercules', 'Hermes', 'Iris', 'Medusa', 'Nemesis', 'Neptune', 'Perseus',
|
||||
'Poseidon', 'Triton', 'Zeus', 'Aquarius', 'Aries', 'Cancer', 'Capricorn', 'Gemini', 'Libra', 'Leo', 'Pisces',
|
||||
'Sagittarius', 'Scorpio', 'Taurus', 'Virgo', 'Andromeda', 'Aquila', 'Cassiopeia', 'Cepheus', 'Cygnus',
|
||||
'Delphinus', 'Drako', 'Lyra', 'Orion', 'Perseus', 'Serpens', 'Triangulum', 'Anaconda', 'Boa', 'Cobra',
|
||||
'Copperhead', 'Cottonmouth', 'Garter', 'Kingsnake', 'Mamba', 'Python', 'Rattler', 'Sidewinder', 'Taipan',
|
||||
'Viper', 'Alligator', 'Barracuda', 'Crocodile', 'Gator', 'GreatWhite', 'Hammerhead', 'Jaws', 'Lionfish',
|
||||
'Mako', 'Moray', 'Orca', 'Piranha', 'Shark', 'Stingray', 'Axe', 'BattleAxe', 'Bayonet', 'Blade', 'Crossbowe',
|
||||
'Dagger', 'Excalibur', 'Halberd', 'Hatchet', 'Machete', 'Saber', 'Samurai', 'Scimitar', 'Scythe', 'Stiletto',
|
||||
'Spear', 'Sword', 'Aurora', 'Avalanche', 'Blizzard', 'Cyclone', 'Dewdrop', 'Downpour', 'Duststorm', 'Fogbank',
|
||||
'Freeze', 'Frost', 'GullyWasher', 'Gust', 'Hurricane', 'IceStorm', 'JetStream', 'Lightning', 'Mist', 'Monsoon',
|
||||
'Rainbow', 'Raindrop', 'SandStorm', 'Seabreeze', 'Snowflake', 'Stratosphere', 'Storm', 'Sunrise', 'Sunset',
|
||||
'Tornado', 'Thunder', 'Thunderbolt', 'Thunderstorm', 'TropicalStorm', 'Twister', 'Typhoon', 'Updraft', 'Vortex',
|
||||
'Waterspout', 'Whirlwind', 'WindChill', 'Archimedes', 'Aristotle', 'Confucius', 'Copernicus', 'Curie',
|
||||
'daVinci', 'Darwin', 'Descartes', 'Edison', 'Einstein', 'Epicurus', 'Freud', 'Galileo', 'Hawking',
|
||||
'Machiavelli', 'Marx', 'Newton', 'Pascal', 'Pasteur', 'Plato', 'Sagan', 'Socrates', 'Tesla', 'Voltaire',
|
||||
'Baccarat', 'Backgammon', 'Blackjack', 'Chess', 'Jenga', 'Jeopardy', 'Keno', 'Monopoly', 'Pictionary', 'Poker',
|
||||
'Scrabble', 'TrivialPursuit', 'Twister', 'Roulette', 'Stratego', 'Yahtzee', 'Aquaman', 'Batman', 'BlackPanther',
|
||||
'BlackWidow', 'CaptainAmerica', 'Catwoman', 'Daredevil', 'Dr.Strange', 'Flash', 'GreenArrow', 'GreenLantern',
|
||||
'Hulk', 'IronMan', 'Phantom', 'Thor', 'SilverSurfer', 'SpiderMan', 'Supergirl', 'Superman', 'WonderWoman',
|
||||
'Wolverine', 'Hypersonic', 'Lightspeed', 'Mach1,2,3,4,etc', 'Supersonic', 'WarpSpeed', 'Amiatina', 'Andalusian',
|
||||
'Appaloosa', 'Clydesdale', 'Colt', 'Falabella', 'Knabstrupper', 'Lipizzan', 'Lucitano', 'Maverick', 'Mustang',
|
||||
'Palomino', 'Pony', 'QuarterHorse', 'Stallion', 'Thoroughbred', 'Zebra', 'Antigua', 'Aruba', 'Azores', 'Baja',
|
||||
'Bali', 'Barbados', 'Bermuda', 'BoraBora', 'Borneo', 'Capri', 'Cayman', 'Corfu', 'Cozumel', 'Curacao', 'Fiji',
|
||||
'Galapagos', 'Hawaii', 'Ibiza', 'Jamaica', 'Kauai', 'Lanai', 'Majorca', 'Maldives', 'Maui', 'Mykonos',
|
||||
'Nantucket', 'Oahu', 'Tahiti', 'Tortuga', 'Roatan', 'Santorini', 'Seychelles', 'St.Johns', 'St.Lucia',
|
||||
'Albatross', 'BaldEagle', 'Blackhawk', 'BlueJay', 'Chukar', 'Condor', 'Crane', 'Dove', 'Eagle', 'Falcon',
|
||||
'Goose(GoldenGoose)', 'Grouse', 'Hawk', 'Heron', 'Hornbill', 'Hummingbird', 'Lark', 'Mallard', 'Oriole',
|
||||
'Osprey', 'Owl', 'Parrot', 'Penguin', 'Peregrine', 'Pelican', 'Pheasant', 'Quail', 'Raptor', 'Raven', 'Robin',
|
||||
'Sandpiper', 'Seagull', 'Sparrow', 'Stork', 'Thunderbird', 'Toucan', 'Vulture', 'Waterfowl', 'Woodpecker',
|
||||
'Wren', 'C-3PO', 'Chewbacca', 'Dagobah', 'DarthVader', 'DeathStar', 'Devaron', 'Droid', 'Endor', 'Ewok', 'Hoth',
|
||||
'Jakku', 'Jedi', 'Leia', 'Lightsaber', 'Lothal', 'Naboo', 'Padawan', 'R2-D2', 'Scarif', 'Sith', 'Skywalker',
|
||||
'Stormtrooper', 'Tatooine', 'Wookie', 'Yoda', 'Zanbar', 'Canoe', 'Catamaran', 'Cruiser', 'Cutter', 'Ferry',
|
||||
'Galleon', 'Gondola', 'Hovercraft', 'Hydrofoil', 'Jetski', 'Kayak', 'Longboat', 'Motorboat', 'Outrigger',
|
||||
'PirateShip', 'Riverboat', 'Sailboat', 'Skipjack', 'Schooner', 'Skiff', 'Sloop', 'Steamboat', 'Tanker',
|
||||
'Trimaran', 'Trawler', 'Tugboat', 'U-boat', 'Yacht', 'Yawl', 'Lancer', 'Volunteer', 'Searchlight', 'Passkey',
|
||||
'Deacon', 'Rawhide', 'Timberwolf', 'Eagle', 'Tumbler', 'Renegade', 'Mogul'
|
||||
]
|
||||
|
||||
this_name = all_names[random.randint(0, len(all_names) - 1)]
|
||||
return this_name
|
||||
|
||||
|
||||
def random_no_phrase():
|
||||
"""Returns a random 'no' phrase."""
|
||||
phrases = [
|
||||
'uhh...no',
|
||||
'lol no',
|
||||
'nope',
|
||||
]
|
||||
return phrases[random.randint(0, len(phrases) - 1)]
|
||||
|
||||
|
||||
def random_gif(search_term: str):
|
||||
"""
|
||||
Fetches a random GIF from Giphy API based on search term.
|
||||
Falls back to random_conf_gif() if request fails or contains 'trump'.
|
||||
"""
|
||||
req_url = f'https://api.giphy.com/v1/gifs/translate?s={search_term}&api_key=H86xibttEuUcslgmMM6uu74IgLEZ7UOD'
|
||||
|
||||
resp = requests.get(req_url, timeout=3)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
if 'trump' in data['data']['title']:
|
||||
return random_conf_gif()
|
||||
else:
|
||||
return data['data']['url']
|
||||
else:
|
||||
logger.warning(resp.text)
|
||||
raise ValueError(f'DB: {resp.text}')
|
||||
|
||||
|
||||
def random_from_list(data_list: list):
|
||||
"""Returns a random item from the provided list."""
|
||||
item = data_list[random.randint(0, len(data_list) - 1)]
|
||||
logger.info(f'random_from_list: {item}')
|
||||
return item
|
||||
|
||||
|
||||
def random_insult() -> str:
|
||||
"""Returns a random insult from the INSULTS constant."""
|
||||
return random_from_list(INSULTS)
|
||||
104
search_utils.py
Normal file
104
search_utils.py
Normal file
@ -0,0 +1,104 @@
|
||||
"""
|
||||
Search Utilities
|
||||
|
||||
This module contains search and fuzzy matching functionality.
|
||||
"""
|
||||
import discord
|
||||
from difflib import get_close_matches
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def fuzzy_search(name, master_list):
|
||||
"""
|
||||
Perform fuzzy string matching against a list of options.
|
||||
|
||||
Args:
|
||||
name: String to search for
|
||||
master_list: List of strings to search against
|
||||
|
||||
Returns:
|
||||
Best match string or raises ValueError if no good matches
|
||||
"""
|
||||
if name.lower() in master_list:
|
||||
return name.lower()
|
||||
|
||||
great_matches = get_close_matches(name, master_list, cutoff=0.8)
|
||||
if len(great_matches) == 1:
|
||||
return great_matches[0]
|
||||
elif len(great_matches) > 0:
|
||||
matches = great_matches
|
||||
else:
|
||||
matches = get_close_matches(name, master_list, n=6)
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
|
||||
if not matches:
|
||||
raise ValueError(f'{name.title()} was not found')
|
||||
|
||||
return matches[0]
|
||||
|
||||
|
||||
async def fuzzy_player_search(ctx, channel, bot, name, master_list):
|
||||
"""
|
||||
Interactive fuzzy player search with Discord UI.
|
||||
|
||||
Takes a name to search and returns the name of the best match.
|
||||
|
||||
Args:
|
||||
ctx: discord context
|
||||
channel: discord channel
|
||||
bot: discord.py bot object
|
||||
name: string to search for
|
||||
master_list: list of names to search against
|
||||
|
||||
Returns:
|
||||
Selected match or None if cancelled
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from discord_ui.confirmations import Question
|
||||
|
||||
matches = fuzzy_search(name, master_list)
|
||||
|
||||
embed = discord.Embed(
|
||||
title="Did You Mean...",
|
||||
description='Enter the number of the card you would like to see.',
|
||||
color=0x7FC600
|
||||
)
|
||||
count = 1
|
||||
for x in matches:
|
||||
embed.add_field(name=f'{count}', value=x, inline=False)
|
||||
count += 1
|
||||
embed.set_footer(text='These are the closest matches. Spell better if they\'re not who you want.')
|
||||
this_q = Question(bot, channel, None, 'int', 45, embed=embed)
|
||||
resp = await this_q.ask([ctx.author])
|
||||
|
||||
if not resp:
|
||||
return None
|
||||
if resp < count:
|
||||
return matches[resp - 1]
|
||||
else:
|
||||
raise ValueError(f'{resp} is not a valid response.')
|
||||
|
||||
|
||||
async def cardset_search(cardset: str, cardset_list: list) -> Optional[dict]:
|
||||
"""
|
||||
Search for a cardset by name and return the cardset data.
|
||||
|
||||
Args:
|
||||
cardset: Cardset name to search for
|
||||
cardset_list: List of available cardset names
|
||||
|
||||
Returns:
|
||||
Cardset dictionary or None if not found
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from api_calls import db_get
|
||||
|
||||
cardset_name = fuzzy_search(cardset, cardset_list)
|
||||
if not cardset_name:
|
||||
return None
|
||||
|
||||
c_query = await db_get('cardsets', params=[('name', cardset_name)])
|
||||
if c_query['count'] == 0:
|
||||
return None
|
||||
return c_query['cardsets'][0]
|
||||
@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
from sqlmodel import Session, select, func
|
||||
|
||||
from command_logic.logic_gameplay import advance_runners, doubles, gb_result_1, get_obc, get_re24, get_wpa, complete_play, log_run_scored, strikeouts, steals, xchecks
|
||||
from command_logic.logic_gameplay import advance_runners, doubles, gb_result_1, get_obc, get_re24, get_wpa, complete_play, log_run_scored, strikeouts, steals, xchecks, walks, popouts, hit_by_pitch, homeruns, singles, triples, bunts, chaos
|
||||
from in_game.gameplay_models import Lineup, Play
|
||||
from tests.factory import session_fixture, Game
|
||||
|
||||
@ -218,3 +218,228 @@ async def test_stealing(session: Session):
|
||||
assert play_2.on_first_final == 2
|
||||
assert play_2.sb == 1
|
||||
assert play_2.runner == play_2.on_first
|
||||
|
||||
|
||||
async def test_walks(session: Session):
|
||||
"""Test walk functionality - both intentional and unintentional walks."""
|
||||
this_game = session.get(Game, 3)
|
||||
play_1 = this_game.initialize_play(session)
|
||||
|
||||
# Test unintentional walk
|
||||
play_1 = await walks(session, None, play_1, 'unintentional')
|
||||
|
||||
assert play_1.ab == 0 # No at-bat for walk
|
||||
assert play_1.bb == 1 # Base on balls
|
||||
assert play_1.ibb == 0 # Not intentional
|
||||
assert play_1.batter_final == 1 # Batter reaches first
|
||||
|
||||
# Set up for intentional walk test
|
||||
play_2 = complete_play(session, play_1)
|
||||
play_2 = await walks(session, None, play_2, 'intentional')
|
||||
|
||||
assert play_2.ab == 0
|
||||
assert play_2.bb == 1
|
||||
assert play_2.ibb == 1 # Intentional walk
|
||||
assert play_2.batter_final == 1
|
||||
|
||||
|
||||
async def test_strikeouts(session: Session):
|
||||
"""Test strikeout functionality."""
|
||||
this_game = session.get(Game, 3)
|
||||
play_1 = this_game.initialize_play(session)
|
||||
|
||||
play_1 = await strikeouts(session, None, play_1)
|
||||
|
||||
assert play_1.so == 1 # Strikeout recorded
|
||||
assert play_1.outs == 1 # One out recorded
|
||||
# Note: batter_final is not set by strikeouts function, handled by advance_runners
|
||||
|
||||
|
||||
async def test_popouts(session: Session):
|
||||
"""Test popout functionality."""
|
||||
this_game = session.get(Game, 3)
|
||||
play_1 = this_game.initialize_play(session)
|
||||
|
||||
play_1 = await popouts(session, None, play_1)
|
||||
|
||||
assert play_1.outs == 1 # One out recorded
|
||||
# Note: batter_final is not set by popouts function, handled by advance_runners
|
||||
|
||||
|
||||
async def test_hit_by_pitch(session: Session):
|
||||
"""Test hit by pitch functionality."""
|
||||
this_game = session.get(Game, 3)
|
||||
play_1 = this_game.initialize_play(session)
|
||||
|
||||
play_1 = await hit_by_pitch(session, None, play_1)
|
||||
|
||||
assert play_1.ab == 0 # No at-bat for HBP
|
||||
assert play_1.hbp == 1 # Hit by pitch recorded
|
||||
assert play_1.batter_final == 1 # Batter reaches first
|
||||
|
||||
|
||||
async def test_homeruns(session: Session):
|
||||
"""Test homerun functionality - both ballpark and no-doubt homers."""
|
||||
this_game = session.get(Game, 3)
|
||||
play_1 = this_game.initialize_play(session)
|
||||
|
||||
# Test ballpark homerun
|
||||
play_1 = await homeruns(session, None, play_1, 'ballpark')
|
||||
|
||||
assert play_1.hit == 1
|
||||
assert play_1.homerun == 1
|
||||
assert play_1.batter_final == 4 # Batter scores
|
||||
assert play_1.run == 1 # Batter scores a run
|
||||
assert play_1.rbi >= 1 # At least 1 RBI (for the batter themselves)
|
||||
assert play_1.bphr == 1 # Ballpark homerun
|
||||
|
||||
# Test no-doubt homerun
|
||||
play_2 = complete_play(session, play_1)
|
||||
play_2 = await homeruns(session, None, play_2, 'no-doubt')
|
||||
|
||||
assert play_2.hit == 1
|
||||
assert play_2.homerun == 1
|
||||
assert play_2.batter_final == 4
|
||||
assert play_2.run == 1
|
||||
assert play_2.rbi >= 1
|
||||
assert play_2.bphr == 0 # Not a ballpark homerun
|
||||
|
||||
|
||||
async def test_singles(session: Session):
|
||||
"""Test single functionality with different types."""
|
||||
this_game = session.get(Game, 3)
|
||||
play_1 = this_game.initialize_play(session)
|
||||
|
||||
# Test regular single (*)
|
||||
play_1 = await singles(session, None, play_1, '*')
|
||||
|
||||
assert play_1.hit == 1
|
||||
assert play_1.batter_final == 1 # Batter reaches first
|
||||
assert play_1.bp1b == 0 # Not a ballpark single
|
||||
|
||||
# Test ballpark single
|
||||
play_2 = complete_play(session, play_1)
|
||||
play_2 = await singles(session, None, play_2, 'ballpark')
|
||||
|
||||
assert play_2.hit == 1
|
||||
assert play_2.batter_final == 1
|
||||
assert play_2.bp1b == 1 # Ballpark single
|
||||
|
||||
# Test double-advance single (**)
|
||||
play_3 = complete_play(session, play_2)
|
||||
play_3 = await singles(session, None, play_3, '**')
|
||||
|
||||
assert play_3.hit == 1
|
||||
assert play_3.batter_final == 1
|
||||
|
||||
|
||||
async def test_doubles(session: Session):
|
||||
"""Test double functionality with different types."""
|
||||
this_game = session.get(Game, 3)
|
||||
play_1 = this_game.initialize_play(session)
|
||||
|
||||
# Test regular double (**)
|
||||
play_1 = await doubles(session, None, play_1, '**')
|
||||
|
||||
assert play_1.hit == 1
|
||||
assert play_1.double == 1
|
||||
assert play_1.batter_final == 2 # Batter reaches second
|
||||
|
||||
# Test triple-advance double (***)
|
||||
play_2 = complete_play(session, play_1)
|
||||
play_2 = await doubles(session, None, play_2, '***')
|
||||
|
||||
assert play_2.hit == 1
|
||||
assert play_2.double == 1
|
||||
assert play_2.batter_final == 2
|
||||
|
||||
|
||||
async def test_triples(session: Session):
|
||||
"""Test triple functionality."""
|
||||
this_game = session.get(Game, 3)
|
||||
play_1 = this_game.initialize_play(session)
|
||||
|
||||
play_1 = await triples(session, None, play_1)
|
||||
|
||||
assert play_1.hit == 1
|
||||
assert play_1.triple == 1
|
||||
assert play_1.batter_final == 3 # Batter reaches third
|
||||
|
||||
|
||||
async def test_bunts(session: Session):
|
||||
"""Test bunt functionality with different types."""
|
||||
this_game = session.get(Game, 3)
|
||||
|
||||
# Test sacrifice bunt
|
||||
play_1 = this_game.initialize_play(session)
|
||||
play_1 = await bunts(session, None, play_1, 'sacrifice')
|
||||
|
||||
assert play_1.ab == 0 # No at-bat for sacrifice
|
||||
assert play_1.sac == 1 # Sacrifice recorded
|
||||
assert play_1.outs == 1 # Out recorded
|
||||
|
||||
# Test bad bunt (batter reaches first safely)
|
||||
play_2 = complete_play(session, play_1)
|
||||
play_2 = await bunts(session, None, play_2, 'bad')
|
||||
|
||||
assert play_2.ab == 1 # At-bat recorded
|
||||
assert play_2.sac == 0 # No sacrifice
|
||||
assert play_2.outs == 1 # Out recorded
|
||||
assert play_2.batter_final == 1 # Batter reaches first
|
||||
|
||||
# Test popout bunt
|
||||
play_3 = complete_play(session, play_2)
|
||||
play_3 = await bunts(session, None, play_3, 'popout')
|
||||
|
||||
assert play_3.ab == 1 # At-bat recorded
|
||||
assert play_3.sac == 0 # No sacrifice
|
||||
assert play_3.outs == 1 # Out recorded
|
||||
|
||||
# Test double-play bunt
|
||||
play_4 = complete_play(session, play_3)
|
||||
play_4 = await bunts(session, None, play_4, 'double-play')
|
||||
|
||||
assert play_4.ab == 1 # At-bat recorded
|
||||
assert play_4.sac == 0 # No sacrifice
|
||||
# Double play outs depend on starting_outs
|
||||
|
||||
|
||||
async def test_chaos(session: Session):
|
||||
"""Test chaos events - wild pitch, passed ball, balk, pickoff."""
|
||||
this_game = session.get(Game, 3)
|
||||
|
||||
# Test wild pitch
|
||||
play_1 = this_game.initialize_play(session)
|
||||
play_1 = await chaos(session, None, play_1, 'wild-pitch')
|
||||
|
||||
assert play_1.pa == 0 # No plate appearance
|
||||
assert play_1.ab == 0 # No at-bat
|
||||
assert play_1.wild_pitch == 1 # Wild pitch recorded
|
||||
assert play_1.rbi == 0 # No RBI on wild pitch
|
||||
|
||||
# Test passed ball
|
||||
play_2 = complete_play(session, play_1)
|
||||
play_2 = await chaos(session, None, play_2, 'passed-ball')
|
||||
|
||||
assert play_2.pa == 0
|
||||
assert play_2.ab == 0
|
||||
assert play_2.passed_ball == 1 # Passed ball recorded
|
||||
assert play_2.rbi == 0
|
||||
|
||||
# Test balk
|
||||
play_3 = complete_play(session, play_2)
|
||||
play_3 = await chaos(session, None, play_3, 'balk')
|
||||
|
||||
assert play_3.pa == 0
|
||||
assert play_3.ab == 0
|
||||
assert play_3.balk == 1 # Balk recorded
|
||||
assert play_3.rbi == 0
|
||||
|
||||
# Test pickoff
|
||||
play_4 = complete_play(session, play_3)
|
||||
play_4 = await chaos(session, None, play_4, 'pickoff')
|
||||
|
||||
assert play_4.pa == 0
|
||||
assert play_4.ab == 0
|
||||
assert play_4.pick_off == 1 # Pickoff recorded
|
||||
assert play_4.outs == 1 # Out recorded
|
||||
|
||||
@ -122,7 +122,7 @@ async def test_groundball_4_2(session: Session):
|
||||
|
||||
assert ending.starting_outs == 2
|
||||
assert ending.on_base_code == 1
|
||||
assert ending.on_first == play_2.batter
|
||||
assert ending.on_first == play_3.batter # Current batter reaches first (gb_4 rule: batter safe at first)
|
||||
assert ending.away_score == 1
|
||||
|
||||
|
||||
|
||||
19
tests/conftest.py
Normal file
19
tests/conftest.py
Normal file
@ -0,0 +1,19 @@
|
||||
"""
|
||||
Global pytest configuration for all tests.
|
||||
Sets up Docker environment for testcontainers.
|
||||
"""
|
||||
import os
|
||||
import pytest
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Configure pytest with required environment variables."""
|
||||
# Set Docker socket for testcontainers
|
||||
if not os.getenv('DOCKER_HOST'):
|
||||
os.environ['DOCKER_HOST'] = 'unix:///home/cal/.docker/desktop/docker.sock'
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
"""Add markers to tests that use testcontainers."""
|
||||
for item in items:
|
||||
# Add slow marker to tests that use database fixtures
|
||||
if 'session' in item.fixturenames:
|
||||
item.add_marker(pytest.mark.slow)
|
||||
775
tests/factory.py
775
tests/factory.py
@ -1,83 +1,89 @@
|
||||
import datetime
|
||||
import os
|
||||
import pytest
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
from sqlmodel.pool import StaticPool
|
||||
from sqlmodel import Session, SQLModel, create_engine, select, text
|
||||
from testcontainers.postgres import PostgresContainer
|
||||
|
||||
from typing import Literal
|
||||
from in_game.gameplay_models import BatterScouting, BattingCard, BattingRatings, Card, Cardset, Game, GameCardsetLink, Lineup, ManagerAi, PitcherScouting, PitchingCard, PitchingRatings, Play, RosterLink, Team, Player
|
||||
from in_game.gameplay_models import BatterScouting, BattingCard, BattingRatings, Card, Cardset, Game, GameCardsetLink, Lineup, ManagerAi, PitcherScouting, PitchingCard, PitchingRatings, Play, PositionRating, RosterLink, Team, Player
|
||||
|
||||
|
||||
@pytest.fixture(name='session')
|
||||
def session_fixture():
|
||||
engine = create_engine(
|
||||
'sqlite://', connect_args={'check_same_thread': False}, poolclass=StaticPool
|
||||
)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
team_1 = Team(
|
||||
id=31, abbrev='NCB', sname='CornBelters', lname='Normal CornBelters', gmid=1234, gmname='Cal', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='006900', season=7, event=False, career=1234, ranking=1337, has_guide=True, is_ai=True
|
||||
)
|
||||
team_2 = Team(
|
||||
id=400, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', gmid=5678, gmname='Evil Cal', gsheet='https://i.postimg.cc/HjDc8bBF/blackbears-transparent.png', wallet=350, team_value=420, collection_value=169, color='6699FF', season=7, event=False, career=2, ranking=969, has_guide=False, is_ai=False
|
||||
)
|
||||
team_3 = Team(
|
||||
id=69, abbrev='NCB3', sname='CornBelters', lname='Normal CornBelters', gmid=1234, gmname='Cal', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='006900', season=7, event=False, career=1234, ranking=1337, has_guide=True, is_ai=False
|
||||
)
|
||||
team_4 = Team(
|
||||
id=420, abbrev='WV4', sname='Black Bears', lname='West Virginia Black Bears', gmid=5678, gmname='Evil Cal', gsheet='https://i.postimg.cc/HjDc8bBF/blackbears-transparent.png', wallet=350, team_value=420, collection_value=169, color='6699FF', season=7, event=False, career=2, ranking=969, has_guide=False, is_ai=True
|
||||
)
|
||||
incomplete_team = Team(
|
||||
id=446, abbrev='CLS', sname='Macho Men', lname='Columbus Macho Men', gmid=181818, gmname='Mason Socc', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='https://i.postimg.cc/8kLZCYXh/S10CLS.png', season=7, event=False, career=0
|
||||
)
|
||||
old_cache_team = Team(
|
||||
id=3, abbrev='BAL', sname='Orioles', lname='Baltimore Orioles', gmid=181818, gmname='Brandon Hyde', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='https://i.postimg.cc/8kLZCYXh/S10CLS.png', season=7, event=False, career=0, ranking=500, has_guide=False, is_ai=False, created=datetime.datetime.today() - datetime.timedelta(days=60)
|
||||
)
|
||||
|
||||
session.add(team_1)
|
||||
session.add(team_2)
|
||||
session.add(team_3)
|
||||
session.add(team_4)
|
||||
# session.add(incomplete_team)
|
||||
session.add(old_cache_team)
|
||||
session.commit()
|
||||
# Ensure Docker socket is set for testcontainers
|
||||
if not os.getenv('DOCKER_HOST'):
|
||||
os.environ['DOCKER_HOST'] = 'unix:///home/cal/.docker/desktop/docker.sock'
|
||||
with PostgresContainer("postgres:13") as postgres:
|
||||
postgres_url = postgres.get_connection_url()
|
||||
engine = create_engine(postgres_url)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
team_1 = Team(
|
||||
id=31, abbrev='NCB', sname='CornBelters', lname='Normal CornBelters', gmid=1234, gmname='Cal', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='006900', season=7, event=False, career=1234, ranking=1337, has_guide=True, is_ai=True
|
||||
)
|
||||
team_2 = Team(
|
||||
id=400, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', gmid=5678, gmname='Evil Cal', gsheet='https://i.postimg.cc/HjDc8bBF/blackbears-transparent.png', wallet=350, team_value=420, collection_value=169, color='6699FF', season=7, event=False, career=2, ranking=969, has_guide=False, is_ai=False
|
||||
)
|
||||
team_3 = Team(
|
||||
id=69, abbrev='NCB3', sname='CornBelters', lname='Normal CornBelters', gmid=1234, gmname='Cal', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='006900', season=7, event=False, career=1234, ranking=1337, has_guide=True, is_ai=False
|
||||
)
|
||||
team_4 = Team(
|
||||
id=420, abbrev='WV4', sname='Black Bears', lname='West Virginia Black Bears', gmid=5678, gmname='Evil Cal', gsheet='https://i.postimg.cc/HjDc8bBF/blackbears-transparent.png', wallet=350, team_value=420, collection_value=169, color='6699FF', season=7, event=False, career=2, ranking=969, has_guide=False, is_ai=True
|
||||
)
|
||||
incomplete_team = Team(
|
||||
id=446, abbrev='CLS', sname='Macho Men', lname='Columbus Macho Men', gmid=181818, gmname='Mason Socc', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='https://i.postimg.cc/8kLZCYXh/S10CLS.png', season=7, event=False, career=0
|
||||
)
|
||||
old_cache_team = Team(
|
||||
id=3, abbrev='BAL', sname='Orioles', lname='Baltimore Orioles', gmid=181818, gmname='Brandon Hyde', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='https://i.postimg.cc/8kLZCYXh/S10CLS.png', season=7, event=False, career=0, ranking=500, has_guide=False, is_ai=False, created=datetime.datetime.today() - datetime.timedelta(days=60)
|
||||
)
|
||||
|
||||
game_1 = Game(away_team_id=31, home_team_id=400, channel_id=1234, season=9, ai_team='away', game_type='minor-league')
|
||||
game_2 = Game(away_team_id=69, home_team_id=420, channel_id=5678, season=9, active=False, is_pd=True, ranked=True, week=6, game_num=9, away_roster_id=69, home_roster_id=420, first_message=12345678, ai_team='home', game_type='minor-league')
|
||||
game_3 = Game(away_team_id=69, home_team_id=420, channel_id=5678, season=9, active=True, is_pd=True, ranked=True, week=6, game_num=10, away_roster_id=69, home_roster_id=420, first_message=34567890, ai_team='home', game_type='minor-league')
|
||||
|
||||
session.add(game_1)
|
||||
session.add(game_2)
|
||||
session.add(game_3)
|
||||
session.commit()
|
||||
session.add(team_1)
|
||||
session.add(team_2)
|
||||
session.add(team_3)
|
||||
session.add(team_4)
|
||||
# session.add(incomplete_team)
|
||||
session.add(old_cache_team)
|
||||
session.commit()
|
||||
|
||||
cardset_1 = Cardset(id=1, name='1969 Live')
|
||||
cardset_2 = Cardset(id=2, name='2024 Season')
|
||||
game_1 = Game(id=1, away_team_id=31, home_team_id=400, channel_id=1234, season=9, ai_team='away', game_type='minor-league')
|
||||
game_2 = Game(id=2, away_team_id=69, home_team_id=420, channel_id=5678, season=9, active=False, is_pd=True, ranked=True, week=6, game_num=9, away_roster_id=69, home_roster_id=420, first_message=12345678, ai_team='home', game_type='minor-league')
|
||||
game_3 = Game(id=3, away_team_id=69, home_team_id=420, channel_id=5678, season=9, active=True, is_pd=True, ranked=True, week=6, game_num=10, away_roster_id=69, home_roster_id=420, first_message=34567890, ai_team='home', game_type='minor-league')
|
||||
|
||||
session.add(cardset_1)
|
||||
session.add(cardset_2)
|
||||
session.commit()
|
||||
session.add(game_1)
|
||||
session.add(game_2)
|
||||
session.add(game_3)
|
||||
session.commit()
|
||||
|
||||
link_1 = GameCardsetLink(game=game_1, cardset=cardset_1, priority=1)
|
||||
link_2 = GameCardsetLink(game=game_1, cardset=cardset_2, priority=1)
|
||||
link_3 = GameCardsetLink(game=game_2, cardset=cardset_1, priority=1)
|
||||
link_4 = GameCardsetLink(game=game_2, cardset=cardset_2, priority=2)
|
||||
cardset_1 = Cardset(id=1, name='1969 Live')
|
||||
cardset_2 = Cardset(id=2, name='2024 Season')
|
||||
|
||||
session.add(link_1)
|
||||
session.add(link_2)
|
||||
session.add(link_3)
|
||||
session.add(link_4)
|
||||
session.commit()
|
||||
session.add(cardset_1)
|
||||
session.add(cardset_2)
|
||||
session.commit()
|
||||
|
||||
all_players = []
|
||||
all_cards = []
|
||||
all_batscouting = []
|
||||
all_pitscouting = []
|
||||
all_pitratings = []
|
||||
all_batratings = []
|
||||
all_batcards = []
|
||||
all_pitcards = []
|
||||
pos_list = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH', 'P']
|
||||
for x in range(40):
|
||||
link_1 = GameCardsetLink(game=game_1, cardset=cardset_1, priority=1)
|
||||
link_2 = GameCardsetLink(game=game_1, cardset=cardset_2, priority=1)
|
||||
link_3 = GameCardsetLink(game=game_2, cardset=cardset_1, priority=1)
|
||||
link_4 = GameCardsetLink(game=game_2, cardset=cardset_2, priority=2)
|
||||
|
||||
session.add(link_1)
|
||||
session.add(link_2)
|
||||
session.add(link_3)
|
||||
session.add(link_4)
|
||||
session.commit()
|
||||
|
||||
all_players = []
|
||||
all_cards = []
|
||||
all_batscouting = []
|
||||
all_pitscouting = []
|
||||
all_pitratings = []
|
||||
all_batratings = []
|
||||
all_batcards = []
|
||||
all_pitcards = []
|
||||
pos_list = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH', 'P']
|
||||
|
||||
# Create players first
|
||||
for x in range(40):
|
||||
if x < 10:
|
||||
mlb_team = 'Baltimore Orioles'
|
||||
team_id = 31
|
||||
@ -104,12 +110,15 @@ def session_fixture():
|
||||
pos_1=pos_list[(x % 10)],
|
||||
description="Live" if x % 2 == 1 else "2024"
|
||||
))
|
||||
|
||||
# Create cards and ratings for each player
|
||||
# Is Batter
|
||||
if x % 10 == 9:
|
||||
if (x + 1) % 10 != 0:
|
||||
all_batcards.append(BattingCard(
|
||||
id=x+1,
|
||||
steal_high=10 + (x % 10),
|
||||
hand='R'
|
||||
hand='R',
|
||||
offense_col=1
|
||||
))
|
||||
all_batratings.append(BattingRatings(
|
||||
id=x+1,
|
||||
@ -119,23 +128,14 @@ def session_fixture():
|
||||
id=x+1001,
|
||||
homerun=x % 10
|
||||
))
|
||||
all_batscouting.append(BatterScouting(
|
||||
id=x+1,
|
||||
battingcard_id=x+1,
|
||||
ratings_vr_id=x+1,
|
||||
ratings_vl_id=x+1001,
|
||||
))
|
||||
all_cards.append(Card(
|
||||
player_id=x+1,
|
||||
team_id=team_id,
|
||||
batterscouting_id=x+1
|
||||
))
|
||||
# Is Pitcher
|
||||
else:
|
||||
all_pitcards.append(PitchingCard(
|
||||
id=x+1,
|
||||
wild_pitch=x / 10,
|
||||
hand='R'
|
||||
hand='R',
|
||||
starter_rating=5,
|
||||
offense_col=1
|
||||
))
|
||||
all_pitratings.append(PitchingRatings(
|
||||
id=x+1,
|
||||
@ -145,116 +145,585 @@ def session_fixture():
|
||||
id=x+1001,
|
||||
homerun=x % 10
|
||||
))
|
||||
all_pitscouting.append(PitcherScouting(
|
||||
|
||||
all_players.append(Player(
|
||||
id=41, name='Player 40', cost=41*3, mlbclub='Junior All-Stars', franchise='Junior All-Stars', cardset=cardset_1, set_num=41, pos_1='DH', description='Live', created=datetime.datetime.today() - datetime.timedelta(days=60), image='player_41_image_URL', rarity_id=1
|
||||
))
|
||||
all_players.append(Player(
|
||||
id=69, name='Player 68', cost=69*3, mlbclub='Junior All-Stars', franchise='Junior All-Stars', cardset=cardset_1, set_num=69, pos_1='DH', description='Live', created=datetime.datetime.today() - datetime.timedelta(days=60), image='player_69_image_URL', rarity_id=1
|
||||
))
|
||||
|
||||
# Add scouting data for player 41 (batter)
|
||||
all_batcards.append(BattingCard(
|
||||
id=41,
|
||||
steal_high=15,
|
||||
hand='R',
|
||||
offense_col=1
|
||||
))
|
||||
all_batratings.append(BattingRatings(
|
||||
id=41,
|
||||
homerun=5
|
||||
))
|
||||
all_batratings.append(BattingRatings(
|
||||
id=41001,
|
||||
homerun=5
|
||||
))
|
||||
all_players.append(Player(
|
||||
id=70, name='Player 69', cost=70*3, mlbclub='Junior All-Stars', franchise='Junior All-Stars', cardset=cardset_1, set_num=70, pos_1='SP', pos_2='DH', description='Live', created=datetime.datetime.today() - datetime.timedelta(days=60), image='player_69_pitchingcard', image2='player_69_battingcard', rarity_id=1
|
||||
))
|
||||
|
||||
# Add scouting data for player 70 (two-way player - pitcher primary)
|
||||
all_pitcards.append(PitchingCard(
|
||||
id=70,
|
||||
wild_pitch=2.0,
|
||||
hand='R',
|
||||
starter_rating=7,
|
||||
offense_col=1
|
||||
))
|
||||
all_pitratings.append(PitchingRatings(
|
||||
id=70,
|
||||
homerun=3
|
||||
))
|
||||
all_pitratings.append(PitchingRatings(
|
||||
id=70001,
|
||||
homerun=3
|
||||
))
|
||||
|
||||
# Also add batting data for player 70 with unique ID
|
||||
all_batcards.append(BattingCard(
|
||||
id=170, # Use unique ID to avoid conflicts
|
||||
steal_high=12,
|
||||
hand='R',
|
||||
offense_col=1
|
||||
))
|
||||
all_batratings.append(BattingRatings(
|
||||
id=170,
|
||||
homerun=4
|
||||
))
|
||||
all_batratings.append(BattingRatings(
|
||||
id=170001,
|
||||
homerun=4
|
||||
))
|
||||
|
||||
# Add records in correct order to avoid foreign key constraint errors
|
||||
# 1. Add players first (no dependencies)
|
||||
for player in all_players:
|
||||
session.add(player)
|
||||
session.commit()
|
||||
|
||||
# 2. Add batting/pitching cards and ratings (depend on players)
|
||||
for batcard in all_batcards:
|
||||
session.add(batcard)
|
||||
for pitcard in all_pitcards:
|
||||
session.add(pitcard)
|
||||
for batrating in all_batratings:
|
||||
session.add(batrating)
|
||||
for pitrating in all_pitratings:
|
||||
session.add(pitrating)
|
||||
session.commit()
|
||||
|
||||
# 3. Create scouting records and cards in a coordinated way
|
||||
# Create cards for each player based on their position
|
||||
for x in range(40):
|
||||
if x < 10:
|
||||
team_id = 31
|
||||
elif x < 20:
|
||||
team_id = 400
|
||||
elif x < 30:
|
||||
team_id = 69
|
||||
else:
|
||||
team_id = 420
|
||||
|
||||
# Is Batter
|
||||
if (x + 1) % 10 != 0:
|
||||
# Create batter scouting (let PostgreSQL assign ID)
|
||||
batscouting = BatterScouting(
|
||||
battingcard_id=x+1,
|
||||
ratings_vr_id=x+1,
|
||||
ratings_vl_id=x+1001,
|
||||
)
|
||||
session.add(batscouting)
|
||||
session.commit()
|
||||
session.refresh(batscouting) # Get the auto-generated ID
|
||||
|
||||
# Create card with the actual scouting ID
|
||||
card = Card(
|
||||
id=x+1,
|
||||
player_id=x+1,
|
||||
team_id=team_id,
|
||||
batterscouting_id=batscouting.id
|
||||
)
|
||||
session.add(card)
|
||||
# Is Pitcher
|
||||
else:
|
||||
# Create pitcher scouting (let PostgreSQL assign ID)
|
||||
pitscouting = PitcherScouting(
|
||||
pitchingcard_id=x+1,
|
||||
ratings_vr_id=x+1,
|
||||
ratings_vl_id=x+1001
|
||||
))
|
||||
all_cards.append(Card(
|
||||
)
|
||||
session.add(pitscouting)
|
||||
session.commit()
|
||||
session.refresh(pitscouting) # Get the auto-generated ID
|
||||
|
||||
# Create card with the actual scouting ID
|
||||
card = Card(
|
||||
id=x+1,
|
||||
player_id=x+1,
|
||||
team_id=team_id,
|
||||
pitcherscouting_id=x+1
|
||||
))
|
||||
pitcherscouting_id=pitscouting.id
|
||||
)
|
||||
session.add(card)
|
||||
|
||||
# Handle special players 41 and 70
|
||||
# Create batter scouting for player 41
|
||||
batscouting_41 = BatterScouting(
|
||||
battingcard_id=41,
|
||||
ratings_vr_id=41,
|
||||
ratings_vl_id=41001,
|
||||
)
|
||||
session.add(batscouting_41)
|
||||
session.commit()
|
||||
session.refresh(batscouting_41)
|
||||
|
||||
card_41 = Card(
|
||||
id=41,
|
||||
player_id=41,
|
||||
team_id=420,
|
||||
created=datetime.datetime.now() - datetime.timedelta(days=60),
|
||||
batterscouting_id=batscouting_41.id
|
||||
)
|
||||
session.add(card_41)
|
||||
|
||||
# Create pitcher scouting for player 70
|
||||
pitscouting_70 = PitcherScouting(
|
||||
pitchingcard_id=70,
|
||||
ratings_vr_id=70,
|
||||
ratings_vl_id=70001
|
||||
)
|
||||
session.add(pitscouting_70)
|
||||
session.commit()
|
||||
session.refresh(pitscouting_70)
|
||||
|
||||
# Create batter scouting for player 70 (two-way player)
|
||||
batscouting_70 = BatterScouting(
|
||||
battingcard_id=170,
|
||||
ratings_vr_id=170,
|
||||
ratings_vl_id=170001,
|
||||
)
|
||||
session.add(batscouting_70)
|
||||
session.commit()
|
||||
session.refresh(batscouting_70)
|
||||
|
||||
card_70 = Card(
|
||||
id=70,
|
||||
player_id=70,
|
||||
team_id=420,
|
||||
pitcherscouting_id=pitscouting_70.id,
|
||||
batterscouting_id=batscouting_70.id
|
||||
)
|
||||
session.add(card_70)
|
||||
|
||||
all_players.append(Player(
|
||||
id=69, name='Player 68', cost=69*3, mlbclub='Junior All-Stars', franchise='Junior All-Stars', cardset=cardset_1, set_num=69, pos_1='DH', description='Live', created=datetime.datetime.today() - datetime.timedelta(days=60), image='player_69_image_URL', rarity_id=1
|
||||
))
|
||||
all_cards.append(Card(
|
||||
player_id=41,
|
||||
team_id=420,
|
||||
created=datetime.datetime.now() - datetime.timedelta(days=60)
|
||||
))
|
||||
all_players.append(Player(
|
||||
id=70, name='Player 69', cost=70*3, mlbclub='Junior All-Stars', franchise='Junior All-Stars', cardset=cardset_1, set_num=70, pos_1='SP', pos_2='DH', description='Live', created=datetime.datetime.today() - datetime.timedelta(days=60), image='player_69_pitchingcard', image2='player_69_battingcard', rarity_id=1
|
||||
))
|
||||
session.commit()
|
||||
|
||||
for player in all_players:
|
||||
session.add(player)
|
||||
for card in all_cards:
|
||||
session.add(card)
|
||||
|
||||
session.commit()
|
||||
|
||||
all_lineups = []
|
||||
player_count = 1
|
||||
for team in [team_1, team_2]:
|
||||
# Add position ratings for players (needed for manager AI decisions)
|
||||
all_position_ratings = []
|
||||
pos_list = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH', 'P']
|
||||
|
||||
for x in range(40):
|
||||
player_pos = pos_list[(x % 10)]
|
||||
# Add defensive rating for each player's primary position
|
||||
all_position_ratings.append(PositionRating(
|
||||
player_id=x+1,
|
||||
variant=0,
|
||||
position=player_pos,
|
||||
innings=100,
|
||||
range=5,
|
||||
error=1,
|
||||
arm=7 if player_pos == 'C' else None, # Catchers need arm rating
|
||||
pb=2 if player_pos == 'C' else None, # Catchers need passed ball rating
|
||||
overthrow=1 if player_pos == 'C' else None
|
||||
))
|
||||
|
||||
# Add position ratings for special players
|
||||
all_position_ratings.append(PositionRating(
|
||||
player_id=41,
|
||||
variant=0,
|
||||
position='DH',
|
||||
innings=100,
|
||||
range=5,
|
||||
error=1
|
||||
))
|
||||
|
||||
all_position_ratings.append(PositionRating(
|
||||
player_id=69,
|
||||
variant=0,
|
||||
position='DH',
|
||||
innings=100,
|
||||
range=5,
|
||||
error=1
|
||||
))
|
||||
|
||||
all_position_ratings.append(PositionRating(
|
||||
player_id=70,
|
||||
variant=0,
|
||||
position='SP',
|
||||
innings=100,
|
||||
range=5,
|
||||
error=1
|
||||
))
|
||||
|
||||
# Also add DH rating for player 70 (two-way player)
|
||||
all_position_ratings.append(PositionRating(
|
||||
player_id=70,
|
||||
variant=0,
|
||||
position='DH',
|
||||
innings=50,
|
||||
range=4,
|
||||
error=2
|
||||
))
|
||||
|
||||
for pos_rating in all_position_ratings:
|
||||
session.add(pos_rating)
|
||||
|
||||
session.commit()
|
||||
|
||||
all_lineups = []
|
||||
player_count = 1
|
||||
lineup_id = 1
|
||||
for team in [team_1, team_2]:
|
||||
for (order, pos) in [(1, 'C'), (2, '1B'), (3, '2B'), (4, '3B'), (5, 'SS'), (6, 'LF'), (7, 'CF'), (8, 'RF'), (9, 'DH'), (10, 'P')]:
|
||||
all_lineups.append(Lineup(
|
||||
id=lineup_id,
|
||||
position=pos,
|
||||
batting_order=order,
|
||||
game=game_1,
|
||||
team=team,
|
||||
game=game_1,
|
||||
team=team,
|
||||
player_id=player_count,
|
||||
card_id=player_count
|
||||
))
|
||||
player_count += 1
|
||||
for team in [team_3, team_4]:
|
||||
lineup_id += 1
|
||||
for team in [team_3, team_4]:
|
||||
for (order, pos) in [(1, 'C'), (2, '1B'), (3, '2B'), (4, '3B'), (5, 'SS'), (6, 'LF'), (7, 'CF'), (8, 'RF'), (9, 'DH'), (10, 'P')]:
|
||||
all_lineups.append(Lineup(
|
||||
id=lineup_id,
|
||||
position=pos,
|
||||
batting_order=order,
|
||||
game=game_3,
|
||||
team=team,
|
||||
game=game_3,
|
||||
team=team,
|
||||
player_id=player_count,
|
||||
card_id=player_count
|
||||
))
|
||||
player_count += 1
|
||||
|
||||
for lineup in all_lineups:
|
||||
lineup_id += 1
|
||||
|
||||
for lineup in all_lineups:
|
||||
session.add(lineup)
|
||||
|
||||
game_1_play_1 = Play(
|
||||
game=game_1,
|
||||
play_num=1,
|
||||
batter=all_lineups[0],
|
||||
batter_pos=all_lineups[0].position,
|
||||
pitcher=all_lineups[19],
|
||||
catcher=all_lineups[10],
|
||||
pa=1,
|
||||
so=1,
|
||||
outs=1,
|
||||
complete=True
|
||||
)
|
||||
game_1_play_2 = Play(
|
||||
game=game_1,
|
||||
play_num=2,
|
||||
batter=all_lineups[1],
|
||||
batter_pos=all_lineups[1].position,
|
||||
pitcher=all_lineups[19],
|
||||
catcher=all_lineups[10],
|
||||
starting_outs=1
|
||||
)
|
||||
|
||||
session.add(game_1_play_1)
|
||||
session.add(game_1_play_2)
|
||||
|
||||
session.commit()
|
||||
game_1_play_1 = Play(
|
||||
game=game_1,
|
||||
play_num=1,
|
||||
batter=all_lineups[0],
|
||||
batter_pos=all_lineups[0].position,
|
||||
pitcher=all_lineups[19],
|
||||
catcher=all_lineups[10],
|
||||
pa=1,
|
||||
so=1,
|
||||
outs=1,
|
||||
complete=True
|
||||
)
|
||||
game_1_play_2 = Play(
|
||||
game=game_1,
|
||||
play_num=2,
|
||||
batter=all_lineups[1],
|
||||
batter_pos=all_lineups[1].position,
|
||||
pitcher=all_lineups[19],
|
||||
catcher=all_lineups[10],
|
||||
starting_outs=1
|
||||
)
|
||||
|
||||
g1_t1_cards = session.exec(select(Card).where(Card.team_id == 31)).all()
|
||||
g1_t2_cards = session.exec(select(Card).where(Card.team_id == 400)).all()
|
||||
g2_t1_cards = session.exec(select(Card).where(Card.team_id == 69)).all()
|
||||
g2_t2_cards = session.exec(select(Card).where(Card.team_id == 420)).all()
|
||||
session.add(game_1_play_1)
|
||||
session.add(game_1_play_2)
|
||||
|
||||
for card in [*g1_t1_cards, *g1_t2_cards]:
|
||||
session.commit()
|
||||
|
||||
g1_t1_cards = session.exec(select(Card).where(Card.team_id == 31)).all()
|
||||
g1_t2_cards = session.exec(select(Card).where(Card.team_id == 400)).all()
|
||||
g2_t1_cards = session.exec(select(Card).where(Card.team_id == 69)).all()
|
||||
g2_t2_cards = session.exec(select(Card).where(Card.team_id == 420)).all()
|
||||
|
||||
for card in [*g1_t1_cards, *g1_t2_cards]:
|
||||
session.add(RosterLink(
|
||||
game_id=1,
|
||||
card_id=card.id,
|
||||
team_id=card.team_id
|
||||
))
|
||||
for card in [*g2_t1_cards, *g2_t2_cards]:
|
||||
for card in [*g2_t1_cards, *g2_t2_cards]:
|
||||
session.add(RosterLink(
|
||||
game_id=3,
|
||||
card_id=card.id,
|
||||
team_id=card.team_id
|
||||
))
|
||||
# session.add(RosterLink(
|
||||
# game_id=3,
|
||||
# card_id=12,
|
||||
# team_id=420
|
||||
# ))
|
||||
# session.add(RosterLink(
|
||||
# game_id=3,
|
||||
# card_id=12,
|
||||
# team_id=420
|
||||
# ))
|
||||
|
||||
session.commit()
|
||||
all_ai = ManagerAi.create_ai(session)
|
||||
session.commit()
|
||||
|
||||
yield session
|
||||
# Create ManagerAi objects with explicit IDs for test compatibility
|
||||
manager_ai_1 = ManagerAi(id=1, name='Balanced')
|
||||
manager_ai_2 = ManagerAi(id=2, name='Yolo', steal=10, running=10, hold=5, catcher_throw=10, uncapped_home=10, uncapped_third=10, uncapped_trail=10, bullpen_matchup=3, behind_aggression=10, ahead_aggression=10, decide_throw=10)
|
||||
manager_ai_3 = ManagerAi(id=3, name='Safe', steal=3, running=3, hold=8, catcher_throw=5, uncapped_home=5, uncapped_third=3, uncapped_trail=5, bullpen_matchup=8, behind_aggression=5, ahead_aggression=1, decide_throw=1)
|
||||
|
||||
session.add(manager_ai_1)
|
||||
session.add(manager_ai_2)
|
||||
session.add(manager_ai_3)
|
||||
session.commit()
|
||||
|
||||
yield session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_ratings_query():
|
||||
"""Factory method for sample ratings query data used in batter scouting tests."""
|
||||
return {
|
||||
"count": 2,
|
||||
"ratings": [
|
||||
{
|
||||
"id": 7673,
|
||||
"battingcard": {
|
||||
"id": 3837,
|
||||
"player": {
|
||||
"player_id": 395,
|
||||
"p_name": "Cedric Mullins",
|
||||
"cost": 256,
|
||||
"image": "https://pd.manticorum.com/api/v2/players/395/battingcard?d=2023-11-19",
|
||||
"image2": None,
|
||||
"mlbclub": "Baltimore Orioles",
|
||||
"franchise": "Baltimore Orioles",
|
||||
"cardset": {
|
||||
"id": 1,
|
||||
"name": "2021 Season",
|
||||
"description": "Cards based on the full 2021 season",
|
||||
"event": None,
|
||||
"for_purchase": True,
|
||||
"total_cards": 791,
|
||||
"in_packs": True,
|
||||
"ranked_legal": False
|
||||
},
|
||||
"set_num": 395,
|
||||
"rarity": {
|
||||
"id": 2,
|
||||
"value": 3,
|
||||
"name": "All-Star",
|
||||
"color": "FFD700"
|
||||
},
|
||||
"pos_1": "CF",
|
||||
"pos_2": None,
|
||||
"pos_3": None,
|
||||
"pos_4": None,
|
||||
"pos_5": None,
|
||||
"pos_6": None,
|
||||
"pos_7": None,
|
||||
"pos_8": None,
|
||||
"headshot": "https://www.baseball-reference.com/req/202206291/images/headshots/2/24bf4355_mlbam.jpg",
|
||||
"vanity_card": None,
|
||||
"strat_code": "17929",
|
||||
"bbref_id": "mullice01",
|
||||
"fangr_id": None,
|
||||
"description": "2021",
|
||||
"quantity": 999,
|
||||
"mlbplayer": {
|
||||
"id": 396,
|
||||
"first_name": "Cedric",
|
||||
"last_name": "Mullins",
|
||||
"key_fangraphs": 17929,
|
||||
"key_bbref": "mullice01",
|
||||
"key_retro": "mullc002",
|
||||
"key_mlbam": 656775,
|
||||
"offense_col": 1
|
||||
}
|
||||
},
|
||||
"variant": 0,
|
||||
"steal_low": 9,
|
||||
"steal_high": 12,
|
||||
"steal_auto": True,
|
||||
"steal_jump": 0.2222222222222222,
|
||||
"bunting": "C",
|
||||
"hit_and_run": "B",
|
||||
"running": 13,
|
||||
"offense_col": 1,
|
||||
"hand": "L"
|
||||
},
|
||||
"vs_hand": "L",
|
||||
"pull_rate": 0.43888889,
|
||||
"center_rate": 0.32777778,
|
||||
"slap_rate": 0.23333333,
|
||||
"homerun": 2.25,
|
||||
"bp_homerun": 5.0,
|
||||
"triple": 0.0,
|
||||
"double_three": 0.0,
|
||||
"double_two": 2.4,
|
||||
"double_pull": 7.2,
|
||||
"single_two": 4.6,
|
||||
"single_one": 3.5,
|
||||
"single_center": 5.85,
|
||||
"bp_single": 5.0,
|
||||
"hbp": 2.0,
|
||||
"walk": 9.0,
|
||||
"strikeout": 23.0,
|
||||
"lineout": 3.0,
|
||||
"popout": 6.0,
|
||||
"flyout_a": 0.0,
|
||||
"flyout_bq": 0.15,
|
||||
"flyout_lf_b": 2.8,
|
||||
"flyout_rf_b": 3.75,
|
||||
"groundout_a": 0.0,
|
||||
"groundout_b": 9.0,
|
||||
"groundout_c": 13.5,
|
||||
"avg": 0.2851851851851852,
|
||||
"obp": 0.387037037037037,
|
||||
"slg": 0.5060185185185185
|
||||
},
|
||||
{
|
||||
"id": 7674,
|
||||
"battingcard": {
|
||||
"id": 3837,
|
||||
"player": {
|
||||
"player_id": 395,
|
||||
"p_name": "Cedric Mullins",
|
||||
"cost": 256,
|
||||
"image": "https://pd.manticorum.com/api/v2/players/395/battingcard?d=2023-11-19",
|
||||
"image2": None,
|
||||
"mlbclub": "Baltimore Orioles",
|
||||
"franchise": "Baltimore Orioles",
|
||||
"cardset": {
|
||||
"id": 1,
|
||||
"name": "2021 Season",
|
||||
"description": "Cards based on the full 2021 season",
|
||||
"event": None,
|
||||
"for_purchase": True,
|
||||
"total_cards": 791,
|
||||
"in_packs": True,
|
||||
"ranked_legal": False
|
||||
},
|
||||
"set_num": 395,
|
||||
"rarity": {
|
||||
"id": 2,
|
||||
"value": 3,
|
||||
"name": "All-Star",
|
||||
"color": "FFD700"
|
||||
},
|
||||
"pos_1": "CF",
|
||||
"pos_2": None,
|
||||
"pos_3": None,
|
||||
"pos_4": None,
|
||||
"pos_5": None,
|
||||
"pos_6": None,
|
||||
"pos_7": None,
|
||||
"pos_8": None,
|
||||
"headshot": "https://www.baseball-reference.com/req/202206291/images/headshots/2/24bf4355_mlbam.jpg",
|
||||
"vanity_card": None,
|
||||
"strat_code": "17929",
|
||||
"bbref_id": "mullice01",
|
||||
"fangr_id": None,
|
||||
"description": "2021",
|
||||
"quantity": 999,
|
||||
"mlbplayer": {
|
||||
"id": 396,
|
||||
"first_name": "Cedric",
|
||||
"last_name": "Mullins",
|
||||
"key_fangraphs": 17929,
|
||||
"key_bbref": "mullice01",
|
||||
"key_retro": "mullc002",
|
||||
"key_mlbam": 656775,
|
||||
"offense_col": 1
|
||||
}
|
||||
},
|
||||
"variant": 0,
|
||||
"steal_low": 9,
|
||||
"steal_high": 12,
|
||||
"steal_auto": True,
|
||||
"steal_jump": 0.2222222222222222,
|
||||
"bunting": "C",
|
||||
"hit_and_run": "B",
|
||||
"running": 13,
|
||||
"offense_col": 1,
|
||||
"hand": "L"
|
||||
},
|
||||
"vs_hand": "R",
|
||||
"pull_rate": 0.43377483,
|
||||
"center_rate": 0.32119205,
|
||||
"slap_rate": 0.24503311,
|
||||
"homerun": 2.0,
|
||||
"bp_homerun": 7.0,
|
||||
"triple": 0.0,
|
||||
"double_three": 0.0,
|
||||
"double_two": 2.7,
|
||||
"double_pull": 7.95,
|
||||
"single_two": 3.3,
|
||||
"single_one": 0.0,
|
||||
"single_center": 8.6,
|
||||
"bp_single": 5.0,
|
||||
"hbp": 1.0,
|
||||
"walk": 12.9,
|
||||
"strikeout": 11.1,
|
||||
"lineout": 8.0,
|
||||
"popout": 11.0,
|
||||
"flyout_a": 0.0,
|
||||
"flyout_bq": 0.4,
|
||||
"flyout_lf_b": 4.05,
|
||||
"flyout_rf_b": 6.0,
|
||||
"groundout_a": 0.0,
|
||||
"groundout_b": 3.0,
|
||||
"groundout_c": 14.0,
|
||||
"avg": 0.2828703703703704,
|
||||
"obp": 0.4115740740740741,
|
||||
"slg": 0.5342592592592592
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def create_incomplete_team(session: Session):
|
||||
"""Factory method for creating an incomplete team for constraint testing."""
|
||||
return Team(
|
||||
id=446,
|
||||
abbrev='CLS',
|
||||
sname='Macho Men',
|
||||
lname='Columbus Macho Men',
|
||||
gmid=181818,
|
||||
gmname='Mason Socc',
|
||||
gsheet='asdf1234',
|
||||
wallet=6969,
|
||||
team_value=69420,
|
||||
collection_value=169420,
|
||||
color='https://i.postimg.cc/8kLZCYXh/S10CLS.png',
|
||||
season=7,
|
||||
event=False,
|
||||
career=0
|
||||
)
|
||||
|
||||
|
||||
def create_sample_play_for_scorebug(session: Session):
|
||||
"""Factory method for creating a play object for scorebug testing."""
|
||||
return Play(
|
||||
game_id=3,
|
||||
play_num=69,
|
||||
batter=session.get(Lineup, 1),
|
||||
batter_pos='DH',
|
||||
pitcher=session.get(Lineup, 20),
|
||||
catcher=session.get(Lineup, 11),
|
||||
starting_outs=1,
|
||||
inning_num=6
|
||||
)
|
||||
|
||||
|
||||
def get_next_play_id(session: Session):
|
||||
"""Get the next available Play ID for SQLite tests."""
|
||||
from sqlmodel import func
|
||||
max_id = session.exec(select(func.max(Play.id))).one()
|
||||
return (max_id or 0) + 1
|
||||
|
||||
@ -3,208 +3,10 @@ from sqlmodel import Session, select, func
|
||||
|
||||
from in_game.gameplay_models import Card
|
||||
from in_game.gameplay_queries import get_batter_scouting_or_none, get_card_or_none, get_pitcher_scouting_or_none
|
||||
from tests.factory import session_fixture
|
||||
|
||||
sample_ratings_query = {
|
||||
"count": 2,
|
||||
"ratings": [
|
||||
{
|
||||
"id": 7673,
|
||||
"battingcard": {
|
||||
"id": 3837,
|
||||
"player": {
|
||||
"player_id": 395,
|
||||
"p_name": "Cedric Mullins",
|
||||
"cost": 256,
|
||||
"image": "https://pd.manticorum.com/api/v2/players/395/battingcard?d=2023-11-19",
|
||||
"image2": None,
|
||||
"mlbclub": "Baltimore Orioles",
|
||||
"franchise": "Baltimore Orioles",
|
||||
"cardset": {
|
||||
"id": 1,
|
||||
"name": "2021 Season",
|
||||
"description": "Cards based on the full 2021 season",
|
||||
"event": None,
|
||||
"for_purchase": True,
|
||||
"total_cards": 791,
|
||||
"in_packs": True,
|
||||
"ranked_legal": False
|
||||
},
|
||||
"set_num": 395,
|
||||
"rarity": {
|
||||
"id": 2,
|
||||
"value": 3,
|
||||
"name": "All-Star",
|
||||
"color": "FFD700"
|
||||
},
|
||||
"pos_1": "CF",
|
||||
"pos_2": None,
|
||||
"pos_3": None,
|
||||
"pos_4": None,
|
||||
"pos_5": None,
|
||||
"pos_6": None,
|
||||
"pos_7": None,
|
||||
"pos_8": None,
|
||||
"headshot": "https://www.baseball-reference.com/req/202206291/images/headshots/2/24bf4355_mlbam.jpg",
|
||||
"vanity_card": None,
|
||||
"strat_code": "17929",
|
||||
"bbref_id": "mullice01",
|
||||
"fangr_id": None,
|
||||
"description": "2021",
|
||||
"quantity": 999,
|
||||
"mlbplayer": {
|
||||
"id": 396,
|
||||
"first_name": "Cedric",
|
||||
"last_name": "Mullins",
|
||||
"key_fangraphs": 17929,
|
||||
"key_bbref": "mullice01",
|
||||
"key_retro": "mullc002",
|
||||
"key_mlbam": 656775,
|
||||
"offense_col": 1
|
||||
}
|
||||
},
|
||||
"variant": 0,
|
||||
"steal_low": 9,
|
||||
"steal_high": 12,
|
||||
"steal_auto": True,
|
||||
"steal_jump": 0.2222222222222222,
|
||||
"bunting": "C",
|
||||
"hit_and_run": "B",
|
||||
"running": 13,
|
||||
"offense_col": 1,
|
||||
"hand": "L"
|
||||
},
|
||||
"vs_hand": "L",
|
||||
"pull_rate": 0.43888889,
|
||||
"center_rate": 0.32777778,
|
||||
"slap_rate": 0.23333333,
|
||||
"homerun": 2.25,
|
||||
"bp_homerun": 5.0,
|
||||
"triple": 0.0,
|
||||
"double_three": 0.0,
|
||||
"double_two": 2.4,
|
||||
"double_pull": 7.2,
|
||||
"single_two": 4.6,
|
||||
"single_one": 3.5,
|
||||
"single_center": 5.85,
|
||||
"bp_single": 5.0,
|
||||
"hbp": 2.0,
|
||||
"walk": 9.0,
|
||||
"strikeout": 23.0,
|
||||
"lineout": 3.0,
|
||||
"popout": 6.0,
|
||||
"flyout_a": 0.0,
|
||||
"flyout_bq": 0.15,
|
||||
"flyout_lf_b": 2.8,
|
||||
"flyout_rf_b": 3.75,
|
||||
"groundout_a": 0.0,
|
||||
"groundout_b": 9.0,
|
||||
"groundout_c": 13.5,
|
||||
"avg": 0.2851851851851852,
|
||||
"obp": 0.387037037037037,
|
||||
"slg": 0.5060185185185185
|
||||
},
|
||||
{
|
||||
"id": 7674,
|
||||
"battingcard": {
|
||||
"id": 3837,
|
||||
"player": {
|
||||
"player_id": 395,
|
||||
"p_name": "Cedric Mullins",
|
||||
"cost": 256,
|
||||
"image": "https://pd.manticorum.com/api/v2/players/395/battingcard?d=2023-11-19",
|
||||
"image2": None,
|
||||
"mlbclub": "Baltimore Orioles",
|
||||
"franchise": "Baltimore Orioles",
|
||||
"cardset": {
|
||||
"id": 1,
|
||||
"name": "2021 Season",
|
||||
"description": "Cards based on the full 2021 season",
|
||||
"event": None,
|
||||
"for_purchase": True,
|
||||
"total_cards": 791,
|
||||
"in_packs": True,
|
||||
"ranked_legal": False
|
||||
},
|
||||
"set_num": 395,
|
||||
"rarity": {
|
||||
"id": 2,
|
||||
"value": 3,
|
||||
"name": "All-Star",
|
||||
"color": "FFD700"
|
||||
},
|
||||
"pos_1": "CF",
|
||||
"pos_2": None,
|
||||
"pos_3": None,
|
||||
"pos_4": None,
|
||||
"pos_5": None,
|
||||
"pos_6": None,
|
||||
"pos_7": None,
|
||||
"pos_8": None,
|
||||
"headshot": "https://www.baseball-reference.com/req/202206291/images/headshots/2/24bf4355_mlbam.jpg",
|
||||
"vanity_card": None,
|
||||
"strat_code": "17929",
|
||||
"bbref_id": "mullice01",
|
||||
"fangr_id": None,
|
||||
"description": "2021",
|
||||
"quantity": 999,
|
||||
"mlbplayer": {
|
||||
"id": 396,
|
||||
"first_name": "Cedric",
|
||||
"last_name": "Mullins",
|
||||
"key_fangraphs": 17929,
|
||||
"key_bbref": "mullice01",
|
||||
"key_retro": "mullc002",
|
||||
"key_mlbam": 656775,
|
||||
"offense_col": 1
|
||||
}
|
||||
},
|
||||
"variant": 0,
|
||||
"steal_low": 9,
|
||||
"steal_high": 12,
|
||||
"steal_auto": True,
|
||||
"steal_jump": 0.2222222222222222,
|
||||
"bunting": "C",
|
||||
"hit_and_run": "B",
|
||||
"running": 13,
|
||||
"offense_col": 1,
|
||||
"hand": "L"
|
||||
},
|
||||
"vs_hand": "R",
|
||||
"pull_rate": 0.43377483,
|
||||
"center_rate": 0.32119205,
|
||||
"slap_rate": 0.24503311,
|
||||
"homerun": 2.0,
|
||||
"bp_homerun": 7.0,
|
||||
"triple": 0.0,
|
||||
"double_three": 0.0,
|
||||
"double_two": 2.7,
|
||||
"double_pull": 7.95,
|
||||
"single_two": 3.3,
|
||||
"single_one": 0.0,
|
||||
"single_center": 8.6,
|
||||
"bp_single": 5.0,
|
||||
"hbp": 1.0,
|
||||
"walk": 12.9,
|
||||
"strikeout": 11.1,
|
||||
"lineout": 8.0,
|
||||
"popout": 11.0,
|
||||
"flyout_a": 0.0,
|
||||
"flyout_bq": 0.4,
|
||||
"flyout_lf_b": 4.05,
|
||||
"flyout_rf_b": 6.0,
|
||||
"groundout_a": 0.0,
|
||||
"groundout_b": 3.0,
|
||||
"groundout_c": 14.0,
|
||||
"avg": 0.2828703703703704,
|
||||
"obp": 0.4115740740740741,
|
||||
"slg": 0.5342592592592592
|
||||
}
|
||||
]
|
||||
}
|
||||
from tests.factory import session_fixture, sample_ratings_query
|
||||
|
||||
|
||||
# async def test_create_scouting(session: Session):
|
||||
# async def test_create_scouting(session: Session, sample_ratings_query):
|
||||
# this_card = await get_card_or_none(session, card_id=1405)
|
||||
|
||||
# assert this_card.player.id == 395
|
||||
|
||||
@ -9,7 +9,7 @@ from tests.factory import session_fixture
|
||||
def test_create_card(session: Session):
|
||||
all_cards = session.exec(select(Card)).all()
|
||||
|
||||
assert len(all_cards) == 41
|
||||
assert len(all_cards) == 42 # Updated count after adding Player 41
|
||||
|
||||
card_1 = session.get(Card, 1)
|
||||
card_2 = session.get(Card, 12)
|
||||
@ -39,7 +39,7 @@ async def test_get_or_create_ai_card(session: Session):
|
||||
dev_mode=True
|
||||
)
|
||||
|
||||
assert new_card_2.id == 42
|
||||
assert new_card_2.id == 71 # Updated to match next available ID after factory cards (1-70)
|
||||
|
||||
|
||||
# async def test_get_card_or_none(session: Session):
|
||||
|
||||
@ -4,7 +4,7 @@ from sqlalchemy.sql.functions import sum, count
|
||||
from sqlmodel import Session, delete, func
|
||||
|
||||
from command_logic.logic_gameplay import complete_play, get_scorebug_embed, homeruns, is_game_over, singles, strikeouts, undo_play
|
||||
from in_game.gameplay_models import Game, Lineup, GameCardsetLink, Play, Team, select
|
||||
from in_game.gameplay_models import Game, Lineup, GameCardsetLink, Play, Team, RosterLink, select
|
||||
from in_game.gameplay_queries import get_channel_game_or_none, get_active_games_by_team, get_db_ready_decisions
|
||||
from tests.factory import session_fixture
|
||||
|
||||
@ -43,7 +43,13 @@ def test_select_all(session: Session):
|
||||
games = session.exec(select(Game)).all()
|
||||
assert len(games) == 3
|
||||
|
||||
session.execute(sadelete(Game))
|
||||
# Delete dependent tables first to avoid foreign key constraint violations
|
||||
# Order matters: delete child tables before parent tables
|
||||
session.execute(sadelete(Play)) # References Game
|
||||
session.execute(sadelete(Lineup)) # References Game
|
||||
session.execute(sadelete(RosterLink)) # References Game
|
||||
session.execute(sadelete(GameCardsetLink)) # References Game
|
||||
session.execute(sadelete(Game)) # Finally delete Game
|
||||
session.commit()
|
||||
|
||||
games = session.exec(select(Game)).all()
|
||||
|
||||
@ -27,12 +27,12 @@ def test_check_jump(session: Session):
|
||||
this_play.on_first = runner
|
||||
|
||||
assert this_play.starting_outs == 1
|
||||
assert balanced_ai.check_jump(session, this_game, to_base=2) == JumpResponse(min_safe=16)
|
||||
assert aggressive_ai.check_jump(session, this_game, to_base=2) == JumpResponse(min_safe=13, run_if_auto_jump=True)
|
||||
assert balanced_ai.check_jump(session, this_game, to_base=2) == JumpResponse(ai_note='- SEND **Player 4** to second if they get the jump', min_safe=16)
|
||||
assert aggressive_ai.check_jump(session, this_game, to_base=2) == JumpResponse(ai_note='- SEND **Player 4** to second if they get the jump', min_safe=13, run_if_auto_jump=True)
|
||||
|
||||
this_play.on_third = runner
|
||||
|
||||
assert balanced_ai.check_jump(session, this_game, to_base=4) == JumpResponse(min_safe=None)
|
||||
assert balanced_ai.check_jump(session, this_game, to_base=4) == JumpResponse(min_safe=8)
|
||||
assert aggressive_ai.check_jump(session, this_game, to_base=4) == JumpResponse(min_safe=5)
|
||||
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ from command_logic.logic_gameplay import complete_play, homeruns, is_game_over,
|
||||
from db_calls_gameplay import advance_runners
|
||||
from in_game.gameplay_models import Lineup, Play, Game
|
||||
from in_game.gameplay_queries import get_db_ready_plays, get_last_team_play
|
||||
from tests.factory import session_fixture
|
||||
from tests.factory import session_fixture, create_sample_play_for_scorebug
|
||||
|
||||
|
||||
def test_create_play(session: Session):
|
||||
@ -28,16 +28,7 @@ def test_get_current_play(session: Session):
|
||||
|
||||
|
||||
def test_scorebug_ascii(session: Session):
|
||||
new_play = Play(
|
||||
game_id=3,
|
||||
play_num=69,
|
||||
batter=session.get(Lineup, 1),
|
||||
batter_pos='DH',
|
||||
pitcher=session.get(Lineup, 20),
|
||||
catcher=session.get(Lineup, 11),
|
||||
starting_outs=1,
|
||||
inning_num=6
|
||||
)
|
||||
new_play = create_sample_play_for_scorebug(session)
|
||||
session.add(new_play)
|
||||
session.commit()
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ from tests.factory import session_fixture
|
||||
def test_create_player(session: Session):
|
||||
all_players = session.exec(select(Player)).all()
|
||||
|
||||
assert len(all_players) == 42
|
||||
assert len(all_players) == 43 # Updated count after adding Player 41
|
||||
|
||||
player_1 = session.get(Player, 1)
|
||||
player_2 = session.get(Player, 12)
|
||||
|
||||
@ -22,11 +22,11 @@ def test_get_available_subs(session: Session):
|
||||
home_team = this_game.home_team
|
||||
home_roster = session.exec(select(RosterLink).where(RosterLink.game == this_game, RosterLink.team == home_team)).all()
|
||||
|
||||
assert len(home_roster) == 11
|
||||
assert len(home_roster) == 12 # Updated count after adding more cards to factory
|
||||
|
||||
cards = get_available_subs(session, this_game, this_game.home_team)
|
||||
|
||||
assert len(cards) == 1
|
||||
assert len(cards) == 2 # Updated count: cards 41 and 70 are available subs
|
||||
|
||||
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ from sqlmodel import Session, select
|
||||
|
||||
from in_game.gameplay_models import Team, CACHE_LIMIT
|
||||
from in_game.gameplay_queries import get_team_or_none
|
||||
from tests.factory import session_fixture, pytest
|
||||
from tests.factory import session_fixture, create_incomplete_team, pytest
|
||||
|
||||
def test_create_team(session: Session):
|
||||
team_31 = session.get(Team, 31)
|
||||
@ -20,16 +20,17 @@ def test_create_team(session: Session):
|
||||
|
||||
|
||||
def test_create_incomplete_team(session: Session):
|
||||
team_1 = Team(
|
||||
id=446, abbrev='CLS', sname='Macho Men', lname='Columbus Macho Men', gmid=181818, gmname='Mason Socc', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='https://i.postimg.cc/8kLZCYXh/S10CLS.png', season=7, event=False, career=0
|
||||
)
|
||||
team_1 = create_incomplete_team(session)
|
||||
|
||||
session.add(team_1)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
with pytest.raises(IntegrityError) as exc_info:
|
||||
session.commit()
|
||||
|
||||
assert str(exc_info) == "<ExceptionInfo IntegrityError('(sqlite3.IntegrityError) NOT NULL constraint failed: team.ranking') tblen=24>"
|
||||
# Check that it's a NOT NULL constraint violation for ranking field (database agnostic)
|
||||
assert "ranking" in str(exc_info.value)
|
||||
assert "null" in str(exc_info.value).lower()
|
||||
|
||||
|
||||
async def test_team_cache(session: Session):
|
||||
|
||||
175
tests/in_game/test_gameplay_queries.py
Normal file
175
tests/in_game/test_gameplay_queries.py
Normal file
@ -0,0 +1,175 @@
|
||||
import pytest
|
||||
from sqlmodel import Session
|
||||
|
||||
from in_game.gameplay_queries import (
|
||||
get_batting_team, get_games_by_channel, get_channel_game_or_none,
|
||||
get_active_games_by_team, get_player_id_from_dict, get_player_name_from_dict,
|
||||
get_game_lineups
|
||||
)
|
||||
from in_game.gameplay_models import Game, Team, Play
|
||||
from tests.factory import session_fixture
|
||||
|
||||
|
||||
class TestUtilityFunctions:
|
||||
"""Test simple utility functions in gameplay_queries."""
|
||||
|
||||
def test_get_player_id_from_dict_with_player_id(self):
|
||||
"""Test extracting player_id from dict with player_id key."""
|
||||
json_data = {'player_id': 123, 'name': 'Test Player'}
|
||||
result = get_player_id_from_dict(json_data)
|
||||
assert result == 123
|
||||
|
||||
def test_get_player_id_from_dict_with_id(self):
|
||||
"""Test extracting player_id from dict with id key."""
|
||||
json_data = {'id': 456, 'name': 'Test Player'}
|
||||
result = get_player_id_from_dict(json_data)
|
||||
assert result == 456
|
||||
|
||||
def test_get_player_id_from_dict_missing_keys(self):
|
||||
"""Test error when both player_id and id keys are missing."""
|
||||
json_data = {'name': 'Test Player', 'team': 'Test Team'}
|
||||
|
||||
with pytest.raises(KeyError) as exc_info:
|
||||
get_player_id_from_dict(json_data)
|
||||
|
||||
assert 'Player ID could not be extracted from json data' in str(exc_info.value)
|
||||
|
||||
def test_get_player_name_from_dict_with_p_name(self):
|
||||
"""Test extracting player name from dict with p_name key."""
|
||||
json_data = {'p_name': 'Test Player', 'id': 123}
|
||||
result = get_player_name_from_dict(json_data)
|
||||
assert result == 'Test Player'
|
||||
|
||||
def test_get_player_name_from_dict_with_name(self):
|
||||
"""Test extracting player name from dict with name key."""
|
||||
json_data = {'name': 'Another Player', 'id': 456}
|
||||
result = get_player_name_from_dict(json_data)
|
||||
assert result == 'Another Player'
|
||||
|
||||
def test_get_player_name_from_dict_missing_keys(self):
|
||||
"""Test error when both p_name and name keys are missing."""
|
||||
json_data = {'id': 123, 'team': 'Test Team'}
|
||||
|
||||
with pytest.raises(KeyError) as exc_info:
|
||||
get_player_name_from_dict(json_data)
|
||||
|
||||
assert 'Player name could not be extracted from json data' in str(exc_info.value)
|
||||
|
||||
|
||||
class TestGameQueries:
|
||||
"""Test game-related query functions."""
|
||||
|
||||
def test_get_batting_team_top_inning(self, session: Session):
|
||||
"""Test getting batting team in top half of inning (away team bats)."""
|
||||
this_game = session.get(Game, 3)
|
||||
play_1 = this_game.initialize_play(session)
|
||||
|
||||
# Top half = away team bats
|
||||
play_1.inning_half = 'top'
|
||||
batting_team = get_batting_team(session, play_1)
|
||||
|
||||
assert batting_team == this_game.away_team
|
||||
assert batting_team.id == this_game.away_team_id
|
||||
|
||||
def test_get_batting_team_bottom_inning(self, session: Session):
|
||||
"""Test getting batting team in bottom half of inning (home team bats)."""
|
||||
this_game = session.get(Game, 3)
|
||||
play_1 = this_game.initialize_play(session)
|
||||
|
||||
# Bottom half = home team bats
|
||||
play_1.inning_half = 'bottom'
|
||||
batting_team = get_batting_team(session, play_1)
|
||||
|
||||
assert batting_team == this_game.home_team
|
||||
assert batting_team.id == this_game.home_team_id
|
||||
|
||||
def test_get_games_by_channel(self, session: Session):
|
||||
"""Test getting games by channel ID."""
|
||||
# Game 3 uses channel 5678 and is active
|
||||
games = get_games_by_channel(session, channel_id=5678)
|
||||
|
||||
assert len(games) == 1
|
||||
assert games[0].id == 3
|
||||
assert games[0].channel_id == 5678
|
||||
assert games[0].active is True
|
||||
|
||||
def test_get_games_by_channel_no_games(self, session: Session):
|
||||
"""Test getting games by channel ID when no games exist."""
|
||||
games = get_games_by_channel(session, channel_id=99999)
|
||||
|
||||
assert len(games) == 0
|
||||
assert games == []
|
||||
|
||||
def test_get_channel_game_or_none_single_game(self, session: Session):
|
||||
"""Test getting single game from channel."""
|
||||
# Channel 5678 has exactly one active game
|
||||
game = get_channel_game_or_none(session, channel_id=5678)
|
||||
|
||||
assert game is not None
|
||||
assert game.id == 3
|
||||
assert game.channel_id == 5678
|
||||
|
||||
def test_get_channel_game_or_none_no_games(self, session: Session):
|
||||
"""Test getting game from channel with no games."""
|
||||
game = get_channel_game_or_none(session, channel_id=99999)
|
||||
|
||||
assert game is None
|
||||
|
||||
def test_get_active_games_by_team(self, session: Session):
|
||||
"""Test getting active games for a specific team."""
|
||||
# Team 420 is in game 3 (active) and game 2 (inactive)
|
||||
team_420 = session.get(Team, 420)
|
||||
games = get_active_games_by_team(session, team_420)
|
||||
|
||||
# Should only return active games
|
||||
assert len(games) == 1
|
||||
assert games[0].id == 3
|
||||
assert games[0].active is True
|
||||
|
||||
# Verify team is in the game
|
||||
assert games[0].home_team_id == 420 or games[0].away_team_id == 420
|
||||
|
||||
def test_get_active_games_by_team_no_active_games(self, session: Session):
|
||||
"""Test getting active games for team with no active games."""
|
||||
# Team 31 is only in game 1 which is active, but let's test with team not in any active games
|
||||
team_3 = session.get(Team, 3)
|
||||
games = get_active_games_by_team(session, team_3)
|
||||
|
||||
# Team 3 (BAL) is not in any active games in our factory
|
||||
assert len(games) == 0
|
||||
|
||||
def test_get_game_lineups_all_lineups(self, session: Session):
|
||||
"""Test getting all lineups for a game."""
|
||||
this_game = session.get(Game, 3)
|
||||
lineups = get_game_lineups(session, this_game)
|
||||
|
||||
# Game 3 should have lineups from both teams
|
||||
assert len(lineups) > 0
|
||||
|
||||
# All lineups should belong to this game
|
||||
for lineup in lineups:
|
||||
assert lineup.game_id == this_game.id
|
||||
|
||||
def test_get_game_lineups_specific_team(self, session: Session):
|
||||
"""Test getting lineups for a specific team in a game."""
|
||||
this_game = session.get(Game, 3)
|
||||
team_420 = session.get(Team, 420)
|
||||
|
||||
lineups = get_game_lineups(session, this_game, specific_team=team_420)
|
||||
|
||||
# All lineups should belong to the specified team
|
||||
for lineup in lineups:
|
||||
assert lineup.game_id == this_game.id
|
||||
assert lineup.team_id == team_420.id
|
||||
|
||||
def test_get_game_lineups_active_only(self, session: Session):
|
||||
"""Test getting only active lineups for a game."""
|
||||
this_game = session.get(Game, 3)
|
||||
|
||||
lineups = get_game_lineups(session, this_game, is_active=True)
|
||||
|
||||
# All lineups should be active
|
||||
for lineup in lineups:
|
||||
assert lineup.game_id == this_game.id
|
||||
# Note: The factory doesn't set active=False for any lineups,
|
||||
# but this tests the query logic
|
||||
261
tests/in_game/test_managerai_responses.py
Normal file
261
tests/in_game/test_managerai_responses.py
Normal file
@ -0,0 +1,261 @@
|
||||
import pytest
|
||||
from in_game.managerai_responses import (
|
||||
AiResponse, RunResponse, JumpResponse, TagResponse,
|
||||
UncappedRunResponse, ThrowResponse, DefenseResponse
|
||||
)
|
||||
|
||||
|
||||
class TestAiResponse:
|
||||
"""Test the base AiResponse class."""
|
||||
|
||||
def test_ai_response_creation(self):
|
||||
"""Test creating AiResponse with default values."""
|
||||
response = AiResponse()
|
||||
assert response.ai_note == ''
|
||||
|
||||
def test_ai_response_with_note(self):
|
||||
"""Test creating AiResponse with custom note."""
|
||||
response = AiResponse(ai_note='Test AI note')
|
||||
assert response.ai_note == 'Test AI note'
|
||||
|
||||
|
||||
class TestRunResponse:
|
||||
"""Test the RunResponse class."""
|
||||
|
||||
def test_run_response_defaults(self):
|
||||
"""Test RunResponse with default values."""
|
||||
response = RunResponse()
|
||||
assert response.ai_note == ''
|
||||
assert response.min_safe is None
|
||||
|
||||
def test_run_response_with_values(self):
|
||||
"""Test RunResponse with custom values."""
|
||||
response = RunResponse(ai_note='Send the runner', min_safe=15)
|
||||
assert response.ai_note == 'Send the runner'
|
||||
assert response.min_safe == 15
|
||||
|
||||
|
||||
class TestJumpResponse:
|
||||
"""Test the JumpResponse class."""
|
||||
|
||||
def test_jump_response_defaults(self):
|
||||
"""Test JumpResponse with default values."""
|
||||
response = JumpResponse()
|
||||
assert response.ai_note == ''
|
||||
assert response.min_safe is None
|
||||
assert response.must_auto_jump is False
|
||||
assert response.run_if_auto_jump is False
|
||||
|
||||
def test_jump_response_with_values(self):
|
||||
"""Test JumpResponse with custom values."""
|
||||
response = JumpResponse(
|
||||
ai_note='- SEND **Player** to second if they get the jump',
|
||||
min_safe=16,
|
||||
must_auto_jump=True,
|
||||
run_if_auto_jump=True
|
||||
)
|
||||
assert response.ai_note == '- SEND **Player** to second if they get the jump'
|
||||
assert response.min_safe == 16
|
||||
assert response.must_auto_jump is True
|
||||
assert response.run_if_auto_jump is True
|
||||
|
||||
|
||||
class TestTagResponse:
|
||||
"""Test the TagResponse class."""
|
||||
|
||||
def test_tag_response_defaults(self):
|
||||
"""Test TagResponse with default values."""
|
||||
response = TagResponse()
|
||||
assert response.ai_note == ''
|
||||
assert response.min_safe is None
|
||||
|
||||
def test_tag_response_with_values(self):
|
||||
"""Test TagResponse with custom values."""
|
||||
response = TagResponse(ai_note='Tag from second', min_safe=8)
|
||||
assert response.ai_note == 'Tag from second'
|
||||
assert response.min_safe == 8
|
||||
|
||||
|
||||
class TestUncappedRunResponse:
|
||||
"""Test the UncappedRunResponse class."""
|
||||
|
||||
def test_uncapped_run_response_defaults(self):
|
||||
"""Test UncappedRunResponse with default values."""
|
||||
response = UncappedRunResponse()
|
||||
assert response.ai_note == ''
|
||||
assert response.min_safe is None
|
||||
assert response.send_trail is False
|
||||
assert response.trail_min_safe == 10
|
||||
assert response.trail_min_safe_delta == 0
|
||||
|
||||
def test_uncapped_run_response_with_values(self):
|
||||
"""Test UncappedRunResponse with custom values."""
|
||||
response = UncappedRunResponse(
|
||||
ai_note='Uncapped advance situation',
|
||||
min_safe=12,
|
||||
send_trail=True,
|
||||
trail_min_safe=8,
|
||||
trail_min_safe_delta=2
|
||||
)
|
||||
assert response.ai_note == 'Uncapped advance situation'
|
||||
assert response.min_safe == 12
|
||||
assert response.send_trail is True
|
||||
assert response.trail_min_safe == 8
|
||||
assert response.trail_min_safe_delta == 2
|
||||
|
||||
|
||||
class TestThrowResponse:
|
||||
"""Test the ThrowResponse class."""
|
||||
|
||||
def test_throw_response_defaults(self):
|
||||
"""Test ThrowResponse with default values."""
|
||||
response = ThrowResponse()
|
||||
assert response.ai_note == ''
|
||||
assert response.cutoff is False
|
||||
assert response.at_lead_runner is True
|
||||
assert response.at_trail_runner is False
|
||||
assert response.trail_max_safe == 10
|
||||
assert response.trail_max_safe_delta == -6
|
||||
|
||||
def test_throw_response_with_values(self):
|
||||
"""Test ThrowResponse with custom values."""
|
||||
response = ThrowResponse(
|
||||
ai_note='Throw decision',
|
||||
cutoff=True,
|
||||
at_lead_runner=False,
|
||||
at_trail_runner=True,
|
||||
trail_max_safe=8,
|
||||
trail_max_safe_delta=-4
|
||||
)
|
||||
assert response.ai_note == 'Throw decision'
|
||||
assert response.cutoff is True
|
||||
assert response.at_lead_runner is False
|
||||
assert response.at_trail_runner is True
|
||||
assert response.trail_max_safe == 8
|
||||
assert response.trail_max_safe_delta == -4
|
||||
|
||||
|
||||
class TestDefenseResponse:
|
||||
"""Test the DefenseResponse class and its defender_in method."""
|
||||
|
||||
def test_defense_response_defaults(self):
|
||||
"""Test DefenseResponse with default values."""
|
||||
response = DefenseResponse()
|
||||
assert response.ai_note == ''
|
||||
assert response.hold_first is False
|
||||
assert response.hold_second is False
|
||||
assert response.hold_third is False
|
||||
assert response.outfield_in is False
|
||||
assert response.infield_in is False
|
||||
assert response.corners_in is False
|
||||
|
||||
def test_defense_response_with_values(self):
|
||||
"""Test DefenseResponse with custom values."""
|
||||
response = DefenseResponse(
|
||||
ai_note='Defensive positioning',
|
||||
hold_first=True,
|
||||
hold_second=False,
|
||||
hold_third=True,
|
||||
outfield_in=True,
|
||||
infield_in=False,
|
||||
corners_in=True
|
||||
)
|
||||
assert response.ai_note == 'Defensive positioning'
|
||||
assert response.hold_first is True
|
||||
assert response.hold_second is False
|
||||
assert response.hold_third is True
|
||||
assert response.outfield_in is True
|
||||
assert response.infield_in is False
|
||||
assert response.corners_in is True
|
||||
|
||||
def test_defender_in_infield_in(self):
|
||||
"""Test defender_in method with infield_in=True."""
|
||||
response = DefenseResponse(infield_in=True)
|
||||
|
||||
# Infield positions should return True
|
||||
assert response.defender_in('C') is True
|
||||
assert response.defender_in('1B') is True
|
||||
assert response.defender_in('2B') is True
|
||||
assert response.defender_in('3B') is True
|
||||
assert response.defender_in('SS') is True
|
||||
assert response.defender_in('P') is True
|
||||
|
||||
# Outfield positions should return False
|
||||
assert response.defender_in('LF') is False
|
||||
assert response.defender_in('CF') is False
|
||||
assert response.defender_in('RF') is False
|
||||
|
||||
def test_defender_in_corners_in(self):
|
||||
"""Test defender_in method with corners_in=True."""
|
||||
response = DefenseResponse(corners_in=True)
|
||||
|
||||
# Corner infield positions should return True
|
||||
assert response.defender_in('C') is True
|
||||
assert response.defender_in('1B') is True
|
||||
assert response.defender_in('3B') is True
|
||||
assert response.defender_in('P') is True
|
||||
|
||||
# Non-corner positions should return False
|
||||
assert response.defender_in('2B') is False
|
||||
assert response.defender_in('SS') is False
|
||||
assert response.defender_in('LF') is False
|
||||
assert response.defender_in('CF') is False
|
||||
assert response.defender_in('RF') is False
|
||||
|
||||
def test_defender_in_outfield_in(self):
|
||||
"""Test defender_in method with outfield_in=True."""
|
||||
response = DefenseResponse(outfield_in=True)
|
||||
|
||||
# Outfield positions should return True
|
||||
assert response.defender_in('LF') is True
|
||||
assert response.defender_in('CF') is True
|
||||
assert response.defender_in('RF') is True
|
||||
|
||||
# Infield positions should return False
|
||||
assert response.defender_in('C') is False
|
||||
assert response.defender_in('1B') is False
|
||||
assert response.defender_in('2B') is False
|
||||
assert response.defender_in('3B') is False
|
||||
assert response.defender_in('SS') is False
|
||||
assert response.defender_in('P') is False
|
||||
|
||||
def test_defender_in_multiple_flags(self):
|
||||
"""Test defender_in method with multiple flags set."""
|
||||
response = DefenseResponse(infield_in=True, outfield_in=True)
|
||||
|
||||
# All positions should return True when both flags are set
|
||||
positions = ['C', '1B', '2B', '3B', 'SS', 'P', 'LF', 'CF', 'RF']
|
||||
for position in positions:
|
||||
assert response.defender_in(position) is True
|
||||
|
||||
def test_defender_in_no_flags(self):
|
||||
"""Test defender_in method with no flags set."""
|
||||
response = DefenseResponse()
|
||||
|
||||
# All positions should return False when no flags are set
|
||||
positions = ['C', '1B', '2B', '3B', 'SS', 'P', 'LF', 'CF', 'RF']
|
||||
for position in positions:
|
||||
assert response.defender_in(position) is False
|
||||
|
||||
def test_defender_in_priority_order(self):
|
||||
"""Test that infield_in takes priority over corners_in for applicable positions."""
|
||||
response = DefenseResponse(infield_in=True, corners_in=True)
|
||||
|
||||
# Corner positions should still return True (both flags apply)
|
||||
assert response.defender_in('C') is True
|
||||
assert response.defender_in('1B') is True
|
||||
assert response.defender_in('3B') is True
|
||||
assert response.defender_in('P') is True
|
||||
|
||||
# Non-corner infield positions should return True (infield_in applies)
|
||||
assert response.defender_in('2B') is True
|
||||
assert response.defender_in('SS') is True
|
||||
|
||||
def test_defender_in_unknown_position(self):
|
||||
"""Test defender_in method with unknown position."""
|
||||
response = DefenseResponse(infield_in=True, outfield_in=True, corners_in=True)
|
||||
|
||||
# Unknown positions should return False
|
||||
assert response.defender_in('DH') is False
|
||||
assert response.defender_in('UNKNOWN') is False
|
||||
assert response.defender_in('') is False
|
||||
425
tests/in_game/test_simulations.py
Normal file
425
tests/in_game/test_simulations.py
Normal file
@ -0,0 +1,425 @@
|
||||
import pytest
|
||||
import random
|
||||
from unittest.mock import Mock, patch, call
|
||||
import discord
|
||||
|
||||
from in_game import simulations, data_cache
|
||||
from tests.factory import session_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_batting_wrapper():
|
||||
"""Create a mock BattingWrapper with test data."""
|
||||
batting_card = data_cache.BattingCard(
|
||||
player_id=123,
|
||||
variant=0,
|
||||
steal_low=9,
|
||||
steal_high=12,
|
||||
steal_auto=True,
|
||||
steal_jump=0.25,
|
||||
bunting='B',
|
||||
hit_and_run='A',
|
||||
running=13,
|
||||
offense_col=1,
|
||||
hand='R'
|
||||
)
|
||||
|
||||
ratings_vl = data_cache.BattingRatings(
|
||||
homerun=5.0,
|
||||
bp_homerun=7.0,
|
||||
triple=2.0,
|
||||
double_three=1.0,
|
||||
double_two=8.0,
|
||||
double_pull=6.0,
|
||||
single_two=4.0,
|
||||
single_one=3.0,
|
||||
single_center=12.0,
|
||||
bp_single=5.0,
|
||||
hbp=2.0,
|
||||
walk=10.0,
|
||||
strikeout=15.0,
|
||||
lineout=3.0,
|
||||
popout=6.0,
|
||||
flyout_a=1.0,
|
||||
flyout_bq=0.5,
|
||||
flyout_lf_b=4.0,
|
||||
flyout_rf_b=8.0,
|
||||
groundout_a=2.0,
|
||||
groundout_b=20.0,
|
||||
groundout_c=15.0,
|
||||
avg=0.285,
|
||||
obp=0.375,
|
||||
slg=0.455,
|
||||
pull_rate=0.42,
|
||||
center_rate=0.33,
|
||||
slap_rate=0.25
|
||||
)
|
||||
|
||||
ratings_vr = data_cache.BattingRatings(
|
||||
homerun=3.0,
|
||||
bp_homerun=6.0,
|
||||
triple=1.0,
|
||||
double_three=0.5,
|
||||
double_two=6.0,
|
||||
double_pull=5.0,
|
||||
single_two=3.5,
|
||||
single_one=2.5,
|
||||
single_center=10.0,
|
||||
bp_single=4.0,
|
||||
hbp=1.5,
|
||||
walk=8.0,
|
||||
strikeout=18.0,
|
||||
lineout=4.0,
|
||||
popout=7.0,
|
||||
flyout_a=1.5,
|
||||
flyout_bq=0.75,
|
||||
flyout_lf_b=5.0,
|
||||
flyout_rf_b=10.0,
|
||||
groundout_a=2.5,
|
||||
groundout_b=22.0,
|
||||
groundout_c=16.0,
|
||||
avg=0.265,
|
||||
obp=0.345,
|
||||
slg=0.425,
|
||||
pull_rate=0.45,
|
||||
center_rate=0.30,
|
||||
slap_rate=0.25
|
||||
)
|
||||
|
||||
return data_cache.BattingWrapper(
|
||||
card=batting_card,
|
||||
ratings_vl=ratings_vl,
|
||||
ratings_vr=ratings_vr
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pitching_wrapper():
|
||||
"""Create a mock PitchingWrapper with test data."""
|
||||
pitching_card = data_cache.PitchingCard(
|
||||
player_id=456,
|
||||
variant=0,
|
||||
balk=1,
|
||||
wild_pitch=2,
|
||||
hold=8,
|
||||
starter_rating=7,
|
||||
relief_rating=5,
|
||||
batting='B',
|
||||
offense_col=1,
|
||||
hand='L',
|
||||
closer_rating=6
|
||||
)
|
||||
|
||||
ratings_vl = data_cache.PitchingRatings(
|
||||
homerun=3.0,
|
||||
bp_homerun=5.0,
|
||||
triple=1.0,
|
||||
double_three=0.5,
|
||||
double_two=5.0,
|
||||
double_cf=4.0,
|
||||
single_two=3.0,
|
||||
single_one=2.0,
|
||||
single_center=8.0,
|
||||
bp_single=4.0,
|
||||
hbp=2.0,
|
||||
walk=12.0,
|
||||
strikeout=25.0,
|
||||
flyout_lf_b=5.0,
|
||||
flyout_cf_b=6.0,
|
||||
flyout_rf_b=6.0,
|
||||
groundout_a=3.0,
|
||||
groundout_b=18.0,
|
||||
xcheck_p=1.0,
|
||||
xcheck_c=1.0,
|
||||
xcheck_1b=1.0,
|
||||
xcheck_2b=1.0,
|
||||
xcheck_3b=1.0,
|
||||
xcheck_ss=1.0,
|
||||
xcheck_lf=1.0,
|
||||
xcheck_cf=1.0,
|
||||
xcheck_rf=1.0,
|
||||
avg=0.245,
|
||||
obp=0.335,
|
||||
slg=0.385
|
||||
)
|
||||
|
||||
ratings_vr = data_cache.PitchingRatings(
|
||||
homerun=4.0,
|
||||
bp_homerun=6.0,
|
||||
triple=1.5,
|
||||
double_three=0.75,
|
||||
double_two=7.0,
|
||||
double_cf=5.0,
|
||||
single_two=4.0,
|
||||
single_one=3.0,
|
||||
single_center=10.0,
|
||||
bp_single=5.0,
|
||||
hbp=1.5,
|
||||
walk=10.0,
|
||||
strikeout=22.0,
|
||||
flyout_lf_b=6.0,
|
||||
flyout_cf_b=7.0,
|
||||
flyout_rf_b=8.0,
|
||||
groundout_a=4.0,
|
||||
groundout_b=20.0,
|
||||
xcheck_p=1.5,
|
||||
xcheck_c=1.5,
|
||||
xcheck_1b=1.5,
|
||||
xcheck_2b=1.5,
|
||||
xcheck_3b=1.5,
|
||||
xcheck_ss=1.5,
|
||||
xcheck_lf=1.5,
|
||||
xcheck_cf=1.5,
|
||||
xcheck_rf=1.5,
|
||||
avg=0.255,
|
||||
obp=0.345,
|
||||
slg=0.405
|
||||
)
|
||||
|
||||
return data_cache.PitchingWrapper(
|
||||
card=pitching_card,
|
||||
ratings_vl=ratings_vl,
|
||||
ratings_vr=ratings_vr
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_player_data():
|
||||
"""Create mock player data for helper function calls."""
|
||||
return {
|
||||
'player_id': 123,
|
||||
'p_name': 'Test Player',
|
||||
'description': '2024',
|
||||
'image': 'test_player_pitchingcard',
|
||||
'image2': 'test_player_battingcard'
|
||||
}
|
||||
|
||||
|
||||
class TestGetResult:
|
||||
"""Test the get_result function which simulates plate appearance outcomes."""
|
||||
|
||||
@patch('random.choice')
|
||||
@patch('random.choices')
|
||||
def test_get_result_pitcher_chosen_left_handed_batter(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper):
|
||||
"""Test get_result when pitcher is chosen and batter is left-handed."""
|
||||
# Make the batter left-handed
|
||||
mock_batting_wrapper.card.hand = 'L'
|
||||
mock_choice.return_value = 'pitcher'
|
||||
mock_choices.return_value = ['strikeout']
|
||||
|
||||
result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper)
|
||||
|
||||
mock_choice.assert_called_once_with(['pitcher', 'batter'])
|
||||
assert result == 'strikeout'
|
||||
|
||||
# Verify the correct ratings were used (pitcher vs left-handed batter)
|
||||
call_args = mock_choices.call_args
|
||||
assert call_args is not None
|
||||
# Should use pitcher's ratings_vl when facing left-handed batter
|
||||
|
||||
@patch('random.choice')
|
||||
@patch('random.choices')
|
||||
def test_get_result_pitcher_chosen_right_handed_batter(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper):
|
||||
"""Test get_result when pitcher is chosen and batter is right-handed."""
|
||||
# Batter is already right-handed in fixture
|
||||
mock_choice.return_value = 'pitcher'
|
||||
mock_choices.return_value = ['groundout_b']
|
||||
|
||||
result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper)
|
||||
|
||||
mock_choice.assert_called_once_with(['pitcher', 'batter'])
|
||||
assert result == 'groundout_b'
|
||||
|
||||
@patch('random.choice')
|
||||
@patch('random.choices')
|
||||
def test_get_result_batter_chosen_left_handed_pitcher(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper):
|
||||
"""Test get_result when batter is chosen and pitcher is left-handed."""
|
||||
# Pitcher is already left-handed in fixture
|
||||
mock_choice.return_value = 'batter'
|
||||
mock_choices.return_value = ['homerun']
|
||||
|
||||
result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper)
|
||||
|
||||
mock_choice.assert_called_once_with(['pitcher', 'batter'])
|
||||
assert result == 'homerun'
|
||||
|
||||
@patch('random.choice')
|
||||
@patch('random.choices')
|
||||
def test_get_result_batter_chosen_right_handed_pitcher(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper):
|
||||
"""Test get_result when batter is chosen and pitcher is right-handed."""
|
||||
# Make the pitcher right-handed
|
||||
mock_pitching_wrapper.card.hand = 'R'
|
||||
mock_choice.return_value = 'batter'
|
||||
mock_choices.return_value = ['single_center']
|
||||
|
||||
result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper)
|
||||
|
||||
mock_choice.assert_called_once_with(['pitcher', 'batter'])
|
||||
assert result == 'single_center'
|
||||
|
||||
@patch('random.choice')
|
||||
@patch('random.choices')
|
||||
def test_get_result_unused_fields_filtered_out(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper):
|
||||
"""Test that unused statistical fields are filtered out from the random selection."""
|
||||
mock_choice.return_value = 'batter'
|
||||
mock_choices.return_value = ['walk']
|
||||
|
||||
result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper)
|
||||
|
||||
# Get the call arguments to verify unused fields were removed
|
||||
call_args = mock_choices.call_args
|
||||
results_list = call_args[0][0] # First positional argument (results)
|
||||
probs_list = call_args[0][1] # Second positional argument (probabilities)
|
||||
|
||||
# Verify unused fields are not in the results
|
||||
unused_fields = ['avg', 'obp', 'slg', 'pull_rate', 'center_rate', 'slap_rate']
|
||||
for field in unused_fields:
|
||||
assert field not in results_list
|
||||
|
||||
# Verify we have valid baseball outcomes
|
||||
assert len(results_list) == len(probs_list)
|
||||
assert all(isinstance(prob, (int, float)) for prob in probs_list)
|
||||
assert result == 'walk'
|
||||
|
||||
|
||||
class TestGetPosEmbeds:
|
||||
"""Test the get_pos_embeds function which creates Discord embeds for matchups."""
|
||||
|
||||
@patch('in_game.simulations.get_result')
|
||||
@patch('helpers.player_desc')
|
||||
@patch('helpers.player_pcard')
|
||||
@patch('helpers.player_bcard')
|
||||
@patch('helpers.SBA_COLOR', 'a6ce39')
|
||||
def test_get_pos_embeds_structure(self, mock_bcard, mock_pcard, mock_desc, mock_get_result,
|
||||
mock_pitching_wrapper, mock_batting_wrapper, mock_player_data):
|
||||
"""Test that get_pos_embeds returns the correct embed structure."""
|
||||
# Setup mocks
|
||||
mock_desc.side_effect = ['2024 Test Pitcher', '2024 Test Batter']
|
||||
mock_pcard.return_value = 'pitcher_card_url'
|
||||
mock_bcard.return_value = 'batter_card_url'
|
||||
mock_get_result.return_value = 'strikeout'
|
||||
|
||||
pitcher_data = {'p_name': 'Test Pitcher', 'description': '2024'}
|
||||
batter_data = {'p_name': 'Test Batter', 'description': '2024'}
|
||||
|
||||
embeds = simulations.get_pos_embeds(pitcher_data, batter_data, mock_pitching_wrapper, mock_batting_wrapper)
|
||||
|
||||
# Verify we get 3 embeds
|
||||
assert len(embeds) == 3
|
||||
assert all(isinstance(embed, discord.Embed) for embed in embeds)
|
||||
|
||||
# Check first embed (pitcher)
|
||||
assert embeds[0].title == '2024 Test Pitcher vs. 2024 Test Batter'
|
||||
assert embeds[0].image.url == 'pitcher_card_url'
|
||||
assert embeds[0].color.value == int('a6ce39', 16)
|
||||
|
||||
# Check second embed (batter)
|
||||
assert embeds[1].image.url == 'batter_card_url'
|
||||
assert embeds[1].color.value == int('a6ce39', 16)
|
||||
|
||||
# Check third embed (result)
|
||||
assert len(embeds[2].fields) == 3
|
||||
assert embeds[2].fields[0].name == 'Pitcher'
|
||||
assert embeds[2].fields[0].value == '2024 Test Pitcher'
|
||||
assert embeds[2].fields[1].name == 'Batter'
|
||||
assert embeds[2].fields[1].value == '2024 Test Batter'
|
||||
assert embeds[2].fields[2].name == 'Result'
|
||||
assert embeds[2].fields[2].value == 'strikeout'
|
||||
assert embeds[2].fields[2].inline is False
|
||||
|
||||
# Verify helper functions were called correctly
|
||||
mock_desc.assert_has_calls([call(pitcher_data), call(batter_data)])
|
||||
mock_pcard.assert_called_once_with(pitcher_data)
|
||||
mock_bcard.assert_called_once_with(batter_data)
|
||||
mock_get_result.assert_called_once_with(mock_pitching_wrapper, mock_batting_wrapper)
|
||||
|
||||
@patch('in_game.simulations.get_result')
|
||||
@patch('helpers.player_desc')
|
||||
@patch('helpers.player_pcard')
|
||||
@patch('helpers.player_bcard')
|
||||
@patch('helpers.SBA_COLOR', 'a6ce39')
|
||||
def test_get_pos_embeds_different_results(self, mock_bcard, mock_pcard, mock_desc, mock_get_result,
|
||||
mock_pitching_wrapper, mock_batting_wrapper, mock_player_data):
|
||||
"""Test get_pos_embeds with different simulation results."""
|
||||
mock_desc.side_effect = ['Live Ace Pitcher', 'Live Power Hitter']
|
||||
mock_pcard.return_value = 'ace_pitcher_card'
|
||||
mock_bcard.return_value = 'power_hitter_card'
|
||||
mock_get_result.return_value = 'homerun'
|
||||
|
||||
pitcher_data = {'p_name': 'Ace Pitcher', 'description': 'Live'}
|
||||
batter_data = {'p_name': 'Power Hitter', 'description': 'Live'}
|
||||
|
||||
embeds = simulations.get_pos_embeds(pitcher_data, batter_data, mock_pitching_wrapper, mock_batting_wrapper)
|
||||
|
||||
assert len(embeds) == 3
|
||||
assert embeds[0].title == 'Live Ace Pitcher vs. Live Power Hitter'
|
||||
assert embeds[2].fields[2].value == 'homerun'
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests that test the functions together with minimal mocking."""
|
||||
|
||||
def test_get_result_randomness_distribution(self, mock_pitching_wrapper, mock_batting_wrapper):
|
||||
"""Test that get_result produces a reasonable distribution of outcomes over many iterations."""
|
||||
# Run many simulations to test randomness
|
||||
results = []
|
||||
for _ in range(1000):
|
||||
result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper)
|
||||
results.append(result)
|
||||
|
||||
# Verify we got various outcomes (not just one result)
|
||||
unique_results = set(results)
|
||||
assert len(unique_results) > 1
|
||||
|
||||
# Verify all results are valid baseball outcomes
|
||||
valid_outcomes = {
|
||||
'homerun', 'triple', 'double_two', 'double_three', 'double_pull', 'double_cf',
|
||||
'single_center', 'single_one', 'single_two', 'bp_single', 'bp_homerun',
|
||||
'walk', 'hbp', 'strikeout', 'lineout', 'popout',
|
||||
'flyout_a', 'flyout_bq', 'flyout_lf_b', 'flyout_cf_b', 'flyout_rf_b',
|
||||
'groundout_a', 'groundout_b', 'groundout_c',
|
||||
'xcheck_p', 'xcheck_c', 'xcheck_1b', 'xcheck_2b', 'xcheck_3b', 'xcheck_ss',
|
||||
'xcheck_lf', 'xcheck_cf', 'xcheck_rf'
|
||||
}
|
||||
|
||||
for result in unique_results:
|
||||
assert result in valid_outcomes
|
||||
|
||||
@patch('helpers.SBA_COLOR', 'a6ce39')
|
||||
def test_full_simulation_flow(self, mock_pitching_wrapper, mock_batting_wrapper):
|
||||
"""Test the complete flow from player data to Discord embeds."""
|
||||
pitcher_data = {
|
||||
'p_name': 'Test Pitcher',
|
||||
'description': '2024',
|
||||
'image': 'test_pitcher_pitchingcard',
|
||||
'image2': None
|
||||
}
|
||||
batter_data = {
|
||||
'p_name': 'Test Batter',
|
||||
'description': '2024',
|
||||
'image': None,
|
||||
'image2': 'test_batter_battingcard'
|
||||
}
|
||||
|
||||
# This will call the actual helper functions, so we need valid player data
|
||||
with patch('helpers.player_desc') as mock_desc, \
|
||||
patch('helpers.player_pcard') as mock_pcard, \
|
||||
patch('helpers.player_bcard') as mock_bcard:
|
||||
|
||||
mock_desc.side_effect = ['2024 Test Pitcher', '2024 Test Batter']
|
||||
mock_pcard.return_value = 'test_pitcher_pitchingcard'
|
||||
mock_bcard.return_value = 'test_batter_battingcard'
|
||||
|
||||
embeds = simulations.get_pos_embeds(pitcher_data, batter_data, mock_pitching_wrapper, mock_batting_wrapper)
|
||||
|
||||
# Verify the complete embed structure
|
||||
assert len(embeds) == 3
|
||||
assert embeds[0].title == '2024 Test Pitcher vs. 2024 Test Batter'
|
||||
assert embeds[0].image.url == 'test_pitcher_pitchingcard'
|
||||
assert embeds[1].image.url == 'test_batter_battingcard'
|
||||
|
||||
# The result should be one of many possible baseball outcomes
|
||||
result_field = embeds[2].fields[2]
|
||||
assert result_field.name == 'Result'
|
||||
assert isinstance(result_field.value, str)
|
||||
assert len(result_field.value) > 0
|
||||
164
tests/test_api_calls.py
Normal file
164
tests/test_api_calls.py
Normal file
@ -0,0 +1,164 @@
|
||||
import pytest
|
||||
import aiohttp
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
from exceptions import DatabaseError
|
||||
import api_calls
|
||||
|
||||
|
||||
class TestUtilityFunctions:
|
||||
"""Test utility functions in api_calls."""
|
||||
|
||||
def test_param_char_with_params(self):
|
||||
"""Test param_char returns & when other_params is truthy."""
|
||||
assert api_calls.param_char(True) == '&'
|
||||
assert api_calls.param_char(['param1']) == '&'
|
||||
assert api_calls.param_char({'key': 'value'}) == '&'
|
||||
assert api_calls.param_char('some_param') == '&'
|
||||
|
||||
def test_param_char_without_params(self):
|
||||
"""Test param_char returns ? when other_params is falsy."""
|
||||
assert api_calls.param_char(False) == '?'
|
||||
assert api_calls.param_char(None) == '?'
|
||||
assert api_calls.param_char([]) == '?'
|
||||
assert api_calls.param_char({}) == '?'
|
||||
assert api_calls.param_char('') == '?'
|
||||
assert api_calls.param_char(0) == '?'
|
||||
|
||||
@patch('api_calls.DB_URL', 'https://test.example.com/api')
|
||||
def test_get_req_url_basic(self):
|
||||
"""Test basic URL generation without object_id or params."""
|
||||
result = api_calls.get_req_url('teams')
|
||||
expected = 'https://test.example.com/api/v2/teams'
|
||||
assert result == expected
|
||||
|
||||
@patch('api_calls.DB_URL', 'https://test.example.com/api')
|
||||
def test_get_req_url_with_version(self):
|
||||
"""Test URL generation with custom API version."""
|
||||
result = api_calls.get_req_url('teams', api_ver=1)
|
||||
expected = 'https://test.example.com/api/v1/teams'
|
||||
assert result == expected
|
||||
|
||||
@patch('api_calls.DB_URL', 'https://test.example.com/api')
|
||||
def test_get_req_url_with_object_id(self):
|
||||
"""Test URL generation with object_id."""
|
||||
result = api_calls.get_req_url('teams', object_id=123)
|
||||
expected = 'https://test.example.com/api/v2/teams/123'
|
||||
assert result == expected
|
||||
|
||||
@patch('api_calls.DB_URL', 'https://test.example.com/api')
|
||||
def test_get_req_url_with_params(self):
|
||||
"""Test URL generation with parameters."""
|
||||
params = [('season', '7'), ('active', 'true')]
|
||||
result = api_calls.get_req_url('teams', params=params)
|
||||
expected = 'https://test.example.com/api/v2/teams?season=7&active=true'
|
||||
assert result == expected
|
||||
|
||||
@patch('api_calls.DB_URL', 'https://test.example.com/api')
|
||||
def test_get_req_url_complete(self):
|
||||
"""Test URL generation with all parameters."""
|
||||
params = [('season', '7'), ('limit', '10')]
|
||||
result = api_calls.get_req_url('games', api_ver=1, object_id=456, params=params)
|
||||
expected = 'https://test.example.com/api/v1/games/456?season=7&limit=10'
|
||||
assert result == expected
|
||||
|
||||
@patch('api_calls.logger')
|
||||
def test_log_return_value_short_string(self, mock_logger):
|
||||
"""Test logging short return values."""
|
||||
api_calls.log_return_value('Short log message')
|
||||
mock_logger.info.assert_called_once_with('\n\nreturn: Short log message')
|
||||
|
||||
@patch('api_calls.logger')
|
||||
def test_log_return_value_long_string(self, mock_logger):
|
||||
"""Test logging long return values that get chunked."""
|
||||
long_string = 'A' * 5000 # 5000 character string
|
||||
api_calls.log_return_value(long_string)
|
||||
|
||||
# Should have been called twice (first chunk + second chunk)
|
||||
assert mock_logger.info.call_count == 2
|
||||
# First call should include the "return:" prefix
|
||||
assert '\n\nreturn: ' in mock_logger.info.call_args_list[0][0][0]
|
||||
|
||||
@patch('api_calls.logger')
|
||||
def test_log_return_value_extremely_long_string(self, mock_logger):
|
||||
"""Test logging extremely long return values that get snipped."""
|
||||
extremely_long_string = 'B' * 400000 # 400k character string (exceeds 300k limit)
|
||||
api_calls.log_return_value(extremely_long_string)
|
||||
|
||||
# Should warn about snipping
|
||||
mock_logger.warning.assert_called_with('[ S N I P P E D ]')
|
||||
|
||||
def test_team_hash(self):
|
||||
"""Test team hash generation."""
|
||||
mock_team = {
|
||||
'sname': 'TestTeam',
|
||||
'gmid': 1234567
|
||||
}
|
||||
|
||||
result = api_calls.team_hash(mock_team)
|
||||
# Expected format: last char + gmid/6950123 + second-to-last char + gmid/42069123
|
||||
expected = f'm{1234567 / 6950123:.0f}a{1234567 / 42069123:.0f}'
|
||||
assert result == expected
|
||||
|
||||
|
||||
# Note: Async database function tests are complex due to aiohttp mocking
|
||||
# For now, focusing on utility functions which provide significant coverage improvement
|
||||
|
||||
|
||||
class TestSpecificFunctions:
|
||||
"""Test specific API wrapper functions."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('api_calls.db_get')
|
||||
async def test_get_team_by_abbrev_found(self, mock_db_get):
|
||||
"""Test get_team_by_abbrev function when team is found."""
|
||||
mock_db_get.return_value = {
|
||||
'count': 1,
|
||||
'teams': [{'id': 123, 'abbrev': 'TEST', 'name': 'Test Team'}]
|
||||
}
|
||||
|
||||
result = await api_calls.get_team_by_abbrev('TEST')
|
||||
|
||||
assert result == {'id': 123, 'abbrev': 'TEST', 'name': 'Test Team'}
|
||||
mock_db_get.assert_called_once_with('teams', params=[('abbrev', 'TEST')])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('api_calls.db_get')
|
||||
async def test_get_team_by_abbrev_not_found(self, mock_db_get):
|
||||
"""Test get_team_by_abbrev function when team is not found."""
|
||||
mock_db_get.return_value = {
|
||||
'count': 0,
|
||||
'teams': []
|
||||
}
|
||||
|
||||
result = await api_calls.get_team_by_abbrev('NONEXISTENT')
|
||||
|
||||
assert result is None
|
||||
mock_db_get.assert_called_once_with('teams', params=[('abbrev', 'NONEXISTENT')])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('api_calls.db_post')
|
||||
async def test_post_to_dex(self, mock_db_post):
|
||||
"""Test post_to_dex function."""
|
||||
mock_db_post.return_value = {'id': 456, 'posted': True}
|
||||
|
||||
mock_player = {'id': 123}
|
||||
mock_team = {'id': 456}
|
||||
|
||||
result = await api_calls.post_to_dex(mock_player, mock_team)
|
||||
|
||||
assert result == {'id': 456, 'posted': True}
|
||||
mock_db_post.assert_called_once_with('paperdex', payload={'player_id': 123, 'team_id': 456})
|
||||
|
||||
|
||||
class TestEnvironmentConfiguration:
|
||||
"""Test environment-based configuration."""
|
||||
|
||||
def test_db_url_exists(self):
|
||||
"""Test that DB_URL is configured."""
|
||||
assert api_calls.DB_URL is not None
|
||||
assert 'manticorum.com' in api_calls.DB_URL
|
||||
|
||||
def test_auth_token_exists(self):
|
||||
"""Test that AUTH_TOKEN is configured."""
|
||||
assert api_calls.AUTH_TOKEN is not None
|
||||
assert 'Authorization' in api_calls.AUTH_TOKEN
|
||||
@ -453,9 +453,12 @@ class SelectBatterSub(discord.ui.Select):
|
||||
position = 'PR'
|
||||
|
||||
logger.info(f'Deactivating last_lineup')
|
||||
last_lineup.active = False
|
||||
self.session.add(last_lineup)
|
||||
logger.info(f'Set {last_lineup.card.player.name_with_desc} as inactive')
|
||||
try:
|
||||
last_lineup.active = False
|
||||
self.session.add(last_lineup)
|
||||
logger.info(f'Set {last_lineup.card.player.name_with_desc} as inactive')
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
|
||||
logger.info(f'new position: {position}')
|
||||
if position not in ['DH', 'PR', 'PH']:
|
||||
@ -489,6 +492,15 @@ class SelectBatterSub(discord.ui.Select):
|
||||
logger.info(f'Setting new sub to current play batter')
|
||||
this_play.batter = human_bat_lineup
|
||||
this_play.batter_pos = position
|
||||
elif this_play.on_first == last_lineup:
|
||||
logger.info(f'Setting new sub to run at first')
|
||||
this_play.on_first = human_bat_lineup
|
||||
elif this_play.on_second == last_lineup:
|
||||
logger.info(f'Setting new sub to run at second')
|
||||
this_play.on_second = human_bat_lineup
|
||||
elif this_play.on_third == last_lineup:
|
||||
logger.info(f'Setting new sub to run at third')
|
||||
this_play.on_third = human_bat_lineup
|
||||
|
||||
logger.info(f'Adding play to session: {this_play}')
|
||||
self.session.add(this_play)
|
||||
|
||||
104
utils.py
Normal file
104
utils.py
Normal file
@ -0,0 +1,104 @@
|
||||
"""
|
||||
General Utilities
|
||||
|
||||
This module contains standalone utility functions with minimal dependencies,
|
||||
including timestamp conversion, position abbreviations, and simple helpers.
|
||||
"""
|
||||
import datetime
|
||||
import discord
|
||||
|
||||
|
||||
def int_timestamp():
|
||||
"""Convert current datetime to integer timestamp."""
|
||||
return int(datetime.datetime.now().timestamp())
|
||||
|
||||
|
||||
def get_pos_abbrev(field_pos: str) -> str:
|
||||
"""Convert position name to standard abbreviation."""
|
||||
if field_pos.lower() == 'catcher':
|
||||
return 'C'
|
||||
elif field_pos.lower() == 'first baseman':
|
||||
return '1B'
|
||||
elif field_pos.lower() == 'second baseman':
|
||||
return '2B'
|
||||
elif field_pos.lower() == 'third baseman':
|
||||
return '3B'
|
||||
elif field_pos.lower() == 'shortstop':
|
||||
return 'SS'
|
||||
elif field_pos.lower() == 'left fielder':
|
||||
return 'LF'
|
||||
elif field_pos.lower() == 'center fielder':
|
||||
return 'CF'
|
||||
elif field_pos.lower() == 'right fielder':
|
||||
return 'RF'
|
||||
else:
|
||||
return 'P'
|
||||
|
||||
|
||||
def position_name_to_abbrev(position_name):
|
||||
"""Convert position name to abbreviation (alternate format)."""
|
||||
if position_name == 'Catcher':
|
||||
return 'C'
|
||||
elif position_name == 'First Base':
|
||||
return '1B'
|
||||
elif position_name == 'Second Base':
|
||||
return '2B'
|
||||
elif position_name == 'Third Base':
|
||||
return '3B'
|
||||
elif position_name == 'Shortstop':
|
||||
return 'SS'
|
||||
elif position_name == 'Left Field':
|
||||
return 'LF'
|
||||
elif position_name == 'Center Field':
|
||||
return 'CF'
|
||||
elif position_name == 'Right Field':
|
||||
return 'RF'
|
||||
elif position_name == 'Pitcher':
|
||||
return 'P'
|
||||
else:
|
||||
return position_name
|
||||
|
||||
|
||||
def user_has_role(user: discord.User | discord.Member, role_name: str) -> bool:
|
||||
"""Check if a Discord user has a specific role."""
|
||||
for x in user.roles:
|
||||
if x.name == role_name:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_roster_sheet_legacy(team):
|
||||
"""Get legacy roster sheet URL for a team."""
|
||||
return f'https://docs.google.com/spreadsheets/d/{team.gsheet}/edit'
|
||||
|
||||
|
||||
def get_roster_sheet(team):
|
||||
"""Get roster sheet URL for a team."""
|
||||
return f'https://docs.google.com/spreadsheets/d/{team["gsheet"]}/edit'
|
||||
|
||||
|
||||
def get_player_url(team, player) -> str:
|
||||
"""Generate player URL for SBA or Baseball Reference."""
|
||||
if team.get('league') == 'SBA':
|
||||
return f'https://statsplus.net/super-baseball-association/player/{player["player_id"]}'
|
||||
else:
|
||||
return f'https://www.baseball-reference.com/players/{player["bbref_id"][0]}/{player["bbref_id"]}.shtml'
|
||||
|
||||
|
||||
def owner_only(ctx) -> bool:
|
||||
"""Check if user is the bot owner."""
|
||||
# ID for discord User Cal
|
||||
owners = [287463767924137994, 1087936030899347516]
|
||||
|
||||
if ctx.author.id in owners:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_cal_user(ctx):
|
||||
"""Get the Cal user from context."""
|
||||
for user in ctx.bot.get_all_members():
|
||||
if user.id == 287463767924137994:
|
||||
return user
|
||||
Loading…
Reference in New Issue
Block a user