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."""
|
"""Combat command handlers."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
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
|
||||||
import mudlib.commands.edit
|
import mudlib.commands.edit
|
||||||
import mudlib.commands.fly
|
import mudlib.commands.fly
|
||||||
|
import mudlib.commands.help
|
||||||
import mudlib.commands.look
|
import mudlib.commands.look
|
||||||
import mudlib.commands.movement
|
import mudlib.commands.movement
|
||||||
import mudlib.commands.quit
|
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