mud/tests/test_paint_mode.py
Jared Miller ee0dc839d8
Offer GMCP/MSDP during connection and guard tick sends
The server never proactively offered GMCP or MSDP to clients, so
telnetlib3 logged "cannot send MSDP without negotiation" every second.
Now the server sends WILL GMCP and WILL MSDP on connection, and
send_msdp_vitals checks negotiation state before attempting to send.
2026-02-12 15:58:54 -05:00

238 lines
6.5 KiB
Python

"""Tests for paint mode - terrain editing admin tool."""
import pytest
from mudlib.player import Player
from mudlib.zone import Zone
@pytest.fixture
def player():
"""Create a test player with a mock writer."""
mock_writer = MockWriter()
player = Player(
name="TestPlayer",
x=5,
y=5,
writer=mock_writer,
)
# Create a simple zone for testing
zone = Zone(
"test_zone",
width=20,
height=20,
terrain=[["." for _ in range(20)] for _ in range(20)],
impassable={"^", "~", "#"}, # Add # as impassable
)
# Add a wall for passability testing
zone.terrain[5][6] = "#"
player.location = zone
return player
class MockWriter:
"""Mock writer that captures output."""
def __init__(self):
self.messages = []
# Mock option negotiation state
from unittest.mock import MagicMock
self.local_option = MagicMock()
self.local_option.enabled = MagicMock(return_value=False)
self.remote_option = MagicMock()
self.remote_option.enabled = MagicMock(return_value=False)
def write(self, message: str):
self.messages.append(message)
async def drain(self):
pass
def send_gmcp(self, package: str, data: dict):
"""Mock GMCP send (no-op for paint mode tests)."""
pass
def send_msdp(self, data: dict):
"""Mock MSDP send (no-op for paint mode tests)."""
pass
def get_output(self) -> str:
return "".join(self.messages)
def clear(self):
self.messages = []
@pytest.mark.asyncio
async def test_enter_paint_mode(player):
"""@paint command sets player.paint_mode = True and sends confirmation."""
from mudlib.commands.paint import cmd_paint
await cmd_paint(player, "")
assert player.paint_mode is True
output = player.writer.get_output()
assert "paint mode" in output.lower()
@pytest.mark.asyncio
async def test_exit_paint_mode(player):
"""@paint when already in paint mode exits it."""
from mudlib.commands.paint import cmd_paint
# Enter paint mode first
await cmd_paint(player, "")
player.writer.clear()
# Exit paint mode
await cmd_paint(player, "")
assert player.paint_mode is False
output = player.writer.get_output()
assert "exit" in output.lower() or "off" in output.lower()
@pytest.mark.asyncio
async def test_paint_mode_default_survey(player):
"""Entering paint mode starts in survey state (player.painting = False)."""
from mudlib.commands.paint import cmd_paint
await cmd_paint(player, "")
assert player.paint_mode is True
assert player.painting is False
@pytest.mark.asyncio
async def test_toggle_painting(player):
"""p command toggles player.painting between True and False."""
from mudlib.commands.paint import cmd_toggle_painting
# Must be in paint mode first
player.paint_mode = True
# Toggle to painting
await cmd_toggle_painting(player, "")
assert player.painting is True
player.writer.clear()
# Toggle back to survey
await cmd_toggle_painting(player, "")
assert player.painting is False
@pytest.mark.asyncio
async def test_toggle_painting_requires_paint_mode(player):
"""p command only works in paint mode."""
from mudlib.commands.paint import cmd_toggle_painting
player.paint_mode = False
await cmd_toggle_painting(player, "")
output = player.writer.get_output()
assert "paint mode" in output.lower()
@pytest.mark.asyncio
async def test_set_brush(player):
"""Typing a single character while in paint mode sets player.paint_brush."""
from mudlib.commands.paint import cmd_set_brush
player.paint_mode = True
await cmd_set_brush(player, "#")
assert player.paint_brush == "#"
await cmd_set_brush(player, "~")
assert player.paint_brush == "~"
@pytest.mark.asyncio
async def test_set_brush_requires_paint_mode(player):
"""Brush command only works in paint mode."""
from mudlib.commands.paint import cmd_set_brush
player.paint_mode = False
await cmd_set_brush(player, "#")
output = player.writer.get_output()
assert "paint mode" in output.lower()
@pytest.mark.asyncio
async def test_set_brush_requires_single_char(player):
"""Brush must be a single character."""
from mudlib.commands.paint import cmd_set_brush
player.paint_mode = True
await cmd_set_brush(player, "##")
output = player.writer.get_output()
assert "single character" in output.lower()
@pytest.mark.asyncio
async def test_paint_mode_movement_ignores_passability(player):
"""Movement in paint mode doesn't check passability."""
from mudlib.commands.movement import move_player
# There's a wall at (6, 5) - normally can't move there
player.paint_mode = True
# Should be able to move into the wall
await move_player(player, 1, 0, "east")
assert player.x == 6
assert player.y == 5
@pytest.mark.asyncio
async def test_painting_places_tile(player):
"""Moving while painting sets terrain tile at old position to brush char."""
from mudlib.commands.movement import move_player
player.paint_mode = True
player.painting = True
player.paint_brush = "~"
# Move from (5,5) to (6,5)
old_x, old_y = player.x, player.y
await move_player(player, 1, 0, "east")
# Check that the old position now has the brush character
zone = player.location
assert isinstance(zone, Zone)
assert zone.terrain[old_y][old_x] == "~"
@pytest.mark.asyncio
async def test_painting_only_in_paint_mode(player):
"""Painting flag has no effect outside paint mode."""
from mudlib.commands.movement import move_player
player.paint_mode = False
player.painting = True
player.paint_brush = "~"
# Try to move into wall at (6, 5)
await move_player(player, 1, 0, "east")
# Should have failed - player still at (5, 5)
assert player.x == 5
assert player.y == 5
@pytest.mark.asyncio
async def test_survey_mode_does_not_paint(player):
"""Survey mode (painting=False) allows movement but doesn't paint."""
from mudlib.commands.movement import move_player
player.paint_mode = True
player.painting = False
player.paint_brush = "~"
old_x, old_y = player.x, player.y
await move_player(player, 1, 0, "east")
# Movement should have happened
assert player.x == 6
assert player.y == 5
# But no painting should have occurred
zone = player.location
assert isinstance(zone, Zone)
assert zone.terrain[old_y][old_x] == "." # Still the original tile