Closes #75. New file app/services/evolution_init.py: - _determine_card_type(player): pure fn mapping pos_1 to 'batter'/'sp'/'rp' - initialize_card_evolution(player_id, team_id, card_type): get_or_create EvolutionCardState with current_tier=0, current_value=0.0, fully_evolved=False - Safe failure: all exceptions caught and logged, never raises - Idempotent: duplicate calls for same (player_id, team_id) are no-ops and do NOT reset existing evolution progress Modified app/routers_v2/cards.py: - Add WP-10 hook after Card.bulk_create in the POST endpoint - For each card posted, call _determine_card_type + initialize_card_evolution - Wrapped in try/except so evolution failures cannot block pack opening - Fix pre-existing lint violations (unused lc_id, bare f-string, unused e) New file tests/test_evolution_init.py (16 tests, all passing): - Unit: track assignment for batter / SP / RP / CP positions - Integration: first card creates state with zeroed fields - Integration: duplicate card is a no-op (progress not reset) - Integration: different players on same team get separate states - Integration: card_type routes to correct EvolutionTrack - Integration: missing track returns None gracefully Fix tests/test_evolution_models.py: correct PlayerSeasonStats import/usage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
139 lines
4.4 KiB
Python
139 lines
4.4 KiB
Python
"""
|
|
WP-10: Pack opening hook — evolution_card_state initialization.
|
|
|
|
Public API
|
|
----------
|
|
initialize_card_evolution(player_id, team_id, card_type)
|
|
Get-or-create an EvolutionCardState for the (player_id, team_id) pair.
|
|
Returns the state instance on success, or None if initialization fails
|
|
(missing track, integrity error, etc.). Never raises.
|
|
|
|
_determine_card_type(player)
|
|
Pure function: inspect player.pos_1 and return 'sp', 'rp', or 'batter'.
|
|
Exported so the cards router and tests can call it directly.
|
|
|
|
Design notes
|
|
------------
|
|
- The function is intentionally fire-and-forget from the caller's perspective.
|
|
All exceptions are caught and logged; pack opening is never blocked.
|
|
- No EvolutionProgress rows are created here. Progress accumulation is a
|
|
separate concern handled by the stats-update pipeline (WP-07/WP-08).
|
|
- AI teams and Gauntlet teams skip Paperdex insertion (cards.py pattern);
|
|
we do NOT replicate that exclusion here — all teams get an evolution state
|
|
so that future rule changes don't require back-filling.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from app.db_engine import DoesNotExist, EvolutionCardState, EvolutionTrack
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _determine_card_type(player) -> str:
|
|
"""Map a player's primary position to an evolution card_type string.
|
|
|
|
Rules (from WP-10 spec):
|
|
- pos_1 contains 'SP' -> 'sp'
|
|
- pos_1 contains 'RP' or 'CP' -> 'rp'
|
|
- anything else -> 'batter'
|
|
|
|
Args:
|
|
player: Any object with a ``pos_1`` attribute (Player model or stub).
|
|
|
|
Returns:
|
|
One of the strings 'batter', 'sp', 'rp'.
|
|
"""
|
|
pos = (player.pos_1 or "").upper()
|
|
if "SP" in pos:
|
|
return "sp"
|
|
if "RP" in pos or "CP" in pos:
|
|
return "rp"
|
|
return "batter"
|
|
|
|
|
|
def initialize_card_evolution(
|
|
player_id: int,
|
|
team_id: int,
|
|
card_type: str,
|
|
) -> Optional[EvolutionCardState]:
|
|
"""Get-or-create an EvolutionCardState for a newly acquired card.
|
|
|
|
Called by the cards POST endpoint after each card is inserted. The
|
|
function is idempotent: if a state row already exists for the
|
|
(player_id, team_id) pair it is returned unchanged — existing
|
|
evolution progress is never reset.
|
|
|
|
Args:
|
|
player_id: Primary key of the Player row (Player.player_id).
|
|
team_id: Primary key of the Team row (Team.id).
|
|
card_type: One of 'batter', 'sp', 'rp'. Determines which
|
|
EvolutionTrack is assigned to the new state.
|
|
|
|
Returns:
|
|
The existing or newly created EvolutionCardState instance, or
|
|
None if initialization could not complete (missing track seed
|
|
data, unexpected DB error, etc.).
|
|
"""
|
|
try:
|
|
track = EvolutionTrack.get(EvolutionTrack.card_type == card_type)
|
|
except DoesNotExist:
|
|
logger.warning(
|
|
"evolution_init: no EvolutionTrack found for card_type=%r "
|
|
"(player_id=%s, team_id=%s) — skipping state creation",
|
|
card_type,
|
|
player_id,
|
|
team_id,
|
|
)
|
|
return None
|
|
except Exception:
|
|
logger.exception(
|
|
"evolution_init: unexpected error fetching track "
|
|
"(card_type=%r, player_id=%s, team_id=%s)",
|
|
card_type,
|
|
player_id,
|
|
team_id,
|
|
)
|
|
return None
|
|
|
|
try:
|
|
state, created = EvolutionCardState.get_or_create(
|
|
player_id=player_id,
|
|
team_id=team_id,
|
|
defaults={
|
|
"track": track,
|
|
"current_tier": 0,
|
|
"current_value": 0.0,
|
|
"fully_evolved": False,
|
|
},
|
|
)
|
|
if created:
|
|
logger.debug(
|
|
"evolution_init: created EvolutionCardState id=%s "
|
|
"(player_id=%s, team_id=%s, card_type=%r)",
|
|
state.id,
|
|
player_id,
|
|
team_id,
|
|
card_type,
|
|
)
|
|
else:
|
|
logger.debug(
|
|
"evolution_init: state already exists id=%s "
|
|
"(player_id=%s, team_id=%s) — no-op",
|
|
state.id,
|
|
player_id,
|
|
team_id,
|
|
)
|
|
return state
|
|
|
|
except Exception:
|
|
logger.exception(
|
|
"evolution_init: failed to get_or_create state "
|
|
"(player_id=%s, team_id=%s, card_type=%r)",
|
|
player_id,
|
|
team_id,
|
|
card_type,
|
|
)
|
|
return None
|