strat-gameplay-webapp/backend/terminal_client/update_docs/phase_4.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

22 KiB

● Terminal Client Improvement Plan - Part 4: Detailed Help System

Overview

Enhance the REPL help system with detailed documentation, examples, and better formatting. This makes the terminal client self-documenting and easier to use for new developers.

Files to Create

  1. Create backend/terminal_client/help_text.py

""" Help text and documentation for terminal client commands.

Provides detailed, formatted help text for all REPL commands with usage examples and option descriptions.

Author: Claude Date: 2025-10-27 """ from typing import Dict from rich.console import Console from rich.table import Table from rich.panel import Panel from rich.markdown import Markdown from rich import box

console = Console()

class HelpFormatter: """Format and display help text for commands."""

  @staticmethod
  def show_command_help(command_name: str, help_data: Dict) -> None:
      """
      Display detailed help for a specific command.
      
      Args:
          command_name: Name of the command
          help_data: Dictionary with help information
              {
                  'summary': 'Brief description',
                  'usage': 'command [OPTIONS]',
                  'options': [
                      {'name': '--option', 'type': 'TYPE', 'desc': 'Description'}
                  ],
                  'examples': ['example 1', 'example 2']
              }
      """
      # Build help text
      help_text = []

      # Summary
      help_text.append(f"**{command_name}** - {help_data.get('summary', 'No description')}")
      help_text.append("")

      # Usage
      if 'usage' in help_data:
          help_text.append("**USAGE:**")
          help_text.append(f"  {help_data['usage']}")
          help_text.append("")

      # Options
      if 'options' in help_data and help_data['options']:
          help_text.append("**OPTIONS:**")

          # Create options table
          table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
          table.add_column("Option", style="cyan", no_wrap=True)
          table.add_column("Type", style="yellow")
          table.add_column("Description", style="white")

          for opt in help_data['options']:
              table.add_row(
                  opt['name'],
                  opt.get('type', ''),
                  opt.get('desc', '')
              )

          console.print(table)
          console.print()

      # Examples
      if 'examples' in help_data and help_data['examples']:
          help_text.append("**EXAMPLES:**")
          for example in help_data['examples']:
              help_text.append(f"  {example}")
          help_text.append("")

      # Display in panel
      if help_text:
          md = Markdown("\n".join(help_text[:3]))  # Just summary and usage
          panel = Panel(
              md,
              title=f"[bold cyan]Help: {command_name}[/bold cyan]",
              border_style="cyan",
              box=box.ROUNDED
          )
          console.print(panel)

          # Print rest outside panel for better formatting
          if len(help_text) > 3:
              console.print()
              for line in help_text[3:]:
                  if line.startswith('**'):
                      console.print(line.replace('**', ''), style="bold cyan")
                  else:
                      console.print(line)

  @staticmethod
  def show_command_list() -> None:
      """Display list of all available commands."""
      console.print("\n[bold cyan]Available Commands:[/bold cyan]\n")

      # Game Management
      console.print("[bold yellow]Game Management:[/bold yellow]")
      console.print("  new_game      Create a new game with test lineups and start it")
      console.print("  list_games    List all games in state manager")
      console.print("  use_game      Switch to a different game")
      console.print("  status        Display current game state")
      console.print("  box_score     Display box score")
      console.print()

      # Gameplay
      console.print("[bold yellow]Gameplay:[/bold yellow]")
      console.print("  defensive     Submit defensive decision")
      console.print("  offensive     Submit offensive decision")
      console.print("  resolve       Resolve the current play")
      console.print("  quick_play    Auto-play multiple plays")
      console.print()

      # Utilities
      console.print("[bold yellow]Utilities:[/bold yellow]")
      console.print("  config        Show configuration")
      console.print("  clear         Clear the screen")
      console.print("  help          Show help for commands")
      console.print("  quit/exit     Exit the REPL")
      console.print()

      console.print("[dim]Type 'help <command>' for detailed information.[/dim]")
      console.print("[dim]Use TAB for auto-completion of commands and options.[/dim]\n")

Detailed help data for each command

HELP_DATA = { 'new_game': { 'summary': 'Create a new game with test lineups and start it immediately', 'usage': 'new_game [--league LEAGUE] [--home-team ID] [--away-team ID]', 'options': [ { 'name': '--league', 'type': 'sba|pd', 'desc': 'League type (default: sba)' }, { 'name': '--home-team', 'type': 'INT', 'desc': 'Home team ID (default: 1)' }, { 'name': '--away-team', 'type': 'INT', 'desc': 'Away team ID (default: 2)' } ], 'examples': [ 'new_game', 'new_game --league pd', 'new_game --league sba --home-team 5 --away-team 3' ] },

  'defensive': {
      'summary': 'Submit defensive decision for the current play',
      'usage': 'defensive [--alignment TYPE] [--infield DEPTH] [--outfield DEPTH] [--hold BASES]',
      'options': [
          {
              'name': '--alignment',
              'type': 'STRING',
              'desc': 'Defensive alignment: normal, shifted_left, shifted_right, extreme_shift (default: normal)'
          },
          {
              'name': '--infield',
              'type': 'STRING',
              'desc': 'Infield depth: in, normal, back, double_play (default: normal)'
          },
          {
              'name': '--outfield',
              'type': 'STRING',
              'desc': 'Outfield depth: in, normal, back (default: normal)'
          },
          {
              'name': '--hold',
              'type': 'LIST',
              'desc': 'Comma-separated bases to hold runners: 1,2,3 (default: none)'
          }
      ],
      'examples': [
          'defensive',
          'defensive --alignment shifted_left',
          'defensive --infield double_play --hold 1,3',
          'defensive --alignment extreme_shift --infield back --outfield back'
      ]
  },

  'offensive': {
      'summary': 'Submit offensive decision for the current play',
      'usage': 'offensive [--approach TYPE] [--steal BASES] [--hit-run] [--bunt]',
      'options': [
          {
              'name': '--approach',
              'type': 'STRING',
              'desc': 'Batting approach: normal, contact, power, patient (default: normal)'
          },
          {
              'name': '--steal',
              'type': 'LIST',
              'desc': 'Comma-separated bases to steal: 2,3 (default: none)'
          },
          {
              'name': '--hit-run',
              'type': 'FLAG',
              'desc': 'Execute hit-and-run play (default: false)'
          },
          {
              'name': '--bunt',
              'type': 'FLAG',
              'desc': 'Attempt bunt (default: false)'
          }
      ],
      'examples': [
          'offensive',
          'offensive --approach power',
          'offensive --steal 2',
          'offensive --steal 2,3 --hit-run',
          'offensive --approach contact --bunt'
      ]
  },

  'resolve': {
      'summary': 'Resolve the current play using submitted decisions',
      'usage': 'resolve',
      'options': [],
      'examples': [
          'resolve'
      ],
      'notes': 'Both defensive and offensive decisions must be submitted before resolving.'
  },

  'quick_play': {
      'summary': 'Auto-play multiple plays with default decisions',
      'usage': 'quick_play [COUNT]',
      'options': [
          {
              'name': 'COUNT',
              'type': 'INT',
              'desc': 'Number of plays to execute (default: 1). Positional argument.'
          }
      ],
      'examples': [
          'quick_play',
          'quick_play 10',
          'quick_play 27     # Play roughly 3 innings',
          'quick_play 100    # Play full game quickly'
      ]
  },

  'status': {
      'summary': 'Display current game state',
      'usage': 'status',
      'options': [],
      'examples': [
          'status'
      ]
  },

  'box_score': {
      'summary': 'Display box score for the current game',
      'usage': 'box_score',
      'options': [],
      'examples': [
          'box_score'
      ]
  },

  'list_games': {
      'summary': 'List all games currently loaded in state manager',
      'usage': 'list_games',
      'options': [],
      'examples': [
          'list_games'
      ]
  },

  'use_game': {
      'summary': 'Switch to a different game',
      'usage': 'use_game <GAME_ID>',
      'options': [
          {
              'name': 'GAME_ID',
              'type': 'UUID',
              'desc': 'UUID of the game to switch to. Positional argument.'
          }
      ],
      'examples': [
          'use_game a1b2c3d4-e5f6-7890-abcd-ef1234567890',
          'use_game <TAB>    # Use tab completion to see available games'
      ]
  },

  'config': {
      'summary': 'Show terminal client configuration',
      'usage': 'config',
      'options': [],
      'examples': [
          'config'
      ]
  },

  'clear': {
      'summary': 'Clear the screen',
      'usage': 'clear',
      'options': [],
      'examples': [
          'clear'
      ]
  }

}

def get_help_text(command: str) -> Dict: """ Get help data for a command.

  Args:
      command: Command name
      
  Returns:
      Help data dictionary or empty dict if not found
  """
  return HELP_DATA.get(command, {})

def show_help(command: str = None) -> None: """ Show help for a command or list all commands.

  Args:
      command: Command name or None for command list
  """
  if command:
      help_data = get_help_text(command)
      if help_data:
          HelpFormatter.show_command_help(command, help_data)
      else:
          console.print(f"[yellow]No help available for '{command}'[/yellow]")
          console.print(f"[dim]Type 'help' to see all available commands.[/dim]")
  else:
      HelpFormatter.show_command_list()

Files to Update

  1. Update backend/terminal_client/repl.py

Add import at top

from terminal_client.help_text import show_help, get_help_text, HelpFormatter

Update the intro to be more helpful

class GameREPL(GameREPLCompletions, cmd.Cmd): """Interactive REPL for game engine testing."""

  intro = """

╔══════════════════════════════════════════════════════════════════════════════╗ ║ Paper Dynasty Game Engine - Terminal Client ║ ║ Interactive Mode ║ ╚══════════════════════════════════════════════════════════════════════════════╝

[cyan]Type 'help' to see all available commands.[/cyan] [cyan]Type 'help ' for detailed information about a specific command.[/cyan] [cyan]Use TAB for auto-completion of commands and options.[/cyan]

[yellow]Quick start:[/yellow] new_game Create and start a new game status Show current game state defensive Submit defensive decision offensive Submit offensive decision resolve Resolve the play quick_play 10 Auto-play 10 plays

Press Ctrl+D or type 'quit' to exit.

""" prompt = ' > '

  # ... rest of __init__ stays the same ...

  # ==================== Enhanced Help System ====================

  def do_help(self, arg):
      """
      Show help for commands.
      
      Usage:
        help              List all commands
        help <command>    Show detailed help for a command
      """
      if arg:
          # Show detailed help for specific command
          show_help(arg)
      else:
          # Show command list
          HelpFormatter.show_command_list()

  def help_new_game(self):
      """Show detailed help for new_game command."""
      show_help('new_game')

  def help_defensive(self):
      """Show detailed help for defensive command."""
      show_help('defensive')

  def help_offensive(self):
      """Show detailed help for offensive command."""
      show_help('offensive')

  def help_resolve(self):
      """Show detailed help for resolve command."""
      show_help('resolve')

  def help_quick_play(self):
      """Show detailed help for quick_play command."""
      show_help('quick_play')

  def help_status(self):
      """Show detailed help for status command."""
      show_help('status')

  def help_box_score(self):
      """Show detailed help for box_score command."""
      show_help('box_score')

  def help_list_games(self):
      """Show detailed help for list_games command."""
      show_help('list_games')

  def help_use_game(self):
      """Show detailed help for use_game command."""
      show_help('use_game')

  def help_config(self):
      """Show detailed help for config command."""
      show_help('config')

  def help_clear(self):
      """Show detailed help for clear command."""
      show_help('clear')

  # ==================== Keep existing command methods ====================
  # All do_* methods remain unchanged
  1. Create backend/terminal_client/main.py Update

Add help command for standalone mode

Add this at the bottom of main.py

@cli.command('help') @click.argument('command', required=False) def show_cli_help(command): """ Show help for terminal client commands.

  Usage:
    python -m terminal_client help              # Show all commands
    python -m terminal_client help new-game     # Show help for specific command
  """
  from terminal_client.help_text import show_help

  # Convert hyphenated command to underscore for lookup
  if command:
      command = command.replace('-', '_')

  show_help(command)

Testing Plan

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

""" Unit tests for help text system. """ import pytest from io import StringIO from unittest.mock import patch

from terminal_client.help_text import ( HelpFormatter, get_help_text, show_help, HELP_DATA )

class TestHelpData: """Tests for help data structure."""

  def test_all_commands_have_help(self):
      """Test that all major commands have help data."""
      required_commands = [
          'new_game', 'defensive', 'offensive', 'resolve',
          'quick_play', 'status', 'use_game', 'list_games'
      ]

      for cmd in required_commands:
          assert cmd in HELP_DATA, f"Missing help data for {cmd}"

  def test_help_data_structure(self):
      """Test that help data has required fields."""
      for cmd, data in HELP_DATA.items():
          assert 'summary' in data, f"{cmd} missing summary"
          assert 'usage' in data, f"{cmd} missing usage"
          assert 'options' in data, f"{cmd} missing options"
          assert 'examples' in data, f"{cmd} missing examples"

          # Validate options structure
          for opt in data['options']:
              assert 'name' in opt, f"{cmd} option missing name"
              assert 'desc' in opt, f"{cmd} option missing desc"

  def test_get_help_text_valid(self):
      """Test getting help text for valid command."""
      result = get_help_text('new_game')
      assert result is not None
      assert 'summary' in result

  def test_get_help_text_invalid(self):
      """Test getting help text for invalid command."""
      result = get_help_text('nonexistent_command')
      assert result == {}

class TestHelpFormatter: """Tests for HelpFormatter."""

  @patch('terminal_client.help_text.console')
  def test_show_command_help(self, mock_console):
      """Test showing help for a command."""
      help_data = {
          'summary': 'Test command',
          'usage': 'test [OPTIONS]',
          'options': [
              {'name': '--option', 'type': 'STRING', 'desc': 'Test option'}
          ],
          'examples': ['test --option value']
      }

      HelpFormatter.show_command_help('test', help_data)

      # Verify console.print was called
      assert mock_console.print.called

  @patch('terminal_client.help_text.console')
  def test_show_command_list(self, mock_console):
      """Test showing command list."""
      HelpFormatter.show_command_list()

      # Verify console.print was called multiple times
      assert mock_console.print.call_count > 5

class TestShowHelp: """Tests for show_help function."""

  @patch('terminal_client.help_text.HelpFormatter.show_command_help')
  def test_show_help_specific_command(self, mock_show):
      """Test showing help for specific command."""
      show_help('new_game')

      mock_show.assert_called_once()
      args = mock_show.call_args[0]
      assert args[0] == 'new_game'
      assert 'summary' in args[1]

  @patch('terminal_client.help_text.HelpFormatter.show_command_list')
  def test_show_help_no_command(self, mock_list):
      """Test showing command list when no command specified."""
      show_help()

      mock_list.assert_called_once()

  @patch('terminal_client.help_text.console')
  def test_show_help_invalid_command(self, mock_console):
      """Test showing help for invalid command."""
      show_help('invalid_command')

      # Should print warning message
      assert mock_console.print.called
      call_args = str(mock_console.print.call_args)
      assert 'No help available' in call_args

Example Output

Command List (help): > help

Available Commands:

Game Management: new_game Create a new game with test lineups and start it list_games List all games in state manager use_game Switch to a different game status Display current game state box_score Display box score

Gameplay: defensive Submit defensive decision offensive Submit offensive decision resolve Resolve the current play quick_play Auto-play multiple plays

Utilities: config Show configuration clear Clear the screen help Show help for commands quit/exit Exit the REPL

Type 'help ' for detailed information. Use TAB for auto-completion of commands and options.

Detailed Help (help defensive): > help defensive

╭─────────────── Help: defensive ───────────────╮ │ defensive - Submit defensive decision for │ │ the current play │ │ │ │ USAGE: │ │ defensive [--alignment TYPE] [--infield │ │ DEPTH] [--outfield DEPTH] [--hold BASES] │ ╰───────────────────────────────────────────────╯

OPTIONS: --alignment STRING Defensive alignment: normal, shifted_left, shifted_right, extreme_shift (default: normal) --infield STRING Infield depth: in, normal, back, double_play (default: normal) --outfield STRING Outfield depth: in, normal, back (default: normal) --hold LIST Comma-separated bases to hold runners: 1,2,3 (default: none)

EXAMPLES: defensive defensive --alignment shifted_left defensive --infield double_play --hold 1,3 defensive --alignment extreme_shift --infield back --outfield back

Benefits

  1. Self-documenting: No need to check external docs for basic usage
  2. Rich formatting: Beautiful output with colors, tables, and panels
  3. Comprehensive: Every option explained with examples
  4. Discoverable: Easy to explore what's available
  5. Consistent: Same help format across all commands
  6. Learning tool: Examples teach proper usage patterns