diff --git a/command_logic/logic_gameplay.py b/command_logic/logic_gameplay.py index c60a8d0..d679cc4 100644 --- a/command_logic/logic_gameplay.py +++ b/command_logic/logic_gameplay.py @@ -4336,7 +4336,6 @@ async def complete_game( await roll_back(db_game["id"], plays=True, decisions=True) log_exception(e, msg="Unable to post decisions to API, rolling back") - # Post game rewards (gauntlet and main team) try: win_reward, loss_reward = await post_game_rewards( @@ -4360,25 +4359,25 @@ async def complete_game( await roll_back(db_game["id"], plays=True, decisions=True) log_exception(e, msg="Error while posting game rewards") - # Post-game evolution processing (non-blocking) - # WP-13: update season stats then evaluate evolution milestones for all + # Post-game refractor processing (non-blocking) + # WP-13: update season stats then evaluate refractor milestones for all # participating players. Wrapped in try/except so any failure here is - # non-fatal — the game is already saved and evolution will catch up on the + # non-fatal — the game is already saved and refractor will catch up on the # next evaluate call. try: await db_post(f"season-stats/update-game/{db_game['id']}") - evo_result = await db_post(f"evolution/evaluate-game/{db_game['id']}") + evo_result = await db_post(f"refractor/evaluate-game/{db_game['id']}") if evo_result and evo_result.get("tier_ups"): for tier_up in evo_result["tier_ups"]: # WP-14 will implement full Discord notification; stub for now logger.info( - f"Evolution tier-up for player {tier_up.get('player_id')}: " + f"Refractor tier-up for player {tier_up.get('player_id')}: " f"{tier_up.get('old_tier')} -> {tier_up.get('new_tier')} " f"(game {db_game['id']})" ) await notify_tier_completion(interaction.channel, tier_up) except Exception as e: - logger.warning(f"Post-game evolution processing failed (non-fatal): {e}") + logger.warning(f"Post-game refractor processing failed (non-fatal): {e}") session.delete(this_play) session.commit() diff --git a/helpers/evolution_notifs.py b/helpers/evolution_notifs.py deleted file mode 100644 index d6acc3b..0000000 --- a/helpers/evolution_notifs.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -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_complete_game_hook.py b/tests/test_complete_game_hook.py index 6b6f07f..b04b689 100644 --- a/tests/test_complete_game_hook.py +++ b/tests/test_complete_game_hook.py @@ -4,13 +4,13 @@ Tests for the WP-13 post-game callback integration hook. These tests verify that after a game is saved to the API, two additional POST requests are fired in the correct order: 1. POST season-stats/update-game/{game_id} — update player_season_stats - 2. POST evolution/evaluate-game/{game_id} — evaluate evolution milestones + 2. POST refractor/evaluate-game/{game_id} — evaluate refractor milestones Key design constraints being tested: - - Season stats MUST be updated before evolution is evaluated (ordering). - - Failure of either evolution call must NOT propagate — the game result has - already been committed; evolution will self-heal on the next evaluate pass. - - Tier-up dicts returned by the evolution endpoint are passed to + - Season stats MUST be updated before refractor is evaluated (ordering). + - Failure of either refractor call must NOT propagate — the game result has + already been committed; refractor will self-heal on the next evaluate pass. + - Tier-up dicts returned by the refractor endpoint are passed to notify_tier_completion so WP-14 can present them to the player. """ @@ -46,7 +46,7 @@ async def _run_hook(db_post_mock, db_game_id: int = 42): try: await db_post_mock(f"season-stats/update-game/{db_game['id']}") - evo_result = await db_post_mock(f"evolution/evaluate-game/{db_game['id']}") + evo_result = await db_post_mock(f"refractor/evaluate-game/{db_game['id']}") if evo_result and evo_result.get("tier_ups"): for tier_up in evo_result["tier_ups"]: await notify_tier_completion(channel, tier_up) @@ -64,10 +64,10 @@ async def _run_hook(db_post_mock, db_game_id: int = 42): @pytest.mark.asyncio async def test_hook_posts_to_both_endpoints_in_order(): """ - Both evolution endpoints are called, and season-stats comes first. + Both refractor endpoints are called, and season-stats comes first. The ordering is critical: player_season_stats must be populated before the - evolution engine tries to read them for milestone evaluation. + refractor engine tries to read them for milestone evaluation. """ db_post_mock = AsyncMock(return_value={}) @@ -77,8 +77,8 @@ async def test_hook_posts_to_both_endpoints_in_order(): calls = db_post_mock.call_args_list # First call must be season-stats assert calls[0] == call("season-stats/update-game/42") - # Second call must be evolution evaluate - assert calls[1] == call("evolution/evaluate-game/42") + # Second call must be refractor evaluate + assert calls[1] == call("refractor/evaluate-game/42") @pytest.mark.asyncio @@ -86,11 +86,11 @@ async def test_hook_is_nonfatal_when_db_post_raises(): """ A failure inside the hook must not raise to the caller. - The game result is already persisted when the hook runs. If the evolution + The game result is already persisted when the hook runs. If the refractor API is down or returns an error, we log a warning and continue — the game completion flow must not be interrupted. """ - db_post_mock = AsyncMock(side_effect=Exception("evolution API unavailable")) + db_post_mock = AsyncMock(side_effect=Exception("refractor API unavailable")) # Should not raise try: @@ -102,7 +102,7 @@ async def test_hook_is_nonfatal_when_db_post_raises(): @pytest.mark.asyncio async def test_hook_processes_tier_ups_from_evo_result(): """ - When the evolution endpoint returns tier_ups, each entry is forwarded to + When the refractor endpoint returns tier_ups, each entry is forwarded to notify_tier_completion. This confirms the data path between the API response and the WP-14 @@ -114,7 +114,7 @@ async def test_hook_processes_tier_ups_from_evo_result(): ] async def fake_db_post(endpoint): - if "evolution" in endpoint: + if "refractor" in endpoint: return {"tier_ups": tier_ups} return {} @@ -129,7 +129,7 @@ async def test_hook_processes_tier_ups_from_evo_result(): try: await db_post_mock(f"season-stats/update-game/{db_game['id']}") - evo_result = await db_post_mock(f"evolution/evaluate-game/{db_game['id']}") + evo_result = await db_post_mock(f"refractor/evaluate-game/{db_game['id']}") if evo_result and evo_result.get("tier_ups"): for tier_up in evo_result["tier_ups"]: await mock_notify(channel, tier_up) @@ -146,14 +146,14 @@ async def test_hook_processes_tier_ups_from_evo_result(): @pytest.mark.asyncio async def test_hook_no_tier_ups_does_not_call_notify(): """ - When the evolution response has no tier_ups (empty list or missing key), + When the refractor response has no tier_ups (empty list or missing key), notify_tier_completion is never called. Avoids spurious Discord messages for routine game completions. """ async def fake_db_post(endpoint): - if "evolution" in endpoint: + if "refractor" in endpoint: return {"tier_ups": []} return {} @@ -168,7 +168,7 @@ async def test_hook_no_tier_ups_does_not_call_notify(): try: await db_post_mock(f"season-stats/update-game/{db_game['id']}") - evo_result = await db_post_mock(f"evolution/evaluate-game/{db_game['id']}") + evo_result = await db_post_mock(f"refractor/evaluate-game/{db_game['id']}") if evo_result and evo_result.get("tier_ups"): for tier_up in evo_result["tier_ups"]: await mock_notify(channel, tier_up) diff --git a/tests/test_evolution_notifications.py b/tests/test_evolution_notifications.py deleted file mode 100644 index 8f7206f..0000000 --- a/tests/test_evolution_notifications.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -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!"