Add play command for starting interactive fiction games
This commit is contained in:
parent
dc342224b1
commit
b133f2febe
3 changed files with 176 additions and 0 deletions
59
src/mudlib/commands/play.py
Normal file
59
src/mudlib/commands/play.py
Normal 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"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
@ -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
116
tests/test_play_command.py
Normal 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
|
||||||
Loading…
Reference in a new issue