diff --git a/src/mudlib/entity.py b/src/mudlib/entity.py index d29ae72..d00e110 100644 --- a/src/mudlib/entity.py +++ b/src/mudlib/entity.py @@ -76,3 +76,7 @@ class Mob(Entity): alive: bool = True moves: list[str] = field(default_factory=list) next_action_at: float = 0.0 + home_x_min: int | None = None + home_x_max: int | None = None + home_y_min: int | None = None + home_y_max: int | None = None diff --git a/src/mudlib/mobs.py b/src/mudlib/mobs.py index 2aaa57d..b1b6ad3 100644 --- a/src/mudlib/mobs.py +++ b/src/mudlib/mobs.py @@ -64,7 +64,9 @@ def load_mob_templates(directory: Path) -> dict[str, MobTemplate]: return templates -def spawn_mob(template: MobTemplate, x: int, y: int, zone: Zone) -> Mob: +def spawn_mob( + template: MobTemplate, x: int, y: int, zone: Zone, home_region: dict | None = None +) -> Mob: """Create a Mob instance from a template at the given position. Args: @@ -72,6 +74,7 @@ def spawn_mob(template: MobTemplate, x: int, y: int, zone: Zone) -> Mob: x: X coordinate in the zone y: Y coordinate in the zone zone: The zone where the mob will be spawned + home_region: Optional home region dict with x and y bounds Returns: The spawned Mob instance @@ -87,6 +90,11 @@ def spawn_mob(template: MobTemplate, x: int, y: int, zone: Zone) -> Mob: description=template.description, moves=list(template.moves), ) + if home_region is not None: + mob.home_x_min = home_region["x"][0] + mob.home_x_max = home_region["x"][1] + mob.home_y_min = home_region["y"][0] + mob.home_y_max = home_region["y"][1] mobs.append(mob) return mob diff --git a/src/mudlib/zone.py b/src/mudlib/zone.py index 643e118..ed3b1be 100644 --- a/src/mudlib/zone.py +++ b/src/mudlib/zone.py @@ -19,6 +19,7 @@ class SpawnRule: mob: str max_count: int = 1 respawn_seconds: int = 300 + home_region: dict | None = None @dataclass(eq=False) diff --git a/src/mudlib/zones.py b/src/mudlib/zones.py index 91e983f..f711cd9 100644 --- a/src/mudlib/zones.py +++ b/src/mudlib/zones.py @@ -81,6 +81,7 @@ def load_zone(path: Path) -> Zone: mob=spawn["mob"], max_count=spawn.get("max_count", 1), respawn_seconds=spawn.get("respawn_seconds", 300), + home_region=spawn.get("home_region"), ) for spawn in spawns_data ] diff --git a/tests/test_mob_home_region.py b/tests/test_mob_home_region.py new file mode 100644 index 0000000..ed1dcc1 --- /dev/null +++ b/tests/test_mob_home_region.py @@ -0,0 +1,219 @@ +"""Tests for mob home region system.""" + +import pathlib +import tempfile + +import pytest + +from mudlib.entity import Mob +from mudlib.mobs import MobTemplate, mobs, spawn_mob +from mudlib.zone import SpawnRule, Zone +from mudlib.zones import load_zone + + +@pytest.fixture(autouse=True) +def clear_mobs(): + mobs.clear() + yield + mobs.clear() + + +@pytest.fixture +def zone(): + terrain = [["." for _ in range(20)] for _ in range(20)] + return Zone( + name="forest", + width=20, + height=20, + terrain=terrain, + toroidal=False, + ) + + +@pytest.fixture +def template(): + return MobTemplate( + name="squirrel", + description="a bushy-tailed squirrel", + pl=10, + stamina=20, + max_stamina=20, + moves=[], + ) + + +# --- SpawnRule --- + + +def test_spawn_rule_default_no_home_region(): + """SpawnRule has no home_region by default.""" + rule = SpawnRule(mob="goblin") + assert rule.home_region is None + + +def test_spawn_rule_with_home_region(): + """SpawnRule can have a home_region.""" + rule = SpawnRule( + mob="goblin", + home_region={"x": [5, 15], "y": [3, 10]}, + ) + assert rule.home_region == {"x": [5, 15], "y": [3, 10]} + + +# --- Mob fields --- + + +def test_mob_home_region_defaults(): + """Mob has no home region by default.""" + mob = Mob(name="rat", x=0, y=0) + assert mob.home_x_min is None + assert mob.home_x_max is None + assert mob.home_y_min is None + assert mob.home_y_max is None + + +def test_mob_home_region_set(): + """Mob can have home region bounds.""" + mob = Mob( + name="rat", + x=5, + y=5, + home_x_min=3, + home_x_max=10, + home_y_min=2, + home_y_max=8, + ) + assert mob.home_x_min == 3 + assert mob.home_x_max == 10 + assert mob.home_y_min == 2 + assert mob.home_y_max == 8 + + +def test_mob_in_home_region(): + """Mob at position within home region.""" + mob = Mob( + name="rat", + x=5, + y=5, + home_x_min=3, + home_x_max=10, + home_y_min=2, + home_y_max=8, + ) + assert mob.home_x_min is not None + assert mob.home_x_max is not None + assert mob.home_y_min is not None + assert mob.home_y_max is not None + assert mob.home_x_min <= mob.x <= mob.home_x_max + assert mob.home_y_min <= mob.y <= mob.home_y_max + + +# --- spawn_mob with home region --- + + +def test_spawn_mob_with_home_region(zone, template): + """spawn_mob sets home region from SpawnRule.""" + rule = SpawnRule( + mob="squirrel", + home_region={"x": [5, 15], "y": [3, 10]}, + ) + mob = spawn_mob(template, 10, 7, zone, home_region=rule.home_region) + assert mob.home_x_min == 5 + assert mob.home_x_max == 15 + assert mob.home_y_min == 3 + assert mob.home_y_max == 10 + + +def test_spawn_mob_without_home_region(zone, template): + """spawn_mob without home_region leaves bounds as None.""" + mob = spawn_mob(template, 10, 7, zone) + assert mob.home_x_min is None + assert mob.home_x_max is None + + +# --- TOML loading --- + + +def test_load_zone_spawn_with_home_region(): + """Zone TOML with home_region on spawn rule.""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: + f.write( + """ +name = "test_zone" +width = 20 +height = 20 + +[terrain] +rows = [ + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", +] + +[[spawns]] +mob = "squirrel" +max_count = 2 +respawn_seconds = 180 +home_region = { x = [5, 15], y = [3, 10] } +""" + ) + temp_path = pathlib.Path(f.name) + + try: + zone = load_zone(temp_path) + assert len(zone.spawn_rules) == 1 + rule = zone.spawn_rules[0] + assert rule.home_region == {"x": [5, 15], "y": [3, 10]} + finally: + temp_path.unlink() + + +def test_load_zone_spawn_without_home_region(): + """Zone TOML without home_region on spawn rule.""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: + f.write( + """ +name = "test_zone" +width = 5 +height = 5 + +[terrain] +rows = [ + ".....", + ".....", + ".....", + ".....", + ".....", +] + +[[spawns]] +mob = "goblin" +max_count = 1 +""" + ) + temp_path = pathlib.Path(f.name) + + try: + zone = load_zone(temp_path) + assert len(zone.spawn_rules) == 1 + rule = zone.spawn_rules[0] + assert rule.home_region is None + finally: + temp_path.unlink()