McRogueFace/tests/unit/test_grid_apply.py
John McCardle bf8557798a 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>
2026-01-11 20:42:47 -05:00

310 lines
9.3 KiB
Python

#!/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)