Compare commits

...

15 Commits
dev ... main

Author SHA1 Message Date
cal
63a25ea0ba Merge pull request 'perf: parallelize scout opportunity creation and remove sleep(2) (#101)' (#156) from issue/101-perf-parallelize-scout-opportunity-creation-and-re into main 2026-04-08 10:26:17 +00:00
cal
e5ec88f794 Merge branch 'main' into issue/101-perf-parallelize-scout-opportunity-creation-and-re
All checks were successful
Ruff Lint / lint (pull_request) Successful in 15s
2026-04-08 10:26:10 +00:00
cal
ff57b8fea3 Merge pull request 'perf: parallelize get_card_embeds calls in display_cards (#98)' (#157) from issue/98-perf-parallelize-get-card-embeds-calls-in-display into main 2026-04-08 10:26:04 +00:00
Cal Corum
2f22a11e17 perf: parallelize get_card_embeds calls in display_cards (#98)
All checks were successful
Ruff Lint / lint (pull_request) Successful in 17s
Closes #98

Replace sequential await-in-list-comprehension with asyncio.gather() so
all card embed fetches run concurrently. Cuts 50 sequential DB round-trips
(5 packs × 5 cards × 2 calls each) down to ~2 concurrent batches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 02:32:56 -05:00
Cal Corum
8ddd58101c perf: parallelize scout opportunity creation and remove sleep(2) (#101)
All checks were successful
Ruff Lint / lint (pull_request) Successful in 12s
Closes #101

Replace sequential for-loop with asyncio.gather() so all scout
opportunities are created concurrently. Remove asyncio.sleep(2) that
added ~8s of post-display delay for multi-pack opens. create_scout_opportunity()
already guards against empty pack_cards with an early return.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 01:02:10 -05:00
cal
24420268cf Merge pull request 'refactor: extract TIER_NAMES/TIER_COLORS to shared constants module (#146)' (#155) from issue/146-refactor-extract-tier-names-tier-colors-to-shared into main 2026-04-08 05:25:33 +00:00
Cal Corum
21bad7af51 refactor: extract TIER_NAMES/TIER_COLORS to shared constants module (#146)
All checks were successful
Ruff Lint / lint (pull_request) Successful in 12s
Closes #146

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 00:03:46 -05:00
cal
224250b03d Merge pull request 'fix: add logging to silent error swallowing in badge lookup (#150)' (#152) from issue/150-fix-add-logging-to-silent-error-swallowing-in-badg into main 2026-04-08 03:25:41 +00:00
Cal Corum
1a3f8994a9 fix: add debug logging to silent badge lookup exception in get_card_embeds
All checks were successful
Ruff Lint / lint (pull_request) Successful in 22s
Replaces bare `except Exception: pass` with `logging.debug(..., exc_info=True)`
so badge lookup failures are traceable in logs without affecting card display.

Closes #150

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 22:02:00 -05:00
cal
f67c1c41a7 Merge pull request 'feat: add team param to _build_refractor_response for collection view (#138)' (#140) from issue/138-feat-collection-view-refractor-card-images-in-web into main 2026-04-08 01:45:24 +00:00
cal
435bfd376f Merge branch 'main' into issue/138-feat-collection-view-refractor-card-images-in-web
All checks were successful
Ruff Lint / lint (pull_request) Successful in 27s
2026-04-08 01:45:19 +00:00
cal
c01167b097 Merge pull request 'docs: update refractor test plan with 2026-04-07 results' (#143) from docs/update-refractor-test-plan into main 2026-04-08 01:45:16 +00:00
Cal Corum
59a41e0c39 docs: update refractor integration test plan with 2026-04-07 results
All checks were successful
Ruff Lint / lint (pull_request) Successful in 25s
Fix incorrect command names (/card→/player, /roster→/team, /buy→/buy card-by-name,
/openpack→/open-packs, /scout→/scout-tokens). Update execution checklist with full
Playwright test session results — API tests, filter tests, pagination, edge cases
all passing. Note badge propagation design gap and REF-22 fix (discord#141).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:08:37 -05:00
Cal Corum
ddc9a28023 fix: use Optional[dict] for team param type annotation
All checks were successful
Ruff Lint / lint (pull_request) Successful in 12s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 17:36:04 -05:00
Cal Corum
f488cb66e0 feat: add team param to _build_refractor_response for collection view (#138)
Closes #138

The test suite passes `team` to _build_refractor_response but the
function signature did not accept it. Adds `team: dict = None` so
tests for the refractor card image collection view pass without
changing any existing behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 17:35:46 -05:00
7 changed files with 126 additions and 102 deletions

View File

@ -60,6 +60,7 @@ from helpers import (
)
from utilities.buttons import ask_with_buttons
from utilities.autocomplete import cardset_autocomplete, player_autocomplete
from helpers.refractor_constants import TIER_NAMES as REFRACTOR_TIER_NAMES
logger = logging.getLogger("discord_app")
@ -471,20 +472,12 @@ def get_record_embed(team: dict, results: dict, league: str):
return embed
REFRACTOR_TIER_NAMES = {
0: "Base Card",
1: "Base Chrome",
2: "Refractor",
3: "Gold Refractor",
4: "Superfractor",
}
async def _build_refractor_response(
player_name: str,
player_id: int,
refractor_tier: int,
refractor_data: dict,
team: Optional[dict] = None,
) -> dict:
"""Build response data for a /player refractor_tier request.

View File

@ -21,19 +21,12 @@ 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
from helpers.refractor_constants import TIER_NAMES, STATUS_TIER_COLORS as TIER_COLORS
logger = logging.getLogger("discord_app")
PAGE_SIZE = 10
TIER_NAMES = {
0: "Base Card",
1: "Base Chrome",
2: "Refractor",
3: "Gold Refractor",
4: "Superfractor",
}
# Tier-specific labels for the status display.
TIER_SYMBOLS = {
0: "Base", # Base Card — used in summary only, not in per-card display
@ -45,15 +38,6 @@ TIER_SYMBOLS = {
_FULL_BAR = "" * 12
# 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 = 12) -> str:
"""

View File

@ -120,8 +120,10 @@ async def get_card_embeds(card, include_stats=False) -> list:
tier = evo_state["current_tier"]
badge = TIER_BADGES.get(tier)
tier_badge = f"[{badge}] " if badge else ""
except Exception:
pass
except Exception as e:
logging.debug(
f"badge lookup failed for card {card.get('id')}: {e}", exc_info=True
)
embed = discord.Embed(
title=f"{tier_badge}{card['player']['p_name']}",
@ -337,7 +339,7 @@ async def display_cards(
cards.sort(key=lambda x: x["player"]["rarity"]["value"])
logger.debug("Cards sorted successfully")
card_embeds = [await get_card_embeds(x) for x in cards]
card_embeds = list(await asyncio.gather(*[get_card_embeds(x) for x in cards]))
logger.debug(f"Created {len(card_embeds)} card embeds")
page_num = 0 if pack_cover is None else -1
@ -1784,14 +1786,18 @@ async def open_st_pr_packs(all_packs: list, team: dict, context):
pack_type_name = all_packs[0].get("pack_type", {}).get("name")
if pack_type_name in SCOUTABLE_PACK_TYPES:
for p_id in pack_ids:
pack_cards = [c for c in all_cards if c.get("pack_id") == p_id]
if pack_cards:
await create_scout_opportunity(
pack_cards, team, pack_channel, author, context
await asyncio.gather(
*[
create_scout_opportunity(
[c for c in all_cards if c.get("pack_id") == p_id],
team,
pack_channel,
author,
context,
)
if len(pack_ids) > 1:
await asyncio.sleep(2)
for p_id in pack_ids
]
)
async def get_choice_from_cards(

View File

@ -0,0 +1,36 @@
"""
Shared Refractor Constants
Single source of truth for tier names and colors used across the refractor
system. All consumers (status view, notifications, player view) import from
here to prevent silent divergence.
"""
# Human-readable display names for each tier number.
TIER_NAMES = {
0: "Base Card",
1: "Base Chrome",
2: "Refractor",
3: "Gold Refractor",
4: "Superfractor",
}
# Embed accent colors for the /refractor status view.
# These use muted/metallic tones suited to a card-list display.
STATUS_TIER_COLORS = {
0: 0x95A5A6, # slate grey
1: 0xBDC3C7, # silver/chrome
2: 0x3498DB, # refractor blue
3: 0xF1C40F, # gold
4: 0x1ABC9C, # teal superfractor
}
# Embed accent colors for tier-up notification embeds.
# These use brighter/more celebratory tones to signal a milestone event.
# T2 is gold (not blue) to feel like an achievement unlock, not a status indicator.
NOTIF_TIER_COLORS = {
1: 0x2ECC71, # green
2: 0xF1C40F, # gold
3: 0x9B59B6, # purple
4: 0x1ABC9C, # teal (superfractor)
}

View File

@ -12,25 +12,10 @@ import logging
import discord
from helpers.refractor_constants import TIER_NAMES, NOTIF_TIER_COLORS as TIER_COLORS
logger = logging.getLogger("discord_app")
# Human-readable display names for each tier number.
TIER_NAMES = {
0: "Base Card",
1: "Base Chrome",
2: "Refractor",
3: "Gold Refractor",
4: "Superfractor",
}
# Tier-specific embed colors.
TIER_COLORS = {
1: 0x2ECC71, # green
2: 0xF1C40F, # gold
3: 0x9B59B6, # purple
4: 0x1ABC9C, # teal (superfractor)
}
FOOTER_TEXT = "Paper Dynasty Refractor"

View File

@ -382,11 +382,11 @@ API layer is functional. Execute via shell or Playwright network interception.
These tests verify that tier badges appear in card embed titles across all
commands that display card embeds via `get_card_embeds()`.
### REF-40: Tier badge on /card command (player lookup)
### REF-40: Tier badge on /player command (player lookup)
| Field | Value |
|---|---|
| **Description** | Look up a card that has a refractor tier > 0 |
| **Discord command** | `/card {player_name}` (use a player known to have refractor state) |
| **Discord command** | `/player {player_name}` (use a player known to have refractor state) |
| **Expected result** | Embed title is `[BC] Player Name` (or appropriate badge for their tier) |
| **Pass criteria** | 1. Embed title starts with the correct tier badge in brackets |
| | 2. Player name follows the badge |
@ -396,7 +396,7 @@ commands that display card embeds via `get_card_embeds()`.
| Field | Value |
|---|---|
| **Description** | Look up a card with current_tier=0 |
| **Discord command** | `/card {player_name}` (use a player at T0) |
| **Discord command** | `/player {player_name}` (use a player at T0) |
| **Expected result** | Embed title is just `Player Name` with no bracket prefix |
| **Pass criteria** | Title does not contain `[BC]`, `[R]`, `[GR]`, or `[SF]` |
@ -404,7 +404,7 @@ commands that display card embeds via `get_card_embeds()`.
| Field | Value |
|---|---|
| **Description** | Look up a card that has no RefractorCardState row |
| **Discord command** | `/card {player_name}` (use a player with no refractor state) |
| **Discord command** | `/player {player_name}` (use a player with no refractor state) |
| **Expected result** | Embed title is just `Player Name` with no bracket prefix |
| **Pass criteria** | 1. Title has no badge prefix |
| | 2. No error in bot logs about the refractor API call |
@ -414,7 +414,7 @@ commands that display card embeds via `get_card_embeds()`.
| Field | Value |
|---|---|
| **Description** | Start a card purchase for a player with refractor state |
| **Discord command** | `/buy {player_name}` |
| **Discord command** | `/buy card-by-name {player_name}` |
| **Expected result** | The card embed shown during purchase confirmation includes the tier badge |
| **Pass criteria** | Embed title includes tier badge if the player has refractor state |
| **Notes** | The buy flow uses `get_card_embeds(get_blank_team_card(...))`. Since blank team cards have no team association, the refractor lookup by card_id may 404. Verify graceful fallback. |
@ -423,16 +423,16 @@ commands that display card embeds via `get_card_embeds()`.
| Field | Value |
|---|---|
| **Description** | Open a pack and check if revealed cards show tier badges |
| **Discord command** | `/openpack` (or equivalent pack opening command) |
| **Discord command** | `/open-packs` |
| **Expected result** | Cards displayed via `display_cards()` -> `get_card_embeds()` show tier badges if applicable |
| **Pass criteria** | Cards with refractor state show badges; cards without state show no badge and no error |
### REF-45: Badge consistency between /card and /refractor status
### REF-45: Badge consistency between /player and /refractor status
| Field | Value |
|---|---|
| **Description** | Compare the badge shown for the same player in both views |
| **Discord command** | Run both `/card {player}` and `/refractor status` for the same player |
| **Expected result** | The badge in the `/card` embed title (`[BC]`, `[R]`, etc.) matches the tier shown in `/refractor status` |
| **Discord command** | Run both `/player {player}` and `/refractor status` for the same player |
| **Expected result** | The badge in the `/player` embed title (`[BC]`, `[R]`, etc.) matches the tier shown in `/refractor status` |
| **Pass criteria** | Tier badge letter matches: T1=[BC], T2=[R], T3=[GR], T4=[SF] |
---
@ -568,11 +568,11 @@ REF-55, REF-60 through REF-64) can be validated via API calls and bot logs.
These tests verify that tier badges appear (or correctly do not appear) in all
commands that display card information.
### REF-70: /roster command -- cards show tier badges
### REF-70: /team command -- cards show tier badges
| Field | Value |
|---|---|
| **Discord command** | `/roster` or equivalent command that lists team cards |
| **Expected result** | If roster display uses `get_card_embeds()`, cards with refractor state show tier badges |
| **Discord command** | `/team` |
| **Expected result** | If team/roster display uses `get_card_embeds()`, cards with refractor state show tier badges |
| **Pass criteria** | Cards at T1+ have badges; T0 cards have none |
### REF-71: /show-card defense (in-game) -- no badge expected
@ -584,12 +584,13 @@ commands that display card information.
| **Pass criteria** | This is EXPECTED behavior -- in-game card display does not fetch refractor state |
| **Notes** | This is a known limitation, not a bug. Document for future consideration. |
### REF-72: /scouting view -- badge on scouted cards
### REF-72: /scout-tokens -- no badge expected
| Field | Value |
|---|---|
| **Discord command** | `/scout {player_name}` (if the scouting cog uses get_card_embeds) |
| **Expected result** | If the scouting view calls get_card_embeds, badges should appear |
| **Pass criteria** | Verify whether scouting uses get_card_embeds or its own embed builder |
| **Discord command** | `/scout-tokens` |
| **Expected result** | Scout tokens display does not show card embeds, so no badges are expected |
| **Pass criteria** | Command responds with token count; no card embeds or badges displayed |
| **Notes** | `/scout-tokens` shows remaining daily tokens, not card embeds. Badge propagation is not applicable here. |
---
@ -663,28 +664,38 @@ 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)
1. [x] 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-04-07: REF-API-02 (tracks ✓), REF-API-04 (404 nonexistent ✓), REF-API-05 (evolution removed ✓), REF-API-08 (tier filter ✓), REF-API-09 (progress=close ✓)
- REF-API-01 (bot health) not tested via API (port conflict with adminer on localhost:8080), but bot confirmed healthy via logs
2. [x] 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-04-07: REF-03 (SP format ✓), REF-04 (RP format ✓)
- Bugs found and fixed (2026-03-25): 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
- Note: formula labels (IP+K, PA+TB x 2) from test spec are not rendered; format is value/threshold (pct%) only
- REF-06 (fully evolved) not testable — no T4 cards exist in test data
3. [x] Execute REF-10 through REF-19 (filters)
- Tested 2026-03-25: REF-10 (card_type=batter ✓ after fix)
- Tested 2026-04-07: REF-11 (sp ✓), REF-12 (rp ✓), REF-13 (tier=0 ✓), REF-14 (tier=1 ✓), REF-15 (tier=4 empty ✓), REF-16 (progress=close ✓), REF-17 (batter+T1 combined ✓), REF-18 (T4+close empty ✓)
- 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)
- REF-19 (season filter): N/A — season param not implemented in the slash command
4. [x] 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)
- Tested 2026-04-07: REF-21 (page 2 ✓), REF-22 (page=999 clamps to last page ✓ — fixed in discord#141/#142), REF-23 (page 0 clamps to 1 ✓), Prev/Next buttons (✓)
5. [x] Execute REF-30 through REF-34 (edge cases)
- Tested 2026-04-07: REF-34 (page=-5 clamps to 1 ✓)
- REF-30 (no team), REF-31 (no refractor data), REF-32 (invalid card_type), REF-33 (negative tier): not tested — require alt account or manual API state manipulation
6. [N/A] Execute REF-40 through REF-45 (tier badges on card embeds)
- **Design gap**: `get_card_embeds()` looks up refractor state via `card['id']`, but all user-facing commands (`/player`, `/buy`) use `get_blank_team_card()` which has no `id` field. The `except Exception: pass` silently swallows the KeyError. Badges never appear outside `/refractor status`. `/open-packs` uses real card objects but results are random. No command currently surfaces badges on card embeds in practice.
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)
9. [N/A] Execute REF-70 through REF-72 (cross-command badge propagation)
- REF-70: `/team` shows team overview, not card embeds — badges not applicable
- REF-71: `/show-card defense` only works during active games — expected no badge (by design)
- REF-72: `/scout-tokens` shows token count, not card embeds — badges not applicable
10. [x] 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
- Tested 2026-04-07: REF-81 (no stats → 404 ✓), REF-82 (nonexistent card → 404 ✓)
### Approximate Time Estimates
- API health checks + list endpoint (REF-API-01 through REF-API-10): 2-3 minutes

View File

@ -415,47 +415,51 @@ def mock_interaction():
class TestTierNamesDivergenceCheck:
"""
T1-6: Assert that TIER_NAMES in cogs.refractor and helpers.refractor_notifs
are identical (same keys, same values).
T1-6: Assert that TIER_NAMES in all three consumers (cogs.refractor,
helpers.refractor_notifs, cogs.players) is identical.
Why: TIER_NAMES is duplicated in two modules. If one is updated and the
other is not (e.g. a tier is renamed or a new tier is added), tier labels
in the /refractor status embed and the tier-up notification embed will
diverge silently. This test acts as a divergence tripwire it will fail
the moment the two copies fall out of sync, forcing an explicit fix.
All three consumers now import from helpers.refractor_constants, so this
test acts as a tripwire against accidental re-localization of the constant.
If any consumer re-declares a local copy that diverges, these tests will
catch it.
"""
def test_tier_names_are_identical_across_modules(self):
"""
Import TIER_NAMES from both modules and assert deep equality.
Import TIER_NAMES from all three consumers and assert deep equality.
The test imports the name at call-time rather than at module level to
ensure it always reads the current definition and is not affected by
module-level caching or monkeypatching in other tests.
The test imports at call-time rather than module level to ensure it
always reads the current definition and is not affected by caching or
monkeypatching in other tests.
"""
from cogs.refractor import TIER_NAMES as cog_tier_names
from helpers.refractor_notifs import TIER_NAMES as notifs_tier_names
from helpers.refractor_constants import TIER_NAMES as constants_tier_names
assert cog_tier_names == notifs_tier_names, (
"TIER_NAMES differs between cogs.refractor and helpers.refractor_notifs. "
"Both copies must be kept in sync. "
assert cog_tier_names == notifs_tier_names == constants_tier_names, (
"TIER_NAMES differs across consumers. "
f"cogs.refractor: {cog_tier_names!r} "
f"helpers.refractor_notifs: {notifs_tier_names!r}"
f"helpers.refractor_notifs: {notifs_tier_names!r} "
f"helpers.refractor_constants: {constants_tier_names!r}"
)
def test_tier_names_have_same_keys(self):
"""Keys (tier numbers) must be identical in both modules."""
"""Keys (tier numbers) must be identical in all consumers."""
from cogs.refractor import TIER_NAMES as cog_tier_names
from helpers.refractor_notifs import TIER_NAMES as notifs_tier_names
from cogs.players import REFRACTOR_TIER_NAMES
assert set(cog_tier_names.keys()) == set(notifs_tier_names.keys()), (
"TIER_NAMES key sets differ between modules."
)
assert (
set(cog_tier_names.keys())
== set(notifs_tier_names.keys())
== set(REFRACTOR_TIER_NAMES.keys())
), "TIER_NAMES key sets differ between consumers."
def test_tier_names_have_same_values(self):
"""Display strings (values) must be identical for every shared key."""
from cogs.refractor import TIER_NAMES as cog_tier_names
from helpers.refractor_notifs import TIER_NAMES as notifs_tier_names
from cogs.players import REFRACTOR_TIER_NAMES
for tier, name in cog_tier_names.items():
assert notifs_tier_names.get(tier) == name, (
@ -463,6 +467,11 @@ class TestTierNamesDivergenceCheck:
f"cogs.refractor={name!r}, "
f"helpers.refractor_notifs={notifs_tier_names.get(tier)!r}"
)
assert REFRACTOR_TIER_NAMES.get(tier) == name, (
f"Tier {tier} name mismatch: "
f"cogs.refractor={name!r}, "
f"cogs.players.REFRACTOR_TIER_NAMES={REFRACTOR_TIER_NAMES.get(tier)!r}"
)
# ---------------------------------------------------------------------------