Extract shared prefix matching helper

This commit is contained in:
Jared Miller 2026-02-08 13:48:00 -05:00
parent 7c21310dcd
commit 68fa08d776
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 69 additions and 54 deletions

View file

@ -63,6 +63,42 @@ def register(defn: CommandDefinition) -> None:
_registry[alias] = defn _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: async def dispatch(player: Player, raw_input: str) -> None:
"""Parse input, find command, call handler. """Parse input, find command, call handler.
@ -80,37 +116,27 @@ async def dispatch(player: Player, raw_input: str) -> None:
command = parts[0].lower() command = parts[0].lower()
args = parts[1] if len(parts) > 1 else "" args = parts[1] if len(parts) > 1 else ""
# Look up the definition - exact match first (fast path) # Resolve command by exact match or prefix
defn = _registry.get(command) result = resolve_prefix(command)
if defn is None: if result is None:
# Try prefix matching # No matches
prefix_matches = [key for key in _registry if key.startswith(command)] player.writer.write(f"Unknown command: {command}\r\n")
await player.writer.drain()
# Deduplicate by CommandDefinition identity return
unique_defns = {} elif isinstance(result, list):
for key in prefix_matches: # Multiple matches - show disambiguation
defn_obj = _registry[key] names = result
unique_defns[id(defn_obj)] = defn_obj if len(names) == 2:
msg = f"{names[0]} or {names[1]}?\r\n"
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()))
else: else:
# Multiple matches - show disambiguation msg = f"{', '.join(names[:-1])}, or {names[-1]}?\r\n"
names = sorted([d.name for d in unique_defns.values()]) player.writer.write(msg)
if len(names) == 2: await player.writer.drain()
msg = f"{names[0]} or {names[1]}?\r\n" return
else: else:
msg = f"{', '.join(names[:-1])}, or {names[-1]}?\r\n" # Unique match
player.writer.write(msg) defn = result
await player.writer.drain()
return
# Check mode restriction # Check mode restriction
if defn.mode != "*" and defn.mode != player.mode: if defn.mode != "*" and defn.mode != player.mode:

View file

@ -2,7 +2,7 @@
from typing import TYPE_CHECKING 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.commands.movement import DIRECTIONS
from mudlib.player import Player 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 from mudlib.combat.commands import combat_moves
# Look up the command in registry - exact match first # Resolve command by exact match or prefix
defn = _registry.get(command_name) result = resolve_prefix(command_name)
# If not found, try prefix matching if isinstance(result, list):
if defn is None: # Multiple matches - show disambiguation
prefix_matches = [key for key in _registry if key.startswith(command_name)] names = result
if len(names) == 2:
# Deduplicate by CommandDefinition identity msg = f"{names[0]} or {names[1]}?\r\n"
unique_defns = {} else:
for key in prefix_matches: msg = f"{', '.join(names[:-1])}, or {names[-1]}?\r\n"
defn_obj = _registry[key] await player.send(msg)
unique_defns[id(defn_obj)] = defn_obj return
else:
if len(unique_defns) == 1: defn = result
# 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 still not in registry, check if it's a combat move name (like "punch left") # If still not in registry, check if it's a combat move name (like "punch left")
if defn is None: if defn is None: