mud/tests/test_mobs.py
Jared Miller 0d21e312e4
Add mob templates, registry, and spawn/despawn/query
Phase 1 of fightable mobs: MobTemplate dataclass loaded from TOML,
global mobs list, spawn_mob/despawn_mob/get_nearby_mob with
wrapping-aware distance. Mob entity gets moves and next_action_at fields.
2026-02-08 22:51:56 -05:00

161 lines
5 KiB
Python

"""Tests for mob templates, registry, and spawn/despawn."""
import tomllib
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from mudlib.entity import Mob
from mudlib.mobs import (
MobTemplate,
despawn_mob,
get_nearby_mob,
load_mob_template,
load_mob_templates,
mobs,
spawn_mob,
)
@pytest.fixture(autouse=True)
def clear_mobs():
"""Clear mobs list before and after each test."""
mobs.clear()
yield
mobs.clear()
@pytest.fixture
def goblin_toml(tmp_path):
"""Create a goblin TOML file."""
path = tmp_path / "goblin.toml"
path.write_text(
'name = "goblin"\n'
'description = "a snarling goblin with a crude club"\n'
"pl = 50.0\n"
"stamina = 40.0\n"
"max_stamina = 40.0\n"
'moves = ["punch left", "punch right", "sweep"]\n'
)
return path
@pytest.fixture
def dummy_toml(tmp_path):
"""Create a training dummy TOML file."""
path = tmp_path / "training_dummy.toml"
path.write_text(
'name = "training dummy"\n'
'description = "a battered wooden training dummy"\n'
"pl = 200.0\n"
"stamina = 100.0\n"
"max_stamina = 100.0\n"
"moves = []\n"
)
return path
class TestLoadTemplate:
def test_load_single_template(self, goblin_toml):
template = load_mob_template(goblin_toml)
assert template.name == "goblin"
assert template.description == "a snarling goblin with a crude club"
assert template.pl == 50.0
assert template.stamina == 40.0
assert template.max_stamina == 40.0
assert template.moves == ["punch left", "punch right", "sweep"]
def test_load_template_no_moves(self, dummy_toml):
template = load_mob_template(dummy_toml)
assert template.name == "training dummy"
assert template.moves == []
def test_load_all_templates(self, goblin_toml, dummy_toml):
templates = load_mob_templates(goblin_toml.parent)
assert "goblin" in templates
assert "training dummy" in templates
assert len(templates) == 2
class TestSpawnDespawn:
def test_spawn_creates_mob(self, goblin_toml):
template = load_mob_template(goblin_toml)
mob = spawn_mob(template, 10, 20)
assert isinstance(mob, Mob)
assert mob.name == "goblin"
assert mob.x == 10
assert mob.y == 20
assert mob.pl == 50.0
assert mob.stamina == 40.0
assert mob.max_stamina == 40.0
assert mob.moves == ["punch left", "punch right", "sweep"]
assert mob.alive is True
assert mob in mobs
def test_spawn_adds_to_registry(self, goblin_toml):
template = load_mob_template(goblin_toml)
spawn_mob(template, 0, 0)
spawn_mob(template, 5, 5)
assert len(mobs) == 2
def test_despawn_removes_from_list(self, goblin_toml):
template = load_mob_template(goblin_toml)
mob = spawn_mob(template, 0, 0)
despawn_mob(mob)
assert mob not in mobs
assert mob.alive is False
def test_despawn_sets_alive_false(self, goblin_toml):
template = load_mob_template(goblin_toml)
mob = spawn_mob(template, 0, 0)
despawn_mob(mob)
assert mob.alive is False
class TestGetNearbyMob:
@pytest.fixture
def mock_world(self):
w = MagicMock()
w.width = 256
w.height = 256
return w
def test_finds_by_name_within_range(self, goblin_toml, mock_world):
template = load_mob_template(goblin_toml)
mob = spawn_mob(template, 5, 5)
found = get_nearby_mob("goblin", 3, 3, mock_world)
assert found is mob
def test_returns_none_when_out_of_range(self, goblin_toml, mock_world):
template = load_mob_template(goblin_toml)
spawn_mob(template, 100, 100)
found = get_nearby_mob("goblin", 0, 0, mock_world)
assert found is None
def test_returns_none_for_wrong_name(self, goblin_toml, mock_world):
template = load_mob_template(goblin_toml)
spawn_mob(template, 5, 5)
found = get_nearby_mob("dragon", 3, 3, mock_world)
assert found is None
def test_picks_closest_when_multiple(self, goblin_toml, mock_world):
template = load_mob_template(goblin_toml)
far_mob = spawn_mob(template, 8, 8)
close_mob = spawn_mob(template, 1, 1)
found = get_nearby_mob("goblin", 0, 0, mock_world)
assert found is close_mob
def test_skips_dead_mobs(self, goblin_toml, mock_world):
template = load_mob_template(goblin_toml)
mob = spawn_mob(template, 5, 5)
mob.alive = False
found = get_nearby_mob("goblin", 3, 3, mock_world)
assert found is None
def test_wrapping_distance(self, goblin_toml, mock_world):
"""Mob near world edge is close to player at opposite edge."""
template = load_mob_template(goblin_toml)
mob = spawn_mob(template, 254, 254)
found = get_nearby_mob("goblin", 2, 2, mock_world, range_=10)
assert found is mob