From 208efd11a63648a4ca428ec8a2e3942b5231efc4 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 13 Mar 2026 17:34:14 -0500 Subject: [PATCH] feat: tier completion notification embeds (WP-14) (#79) Closes paper-dynasty-database#79 Co-Authored-By: Claude Sonnet 4.6 --- tests/test_evolution_notifications.py | 154 ++++++++++++++++++++++++++ utilities/evolution_notifications.py | 59 ++++++++++ 2 files changed, 213 insertions(+) create mode 100644 tests/test_evolution_notifications.py create mode 100644 utilities/evolution_notifications.py diff --git a/tests/test_evolution_notifications.py b/tests/test_evolution_notifications.py new file mode 100644 index 0000000..8f7206f --- /dev/null +++ b/tests/test_evolution_notifications.py @@ -0,0 +1,154 @@ +""" +Tests for evolution tier completion notification embeds (WP-14). + +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. +""" + +import discord + +from utilities.evolution_notifications import ( + TIER_COLORS, + build_tier_embeds, + tier_up_embed, +) + + +class TestTierUpEmbed: + """Unit tests for tier_up_embed() — standard (T1–T3) and fully-evolved (T4) paths.""" + + 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" + ) + 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_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_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 + + +class TestFullyEvolvedEmbed: + """Unit tests for the fully-evolved (T4) embed — distinct title, description, and footer.""" + + 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" + ) + 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" + ) + assert ( + embed.description + == "Mike Trout has reached maximum evolution on the Batter track" + ) + + 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] + + +class TestBuildTierEmbeds: + """Unit tests for build_tier_embeds() — list construction and edge 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) + + 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 + 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" + ) + + 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", + } + ] + 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!" diff --git a/utilities/evolution_notifications.py b/utilities/evolution_notifications.py new file mode 100644 index 0000000..9cbf45b --- /dev/null +++ b/utilities/evolution_notifications.py @@ -0,0 +1,59 @@ +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 + ] -- 2.25.1