diff --git a/src/mudlib/zone.py b/src/mudlib/zone.py index f43b94a..0b49bd8 100644 --- a/src/mudlib/zone.py +++ b/src/mudlib/zone.py @@ -67,3 +67,37 @@ class Zone(Object): def contents_at(self, x: int, y: int) -> list[Object]: """Return all contents at the given coordinates.""" return [obj for obj in self._contents if obj.x == x and obj.y == y] + + def contents_near(self, x: int, y: int, range_: int) -> list[Object]: + """Return all contents within range_ tiles of (x, y). + + Uses wrapping-aware Manhattan distance for toroidal zones. + Includes objects at exactly (x, y). + + Args: + x: X coordinate of center point + y: Y coordinate of center point + range_: Maximum Manhattan distance (inclusive) + + Returns: + List of all objects within range + """ + nearby = [] + for obj in self._contents: + # Skip objects without coordinates + if obj.x is None or obj.y is None: + continue + + dx_dist = abs(obj.x - x) + dy_dist = abs(obj.y - y) + + if self.toroidal: + # Use wrapping-aware distance + dx_dist = min(dx_dist, self.width - dx_dist) + dy_dist = min(dy_dist, self.height - dy_dist) + + manhattan_dist = dx_dist + dy_dist + if manhattan_dist <= range_: + nearby.append(obj) + + return nearby diff --git a/tests/test_zone.py b/tests/test_zone.py index ceef375..e3413d1 100644 --- a/tests/test_zone.py +++ b/tests/test_zone.py @@ -200,3 +200,82 @@ def test_contents_at_multiple(): assert rock in result assert gem in result assert len(result) == 2 + + +# --- contents_near --- + + +def test_contents_near_empty(): + """contents_near returns empty list when nothing nearby.""" + terrain = [["." for _ in range(10)] for _ in range(10)] + zone = Zone(name="test", width=10, height=10, terrain=terrain) + assert zone.contents_near(5, 5, 3) == [] + + +def test_contents_near_includes_exact_position(): + """contents_near includes objects at the exact coordinates.""" + terrain = [["." for _ in range(10)] for _ in range(10)] + zone = Zone(name="test", width=10, height=10, terrain=terrain) + obj = Object(name="rock", location=zone, x=5, y=5) + result = zone.contents_near(5, 5, 3) + assert obj in result + + +def test_contents_near_within_range(): + """contents_near returns objects within Manhattan distance range.""" + terrain = [["." for _ in range(20)] for _ in range(20)] + zone = Zone(name="test", width=20, height=20, terrain=terrain) + center = Object(name="center", location=zone, x=10, y=10) + nearby1 = Object(name="nearby1", location=zone, x=11, y=10) # distance 1 + nearby2 = Object(name="nearby2", location=zone, x=10, y=12) # distance 2 + nearby3 = Object(name="nearby3", location=zone, x=12, y=12) # distance 4 + far = Object(name="far", location=zone, x=15, y=15) # distance 10 + + result = zone.contents_near(10, 10, 3) + assert center in result + assert nearby1 in result + assert nearby2 in result + assert nearby3 not in result # distance 4 > range 3 + assert far not in result + + +def test_contents_near_wrapping_toroidal(): + """contents_near uses wrapping-aware distance on toroidal zones.""" + terrain = [["." for _ in range(10)] for _ in range(10)] + zone = Zone(name="test", width=10, height=10, terrain=terrain, toroidal=True) + # Object at (1, 1), query at (9, 9) + # Without wrapping: dx=8, dy=8 + # With wrapping: dx=min(8, 10-8)=2, dy=min(8, 10-8)=2, total=4 + obj = Object(name="wrapped", location=zone, x=1, y=1) + result = zone.contents_near(9, 9, 5) + assert obj in result # within range due to wrapping + + result_tight = zone.contents_near(9, 9, 3) + assert obj not in result_tight # outside tighter range + + +def test_contents_near_no_wrapping_bounded(): + """contents_near uses straight distance on bounded zones.""" + terrain = [["." for _ in range(10)] for _ in range(10)] + zone = Zone(name="test", width=10, height=10, terrain=terrain, toroidal=False) + # Object at (1, 1), query at (9, 9) + # Straight distance: dx=8, dy=8, total=16 + obj = Object(name="far", location=zone, x=1, y=1) + result = zone.contents_near(9, 9, 5) + assert obj not in result # too far on bounded zone + + +def test_contents_near_multiple(): + """contents_near returns all objects within range.""" + terrain = [["." for _ in range(20)] for _ in range(20)] + zone = Zone(name="test", width=20, height=20, terrain=terrain) + obj1 = Object(name="obj1", location=zone, x=10, y=10) + obj2 = Object(name="obj2", location=zone, x=11, y=10) + obj3 = Object(name="obj3", location=zone, x=10, y=11) + Object(name="far", location=zone, x=20, y=20) # far away + + result = zone.contents_near(10, 10, 2) + assert len(result) == 3 + assert obj1 in result + assert obj2 in result + assert obj3 in result