strat-gameplay-webapp/backend/terminal_client/arg_parser.py
Cal Corum e165b449f5 CLAUDE: Refactor offensive decisions - replace approach with action field
Backend refactor complete - removed all deprecated parameters and replaced
with clean action-based system.

Changes:
- OffensiveDecision model: Added 'action' field (6 choices), removed
  deprecated 'hit_and_run' and 'bunt_attempt' boolean fields
- Validators: Added action-specific validation (squeeze_bunt, check_jump,
  sac_bunt, hit_and_run situational constraints)
- WebSocket handler: Updated submit_offensive_decision to use action field
- Terminal client: Updated CLI, REPL, arg parser, and display for actions
- Tests: Updated all 739 unit tests (100% passing)

Action field values:
- swing_away (default)
- steal (requires steal_attempts parameter)
- check_jump (requires runner on base)
- hit_and_run (requires runner on base)
- sac_bunt (cannot use with 2 outs)
- squeeze_bunt (requires R3, not with bases loaded, not with 2 outs)

Breaking changes:
- Removed: hit_and_run boolean → use action="hit_and_run"
- Removed: bunt_attempt boolean → use action="sac_bunt" or "squeeze_bunt"
- Removed: approach field → use action field

Files modified:
- app/models/game_models.py
- app/core/validators.py
- app/websocket/handlers.py
- terminal_client/main.py
- terminal_client/arg_parser.py
- terminal_client/commands.py
- terminal_client/repl.py
- terminal_client/display.py
- tests/unit/models/test_game_models.py
- tests/unit/core/test_validators.py
- tests/unit/terminal_client/test_arg_parser.py
- tests/unit/terminal_client/test_commands.py

Test results: 739/739 passing (100%)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 15:07:54 -06:00

216 lines
7.0 KiB
Python

"""
Argument parsing utilities for terminal client commands.
Provides robust parsing for both REPL and CLI commands using shlex
to handle quoted strings, spaces, and complex arguments.
Author: Claude
Date: 2025-10-27
"""
import shlex
import logging
from typing import Dict, Any, List, Optional, Tuple
logger = logging.getLogger(f'{__name__}.arg_parser')
class ArgumentParseError(Exception):
"""Raised when argument parsing fails."""
pass
class CommandArgumentParser:
"""Parse command-line style arguments for terminal client."""
@staticmethod
def parse_args(arg_string: str, schema: Dict[str, Any]) -> Dict[str, Any]:
"""
Parse argument string according to schema.
Args:
arg_string: Raw argument string from command
schema: Dictionary defining expected arguments
{
'league': {'type': str, 'default': 'sba'},
'home_team': {'type': int, 'default': 1},
'count': {'type': int, 'default': 1, 'positional': True},
'verbose': {'type': bool, 'flag': True}
}
Returns:
Dictionary of parsed arguments with defaults applied
Raises:
ArgumentParseError: If parsing fails or validation fails
"""
try:
# Use shlex for robust parsing
tokens = shlex.split(arg_string) if arg_string.strip() else []
except ValueError as e:
raise ArgumentParseError(f"Invalid argument syntax: {e}")
# Initialize result with defaults
result = {}
for key, spec in schema.items():
if 'default' in spec:
result[key] = spec['default']
# Track which positional arg we're on
positional_keys = [k for k, v in schema.items() if v.get('positional', False)]
positional_index = 0
i = 0
while i < len(tokens):
token = tokens[i]
# Handle flags (--option or -o)
if token.startswith('--'):
option_name = token[2:]
# Convert hyphen to underscore for Python compatibility
option_key = option_name.replace('-', '_')
if option_key not in schema:
raise ArgumentParseError(f"Unknown option: {token}")
spec = schema[option_key]
# Boolean flags don't need a value
if spec.get('flag', False):
result[option_key] = True
i += 1
continue
# Option requires a value
if i + 1 >= len(tokens):
raise ArgumentParseError(f"Option {token} requires a value")
value_str = tokens[i + 1]
# Type conversion
try:
if spec['type'] == int:
result[option_key] = int(value_str)
elif spec['type'] == float:
result[option_key] = float(value_str)
elif spec['type'] == list:
# Parse comma-separated list
result[option_key] = [item.strip() for item in value_str.split(',')]
elif spec['type'] == 'int_list':
# Parse comma-separated integers
result[option_key] = [int(item.strip()) for item in value_str.split(',')]
else:
result[option_key] = value_str
except ValueError as e:
raise ArgumentParseError(
f"Invalid value for {token}: expected {spec['type'].__name__ if hasattr(spec['type'], '__name__') else spec['type']}, got '{value_str}'"
)
i += 2
# Handle positional arguments
else:
if positional_index >= len(positional_keys):
raise ArgumentParseError(f"Unexpected positional argument: {token}")
key = positional_keys[positional_index]
spec = schema[key]
try:
if spec['type'] == int:
result[key] = int(token)
elif spec['type'] == float:
result[key] = float(token)
else:
result[key] = token
except ValueError as e:
raise ArgumentParseError(
f"Invalid value for {key}: expected {spec['type'].__name__}, got '{token}'"
)
positional_index += 1
i += 1
return result
@staticmethod
def parse_game_id(arg_string: str) -> Optional[str]:
"""
Parse a game ID from argument string.
Args:
arg_string: Raw argument string
Returns:
Game ID string or None
"""
try:
tokens = shlex.split(arg_string) if arg_string.strip() else []
# Look for --game-id option
for i, token in enumerate(tokens):
if token == '--game-id' and i + 1 < len(tokens):
return tokens[i + 1]
# If no option, check if there's a positional UUID-like argument
if tokens and len(tokens[0]) == 36: # UUID length
return tokens[0]
return None
except ValueError:
return None
# Predefined schemas for common commands
NEW_GAME_SCHEMA = {
'league': {'type': str, 'default': 'sba'},
'home_team': {'type': int, 'default': 1},
'away_team': {'type': int, 'default': 2}
}
DEFENSIVE_SCHEMA = {
'alignment': {'type': str, 'default': 'normal'},
'infield': {'type': str, 'default': 'normal'},
'outfield': {'type': str, 'default': 'normal'},
'hold': {'type': 'int_list', 'default': []}
}
OFFENSIVE_SCHEMA = {
'action': {'type': str, 'default': 'swing_away'}, # Session 2: changed from approach
'steal': {'type': 'int_list', 'default': []}
}
QUICK_PLAY_SCHEMA = {
'count': {'type': int, 'default': 1, 'positional': True}
}
USE_GAME_SCHEMA = {
'game_id': {'type': str, 'positional': True}
}
def parse_new_game_args(arg_string: str) -> Dict[str, Any]:
"""Parse arguments for new_game command."""
return CommandArgumentParser.parse_args(arg_string, NEW_GAME_SCHEMA)
def parse_defensive_args(arg_string: str) -> Dict[str, Any]:
"""Parse arguments for defensive command."""
return CommandArgumentParser.parse_args(arg_string, DEFENSIVE_SCHEMA)
def parse_offensive_args(arg_string: str) -> Dict[str, Any]:
"""Parse arguments for offensive command."""
return CommandArgumentParser.parse_args(arg_string, OFFENSIVE_SCHEMA)
def parse_quick_play_args(arg_string: str) -> Dict[str, Any]:
"""Parse arguments for quick_play command."""
return CommandArgumentParser.parse_args(arg_string, QUICK_PLAY_SCHEMA)
def parse_use_game_args(arg_string: str) -> Dict[str, Any]:
"""Parse arguments for use_game command."""
return CommandArgumentParser.parse_args(arg_string, USE_GAME_SCHEMA)