Add contents_near() spatial query to Zone
This commit is contained in:
parent
b4fca95830
commit
6f58ae0501
2 changed files with 113 additions and 0 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue