Clean up global state, migrate broadcast_to_spectators to Zone
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.
This commit is contained in:
parent
f5646589b5
commit
957a411601
5 changed files with 52 additions and 27 deletions
|
|
@ -236,8 +236,12 @@ class IFSession:
|
|||
|
||||
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
|
||||
from mudlib.entity import Entity
|
||||
from mudlib.zone import Zone
|
||||
|
||||
for other in players.values():
|
||||
if other.name != player.name and other.x == player.x and other.y == player.y:
|
||||
await other.send(message)
|
||||
# Use zone spatial query to find all objects at player's exact coordinates
|
||||
assert isinstance(player.location, Zone), "Player must be in a zone"
|
||||
for obj in player.location.contents_at(player.x, player.y):
|
||||
# Filter for Entity instances (players/mobs) and exclude self
|
||||
if obj is not player and isinstance(obj, Entity):
|
||||
await obj.send(message)
|
||||
|
|
|
|||
|
|
@ -53,8 +53,7 @@ TICK_RATE = 10 # ticks per second
|
|||
TICK_INTERVAL = 1.0 / TICK_RATE
|
||||
AUTOSAVE_INTERVAL = 60.0 # seconds between auto-saves
|
||||
|
||||
# Module-level world instance, generated once at startup
|
||||
_world: World | None = None
|
||||
# Module-level overworld zone instance, created once at startup
|
||||
_overworld: Zone | None = None
|
||||
|
||||
|
||||
|
|
@ -211,7 +210,6 @@ async def shell(
|
|||
_reader = cast(telnetlib3.TelnetReaderUnicode, reader)
|
||||
_writer = cast(telnetlib3.TelnetWriterUnicode, writer)
|
||||
|
||||
assert _world is not None, "World must be initialized before accepting connections"
|
||||
assert _overworld is not None, (
|
||||
"Overworld zone must be initialized before accepting connections"
|
||||
)
|
||||
|
|
@ -401,7 +399,7 @@ async def shell(
|
|||
|
||||
async def run_server() -> None:
|
||||
"""Start the MUD telnet server."""
|
||||
global _world, _overworld
|
||||
global _overworld
|
||||
|
||||
# Initialize database
|
||||
data_dir = pathlib.Path(__file__).resolve().parents[2] / "data"
|
||||
|
|
@ -420,14 +418,14 @@ async def run_server() -> None:
|
|||
world_cfg["height"],
|
||||
)
|
||||
t0 = time.monotonic()
|
||||
_world = World(
|
||||
world = World(
|
||||
seed=world_cfg["seed"],
|
||||
width=world_cfg["width"],
|
||||
height=world_cfg["height"],
|
||||
cache_dir=cache_dir,
|
||||
)
|
||||
elapsed = time.monotonic() - t0
|
||||
if _world.cached:
|
||||
if world.cached:
|
||||
log.info("world loaded from cache in %.2fs", elapsed)
|
||||
else:
|
||||
log.info("world generated in %.2fs (cached for next run)", elapsed)
|
||||
|
|
@ -435,12 +433,12 @@ async def run_server() -> None:
|
|||
# Create overworld zone from generated terrain
|
||||
_overworld = Zone(
|
||||
name="overworld",
|
||||
width=_world.width,
|
||||
height=_world.height,
|
||||
terrain=_world.terrain,
|
||||
width=world.width,
|
||||
height=world.height,
|
||||
terrain=world.terrain,
|
||||
toroidal=True,
|
||||
)
|
||||
log.info("created overworld zone (%dx%d, toroidal)", _world.width, _world.height)
|
||||
log.info("created overworld zone (%dx%d, toroidal)", world.width, world.height)
|
||||
|
||||
# Load content-defined commands from TOML files
|
||||
content_dir = pathlib.Path(__file__).resolve().parents[2] / "content" / "commands"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import pytest
|
|||
|
||||
from mudlib.if_session import IFResponse
|
||||
from mudlib.player import Player, players
|
||||
from mudlib.zone import Zone
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -30,33 +31,47 @@ def clear_players():
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def player_a():
|
||||
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", x=5, y=5, reader=reader, writer=writer)
|
||||
return Player(
|
||||
name="PlayerA", location=test_zone, x=5, y=5, reader=reader, writer=writer
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def player_b():
|
||||
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", x=5, y=5, reader=reader, writer=writer)
|
||||
return Player(
|
||||
name="PlayerB", location=test_zone, x=5, y=5, reader=reader, writer=writer
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def player_c():
|
||||
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", x=10, y=10, reader=reader, writer=writer)
|
||||
return Player(
|
||||
name="PlayerC", location=test_zone, x=10, y=10, reader=reader, writer=writer
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -169,14 +184,16 @@ async def test_broadcast_to_spectators_skips_self(player_a, player_b):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_spectators(player_a, player_b):
|
||||
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", x=5, y=5, reader=reader_d, writer=writer_d)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
|||
|
||||
import pytest
|
||||
|
||||
from mudlib.zone import Zone
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_writer():
|
||||
|
|
@ -14,10 +16,17 @@ def mock_writer():
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def player(mock_writer):
|
||||
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", x=5, y=5, writer=mock_writer)
|
||||
return Player(name="tester", location=test_zone, x=5, y=5, writer=mock_writer)
|
||||
|
||||
|
||||
def test_play_command_registered():
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ async def test_shell_greets_and_accepts_commands(temp_db):
|
|||
zone = Zone(
|
||||
name="overworld", width=100, height=100, terrain=world.terrain, toroidal=True
|
||||
)
|
||||
server._world = world
|
||||
server._overworld = zone
|
||||
|
||||
reader = AsyncMock()
|
||||
|
|
@ -94,7 +93,6 @@ async def test_shell_handles_eof():
|
|||
zone = Zone(
|
||||
name="overworld", width=100, height=100, terrain=world.terrain, toroidal=True
|
||||
)
|
||||
server._world = world
|
||||
server._overworld = zone
|
||||
|
||||
reader = AsyncMock()
|
||||
|
|
@ -117,7 +115,6 @@ async def test_shell_handles_quit(temp_db):
|
|||
zone = Zone(
|
||||
name="overworld", width=100, height=100, terrain=world.terrain, toroidal=True
|
||||
)
|
||||
server._world = world
|
||||
server._overworld = zone
|
||||
|
||||
reader = AsyncMock()
|
||||
|
|
|
|||
Loading…
Reference in a new issue