"""Tests for Corpse class and create_corpse factory.""" import time from unittest.mock import MagicMock import pytest from mudlib.container import Container from mudlib.corpse import Corpse, create_corpse from mudlib.entity import Mob from mudlib.mobs import mobs from mudlib.thing import Thing from mudlib.zone import Zone @pytest.fixture(autouse=True) def clear_mobs(): """Clear mobs registry before and after each test.""" mobs.clear() yield mobs.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 def goblin_mob(test_zone): """Create a goblin mob at (5, 10) in the test zone.""" mob = Mob( name="goblin", x=5, y=10, location=test_zone, pl=50.0, stamina=40.0, ) mobs.append(mob) return mob @pytest.fixture def sword(): """Create a sword Thing.""" return Thing(name="sword", description="a rusty sword", portable=True) @pytest.fixture def potion(): """Create a potion Thing.""" return Thing(name="potion", description="a health potion", portable=True) class TestCorpseClass: def test_corpse_is_container_subclass(self): """Corpse is a subclass of Container.""" assert issubclass(Corpse, Container) def test_corpse_not_portable(self, test_zone): """Corpse is not portable (can't pick up a corpse).""" corpse = Corpse(name="test corpse", location=test_zone, x=0, y=0) assert corpse.portable is False def test_corpse_always_open(self, test_zone): """Corpse is always open (closed=False).""" corpse = Corpse(name="test corpse", location=test_zone, x=0, y=0) assert corpse.closed is False def test_corpse_has_decompose_at_field(self, test_zone): """Corpse has decompose_at field (float, monotonic time).""" decompose_time = time.monotonic() + 300 corpse = Corpse( name="test corpse", location=test_zone, x=0, y=0, decompose_at=decompose_time, ) assert hasattr(corpse, "decompose_at") assert isinstance(corpse.decompose_at, float) assert corpse.decompose_at == decompose_time class TestCreateCorpseFactory: def test_creates_corpse_at_mob_position(self, goblin_mob, test_zone): """create_corpse creates a corpse at mob's x, y in the zone.""" corpse = create_corpse(goblin_mob, test_zone) assert isinstance(corpse, Corpse) assert corpse.x == 5 assert corpse.y == 10 assert corpse.location is test_zone def test_corpse_name_from_mob(self, goblin_mob, test_zone): """Corpse name is '{mob.name}'s corpse'.""" corpse = create_corpse(goblin_mob, test_zone) assert corpse.name == "goblin's corpse" def test_transfers_mob_inventory(self, goblin_mob, test_zone, sword, potion): """Transfers mob's inventory items into the corpse.""" # Add items to mob's inventory sword.move_to(goblin_mob) potion.move_to(goblin_mob) corpse = create_corpse(goblin_mob, test_zone) # Items should now be in the corpse assert sword in corpse._contents assert potion in corpse._contents assert sword.location is corpse assert potion.location is corpse def test_sets_decompose_at(self, goblin_mob, test_zone): """Sets decompose_at = time.monotonic() + ttl.""" before = time.monotonic() corpse = create_corpse(goblin_mob, test_zone, ttl=300) after = time.monotonic() # decompose_at should be within reasonable range assert corpse.decompose_at >= before + 300 assert corpse.decompose_at <= after + 300 def test_custom_ttl(self, goblin_mob, test_zone): """create_corpse respects custom ttl parameter.""" before = time.monotonic() corpse = create_corpse(goblin_mob, test_zone, ttl=600) after = time.monotonic() assert corpse.decompose_at >= before + 600 assert corpse.decompose_at <= after + 600 def test_calls_despawn_mob(self, goblin_mob, test_zone): """create_corpse calls despawn_mob to remove mob from registry.""" assert goblin_mob in mobs assert goblin_mob.alive is True create_corpse(goblin_mob, test_zone) assert goblin_mob not in mobs assert goblin_mob.alive is False def test_returns_corpse(self, goblin_mob, test_zone): """create_corpse returns the created corpse.""" corpse = create_corpse(goblin_mob, test_zone) assert isinstance(corpse, Corpse) assert corpse.name == "goblin's corpse" def test_empty_inventory(self, goblin_mob, test_zone): """create_corpse with a mob that has no inventory creates empty corpse.""" corpse = create_corpse(goblin_mob, test_zone) assert len(corpse._contents) == 0 assert corpse.name == "goblin's corpse" class TestCorpseAsContainer: def test_can_put_things_in_corpse(self, goblin_mob, test_zone, sword): """Container commands work: Things can be put into corpses.""" corpse = create_corpse(goblin_mob, test_zone) # Manually move item into corpse (simulating "put" command) sword.move_to(corpse) assert sword in corpse._contents assert sword.location is corpse def test_can_take_things_from_corpse(self, goblin_mob, test_zone, sword, potion): """Container commands work: Things can be taken out of corpses.""" sword.move_to(goblin_mob) potion.move_to(goblin_mob) corpse = create_corpse(goblin_mob, test_zone) # Items start in corpse assert sword in corpse._contents assert potion in corpse._contents # Take sword out (move to zone) sword.move_to(test_zone, x=5, y=10) assert sword not in corpse._contents assert sword.location is test_zone assert potion in corpse._contents def test_corpse_can_accept_things(self, goblin_mob, test_zone, sword): """Corpse.can_accept returns True for Things.""" corpse = create_corpse(goblin_mob, test_zone) assert corpse.can_accept(sword) is True def test_corpse_not_portable(self, goblin_mob, test_zone): """Corpse cannot be picked up (portable=False).""" corpse = create_corpse(goblin_mob, test_zone) # Try to move corpse to a mock entity (simulating "get corpse") mock_entity = MagicMock() mock_entity._contents = [] # Corpse is not portable, so entity.can_accept would return False # (Entity.can_accept checks obj.portable) from mudlib.entity import Entity dummy_entity = Entity(name="dummy", x=0, y=0, location=test_zone) assert dummy_entity.can_accept(corpse) is False