Add play command for starting interactive fiction games

This commit is contained in:
Jared Miller 2026-02-09 16:00:43 -05:00
parent dc342224b1
commit b133f2febe
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
3 changed files with 176 additions and 0 deletions

View file

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

View file

@ -18,6 +18,7 @@ import mudlib.commands.fly
import mudlib.commands.help import mudlib.commands.help
import mudlib.commands.look import mudlib.commands.look
import mudlib.commands.movement import mudlib.commands.movement
import mudlib.commands.play
import mudlib.commands.quit import mudlib.commands.quit
import mudlib.commands.reload import mudlib.commands.reload
import mudlib.commands.spawn import mudlib.commands.spawn

116
tests/test_play_command.py Normal file
View file

@ -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