strat-gameplay-webapp/backend/terminal_client/completions.py
Cal Corum d7caa75310 CLAUDE: Add manual outcome testing to terminal client and Phase 3 planning
Terminal Client Enhancements:
- Added list_outcomes command to display all PlayOutcome values
- Added resolve_with <outcome> command for testing specific scenarios
- TAB completion for all outcome names
- Full help documentation and examples
- Infrastructure ready for Week 7 integration

Files Modified:
- terminal_client/commands.py - list_outcomes() and forced outcome support
- terminal_client/repl.py - do_list_outcomes() and do_resolve_with() commands
- terminal_client/completions.py - VALID_OUTCOMES and complete_resolve_with()
- terminal_client/help_text.py - Help entries for new commands

Phase 3 Planning:
- Created comprehensive Week 7 implementation plan (25 pages)
- 6 major tasks covering strategic decisions and result charts
- Updated 00-index.md to mark Week 6 as 100% complete
- Documented manual outcome testing feature

Week 6: 100% Complete 
Phase 3 Week 7: Ready to begin

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 20:53:47 -05:00

337 lines
11 KiB
Python

"""
Tab completion support for terminal client REPL.
Provides intelligent completion for commands, options, and values
using Python's cmd module completion hooks.
Author: Claude
Date: 2025-10-27
"""
import logging
from typing import List, Optional
logger = logging.getLogger(f'{__name__}.completions')
class CompletionHelper:
"""Helper class for generating tab completions."""
# Valid values for common options
VALID_LEAGUES = ['sba', 'pd']
VALID_ALIGNMENTS = ['normal', 'shifted_left', 'shifted_right', 'extreme_shift']
VALID_INFIELD_DEPTHS = ['in', 'normal', 'back', 'double_play']
VALID_OUTFIELD_DEPTHS = ['in', 'normal', 'back']
VALID_APPROACHES = ['normal', 'contact', 'power', 'patient']
# Valid bases for stealing/holding
VALID_BASES = ['1', '2', '3']
# Valid PlayOutcome values for resolve_with command
VALID_OUTCOMES = [
'strikeout',
'groundball_a', 'groundball_b', 'groundball_c',
'flyout_a', 'flyout_b', 'flyout_c',
'lineout', 'popout',
'single_1', 'single_2', 'single_uncapped',
'double_2', 'double_3', 'double_uncapped',
'triple', 'homerun',
'walk', 'hbp', 'intentional_walk',
'error',
'wild_pitch', 'passed_ball', 'stolen_base', 'caught_stealing', 'balk', 'pick_off',
'bp_homerun', 'bp_single', 'bp_flyout', 'bp_lineout'
]
@staticmethod
def filter_completions(text: str, options: List[str]) -> List[str]:
"""
Filter options that start with the given text.
Args:
text: Partial text to match
options: List of possible completions
Returns:
List of matching options
"""
if not text:
return options
return [opt for opt in options if opt.startswith(text)]
@staticmethod
def complete_option(text: str, line: str, available_options: List[str]) -> List[str]:
"""
Complete option names (--option).
Args:
text: Current text being completed
line: Full command line
available_options: List of valid option names
Returns:
List of matching options with -- prefix
"""
if text.startswith('--'):
# Completing option name
prefix = text[2:]
matches = [opt for opt in available_options if opt.startswith(prefix)]
return [f'--{match}' for match in matches]
elif not text:
# Show all options
return [f'--{opt}' for opt in available_options]
return []
@staticmethod
def get_current_option(line: str, endidx: int) -> Optional[str]:
"""
Determine which option we're currently completing the value for.
Args:
line: Full command line
endidx: Current cursor position
Returns:
Option name (without --) or None
"""
# Split line up to cursor position
before_cursor = line[:endidx]
tokens = before_cursor.split()
# Look for the last --option before cursor
for i in range(len(tokens) - 1, -1, -1):
if tokens[i].startswith('--'):
return tokens[i][2:].replace('-', '_')
return None
class GameREPLCompletions:
"""Mixin class providing tab completion methods for GameREPL."""
def __init__(self):
"""Initialize completion helper."""
self.completion_helper = CompletionHelper()
# ==================== Command Completions ====================
def complete_new_game(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""
Complete new_game command.
Available options:
--league sba|pd
--home-team N
--away-team N
"""
available_options = ['league', 'home-team', 'away-team']
# Check if we're completing an option value first
current_option = self.completion_helper.get_current_option(line, endidx)
if current_option == 'league':
return self.completion_helper.filter_completions(
text, self.completion_helper.VALID_LEAGUES
)
# Check if we're completing an option name
if text.startswith('--'):
return self.completion_helper.complete_option(text, line, available_options)
elif not text:
return self.completion_helper.complete_option(text, line, available_options)
return []
def complete_defensive(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""
Complete defensive command.
Available options:
--alignment normal|shifted_left|shifted_right|extreme_shift
--infield in|normal|back|double_play
--outfield in|normal|back
--hold 1,2,3
"""
available_options = ['alignment', 'infield', 'outfield', 'hold']
# Check if we're completing an option value first
current_option = self.completion_helper.get_current_option(line, endidx)
if current_option == 'alignment':
return self.completion_helper.filter_completions(
text, self.completion_helper.VALID_ALIGNMENTS
)
elif current_option == 'infield':
return self.completion_helper.filter_completions(
text, self.completion_helper.VALID_INFIELD_DEPTHS
)
elif current_option == 'outfield':
return self.completion_helper.filter_completions(
text, self.completion_helper.VALID_OUTFIELD_DEPTHS
)
elif current_option == 'hold':
# For comma-separated values, complete the last item
if ',' in text:
prefix = text.rsplit(',', 1)[0] + ','
last_item = text.rsplit(',', 1)[1]
matches = self.completion_helper.filter_completions(
last_item, self.completion_helper.VALID_BASES
)
return [f'{prefix}{match}' for match in matches]
else:
return self.completion_helper.filter_completions(
text, self.completion_helper.VALID_BASES
)
# Check if we're completing an option name
if text.startswith('--'):
return self.completion_helper.complete_option(text, line, available_options)
elif not text:
return self.completion_helper.complete_option(text, line, available_options)
return []
def complete_offensive(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""
Complete offensive command.
Available options:
--approach normal|contact|power|patient
--steal 2,3
--hit-run (flag)
--bunt (flag)
"""
available_options = ['approach', 'steal', 'hit-run', 'bunt']
# Check if we're completing an option value first
current_option = self.completion_helper.get_current_option(line, endidx)
if current_option == 'approach':
return self.completion_helper.filter_completions(
text, self.completion_helper.VALID_APPROACHES
)
elif current_option == 'steal':
# Only bases 2 and 3 can be stolen
valid_steal_bases = ['2', '3']
if ',' in text:
prefix = text.rsplit(',', 1)[0] + ','
last_item = text.rsplit(',', 1)[1]
matches = self.completion_helper.filter_completions(
last_item, valid_steal_bases
)
return [f'{prefix}{match}' for match in matches]
else:
return self.completion_helper.filter_completions(
text, valid_steal_bases
)
# Check if we're completing an option name
if text.startswith('--'):
return self.completion_helper.complete_option(text, line, available_options)
elif not text:
return self.completion_helper.complete_option(text, line, available_options)
return []
def complete_use_game(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""
Complete use_game command with available game IDs.
"""
# Import here to avoid circular dependency
from app.core.state_manager import state_manager
# Get list of active games
game_ids = state_manager.list_games()
game_id_strs = [str(gid) for gid in game_ids]
return self.completion_helper.filter_completions(text, game_id_strs)
def complete_quick_play(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""
Complete quick_play command with common counts.
"""
# Suggest common play counts
common_counts = ['1', '5', '10', '27', '50', '100']
# Check if completing a positional number
if text and text.isdigit():
return self.completion_helper.filter_completions(text, common_counts)
elif not text and line.strip() == 'quick_play':
return common_counts
return []
# ==================== Helper Methods ====================
def completedefault(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""
Default completion handler for commands without specific completers.
Provides basic option completion if line contains options.
"""
# If text starts with --, try to show common options
if text.startswith('--'):
common_options = ['help', 'verbose', 'debug']
return self.completion_helper.complete_option(text, line, common_options)
return []
def complete_resolve_with(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""
Complete resolve_with command with PlayOutcome values.
Usage: resolve_with <outcome>
Provides completions for all valid PlayOutcome enum values.
"""
# Complete the outcome argument (first and only argument)
return self.completion_helper.filter_completions(
text, self.completion_helper.VALID_OUTCOMES
)
def completenames(self, text: str, *ignored) -> List[str]:
"""
Override completenames to provide better command completion.
This is called when completing the first word (command name).
"""
# Get all do_* methods
dotext = 'do_' + text
commands = [name[3:] for name in self.get_names() if name.startswith(dotext)]
# Add aliases
if 'exit'.startswith(text):
commands.append('exit')
if 'quit'.startswith(text):
commands.append('quit')
return commands
# Example completion mappings for reference
COMPLETION_EXAMPLES = """
# Example Tab Completion Usage:
⚾ > new_game --<TAB>
--league --home-team --away-team
⚾ > new_game --league <TAB>
sba pd
⚾ > defensive --<TAB>
--alignment --infield --outfield --hold
⚾ > defensive --alignment <TAB>
normal shifted_left shifted_right extreme_shift
⚾ > defensive --hold 1,<TAB>
1,2 1,3
⚾ > offensive --approach <TAB>
normal contact power patient
⚾ > use_game <TAB>
[shows all active game UUIDs]
⚾ > quick_play <TAB>
1 5 10 27 50 100
"""