diff --git a/src/mudlib/commands/__init__.py b/src/mudlib/commands/__init__.py index 7a3d47e..46e5cbc 100644 --- a/src/mudlib/commands/__init__.py +++ b/src/mudlib/commands/__init__.py @@ -63,6 +63,42 @@ def register(defn: CommandDefinition) -> None: _registry[alias] = defn +def resolve_prefix(command: str) -> CommandDefinition | list[str] | None: + """Resolve a command by exact match or shortest unique prefix. + + Args: + command: Command name or prefix to resolve + + Returns: + CommandDefinition if unique match found + list of command names if ambiguous + None if no match + """ + # Try exact match first (fast path) + defn = _registry.get(command) + if defn is not None: + return defn + + # Try prefix matching + prefix_matches = [key for key in _registry if key.startswith(command)] + + # Deduplicate by CommandDefinition identity + unique_defns = {} + for key in prefix_matches: + defn_obj = _registry[key] + unique_defns[id(defn_obj)] = defn_obj + + if len(unique_defns) == 0: + # No matches + return None + elif len(unique_defns) == 1: + # Unique match + return next(iter(unique_defns.values())) + else: + # Multiple matches - return list of names + return sorted([d.name for d in unique_defns.values()]) + + async def dispatch(player: Player, raw_input: str) -> None: """Parse input, find command, call handler. @@ -80,37 +116,27 @@ async def dispatch(player: Player, raw_input: str) -> None: command = parts[0].lower() args = parts[1] if len(parts) > 1 else "" - # Look up the definition - exact match first (fast path) - defn = _registry.get(command) + # Resolve command by exact match or prefix + result = resolve_prefix(command) - if defn is None: - # Try prefix matching - prefix_matches = [key for key in _registry if key.startswith(command)] - - # Deduplicate by CommandDefinition identity - unique_defns = {} - for key in prefix_matches: - defn_obj = _registry[key] - unique_defns[id(defn_obj)] = defn_obj - - if len(unique_defns) == 0: - # No matches - player.writer.write(f"Unknown command: {command}\r\n") - await player.writer.drain() - return - elif len(unique_defns) == 1: - # Unique match - defn = next(iter(unique_defns.values())) + if result is None: + # No matches + player.writer.write(f"Unknown command: {command}\r\n") + await player.writer.drain() + return + elif isinstance(result, list): + # Multiple matches - show disambiguation + names = result + if len(names) == 2: + msg = f"{names[0]} or {names[1]}?\r\n" else: - # Multiple matches - show disambiguation - names = sorted([d.name for d in unique_defns.values()]) - if len(names) == 2: - msg = f"{names[0]} or {names[1]}?\r\n" - else: - msg = f"{', '.join(names[:-1])}, or {names[-1]}?\r\n" - player.writer.write(msg) - await player.writer.drain() - return + msg = f"{', '.join(names[:-1])}, or {names[-1]}?\r\n" + player.writer.write(msg) + await player.writer.drain() + return + else: + # Unique match + defn = result # Check mode restriction if defn.mode != "*" and defn.mode != player.mode: diff --git a/src/mudlib/commands/help.py b/src/mudlib/commands/help.py index 97a3285..63b3576 100644 --- a/src/mudlib/commands/help.py +++ b/src/mudlib/commands/help.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from mudlib.commands import CommandDefinition, _registry, register +from mudlib.commands import CommandDefinition, _registry, register, resolve_prefix from mudlib.commands.movement import DIRECTIONS from mudlib.player import Player @@ -19,31 +19,20 @@ async def _show_command_detail(player: Player, command_name: str) -> None: """ from mudlib.combat.commands import combat_moves - # Look up the command in registry - exact match first - defn = _registry.get(command_name) + # Resolve command by exact match or prefix + result = resolve_prefix(command_name) - # If not found, try prefix matching - if defn is None: - prefix_matches = [key for key in _registry if key.startswith(command_name)] - - # Deduplicate by CommandDefinition identity - unique_defns = {} - for key in prefix_matches: - defn_obj = _registry[key] - unique_defns[id(defn_obj)] = defn_obj - - if len(unique_defns) == 1: - # Unique match via prefix - defn = next(iter(unique_defns.values())) - elif len(unique_defns) > 1: - # Multiple matches - show disambiguation - names = sorted([d.name for d in unique_defns.values()]) - if len(names) == 2: - msg = f"{names[0]} or {names[1]}?\r\n" - else: - msg = f"{', '.join(names[:-1])}, or {names[-1]}?\r\n" - await player.send(msg) - return + if isinstance(result, list): + # Multiple matches - show disambiguation + names = result + if len(names) == 2: + msg = f"{names[0]} or {names[1]}?\r\n" + else: + msg = f"{', '.join(names[:-1])}, or {names[-1]}?\r\n" + await player.send(msg) + return + else: + defn = result # If still not in registry, check if it's a combat move name (like "punch left") if defn is None: