# Game Models Specification

**Purpose**: Game-optimized player models used by GameEngine and PlayResolver

**File**: `backend/app/models/player_models.py`

---

## Overview

Game models contain **only** the fields needed for gameplay:
- Player identification (id, name)
- Positions (normalized list)
- Image URL (for UI)
- **PD only**: Outcome probabilities (batting/pitching ratings)

Everything else from the API (team hierarchy, metadata, etc.) is discarded.

---

## Design Principles

1. **Minimal Data**: Only fields used during gameplay
2. **Fast Serialization**: Small models for WebSocket broadcasts
3. **No Nested Objects**: Flatten complex structures (team → team_name)
4. **League Polymorphism**: Common base class with league-specific subclasses

---

## Base Player Model

### BasePlayer (Abstract)

**Purpose**: Common interface for all players

```python
from abc import ABC, abstractmethod
from typing import List
from pydantic import BaseModel


class BasePlayer(BaseModel, ABC):
    """
    Abstract base player model for gameplay

    All players have basic identification regardless of league.
    League-specific data in subclasses.
    """

    # Core identification
    player_id: int  # Standardized field name (maps from 'id' or 'player_id')
    name: str

    # Gameplay essentials
    positions: List[str]  # Extracted from pos_1-8, e.g., ["RF", "CF"]
    image_url: str  # Card image

    @abstractmethod
    def get_primary_position(self) -> str:
        """Get primary position (first in list)"""
        pass

    @abstractmethod
    def can_play_position(self, position: str) -> bool:
        """Check if player can play position"""
        pass

    model_config = {
        "frozen": False  # Allow mutation during game
    }
```

---

## SBA Game Model

### SbaPlayer

**Purpose**: Simplified SBA player for gameplay

**What's Included**:
- Basic player info
- Positions
- Team name (flattened from nested team object)
- Image URL

**What's Excluded**:
- Full team object (manager, division, stadium, etc.)
- Injury data (not needed during gameplay)
- Season/WARA stats
- Baseball Reference IDs

```python
class SbaPlayer(BasePlayer):
    """
    SBA player model for gameplay

    Simplified from SbaPlayerApi - only gameplay essentials.
    """

    # Additional SBA-specific fields
    team_name: str  # Flattened from team.lname
    team_abbrev: str  # Flattened from team.abbrev
    strat_code: Optional[str] = None  # For reference

    def get_primary_position(self) -> str:
        """Get primary position"""
        return self.positions[0] if self.positions else "UTIL"

    def can_play_position(self, position: str) -> bool:
        """Check if player can play position"""
        return position in self.positions

    def get_display_name(self) -> str:
        """Get display name for UI"""
        primary_pos = self.get_primary_position()
        return f"{self.name} ({primary_pos})"

    model_config = {
        "json_schema_extra": {
            "example": {
                "player_id": 12288,
                "name": "Ronald Acuna Jr",
                "positions": ["RF"],
                "image_url": "https://sba-cards-2024.s3.us-east-1.amazonaws.com/2024-cards/ronald-acuna-jr.png",
                "team_name": "West Virginia Black Bears",
                "team_abbrev": "WV",
                "strat_code": "Acuna,R"
            }
        }
    }
```

---

## PD Game Models

### PdBattingRating

**Purpose**: Batting outcome probabilities for one handedness (vs L or vs R)

**What's Included**:
- Outcome probabilities (homerun, strikeout, walk, etc.)
- Summary stats (avg, obp, slg) for quick reference

**What's Excluded**:
- Full battingcard object (card metadata not needed)
- ID fields
- Player reference (already have player)

```python
class PdBattingRating(BaseModel):
    """
    Batting outcome probabilities for one handedness

    Extracted from PdBattingRatingApi - just the probabilities needed
    for play resolution.
    """

    vs_hand: str  # "L" or "R" (vs LHP or RHP)

    # Hit outcomes (probabilities)
    homerun: float
    triple: float
    double: float  # Sum of double_three, double_two, double_pull
    single: float  # Sum of single_two, single_one, single_center

    # Plate discipline
    walk: float
    hbp: float
    strikeout: float

    # Out types
    flyout: float  # Sum of all flyout types
    lineout: float
    popout: float
    groundout: float  # Sum of groundout_a, groundout_b, groundout_c

    # Ballpark outcomes
    bp_homerun: float
    bp_single: float

    # Summary stats (for quick checks)
    avg: float
    obp: float
    slg: float

    def total_probability(self) -> float:
        """Calculate total probability (should be ~100.0)"""
        return (
            self.homerun + self.triple + self.double + self.single +
            self.walk + self.hbp + self.strikeout +
            self.flyout + self.lineout + self.popout + self.groundout +
            self.bp_homerun + self.bp_single
        )
```

### PdPitchingRating

**Purpose**: Pitching outcome probabilities for one handedness (vs L or vs R)

```python
class PdPitchingRating(BaseModel):
    """
    Pitching outcome probabilities for one handedness

    Extracted from PdPitchingRatingApi - just the probabilities needed
    for play resolution.
    """

    vs_hand: str  # "L" or "R" (vs LHB or RHB)

    # Hit outcomes
    homerun: float
    triple: float
    double: float  # Sum of double_three, double_two, double_cf
    single: float  # Sum of single_two, single_one, single_center

    # Plate discipline
    walk: float
    hbp: float
    strikeout: float

    # Out types
    flyout: float  # Sum of flyout types
    groundout: float  # Sum of groundout_a, groundout_b

    # Ballpark outcomes
    bp_homerun: float
    bp_single: float

    # X-check probabilities (fielding checks)
    xcheck: dict[str, float]  # Position → probability

    # Summary stats
    avg: float
    obp: float
    slg: float

    def total_probability(self) -> float:
        """Calculate total probability (should be ~100.0)"""
        return (
            self.homerun + self.triple + self.double + self.single +
            self.walk + self.hbp + self.strikeout +
            self.flyout + self.groundout +
            self.bp_homerun + self.bp_single
        )
```

### PdPlayer

**Purpose**: PD player with attached ratings for gameplay

**What's Included**:
- Basic player info
- Batting ratings (vs L and vs R)
- Pitching ratings (vs L and vs R) if pitcher
- Cardset name (for eligibility checks)

**What's Excluded**:
- Full cardset object (just need name/id)
- Rarity (not used in gameplay)
- MLB player data (not used in gameplay)
- Paperdex (ownership tracking, not gameplay)

```python
class PdPlayer(BasePlayer):
    """
    PD player model for gameplay

    Includes outcome probability ratings from batting/pitching cards.
    Much more complex than SBA due to detailed simulation model.
    """

    # Basic info
    team_name: str  # From mlbclub
    cardset_id: int
    cardset_name: str  # From cardset.name

    # Player type
    is_pitcher: bool  # Determines which ratings to use
    hand: str  # R, L, S (batter handedness or pitcher throws)

    # Batting ratings (always present, even for pitchers)
    batting_vs_lhp: Optional[PdBattingRating] = None
    batting_vs_rhp: Optional[PdBattingRating] = None

    # Pitching ratings (only for pitchers)
    pitching_vs_lhb: Optional[PdPitchingRating] = None
    pitching_vs_rhb: Optional[PdPitchingRating] = None

    # Baserunning (from batting card)
    steal_rating: Optional[str] = None  # e.g., "8-11" (steal_low-steal_high)
    running_speed: Optional[int] = None  # 1-20 scale

    # Pitcher specific
    wild_pitch_range: Optional[int] = None  # d20 range (e.g., 20)

    def get_primary_position(self) -> str:
        """Get primary position"""
        return self.positions[0] if self.positions else "UTIL"

    def can_play_position(self, position: str) -> bool:
        """Check if player can play position"""
        return position in self.positions

    def get_batting_rating(self, vs_hand: str) -> Optional[PdBattingRating]:
        """Get batting rating vs pitcher hand"""
        if vs_hand == "L":
            return self.batting_vs_lhp
        elif vs_hand == "R":
            return self.batting_vs_rhp
        return None

    def get_pitching_rating(self, vs_hand: str) -> Optional[PdPitchingRating]:
        """Get pitching rating vs batter hand"""
        if not self.is_pitcher:
            return None
        if vs_hand == "L":
            return self.pitching_vs_lhb
        elif vs_hand == "R":
            return self.pitching_vs_rhb
        return None

    def get_display_name(self) -> str:
        """Get display name with handedness indicator"""
        hand_symbol = {
            "R": "⟩",  # Right-handed arrow
            "L": "⟨",  # Left-handed arrow
            "S": "⟨⟩"  # Switch hitter
        }.get(self.hand, "")
        primary_pos = self.get_primary_position()
        return f"{self.name} {hand_symbol} ({primary_pos})"

    model_config = {
        "json_schema_extra": {
            "example": {
                "player_id": 10633,
                "name": "Chuck Knoblauch",
                "positions": ["2B"],
                "image_url": "https://pd.manticorum.com/api/v2/players/10633/battingcard?d=2025-4-14",
                "team_name": "New York Yankees",
                "cardset_id": 20,
                "cardset_name": "1998 Season",
                "is_pitcher": False,
                "hand": "R",
                "batting_vs_lhp": {
                    "vs_hand": "L",
                    "homerun": 0.0,
                    "triple": 1.4,
                    "double": 10.2,
                    "single": 9.3,
                    "walk": 18.25,
                    "hbp": 2.0,
                    "strikeout": 9.75,
                    "flyout": 3.55,
                    "lineout": 9.0,
                    "popout": 16.0,
                    "groundout": 19.5,
                    "bp_homerun": 2.0,
                    "bp_single": 5.0,
                    "avg": 0.226,
                    "obp": 0.414,
                    "slg": 0.375
                },
                "batting_vs_rhp": {
                    "vs_hand": "R",
                    "homerun": 1.05,
                    "triple": 1.2,
                    "double": 7.0,
                    "single": 13.1,
                    "walk": 12.1,
                    "hbp": 3.0,
                    "strikeout": 9.9,
                    "flyout": 3.15,
                    "lineout": 11.0,
                    "popout": 13.0,
                    "groundout": 23.6,
                    "bp_homerun": 3.0,
                    "bp_single": 5.0,
                    "avg": 0.244,
                    "obp": 0.384,
                    "slg": 0.402
                },
                "steal_rating": "8-11",
                "running_speed": 13
            }
        }
    }
```

---

## Field Selection Rationale

### Why These Fields?

**Included in Game Models**:
| Field | Why Needed |
|-------|------------|
| `player_id` | Primary key for references |
| `name` | Display to users |
| `positions` | Position eligibility validation |
| `image_url` | Display card image |
| `team_name` | Display team affiliation |
| **PD**: `ratings` | **Critical** - drive play resolution |
| **PD**: `hand` | Determines which rating to use |
| **PD**: `is_pitcher` | Determines card type |

**Excluded from Game Models**:
| Field | Why Not Needed |
|-------|----------------|
| Full team object | Only need team name string |
| Manager data | Not used in gameplay |
| Division data | Not used in gameplay |
| Rarity | Doesn't affect gameplay |
| Paperdex | Collection tracking, not gameplay |
| MLB IDs | External references, not gameplay |
| Injury data | Roster management, not gameplay |

---

## Size Comparison

### SBA Player
- **API Model**: ~50 fields (with nested team/manager/division)
- **Game Model**: ~7 fields
- **Size Reduction**: ~85%

### PD Player
- **API Models Combined**: ~150 fields (player + 2 batting ratings + 2 pitching ratings)
- **Game Model**: ~15 fields (with flattened ratings)
- **Size Reduction**: ~90%

---

## Usage in GameEngine

```python
from app.models.player_models import SbaPlayer, PdPlayer

# In PlayResolver
def resolve_play(
    self,
    batter: BasePlayer,
    pitcher: BasePlayer,
    ...
) -> PlayResult:

    # Type-safe access to league-specific features
    if isinstance(batter, PdPlayer) and isinstance(pitcher, PdPlayer):
        # Use detailed probability model
        batting_rating = batter.get_batting_rating(pitcher.hand)
        pitching_rating = pitcher.get_pitching_rating(batter.hand)

        # Resolve using outcome probabilities
        outcome = self._resolve_from_probabilities(batting_rating, pitching_rating)

    elif isinstance(batter, SbaPlayer):
        # Use simplified result chart
        outcome = self._resolve_from_chart(dice_roll)

    return outcome
```

---

## Validation

All game models include validation:

```python
# In tests
def test_pd_batting_rating_probabilities_sum_to_100():
    """Ensure all probabilities sum to ~100"""
    rating = PdBattingRating(...)
    total = rating.total_probability()
    assert 99.0 <= total <= 101.0  # Allow small float rounding

def test_position_list_not_empty():
    """Players must have at least one position"""
    with pytest.raises(ValidationError):
        SbaPlayer(
            player_id=1,
            name="Test",
            positions=[],  # Invalid!
            ...
        )
```

---

**Next**: See [mappers-and-factories.md](./mappers-and-factories.md) for API → Game transformations
