208 lines
6.1 KiB
Python
208 lines
6.1 KiB
Python
|
|
# pathfinding_3d_test.py - Unit tests for 3D pathfinding
|
||
|
|
# Tests A* pathfinding on VoxelPoint navigation grid
|
||
|
|
|
||
|
|
import mcrfpy
|
||
|
|
import sys
|
||
|
|
|
||
|
|
def test_simple_path():
|
||
|
|
"""Test pathfinding on an open grid"""
|
||
|
|
print("Testing simple pathfinding...")
|
||
|
|
|
||
|
|
viewport = mcrfpy.Viewport3D(pos=(0, 0), size=(320, 240))
|
||
|
|
viewport.grid_size = (10, 10)
|
||
|
|
|
||
|
|
# Find path from corner to corner
|
||
|
|
path = viewport.find_path((0, 0), (9, 9))
|
||
|
|
|
||
|
|
# Should find a path
|
||
|
|
assert len(path) > 0, "Expected a path, got empty list"
|
||
|
|
|
||
|
|
# Path should end at destination (start is not included)
|
||
|
|
assert path[-1] == (9, 9), f"Path should end at (9, 9), got {path[-1]}"
|
||
|
|
|
||
|
|
# Path length should be reasonable (diagonal allows shorter paths)
|
||
|
|
# Manhattan distance is 18, but with diagonals it can be ~9-14 steps
|
||
|
|
assert len(path) >= 9 and len(path) <= 18, f"Path length {len(path)} is unexpected"
|
||
|
|
|
||
|
|
print(f" PASS: Simple pathfinding ({len(path)} steps)")
|
||
|
|
|
||
|
|
|
||
|
|
def test_path_with_obstacles():
|
||
|
|
"""Test pathfinding around obstacles"""
|
||
|
|
print("Testing pathfinding with obstacles...")
|
||
|
|
|
||
|
|
viewport = mcrfpy.Viewport3D(pos=(0, 0), size=(320, 240))
|
||
|
|
viewport.grid_size = (10, 10)
|
||
|
|
|
||
|
|
# Create a wall blocking direct path
|
||
|
|
# Wall from (4, 0) to (4, 8)
|
||
|
|
for z in range(9):
|
||
|
|
viewport.at(4, z).walkable = False
|
||
|
|
|
||
|
|
# Find path from left side to right side
|
||
|
|
path = viewport.find_path((2, 5), (7, 5))
|
||
|
|
|
||
|
|
# Should find a path (going around the wall via z=9)
|
||
|
|
assert len(path) > 0, "Expected a path around the wall"
|
||
|
|
|
||
|
|
# Verify path doesn't go through wall
|
||
|
|
for x, z in path:
|
||
|
|
if x == 4 and z < 9:
|
||
|
|
assert False, f"Path goes through wall at ({x}, {z})"
|
||
|
|
|
||
|
|
print(f" PASS: Pathfinding with obstacles ({len(path)} steps)")
|
||
|
|
|
||
|
|
|
||
|
|
def test_no_path():
|
||
|
|
"""Test pathfinding when no path exists"""
|
||
|
|
print("Testing no path scenario...")
|
||
|
|
|
||
|
|
viewport = mcrfpy.Viewport3D(pos=(0, 0), size=(320, 240))
|
||
|
|
viewport.grid_size = (10, 10)
|
||
|
|
|
||
|
|
# Create a complete wall blocking all paths
|
||
|
|
# Wall from (5, 0) to (5, 9) - blocks entire grid
|
||
|
|
for z in range(10):
|
||
|
|
viewport.at(5, z).walkable = False
|
||
|
|
|
||
|
|
# Try to find path from left to right
|
||
|
|
path = viewport.find_path((2, 5), (7, 5))
|
||
|
|
|
||
|
|
# Should return empty list (no path)
|
||
|
|
assert len(path) == 0, f"Expected empty path, got {len(path)} steps"
|
||
|
|
|
||
|
|
print(" PASS: No path returns empty list")
|
||
|
|
|
||
|
|
|
||
|
|
def test_start_equals_end():
|
||
|
|
"""Test pathfinding when start equals end"""
|
||
|
|
print("Testing start equals end...")
|
||
|
|
|
||
|
|
viewport = mcrfpy.Viewport3D(pos=(0, 0), size=(320, 240))
|
||
|
|
viewport.grid_size = (10, 10)
|
||
|
|
|
||
|
|
# Find path to same location
|
||
|
|
path = viewport.find_path((5, 5), (5, 5))
|
||
|
|
|
||
|
|
# Should return empty path (already there)
|
||
|
|
assert len(path) == 0, f"Expected empty path for start==end, got {len(path)} steps"
|
||
|
|
|
||
|
|
print(" PASS: Start equals end")
|
||
|
|
|
||
|
|
|
||
|
|
def test_adjacent_path():
|
||
|
|
"""Test pathfinding to adjacent cell"""
|
||
|
|
print("Testing adjacent cell pathfinding...")
|
||
|
|
|
||
|
|
viewport = mcrfpy.Viewport3D(pos=(0, 0), size=(320, 240))
|
||
|
|
viewport.grid_size = (10, 10)
|
||
|
|
|
||
|
|
# Find path to adjacent cell
|
||
|
|
path = viewport.find_path((5, 5), (5, 6))
|
||
|
|
|
||
|
|
# Should be a single step
|
||
|
|
assert len(path) == 1, f"Expected 1 step, got {len(path)}"
|
||
|
|
assert path[0] == (5, 6), f"Expected (5, 6), got {path[0]}"
|
||
|
|
|
||
|
|
print(" PASS: Adjacent cell pathfinding")
|
||
|
|
|
||
|
|
|
||
|
|
def test_heightmap_threshold():
|
||
|
|
"""Test apply_threshold sets walkability"""
|
||
|
|
print("Testing HeightMap threshold...")
|
||
|
|
|
||
|
|
viewport = mcrfpy.Viewport3D(pos=(0, 0), size=(320, 240))
|
||
|
|
|
||
|
|
# Create a heightmap
|
||
|
|
hm = mcrfpy.HeightMap((10, 10))
|
||
|
|
|
||
|
|
# Set heights: left half low (0.2), right half high (0.8)
|
||
|
|
for z in range(10):
|
||
|
|
for x in range(5):
|
||
|
|
hm[x, z] = 0.2
|
||
|
|
for x in range(5, 10):
|
||
|
|
hm[x, z] = 0.8
|
||
|
|
|
||
|
|
# Initialize grid
|
||
|
|
viewport.grid_size = (10, 10)
|
||
|
|
|
||
|
|
# Apply threshold: mark high areas (>0.6) as unwalkable
|
||
|
|
viewport.apply_threshold(hm, 0.6, 1.0, walkable=False)
|
||
|
|
|
||
|
|
# Check left side is walkable
|
||
|
|
assert viewport.at(2, 5).walkable == True, "Left side should be walkable"
|
||
|
|
|
||
|
|
# Check right side is unwalkable
|
||
|
|
assert viewport.at(7, 5).walkable == False, "Right side should be unwalkable"
|
||
|
|
|
||
|
|
# Pathfinding should fail to cross
|
||
|
|
path = viewport.find_path((2, 5), (7, 5))
|
||
|
|
assert len(path) == 0, "Path should not exist through unwalkable terrain"
|
||
|
|
|
||
|
|
print(" PASS: HeightMap threshold")
|
||
|
|
|
||
|
|
|
||
|
|
def test_slope_cost():
|
||
|
|
"""Test slope cost calculation"""
|
||
|
|
print("Testing slope cost...")
|
||
|
|
|
||
|
|
viewport = mcrfpy.Viewport3D(pos=(0, 0), size=(320, 240))
|
||
|
|
viewport.grid_size = (10, 10)
|
||
|
|
|
||
|
|
# Create terrain with a steep slope
|
||
|
|
# Set heights manually
|
||
|
|
for z in range(10):
|
||
|
|
for x in range(10):
|
||
|
|
viewport.at(x, z).height = 0.0
|
||
|
|
|
||
|
|
# Create a cliff at x=5
|
||
|
|
for z in range(10):
|
||
|
|
for x in range(5, 10):
|
||
|
|
viewport.at(x, z).height = 2.0 # 2.0 units high
|
||
|
|
|
||
|
|
# Apply slope cost: max slope 0.5, mark steeper as unwalkable
|
||
|
|
viewport.set_slope_cost(max_slope=0.5, cost_multiplier=2.0)
|
||
|
|
|
||
|
|
# Check that cells at the cliff edge are marked unwalkable
|
||
|
|
# Cell at (4, 5) borders (5, 5) which is 2.0 higher
|
||
|
|
assert viewport.at(4, 5).walkable == False, "Cliff edge should be unwalkable"
|
||
|
|
assert viewport.at(5, 5).walkable == False, "Cliff top edge should be unwalkable"
|
||
|
|
|
||
|
|
# Cells away from cliff should still be walkable
|
||
|
|
assert viewport.at(0, 5).walkable == True, "Flat area should be walkable"
|
||
|
|
assert viewport.at(9, 5).walkable == True, "Flat high area should be walkable"
|
||
|
|
|
||
|
|
print(" PASS: Slope cost")
|
||
|
|
|
||
|
|
|
||
|
|
def run_all_tests():
|
||
|
|
"""Run all unit tests"""
|
||
|
|
print("=" * 60)
|
||
|
|
print("3D Pathfinding Unit Tests")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
try:
|
||
|
|
test_simple_path()
|
||
|
|
test_path_with_obstacles()
|
||
|
|
test_no_path()
|
||
|
|
test_start_equals_end()
|
||
|
|
test_adjacent_path()
|
||
|
|
test_heightmap_threshold()
|
||
|
|
test_slope_cost()
|
||
|
|
|
||
|
|
print("=" * 60)
|
||
|
|
print("ALL TESTS PASSED")
|
||
|
|
print("=" * 60)
|
||
|
|
sys.exit(0)
|
||
|
|
except AssertionError as e:
|
||
|
|
print(f"FAIL: {e}")
|
||
|
|
sys.exit(1)
|
||
|
|
except Exception as e:
|
||
|
|
print(f"ERROR: {e}")
|
||
|
|
import traceback
|
||
|
|
traceback.print_exc()
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
|
||
|
|
# Run tests
|
||
|
|
run_all_tests()
|