From 3dd095b9ea202b7e375fa3d734e3c24b06306c81 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Mon, 9 Feb 2026 16:24:48 -0500 Subject: [PATCH] Add broadcast_to_spectators helper Helper function sends messages to all players at the same x,y coordinates as the source player, skipping the source player themselves. Used for IF spectator broadcasting. --- src/mudlib/commands/play.py | 4 +-- src/mudlib/if_session.py | 13 +++++++++ tests/test_if_spectator.py | 54 ++++++++++++++++++------------------- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/src/mudlib/commands/play.py b/src/mudlib/commands/play.py index 8aca345..b502644 100644 --- a/src/mudlib/commands/play.py +++ b/src/mudlib/commands/play.py @@ -33,9 +33,7 @@ def _list_stories() -> list[str]: if not _stories_dir.exists(): return [] return sorted( - p.stem - for p in _stories_dir.iterdir() - if p.suffix in _STORY_EXTENSIONS + p.stem for p in _stories_dir.iterdir() if p.suffix in _STORY_EXTENSIONS ) diff --git a/src/mudlib/if_session.py b/src/mudlib/if_session.py index 3cf26bc..0cee9ec 100644 --- a/src/mudlib/if_session.py +++ b/src/mudlib/if_session.py @@ -3,6 +3,10 @@ import asyncio from dataclasses import dataclass from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from mudlib.player import Player @dataclass @@ -103,3 +107,12 @@ class IFSession: result = "".join(output) return result.rstrip() + + +async def broadcast_to_spectators(player: "Player", message: str) -> None: + """Send message to all other players at the same location.""" + from mudlib.player import players + + for other in players.values(): + if other.name != player.name and other.x == player.x and other.y == player.y: + await other.send(message) diff --git a/tests/test_if_spectator.py b/tests/test_if_spectator.py index 321a454..aceb07f 100644 --- a/tests/test_if_spectator.py +++ b/tests/test_if_spectator.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock import pytest -from mudlib.if_session import IFResponse, IFSession +from mudlib.if_session import IFResponse from mudlib.player import Player, players @@ -30,27 +30,33 @@ def clear_players(): @pytest.fixture -def player_a(mock_reader, mock_writer): +def player_a(): """Player A at (5, 5) who will be playing IF.""" - return Player( - name="PlayerA", x=5, y=5, reader=mock_reader, writer=mock_writer - ) + writer = MagicMock() + writer.write = MagicMock() + writer.drain = AsyncMock() + reader = MagicMock() + return Player(name="PlayerA", x=5, y=5, reader=reader, writer=writer) @pytest.fixture -def player_b(mock_reader, mock_writer): +def player_b(): """Player B at (5, 5) who will be spectating.""" - return Player( - name="PlayerB", x=5, y=5, reader=mock_reader, writer=mock_writer - ) + writer = MagicMock() + writer.write = MagicMock() + writer.drain = AsyncMock() + reader = MagicMock() + return Player(name="PlayerB", x=5, y=5, reader=reader, writer=writer) @pytest.fixture -def player_c(mock_reader, mock_writer): +def player_c(): """Player C at different coords (10, 10).""" - return Player( - name="PlayerC", x=10, y=10, reader=mock_reader, writer=mock_writer - ) + writer = MagicMock() + writer.write = MagicMock() + writer.drain = AsyncMock() + reader = MagicMock() + return Player(name="PlayerC", x=10, y=10, reader=reader, writer=writer) @pytest.mark.asyncio @@ -79,11 +85,7 @@ async def test_spectator_sees_if_output(player_a, player_b): 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}" - ) + 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 @@ -118,11 +120,7 @@ async def test_spectator_not_on_same_tile_sees_nothing(player_a, player_c): 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}" - ) + 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 @@ -171,12 +169,14 @@ async def test_broadcast_to_spectators_skips_self(player_a, player_b): @pytest.mark.asyncio -async def test_multiple_spectators(player_a, player_b, mock_reader, mock_writer): +async def test_multiple_spectators(player_a, player_b): """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 - ) + writer_d = MagicMock() + writer_d.write = MagicMock() + writer_d.drain = AsyncMock() + reader_d = MagicMock() + player_d = Player(name="PlayerD", x=5, y=5, reader=reader_d, writer=writer_d) # Register all players players[player_a.name] = player_a