All checks were successful
Ruff Lint / lint (pull_request) Successful in 13s
Remove unused `player_name` param from `_execute_refractor_test` and unused `final` param from `update_embed` closure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
390 lines
14 KiB
Python
390 lines
14 KiB
Python
"""Dev-only tools for testing Paper Dynasty systems.
|
|
|
|
This cog is only loaded when DATABASE != prod. It provides commands
|
|
for integration testing that create and clean up synthetic test data.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import date
|
|
|
|
import discord
|
|
from discord import app_commands
|
|
from discord.ext import commands
|
|
|
|
from api_calls import db_delete, db_get, db_post
|
|
from helpers.constants import PD_SEASON
|
|
from helpers.main import get_team_by_owner
|
|
from helpers.refractor_constants import TIER_NAMES
|
|
from helpers.refractor_test_data import (
|
|
build_batter_plays,
|
|
build_decision_data,
|
|
build_game_data,
|
|
build_pitcher_plays,
|
|
calculate_plays_needed,
|
|
)
|
|
|
|
CURRENT_SEASON = PD_SEASON
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CleanupView(discord.ui.View):
|
|
"""Post-test buttons to clean up or keep synthetic game data."""
|
|
|
|
def __init__(
|
|
self, owner_id: int, game_id: int, embed: discord.Embed, timeout: float = 300.0
|
|
):
|
|
super().__init__(timeout=timeout)
|
|
self.owner_id = owner_id
|
|
self.game_id = game_id
|
|
self.embed = embed
|
|
|
|
@discord.ui.button(
|
|
label="Clean Up Test Data", style=discord.ButtonStyle.danger, emoji="🧹"
|
|
)
|
|
async def cleanup_btn(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
if interaction.user.id != self.owner_id:
|
|
return
|
|
|
|
try:
|
|
await db_delete("decisions/game", self.game_id)
|
|
await db_delete("plays/game", self.game_id)
|
|
await db_delete("games", self.game_id)
|
|
self.embed.add_field(
|
|
name="",
|
|
value=f"🧹 Test data cleaned up (game #{self.game_id} removed)",
|
|
inline=False,
|
|
)
|
|
except Exception as e:
|
|
self.embed.add_field(
|
|
name="",
|
|
value=f"❌ Cleanup failed: {e}",
|
|
inline=False,
|
|
)
|
|
|
|
self.clear_items()
|
|
await interaction.response.edit_message(embed=self.embed, view=self)
|
|
self.stop()
|
|
|
|
@discord.ui.button(
|
|
label="Keep Test Data", style=discord.ButtonStyle.secondary, emoji="📌"
|
|
)
|
|
async def keep_btn(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
if interaction.user.id != self.owner_id:
|
|
return
|
|
|
|
self.embed.add_field(
|
|
name="",
|
|
value=f"📌 Test data kept (game #{self.game_id})",
|
|
inline=False,
|
|
)
|
|
self.clear_items()
|
|
await interaction.response.edit_message(embed=self.embed, view=self)
|
|
self.stop()
|
|
|
|
async def on_timeout(self):
|
|
# Note: clear_items() updates the local view but cannot push to Discord
|
|
# without a message reference. Buttons will become unresponsive after timeout.
|
|
self.clear_items()
|
|
|
|
|
|
class DevToolsCog(commands.Cog):
|
|
"""Dev-only commands for integration testing.
|
|
Only loaded when DATABASE env var is not 'prod'.
|
|
"""
|
|
|
|
def __init__(self, bot: commands.Bot):
|
|
self.bot = bot
|
|
|
|
group_dev = app_commands.Group(name="dev", description="Dev-only testing tools")
|
|
|
|
@group_dev.command(
|
|
name="refractor-test", description="Run refractor integration test on a card"
|
|
)
|
|
@app_commands.describe(card_id="The batting or pitching card ID to test")
|
|
async def refractor_test(self, interaction: discord.Interaction, card_id: int):
|
|
await interaction.response.defer()
|
|
|
|
# --- Phase 1: Setup ---
|
|
# Look up card (try batting first, then pitching)
|
|
card = await db_get("battingcards", object_id=card_id)
|
|
card_type_key = "batting"
|
|
if card is None:
|
|
card = await db_get("pitchingcards", object_id=card_id)
|
|
card_type_key = "pitching"
|
|
|
|
if card is None:
|
|
await interaction.edit_original_response(
|
|
content=f"❌ Card #{card_id} not found (checked batting and pitching)."
|
|
)
|
|
return
|
|
|
|
player_id = card["player"]["id"]
|
|
player_name = card["player"]["p_name"]
|
|
team_id = card.get("team_id") or card["player"].get("team_id")
|
|
|
|
if team_id is None:
|
|
team = await get_team_by_owner(interaction.user.id)
|
|
if team is None:
|
|
await interaction.edit_original_response(
|
|
content="❌ Could not determine team ID. You must own a team."
|
|
)
|
|
return
|
|
team_id = team["id"]
|
|
|
|
# Fetch refractor state
|
|
refractor_data = await db_get(
|
|
"refractor/cards",
|
|
params=[("team_id", team_id), ("limit", 100)],
|
|
)
|
|
|
|
# Find this player's entry
|
|
card_state = None
|
|
if refractor_data and refractor_data.get("items"):
|
|
for item in refractor_data["items"]:
|
|
if item["player_id"] == player_id:
|
|
card_state = item
|
|
break
|
|
|
|
# Determine current state and thresholds
|
|
if card_state:
|
|
current_tier = card_state["current_tier"]
|
|
current_value = card_state["current_value"]
|
|
card_type = card_state["track"]["card_type"]
|
|
next_threshold = card_state["next_threshold"]
|
|
else:
|
|
current_tier = 0
|
|
current_value = 0
|
|
card_type = "batter" if card_type_key == "batting" else "sp"
|
|
next_threshold = (
|
|
37 if card_type == "batter" else (10 if card_type == "sp" else 3)
|
|
)
|
|
|
|
if current_tier >= 4:
|
|
await interaction.edit_original_response(
|
|
content=f"⚠️ {player_name} is already at T4 Superfractor — fully evolved."
|
|
)
|
|
return
|
|
|
|
# Calculate plan
|
|
gap = max(0, next_threshold - current_value)
|
|
plan = calculate_plays_needed(gap, card_type)
|
|
|
|
# Find an opposing player
|
|
if card_type == "batter":
|
|
opposing_cards = await db_get(
|
|
"pitchingcards",
|
|
params=[("team_id", team_id), ("variant", 0)],
|
|
)
|
|
else:
|
|
opposing_cards = await db_get(
|
|
"battingcards",
|
|
params=[("team_id", team_id), ("variant", 0)],
|
|
)
|
|
|
|
if not opposing_cards or not opposing_cards.get("cards"):
|
|
await interaction.edit_original_response(
|
|
content=f"❌ No opposing {'pitcher' if card_type == 'batter' else 'batter'} cards found on team {team_id}."
|
|
)
|
|
return
|
|
|
|
opposing_player_id = opposing_cards["cards"][0]["player"]["id"]
|
|
|
|
# Build and send initial embed
|
|
tier_name = TIER_NAMES.get(current_tier, f"T{current_tier}")
|
|
next_tier_name = TIER_NAMES.get(current_tier + 1, f"T{current_tier + 1}")
|
|
play_desc = (
|
|
f"{plan['num_plays']} HR plays"
|
|
if card_type == "batter"
|
|
else f"{plan['num_plays']} K plays"
|
|
)
|
|
|
|
embed = discord.Embed(
|
|
title="Refractor Integration Test",
|
|
color=0x3498DB,
|
|
)
|
|
embed.add_field(
|
|
name="Setup",
|
|
value=(
|
|
f"**Player:** {player_name} (card #{card_id})\n"
|
|
f"**Type:** {card_type_key.title()}\n"
|
|
f"**Current:** T{current_tier} {tier_name} → **Target:** T{current_tier + 1} {next_tier_name}\n"
|
|
f"**Value:** {current_value} / {next_threshold} (need {gap} more)\n"
|
|
f"**Plan:** {play_desc} (+{plan['total_value']:.0f} value)"
|
|
),
|
|
inline=False,
|
|
)
|
|
embed.add_field(name="", value="⏳ Executing...", inline=False)
|
|
await interaction.edit_original_response(embed=embed)
|
|
|
|
# --- Phase 2: Execute ---
|
|
await self._execute_refractor_test(
|
|
interaction=interaction,
|
|
embed=embed,
|
|
player_id=player_id,
|
|
team_id=team_id,
|
|
card_type=card_type,
|
|
card_type_key=card_type_key,
|
|
opposing_player_id=opposing_player_id,
|
|
num_plays=plan["num_plays"],
|
|
)
|
|
|
|
async def _execute_refractor_test(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
embed: discord.Embed,
|
|
player_id: int,
|
|
team_id: int,
|
|
card_type: str,
|
|
card_type_key: str,
|
|
opposing_player_id: int,
|
|
num_plays: int,
|
|
):
|
|
"""Execute the refractor integration test chain.
|
|
|
|
Creates synthetic game data, runs the real refractor pipeline,
|
|
and reports pass/fail at each step. Stops on first failure.
|
|
"""
|
|
results = []
|
|
game_id = None
|
|
|
|
# Remove the "Executing..." field
|
|
if len(embed.fields) > 1:
|
|
embed.remove_field(len(embed.fields) - 1)
|
|
|
|
# Helper to update the embed with current results
|
|
async def update_embed(view: discord.ui.View | None = None):
|
|
results_text = "\n".join(results)
|
|
# Remove old results field if present, add new one
|
|
while len(embed.fields) > 1:
|
|
embed.remove_field(len(embed.fields) - 1)
|
|
embed.add_field(name="Results", value=results_text, inline=False)
|
|
if view is not None:
|
|
await interaction.edit_original_response(embed=embed, view=view)
|
|
else:
|
|
await interaction.edit_original_response(embed=embed)
|
|
|
|
try:
|
|
# Step 1: Create game
|
|
game_data = build_game_data(team_id=team_id, season=CURRENT_SEASON)
|
|
game_resp = await db_post("games", payload=game_data)
|
|
game_id = game_resp["id"]
|
|
results.append(f"✅ Game created (#{game_id})")
|
|
await update_embed()
|
|
except Exception as e:
|
|
results.append(f"❌ Game creation failed: {e}")
|
|
await update_embed()
|
|
return
|
|
|
|
try:
|
|
# Step 2: Create plays
|
|
if card_type == "batter":
|
|
plays = build_batter_plays(
|
|
game_id, player_id, team_id, opposing_player_id, num_plays
|
|
)
|
|
else:
|
|
plays = build_pitcher_plays(
|
|
game_id, player_id, team_id, opposing_player_id, num_plays
|
|
)
|
|
await db_post("plays", payload={"plays": plays})
|
|
results.append(f"✅ {num_plays} plays inserted")
|
|
await update_embed()
|
|
except Exception as e:
|
|
results.append(f"❌ Play insertion failed: {e}")
|
|
await update_embed(view=CleanupView(interaction.user.id, game_id, embed))
|
|
return
|
|
|
|
try:
|
|
# Step 3: Create pitcher decision
|
|
pitcher_id = opposing_player_id if card_type == "batter" else player_id
|
|
decision_data = build_decision_data(
|
|
game_id, pitcher_id, team_id, CURRENT_SEASON
|
|
)
|
|
await db_post("decisions", payload=decision_data)
|
|
results.append("✅ Pitcher decision inserted")
|
|
await update_embed()
|
|
except Exception as e:
|
|
results.append(f"❌ Decision insertion failed: {e}")
|
|
await update_embed(view=CleanupView(interaction.user.id, game_id, embed))
|
|
return
|
|
|
|
try:
|
|
# Step 4: Update season stats
|
|
stats_resp = await db_post(f"season-stats/update-game/{game_id}")
|
|
if stats_resp and stats_resp.get("skipped"):
|
|
results.append("⚠️ Season stats skipped (already processed)")
|
|
else:
|
|
updated = stats_resp.get("updated", "?") if stats_resp else "?"
|
|
results.append(f"✅ Season stats updated ({updated} players)")
|
|
await update_embed()
|
|
except Exception as e:
|
|
results.append(f"❌ Season stats update failed: {e}")
|
|
results.append("⏭️ Skipped: evaluate-game (depends on season stats)")
|
|
await update_embed(view=CleanupView(interaction.user.id, game_id, embed))
|
|
return
|
|
|
|
try:
|
|
# Step 5: Evaluate refractor
|
|
eval_resp = await db_post(f"refractor/evaluate-game/{game_id}")
|
|
tier_ups = eval_resp.get("tier_ups", []) if eval_resp else []
|
|
|
|
if tier_ups:
|
|
for tu in tier_ups:
|
|
old_t = tu.get("old_tier", "?")
|
|
new_t = tu.get("new_tier", "?")
|
|
variant = tu.get("variant_created", "")
|
|
results.append(f"✅ Tier-up detected! T{old_t} → T{new_t}")
|
|
if variant:
|
|
results.append(f"✅ Variant card created (variant: {variant})")
|
|
else:
|
|
evaluated = eval_resp.get("evaluated", "?") if eval_resp else "?"
|
|
results.append(f"⚠️ No tier-up detected (evaluated {evaluated} cards)")
|
|
await update_embed()
|
|
except Exception as e:
|
|
results.append(f"❌ Evaluate-game failed: {e}")
|
|
await update_embed(view=CleanupView(interaction.user.id, game_id, embed))
|
|
return
|
|
|
|
# Step 6: Trigger card render (if tier-up)
|
|
if tier_ups:
|
|
for tu in tier_ups:
|
|
variant = tu.get("variant_created")
|
|
if not variant:
|
|
continue
|
|
try:
|
|
today = date.today().isoformat()
|
|
render_resp = await db_get(
|
|
f"players/{tu['player_id']}/{card_type_key}card/{today}/{variant}",
|
|
none_okay=True,
|
|
)
|
|
if render_resp:
|
|
results.append("✅ Card rendered + S3 upload triggered")
|
|
img_url = (
|
|
render_resp
|
|
if isinstance(render_resp, str)
|
|
else render_resp.get("image_url")
|
|
)
|
|
if (
|
|
img_url
|
|
and isinstance(img_url, str)
|
|
and img_url.startswith("http")
|
|
):
|
|
embed.set_image(url=img_url)
|
|
else:
|
|
results.append(
|
|
"⚠️ Card render returned no data (may still be processing)"
|
|
)
|
|
except Exception as e:
|
|
results.append(f"⚠️ Card render failed (non-fatal): {e}")
|
|
|
|
# Final update with cleanup buttons
|
|
await update_embed(view=CleanupView(interaction.user.id, game_id, embed))
|
|
|
|
|
|
async def setup(bot: commands.Bot):
|
|
await bot.add_cog(DevToolsCog(bot))
|