Removes dependency on global players dict for spatial queries by using Zone.contents_at() for spectator lookup. Makes _world local to run_server() since it's only used during initialization to create the overworld Zone. Updates test fixtures to provide zones for spatial query tests.
199 lines
6.1 KiB
Python
199 lines
6.1 KiB
Python
"""Tests for the play command."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
from mudlib.zone import Zone
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_writer():
|
|
writer = MagicMock()
|
|
writer.write = MagicMock()
|
|
writer.drain = AsyncMock()
|
|
return writer
|
|
|
|
|
|
@pytest.fixture
|
|
def test_zone():
|
|
"""Create a test zone for spatial queries."""
|
|
terrain = [["."] * 20 for _ in range(20)]
|
|
return Zone(name="test", width=20, height=20, terrain=terrain, toroidal=False)
|
|
|
|
|
|
@pytest.fixture
|
|
def player(mock_writer, test_zone):
|
|
from mudlib.player import Player
|
|
|
|
return Player(name="tester", location=test_zone, 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" 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 pathlib import Path
|
|
|
|
from mudlib.commands.play import cmd_play
|
|
|
|
# Mock IFSession
|
|
mock_session = Mock()
|
|
mock_session.start = AsyncMock(return_value="Welcome to Zork!")
|
|
mock_session.save_path = Mock(spec=Path)
|
|
mock_session.save_path.exists = Mock(return_value=False)
|
|
|
|
with patch("mudlib.commands.play.EmbeddedIFSession") as MockSession:
|
|
MockSession.return_value = mock_session
|
|
|
|
with patch("mudlib.commands.play._find_story") as mock_find:
|
|
mock_find.return_value = "/fake/path/zork1.z5"
|
|
|
|
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 pathlib import Path
|
|
|
|
from mudlib.commands.play import cmd_play
|
|
|
|
# Mock IFSession to raise FileNotFoundError on start
|
|
mock_session = Mock()
|
|
mock_session.start = AsyncMock(side_effect=FileNotFoundError())
|
|
mock_session.stop = AsyncMock()
|
|
mock_session.save_path = Mock(spec=Path)
|
|
mock_session.save_path.exists = Mock(return_value=False)
|
|
|
|
with patch("mudlib.commands.play.IFSession") as MockIFSession:
|
|
MockIFSession.return_value = mock_session
|
|
|
|
# Use .zblorb to test dfrotz path (z3/z5/z8 go to embedded)
|
|
with patch("mudlib.commands.play._find_story") as mock_find:
|
|
mock_find.return_value = "/fake/path/game.zblorb"
|
|
|
|
await cmd_play(player, "game")
|
|
|
|
# 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
|
|
|
|
# Verify session.stop() was called
|
|
mock_session.stop.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_play_restores_save_if_exists(player):
|
|
"""Playing restores saved game if save file exists (via start())."""
|
|
from mudlib.commands.play import cmd_play
|
|
|
|
# Mock IFSession - restore now happens in start() before thread launches
|
|
mock_session = Mock()
|
|
restored_output = (
|
|
"restoring saved game...\r\nrestored.\r\n\r\n"
|
|
"West of House\nYou are standing in an open field."
|
|
)
|
|
mock_session.start = AsyncMock(return_value=restored_output)
|
|
|
|
with patch("mudlib.commands.play.EmbeddedIFSession") as MockSession:
|
|
MockSession.return_value = mock_session
|
|
|
|
with patch("mudlib.commands.play._find_story") as mock_find:
|
|
mock_find.return_value = "/fake/path/zork1.z5"
|
|
|
|
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 restored text was sent (start() returns full output with restore)
|
|
calls = [call[0][0] for call in player.writer.write.call_args_list]
|
|
full_output = "".join(calls)
|
|
assert "West of House" in full_output
|
|
assert "open field" in full_output
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_play_no_restore_if_no_save(player):
|
|
"""Playing does not restore if no save file exists."""
|
|
|
|
from mudlib.commands.play import cmd_play
|
|
|
|
# Mock EmbeddedIFSession
|
|
mock_session = Mock()
|
|
mock_session.start = AsyncMock(return_value="Welcome to Zork!")
|
|
|
|
with patch("mudlib.commands.play.EmbeddedIFSession") as MockSession:
|
|
MockSession.return_value = mock_session
|
|
|
|
with patch("mudlib.commands.play._find_story") as mock_find:
|
|
mock_find.return_value = "/fake/path/zork1.z5"
|
|
|
|
await cmd_play(player, "zork1")
|
|
|
|
# Verify session was created and started
|
|
mock_session.start.assert_called_once()
|
|
|
|
# Verify intro was sent but not restore message
|
|
calls = [call[0][0] for call in player.writer.write.call_args_list]
|
|
full_output = "".join(calls)
|
|
assert "Welcome to Zork!" in full_output
|
|
assert "restoring" not in full_output.lower()
|