paper-dynasty-discord/cogs/refractor.py
Cal Corum 6b4957ec70
All checks were successful
Build Docker Image / build (pull_request) Successful in 1m34s
refactor: rename Evolution to Refractor system
- cogs/evolution.py → cogs/refractor.py (class, group, command names)
- Tier names: Base Chrome, Refractor, Gold Refractor, Superfractor
- Fix import: helpers.main.get_team_by_owner
- Fix shadowed builtin: type → card_type parameter
- Tests renamed and updated (39/39 pass)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:48:31 -05:00

203 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Refractor cog — /refractor status slash command.
Displays a team's refractor progress: formula value vs next threshold
with a progress bar, paginated 10 cards per page.
Depends on WP-07 (evolution/cards API endpoint).
"""
import logging
from typing import Optional
import discord
from discord import app_commands
from discord.ext import commands
from api_calls import db_get
from helpers.main import get_team_by_owner
logger = logging.getLogger("discord_app")
PAGE_SIZE = 10
TIER_NAMES = {
0: "Base Chrome",
1: "Refractor",
2: "Gold Refractor",
3: "Superfractor",
4: "Superfractor",
}
FORMULA_LABELS = {
"batter": "PA+TB×2",
"sp": "IP+K",
"rp": "IP+K",
}
def render_progress_bar(current: int, threshold: int, width: int = 10) -> str:
"""
Render a fixed-width ASCII progress bar.
Examples:
render_progress_bar(120, 149) -> '[========--]'
render_progress_bar(0, 100) -> '[----------]'
render_progress_bar(100, 100) -> '[==========]'
"""
if threshold <= 0:
filled = width
else:
ratio = min(current / threshold, 1.0)
filled = round(ratio * width)
empty = width - filled
return f"[{'=' * filled}{'-' * empty}]"
def format_refractor_entry(card_state: dict) -> str:
"""
Format a single card state dict as a display string.
Expected keys: player_name, card_type, current_tier, formula_value,
next_threshold (None if fully evolved).
Output example:
**Mike Trout** (Refractor)
[========--] 120/149 (PA+TB×2) — T1 → T2
"""
player_name = card_state.get("player_name", "Unknown")
card_type = card_state.get("card_type", "batter")
current_tier = card_state.get("current_tier", 0)
formula_value = card_state.get("formula_value", 0)
next_threshold = card_state.get("next_threshold")
tier_label = TIER_NAMES.get(current_tier, f"T{current_tier}")
formula_label = FORMULA_LABELS.get(card_type, card_type)
if current_tier >= 4 or next_threshold is None:
bar = "[==========]"
detail = "FULLY EVOLVED ★"
else:
bar = render_progress_bar(formula_value, next_threshold)
detail = f"{formula_value}/{next_threshold} ({formula_label}) — T{current_tier} → T{current_tier + 1}"
first_line = f"**{player_name}** ({tier_label})"
second_line = f"{bar} {detail}"
return f"{first_line}\n{second_line}"
def apply_close_filter(card_states: list) -> list:
"""
Return only cards within 80% of their next tier threshold.
Fully evolved cards (T4 or no next_threshold) are excluded.
"""
result = []
for state in card_states:
current_tier = state.get("current_tier", 0)
formula_value = state.get("formula_value", 0)
next_threshold = state.get("next_threshold")
if current_tier >= 4 or not next_threshold:
continue
if formula_value >= 0.8 * next_threshold:
result.append(state)
return result
def paginate(items: list, page: int, page_size: int = PAGE_SIZE) -> tuple:
"""
Slice items for the given 1-indexed page.
Returns (page_items, total_pages). Page is clamped to valid range.
"""
total_pages = max(1, (len(items) + page_size - 1) // page_size)
page = max(1, min(page, total_pages))
start = (page - 1) * page_size
return items[start : start + page_size], total_pages
class Refractor(commands.Cog):
"""Refractor progress tracking slash commands."""
def __init__(self, bot):
self.bot = bot
refractor_group = app_commands.Group(
name="refractor", description="Refractor tracking commands"
)
@refractor_group.command(
name="status", description="Show your team's refractor progress"
)
@app_commands.describe(
card_type="Card type filter (batter, sp, rp)",
season="Season number (default: current)",
tier="Filter by current tier (0-4)",
progress='Use "close" to show cards within 80% of their next tier',
page="Page number (default: 1, 10 cards per page)",
)
async def refractor_status(
self,
interaction: discord.Interaction,
card_type: Optional[str] = None,
season: Optional[int] = None,
tier: Optional[int] = None,
progress: Optional[str] = None,
page: int = 1,
):
"""Show a paginated view of the invoking user's team refractor progress."""
await interaction.response.defer(ephemeral=True)
team = await get_team_by_owner(interaction.user.id)
if not team:
await interaction.edit_original_response(
content="You don't have a team. Sign up with /newteam first."
)
return
params = [("team_id", team["id"])]
if card_type:
params.append(("card_type", card_type))
if season is not None:
params.append(("season", season))
if tier is not None:
params.append(("tier", tier))
data = await db_get("evolution/cards", params=params)
if not data:
await interaction.edit_original_response(
content="No refractor data found for your team."
)
return
items = data if isinstance(data, list) else data.get("cards", [])
if not items:
await interaction.edit_original_response(
content="No refractor data found for your team."
)
return
if progress == "close":
items = apply_close_filter(items)
if not items:
await interaction.edit_original_response(
content="No cards are currently close to a tier advancement."
)
return
page_items, total_pages = paginate(items, page)
lines = [format_refractor_entry(state) for state in page_items]
embed = discord.Embed(
title=f"{team['sname']} Refractor Status",
description="\n\n".join(lines),
color=0x6F42C1,
)
embed.set_footer(text=f"Page {page}/{total_pages} · {len(items)} card(s) total")
await interaction.edit_original_response(embed=embed)
async def setup(bot):
await bot.add_cog(Refractor(bot))