237 lines
5 KiB
Python
237 lines
5 KiB
Python
"""Tests for safe zone combat prevention."""
|
|
|
|
import pathlib
|
|
import tempfile
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from mudlib.combat.engine import active_encounters, get_encounter
|
|
from mudlib.entity import Mob
|
|
from mudlib.player import Player, players
|
|
from mudlib.zone import Zone
|
|
from mudlib.zones import load_zone
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_state():
|
|
"""Clear global state between tests."""
|
|
players.clear()
|
|
active_encounters.clear()
|
|
yield
|
|
players.clear()
|
|
active_encounters.clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def safe_zone():
|
|
terrain = [["." for _ in range(10)] for _ in range(10)]
|
|
return Zone(
|
|
name="sanctuary",
|
|
width=10,
|
|
height=10,
|
|
terrain=terrain,
|
|
safe=True,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def unsafe_zone():
|
|
terrain = [["." for _ in range(10)] for _ in range(10)]
|
|
return Zone(
|
|
name="wilderness",
|
|
width=10,
|
|
height=10,
|
|
terrain=terrain,
|
|
safe=False,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_writer():
|
|
writer = MagicMock()
|
|
writer.write = MagicMock()
|
|
writer.drain = AsyncMock()
|
|
return writer
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_reader():
|
|
return MagicMock()
|
|
|
|
|
|
# --- Zone property tests ---
|
|
|
|
|
|
def test_zone_safe_default_false():
|
|
"""Zones are unsafe by default."""
|
|
zone = Zone(name="test")
|
|
assert zone.safe is False
|
|
|
|
|
|
def test_zone_safe_true():
|
|
"""Zone can be created as safe."""
|
|
zone = Zone(name="test", safe=True)
|
|
assert zone.safe is True
|
|
|
|
|
|
# --- TOML loading ---
|
|
|
|
|
|
def test_load_zone_safe_flag():
|
|
"""Load a zone with safe = true from TOML."""
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
|
|
f.write(
|
|
"""
|
|
name = "sanctuary"
|
|
description = "a peaceful place"
|
|
width = 3
|
|
height = 3
|
|
safe = true
|
|
|
|
[terrain]
|
|
rows = [
|
|
"...",
|
|
"...",
|
|
"...",
|
|
]
|
|
"""
|
|
)
|
|
temp_path = pathlib.Path(f.name)
|
|
|
|
try:
|
|
zone = load_zone(temp_path)
|
|
assert zone.safe is True
|
|
finally:
|
|
temp_path.unlink()
|
|
|
|
|
|
def test_load_zone_safe_default():
|
|
"""Zone without safe field defaults to False."""
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
|
|
f.write(
|
|
"""
|
|
name = "wilderness"
|
|
description = "dangerous lands"
|
|
width = 3
|
|
height = 3
|
|
|
|
[terrain]
|
|
rows = [
|
|
"...",
|
|
"...",
|
|
"...",
|
|
]
|
|
"""
|
|
)
|
|
temp_path = pathlib.Path(f.name)
|
|
|
|
try:
|
|
zone = load_zone(temp_path)
|
|
assert zone.safe is False
|
|
finally:
|
|
temp_path.unlink()
|
|
|
|
|
|
def test_tavern_is_safe():
|
|
"""The tavern zone file has safe = true."""
|
|
project_root = pathlib.Path(__file__).resolve().parents[1]
|
|
tavern_path = project_root / "content" / "zones" / "tavern.toml"
|
|
zone = load_zone(tavern_path)
|
|
assert zone.safe is True
|
|
|
|
|
|
# --- Combat prevention ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_attack_blocked_in_safe_zone(safe_zone, mock_writer, mock_reader):
|
|
"""Attacking in a safe zone sends error message."""
|
|
from mudlib.combat.commands import do_attack
|
|
from mudlib.combat.moves import CombatMove
|
|
|
|
player = Player(
|
|
name="attacker",
|
|
x=5,
|
|
y=5,
|
|
writer=mock_writer,
|
|
reader=mock_reader,
|
|
location=safe_zone,
|
|
)
|
|
safe_zone._contents.append(player)
|
|
players["attacker"] = player
|
|
|
|
target = Mob(
|
|
name="goblin",
|
|
x=5,
|
|
y=5,
|
|
location=safe_zone,
|
|
pl=50,
|
|
stamina=40,
|
|
max_stamina=40,
|
|
)
|
|
safe_zone._contents.append(target)
|
|
|
|
move = CombatMove(
|
|
name="punch left",
|
|
command="punch",
|
|
variant="left",
|
|
move_type="attack",
|
|
stamina_cost=5,
|
|
timing_window_ms=850,
|
|
damage_pct=0.15,
|
|
)
|
|
|
|
await do_attack(player, "goblin", move)
|
|
|
|
# Should have sent the safe zone message
|
|
mock_writer.write.assert_called()
|
|
written = mock_writer.write.call_args_list[0][0][0]
|
|
assert "can't fight here" in written.lower()
|
|
|
|
# Should NOT have started combat
|
|
assert get_encounter(player) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_attack_allowed_in_unsafe_zone(unsafe_zone, mock_writer, mock_reader):
|
|
"""Attacking in an unsafe zone works normally."""
|
|
from mudlib.combat.commands import do_attack
|
|
from mudlib.combat.moves import CombatMove
|
|
|
|
player = Player(
|
|
name="fighter",
|
|
x=5,
|
|
y=5,
|
|
writer=mock_writer,
|
|
reader=mock_reader,
|
|
location=unsafe_zone,
|
|
)
|
|
unsafe_zone._contents.append(player)
|
|
players["fighter"] = player
|
|
|
|
target = Mob(
|
|
name="goblin",
|
|
x=5,
|
|
y=5,
|
|
location=unsafe_zone,
|
|
pl=50,
|
|
stamina=40,
|
|
max_stamina=40,
|
|
)
|
|
unsafe_zone._contents.append(target)
|
|
|
|
move = CombatMove(
|
|
name="punch left",
|
|
command="punch",
|
|
variant="left",
|
|
move_type="attack",
|
|
stamina_cost=5,
|
|
timing_window_ms=850,
|
|
damage_pct=0.15,
|
|
)
|
|
|
|
await do_attack(player, "goblin", move)
|
|
|
|
# Combat SHOULD have started
|
|
assert get_encounter(player) is not None
|