paper-dynasty-discord/helpers/scouting.py
Cal Corum 77c3f3004c
All checks were successful
Build Docker Image / build (push) Successful in 1m22s
fix: align scouting rarity symbols with system colors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:03:15 -06:00

204 lines
6.1 KiB
Python

"""
Scouting Helper Functions
Handles creation of scout opportunities after pack openings
and embed formatting for the scouting feature.
"""
import datetime
import logging
import random
import discord
from api_calls import db_get, db_post
from helpers.utils import int_timestamp, midnight_timestamp
from helpers.discord_utils import get_team_embed
from helpers.constants import IMAGES, PD_SEASON
logger = logging.getLogger("discord_app")
SCOUT_TOKENS_PER_DAY = 2
SCOUT_WINDOW_SECONDS = 1800 # 30 minutes
# Rarity value → display symbol
RARITY_SYMBOLS = {
8: "\U0001f7e3", # HoF — purple (#751cea)
5: "\U0001f535", # MVP — cyan/blue (#56f1fa)
3: "\U0001f7e1", # All-Star — gold (#FFD700)
2: "\u26aa", # Starter — silver (#C0C0C0)
1: "\U0001f7e4", # Reserve — bronze (#CD7F32)
0: "\u26ab", # Replacement — dark gray (#454545)
}
async def get_scout_tokens_used(team_id: int) -> int:
"""Return how many scout tokens a team has used today."""
used_today = await db_get(
"rewards",
params=[
("name", "Scout Token"),
("team_id", team_id),
("created_after", midnight_timestamp()),
],
)
return used_today["count"] if used_today else 0
def _build_card_lines(cards: list[dict]) -> list[tuple[int, str]]:
"""Build a shuffled list of (player_id, display_line) tuples."""
lines = []
for card in cards:
player = card["player"]
rarity_val = player["rarity"]["value"]
symbol = RARITY_SYMBOLS.get(rarity_val, "\u26ab")
lines.append(
(
player["player_id"],
f"{symbol} {player['rarity']['name']}{player['p_name']}",
)
)
random.shuffle(lines)
return lines
def build_scout_embed(
opener_team: dict,
cards: list[dict],
card_lines: list[tuple[int, str]] = None,
expires_unix: int = None,
) -> tuple[discord.Embed, list[tuple[int, str]]]:
"""Build the embed shown above the scout buttons.
Shows a shuffled list of cards (rarity + player name) so scouters
know what's in the pack but not which button maps to which card.
Returns (embed, card_lines) so the view can store the shuffled order.
"""
embed = get_team_embed(title="Scout Opportunity!", team=opener_team)
if card_lines is None:
card_lines = _build_card_lines(cards)
card_list = "\n".join(line for _, line in card_lines)
if expires_unix:
time_line = f"Scout window closes <t:{expires_unix}:R>."
else:
time_line = "Scout window closes in **30 minutes**."
embed.description = (
f"**{opener_team['lname']}** just opened a pack!\n\n"
f"**Cards in this pack:**\n{card_list}\n\n"
f"Pick a card — but which is which?\n"
f"Costs 1 Scout Token (2 per day, resets at midnight Central).\n"
f"{time_line}"
)
embed.set_footer(
text=f"Paper Dynasty Season {PD_SEASON} \u2022 One scout per player",
icon_url=IMAGES["logo"],
)
return embed, card_lines
def build_scouted_card_list(
card_lines: list[tuple[int, str]],
scouted_cards: dict[int, list[str]],
) -> str:
"""Rebuild the card list marking scouted cards with scouter team names.
Parameters
----------
card_lines : shuffled list of (player_id, display_line) tuples
scouted_cards : {player_id: [team_name, ...]} for each claimed card
"""
result = []
for player_id, line in card_lines:
teams = scouted_cards.get(player_id)
if teams:
count = len(teams)
names = ", ".join(f"*{t}*" for t in teams)
if count == 1:
result.append(f"{line} \u2014 \u2714\ufe0f {names}")
else:
result.append(f"{line} \u2014 \u2714\ufe0f x{count} ({names})")
else:
result.append(line)
return "\n".join(result)
async def create_scout_opportunity(
pack_cards: list[dict],
opener_team: dict,
channel: discord.TextChannel,
opener_user,
context,
) -> None:
"""Create a scout opportunity and post the ScoutView to the channel.
Called after display_cards() completes in open_st_pr_packs().
Wrapped in try/except so scouting failures never crash pack opening.
Parameters
----------
pack_cards : list of card dicts from a single pack
opener_team : team dict for the pack opener
channel : the #pack-openings channel
opener_user : discord.Member or discord.User who opened the pack
context : the command context (Context or Interaction), used to get bot
"""
from discord_ui.scout_view import ScoutView
# Only create scout opportunities in the pack-openings channel
if not channel or channel.name != "pack-openings":
return
if not pack_cards:
return
now = datetime.datetime.now()
expires_at = int_timestamp(now + datetime.timedelta(seconds=SCOUT_WINDOW_SECONDS))
created = int_timestamp(now)
card_ids = [c["id"] for c in pack_cards]
try:
scout_opp = await db_post(
"scout_opportunities",
payload={
"pack_id": pack_cards[0].get("pack_id"),
"opener_team_id": opener_team["id"],
"card_ids": card_ids,
"expires_at": expires_at,
"created": created,
},
)
except Exception as e:
logger.error(f"Failed to create scout opportunity: {e}")
return
expires_unix = int(
(now + datetime.timedelta(seconds=SCOUT_WINDOW_SECONDS)).timestamp()
)
embed, card_lines = build_scout_embed(
opener_team, pack_cards, expires_unix=expires_unix
)
# Get bot reference from context
bot = getattr(context, "bot", None) or getattr(context, "client", None)
view = ScoutView(
scout_opp_id=scout_opp["id"],
cards=pack_cards,
opener_team=opener_team,
opener_user_id=opener_user.id,
bot=bot,
expires_unix=expires_unix,
)
view.card_lines = card_lines
try:
msg = await channel.send(embed=embed, view=view)
view.message = msg
except Exception as e:
logger.error(f"Failed to post scout opportunity message: {e}")