paper-dynasty-discord/paperdynasty.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

152 lines
4.8 KiB
Python

import discord
import datetime
import logging
from logging.handlers import RotatingFileHandler
import asyncio
import os
from discord.ext import commands
from in_game.gameplay_models import create_db_and_tables, Session, engine
from in_game.gameplay_queries import get_channel_game_or_none
from health_server import run_health_server
from notify_restart import send_restart_notification
raw_log_level = os.getenv("LOG_LEVEL")
if raw_log_level == "DEBUG":
log_level = logging.DEBUG
elif raw_log_level == "INFO":
log_level = logging.INFO
elif raw_log_level == "WARN":
log_level = logging.WARNING
else:
log_level = logging.ERROR
# date = f'{datetime.datetime.now().year}-{datetime.datetime.now().month}-{datetime.datetime.now().day}'
# logger.basicConfig(
# filename=f'logs/{date}.log',
# format='%(asctime)s - %(levelname)s - %(message)s',
# level=log_level
# )
# logger.getLogger('discord.http').setLevel(logger.INFO)
logger = logging.getLogger("discord_app")
logger.setLevel(log_level)
handler = RotatingFileHandler(
filename="logs/discord.log",
# encoding='utf-8',
maxBytes=32 * 1024 * 1024, # 32 MiB
backupCount=5, # Rotate through 5 files
)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
# dt_fmt = '%Y-%m-%d %H:%M:%S'
# formatter = logger.Formatter('[{asctime}] [{levelname:<8}] {name}: {message}', dt_fmt, style='{')
# handler.setFormatter(formatter)
logger.addHandler(handler)
COGS = [
"cogs.owner",
"cogs.admins",
"cogs.economy",
"cogs.players",
"cogs.gameplay",
"cogs.economy_new.scouting",
]
intents = discord.Intents.default()
intents.members = True
intents.message_content = True
bot = commands.Bot(
command_prefix=".",
intents=intents,
# help_command=None,
description="The Paper Dynasty Bot\nIf you have questions, feel free to contact Cal.",
case_insensitive=True,
owner_id=258104532423147520,
)
@bot.event
async def on_ready():
logger.info("Logged in as:")
logger.info(bot.user.name)
logger.info(bot.user.id)
# Send restart notification if configured
send_restart_notification()
@bot.tree.error
async def on_app_command_error(
interaction: discord.Interaction, error: discord.app_commands.AppCommandError
):
"""Global error handler for all app commands (slash commands)."""
logger.error(f"App command error in {interaction.command}: {error}", exc_info=error)
# CRITICAL: Release play lock if command failed during gameplay
# This prevents permanent user lockouts when exceptions occur
try:
with Session(engine) as session:
game = get_channel_game_or_none(session, interaction.channel_id)
if game:
current_play = game.current_play_or_none(session)
if current_play and current_play.locked and not current_play.complete:
logger.warning(
f"Releasing stuck play lock {current_play.id} in game {game.id} "
f"after command error: {error}"
)
current_play.locked = False
session.add(current_play)
session.commit()
except Exception as lock_error:
logger.error(
f"Failed to release play lock after error: {lock_error}",
exc_info=lock_error,
)
# Try to respond to the user
try:
if not interaction.response.is_done():
await interaction.response.send_message(
f"❌ An error occurred: {str(error)}", ephemeral=True
)
else:
await interaction.followup.send(
f"❌ An error occurred: {str(error)}", ephemeral=True
)
except Exception as e:
logger.error(f"Failed to send error message to user: {e}")
async def main():
create_db_and_tables()
for c in COGS:
try:
await bot.load_extension(c)
logger.info(f"Loaded cog: {c}")
except Exception as e:
logger.error(f"Failed to load cog: {c}")
logger.error(f"{e}")
# Start health server and bot concurrently
async with bot:
# Create health server task
health_task = asyncio.create_task(run_health_server(bot))
try:
# Start bot (this blocks until bot stops)
await bot.start(os.environ.get("BOT_TOKEN", "NONE"))
finally:
# Cleanup: cancel health server when bot stops
health_task.cancel()
try:
await health_task
except asyncio.CancelledError:
pass
asyncio.run(main())