# Player Data Catalog - In-Memory Cache Specification **Purpose**: Comprehensive catalog of all player data fields to cache in memory for fast gameplay **Source**: Paper Dynasty Discord Bot (proven production system) **Date**: 2025-10-22 --- ## Overview This document catalogs all player data that should be cached in memory during active gameplay. The goal is to avoid database queries during play resolution while maintaining complete access to all necessary ratings and attributes. **Memory Cost Estimate**: ~500 bytes per player × 20 players = **~10KB per game** --- ## Core Player Identity **Source**: `Player` model (players.py) ```python class PlayerIdentity: """Basic player identification and display""" # Database IDs player_id: int # Database player ID card_id: int # Specific card variant ID lineup_id: int # Game lineup entry ID game_id: int # Game ID since (in PD) could be active in multiple games # Display data name: str # Player name image: str # Primary card image URL image2: Optional[str] # Alternate card image headshot: Optional[str] # Player headshot URL # Team/Set info cardset_id: Optional[int] # Which cardset this card is from set_num: Optional[int] # Card number in set rarity_id: Optional[int] # Card rarity tier cost: Optional[int] # Card value/cost # Metadata team_name: Optional[int] # Maps to PdPlayer.mlbteam.name and SbaPlayer.team.lname franchise: Optional[int] # Historical franchise strat_code: Optional[str] # Strat-O-Matic code description: Optional[int] # Card description/flavor text # Lineup data position: str # Current position in game (P, C, 1B, etc.) batting_order: Optional[int] # 1-9 or None for pitchers is_starter: bool # Original lineup vs substitute is_active: bool # Currently in game ``` **Usage**: Display in UI, WebSocket broadcasts, substitution tracking --- ## Batting Card Data ### Basic Batting Attributes **Source**: `BattingCard` model (battingcards.py) ```python class BattingAttributes: """Baserunning and situational hitting""" # Stealing steal_low: int = 3 # Minimum dice roll to attempt steal (3-20) steal_high: int = 20 # Maximum dice roll for successful steal (3-20) steal_auto: bool # Automatic steal success (special speedsters) steal_jump: float # Jump rating (affects steal success), -1.0 to +1.0 # Situational hitting bunting: str # Bunting ability: A, B, C, D (A = best) hit_and_run: str # Hit & run ability: A, B, C, D running: int = 10 # Base running rating: 1-20 (10 = average, 20 = fastest) # Card metadata offense_col: Optional[int] # Offensive column number (for result charts) hand: str # Batting handedness: R, L, S (switch) ``` **Usage**: - Stolen base decisions and resolution - Bunt attempt validation - Hit & run play processing - Base advancement calculations - Running evaluation for extra bases ### Detailed Batting Ratings (vs LHP and RHP) **Source**: `BattingCardRatings` model (battingcardratings.py) **CRITICAL**: Each batter has TWO sets of ratings (vs LHP and vs RHP), stored separately ```python class BattingRatings: """ Detailed batting result probabilities (108 total chances) IMPORTANT: These ratings exist TWICE per card: - vs_hand = 'vL' (vs Left-handed pitchers) - vs_hand = 'vR' (vs Right-handed pitchers) """ vs_hand: str # 'vL' or 'vR' - which rating set this is # Extra-base hits (out of 108) homerun: float # Home run chances bp_homerun: float # Ballpark home run (depends on park) triple: float # Triple chances double_three: float # Double (3-base advancement) double_two: float # Double (2-base advancement) double_pull: float # Pull double (to pull field) # Singles single_two: float # Single (2-base advancement) single_one: float # Single (1-base advancement) single_center: float # Single to center field bp_single: float # Ballpark single # Walks/HBP hbp: float # Hit by pitch chances walk: float # Base on balls chances # Strikeouts strikeout: float # Strikeout chances # Outs (air) lineout: float # Line drive out popout: float # Pop fly out flyout_a: float # Fly out (A range) flyout_bq: float # Fly out (B/Q range) flyout_lf_b: float # Fly out to LF (B range) flyout_rf_b: float # Fly out to RF (B range) # Outs (ground) groundout_a: float # Ground out (A range) groundout_b: float # Ground out (B range) groundout_c: float # Ground out (C range) - double play risk # Calculated stats (derived from above) avg: float # Batting average obp: float # On-base percentage slg: float # Slugging percentage pull_rate: float # Pull tendency percentage center_rate: float # Center field tendency slap_rate: float # Opposite field tendency ``` **Storage Strategy**: ```python # Cache BOTH rating sets per batter batting_ratings_vL: BattingRatings # vs Left-handed pitchers batting_ratings_vR: BattingRatings # vs Right-handed pitchers ``` **Usage**: - Play resolution (dice roll → result lookup) - Result selection (show available outcomes to player) - Probability calculations for AI decisions - Matchup analysis (L/R splits) **Total**: 27 float fields × 2 platoon splits = 54 values per batter --- ## Pitching Card Data ### Basic Pitching Attributes **Source**: `PitchingCard` model (pitchingcards.py) ```python class PitchingAttributes: """Pitcher-specific ratings and metadata""" # Chaos rolls (special events) balk: int = 0 # Balk rating (0-20, higher = more balks) wild_pitch: int = 0 # Wild pitch rating (0-20) hold: int = 0 # Pickoff/hold runner rating (0-20) # Pitcher usage starter_rating: int = 1 # Innings as starter (1-9+) relief_rating: int = 0 # Effectiveness in relief (0-20) closer_rating: Optional[int] # Closer rating if applicable # Pitcher batting batting: str = "#1WR-C" # Pitcher batting result code offense_col: Optional[int] # Offensive column (rarely used) # Handedness hand: str = 'R' # R, L ``` **Usage**: - Chaos roll resolution (wild pitch, balk checks) - Pickoff attempts - Fatigue/substitution decisions - Pitcher batting when DH not used ### Detailed Pitching Ratings (vs LHB and RHB) **Source**: `PitchingCardRatings` model (pitchingcardratings.py) **CRITICAL**: Each pitcher has TWO sets of ratings (vs LHB and vs RHB) ```python class PitchingRatings: """ Detailed pitching result probabilities (108 total chances) IMPORTANT: These ratings exist TWICE per card: - vs_hand = 'vL' (vs Left-handed batters) - vs_hand = 'vR' (vs Right-handed batters) """ vs_hand: str # 'vL' or 'vR' # Extra-base hits allowed (out of 108) homerun: float # Home runs allowed bp_homerun: float # Ballpark home runs triple: float # Triples allowed double_three: float # Doubles (3-base) double_two: float # Doubles (2-base) double_cf: float # Double to CF # Singles allowed single_two: float # Singles (2-base advancement) single_one: float # Singles (1-base) single_center: float # Singles to CF bp_single: float # Ballpark singles # Walks/HBP hbp: float # Hit batters walk: float # Walks issued # Strikeouts (good for pitcher!) strikeout: float # Strikeouts # Flyouts flyout_lf_b: float # Flyout to LF (B range) flyout_cf_b: float # Flyout to CF (B range) flyout_rf_b: float # Flyout to RF (B range) # Groundouts groundout_a: float # Groundout (A range) groundout_b: float # Groundout (B range) # X-Checks (difficult defensive plays by position) xcheck_p: float # X-check to pitcher xcheck_c: float # X-check to catcher xcheck_1b: float # X-check to first base xcheck_2b: float # X-check to second base xcheck_3b: float # X-check to third base xcheck_ss: float # X-check to shortstop xcheck_lf: float # X-check to left field xcheck_cf: float # X-check to center field xcheck_rf: float # X-check to right field # Calculated stats avg: float # Batting average against obp: float # OBP against slg: float # Slugging against ``` **Storage Strategy**: ```python # Cache BOTH rating sets per pitcher pitching_ratings_vL: PitchingRatings # vs Left-handed batters pitching_ratings_vR: PitchingRatings # vs Right-handed batters ``` **Usage**: - Play resolution when pitcher's card is rolled - X-check position determination - Matchup analysis - AI decision making **Total**: 30 float fields × 2 platoon splits = 60 values per pitcher --- ## Defensive Ratings (All Positions) **Source**: `CardPosition` model (cardpositions.py) **CRITICAL**: Players can have defensive ratings for MULTIPLE positions ```python class DefensivePosition: """ Defensive ratings for a specific position A player may have ratings for multiple positions. Example: Utility player might have ratings at 2B, SS, 3B, LF """ position: str # P, C, 1B, 2B, 3B, SS, LF, CF, RF, DH innings: int = 1 # Innings playable at position (1-9+) # Core defensive ratings range: int = 5 # Fielding range (1-9, 5 = average) error: int = 0 # Error frequency (0-20, lower = better) # Position-specific ratings arm: Optional[int] = None # Throwing arm (1-9) - Required for C, LF, CF, RF # Catcher-only ratings pb: Optional[int] = None # Passed ball rating (0-20) - Catchers only overthrow: Optional[int] = None # Overthrow rating (0-20) - Catchers only ``` **Storage Strategy**: ```python # Store as dictionary keyed by position defense_ratings: Dict[str, DefensivePosition] # Example: { 'SS': DefensivePosition(position='SS', range=7, error=5, innings=9), '2B': DefensivePosition(position='2B', range=6, error=6, innings=5), 'LF': DefensivePosition(position='LF', range=5, error=8, arm=6, innings=3) } ``` **Usage**: - X-check resolution (range + error checks) - Defensive substitution validation - Chaos rolls (catcher PB, overthrow) - Outfield throw calculations (arm rating) - Position eligibility checks **Validation Rules** (from Discord bot): - Catchers (C) MUST have: `arm`, `pb`, `overthrow` - Outfielders (LF, CF, RF) MUST have: `arm` - All positions have: `range`, `error` --- ## Complete Cached Player Model Combining all the above into one comprehensive structure: ```python from typing import Dict, Optional from pydantic import BaseModel class CachedPlayer(BaseModel): """ Complete player data cached in memory during active gameplay Estimated size: ~500 bytes per player Total for 20-player game: ~10KB """ # ======================================== # IDENTITY & DISPLAY (always present) # ======================================== lineup_id: int # Unique ID in this game's lineup player_id: int # Database player ID card_id: int # Specific card variant game_id: int # Support multiple conccurrent players name: str # Display name image: str # Card image URL headshot: Optional[str] # Player photo URL position: str # Current position (P, C, 1B, etc.) batting_order: Optional[int] # 1-9 or None hand: str # R, L, S # Team/set metadata cardset_id: Optional[int] # Only required in PD mlbclub: Optional[int] cost: Optional[int] # ======================================== # BATTING DATA (if position player) # ======================================== batting_attrs: Optional[BattingAttributes] = None batting_ratings_vL: Optional[BattingRatings] = None # vs LHP batting_ratings_vR: Optional[BattingRatings] = None # vs RHP # ======================================== # PITCHING DATA (if pitcher) # ======================================== pitching_attrs: Optional[PitchingAttributes] = None pitching_ratings_vL: Optional[PitchingRatings] = None # vs LHB pitching_ratings_vR: Optional[PitchingRatings] = None # vs RHB # ======================================== # DEFENSIVE DATA (all positions playable) # ======================================== defense_ratings: Dict[str, DefensivePosition] # ======================================== # COMPUTED FLAGS (for quick lookups) # ======================================== is_pitcher: bool # position == 'P' can_catch: bool # 'C' in defense_ratings # ======================================== # HELPER METHODS # ======================================== def get_batting_vs(self, pitcher_hand: str) -> Optional[BattingRatings]: """Get batting ratings based on pitcher handedness""" if pitcher_hand == 'L': return self.batting_ratings_vL else: # R or S return self.batting_ratings_vR def get_pitching_vs(self, batter_hand: str) -> Optional[PitchingRatings]: """Get pitching ratings based on batter handedness""" if batter_hand == 'L': return self.pitching_ratings_vL else: # R or S return self.pitching_ratings_vR def get_defense_at(self, pos: str) -> Optional[DefensivePosition]: """Get defensive ratings for specific position""" return self.defense_ratings.get(pos) def can_play_position(self, pos: str) -> bool: """Check if player can play position""" return pos in self.defense_ratings ``` --- ## Data Loading Strategy ### Initial Game Load ```python async def load_game_with_full_lineups(game_id: UUID): """ Load game and populate complete player cache This happens ONCE at game start, then players stay cached. """ # 1. Load basic game + lineup from database game = await db_ops.get_game(game_id) home_lineup = await db_ops.get_active_lineup(game_id, game.home_team_id) away_lineup = await db_ops.get_active_lineup(game_id, game.away_team_id) # 2. Fetch ALL player data from League API all_card_ids = [l.card_id for l in home_lineup + away_lineup] # Single API call for all players (batching) players_data = await league_api.fetch_complete_player_data( card_ids=all_card_ids, include_batting=True, include_pitching=True, include_defense=True, include_ratings=True # Both platoon splits ) # 3. Build CachedPlayer objects cached_players = { player_data['lineup_id']: CachedPlayer(**player_data) for player_data in players_data } # 4. Store in GameState state = GameState( game_id=game_id, home_lineup=cached_players_home, away_lineup=cached_players_away, # ... other state ) return state ``` ### Substitution Updates ```python async def make_substitution(game_id: UUID, old_player_id: int, new_card_id: int): """ Add new player to cache on substitution Only need to fetch ONE player's data. """ # Fetch complete data for new player new_player_data = await league_api.fetch_complete_player_data( card_ids=[new_card_id], include_batting=True, include_pitching=True, include_defense=True, include_ratings=True ) # Create cached player new_player = CachedPlayer(**new_player_data[0]) # Update state state = state_manager.get_state(game_id) if is_home_team: state.home_lineup[new_player.lineup_id] = new_player else: state.away_lineup[new_player.lineup_id] = new_player # Remove old player del state.home_lineup[old_player_id] ``` --- ## Usage Examples ### Example 1: Resolve Hit - Need Batter Ratings ```python async def resolve_hit(state: GameState): """Player decision selects result from available outcomes""" # Get current batter batter = state.get_current_batter() # Returns CachedPlayer # Get current pitcher for platoon matchup pitcher = state.get_current_pitcher() # Get appropriate batting ratings based on pitcher hand batting_ratings = batter.get_batting_vs(pitcher.hand) # Roll dice roll = dice.roll_d20() # Determine available results from rating chart # (This is where the 108-chance probabilities come into play) available_results = result_chart.get_options( roll=roll, ratings=batting_ratings ) # Return options to player for selection return { "roll": roll, "batter_image": batter.image, "available_results": available_results } ``` ### Example 2: X-Check - Need Random Fielder Range/Error ```python async def resolve_xcheck(state: GameState, position: str): """Difficult defensive play at specified position""" # Get fielder at position fielder_id = state.get_fielder_at_position(position) fielder = state.home_lineup[fielder_id] # Get defensive ratings for that position defense = fielder.get_defense_at(position) # Roll against range range_roll = dice.roll_d20() if range_roll <= defense.range: # In range - now check for error error_roll = dice.roll_d20() if error_roll <= defense.error: # ERROR! return PlayResult(outcome="error", fielder=fielder.name) else: # OUT! return PlayResult(outcome="out", fielder=fielder.name) else: # Out of range - HIT! return PlayResult(outcome="hit", fielder=fielder.name) ``` ### Example 3: Chaos Roll - Need Catcher PB and Pitcher WP ```python async def check_chaos_event(state: GameState): """Wild pitch or passed ball check when runners on base""" if not state.runners: return None # No chaos without runners # Get pitcher and catcher pitcher = state.get_current_pitcher() catcher = state.get_current_catcher() # Roll for chaos chaos_roll = dice.roll_d20() # Check wild pitch first if chaos_roll <= pitcher.pitching_attrs.wild_pitch: return ChaosEvent( type="wild_pitch", pitcher=pitcher.name, advance_runners=True ) # Check passed ball catcher_defense = catcher.get_defense_at('C') if chaos_roll <= catcher_defense.pb: return ChaosEvent( type="passed_ball", catcher=catcher.name, advance_runners=True ) return None # No chaos this time ``` ### Example 4: Stolen Base - Need Batter Steal Ratings ```python async def attempt_stolen_base(state: GameState, runner_id: int, target_base: int): """Runner attempts to steal""" runner = state.get_runner_by_id(runner_id) catcher = state.get_current_catcher() pitcher = state.get_current_pitcher() # Check if runner can attempt if runner.batting_attrs.steal_auto: # Auto-steal success! return StealResult(success=True, reason="auto_steal") # Roll dice steal_roll = dice.roll_d20() # Check against runner's steal range if steal_roll < runner.batting_attrs.steal_low: return StealResult(success=False, reason="too_low", caught=True) if steal_roll > runner.batting_attrs.steal_high: return StealResult(success=False, reason="too_high", caught=True) # In range - now apply modifiers # - runner.batting_attrs.steal_jump # - pitcher.pitching_attrs.hold # - catcher defensive arm rating # ... complex calculation ... return StealResult(success=True) ``` --- ## Memory Usage Breakdown ``` Per Player: - Identity fields: ~100 bytes - Batting attributes: ~50 bytes - Batting ratings (2 platoons × 27 fields): ~200 bytes - Pitching attributes: ~50 bytes - Pitching ratings (2 platoons × 30 fields): ~240 bytes - Defense ratings (avg 2 positions): ~100 bytes ----------------------------------------------- Total per player: ~740 bytes (conservative estimate) Per Game (20 players): ~740 bytes × 20 = ~14.8 KB 100 concurrent games: ~14.8 KB × 100 = ~1.48 MB CONCLUSION: Memory usage is negligible! ``` --- ## Cache Invalidation Strategy **When to update cache:** 1. **Game Start**: Load full lineups 2. **Substitution**: Add new player, mark old as inactive 3. **Half Inning**: Update current pitcher/catcher IDs 4. **Game End**: Clear from memory **When NOT to update cache:** - After each play (only update game state counters) - For historical data (read from DB on demand) - For box scores (aggregate from DB) **Consistency**: - In-memory cache is source of truth for active games - Database is async backup for crash recovery - On crash: Rebuild cache from DB + last saved state --- ## SBA League Simplifications For SBA league (simpler model), many fields will be `None` or defaults: **SBA Players will have:** - ✅ Basic identity (name, image, position) - ✅ Basic batting attributes (stealing, bunting, running) - ❌ NO detailed batting ratings (SBA uses simplified charts) - ❌ NO detailed pitching ratings **Implementation**: ```python # For SBA, ratings are simpler class SbaPlayer(CachedPlayer): """SBA league players have minimal data""" batting_ratings_vL: None # Not used in SBA batting_ratings_vR: None # Uses simplified result selection instead ``` --- ## Next Steps 1. **Review & Supplement**: Review this catalog and add any missing fields 2. **Implement Models**: Create Pydantic models matching this spec 3. **API Client**: Build API client to fetch this data 4. **State Manager**: Integrate into StateManager cache 5. **Test Loading**: Verify data loads correctly and completely --- **Document Status**: Draft - Ready for Review **Last Updated**: 2025-10-22 **Next Review**: Before Week 4 implementation begins