- Add SCOUTABLE_PACK_TYPES env var (default: Standard,Premium) to control
which pack types offer scout opportunities
- Unify embed construction into build_scout_embed() — removes 3 near-duplicate
embed builders across scout_view.py and scouting.py
- Replace manual total_scouts counter with derived property from claims dict
- Remove redundant db_get("current") API call per scout click — use PD_SEASON
- Remove duplicate expiry computation in create_scout_opportunity
- Move send_to_channel to top-level import, remove redundant local import
- Update tests to match simplified code
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
244 lines
7.5 KiB
Python
244 lines
7.5 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_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}")
|