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.
212 lines
6.6 KiB
Python
212 lines
6.6 KiB
Python
"""Tests for spectator broadcasting in IF mode."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from mudlib.if_session import IFResponse
|
|
from mudlib.player import Player, players
|
|
from mudlib.zone import Zone
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_writer():
|
|
writer = MagicMock()
|
|
writer.write = MagicMock()
|
|
writer.drain = AsyncMock()
|
|
return writer
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_reader():
|
|
return MagicMock()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_players():
|
|
"""Clear players registry before and after each test."""
|
|
players.clear()
|
|
yield
|
|
players.clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def test_zone():
|
|
"""Create a test zone for spatial queries."""
|
|
# Create a small test zone with passable terrain
|
|
terrain = [["."] * 20 for _ in range(20)]
|
|
return Zone(name="test", width=20, height=20, terrain=terrain, toroidal=False)
|
|
|
|
|
|
@pytest.fixture
|
|
def player_a(test_zone):
|
|
"""Player A at (5, 5) who will be playing IF."""
|
|
writer = MagicMock()
|
|
writer.write = MagicMock()
|
|
writer.drain = AsyncMock()
|
|
reader = MagicMock()
|
|
return Player(
|
|
name="PlayerA", location=test_zone, x=5, y=5, reader=reader, writer=writer
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def player_b(test_zone):
|
|
"""Player B at (5, 5) who will be spectating."""
|
|
writer = MagicMock()
|
|
writer.write = MagicMock()
|
|
writer.drain = AsyncMock()
|
|
reader = MagicMock()
|
|
return Player(
|
|
name="PlayerB", location=test_zone, x=5, y=5, reader=reader, writer=writer
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def player_c(test_zone):
|
|
"""Player C at different coords (10, 10)."""
|
|
writer = MagicMock()
|
|
writer.write = MagicMock()
|
|
writer.drain = AsyncMock()
|
|
reader = MagicMock()
|
|
return Player(
|
|
name="PlayerC", location=test_zone, x=10, y=10, reader=reader, writer=writer
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_spectator_sees_if_output(player_a, player_b):
|
|
"""Spectator at same location sees IF output with header and input."""
|
|
# Register both players
|
|
players[player_a.name] = player_a
|
|
players[player_b.name] = player_b
|
|
|
|
# Create mock IF session for player A
|
|
mock_session = MagicMock()
|
|
mock_session.handle_input = AsyncMock(
|
|
return_value=IFResponse(
|
|
output="Opening the small mailbox reveals a leaflet.", done=False
|
|
)
|
|
)
|
|
player_a.if_session = mock_session
|
|
player_a.mode_stack.append("if")
|
|
|
|
# Import the broadcast function (will be created in implementation)
|
|
from mudlib.if_session import broadcast_to_spectators
|
|
|
|
# Player A sends input
|
|
command = "open mailbox"
|
|
response = await player_a.if_session.handle_input(command)
|
|
await player_a.send(response.output)
|
|
|
|
# Broadcast to spectators
|
|
spectator_msg = f"[{player_a.name}'s terminal]\r\n> {command}\r\n{response.output}"
|
|
await broadcast_to_spectators(player_a, spectator_msg)
|
|
|
|
# Player B should have received the message
|
|
assert player_b.writer.write.called
|
|
calls = player_b.writer.write.call_args_list
|
|
sent_text = "".join(call[0][0] for call in calls)
|
|
assert "[PlayerA's terminal]" in sent_text
|
|
assert "> open mailbox" in sent_text
|
|
assert "Opening the small mailbox reveals a leaflet." in sent_text
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_spectator_not_on_same_tile_sees_nothing(player_a, player_c):
|
|
"""Spectator at different location does not see IF output."""
|
|
# Register both players (player_c is at different coords)
|
|
players[player_a.name] = player_a
|
|
players[player_c.name] = player_c
|
|
|
|
# Create mock IF session for player A
|
|
mock_session = MagicMock()
|
|
mock_session.handle_input = AsyncMock(
|
|
return_value=IFResponse(output="You open the door.", done=False)
|
|
)
|
|
player_a.if_session = mock_session
|
|
player_a.mode_stack.append("if")
|
|
|
|
from mudlib.if_session import broadcast_to_spectators
|
|
|
|
# Player A sends input
|
|
command = "open door"
|
|
response = await player_a.if_session.handle_input(command)
|
|
await player_a.send(response.output)
|
|
|
|
# Broadcast to spectators
|
|
spectator_msg = f"[{player_a.name}'s terminal]\r\n> {command}\r\n{response.output}"
|
|
await broadcast_to_spectators(player_a, spectator_msg)
|
|
|
|
# Player C should NOT have received anything
|
|
assert not player_c.writer.write.called
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_spectator_sees_game_start(player_a, player_b):
|
|
"""Spectator sees formatted intro text when player starts game."""
|
|
# Register both players
|
|
players[player_a.name] = player_a
|
|
players[player_b.name] = player_b
|
|
|
|
from mudlib.if_session import broadcast_to_spectators
|
|
|
|
# Simulate game start with intro text
|
|
intro = "ZORK I: The Great Underground Empire\nCopyright (c) 1981"
|
|
spectator_msg = f"[{player_a.name}'s terminal]\r\n{intro}\r\n"
|
|
|
|
await broadcast_to_spectators(player_a, spectator_msg)
|
|
|
|
# Player B should see the intro
|
|
assert player_b.writer.write.called
|
|
calls = player_b.writer.write.call_args_list
|
|
sent_text = "".join(call[0][0] for call in calls)
|
|
assert "[PlayerA's terminal]" in sent_text
|
|
assert "ZORK I: The Great Underground Empire" in sent_text
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_broadcast_to_spectators_skips_self(player_a, player_b):
|
|
"""broadcast_to_spectators does not send to the playing player."""
|
|
# Register both players
|
|
players[player_a.name] = player_a
|
|
players[player_b.name] = player_b
|
|
|
|
from mudlib.if_session import broadcast_to_spectators
|
|
|
|
message = "[PlayerA's terminal]\r\n> look\r\nYou see a room.\r\n"
|
|
await broadcast_to_spectators(player_a, message)
|
|
|
|
# Player A should NOT have received the message
|
|
assert not player_a.writer.write.called
|
|
# Player B should have received it
|
|
assert player_b.writer.write.called
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_spectators(player_a, player_b, test_zone):
|
|
"""Multiple spectators at same location all see IF output."""
|
|
# Create a third player at same location
|
|
writer_d = MagicMock()
|
|
writer_d.write = MagicMock()
|
|
writer_d.drain = AsyncMock()
|
|
reader_d = MagicMock()
|
|
player_d = Player(
|
|
name="PlayerD", location=test_zone, x=5, y=5, reader=reader_d, writer=writer_d
|
|
)
|
|
|
|
# Register all players
|
|
players[player_a.name] = player_a
|
|
players[player_b.name] = player_b
|
|
players[player_d.name] = player_d
|
|
|
|
from mudlib.if_session import broadcast_to_spectators
|
|
|
|
message = "[PlayerA's terminal]\r\n> inventory\r\nYou are empty-handed.\r\n"
|
|
await broadcast_to_spectators(player_a, message)
|
|
|
|
# Both spectators should see the message
|
|
assert player_b.writer.write.called
|
|
assert player_d.writer.write.called
|
|
# Player A should not
|
|
assert not player_a.writer.write.called
|