diff --git a/cogs/refractor.py b/cogs/refractor.py index 18cfbaa..b0593a7 100644 --- a/cogs/refractor.py +++ b/cogs/refractor.py @@ -19,6 +19,7 @@ from discord.app_commands import Choice from discord.ext import commands from api_calls import db_get +from helpers.discord_utils import get_team_embed from helpers.main import get_team_by_owner logger = logging.getLogger("discord_app") @@ -39,40 +40,61 @@ FORMULA_LABELS = { "rp": "IP+K", } -TIER_BADGES = {1: "[BC]", 2: "[R]", 3: "[GR]", 4: "[SF]"} +# Tier-specific symbols for visual hierarchy in the status display. +TIER_SYMBOLS = { + 0: "○", # Base Card — hollow circle + 1: "◈", # Base Chrome — diamond with dot + 2: "◆", # Refractor — filled diamond + 3: "✦", # Gold Refractor — four-pointed star + 4: "★", # Superfractor — filled star +} + +# Embed accent colors per tier (used for single-tier filtered views). +TIER_COLORS = { + 0: 0x95A5A6, # slate grey + 1: 0xBDC3C7, # silver/chrome + 2: 0x3498DB, # refractor blue + 3: 0xF1C40F, # gold + 4: 0x1ABC9C, # teal superfractor +} -def render_progress_bar(current: int, threshold: int, width: int = 10) -> str: +def render_progress_bar(current: int, threshold: int, width: int = 12) -> str: """ - Render a fixed-width ASCII progress bar. + Render a Unicode block progress bar. Examples: - render_progress_bar(120, 149) -> '[========--]' - render_progress_bar(0, 100) -> '[----------]' - render_progress_bar(100, 100) -> '[==========]' + 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) + ratio = max(0.0, min(current / threshold, 1.0)) filled = round(ratio * width) empty = width - filled - return f"[{'=' * filled}{'-' * empty}]" + return f"{'▰' * filled}{'▱' * empty}" + + +def _pct_label(current: int, threshold: int) -> str: + """Return a percentage string like '80%'.""" + if threshold <= 0: + return "100%" + return f"{min(current / threshold, 1.0):.0%}" 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 rich display string. - Expected keys: player_name, card_type, current_tier, formula_value, - next_threshold (None if fully evolved). + Output example (in-progress): + ◈ **Mike Trout** — Base Chrome + ▰▰▰▰▰▰▰▰▰▰▱▱ `120/149` 80% · PA+TB×2 · T1 → T2 - A tier badge prefix (e.g. [BC], [R], [GR], [SF]) is prepended to the - player name for tiers 1-4. T0 cards have no badge. - - Output example: - **[BC] Mike Trout** (Base Chrome) - [========--] 120/149 (PA+TB×2) — T1 → T2 + Output example (fully evolved): + ★ **Barry Bonds** — Superfractor + ▰▰▰▰▰▰▰▰▰▰▰▰ ✧ FULLY EVOLVED ✧ """ player_name = card_state.get("player_name", "Unknown") track = card_state.get("track", {}) @@ -83,22 +105,85 @@ def format_refractor_entry(card_state: dict) -> str: 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, "·") - badge = TIER_BADGES.get(current_tier, "") - display_name = f"{badge} {player_name}" if badge else player_name + first_line = f"{symbol} **{player_name}** — {tier_label}" if current_tier >= 4 or next_threshold is None: - bar = "[==========]" - detail = "FULLY EVOLVED ★" + bar = render_progress_bar(1, 1) + second_line = f"{bar} ✧ 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}" + 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}" + ) - first_line = f"**{display_name}** ({tier_label})" - second_line = f"{bar} {detail}" return f"{first_line}\n{second_line}" +def build_tier_summary(items: list, total_count: int) -> str: + """ + Build a one-line summary of tier distribution from the current page items. + + Returns something like: '○ 3 ◈ 12 ◆ 8 ✦ 5 ★ 2 — 30 cards' + """ + counts = {t: 0 for t in range(5)} + for item in items: + t = item.get("current_tier", 0) + if t in counts: + counts[t] += 1 + + parts = [] + for t in range(5): + if counts[t] > 0: + parts.append(f"{TIER_SYMBOLS[t]} {counts[t]}") + summary = " ".join(parts) if parts else "No cards" + return f"{summary} — {total_count} total" + + +def build_status_embed( + team: dict, + items: list, + page: int, + total_pages: int, + total_count: int, + tier_filter: Optional[int] = None, +) -> discord.Embed: + """ + Build the refractor status embed with team branding. + + Uses get_team_embed for consistent team color/logo/footer, then layers + on the refractor-specific content. + """ + embed = get_team_embed(f"{team['sname']} — Refractor Status", team=team) + + # Override color for single-tier views to match the tier's identity. + 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( + text=f"{page_text} · {existing_footer}" if existing_footer else page_text, + icon_url=embed.footer.icon_url, + ) + + return embed + + def apply_close_filter(card_states: list) -> list: """ Return only cards within 80% of their next tier threshold. @@ -140,6 +225,7 @@ class RefractorPaginationView(discord.ui.View): total_count: int, params: list, owner_id: int, + tier_filter: Optional[int] = None, timeout: float = 120.0, ): super().__init__(timeout=timeout) @@ -149,6 +235,7 @@ class RefractorPaginationView(discord.ui.View): self.total_count = total_count self.base_params = params self.owner_id = owner_id + self.tier_filter = tier_filter self._update_buttons() def _update_buttons(self): @@ -170,19 +257,18 @@ class RefractorPaginationView(discord.ui.View): self.total_pages = max(1, (self.total_count + PAGE_SIZE - 1) // PAGE_SIZE) self.page = min(self.page, self.total_pages) - lines = [format_refractor_entry(state) for state in items] - embed = discord.Embed( - title=f"{self.team['sname']} Refractor Status", - description="\n\n".join(lines) if lines else "No cards found.", - color=0x6F42C1, - ) - embed.set_footer( - text=f"Page {self.page}/{self.total_pages} · {self.total_count} card(s) total" + embed = build_status_embed( + self.team, + items, + self.page, + self.total_pages, + self.total_count, + tier_filter=self.tier_filter, ) self._update_buttons() await interaction.response.edit_message(embed=embed, view=self) - @discord.ui.button(label="◀ Prev", style=discord.ButtonStyle.blurple) + @discord.ui.button(label="◀ Prev", style=discord.ButtonStyle.grey) async def prev_btn( self, interaction: discord.Interaction, button: discord.ui.Button ): @@ -191,7 +277,7 @@ class RefractorPaginationView(discord.ui.View): self.page = max(1, self.page - 1) await self._fetch_and_update(interaction) - @discord.ui.button(label="Next ▶", style=discord.ButtonStyle.blurple) + @discord.ui.button(label="Next ▶", style=discord.ButtonStyle.grey) async def next_btn( self, interaction: discord.Interaction, button: discord.ui.Button ): @@ -269,6 +355,8 @@ class Refractor(commands.Cog): if progress: params.append(("progress", progress.value)) + tier_filter = int(tier.value) if tier is not None else None + data = await db_get("refractor/cards", params=params) if not data: logger.error( @@ -322,15 +410,9 @@ class Refractor(commands.Cog): total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE) page = min(page, total_pages) - lines = [format_refractor_entry(state) for state in 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} · {total_count} card(s) total" + embed = build_status_embed( + team, items, page, total_pages, total_count, tier_filter=tier_filter ) if total_pages > 1: @@ -341,6 +423,7 @@ class Refractor(commands.Cog): total_count=total_count, params=params, owner_id=interaction.user.id, + tier_filter=tier_filter, ) await interaction.edit_original_response(embed=embed, view=view) else: diff --git a/tests/refractor-integration-test-plan.md b/tests/refractor-integration-test-plan.md index 7cd9c48..d9bd5d4 100644 --- a/tests/refractor-integration-test-plan.md +++ b/tests/refractor-integration-test-plan.md @@ -665,16 +665,28 @@ design but means tier-up notifications are best-effort. Run order for Playwright automation: -1. [ ] Execute REF-API-01 through REF-API-10 (API health + list endpoint) -2. [ ] Execute REF-01 through REF-06 (basic /refractor status) -3. [ ] Execute REF-10 through REF-19 (filters) -4. [ ] Execute REF-20 through REF-23 (pagination) +1. [~] Execute REF-API-01 through REF-API-10 (API health + list endpoint) + - Tested 2026-03-25: REF-API-03 (single card ✓), REF-API-06 (list ✓), REF-API-07 (card_type filter ✓), REF-API-10 (pagination ✓) + - Not yet tested: REF-API-01, REF-API-02, REF-API-04, REF-API-05, REF-API-08, REF-API-09 +2. [~] Execute REF-01 through REF-06 (basic /refractor status) + - Tested 2026-03-25: REF-01 (embed appears ✓), REF-02 (batter entry format ✓), REF-05 (tier badges [BC] ✓) + - Bugs found and fixed: wrong response key ("cards" vs "items"), wrong field names (formula_value vs current_value, card_type nesting), limit=500 exceeding API max, floating point display + - Not yet tested: REF-03 (SP format), REF-04 (RP format), REF-06 (fully evolved) +3. [~] Execute REF-10 through REF-19 (filters) + - Tested 2026-03-25: REF-10 (card_type=batter ✓ after fix) + - Choice dropdown menus added for all filter params (PR #126) + - Not yet tested: REF-11 through REF-19 +4. [~] Execute REF-20 through REF-23 (pagination) + - Tested 2026-03-25: REF-20 (page 1 footer ✓), pagination buttons added (PR #127) + - Not yet tested: REF-21 (page 2), REF-22 (beyond total), REF-23 (page 0) 5. [ ] Execute REF-30 through REF-34 (edge cases) 6. [ ] Execute REF-40 through REF-45 (tier badges on card embeds) 7. [ ] Execute REF-50 through REF-55 (post-game hook -- requires live game) 8. [ ] Execute REF-60 through REF-64 (tier-up notifications -- requires threshold crossing) 9. [ ] Execute REF-70 through REF-72 (cross-command badge propagation) -10. [ ] Execute REF-80 through REF-82 (force-evaluate API) +10. [~] Execute REF-80 through REF-82 (force-evaluate API) + - Tested 2026-03-25: REF-80 (force evaluate ✓ — used to seed 100 cards for team 31) + - Not yet tested: REF-81, REF-82 ### Approximate Time Estimates - API health checks + list endpoint (REF-API-01 through REF-API-10): 2-3 minutes diff --git a/tests/test_card_embed_refractor.py b/tests/test_card_embed_refractor.py index f4bfc06..d04f445 100644 --- a/tests/test_card_embed_refractor.py +++ b/tests/test_card_embed_refractor.py @@ -251,78 +251,36 @@ class TestEmbedColorUnchanged: # --------------------------------------------------------------------------- -class TestTierBadgesFormatConsistency: +class TestTierSymbolsCompleteness: """ - T1-7: Assert that TIER_BADGES in cogs.refractor (format: "[BC]") and - helpers.main (format: "BC") are consistent — wrapping the helpers.main - value in brackets must produce the cogs.refractor value. + T1-7: Assert that TIER_SYMBOLS in cogs.refractor covers all tiers 0-4 + and that helpers.main TIER_BADGES still covers tiers 1-4 for card embeds. - Why: The two modules intentionally use different formats for different - rendering contexts: - - helpers.main uses bare strings ("BC") because get_card_embeds - wraps them in brackets when building the embed title. - - cogs.refractor uses bracket strings ("[BC]") because - format_refractor_entry inlines them directly into the display string. - - If either definition is updated without updating the other, embed titles - and /refractor status output will display inconsistent badges. This test - acts as an explicit contract check so any future change to either dict - is immediately surfaced here. + Why: The refractor status command uses Unicode TIER_SYMBOLS for display, + while card embed titles use helpers.main TIER_BADGES in bracket format. + Both must cover the full tier range for their respective contexts. """ - def test_cogs_badge_equals_bracketed_helpers_badge_for_all_tiers(self): - """ - For every tier in cogs.refractor TIER_BADGES, wrapping the - helpers.main TIER_BADGES value in square brackets must produce - the cogs.refractor value. + def test_tier_symbols_covers_all_tiers(self): + """TIER_SYMBOLS must have entries for T0 through T4.""" + from cogs.refractor import TIER_SYMBOLS - i.e., f"[{helpers_badge}]" == cog_badge for all tiers. - """ - from cogs.refractor import TIER_BADGES as cog_badges - from helpers.main import TIER_BADGES as helpers_badges + for tier in range(5): + assert tier in TIER_SYMBOLS, f"TIER_SYMBOLS missing tier {tier}" - assert set(cog_badges.keys()) == set(helpers_badges.keys()), ( - "TIER_BADGES key sets differ between cogs.refractor and helpers.main. " - f"cogs keys: {set(cog_badges.keys())}, helpers keys: {set(helpers_badges.keys())}" - ) + def test_tier_badges_covers_evolved_tiers(self): + """helpers.main TIER_BADGES must have entries for T1 through T4.""" + from helpers.main import TIER_BADGES - for tier, cog_badge in cog_badges.items(): - helpers_badge = helpers_badges[tier] - expected = f"[{helpers_badge}]" - assert cog_badge == expected, ( - f"Tier {tier} badge mismatch: " - f"cogs.refractor={cog_badge!r}, " - f"helpers.main={helpers_badge!r} " - f"(expected cog badge to equal '[{helpers_badge}]')" - ) + for tier in range(1, 5): + assert tier in TIER_BADGES, f"TIER_BADGES missing tier {tier}" - def test_t1_badge_relationship(self): - """T1: helpers.main 'BC' wrapped in brackets equals cogs.refractor '[BC]'.""" - from cogs.refractor import TIER_BADGES as cog_badges - from helpers.main import TIER_BADGES as helpers_badges + def test_tier_symbols_are_unique(self): + """Each tier must have a distinct symbol.""" + from cogs.refractor import TIER_SYMBOLS - assert f"[{helpers_badges[1]}]" == cog_badges[1] - - def test_t2_badge_relationship(self): - """T2: helpers.main 'R' wrapped in brackets equals cogs.refractor '[R]'.""" - from cogs.refractor import TIER_BADGES as cog_badges - from helpers.main import TIER_BADGES as helpers_badges - - assert f"[{helpers_badges[2]}]" == cog_badges[2] - - def test_t3_badge_relationship(self): - """T3: helpers.main 'GR' wrapped in brackets equals cogs.refractor '[GR]'.""" - from cogs.refractor import TIER_BADGES as cog_badges - from helpers.main import TIER_BADGES as helpers_badges - - assert f"[{helpers_badges[3]}]" == cog_badges[3] - - def test_t4_badge_relationship(self): - """T4: helpers.main 'SF' wrapped in brackets equals cogs.refractor '[SF]'.""" - from cogs.refractor import TIER_BADGES as cog_badges - from helpers.main import TIER_BADGES as helpers_badges - - assert f"[{helpers_badges[4]}]" == cog_badges[4] + values = list(TIER_SYMBOLS.values()) + assert len(values) == len(set(values)), f"Duplicate symbols found: {values}" # --------------------------------------------------------------------------- diff --git a/tests/test_refractor_commands.py b/tests/test_refractor_commands.py index 87a9fcf..b2e8cf6 100644 --- a/tests/test_refractor_commands.py +++ b/tests/test_refractor_commands.py @@ -29,7 +29,7 @@ from cogs.refractor import ( apply_close_filter, paginate, TIER_NAMES, - TIER_BADGES, + TIER_SYMBOLS, PAGE_SIZE, ) @@ -40,12 +40,12 @@ from cogs.refractor import ( @pytest.fixture def batter_state(): - """A mid-progress batter card state.""" + """A mid-progress batter card state (API response shape).""" return { "player_name": "Mike Trout", - "card_type": "batter", + "track": {"card_type": "batter", "formula": "pa + tb * 2"}, "current_tier": 1, - "formula_value": 120, + "current_value": 120, "next_threshold": 149, } @@ -55,9 +55,9 @@ def evolved_state(): """A fully evolved card state (T4).""" return { "player_name": "Shohei Ohtani", - "card_type": "batter", + "track": {"card_type": "batter", "formula": "pa + tb * 2"}, "current_tier": 4, - "formula_value": 300, + "current_value": 300, "next_threshold": None, } @@ -67,9 +67,9 @@ def sp_state(): """A starting pitcher card state at T2.""" return { "player_name": "Sandy Alcantara", - "card_type": "sp", + "track": {"card_type": "sp", "formula": "ip + k"}, "current_tier": 2, - "formula_value": 95, + "current_value": 95, "next_threshold": 120, } @@ -84,38 +84,44 @@ class TestRenderProgressBar: Tests for render_progress_bar(). Verifies width, fill character, empty character, boundary conditions, - and clamping when current exceeds threshold. + and clamping when current exceeds threshold. Default width is 12. + Uses Unicode block chars: ▰ (filled) and ▱ (empty). """ def test_empty_bar(self): - """current=0 → all dashes.""" - assert render_progress_bar(0, 100) == "[----------]" + """current=0 → all empty blocks.""" + assert render_progress_bar(0, 100) == "▱" * 12 def test_full_bar(self): - """current == threshold → all equals.""" - assert render_progress_bar(100, 100) == "[==========]" + """current == threshold → all filled blocks.""" + assert render_progress_bar(100, 100) == "▰" * 12 def test_partial_fill(self): - """120/149 ≈ 80.5% → 8 filled of 10.""" + """120/149 ≈ 80.5% → ~10 filled of 12.""" bar = render_progress_bar(120, 149) - assert bar == "[========--]" + filled = bar.count("▰") + empty = bar.count("▱") + assert filled + empty == 12 + assert filled == 10 # round(0.805 * 12) = 10 def test_half_fill(self): - """50/100 = 50% → 5 filled.""" - assert render_progress_bar(50, 100) == "[=====-----]" + """50/100 = 50% → 6 filled.""" + bar = render_progress_bar(50, 100) + assert bar.count("▰") == 6 + assert bar.count("▱") == 6 def test_over_threshold_clamps_to_full(self): """current > threshold should not overflow the bar.""" - assert render_progress_bar(200, 100) == "[==========]" + assert render_progress_bar(200, 100) == "▰" * 12 def test_zero_threshold_returns_full_bar(self): """threshold=0 avoids division by zero and returns full bar.""" - assert render_progress_bar(0, 0) == "[==========]" + assert render_progress_bar(0, 0) == "▰" * 12 def test_custom_width(self): """Width parameter controls bar length.""" bar = render_progress_bar(5, 10, width=4) - assert bar == "[==--]" + assert bar == "▰▰▱▱" # --------------------------------------------------------------------------- @@ -140,7 +146,7 @@ class TestFormatRefractorEntry: def test_tier_label_in_output(self, batter_state): """Current tier name (Base Chrome for T1) appears in output.""" result = format_refractor_entry(batter_state) - assert "(Base Chrome)" in result + assert "Base Chrome" in result def test_progress_values_in_output(self, batter_state): """current/threshold values appear in output.""" @@ -191,69 +197,66 @@ class TestFormatRefractorEntry: # --------------------------------------------------------------------------- -class TestTierBadges: +class TestTierSymbols: """ - Verify TIER_BADGES values and that format_refractor_entry prepends badges - correctly for T1-T4. T0 cards should have no badge prefix. + Verify TIER_SYMBOLS values and that format_refractor_entry prepends + the correct symbol for each tier. Each tier has a unique Unicode symbol. """ - def test_t1_badge_value(self): - """T1 badge is [BC] (Base Chrome).""" - assert TIER_BADGES[1] == "[BC]" + def test_t0_symbol(self): + """T0 symbol is ○ (hollow circle).""" + assert TIER_SYMBOLS[0] == "○" - def test_t2_badge_value(self): - """T2 badge is [R] (Refractor).""" - assert TIER_BADGES[2] == "[R]" + def test_t1_symbol(self): + """T1 symbol is ◈ (diamond with dot).""" + assert TIER_SYMBOLS[1] == "◈" - def test_t3_badge_value(self): - """T3 badge is [GR] (Gold Refractor).""" - assert TIER_BADGES[3] == "[GR]" + def test_t2_symbol(self): + """T2 symbol is ◆ (filled diamond).""" + assert TIER_SYMBOLS[2] == "◆" - def test_t4_badge_value(self): - """T4 badge is [SF] (Superfractor).""" - assert TIER_BADGES[4] == "[SF]" + def test_t3_symbol(self): + """T3 symbol is ✦ (four-pointed star).""" + assert TIER_SYMBOLS[3] == "✦" - def test_t0_no_badge(self): - """T0 has no badge entry in TIER_BADGES.""" - assert 0 not in TIER_BADGES + def test_t4_symbol(self): + """T4 symbol is ★ (filled star).""" + assert TIER_SYMBOLS[4] == "★" - def test_format_entry_t1_badge_present(self, batter_state): - """format_refractor_entry prepends [BC] badge for T1 cards.""" + def test_format_entry_t1_symbol_present(self, batter_state): + """format_refractor_entry prepends ◈ symbol for T1 cards.""" result = format_refractor_entry(batter_state) - assert "[BC]" in result + assert "◈" in result - def test_format_entry_t2_badge_present(self, sp_state): - """format_refractor_entry prepends [R] badge for T2 cards.""" + def test_format_entry_t2_symbol_present(self, sp_state): + """format_refractor_entry prepends ◆ symbol for T2 cards.""" result = format_refractor_entry(sp_state) - assert "[R]" in result + assert "◆" in result - def test_format_entry_t4_badge_present(self, evolved_state): - """format_refractor_entry prepends [SF] badge for T4 cards.""" + def test_format_entry_t4_symbol_present(self, evolved_state): + """format_refractor_entry prepends ★ symbol for T4 cards.""" result = format_refractor_entry(evolved_state) - assert "[SF]" in result + assert "★" in result - def test_format_entry_t0_no_badge(self): - """format_refractor_entry does not prepend any badge for T0 cards.""" + def test_format_entry_t0_uses_hollow_circle(self): + """T0 cards use the ○ symbol.""" state = { "player_name": "Rookie Player", - "card_type": "batter", + "track": {"card_type": "batter"}, "current_tier": 0, - "formula_value": 10, + "current_value": 10, "next_threshold": 50, } result = format_refractor_entry(state) - assert "[BC]" not in result - assert "[R]" not in result - assert "[GR]" not in result - assert "[SF]" not in result + assert "○" in result - def test_format_entry_badge_before_name(self, batter_state): - """Badge appears before the player name in the bold section.""" + def test_format_entry_symbol_before_name(self, batter_state): + """Symbol appears before the player name in the first line.""" result = format_refractor_entry(batter_state) first_line = result.split("\n")[0] - badge_pos = first_line.find("[BC]") + symbol_pos = first_line.find("◈") name_pos = first_line.find("Mike Trout") - assert badge_pos < name_pos + assert symbol_pos < name_pos # --------------------------------------------------------------------------- @@ -271,34 +274,34 @@ class TestApplyCloseFilter: def test_close_card_included(self): """Card at exactly 80% is included.""" - state = {"current_tier": 1, "formula_value": 80, "next_threshold": 100} + state = {"current_tier": 1, "current_value": 80, "next_threshold": 100} assert apply_close_filter([state]) == [state] def test_above_80_percent_included(self): """Card above 80% is included.""" - state = {"current_tier": 0, "formula_value": 95, "next_threshold": 100} + state = {"current_tier": 0, "current_value": 95, "next_threshold": 100} assert apply_close_filter([state]) == [state] def test_below_80_percent_excluded(self): """Card below 80% threshold is excluded.""" - state = {"current_tier": 1, "formula_value": 79, "next_threshold": 100} + state = {"current_tier": 1, "current_value": 79, "next_threshold": 100} assert apply_close_filter([state]) == [] def test_fully_evolved_excluded(self): """T4 cards are never returned by close filter.""" - state = {"current_tier": 4, "formula_value": 300, "next_threshold": None} + state = {"current_tier": 4, "current_value": 300, "next_threshold": None} assert apply_close_filter([state]) == [] def test_none_threshold_excluded(self): """Cards with no next_threshold (regardless of tier) are excluded.""" - state = {"current_tier": 3, "formula_value": 200, "next_threshold": None} + state = {"current_tier": 3, "current_value": 200, "next_threshold": None} assert apply_close_filter([state]) == [] def test_mixed_list(self): """Only qualifying cards are returned from a mixed list.""" - close = {"current_tier": 1, "formula_value": 90, "next_threshold": 100} - not_close = {"current_tier": 1, "formula_value": 50, "next_threshold": 100} - evolved = {"current_tier": 4, "formula_value": 300, "next_threshold": None} + close = {"current_tier": 1, "current_value": 90, "next_threshold": 100} + not_close = {"current_tier": 1, "current_value": 50, "next_threshold": 100} + evolved = {"current_tier": 4, "current_value": 300, "next_threshold": None} result = apply_close_filter([close, not_close, evolved]) assert result == [close] @@ -506,9 +509,9 @@ class TestApplyCloseFilterWithAllT4Cards: the "no cards close to advancement" message rather than an empty embed. """ t4_cards = [ - {"current_tier": 4, "formula_value": 300, "next_threshold": None}, - {"current_tier": 4, "formula_value": 500, "next_threshold": None}, - {"current_tier": 4, "formula_value": 275, "next_threshold": None}, + {"current_tier": 4, "current_value": 300, "next_threshold": None}, + {"current_tier": 4, "current_value": 500, "next_threshold": None}, + {"current_tier": 4, "current_value": 275, "next_threshold": None}, ] result = apply_close_filter(t4_cards) assert result == [], ( @@ -523,7 +526,7 @@ class TestApplyCloseFilterWithAllT4Cards: """ t4_high_value = { "current_tier": 4, - "formula_value": 9999, + "current_value": 9999, "next_threshold": None, } assert apply_close_filter([t4_high_value]) == [] @@ -552,9 +555,9 @@ class TestFormatRefractorEntryMalformedInput: than crashing with a KeyError. """ state = { - "card_type": "batter", + "track": {"card_type": "batter"}, "current_tier": 1, - "formula_value": 100, + "current_value": 100, "next_threshold": 150, } result = format_refractor_entry(state) @@ -562,12 +565,12 @@ class TestFormatRefractorEntryMalformedInput: def test_missing_formula_value_uses_zero(self): """ - When formula_value is absent, the progress calculation should use 0 + When current_value is absent, the progress calculation should use 0 without raising a TypeError. """ state = { "player_name": "Test Player", - "card_type": "batter", + "track": {"card_type": "batter"}, "current_tier": 1, "next_threshold": 150, } @@ -587,14 +590,14 @@ class TestFormatRefractorEntryMalformedInput: def test_missing_card_type_uses_raw_fallback(self): """ - When card_type is absent, 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 defaults to 'batter' + internally (via .get("card_type", "batter")), so "PA+TB×2" should + appear as the formula label. """ state = { "player_name": "Test Player", "current_tier": 1, - "formula_value": 50, + "current_value": 50, "next_threshold": 100, } result = format_refractor_entry(state) @@ -623,30 +626,27 @@ class TestRenderProgressBarBoundaryPrecision: rest empty. The bar must not appear more than minimally filled. """ bar = render_progress_bar(1, 100) - # Interior is 10 chars: count '=' vs '-' - interior = bar[1:-1] # strip '[' and ']' - filled_count = interior.count("=") + filled_count = bar.count("▰") assert filled_count <= 1, ( f"1/100 should show 0 or 1 filled segment, got {filled_count}: {bar!r}" ) def test_ninety_nine_of_hundred_is_nearly_full(self): """ - 99/100 = 99% — should produce a bar with 9 or 10 filled segments. - The bar must NOT be completely empty or show fewer than 9 filled. + 99/100 = 99% — should produce a bar with 11 or 12 filled segments. + The bar must NOT be completely empty or show fewer than 11 filled. """ bar = render_progress_bar(99, 100) - interior = bar[1:-1] - filled_count = interior.count("=") - assert filled_count >= 9, ( - f"99/100 should show 9 or 10 filled segments, got {filled_count}: {bar!r}" + filled_count = bar.count("▰") + assert filled_count >= 11, ( + f"99/100 should show 11 or 12 filled segments, got {filled_count}: {bar!r}" ) - # But it must not overflow the bar width - assert len(interior) == 10 + # Bar width must be exactly 12 + assert len(bar) == 12 def test_zero_of_hundred_is_completely_empty(self): - """0/100 = all dashes — re-verify the all-empty baseline.""" - assert render_progress_bar(0, 100) == "[----------]" + """0/100 = all empty blocks — re-verify the all-empty baseline.""" + assert render_progress_bar(0, 100) == "▱" * 12 def test_negative_current_does_not_overflow_bar(self): """ @@ -656,14 +656,12 @@ class TestRenderProgressBarBoundaryPrecision: a future refactor removing the clamp. """ bar = render_progress_bar(-5, 100) - interior = bar[1:-1] - # No filled segments should exist for a negative value - filled_count = interior.count("=") + filled_count = bar.count("▰") assert filled_count == 0, ( f"Negative current should produce 0 filled segments, got {filled_count}: {bar!r}" ) - # Bar width must be exactly 10 - assert len(interior) == 10 + # Bar width must be exactly 12 + assert len(bar) == 12 # --------------------------------------------------------------------------- @@ -689,9 +687,9 @@ class TestRPFormulaLabel: """ rp_state = { "player_name": "Edwin Diaz", - "card_type": "rp", + "track": {"card_type": "rp"}, "current_tier": 1, - "formula_value": 45, + "current_value": 45, "next_threshold": 60, } result = format_refractor_entry(rp_state) @@ -724,9 +722,9 @@ class TestUnknownCardTypeFallback: """ util_state = { "player_name": "Ben Zobrist", - "card_type": "util", + "track": {"card_type": "util"}, "current_tier": 2, - "formula_value": 80, + "current_value": 80, "next_threshold": 120, } result = format_refractor_entry(util_state) @@ -741,9 +739,9 @@ class TestUnknownCardTypeFallback: """ state = { "player_name": "Test Player", - "card_type": "dh", + "track": {"card_type": "dh"}, "current_tier": 1, - "formula_value": 30, + "current_value": 30, "next_threshold": 50, } result = format_refractor_entry(state)