From 529320acb5466131743f11633f839b42c210aefc Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sun, 8 Feb 2026 13:15:04 -0500 Subject: [PATCH] Split commands and skills into separate listings --- src/mudlib/commands/help.py | 94 +++++++++++++++++++++++++------------ tests/test_commands_list.py | 41 ++++++---------- 2 files changed, 79 insertions(+), 56 deletions(-) diff --git a/src/mudlib/commands/help.py b/src/mudlib/commands/help.py index fb59f5f..b08d59e 100644 --- a/src/mudlib/commands/help.py +++ b/src/mudlib/commands/help.py @@ -175,29 +175,21 @@ async def cmd_commands(player: Player, args: str) -> None: seen.add(defn_id) unique_commands.append(defn) - # Group commands by type + # Group commands by type (excluding combat moves) movement: list[CommandDefinition] = [] - combat_attack: list[CommandDefinition] = [] - combat_defend: list[CommandDefinition] = [] other: list[CommandDefinition] = [] for defn in unique_commands: # Check if it's a movement command if defn.name in DIRECTIONS: movement.append(defn) - # Check if it's a combat attack - elif defn.help.startswith("Attack with"): - combat_attack.append(defn) - # Check if it's a combat defense - elif defn.help.startswith("Defend with"): - combat_defend.append(defn) + # Skip combat attacks and defenses + elif defn.help.startswith("Attack with") or defn.help.startswith("Defend with"): + continue # Everything else else: other.append(defn) - # Format output - output_lines = [] - def format_command(defn: CommandDefinition) -> str: """Format a command with its aliases in parens.""" if defn.aliases: @@ -205,27 +197,60 @@ async def cmd_commands(player: Player, args: str) -> None: return f"{defn.name}({aliases_str})" return defn.name - if movement: - movement.sort(key=lambda d: d.name) - movement_str = ", ".join(format_command(d) for d in movement) - output_lines.append(f"Movement: {movement_str}") + all_commands = movement + other + all_commands.sort(key=lambda d: d.name) + names = [format_command(d) for d in all_commands] - if combat_attack: - combat_attack.sort(key=lambda d: d.name) - attack_str = ", ".join(format_command(d) for d in combat_attack) - output_lines.append(f"Combat (attack): {attack_str}") + await player.send(" ".join(names) + "\r\n") - if combat_defend: - combat_defend.sort(key=lambda d: d.name) - defend_str = ", ".join(format_command(d) for d in combat_defend) - output_lines.append(f"Combat (defend): {defend_str}") - if other: - other.sort(key=lambda d: d.name) - other_str = ", ".join(format_command(d) for d in other) - output_lines.append(f"Other: {other_str}") +async def cmd_skills(player: Player, args: str) -> None: + """List all combat skills (attacks and defenses), or show detail for one. - await player.send("\r\n".join(output_lines) + "\r\n") + Args: + player: The player executing the command + args: Command arguments (skill name for detail view, empty for list) + """ + # If args provided, show detail view for that skill + if args.strip(): + await _show_command_detail(player, args.strip()) + return + + # Otherwise, show full list + # Collect unique commands by CommandDefinition id (avoids alias duplication) + seen: set[int] = set() + unique_commands: list[CommandDefinition] = [] + + for defn in _registry.values(): + defn_id = id(defn) + if defn_id not in seen and not defn.hidden: + seen.add(defn_id) + unique_commands.append(defn) + + # Group combat moves + attacks: list[CommandDefinition] = [] + defenses: list[CommandDefinition] = [] + + for defn in unique_commands: + # Check if it's a combat attack + if defn.help.startswith("Attack with"): + attacks.append(defn) + # Check if it's a combat defense + elif defn.help.startswith("Defend with"): + defenses.append(defn) + + def format_command(defn: CommandDefinition) -> str: + """Format a command with its aliases in parens.""" + if defn.aliases: + aliases_str = ", ".join(defn.aliases) + return f"{defn.name}({aliases_str})" + return defn.name + + all_skills = attacks + defenses + all_skills.sort(key=lambda d: d.name) + names = [format_command(d) for d in all_skills] + + await player.send(" ".join(names) + "\r\n") # Register the commands command @@ -238,3 +263,14 @@ register( help="list available commands", ) ) + +# Register the skills command +register( + CommandDefinition( + "skills", + cmd_skills, + aliases=[], + mode="*", + help="list combat skills", + ) +) diff --git a/tests/test_commands_list.py b/tests/test_commands_list.py index e2307ff..18c9ede 100644 --- a/tests/test_commands_list.py +++ b/tests/test_commands_list.py @@ -84,16 +84,16 @@ async def test_commands_lists_all_commands(player): # Collect all output output = "".join([call[0][0] for call in player.writer.write.call_args_list]) - # Should contain at least movement and other groups - assert "Movement:" in output - assert "Other:" in output - # Should show some specific commands with their primary names assert "north" in output assert "look" in output assert "quit" in output assert "commands" in output + # Should not contain combat moves + assert "punch" not in output + assert "roundhouse" not in output + @pytest.mark.asyncio async def test_commands_shows_aliases(player): @@ -126,29 +126,16 @@ async def test_commands_deduplicates_entries(player): @pytest.mark.asyncio -async def test_commands_groups_correctly(player): - """Test that commands are grouped by type.""" +async def test_commands_sorted_alphabetically(player): + """Test that commands are sorted alphabetically.""" await commands.dispatch(player, "commands") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) - # Find the groups - movement_pos = output.find("Movement:") - other_pos = output.find("Other:") - - # At minimum, movement and other should exist - assert movement_pos != -1 - assert other_pos != -1 - - # Check that movement commands appear in the movement section - # (before the "Other:" section) - movement_section = output[movement_pos:other_pos] - assert "north" in movement_section - assert "east" in movement_section - - # Check that non-movement commands appear in other section - other_section = output[other_pos:] - assert "quit" in other_section - assert "look" in other_section + # All commands should appear in a single sorted list + assert "north" in output + assert "east" in output + assert "quit" in output + assert "look" in output @pytest.mark.asyncio @@ -157,9 +144,9 @@ async def test_commands_via_alias(player): await commands.dispatch(player, "cmds") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) - # Should produce the same output structure - assert "Movement:" in output - assert "Other:" in output + # Should produce the same output as commands + assert "north" in output + assert "look" in output @pytest.mark.asyncio