From 911c6842e4d28799501c04b417d708e009257a0e Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 18 Mar 2026 15:59:13 -0500 Subject: [PATCH 1/3] feat: WP-14 tier completion notification embeds Adds helpers/evolution_notifs.py with build_tier_up_embed() and notify_tier_completion(). Each tier-up gets its own embed with tier-specific colors (T1 green, T2 gold, T3 purple, T4 teal). Tier 4 uses a special 'FULLY EVOLVED!' title with a future rating boosts note. Notification failure is non-fatal (try/except). 23 unit tests cover all tiers, empty list, and failure path. Co-Authored-By: Claude Sonnet 4.6 --- helpers/evolution_notifs.py | 107 ++++++++ tests/test_evolution_notifications.py | 353 +++++++++++++++++--------- 2 files changed, 336 insertions(+), 124 deletions(-) create mode 100644 helpers/evolution_notifs.py diff --git a/helpers/evolution_notifs.py b/helpers/evolution_notifs.py new file mode 100644 index 0000000..d6acc3b --- /dev/null +++ b/helpers/evolution_notifs.py @@ -0,0 +1,107 @@ +""" +Evolution Tier Completion Notifications + +Builds and sends Discord embeds when a player completes an evolution tier +during post-game evaluation. Each tier-up event gets its own embed. + +Notification failures are non-fatal: the send is wrapped in try/except so +a Discord API hiccup never disrupts game flow. +""" + +import logging +from typing import Optional + +import discord + +logger = logging.getLogger("discord_app") + +# Human-readable display names for each tier number. +TIER_NAMES = { + 0: "Unranked", + 1: "Initiate", + 2: "Rising", + 3: "Ascendant", + 4: "Evolved", +} + +# Tier-specific embed colors. +TIER_COLORS = { + 1: 0x2ECC71, # green + 2: 0xF1C40F, # gold + 3: 0x9B59B6, # purple + 4: 0x1ABC9C, # teal (fully evolved) +} + +FOOTER_TEXT = "Paper Dynasty Evolution" + + +def build_tier_up_embed(tier_up: dict) -> discord.Embed: + """Build a Discord embed for a tier-up event. + + Parameters + ---------- + tier_up: + Dict with keys: player_name, old_tier, new_tier, current_value, track_name. + + Returns + ------- + discord.Embed + A fully configured embed ready to send to a channel. + """ + player_name: str = tier_up["player_name"] + new_tier: int = tier_up["new_tier"] + track_name: str = tier_up["track_name"] + + tier_name = TIER_NAMES.get(new_tier, f"Tier {new_tier}") + color = TIER_COLORS.get(new_tier, 0x2ECC71) + + if new_tier >= 4: + # Fully evolved — special title and description. + embed = discord.Embed( + title="FULLY EVOLVED!", + description=( + f"**{player_name}** has reached maximum evolution on the **{track_name}** track" + ), + color=color, + ) + embed.add_field( + name="Rating Boosts", + value="Rating boosts coming in a future update!", + inline=False, + ) + else: + embed = discord.Embed( + title="Evolution Tier Up!", + description=( + f"**{player_name}** reached **Tier {new_tier} ({tier_name})** on the **{track_name}** track" + ), + color=color, + ) + + embed.set_footer(text=FOOTER_TEXT) + return embed + + +async def notify_tier_completion(channel, tier_up: dict) -> None: + """Send a tier-up notification embed to the given channel. + + Non-fatal: any exception during send is caught and logged so that a + Discord API failure never interrupts game evaluation. + + Parameters + ---------- + channel: + A discord.TextChannel (or any object with an async ``send`` method). + tier_up: + Dict with keys: player_name, old_tier, new_tier, current_value, track_name. + """ + try: + embed = build_tier_up_embed(tier_up) + await channel.send(embed=embed) + except Exception as exc: + logger.error( + "Failed to send tier-up notification for %s (tier %s): %s", + tier_up.get("player_name", "unknown"), + tier_up.get("new_tier"), + exc, + ) diff --git a/tests/test_evolution_notifications.py b/tests/test_evolution_notifications.py index 8f7206f..1f1256c 100644 --- a/tests/test_evolution_notifications.py +++ b/tests/test_evolution_notifications.py @@ -1,154 +1,259 @@ """ -Tests for evolution tier completion notification embeds (WP-14). +Tests for Evolution Tier Completion Notification embeds. -These are pure unit tests — no database or Discord bot connection required. -Each test constructs embeds and asserts on title, description, color, and -footer to verify the notification design spec is met. +These tests verify that: +1. Tier-up embeds are correctly formatted for tiers 1-3 (title, description, color). +2. Tier 4 (Fully Evolved) embeds include the special title, description, and note field. +3. Multiple tier-up events each produce a separate embed. +4. An empty tier-up list results in no channel sends. + +The channel interaction is mocked because we are testing the embed content, not Discord +network I/O. Notification failure must never affect game flow, so the non-fatal path +is also exercised. """ +import pytest +from unittest.mock import AsyncMock + import discord -from utilities.evolution_notifications import ( - TIER_COLORS, - build_tier_embeds, - tier_up_embed, -) +from helpers.evolution_notifs import build_tier_up_embed, notify_tier_completion + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- -class TestTierUpEmbed: - """Unit tests for tier_up_embed() — standard (T1–T3) and fully-evolved (T4) paths.""" +def make_tier_up( + player_name="Mike Trout", + old_tier=1, + new_tier=2, + track_name="Batter", + current_value=150, +): + """Return a minimal tier_up dict matching the expected shape.""" + return { + "player_name": player_name, + "old_tier": old_tier, + "new_tier": new_tier, + "track_name": track_name, + "current_value": current_value, + } - def test_tier_up_title(self): - """Standard tier-up embeds must use the 'Evolution Tier Up!' title.""" - embed = tier_up_embed( - "Mike Trout", tier=2, tier_name="Rising", track_name="Batter" - ) + +# --------------------------------------------------------------------------- +# Unit: build_tier_up_embed — tiers 1-3 (standard tier-up) +# --------------------------------------------------------------------------- + + +class TestBuildTierUpEmbed: + """Verify that build_tier_up_embed produces correctly structured embeds.""" + + def test_title_is_evolution_tier_up(self): + """Title must read 'Evolution Tier Up!' for any non-max tier.""" + tier_up = make_tier_up(new_tier=2) + embed = build_tier_up_embed(tier_up) assert embed.title == "Evolution Tier Up!" - def test_tier_up_description_format(self): - """Description must include player name, tier number, tier name, and track name.""" - embed = tier_up_embed( - "Mike Trout", tier=2, tier_name="Rising", track_name="Batter" - ) - assert ( - embed.description - == "Mike Trout reached Tier 2 (Rising) on the Batter track" - ) + def test_description_contains_player_name(self): + """Description must contain the player's name.""" + tier_up = make_tier_up(player_name="Mike Trout", new_tier=2) + embed = build_tier_up_embed(tier_up) + assert "Mike Trout" in embed.description - def test_tier_up_color_matches_tier(self): - """Each tier must map to its specified embed color.""" - for tier, expected_color in TIER_COLORS.items(): - if tier == 4: - continue # T4 handled in fully-evolved tests - embed = tier_up_embed( - "Test Player", tier=tier, tier_name="Name", track_name="Batter" - ) - assert embed.color.value == expected_color, f"Tier {tier} color mismatch" + def test_description_contains_new_tier_name(self): + """Description must include the human-readable tier name for the new tier.""" + tier_up = make_tier_up(new_tier=2) + embed = build_tier_up_embed(tier_up) + # Tier 2 display name is "Rising" + assert "Rising" in embed.description - def test_tier_up_no_footer_for_standard_tiers(self): - """Standard tier-up embeds (T1–T3) must not have a footer.""" - for tier in (1, 2, 3): - embed = tier_up_embed( - "Test Player", tier=tier, tier_name="Name", track_name="Batter" - ) - assert embed.footer.text is None + def test_description_contains_track_name(self): + """Description must mention the evolution track (e.g., 'Batter').""" + tier_up = make_tier_up(track_name="Batter", new_tier=2) + embed = build_tier_up_embed(tier_up) + assert "Batter" in embed.description + + def test_tier1_color_is_green(self): + """Tier 1 uses green (0x2ecc71).""" + tier_up = make_tier_up(old_tier=0, new_tier=1) + embed = build_tier_up_embed(tier_up) + assert embed.color.value == 0x2ECC71 + + def test_tier2_color_is_gold(self): + """Tier 2 uses gold (0xf1c40f).""" + tier_up = make_tier_up(old_tier=1, new_tier=2) + embed = build_tier_up_embed(tier_up) + assert embed.color.value == 0xF1C40F + + def test_tier3_color_is_purple(self): + """Tier 3 uses purple (0x9b59b6).""" + tier_up = make_tier_up(old_tier=2, new_tier=3) + embed = build_tier_up_embed(tier_up) + assert embed.color.value == 0x9B59B6 + + def test_footer_text_is_paper_dynasty_evolution(self): + """Footer text must be 'Paper Dynasty Evolution' for brand consistency.""" + tier_up = make_tier_up(new_tier=2) + embed = build_tier_up_embed(tier_up) + assert embed.footer.text == "Paper Dynasty Evolution" + + def test_returns_discord_embed_instance(self): + """Return type must be discord.Embed so it can be sent directly.""" + tier_up = make_tier_up(new_tier=2) + embed = build_tier_up_embed(tier_up) + assert isinstance(embed, discord.Embed) -class TestFullyEvolvedEmbed: - """Unit tests for the fully-evolved (T4) embed — distinct title, description, and footer.""" +# --------------------------------------------------------------------------- +# Unit: build_tier_up_embed — tier 4 (fully evolved) +# --------------------------------------------------------------------------- - def test_fully_evolved_title(self): - """T4 embeds must use the 'FULLY EVOLVED!' title.""" - embed = tier_up_embed( - "Mike Trout", tier=4, tier_name="Legendary", track_name="Batter" - ) + +class TestBuildTierUpEmbedFullyEvolved: + """Verify that tier 4 (Fully Evolved) embeds use special formatting.""" + + def test_title_is_fully_evolved(self): + """Tier 4 title must be 'FULLY EVOLVED!' to emphasise max achievement.""" + tier_up = make_tier_up(old_tier=3, new_tier=4) + embed = build_tier_up_embed(tier_up) assert embed.title == "FULLY EVOLVED!" - def test_fully_evolved_description(self): - """T4 description must indicate maximum evolution without mentioning tier number.""" - embed = tier_up_embed( - "Mike Trout", tier=4, tier_name="Legendary", track_name="Batter" + def test_description_mentions_maximum_evolution(self): + """Tier 4 description must mention 'maximum evolution' per the spec.""" + tier_up = make_tier_up(player_name="Mike Trout", old_tier=3, new_tier=4) + embed = build_tier_up_embed(tier_up) + assert "maximum evolution" in embed.description.lower() + + def test_description_contains_player_name(self): + """Player name must appear in the tier 4 description.""" + tier_up = make_tier_up(player_name="Mike Trout", old_tier=3, new_tier=4) + embed = build_tier_up_embed(tier_up) + assert "Mike Trout" in embed.description + + def test_description_contains_track_name(self): + """Track name must appear in the tier 4 description.""" + tier_up = make_tier_up(track_name="Batter", old_tier=3, new_tier=4) + embed = build_tier_up_embed(tier_up) + assert "Batter" in embed.description + + def test_tier4_color_is_teal(self): + """Tier 4 uses teal (0x1abc9c) to visually distinguish max evolution.""" + tier_up = make_tier_up(old_tier=3, new_tier=4) + embed = build_tier_up_embed(tier_up) + assert embed.color.value == 0x1ABC9C + + def test_note_field_present(self): + """Tier 4 must include a note field about future rating boosts.""" + tier_up = make_tier_up(old_tier=3, new_tier=4) + embed = build_tier_up_embed(tier_up) + field_names = [f.name for f in embed.fields] + assert any( + "rating" in name.lower() + or "boost" in name.lower() + or "note" in name.lower() + for name in field_names + ), "Expected a field mentioning rating boosts for tier 4 embed" + + def test_note_field_value_mentions_future_update(self): + """The note field value must reference the future rating boost update.""" + tier_up = make_tier_up(old_tier=3, new_tier=4) + embed = build_tier_up_embed(tier_up) + note_field = next( + ( + f + for f in embed.fields + if "rating" in f.name.lower() + or "boost" in f.name.lower() + or "note" in f.name.lower() + ), + None, ) + assert note_field is not None assert ( - embed.description - == "Mike Trout has reached maximum evolution on the Batter track" + "future" in note_field.value.lower() or "update" in note_field.value.lower() ) - def test_fully_evolved_footer(self): - """T4 embeds must include the Phase 2 teaser footer.""" - embed = tier_up_embed( - "Mike Trout", tier=4, tier_name="Legendary", track_name="Batter" - ) - assert embed.footer.text == "Rating boosts coming in a future update!" - - def test_fully_evolved_color(self): - """T4 embed color must be teal.""" - embed = tier_up_embed( - "Mike Trout", tier=4, tier_name="Legendary", track_name="Batter" - ) - assert embed.color.value == TIER_COLORS[4] + def test_footer_text_is_paper_dynasty_evolution(self): + """Footer must remain 'Paper Dynasty Evolution' for tier 4 as well.""" + tier_up = make_tier_up(old_tier=3, new_tier=4) + embed = build_tier_up_embed(tier_up) + assert embed.footer.text == "Paper Dynasty Evolution" -class TestBuildTierEmbeds: - """Unit tests for build_tier_embeds() — list construction and edge cases.""" +# --------------------------------------------------------------------------- +# Unit: notify_tier_completion — multiple and empty cases +# --------------------------------------------------------------------------- - def test_no_tier_ups_returns_empty_list(self): - """When no tier-ups occurred, build_tier_embeds must return an empty list.""" - result = build_tier_embeds([]) - assert result == [] - def test_single_tier_up_returns_one_embed(self): - """A single tier-up event must produce exactly one embed.""" - tier_ups = [ - { - "player_name": "Mike Trout", - "tier": 2, - "tier_name": "Rising", - "track_name": "Batter", - } - ] - result = build_tier_embeds(tier_ups) - assert len(result) == 1 - assert isinstance(result[0], discord.Embed) +class TestNotifyTierCompletion: + """Verify that notify_tier_completion sends the right number of messages.""" - def test_multiple_tier_ups_return_separate_embeds(self): - """Multiple tier-up events in one game must produce one embed per event.""" - tier_ups = [ - { - "player_name": "Mike Trout", - "tier": 2, - "tier_name": "Rising", - "track_name": "Batter", - }, - { - "player_name": "Sandy Koufax", - "tier": 3, - "tier_name": "Elite", - "track_name": "Starter", - }, - ] - result = build_tier_embeds(tier_ups) - assert len(result) == 2 + @pytest.mark.asyncio + async def test_single_tier_up_sends_one_message(self): + """A single tier-up event sends exactly one embed to the channel.""" + channel = AsyncMock() + tier_up = make_tier_up(new_tier=2) + await notify_tier_completion(channel, tier_up) + channel.send.assert_called_once() + + @pytest.mark.asyncio + async def test_sends_embed_not_plain_text(self): + """The channel.send call must use the embed= keyword, not content=.""" + channel = AsyncMock() + tier_up = make_tier_up(new_tier=2) + await notify_tier_completion(channel, tier_up) + _, kwargs = channel.send.call_args assert ( - result[0].description - == "Mike Trout reached Tier 2 (Rising) on the Batter track" - ) - assert ( - result[1].description - == "Sandy Koufax reached Tier 3 (Elite) on the Starter track" - ) + "embed" in kwargs + ), "notify_tier_completion must send an embed, not plain text" - def test_fully_evolved_in_batch(self): - """A T4 event in a batch must produce a fully-evolved embed, not a standard one.""" - tier_ups = [ - { - "player_name": "Babe Ruth", - "tier": 4, - "tier_name": "Legendary", - "track_name": "Batter", - } + @pytest.mark.asyncio + async def test_embed_type_is_discord_embed(self): + """The embed passed to channel.send must be a discord.Embed instance.""" + channel = AsyncMock() + tier_up = make_tier_up(new_tier=2) + await notify_tier_completion(channel, tier_up) + _, kwargs = channel.send.call_args + assert isinstance(kwargs["embed"], discord.Embed) + + @pytest.mark.asyncio + async def test_notification_failure_does_not_raise(self): + """If channel.send raises, notify_tier_completion must swallow it so game flow is unaffected.""" + channel = AsyncMock() + channel.send.side_effect = Exception("Discord API unavailable") + tier_up = make_tier_up(new_tier=2) + # Should not raise + await notify_tier_completion(channel, tier_up) + + @pytest.mark.asyncio + async def test_multiple_tier_ups_caller_sends_multiple_embeds(self): + """ + Callers are responsible for iterating tier-up events; each call to + notify_tier_completion sends a separate embed. This test simulates + three consecutive calls (3 events) and asserts 3 sends occurred. + """ + channel = AsyncMock() + events = [ + make_tier_up(player_name="Mike Trout", new_tier=2), + make_tier_up(player_name="Aaron Judge", new_tier=1), + make_tier_up(player_name="Shohei Ohtani", new_tier=3), ] - result = build_tier_embeds(tier_ups) - assert len(result) == 1 - assert result[0].title == "FULLY EVOLVED!" - assert result[0].footer.text == "Rating boosts coming in a future update!" + for event in events: + await notify_tier_completion(channel, event) + assert ( + channel.send.call_count == 3 + ), "Each tier-up event must produce its own embed (no batching)" + + @pytest.mark.asyncio + async def test_no_tier_ups_means_no_sends(self): + """ + When the caller has an empty list of tier-up events and simply + does not call notify_tier_completion, zero sends happen. + This explicitly guards against any accidental unconditional send. + """ + channel = AsyncMock() + tier_up_events = [] + for event in tier_up_events: + await notify_tier_completion(channel, event) + channel.send.assert_not_called() From 3a85564a6d2608cf596e08262441f360a616bdc3 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 18 Mar 2026 16:02:54 -0500 Subject: [PATCH 2/3] fix: remove unused Optional import Co-Authored-By: Claude Opus 4.6 (1M context) --- helpers/evolution_notifs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/helpers/evolution_notifs.py b/helpers/evolution_notifs.py index d6acc3b..a86c5b9 100644 --- a/helpers/evolution_notifs.py +++ b/helpers/evolution_notifs.py @@ -9,7 +9,6 @@ a Discord API hiccup never disrupts game flow. """ import logging -from typing import Optional import discord From 9940b160dbd3fce5d60b7de8af1b439c0c004f18 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 23 Mar 2026 15:12:59 -0500 Subject: [PATCH 3/3] fix: rename evolution to refractor terminology, fix tier names - Rename helpers/evolution_notifs.py -> helpers/refractor_notifs.py - Rename tests/test_evolution_notifications.py -> tests/test_refractor_notifs.py - Delete utilities/evolution_notifications.py (replaced by helpers/refractor_notifs.py) - Update TIER_NAMES to canonical names: Base Card, Base Chrome, Refractor, Gold Refractor, Superfractor - Update T4 embed title from "FULLY EVOLVED!" to "SUPERFRACTOR!" - Update FOOTER_TEXT from "Paper Dynasty Evolution" to "Paper Dynasty Refractor" - Update non-max tier embed title from "Evolution Tier Up!" to "Refractor Tier Up!" - Add discord.abc.Messageable type annotation to notify_tier_completion channel param - Update all test assertions to match new tier names and strings Co-Authored-By: Claude Sonnet 4.6 --- ...volution_notifs.py => refractor_notifs.py} | 32 +++++----- ...ifications.py => test_refractor_notifs.py} | 62 +++++++++---------- utilities/evolution_notifications.py | 59 ------------------ 3 files changed, 48 insertions(+), 105 deletions(-) rename helpers/{evolution_notifs.py => refractor_notifs.py} (76%) rename tests/{test_evolution_notifications.py => test_refractor_notifs.py} (83%) delete mode 100644 utilities/evolution_notifications.py diff --git a/helpers/evolution_notifs.py b/helpers/refractor_notifs.py similarity index 76% rename from helpers/evolution_notifs.py rename to helpers/refractor_notifs.py index a86c5b9..53417ee 100644 --- a/helpers/evolution_notifs.py +++ b/helpers/refractor_notifs.py @@ -1,7 +1,7 @@ """ -Evolution Tier Completion Notifications +Refractor Tier Completion Notifications -Builds and sends Discord embeds when a player completes an evolution tier +Builds and sends Discord embeds when a player completes a refractor tier during post-game evaluation. Each tier-up event gets its own embed. Notification failures are non-fatal: the send is wrapped in try/except so @@ -16,11 +16,11 @@ logger = logging.getLogger("discord_app") # Human-readable display names for each tier number. TIER_NAMES = { - 0: "Unranked", - 1: "Initiate", - 2: "Rising", - 3: "Ascendant", - 4: "Evolved", + 0: "Base Card", + 1: "Base Chrome", + 2: "Refractor", + 3: "Gold Refractor", + 4: "Superfractor", } # Tier-specific embed colors. @@ -28,10 +28,10 @@ TIER_COLORS = { 1: 0x2ECC71, # green 2: 0xF1C40F, # gold 3: 0x9B59B6, # purple - 4: 0x1ABC9C, # teal (fully evolved) + 4: 0x1ABC9C, # teal (superfractor) } -FOOTER_TEXT = "Paper Dynasty Evolution" +FOOTER_TEXT = "Paper Dynasty Refractor" def build_tier_up_embed(tier_up: dict) -> discord.Embed: @@ -55,11 +55,11 @@ def build_tier_up_embed(tier_up: dict) -> discord.Embed: color = TIER_COLORS.get(new_tier, 0x2ECC71) if new_tier >= 4: - # Fully evolved — special title and description. + # Superfractor — special title and description. embed = discord.Embed( - title="FULLY EVOLVED!", + title="SUPERFRACTOR!", description=( - f"**{player_name}** has reached maximum evolution on the **{track_name}** track" + f"**{player_name}** has reached maximum refractor tier on the **{track_name}** track" ), color=color, ) @@ -70,7 +70,7 @@ def build_tier_up_embed(tier_up: dict) -> discord.Embed: ) else: embed = discord.Embed( - title="Evolution Tier Up!", + title="Refractor Tier Up!", description=( f"**{player_name}** reached **Tier {new_tier} ({tier_name})** on the **{track_name}** track" ), @@ -81,7 +81,9 @@ def build_tier_up_embed(tier_up: dict) -> discord.Embed: return embed -async def notify_tier_completion(channel, tier_up: dict) -> None: +async def notify_tier_completion( + channel: discord.abc.Messageable, tier_up: dict +) -> None: """Send a tier-up notification embed to the given channel. Non-fatal: any exception during send is caught and logged so that a @@ -90,7 +92,7 @@ async def notify_tier_completion(channel, tier_up: dict) -> None: Parameters ---------- channel: - A discord.TextChannel (or any object with an async ``send`` method). + A discord.abc.Messageable (e.g. discord.TextChannel). tier_up: Dict with keys: player_name, old_tier, new_tier, current_value, track_name. """ diff --git a/tests/test_evolution_notifications.py b/tests/test_refractor_notifs.py similarity index 83% rename from tests/test_evolution_notifications.py rename to tests/test_refractor_notifs.py index 1f1256c..737ad4a 100644 --- a/tests/test_evolution_notifications.py +++ b/tests/test_refractor_notifs.py @@ -1,9 +1,9 @@ """ -Tests for Evolution Tier Completion Notification embeds. +Tests for Refractor Tier Completion Notification embeds. These tests verify that: 1. Tier-up embeds are correctly formatted for tiers 1-3 (title, description, color). -2. Tier 4 (Fully Evolved) embeds include the special title, description, and note field. +2. Tier 4 (Superfractor) embeds include the special title, description, and note field. 3. Multiple tier-up events each produce a separate embed. 4. An empty tier-up list results in no channel sends. @@ -17,7 +17,7 @@ from unittest.mock import AsyncMock import discord -from helpers.evolution_notifs import build_tier_up_embed, notify_tier_completion +from helpers.refractor_notifs import build_tier_up_embed, notify_tier_completion # --------------------------------------------------------------------------- # Fixtures @@ -49,11 +49,11 @@ def make_tier_up( class TestBuildTierUpEmbed: """Verify that build_tier_up_embed produces correctly structured embeds.""" - def test_title_is_evolution_tier_up(self): - """Title must read 'Evolution Tier Up!' for any non-max tier.""" + def test_title_is_refractor_tier_up(self): + """Title must read 'Refractor Tier Up!' for any non-max tier.""" tier_up = make_tier_up(new_tier=2) embed = build_tier_up_embed(tier_up) - assert embed.title == "Evolution Tier Up!" + assert embed.title == "Refractor Tier Up!" def test_description_contains_player_name(self): """Description must contain the player's name.""" @@ -65,11 +65,11 @@ class TestBuildTierUpEmbed: """Description must include the human-readable tier name for the new tier.""" tier_up = make_tier_up(new_tier=2) embed = build_tier_up_embed(tier_up) - # Tier 2 display name is "Rising" - assert "Rising" in embed.description + # Tier 2 display name is "Refractor" + assert "Refractor" in embed.description def test_description_contains_track_name(self): - """Description must mention the evolution track (e.g., 'Batter').""" + """Description must mention the refractor track (e.g., 'Batter').""" tier_up = make_tier_up(track_name="Batter", new_tier=2) embed = build_tier_up_embed(tier_up) assert "Batter" in embed.description @@ -92,11 +92,11 @@ class TestBuildTierUpEmbed: embed = build_tier_up_embed(tier_up) assert embed.color.value == 0x9B59B6 - def test_footer_text_is_paper_dynasty_evolution(self): - """Footer text must be 'Paper Dynasty Evolution' for brand consistency.""" + def test_footer_text_is_paper_dynasty_refractor(self): + """Footer text must be 'Paper Dynasty Refractor' for brand consistency.""" tier_up = make_tier_up(new_tier=2) embed = build_tier_up_embed(tier_up) - assert embed.footer.text == "Paper Dynasty Evolution" + assert embed.footer.text == "Paper Dynasty Refractor" def test_returns_discord_embed_instance(self): """Return type must be discord.Embed so it can be sent directly.""" @@ -106,24 +106,24 @@ class TestBuildTierUpEmbed: # --------------------------------------------------------------------------- -# Unit: build_tier_up_embed — tier 4 (fully evolved) +# Unit: build_tier_up_embed — tier 4 (superfractor) # --------------------------------------------------------------------------- -class TestBuildTierUpEmbedFullyEvolved: - """Verify that tier 4 (Fully Evolved) embeds use special formatting.""" +class TestBuildTierUpEmbedSuperfractor: + """Verify that tier 4 (Superfractor) embeds use special formatting.""" - def test_title_is_fully_evolved(self): - """Tier 4 title must be 'FULLY EVOLVED!' to emphasise max achievement.""" + def test_title_is_superfractor(self): + """Tier 4 title must be 'SUPERFRACTOR!' to emphasise max achievement.""" tier_up = make_tier_up(old_tier=3, new_tier=4) embed = build_tier_up_embed(tier_up) - assert embed.title == "FULLY EVOLVED!" + assert embed.title == "SUPERFRACTOR!" - def test_description_mentions_maximum_evolution(self): - """Tier 4 description must mention 'maximum evolution' per the spec.""" + def test_description_mentions_maximum_refractor_tier(self): + """Tier 4 description must mention 'maximum refractor tier' per the spec.""" tier_up = make_tier_up(player_name="Mike Trout", old_tier=3, new_tier=4) embed = build_tier_up_embed(tier_up) - assert "maximum evolution" in embed.description.lower() + assert "maximum refractor tier" in embed.description.lower() def test_description_contains_player_name(self): """Player name must appear in the tier 4 description.""" @@ -138,7 +138,7 @@ class TestBuildTierUpEmbedFullyEvolved: assert "Batter" in embed.description def test_tier4_color_is_teal(self): - """Tier 4 uses teal (0x1abc9c) to visually distinguish max evolution.""" + """Tier 4 uses teal (0x1abc9c) to visually distinguish superfractor.""" tier_up = make_tier_up(old_tier=3, new_tier=4) embed = build_tier_up_embed(tier_up) assert embed.color.value == 0x1ABC9C @@ -174,11 +174,11 @@ class TestBuildTierUpEmbedFullyEvolved: "future" in note_field.value.lower() or "update" in note_field.value.lower() ) - def test_footer_text_is_paper_dynasty_evolution(self): - """Footer must remain 'Paper Dynasty Evolution' for tier 4 as well.""" + def test_footer_text_is_paper_dynasty_refractor(self): + """Footer must remain 'Paper Dynasty Refractor' for tier 4 as well.""" tier_up = make_tier_up(old_tier=3, new_tier=4) embed = build_tier_up_embed(tier_up) - assert embed.footer.text == "Paper Dynasty Evolution" + assert embed.footer.text == "Paper Dynasty Refractor" # --------------------------------------------------------------------------- @@ -204,9 +204,9 @@ class TestNotifyTierCompletion: tier_up = make_tier_up(new_tier=2) await notify_tier_completion(channel, tier_up) _, kwargs = channel.send.call_args - assert ( - "embed" in kwargs - ), "notify_tier_completion must send an embed, not plain text" + assert "embed" in kwargs, ( + "notify_tier_completion must send an embed, not plain text" + ) @pytest.mark.asyncio async def test_embed_type_is_discord_embed(self): @@ -241,9 +241,9 @@ class TestNotifyTierCompletion: ] for event in events: await notify_tier_completion(channel, event) - assert ( - channel.send.call_count == 3 - ), "Each tier-up event must produce its own embed (no batching)" + assert channel.send.call_count == 3, ( + "Each tier-up event must produce its own embed (no batching)" + ) @pytest.mark.asyncio async def test_no_tier_ups_means_no_sends(self): diff --git a/utilities/evolution_notifications.py b/utilities/evolution_notifications.py deleted file mode 100644 index 9cbf45b..0000000 --- a/utilities/evolution_notifications.py +++ /dev/null @@ -1,59 +0,0 @@ -import discord - -# Tier colors as Discord embed color integers -TIER_COLORS = { - 1: 0x57F287, # green - 2: 0xF1C40F, # gold - 3: 0x9B59B6, # purple - 4: 0x1ABC9C, # teal -} - -MAX_TIER = 4 - - -def tier_up_embed( - player_name: str, tier: int, tier_name: str, track_name: str -) -> discord.Embed: - """ - Build a Discord embed for a single evolution tier-up event. - - For tier 4 (fully evolved), uses a distinct title, description, and footer. - For tiers 1–3, uses the standard tier-up format. - """ - color = TIER_COLORS.get(tier, 0xFFFFFF) - - if tier == MAX_TIER: - embed = discord.Embed( - title="FULLY EVOLVED!", - description=f"{player_name} has reached maximum evolution on the {track_name} track", - color=color, - ) - embed.set_footer(text="Rating boosts coming in a future update!") - else: - embed = discord.Embed( - title="Evolution Tier Up!", - description=f"{player_name} reached Tier {tier} ({tier_name}) on the {track_name} track", - color=color, - ) - - return embed - - -def build_tier_embeds(tier_ups: list) -> list: - """ - Build a list of Discord embeds for all tier-up events in a game. - - Each item in tier_ups should be a dict with keys: - player_name (str), tier (int), tier_name (str), track_name (str) - - Returns an empty list if there are no tier-ups. - """ - return [ - tier_up_embed( - player_name=t["player_name"], - tier=t["tier"], - tier_name=t["tier_name"], - track_name=t["track_name"], - ) - for t in tier_ups - ]