diff --git a/src/mudlib/player.py b/src/mudlib/player.py index 42ca584..7806250 100644 --- a/src/mudlib/player.py +++ b/src/mudlib/player.py @@ -10,6 +10,7 @@ from mudlib.entity import Entity if TYPE_CHECKING: from mudlib.editor import Editor + from mudlib.if_session import IFSession @dataclass @@ -22,6 +23,7 @@ class Player(Entity): mode_stack: list[str] = field(default_factory=lambda: ["normal"]) caps: ClientCaps = field(default_factory=lambda: ClientCaps(ansi=True)) editor: Editor | None = None + if_session: IFSession | None = None @property def mode(self) -> str: diff --git a/src/mudlib/server.py b/src/mudlib/server.py index 6d80a95..a62cdcd 100644 --- a/src/mudlib/server.py +++ b/src/mudlib/server.py @@ -312,6 +312,8 @@ async def shell( # Show appropriate prompt based on mode if player.mode == "editor" and player.editor: _writer.write(f" {player.editor.cursor + 1}> ") + elif player.mode == "if" and player.if_session: + _writer.write("> ") else: _writer.write("mud> ") await _writer.drain() @@ -321,7 +323,7 @@ async def shell( break command = inp.strip() - if not command and player.mode != "editor": + if not command and player.mode not in ("editor", "if"): continue # Handle editor mode @@ -332,6 +334,16 @@ async def shell( if response.done: player.editor = None player.mode_stack.pop() + # Handle IF mode + elif player.mode == "if" and player.if_session: + response = await player.if_session.handle_input(command) + if response.output: + await player.send(response.output) + if response.done: + await player.if_session.stop() + player.if_session = None + player.mode_stack.pop() + await player.send("you leave the terminal.\r\n") else: # Dispatch normal command await mudlib.commands.dispatch(player, command) diff --git a/tests/test_if_mode.py b/tests/test_if_mode.py new file mode 100644 index 0000000..9b708af --- /dev/null +++ b/tests/test_if_mode.py @@ -0,0 +1,121 @@ +"""Tests for IF mode integration with player model and server shell.""" + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from mudlib.if_session import IFResponse, IFSession +from mudlib.player import Player + + +@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): + return Player(name="TestPlayer", x=5, y=5, reader=mock_reader, writer=mock_writer) + + +def test_player_has_if_session_attribute(player): + """Player has if_session attribute that defaults to None.""" + assert hasattr(player, "if_session") + assert player.if_session is None + + +def test_player_if_session_can_be_set(player): + """Player.if_session can be set to an IFSession instance.""" + session = IFSession(player, "/path/to/story.z5", "story") + player.if_session = session + assert player.if_session == session + + +@pytest.mark.asyncio +async def test_if_mode_routes_input_to_if_session(player): + """When mode is 'if' and if_session is set, input routes to if_session.handle_input.""" + # Create a mock IF session + mock_session = MagicMock() + mock_session.handle_input = AsyncMock( + return_value=IFResponse(output="You see a room.\r\n", done=False) + ) + + player.if_session = mock_session + player.mode_stack.append("if") + + # Simulate input routing (what shell loop should do) + if player.mode == "if" and player.if_session: + response = await player.if_session.handle_input("look") + assert response.output == "You see a room.\r\n" + assert response.done is False + + mock_session.handle_input.assert_called_once_with("look") + + +@pytest.mark.asyncio +async def test_if_mode_done_clears_session_and_pops_mode(player): + """When handle_input returns done=True, mode pops and if_session is cleared.""" + # Create a mock IF session that returns done=True + mock_session = MagicMock() + mock_session.handle_input = AsyncMock( + return_value=IFResponse(output="Goodbye.\r\n", done=True) + ) + mock_session.stop = AsyncMock() + + player.if_session = mock_session + player.mode_stack.append("if") + + # Simulate shell loop handling done response + if player.mode == "if" and player.if_session: + response = await player.if_session.handle_input("::quit") + if response.done: + await player.if_session.stop() + player.if_session = None + player.mode_stack.pop() + + assert player.mode == "normal" + assert player.if_session is None + mock_session.stop.assert_called_once() + + +@pytest.mark.asyncio +async def test_mode_stack_push_and_pop_for_if(player): + """Test mode stack mechanics for IF mode.""" + assert player.mode_stack == ["normal"] + assert player.mode == "normal" + + # Enter IF mode + player.mode_stack.append("if") + assert player.mode == "if" + assert player.mode_stack == ["normal", "if"] + + # Exit IF mode + player.mode_stack.pop() + assert player.mode == "normal" + assert player.mode_stack == ["normal"] + + +@pytest.mark.asyncio +async def test_empty_input_allowed_in_if_mode(player): + """Test that empty input is allowed in IF mode (some IF games accept blank input).""" + # Create a mock IF session + mock_session = MagicMock() + mock_session.handle_input = AsyncMock( + return_value=IFResponse(output="Time passes.\r\n", done=False) + ) + + player.if_session = mock_session + player.mode_stack.append("if") + + # Empty input should still be passed to IF session + response = await player.if_session.handle_input("") + assert response.output == "Time passes.\r\n" + mock_session.handle_input.assert_called_once_with("")