Grid: add apply_threshold and apply_ranges for HeightMap (closes #199)
Add methods to apply HeightMap data to Grid walkable/transparent properties: - apply_threshold(source, range, walkable, transparent): Apply properties to cells where HeightMap value is in the specified range - apply_ranges(source, ranges): Apply multiple threshold rules in one pass Features: - Size mismatch between HeightMap and Grid raises ValueError - Both methods return self for chaining - Uses dynamic type lookup via module for HeightMap type checking - First matching range wins in apply_ranges - Cells not matching any range remain unchanged - TCOD map is synced after changes for FOV/pathfinding Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8d6d564d6b
commit
bf8557798a
3 changed files with 596 additions and 0 deletions
310
tests/unit/test_grid_apply.py
Normal file
310
tests/unit/test_grid_apply.py
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Unit tests for Grid.apply_threshold and Grid.apply_ranges (#199)
|
||||
|
||||
Tests the Grid methods for applying HeightMap data to walkable/transparent properties.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import mcrfpy
|
||||
|
||||
|
||||
def test_apply_threshold_walkable():
|
||||
"""apply_threshold sets walkable property correctly"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||
|
||||
# All cells start with default walkable
|
||||
grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=True)
|
||||
|
||||
# Check a few cells
|
||||
assert grid.at((5, 5)).walkable == True
|
||||
assert grid.at((0, 0)).walkable == True
|
||||
assert grid.at((9, 9)).walkable == True
|
||||
print("PASS: test_apply_threshold_walkable")
|
||||
|
||||
|
||||
def test_apply_threshold_transparent():
|
||||
"""apply_threshold sets transparent property correctly"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||
|
||||
grid.apply_threshold(hmap, range=(0.0, 1.0), transparent=False)
|
||||
|
||||
assert grid.at((5, 5)).transparent == False
|
||||
print("PASS: test_apply_threshold_transparent")
|
||||
|
||||
|
||||
def test_apply_threshold_both():
|
||||
"""apply_threshold sets both walkable and transparent"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||
|
||||
grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=True, transparent=True)
|
||||
|
||||
point = grid.at((5, 5))
|
||||
assert point.walkable == True
|
||||
assert point.transparent == True
|
||||
print("PASS: test_apply_threshold_both")
|
||||
|
||||
|
||||
def test_apply_threshold_out_of_range():
|
||||
"""apply_threshold doesn't affect cells outside range"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||
|
||||
# Set initial state
|
||||
grid.at((5, 5)).walkable = False
|
||||
grid.at((5, 5)).transparent = False
|
||||
|
||||
# Apply threshold with range that excludes 0.5
|
||||
grid.apply_threshold(hmap, range=(0.0, 0.4), walkable=True, transparent=True)
|
||||
|
||||
# Cell should remain unchanged
|
||||
assert grid.at((5, 5)).walkable == False
|
||||
assert grid.at((5, 5)).transparent == False
|
||||
print("PASS: test_apply_threshold_out_of_range")
|
||||
|
||||
|
||||
def test_apply_threshold_returns_self():
|
||||
"""apply_threshold returns self for chaining"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||
|
||||
result = grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=True)
|
||||
assert result is grid, "apply_threshold should return self"
|
||||
print("PASS: test_apply_threshold_returns_self")
|
||||
|
||||
|
||||
def test_apply_threshold_size_mismatch():
|
||||
"""apply_threshold raises ValueError for size mismatch"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((20, 20), fill=0.5)
|
||||
|
||||
try:
|
||||
grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=True)
|
||||
print("FAIL: test_apply_threshold_size_mismatch - should have raised ValueError")
|
||||
sys.exit(1)
|
||||
except ValueError as e:
|
||||
assert "size" in str(e).lower()
|
||||
|
||||
print("PASS: test_apply_threshold_size_mismatch")
|
||||
|
||||
|
||||
def test_apply_threshold_invalid_source():
|
||||
"""apply_threshold raises TypeError for non-HeightMap source"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
|
||||
try:
|
||||
grid.apply_threshold("not a heightmap", range=(0.0, 1.0), walkable=True)
|
||||
print("FAIL: test_apply_threshold_invalid_source - should have raised TypeError")
|
||||
sys.exit(1)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
print("PASS: test_apply_threshold_invalid_source")
|
||||
|
||||
|
||||
def test_apply_threshold_none_values():
|
||||
"""apply_threshold with None values leaves properties unchanged"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||
|
||||
# Set initial state
|
||||
grid.at((5, 5)).walkable = True
|
||||
grid.at((5, 5)).transparent = False
|
||||
|
||||
# Apply with only walkable=False, transparent should stay unchanged
|
||||
grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=False)
|
||||
|
||||
assert grid.at((5, 5)).walkable == False
|
||||
assert grid.at((5, 5)).transparent == False # Unchanged
|
||||
print("PASS: test_apply_threshold_none_values")
|
||||
|
||||
|
||||
def test_apply_ranges_basic():
|
||||
"""apply_ranges applies multiple ranges correctly"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||
|
||||
# Apply a range that covers 0.5
|
||||
grid.apply_ranges(hmap, [
|
||||
((0.4, 0.6), {"walkable": True, "transparent": True}),
|
||||
])
|
||||
|
||||
assert grid.at((5, 5)).walkable == True
|
||||
assert grid.at((5, 5)).transparent == True
|
||||
print("PASS: test_apply_ranges_basic")
|
||||
|
||||
|
||||
def test_apply_ranges_first_match_wins():
|
||||
"""apply_ranges uses first matching range"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||
|
||||
# Both ranges cover 0.5, first should win
|
||||
grid.apply_ranges(hmap, [
|
||||
((0.0, 0.6), {"walkable": True}),
|
||||
((0.4, 1.0), {"walkable": False}),
|
||||
])
|
||||
|
||||
assert grid.at((5, 5)).walkable == True # First match wins
|
||||
print("PASS: test_apply_ranges_first_match_wins")
|
||||
|
||||
|
||||
def test_apply_ranges_returns_self():
|
||||
"""apply_ranges returns self for chaining"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||
|
||||
result = grid.apply_ranges(hmap, [
|
||||
((0.0, 1.0), {"walkable": True}),
|
||||
])
|
||||
assert result is grid, "apply_ranges should return self"
|
||||
print("PASS: test_apply_ranges_returns_self")
|
||||
|
||||
|
||||
def test_apply_ranges_size_mismatch():
|
||||
"""apply_ranges raises ValueError for size mismatch"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((5, 5), fill=0.5)
|
||||
|
||||
try:
|
||||
grid.apply_ranges(hmap, [
|
||||
((0.0, 1.0), {"walkable": True}),
|
||||
])
|
||||
print("FAIL: test_apply_ranges_size_mismatch - should have raised ValueError")
|
||||
sys.exit(1)
|
||||
except ValueError as e:
|
||||
assert "size" in str(e).lower()
|
||||
|
||||
print("PASS: test_apply_ranges_size_mismatch")
|
||||
|
||||
|
||||
def test_apply_ranges_empty_list():
|
||||
"""apply_ranges with empty list doesn't change anything"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||
|
||||
grid.at((5, 5)).walkable = True
|
||||
grid.at((5, 5)).transparent = False
|
||||
|
||||
grid.apply_ranges(hmap, [])
|
||||
|
||||
# Should remain unchanged
|
||||
assert grid.at((5, 5)).walkable == True
|
||||
assert grid.at((5, 5)).transparent == False
|
||||
print("PASS: test_apply_ranges_empty_list")
|
||||
|
||||
|
||||
def test_apply_ranges_no_match():
|
||||
"""apply_ranges leaves cells unchanged when no range matches"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||
|
||||
grid.at((5, 5)).walkable = True
|
||||
grid.at((5, 5)).transparent = True
|
||||
|
||||
# Ranges that don't include 0.5
|
||||
grid.apply_ranges(hmap, [
|
||||
((0.0, 0.4), {"walkable": False}),
|
||||
((0.6, 1.0), {"transparent": False}),
|
||||
])
|
||||
|
||||
# Should remain unchanged
|
||||
assert grid.at((5, 5)).walkable == True
|
||||
assert grid.at((5, 5)).transparent == True
|
||||
print("PASS: test_apply_ranges_no_match")
|
||||
|
||||
|
||||
def test_apply_ranges_invalid_format():
|
||||
"""apply_ranges raises TypeError for invalid format"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||
|
||||
# Invalid: not a list
|
||||
try:
|
||||
grid.apply_ranges(hmap, "not a list")
|
||||
print("FAIL: should have raised TypeError for non-list")
|
||||
sys.exit(1)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# Invalid: entry not a tuple
|
||||
try:
|
||||
grid.apply_ranges(hmap, ["not a tuple"])
|
||||
print("FAIL: should have raised TypeError for non-tuple entry")
|
||||
sys.exit(1)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# Invalid: range not a tuple
|
||||
try:
|
||||
grid.apply_ranges(hmap, [
|
||||
([0.0, 1.0], {"walkable": True}), # list instead of tuple for range
|
||||
])
|
||||
print("FAIL: should have raised TypeError for non-tuple range")
|
||||
sys.exit(1)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# Invalid: props not a dict
|
||||
try:
|
||||
grid.apply_ranges(hmap, [
|
||||
((0.0, 1.0), "not a dict"),
|
||||
])
|
||||
print("FAIL: should have raised TypeError for non-dict props")
|
||||
sys.exit(1)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
print("PASS: test_apply_ranges_invalid_format")
|
||||
|
||||
|
||||
def test_chaining():
|
||||
"""Methods can be chained together"""
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||
hmap = mcrfpy.HeightMap((10, 10))
|
||||
|
||||
# Chain multiple operations
|
||||
hmap.fill(0.5)
|
||||
|
||||
result = (grid
|
||||
.apply_threshold(hmap, range=(0.0, 0.4), walkable=False)
|
||||
.apply_threshold(hmap, range=(0.6, 1.0), transparent=False)
|
||||
.apply_ranges(hmap, [
|
||||
((0.4, 0.6), {"walkable": True, "transparent": True}),
|
||||
]))
|
||||
|
||||
assert result is grid
|
||||
print("PASS: test_chaining")
|
||||
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all tests"""
|
||||
print("Running Grid apply method tests...")
|
||||
print()
|
||||
|
||||
test_apply_threshold_walkable()
|
||||
test_apply_threshold_transparent()
|
||||
test_apply_threshold_both()
|
||||
test_apply_threshold_out_of_range()
|
||||
test_apply_threshold_returns_self()
|
||||
test_apply_threshold_size_mismatch()
|
||||
test_apply_threshold_invalid_source()
|
||||
test_apply_threshold_none_values()
|
||||
test_apply_ranges_basic()
|
||||
test_apply_ranges_first_match_wins()
|
||||
test_apply_ranges_returns_self()
|
||||
test_apply_ranges_size_mismatch()
|
||||
test_apply_ranges_empty_list()
|
||||
test_apply_ranges_no_match()
|
||||
test_apply_ranges_invalid_format()
|
||||
test_chaining()
|
||||
|
||||
print()
|
||||
print("All Grid apply method tests PASSED!")
|
||||
|
||||
|
||||
# Run tests directly
|
||||
run_all_tests()
|
||||
sys.exit(0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue