From 0304753e922800034b1a7c64d197146da2da44f8 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 13 Mar 2026 15:07:35 -0500 Subject: [PATCH 1/5] feat: tier badge prefix in card embed title (WP-12) (#77) Add evolution tier badge to get_card_embeds() title. Fetches evolution/cards/{id} endpoint; prepends [T1]/[T2]/[T3]/[EVO] when current_tier > 0. API failure is silently swallowed so card display is never broken. Also add ruff.toml to suppress legacy star-import rules (F403/F405) and bare-except/type-comparison rules (E722/E721) for helpers/main.py, which predates the pre-commit hook installation. Closes #77 Co-Authored-By: Claude Sonnet 4.6 --- helpers/main.py | 105 ++++++------ ruff.toml | 5 + tests/test_card_embed_evolution.py | 261 +++++++++++++++++++++++++++++ 3 files changed, 317 insertions(+), 54 deletions(-) create mode 100644 ruff.toml create mode 100644 tests/test_card_embed_evolution.py diff --git a/helpers/main.py b/helpers/main.py index 0b989a5..829654c 100644 --- a/helpers/main.py +++ b/helpers/main.py @@ -2,35 +2,23 @@ import asyncio import datetime import logging import math -import os import random -import traceback import discord -import pygsheets import aiohttp from discord.ext import commands from api_calls import * from bs4 import BeautifulSoup -from difflib import get_close_matches -from dataclasses import dataclass -from typing import Optional, Literal, Union, List +from typing import Optional, Union, List -from exceptions import log_exception from in_game.gameplay_models import Team from constants import * from discord_ui import * from random_content import * from utils import ( - position_name_to_abbrev, - user_has_role, - get_roster_sheet_legacy, get_roster_sheet, - get_player_url, - owner_only, get_cal_user, - get_context_user, ) from search_utils import * from discord_utils import * @@ -122,8 +110,17 @@ async def share_channel(channel, user, read_only=False): async def get_card_embeds(card, include_stats=False) -> list: + tier_badge = "" + try: + evo_state = await db_get(f"evolution/cards/{card['id']}") + if evo_state and evo_state.get("current_tier", 0) > 0: + tier = evo_state["current_tier"] + tier_badge = f"[{'EVO' if tier >= 4 else f'T{tier}'}] " + except Exception: + pass + embed = discord.Embed( - title=f"{card['player']['p_name']}", + title=f"{tier_badge}{card['player']['p_name']}", color=int(card["player"]["rarity"]["color"], 16), ) # embed.description = card['team']['lname'] @@ -166,7 +163,7 @@ async def get_card_embeds(card, include_stats=False) -> list: ] if any(bool_list): if count == 1: - coll_string = f"Only you" + coll_string = "Only you" else: coll_string = ( f"You and {count - 1} other{'s' if count - 1 != 1 else ''}" @@ -174,7 +171,7 @@ async def get_card_embeds(card, include_stats=False) -> list: elif count: coll_string = f"{count} other team{'s' if count != 1 else ''}" else: - coll_string = f"0 teams" + coll_string = "0 teams" embed.add_field(name="Collected By", value=coll_string) else: embed.add_field( @@ -213,7 +210,7 @@ async def get_card_embeds(card, include_stats=False) -> list: ) if evo_mon is not None: embed.add_field(name="Evolves Into", value=f"{evo_mon['p_name']}") - except Exception as e: + except Exception: logging.error( "could not pull evolution: {e}", exc_info=True, stack_info=True ) @@ -224,7 +221,7 @@ async def get_card_embeds(card, include_stats=False) -> list: ) if evo_mon is not None: embed.add_field(name="Evolves From", value=f"{evo_mon['p_name']}") - except Exception as e: + except Exception: logging.error( "could not pull evolution: {e}", exc_info=True, stack_info=True ) @@ -326,7 +323,7 @@ async def display_cards( ) try: cards.sort(key=lambda x: x["player"]["rarity"]["value"]) - logger.debug(f"Cards sorted successfully") + logger.debug("Cards sorted successfully") card_embeds = [await get_card_embeds(x) for x in cards] logger.debug(f"Created {len(card_embeds)} card embeds") @@ -347,15 +344,15 @@ async def display_cards( r_emoji = "→" view.left_button.disabled = True view.left_button.label = f"{l_emoji}Prev: -/{len(card_embeds)}" - view.cancel_button.label = f"Close Pack" + view.cancel_button.label = "Close Pack" view.right_button.label = f"Next: {page_num + 2}/{len(card_embeds)}{r_emoji}" if len(cards) == 1: view.right_button.disabled = True - logger.debug(f"Pagination view created successfully") + logger.debug("Pagination view created successfully") if pack_cover: - logger.debug(f"Sending pack cover message") + logger.debug("Sending pack cover message") msg = await channel.send( content=None, embed=image_embed(pack_cover, title=f"{team['lname']}", desc=pack_name), @@ -367,7 +364,7 @@ async def display_cards( content=None, embeds=card_embeds[page_num], view=view ) - logger.debug(f"Initial message sent successfully") + logger.debug("Initial message sent successfully") except Exception as e: logger.error( f"Error creating view or sending initial message: {e}", exc_info=True @@ -384,12 +381,12 @@ async def display_cards( f"{user.mention} you've got {len(cards)} cards here" ) - logger.debug(f"Follow-up message sent successfully") + logger.debug("Follow-up message sent successfully") except Exception as e: logger.error(f"Error sending follow-up message: {e}", exc_info=True) return False - logger.debug(f"Starting main interaction loop") + logger.debug("Starting main interaction loop") while True: try: logger.debug(f"Waiting for user interaction on page {page_num}") @@ -455,7 +452,7 @@ async def display_cards( ), view=view, ) - logger.debug(f"MVP display updated successfully") + logger.debug("MVP display updated successfully") except Exception as e: logger.error( f"Error processing shiny card on page {page_num}: {e}", exc_info=True @@ -463,19 +460,19 @@ async def display_cards( # Continue with regular flow instead of crashing try: tmp_msg = await channel.send( - content=f"<@&1163537676885033010> we've got an MVP!" + content="<@&1163537676885033010> we've got an MVP!" ) await follow_up.edit( - content=f"<@&1163537676885033010> we've got an MVP!" + content="<@&1163537676885033010> we've got an MVP!" ) await tmp_msg.delete() except discord.errors.NotFound: # Role might not exist or message was already deleted - await follow_up.edit(content=f"We've got an MVP!") + await follow_up.edit(content="We've got an MVP!") except Exception as e: # Log error but don't crash the function logger.error(f"Error handling MVP notification: {e}") - await follow_up.edit(content=f"We've got an MVP!") + await follow_up.edit(content="We've got an MVP!") await view.wait() view = Pagination([user], timeout=10) @@ -483,7 +480,7 @@ async def display_cards( view.right_button.label = ( f"Next: {page_num + 2}/{len(card_embeds)}{r_emoji}" ) - view.cancel_button.label = f"Close Pack" + view.cancel_button.label = "Close Pack" view.left_button.label = f"{l_emoji}Prev: {page_num}/{len(card_embeds)}" if page_num == 0: view.left_button.label = f"{l_emoji}Prev: -/{len(card_embeds)}" @@ -531,7 +528,7 @@ async def embed_pagination( l_emoji = "" r_emoji = "" view.right_button.label = f"Next: {page_num + 2}/{len(all_embeds)}{r_emoji}" - view.cancel_button.label = f"Cancel" + view.cancel_button.label = "Cancel" view.left_button.label = f"{l_emoji}Prev: {page_num}/{len(all_embeds)}" if page_num == 0: view.left_button.label = f"{l_emoji}Prev: -/{len(all_embeds)}" @@ -566,7 +563,7 @@ async def embed_pagination( view = Pagination([user], timeout=timeout) view.right_button.label = f"Next: {page_num + 2}/{len(all_embeds)}{r_emoji}" - view.cancel_button.label = f"Cancel" + view.cancel_button.label = "Cancel" view.left_button.label = f"{l_emoji}Prev: {page_num}/{len(all_embeds)}" if page_num == 0: view.left_button.label = f"{l_emoji}Prev: -/{len(all_embeds)}" @@ -880,7 +877,7 @@ async def roll_for_cards(all_packs: list, extra_val=None) -> list: timeout=10, ) if not success: - raise ConnectionError(f"Failed to create this pack of cards.") + raise ConnectionError("Failed to create this pack of cards.") await db_patch( "packs", @@ -946,7 +943,7 @@ def get_sheets(bot): except Exception as e: logger.error(f"Could not grab sheets auth: {e}") raise ConnectionError( - f"Bot has not authenticated with discord; please try again in 1 minute." + "Bot has not authenticated with discord; please try again in 1 minute." ) @@ -1056,7 +1053,7 @@ def get_blank_team_card(player): def get_rosters(team, bot, roster_num: Optional[int] = None) -> list: sheets = get_sheets(bot) this_sheet = sheets.open_by_key(team["gsheet"]) - r_sheet = this_sheet.worksheet_by_title(f"My Rosters") + r_sheet = this_sheet.worksheet_by_title("My Rosters") logger.debug(f"this_sheet: {this_sheet} / r_sheet = {r_sheet}") all_rosters = [None, None, None] @@ -1137,11 +1134,11 @@ def get_roster_lineups(team, bot, roster_num, lineup_num) -> list: try: lineup_cells = [(row[0].value, int(row[1].value)) for row in raw_cells] - except ValueError as e: + except ValueError: logger.error(f"Could not pull roster for {team['abbrev']} due to a ValueError") raise ValueError( - f"Uh oh. Looks like your roster might not be saved. I am reading blanks when I try to " - f"get the card IDs" + "Uh oh. Looks like your roster might not be saved. I am reading blanks when I try to " + "get the card IDs" ) logger.debug(f"lineup_cells: {lineup_cells}") @@ -1536,7 +1533,7 @@ def get_ratings_guide(sheets): } for x in p_data ] - except Exception as e: + except Exception: return {"valid": False} return {"valid": True, "batter_ratings": batters, "pitcher_ratings": pitchers} @@ -1748,7 +1745,7 @@ async def open_st_pr_packs(all_packs: list, team: dict, context): pack_ids = await roll_for_cards(all_packs) if not pack_ids: logger.error(f"open_packs - unable to roll_for_cards for packs: {all_packs}") - raise ValueError(f"I was not able to unpack these cards") + raise ValueError("I was not able to unpack these cards") all_cards = [] for p_id in pack_ids: @@ -1759,7 +1756,7 @@ async def open_st_pr_packs(all_packs: list, team: dict, context): if not all_cards: logger.error(f"open_packs - unable to get cards for packs: {pack_ids}") - raise ValueError(f"I was not able to display these cards") + raise ValueError("I was not able to display these cards") # Present cards to opening channel if type(context) == commands.Context: @@ -1818,7 +1815,7 @@ async def get_choice_from_cards( view = Pagination([interaction.user], timeout=30) view.left_button.disabled = True view.left_button.label = f"Prev: -/{len(card_embeds)}" - view.cancel_button.label = f"Take This Card" + view.cancel_button.label = "Take This Card" view.cancel_button.style = discord.ButtonStyle.success view.cancel_button.disabled = True view.right_button.label = f"Next: 1/{len(card_embeds)}" @@ -1836,7 +1833,7 @@ async def get_choice_from_cards( view = Pagination([interaction.user], timeout=30) view.left_button.label = f"Prev: -/{len(card_embeds)}" view.left_button.disabled = True - view.cancel_button.label = f"Take This Card" + view.cancel_button.label = "Take This Card" view.cancel_button.style = discord.ButtonStyle.success view.right_button.label = f"Next: {page_num + 1}/{len(card_embeds)}" @@ -1879,7 +1876,7 @@ async def get_choice_from_cards( view = Pagination([interaction.user], timeout=30) view.left_button.label = f"Prev: {page_num - 1}/{len(card_embeds)}" - view.cancel_button.label = f"Take This Card" + view.cancel_button.label = "Take This Card" view.cancel_button.style = discord.ButtonStyle.success view.right_button.label = f"Next: {page_num + 1}/{len(card_embeds)}" if page_num == 1: @@ -1925,7 +1922,7 @@ async def open_choice_pack( players = pl["players"] elif pack_type == "Team Choice": if this_pack["pack_team"] is None: - raise KeyError(f"Team not listed for Team Choice pack") + raise KeyError("Team not listed for Team Choice pack") d1000 = random.randint(1, 1000) pack_cover = this_pack["pack_team"]["logo"] @@ -1964,7 +1961,7 @@ async def open_choice_pack( rarity_id += 1 elif pack_type == "Promo Choice": if this_pack["pack_cardset"] is None: - raise KeyError(f"Cardset not listed for Promo Choice pack") + raise KeyError("Cardset not listed for Promo Choice pack") d1000 = random.randint(1, 1000) pack_cover = IMAGES["mvp-hype"] @@ -2021,8 +2018,8 @@ async def open_choice_pack( rarity_id += 3 if len(players) == 0: - logger.error(f"Could not create choice pack") - raise ConnectionError(f"Could not create choice pack") + logger.error("Could not create choice pack") + raise ConnectionError("Could not create choice pack") if type(context) == commands.Context: author = context.author @@ -2045,7 +2042,7 @@ async def open_choice_pack( view = Pagination([author], timeout=30) view.left_button.disabled = True view.left_button.label = f"Prev: -/{len(card_embeds)}" - view.cancel_button.label = f"Take This Card" + view.cancel_button.label = "Take This Card" view.cancel_button.style = discord.ButtonStyle.success view.cancel_button.disabled = True view.right_button.label = f"Next: 1/{len(card_embeds)}" @@ -2063,10 +2060,10 @@ async def open_choice_pack( ) if rarity_id >= 5: tmp_msg = await pack_channel.send( - content=f"<@&1163537676885033010> we've got an MVP!" + content="<@&1163537676885033010> we've got an MVP!" ) else: - tmp_msg = await pack_channel.send(content=f"We've got a choice pack here!") + tmp_msg = await pack_channel.send(content="We've got a choice pack here!") while True: await view.wait() @@ -2081,7 +2078,7 @@ async def open_choice_pack( ) except Exception as e: logger.error(f"failed to create cards: {e}") - raise ConnectionError(f"Failed to distribute these cards.") + raise ConnectionError("Failed to distribute these cards.") await db_patch( "packs", @@ -2115,7 +2112,7 @@ async def open_choice_pack( view = Pagination([author], timeout=30) view.left_button.label = f"Prev: {page_num - 1}/{len(card_embeds)}" - view.cancel_button.label = f"Take This Card" + view.cancel_button.label = "Take This Card" view.cancel_button.style = discord.ButtonStyle.success view.right_button.label = f"Next: {page_num + 1}/{len(card_embeds)}" if page_num == 1: diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..1971b0c --- /dev/null +++ b/ruff.toml @@ -0,0 +1,5 @@ +[lint.per-file-ignores] +# helpers/main.py uses star imports as a legacy pattern (api_calls, constants, +# discord_ui, etc.). F403/F405 are suppressed here to allow the pre-commit hook +# to pass without requiring a full refactor of the star import chain. +"helpers/main.py" = ["F403", "F405", "E722", "E721"] diff --git a/tests/test_card_embed_evolution.py b/tests/test_card_embed_evolution.py new file mode 100644 index 0000000..e0bcd28 --- /dev/null +++ b/tests/test_card_embed_evolution.py @@ -0,0 +1,261 @@ +""" +Tests for WP-12: Tier Badge on Card Embed. + +Verifies that get_card_embeds() prepends a tier badge to the card title when a +card has evolution progress, and falls back gracefully when the evolution API +is unavailable or returns no state. +""" + +import pytest +from unittest.mock import AsyncMock, patch + +import discord + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +def _make_card(card_id=1, player_name="Mike Trout", rarity_color="FFD700"): + """Minimal card dict matching the API shape consumed by get_card_embeds.""" + return { + "id": card_id, + "player": { + "player_id": 101, + "p_name": player_name, + "rarity": {"name": "MVP", "value": 5, "color": rarity_color}, + "cost": 500, + "image": "https://example.com/card.png", + "image2": None, + "mlbclub": "Los Angeles Angels", + "franchise": "Los Angeles Angels", + "headshot": "https://example.com/headshot.jpg", + "cardset": {"name": "2023 Season"}, + "pos_1": "CF", + "pos_2": None, + "pos_3": None, + "pos_4": None, + "pos_5": None, + "pos_6": None, + "pos_7": None, + "bbref_id": "troutmi01", + "strat_code": "420420", + "fangr_id": None, + "vanity_card": None, + }, + "team": { + "id": 10, + "lname": "Paper Dynasty", + "logo": "https://example.com/logo.png", + "season": 7, + }, + } + + +def _make_paperdex(): + """Minimal paperdex response.""" + return {"count": 0, "paperdex": []} + + +# --------------------------------------------------------------------------- +# Helpers to patch the async dependencies of get_card_embeds +# --------------------------------------------------------------------------- + + +def _patch_db_get(evo_response=None, paperdex_response=None): + """ + Return a side_effect callable that routes db_get calls to the right mock + responses, so other get_card_embeds internals still behave. + """ + if paperdex_response is None: + paperdex_response = _make_paperdex() + + async def _side_effect(endpoint, *args, **kwargs): + if str(endpoint).startswith("evolution/cards/"): + return evo_response + if endpoint == "paperdex": + return paperdex_response + # Fallback for any other endpoint (e.g. plays/batting, plays/pitching) + return None + + return _side_effect + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +class TestTierBadgeFormat: + """Unit: tier badge string format for each tier level.""" + + @pytest.mark.asyncio + async def test_tier_zero_no_badge(self): + """T0 evolution state (current_tier=0) should produce no badge in title.""" + card = _make_card() + evo_state = {"current_tier": 0, "card_id": 1} + + with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: + mock_db.side_effect = _patch_db_get(evo_response=evo_state) + embeds = await _call_get_card_embeds(card) + + assert embeds[0].title == "Mike Trout" + + @pytest.mark.asyncio + async def test_tier_one_badge(self): + """current_tier=1 should prefix title with [T1].""" + card = _make_card() + evo_state = {"current_tier": 1, "card_id": 1} + + with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: + mock_db.side_effect = _patch_db_get(evo_response=evo_state) + embeds = await _call_get_card_embeds(card) + + assert embeds[0].title == "[T1] Mike Trout" + + @pytest.mark.asyncio + async def test_tier_two_badge(self): + """current_tier=2 should prefix title with [T2].""" + card = _make_card() + evo_state = {"current_tier": 2, "card_id": 1} + + with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: + mock_db.side_effect = _patch_db_get(evo_response=evo_state) + embeds = await _call_get_card_embeds(card) + + assert embeds[0].title == "[T2] Mike Trout" + + @pytest.mark.asyncio + async def test_tier_three_badge(self): + """current_tier=3 should prefix title with [T3].""" + card = _make_card() + evo_state = {"current_tier": 3, "card_id": 1} + + with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: + mock_db.side_effect = _patch_db_get(evo_response=evo_state) + embeds = await _call_get_card_embeds(card) + + assert embeds[0].title == "[T3] Mike Trout" + + @pytest.mark.asyncio + async def test_tier_four_evo_badge(self): + """current_tier=4 (fully evolved) should prefix title with [EVO].""" + card = _make_card() + evo_state = {"current_tier": 4, "card_id": 1} + + with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: + mock_db.side_effect = _patch_db_get(evo_response=evo_state) + embeds = await _call_get_card_embeds(card) + + assert embeds[0].title == "[EVO] Mike Trout" + + +class TestTierBadgeInTitle: + """Unit: badge appears correctly in the embed title.""" + + @pytest.mark.asyncio + async def test_badge_prepended_to_player_name(self): + """Badge should be prepended so title reads '[Tx] '.""" + card = _make_card(player_name="Juan Soto") + evo_state = {"current_tier": 2, "card_id": 1} + + with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: + mock_db.side_effect = _patch_db_get(evo_response=evo_state) + embeds = await _call_get_card_embeds(card) + + assert embeds[0].title.startswith("[T2] ") + assert "Juan Soto" in embeds[0].title + + +class TestFullyEvolvedBadge: + """Unit: fully evolved card shows [EVO] badge.""" + + @pytest.mark.asyncio + async def test_fully_evolved_badge(self): + """T4 card should show [EVO] prefix, not [T4].""" + card = _make_card() + evo_state = {"current_tier": 4} + + with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: + mock_db.side_effect = _patch_db_get(evo_response=evo_state) + embeds = await _call_get_card_embeds(card) + + assert embeds[0].title.startswith("[EVO] ") + assert "[T4]" not in embeds[0].title + + +class TestNoBadgeGracefulFallback: + """Unit: embed renders correctly when evolution state is absent or API fails.""" + + @pytest.mark.asyncio + async def test_no_evolution_state_no_badge(self): + """When evolution API returns None (404), title has no badge.""" + card = _make_card() + + with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: + mock_db.side_effect = _patch_db_get(evo_response=None) + embeds = await _call_get_card_embeds(card) + + assert embeds[0].title == "Mike Trout" + + @pytest.mark.asyncio + async def test_api_exception_no_badge(self): + """When evolution API raises an exception, card display is unaffected.""" + card = _make_card() + + async def _failing_db_get(endpoint, *args, **kwargs): + if str(endpoint).startswith("evolution/cards/"): + raise ConnectionError("API unreachable") + if endpoint == "paperdex": + return _make_paperdex() + return None + + with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: + mock_db.side_effect = _failing_db_get + embeds = await _call_get_card_embeds(card) + + assert embeds[0].title == "Mike Trout" + + +class TestEmbedColorUnchanged: + """Unit: embed color comes from card rarity, not affected by evolution state.""" + + @pytest.mark.asyncio + async def test_embed_color_from_rarity_with_evolution(self): + """Color is still derived from rarity even when a tier badge is present.""" + rarity_color = "FF0000" + card = _make_card(rarity_color=rarity_color) + evo_state = {"current_tier": 2} + + with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: + mock_db.side_effect = _patch_db_get(evo_response=evo_state) + embeds = await _call_get_card_embeds(card) + + assert embeds[0].color == discord.Color(int(rarity_color, 16)) + + @pytest.mark.asyncio + async def test_embed_color_from_rarity_without_evolution(self): + """Color is derived from rarity when no evolution state exists.""" + rarity_color = "00FF00" + card = _make_card(rarity_color=rarity_color) + + with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: + mock_db.side_effect = _patch_db_get(evo_response=None) + embeds = await _call_get_card_embeds(card) + + assert embeds[0].color == discord.Color(int(rarity_color, 16)) + + +# --------------------------------------------------------------------------- +# Helper: call get_card_embeds and return embed list +# --------------------------------------------------------------------------- + + +async def _call_get_card_embeds(card): + """Import and call get_card_embeds, returning the list of embeds.""" + from helpers.main import get_card_embeds + + result = await get_card_embeds(card) + if isinstance(result, list): + return result + return [result] From fc8508fbd593b683314c58606687d41533c5e6b8 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 23 Mar 2026 08:50:11 -0500 Subject: [PATCH 2/5] refactor: rename Evolution badges to Refractor tier names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Badge labels: [R] Refractor, [GR] Gold Refractor, [SF] Superfractor, [SF★] fully evolved - Fix broken {e} log format strings (restore `as e` + add f-string prefix) - Restore ruff.toml from main (branch had stripped global config) - Update all test assertions for new badge names (11/11 pass) Co-Authored-By: Claude Opus 4.6 (1M context) --- helpers/main.py | 11 ++++---- ruff.toml | 40 +++++++++++++++++++++++++++--- tests/test_card_embed_evolution.py | 26 +++++++++---------- 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/helpers/main.py b/helpers/main.py index 829654c..535ac29 100644 --- a/helpers/main.py +++ b/helpers/main.py @@ -115,7 +115,8 @@ async def get_card_embeds(card, include_stats=False) -> list: evo_state = await db_get(f"evolution/cards/{card['id']}") if evo_state and evo_state.get("current_tier", 0) > 0: tier = evo_state["current_tier"] - tier_badge = f"[{'EVO' if tier >= 4 else f'T{tier}'}] " + TIER_BADGES = {1: "R", 2: "GR", 3: "SF"} + tier_badge = f"[{TIER_BADGES.get(tier, 'SF★')}] " except Exception: pass @@ -210,9 +211,9 @@ async def get_card_embeds(card, include_stats=False) -> list: ) if evo_mon is not None: embed.add_field(name="Evolves Into", value=f"{evo_mon['p_name']}") - except Exception: + except Exception as e: logging.error( - "could not pull evolution: {e}", exc_info=True, stack_info=True + f"could not pull evolution: {e}", exc_info=True, stack_info=True ) if "420420" not in card["player"]["strat_code"]: try: @@ -221,9 +222,9 @@ async def get_card_embeds(card, include_stats=False) -> list: ) if evo_mon is not None: embed.add_field(name="Evolves From", value=f"{evo_mon['p_name']}") - except Exception: + except Exception as e: logging.error( - "could not pull evolution: {e}", exc_info=True, stack_info=True + f"could not pull evolution: {e}", exc_info=True, stack_info=True ) if include_stats: diff --git a/ruff.toml b/ruff.toml index 1971b0c..1b5220c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,37 @@ +# Ruff configuration for paper-dynasty discord bot +# See https://docs.astral.sh/ruff/configuration/ + +[lint] +# Rules suppressed globally because they reflect intentional project patterns: +# F403/F405: star imports — __init__.py files use `from .module import *` for re-exports +# E712: SQLAlchemy/SQLModel ORM comparisons require == syntax (not `is`) +# F541: f-strings without placeholders — 1000+ legacy occurrences; cosmetic, deferred +ignore = ["F403", "F405", "F541", "E712"] + +# Per-file suppressions for pre-existing violations in legacy code. +# New files outside these paths get the full rule set. +# Remove entries here as files are cleaned up. [lint.per-file-ignores] -# helpers/main.py uses star imports as a legacy pattern (api_calls, constants, -# discord_ui, etc.). F403/F405 are suppressed here to allow the pre-commit hook -# to pass without requiring a full refactor of the star import chain. -"helpers/main.py" = ["F403", "F405", "E722", "E721"] +# Core cogs — F841/F401 widespread; E711/E713/F811 pre-existing +"cogs/**" = ["F841", "F401", "E711", "E713", "F811"] +# Game engine — F841/F401 widespread; E722/F811 pre-existing bare-excepts and redefinitions +"in_game/**" = ["F841", "F401", "E722", "F811"] +# Helpers — F841/F401 widespread; E721/E722 pre-existing type-comparison and bare-excepts +"helpers/**" = ["F841", "F401", "E721", "E722"] +# Game logic and commands +"command_logic/**" = ["F841", "F401"] +# Test suite — E711/F811/F821 pre-existing test assertion patterns +"tests/**" = ["F841", "F401", "E711", "F811", "F821"] +# Utilities +"utilities/**" = ["F841", "F401"] +# Migrations +"migrations/**" = ["F401"] +# Top-level legacy files +"db_calls_gameplay.py" = ["F841", "F401"] +"gauntlets.py" = ["F841", "F401"] +"dice.py" = ["F841", "E711"] +"manual_pack_distribution.py" = ["F841"] +"play_lock.py" = ["F821"] +"paperdynasty.py" = ["F401"] +"api_calls.py" = ["F401"] +"health_server.py" = ["F401"] diff --git a/tests/test_card_embed_evolution.py b/tests/test_card_embed_evolution.py index e0bcd28..413ba28 100644 --- a/tests/test_card_embed_evolution.py +++ b/tests/test_card_embed_evolution.py @@ -103,7 +103,7 @@ class TestTierBadgeFormat: @pytest.mark.asyncio async def test_tier_one_badge(self): - """current_tier=1 should prefix title with [T1].""" + """current_tier=1 should prefix title with [R] (Refractor).""" card = _make_card() evo_state = {"current_tier": 1, "card_id": 1} @@ -111,11 +111,11 @@ class TestTierBadgeFormat: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) - assert embeds[0].title == "[T1] Mike Trout" + assert embeds[0].title == "[R] Mike Trout" @pytest.mark.asyncio async def test_tier_two_badge(self): - """current_tier=2 should prefix title with [T2].""" + """current_tier=2 should prefix title with [GR] (Gold Refractor).""" card = _make_card() evo_state = {"current_tier": 2, "card_id": 1} @@ -123,11 +123,11 @@ class TestTierBadgeFormat: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) - assert embeds[0].title == "[T2] Mike Trout" + assert embeds[0].title == "[GR] Mike Trout" @pytest.mark.asyncio async def test_tier_three_badge(self): - """current_tier=3 should prefix title with [T3].""" + """current_tier=3 should prefix title with [SF] (Superfractor).""" card = _make_card() evo_state = {"current_tier": 3, "card_id": 1} @@ -135,11 +135,11 @@ class TestTierBadgeFormat: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) - assert embeds[0].title == "[T3] Mike Trout" + assert embeds[0].title == "[SF] Mike Trout" @pytest.mark.asyncio - async def test_tier_four_evo_badge(self): - """current_tier=4 (fully evolved) should prefix title with [EVO].""" + async def test_tier_four_superfractor_badge(self): + """current_tier=4 (fully evolved) should prefix title with [SF★].""" card = _make_card() evo_state = {"current_tier": 4, "card_id": 1} @@ -147,7 +147,7 @@ class TestTierBadgeFormat: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) - assert embeds[0].title == "[EVO] Mike Trout" + assert embeds[0].title == "[SF★] Mike Trout" class TestTierBadgeInTitle: @@ -163,16 +163,16 @@ class TestTierBadgeInTitle: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) - assert embeds[0].title.startswith("[T2] ") + assert embeds[0].title.startswith("[GR] ") assert "Juan Soto" in embeds[0].title class TestFullyEvolvedBadge: - """Unit: fully evolved card shows [EVO] badge.""" + """Unit: fully evolved card shows [SF★] badge.""" @pytest.mark.asyncio async def test_fully_evolved_badge(self): - """T4 card should show [EVO] prefix, not [T4].""" + """T4 card should show [SF★] prefix, not [T4].""" card = _make_card() evo_state = {"current_tier": 4} @@ -180,7 +180,7 @@ class TestFullyEvolvedBadge: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) - assert embeds[0].title.startswith("[EVO] ") + assert embeds[0].title.startswith("[SF★] ") assert "[T4]" not in embeds[0].title From cc02d6db1e0c1b3fe3581007317507881375ed33 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 23 Mar 2026 10:32:10 -0500 Subject: [PATCH 3/5] fix: align badge labels with updated tier names Tier badges shifted to match updated spec: T1=[BC] Base Chrome, T2=[R] Refractor, T3=[GR] Gold Refractor, T4=[SF] Superfractor T0 (Base Card) shows no badge. All 11 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- helpers/main.py | 4 +-- tests/test_card_embed_evolution.py | 44 +++++++++++++++--------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/helpers/main.py b/helpers/main.py index 535ac29..9581032 100644 --- a/helpers/main.py +++ b/helpers/main.py @@ -115,8 +115,8 @@ async def get_card_embeds(card, include_stats=False) -> list: evo_state = await db_get(f"evolution/cards/{card['id']}") if evo_state and evo_state.get("current_tier", 0) > 0: tier = evo_state["current_tier"] - TIER_BADGES = {1: "R", 2: "GR", 3: "SF"} - tier_badge = f"[{TIER_BADGES.get(tier, 'SF★')}] " + TIER_BADGES = {1: "BC", 2: "R", 3: "GR", 4: "SF"} + tier_badge = f"[{TIER_BADGES.get(tier, 'SF')}] " except Exception: pass diff --git a/tests/test_card_embed_evolution.py b/tests/test_card_embed_evolution.py index 413ba28..f720463 100644 --- a/tests/test_card_embed_evolution.py +++ b/tests/test_card_embed_evolution.py @@ -103,10 +103,22 @@ class TestTierBadgeFormat: @pytest.mark.asyncio async def test_tier_one_badge(self): - """current_tier=1 should prefix title with [R] (Refractor).""" + """current_tier=1 should prefix title with [BC] (Base Chrome).""" card = _make_card() evo_state = {"current_tier": 1, "card_id": 1} + with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: + mock_db.side_effect = _patch_db_get(evo_response=evo_state) + embeds = await _call_get_card_embeds(card) + + assert embeds[0].title == "[BC] Mike Trout" + + @pytest.mark.asyncio + async def test_tier_two_badge(self): + """current_tier=2 should prefix title with [R] (Refractor).""" + card = _make_card() + evo_state = {"current_tier": 2, "card_id": 1} + with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) @@ -114,10 +126,10 @@ class TestTierBadgeFormat: assert embeds[0].title == "[R] Mike Trout" @pytest.mark.asyncio - async def test_tier_two_badge(self): - """current_tier=2 should prefix title with [GR] (Gold Refractor).""" + async def test_tier_three_badge(self): + """current_tier=3 should prefix title with [GR] (Gold Refractor).""" card = _make_card() - evo_state = {"current_tier": 2, "card_id": 1} + evo_state = {"current_tier": 3, "card_id": 1} with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: mock_db.side_effect = _patch_db_get(evo_response=evo_state) @@ -125,21 +137,9 @@ class TestTierBadgeFormat: assert embeds[0].title == "[GR] Mike Trout" - @pytest.mark.asyncio - async def test_tier_three_badge(self): - """current_tier=3 should prefix title with [SF] (Superfractor).""" - card = _make_card() - evo_state = {"current_tier": 3, "card_id": 1} - - with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: - mock_db.side_effect = _patch_db_get(evo_response=evo_state) - embeds = await _call_get_card_embeds(card) - - assert embeds[0].title == "[SF] Mike Trout" - @pytest.mark.asyncio async def test_tier_four_superfractor_badge(self): - """current_tier=4 (fully evolved) should prefix title with [SF★].""" + """current_tier=4 (Superfractor) should prefix title with [SF].""" card = _make_card() evo_state = {"current_tier": 4, "card_id": 1} @@ -147,7 +147,7 @@ class TestTierBadgeFormat: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) - assert embeds[0].title == "[SF★] Mike Trout" + assert embeds[0].title == "[SF] Mike Trout" class TestTierBadgeInTitle: @@ -163,16 +163,16 @@ class TestTierBadgeInTitle: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) - assert embeds[0].title.startswith("[GR] ") + assert embeds[0].title.startswith("[R] ") assert "Juan Soto" in embeds[0].title class TestFullyEvolvedBadge: - """Unit: fully evolved card shows [SF★] badge.""" + """Unit: fully evolved card shows [SF] badge (Superfractor).""" @pytest.mark.asyncio async def test_fully_evolved_badge(self): - """T4 card should show [SF★] prefix, not [T4].""" + """T4 card should show [SF] prefix, not [T4].""" card = _make_card() evo_state = {"current_tier": 4} @@ -180,7 +180,7 @@ class TestFullyEvolvedBadge: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) - assert embeds[0].title.startswith("[SF★] ") + assert embeds[0].title.startswith("[SF] ") assert "[T4]" not in embeds[0].title From 1f26020bd7db14c8764f3afc0f70c91ea03ec675 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 23 Mar 2026 10:39:56 -0500 Subject: [PATCH 4/5] fix: move TIER_BADGES to module level and fix unknown tier fallback - TIER_BADGES dict moved from inside get_card_embeds() to module level - Unknown tiers now show no badge instead of silently promoting to [SF] Co-Authored-By: Claude Opus 4.6 (1M context) --- helpers/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/helpers/main.py b/helpers/main.py index 9581032..72f0922 100644 --- a/helpers/main.py +++ b/helpers/main.py @@ -23,6 +23,9 @@ from utils import ( from search_utils import * from discord_utils import * +# Refractor tier badge prefixes for card embeds (T0 = no badge) +TIER_BADGES = {1: "BC", 2: "R", 3: "GR", 4: "SF"} + async def get_player_photo(player): search_term = player["bbref_id"] if player["bbref_id"] else player["p_name"] @@ -115,8 +118,8 @@ async def get_card_embeds(card, include_stats=False) -> list: evo_state = await db_get(f"evolution/cards/{card['id']}") if evo_state and evo_state.get("current_tier", 0) > 0: tier = evo_state["current_tier"] - TIER_BADGES = {1: "BC", 2: "R", 3: "GR", 4: "SF"} - tier_badge = f"[{TIER_BADGES.get(tier, 'SF')}] " + badge = TIER_BADGES.get(tier) + tier_badge = f"[{badge}] " if badge else "" except Exception: pass From 687b91a009a0787e748077e7b5b64e2ed9302450 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 23 Mar 2026 12:31:38 -0500 Subject: [PATCH 5/5] fix: rename test file and docstring to Refractor terminology Renames tests/test_card_embed_evolution.py to tests/test_card_embed_refractor.py and updates the module-level docstring to use "Refractor tier progression" / "Refractor API" instead of "evolution progress" / "evolution API". Co-Authored-By: Claude Sonnet 4.6 --- ...t_card_embed_evolution.py => test_card_embed_refractor.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename tests/{test_card_embed_evolution.py => test_card_embed_refractor.py} (98%) diff --git a/tests/test_card_embed_evolution.py b/tests/test_card_embed_refractor.py similarity index 98% rename from tests/test_card_embed_evolution.py rename to tests/test_card_embed_refractor.py index f720463..bd6ad4a 100644 --- a/tests/test_card_embed_evolution.py +++ b/tests/test_card_embed_refractor.py @@ -2,8 +2,8 @@ Tests for WP-12: Tier Badge on Card Embed. Verifies that get_card_embeds() prepends a tier badge to the card title when a -card has evolution progress, and falls back gracefully when the evolution API -is unavailable or returns no state. +card has Refractor tier progression, and falls back gracefully when the Refractor +API is unavailable or returns no state. """ import pytest