fix: clean up refractor status display — suffix tags, compact layout, dead code removal
All checks were successful
Ruff Lint / lint (pull_request) Successful in 20s
All checks were successful
Ruff Lint / lint (pull_request) Successful in 20s
- Tier labels as suffix tags: **Name** — Base Chrome [T1] (T0 gets no suffix) - Compact progress line: bar value/threshold (pct) — removed formula and tier arrow - Fully evolved shows `MAX` instead of FULLY EVOLVED - Deleted unused FORMULA_LABELS dict - Added _FULL_BAR constant, moved T0-branch lookups into else - Fixed mock API shape in test (cards → items) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2d7c19814e
commit
bbad1daba2
@ -34,21 +34,17 @@ TIER_NAMES = {
|
|||||||
4: "Superfractor",
|
4: "Superfractor",
|
||||||
}
|
}
|
||||||
|
|
||||||
FORMULA_LABELS = {
|
# Tier-specific labels for the status display.
|
||||||
"batter": "PA+TB×2",
|
|
||||||
"sp": "IP+K",
|
|
||||||
"rp": "IP+K",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Tier-specific labels for the status display. T0 is blank (base cards need no prefix).
|
|
||||||
TIER_SYMBOLS = {
|
TIER_SYMBOLS = {
|
||||||
0: "", # Base Card — no prefix
|
0: "Base", # Base Card — used in summary only, not in per-card display
|
||||||
1: "T1", # Base Chrome
|
1: "T1", # Base Chrome
|
||||||
2: "T2", # Refractor
|
2: "T2", # Refractor
|
||||||
3: "T3", # Gold Refractor
|
3: "T3", # Gold Refractor
|
||||||
4: "T4★", # Superfractor — star earned
|
4: "T4★", # Superfractor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_FULL_BAR = "▰" * 12
|
||||||
|
|
||||||
# Embed accent colors per tier (used for single-tier filtered views).
|
# Embed accent colors per tier (used for single-tier filtered views).
|
||||||
TIER_COLORS = {
|
TIER_COLORS = {
|
||||||
0: 0x95A5A6, # slate grey
|
0: 0x95A5A6, # slate grey
|
||||||
@ -86,40 +82,38 @@ def _pct_label(current: int, threshold: int) -> str:
|
|||||||
|
|
||||||
def format_refractor_entry(card_state: dict) -> str:
|
def format_refractor_entry(card_state: dict) -> str:
|
||||||
"""
|
"""
|
||||||
Format a single card state dict as a rich display string.
|
Format a single card state dict as a compact two-line display string.
|
||||||
|
|
||||||
Output example (in-progress):
|
Output example (base card — no suffix):
|
||||||
◈ **Mike Trout** — Base Chrome
|
**Mike Trout**
|
||||||
▰▰▰▰▰▰▰▰▰▰▱▱ `120/149` 80% · PA+TB×2 · T1 → T2
|
▰▰▰▰▰▰▰▰▰▰▱▱ 120/149 (80%)
|
||||||
|
|
||||||
|
Output example (evolved — suffix tag):
|
||||||
|
**Mike Trout** — Base Chrome [T1]
|
||||||
|
▰▰▰▰▰▰▰▰▰▰▱▱ 120/149 (80%)
|
||||||
|
|
||||||
Output example (fully evolved):
|
Output example (fully evolved):
|
||||||
★ **Barry Bonds** — Superfractor
|
**Barry Bonds** — Superfractor [T4★]
|
||||||
▰▰▰▰▰▰▰▰▰▰▰▰ ✧ FULLY EVOLVED ✧
|
▰▰▰▰▰▰▰▰▰▰▰▰ `MAX`
|
||||||
"""
|
"""
|
||||||
player_name = card_state.get("player_name", "Unknown")
|
player_name = card_state.get("player_name", "Unknown")
|
||||||
track = card_state.get("track", {})
|
|
||||||
card_type = track.get("card_type", "batter")
|
|
||||||
current_tier = card_state.get("current_tier", 0)
|
current_tier = card_state.get("current_tier", 0)
|
||||||
formula_value = int(card_state.get("current_value", 0))
|
formula_value = int(card_state.get("current_value", 0))
|
||||||
next_threshold = int(card_state.get("next_threshold") or 0) or None
|
next_threshold = int(card_state.get("next_threshold") or 0) or None
|
||||||
|
|
||||||
tier_label = TIER_NAMES.get(current_tier, f"T{current_tier}")
|
if current_tier == 0:
|
||||||
formula_label = FORMULA_LABELS.get(card_type, card_type)
|
first_line = f"**{player_name}**"
|
||||||
symbol = TIER_SYMBOLS.get(current_tier, "")
|
else:
|
||||||
prefix = f"{symbol} " if symbol else ""
|
tier_label = TIER_NAMES.get(current_tier, f"T{current_tier}")
|
||||||
|
symbol = TIER_SYMBOLS.get(current_tier, "")
|
||||||
first_line = f"{prefix}**{player_name}** — {tier_label}"
|
first_line = f"**{player_name}** — {tier_label} [{symbol}]"
|
||||||
|
|
||||||
if current_tier >= 4 or next_threshold is None:
|
if current_tier >= 4 or next_threshold is None:
|
||||||
bar = render_progress_bar(1, 1)
|
second_line = f"{_FULL_BAR} `MAX`"
|
||||||
second_line = f"{bar} ✧ FULLY EVOLVED ✧"
|
|
||||||
else:
|
else:
|
||||||
bar = render_progress_bar(formula_value, next_threshold)
|
bar = render_progress_bar(formula_value, next_threshold)
|
||||||
pct = _pct_label(formula_value, next_threshold)
|
pct = _pct_label(formula_value, next_threshold)
|
||||||
second_line = (
|
second_line = f"{bar} {formula_value}/{next_threshold} ({pct})"
|
||||||
f"{bar} `{formula_value}/{next_threshold}` {pct}"
|
|
||||||
f" · {formula_label} · T{current_tier} → T{current_tier + 1}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return f"{first_line}\n{second_line}"
|
return f"{first_line}\n{second_line}"
|
||||||
|
|
||||||
@ -139,8 +133,7 @@ def build_tier_summary(items: list, total_count: int) -> str:
|
|||||||
parts = []
|
parts = []
|
||||||
for t in range(5):
|
for t in range(5):
|
||||||
if counts[t] > 0:
|
if counts[t] > 0:
|
||||||
label = TIER_SYMBOLS[t] or "Base"
|
parts.append(f"{TIER_SYMBOLS[t]}: {counts[t]}")
|
||||||
parts.append(f"{label}: {counts[t]}")
|
|
||||||
summary = " ".join(parts) if parts else "No cards"
|
summary = " ".join(parts) if parts else "No cards"
|
||||||
return f"{summary} — {total_count} total"
|
return f"{summary} — {total_count} total"
|
||||||
|
|
||||||
@ -165,17 +158,11 @@ def build_status_embed(
|
|||||||
if tier_filter is not None and tier_filter in TIER_COLORS:
|
if tier_filter is not None and tier_filter in TIER_COLORS:
|
||||||
embed.color = TIER_COLORS[tier_filter]
|
embed.color = TIER_COLORS[tier_filter]
|
||||||
|
|
||||||
# Header: tier distribution summary
|
|
||||||
header = build_tier_summary(items, total_count)
|
header = build_tier_summary(items, total_count)
|
||||||
|
|
||||||
# Card entries
|
|
||||||
lines = [format_refractor_entry(state) for state in items]
|
lines = [format_refractor_entry(state) for state in items]
|
||||||
body = "\n\n".join(lines) if lines else "*No cards found.*"
|
body = "\n\n".join(lines) if lines else "*No cards found.*"
|
||||||
|
|
||||||
# Separator between header and cards
|
|
||||||
embed.description = f"```{header}```\n{body}"
|
embed.description = f"```{header}```\n{body}"
|
||||||
|
|
||||||
# Page indicator in footer (append to existing footer text)
|
|
||||||
existing_footer = embed.footer.text or ""
|
existing_footer = embed.footer.text or ""
|
||||||
page_text = f"Page {page}/{total_pages}"
|
page_text = f"Page {page}/{total_pages}"
|
||||||
embed.set_footer(
|
embed.set_footer(
|
||||||
|
|||||||
@ -153,37 +153,22 @@ class TestFormatRefractorEntry:
|
|||||||
result = format_refractor_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_percentage_in_output(self, batter_state):
|
||||||
"""Batter formula label PA+TB×2 appears in output."""
|
"""Percentage appears in parentheses in output."""
|
||||||
result = format_refractor_entry(batter_state)
|
result = format_refractor_entry(batter_state)
|
||||||
assert "PA+TB×2" in result
|
assert "(80%)" in result or "(81%)" in result
|
||||||
|
|
||||||
def test_tier_progression_arrow(self, batter_state):
|
|
||||||
"""T1 → T2 arrow progression appears for non-evolved cards."""
|
|
||||||
result = format_refractor_entry(batter_state)
|
|
||||||
assert "T1 → T2" in result
|
|
||||||
|
|
||||||
def test_sp_formula_label(self, sp_state):
|
|
||||||
"""SP formula label IP+K appears for starting pitchers."""
|
|
||||||
result = format_refractor_entry(sp_state)
|
|
||||||
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 MAX."""
|
||||||
result = format_refractor_entry(evolved_state)
|
result = format_refractor_entry(evolved_state)
|
||||||
assert "FULLY EVOLVED" in result
|
assert "`MAX`" 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_refractor_entry(batter_state)
|
result = format_refractor_entry(batter_state)
|
||||||
assert "FULLY EVOLVED" in result
|
assert "`MAX`" in result
|
||||||
|
|
||||||
def test_fully_evolved_no_arrow(self, evolved_state):
|
|
||||||
"""Fully evolved cards don't show a tier arrow."""
|
|
||||||
result = format_refractor_entry(evolved_state)
|
|
||||||
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)."""
|
||||||
@ -205,7 +190,7 @@ class TestTierSymbols:
|
|||||||
|
|
||||||
def test_t0_symbol(self):
|
def test_t0_symbol(self):
|
||||||
"""T0 label is empty (base cards get no prefix)."""
|
"""T0 label is empty (base cards get no prefix)."""
|
||||||
assert TIER_SYMBOLS[0] == ""
|
assert TIER_SYMBOLS[0] == "Base"
|
||||||
|
|
||||||
def test_t1_symbol(self):
|
def test_t1_symbol(self):
|
||||||
"""T1 label is 'T1'."""
|
"""T1 label is 'T1'."""
|
||||||
@ -223,41 +208,40 @@ class TestTierSymbols:
|
|||||||
"""T4 label is 'T4★'."""
|
"""T4 label is 'T4★'."""
|
||||||
assert TIER_SYMBOLS[4] == "T4★"
|
assert TIER_SYMBOLS[4] == "T4★"
|
||||||
|
|
||||||
def test_format_entry_t1_label_present(self, batter_state):
|
def test_format_entry_t1_suffix_tag(self, batter_state):
|
||||||
"""format_refractor_entry prepends T1 label for T1 cards."""
|
"""T1 cards show [T1] suffix tag after the tier name."""
|
||||||
result = format_refractor_entry(batter_state)
|
result = format_refractor_entry(batter_state)
|
||||||
assert "T1 " in result
|
assert "[T1]" in result
|
||||||
|
|
||||||
def test_format_entry_t2_label_present(self, sp_state):
|
def test_format_entry_t2_suffix_tag(self, sp_state):
|
||||||
"""format_refractor_entry prepends T2 label for T2 cards."""
|
"""T2 cards show [T2] suffix tag."""
|
||||||
result = format_refractor_entry(sp_state)
|
result = format_refractor_entry(sp_state)
|
||||||
assert "T2 " in result
|
assert "[T2]" in result
|
||||||
|
|
||||||
def test_format_entry_t4_label_present(self, evolved_state):
|
def test_format_entry_t4_suffix_tag(self, evolved_state):
|
||||||
"""format_refractor_entry prepends T4★ label for T4 cards."""
|
"""T4 cards show [T4★] suffix tag."""
|
||||||
result = format_refractor_entry(evolved_state)
|
result = format_refractor_entry(evolved_state)
|
||||||
assert "T4★" in result
|
assert "[T4★]" in result
|
||||||
|
|
||||||
def test_format_entry_t0_no_prefix(self):
|
def test_format_entry_t0_name_only(self):
|
||||||
"""T0 cards have no tier prefix — name starts the line."""
|
"""T0 cards show just the bold name, no tier suffix."""
|
||||||
state = {
|
state = {
|
||||||
"player_name": "Rookie Player",
|
"player_name": "Rookie Player",
|
||||||
"track": {"card_type": "batter"},
|
|
||||||
"current_tier": 0,
|
"current_tier": 0,
|
||||||
"current_value": 10,
|
"current_value": 10,
|
||||||
"next_threshold": 50,
|
"next_threshold": 50,
|
||||||
}
|
}
|
||||||
result = format_refractor_entry(state)
|
result = format_refractor_entry(state)
|
||||||
first_line = result.split("\n")[0]
|
first_line = result.split("\n")[0]
|
||||||
assert first_line.startswith("**Rookie Player**")
|
assert first_line == "**Rookie Player**"
|
||||||
|
|
||||||
def test_format_entry_label_before_name(self, batter_state):
|
def test_format_entry_tag_after_name(self, batter_state):
|
||||||
"""Label appears before the player name in the first line."""
|
"""Tag appears after the player name in the first line."""
|
||||||
result = format_refractor_entry(batter_state)
|
result = format_refractor_entry(batter_state)
|
||||||
first_line = result.split("\n")[0]
|
first_line = result.split("\n")[0]
|
||||||
label_pos = first_line.find("T1")
|
|
||||||
name_pos = first_line.find("Mike Trout")
|
name_pos = first_line.find("Mike Trout")
|
||||||
assert label_pos < name_pos
|
tag_pos = first_line.find("[T1]")
|
||||||
|
assert name_pos < tag_pos
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -589,11 +573,10 @@ class TestFormatRefractorEntryMalformedInput:
|
|||||||
lines = result.split("\n")
|
lines = result.split("\n")
|
||||||
assert len(lines) == 2
|
assert len(lines) == 2
|
||||||
|
|
||||||
def test_missing_card_type_uses_raw_fallback(self):
|
def test_missing_card_type_does_not_crash(self):
|
||||||
"""
|
"""
|
||||||
When card_type is absent from the track, the code defaults to 'batter'
|
When card_type is absent from the track, the code should still
|
||||||
internally (via .get("card_type", "batter")), so "PA+TB×2" should
|
produce a valid two-line output without crashing.
|
||||||
appear as the formula label.
|
|
||||||
"""
|
"""
|
||||||
state = {
|
state = {
|
||||||
"player_name": "Test Player",
|
"player_name": "Test Player",
|
||||||
@ -602,7 +585,7 @@ class TestFormatRefractorEntryMalformedInput:
|
|||||||
"next_threshold": 100,
|
"next_threshold": 100,
|
||||||
}
|
}
|
||||||
result = format_refractor_entry(state)
|
result = format_refractor_entry(state)
|
||||||
assert "PA+TB×2" in result
|
assert "50/100" in result
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -670,22 +653,14 @@ class TestRenderProgressBarBoundaryPrecision:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestRPFormulaLabel:
|
class TestCardTypeVariants:
|
||||||
"""
|
"""
|
||||||
T3-4: Verify that relief pitchers (card_type="rp") show the "IP+K" formula
|
T3-4/T3-5: Verify that format_refractor_entry produces valid output for
|
||||||
label in format_refractor_entry output.
|
all card types including unknown ones, without crashing.
|
||||||
|
|
||||||
Why: FORMULA_LABELS maps both "sp" and "rp" to "IP+K". The existing test
|
|
||||||
suite only verifies "sp" (via the sp_state fixture). Adding "rp" explicitly
|
|
||||||
prevents a future refactor from accidentally giving RPs a different label
|
|
||||||
or falling through to the raw card_type fallback.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_rp_formula_label_is_ip_plus_k(self):
|
def test_rp_card_produces_valid_output(self):
|
||||||
"""
|
"""Relief pitcher card produces a valid two-line string."""
|
||||||
A card with card_type="rp" must show "IP+K" as the formula label
|
|
||||||
in its progress line.
|
|
||||||
"""
|
|
||||||
rp_state = {
|
rp_state = {
|
||||||
"player_name": "Edwin Diaz",
|
"player_name": "Edwin Diaz",
|
||||||
"track": {"card_type": "rp"},
|
"track": {"card_type": "rp"},
|
||||||
@ -694,50 +669,11 @@ class TestRPFormulaLabel:
|
|||||||
"next_threshold": 60,
|
"next_threshold": 60,
|
||||||
}
|
}
|
||||||
result = format_refractor_entry(rp_state)
|
result = format_refractor_entry(rp_state)
|
||||||
assert "IP+K" in result, (
|
assert "Edwin Diaz" in result
|
||||||
f"Relief pitcher card should show 'IP+K' formula label, got: {result!r}"
|
assert "45/60" in result
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# T3-5: Unknown card_type fallback
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestUnknownCardTypeFallback:
|
|
||||||
"""
|
|
||||||
T3-5: format_refractor_entry should use the raw card_type string as the
|
|
||||||
formula label when the type is not in FORMULA_LABELS, rather than crashing.
|
|
||||||
|
|
||||||
Why: FORMULA_LABELS only covers "batter", "sp", and "rp". If the API
|
|
||||||
introduces a new card type (e.g. "util" for utility players) before the
|
|
||||||
bot is updated, FORMULA_LABELS.get(card_type, card_type) will fall back to
|
|
||||||
the raw string. This test ensures that fallback path produces readable
|
|
||||||
output rather than an error, and explicitly documents what to expect.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_unknown_card_type_uses_raw_string_as_label(self):
|
|
||||||
"""
|
|
||||||
card_type="util" is not in FORMULA_LABELS. The output should include
|
|
||||||
"util" as the formula label (the raw fallback) and must not raise.
|
|
||||||
"""
|
|
||||||
util_state = {
|
|
||||||
"player_name": "Ben Zobrist",
|
|
||||||
"track": {"card_type": "util"},
|
|
||||||
"current_tier": 2,
|
|
||||||
"current_value": 80,
|
|
||||||
"next_threshold": 120,
|
|
||||||
}
|
|
||||||
result = format_refractor_entry(util_state)
|
|
||||||
assert "util" in result, (
|
|
||||||
f"Unknown card_type should appear verbatim as the formula label, got: {result!r}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_unknown_card_type_does_not_crash(self):
|
def test_unknown_card_type_does_not_crash(self):
|
||||||
"""
|
"""Unknown card_type produces a valid two-line string."""
|
||||||
Any unknown card_type must produce a valid two-line string without
|
|
||||||
raising an exception.
|
|
||||||
"""
|
|
||||||
state = {
|
state = {
|
||||||
"player_name": "Test Player",
|
"player_name": "Test Player",
|
||||||
"track": {"card_type": "dh"},
|
"track": {"card_type": "dh"},
|
||||||
@ -793,7 +729,10 @@ async def test_refractor_status_empty_roster(mock_bot, mock_interaction):
|
|||||||
team = {"id": 1, "sname": "Test"}
|
team = {"id": 1, "sname": "Test"}
|
||||||
|
|
||||||
with patch("cogs.refractor.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.refractor.db_get", new=AsyncMock(return_value={"cards": []})):
|
with patch(
|
||||||
|
"cogs.refractor.db_get",
|
||||||
|
new=AsyncMock(return_value={"items": [], "count": 0}),
|
||||||
|
):
|
||||||
await cog.refractor_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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user