Merge pull request 'perf: parallelize roll_for_cards and bump pack limit to 20' (#102) from performance/97-parallelize-roll-for-cards into next-release
Reviewed-on: #102
This commit is contained in:
commit
44d83b321f
@ -3,126 +3,148 @@ 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 helpers.constants import ALL_MLB_TEAMS, IMAGES, normalize_franchise
|
||||
|
||||
logger = logging.getLogger('discord_app')
|
||||
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,
|
||||
'Athletics': 20, # Alias for post-Oakland move
|
||||
'Seattle Mariners': 24,
|
||||
'Tampa Bay Rays': 27,
|
||||
'Texas Rangers': 28,
|
||||
'Toronto Blue Jays': 29
|
||||
"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,
|
||||
"Athletics": 20, # Alias for post-Oakland move
|
||||
"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
|
||||
"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']
|
||||
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 Season': 24,
|
||||
'2005 Live': 27,
|
||||
'Pokemon - Brilliant Stars': 23
|
||||
"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 Season": 24,
|
||||
"2005 Live": 27,
|
||||
"Pokemon - Brilliant Stars": 23,
|
||||
}
|
||||
|
||||
|
||||
def _get_team_id(team_name: str, league: Literal['AL', 'NL']) -> int:
|
||||
def _get_team_id(team_name: str, league: Literal["AL", "NL"]) -> int:
|
||||
"""Get team ID from team name and league."""
|
||||
if league == 'AL':
|
||||
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')
|
||||
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):
|
||||
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':
|
||||
|
||||
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)
|
||||
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]}')
|
||||
raise ValueError(f"Unknown team: {self.values[0]}")
|
||||
|
||||
await interaction.response.edit_message(content=f'You selected the **{self.values[0]}**', view=None)
|
||||
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)
|
||||
("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')
|
||||
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("Unable to open packs")
|
||||
|
||||
this_pack = await db_patch('packs', object_id=p_query['packs'][0]['id'], params=[('pack_team_id', team_id)])
|
||||
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)
|
||||
|
||||
@ -130,104 +152,116 @@ class SelectChoicePackTeam(discord.ui.Select):
|
||||
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)
|
||||
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}')
|
||||
|
||||
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)]
|
||||
params = [
|
||||
("team_id", self.owner_team["id"]),
|
||||
("opened", False),
|
||||
("limit", 20),
|
||||
("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))
|
||||
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}')
|
||||
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
|
||||
# Handle Team Choice packs with no team/cardset assigned
|
||||
if 'Team Choice' in pack_vals and 'Team' not in pack_vals and 'Cardset' not in pack_vals:
|
||||
if (
|
||||
"Team Choice" in pack_vals
|
||||
and "Team" not in pack_vals
|
||||
and "Cardset" not in pack_vals
|
||||
):
|
||||
await interaction.followup.send(
|
||||
content='This Team Choice pack needs to be assigned a team and cardset. '
|
||||
'Please contact an admin to configure this pack.',
|
||||
ephemeral=True
|
||||
content="This Team Choice pack needs to be assigned a team and cardset. "
|
||||
"Please contact an admin to configure this pack.",
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
elif 'Team Choice' in pack_vals and 'Cardset' in pack_vals:
|
||||
elif "Team Choice" in pack_vals and "Cardset" in pack_vals:
|
||||
# cardset_id = pack_vals[2]
|
||||
cardset_index = pack_vals.index('Cardset')
|
||||
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:
|
||||
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
|
||||
[
|
||||
SelectChoicePackTeam("AL", self.owner_team, cardset_id),
|
||||
SelectChoicePackTeam("NL", self.owner_team, cardset_id),
|
||||
],
|
||||
timeout=30,
|
||||
)
|
||||
await interaction.followup.send(
|
||||
content='Please select a team for your Team Choice pack:',
|
||||
view=view
|
||||
content="Please select a team for your Team Choice pack:", 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}')
|
||||
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}")
|
||||
await interaction.followup.send(
|
||||
content='Unable to find the selected pack. Please contact an admin.',
|
||||
ephemeral=True
|
||||
content="Unable to find the selected pack. Please contact an admin.",
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
# Open the packs
|
||||
try:
|
||||
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)
|
||||
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
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to open pack: {e}')
|
||||
logger.error(f"Failed to open pack: {e}")
|
||||
await interaction.followup.send(
|
||||
content=f'Failed to open pack. Please contact an admin. Error: {str(e)}',
|
||||
ephemeral=True
|
||||
content=f"Failed to open pack. Please contact an admin. Error: {str(e)}",
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
@ -235,275 +269,317 @@ class SelectOpenPack(discord.ui.Select):
|
||||
class SelectPaperdexCardset(discord.ui.Select):
|
||||
def __init__(self):
|
||||
options = [
|
||||
discord.SelectOption(label='2005 Live'),
|
||||
discord.SelectOption(label='2025 Season'),
|
||||
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')
|
||||
discord.SelectOption(label="2005 Live"),
|
||||
discord.SelectOption(label="2025 Season"),
|
||||
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)
|
||||
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]}')
|
||||
|
||||
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]}')
|
||||
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)
|
||||
c_query = await db_get("cardsets", object_id=cardset_id, none_okay=False)
|
||||
await interaction.response.edit_message(
|
||||
content="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
|
||||
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']):
|
||||
def __init__(self, which: Literal["AL", "NL"]):
|
||||
self.which = which
|
||||
|
||||
if which == 'AL':
|
||||
|
||||
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)
|
||||
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]}')
|
||||
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)
|
||||
t_query = await db_get("teams", object_id=team_id, none_okay=False)
|
||||
await interaction.response.edit_message(
|
||||
content="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)
|
||||
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):
|
||||
def __init__(
|
||||
self,
|
||||
team: dict,
|
||||
quantity: int,
|
||||
pack_type_id: int,
|
||||
pack_embed: discord.Embed,
|
||||
cost: int,
|
||||
):
|
||||
options = [
|
||||
discord.SelectOption(label='2005 Live'),
|
||||
discord.SelectOption(label='2025 Season'),
|
||||
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')
|
||||
discord.SelectOption(label="2005 Live"),
|
||||
discord.SelectOption(label="2025 Season"),
|
||||
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)
|
||||
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]}')
|
||||
|
||||
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'])
|
||||
raise ValueError(f"Unknown cardset: {self.values[0]}")
|
||||
|
||||
self.pack_embed.description = f'{self.pack_embed.description} - {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
|
||||
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
|
||||
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
|
||||
)
|
||||
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
|
||||
"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 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
|
||||
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: 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':
|
||||
|
||||
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)
|
||||
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]}')
|
||||
raise ValueError(f"Unknown team: {self.values[0]}")
|
||||
|
||||
self.pack_embed.description = f'{self.pack_embed.description} - {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
|
||||
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
|
||||
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
|
||||
)
|
||||
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
|
||||
"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 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
|
||||
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):
|
||||
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':
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
# Check if already assigned - compare against both normalized franchise and full mlbclub
|
||||
normalized_selection = normalize_franchise(self.values[0])
|
||||
if normalized_selection == self.player['franchise'] or self.values[0] == self.player['mlbclub']:
|
||||
if (
|
||||
normalized_selection == 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"]}**.'
|
||||
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
|
||||
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
|
||||
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', normalize_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 db_patch(
|
||||
"players",
|
||||
object_id=self.player["player_id"],
|
||||
params=[
|
||||
("mlbclub", self.values[0]),
|
||||
("franchise", normalize_franchise(self.values[0])),
|
||||
],
|
||||
)
|
||||
await interaction.channel.send(f'All done!')
|
||||
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("All done!")
|
||||
|
||||
|
||||
class SelectView(discord.ui.View):
|
||||
@ -511,4 +587,4 @@ class SelectView(discord.ui.View):
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
for x in select_objects:
|
||||
self.add_item(x)
|
||||
self.add_item(x)
|
||||
|
||||
391
helpers/main.py
391
helpers/main.py
@ -2,35 +2,23 @@ import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import traceback
|
||||
|
||||
import discord
|
||||
import pygsheets
|
||||
import aiohttp
|
||||
from discord.ext import commands
|
||||
from api_calls import *
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from difflib import get_close_matches
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Literal, Union, List
|
||||
from typing import Optional, Union, List
|
||||
|
||||
from exceptions import log_exception
|
||||
from in_game.gameplay_models import Team
|
||||
from constants import *
|
||||
from discord_ui import *
|
||||
from random_content import *
|
||||
from utils import (
|
||||
position_name_to_abbrev,
|
||||
user_has_role,
|
||||
get_roster_sheet_legacy,
|
||||
get_roster_sheet,
|
||||
get_player_url,
|
||||
owner_only,
|
||||
get_cal_user,
|
||||
get_context_user,
|
||||
)
|
||||
from search_utils import *
|
||||
from discord_utils import *
|
||||
@ -182,7 +170,7 @@ async def get_card_embeds(card, include_stats=False) -> list:
|
||||
]
|
||||
if any(bool_list):
|
||||
if count == 1:
|
||||
coll_string = f"Only you"
|
||||
coll_string = "Only you"
|
||||
else:
|
||||
coll_string = (
|
||||
f"You and {count - 1} other{'s' if count - 1 != 1 else ''}"
|
||||
@ -190,7 +178,7 @@ async def get_card_embeds(card, include_stats=False) -> list:
|
||||
elif count:
|
||||
coll_string = f"{count} other team{'s' if count != 1 else ''}"
|
||||
else:
|
||||
coll_string = f"0 teams"
|
||||
coll_string = "0 teams"
|
||||
embed.add_field(name="Collected By", value=coll_string)
|
||||
else:
|
||||
embed.add_field(
|
||||
@ -229,7 +217,7 @@ async def get_card_embeds(card, include_stats=False) -> list:
|
||||
)
|
||||
if evo_mon is not None:
|
||||
embed.add_field(name="Evolves Into", value=f"{evo_mon['p_name']}")
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logging.error(
|
||||
"could not pull evolution: {e}", exc_info=True, stack_info=True
|
||||
)
|
||||
@ -240,7 +228,7 @@ async def get_card_embeds(card, include_stats=False) -> list:
|
||||
)
|
||||
if evo_mon is not None:
|
||||
embed.add_field(name="Evolves From", value=f"{evo_mon['p_name']}")
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logging.error(
|
||||
"could not pull evolution: {e}", exc_info=True, stack_info=True
|
||||
)
|
||||
@ -342,7 +330,7 @@ async def display_cards(
|
||||
)
|
||||
try:
|
||||
cards.sort(key=lambda x: x["player"]["rarity"]["value"])
|
||||
logger.debug(f"Cards sorted successfully")
|
||||
logger.debug("Cards sorted successfully")
|
||||
|
||||
card_embeds = [await get_card_embeds(x) for x in cards]
|
||||
logger.debug(f"Created {len(card_embeds)} card embeds")
|
||||
@ -363,15 +351,15 @@ async def display_cards(
|
||||
r_emoji = "→"
|
||||
view.left_button.disabled = True
|
||||
view.left_button.label = f"{l_emoji}Prev: -/{len(card_embeds)}"
|
||||
view.cancel_button.label = f"Close Pack"
|
||||
view.cancel_button.label = "Close Pack"
|
||||
view.right_button.label = f"Next: {page_num + 2}/{len(card_embeds)}{r_emoji}"
|
||||
if len(cards) == 1:
|
||||
view.right_button.disabled = True
|
||||
|
||||
logger.debug(f"Pagination view created successfully")
|
||||
logger.debug("Pagination view created successfully")
|
||||
|
||||
if pack_cover:
|
||||
logger.debug(f"Sending pack cover message")
|
||||
logger.debug("Sending pack cover message")
|
||||
msg = await channel.send(
|
||||
content=None,
|
||||
embed=image_embed(pack_cover, title=f"{team['lname']}", desc=pack_name),
|
||||
@ -383,7 +371,7 @@ async def display_cards(
|
||||
content=None, embeds=card_embeds[page_num], view=view
|
||||
)
|
||||
|
||||
logger.debug(f"Initial message sent successfully")
|
||||
logger.debug("Initial message sent successfully")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error creating view or sending initial message: {e}", exc_info=True
|
||||
@ -400,12 +388,12 @@ async def display_cards(
|
||||
f"{user.mention} you've got {len(cards)} cards here"
|
||||
)
|
||||
|
||||
logger.debug(f"Follow-up message sent successfully")
|
||||
logger.debug("Follow-up message sent successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending follow-up message: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
logger.debug(f"Starting main interaction loop")
|
||||
logger.debug("Starting main interaction loop")
|
||||
while True:
|
||||
try:
|
||||
logger.debug(f"Waiting for user interaction on page {page_num}")
|
||||
@ -471,7 +459,7 @@ async def display_cards(
|
||||
),
|
||||
view=view,
|
||||
)
|
||||
logger.debug(f"MVP display updated successfully")
|
||||
logger.debug("MVP display updated successfully")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error processing shiny card on page {page_num}: {e}", exc_info=True
|
||||
@ -479,19 +467,19 @@ async def display_cards(
|
||||
# Continue with regular flow instead of crashing
|
||||
try:
|
||||
tmp_msg = await channel.send(
|
||||
content=f"<@&1163537676885033010> we've got an MVP!"
|
||||
content="<@&1163537676885033010> we've got an MVP!"
|
||||
)
|
||||
await follow_up.edit(
|
||||
content=f"<@&1163537676885033010> we've got an MVP!"
|
||||
content="<@&1163537676885033010> we've got an MVP!"
|
||||
)
|
||||
await tmp_msg.delete()
|
||||
except discord.errors.NotFound:
|
||||
# Role might not exist or message was already deleted
|
||||
await follow_up.edit(content=f"We've got an MVP!")
|
||||
await follow_up.edit(content="We've got an MVP!")
|
||||
except Exception as e:
|
||||
# Log error but don't crash the function
|
||||
logger.error(f"Error handling MVP notification: {e}")
|
||||
await follow_up.edit(content=f"We've got an MVP!")
|
||||
await follow_up.edit(content="We've got an MVP!")
|
||||
await view.wait()
|
||||
|
||||
view = Pagination([user], timeout=10)
|
||||
@ -499,7 +487,7 @@ async def display_cards(
|
||||
view.right_button.label = (
|
||||
f"Next: {page_num + 2}/{len(card_embeds)}{r_emoji}"
|
||||
)
|
||||
view.cancel_button.label = f"Close Pack"
|
||||
view.cancel_button.label = "Close Pack"
|
||||
view.left_button.label = f"{l_emoji}Prev: {page_num}/{len(card_embeds)}"
|
||||
if page_num == 0:
|
||||
view.left_button.label = f"{l_emoji}Prev: -/{len(card_embeds)}"
|
||||
@ -547,7 +535,7 @@ async def embed_pagination(
|
||||
l_emoji = ""
|
||||
r_emoji = ""
|
||||
view.right_button.label = f"Next: {page_num + 2}/{len(all_embeds)}{r_emoji}"
|
||||
view.cancel_button.label = f"Cancel"
|
||||
view.cancel_button.label = "Cancel"
|
||||
view.left_button.label = f"{l_emoji}Prev: {page_num}/{len(all_embeds)}"
|
||||
if page_num == 0:
|
||||
view.left_button.label = f"{l_emoji}Prev: -/{len(all_embeds)}"
|
||||
@ -582,7 +570,7 @@ async def embed_pagination(
|
||||
|
||||
view = Pagination([user], timeout=timeout)
|
||||
view.right_button.label = f"Next: {page_num + 2}/{len(all_embeds)}{r_emoji}"
|
||||
view.cancel_button.label = f"Cancel"
|
||||
view.cancel_button.label = "Cancel"
|
||||
view.left_button.label = f"{l_emoji}Prev: {page_num}/{len(all_embeds)}"
|
||||
if page_num == 0:
|
||||
view.left_button.label = f"{l_emoji}Prev: -/{len(all_embeds)}"
|
||||
@ -661,21 +649,15 @@ async def get_test_pack(ctx, team):
|
||||
|
||||
|
||||
async def roll_for_cards(all_packs: list, extra_val=None) -> list:
|
||||
"""Open packs by rolling dice, fetching random players, and creating cards.
|
||||
|
||||
Parallelizes DB calls: one fetch per rarity tier across all packs,
|
||||
then gathers all card creates and pack patches concurrently.
|
||||
"""
|
||||
Pack odds are calculated based on the pack type
|
||||
|
||||
Parameters
|
||||
----------
|
||||
extra_val
|
||||
all_packs
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
all_players = []
|
||||
team = all_packs[0]["team"]
|
||||
pack_ids = []
|
||||
|
||||
# --- Phase A: Roll dice for every pack (CPU-only, no I/O) ---
|
||||
pack_counts = []
|
||||
for pack in all_packs:
|
||||
counts = {
|
||||
"Rep": {"count": 0, "rarity": 0},
|
||||
@ -685,10 +667,9 @@ async def roll_for_cards(all_packs: list, extra_val=None) -> list:
|
||||
"MVP": {"count": 0, "rarity": 5},
|
||||
"HoF": {"count": 0, "rarity": 8},
|
||||
}
|
||||
this_pack_players = []
|
||||
if pack["pack_type"]["name"] == "Standard":
|
||||
# Cards 1 - 2
|
||||
for x in range(2):
|
||||
for _ in range(2):
|
||||
d_1000 = random.randint(1, 1000)
|
||||
if d_1000 <= 450:
|
||||
counts["Rep"]["count"] += 1
|
||||
@ -808,7 +789,6 @@ async def roll_for_cards(all_packs: list, extra_val=None) -> list:
|
||||
logger.info(
|
||||
f"Building Check-In Pack // extra_val (type): {extra_val} {type(extra_val)}"
|
||||
)
|
||||
# Single Card
|
||||
mod = 0
|
||||
if isinstance(extra_val, int):
|
||||
mod = extra_val
|
||||
@ -826,106 +806,195 @@ async def roll_for_cards(all_packs: list, extra_val=None) -> list:
|
||||
else:
|
||||
raise TypeError(f"Pack type not recognized: {pack['pack_type']['name']}")
|
||||
|
||||
pull_notifs = []
|
||||
for key in counts:
|
||||
mvp_flag = None
|
||||
pack_counts.append(counts)
|
||||
|
||||
if counts[key]["count"] > 0:
|
||||
params = [
|
||||
("min_rarity", counts[key]["rarity"]),
|
||||
("max_rarity", counts[key]["rarity"]),
|
||||
("limit", counts[key]["count"]),
|
||||
]
|
||||
if all_packs[0]["pack_team"] is not None:
|
||||
params.extend(
|
||||
[
|
||||
("franchise", all_packs[0]["pack_team"]["sname"]),
|
||||
("in_packs", True),
|
||||
]
|
||||
)
|
||||
elif all_packs[0]["pack_cardset"] is not None:
|
||||
params.append(("cardset_id", all_packs[0]["pack_cardset"]["id"]))
|
||||
else:
|
||||
params.append(("in_packs", True))
|
||||
# --- Phase B: Fetch players — one call per rarity tier, all gathered ---
|
||||
# Sum counts across all packs per rarity tier
|
||||
rarity_keys = ["Rep", "Res", "Sta", "All", "MVP", "HoF"]
|
||||
summed = {key: 0 for key in rarity_keys}
|
||||
for counts in pack_counts:
|
||||
for key in rarity_keys:
|
||||
summed[key] += counts[key]["count"]
|
||||
|
||||
pl = await db_get("players/random", params=params)
|
||||
|
||||
if pl["count"] != counts[key]["count"]:
|
||||
mvp_flag = counts[key]["count"] - pl["count"]
|
||||
logging.info(
|
||||
f"Set mvp flag to {mvp_flag} / cardset_id: {all_packs[0]['pack_cardset']['id']}"
|
||||
)
|
||||
|
||||
for x in pl["players"]:
|
||||
this_pack_players.append(x)
|
||||
all_players.append(x)
|
||||
|
||||
if x["rarity"]["value"] >= 3:
|
||||
pull_notifs.append(x)
|
||||
|
||||
if mvp_flag and all_packs[0]["pack_cardset"]["id"] not in [23]:
|
||||
logging.info(f"Adding {mvp_flag} MVPs for missing cards")
|
||||
pl = await db_get(
|
||||
"players/random", params=[("min_rarity", 5), ("limit", mvp_flag)]
|
||||
)
|
||||
|
||||
for x in pl["players"]:
|
||||
this_pack_players.append(x)
|
||||
all_players.append(x)
|
||||
|
||||
# Add dupes of Replacement/Reserve cards
|
||||
elif mvp_flag:
|
||||
logging.info(f"Adding {mvp_flag} duplicate pokemon cards")
|
||||
for count in range(mvp_flag):
|
||||
logging.info(f"Adding {pl['players'][0]['p_name']} to the pack")
|
||||
this_pack_players.append(x)
|
||||
all_players.append(pl["players"][0])
|
||||
|
||||
success = await db_post(
|
||||
"cards",
|
||||
payload={
|
||||
"cards": [
|
||||
{
|
||||
"player_id": x["player_id"],
|
||||
"team_id": pack["team"]["id"],
|
||||
"pack_id": pack["id"],
|
||||
}
|
||||
for x in this_pack_players
|
||||
]
|
||||
},
|
||||
timeout=10,
|
||||
# Build shared filter params
|
||||
base_params = []
|
||||
if all_packs[0]["pack_team"] is not None:
|
||||
base_params.extend(
|
||||
[
|
||||
("franchise", all_packs[0]["pack_team"]["sname"]),
|
||||
("in_packs", True),
|
||||
]
|
||||
)
|
||||
if not success:
|
||||
raise ConnectionError(f"Failed to create this pack of cards.")
|
||||
elif all_packs[0]["pack_cardset"] is not None:
|
||||
base_params.append(("cardset_id", all_packs[0]["pack_cardset"]["id"]))
|
||||
else:
|
||||
base_params.append(("in_packs", True))
|
||||
|
||||
await db_patch(
|
||||
"packs",
|
||||
object_id=pack["id"],
|
||||
params=[
|
||||
(
|
||||
"open_time",
|
||||
int(datetime.datetime.timestamp(datetime.datetime.now()) * 1000),
|
||||
)
|
||||
],
|
||||
)
|
||||
pack_ids.append(pack["id"])
|
||||
# Fire one request per non-zero rarity tier concurrently
|
||||
rarity_values = {
|
||||
"Rep": 0,
|
||||
"Res": 1,
|
||||
"Sta": 2,
|
||||
"All": 3,
|
||||
"MVP": 5,
|
||||
"HoF": 8,
|
||||
}
|
||||
fetch_keys = [key for key in rarity_keys if summed[key] > 0]
|
||||
fetch_coros = []
|
||||
for key in fetch_keys:
|
||||
params = [
|
||||
("min_rarity", rarity_values[key]),
|
||||
("max_rarity", rarity_values[key]),
|
||||
("limit", summed[key]),
|
||||
] + base_params
|
||||
fetch_coros.append(db_get("players/random", params=params))
|
||||
|
||||
for pull in pull_notifs:
|
||||
logger.info(f"good pull: {pull}")
|
||||
await db_post(
|
||||
"notifs",
|
||||
payload={
|
||||
"created": int(
|
||||
datetime.datetime.timestamp(datetime.datetime.now()) * 1000
|
||||
),
|
||||
"title": "Rare Pull",
|
||||
"field_name": f"{player_desc(pull)} ({pull['rarity']['name']})",
|
||||
"message": f"Pulled by {team['abbrev']}",
|
||||
"about": f"Player-{pull['player_id']}",
|
||||
},
|
||||
fetch_results = await asyncio.gather(*fetch_coros)
|
||||
|
||||
# Map results back: rarity key -> list of players
|
||||
fetched_players = {}
|
||||
for key, result in zip(fetch_keys, fetch_results):
|
||||
fetched_players[key] = result.get("players", [])
|
||||
|
||||
# Handle shortfalls — collect total MVP backfill needed
|
||||
total_mvp_shortfall = 0
|
||||
# Track per-tier shortfall for dupe-branch (cardset 23 exclusion)
|
||||
tier_shortfalls = {}
|
||||
for key in fetch_keys:
|
||||
returned = len(fetched_players[key])
|
||||
requested = summed[key]
|
||||
if returned < requested:
|
||||
shortfall = requested - returned
|
||||
tier_shortfalls[key] = shortfall
|
||||
total_mvp_shortfall += shortfall
|
||||
logging.info(
|
||||
f"Shortfall in {key}: requested {requested}, got {returned} "
|
||||
f"(cardset_id: {all_packs[0]['pack_cardset']['id'] if all_packs[0]['pack_cardset'] else 'N/A'})"
|
||||
)
|
||||
|
||||
return pack_ids
|
||||
# Fetch MVP backfill or duplicate existing players
|
||||
backfill_players = []
|
||||
is_dupe_cardset = all_packs[0]["pack_cardset"] is not None and all_packs[0][
|
||||
"pack_cardset"
|
||||
]["id"] in [23]
|
||||
if total_mvp_shortfall > 0 and not is_dupe_cardset:
|
||||
logging.info(f"Adding {total_mvp_shortfall} MVPs for missing cards")
|
||||
mvp_result = await db_get(
|
||||
"players/random",
|
||||
params=[("min_rarity", 5), ("limit", total_mvp_shortfall)],
|
||||
)
|
||||
backfill_players = mvp_result.get("players", [])
|
||||
elif total_mvp_shortfall > 0 and is_dupe_cardset:
|
||||
logging.info(
|
||||
f"Adding {total_mvp_shortfall} duplicate cards for excluded cardset"
|
||||
)
|
||||
# Duplicate from first available player in the fetched results
|
||||
for key in fetch_keys:
|
||||
if fetched_players[key]:
|
||||
for _ in range(total_mvp_shortfall):
|
||||
backfill_players.append(fetched_players[key][0])
|
||||
break
|
||||
|
||||
# Slice fetched players back into per-pack groups
|
||||
# Track consumption offset per rarity tier
|
||||
tier_offsets = {key: 0 for key in rarity_keys}
|
||||
backfill_offset = 0
|
||||
per_pack_players = []
|
||||
all_pull_notifs = []
|
||||
|
||||
for pack_idx, counts in enumerate(pack_counts):
|
||||
this_pack_players = []
|
||||
pack_shortfall = 0
|
||||
|
||||
for key in rarity_keys:
|
||||
needed = counts[key]["count"]
|
||||
if needed == 0:
|
||||
continue
|
||||
|
||||
available = fetched_players.get(key, [])
|
||||
start = tier_offsets[key]
|
||||
end = start + needed
|
||||
got = available[start:end]
|
||||
this_pack_players.extend(got)
|
||||
tier_offsets[key] = end
|
||||
|
||||
# Track shortfall for this pack
|
||||
if len(got) < needed:
|
||||
pack_shortfall += needed - len(got)
|
||||
|
||||
# Distribute backfill players to this pack
|
||||
if pack_shortfall > 0 and backfill_offset < len(backfill_players):
|
||||
bf_slice = backfill_players[
|
||||
backfill_offset : backfill_offset + pack_shortfall
|
||||
]
|
||||
this_pack_players.extend(bf_slice)
|
||||
backfill_offset += len(bf_slice)
|
||||
|
||||
# Collect rare pull notifications
|
||||
for player in this_pack_players:
|
||||
if player["rarity"]["value"] >= 3:
|
||||
all_pull_notifs.append(player)
|
||||
|
||||
per_pack_players.append(this_pack_players)
|
||||
|
||||
# --- Phase C: Write cards + mark packs opened, all gathered ---
|
||||
open_time = int(datetime.datetime.timestamp(datetime.datetime.now()) * 1000)
|
||||
|
||||
write_coros = []
|
||||
for pack, this_pack_players in zip(all_packs, per_pack_players):
|
||||
write_coros.append(
|
||||
db_post(
|
||||
"cards",
|
||||
payload={
|
||||
"cards": [
|
||||
{
|
||||
"player_id": p["player_id"],
|
||||
"team_id": pack["team"]["id"],
|
||||
"pack_id": pack["id"],
|
||||
}
|
||||
for p in this_pack_players
|
||||
]
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
)
|
||||
write_coros.append(
|
||||
db_patch(
|
||||
"packs",
|
||||
object_id=pack["id"],
|
||||
params=[("open_time", open_time)],
|
||||
)
|
||||
)
|
||||
|
||||
write_results = await asyncio.gather(*write_coros)
|
||||
|
||||
# Check card creation results (every other result starting at index 0)
|
||||
for i in range(0, len(write_results), 2):
|
||||
if not write_results[i]:
|
||||
raise ConnectionError("Failed to create this pack of cards.")
|
||||
|
||||
# --- Gather notification posts ---
|
||||
if all_pull_notifs:
|
||||
notif_coros = []
|
||||
for pull in all_pull_notifs:
|
||||
logger.info(f"good pull: {pull}")
|
||||
notif_coros.append(
|
||||
db_post(
|
||||
"notifs",
|
||||
payload={
|
||||
"created": int(
|
||||
datetime.datetime.timestamp(datetime.datetime.now()) * 1000
|
||||
),
|
||||
"title": "Rare Pull",
|
||||
"field_name": f"{player_desc(pull)} ({pull['rarity']['name']})",
|
||||
"message": f"Pulled by {team['abbrev']}",
|
||||
"about": f"Player-{pull['player_id']}",
|
||||
},
|
||||
)
|
||||
)
|
||||
await asyncio.gather(*notif_coros)
|
||||
|
||||
return [pack["id"] for pack in all_packs]
|
||||
|
||||
|
||||
async def give_packs(team: dict, num_packs: int, pack_type: dict = None) -> dict:
|
||||
@ -962,7 +1031,7 @@ def get_sheets(bot):
|
||||
except Exception as e:
|
||||
logger.error(f"Could not grab sheets auth: {e}")
|
||||
raise ConnectionError(
|
||||
f"Bot has not authenticated with discord; please try again in 1 minute."
|
||||
"Bot has not authenticated with discord; please try again in 1 minute."
|
||||
)
|
||||
|
||||
|
||||
@ -1072,7 +1141,7 @@ def get_blank_team_card(player):
|
||||
def get_rosters(team, bot, roster_num: Optional[int] = None) -> list:
|
||||
sheets = get_sheets(bot)
|
||||
this_sheet = sheets.open_by_key(team["gsheet"])
|
||||
r_sheet = this_sheet.worksheet_by_title(f"My Rosters")
|
||||
r_sheet = this_sheet.worksheet_by_title("My Rosters")
|
||||
logger.debug(f"this_sheet: {this_sheet} / r_sheet = {r_sheet}")
|
||||
|
||||
all_rosters = [None, None, None]
|
||||
@ -1153,11 +1222,11 @@ def get_roster_lineups(team, bot, roster_num, lineup_num) -> list:
|
||||
|
||||
try:
|
||||
lineup_cells = [(row[0].value, int(row[1].value)) for row in raw_cells]
|
||||
except ValueError as e:
|
||||
except ValueError:
|
||||
logger.error(f"Could not pull roster for {team['abbrev']} due to a ValueError")
|
||||
raise ValueError(
|
||||
f"Uh oh. Looks like your roster might not be saved. I am reading blanks when I try to "
|
||||
f"get the card IDs"
|
||||
"Uh oh. Looks like your roster might not be saved. I am reading blanks when I try to "
|
||||
"get the card IDs"
|
||||
)
|
||||
logger.debug(f"lineup_cells: {lineup_cells}")
|
||||
|
||||
@ -1552,7 +1621,7 @@ def get_ratings_guide(sheets):
|
||||
}
|
||||
for x in p_data
|
||||
]
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
return {"valid": False}
|
||||
|
||||
return {"valid": True, "batter_ratings": batters, "pitcher_ratings": pitchers}
|
||||
@ -1764,7 +1833,7 @@ async def open_st_pr_packs(all_packs: list, team: dict, context):
|
||||
pack_ids = await roll_for_cards(all_packs)
|
||||
if not pack_ids:
|
||||
logger.error(f"open_packs - unable to roll_for_cards for packs: {all_packs}")
|
||||
raise ValueError(f"I was not able to unpack these cards")
|
||||
raise ValueError("I was not able to unpack these cards")
|
||||
|
||||
all_cards = []
|
||||
for p_id in pack_ids:
|
||||
@ -1775,7 +1844,7 @@ async def open_st_pr_packs(all_packs: list, team: dict, context):
|
||||
|
||||
if not all_cards:
|
||||
logger.error(f"open_packs - unable to get cards for packs: {pack_ids}")
|
||||
raise ValueError(f"I was not able to display these cards")
|
||||
raise ValueError("I was not able to display these cards")
|
||||
|
||||
# Present cards to opening channel
|
||||
if type(context) == commands.Context:
|
||||
@ -1834,7 +1903,7 @@ async def get_choice_from_cards(
|
||||
view = Pagination([interaction.user], timeout=30)
|
||||
view.left_button.disabled = True
|
||||
view.left_button.label = f"Prev: -/{len(card_embeds)}"
|
||||
view.cancel_button.label = f"Take This Card"
|
||||
view.cancel_button.label = "Take This Card"
|
||||
view.cancel_button.style = discord.ButtonStyle.success
|
||||
view.cancel_button.disabled = True
|
||||
view.right_button.label = f"Next: 1/{len(card_embeds)}"
|
||||
@ -1852,7 +1921,7 @@ async def get_choice_from_cards(
|
||||
view = Pagination([interaction.user], timeout=30)
|
||||
view.left_button.label = f"Prev: -/{len(card_embeds)}"
|
||||
view.left_button.disabled = True
|
||||
view.cancel_button.label = f"Take This Card"
|
||||
view.cancel_button.label = "Take This Card"
|
||||
view.cancel_button.style = discord.ButtonStyle.success
|
||||
view.right_button.label = f"Next: {page_num + 1}/{len(card_embeds)}"
|
||||
|
||||
@ -1895,7 +1964,7 @@ async def get_choice_from_cards(
|
||||
|
||||
view = Pagination([interaction.user], timeout=30)
|
||||
view.left_button.label = f"Prev: {page_num - 1}/{len(card_embeds)}"
|
||||
view.cancel_button.label = f"Take This Card"
|
||||
view.cancel_button.label = "Take This Card"
|
||||
view.cancel_button.style = discord.ButtonStyle.success
|
||||
view.right_button.label = f"Next: {page_num + 1}/{len(card_embeds)}"
|
||||
if page_num == 1:
|
||||
@ -1941,7 +2010,7 @@ async def open_choice_pack(
|
||||
players = pl["players"]
|
||||
elif pack_type == "Team Choice":
|
||||
if this_pack["pack_team"] is None:
|
||||
raise KeyError(f"Team not listed for Team Choice pack")
|
||||
raise KeyError("Team not listed for Team Choice pack")
|
||||
|
||||
d1000 = random.randint(1, 1000)
|
||||
pack_cover = this_pack["pack_team"]["logo"]
|
||||
@ -1980,7 +2049,7 @@ async def open_choice_pack(
|
||||
rarity_id += 1
|
||||
elif pack_type == "Promo Choice":
|
||||
if this_pack["pack_cardset"] is None:
|
||||
raise KeyError(f"Cardset not listed for Promo Choice pack")
|
||||
raise KeyError("Cardset not listed for Promo Choice pack")
|
||||
|
||||
d1000 = random.randint(1, 1000)
|
||||
pack_cover = IMAGES["mvp-hype"]
|
||||
@ -2037,8 +2106,8 @@ async def open_choice_pack(
|
||||
rarity_id += 3
|
||||
|
||||
if len(players) == 0:
|
||||
logger.error(f"Could not create choice pack")
|
||||
raise ConnectionError(f"Could not create choice pack")
|
||||
logger.error("Could not create choice pack")
|
||||
raise ConnectionError("Could not create choice pack")
|
||||
|
||||
if type(context) == commands.Context:
|
||||
author = context.author
|
||||
@ -2061,7 +2130,7 @@ async def open_choice_pack(
|
||||
view = Pagination([author], timeout=30)
|
||||
view.left_button.disabled = True
|
||||
view.left_button.label = f"Prev: -/{len(card_embeds)}"
|
||||
view.cancel_button.label = f"Take This Card"
|
||||
view.cancel_button.label = "Take This Card"
|
||||
view.cancel_button.style = discord.ButtonStyle.success
|
||||
view.cancel_button.disabled = True
|
||||
view.right_button.label = f"Next: 1/{len(card_embeds)}"
|
||||
@ -2079,10 +2148,10 @@ async def open_choice_pack(
|
||||
)
|
||||
if rarity_id >= 5:
|
||||
tmp_msg = await pack_channel.send(
|
||||
content=f"<@&1163537676885033010> we've got an MVP!"
|
||||
content="<@&1163537676885033010> we've got an MVP!"
|
||||
)
|
||||
else:
|
||||
tmp_msg = await pack_channel.send(content=f"We've got a choice pack here!")
|
||||
tmp_msg = await pack_channel.send(content="We've got a choice pack here!")
|
||||
|
||||
while True:
|
||||
await view.wait()
|
||||
@ -2097,7 +2166,7 @@ async def open_choice_pack(
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"failed to create cards: {e}")
|
||||
raise ConnectionError(f"Failed to distribute these cards.")
|
||||
raise ConnectionError("Failed to distribute these cards.")
|
||||
|
||||
await db_patch(
|
||||
"packs",
|
||||
@ -2131,7 +2200,7 @@ async def open_choice_pack(
|
||||
|
||||
view = Pagination([author], timeout=30)
|
||||
view.left_button.label = f"Prev: {page_num - 1}/{len(card_embeds)}"
|
||||
view.cancel_button.label = f"Take This Card"
|
||||
view.cancel_button.label = "Take This Card"
|
||||
view.cancel_button.style = discord.ButtonStyle.success
|
||||
view.right_button.label = f"Next: {page_num + 1}/{len(card_embeds)}"
|
||||
if page_num == 1:
|
||||
|
||||
530
tests/test_roll_for_cards.py
Normal file
530
tests/test_roll_for_cards.py
Normal file
@ -0,0 +1,530 @@
|
||||
"""Tests for roll_for_cards parallelized implementation.
|
||||
|
||||
Validates dice rolling, batched player fetches, card creation,
|
||||
pack marking, MVP backfill, cardset-23 dupe fallback, and notifications.
|
||||
"""
|
||||
|
||||
import os
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _make_player(
|
||||
player_id, rarity_value=0, rarity_name="Replacement", p_name="Test Player"
|
||||
):
|
||||
"""Factory for player dicts matching API shape."""
|
||||
return {
|
||||
"player_id": player_id,
|
||||
"rarity": {"value": rarity_value, "name": rarity_name},
|
||||
"p_name": p_name,
|
||||
"description": f"2024 {p_name}",
|
||||
}
|
||||
|
||||
|
||||
_UNSET = object()
|
||||
|
||||
|
||||
def _make_pack(
|
||||
pack_id, team_id=1, pack_type="Standard", pack_team=None, pack_cardset=_UNSET
|
||||
):
|
||||
"""Factory for pack dicts matching API shape."""
|
||||
return {
|
||||
"id": pack_id,
|
||||
"team": {"id": team_id, "abbrev": "TST"},
|
||||
"pack_type": {"name": pack_type},
|
||||
"pack_team": pack_team,
|
||||
"pack_cardset": {"id": 10} if pack_cardset is _UNSET else pack_cardset,
|
||||
}
|
||||
|
||||
|
||||
def _random_response(players):
|
||||
"""Wrap a list of player dicts in the API response shape."""
|
||||
return {"count": len(players), "players": players}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db():
|
||||
"""Patch db_get, db_post, db_patch in helpers.main."""
|
||||
with (
|
||||
patch("helpers.main.db_get", new_callable=AsyncMock) as mock_get,
|
||||
patch("helpers.main.db_post", new_callable=AsyncMock) as mock_post,
|
||||
patch("helpers.main.db_patch", new_callable=AsyncMock) as mock_patch,
|
||||
):
|
||||
mock_post.return_value = True
|
||||
yield mock_get, mock_post, mock_patch
|
||||
|
||||
|
||||
class TestSinglePack:
|
||||
"""Single pack opening — verifies basic flow."""
|
||||
|
||||
async def test_single_standard_pack_creates_cards_and_marks_opened(self, mock_db):
|
||||
"""A single standard pack should fetch players, create cards, and patch open_time.
|
||||
|
||||
Why: Validates the core happy path — dice roll → fetch → create → mark opened.
|
||||
"""
|
||||
mock_get, mock_post, mock_patch = mock_db
|
||||
|
||||
# Return enough players for any rarity tier requested
|
||||
mock_get.return_value = _random_response([_make_player(i) for i in range(10)])
|
||||
|
||||
pack = _make_pack(100)
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
result = await roll_for_cards([pack])
|
||||
|
||||
assert result == [100]
|
||||
# At least one db_get for player fetches
|
||||
assert mock_get.call_count >= 1
|
||||
# Exactly one db_post for cards (may have notif posts too)
|
||||
card_posts = [c for c in mock_post.call_args_list if c.args[0] == "cards"]
|
||||
assert len(card_posts) == 1
|
||||
# Exactly one db_patch for marking pack opened
|
||||
assert mock_patch.call_count == 1
|
||||
assert mock_patch.call_args.kwargs["object_id"] == 100
|
||||
|
||||
async def test_checkin_pack_uses_extra_val(self, mock_db):
|
||||
"""Check-In Player packs should apply extra_val modifier to dice range.
|
||||
|
||||
Why: extra_val shifts the d1000 ceiling, affecting rarity odds for check-in rewards.
|
||||
"""
|
||||
mock_get, mock_post, mock_patch = mock_db
|
||||
mock_get.return_value = _random_response([_make_player(1)])
|
||||
|
||||
pack = _make_pack(200, pack_type="Check-In Player")
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
result = await roll_for_cards([pack], extra_val=500)
|
||||
|
||||
assert result == [200]
|
||||
assert mock_get.call_count >= 1
|
||||
|
||||
async def test_unknown_pack_type_raises(self, mock_db):
|
||||
"""Unrecognized pack types must raise TypeError.
|
||||
|
||||
Why: Guards against silent failures if a new pack type is added without dice logic.
|
||||
"""
|
||||
mock_get, mock_post, mock_patch = mock_db
|
||||
|
||||
pack = _make_pack(300, pack_type="Unknown")
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
with pytest.raises(TypeError, match="Pack type not recognized"):
|
||||
await roll_for_cards([pack])
|
||||
|
||||
|
||||
class TestMultiplePacks:
|
||||
"""Multiple packs — verifies batching and distribution."""
|
||||
|
||||
async def test_multiple_packs_return_all_ids(self, mock_db):
|
||||
"""Opening multiple packs should return all pack IDs.
|
||||
|
||||
Why: Callers use the returned IDs to know which packs were successfully opened.
|
||||
"""
|
||||
mock_get, mock_post, mock_patch = mock_db
|
||||
mock_get.return_value = _random_response([_make_player(i) for i in range(50)])
|
||||
|
||||
packs = [_make_pack(i) for i in range(5)]
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
result = await roll_for_cards(packs)
|
||||
|
||||
assert result == [0, 1, 2, 3, 4]
|
||||
|
||||
async def test_multiple_packs_batch_fetches(self, mock_db):
|
||||
"""Multiple packs should batch fetches — one db_get per rarity tier, not per pack.
|
||||
|
||||
Why: This is the core performance optimization. 5 packs should NOT make 20-30 calls.
|
||||
"""
|
||||
mock_get, mock_post, mock_patch = mock_db
|
||||
mock_get.return_value = _random_response([_make_player(i) for i in range(50)])
|
||||
|
||||
packs = [_make_pack(i) for i in range(5)]
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
await roll_for_cards(packs)
|
||||
|
||||
# Standard packs have up to 6 rarity tiers, but typically fewer are non-zero.
|
||||
# The key assertion: far fewer fetches than 5 packs * ~4 tiers = 20.
|
||||
player_fetches = [
|
||||
c for c in mock_get.call_args_list if c.args[0] == "players/random"
|
||||
]
|
||||
# At most 6 tier fetches + possible 1 MVP backfill = 7
|
||||
assert len(player_fetches) <= 7
|
||||
|
||||
async def test_multiple_packs_create_cards_per_pack(self, mock_db):
|
||||
"""Each pack should get its own db_post('cards') call with correct pack_id.
|
||||
|
||||
Why: Cards must be associated with the correct pack for display and tracking.
|
||||
"""
|
||||
mock_get, mock_post, mock_patch = mock_db
|
||||
mock_get.return_value = _random_response([_make_player(i) for i in range(50)])
|
||||
|
||||
packs = [_make_pack(i) for i in range(3)]
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
await roll_for_cards(packs)
|
||||
|
||||
card_posts = [c for c in mock_post.call_args_list if c.args[0] == "cards"]
|
||||
assert len(card_posts) == 3
|
||||
# Each card post should reference the correct pack_id
|
||||
for i, post_call in enumerate(card_posts):
|
||||
payload = post_call.kwargs["payload"]
|
||||
pack_ids_in_cards = {card["pack_id"] for card in payload["cards"]}
|
||||
assert pack_ids_in_cards == {i}
|
||||
|
||||
|
||||
class TestMVPBackfill:
|
||||
"""MVP fallback when a rarity tier returns fewer players than requested."""
|
||||
|
||||
async def test_shortfall_triggers_mvp_backfill(self, mock_db):
|
||||
"""When a tier returns fewer players than needed, MVP backfill should fire.
|
||||
|
||||
Why: Packs must always contain the expected number of cards. Shortfalls are
|
||||
filled with MVP-tier players as a fallback.
|
||||
"""
|
||||
mock_get, mock_post, mock_patch = mock_db
|
||||
|
||||
call_count = 0
|
||||
|
||||
async def side_effect(endpoint, params=None):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if params and any(
|
||||
p[0] == "min_rarity"
|
||||
and p[1] == 5
|
||||
and any(q[0] == "max_rarity" for q in params) is False
|
||||
for p in params
|
||||
):
|
||||
# MVP backfill call (no max_rarity)
|
||||
return _random_response([_make_player(900, 5, "MVP")])
|
||||
|
||||
# For tier-specific calls, check if this is the MVP backfill
|
||||
if params:
|
||||
param_dict = dict(params)
|
||||
if "max_rarity" not in param_dict:
|
||||
return _random_response([_make_player(900, 5, "MVP")])
|
||||
|
||||
# Return fewer than requested to trigger shortfall
|
||||
requested = 5
|
||||
if params:
|
||||
for key, val in params:
|
||||
if key == "limit":
|
||||
requested = val
|
||||
break
|
||||
return _random_response(
|
||||
[_make_player(i) for i in range(max(0, requested - 1))]
|
||||
)
|
||||
|
||||
mock_get.side_effect = side_effect
|
||||
|
||||
pack = _make_pack(100)
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
result = await roll_for_cards([pack])
|
||||
|
||||
assert result == [100]
|
||||
# Should have at least the tier fetch + backfill call
|
||||
assert mock_get.call_count >= 2
|
||||
|
||||
|
||||
class TestCardsetExclusion:
|
||||
"""Cardset 23 should duplicate existing players instead of MVP backfill."""
|
||||
|
||||
async def test_cardset_23_duplicates_instead_of_mvp(self, mock_db):
|
||||
"""For cardset 23, shortfalls should duplicate existing players, not fetch MVPs.
|
||||
|
||||
Why: Cardset 23 (special/limited cardset) shouldn't pull from the MVP pool —
|
||||
it should fill gaps by duplicating from what's already available.
|
||||
"""
|
||||
mock_get, mock_post, mock_patch = mock_db
|
||||
|
||||
async def side_effect(endpoint, params=None):
|
||||
if params:
|
||||
param_dict = dict(params)
|
||||
# If this is a backfill call (no max_rarity), it shouldn't happen
|
||||
if "max_rarity" not in param_dict:
|
||||
pytest.fail("Should not make MVP backfill call for cardset 23")
|
||||
# Return fewer than requested
|
||||
return _random_response([_make_player(1)])
|
||||
|
||||
mock_get.side_effect = side_effect
|
||||
|
||||
pack = _make_pack(100, pack_cardset={"id": 23})
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
# Force specific dice rolls to ensure a shortfall
|
||||
with patch("helpers.main.random.randint", return_value=1):
|
||||
# d1000=1 for Standard: Rep, Rep, Rep, Rep, Rep → 5 Reps needed
|
||||
result = await roll_for_cards([pack])
|
||||
|
||||
assert result == [100]
|
||||
|
||||
|
||||
class TestNotifications:
|
||||
"""Rare pull notifications should be gathered and sent."""
|
||||
|
||||
async def test_rare_pulls_generate_notifications(self, mock_db):
|
||||
"""Players with rarity >= 3 should trigger notification posts.
|
||||
|
||||
Why: Rare pulls are announced to the community — all notifs should be sent.
|
||||
"""
|
||||
mock_get, mock_post, mock_patch = mock_db
|
||||
|
||||
rare_player = _make_player(
|
||||
42, rarity_value=3, rarity_name="All-Star", p_name="Mike Trout"
|
||||
)
|
||||
mock_get.return_value = _random_response([rare_player])
|
||||
|
||||
pack = _make_pack(100)
|
||||
# Force all dice to land on All-Star tier (d1000=951 for card 3)
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
with patch("helpers.main.random.randint", return_value=960):
|
||||
await roll_for_cards([pack])
|
||||
|
||||
notif_posts = [c for c in mock_post.call_args_list if c.args[0] == "notifs"]
|
||||
assert len(notif_posts) >= 1
|
||||
payload = notif_posts[0].kwargs["payload"]
|
||||
assert payload["title"] == "Rare Pull"
|
||||
assert "Mike Trout" in payload["field_name"]
|
||||
|
||||
async def test_no_notifications_for_common_pulls(self, mock_db):
|
||||
"""Players with rarity < 3 should NOT trigger notifications.
|
||||
|
||||
Why: Only rare pulls are noteworthy — common cards would spam the notif feed.
|
||||
"""
|
||||
mock_get, mock_post, mock_patch = mock_db
|
||||
|
||||
common_player = _make_player(1, rarity_value=0, rarity_name="Replacement")
|
||||
mock_get.return_value = _random_response([common_player])
|
||||
|
||||
pack = _make_pack(100)
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
# Force low dice rolls (all Replacement)
|
||||
with patch("helpers.main.random.randint", return_value=1):
|
||||
await roll_for_cards([pack])
|
||||
|
||||
notif_posts = [c for c in mock_post.call_args_list if c.args[0] == "notifs"]
|
||||
assert len(notif_posts) == 0
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
"""Error propagation from gathered writes."""
|
||||
|
||||
async def test_card_creation_failure_raises(self, mock_db):
|
||||
"""If db_post('cards') returns falsy, ConnectionError must propagate.
|
||||
|
||||
Why: Card creation failure means the pack wasn't properly opened — caller
|
||||
needs to know so it can report the error to the user.
|
||||
"""
|
||||
mock_get, mock_post, mock_patch = mock_db
|
||||
mock_get.return_value = _random_response([_make_player(1)])
|
||||
mock_post.return_value = False # Simulate failure
|
||||
|
||||
pack = _make_pack(100)
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
with pytest.raises(ConnectionError, match="Failed to create"):
|
||||
await roll_for_cards([pack])
|
||||
|
||||
|
||||
class TestPackTeamFiltering:
|
||||
"""Verify correct filter params are passed to player fetch."""
|
||||
|
||||
async def test_pack_team_adds_franchise_filter(self, mock_db):
|
||||
"""When pack has a pack_team, franchise filter should be applied.
|
||||
|
||||
Why: Team-specific packs should only contain players from that franchise.
|
||||
"""
|
||||
mock_get, mock_post, mock_patch = mock_db
|
||||
mock_get.return_value = _random_response([_make_player(1)])
|
||||
|
||||
pack = _make_pack(
|
||||
100,
|
||||
pack_team={"sname": "NYY"},
|
||||
pack_cardset=None,
|
||||
)
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
with patch("helpers.main.random.randint", return_value=1):
|
||||
await roll_for_cards([pack])
|
||||
|
||||
# Check that tier-fetch calls (those with max_rarity) include franchise filter
|
||||
tier_calls = [
|
||||
c
|
||||
for c in mock_get.call_args_list
|
||||
if any(p[0] == "max_rarity" for p in (c.kwargs.get("params") or []))
|
||||
]
|
||||
assert len(tier_calls) >= 1
|
||||
for c in tier_calls:
|
||||
param_dict = dict(c.kwargs.get("params") or [])
|
||||
assert param_dict.get("franchise") == "NYY"
|
||||
assert param_dict.get("in_packs") is True
|
||||
|
||||
async def test_no_team_no_cardset_adds_in_packs(self, mock_db):
|
||||
"""When pack has no team or cardset, in_packs filter should be applied.
|
||||
|
||||
Why: Generic packs still need the in_packs filter to exclude non-packable players.
|
||||
"""
|
||||
mock_get, mock_post, mock_patch = mock_db
|
||||
mock_get.return_value = _random_response([_make_player(1)])
|
||||
|
||||
pack = _make_pack(100, pack_team=None, pack_cardset=None)
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
with patch("helpers.main.random.randint", return_value=1):
|
||||
await roll_for_cards([pack])
|
||||
|
||||
# Check that tier-fetch calls (those with max_rarity) include in_packs filter
|
||||
tier_calls = [
|
||||
c
|
||||
for c in mock_get.call_args_list
|
||||
if any(p[0] == "max_rarity" for p in (c.kwargs.get("params") or []))
|
||||
]
|
||||
assert len(tier_calls) >= 1
|
||||
for c in tier_calls:
|
||||
param_dict = dict(c.kwargs.get("params") or [])
|
||||
assert param_dict.get("in_packs") is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integration tests — hit real dev API for reads, mock all writes
|
||||
# ---------------------------------------------------------------------------
|
||||
requires_api = pytest.mark.skipif(
|
||||
not os.environ.get("API_TOKEN"),
|
||||
reason="API_TOKEN not set — skipping integration tests",
|
||||
)
|
||||
|
||||
|
||||
@requires_api
|
||||
class TestIntegrationRealFetches:
|
||||
"""Integration tests that hit the real dev API for player fetches.
|
||||
|
||||
Only db_get is real — db_post and db_patch are mocked to prevent writes.
|
||||
Run with: API_TOKEN=<token> python -m pytest tests/test_roll_for_cards.py -k integration -v
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_writes(self):
|
||||
"""Mock only write operations, let reads hit the real API."""
|
||||
with (
|
||||
patch("helpers.main.db_post", new_callable=AsyncMock) as mock_post,
|
||||
patch("helpers.main.db_patch", new_callable=AsyncMock) as mock_patch,
|
||||
):
|
||||
mock_post.return_value = True
|
||||
yield mock_post, mock_patch
|
||||
|
||||
async def test_integration_single_pack_fetches_real_players(self, mock_writes):
|
||||
"""A single standard pack should fetch real players from the dev API.
|
||||
|
||||
Why: Validates that the batched fetch params (min_rarity, max_rarity, limit,
|
||||
in_packs) produce valid responses from the real API and that the returned
|
||||
players have the expected structure.
|
||||
"""
|
||||
mock_post, mock_patch = mock_writes
|
||||
|
||||
pack = _make_pack(9999)
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
result = await roll_for_cards([pack])
|
||||
|
||||
assert result == [9999]
|
||||
# Cards were "created" (mocked)
|
||||
card_posts = [c for c in mock_post.call_args_list if c.args[0] == "cards"]
|
||||
assert len(card_posts) == 1
|
||||
payload = card_posts[0].kwargs["payload"]
|
||||
# Standard pack produces 5 cards
|
||||
assert len(payload["cards"]) == 5
|
||||
# Each card has the expected structure
|
||||
for card in payload["cards"]:
|
||||
assert "player_id" in card
|
||||
assert card["team_id"] == 1
|
||||
assert card["pack_id"] == 9999
|
||||
|
||||
async def test_integration_multiple_packs_batch_correctly(self, mock_writes):
|
||||
"""Multiple packs should batch fetches and distribute players correctly.
|
||||
|
||||
Why: Validates the core optimization — summing counts across packs, making
|
||||
fewer API calls, and slicing players back into per-pack groups with real data.
|
||||
"""
|
||||
mock_post, mock_patch = mock_writes
|
||||
|
||||
packs = [_make_pack(i + 9000) for i in range(3)]
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
result = await roll_for_cards(packs)
|
||||
|
||||
assert result == [9000, 9001, 9002]
|
||||
card_posts = [c for c in mock_post.call_args_list if c.args[0] == "cards"]
|
||||
assert len(card_posts) == 3
|
||||
# Each pack should have exactly 5 cards (Standard packs)
|
||||
total_cards = 0
|
||||
for post_call in card_posts:
|
||||
cards = post_call.kwargs["payload"]["cards"]
|
||||
assert len(cards) == 5
|
||||
total_cards += len(cards)
|
||||
assert total_cards == 15
|
||||
|
||||
async def test_integration_players_have_valid_rarity(self, mock_writes):
|
||||
"""Fetched players should have rarity values matching their requested tier.
|
||||
|
||||
Why: Confirms the API respects min_rarity/max_rarity filters and that
|
||||
the player distribution logic assigns correct-tier players to each pack.
|
||||
"""
|
||||
mock_post, mock_patch = mock_writes
|
||||
|
||||
pack = _make_pack(9999)
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
# Use fixed dice to get known rarity distribution
|
||||
# d1000=500 for Standard: Rep, Res, Sta, Res, Sta (mix of low tiers)
|
||||
with patch("helpers.main.random.randint", return_value=500):
|
||||
await roll_for_cards([pack])
|
||||
|
||||
card_posts = [c for c in mock_post.call_args_list if c.args[0] == "cards"]
|
||||
assert len(card_posts) == 1
|
||||
cards = card_posts[0].kwargs["payload"]["cards"]
|
||||
# All cards should have valid player_ids (positive integers from real API)
|
||||
for card in cards:
|
||||
assert isinstance(card["player_id"], int)
|
||||
assert card["player_id"] > 0
|
||||
|
||||
async def test_integration_cardset_filter(self, mock_writes):
|
||||
"""Packs with a specific cardset should only fetch players from that cardset.
|
||||
|
||||
Why: Validates that the cardset_id parameter is correctly passed through
|
||||
the batched fetch and the API filters accordingly.
|
||||
"""
|
||||
mock_post, mock_patch = mock_writes
|
||||
|
||||
pack = _make_pack(9999, pack_cardset={"id": 24})
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
with patch("helpers.main.random.randint", return_value=500):
|
||||
result = await roll_for_cards([pack])
|
||||
|
||||
assert result == [9999]
|
||||
card_posts = [c for c in mock_post.call_args_list if c.args[0] == "cards"]
|
||||
assert len(card_posts) == 1
|
||||
assert len(card_posts[0].kwargs["payload"]["cards"]) == 5
|
||||
|
||||
async def test_integration_checkin_pack(self, mock_writes):
|
||||
"""Check-In Player pack should fetch exactly 1 player from the real API.
|
||||
|
||||
Why: Check-in packs produce a single card — validates the simplest
|
||||
path through the batched fetch logic with real data.
|
||||
"""
|
||||
mock_post, mock_patch = mock_writes
|
||||
|
||||
pack = _make_pack(9999, pack_type="Check-In Player")
|
||||
from helpers.main import roll_for_cards
|
||||
|
||||
result = await roll_for_cards([pack])
|
||||
|
||||
assert result == [9999]
|
||||
card_posts = [c for c in mock_post.call_args_list if c.args[0] == "cards"]
|
||||
assert len(card_posts) == 1
|
||||
# Check-in packs produce exactly 1 card
|
||||
assert len(card_posts[0].kwargs["payload"]["cards"]) == 1
|
||||
Loading…
Reference in New Issue
Block a user