Compare commits
1 Commits
main
...
fix/refrac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf440e8301 |
54
.env.example
54
.env.example
@ -1,54 +0,0 @@
|
||||
# Paper Dynasty Discord Bot - Environment Configuration
|
||||
# Copy this file to .env and fill in your actual values.
|
||||
# DO NOT commit .env to version control!
|
||||
|
||||
# =============================================================================
|
||||
# BOT CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Discord bot token (from Discord Developer Portal)
|
||||
BOT_TOKEN=your-discord-bot-token-here
|
||||
|
||||
# Discord server (guild) ID
|
||||
GUILD_ID=your-guild-id-here
|
||||
|
||||
# Channel ID for scoreboard messages
|
||||
SCOREBOARD_CHANNEL=your-scoreboard-channel-id-here
|
||||
|
||||
# =============================================================================
|
||||
# API CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Paper Dynasty API authentication token
|
||||
API_TOKEN=your-api-token-here
|
||||
|
||||
# Target database environment: 'dev' or 'prod'
|
||||
# Default: dev
|
||||
DATABASE=dev
|
||||
|
||||
# =============================================================================
|
||||
# APPLICATION CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Logging level: DEBUG, INFO, WARNING, ERROR
|
||||
# Default: INFO
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# Python hash seed (set for reproducibility in tests)
|
||||
PYTHONHASHSEED=0
|
||||
|
||||
# =============================================================================
|
||||
# DATABASE CONFIGURATION (PostgreSQL — used by gameplay_models.py)
|
||||
# =============================================================================
|
||||
|
||||
DB_USERNAME=your_db_username
|
||||
DB_PASSWORD=your_db_password
|
||||
DB_URL=localhost
|
||||
DB_NAME=postgres
|
||||
|
||||
# =============================================================================
|
||||
# WEBHOOKS
|
||||
# =============================================================================
|
||||
|
||||
# Discord webhook URL for restart notifications
|
||||
RESTART_WEBHOOK_URL=https://discord.com/api/webhooks/your-webhook-id/your-webhook-token
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -133,7 +133,6 @@ dmypy.json
|
||||
storage*
|
||||
storage/paper-dynasty-service-creds.json
|
||||
*compose.yml
|
||||
!docker-compose.example.yml
|
||||
**.db
|
||||
**/htmlcov
|
||||
.vscode/**
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
# ruff: noqa: F403, F405
|
||||
import copy
|
||||
|
||||
import helpers
|
||||
@ -696,9 +695,6 @@ class Economy(commands.Cog):
|
||||
)
|
||||
return
|
||||
|
||||
# Pack types that are auto-opened and should not appear in the manual open menu
|
||||
AUTO_OPEN_TYPES = {"Check-In Player"}
|
||||
|
||||
# Group packs by type and customization (e.g. Standard, Standard-Orioles, Standard-2012, Premium)
|
||||
p_count = 0
|
||||
p_data = {
|
||||
@ -715,11 +711,6 @@ class Economy(commands.Cog):
|
||||
p_group = None
|
||||
logger.debug(f"pack: {pack}")
|
||||
logger.debug(f"pack cardset: {pack['pack_cardset']}")
|
||||
if pack["pack_type"]["name"] in AUTO_OPEN_TYPES:
|
||||
logger.debug(
|
||||
f"Skipping auto-open pack type: {pack['pack_type']['name']}"
|
||||
)
|
||||
continue
|
||||
if pack["pack_team"] is None and pack["pack_cardset"] is None:
|
||||
p_group = pack["pack_type"]["name"]
|
||||
# Add to p_data if this is a new pack type
|
||||
@ -782,9 +773,6 @@ class Economy(commands.Cog):
|
||||
pretty_name = f"{key.split('-')[0]} - {key.split('-')[3]}"
|
||||
elif "Cardset" in key:
|
||||
pretty_name = f"{key.split('-')[0]} - {key.split('-')[3]}"
|
||||
else:
|
||||
# Pack type name contains a hyphen (e.g. "Check-In Player")
|
||||
pretty_name = key
|
||||
|
||||
if pretty_name is not None:
|
||||
embed.add_field(name=pretty_name, value=f"Qty: {len(p_data[key])}")
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Economy Packs Module
|
||||
# Economy Packs Module
|
||||
# Contains pack opening, daily rewards, and donation commands from the original economy.py
|
||||
|
||||
import logging
|
||||
@ -9,135 +9,97 @@ import datetime
|
||||
|
||||
# Import specific utilities needed by this module
|
||||
import random
|
||||
from api_calls import db_get, db_post
|
||||
from api_calls import db_get, db_post, db_patch
|
||||
from helpers.constants import PD_PLAYERS_ROLE_NAME, PD_PLAYERS, IMAGES
|
||||
from helpers import (
|
||||
get_team_by_owner,
|
||||
display_cards,
|
||||
give_packs,
|
||||
legal_channel,
|
||||
get_channel,
|
||||
get_cal_user,
|
||||
refresh_sheet,
|
||||
roll_for_cards,
|
||||
int_timestamp,
|
||||
get_context_user,
|
||||
get_team_by_owner, display_cards, give_packs, legal_channel, get_channel,
|
||||
get_cal_user, refresh_sheet, roll_for_cards, int_timestamp, get_context_user
|
||||
)
|
||||
from helpers.discord_utils import get_team_embed, get_emoji
|
||||
from helpers.discord_utils import get_team_embed, send_to_channel, get_emoji
|
||||
from discord_ui import SelectView, SelectOpenPack
|
||||
|
||||
|
||||
logger = logging.getLogger("discord_app")
|
||||
logger = logging.getLogger('discord_app')
|
||||
|
||||
|
||||
class Packs(commands.Cog):
|
||||
"""Pack management, daily rewards, and donation system for Paper Dynasty."""
|
||||
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.hybrid_group(name="donation", help="Mod: Give packs for PD donations")
|
||||
@commands.hybrid_group(name='donation', help='Mod: Give packs for PD donations')
|
||||
@commands.has_any_role(PD_PLAYERS_ROLE_NAME)
|
||||
async def donation(self, ctx: commands.Context):
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send(
|
||||
"To buy packs, visit https://ko-fi.com/manticorum/shop and include your discord username!"
|
||||
)
|
||||
await ctx.send('To buy packs, visit https://ko-fi.com/manticorum/shop and include your discord username!')
|
||||
|
||||
@donation.command(
|
||||
name="premium", help="Mod: Give premium packs", aliases=["p", "prem"]
|
||||
)
|
||||
@donation.command(name='premium', help='Mod: Give premium packs', aliases=['p', 'prem'])
|
||||
async def donation_premium(self, ctx: commands.Context, num_packs: int, gm: Member):
|
||||
if ctx.author.id != self.bot.owner_id:
|
||||
await ctx.send("Wait a second. You're not in charge here!")
|
||||
await ctx.send('Wait a second. You\'re not in charge here!')
|
||||
return
|
||||
|
||||
team = await get_team_by_owner(gm.id)
|
||||
p_query = await db_get("packtypes", params=[("name", "Premium")])
|
||||
if p_query["count"] == 0:
|
||||
await ctx.send("Oof. I couldn't find a Premium Pack")
|
||||
p_query = await db_get('packtypes', params=[('name', 'Premium')])
|
||||
if p_query['count'] == 0:
|
||||
await ctx.send('Oof. I couldn\'t find a Premium Pack')
|
||||
return
|
||||
|
||||
total_packs = await give_packs(
|
||||
team, num_packs, pack_type=p_query["packtypes"][0]
|
||||
)
|
||||
await ctx.send(
|
||||
f"The {team['lname']} now have {total_packs['count']} total packs!"
|
||||
)
|
||||
total_packs = await give_packs(team, num_packs, pack_type=p_query['packtypes'][0])
|
||||
await ctx.send(f'The {team["lname"]} now have {total_packs["count"]} total packs!')
|
||||
|
||||
@donation.command(
|
||||
name="standard", help="Mod: Give standard packs", aliases=["s", "sta"]
|
||||
)
|
||||
async def donation_standard(
|
||||
self, ctx: commands.Context, num_packs: int, gm: Member
|
||||
):
|
||||
@donation.command(name='standard', help='Mod: Give standard packs', aliases=['s', 'sta'])
|
||||
async def donation_standard(self, ctx: commands.Context, num_packs: int, gm: Member):
|
||||
if ctx.author.id != self.bot.owner_id:
|
||||
await ctx.send("Wait a second. You're not in charge here!")
|
||||
await ctx.send('Wait a second. You\'re not in charge here!')
|
||||
return
|
||||
|
||||
team = await get_team_by_owner(gm.id)
|
||||
p_query = await db_get("packtypes", params=[("name", "Standard")])
|
||||
if p_query["count"] == 0:
|
||||
await ctx.send("Oof. I couldn't find a Standard Pack")
|
||||
p_query = await db_get('packtypes', params=[('name', 'Standard')])
|
||||
if p_query['count'] == 0:
|
||||
await ctx.send('Oof. I couldn\'t find a Standard Pack')
|
||||
return
|
||||
|
||||
total_packs = await give_packs(
|
||||
team, num_packs, pack_type=p_query["packtypes"][0]
|
||||
)
|
||||
await ctx.send(
|
||||
f"The {team['lname']} now have {total_packs['count']} total packs!"
|
||||
)
|
||||
total_packs = await give_packs(team, num_packs, pack_type=p_query['packtypes'][0])
|
||||
await ctx.send(f'The {team["lname"]} now have {total_packs["count"]} total packs!')
|
||||
|
||||
@commands.hybrid_command(name="lastpack", help="Replay your last pack")
|
||||
@commands.hybrid_command(name='lastpack', help='Replay your last pack')
|
||||
@commands.check(legal_channel)
|
||||
@commands.has_any_role(PD_PLAYERS_ROLE_NAME)
|
||||
async def last_pack_command(self, ctx: commands.Context):
|
||||
team = await get_team_by_owner(get_context_user(ctx).id)
|
||||
if not team:
|
||||
await ctx.send(
|
||||
"I don't see a team for you, yet. You can sign up with the `/newteam` command!"
|
||||
)
|
||||
await ctx.send(f'I don\'t see a team for you, yet. You can sign up with the `/newteam` command!')
|
||||
return
|
||||
|
||||
p_query = await db_get(
|
||||
"packs",
|
||||
params=[
|
||||
("opened", True),
|
||||
("team_id", team["id"]),
|
||||
("new_to_old", True),
|
||||
("limit", 1),
|
||||
],
|
||||
'packs',
|
||||
params=[('opened', True), ('team_id', team['id']), ('new_to_old', True), ('limit', 1)]
|
||||
)
|
||||
if not p_query["count"]:
|
||||
await ctx.send("I do not see any packs for you, bub.")
|
||||
if not p_query['count']:
|
||||
await ctx.send(f'I do not see any packs for you, bub.')
|
||||
return
|
||||
|
||||
pack_name = p_query["packs"][0]["pack_type"]["name"]
|
||||
if pack_name == "Standard":
|
||||
pack_cover = IMAGES["pack-sta"]
|
||||
elif pack_name == "Premium":
|
||||
pack_cover = IMAGES["pack-pre"]
|
||||
pack_name = p_query['packs'][0]['pack_type']['name']
|
||||
if pack_name == 'Standard':
|
||||
pack_cover = IMAGES['pack-sta']
|
||||
elif pack_name == 'Premium':
|
||||
pack_cover = IMAGES['pack-pre']
|
||||
else:
|
||||
pack_cover = None
|
||||
|
||||
c_query = await db_get("cards", params=[("pack_id", p_query["packs"][0]["id"])])
|
||||
if not c_query["count"]:
|
||||
await ctx.send("Hmm...I didn't see any cards in that pack.")
|
||||
c_query = await db_get(
|
||||
'cards',
|
||||
params=[('pack_id', p_query['packs'][0]['id'])]
|
||||
)
|
||||
if not c_query['count']:
|
||||
await ctx.send(f'Hmm...I didn\'t see any cards in that pack.')
|
||||
return
|
||||
|
||||
await display_cards(
|
||||
c_query["cards"],
|
||||
team,
|
||||
ctx.channel,
|
||||
ctx.author,
|
||||
self.bot,
|
||||
pack_cover=pack_cover,
|
||||
)
|
||||
await display_cards(c_query['cards'], team, ctx.channel, ctx.author, self.bot, pack_cover=pack_cover)
|
||||
|
||||
@app_commands.command(
|
||||
name="comeonmanineedthis",
|
||||
description="Daily check-in for cards, currency, and packs",
|
||||
)
|
||||
@app_commands.command(name='comeonmanineedthis', description='Daily check-in for cards, currency, and packs')
|
||||
@commands.has_any_role(PD_PLAYERS)
|
||||
@commands.check(legal_channel)
|
||||
async def daily_checkin(self, interaction: discord.Interaction):
|
||||
@ -145,127 +107,97 @@ class Packs(commands.Cog):
|
||||
team = await get_team_by_owner(interaction.user.id)
|
||||
if not team:
|
||||
await interaction.edit_original_response(
|
||||
content="I don't see a team for you, yet. You can sign up with the `/newteam` command!"
|
||||
content=f'I don\'t see a team for you, yet. You can sign up with the `/newteam` command!'
|
||||
)
|
||||
return
|
||||
|
||||
current = await db_get("current")
|
||||
current = await db_get('current')
|
||||
now = datetime.datetime.now()
|
||||
midnight = int_timestamp(
|
||||
datetime.datetime(now.year, now.month, now.day, 0, 0, 0)
|
||||
)
|
||||
daily = await db_get(
|
||||
"rewards",
|
||||
params=[
|
||||
("name", "Daily Check-in"),
|
||||
("team_id", team["id"]),
|
||||
("created_after", midnight),
|
||||
],
|
||||
)
|
||||
logger.debug(f"midnight: {midnight} / now: {int_timestamp(now)}")
|
||||
logger.debug(f"daily_return: {daily}")
|
||||
midnight = int_timestamp(datetime.datetime(now.year, now.month, now.day, 0, 0, 0))
|
||||
daily = await db_get('rewards', params=[
|
||||
('name', 'Daily Check-in'), ('team_id', team['id']), ('created_after', midnight)
|
||||
])
|
||||
logger.debug(f'midnight: {midnight} / now: {int_timestamp(now)}')
|
||||
logger.debug(f'daily_return: {daily}')
|
||||
|
||||
if daily:
|
||||
await interaction.edit_original_response(
|
||||
content="Looks like you already checked in today - come back at midnight Central!"
|
||||
content=f'Looks like you already checked in today - come back at midnight Central!'
|
||||
)
|
||||
return
|
||||
|
||||
await db_post(
|
||||
"rewards",
|
||||
payload={
|
||||
"name": "Daily Check-in",
|
||||
"team_id": team["id"],
|
||||
"season": current["season"],
|
||||
"week": current["week"],
|
||||
"created": int_timestamp(now),
|
||||
},
|
||||
)
|
||||
current = await db_get("current")
|
||||
check_ins = await db_get(
|
||||
"rewards",
|
||||
params=[
|
||||
("name", "Daily Check-in"),
|
||||
("team_id", team["id"]),
|
||||
("season", current["season"]),
|
||||
],
|
||||
)
|
||||
await db_post('rewards', payload={
|
||||
'name': 'Daily Check-in', 'team_id': team['id'], 'season': current['season'], 'week': current['week'],
|
||||
'created': int_timestamp(now)
|
||||
})
|
||||
current = await db_get('current')
|
||||
check_ins = await db_get('rewards', params=[
|
||||
('name', 'Daily Check-in'), ('team_id', team['id']), ('season', current['season'])
|
||||
])
|
||||
|
||||
check_count = check_ins["count"] % 5
|
||||
check_count = check_ins['count'] % 5
|
||||
|
||||
# 2nd, 4th, and 5th check-ins
|
||||
if check_count == 0 or check_count % 2 == 0:
|
||||
# Every fifth check-in
|
||||
if check_count == 0:
|
||||
greeting = await interaction.edit_original_response(
|
||||
content="Hey, you just earned a Standard pack of cards!"
|
||||
content=f'Hey, you just earned a Standard pack of cards!'
|
||||
)
|
||||
pack_channel = get_channel(interaction, "pack-openings")
|
||||
pack_channel = get_channel(interaction, 'pack-openings')
|
||||
|
||||
p_query = await db_get("packtypes", params=[("name", "Standard")])
|
||||
p_query = await db_get('packtypes', params=[('name', 'Standard')])
|
||||
if not p_query:
|
||||
await interaction.edit_original_response(
|
||||
content=f"I was not able to pull this pack for you. "
|
||||
f"Maybe ping {get_cal_user(interaction).mention}?"
|
||||
content=f'I was not able to pull this pack for you. '
|
||||
f'Maybe ping {get_cal_user(interaction).mention}?'
|
||||
)
|
||||
return
|
||||
|
||||
# Every second and fourth check-in
|
||||
else:
|
||||
greeting = await interaction.edit_original_response(
|
||||
content="Hey, you just earned a player card!"
|
||||
content=f'Hey, you just earned a player card!'
|
||||
)
|
||||
pack_channel = interaction.channel
|
||||
|
||||
p_query = await db_get(
|
||||
"packtypes", params=[("name", "Check-In Player")]
|
||||
)
|
||||
p_query = await db_get('packtypes', params=[('name', 'Check-In Player')])
|
||||
if not p_query:
|
||||
await interaction.edit_original_response(
|
||||
content=f"I was not able to pull this card for you. "
|
||||
f"Maybe ping {get_cal_user(interaction).mention}?"
|
||||
content=f'I was not able to pull this card for you. '
|
||||
f'Maybe ping {get_cal_user(interaction).mention}?'
|
||||
)
|
||||
return
|
||||
|
||||
await give_packs(team, 1, p_query["packtypes"][0])
|
||||
await give_packs(team, 1, p_query['packtypes'][0])
|
||||
p_query = await db_get(
|
||||
"packs",
|
||||
params=[
|
||||
("opened", False),
|
||||
("team_id", team["id"]),
|
||||
("new_to_old", True),
|
||||
("limit", 1),
|
||||
],
|
||||
'packs',
|
||||
params=[('opened', False), ('team_id', team['id']), ('new_to_old', True), ('limit', 1)]
|
||||
)
|
||||
if not p_query["count"]:
|
||||
if not p_query['count']:
|
||||
await interaction.edit_original_response(
|
||||
content=f"I do not see any packs in here. {await get_emoji(interaction, 'ConfusedPsyduck')}"
|
||||
)
|
||||
content=f'I do not see any packs in here. {await get_emoji(interaction, "ConfusedPsyduck")}')
|
||||
return
|
||||
|
||||
pack_ids = await roll_for_cards(
|
||||
p_query["packs"], extra_val=check_ins["count"]
|
||||
)
|
||||
pack_ids = await roll_for_cards(p_query['packs'], extra_val=check_ins['count'])
|
||||
if not pack_ids:
|
||||
await greeting.edit(
|
||||
content=f"I was not able to create these cards {await get_emoji(interaction, 'slight_frown')}"
|
||||
content=f'I was not able to create these cards {await get_emoji(interaction, "slight_frown")}'
|
||||
)
|
||||
return
|
||||
|
||||
all_cards = []
|
||||
for p_id in pack_ids:
|
||||
new_cards = await db_get("cards", params=[("pack_id", p_id)])
|
||||
all_cards.extend(new_cards["cards"])
|
||||
new_cards = await db_get('cards', params=[('pack_id', p_id)])
|
||||
all_cards.extend(new_cards['cards'])
|
||||
|
||||
if not all_cards:
|
||||
await interaction.edit_original_response(
|
||||
content=f"I was not able to pull these cards {await get_emoji(interaction, 'slight_frown')}"
|
||||
content=f'I was not able to pull these cards {await get_emoji(interaction, "slight_frown")}'
|
||||
)
|
||||
return
|
||||
|
||||
await display_cards(
|
||||
all_cards, team, pack_channel, interaction.user, self.bot
|
||||
)
|
||||
await display_cards(all_cards, team, pack_channel, interaction.user, self.bot)
|
||||
await refresh_sheet(team, self.bot)
|
||||
return
|
||||
|
||||
@ -283,102 +215,87 @@ class Packs(commands.Cog):
|
||||
else:
|
||||
m_reward = 25
|
||||
|
||||
team = await db_post(f"teams/{team['id']}/money/{m_reward}")
|
||||
team = await db_post(f'teams/{team["id"]}/money/{m_reward}')
|
||||
await interaction.edit_original_response(
|
||||
content=f"You just earned {m_reward}₼! That brings your wallet to {team['wallet']}₼!"
|
||||
)
|
||||
content=f'You just earned {m_reward}₼! That brings your wallet to {team["wallet"]}₼!')
|
||||
|
||||
@app_commands.command(
|
||||
name="open-packs", description="Open packs from your inventory"
|
||||
)
|
||||
@app_commands.command(name='open-packs', description='Open packs from your inventory')
|
||||
@app_commands.checks.has_any_role(PD_PLAYERS)
|
||||
async def open_packs_slash(self, interaction: discord.Interaction):
|
||||
if interaction.channel.name in [
|
||||
"paper-dynasty-chat",
|
||||
"pd-news-ticker",
|
||||
"pd-network-news",
|
||||
]:
|
||||
if interaction.channel.name in ['paper-dynasty-chat', 'pd-news-ticker', 'pd-network-news']:
|
||||
await interaction.response.send_message(
|
||||
f"Please head to down to {get_channel(interaction, 'pd-bot-hole')} to run this command.",
|
||||
ephemeral=True,
|
||||
f'Please head to down to {get_channel(interaction, "pd-bot-hole")} to run this command.',
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
owner_team = await get_team_by_owner(interaction.user.id)
|
||||
if not owner_team:
|
||||
await interaction.response.send_message(
|
||||
"I don't see a team for you, yet. You can sign up with the `/newteam` command!"
|
||||
f'I don\'t see a team for you, yet. You can sign up with the `/newteam` command!'
|
||||
)
|
||||
return
|
||||
|
||||
p_query = await db_get(
|
||||
"packs", params=[("team_id", owner_team["id"]), ("opened", False)]
|
||||
)
|
||||
if p_query["count"] == 0:
|
||||
p_query = await db_get('packs', params=[
|
||||
('team_id', owner_team['id']), ('opened', False)
|
||||
])
|
||||
if p_query['count'] == 0:
|
||||
await interaction.response.send_message(
|
||||
"Looks like you are clean out of packs, friendo. You can earn them by playing PD games or by "
|
||||
"donating to the league."
|
||||
f'Looks like you are clean out of packs, friendo. You can earn them by playing PD games or by '
|
||||
f'donating to the league.'
|
||||
)
|
||||
return
|
||||
|
||||
# Pack types that are auto-opened and should not appear in the manual open menu
|
||||
AUTO_OPEN_TYPES = {"Check-In Player"}
|
||||
|
||||
# Group packs by type and customization (e.g. Standard, Standard-Orioles, Standard-2012, Premium)
|
||||
p_count = 0
|
||||
p_data = {
|
||||
"Standard": [],
|
||||
"Premium": [],
|
||||
"Daily": [],
|
||||
"MVP": [],
|
||||
"All Star": [],
|
||||
"Mario": [],
|
||||
"Team Choice": [],
|
||||
'Standard': [],
|
||||
'Premium': [],
|
||||
'Daily': [],
|
||||
'MVP': [],
|
||||
'All Star': [],
|
||||
'Mario': [],
|
||||
'Team Choice': []
|
||||
}
|
||||
logger.debug("Parsing packs...")
|
||||
for pack in p_query["packs"]:
|
||||
logger.debug(f'Parsing packs...')
|
||||
for pack in p_query['packs']:
|
||||
p_group = None
|
||||
logger.debug(f"pack: {pack}")
|
||||
logger.debug(f"pack cardset: {pack['pack_cardset']}")
|
||||
if pack["pack_type"]["name"] in AUTO_OPEN_TYPES:
|
||||
logger.debug(
|
||||
f"Skipping auto-open pack type: {pack['pack_type']['name']}"
|
||||
)
|
||||
continue
|
||||
if pack["pack_team"] is None and pack["pack_cardset"] is None:
|
||||
p_group = pack["pack_type"]["name"]
|
||||
logger.debug(f'pack: {pack}')
|
||||
logger.debug(f'pack cardset: {pack["pack_cardset"]}')
|
||||
if pack['pack_team'] is None and pack['pack_cardset'] is None:
|
||||
p_group = pack['pack_type']['name']
|
||||
# Add to p_data if this is a new pack type
|
||||
if p_group not in p_data:
|
||||
p_data[p_group] = []
|
||||
|
||||
elif pack["pack_team"] is not None:
|
||||
if pack["pack_type"]["name"] == "Standard":
|
||||
p_group = f"Standard-Team-{pack['pack_team']['id']}-{pack['pack_team']['sname']}"
|
||||
elif pack["pack_type"]["name"] == "Premium":
|
||||
p_group = f"Premium-Team-{pack['pack_team']['id']}-{pack['pack_team']['sname']}"
|
||||
elif pack["pack_type"]["name"] == "Team Choice":
|
||||
p_group = f"Team Choice-Team-{pack['pack_team']['id']}-{pack['pack_team']['sname']}"
|
||||
elif pack["pack_type"]["name"] == "MVP":
|
||||
p_group = f"MVP-Team-{pack['pack_team']['id']}-{pack['pack_team']['sname']}"
|
||||
elif pack['pack_team'] is not None:
|
||||
if pack['pack_type']['name'] == 'Standard':
|
||||
p_group = f'Standard-Team-{pack["pack_team"]["id"]}-{pack["pack_team"]["sname"]}'
|
||||
elif pack['pack_type']['name'] == 'Premium':
|
||||
p_group = f'Premium-Team-{pack["pack_team"]["id"]}-{pack["pack_team"]["sname"]}'
|
||||
elif pack['pack_type']['name'] == 'Team Choice':
|
||||
p_group = f'Team Choice-Team-{pack["pack_team"]["id"]}-{pack["pack_team"]["sname"]}'
|
||||
elif pack['pack_type']['name'] == 'MVP':
|
||||
p_group = f'MVP-Team-{pack["pack_team"]["id"]}-{pack["pack_team"]["sname"]}'
|
||||
|
||||
if pack["pack_cardset"] is not None:
|
||||
p_group += f"-Cardset-{pack['pack_cardset']['id']}"
|
||||
if pack['pack_cardset'] is not None:
|
||||
p_group += f'-Cardset-{pack["pack_cardset"]["id"]}'
|
||||
|
||||
elif pack["pack_cardset"] is not None:
|
||||
if pack["pack_type"]["name"] == "Standard":
|
||||
p_group = f"Standard-Cardset-{pack['pack_cardset']['id']}-{pack['pack_cardset']['name']}"
|
||||
elif pack["pack_type"]["name"] == "Premium":
|
||||
p_group = f"Premium-Cardset-{pack['pack_cardset']['id']}-{pack['pack_cardset']['name']}"
|
||||
elif pack["pack_type"]["name"] == "Team Choice":
|
||||
p_group = f"Team Choice-Cardset-{pack['pack_cardset']['id']}-{pack['pack_cardset']['name']}"
|
||||
elif pack["pack_type"]["name"] == "All Star":
|
||||
p_group = f"All Star-Cardset-{pack['pack_cardset']['id']}-{pack['pack_cardset']['name']}"
|
||||
elif pack["pack_type"]["name"] == "MVP":
|
||||
p_group = f"MVP-Cardset-{pack['pack_cardset']['id']}-{pack['pack_cardset']['name']}"
|
||||
elif pack["pack_type"]["name"] == "Promo Choice":
|
||||
p_group = f"Promo Choice-Cardset-{pack['pack_cardset']['id']}-{pack['pack_cardset']['name']}"
|
||||
elif pack['pack_cardset'] is not None:
|
||||
if pack['pack_type']['name'] == 'Standard':
|
||||
p_group = f'Standard-Cardset-{pack["pack_cardset"]["id"]}-{pack["pack_cardset"]["name"]}'
|
||||
elif pack['pack_type']['name'] == 'Premium':
|
||||
p_group = f'Premium-Cardset-{pack["pack_cardset"]["id"]}-{pack["pack_cardset"]["name"]}'
|
||||
elif pack['pack_type']['name'] == 'Team Choice':
|
||||
p_group = f'Team Choice-Cardset-{pack["pack_cardset"]["id"]}-{pack["pack_cardset"]["name"]}'
|
||||
elif pack['pack_type']['name'] == 'All Star':
|
||||
p_group = f'All Star-Cardset-{pack["pack_cardset"]["id"]}-{pack["pack_cardset"]["name"]}'
|
||||
elif pack['pack_type']['name'] == 'MVP':
|
||||
p_group = f'MVP-Cardset-{pack["pack_cardset"]["id"]}-{pack["pack_cardset"]["name"]}'
|
||||
elif pack['pack_type']['name'] == 'Promo Choice':
|
||||
p_group = f'Promo Choice-Cardset-{pack["pack_cardset"]["id"]}-{pack["pack_cardset"]["name"]}'
|
||||
|
||||
logger.info(f"p_group: {p_group}")
|
||||
logger.info(f'p_group: {p_group}')
|
||||
if p_group is not None:
|
||||
p_count += 1
|
||||
if p_group not in p_data:
|
||||
@ -388,41 +305,34 @@ class Packs(commands.Cog):
|
||||
|
||||
if p_count == 0:
|
||||
await interaction.response.send_message(
|
||||
"Looks like you are clean out of packs, friendo. You can earn them by playing PD games or by "
|
||||
"donating to the league."
|
||||
f'Looks like you are clean out of packs, friendo. You can earn them by playing PD games or by '
|
||||
f'donating to the league.'
|
||||
)
|
||||
return
|
||||
|
||||
# Display options and ask which group to open
|
||||
embed = get_team_embed("Unopened Packs", team=owner_team)
|
||||
embed.description = owner_team["lname"]
|
||||
embed = get_team_embed(f'Unopened Packs', team=owner_team)
|
||||
embed.description = owner_team['lname']
|
||||
select_options = []
|
||||
for key in p_data:
|
||||
if len(p_data[key]) > 0:
|
||||
pretty_name = None
|
||||
# Not a specific pack
|
||||
if "-" not in key:
|
||||
pretty_name = key
|
||||
elif "Team" in key:
|
||||
pretty_name = f"{key.split('-')[0]} - {key.split('-')[3]}"
|
||||
elif "Cardset" in key:
|
||||
pretty_name = f"{key.split('-')[0]} - {key.split('-')[3]}"
|
||||
else:
|
||||
# Pack type name contains a hyphen (e.g. "Check-In Player")
|
||||
if '-' not in key:
|
||||
pretty_name = key
|
||||
elif 'Team' in key:
|
||||
pretty_name = f'{key.split("-")[0]} - {key.split("-")[3]}'
|
||||
elif 'Cardset' in key:
|
||||
pretty_name = f'{key.split("-")[0]} - {key.split("-")[3]}'
|
||||
|
||||
if pretty_name is not None:
|
||||
embed.add_field(name=pretty_name, value=f"Qty: {len(p_data[key])}")
|
||||
select_options.append(
|
||||
discord.SelectOption(label=pretty_name, value=key)
|
||||
)
|
||||
embed.add_field(name=pretty_name, value=f'Qty: {len(p_data[key])}')
|
||||
select_options.append(discord.SelectOption(label=pretty_name, value=key))
|
||||
|
||||
view = SelectView(
|
||||
select_objects=[SelectOpenPack(select_options, owner_team)], timeout=15
|
||||
)
|
||||
view = SelectView(select_objects=[SelectOpenPack(select_options, owner_team)], timeout=15)
|
||||
await interaction.response.send_message(embed=embed, view=view)
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
"""Setup function for the Packs cog."""
|
||||
await bot.add_cog(Packs(bot))
|
||||
await bot.add_cog(Packs(bot))
|
||||
@ -15,11 +15,9 @@ from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord import app_commands
|
||||
from discord.app_commands import Choice
|
||||
from discord.ext import commands
|
||||
|
||||
from api_calls import db_get
|
||||
from helpers.discord_utils import get_team_embed
|
||||
from helpers.main import get_team_by_owner
|
||||
|
||||
logger = logging.getLogger("discord_app")
|
||||
@ -34,145 +32,72 @@ TIER_NAMES = {
|
||||
4: "Superfractor",
|
||||
}
|
||||
|
||||
# Tier-specific labels for the status display.
|
||||
TIER_SYMBOLS = {
|
||||
0: "Base", # Base Card — used in summary only, not in per-card display
|
||||
1: "T1", # Base Chrome
|
||||
2: "T2", # Refractor
|
||||
3: "T3", # Gold Refractor
|
||||
4: "T4★", # Superfractor
|
||||
FORMULA_LABELS = {
|
||||
"batter": "PA+TB×2",
|
||||
"sp": "IP+K",
|
||||
"rp": "IP+K",
|
||||
}
|
||||
|
||||
_FULL_BAR = "▰" * 12
|
||||
|
||||
# Embed accent colors per tier (used for single-tier filtered views).
|
||||
TIER_COLORS = {
|
||||
0: 0x95A5A6, # slate grey
|
||||
1: 0xBDC3C7, # silver/chrome
|
||||
2: 0x3498DB, # refractor blue
|
||||
3: 0xF1C40F, # gold
|
||||
4: 0x1ABC9C, # teal superfractor
|
||||
}
|
||||
TIER_BADGES = {1: "[BC]", 2: "[R]", 3: "[GR]", 4: "[SF]"}
|
||||
|
||||
|
||||
def render_progress_bar(current: int, threshold: int, width: int = 12) -> str:
|
||||
def render_progress_bar(current: int, threshold: int, width: int = 10) -> str:
|
||||
"""
|
||||
Render a Unicode block progress bar.
|
||||
Render a fixed-width ASCII progress bar.
|
||||
|
||||
Examples:
|
||||
render_progress_bar(120, 149) -> '▰▰▰▰▰▰▰▰▰▰▱▱'
|
||||
render_progress_bar(0, 100) -> '▱▱▱▱▱▱▱▱▱▱▱▱'
|
||||
render_progress_bar(100, 100) -> '▰▰▰▰▰▰▰▰▰▰▰▰'
|
||||
render_progress_bar(120, 149) -> '[========--]'
|
||||
render_progress_bar(0, 100) -> '[----------]'
|
||||
render_progress_bar(100, 100) -> '[==========]'
|
||||
"""
|
||||
if threshold <= 0:
|
||||
filled = width
|
||||
else:
|
||||
ratio = max(0.0, min(current / threshold, 1.0))
|
||||
ratio = min(current / threshold, 1.0)
|
||||
filled = round(ratio * width)
|
||||
empty = width - filled
|
||||
return f"{'▰' * filled}{'▱' * empty}"
|
||||
|
||||
|
||||
def _pct_label(current: int, threshold: int) -> str:
|
||||
"""Return a percentage string like '80%'."""
|
||||
if threshold <= 0:
|
||||
return "100%"
|
||||
return f"{min(current / threshold, 1.0):.0%}"
|
||||
return f"[{'=' * filled}{'-' * empty}]"
|
||||
|
||||
|
||||
def format_refractor_entry(card_state: dict) -> str:
|
||||
"""
|
||||
Format a single card state dict as a compact two-line display string.
|
||||
Format a single card state dict as a display string.
|
||||
|
||||
Output example (base card — no suffix):
|
||||
**Mike Trout**
|
||||
▰▰▰▰▰▰▰▰▰▰▱▱ 120/149 (80%)
|
||||
Expected keys: player_name, card_type, current_tier, formula_value,
|
||||
next_threshold (None if fully evolved).
|
||||
|
||||
Output example (evolved — suffix tag):
|
||||
**Mike Trout** — Base Chrome [T1]
|
||||
▰▰▰▰▰▰▰▰▰▰▱▱ 120/149 (80%)
|
||||
A tier badge prefix (e.g. [BC], [R], [GR], [SF]) is prepended to the
|
||||
player name for tiers 1-4. T0 cards have no badge.
|
||||
|
||||
Output example (fully evolved):
|
||||
**Barry Bonds** — Superfractor [T4★]
|
||||
▰▰▰▰▰▰▰▰▰▰▰▰ `MAX`
|
||||
Output example:
|
||||
**[BC] Mike Trout** (Base Chrome)
|
||||
[========--] 120/149 (PA+TB×2) — T1 → T2
|
||||
"""
|
||||
player_name = card_state.get("player_name", "Unknown")
|
||||
track = card_state.get("track", {})
|
||||
card_type = track.get("card_type", "batter")
|
||||
current_tier = card_state.get("current_tier", 0)
|
||||
formula_value = int(card_state.get("current_value", 0))
|
||||
next_threshold = int(card_state.get("next_threshold") or 0) or None
|
||||
formula_value = card_state.get("current_value", 0)
|
||||
next_threshold = card_state.get("next_threshold")
|
||||
|
||||
if current_tier == 0:
|
||||
first_line = f"**{player_name}**"
|
||||
else:
|
||||
tier_label = TIER_NAMES.get(current_tier, f"T{current_tier}")
|
||||
symbol = TIER_SYMBOLS.get(current_tier, "")
|
||||
first_line = f"**{player_name}** — {tier_label} [{symbol}]"
|
||||
tier_label = TIER_NAMES.get(current_tier, f"T{current_tier}")
|
||||
formula_label = FORMULA_LABELS.get(card_type, card_type)
|
||||
|
||||
badge = TIER_BADGES.get(current_tier, "")
|
||||
display_name = f"{badge} {player_name}" if badge else player_name
|
||||
|
||||
if current_tier >= 4 or next_threshold is None:
|
||||
second_line = f"{_FULL_BAR} `MAX`"
|
||||
bar = "[==========]"
|
||||
detail = "FULLY EVOLVED ★"
|
||||
else:
|
||||
bar = render_progress_bar(formula_value, next_threshold)
|
||||
pct = _pct_label(formula_value, next_threshold)
|
||||
second_line = f"{bar} {formula_value}/{next_threshold} ({pct})"
|
||||
detail = f"{formula_value}/{next_threshold} ({formula_label}) — T{current_tier} → T{current_tier + 1}"
|
||||
|
||||
first_line = f"**{display_name}** ({tier_label})"
|
||||
second_line = f"{bar} {detail}"
|
||||
return f"{first_line}\n{second_line}"
|
||||
|
||||
|
||||
def build_tier_summary(items: list, total_count: int) -> str:
|
||||
"""
|
||||
Build a one-line summary of tier distribution from the current page items.
|
||||
|
||||
Returns something like: 'T0: 3 T1: 12 T2: 8 T3: 5 T4★: 2 — 30 total'
|
||||
"""
|
||||
counts = {t: 0 for t in range(5)}
|
||||
for item in items:
|
||||
t = item.get("current_tier", 0)
|
||||
if t in counts:
|
||||
counts[t] += 1
|
||||
|
||||
parts = []
|
||||
for t in range(5):
|
||||
if counts[t] > 0:
|
||||
parts.append(f"{TIER_SYMBOLS[t]}: {counts[t]}")
|
||||
summary = " ".join(parts) if parts else "No cards"
|
||||
return f"{summary} — {total_count} total"
|
||||
|
||||
|
||||
def build_status_embed(
|
||||
team: dict,
|
||||
items: list,
|
||||
page: int,
|
||||
total_pages: int,
|
||||
total_count: int,
|
||||
tier_filter: Optional[int] = None,
|
||||
) -> discord.Embed:
|
||||
"""
|
||||
Build the refractor status embed with team branding.
|
||||
|
||||
Uses get_team_embed for consistent team color/logo/footer, then layers
|
||||
on the refractor-specific content.
|
||||
"""
|
||||
embed = get_team_embed(f"{team['sname']} — Refractor Status", team=team)
|
||||
|
||||
# Override color for single-tier views to match the tier's identity.
|
||||
if tier_filter is not None and tier_filter in TIER_COLORS:
|
||||
embed.color = TIER_COLORS[tier_filter]
|
||||
|
||||
header = build_tier_summary(items, total_count)
|
||||
lines = [format_refractor_entry(state) for state in items]
|
||||
body = "\n\n".join(lines) if lines else "*No cards found.*"
|
||||
embed.description = f"```{header}```\n{body}"
|
||||
|
||||
existing_footer = embed.footer.text or ""
|
||||
page_text = f"Page {page}/{total_pages}"
|
||||
embed.set_footer(
|
||||
text=f"{page_text} · {existing_footer}" if existing_footer else page_text,
|
||||
icon_url=embed.footer.icon_url,
|
||||
)
|
||||
|
||||
return embed
|
||||
|
||||
|
||||
def apply_close_filter(card_states: list) -> list:
|
||||
"""
|
||||
Return only cards within 80% of their next tier threshold.
|
||||
@ -182,11 +107,11 @@ def apply_close_filter(card_states: list) -> list:
|
||||
result = []
|
||||
for state in card_states:
|
||||
current_tier = state.get("current_tier", 0)
|
||||
formula_value = int(state.get("current_value", 0))
|
||||
formula_value = state.get("current_value", 0)
|
||||
next_threshold = state.get("next_threshold")
|
||||
if current_tier >= 4 or not next_threshold:
|
||||
continue
|
||||
if formula_value >= 0.8 * int(next_threshold):
|
||||
if formula_value >= 0.8 * next_threshold:
|
||||
result.append(state)
|
||||
return result
|
||||
|
||||
@ -203,83 +128,6 @@ def paginate(items: list, page: int, page_size: int = PAGE_SIZE) -> tuple:
|
||||
return items[start : start + page_size], total_pages
|
||||
|
||||
|
||||
class RefractorPaginationView(discord.ui.View):
|
||||
"""Prev/Next buttons for refractor status pagination."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
team: dict,
|
||||
page: int,
|
||||
total_pages: int,
|
||||
total_count: int,
|
||||
params: list,
|
||||
owner_id: int,
|
||||
tier_filter: Optional[int] = None,
|
||||
timeout: float = 120.0,
|
||||
):
|
||||
super().__init__(timeout=timeout)
|
||||
self.team = team
|
||||
self.page = page
|
||||
self.total_pages = total_pages
|
||||
self.total_count = total_count
|
||||
self.base_params = params
|
||||
self.owner_id = owner_id
|
||||
self.tier_filter = tier_filter
|
||||
self._update_buttons()
|
||||
|
||||
def _update_buttons(self):
|
||||
self.prev_btn.disabled = self.page <= 1
|
||||
self.next_btn.disabled = self.page >= self.total_pages
|
||||
|
||||
async def _fetch_and_update(self, interaction: discord.Interaction):
|
||||
offset = (self.page - 1) * PAGE_SIZE
|
||||
params = [(k, v) for k, v in self.base_params if k != "offset"]
|
||||
params.append(("offset", offset))
|
||||
|
||||
data = await db_get("refractor/cards", params=params)
|
||||
items = data.get("items", []) if isinstance(data, dict) else []
|
||||
self.total_count = (
|
||||
data.get("count", self.total_count)
|
||||
if isinstance(data, dict)
|
||||
else self.total_count
|
||||
)
|
||||
self.total_pages = max(1, (self.total_count + PAGE_SIZE - 1) // PAGE_SIZE)
|
||||
self.page = min(self.page, self.total_pages)
|
||||
|
||||
embed = build_status_embed(
|
||||
self.team,
|
||||
items,
|
||||
self.page,
|
||||
self.total_pages,
|
||||
self.total_count,
|
||||
tier_filter=self.tier_filter,
|
||||
)
|
||||
self._update_buttons()
|
||||
await interaction.response.edit_message(embed=embed, view=self)
|
||||
|
||||
@discord.ui.button(label="◀ Prev", style=discord.ButtonStyle.grey)
|
||||
async def prev_btn(
|
||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
||||
):
|
||||
if interaction.user.id != self.owner_id:
|
||||
return
|
||||
self.page = max(1, self.page - 1)
|
||||
await self._fetch_and_update(interaction)
|
||||
|
||||
@discord.ui.button(label="Next ▶", style=discord.ButtonStyle.grey)
|
||||
async def next_btn(
|
||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
||||
):
|
||||
if interaction.user.id != self.owner_id:
|
||||
return
|
||||
self.page = min(self.total_pages, self.page + 1)
|
||||
await self._fetch_and_update(interaction)
|
||||
|
||||
async def on_timeout(self):
|
||||
self.prev_btn.disabled = True
|
||||
self.next_btn.disabled = True
|
||||
|
||||
|
||||
class Refractor(commands.Cog):
|
||||
"""Refractor progress tracking slash commands."""
|
||||
|
||||
@ -294,34 +142,19 @@ class Refractor(commands.Cog):
|
||||
name="status", description="Show your team's refractor progress"
|
||||
)
|
||||
@app_commands.describe(
|
||||
card_type="Filter by card type",
|
||||
tier="Filter by current tier",
|
||||
progress="Filter by advancement progress",
|
||||
card_type="Card type filter (batter, sp, rp)",
|
||||
season="Season number (default: current)",
|
||||
tier="Filter by current tier (0-4)",
|
||||
progress='Use "close" to show cards within 80% of their next tier',
|
||||
page="Page number (default: 1, 10 cards per page)",
|
||||
)
|
||||
@app_commands.choices(
|
||||
card_type=[
|
||||
Choice(value="batter", name="Batter"),
|
||||
Choice(value="sp", name="Starting Pitcher"),
|
||||
Choice(value="rp", name="Relief Pitcher"),
|
||||
],
|
||||
tier=[
|
||||
Choice(value="0", name="T0 — Base Card"),
|
||||
Choice(value="1", name="T1 — Base Chrome"),
|
||||
Choice(value="2", name="T2 — Refractor"),
|
||||
Choice(value="3", name="T3 — Gold Refractor"),
|
||||
Choice(value="4", name="T4 — Superfractor"),
|
||||
],
|
||||
progress=[
|
||||
Choice(value="close", name="Close to next tier (≥80%)"),
|
||||
],
|
||||
)
|
||||
async def refractor_status(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
card_type: Optional[Choice[str]] = None,
|
||||
tier: Optional[Choice[str]] = None,
|
||||
progress: Optional[Choice[str]] = None,
|
||||
card_type: Optional[str] = None,
|
||||
season: Optional[int] = None,
|
||||
tier: Optional[int] = None,
|
||||
progress: Optional[str] = None,
|
||||
page: int = 1,
|
||||
):
|
||||
"""Show a paginated view of the invoking user's team refractor progress."""
|
||||
@ -338,13 +171,13 @@ class Refractor(commands.Cog):
|
||||
offset = (page - 1) * PAGE_SIZE
|
||||
params = [("team_id", team["id"]), ("limit", PAGE_SIZE), ("offset", offset)]
|
||||
if card_type:
|
||||
params.append(("card_type", card_type.value))
|
||||
params.append(("card_type", card_type))
|
||||
if season is not None:
|
||||
params.append(("season", season))
|
||||
if tier is not None:
|
||||
params.append(("tier", tier.value))
|
||||
params.append(("tier", tier))
|
||||
if progress:
|
||||
params.append(("progress", progress.value))
|
||||
|
||||
tier_filter = int(tier.value) if tier is not None else None
|
||||
params.append(("progress", progress))
|
||||
|
||||
data = await db_get("refractor/cards", params=params)
|
||||
if not data:
|
||||
@ -378,18 +211,9 @@ class Refractor(commands.Cog):
|
||||
page,
|
||||
)
|
||||
if not items:
|
||||
has_filters = card_type or tier is not None or progress
|
||||
if has_filters:
|
||||
parts = []
|
||||
if card_type:
|
||||
parts.append(f"**{card_type.name}**")
|
||||
if tier is not None:
|
||||
parts.append(f"**{tier.name}**")
|
||||
if progress:
|
||||
parts.append(f"progress: **{progress.name}**")
|
||||
filter_str = ", ".join(parts)
|
||||
if progress == "close":
|
||||
await interaction.edit_original_response(
|
||||
content=f"No cards match your filters ({filter_str}). Try `/refractor status` with no filters to see all cards."
|
||||
content="No cards are currently close to a tier advancement."
|
||||
)
|
||||
else:
|
||||
await interaction.edit_original_response(
|
||||
@ -399,24 +223,19 @@ class Refractor(commands.Cog):
|
||||
|
||||
total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE)
|
||||
page = min(page, total_pages)
|
||||
page_items = items
|
||||
lines = [format_refractor_entry(state) for state in page_items]
|
||||
|
||||
embed = build_status_embed(
|
||||
team, items, page, total_pages, total_count, tier_filter=tier_filter
|
||||
embed = discord.Embed(
|
||||
title=f"{team['sname']} Refractor Status",
|
||||
description="\n\n".join(lines),
|
||||
color=0x6F42C1,
|
||||
)
|
||||
embed.set_footer(
|
||||
text=f"Page {page}/{total_pages} · {total_count} card(s) total"
|
||||
)
|
||||
|
||||
if total_pages > 1:
|
||||
view = RefractorPaginationView(
|
||||
team=team,
|
||||
page=page,
|
||||
total_pages=total_pages,
|
||||
total_count=total_count,
|
||||
params=params,
|
||||
owner_id=interaction.user.id,
|
||||
tier_filter=tier_filter,
|
||||
)
|
||||
await interaction.edit_original_response(embed=embed, view=view)
|
||||
else:
|
||||
await interaction.edit_original_response(embed=embed)
|
||||
await interaction.edit_original_response(embed=embed)
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
|
||||
@ -3,148 +3,126 @@ 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("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(f'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)
|
||||
|
||||
@ -152,124 +130,104 @@ 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', 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))
|
||||
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:
|
||||
logger.error(
|
||||
f"Unrecognized pack type in selector: {self.values[0]} (split: {pack_vals})"
|
||||
)
|
||||
await interaction.response.edit_message(view=None)
|
||||
await interaction.followup.send(
|
||||
content="This pack type cannot be opened manually. Please contact Cal.",
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
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 Cal 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]))
|
||||
|
||||
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))
|
||||
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}")
|
||||
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 Cal.",
|
||||
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 Cal. Error: {str(e)}",
|
||||
ephemeral=True,
|
||||
content=f'Failed to open pack. Please contact an admin. Error: {str(e)}',
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
@ -277,317 +235,275 @@ 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="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=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
|
||||
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="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=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
|
||||
)
|
||||
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]}")
|
||||
raise ValueError(f'Unknown cardset: {self.values[0]}')
|
||||
|
||||
if self.values[0] == 'Pokemon - Brilliant Stars':
|
||||
self.pack_embed.set_image(url=IMAGES['pack-pkmnbs'])
|
||||
|
||||
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]}"
|
||||
)
|
||||
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,
|
||||
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
|
||||
)
|
||||
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 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]}**",
|
||||
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!")
|
||||
await interaction.channel.send(f'All done!')
|
||||
|
||||
|
||||
class SelectView(discord.ui.View):
|
||||
@ -595,4 +511,4 @@ class SelectView(discord.ui.View):
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
for x in select_objects:
|
||||
self.add_item(x)
|
||||
self.add_item(x)
|
||||
@ -1,67 +0,0 @@
|
||||
services:
|
||||
discord-app:
|
||||
image: manticorum67/paper-dynasty-discordapp:dev
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /path/to/dev-storage:/usr/src/app/storage
|
||||
- /path/to/dev-logs:/usr/src/app/logs
|
||||
environment:
|
||||
- PYTHONBUFFERED=0
|
||||
- GUILD_ID=your-guild-id-here
|
||||
- BOT_TOKEN=your-bot-token-here
|
||||
# - API_TOKEN=your-old-api-token-here
|
||||
- LOG_LEVEL=INFO
|
||||
- API_TOKEN=your-api-token-here
|
||||
- SCOREBOARD_CHANNEL=your-scoreboard-channel-id-here
|
||||
- TZ=America/Chicago
|
||||
- PYTHONHASHSEED=1749583062
|
||||
- DATABASE=Dev
|
||||
- DB_USERNAME=postgres
|
||||
- DB_PASSWORD=your-db-password-here
|
||||
- DB_URL=db
|
||||
- DB_NAME=postgres
|
||||
- RESTART_WEBHOOK_URL=your-discord-webhook-url-here
|
||||
networks:
|
||||
- backend
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- 8081:8081
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "python3 -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8081/health\", timeout=5)' || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_PASSWORD: your-db-password-here
|
||||
volumes:
|
||||
- pd_postgres:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- backend
|
||||
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:8080
|
||||
networks:
|
||||
- backend
|
||||
|
||||
|
||||
networks:
|
||||
backend:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
pd_postgres:
|
||||
@ -665,28 +665,16 @@ design but means tier-up notifications are best-effort.
|
||||
|
||||
Run order for Playwright automation:
|
||||
|
||||
1. [~] Execute REF-API-01 through REF-API-10 (API health + list endpoint)
|
||||
- Tested 2026-03-25: REF-API-03 (single card ✓), REF-API-06 (list ✓), REF-API-07 (card_type filter ✓), REF-API-10 (pagination ✓)
|
||||
- Not yet tested: REF-API-01, REF-API-02, REF-API-04, REF-API-05, REF-API-08, REF-API-09
|
||||
2. [~] Execute REF-01 through REF-06 (basic /refractor status)
|
||||
- Tested 2026-03-25: REF-01 (embed appears ✓), REF-02 (batter entry format ✓), REF-05 (tier badges [BC] ✓)
|
||||
- Bugs found and fixed: wrong response key ("cards" vs "items"), wrong field names (formula_value vs current_value, card_type nesting), limit=500 exceeding API max, floating point display
|
||||
- Not yet tested: REF-03 (SP format), REF-04 (RP format), REF-06 (fully evolved)
|
||||
3. [~] Execute REF-10 through REF-19 (filters)
|
||||
- Tested 2026-03-25: REF-10 (card_type=batter ✓ after fix)
|
||||
- Choice dropdown menus added for all filter params (PR #126)
|
||||
- Not yet tested: REF-11 through REF-19
|
||||
4. [~] Execute REF-20 through REF-23 (pagination)
|
||||
- Tested 2026-03-25: REF-20 (page 1 footer ✓), pagination buttons added (PR #127)
|
||||
- Not yet tested: REF-21 (page 2), REF-22 (beyond total), REF-23 (page 0)
|
||||
1. [ ] Execute REF-API-01 through REF-API-10 (API health + list endpoint)
|
||||
2. [ ] Execute REF-01 through REF-06 (basic /refractor status)
|
||||
3. [ ] Execute REF-10 through REF-19 (filters)
|
||||
4. [ ] Execute REF-20 through REF-23 (pagination)
|
||||
5. [ ] Execute REF-30 through REF-34 (edge cases)
|
||||
6. [ ] Execute REF-40 through REF-45 (tier badges on card embeds)
|
||||
7. [ ] Execute REF-50 through REF-55 (post-game hook -- requires live game)
|
||||
8. [ ] Execute REF-60 through REF-64 (tier-up notifications -- requires threshold crossing)
|
||||
9. [ ] Execute REF-70 through REF-72 (cross-command badge propagation)
|
||||
10. [~] Execute REF-80 through REF-82 (force-evaluate API)
|
||||
- Tested 2026-03-25: REF-80 (force evaluate ✓ — used to seed 100 cards for team 31)
|
||||
- Not yet tested: REF-81, REF-82
|
||||
10. [ ] Execute REF-80 through REF-82 (force-evaluate API)
|
||||
|
||||
### Approximate Time Estimates
|
||||
- API health checks + list endpoint (REF-API-01 through REF-API-10): 2-3 minutes
|
||||
|
||||
@ -251,36 +251,78 @@ class TestEmbedColorUnchanged:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTierSymbolsCompleteness:
|
||||
class TestTierBadgesFormatConsistency:
|
||||
"""
|
||||
T1-7: Assert that TIER_SYMBOLS in cogs.refractor covers all tiers 0-4
|
||||
and that helpers.main TIER_BADGES still covers tiers 1-4 for card embeds.
|
||||
T1-7: Assert that TIER_BADGES in cogs.refractor (format: "[BC]") and
|
||||
helpers.main (format: "BC") are consistent — wrapping the helpers.main
|
||||
value in brackets must produce the cogs.refractor value.
|
||||
|
||||
Why: The refractor status command uses Unicode TIER_SYMBOLS for display,
|
||||
while card embed titles use helpers.main TIER_BADGES in bracket format.
|
||||
Both must cover the full tier range for their respective contexts.
|
||||
Why: The two modules intentionally use different formats for different
|
||||
rendering contexts:
|
||||
- helpers.main uses bare strings ("BC") because get_card_embeds
|
||||
wraps them in brackets when building the embed title.
|
||||
- cogs.refractor uses bracket strings ("[BC]") because
|
||||
format_refractor_entry inlines them directly into the display string.
|
||||
|
||||
If either definition is updated without updating the other, embed titles
|
||||
and /refractor status output will display inconsistent badges. This test
|
||||
acts as an explicit contract check so any future change to either dict
|
||||
is immediately surfaced here.
|
||||
"""
|
||||
|
||||
def test_tier_symbols_covers_all_tiers(self):
|
||||
"""TIER_SYMBOLS must have entries for T0 through T4."""
|
||||
from cogs.refractor import TIER_SYMBOLS
|
||||
def test_cogs_badge_equals_bracketed_helpers_badge_for_all_tiers(self):
|
||||
"""
|
||||
For every tier in cogs.refractor TIER_BADGES, wrapping the
|
||||
helpers.main TIER_BADGES value in square brackets must produce
|
||||
the cogs.refractor value.
|
||||
|
||||
for tier in range(5):
|
||||
assert tier in TIER_SYMBOLS, f"TIER_SYMBOLS missing tier {tier}"
|
||||
i.e., f"[{helpers_badge}]" == cog_badge for all tiers.
|
||||
"""
|
||||
from cogs.refractor import TIER_BADGES as cog_badges
|
||||
from helpers.main import TIER_BADGES as helpers_badges
|
||||
|
||||
def test_tier_badges_covers_evolved_tiers(self):
|
||||
"""helpers.main TIER_BADGES must have entries for T1 through T4."""
|
||||
from helpers.main import TIER_BADGES
|
||||
assert set(cog_badges.keys()) == set(helpers_badges.keys()), (
|
||||
"TIER_BADGES key sets differ between cogs.refractor and helpers.main. "
|
||||
f"cogs keys: {set(cog_badges.keys())}, helpers keys: {set(helpers_badges.keys())}"
|
||||
)
|
||||
|
||||
for tier in range(1, 5):
|
||||
assert tier in TIER_BADGES, f"TIER_BADGES missing tier {tier}"
|
||||
for tier, cog_badge in cog_badges.items():
|
||||
helpers_badge = helpers_badges[tier]
|
||||
expected = f"[{helpers_badge}]"
|
||||
assert cog_badge == expected, (
|
||||
f"Tier {tier} badge mismatch: "
|
||||
f"cogs.refractor={cog_badge!r}, "
|
||||
f"helpers.main={helpers_badge!r} "
|
||||
f"(expected cog badge to equal '[{helpers_badge}]')"
|
||||
)
|
||||
|
||||
def test_tier_symbols_are_unique(self):
|
||||
"""Each tier must have a distinct symbol."""
|
||||
from cogs.refractor import TIER_SYMBOLS
|
||||
def test_t1_badge_relationship(self):
|
||||
"""T1: helpers.main 'BC' wrapped in brackets equals cogs.refractor '[BC]'."""
|
||||
from cogs.refractor import TIER_BADGES as cog_badges
|
||||
from helpers.main import TIER_BADGES as helpers_badges
|
||||
|
||||
values = list(TIER_SYMBOLS.values())
|
||||
assert len(values) == len(set(values)), f"Duplicate symbols found: {values}"
|
||||
assert f"[{helpers_badges[1]}]" == cog_badges[1]
|
||||
|
||||
def test_t2_badge_relationship(self):
|
||||
"""T2: helpers.main 'R' wrapped in brackets equals cogs.refractor '[R]'."""
|
||||
from cogs.refractor import TIER_BADGES as cog_badges
|
||||
from helpers.main import TIER_BADGES as helpers_badges
|
||||
|
||||
assert f"[{helpers_badges[2]}]" == cog_badges[2]
|
||||
|
||||
def test_t3_badge_relationship(self):
|
||||
"""T3: helpers.main 'GR' wrapped in brackets equals cogs.refractor '[GR]'."""
|
||||
from cogs.refractor import TIER_BADGES as cog_badges
|
||||
from helpers.main import TIER_BADGES as helpers_badges
|
||||
|
||||
assert f"[{helpers_badges[3]}]" == cog_badges[3]
|
||||
|
||||
def test_t4_badge_relationship(self):
|
||||
"""T4: helpers.main 'SF' wrapped in brackets equals cogs.refractor '[SF]'."""
|
||||
from cogs.refractor import TIER_BADGES as cog_badges
|
||||
from helpers.main import TIER_BADGES as helpers_badges
|
||||
|
||||
assert f"[{helpers_badges[4]}]" == cog_badges[4]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@ -29,7 +29,7 @@ from cogs.refractor import (
|
||||
apply_close_filter,
|
||||
paginate,
|
||||
TIER_NAMES,
|
||||
TIER_SYMBOLS,
|
||||
TIER_BADGES,
|
||||
PAGE_SIZE,
|
||||
)
|
||||
|
||||
@ -40,12 +40,12 @@ from cogs.refractor import (
|
||||
|
||||
@pytest.fixture
|
||||
def batter_state():
|
||||
"""A mid-progress batter card state (API response shape)."""
|
||||
"""A mid-progress batter card state."""
|
||||
return {
|
||||
"player_name": "Mike Trout",
|
||||
"track": {"card_type": "batter", "formula": "pa + tb * 2"},
|
||||
"card_type": "batter",
|
||||
"current_tier": 1,
|
||||
"current_value": 120,
|
||||
"formula_value": 120,
|
||||
"next_threshold": 149,
|
||||
}
|
||||
|
||||
@ -55,9 +55,9 @@ def evolved_state():
|
||||
"""A fully evolved card state (T4)."""
|
||||
return {
|
||||
"player_name": "Shohei Ohtani",
|
||||
"track": {"card_type": "batter", "formula": "pa + tb * 2"},
|
||||
"card_type": "batter",
|
||||
"current_tier": 4,
|
||||
"current_value": 300,
|
||||
"formula_value": 300,
|
||||
"next_threshold": None,
|
||||
}
|
||||
|
||||
@ -67,9 +67,9 @@ def sp_state():
|
||||
"""A starting pitcher card state at T2."""
|
||||
return {
|
||||
"player_name": "Sandy Alcantara",
|
||||
"track": {"card_type": "sp", "formula": "ip + k"},
|
||||
"card_type": "sp",
|
||||
"current_tier": 2,
|
||||
"current_value": 95,
|
||||
"formula_value": 95,
|
||||
"next_threshold": 120,
|
||||
}
|
||||
|
||||
@ -84,44 +84,38 @@ class TestRenderProgressBar:
|
||||
Tests for render_progress_bar().
|
||||
|
||||
Verifies width, fill character, empty character, boundary conditions,
|
||||
and clamping when current exceeds threshold. Default width is 12.
|
||||
Uses Unicode block chars: ▰ (filled) and ▱ (empty).
|
||||
and clamping when current exceeds threshold.
|
||||
"""
|
||||
|
||||
def test_empty_bar(self):
|
||||
"""current=0 → all empty blocks."""
|
||||
assert render_progress_bar(0, 100) == "▱" * 12
|
||||
"""current=0 → all dashes."""
|
||||
assert render_progress_bar(0, 100) == "[----------]"
|
||||
|
||||
def test_full_bar(self):
|
||||
"""current == threshold → all filled blocks."""
|
||||
assert render_progress_bar(100, 100) == "▰" * 12
|
||||
"""current == threshold → all equals."""
|
||||
assert render_progress_bar(100, 100) == "[==========]"
|
||||
|
||||
def test_partial_fill(self):
|
||||
"""120/149 ≈ 80.5% → ~10 filled of 12."""
|
||||
"""120/149 ≈ 80.5% → 8 filled of 10."""
|
||||
bar = render_progress_bar(120, 149)
|
||||
filled = bar.count("▰")
|
||||
empty = bar.count("▱")
|
||||
assert filled + empty == 12
|
||||
assert filled == 10 # round(0.805 * 12) = 10
|
||||
assert bar == "[========--]"
|
||||
|
||||
def test_half_fill(self):
|
||||
"""50/100 = 50% → 6 filled."""
|
||||
bar = render_progress_bar(50, 100)
|
||||
assert bar.count("▰") == 6
|
||||
assert bar.count("▱") == 6
|
||||
"""50/100 = 50% → 5 filled."""
|
||||
assert render_progress_bar(50, 100) == "[=====-----]"
|
||||
|
||||
def test_over_threshold_clamps_to_full(self):
|
||||
"""current > threshold should not overflow the bar."""
|
||||
assert render_progress_bar(200, 100) == "▰" * 12
|
||||
assert render_progress_bar(200, 100) == "[==========]"
|
||||
|
||||
def test_zero_threshold_returns_full_bar(self):
|
||||
"""threshold=0 avoids division by zero and returns full bar."""
|
||||
assert render_progress_bar(0, 0) == "▰" * 12
|
||||
assert render_progress_bar(0, 0) == "[==========]"
|
||||
|
||||
def test_custom_width(self):
|
||||
"""Width parameter controls bar length."""
|
||||
bar = render_progress_bar(5, 10, width=4)
|
||||
assert bar == "▰▰▱▱"
|
||||
assert bar == "[==--]"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -146,29 +140,44 @@ class TestFormatRefractorEntry:
|
||||
def test_tier_label_in_output(self, batter_state):
|
||||
"""Current tier name (Base Chrome for T1) appears in output."""
|
||||
result = format_refractor_entry(batter_state)
|
||||
assert "Base Chrome" in result
|
||||
assert "(Base Chrome)" in result
|
||||
|
||||
def test_progress_values_in_output(self, batter_state):
|
||||
"""current/threshold values appear in output."""
|
||||
result = format_refractor_entry(batter_state)
|
||||
assert "120/149" in result
|
||||
|
||||
def test_percentage_in_output(self, batter_state):
|
||||
"""Percentage appears in parentheses in output."""
|
||||
def test_formula_label_batter(self, batter_state):
|
||||
"""Batter formula label PA+TB×2 appears in output."""
|
||||
result = format_refractor_entry(batter_state)
|
||||
assert "(80%)" in result or "(81%)" in result
|
||||
assert "PA+TB×2" in result
|
||||
|
||||
def test_tier_progression_arrow(self, batter_state):
|
||||
"""T1 → T2 arrow progression appears for non-evolved cards."""
|
||||
result = format_refractor_entry(batter_state)
|
||||
assert "T1 → T2" in result
|
||||
|
||||
def test_sp_formula_label(self, sp_state):
|
||||
"""SP formula label IP+K appears for starting pitchers."""
|
||||
result = format_refractor_entry(sp_state)
|
||||
assert "IP+K" in result
|
||||
|
||||
def test_fully_evolved_no_threshold(self, evolved_state):
|
||||
"""T4 card with next_threshold=None shows MAX."""
|
||||
"""T4 card with next_threshold=None shows FULLY EVOLVED."""
|
||||
result = format_refractor_entry(evolved_state)
|
||||
assert "`MAX`" in result
|
||||
assert "FULLY EVOLVED" in result
|
||||
|
||||
def test_fully_evolved_by_tier(self, batter_state):
|
||||
"""current_tier=4 triggers fully evolved display even with a threshold."""
|
||||
batter_state["current_tier"] = 4
|
||||
batter_state["next_threshold"] = 200
|
||||
result = format_refractor_entry(batter_state)
|
||||
assert "`MAX`" in result
|
||||
assert "FULLY EVOLVED" in result
|
||||
|
||||
def test_fully_evolved_no_arrow(self, evolved_state):
|
||||
"""Fully evolved cards don't show a tier arrow."""
|
||||
result = format_refractor_entry(evolved_state)
|
||||
assert "→" not in result
|
||||
|
||||
def test_two_line_output(self, batter_state):
|
||||
"""Output always has exactly two lines (name line + bar line)."""
|
||||
@ -182,66 +191,69 @@ class TestFormatRefractorEntry:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTierSymbols:
|
||||
class TestTierBadges:
|
||||
"""
|
||||
Verify TIER_SYMBOLS values and that format_refractor_entry prepends
|
||||
the correct label for each tier. Labels use short readable text (T0-T4).
|
||||
Verify TIER_BADGES values and that format_refractor_entry prepends badges
|
||||
correctly for T1-T4. T0 cards should have no badge prefix.
|
||||
"""
|
||||
|
||||
def test_t0_symbol(self):
|
||||
"""T0 label is empty (base cards get no prefix)."""
|
||||
assert TIER_SYMBOLS[0] == "Base"
|
||||
def test_t1_badge_value(self):
|
||||
"""T1 badge is [BC] (Base Chrome)."""
|
||||
assert TIER_BADGES[1] == "[BC]"
|
||||
|
||||
def test_t1_symbol(self):
|
||||
"""T1 label is 'T1'."""
|
||||
assert TIER_SYMBOLS[1] == "T1"
|
||||
def test_t2_badge_value(self):
|
||||
"""T2 badge is [R] (Refractor)."""
|
||||
assert TIER_BADGES[2] == "[R]"
|
||||
|
||||
def test_t2_symbol(self):
|
||||
"""T2 label is 'T2'."""
|
||||
assert TIER_SYMBOLS[2] == "T2"
|
||||
def test_t3_badge_value(self):
|
||||
"""T3 badge is [GR] (Gold Refractor)."""
|
||||
assert TIER_BADGES[3] == "[GR]"
|
||||
|
||||
def test_t3_symbol(self):
|
||||
"""T3 label is 'T3'."""
|
||||
assert TIER_SYMBOLS[3] == "T3"
|
||||
def test_t4_badge_value(self):
|
||||
"""T4 badge is [SF] (Superfractor)."""
|
||||
assert TIER_BADGES[4] == "[SF]"
|
||||
|
||||
def test_t4_symbol(self):
|
||||
"""T4 label is 'T4★'."""
|
||||
assert TIER_SYMBOLS[4] == "T4★"
|
||||
def test_t0_no_badge(self):
|
||||
"""T0 has no badge entry in TIER_BADGES."""
|
||||
assert 0 not in TIER_BADGES
|
||||
|
||||
def test_format_entry_t1_suffix_tag(self, batter_state):
|
||||
"""T1 cards show [T1] suffix tag after the tier name."""
|
||||
def test_format_entry_t1_badge_present(self, batter_state):
|
||||
"""format_refractor_entry prepends [BC] badge for T1 cards."""
|
||||
result = format_refractor_entry(batter_state)
|
||||
assert "[T1]" in result
|
||||
assert "[BC]" in result
|
||||
|
||||
def test_format_entry_t2_suffix_tag(self, sp_state):
|
||||
"""T2 cards show [T2] suffix tag."""
|
||||
def test_format_entry_t2_badge_present(self, sp_state):
|
||||
"""format_refractor_entry prepends [R] badge for T2 cards."""
|
||||
result = format_refractor_entry(sp_state)
|
||||
assert "[T2]" in result
|
||||
assert "[R]" in result
|
||||
|
||||
def test_format_entry_t4_suffix_tag(self, evolved_state):
|
||||
"""T4 cards show [T4★] suffix tag."""
|
||||
def test_format_entry_t4_badge_present(self, evolved_state):
|
||||
"""format_refractor_entry prepends [SF] badge for T4 cards."""
|
||||
result = format_refractor_entry(evolved_state)
|
||||
assert "[T4★]" in result
|
||||
assert "[SF]" in result
|
||||
|
||||
def test_format_entry_t0_name_only(self):
|
||||
"""T0 cards show just the bold name, no tier suffix."""
|
||||
def test_format_entry_t0_no_badge(self):
|
||||
"""format_refractor_entry does not prepend any badge for T0 cards."""
|
||||
state = {
|
||||
"player_name": "Rookie Player",
|
||||
"card_type": "batter",
|
||||
"current_tier": 0,
|
||||
"current_value": 10,
|
||||
"formula_value": 10,
|
||||
"next_threshold": 50,
|
||||
}
|
||||
result = format_refractor_entry(state)
|
||||
first_line = result.split("\n")[0]
|
||||
assert first_line == "**Rookie Player**"
|
||||
assert "[BC]" not in result
|
||||
assert "[R]" not in result
|
||||
assert "[GR]" not in result
|
||||
assert "[SF]" not in result
|
||||
|
||||
def test_format_entry_tag_after_name(self, batter_state):
|
||||
"""Tag appears after the player name in the first line."""
|
||||
def test_format_entry_badge_before_name(self, batter_state):
|
||||
"""Badge appears before the player name in the bold section."""
|
||||
result = format_refractor_entry(batter_state)
|
||||
first_line = result.split("\n")[0]
|
||||
badge_pos = first_line.find("[BC]")
|
||||
name_pos = first_line.find("Mike Trout")
|
||||
tag_pos = first_line.find("[T1]")
|
||||
assert name_pos < tag_pos
|
||||
assert badge_pos < name_pos
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -259,34 +271,34 @@ class TestApplyCloseFilter:
|
||||
|
||||
def test_close_card_included(self):
|
||||
"""Card at exactly 80% is included."""
|
||||
state = {"current_tier": 1, "current_value": 80, "next_threshold": 100}
|
||||
state = {"current_tier": 1, "formula_value": 80, "next_threshold": 100}
|
||||
assert apply_close_filter([state]) == [state]
|
||||
|
||||
def test_above_80_percent_included(self):
|
||||
"""Card above 80% is included."""
|
||||
state = {"current_tier": 0, "current_value": 95, "next_threshold": 100}
|
||||
state = {"current_tier": 0, "formula_value": 95, "next_threshold": 100}
|
||||
assert apply_close_filter([state]) == [state]
|
||||
|
||||
def test_below_80_percent_excluded(self):
|
||||
"""Card below 80% threshold is excluded."""
|
||||
state = {"current_tier": 1, "current_value": 79, "next_threshold": 100}
|
||||
state = {"current_tier": 1, "formula_value": 79, "next_threshold": 100}
|
||||
assert apply_close_filter([state]) == []
|
||||
|
||||
def test_fully_evolved_excluded(self):
|
||||
"""T4 cards are never returned by close filter."""
|
||||
state = {"current_tier": 4, "current_value": 300, "next_threshold": None}
|
||||
state = {"current_tier": 4, "formula_value": 300, "next_threshold": None}
|
||||
assert apply_close_filter([state]) == []
|
||||
|
||||
def test_none_threshold_excluded(self):
|
||||
"""Cards with no next_threshold (regardless of tier) are excluded."""
|
||||
state = {"current_tier": 3, "current_value": 200, "next_threshold": None}
|
||||
state = {"current_tier": 3, "formula_value": 200, "next_threshold": None}
|
||||
assert apply_close_filter([state]) == []
|
||||
|
||||
def test_mixed_list(self):
|
||||
"""Only qualifying cards are returned from a mixed list."""
|
||||
close = {"current_tier": 1, "current_value": 90, "next_threshold": 100}
|
||||
not_close = {"current_tier": 1, "current_value": 50, "next_threshold": 100}
|
||||
evolved = {"current_tier": 4, "current_value": 300, "next_threshold": None}
|
||||
close = {"current_tier": 1, "formula_value": 90, "next_threshold": 100}
|
||||
not_close = {"current_tier": 1, "formula_value": 50, "next_threshold": 100}
|
||||
evolved = {"current_tier": 4, "formula_value": 300, "next_threshold": None}
|
||||
result = apply_close_filter([close, not_close, evolved])
|
||||
assert result == [close]
|
||||
|
||||
@ -494,9 +506,9 @@ class TestApplyCloseFilterWithAllT4Cards:
|
||||
the "no cards close to advancement" message rather than an empty embed.
|
||||
"""
|
||||
t4_cards = [
|
||||
{"current_tier": 4, "current_value": 300, "next_threshold": None},
|
||||
{"current_tier": 4, "current_value": 500, "next_threshold": None},
|
||||
{"current_tier": 4, "current_value": 275, "next_threshold": None},
|
||||
{"current_tier": 4, "formula_value": 300, "next_threshold": None},
|
||||
{"current_tier": 4, "formula_value": 500, "next_threshold": None},
|
||||
{"current_tier": 4, "formula_value": 275, "next_threshold": None},
|
||||
]
|
||||
result = apply_close_filter(t4_cards)
|
||||
assert result == [], (
|
||||
@ -511,7 +523,7 @@ class TestApplyCloseFilterWithAllT4Cards:
|
||||
"""
|
||||
t4_high_value = {
|
||||
"current_tier": 4,
|
||||
"current_value": 9999,
|
||||
"formula_value": 9999,
|
||||
"next_threshold": None,
|
||||
}
|
||||
assert apply_close_filter([t4_high_value]) == []
|
||||
@ -540,9 +552,9 @@ class TestFormatRefractorEntryMalformedInput:
|
||||
than crashing with a KeyError.
|
||||
"""
|
||||
state = {
|
||||
"track": {"card_type": "batter"},
|
||||
"card_type": "batter",
|
||||
"current_tier": 1,
|
||||
"current_value": 100,
|
||||
"formula_value": 100,
|
||||
"next_threshold": 150,
|
||||
}
|
||||
result = format_refractor_entry(state)
|
||||
@ -550,12 +562,12 @@ class TestFormatRefractorEntryMalformedInput:
|
||||
|
||||
def test_missing_formula_value_uses_zero(self):
|
||||
"""
|
||||
When current_value is absent, the progress calculation should use 0
|
||||
When formula_value is absent, the progress calculation should use 0
|
||||
without raising a TypeError.
|
||||
"""
|
||||
state = {
|
||||
"player_name": "Test Player",
|
||||
"track": {"card_type": "batter"},
|
||||
"card_type": "batter",
|
||||
"current_tier": 1,
|
||||
"next_threshold": 150,
|
||||
}
|
||||
@ -573,19 +585,20 @@ class TestFormatRefractorEntryMalformedInput:
|
||||
lines = result.split("\n")
|
||||
assert len(lines) == 2
|
||||
|
||||
def test_missing_card_type_does_not_crash(self):
|
||||
def test_missing_card_type_uses_raw_fallback(self):
|
||||
"""
|
||||
When card_type is absent from the track, the code should still
|
||||
produce a valid two-line output without crashing.
|
||||
When card_type is absent, the code defaults to 'batter' internally
|
||||
(via .get("card_type", "batter")), so "PA+TB×2" should appear as the
|
||||
formula label.
|
||||
"""
|
||||
state = {
|
||||
"player_name": "Test Player",
|
||||
"current_tier": 1,
|
||||
"current_value": 50,
|
||||
"formula_value": 50,
|
||||
"next_threshold": 100,
|
||||
}
|
||||
result = format_refractor_entry(state)
|
||||
assert "50/100" in result
|
||||
assert "PA+TB×2" in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -610,27 +623,30 @@ class TestRenderProgressBarBoundaryPrecision:
|
||||
rest empty. The bar must not appear more than minimally filled.
|
||||
"""
|
||||
bar = render_progress_bar(1, 100)
|
||||
filled_count = bar.count("▰")
|
||||
# Interior is 10 chars: count '=' vs '-'
|
||||
interior = bar[1:-1] # strip '[' and ']'
|
||||
filled_count = interior.count("=")
|
||||
assert filled_count <= 1, (
|
||||
f"1/100 should show 0 or 1 filled segment, got {filled_count}: {bar!r}"
|
||||
)
|
||||
|
||||
def test_ninety_nine_of_hundred_is_nearly_full(self):
|
||||
"""
|
||||
99/100 = 99% — should produce a bar with 11 or 12 filled segments.
|
||||
The bar must NOT be completely empty or show fewer than 11 filled.
|
||||
99/100 = 99% — should produce a bar with 9 or 10 filled segments.
|
||||
The bar must NOT be completely empty or show fewer than 9 filled.
|
||||
"""
|
||||
bar = render_progress_bar(99, 100)
|
||||
filled_count = bar.count("▰")
|
||||
assert filled_count >= 11, (
|
||||
f"99/100 should show 11 or 12 filled segments, got {filled_count}: {bar!r}"
|
||||
interior = bar[1:-1]
|
||||
filled_count = interior.count("=")
|
||||
assert filled_count >= 9, (
|
||||
f"99/100 should show 9 or 10 filled segments, got {filled_count}: {bar!r}"
|
||||
)
|
||||
# Bar width must be exactly 12
|
||||
assert len(bar) == 12
|
||||
# But it must not overflow the bar width
|
||||
assert len(interior) == 10
|
||||
|
||||
def test_zero_of_hundred_is_completely_empty(self):
|
||||
"""0/100 = all empty blocks — re-verify the all-empty baseline."""
|
||||
assert render_progress_bar(0, 100) == "▱" * 12
|
||||
"""0/100 = all dashes — re-verify the all-empty baseline."""
|
||||
assert render_progress_bar(0, 100) == "[----------]"
|
||||
|
||||
def test_negative_current_does_not_overflow_bar(self):
|
||||
"""
|
||||
@ -640,12 +656,14 @@ class TestRenderProgressBarBoundaryPrecision:
|
||||
a future refactor removing the clamp.
|
||||
"""
|
||||
bar = render_progress_bar(-5, 100)
|
||||
filled_count = bar.count("▰")
|
||||
interior = bar[1:-1]
|
||||
# No filled segments should exist for a negative value
|
||||
filled_count = interior.count("=")
|
||||
assert filled_count == 0, (
|
||||
f"Negative current should produce 0 filled segments, got {filled_count}: {bar!r}"
|
||||
)
|
||||
# Bar width must be exactly 12
|
||||
assert len(bar) == 12
|
||||
# Bar width must be exactly 10
|
||||
assert len(interior) == 10
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -653,32 +671,79 @@ class TestRenderProgressBarBoundaryPrecision:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCardTypeVariants:
|
||||
class TestRPFormulaLabel:
|
||||
"""
|
||||
T3-4/T3-5: Verify that format_refractor_entry produces valid output for
|
||||
all card types including unknown ones, without crashing.
|
||||
T3-4: Verify that relief pitchers (card_type="rp") show the "IP+K" formula
|
||||
label in format_refractor_entry output.
|
||||
|
||||
Why: FORMULA_LABELS maps both "sp" and "rp" to "IP+K". The existing test
|
||||
suite only verifies "sp" (via the sp_state fixture). Adding "rp" explicitly
|
||||
prevents a future refactor from accidentally giving RPs a different label
|
||||
or falling through to the raw card_type fallback.
|
||||
"""
|
||||
|
||||
def test_rp_card_produces_valid_output(self):
|
||||
"""Relief pitcher card produces a valid two-line string."""
|
||||
def test_rp_formula_label_is_ip_plus_k(self):
|
||||
"""
|
||||
A card with card_type="rp" must show "IP+K" as the formula label
|
||||
in its progress line.
|
||||
"""
|
||||
rp_state = {
|
||||
"player_name": "Edwin Diaz",
|
||||
"track": {"card_type": "rp"},
|
||||
"card_type": "rp",
|
||||
"current_tier": 1,
|
||||
"current_value": 45,
|
||||
"formula_value": 45,
|
||||
"next_threshold": 60,
|
||||
}
|
||||
result = format_refractor_entry(rp_state)
|
||||
assert "Edwin Diaz" in result
|
||||
assert "45/60" in result
|
||||
assert "IP+K" in result, (
|
||||
f"Relief pitcher card should show 'IP+K' formula label, got: {result!r}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T3-5: Unknown card_type fallback
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestUnknownCardTypeFallback:
|
||||
"""
|
||||
T3-5: format_refractor_entry should use the raw card_type string as the
|
||||
formula label when the type is not in FORMULA_LABELS, rather than crashing.
|
||||
|
||||
Why: FORMULA_LABELS only covers "batter", "sp", and "rp". If the API
|
||||
introduces a new card type (e.g. "util" for utility players) before the
|
||||
bot is updated, FORMULA_LABELS.get(card_type, card_type) will fall back to
|
||||
the raw string. This test ensures that fallback path produces readable
|
||||
output rather than an error, and explicitly documents what to expect.
|
||||
"""
|
||||
|
||||
def test_unknown_card_type_uses_raw_string_as_label(self):
|
||||
"""
|
||||
card_type="util" is not in FORMULA_LABELS. The output should include
|
||||
"util" as the formula label (the raw fallback) and must not raise.
|
||||
"""
|
||||
util_state = {
|
||||
"player_name": "Ben Zobrist",
|
||||
"card_type": "util",
|
||||
"current_tier": 2,
|
||||
"formula_value": 80,
|
||||
"next_threshold": 120,
|
||||
}
|
||||
result = format_refractor_entry(util_state)
|
||||
assert "util" in result, (
|
||||
f"Unknown card_type should appear verbatim as the formula label, got: {result!r}"
|
||||
)
|
||||
|
||||
def test_unknown_card_type_does_not_crash(self):
|
||||
"""Unknown card_type produces a valid two-line string."""
|
||||
"""
|
||||
Any unknown card_type must produce a valid two-line string without
|
||||
raising an exception.
|
||||
"""
|
||||
state = {
|
||||
"player_name": "Test Player",
|
||||
"track": {"card_type": "dh"},
|
||||
"card_type": "dh",
|
||||
"current_tier": 1,
|
||||
"current_value": 30,
|
||||
"formula_value": 30,
|
||||
"next_threshold": 50,
|
||||
}
|
||||
result = format_refractor_entry(state)
|
||||
@ -729,10 +794,7 @@ async def test_refractor_status_empty_roster(mock_bot, mock_interaction):
|
||||
team = {"id": 1, "sname": "Test"}
|
||||
|
||||
with patch("cogs.refractor.get_team_by_owner", new=AsyncMock(return_value=team)):
|
||||
with patch(
|
||||
"cogs.refractor.db_get",
|
||||
new=AsyncMock(return_value={"items": [], "count": 0}),
|
||||
):
|
||||
with patch("cogs.refractor.db_get", new=AsyncMock(return_value={"cards": []})):
|
||||
await cog.refractor_status.callback(cog, mock_interaction)
|
||||
|
||||
call_kwargs = mock_interaction.edit_original_response.call_args
|
||||
|
||||
Loading…
Reference in New Issue
Block a user