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]:
|
||||
"""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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue