From d220835f7d6c6c08b6d59e2103db3d6c56d58d23 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sat, 7 Feb 2026 16:17:01 -0500 Subject: [PATCH] Add mode stack to Player and mode check in dispatch --- src/mudlib/commands/__init__.py | 6 ++++ src/mudlib/player.py | 8 ++++- tests/test_commands.py | 53 +++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/mudlib/commands/__init__.py b/src/mudlib/commands/__init__.py index a3865d4..63776a0 100644 --- a/src/mudlib/commands/__init__.py +++ b/src/mudlib/commands/__init__.py @@ -60,5 +60,11 @@ async def dispatch(player: Player, raw_input: str) -> None: await player.writer.drain() return + # Check mode restriction + if defn.mode != "*" and defn.mode != player.mode: + player.writer.write("You can't do that right now.\r\n") + await player.writer.drain() + return + # Execute the handler await defn.handler(player, args) diff --git a/src/mudlib/player.py b/src/mudlib/player.py index 5509bb3..26d6455 100644 --- a/src/mudlib/player.py +++ b/src/mudlib/player.py @@ -1,6 +1,6 @@ """Player state and registry.""" -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any @@ -14,6 +14,12 @@ class Player: writer: Any # telnetlib3 TelnetWriter for sending output reader: Any # telnetlib3 TelnetReader for reading input flying: bool = False + mode_stack: list[str] = field(default_factory=lambda: ["normal"]) + + @property + def mode(self) -> str: + """Current mode is the top of the stack.""" + return self.mode_stack[-1] # Global registry of connected players diff --git a/tests/test_commands.py b/tests/test_commands.py index 2ee868b..a742e52 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -318,3 +318,56 @@ async def test_effects_dont_override_player_marker(player, mock_world): assert "@" in output active_effects.clear() + + +# Test mode stack +def test_mode_stack_default(player): + """Player starts in normal mode.""" + assert player.mode == "normal" + assert player.mode_stack == ["normal"] + + +@pytest.mark.asyncio +async def test_dispatch_blocks_wrong_mode(player, mock_writer): + """Commands with wrong mode get rejected.""" + + async def combat_handler(p, args): + pass + + commands.register(CommandDefinition("strike", combat_handler, mode="combat")) + await commands.dispatch(player, "strike") + + assert mock_writer.write.called + written = mock_writer.write.call_args[0][0] + assert "can't" in written.lower() + + +@pytest.mark.asyncio +async def test_dispatch_allows_wildcard_mode(player): + """Commands with mode='*' work from any mode.""" + called = False + + async def any_handler(p, args): + nonlocal called + called = True + + commands.register(CommandDefinition("universal", any_handler, mode="*")) + await commands.dispatch(player, "universal") + + assert called + + +@pytest.mark.asyncio +async def test_dispatch_allows_matching_mode(player): + """Commands work when player mode matches command mode.""" + called = False + + async def combat_handler(p, args): + nonlocal called + called = True + + commands.register(CommandDefinition("strike", combat_handler, mode="combat")) + player.mode_stack.append("combat") + await commands.dispatch(player, "strike") + + assert called