Wire IF mode into server shell loop and player model

This commit is contained in:
Jared Miller 2026-02-09 15:57:24 -05:00
parent d210033f33
commit dc342224b1
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
3 changed files with 136 additions and 1 deletions

View file

@ -10,6 +10,7 @@ from mudlib.entity import Entity
if TYPE_CHECKING: if TYPE_CHECKING:
from mudlib.editor import Editor from mudlib.editor import Editor
from mudlib.if_session import IFSession
@dataclass @dataclass
@ -22,6 +23,7 @@ class Player(Entity):
mode_stack: list[str] = field(default_factory=lambda: ["normal"]) mode_stack: list[str] = field(default_factory=lambda: ["normal"])
caps: ClientCaps = field(default_factory=lambda: ClientCaps(ansi=True)) caps: ClientCaps = field(default_factory=lambda: ClientCaps(ansi=True))
editor: Editor | None = None editor: Editor | None = None
if_session: IFSession | None = None
@property @property
def mode(self) -> str: def mode(self) -> str:

View file

@ -312,6 +312,8 @@ async def shell(
# Show appropriate prompt based on mode # Show appropriate prompt based on mode
if player.mode == "editor" and player.editor: if player.mode == "editor" and player.editor:
_writer.write(f" {player.editor.cursor + 1}> ") _writer.write(f" {player.editor.cursor + 1}> ")
elif player.mode == "if" and player.if_session:
_writer.write("> ")
else: else:
_writer.write("mud> ") _writer.write("mud> ")
await _writer.drain() await _writer.drain()
@ -321,7 +323,7 @@ async def shell(
break break
command = inp.strip() command = inp.strip()
if not command and player.mode != "editor": if not command and player.mode not in ("editor", "if"):
continue continue
# Handle editor mode # Handle editor mode
@ -332,6 +334,16 @@ async def shell(
if response.done: if response.done:
player.editor = None player.editor = None
player.mode_stack.pop() 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: else:
# Dispatch normal command # Dispatch normal command
await mudlib.commands.dispatch(player, command) await mudlib.commands.dispatch(player, command)

121
tests/test_if_mode.py Normal file
View file

@ -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("")