"""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(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