diff --git a/src/mudlib/commands/play.py b/src/mudlib/commands/play.py new file mode 100644 index 0000000..df5cfbd --- /dev/null +++ b/src/mudlib/commands/play.py @@ -0,0 +1,59 @@ +"""Play interactive fiction games.""" + +import pathlib + +from mudlib.commands import CommandDefinition, register +from mudlib.if_session import IFSession +from mudlib.player import Player + +# Story files directory +_stories_dir = pathlib.Path(__file__).resolve().parents[3] / "content" / "stories" + +# Map of game name -> file extension for lookup +_STORY_EXTENSIONS = (".z3", ".z5", ".z8", ".zblorb") + + +def _find_story(name: str) -> pathlib.Path | None: + """Find a story file by name in content/stories/.""" + for ext in _STORY_EXTENSIONS: + path = _stories_dir / f"{name}{ext}" + if path.exists(): + return path + return None + + +async def cmd_play(player: Player, args: str) -> None: + """Start playing an interactive fiction game.""" + game_name = args.strip().lower() + if not game_name: + await player.send("play what? (try: play zork1)\r\n") + return + + story_path = _find_story(game_name) + if not story_path: + await player.send(f"no story found for '{game_name}'.\r\n") + return + + # Create and start IF session + session = IFSession(player, str(story_path), game_name) + try: + intro = await session.start() + except FileNotFoundError: + await player.send("error: dfrotz not found. cannot play IF games.\r\n") + return + except OSError as e: + await player.send(f"error starting game: {e}\r\n") + return + + player.if_session = session + player.mode_stack.append("if") + + if intro: + await player.send(intro + "\r\n") + + +register( + CommandDefinition( + "play", cmd_play, mode="normal", help="play an interactive fiction game" + ) +) diff --git a/src/mudlib/server.py b/src/mudlib/server.py index a62cdcd..ffa197d 100644 --- a/src/mudlib/server.py +++ b/src/mudlib/server.py @@ -18,6 +18,7 @@ import mudlib.commands.fly import mudlib.commands.help import mudlib.commands.look import mudlib.commands.movement +import mudlib.commands.play import mudlib.commands.quit import mudlib.commands.reload import mudlib.commands.spawn diff --git a/tests/test_play_command.py b/tests/test_play_command.py new file mode 100644 index 0000000..7d122e2 --- /dev/null +++ b/tests/test_play_command.py @@ -0,0 +1,116 @@ +"""Tests for the play command.""" + +from unittest.mock import AsyncMock, MagicMock, Mock, patch + +import pytest + + +@pytest.fixture +def mock_writer(): + writer = MagicMock() + writer.write = MagicMock() + writer.drain = AsyncMock() + return writer + + +@pytest.fixture +def player(mock_writer): + from mudlib.player import Player + + return Player(name="tester", x=5, y=5, writer=mock_writer) + + +def test_play_command_registered(): + """Verify play command is registered.""" + import mudlib.commands.play # noqa: F401 + from mudlib import commands + + assert "play" in commands._registry + cmd = commands._registry["play"] + assert cmd.name == "play" + assert cmd.mode == "normal" + + +@pytest.mark.asyncio +async def test_play_no_args(player): + """Playing with no args sends usage message.""" + from mudlib.commands.play import cmd_play + + await cmd_play(player, "") + player.writer.write.assert_called() + output = player.writer.write.call_args[0][0] + assert "play what?" in output.lower() + + +@pytest.mark.asyncio +async def test_play_unknown_story(player): + """Playing unknown story sends error message.""" + from mudlib.commands.play import cmd_play + + await cmd_play(player, "nosuchgame") + player.writer.write.assert_called() + output = player.writer.write.call_args[0][0] + assert "no story found" in output.lower() + assert "nosuchgame" in output.lower() + + +@pytest.mark.asyncio +async def test_play_enters_if_mode(player): + """Playing a valid story enters IF mode and creates session.""" + from mudlib.commands.play import cmd_play + + # Mock IFSession + mock_session = Mock() + mock_session.start = AsyncMock(return_value="Welcome to Zork!") + + with patch("mudlib.commands.play.IFSession") as MockIFSession: + MockIFSession.return_value = mock_session + + # Ensure story file exists check passes + with patch("mudlib.commands.play._find_story") as mock_find: + mock_find.return_value = "/fake/path/zork1.z3" + + await cmd_play(player, "zork1") + + # Verify session was created and started + mock_session.start.assert_called_once() + + # Verify mode was pushed + assert "if" in player.mode_stack + + # Verify session was attached to player + assert player.if_session is mock_session + + # Verify intro was sent + player.writer.write.assert_called() + output = player.writer.write.call_args[0][0] + assert "Welcome to Zork!" in output + + +@pytest.mark.asyncio +async def test_play_handles_dfrotz_missing(player): + """Playing when dfrotz is missing sends error.""" + from mudlib.commands.play import cmd_play + + # Mock IFSession to raise FileNotFoundError on start + mock_session = Mock() + mock_session.start = AsyncMock(side_effect=FileNotFoundError()) + + with patch("mudlib.commands.play.IFSession") as MockIFSession: + MockIFSession.return_value = mock_session + + with patch("mudlib.commands.play._find_story") as mock_find: + mock_find.return_value = "/fake/path/zork1.z3" + + await cmd_play(player, "zork1") + + # Verify error message was sent + player.writer.write.assert_called() + output = player.writer.write.call_args[0][0] + assert "dfrotz not found" in output.lower() + + # Verify mode was NOT pushed + assert "if" not in player.mode_stack + + # Verify session was NOT attached + assert player.if_session is None