Migrate movement to use player.location (Zone)

Movement commands now access the zone through player.location instead of
a module-level world variable. send_nearby_message uses
zone.contents_near() to find nearby entities, eliminating the need for
the global players dict and manual distance calculations.

Tests updated to create zones and add entities via location assignment.
This commit is contained in:
Jared Miller 2026-02-11 19:28:27 -05:00
parent 66c6e1ebd4
commit 404a1cdf0c
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
9 changed files with 232 additions and 137 deletions

View file

@ -1,13 +1,9 @@
"""Movement commands for navigating the world.""" """Movement commands for navigating the world."""
from typing import Any
from mudlib.commands import CommandDefinition, register from mudlib.commands import CommandDefinition, register
from mudlib.entity import Entity from mudlib.entity import Entity
from mudlib.player import Player, players from mudlib.player import Player
from mudlib.zone import Zone
# World instance will be injected by the server
world: Any = None
# Direction mappings: command -> (dx, dy) # Direction mappings: command -> (dx, dy)
DIRECTIONS: dict[str, tuple[int, int]] = { DIRECTIONS: dict[str, tuple[int, int]] = {
@ -66,10 +62,12 @@ async def move_player(player: Player, dx: int, dy: int, direction_name: str) ->
dy: Y delta dy: Y delta
direction_name: Full name of the direction for messages direction_name: Full name of the direction for messages
""" """
target_x, target_y = world.wrap(player.x + dx, player.y + dy) zone = player.location
assert isinstance(zone, Zone), "Player must be in a zone to move"
target_x, target_y = zone.wrap(player.x + dx, player.y + dy)
# Check if the target is passable # Check if the target is passable
if not world.is_passable(target_x, target_y): if not zone.is_passable(target_x, target_y):
await player.send("You can't go that way.\r\n") await player.send("You can't go that way.\r\n")
return return
@ -106,17 +104,11 @@ async def send_nearby_message(entity: Entity, x: int, y: int, message: str) -> N
# For now, use a simple viewport range (could be configurable) # For now, use a simple viewport range (could be configurable)
viewport_range = 10 viewport_range = 10
for other in players.values(): zone = entity.location
if other.name == entity.name: assert isinstance(zone, Zone), "Entity must be in a zone to send nearby messages"
continue for obj in zone.contents_near(x, y, viewport_range):
if obj is not entity and isinstance(obj, Entity):
# Check if other player is within viewport range (wrapping) await obj.send(message)
dx_dist = abs(other.x - x)
dy_dist = abs(other.y - y)
dx_dist = min(dx_dist, world.width - dx_dist)
dy_dist = min(dy_dist, world.height - dy_dist)
if dx_dist <= viewport_range and dy_dist <= viewport_range:
await other.send(message)
# Define individual movement command handlers # Define individual movement command handlers

View file

@ -431,7 +431,6 @@ async def run_server() -> None:
# Inject world into command modules # Inject world into command modules
mudlib.commands.fly.world = _world mudlib.commands.fly.world = _world
mudlib.commands.look.world = _world mudlib.commands.look.world = _world
mudlib.commands.movement.world = _world
mudlib.combat.commands.world = _world mudlib.combat.commands.world = _world
# Load content-defined commands from TOML files # Load content-defined commands from TOML files

View file

@ -6,11 +6,11 @@ from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
import mudlib.commands.movement as movement_mod
from mudlib.combat import commands as combat_commands from mudlib.combat import commands as combat_commands
from mudlib.combat.engine import active_encounters, get_encounter from mudlib.combat.engine import active_encounters, get_encounter
from mudlib.combat.moves import load_moves from mudlib.combat.moves import load_moves
from mudlib.player import Player, players from mudlib.player import Player, players
from mudlib.zone import Zone
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -23,16 +23,19 @@ def clear_state():
players.clear() players.clear()
@pytest.fixture(autouse=True) @pytest.fixture
def mock_world(): def test_zone():
"""Inject a mock world for send_nearby_message.""" """Create a test zone for players."""
fake_world = MagicMock() terrain = [["." for _ in range(256)] for _ in range(256)]
fake_world.width = 256 zone = Zone(
fake_world.height = 256 name="testzone",
old = movement_mod.world width=256,
movement_mod.world = fake_world height=256,
yield fake_world toroidal=True,
movement_mod.world = old terrain=terrain,
impassable=set(),
)
return zone
@pytest.fixture @pytest.fixture
@ -49,15 +52,19 @@ def mock_reader():
@pytest.fixture @pytest.fixture
def player(mock_reader, mock_writer): def player(mock_reader, mock_writer, test_zone):
p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer) p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer)
p.location = test_zone
test_zone._contents.append(p)
players[p.name] = p players[p.name] = p
return p return p
@pytest.fixture @pytest.fixture
def target(mock_reader, mock_writer): def target(mock_reader, mock_writer, test_zone):
t = Player(name="Vegeta", x=0, y=0, reader=mock_reader, writer=mock_writer) t = Player(name="Vegeta", x=0, y=0, reader=mock_reader, writer=mock_writer)
t.location = test_zone
test_zone._contents.append(t)
players[t.name] = t players[t.name] = t
return t return t

View file

@ -9,6 +9,7 @@ from mudlib.commands import CommandDefinition, look, movement
from mudlib.effects import active_effects, add_effect from mudlib.effects import active_effects, add_effect
from mudlib.player import Player from mudlib.player import Player
from mudlib.render.ansi import RESET from mudlib.render.ansi import RESET
from mudlib.zone import Zone
@pytest.fixture @pytest.fixture
@ -25,21 +26,26 @@ def mock_reader():
@pytest.fixture @pytest.fixture
def mock_world(): def test_zone():
world = MagicMock() # Create a 100x100 zone filled with passable terrain
world.width = 100 terrain = [["." for _ in range(100)] for _ in range(100)]
world.height = 100 zone = Zone(
world.is_passable = MagicMock(return_value=True) name="testzone",
world.wrap = MagicMock(side_effect=lambda x, y: (x % 100, y % 100)) width=100,
# Create a 21x11 viewport filled with "." height=100,
viewport = [["." for _ in range(21)] for _ in range(11)] toroidal=True,
world.get_viewport = MagicMock(return_value=viewport) terrain=terrain,
return world impassable=set(), # All terrain is passable for tests
)
return zone
@pytest.fixture @pytest.fixture
def player(mock_reader, mock_writer): def player(mock_reader, mock_writer, test_zone):
return Player(name="TestPlayer", x=5, y=5, reader=mock_reader, writer=mock_writer) p = Player(name="TestPlayer", x=5, y=5, reader=mock_reader, writer=mock_writer)
p.location = test_zone
test_zone._contents.append(p)
return p
# Test command registration # Test command registration
@ -132,11 +138,10 @@ def test_direction_deltas(direction, expected_delta):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_movement_updates_position(player, mock_world): async def test_movement_updates_position(player, test_zone):
"""Test that movement updates player position when passable.""" """Test that movement updates player position when passable."""
# Inject mock world into both movement and look modules # Inject test_zone into look command (still uses module-level world)
movement.world = mock_world look.world = test_zone
look.world = mock_world
# Clear players registry to avoid test pollution # Clear players registry to avoid test pollution
from mudlib.player import players from mudlib.player import players
@ -148,14 +153,15 @@ async def test_movement_updates_position(player, mock_world):
assert player.x == original_x assert player.x == original_x
assert player.y == original_y - 1 assert player.y == original_y - 1
assert mock_world.is_passable.called
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_movement_blocked_by_impassable_terrain(player, mock_world, mock_writer): async def test_movement_blocked_by_impassable_terrain(player, test_zone, mock_writer):
"""Test that movement is blocked by impassable terrain.""" """Test that movement is blocked by impassable terrain."""
mock_world.is_passable.return_value = False # Make the target position impassable
movement.world = mock_world target_y = player.y - 1
test_zone.terrain[target_y][player.x] = "^" # mountain
test_zone.impassable = {"^"}
original_x, original_y = player.x, player.y original_x, original_y = player.x, player.y
await movement.move_north(player, "") await movement.move_north(player, "")
@ -171,10 +177,9 @@ async def test_movement_blocked_by_impassable_terrain(player, mock_world, mock_w
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_movement_sends_departure_message(player, mock_world): async def test_movement_sends_departure_message(player, test_zone):
"""Test that movement sends departure message to nearby players.""" """Test that movement sends departure message to nearby players."""
movement.world = mock_world look.world = test_zone
look.world = mock_world
# Create another player in the area # Create another player in the area
other_writer = MagicMock() other_writer = MagicMock()
@ -183,6 +188,8 @@ async def test_movement_sends_departure_message(player, mock_world):
other_player = Player( other_player = Player(
name="OtherPlayer", x=5, y=4, reader=MagicMock(), writer=other_writer name="OtherPlayer", x=5, y=4, reader=MagicMock(), writer=other_writer
) )
other_player.location = test_zone
test_zone._contents.append(other_player)
# Register both players # Register both players
from mudlib.player import players from mudlib.player import players
@ -199,10 +206,9 @@ async def test_movement_sends_departure_message(player, mock_world):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_arrival_message_uses_opposite_direction(player, mock_world): async def test_arrival_message_uses_opposite_direction(player, test_zone):
"""Test that arrival messages use the opposite direction.""" """Test that arrival messages use the opposite direction."""
movement.world = mock_world look.world = test_zone
look.world = mock_world
# Create another player at the destination # Create another player at the destination
other_writer = MagicMock() other_writer = MagicMock()
@ -211,6 +217,8 @@ async def test_arrival_message_uses_opposite_direction(player, mock_world):
other_player = Player( other_player = Player(
name="OtherPlayer", x=5, y=3, reader=MagicMock(), writer=other_writer name="OtherPlayer", x=5, y=3, reader=MagicMock(), writer=other_writer
) )
other_player.location = test_zone
test_zone._contents.append(other_player)
from mudlib.player import players from mudlib.player import players
@ -228,20 +236,20 @@ async def test_arrival_message_uses_opposite_direction(player, mock_world):
# Test look command # Test look command
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_look_command_sends_viewport(player, mock_world): async def test_look_command_sends_viewport(player, test_zone):
"""Test that look command sends the viewport to the player.""" """Test that look command sends the viewport to the player."""
look.world = mock_world # look.py still uses module-level world, so inject test_zone
look.world = test_zone
await look.cmd_look(player, "") await look.cmd_look(player, "")
assert mock_world.get_viewport.called
assert player.writer.write.called assert player.writer.write.called
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_look_command_shows_player_at_center(player, mock_world): async def test_look_command_shows_player_at_center(player, test_zone):
"""Test that look command shows player @ at center.""" """Test that look command shows player @ at center."""
look.world = mock_world look.world = test_zone
await look.cmd_look(player, "") await look.cmd_look(player, "")
@ -251,9 +259,9 @@ async def test_look_command_shows_player_at_center(player, mock_world):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_look_command_shows_other_players(player, mock_world): async def test_look_command_shows_other_players(player, test_zone):
"""Test that look command shows other players as *.""" """Test that look command shows other players as *."""
look.world = mock_world look.world = test_zone
# Create another player in the viewport # Create another player in the viewport
other_player = Player( other_player = Player(
@ -263,6 +271,8 @@ async def test_look_command_shows_other_players(player, mock_world):
reader=MagicMock(), reader=MagicMock(),
writer=MagicMock(), writer=MagicMock(),
) )
other_player.location = test_zone
test_zone._contents.append(other_player)
from mudlib.player import players from mudlib.player import players
@ -278,9 +288,9 @@ async def test_look_command_shows_other_players(player, mock_world):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_look_shows_effects_on_viewport(player, mock_world): async def test_look_shows_effects_on_viewport(player, test_zone):
"""Test that active effects overlay on the viewport.""" """Test that active effects overlay on the viewport."""
look.world = mock_world look.world = test_zone
from mudlib.player import players from mudlib.player import players
@ -303,9 +313,9 @@ async def test_look_shows_effects_on_viewport(player, mock_world):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_effects_dont_override_player_marker(player, mock_world): async def test_effects_dont_override_player_marker(player, test_zone):
"""Effects at the player's position should not hide the @ marker.""" """Effects at the player's position should not hide the @ marker."""
look.world = mock_world look.world = test_zone
from mudlib.player import players from mudlib.player import players

View file

@ -5,9 +5,10 @@ from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
from mudlib.commands import fly, look, movement from mudlib.commands import fly, look
from mudlib.effects import active_effects from mudlib.effects import active_effects
from mudlib.player import Player, players from mudlib.player import Player, players
from mudlib.zone import Zone
@pytest.fixture @pytest.fixture
@ -24,27 +25,32 @@ def mock_reader():
@pytest.fixture @pytest.fixture
def mock_world(): def test_zone():
world = MagicMock() terrain = [["." for _ in range(100)] for _ in range(100)]
world.width = 100 zone = Zone(
world.height = 100 name="testzone",
world.wrap = MagicMock(side_effect=lambda x, y: (x % 100, y % 100)) width=100,
viewport = [["." for _ in range(21)] for _ in range(11)] height=100,
world.get_viewport = MagicMock(return_value=viewport) toroidal=True,
return world terrain=terrain,
impassable=set(),
)
return zone
@pytest.fixture @pytest.fixture
def player(mock_reader, mock_writer): def player(mock_reader, mock_writer, test_zone):
return Player(name="shmup", x=50, y=50, reader=mock_reader, writer=mock_writer) p = Player(name="shmup", x=50, y=50, reader=mock_reader, writer=mock_writer)
p.location = test_zone
test_zone._contents.append(p)
return p
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def clean_state(mock_world): def clean_state(test_zone):
"""Clean global state before/after each test.""" """Clean global state before/after each test."""
fly.world = mock_world fly.world = test_zone
look.world = mock_world look.world = test_zone
movement.world = mock_world
players.clear() players.clear()
active_effects.clear() active_effects.clear()
yield yield
@ -82,7 +88,7 @@ async def test_fly_toggles_off(player, mock_writer):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_fly_toggle_on_notifies_nearby(player): async def test_fly_toggle_on_notifies_nearby(player, test_zone):
"""Others see liftoff message.""" """Others see liftoff message."""
players[player.name] = player players[player.name] = player
@ -96,6 +102,8 @@ async def test_fly_toggle_on_notifies_nearby(player):
reader=MagicMock(), reader=MagicMock(),
writer=other_writer, writer=other_writer,
) )
other.location = test_zone
test_zone._contents.append(other)
players[other.name] = other players[other.name] = other
await fly.cmd_fly(player, "") await fly.cmd_fly(player, "")
@ -105,7 +113,7 @@ async def test_fly_toggle_on_notifies_nearby(player):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_fly_toggle_off_notifies_nearby(player): async def test_fly_toggle_off_notifies_nearby(player, test_zone):
"""Others see landing message.""" """Others see landing message."""
players[player.name] = player players[player.name] = player
player.flying = True player.flying = True
@ -120,6 +128,8 @@ async def test_fly_toggle_off_notifies_nearby(player):
reader=MagicMock(), reader=MagicMock(),
writer=other_writer, writer=other_writer,
) )
other.location = test_zone
test_zone._contents.append(other)
players[other.name] = other players[other.name] = other
await fly.cmd_fly(player, "") await fly.cmd_fly(player, "")
@ -279,13 +289,14 @@ async def test_fly_bad_direction_gives_error(player, mock_writer):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_fly_triggers_look(player, mock_world): async def test_fly_triggers_look(player, test_zone):
"""Flying should auto-look at the destination.""" """Flying should auto-look at the destination."""
players[player.name] = player players[player.name] = player
player.flying = True player.flying = True
await fly.cmd_fly(player, "east") await fly.cmd_fly(player, "east")
assert mock_world.get_viewport.called # look was called (check that writer was written to)
assert player.writer.write.called
@pytest.mark.asyncio @pytest.mark.asyncio

View file

@ -6,7 +6,6 @@ from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
import mudlib.commands.movement as movement_mod
from mudlib.combat import commands as combat_commands from mudlib.combat import commands as combat_commands
from mudlib.combat.encounter import CombatState from mudlib.combat.encounter import CombatState
from mudlib.combat.engine import ( from mudlib.combat.engine import (
@ -22,6 +21,7 @@ from mudlib.mobs import (
spawn_mob, spawn_mob,
) )
from mudlib.player import Player, players from mudlib.player import Player, players
from mudlib.zone import Zone
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -36,21 +36,49 @@ def clear_state():
players.clear() players.clear()
@pytest.fixture
def test_zone():
"""Create a test zone for entities."""
terrain = [["." for _ in range(256)] for _ in range(256)]
zone = Zone(
name="testzone",
width=256,
height=256,
toroidal=True,
terrain=terrain,
impassable=set(),
)
return zone
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_world(): def inject_world_for_combat(test_zone):
"""Inject a mock world for movement and combat commands.""" """Inject test_zone into combat commands (still uses module-level world)."""
fake_world = MagicMock()
fake_world.width = 256
fake_world.height = 256
old_movement = movement_mod.world
old_combat = combat_commands.world old_combat = combat_commands.world
movement_mod.world = fake_world combat_commands.world = test_zone
combat_commands.world = fake_world yield test_zone
yield fake_world
movement_mod.world = old_movement
combat_commands.world = old_combat combat_commands.world = old_combat
@pytest.fixture(autouse=True)
def auto_zone_mobs(test_zone, monkeypatch):
"""Monkeypatch spawn_mob to automatically add mobs to test_zone."""
original_spawn = spawn_mob
def spawn_and_zone(template, x, y):
mob = original_spawn(template, x, y)
mob.location = test_zone
test_zone._contents.append(mob)
return mob
monkeypatch.setattr("mudlib.mobs.spawn_mob", spawn_and_zone)
# Also patch it in the test module's import
import mudlib.mob_ai as mob_ai_mod
if hasattr(mob_ai_mod, "spawn_mob"):
monkeypatch.setattr(mob_ai_mod, "spawn_mob", spawn_and_zone)
@pytest.fixture @pytest.fixture
def mock_writer(): def mock_writer():
writer = MagicMock() writer = MagicMock()
@ -65,8 +93,10 @@ def mock_reader():
@pytest.fixture @pytest.fixture
def player(mock_reader, mock_writer): def player(mock_reader, mock_writer, test_zone):
p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer) p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer)
p.location = test_zone
test_zone._contents.append(p)
players[p.name] = p players[p.name] = p
return p return p

View file

@ -5,7 +5,6 @@ from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
import mudlib.commands.movement as movement_mod
from mudlib.combat import commands as combat_commands from mudlib.combat import commands as combat_commands
from mudlib.combat.encounter import CombatState from mudlib.combat.encounter import CombatState
from mudlib.combat.engine import active_encounters, get_encounter from mudlib.combat.engine import active_encounters, get_encounter
@ -20,6 +19,7 @@ from mudlib.mobs import (
spawn_mob, spawn_mob,
) )
from mudlib.player import Player, players from mudlib.player import Player, players
from mudlib.zone import Zone
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -34,21 +34,44 @@ def clear_state():
players.clear() players.clear()
@pytest.fixture
def test_zone():
"""Create a test zone for entities."""
terrain = [["." for _ in range(256)] for _ in range(256)]
zone = Zone(
name="testzone",
width=256,
height=256,
toroidal=True,
terrain=terrain,
impassable=set(),
)
return zone
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_world(): def inject_world_for_combat(test_zone):
"""Inject a mock world for movement and combat commands.""" """Inject test_zone into combat commands (still uses module-level world)."""
fake_world = MagicMock()
fake_world.width = 256
fake_world.height = 256
old_movement = movement_mod.world
old_combat = combat_commands.world old_combat = combat_commands.world
movement_mod.world = fake_world combat_commands.world = test_zone
combat_commands.world = fake_world yield test_zone
yield fake_world
movement_mod.world = old_movement
combat_commands.world = old_combat combat_commands.world = old_combat
@pytest.fixture(autouse=True)
def auto_zone_mobs(test_zone, monkeypatch):
"""Monkeypatch spawn_mob to automatically add mobs to test_zone."""
original_spawn = spawn_mob
def spawn_and_zone(template, x, y):
mob = original_spawn(template, x, y)
mob.location = test_zone
test_zone._contents.append(mob)
return mob
monkeypatch.setattr("mudlib.mobs.spawn_mob", spawn_and_zone)
@pytest.fixture @pytest.fixture
def goblin_toml(tmp_path): def goblin_toml(tmp_path):
"""Create a goblin TOML file.""" """Create a goblin TOML file."""

View file

@ -4,10 +4,10 @@ from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
import mudlib.commands.movement as movement_mod
from mudlib.commands.rest import cmd_rest from mudlib.commands.rest import cmd_rest
from mudlib.player import Player, players from mudlib.player import Player, players
from mudlib.resting import process_resting from mudlib.resting import process_resting
from mudlib.zone import Zone
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -18,16 +18,19 @@ def clear_state():
players.clear() players.clear()
@pytest.fixture(autouse=True) @pytest.fixture
def mock_world(): def test_zone():
"""Inject a mock world for send_nearby_message.""" """Create a test zone for players."""
fake_world = MagicMock() terrain = [["." for _ in range(256)] for _ in range(256)]
fake_world.width = 256 zone = Zone(
fake_world.height = 256 name="testzone",
old = movement_mod.world width=256,
movement_mod.world = fake_world height=256,
yield fake_world toroidal=True,
movement_mod.world = old terrain=terrain,
impassable=set(),
)
return zone
@pytest.fixture @pytest.fixture
@ -44,15 +47,19 @@ def mock_reader():
@pytest.fixture @pytest.fixture
def player(mock_reader, mock_writer): def player(mock_reader, mock_writer, test_zone):
p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer) p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer)
p.location = test_zone
test_zone._contents.append(p)
players[p.name] = p players[p.name] = p
return p return p
@pytest.fixture @pytest.fixture
def nearby_player(mock_reader, mock_writer): def nearby_player(mock_reader, mock_writer, test_zone):
p = Player(name="Vegeta", x=0, y=0, reader=mock_reader, writer=mock_writer) p = Player(name="Vegeta", x=0, y=0, reader=mock_reader, writer=mock_writer)
p.location = test_zone
test_zone._contents.append(p)
players[p.name] = p players[p.name] = p
return p return p

View file

@ -5,11 +5,11 @@ from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
import mudlib.commands.movement as movement_mod
from mudlib.combat import commands as combat_commands from mudlib.combat import commands as combat_commands
from mudlib.combat.engine import active_encounters from mudlib.combat.engine import active_encounters
from mudlib.combat.moves import load_moves from mudlib.combat.moves import load_moves
from mudlib.player import Player, players from mudlib.player import Player, players
from mudlib.zone import Zone
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -22,16 +22,28 @@ def clear_state():
players.clear() players.clear()
@pytest.fixture
def test_zone():
"""Create a test zone for players."""
terrain = [["." for _ in range(256)] for _ in range(256)]
zone = Zone(
name="testzone",
width=256,
height=256,
toroidal=True,
terrain=terrain,
impassable=set(),
)
return zone
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_world(): def inject_world_for_combat(test_zone):
"""Inject a mock world for send_nearby_message.""" """Inject test_zone into combat commands (still uses module-level world)."""
fake_world = MagicMock() old_combat = combat_commands.world
fake_world.width = 256 combat_commands.world = test_zone
fake_world.height = 256 yield test_zone
old = movement_mod.world combat_commands.world = old_combat
movement_mod.world = fake_world
yield fake_world
movement_mod.world = old
@pytest.fixture @pytest.fixture
@ -48,15 +60,19 @@ def mock_reader():
@pytest.fixture @pytest.fixture
def player(mock_reader, mock_writer): def player(mock_reader, mock_writer, test_zone):
p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer) p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer)
p.location = test_zone
test_zone._contents.append(p)
players[p.name] = p players[p.name] = p
return p return p
@pytest.fixture @pytest.fixture
def target(mock_reader, mock_writer): def target(mock_reader, mock_writer, test_zone):
t = Player(name="Vegeta", x=0, y=0, reader=mock_reader, writer=mock_writer) t = Player(name="Vegeta", x=0, y=0, reader=mock_reader, writer=mock_writer)
t.location = test_zone
test_zone._contents.append(t)
players[t.name] = t players[t.name] = t
return t return t