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.look
|
||||
import mudlib.commands.movement
|
||||
import mudlib.commands.play
|
||||
import mudlib.commands.quit
|
||||
import mudlib.commands.reload
|
||||
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