paper-dynasty-discord/helpers/scouting.py
Cal Corum 2d5bd86d52 feat: Add Scouting feature (Wonder Pick-style social pack opening)
When a player opens a pack, a scout opportunity is posted to #pack-openings
with face-down card buttons. Other players can blind-pick one card using
daily scout tokens (2/day), receiving a copy. The opener keeps all cards.

New files:
- discord_ui/scout_view.py: ScoutView with dynamic buttons and claim logic
- helpers/scouting.py: create_scout_opportunity() and embed builder
- cogs/economy_new/scouting.py: /scout-tokens command and cleanup task

Modified:
- helpers/main.py: Hook into open_st_pr_packs() after display_cards()
- paperdynasty.py: Register scouting cog

Requires new API endpoints in paper-dynasty-database (scout_opportunities).
Tracks #44.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:22:58 +00:00

174 lines
5.1 KiB
Python

"""
Scouting Helper Functions
Handles creation of scout opportunities after pack openings
and embed formatting for the scouting feature.
"""
import asyncio
import datetime
import logging
import random
import discord
from api_calls import db_post
from helpers.utils import int_timestamp
from helpers.discord_utils import get_team_embed
from helpers.constants import IMAGES, PD_SEASON
logger = logging.getLogger("discord_app")
SCOUT_WINDOW_SECONDS = 1800 # 30 minutes
# Rarity value → display symbol
RARITY_SYMBOLS = {
8: "\U0001f7e1", # HoF — yellow
5: "\U0001f7e3", # MVP — purple
3: "\U0001f535", # All-Star — blue
2: "\U0001f7e2", # Starter — green
1: "\u26aa", # Reserve — white
0: "\u26ab", # Replacement — black
}
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,
) -> discord.Embed:
"""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)
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"This window closes in **30 minutes**."
)
embed.set_footer(
text=f"Paper Dynasty Season {PD_SEASON} \u2022 One player per pack",
icon_url=IMAGES["logo"],
)
return embed, card_lines
def build_scouted_card_list(
card_lines: list[tuple[int, str]],
scouted_cards: dict[int, str],
) -> str:
"""Rebuild the card list marking scouted cards with the scouter's team name.
Parameters
----------
card_lines : shuffled list of (player_id, display_line) tuples
scouted_cards : {player_id: scouter_team_name} for each claimed card
"""
result = []
for player_id, line in card_lines:
if player_id in scouted_cards:
team_name = scouted_cards[player_id]
result.append(f"{line} \u2014 \u2714\ufe0f *{team_name}*")
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
embed, card_lines = build_scout_embed(opener_team, pack_cards)
# 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,
)
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}")