refactor: rename Evolution to Refractor system
All checks were successful
Build Docker Image / build (pull_request) Successful in 1m34s
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:
parent
d12cdb8d97
commit
6b4957ec70
@ -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))
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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()
|
||||||
Loading…
Reference in New Issue
Block a user