2026-01-11 20:42:33 -05:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Unit tests for mcrfpy.HeightMap query methods (#196)
|
|
|
|
|
|
|
|
|
|
Tests the HeightMap query methods: get, get_interpolated, get_slope, get_normal, min_max, count_in_range
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
import math
|
|
|
|
|
import mcrfpy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_basic():
|
|
|
|
|
"""get() returns correct value at position"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
|
|
|
value = hmap.get((5, 5))
|
|
|
|
|
assert abs(value - 0.5) < 0.001, f"Expected 0.5, got {value}"
|
|
|
|
|
print("PASS: test_get_basic")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_corners():
|
|
|
|
|
"""get() works at all corners"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.25)
|
|
|
|
|
|
|
|
|
|
# All corners should have the fill value
|
|
|
|
|
assert abs(hmap.get((0, 0)) - 0.25) < 0.001
|
|
|
|
|
assert abs(hmap.get((9, 0)) - 0.25) < 0.001
|
|
|
|
|
assert abs(hmap.get((0, 9)) - 0.25) < 0.001
|
|
|
|
|
assert abs(hmap.get((9, 9)) - 0.25) < 0.001
|
|
|
|
|
print("PASS: test_get_corners")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_out_of_bounds():
|
|
|
|
|
"""get() raises IndexError for out-of-bounds position"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10))
|
|
|
|
|
|
|
|
|
|
# Test various out-of-bounds positions
|
|
|
|
|
for pos in [(-1, 0), (0, -1), (10, 0), (0, 10), (10, 10)]:
|
|
|
|
|
try:
|
|
|
|
|
hmap.get(pos)
|
|
|
|
|
print(f"FAIL: test_get_out_of_bounds - should have raised IndexError for {pos}")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
except IndexError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
print("PASS: test_get_out_of_bounds")
|
|
|
|
|
|
|
|
|
|
|
2026-01-11 21:43:44 -05:00
|
|
|
def test_get_flexible_input():
|
|
|
|
|
"""get() accepts tuple, list, Vector, and two args"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
2026-01-11 20:42:33 -05:00
|
|
|
|
2026-01-11 21:43:44 -05:00
|
|
|
# Tuple works
|
|
|
|
|
assert abs(hmap.get((5, 5)) - 0.5) < 0.001
|
2026-01-11 20:42:33 -05:00
|
|
|
|
2026-01-11 21:43:44 -05:00
|
|
|
# List works
|
|
|
|
|
assert abs(hmap.get([5, 5]) - 0.5) < 0.001
|
|
|
|
|
|
|
|
|
|
# Two args work (no tuple needed)
|
|
|
|
|
assert abs(hmap.get(5, 5) - 0.5) < 0.001
|
|
|
|
|
|
|
|
|
|
# Vector works
|
|
|
|
|
vec = mcrfpy.Vector(5, 5)
|
|
|
|
|
assert abs(hmap.get(vec) - 0.5) < 0.001
|
|
|
|
|
|
|
|
|
|
print("PASS: test_get_flexible_input")
|
2026-01-11 20:42:33 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_interpolated_basic():
|
|
|
|
|
"""get_interpolated() returns value at float position"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
|
|
|
value = hmap.get_interpolated((5.5, 5.5))
|
|
|
|
|
# With uniform fill, interpolation should return same value
|
|
|
|
|
assert abs(value - 0.5) < 0.001, f"Expected ~0.5, got {value}"
|
|
|
|
|
print("PASS: test_get_interpolated_basic")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_interpolated_at_integers():
|
|
|
|
|
"""get_interpolated() matches get() at integer positions"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.75)
|
|
|
|
|
|
|
|
|
|
int_value = hmap.get((3, 4))
|
|
|
|
|
interp_value = hmap.get_interpolated((3.0, 4.0))
|
|
|
|
|
|
|
|
|
|
assert abs(int_value - interp_value) < 0.001, f"Values differ: {int_value} vs {interp_value}"
|
|
|
|
|
print("PASS: test_get_interpolated_at_integers")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_slope_flat():
|
|
|
|
|
"""get_slope() returns 0 for flat terrain"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
|
|
|
slope = hmap.get_slope((5, 5))
|
|
|
|
|
# Flat terrain should have slope near 0
|
|
|
|
|
assert abs(slope) < 0.01, f"Expected ~0 for flat terrain, got {slope}"
|
|
|
|
|
print("PASS: test_get_slope_flat")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_slope_out_of_bounds():
|
|
|
|
|
"""get_slope() raises IndexError for out-of-bounds position"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10))
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
hmap.get_slope((10, 5))
|
|
|
|
|
print("FAIL: test_get_slope_out_of_bounds - should have raised IndexError")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
except IndexError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
print("PASS: test_get_slope_out_of_bounds")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_normal_flat():
|
|
|
|
|
"""get_normal() returns up vector for flat terrain"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
|
|
|
nx, ny, nz = hmap.get_normal((5.0, 5.0))
|
|
|
|
|
|
|
|
|
|
# Flat terrain should have normal pointing up (0, 0, 1)
|
|
|
|
|
assert abs(nx) < 0.01, f"Expected nx~0, got {nx}"
|
|
|
|
|
assert abs(ny) < 0.01, f"Expected ny~0, got {ny}"
|
|
|
|
|
assert abs(nz - 1.0) < 0.01, f"Expected nz~1, got {nz}"
|
|
|
|
|
print("PASS: test_get_normal_flat")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_normal_with_water_level():
|
|
|
|
|
"""get_normal() accepts water_level parameter"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
|
|
|
nx, ny, nz = hmap.get_normal((5.0, 5.0), water_level=0.3)
|
|
|
|
|
|
|
|
|
|
# Should still return valid normal
|
|
|
|
|
assert isinstance(nx, float)
|
|
|
|
|
assert isinstance(ny, float)
|
|
|
|
|
assert isinstance(nz, float)
|
|
|
|
|
print("PASS: test_get_normal_with_water_level")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_min_max_uniform():
|
|
|
|
|
"""min_max() returns correct values for uniform heightmap"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
|
|
|
min_val, max_val = hmap.min_max()
|
|
|
|
|
|
|
|
|
|
assert abs(min_val - 0.5) < 0.001, f"Expected min=0.5, got {min_val}"
|
|
|
|
|
assert abs(max_val - 0.5) < 0.001, f"Expected max=0.5, got {max_val}"
|
|
|
|
|
print("PASS: test_min_max_uniform")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_min_max_after_operations():
|
|
|
|
|
"""min_max() updates after operations"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10))
|
|
|
|
|
hmap.fill(0.0).add_constant(0.5).scale(2.0)
|
|
|
|
|
|
|
|
|
|
min_val, max_val = hmap.min_max()
|
|
|
|
|
expected = 1.0 # 0.0 + 0.5 * 2.0
|
|
|
|
|
|
|
|
|
|
assert abs(min_val - expected) < 0.001, f"Expected min={expected}, got {min_val}"
|
|
|
|
|
assert abs(max_val - expected) < 0.001, f"Expected max={expected}, got {max_val}"
|
|
|
|
|
print("PASS: test_min_max_after_operations")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_count_in_range_all():
|
|
|
|
|
"""count_in_range() returns all cells for uniform map in range"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
|
|
|
count = hmap.count_in_range((0.0, 1.0))
|
|
|
|
|
|
|
|
|
|
assert count == 100, f"Expected 100 cells, got {count}"
|
|
|
|
|
print("PASS: test_count_in_range_all")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_count_in_range_none():
|
|
|
|
|
"""count_in_range() returns 0 when no cells in range"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
|
|
|
count = hmap.count_in_range((0.0, 0.4))
|
|
|
|
|
|
|
|
|
|
assert count == 0, f"Expected 0 cells, got {count}"
|
|
|
|
|
print("PASS: test_count_in_range_none")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_count_in_range_exact():
|
|
|
|
|
"""count_in_range() with exact bounds"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
|
|
|
count = hmap.count_in_range((0.5, 0.5))
|
|
|
|
|
|
|
|
|
|
# Should count all cells since fill value is exactly 0.5
|
|
|
|
|
assert count == 100, f"Expected 100 cells at exact value, got {count}"
|
|
|
|
|
print("PASS: test_count_in_range_exact")
|
|
|
|
|
|
|
|
|
|
|
2026-01-11 21:43:44 -05:00
|
|
|
def test_count_in_range_accepts_list():
|
|
|
|
|
"""count_in_range() accepts list or tuple"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
|
|
|
|
|
|
|
|
# Tuple works
|
|
|
|
|
count1 = hmap.count_in_range((0.0, 1.0))
|
|
|
|
|
assert count1 == 100
|
|
|
|
|
|
|
|
|
|
# List also works
|
|
|
|
|
count2 = hmap.count_in_range([0.0, 1.0])
|
|
|
|
|
assert count2 == 100
|
|
|
|
|
|
|
|
|
|
print("PASS: test_count_in_range_accepts_list")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_count_in_range_invalid_range():
|
|
|
|
|
"""count_in_range() raises ValueError when min > max"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
hmap.count_in_range((1.0, 0.0)) # min > max
|
|
|
|
|
print("FAIL: test_count_in_range_invalid_range - should have raised ValueError")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
assert "min" in str(e).lower()
|
|
|
|
|
|
|
|
|
|
print("PASS: test_count_in_range_invalid_range")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_subscript_basic():
|
|
|
|
|
"""hmap[x, y] works as shorthand for get()"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.75)
|
|
|
|
|
|
|
|
|
|
# Subscript with tuple
|
|
|
|
|
value = hmap[5, 5]
|
|
|
|
|
assert abs(value - 0.75) < 0.001
|
|
|
|
|
|
|
|
|
|
print("PASS: test_subscript_basic")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_subscript_flexible():
|
|
|
|
|
"""hmap[] accepts tuple, list, Vector"""
|
|
|
|
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.25)
|
|
|
|
|
|
|
|
|
|
# Tuple
|
|
|
|
|
assert abs(hmap[(3, 4)] - 0.25) < 0.001
|
|
|
|
|
|
|
|
|
|
# List
|
|
|
|
|
assert abs(hmap[[3, 4]] - 0.25) < 0.001
|
|
|
|
|
|
|
|
|
|
# Vector
|
|
|
|
|
vec = mcrfpy.Vector(3, 4)
|
|
|
|
|
assert abs(hmap[vec] - 0.25) < 0.001
|
|
|
|
|
|
|
|
|
|
print("PASS: test_subscript_flexible")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_subscript_out_of_bounds():
|
|
|
|
|
"""hmap[] raises IndexError for out-of-bounds"""
|
2026-01-11 20:42:33 -05:00
|
|
|
hmap = mcrfpy.HeightMap((10, 10))
|
|
|
|
|
|
|
|
|
|
try:
|
2026-01-11 21:43:44 -05:00
|
|
|
_ = hmap[10, 5]
|
|
|
|
|
print("FAIL: test_subscript_out_of_bounds - should have raised IndexError")
|
2026-01-11 20:42:33 -05:00
|
|
|
sys.exit(1)
|
2026-01-11 21:43:44 -05:00
|
|
|
except IndexError:
|
2026-01-11 20:42:33 -05:00
|
|
|
pass
|
|
|
|
|
|
2026-01-11 21:43:44 -05:00
|
|
|
print("PASS: test_subscript_out_of_bounds")
|
2026-01-11 20:42:33 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_all_tests():
|
|
|
|
|
"""Run all tests"""
|
|
|
|
|
print("Running HeightMap query method tests...")
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
test_get_basic()
|
|
|
|
|
test_get_corners()
|
|
|
|
|
test_get_out_of_bounds()
|
2026-01-11 21:43:44 -05:00
|
|
|
test_get_flexible_input()
|
2026-01-11 20:42:33 -05:00
|
|
|
test_get_interpolated_basic()
|
|
|
|
|
test_get_interpolated_at_integers()
|
|
|
|
|
test_get_slope_flat()
|
|
|
|
|
test_get_slope_out_of_bounds()
|
|
|
|
|
test_get_normal_flat()
|
|
|
|
|
test_get_normal_with_water_level()
|
|
|
|
|
test_min_max_uniform()
|
|
|
|
|
test_min_max_after_operations()
|
|
|
|
|
test_count_in_range_all()
|
|
|
|
|
test_count_in_range_none()
|
|
|
|
|
test_count_in_range_exact()
|
2026-01-11 21:43:44 -05:00
|
|
|
test_count_in_range_accepts_list()
|
|
|
|
|
test_count_in_range_invalid_range()
|
|
|
|
|
test_subscript_basic()
|
|
|
|
|
test_subscript_flexible()
|
|
|
|
|
test_subscript_out_of_bounds()
|
2026-01-11 20:42:33 -05:00
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
print("All HeightMap query method tests PASSED!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Run tests directly
|
|
|
|
|
run_all_tests()
|
|
|
|
|
sys.exit(0)
|