refactor: rename Evolution to Refractor system
All checks were successful
Build Docker Image / build (pull_request) Successful in 1m34s

- 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>
This commit is contained in:
Cal Corum 2026-03-23 08:48:31 -05:00
parent d12cdb8d97
commit 6b4957ec70
3 changed files with 80 additions and 80 deletions

View File

@ -1,7 +1,7 @@
""" """
Evolution cog /evo status slash command. Refractor cog /refractor status slash command.
Displays a team's evolution progress: formula value vs next threshold Displays a team's refractor progress: formula value vs next threshold
with a progress bar, paginated 10 cards per page. with a progress bar, paginated 10 cards per page.
Depends on WP-07 (evolution/cards API endpoint). Depends on WP-07 (evolution/cards API endpoint).
@ -15,22 +15,22 @@ from discord import app_commands
from discord.ext import commands from discord.ext import commands
from api_calls import db_get from api_calls import db_get
from helpers import get_team_by_owner from helpers.main import get_team_by_owner
logger = logging.getLogger("discord_app") logger = logging.getLogger("discord_app")
PAGE_SIZE = 10 PAGE_SIZE = 10
TIER_NAMES = { TIER_NAMES = {
0: "Unranked", 0: "Base Chrome",
1: "Initiate", 1: "Refractor",
2: "Rising", 2: "Gold Refractor",
3: "Ascendant", 3: "Superfractor",
4: "Evolved", 4: "Superfractor",
} }
FORMULA_LABELS = { FORMULA_LABELS = {
"batter": "PA+TB\u00d72", "batter": "PA+TB×2",
"sp": "IP+K", "sp": "IP+K",
"rp": "IP+K", "rp": "IP+K",
} }
@ -54,7 +54,7 @@ def render_progress_bar(current: int, threshold: int, width: int = 10) -> str:
return f"[{'=' * filled}{'-' * empty}]" return f"[{'=' * filled}{'-' * empty}]"
def format_evo_entry(card_state: dict) -> str: def format_refractor_entry(card_state: dict) -> str:
""" """
Format a single card state dict as a display string. Format a single card state dict as a display string.
@ -62,7 +62,7 @@ def format_evo_entry(card_state: dict) -> str:
next_threshold (None if fully evolved). next_threshold (None if fully evolved).
Output example: Output example:
**Mike Trout** (Initiate) **Mike Trout** (Refractor)
[========--] 120/149 (PA+TB×2) T1 T2 [========--] 120/149 (PA+TB×2) T1 T2
""" """
player_name = card_state.get("player_name", "Unknown") player_name = card_state.get("player_name", "Unknown")
@ -76,10 +76,10 @@ def format_evo_entry(card_state: dict) -> str:
if current_tier >= 4 or next_threshold is None: if current_tier >= 4 or next_threshold is None:
bar = "[==========]" bar = "[==========]"
detail = "FULLY EVOLVED \u2605" detail = "FULLY EVOLVED "
else: else:
bar = render_progress_bar(formula_value, next_threshold) bar = render_progress_bar(formula_value, next_threshold)
detail = f"{formula_value}/{next_threshold} ({formula_label}) \u2014 T{current_tier} \u2192 T{current_tier + 1}" detail = f"{formula_value}/{next_threshold} ({formula_label}) — T{current_tier} T{current_tier + 1}"
first_line = f"**{player_name}** ({tier_label})" first_line = f"**{player_name}** ({tier_label})"
second_line = f"{bar} {detail}" second_line = f"{bar} {detail}"
@ -116,34 +116,36 @@ def paginate(items: list, page: int, page_size: int = PAGE_SIZE) -> tuple:
return items[start : start + page_size], total_pages return items[start : start + page_size], total_pages
class Evolution(commands.Cog): class Refractor(commands.Cog):
"""Evolution progress tracking slash commands.""" """Refractor progress tracking slash commands."""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
evo_group = app_commands.Group( refractor_group = app_commands.Group(
name="evo", description="Evolution tracking commands" name="refractor", description="Refractor tracking commands"
) )
@evo_group.command(name="status", description="Show your team's evolution progress") @refractor_group.command(
name="status", description="Show your team's refractor progress"
)
@app_commands.describe( @app_commands.describe(
type="Card type filter (batter, sp, rp)", card_type="Card type filter (batter, sp, rp)",
season="Season number (default: current)", season="Season number (default: current)",
tier="Filter by current tier (0-4)", tier="Filter by current tier (0-4)",
progress='Use "close" to show cards within 80% of their next tier', progress='Use "close" to show cards within 80% of their next tier',
page="Page number (default: 1, 10 cards per page)", page="Page number (default: 1, 10 cards per page)",
) )
async def evo_status( async def refractor_status(
self, self,
interaction: discord.Interaction, interaction: discord.Interaction,
type: Optional[str] = None, card_type: Optional[str] = None,
season: Optional[int] = None, season: Optional[int] = None,
tier: Optional[int] = None, tier: Optional[int] = None,
progress: Optional[str] = None, progress: Optional[str] = None,
page: int = 1, page: int = 1,
): ):
"""Show a paginated view of the invoking user's team evolution progress.""" """Show a paginated view of the invoking user's team refractor progress."""
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
team = await get_team_by_owner(interaction.user.id) team = await get_team_by_owner(interaction.user.id)
@ -154,8 +156,8 @@ class Evolution(commands.Cog):
return return
params = [("team_id", team["id"])] params = [("team_id", team["id"])]
if type: if card_type:
params.append(("card_type", type)) params.append(("card_type", card_type))
if season is not None: if season is not None:
params.append(("season", season)) params.append(("season", season))
if tier is not None: if tier is not None:
@ -164,14 +166,14 @@ class Evolution(commands.Cog):
data = await db_get("evolution/cards", params=params) data = await db_get("evolution/cards", params=params)
if not data: if not data:
await interaction.edit_original_response( await interaction.edit_original_response(
content="No evolution data found for your team." content="No refractor data found for your team."
) )
return return
items = data if isinstance(data, list) else data.get("cards", []) items = data if isinstance(data, list) else data.get("cards", [])
if not items: if not items:
await interaction.edit_original_response( await interaction.edit_original_response(
content="No evolution data found for your team." content="No refractor data found for your team."
) )
return return
@ -184,19 +186,17 @@ class Evolution(commands.Cog):
return return
page_items, total_pages = paginate(items, page) page_items, total_pages = paginate(items, page)
lines = [format_evo_entry(state) for state in page_items] lines = [format_refractor_entry(state) for state in page_items]
embed = discord.Embed( embed = discord.Embed(
title=f"{team['sname']} Evolution Status", title=f"{team['sname']} Refractor Status",
description="\n\n".join(lines), description="\n\n".join(lines),
color=0x6F42C1, color=0x6F42C1,
) )
embed.set_footer( embed.set_footer(text=f"Page {page}/{total_pages} · {len(items)} card(s) total")
text=f"Page {page}/{total_pages} \u00b7 {len(items)} card(s) total"
)
await interaction.edit_original_response(embed=embed) await interaction.edit_original_response(embed=embed)
async def setup(bot): async def setup(bot):
await bot.add_cog(Evolution(bot)) await bot.add_cog(Refractor(bot))

View File

@ -53,7 +53,7 @@ COGS = [
"cogs.players", "cogs.players",
"cogs.gameplay", "cogs.gameplay",
"cogs.economy_new.scouting", "cogs.economy_new.scouting",
"cogs.evolution", "cogs.refractor",
] ]
intents = discord.Intents.default() intents = discord.Intents.default()

View File

@ -1,9 +1,9 @@
""" """
Unit tests for evolution command helper functions (WP-11). Unit tests for refractor command helper functions (WP-11).
Tests cover: Tests cover:
- render_progress_bar: ASCII bar rendering at various fill levels - render_progress_bar: ASCII bar rendering at various fill levels
- format_evo_entry: Full card state formatting including fully evolved case - format_refractor_entry: Full card state formatting including fully evolved case
- apply_close_filter: 80% proximity filter logic - apply_close_filter: 80% proximity filter logic
- paginate: 1-indexed page slicing and total-page calculation - paginate: 1-indexed page slicing and total-page calculation
- TIER_NAMES: Display names for all tiers - TIER_NAMES: Display names for all tiers
@ -23,9 +23,9 @@ from discord.ext import commands
# Make the repo root importable # Make the repo root importable
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from cogs.evolution import ( from cogs.refractor import (
render_progress_bar, render_progress_bar,
format_evo_entry, format_refractor_entry,
apply_close_filter, apply_close_filter,
paginate, paginate,
TIER_NAMES, TIER_NAMES,
@ -118,13 +118,13 @@ class TestRenderProgressBar:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# format_evo_entry # format_refractor_entry
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestFormatEvoEntry: class TestFormatRefractorEntry:
""" """
Tests for format_evo_entry(). Tests for format_refractor_entry().
Verifies player name, tier label, progress bar, formula label, Verifies player name, tier label, progress bar, formula label,
and the special fully-evolved formatting. and the special fully-evolved formatting.
@ -132,54 +132,54 @@ class TestFormatEvoEntry:
def test_player_name_in_output(self, batter_state): def test_player_name_in_output(self, batter_state):
"""Player name is bold in the first line.""" """Player name is bold in the first line."""
result = format_evo_entry(batter_state) result = format_refractor_entry(batter_state)
assert "**Mike Trout**" in result assert "**Mike Trout**" in result
def test_tier_label_in_output(self, batter_state): def test_tier_label_in_output(self, batter_state):
"""Current tier name (Initiate for T1) appears in output.""" """Current tier name (Refractor for T1) appears in output."""
result = format_evo_entry(batter_state) result = format_refractor_entry(batter_state)
assert "(Initiate)" in result assert "(Refractor)" in result
def test_progress_values_in_output(self, batter_state): def test_progress_values_in_output(self, batter_state):
"""current/threshold values appear in output.""" """current/threshold values appear in output."""
result = format_evo_entry(batter_state) result = format_refractor_entry(batter_state)
assert "120/149" in result assert "120/149" in result
def test_formula_label_batter(self, batter_state): def test_formula_label_batter(self, batter_state):
"""Batter formula label PA+TB×2 appears in output.""" """Batter formula label PA+TB×2 appears in output."""
result = format_evo_entry(batter_state) result = format_refractor_entry(batter_state)
assert "PA+TB\u00d72" in result assert "PA+TB×2" in result
def test_tier_progression_arrow(self, batter_state): def test_tier_progression_arrow(self, batter_state):
"""T1 → T2 arrow progression appears for non-evolved cards.""" """T1 → T2 arrow progression appears for non-evolved cards."""
result = format_evo_entry(batter_state) result = format_refractor_entry(batter_state)
assert "T1 \u2192 T2" in result assert "T1 T2" in result
def test_sp_formula_label(self, sp_state): def test_sp_formula_label(self, sp_state):
"""SP formula label IP+K appears for starting pitchers.""" """SP formula label IP+K appears for starting pitchers."""
result = format_evo_entry(sp_state) result = format_refractor_entry(sp_state)
assert "IP+K" in result assert "IP+K" in result
def test_fully_evolved_no_threshold(self, evolved_state): def test_fully_evolved_no_threshold(self, evolved_state):
"""T4 card with next_threshold=None shows FULLY EVOLVED.""" """T4 card with next_threshold=None shows FULLY EVOLVED."""
result = format_evo_entry(evolved_state) result = format_refractor_entry(evolved_state)
assert "FULLY EVOLVED" in result assert "FULLY EVOLVED" in result
def test_fully_evolved_by_tier(self, batter_state): def test_fully_evolved_by_tier(self, batter_state):
"""current_tier=4 triggers fully evolved display even with a threshold.""" """current_tier=4 triggers fully evolved display even with a threshold."""
batter_state["current_tier"] = 4 batter_state["current_tier"] = 4
batter_state["next_threshold"] = 200 batter_state["next_threshold"] = 200
result = format_evo_entry(batter_state) result = format_refractor_entry(batter_state)
assert "FULLY EVOLVED" in result assert "FULLY EVOLVED" in result
def test_fully_evolved_no_arrow(self, evolved_state): def test_fully_evolved_no_arrow(self, evolved_state):
"""Fully evolved cards don't show a tier arrow.""" """Fully evolved cards don't show a tier arrow."""
result = format_evo_entry(evolved_state) result = format_refractor_entry(evolved_state)
assert "\u2192" not in result assert "" not in result
def test_two_line_output(self, batter_state): def test_two_line_output(self, batter_state):
"""Output always has exactly two lines (name line + bar line).""" """Output always has exactly two lines (name line + bar line)."""
result = format_evo_entry(batter_state) result = format_refractor_entry(batter_state)
lines = result.split("\n") lines = result.split("\n")
assert len(lines) == 2 assert len(lines) == 2
@ -307,23 +307,23 @@ class TestTierNames:
""" """
Verify all tier display names are correctly defined. Verify all tier display names are correctly defined.
T0=Unranked, T1=Initiate, T2=Rising, T3=Ascendant, T4=Evolved T0=Base Chrome, T1=Refractor, T2=Gold Refractor, T3=Superfractor, T4=Superfractor
""" """
def test_t0_unranked(self): def test_t0_base_chrome(self):
assert TIER_NAMES[0] == "Unranked" assert TIER_NAMES[0] == "Base Chrome"
def test_t1_initiate(self): def test_t1_refractor(self):
assert TIER_NAMES[1] == "Initiate" assert TIER_NAMES[1] == "Refractor"
def test_t2_rising(self): def test_t2_gold_refractor(self):
assert TIER_NAMES[2] == "Rising" assert TIER_NAMES[2] == "Gold Refractor"
def test_t3_ascendant(self): def test_t3_superfractor(self):
assert TIER_NAMES[3] == "Ascendant" assert TIER_NAMES[3] == "Superfractor"
def test_t4_evolved(self): def test_t4_superfractor(self):
assert TIER_NAMES[4] == "Evolved" assert TIER_NAMES[4] == "Superfractor"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -349,7 +349,7 @@ def mock_interaction():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_evo_status_no_team(mock_bot, mock_interaction): async def test_refractor_status_no_team(mock_bot, mock_interaction):
""" """
When the user has no team, the command replies with a signup prompt When the user has no team, the command replies with a signup prompt
and does not call db_get. and does not call db_get.
@ -357,13 +357,13 @@ async def test_evo_status_no_team(mock_bot, mock_interaction):
Why: get_team_by_owner returning None means the user is unregistered; Why: get_team_by_owner returning None means the user is unregistered;
the command must short-circuit before hitting the API. the command must short-circuit before hitting the API.
""" """
from cogs.evolution import Evolution from cogs.refractor import Refractor
cog = Evolution(mock_bot) cog = Refractor(mock_bot)
with patch("cogs.evolution.get_team_by_owner", new=AsyncMock(return_value=None)): with patch("cogs.refractor.get_team_by_owner", new=AsyncMock(return_value=None)):
with patch("cogs.evolution.db_get", new=AsyncMock()) as mock_db: with patch("cogs.refractor.db_get", new=AsyncMock()) as mock_db:
await cog.evo_status.callback(cog, mock_interaction) await cog.refractor_status.callback(cog, mock_interaction)
mock_db.assert_not_called() mock_db.assert_not_called()
call_kwargs = mock_interaction.edit_original_response.call_args call_kwargs = mock_interaction.edit_original_response.call_args
@ -372,23 +372,23 @@ async def test_evo_status_no_team(mock_bot, mock_interaction):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_evo_status_empty_roster(mock_bot, mock_interaction): async def test_refractor_status_empty_roster(mock_bot, mock_interaction):
""" """
When the API returns an empty card list, the command sends an When the API returns an empty card list, the command sends an
informative 'no data' message rather than an empty embed. informative 'no data' message rather than an empty embed.
Why: An empty list is valid (team has no evolved cards yet); Why: An empty list is valid (team has no refractor cards yet);
the command should not crash or send a blank embed. the command should not crash or send a blank embed.
""" """
from cogs.evolution import Evolution from cogs.refractor import Refractor
cog = Evolution(mock_bot) cog = Refractor(mock_bot)
team = {"id": 1, "sname": "Test"} team = {"id": 1, "sname": "Test"}
with patch("cogs.evolution.get_team_by_owner", new=AsyncMock(return_value=team)): with patch("cogs.refractor.get_team_by_owner", new=AsyncMock(return_value=team)):
with patch("cogs.evolution.db_get", new=AsyncMock(return_value={"cards": []})): with patch("cogs.refractor.db_get", new=AsyncMock(return_value={"cards": []})):
await cog.evo_status.callback(cog, mock_interaction) await cog.refractor_status.callback(cog, mock_interaction)
call_kwargs = mock_interaction.edit_original_response.call_args call_kwargs = mock_interaction.edit_original_response.call_args
content = call_kwargs.kwargs.get("content", "") content = call_kwargs.kwargs.get("content", "")
assert "no evolution data" in content.lower() assert "no refractor data" in content.lower()