Add contents_near() spatial query to Zone

This commit is contained in:
Jared Miller 2026-02-11 19:17:17 -05:00
parent b4fca95830
commit 6f58ae0501
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 113 additions and 0 deletions

View file

@ -67,3 +67,37 @@ class Zone(Object):
def contents_at(self, x: int, y: int) -> list[Object]: def contents_at(self, x: int, y: int) -> list[Object]:
"""Return all contents at the given coordinates.""" """Return all contents at the given coordinates."""
return [obj for obj in self._contents if obj.x == x and obj.y == y] 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

View file

@ -200,3 +200,82 @@ def test_contents_at_multiple():
assert rock in result assert rock in result
assert gem in result assert gem in result
assert len(result) == 2 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