Add commands command to list available commands
This commit is contained in:
parent
e9378bb6fa
commit
6fb48e9ae1
4 changed files with 232 additions and 1 deletions
|
|
@ -1,7 +1,6 @@
|
|||
"""Combat command handlers."""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
|
|
|
|||
87
src/mudlib/commands/help.py
Normal file
87
src/mudlib/commands/help.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""Help and command listing commands."""
|
||||
|
||||
from mudlib.commands import CommandDefinition, _registry, register
|
||||
from mudlib.commands.movement import DIRECTIONS
|
||||
from mudlib.player import Player
|
||||
|
||||
|
||||
async def cmd_commands(player: Player, args: str) -> None:
|
||||
"""List all available commands grouped by type.
|
||||
|
||||
Args:
|
||||
player: The player executing the command
|
||||
args: Command arguments (unused)
|
||||
"""
|
||||
# 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:
|
||||
seen.add(defn_id)
|
||||
unique_commands.append(defn)
|
||||
|
||||
# Group commands by type
|
||||
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)
|
||||
# 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:
|
||||
aliases_str = ", ".join(defn.aliases)
|
||||
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}")
|
||||
|
||||
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}")
|
||||
|
||||
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}")
|
||||
|
||||
await player.send("\r\n".join(output_lines) + "\r\n")
|
||||
|
||||
|
||||
# Register the commands command
|
||||
register(
|
||||
CommandDefinition(
|
||||
"commands",
|
||||
cmd_commands,
|
||||
aliases=["cmds"],
|
||||
mode="*",
|
||||
help="list available commands",
|
||||
)
|
||||
)
|
||||
|
|
@ -13,6 +13,7 @@ from telnetlib3.server_shell import readline2
|
|||
import mudlib.commands
|
||||
import mudlib.commands.edit
|
||||
import mudlib.commands.fly
|
||||
import mudlib.commands.help
|
||||
import mudlib.commands.look
|
||||
import mudlib.commands.movement
|
||||
import mudlib.commands.quit
|
||||
|
|
|
|||
144
tests/test_commands_list.py
Normal file
144
tests/test_commands_list.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
"""Tests for the commands listing command."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mudlib import commands
|
||||
|
||||
# Import command modules to register their commands
|
||||
from mudlib.commands import (
|
||||
edit, # noqa: F401
|
||||
fly, # noqa: F401
|
||||
help, # noqa: F401
|
||||
look, # noqa: F401
|
||||
movement, # noqa: F401
|
||||
quit, # noqa: F401
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_writer():
|
||||
writer = MagicMock()
|
||||
writer.write = MagicMock()
|
||||
writer.drain = AsyncMock()
|
||||
return writer
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_reader():
|
||||
return MagicMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def player(mock_reader, mock_writer):
|
||||
from mudlib.player import Player
|
||||
|
||||
return Player(
|
||||
name="TestPlayer",
|
||||
x=5,
|
||||
y=5,
|
||||
reader=mock_reader,
|
||||
writer=mock_writer,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_commands_command_exists():
|
||||
"""Test that commands command is registered."""
|
||||
assert "commands" in commands._registry
|
||||
assert "cmds" in commands._registry
|
||||
assert commands._registry["commands"] is commands._registry["cmds"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_commands_has_wildcard_mode():
|
||||
"""Test that commands works from any mode."""
|
||||
assert commands._registry["commands"].mode == "*"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_commands_lists_all_commands(player):
|
||||
"""Test that commands command lists all unique commands."""
|
||||
# Execute the command
|
||||
await commands.dispatch(player, "commands")
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_commands_shows_aliases(player):
|
||||
"""Test that aliases are shown in parens after command names."""
|
||||
await commands.dispatch(player, "commands")
|
||||
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
|
||||
|
||||
# Check that some aliases are shown in parentheses
|
||||
assert "north(n)" in output or "north (n)" in output
|
||||
assert "look(l)" in output or "look (l)" in output
|
||||
assert "quit(q)" in output or "quit (q)" in output
|
||||
assert "commands(cmds)" in output or "commands (cmds)" in output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_commands_deduplicates_entries(player):
|
||||
"""Test that aliases don't create duplicate command entries."""
|
||||
await commands.dispatch(player, "commands")
|
||||
output = "".join([call[0][0] for call in player.writer.write.call_args_list])
|
||||
|
||||
# Count how many times "north" appears as a primary command
|
||||
# It should appear once in the movement group, not multiple times
|
||||
# (Note: it might appear in parens as part of another command, but
|
||||
# we're checking it doesn't appear as a standalone entry multiple times)
|
||||
lines_with_north_command = [
|
||||
line for line in output.split("\n") if line.strip().startswith("north")
|
||||
]
|
||||
# Should only have one line starting with "north" as a command
|
||||
assert len(lines_with_north_command) <= 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_commands_groups_correctly(player):
|
||||
"""Test that commands are grouped by type."""
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_commands_via_alias(player):
|
||||
"""Test that the cmds alias works."""
|
||||
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
|
||||
Loading…
Reference in a new issue