Removed identical local copies from 45 test files. These fixtures are already defined in conftest.py.
294 lines
7.5 KiB
Python
294 lines
7.5 KiB
Python
"""Tests for the fly command."""
|
|
|
|
import time
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from mudlib.commands import fly
|
|
from mudlib.effects import active_effects
|
|
from mudlib.player import Player, players
|
|
from mudlib.zone import Zone
|
|
|
|
|
|
@pytest.fixture
|
|
def test_zone():
|
|
terrain = [["." for _ in range(100)] for _ in range(100)]
|
|
zone = Zone(
|
|
name="testzone",
|
|
width=100,
|
|
height=100,
|
|
toroidal=True,
|
|
terrain=terrain,
|
|
impassable=set(),
|
|
)
|
|
return zone
|
|
|
|
|
|
@pytest.fixture
|
|
def player(mock_reader, mock_writer, test_zone):
|
|
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)
|
|
def clean_state(test_zone):
|
|
"""Clean global state before/after each test."""
|
|
players.clear()
|
|
active_effects.clear()
|
|
yield
|
|
players.clear()
|
|
active_effects.clear()
|
|
|
|
|
|
# --- toggle ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_toggles_on(player, mock_writer):
|
|
"""fly with no args sets flying=True."""
|
|
players[player.name] = player
|
|
assert not player.flying
|
|
|
|
await fly.cmd_fly(player, "")
|
|
|
|
assert player.flying
|
|
calls = [str(c) for c in mock_writer.write.call_args_list]
|
|
assert any("fly" in c.lower() and "air" in c.lower() for c in calls)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_toggles_off(player, mock_writer):
|
|
"""fly again sets flying=False."""
|
|
players[player.name] = player
|
|
player.flying = True
|
|
|
|
await fly.cmd_fly(player, "")
|
|
|
|
assert not player.flying
|
|
calls = [str(c) for c in mock_writer.write.call_args_list]
|
|
assert any("land" in c.lower() for c in calls)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_toggle_on_notifies_nearby(player, test_zone):
|
|
"""Others see liftoff message."""
|
|
players[player.name] = player
|
|
|
|
other_writer = MagicMock()
|
|
other_writer.write = MagicMock()
|
|
other_writer.drain = AsyncMock()
|
|
other = Player(
|
|
name="bystander",
|
|
x=52,
|
|
y=50,
|
|
reader=MagicMock(),
|
|
writer=other_writer,
|
|
)
|
|
other.location = test_zone
|
|
test_zone._contents.append(other)
|
|
players[other.name] = other
|
|
|
|
await fly.cmd_fly(player, "")
|
|
|
|
calls = [str(c) for c in other_writer.write.call_args_list]
|
|
assert any("shmup" in c.lower() and "lifts" in c.lower() for c in calls)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_toggle_off_notifies_nearby(player, test_zone):
|
|
"""Others see landing message."""
|
|
players[player.name] = player
|
|
player.flying = True
|
|
|
|
other_writer = MagicMock()
|
|
other_writer.write = MagicMock()
|
|
other_writer.drain = AsyncMock()
|
|
other = Player(
|
|
name="bystander",
|
|
x=52,
|
|
y=50,
|
|
reader=MagicMock(),
|
|
writer=other_writer,
|
|
)
|
|
other.location = test_zone
|
|
test_zone._contents.append(other)
|
|
players[other.name] = other
|
|
|
|
await fly.cmd_fly(player, "")
|
|
|
|
calls = [str(c) for c in other_writer.write.call_args_list]
|
|
assert any("shmup" in c.lower() and "lands" in c.lower() for c in calls)
|
|
|
|
|
|
# --- must be flying to move ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_direction_without_flying_errors(player, mock_writer):
|
|
"""fly east when not flying gives an error."""
|
|
players[player.name] = player
|
|
assert not player.flying
|
|
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
calls = [str(c) for c in mock_writer.write.call_args_list]
|
|
assert any("aren't flying" in c.lower() for c in calls)
|
|
assert player.x == 50
|
|
assert player.y == 50
|
|
|
|
|
|
# --- movement while flying ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_east_moves_5_tiles(player):
|
|
"""fly east should move player 5 tiles east."""
|
|
players[player.name] = player
|
|
player.flying = True
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
assert player.x == 55
|
|
assert player.y == 50
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_north_moves_5_tiles(player):
|
|
players[player.name] = player
|
|
player.flying = True
|
|
await fly.cmd_fly(player, "north")
|
|
|
|
assert player.x == 50
|
|
assert player.y == 45
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_northwest_moves_5_diagonal(player):
|
|
players[player.name] = player
|
|
player.flying = True
|
|
await fly.cmd_fly(player, "nw")
|
|
|
|
assert player.x == 45
|
|
assert player.y == 45
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_wraps_around_world(player):
|
|
"""Flying near an edge should wrap toroidally."""
|
|
player.x = 98
|
|
player.y = 50
|
|
player.flying = True
|
|
players[player.name] = player
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
assert player.x == 3
|
|
assert player.y == 50
|
|
|
|
|
|
# --- cloud trail ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_creates_cloud_trail(player):
|
|
"""Flying should leave ~ effects along the path."""
|
|
players[player.name] = player
|
|
player.flying = True
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
assert len(active_effects) == 5
|
|
trail_positions = [(e.x, e.y) for e in active_effects]
|
|
for i in range(5):
|
|
assert (50 + i, 50) in trail_positions
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cloud_trail_chars_are_tilde(player):
|
|
players[player.name] = player
|
|
player.flying = True
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
for e in active_effects:
|
|
assert e.char == "~"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cloud_trail_dissolves_from_origin(player):
|
|
"""Origin cloud expires first, trail shrinks toward player."""
|
|
players[player.name] = player
|
|
player.flying = True
|
|
|
|
before = time.monotonic()
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
# effects are in path order (step 0..4)
|
|
expiries = [e.expires_at for e in active_effects]
|
|
# each should expire after the previous one (origin fades first)
|
|
for i in range(1, len(expiries)):
|
|
assert expiries[i] > expiries[i - 1]
|
|
# origin cloud ~1.5s, near-dest cloud ~1.5 + 4*0.4 = ~3.1s
|
|
assert 1.3 <= expiries[0] - before <= 1.7
|
|
assert 2.9 <= expiries[-1] - before <= 3.3
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_diagonal_trail(player):
|
|
"""Diagonal flight should leave trail at each intermediate step."""
|
|
players[player.name] = player
|
|
player.flying = True
|
|
await fly.cmd_fly(player, "se")
|
|
|
|
assert len(active_effects) == 5
|
|
trail_positions = [(e.x, e.y) for e in active_effects]
|
|
for i in range(5):
|
|
assert (50 + i, 50 + i) in trail_positions
|
|
|
|
|
|
# --- no movement without flying leaves no trail ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_trail_when_not_flying(player):
|
|
"""Trying to fly a direction while grounded creates no effects."""
|
|
players[player.name] = player
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
assert len(active_effects) == 0
|
|
|
|
|
|
# --- error cases ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_bad_direction_gives_error(player, mock_writer):
|
|
"""fly with an invalid direction should give feedback."""
|
|
players[player.name] = player
|
|
player.flying = True
|
|
await fly.cmd_fly(player, "sideways")
|
|
|
|
calls = [str(c) for c in mock_writer.write.call_args_list]
|
|
assert any("direction" in c.lower() for c in calls)
|
|
assert player.x == 50
|
|
assert player.y == 50
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fly_triggers_look(player, test_zone):
|
|
"""Flying should auto-look at the destination."""
|
|
players[player.name] = player
|
|
player.flying = True
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
# look was called (check that writer was written to)
|
|
assert player.writer.write.called
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stays_flying_after_move(player):
|
|
"""Moving while flying doesn't turn off flying."""
|
|
players[player.name] = player
|
|
player.flying = True
|
|
await fly.cmd_fly(player, "east")
|
|
|
|
assert player.flying
|