DiscreteMap class - mask for operations or uint8 tile data

This commit is contained in:
John McCardle 2026-02-03 20:36:42 -05:00
commit d8fec5fea0
7 changed files with 2817 additions and 0 deletions

View file

@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""Unit tests for DiscreteMap arithmetic and bitwise operations."""
import mcrfpy
import sys
def test_add_scalar():
"""Test adding a scalar value."""
dmap = mcrfpy.DiscreteMap((10, 10), fill=50)
dmap.add(25)
assert dmap[5, 5] == 75, f"Expected 75, got {dmap[5, 5]}"
# Test saturation at 255
dmap.fill(250)
dmap.add(20) # 250 + 20 = 270 -> saturates to 255
assert dmap[0, 0] == 255, f"Expected 255 (saturated), got {dmap[0, 0]}"
print(" [PASS] Add scalar")
def test_add_map():
"""Test adding another DiscreteMap."""
dmap1 = mcrfpy.DiscreteMap((10, 10), fill=50)
dmap2 = mcrfpy.DiscreteMap((10, 10), fill=30)
dmap1.add(dmap2)
assert dmap1[5, 5] == 80, f"Expected 80, got {dmap1[5, 5]}"
# Test saturation
dmap1.fill(200)
dmap2.fill(100)
dmap1.add(dmap2)
assert dmap1[0, 0] == 255, f"Expected 255 (saturated), got {dmap1[0, 0]}"
print(" [PASS] Add map")
def test_subtract_scalar():
"""Test subtracting a scalar value."""
dmap = mcrfpy.DiscreteMap((10, 10), fill=100)
dmap.subtract(25)
assert dmap[5, 5] == 75, f"Expected 75, got {dmap[5, 5]}"
# Test saturation at 0
dmap.fill(10)
dmap.subtract(20) # 10 - 20 = -10 -> saturates to 0
assert dmap[0, 0] == 0, f"Expected 0 (saturated), got {dmap[0, 0]}"
print(" [PASS] Subtract scalar")
def test_subtract_map():
"""Test subtracting another DiscreteMap."""
dmap1 = mcrfpy.DiscreteMap((10, 10), fill=100)
dmap2 = mcrfpy.DiscreteMap((10, 10), fill=30)
dmap1.subtract(dmap2)
assert dmap1[5, 5] == 70, f"Expected 70, got {dmap1[5, 5]}"
# Test saturation
dmap1.fill(50)
dmap2.fill(100)
dmap1.subtract(dmap2)
assert dmap1[0, 0] == 0, f"Expected 0 (saturated), got {dmap1[0, 0]}"
print(" [PASS] Subtract map")
def test_multiply():
"""Test scalar multiplication."""
dmap = mcrfpy.DiscreteMap((10, 10), fill=50)
dmap.multiply(2.0)
assert dmap[5, 5] == 100, f"Expected 100, got {dmap[5, 5]}"
# Test saturation
dmap.fill(100)
dmap.multiply(3.0) # 100 * 3 = 300 -> saturates to 255
assert dmap[0, 0] == 255, f"Expected 255 (saturated), got {dmap[0, 0]}"
# Test fractional
dmap.fill(100)
dmap.multiply(0.5)
assert dmap[0, 0] == 50, f"Expected 50, got {dmap[0, 0]}"
print(" [PASS] Multiply")
def test_copy_from():
"""Test copy_from operation."""
dmap1 = mcrfpy.DiscreteMap((10, 10), fill=0)
dmap2 = mcrfpy.DiscreteMap((5, 5), fill=99)
dmap1.copy_from(dmap2, pos=(2, 2))
assert dmap1[2, 2] == 99, f"Expected 99 at (2,2), got {dmap1[2, 2]}"
assert dmap1[6, 6] == 99, f"Expected 99 at (6,6), got {dmap1[6, 6]}"
assert dmap1[0, 0] == 0, f"Expected 0 at (0,0), got {dmap1[0, 0]}"
assert dmap1[7, 7] == 0, f"Expected 0 at (7,7), got {dmap1[7, 7]}"
print(" [PASS] Copy from")
def test_max():
"""Test element-wise max."""
dmap1 = mcrfpy.DiscreteMap((10, 10), fill=50)
dmap2 = mcrfpy.DiscreteMap((10, 10), fill=70)
# Set some values in dmap1 higher
dmap1[3, 3] = 100
dmap1.max(dmap2)
assert dmap1[0, 0] == 70, f"Expected 70 at (0,0), got {dmap1[0, 0]}"
assert dmap1[3, 3] == 100, f"Expected 100 at (3,3), got {dmap1[3, 3]}"
print(" [PASS] Max")
def test_min():
"""Test element-wise min."""
dmap1 = mcrfpy.DiscreteMap((10, 10), fill=50)
dmap2 = mcrfpy.DiscreteMap((10, 10), fill=30)
# Set some values in dmap1 lower
dmap1[3, 3] = 10
dmap1.min(dmap2)
assert dmap1[0, 0] == 30, f"Expected 30 at (0,0), got {dmap1[0, 0]}"
assert dmap1[3, 3] == 10, f"Expected 10 at (3,3), got {dmap1[3, 3]}"
print(" [PASS] Min")
def test_bitwise_and():
"""Test bitwise AND."""
dmap1 = mcrfpy.DiscreteMap((10, 10), fill=0xFF) # 11111111
dmap2 = mcrfpy.DiscreteMap((10, 10), fill=0x0F) # 00001111
dmap1.bitwise_and(dmap2)
assert dmap1[0, 0] == 0x0F, f"Expected 0x0F, got {hex(dmap1[0, 0])}"
# Test specific pattern
dmap1.fill(0b10101010)
dmap2.fill(0b11110000)
dmap1.bitwise_and(dmap2)
assert dmap1[0, 0] == 0b10100000, f"Expected 0b10100000, got {bin(dmap1[0, 0])}"
print(" [PASS] Bitwise AND")
def test_bitwise_or():
"""Test bitwise OR."""
dmap1 = mcrfpy.DiscreteMap((10, 10), fill=0x0F) # 00001111
dmap2 = mcrfpy.DiscreteMap((10, 10), fill=0xF0) # 11110000
dmap1.bitwise_or(dmap2)
assert dmap1[0, 0] == 0xFF, f"Expected 0xFF, got {hex(dmap1[0, 0])}"
print(" [PASS] Bitwise OR")
def test_bitwise_xor():
"""Test bitwise XOR."""
dmap1 = mcrfpy.DiscreteMap((10, 10), fill=0xFF)
dmap2 = mcrfpy.DiscreteMap((10, 10), fill=0xFF)
dmap1.bitwise_xor(dmap2)
assert dmap1[0, 0] == 0x00, f"Expected 0x00, got {hex(dmap1[0, 0])}"
dmap1.fill(0b10101010)
dmap2.fill(0b11110000)
dmap1.bitwise_xor(dmap2)
assert dmap1[0, 0] == 0b01011010, f"Expected 0b01011010, got {bin(dmap1[0, 0])}"
print(" [PASS] Bitwise XOR")
def test_invert():
"""Test invert (returns new map)."""
dmap = mcrfpy.DiscreteMap((10, 10), fill=100)
result = dmap.invert()
# Original unchanged
assert dmap[0, 0] == 100, f"Original should be unchanged, got {dmap[0, 0]}"
# Result is inverted
assert result[0, 0] == 155, f"Expected 155 (255-100), got {result[0, 0]}"
# Test edge cases
dmap.fill(0)
result = dmap.invert()
assert result[0, 0] == 255, f"Expected 255, got {result[0, 0]}"
dmap.fill(255)
result = dmap.invert()
assert result[0, 0] == 0, f"Expected 0, got {result[0, 0]}"
print(" [PASS] Invert")
def test_region_operations():
"""Test operations with region parameters."""
dmap1 = mcrfpy.DiscreteMap((20, 20), fill=10)
dmap2 = mcrfpy.DiscreteMap((20, 20), fill=5)
# Add only in a region
dmap1.add(dmap2, pos=(5, 5), source_pos=(0, 0), size=(5, 5))
assert dmap1[5, 5] == 15, f"Expected 15 in region, got {dmap1[5, 5]}"
assert dmap1[9, 9] == 15, f"Expected 15 in region, got {dmap1[9, 9]}"
assert dmap1[0, 0] == 10, f"Expected 10 outside region, got {dmap1[0, 0]}"
assert dmap1[10, 10] == 10, f"Expected 10 outside region, got {dmap1[10, 10]}"
print(" [PASS] Region operations")
def main():
print("Running DiscreteMap arithmetic tests...")
test_add_scalar()
test_add_map()
test_subtract_scalar()
test_subtract_map()
test_multiply()
test_copy_from()
test_max()
test_min()
test_bitwise_and()
test_bitwise_or()
test_bitwise_xor()
test_invert()
test_region_operations()
print("All DiscreteMap arithmetic tests PASSED!")
sys.exit(0)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""Unit tests for DiscreteMap basic operations."""
import mcrfpy
import sys
def test_construction():
"""Test basic construction."""
# Default construction
dmap = mcrfpy.DiscreteMap((100, 100))
assert dmap.size == (100, 100), f"Expected (100, 100), got {dmap.size}"
# With fill value
dmap2 = mcrfpy.DiscreteMap((50, 50), fill=42)
assert dmap2[0, 0] == 42, f"Expected 42, got {dmap2[0, 0]}"
assert dmap2[25, 25] == 42, f"Expected 42, got {dmap2[25, 25]}"
print(" [PASS] Construction")
def test_size_property():
"""Test size property."""
dmap = mcrfpy.DiscreteMap((123, 456))
w, h = dmap.size
assert w == 123, f"Expected width 123, got {w}"
assert h == 456, f"Expected height 456, got {h}"
print(" [PASS] Size property")
def test_get_set():
"""Test get/set methods."""
dmap = mcrfpy.DiscreteMap((10, 10))
# Test set/get
dmap.set(5, 5, 100)
assert dmap.get(5, 5) == 100, f"Expected 100, got {dmap.get(5, 5)}"
# Test subscript
dmap[3, 7] = 200
assert dmap[3, 7] == 200, f"Expected 200, got {dmap[3, 7]}"
# Test tuple subscript
dmap[(1, 2)] = 150
assert dmap[(1, 2)] == 150, f"Expected 150, got {dmap[(1, 2)]}"
print(" [PASS] Get/set methods")
def test_bounds_checking():
"""Test that out-of-bounds access raises IndexError."""
dmap = mcrfpy.DiscreteMap((10, 10))
# Test out of bounds get
try:
_ = dmap[10, 10]
print(" [FAIL] Should have raised IndexError for (10, 10)")
return False
except IndexError:
pass
try:
_ = dmap[-1, 0]
print(" [FAIL] Should have raised IndexError for (-1, 0)")
return False
except IndexError:
pass
# Test out of bounds set
try:
dmap[100, 100] = 5
print(" [FAIL] Should have raised IndexError for set")
return False
except IndexError:
pass
print(" [PASS] Bounds checking")
return True
def test_value_range():
"""Test that values are clamped to 0-255."""
dmap = mcrfpy.DiscreteMap((10, 10))
# Test valid range
dmap[0, 0] = 0
dmap[0, 1] = 255
assert dmap[0, 0] == 0
assert dmap[0, 1] == 255
# Test invalid values
try:
dmap[0, 0] = -1
print(" [FAIL] Should have raised ValueError for -1")
return False
except ValueError:
pass
try:
dmap[0, 0] = 256
print(" [FAIL] Should have raised ValueError for 256")
return False
except ValueError:
pass
print(" [PASS] Value range")
return True
def test_fill():
"""Test fill operation."""
dmap = mcrfpy.DiscreteMap((10, 10))
# Fill entire map
dmap.fill(77)
for y in range(10):
for x in range(10):
assert dmap[x, y] == 77, f"Expected 77 at ({x}, {y}), got {dmap[x, y]}"
# Fill region
dmap.fill(88, pos=(2, 2), size=(3, 3))
assert dmap[2, 2] == 88, "Region fill failed at start"
assert dmap[4, 4] == 88, "Region fill failed at end"
assert dmap[1, 1] == 77, "Region fill affected outside area"
assert dmap[5, 5] == 77, "Region fill affected outside area"
print(" [PASS] Fill operation")
def test_clear():
"""Test clear operation."""
dmap = mcrfpy.DiscreteMap((10, 10), fill=100)
dmap.clear()
for y in range(10):
for x in range(10):
assert dmap[x, y] == 0, f"Expected 0 at ({x}, {y}), got {dmap[x, y]}"
print(" [PASS] Clear operation")
def test_repr():
"""Test repr output."""
dmap = mcrfpy.DiscreteMap((100, 50))
r = repr(dmap)
assert "DiscreteMap" in r, f"Expected 'DiscreteMap' in repr, got {r}"
assert "100" in r, f"Expected '100' in repr, got {r}"
assert "50" in r, f"Expected '50' in repr, got {r}"
print(" [PASS] Repr")
def test_chaining():
"""Test method chaining."""
dmap = mcrfpy.DiscreteMap((10, 10))
# Methods should return self
result = dmap.fill(50).clear().fill(100)
assert result is dmap, "Method chaining should return self"
assert dmap[5, 5] == 100, "Chained operations should work"
print(" [PASS] Method chaining")
def main():
print("Running DiscreteMap basic tests...")
test_construction()
test_size_property()
test_get_set()
if not test_bounds_checking():
sys.exit(1)
if not test_value_range():
sys.exit(1)
test_fill()
test_clear()
test_repr()
test_chaining()
print("All DiscreteMap basic tests PASSED!")
sys.exit(0)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,280 @@
#!/usr/bin/env python3
"""Unit tests for DiscreteMap <-> HeightMap integration."""
import mcrfpy
import sys
from enum import IntEnum
class Terrain(IntEnum):
WATER = 0
SAND = 1
GRASS = 2
FOREST = 3
MOUNTAIN = 4
def test_from_heightmap_basic():
"""Test basic HeightMap to DiscreteMap conversion."""
# Create a simple heightmap
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
# Create a simple mapping
mapping = [
((0.0, 0.3), 0),
((0.3, 0.6), 1),
((0.6, 1.0), 2),
]
dmap = mcrfpy.DiscreteMap.from_heightmap(hmap, mapping)
# 0.5 should map to category 1
assert dmap[5, 5] == 1, f"Expected 1, got {dmap[5, 5]}"
print(" [PASS] from_heightmap basic")
def test_from_heightmap_full_range():
"""Test conversion with values spanning the full range."""
hmap = mcrfpy.HeightMap((100, 1))
# Create gradient
for x in range(100):
hmap[x, 0] = x / 100.0 # 0.0 to 0.99
mapping = [
((0.0, 0.25), Terrain.WATER),
((0.25, 0.5), Terrain.SAND),
((0.5, 0.75), Terrain.GRASS),
((0.75, 1.0), Terrain.FOREST),
]
dmap = mcrfpy.DiscreteMap.from_heightmap(hmap, mapping)
# Check values at key positions
assert dmap[10, 0] == Terrain.WATER, f"Expected WATER at 10, got {dmap[10, 0]}"
assert dmap[30, 0] == Terrain.SAND, f"Expected SAND at 30, got {dmap[30, 0]}"
assert dmap[60, 0] == Terrain.GRASS, f"Expected GRASS at 60, got {dmap[60, 0]}"
assert dmap[80, 0] == Terrain.FOREST, f"Expected FOREST at 80, got {dmap[80, 0]}"
print(" [PASS] from_heightmap full range")
def test_from_heightmap_with_enum():
"""Test from_heightmap with enum parameter."""
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
mapping = [
((0.0, 0.3), Terrain.WATER),
((0.3, 0.7), Terrain.GRASS),
((0.7, 1.0), Terrain.MOUNTAIN),
]
dmap = mcrfpy.DiscreteMap.from_heightmap(hmap, mapping, enum=Terrain)
# Value should be returned as enum member
val = dmap[5, 5]
assert val == Terrain.GRASS, f"Expected Terrain.GRASS, got {val}"
assert isinstance(val, Terrain), f"Expected Terrain type, got {type(val)}"
print(" [PASS] from_heightmap with enum")
def test_to_heightmap_basic():
"""Test basic DiscreteMap to HeightMap conversion."""
dmap = mcrfpy.DiscreteMap((10, 10), fill=100)
hmap = dmap.to_heightmap()
# Direct conversion: uint8 -> float
assert abs(hmap[5, 5] - 100.0) < 0.001, f"Expected 100.0, got {hmap[5, 5]}"
print(" [PASS] to_heightmap basic")
def test_to_heightmap_with_mapping():
"""Test to_heightmap with value mapping."""
dmap = mcrfpy.DiscreteMap((10, 10))
# Create pattern
dmap.fill(0, pos=(0, 0), size=(5, 10)) # Left half = 0
dmap.fill(1, pos=(5, 0), size=(5, 10)) # Right half = 1
# Map discrete values to heights
mapping = {
0: 0.2,
1: 0.8,
}
hmap = dmap.to_heightmap(mapping)
assert abs(hmap[2, 5] - 0.2) < 0.001, f"Expected 0.2, got {hmap[2, 5]}"
assert abs(hmap[7, 5] - 0.8) < 0.001, f"Expected 0.8, got {hmap[7, 5]}"
print(" [PASS] to_heightmap with mapping")
def test_roundtrip():
"""Test HeightMap -> DiscreteMap -> HeightMap roundtrip."""
# Create original heightmap
original = mcrfpy.HeightMap((50, 50))
for y in range(50):
for x in range(50):
original[x, y] = (x + y) / 100.0 # Gradient 0.0 to 0.98
# Convert to discrete with specific ranges
mapping = [
((0.0, 0.33), 0),
((0.33, 0.66), 1),
((0.66, 1.0), 2),
]
dmap = mcrfpy.DiscreteMap.from_heightmap(original, mapping)
# Convert back with value mapping
reverse_mapping = {
0: 0.15, # Midpoint of first range
1: 0.5, # Midpoint of second range
2: 0.85, # Midpoint of third range
}
restored = dmap.to_heightmap(reverse_mapping)
# Verify approximate restoration
assert abs(restored[0, 0] - 0.15) < 0.01, f"Expected ~0.15 at (0,0), got {restored[0, 0]}"
assert abs(restored[25, 25] - 0.5) < 0.01, f"Expected ~0.5 at (25,25), got {restored[25, 25]}"
print(" [PASS] Roundtrip conversion")
def test_query_methods():
"""Test count, count_range, min_max, histogram."""
dmap = mcrfpy.DiscreteMap((10, 10))
# Create pattern with different values
dmap.fill(0, pos=(0, 0), size=(5, 5)) # 25 cells with 0
dmap.fill(1, pos=(5, 0), size=(5, 5)) # 25 cells with 1
dmap.fill(2, pos=(0, 5), size=(5, 5)) # 25 cells with 2
dmap.fill(3, pos=(5, 5), size=(5, 5)) # 25 cells with 3
# Test count
assert dmap.count(0) == 25, f"Expected 25 zeros, got {dmap.count(0)}"
assert dmap.count(1) == 25, f"Expected 25 ones, got {dmap.count(1)}"
assert dmap.count(4) == 0, f"Expected 0 fours, got {dmap.count(4)}"
# Test count_range
assert dmap.count_range(0, 1) == 50, f"Expected 50 in range 0-1, got {dmap.count_range(0, 1)}"
assert dmap.count_range(0, 3) == 100, f"Expected 100 in range 0-3, got {dmap.count_range(0, 3)}"
# Test min_max
min_val, max_val = dmap.min_max()
assert min_val == 0, f"Expected min 0, got {min_val}"
assert max_val == 3, f"Expected max 3, got {max_val}"
# Test histogram
hist = dmap.histogram()
assert hist[0] == 25, f"Expected 25 for value 0, got {hist.get(0)}"
assert hist[1] == 25, f"Expected 25 for value 1, got {hist.get(1)}"
assert hist[2] == 25, f"Expected 25 for value 2, got {hist.get(2)}"
assert hist[3] == 25, f"Expected 25 for value 3, got {hist.get(3)}"
assert 4 not in hist, "Value 4 should not be in histogram"
print(" [PASS] Query methods")
def test_bool_int():
"""Test bool() with integer condition."""
dmap = mcrfpy.DiscreteMap((10, 10), fill=0)
dmap.fill(1, pos=(2, 2), size=(3, 3))
mask = dmap.bool(1)
# Should be 1 where original is 1, 0 elsewhere
assert mask[0, 0] == 0, f"Expected 0 outside region, got {mask[0, 0]}"
assert mask[3, 3] == 1, f"Expected 1 inside region, got {mask[3, 3]}"
assert mask.count(1) == 9, f"Expected 9 ones, got {mask.count(1)}"
print(" [PASS] bool() with int")
def test_bool_set():
"""Test bool() with set condition."""
dmap = mcrfpy.DiscreteMap((10, 10))
dmap.fill(0, pos=(0, 0), size=(5, 5))
dmap.fill(1, pos=(5, 0), size=(5, 5))
dmap.fill(2, pos=(0, 5), size=(5, 5))
dmap.fill(3, pos=(5, 5), size=(5, 5))
# Match 0 or 2
mask = dmap.bool({0, 2})
assert mask[2, 2] == 1, "Expected 1 where value is 0"
assert mask[7, 2] == 0, "Expected 0 where value is 1"
assert mask[2, 7] == 1, "Expected 1 where value is 2"
assert mask[7, 7] == 0, "Expected 0 where value is 3"
assert mask.count(1) == 50, f"Expected 50 ones, got {mask.count(1)}"
print(" [PASS] bool() with set")
def test_bool_callable():
"""Test bool() with callable condition."""
dmap = mcrfpy.DiscreteMap((10, 10), fill=0)
for y in range(10):
for x in range(10):
dmap[x, y] = x + y # Values 0-18
# Match where value > 10
mask = dmap.bool(lambda v: v > 10)
assert mask[5, 5] == 0, "Expected 0 where value is 10"
assert mask[6, 6] == 1, "Expected 1 where value is 12"
assert mask[9, 9] == 1, "Expected 1 where value is 18"
print(" [PASS] bool() with callable")
def test_mask_memoryview():
"""Test mask() returns working memoryview."""
dmap = mcrfpy.DiscreteMap((10, 10), fill=42)
mv = dmap.mask()
assert len(mv) == 100, f"Expected 100 bytes, got {len(mv)}"
assert mv[0] == 42, f"Expected 42, got {mv[0]}"
# Test writing through memoryview
mv[50] = 99
assert dmap[0, 5] == 99, f"Expected 99, got {dmap[0, 5]}"
print(" [PASS] mask() memoryview")
def test_enum_type_property():
"""Test enum_type property getter/setter."""
dmap = mcrfpy.DiscreteMap((10, 10), fill=1)
# Initially no enum
assert dmap.enum_type is None, "Expected None initially"
# Set enum type
dmap.enum_type = Terrain
assert dmap.enum_type is Terrain, "Expected Terrain enum"
# Value should now return enum member
val = dmap[5, 5]
assert val == Terrain.SAND, f"Expected Terrain.SAND, got {val}"
# Clear enum type
dmap.enum_type = None
val = dmap[5, 5]
assert isinstance(val, int), f"Expected int after clearing enum, got {type(val)}"
print(" [PASS] enum_type property")
def main():
print("Running DiscreteMap HeightMap integration tests...")
test_from_heightmap_basic()
test_from_heightmap_full_range()
test_from_heightmap_with_enum()
test_to_heightmap_basic()
test_to_heightmap_with_mapping()
test_roundtrip()
test_query_methods()
test_bool_int()
test_bool_set()
test_bool_callable()
test_mask_memoryview()
test_enum_type_property()
print("All DiscreteMap HeightMap integration tests PASSED!")
sys.exit(0)
if __name__ == "__main__":
main()