paper-dynasty-discord/helpers/scouting.py
Cal Corum 33260fd5fa feat: add buy-scout-token option when daily limit exceeded
When a user exceeds their 2/day scout token limit, they are now offered
a button to purchase an extra token for 200₼ instead of being blocked.
Updates /scout-tokens message to mention the purchase option.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:12:35 -05:00

245 lines
7.6 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 os
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_TOKEN_COST = 200 # Currency cost to buy an extra scout token
SCOUT_WINDOW_SECONDS = 1800 # 30 minutes
_scoutable_raw = os.environ.get("SCOUTABLE_PACK_TYPES", "Standard,Premium")
SCOUTABLE_PACK_TYPES = {s.strip() for s in _scoutable_raw.split(",") if s.strip()}
# 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")
desc = player.get("description", "")
image_url = player.get("image", "")
name_display = (
f"[{desc} {player['p_name']}]({image_url})"
if image_url
else f"{desc} {player['p_name']}"
)
lines.append(
(
player["player_id"],
f"{symbol} {player['rarity']['name']}{name_display}",
)
)
random.shuffle(lines)
return lines
def build_scout_embed(
opener_team: dict,
cards: list[dict] = None,
card_lines: list[tuple[int, str]] = None,
expires_unix: int = None,
claims: dict[int, list[str]] = None,
total_scouts: int = 0,
closed: bool = False,
) -> 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.
Parameters
----------
closed : if True, renders the "Scout Window Closed" variant
claims : scouted card tracking dict for build_scouted_card_list
total_scouts : number of scouts so far (for title display)
"""
if card_lines is None and cards is not None:
card_lines = _build_card_lines(cards)
if claims and card_lines:
card_list = build_scouted_card_list(card_lines, claims)
elif card_lines:
card_list = "\n".join(line for _, line in card_lines)
else:
card_list = ""
if closed:
if total_scouts > 0:
title = f"Scout Window Closed ({total_scouts} scouted)"
else:
title = "Scout Window Closed"
elif total_scouts > 0:
title = f"Scout Opportunity! ({total_scouts} scouted)"
else:
title = "Scout Opportunity!"
embed = get_team_embed(title=title, team=opener_team)
if closed:
embed.description = f"**{opener_team['lname']}**'s pack\n\n" f"{card_list}"
embed.set_footer(
text=f"Paper Dynasty Season {PD_SEASON}",
icon_url=IMAGES["logo"],
)
else:
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']}**'s pack\n\n"
f"{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_dt = now + datetime.timedelta(seconds=SCOUT_WINDOW_SECONDS)
expires_at = int_timestamp(expires_dt)
expires_unix = int(expires_dt.timestamp())
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
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}")