This commit includes Week 6 player models implementation and critical performance optimizations discovered during testing. ## Player Models (Week 6 - 50% Complete) **New Files:** - app/models/player_models.py (516 lines) - BasePlayer abstract class with polymorphic interface - SbaPlayer with API parsing factory method - PdPlayer with batting/pitching scouting data support - Supporting models: PdCardset, PdRarity, PdBattingCard, PdPitchingCard - tests/unit/models/test_player_models.py (692 lines) - 32 comprehensive unit tests, all passing - Tests for BasePlayer, SbaPlayer, PdPlayer, polymorphism **Architecture:** - Simplified single-layer approach vs planned two-layer - Factory methods handle API → Game transformation directly - SbaPlayer.from_api_response(data) - parses SBA API inline - PdPlayer.from_api_response(player_data, batting_data, pitching_data) - Full Pydantic validation, type safety, and polymorphism ## Performance Optimizations **Database Query Reduction (60% fewer queries per play):** - Before: 5 queries per play (INSERT play, SELECT play with JOINs, SELECT games, 2x SELECT lineups) - After: 2 queries per play (INSERT play, UPDATE games conditionally) Changes: 1. Lineup caching (game_engine.py:384-425) - Check state_manager.get_lineup() cache before DB fetch - Eliminates 2 SELECT queries per play 2. Remove unnecessary refresh (operations.py:281-302) - Removed session.refresh(play) after INSERT - Eliminates 1 SELECT with 3 expensive LEFT JOINs 3. Direct UPDATE statement (operations.py:109-165) - Changed update_game_state() to use direct UPDATE - No longer does SELECT + modify + commit 4. Conditional game state updates (game_engine.py:200-217) - Only UPDATE games table when score/inning/status changes - Captures state before/after and compares - ~40-60% fewer updates (many plays don't score) ## Bug Fixes 1. Fixed outs_before tracking (game_engine.py:551) - Was incorrectly calculating: state.outs - result.outs_recorded - Now correctly captures: state.outs (before applying result) - All play records now have accurate out counts 2. Fixed game recovery (state_manager.py:312-314) - AttributeError when recovering: 'GameState' has no attribute 'runners' - Changed to use state.get_all_runners() method - Games can now be properly recovered from database ## Enhanced Terminal Client **Status Display Improvements (terminal_client/display.py:75-97):** - Added "⚠️ WAITING FOR ACTION" section when play is pending - Shows specific guidance: - "The defense needs to submit their decision" → Run defensive [OPTIONS] - "The offense needs to submit their decision" → Run offensive [OPTIONS] - "Ready to resolve play" → Run resolve - Color-coded command hints for better UX ## Documentation Updates **backend/CLAUDE.md:** - Added comprehensive Player Models section (204 lines) - Updated Current Phase status to Week 6 (~50% complete) - Documented all optimizations and bug fixes - Added integration examples and usage patterns **New Files:** - .claude/implementation/week6-status-assessment.md - Comprehensive Week 6 progress review - Architecture decision rationale (single-layer vs two-layer) - Completion status and next priorities - Updated roadmap for remaining Week 6 work ## Test Results - Player models: 32/32 tests passing - All existing tests continue to pass - Performance improvements verified with terminal client ## Next Steps (Week 6 Remaining) 1. Configuration system (BaseConfig, SbaConfig, PdConfig) 2. Result charts & PD play resolution with ratings 3. API client for live roster data (deferred) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
668 lines
24 KiB
Python
668 lines
24 KiB
Python
"""
|
|
Unit tests for player models.
|
|
|
|
Tests polymorphic player model system (BasePlayer, SbaPlayer, PdPlayer)
|
|
with factory methods and API response parsing.
|
|
|
|
Author: Claude
|
|
Date: 2025-10-28
|
|
"""
|
|
import pytest
|
|
from abc import ABC
|
|
from app.models.player_models import (
|
|
BasePlayer,
|
|
SbaPlayer,
|
|
PdPlayer,
|
|
PdCardset,
|
|
PdRarity,
|
|
PdBattingRating,
|
|
PdPitchingRating,
|
|
PdBattingCard,
|
|
PdPitchingCard,
|
|
)
|
|
|
|
|
|
# ==================== BasePlayer Tests ====================
|
|
|
|
class TestBasePlayer:
|
|
"""Test BasePlayer abstract base class."""
|
|
|
|
def test_cannot_instantiate_base_player(self):
|
|
"""BasePlayer is abstract and cannot be instantiated directly."""
|
|
with pytest.raises(TypeError, match="Can't instantiate abstract class"):
|
|
BasePlayer(id=1, name="Test")
|
|
|
|
def test_base_player_is_abc(self):
|
|
"""BasePlayer inherits from ABC."""
|
|
assert issubclass(BasePlayer, ABC)
|
|
|
|
def test_required_abstract_methods(self):
|
|
"""BasePlayer defines required abstract methods."""
|
|
abstract_methods = BasePlayer.__abstractmethods__
|
|
assert "get_image_url" in abstract_methods
|
|
assert "get_positions" in abstract_methods
|
|
assert "get_display_name" in abstract_methods
|
|
|
|
|
|
# ==================== SbaPlayer Tests ====================
|
|
|
|
class TestSbaPlayer:
|
|
"""Test SbaPlayer model and factory methods."""
|
|
|
|
@pytest.fixture
|
|
def minimal_sba_data(self):
|
|
"""Minimal valid SBA API response."""
|
|
return {
|
|
"id": 12288,
|
|
"name": "Ronald Acuna Jr",
|
|
"wara": 0.0,
|
|
}
|
|
|
|
@pytest.fixture
|
|
def full_sba_data(self):
|
|
"""Full SBA API response with all fields."""
|
|
return {
|
|
"id": 12288,
|
|
"name": "Ronald Acuna Jr",
|
|
"wara": 5.2,
|
|
"image": "https://example.com/acuna.png",
|
|
"image2": "https://example.com/acuna2.png",
|
|
"team": {
|
|
"id": 499,
|
|
"abbrev": "WV",
|
|
"lname": "West Virginia Black Bears",
|
|
},
|
|
"season": 12,
|
|
"pos_1": "RF",
|
|
"pos_2": "CF",
|
|
"pos_3": "LF",
|
|
"headshot": "https://example.com/headshot.jpg",
|
|
"vanity_card": "https://example.com/vanity.png",
|
|
"strat_code": "Acuna,R",
|
|
"bbref_id": "acunaro01",
|
|
"injury_rating": "5p30",
|
|
}
|
|
|
|
def test_create_from_minimal_api_response(self, minimal_sba_data):
|
|
"""Can create SbaPlayer with minimal required fields."""
|
|
player = SbaPlayer.from_api_response(minimal_sba_data)
|
|
|
|
assert player.id == 12288
|
|
assert player.name == "Ronald Acuna Jr"
|
|
assert player.wara == 0.0
|
|
assert player.image is None
|
|
assert player.team_id is None
|
|
|
|
def test_create_from_full_api_response(self, full_sba_data):
|
|
"""Can create SbaPlayer with all fields populated."""
|
|
player = SbaPlayer.from_api_response(full_sba_data)
|
|
|
|
assert player.id == 12288
|
|
assert player.name == "Ronald Acuna Jr"
|
|
assert player.wara == 5.2
|
|
assert player.image == "https://example.com/acuna.png"
|
|
assert player.image2 == "https://example.com/acuna2.png"
|
|
assert player.team_id == 499
|
|
assert player.team_name == "West Virginia Black Bears"
|
|
assert player.season == 12
|
|
assert player.pos_1 == "RF"
|
|
assert player.pos_2 == "CF"
|
|
assert player.pos_3 == "LF"
|
|
assert player.headshot == "https://example.com/headshot.jpg"
|
|
assert player.strat_code == "Acuna,R"
|
|
assert player.bbref_id == "acunaro01"
|
|
|
|
def test_get_positions_filters_none(self, full_sba_data):
|
|
"""get_positions() returns only non-None positions."""
|
|
player = SbaPlayer.from_api_response(full_sba_data)
|
|
positions = player.get_positions()
|
|
|
|
assert positions == ["RF", "CF", "LF"]
|
|
assert None not in positions
|
|
|
|
def test_get_positions_empty_when_no_positions(self, minimal_sba_data):
|
|
"""get_positions() returns empty list when no positions set."""
|
|
player = SbaPlayer.from_api_response(minimal_sba_data)
|
|
positions = player.get_positions()
|
|
|
|
assert positions == []
|
|
|
|
def test_get_image_url_primary(self, full_sba_data):
|
|
"""get_image_url() returns primary image when available."""
|
|
player = SbaPlayer.from_api_response(full_sba_data)
|
|
assert player.get_image_url() == "https://example.com/acuna.png"
|
|
|
|
def test_get_image_url_fallback_to_image2(self):
|
|
"""get_image_url() falls back to image2 when primary missing."""
|
|
data = {
|
|
"id": 1,
|
|
"name": "Test",
|
|
"wara": 0.0,
|
|
"image2": "https://example.com/image2.png",
|
|
}
|
|
player = SbaPlayer.from_api_response(data)
|
|
assert player.get_image_url() == "https://example.com/image2.png"
|
|
|
|
def test_get_image_url_fallback_to_headshot(self):
|
|
"""get_image_url() falls back to headshot when others missing."""
|
|
data = {
|
|
"id": 1,
|
|
"name": "Test",
|
|
"wara": 0.0,
|
|
"headshot": "https://example.com/headshot.jpg",
|
|
}
|
|
player = SbaPlayer.from_api_response(data)
|
|
assert player.get_image_url() == "https://example.com/headshot.jpg"
|
|
|
|
def test_get_image_url_empty_when_none(self, minimal_sba_data):
|
|
"""get_image_url() returns empty string when no images."""
|
|
player = SbaPlayer.from_api_response(minimal_sba_data)
|
|
assert player.get_image_url() == ""
|
|
|
|
def test_get_display_name(self, full_sba_data):
|
|
"""get_display_name() returns player name."""
|
|
player = SbaPlayer.from_api_response(full_sba_data)
|
|
assert player.get_display_name() == "Ronald Acuna Jr"
|
|
|
|
def test_team_extraction_from_nested_object(self, full_sba_data):
|
|
"""Team ID and name extracted from nested 'team' object."""
|
|
player = SbaPlayer.from_api_response(full_sba_data)
|
|
assert player.team_id == 499
|
|
assert player.team_name == "West Virginia Black Bears"
|
|
|
|
def test_team_none_when_missing(self, minimal_sba_data):
|
|
"""Team fields are None when team object missing."""
|
|
player = SbaPlayer.from_api_response(minimal_sba_data)
|
|
assert player.team_id is None
|
|
assert player.team_name is None
|
|
|
|
|
|
# ==================== PdPlayer Tests ====================
|
|
|
|
class TestPdPlayer:
|
|
"""Test PdPlayer model and factory methods."""
|
|
|
|
@pytest.fixture
|
|
def pd_player_data(self):
|
|
"""PD player API response (without scouting data)."""
|
|
return {
|
|
"player_id": 10633,
|
|
"p_name": "Chuck Knoblauch",
|
|
"cost": 77,
|
|
"image": "https://pd.example.com/players/10633/battingcard",
|
|
"cardset": {
|
|
"id": 20,
|
|
"name": "1998 Season",
|
|
"description": "Cards based on the 1998 MLB season",
|
|
"ranked_legal": True,
|
|
},
|
|
"set_num": 609,
|
|
"rarity": {
|
|
"id": 3,
|
|
"value": 2,
|
|
"name": "Starter",
|
|
"color": "C0C0C0",
|
|
},
|
|
"mlbclub": "New York Yankees",
|
|
"franchise": "New York Yankees",
|
|
"pos_1": "2B",
|
|
"pos_2": "SS",
|
|
"headshot": "https://example.com/headshot.jpg",
|
|
"strat_code": None,
|
|
"bbref_id": "knoblch01",
|
|
"fangr_id": "609",
|
|
"description": "1998 Season",
|
|
"quantity": 999,
|
|
}
|
|
|
|
@pytest.fixture
|
|
def pd_batting_data(self):
|
|
"""PD batting card API response."""
|
|
return {
|
|
"count": 2,
|
|
"ratings": [
|
|
{
|
|
"battingcard": {
|
|
"steal_low": 8,
|
|
"steal_high": 11,
|
|
"steal_auto": True,
|
|
"steal_jump": 0.25,
|
|
"bunting": "C",
|
|
"hit_and_run": "D",
|
|
"running": 13,
|
|
"offense_col": 1,
|
|
"hand": "R",
|
|
},
|
|
"vs_hand": "L",
|
|
"pull_rate": 0.29379,
|
|
"center_rate": 0.41243,
|
|
"slap_rate": 0.29378,
|
|
"homerun": 0.0,
|
|
"bp_homerun": 2.0,
|
|
"triple": 1.4,
|
|
"double_three": 0.0,
|
|
"double_two": 5.1,
|
|
"double_pull": 5.1,
|
|
"single_two": 3.5,
|
|
"single_one": 4.5,
|
|
"single_center": 1.35,
|
|
"bp_single": 5.0,
|
|
"hbp": 2.0,
|
|
"walk": 18.25,
|
|
"strikeout": 9.75,
|
|
"lineout": 9.0,
|
|
"popout": 16.0,
|
|
"flyout_a": 0.0,
|
|
"flyout_bq": 1.65,
|
|
"flyout_lf_b": 1.9,
|
|
"flyout_rf_b": 2.0,
|
|
"groundout_a": 7.0,
|
|
"groundout_b": 10.5,
|
|
"groundout_c": 2.0,
|
|
"avg": 0.2263888888888889,
|
|
"obp": 0.41388888888888886,
|
|
"slg": 0.37453703703703706,
|
|
},
|
|
{
|
|
"battingcard": {
|
|
"steal_low": 8,
|
|
"steal_high": 11,
|
|
"steal_auto": True,
|
|
"steal_jump": 0.25,
|
|
"bunting": "C",
|
|
"hit_and_run": "D",
|
|
"running": 13,
|
|
"offense_col": 1,
|
|
"hand": "R",
|
|
},
|
|
"vs_hand": "R",
|
|
"pull_rate": 0.25824,
|
|
"center_rate": 0.1337,
|
|
"slap_rate": 0.60806,
|
|
"homerun": 1.05,
|
|
"bp_homerun": 3.0,
|
|
"triple": 1.2,
|
|
"double_three": 0.0,
|
|
"double_two": 3.5,
|
|
"double_pull": 3.5,
|
|
"single_two": 4.3,
|
|
"single_one": 6.4,
|
|
"single_center": 2.4,
|
|
"bp_single": 5.0,
|
|
"hbp": 3.0,
|
|
"walk": 12.1,
|
|
"strikeout": 9.9,
|
|
"lineout": 11.0,
|
|
"popout": 13.0,
|
|
"flyout_a": 0.0,
|
|
"flyout_bq": 1.6,
|
|
"flyout_lf_b": 1.5,
|
|
"flyout_rf_b": 1.95,
|
|
"groundout_a": 12.0,
|
|
"groundout_b": 11.6,
|
|
"groundout_c": 0.0,
|
|
"avg": 0.2439814814814815,
|
|
"obp": 0.3837962962962963,
|
|
"slg": 0.40185185185185185,
|
|
},
|
|
],
|
|
}
|
|
|
|
@pytest.fixture
|
|
def pd_pitching_data(self):
|
|
"""PD pitching card API response."""
|
|
return {
|
|
"count": 2,
|
|
"ratings": [
|
|
{
|
|
"pitchingcard": {
|
|
"balk": 0,
|
|
"wild_pitch": 20,
|
|
"hold": 9,
|
|
"starter_rating": 1,
|
|
"relief_rating": 2,
|
|
"closer_rating": None,
|
|
"batting": "#1WR-C",
|
|
"offense_col": 1,
|
|
"hand": "R",
|
|
},
|
|
"vs_hand": "L",
|
|
"homerun": 2.6,
|
|
"bp_homerun": 6.0,
|
|
"triple": 2.1,
|
|
"double_three": 0.0,
|
|
"double_two": 7.1,
|
|
"double_cf": 0.0,
|
|
"single_two": 1.0,
|
|
"single_one": 1.0,
|
|
"single_center": 0.0,
|
|
"bp_single": 5.0,
|
|
"hbp": 6.0,
|
|
"walk": 17.6,
|
|
"strikeout": 11.4,
|
|
"flyout_lf_b": 0.0,
|
|
"flyout_cf_b": 7.75,
|
|
"flyout_rf_b": 3.6,
|
|
"groundout_a": 1.75,
|
|
"groundout_b": 6.1,
|
|
"xcheck_p": 1.0,
|
|
"xcheck_c": 3.0,
|
|
"xcheck_1b": 2.0,
|
|
"xcheck_2b": 6.0,
|
|
"xcheck_3b": 3.0,
|
|
"xcheck_ss": 7.0,
|
|
"xcheck_lf": 2.0,
|
|
"xcheck_cf": 3.0,
|
|
"xcheck_rf": 2.0,
|
|
"avg": 0.17870370370370367,
|
|
"obp": 0.3972222222222222,
|
|
"slg": 0.4388888888888889,
|
|
},
|
|
{
|
|
"pitchingcard": {
|
|
"balk": 0,
|
|
"wild_pitch": 20,
|
|
"hold": 9,
|
|
"starter_rating": 1,
|
|
"relief_rating": 2,
|
|
"closer_rating": None,
|
|
"batting": "#1WR-C",
|
|
"offense_col": 1,
|
|
"hand": "R",
|
|
},
|
|
"vs_hand": "R",
|
|
"homerun": 5.0,
|
|
"bp_homerun": 2.0,
|
|
"triple": 1.0,
|
|
"double_three": 0.0,
|
|
"double_two": 0.0,
|
|
"double_cf": 6.15,
|
|
"single_two": 6.5,
|
|
"single_one": 0.0,
|
|
"single_center": 6.5,
|
|
"bp_single": 5.0,
|
|
"hbp": 2.0,
|
|
"walk": 10.2,
|
|
"strikeout": 26.65,
|
|
"flyout_lf_b": 0.0,
|
|
"flyout_cf_b": 0.0,
|
|
"flyout_rf_b": 0.0,
|
|
"groundout_a": 6.0,
|
|
"groundout_b": 2.0,
|
|
"xcheck_p": 1.0,
|
|
"xcheck_c": 3.0,
|
|
"xcheck_1b": 2.0,
|
|
"xcheck_2b": 6.0,
|
|
"xcheck_3b": 3.0,
|
|
"xcheck_ss": 7.0,
|
|
"xcheck_lf": 2.0,
|
|
"xcheck_cf": 3.0,
|
|
"xcheck_rf": 2.0,
|
|
"avg": 0.2652777777777778,
|
|
"obp": 0.37824074074074077,
|
|
"slg": 0.5074074074074074,
|
|
},
|
|
],
|
|
}
|
|
|
|
def test_create_without_scouting_data(self, pd_player_data):
|
|
"""Can create PdPlayer without scouting data."""
|
|
player = PdPlayer.from_api_response(pd_player_data)
|
|
|
|
assert player.player_id == 10633
|
|
assert player.name == "Chuck Knoblauch"
|
|
assert player.cost == 77
|
|
assert player.pos_1 == "2B"
|
|
assert player.pos_2 == "SS"
|
|
assert player.batting_card is None
|
|
assert player.pitching_card is None
|
|
|
|
def test_create_with_batting_data(self, pd_player_data, pd_batting_data):
|
|
"""Can create PdPlayer with batting scouting data."""
|
|
player = PdPlayer.from_api_response(
|
|
pd_player_data,
|
|
batting_data=pd_batting_data
|
|
)
|
|
|
|
assert player.batting_card is not None
|
|
assert player.batting_card.hand == "R"
|
|
assert player.batting_card.steal_low == 8
|
|
assert player.batting_card.steal_high == 11
|
|
assert player.batting_card.bunting == "C"
|
|
assert "L" in player.batting_card.ratings
|
|
assert "R" in player.batting_card.ratings
|
|
assert player.pitching_card is None
|
|
|
|
def test_create_with_pitching_data(self, pd_player_data, pd_pitching_data):
|
|
"""Can create PdPlayer with pitching scouting data."""
|
|
player = PdPlayer.from_api_response(
|
|
pd_player_data,
|
|
pitching_data=pd_pitching_data
|
|
)
|
|
|
|
assert player.pitching_card is not None
|
|
assert player.pitching_card.hand == "R"
|
|
assert player.pitching_card.balk == 0
|
|
assert player.pitching_card.wild_pitch == 20
|
|
assert player.pitching_card.starter_rating == 1
|
|
assert "L" in player.pitching_card.ratings
|
|
assert "R" in player.pitching_card.ratings
|
|
assert player.batting_card is None
|
|
|
|
def test_create_with_full_scouting_data(
|
|
self, pd_player_data, pd_batting_data, pd_pitching_data
|
|
):
|
|
"""Can create PdPlayer with both batting and pitching data."""
|
|
player = PdPlayer.from_api_response(
|
|
pd_player_data,
|
|
batting_data=pd_batting_data,
|
|
pitching_data=pd_pitching_data
|
|
)
|
|
|
|
assert player.batting_card is not None
|
|
assert player.pitching_card is not None
|
|
|
|
def test_get_batting_rating_vs_lefties(self, pd_player_data, pd_batting_data):
|
|
"""get_batting_rating('L') returns rating vs left-handed pitchers."""
|
|
player = PdPlayer.from_api_response(
|
|
pd_player_data,
|
|
batting_data=pd_batting_data
|
|
)
|
|
|
|
rating = player.get_batting_rating('L')
|
|
|
|
assert rating is not None
|
|
assert rating.vs_hand == 'L'
|
|
assert rating.homerun == 0.0
|
|
assert rating.walk == 18.25
|
|
assert rating.strikeout == 9.75
|
|
assert rating.avg == pytest.approx(0.2263888888888889)
|
|
|
|
def test_get_batting_rating_vs_righties(self, pd_player_data, pd_batting_data):
|
|
"""get_batting_rating('R') returns rating vs right-handed pitchers."""
|
|
player = PdPlayer.from_api_response(
|
|
pd_player_data,
|
|
batting_data=pd_batting_data
|
|
)
|
|
|
|
rating = player.get_batting_rating('R')
|
|
|
|
assert rating is not None
|
|
assert rating.vs_hand == 'R'
|
|
assert rating.homerun == 1.05
|
|
assert rating.walk == 12.1
|
|
assert rating.strikeout == 9.9
|
|
|
|
def test_get_batting_rating_none_when_no_data(self, pd_player_data):
|
|
"""get_batting_rating() returns None when no batting data."""
|
|
player = PdPlayer.from_api_response(pd_player_data)
|
|
|
|
assert player.get_batting_rating('L') is None
|
|
assert player.get_batting_rating('R') is None
|
|
|
|
def test_get_pitching_rating_vs_lefties(self, pd_player_data, pd_pitching_data):
|
|
"""get_pitching_rating('L') returns rating vs left-handed batters."""
|
|
player = PdPlayer.from_api_response(
|
|
pd_player_data,
|
|
pitching_data=pd_pitching_data
|
|
)
|
|
|
|
rating = player.get_pitching_rating('L')
|
|
|
|
assert rating is not None
|
|
assert rating.vs_hand == 'L'
|
|
assert rating.homerun == 2.6
|
|
assert rating.walk == 17.6
|
|
assert rating.strikeout == 11.4
|
|
assert rating.xcheck_ss == 7.0
|
|
|
|
def test_get_pitching_rating_vs_righties(self, pd_player_data, pd_pitching_data):
|
|
"""get_pitching_rating('R') returns rating vs right-handed batters."""
|
|
player = PdPlayer.from_api_response(
|
|
pd_player_data,
|
|
pitching_data=pd_pitching_data
|
|
)
|
|
|
|
rating = player.get_pitching_rating('R')
|
|
|
|
assert rating is not None
|
|
assert rating.vs_hand == 'R'
|
|
assert rating.homerun == 5.0
|
|
assert rating.walk == 10.2
|
|
assert rating.strikeout == 26.65
|
|
|
|
def test_get_pitching_rating_none_when_no_data(self, pd_player_data):
|
|
"""get_pitching_rating() returns None when no pitching data."""
|
|
player = PdPlayer.from_api_response(pd_player_data)
|
|
|
|
assert player.get_pitching_rating('L') is None
|
|
assert player.get_pitching_rating('R') is None
|
|
|
|
def test_get_positions(self, pd_player_data):
|
|
"""get_positions() returns non-None positions."""
|
|
player = PdPlayer.from_api_response(pd_player_data)
|
|
|
|
positions = player.get_positions()
|
|
|
|
assert positions == ["2B", "SS"]
|
|
|
|
def test_get_display_name_with_description(self, pd_player_data):
|
|
"""get_display_name() includes description in parentheses."""
|
|
player = PdPlayer.from_api_response(pd_player_data)
|
|
|
|
assert player.get_display_name() == "Chuck Knoblauch (1998 Season)"
|
|
|
|
def test_get_image_url_primary(self, pd_player_data):
|
|
"""get_image_url() returns primary image."""
|
|
player = PdPlayer.from_api_response(pd_player_data)
|
|
|
|
assert player.get_image_url() == "https://pd.example.com/players/10633/battingcard"
|
|
|
|
def test_get_image_url_fallback_to_headshot(self):
|
|
"""get_image_url() falls back to headshot when primary missing."""
|
|
data = {
|
|
"player_id": 1,
|
|
"p_name": "Test",
|
|
"cost": 1,
|
|
"cardset": {"id": 1, "name": "Test", "description": "Test", "ranked_legal": True},
|
|
"set_num": 1,
|
|
"rarity": {"id": 1, "value": 1, "name": "Test", "color": "FFF"},
|
|
"mlbclub": "Test",
|
|
"franchise": "Test",
|
|
"description": "Test",
|
|
"headshot": "https://example.com/headshot.jpg",
|
|
}
|
|
player = PdPlayer.from_api_response(data)
|
|
|
|
assert player.get_image_url() == "https://example.com/headshot.jpg"
|
|
|
|
def test_cardset_parsing(self, pd_player_data):
|
|
"""Cardset is properly parsed into PdCardset model."""
|
|
player = PdPlayer.from_api_response(pd_player_data)
|
|
|
|
assert isinstance(player.cardset, PdCardset)
|
|
assert player.cardset.id == 20
|
|
assert player.cardset.name == "1998 Season"
|
|
assert player.cardset.ranked_legal is True
|
|
|
|
def test_rarity_parsing(self, pd_player_data):
|
|
"""Rarity is properly parsed into PdRarity model."""
|
|
player = PdPlayer.from_api_response(pd_player_data)
|
|
|
|
assert isinstance(player.rarity, PdRarity)
|
|
assert player.rarity.id == 3
|
|
assert player.rarity.value == 2
|
|
assert player.rarity.name == "Starter"
|
|
assert player.rarity.color == "C0C0C0"
|
|
|
|
|
|
# ==================== Polymorphism Tests ====================
|
|
|
|
class TestPolymorphism:
|
|
"""Test polymorphic behavior across player types."""
|
|
|
|
def test_both_implement_base_player_interface(self):
|
|
"""Both SbaPlayer and PdPlayer implement BasePlayer interface."""
|
|
sba_data = {"id": 1, "name": "SBA Player", "wara": 0.0, "pos_1": "1B"}
|
|
pd_data = {
|
|
"player_id": 1,
|
|
"p_name": "PD Player",
|
|
"cost": 1,
|
|
"cardset": {"id": 1, "name": "Test", "description": "Test", "ranked_legal": True},
|
|
"set_num": 1,
|
|
"rarity": {"id": 1, "value": 1, "name": "Test", "color": "FFF"},
|
|
"mlbclub": "Test",
|
|
"franchise": "Test",
|
|
"description": "Test",
|
|
"pos_1": "1B",
|
|
}
|
|
|
|
sba_player = SbaPlayer.from_api_response(sba_data)
|
|
pd_player = PdPlayer.from_api_response(pd_data)
|
|
|
|
# Both are BasePlayer instances
|
|
assert isinstance(sba_player, BasePlayer)
|
|
assert isinstance(pd_player, BasePlayer)
|
|
|
|
def test_polymorphic_function_works_with_both(self):
|
|
"""Function accepting BasePlayer works with both implementations."""
|
|
def process_player(player: BasePlayer) -> dict:
|
|
"""Example function that works with any BasePlayer."""
|
|
return {
|
|
"name": player.get_display_name(),
|
|
"positions": player.get_positions(),
|
|
"image": player.get_image_url(),
|
|
}
|
|
|
|
sba_data = {
|
|
"id": 1,
|
|
"name": "SBA Player",
|
|
"wara": 0.0,
|
|
"pos_1": "1B",
|
|
"image": "https://example.com/sba.png",
|
|
}
|
|
pd_data = {
|
|
"player_id": 1,
|
|
"p_name": "PD Player",
|
|
"cost": 1,
|
|
"cardset": {"id": 1, "name": "Test", "description": "Test", "ranked_legal": True},
|
|
"set_num": 1,
|
|
"rarity": {"id": 1, "value": 1, "name": "Test", "color": "FFF"},
|
|
"mlbclub": "Test",
|
|
"franchise": "Test",
|
|
"description": "2024",
|
|
"pos_1": "1B",
|
|
"image": "https://example.com/pd.png",
|
|
}
|
|
|
|
sba_result = process_player(SbaPlayer.from_api_response(sba_data))
|
|
pd_result = process_player(PdPlayer.from_api_response(pd_data))
|
|
|
|
assert sba_result["name"] == "SBA Player"
|
|
assert sba_result["positions"] == ["1B"]
|
|
assert sba_result["image"] == "https://example.com/sba.png"
|
|
|
|
assert pd_result["name"] == "PD Player (2024)"
|
|
assert pd_result["positions"] == ["1B"]
|
|
assert pd_result["image"] == "https://example.com/pd.png"
|