strat-gameplay-webapp/backend/terminal_client/update_docs/phase_5.md
Cal Corum 1c32787195 CLAUDE: Refactor game models and modularize terminal client
This commit includes cleanup from model refactoring and terminal client
modularization for better code organization and maintainability.

## Game Models Refactor

**Removed RunnerState class:**
- Eliminated separate RunnerState model (was redundant)
- Replaced runners: List[RunnerState] with direct base references:
  - on_first: Optional[LineupPlayerState]
  - on_second: Optional[LineupPlayerState]
  - on_third: Optional[LineupPlayerState]
- Updated helper methods:
  - get_runner_at_base() now returns LineupPlayerState directly
  - get_all_runners() returns List[Tuple[int, LineupPlayerState]]
  - is_runner_on_X() simplified to direct None checks

**Benefits:**
- Matches database structure (plays table has on_first_id, etc.)
- Simpler state management (direct references vs list management)
- Better type safety (LineupPlayerState vs generic runner)
- Easier to work with in game engine logic

**Updated files:**
- app/models/game_models.py - Removed RunnerState, updated GameState
- app/core/play_resolver.py - Use get_all_runners() instead of state.runners
- app/core/validators.py - Updated runner access patterns
- tests/unit/models/test_game_models.py - Updated test assertions
- tests/unit/core/test_play_resolver.py - Updated test data
- tests/unit/core/test_validators.py - Updated test data

## Terminal Client Refactor

**Modularization (DRY principle):**
Created separate modules for better code organization:

1. **terminal_client/commands.py** (10,243 bytes)
   - Shared command functions for game operations
   - Used by both CLI (main.py) and REPL (repl.py)
   - Functions: submit_defensive_decision, submit_offensive_decision,
     resolve_play, quick_play_sequence
   - Single source of truth for command logic

2. **terminal_client/arg_parser.py** (7,280 bytes)
   - Centralized argument parsing and validation
   - Handles defensive/offensive decision arguments
   - Validates formats (alignment, depths, hold runners, steal attempts)

3. **terminal_client/completions.py** (10,357 bytes)
   - TAB completion support for REPL mode
   - Command completions, option completions, dynamic completions
   - Game ID completions, defensive/offensive option suggestions

4. **terminal_client/help_text.py** (10,839 bytes)
   - Centralized help text and command documentation
   - Detailed command descriptions
   - Usage examples for all commands

**Updated main modules:**
- terminal_client/main.py - Simplified by using shared commands module
- terminal_client/repl.py - Cleaner with shared functions and completions

**Benefits:**
- DRY: Behavior consistent between CLI and REPL modes
- Maintainability: Changes in one place affect both interfaces
- Testability: Can test commands module independently
- Organization: Clear separation of concerns

## Documentation

**New files:**
- app/models/visual_model_relationships.md
  - Visual documentation of model relationships
  - Helps understand data flow between models
- terminal_client/update_docs/ (6 phase documentation files)
  - Phased documentation for terminal client evolution
  - Historical context for implementation decisions

## Tests

**New test files:**
- tests/unit/terminal_client/__init__.py
- tests/unit/terminal_client/test_arg_parser.py
- tests/unit/terminal_client/test_commands.py
- tests/unit/terminal_client/test_completions.py
- tests/unit/terminal_client/test_help_text.py

**Updated tests:**
- Integration tests updated for new runner model
- Unit tests updated for model changes
- All tests passing with new structure

## Summary

-  Simplified game state model (removed RunnerState)
-  Better alignment with database structure
-  Modularized terminal client (DRY principle)
-  Shared command logic between CLI and REPL
-  Comprehensive test coverage
-  Improved documentation

Total changes: 26 files modified/created

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 14:16:38 -05:00

27 KiB

● Terminal Client Improvement Plan - Part 5: Player Name Caching and Display

Overview

Enhance the display system to show player names, positions, and stats instead of just lineup IDs. This requires integration with the player model system from Week 6 and adds a caching layer for performance.

Note: This enhancement requires Week 6 player models to be implemented first. The code provided here creates the infrastructure that will be activated once player models are available.

Files to Create

  1. Create backend/terminal_client/player_cache.py

""" Player data caching for terminal client.

Caches player/card data from league APIs to avoid repeated lookups and provide fast name/stat display in the REPL.

Author: Claude Date: 2025-10-27 """ import logging from typing import Dict, Optional, Any from uuid import UUID from dataclasses import dataclass from datetime import datetime, timedelta

logger = logging.getLogger(f'{name}.player_cache')

@dataclass class CachedPlayer: """Cached player data."""

  # Identification
  card_id: Optional[int] = None
  player_id: Optional[int] = None

  # Basic info
  name: str = "Unknown"
  position: str = "?"
  team_id: int = 0

  # Display info
  image_url: Optional[str] = None
  handedness: Optional[str] = None

  # League-specific
  league: str = "unknown"

  # Cache metadata
  cached_at: datetime = None

  def get_display_name(self) -> str:
      """Get formatted display name."""
      if self.handedness:
          hand_symbol = {"R": "⟩", "L": "⟨", "S": "⟨⟩"}.get(self.handedness, "")
          return f"{self.name} {hand_symbol} ({self.position})"
      return f"{self.name} ({self.position})"

  def get_short_display(self) -> str:
      """Get short display format."""
      return f"{self.name} {self.position}"

class PlayerCache: """ Cache for player data to avoid repeated API lookups.

  Maintains in-memory cache with TTL and per-game player rosters.
  """

  def __init__(self, ttl_seconds: int = 3600):
      """
      Initialize player cache.
      
      Args:
          ttl_seconds: Time to live for cached entries (default: 1 hour)
      """
      self.ttl_seconds = ttl_seconds

      # Cache by card_id (PD league)
      self._card_cache: Dict[int, CachedPlayer] = {}

      # Cache by player_id (SBA league)
      self._player_cache: Dict[int, CachedPlayer] = {}

      # Cache by lineup_id for quick lookups
      self._lineup_cache: Dict[int, CachedPlayer] = {}

      # Track which game uses which players
      self._game_rosters: Dict[UUID, Dict[int, CachedPlayer]] = {}

      logger.info(f"PlayerCache initialized with TTL={ttl_seconds}s")

  def get_by_lineup_id(self, lineup_id: int) -> Optional[CachedPlayer]:
      """
      Get player by lineup ID (fastest lookup for display).
      
      Args:
          lineup_id: Lineup entry ID
          
      Returns:
          CachedPlayer or None if not cached
      """
      player = self._lineup_cache.get(lineup_id)

      if player and self._is_expired(player):
          logger.debug(f"Lineup cache expired for {lineup_id}")
          del self._lineup_cache[lineup_id]
          return None

      return player

  def get_by_card_id(self, card_id: int) -> Optional[CachedPlayer]:
      """
      Get player by card ID (PD league).
      
      Args:
          card_id: Card ID
          
      Returns:
          CachedPlayer or None if not cached
      """
      player = self._card_cache.get(card_id)

      if player and self._is_expired(player):
          logger.debug(f"Card cache expired for {card_id}")
          del self._card_cache[card_id]
          return None

      return player

  def get_by_player_id(self, player_id: int) -> Optional[CachedPlayer]:
      """
      Get player by player ID (SBA league).
      
      Args:
          player_id: Player ID
          
      Returns:
          CachedPlayer or None if not cached
      """
      player = self._player_cache.get(player_id)

      if player and self._is_expired(player):
          logger.debug(f"Player cache expired for {player_id}")
          del self._player_cache[player_id]
          return None

      return player

  def add_player(
      self,
      player_data: Dict[str, Any],
      lineup_id: Optional[int] = None
  ) -> CachedPlayer:
      """
      Add player to cache.
      
      Args:
          player_data: Player data from database/API
          lineup_id: Optional lineup ID for quick lookups
          
      Returns:
          CachedPlayer instance
      """
      now = datetime.utcnow()

      player = CachedPlayer(
          card_id=player_data.get('card_id'),
          player_id=player_data.get('player_id'),
          name=player_data.get('name', 'Unknown'),
          position=player_data.get('position', '?'),
          team_id=player_data.get('team_id', 0),
          image_url=player_data.get('image_url'),
          handedness=player_data.get('handedness'),
          league=player_data.get('league', 'unknown'),
          cached_at=now
      )

      # Cache by appropriate ID
      if player.card_id:
          self._card_cache[player.card_id] = player
          logger.debug(f"Cached card {player.card_id}: {player.name}")

      if player.player_id:
          self._player_cache[player.player_id] = player
          logger.debug(f"Cached player {player.player_id}: {player.name}")

      # Cache by lineup ID if provided
      if lineup_id:
          self._lineup_cache[lineup_id] = player
          logger.debug(f"Cached lineup {lineup_id}: {player.name}")

      return player

  async def load_game_roster(self, game_id: UUID, league: str) -> int:
      """
      Load all players for a game into cache.
      
      This should be called when switching to a game to pre-populate
      the cache with all players that will be referenced.
      
      Args:
          game_id: Game UUID
          league: League ID ('sba' or 'pd')
          
      Returns:
          Number of players loaded
      """
      # Import here to avoid circular dependency
      from app.database.operations import DatabaseOperations

      db_ops = DatabaseOperations()

      try:
          # Get both team lineups
          home_lineup = await db_ops.get_active_lineup(game_id, None)  # Will need team_id
          away_lineup = await db_ops.get_active_lineup(game_id, None)

          all_players = home_lineup + away_lineup
          game_roster = {}

          for lineup_entry in all_players:
              # Extract player data based on league
              player_data = {
                  'card_id': getattr(lineup_entry, 'card_id', None),
                  'player_id': getattr(lineup_entry, 'player_id', None),
                  'name': getattr(lineup_entry, 'name', 'Unknown'),
                  'position': lineup_entry.position,
                  'team_id': lineup_entry.team_id,
                  'league': league
              }

              # Add to cache
              player = self.add_player(player_data, lineup_id=lineup_entry.id)
              game_roster[lineup_entry.id] = player

          # Store game roster mapping
          self._game_rosters[game_id] = game_roster

          logger.info(f"Loaded {len(all_players)} players for game {game_id}")
          return len(all_players)

      except Exception as e:
          logger.error(f"Failed to load game roster: {e}", exc_info=True)
          return 0

  def get_game_roster(self, game_id: UUID) -> Dict[int, CachedPlayer]:
      """
      Get cached roster for a game.
      
      Args:
          game_id: Game UUID
          
      Returns:
          Dictionary mapping lineup_id to CachedPlayer
      """
      return self._game_rosters.get(game_id, {})

  def clear_game(self, game_id: UUID) -> None:
      """
      Clear cached data for a specific game.
      
      Args:
          game_id: Game UUID
      """
      if game_id in self._game_rosters:
          del self._game_rosters[game_id]
          logger.info(f"Cleared cache for game {game_id}")

  def clear_expired(self) -> int:
      """
      Remove expired entries from cache.
      
      Returns:
          Number of entries removed
      """
      count = 0

      # Clear card cache
      expired_cards = [
          cid for cid, player in self._card_cache.items()
          if self._is_expired(player)
      ]
      for cid in expired_cards:
          del self._card_cache[cid]
          count += 1

      # Clear player cache
      expired_players = [
          pid for pid, player in self._player_cache.items()
          if self._is_expired(player)
      ]
      for pid in expired_players:
          del self._player_cache[pid]
          count += 1

      # Clear lineup cache
      expired_lineups = [
          lid for lid, player in self._lineup_cache.items()
          if self._is_expired(player)
      ]
      for lid in expired_lineups:
          del self._lineup_cache[lid]
          count += 1

      if count > 0:
          logger.info(f"Cleared {count} expired cache entries")

      return count

  def get_stats(self) -> Dict[str, int]:
      """
      Get cache statistics.
      
      Returns:
          Dictionary with cache stats
      """
      return {
          'card_cache_size': len(self._card_cache),
          'player_cache_size': len(self._player_cache),
          'lineup_cache_size': len(self._lineup_cache),
          'games_cached': len(self._game_rosters),
          'ttl_seconds': self.ttl_seconds
      }

  def _is_expired(self, player: CachedPlayer) -> bool:
      """Check if cached player is expired."""
      if player.cached_at is None:
          return True

      age = (datetime.utcnow() - player.cached_at).total_seconds()
      return age > self.ttl_seconds

Global cache instance

player_cache = PlayerCache()

  1. Create backend/terminal_client/enhanced_display.py

""" Enhanced display functions with player names.

Extends the base display module with player name lookups and richer formatting.

Author: Claude Date: 2025-10-27 """ import logging from typing import Optional from rich.text import Text

from app.models.game_models import GameState from terminal_client import display from terminal_client.player_cache import player_cache, CachedPlayer

logger = logging.getLogger(f'{name}.enhanced_display')

def get_player_display(lineup_id: Optional[int]) -> str: """ Get display string for a player by lineup ID.

  Args:
      lineup_id: Lineup entry ID
      
  Returns:
      Formatted player string (name if available, ID otherwise)
  """
  if lineup_id is None:
      return "None"

  # Try to get from cache
  player = player_cache.get_by_lineup_id(lineup_id)

  if player:
      return player.get_short_display()

  # Fall back to lineup ID
  return f"Lineup #{lineup_id}"

def display_game_state_enhanced(state: GameState) -> None: """ Display game state with player names (enhanced version).

  This is a drop-in replacement for display.display_game_state()
  that adds player names when available.
  
  Args:
      state: Current game state
  """
  from rich.panel import Panel
  from rich import box

  # Status color based on game status
  status_color = {
      "pending": "yellow",
      "active": "green",
      "paused": "yellow",
      "completed": "blue"
  }

  # Build state display
  state_text = Text()
  state_text.append(f"Game ID: {state.game_id}\n", style="bold")
  state_text.append(f"League: {state.league_id.upper()}\n")
  state_text.append(f"Status: ", style="bold")
  state_text.append(f"{state.status}\n", style=status_color.get(state.status, "white"))
  state_text.append("\n")

  # Score
  state_text.append("Score: ", style="bold cyan")
  state_text.append(f"Away {state.away_score} - {state.home_score} Home\n", style="cyan")

  # Inning
  state_text.append("Inning: ", style="bold magenta")
  state_text.append(f"{state.inning} {state.half.capitalize()}\n", style="magenta")

  # Outs
  state_text.append("Outs: ", style="bold yellow")
  state_text.append(f"{state.outs}\n", style="yellow")

  # Runners (enhanced with names)
  runners = state.get_all_runners()
  if runners:
      state_text.append("\nRunners: ", style="bold green")
      runner_displays = []
      for base, runner in runners:
          player_name = get_player_display(runner.lineup_id)
          runner_displays.append(f"{base}B: {player_name}")
      state_text.append(f"{', '.join(runner_displays)}\n", style="green")
  else:
      state_text.append("\nBases: ", style="bold")
      state_text.append("Empty\n", style="dim")

  # Current players (enhanced with names)
  if state.current_batter_lineup_id:
      batter_name = get_player_display(state.current_batter_lineup_id)
      state_text.append(f"\nBatter: {batter_name}\n")

  if state.current_pitcher_lineup_id:
      pitcher_name = get_player_display(state.current_pitcher_lineup_id)
      state_text.append(f"Pitcher: {pitcher_name}\n")

  # Pending decision
  if state.pending_decision:
      state_text.append(f"\nPending: ", style="bold red")
      state_text.append(f"{state.pending_decision} decision\n", style="red")

  # Last play result
  if state.last_play_result:
      state_text.append(f"\nLast Play: ", style="bold")
      state_text.append(f"{state.last_play_result}\n", style="italic")

  # Display panel
  panel = Panel(
      state_text,
      title=f"[bold]Game State[/bold]",
      border_style="blue",
      box=box.ROUNDED
  )
  display.console.print(panel)

def show_cache_stats() -> None: """Display player cache statistics.""" stats = player_cache.get_stats()

  display.console.print("\n[bold cyan]Player Cache Statistics:[/bold cyan]")
  display.console.print(f"  Card cache: {stats['card_cache_size']} entries")
  display.console.print(f"  Player cache: {stats['player_cache_size']} entries")
  display.console.print(f"  Lineup cache: {stats['lineup_cache_size']} entries")
  display.console.print(f"  Games cached: {stats['games_cached']}")
  display.console.print(f"  TTL: {stats['ttl_seconds']} seconds\n")

Files to Update

  1. Update backend/terminal_client/repl.py

Add imports at top

from terminal_client.player_cache import player_cache from terminal_client.enhanced_display import ( display_game_state_enhanced, show_cache_stats )

class GameREPL(GameREPLCompletions, cmd.Cmd): # ... existing code ...

  async def _ensure_game_loaded(self, game_id: UUID) -> None:
      """
      Ensure game is loaded in state_manager.
      
      If game exists in database but not in memory, recover it.
      Also pre-load player cache for better display.
      """
      # Check if already in memory
      state = state_manager.get_state(game_id)
      if state is not None:
          # Game loaded, check if cache needs refresh
          roster = player_cache.get_game_roster(game_id)
          if not roster:
              # Load player names into cache
              try:
                  count = await player_cache.load_game_roster(game_id, state.league_id)
                  if count > 0:
                      logger.debug(f"Loaded {count} players into cache")
              except Exception as e:
                  logger.warning(f"Could not load player cache: {e}")
          return

      # Try to recover from database
      try:
          display.print_info(f"Loading game {game_id} from database...")
          recovered_state = await state_manager.recover_game(game_id)

          if recovered_state and recovered_state.status == "active":
              await game_engine._prepare_next_play(recovered_state)
              logger.debug(f"Prepared snapshot for recovered game {game_id}")

              # Load player cache
              try:
                  count = await player_cache.load_game_roster(
                      game_id,
                      recovered_state.league_id
                  )
                  if count > 0:
                      display.print_success(f"Loaded {count} players into display cache")
              except Exception as e:
                  logger.warning(f"Could not load player cache: {e}")

          display.print_success("Game loaded successfully")
      except Exception as e:
          display.print_error(f"Failed to load game: {e}")
          logger.error(f"Game recovery failed for {game_id}: {e}", exc_info=True)
          raise ValueError(f"Game {game_id} not found")

  # Add new command for cache management
  def do_cache(self, arg):
      """
      Manage player cache.
      
      Usage:
        cache              Show cache statistics
        cache clear        Clear expired entries
        cache reload       Reload current game roster
      """
      async def _cache():
          args = arg.split()

          if not args or args[0] == 'stats':
              # Show stats
              show_cache_stats()

          elif args[0] == 'clear':
              # Clear expired
              count = player_cache.clear_expired()
              display.print_success(f"Cleared {count} expired entries")

          elif args[0] == 'reload':
              # Reload current game
              try:
                  gid = self._ensure_game()
                  state = state_manager.get_state(gid)
                  if state:
                      count = await player_cache.load_game_roster(gid, state.league_id)
                      display.print_success(f"Loaded {count} players")
                  else:
                      display.print_error("No game state found")
              except ValueError:
                  pass

          else:
              display.print_error(f"Unknown cache command: {args[0]}")
              display.print_info("Usage: cache [stats|clear|reload]")

      self._run_async(_cache())

  # Update status command to use enhanced display
  def do_status(self, arg):
      """Display current game state with player names."""
      async def _status():
          try:
              gid = self._ensure_game()
              await self._ensure_game_loaded(gid)

              state = await game_engine.get_game_state(gid)
              if state:
                  # Use enhanced display if cache is populated
                  roster = player_cache.get_game_roster(gid)
                  if roster:
                      display_game_state_enhanced(state)
                  else:
                      display.display_game_state(state)
              else:
                  display.print_error("Game state not found")

          except ValueError:
              pass
          except Exception as e:
              display.print_error(f"Failed: {e}")

      self._run_async(_status())

Testing Plan

  1. Create backend/tests/unit/terminal_client/test_player_cache.py

""" Unit tests for player cache. """ import pytest from datetime import datetime, timedelta from uuid import uuid4

from terminal_client.player_cache import PlayerCache, CachedPlayer

class TestCachedPlayer: """Tests for CachedPlayer dataclass."""

  def test_get_display_name_with_handedness(self):
      """Test display name includes handedness symbol."""
      player = CachedPlayer(
          name="Mike Trout",
          position="CF",
          handedness="R"
      )
      assert "⟩" in player.get_display_name()
      assert "Mike Trout" in player.get_display_name()
      assert "CF" in player.get_display_name()

  def test_get_display_name_without_handedness(self):
      """Test display name without handedness."""
      player = CachedPlayer(
          name="Mike Trout",
          position="CF"
      )
      result = player.get_display_name()
      assert result == "Mike Trout (CF)"

  def test_get_short_display(self):
      """Test short display format."""
      player = CachedPlayer(
          name="Mike Trout",
          position="CF"
      )
      assert player.get_short_display() == "Mike Trout CF"

class TestPlayerCache: """Tests for PlayerCache."""

  @pytest.fixture
  def cache(self):
      """Create fresh cache for each test."""
      return PlayerCache(ttl_seconds=3600)

  def test_add_and_get_by_card_id(self, cache):
      """Test adding and retrieving by card ID."""
      player_data = {
          'card_id': 123,
          'name': 'Test Player',
          'position': 'SS',
          'team_id': 1,
          'league': 'pd'
      }

      player = cache.add_player(player_data)

      assert player.name == 'Test Player'
      assert player.position == 'SS'

      # Retrieve by card ID
      retrieved = cache.get_by_card_id(123)
      assert retrieved is not None
      assert retrieved.name == 'Test Player'

  def test_add_and_get_by_player_id(self, cache):
      """Test adding and retrieving by player ID."""
      player_data = {
          'player_id': 456,
          'name': 'Test Player',
          'position': 'CF',
          'team_id': 2,
          'league': 'sba'
      }

      player = cache.add_player(player_data)

      # Retrieve by player ID
      retrieved = cache.get_by_player_id(456)
      assert retrieved is not None
      assert retrieved.name == 'Test Player'

  def test_add_with_lineup_id(self, cache):
      """Test adding with lineup ID for quick lookups."""
      player_data = {
          'card_id': 123,
          'name': 'Test Player',
          'position': 'SS',
          'team_id': 1,
          'league': 'pd'
      }

      player = cache.add_player(player_data, lineup_id=10)

      # Retrieve by lineup ID
      retrieved = cache.get_by_lineup_id(10)
      assert retrieved is not None
      assert retrieved.name == 'Test Player'

  def test_expired_entry_removed(self, cache):
      """Test that expired entries are not returned."""
      cache_short_ttl = PlayerCache(ttl_seconds=0)

      player_data = {
          'card_id': 123,
          'name': 'Test Player',
          'position': 'SS',
          'team_id': 1,
          'league': 'pd'
      }

      player = cache_short_ttl.add_player(player_data)
      player.cached_at = datetime.utcnow() - timedelta(seconds=10)

      # Should return None for expired entry
      retrieved = cache_short_ttl.get_by_card_id(123)
      assert retrieved is None

  def test_clear_expired(self, cache):
      """Test clearing expired entries."""
      # Add fresh entry
      cache.add_player({
          'card_id': 1,
          'name': 'Fresh',
          'position': 'P',
          'team_id': 1,
          'league': 'pd'
      })

      # Add expired entry
      old_player = cache.add_player({
          'card_id': 2,
          'name': 'Old',
          'position': 'C',
          'team_id': 1,
          'league': 'pd'
      })
      old_player.cached_at = datetime.utcnow() - timedelta(seconds=10000)

      # Clear expired
      count = cache.clear_expired()

      assert count > 0
      assert cache.get_by_card_id(1) is not None  # Fresh still there
      assert cache.get_by_card_id(2) is None  # Old removed

  def test_get_stats(self, cache):
      """Test getting cache statistics."""
      cache.add_player({'card_id': 1, 'name': 'P1', 'position': 'P', 'team_id': 1, 'league': 'pd'})
      cache.add_player({'player_id': 2, 'name': 'P2', 'position': 'C', 'team_id': 1, 'league': 'sba'})

      stats = cache.get_stats()

      assert stats['card_cache_size'] == 1
      assert stats['player_cache_size'] == 1
      assert stats['ttl_seconds'] == 3600

  def test_clear_game(self, cache):
      """Test clearing game-specific cache."""
      game_id = uuid4()

      # Simulate game roster
      cache._game_rosters[game_id] = {
          1: CachedPlayer(name="Player 1", position="P"),
          2: CachedPlayer(name="Player 2", position="C")
      }

      assert len(cache.get_game_roster(game_id)) == 2

      cache.clear_game(game_id)

      assert len(cache.get_game_roster(game_id)) == 0

Configuration and Activation

  1. Create backend/terminal_client/config.py update

Add to existing config.py

class ClientConfig: """Configuration for terminal client features."""

  # Feature flags
  ENABLE_PLAYER_NAMES = False  # Set to True when Week 6 models are ready
  ENABLE_PLAYER_CACHE = False  # Set to True when Week 6 models are ready

  # Cache settings
  PLAYER_CACHE_TTL = 3600  # 1 hour
  AUTO_LOAD_ROSTER = True  # Auto-load roster when switching games

  @classmethod
  def is_enhanced_display_enabled(cls) -> bool:
      """Check if enhanced display with player names is enabled."""
      return cls.ENABLE_PLAYER_NAMES and cls.ENABLE_PLAYER_CACHE

Activation Instructions (For Future)

When Week 6 player models are implemented:

  1. Update feature flags:

In terminal_client/config.py

ENABLE_PLAYER_NAMES = True ENABLE_PLAYER_CACHE = True 2. Implement player data fetching: - Add methods to fetch player data from league APIs - Integrate with backend/app/data/api_client.py 3. Update database operations: - Ensure lineup queries return player names - Add joins to get player/card details 4. Test with real data: - Verify cache performance - Check TTL behavior - Validate display formatting

Example Output (When Activated)

Before (Current): Batter: Lineup #1 Pitcher: Lineup #10 Runners: 1B(#2), 3B(#5)

After (With Player Names): Batter: Mike Trout CF Pitcher: Clayton Kershaw P Runners: 1B: Aaron Judge RF, 3B: Mookie Betts RF

Benefits

  1. Better UX: See player names instead of cryptic IDs
  2. Performance: Caching prevents repeated database/API lookups
  3. Flexibility: Works with both PD and SBA leagues
  4. Future-ready: Infrastructure ready for Week 6 integration
  5. Optional: Can be disabled if not needed