diff --git a/tests/test_if_spectator.py b/tests/test_if_spectator.py new file mode 100644 index 0000000..321a454 --- /dev/null +++ b/tests/test_if_spectator.py @@ -0,0 +1,195 @@ +"""Tests for spectator broadcasting in IF mode.""" + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from mudlib.if_session import IFResponse, IFSession +from mudlib.player import Player, players + + +@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 player_a(mock_reader, mock_writer): + """Player A at (5, 5) who will be playing IF.""" + return Player( + name="PlayerA", x=5, y=5, reader=mock_reader, writer=mock_writer + ) + + +@pytest.fixture +def player_b(mock_reader, mock_writer): + """Player B at (5, 5) who will be spectating.""" + return Player( + name="PlayerB", x=5, y=5, reader=mock_reader, writer=mock_writer + ) + + +@pytest.fixture +def player_c(mock_reader, mock_writer): + """Player C at different coords (10, 10).""" + return Player( + name="PlayerC", x=10, y=10, reader=mock_reader, writer=mock_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" + f"> {command}\r\n" + f"{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" + f"> {command}\r\n" + f"{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, mock_reader, mock_writer): + """Multiple spectators at same location all see IF output.""" + # Create a third player at same location + player_d = Player( + name="PlayerD", x=5, y=5, reader=mock_reader, writer=mock_writer + ) + + # 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