Merge pull request 'fix: clean up refractor status — suffix tags, compact layout' (#133) from fix/refractor-status-cleanup into main
All checks were successful
Build Docker Image / build (push) Successful in 2m47s
All checks were successful
Build Docker Image / build (push) Successful in 2m47s
This commit is contained in:
commit
f843b45099
@ -34,21 +34,17 @@ TIER_NAMES = {
|
||||
4: "Superfractor",
|
||||
}
|
||||
|
||||
FORMULA_LABELS = {
|
||||
"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-specific labels for the status display.
|
||||
TIER_SYMBOLS = {
|
||||
0: "", # Base Card — no prefix
|
||||
0: "Base", # Base Card — used in summary only, not in per-card display
|
||||
1: "T1", # Base Chrome
|
||||
2: "T2", # 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).
|
||||
TIER_COLORS = {
|
||||
0: 0x95A5A6, # slate grey
|
||||
@ -86,40 +82,38 @@ def _pct_label(current: int, threshold: int) -> 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):
|
||||
◈ **Mike Trout** — Base Chrome
|
||||
▰▰▰▰▰▰▰▰▰▰▱▱ `120/149` 80% · PA+TB×2 · T1 → T2
|
||||
Output example (base card — no suffix):
|
||||
**Mike Trout**
|
||||
▰▰▰▰▰▰▰▰▰▰▱▱ 120/149 (80%)
|
||||
|
||||
Output example (evolved — suffix tag):
|
||||
**Mike Trout** — Base Chrome [T1]
|
||||
▰▰▰▰▰▰▰▰▰▰▱▱ 120/149 (80%)
|
||||
|
||||
Output example (fully evolved):
|
||||
★ **Barry Bonds** — Superfractor
|
||||
▰▰▰▰▰▰▰▰▰▰▰▰ ✧ FULLY EVOLVED ✧
|
||||
**Barry Bonds** — Superfractor [T4★]
|
||||
▰▰▰▰▰▰▰▰▰▰▰▰ `MAX`
|
||||
"""
|
||||
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)
|
||||
formula_value = int(card_state.get("current_value", 0))
|
||||
next_threshold = int(card_state.get("next_threshold") or 0) or None
|
||||
|
||||
tier_label = TIER_NAMES.get(current_tier, f"T{current_tier}")
|
||||
formula_label = FORMULA_LABELS.get(card_type, card_type)
|
||||
symbol = TIER_SYMBOLS.get(current_tier, "")
|
||||
prefix = f"{symbol} " if symbol else ""
|
||||
|
||||
first_line = f"{prefix}**{player_name}** — {tier_label}"
|
||||
if current_tier == 0:
|
||||
first_line = f"**{player_name}**"
|
||||
else:
|
||||
tier_label = TIER_NAMES.get(current_tier, f"T{current_tier}")
|
||||
symbol = TIER_SYMBOLS.get(current_tier, "")
|
||||
first_line = f"**{player_name}** — {tier_label} [{symbol}]"
|
||||
|
||||
if current_tier >= 4 or next_threshold is None:
|
||||
bar = render_progress_bar(1, 1)
|
||||
second_line = f"{bar} ✧ FULLY EVOLVED ✧"
|
||||
second_line = f"{_FULL_BAR} `MAX`"
|
||||
else:
|
||||
bar = render_progress_bar(formula_value, next_threshold)
|
||||
pct = _pct_label(formula_value, next_threshold)
|
||||
second_line = (
|
||||
f"{bar} `{formula_value}/{next_threshold}` {pct}"
|
||||
f" · {formula_label} · T{current_tier} → T{current_tier + 1}"
|
||||
)
|
||||
second_line = f"{bar} {formula_value}/{next_threshold} ({pct})"
|
||||
|
||||
return f"{first_line}\n{second_line}"
|
||||
|
||||
@ -139,8 +133,7 @@ def build_tier_summary(items: list, total_count: int) -> str:
|
||||
parts = []
|
||||
for t in range(5):
|
||||
if counts[t] > 0:
|
||||
label = TIER_SYMBOLS[t] or "Base"
|
||||
parts.append(f"{label}: {counts[t]}")
|
||||
parts.append(f"{TIER_SYMBOLS[t]}: {counts[t]}")
|
||||
summary = " ".join(parts) if parts else "No cards"
|
||||
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:
|
||||
embed.color = TIER_COLORS[tier_filter]
|
||||
|
||||
# Header: tier distribution summary
|
||||
header = build_tier_summary(items, total_count)
|
||||
|
||||
# Card entries
|
||||
lines = [format_refractor_entry(state) for state in items]
|
||||
body = "\n\n".join(lines) if lines else "*No cards found.*"
|
||||
|
||||
# Separator between header and cards
|
||||
embed.description = f"```{header}```\n{body}"
|
||||
|
||||
# Page indicator in footer (append to existing footer text)
|
||||
existing_footer = embed.footer.text or ""
|
||||
page_text = f"Page {page}/{total_pages}"
|
||||
embed.set_footer(
|
||||
|
||||
@ -153,37 +153,22 @@ class TestFormatRefractorEntry:
|
||||
result = format_refractor_entry(batter_state)
|
||||
assert "120/149" in result
|
||||
|
||||
def test_formula_label_batter(self, batter_state):
|
||||
"""Batter formula label PA+TB×2 appears in output."""
|
||||
def test_percentage_in_output(self, batter_state):
|
||||
"""Percentage appears in parentheses in output."""
|
||||
result = format_refractor_entry(batter_state)
|
||||
assert "PA+TB×2" 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
|
||||
assert "(80%)" in result or "(81%)" in result
|
||||
|
||||
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)
|
||||
assert "FULLY EVOLVED" in result
|
||||
assert "`MAX`" in result
|
||||
|
||||
def test_fully_evolved_by_tier(self, batter_state):
|
||||
"""current_tier=4 triggers fully evolved display even with a threshold."""
|
||||
batter_state["current_tier"] = 4
|
||||
batter_state["next_threshold"] = 200
|
||||
result = format_refractor_entry(batter_state)
|
||||
assert "FULLY EVOLVED" 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
|
||||
assert "`MAX`" in result
|
||||
|
||||
def test_two_line_output(self, batter_state):
|
||||
"""Output always has exactly two lines (name line + bar line)."""
|
||||
@ -205,7 +190,7 @@ class TestTierSymbols:
|
||||
|
||||
def test_t0_symbol(self):
|
||||
"""T0 label is empty (base cards get no prefix)."""
|
||||
assert TIER_SYMBOLS[0] == ""
|
||||
assert TIER_SYMBOLS[0] == "Base"
|
||||
|
||||
def test_t1_symbol(self):
|
||||
"""T1 label is 'T1'."""
|
||||
@ -223,41 +208,40 @@ class TestTierSymbols:
|
||||
"""T4 label is 'T4★'."""
|
||||
assert TIER_SYMBOLS[4] == "T4★"
|
||||
|
||||
def test_format_entry_t1_label_present(self, batter_state):
|
||||
"""format_refractor_entry prepends T1 label for T1 cards."""
|
||||
def test_format_entry_t1_suffix_tag(self, batter_state):
|
||||
"""T1 cards show [T1] suffix tag after the tier name."""
|
||||
result = format_refractor_entry(batter_state)
|
||||
assert "T1 " in result
|
||||
assert "[T1]" in result
|
||||
|
||||
def test_format_entry_t2_label_present(self, sp_state):
|
||||
"""format_refractor_entry prepends T2 label for T2 cards."""
|
||||
def test_format_entry_t2_suffix_tag(self, sp_state):
|
||||
"""T2 cards show [T2] suffix tag."""
|
||||
result = format_refractor_entry(sp_state)
|
||||
assert "T2 " in result
|
||||
assert "[T2]" in result
|
||||
|
||||
def test_format_entry_t4_label_present(self, evolved_state):
|
||||
"""format_refractor_entry prepends T4★ label for T4 cards."""
|
||||
def test_format_entry_t4_suffix_tag(self, evolved_state):
|
||||
"""T4 cards show [T4★] suffix tag."""
|
||||
result = format_refractor_entry(evolved_state)
|
||||
assert "T4★" in result
|
||||
assert "[T4★]" in result
|
||||
|
||||
def test_format_entry_t0_no_prefix(self):
|
||||
"""T0 cards have no tier prefix — name starts the line."""
|
||||
def test_format_entry_t0_name_only(self):
|
||||
"""T0 cards show just the bold name, no tier suffix."""
|
||||
state = {
|
||||
"player_name": "Rookie Player",
|
||||
"track": {"card_type": "batter"},
|
||||
"current_tier": 0,
|
||||
"current_value": 10,
|
||||
"next_threshold": 50,
|
||||
}
|
||||
result = format_refractor_entry(state)
|
||||
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):
|
||||
"""Label appears before the player name in the first line."""
|
||||
def test_format_entry_tag_after_name(self, batter_state):
|
||||
"""Tag appears after the player name in the first line."""
|
||||
result = format_refractor_entry(batter_state)
|
||||
first_line = result.split("\n")[0]
|
||||
label_pos = first_line.find("T1")
|
||||
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")
|
||||
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'
|
||||
internally (via .get("card_type", "batter")), so "PA+TB×2" should
|
||||
appear as the formula label.
|
||||
When card_type is absent from the track, the code should still
|
||||
produce a valid two-line output without crashing.
|
||||
"""
|
||||
state = {
|
||||
"player_name": "Test Player",
|
||||
@ -602,7 +585,7 @@ class TestFormatRefractorEntryMalformedInput:
|
||||
"next_threshold": 100,
|
||||
}
|
||||
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
|
||||
label in format_refractor_entry output.
|
||||
|
||||
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.
|
||||
T3-4/T3-5: Verify that format_refractor_entry produces valid output for
|
||||
all card types including unknown ones, without crashing.
|
||||
"""
|
||||
|
||||
def test_rp_formula_label_is_ip_plus_k(self):
|
||||
"""
|
||||
A card with card_type="rp" must show "IP+K" as the formula label
|
||||
in its progress line.
|
||||
"""
|
||||
def test_rp_card_produces_valid_output(self):
|
||||
"""Relief pitcher card produces a valid two-line string."""
|
||||
rp_state = {
|
||||
"player_name": "Edwin Diaz",
|
||||
"track": {"card_type": "rp"},
|
||||
@ -694,50 +669,11 @@ class TestRPFormulaLabel:
|
||||
"next_threshold": 60,
|
||||
}
|
||||
result = format_refractor_entry(rp_state)
|
||||
assert "IP+K" in result, (
|
||||
f"Relief pitcher card should show 'IP+K' formula label, got: {result!r}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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}"
|
||||
)
|
||||
assert "Edwin Diaz" in result
|
||||
assert "45/60" in result
|
||||
|
||||
def test_unknown_card_type_does_not_crash(self):
|
||||
"""
|
||||
Any unknown card_type must produce a valid two-line string without
|
||||
raising an exception.
|
||||
"""
|
||||
"""Unknown card_type produces a valid two-line string."""
|
||||
state = {
|
||||
"player_name": "Test Player",
|
||||
"track": {"card_type": "dh"},
|
||||
@ -793,7 +729,10 @@ async def test_refractor_status_empty_roster(mock_bot, mock_interaction):
|
||||
team = {"id": 1, "sname": "Test"}
|
||||
|
||||
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)
|
||||
|
||||
call_kwargs = mock_interaction.edit_original_response.call_args
|
||||
|
||||
Loading…
Reference in New Issue
Block a user