"""Unit tests for HeightMap direct source sampling (Issue #209) Tests: - add_noise() - sample noise and add to heightmap - multiply_noise() - sample noise and multiply with heightmap - add_bsp() - add BSP regions to heightmap - multiply_bsp() - multiply by BSP regions (masking) - Equivalence with intermediate HeightMap approach - Error handling """ import mcrfpy import sys def test_add_noise_basic(): """Test basic add_noise() operation""" noise = mcrfpy.NoiseSource(dimensions=2, seed=42) hmap = mcrfpy.HeightMap((50, 50), fill=0.0) result = hmap.add_noise(noise) # Should return self for chaining assert result is hmap, "add_noise() should return self" # Check that values have been modified min_val, max_val = hmap.min_max() assert max_val > 0 or min_val < 0, "Noise should add non-zero values" print(" PASS: add_noise() basic") def test_add_noise_modes(): """Test add_noise() with different modes""" noise = mcrfpy.NoiseSource(dimensions=2, seed=42) for mode in ["flat", "fbm", "turbulence"]: hmap = mcrfpy.HeightMap((30, 30), fill=0.0) hmap.add_noise(noise, mode=mode) min_val, max_val = hmap.min_max() assert min_val >= -2.0 and max_val <= 2.0, f"Mode '{mode}': values out of expected range" print(" PASS: add_noise() modes") def test_add_noise_scale(): """Test add_noise() with scale parameter""" noise = mcrfpy.NoiseSource(dimensions=2, seed=42) # Scale 0.5 hmap1 = mcrfpy.HeightMap((30, 30), fill=0.0) hmap1.add_noise(noise, scale=0.5) min1, max1 = hmap1.min_max() # Scale 1.0 hmap2 = mcrfpy.HeightMap((30, 30), fill=0.0) hmap2.add_noise(noise, scale=1.0) min2, max2 = hmap2.min_max() # Scale 0.5 should have smaller range range1 = max1 - min1 range2 = max2 - min2 assert range1 < range2 or abs(range1 - range2 * 0.5) < 0.1, "Scale should affect value range" print(" PASS: add_noise() scale") def test_add_noise_equivalence(): """Test that add_noise() produces same result as sample() + add()""" noise = mcrfpy.NoiseSource(dimensions=2, seed=42) # Method 1: Using sample() then add() hmap1 = mcrfpy.HeightMap((40, 40), fill=5.0) sampled = noise.sample(size=(40, 40), mode="fbm", octaves=4) hmap1.add(sampled) # Method 2: Using add_noise() directly hmap2 = mcrfpy.HeightMap((40, 40), fill=5.0) hmap2.add_noise(noise, mode="fbm", octaves=4) # Compare values - should be identical differences = 0 for y in range(40): for x in range(40): v1 = hmap1.get((x, y)) v2 = hmap2.get((x, y)) if abs(v1 - v2) > 0.0001: differences += 1 assert differences == 0, f"add_noise() should produce same result as sample()+add(), got {differences} differences" print(" PASS: add_noise() equivalence") def test_multiply_noise_basic(): """Test basic multiply_noise() operation""" noise = mcrfpy.NoiseSource(dimensions=2, seed=42) hmap = mcrfpy.HeightMap((50, 50), fill=1.0) result = hmap.multiply_noise(noise) assert result is hmap, "multiply_noise() should return self" # Values should now be in a range around 0 (noise * 1.0) min_val, max_val = hmap.min_max() # FBM noise ranges from ~-1 to ~1, so multiplied values should too assert min_val < 0.5, "multiply_noise() should produce values less than 0.5" print(" PASS: multiply_noise() basic") def test_multiply_noise_scale(): """Test multiply_noise() with scale parameter""" noise = mcrfpy.NoiseSource(dimensions=2, seed=42) # Start with values of 10 hmap = mcrfpy.HeightMap((30, 30), fill=10.0) # Multiply by noise scaled to 0.5 # Result should be 10 * (noise_value * 0.5) hmap.multiply_noise(noise, scale=0.5) min_val, max_val = hmap.min_max() # With scale 0.5, max possible is 10 * 0.5 = 5, min is 10 * -0.5 = -5 assert max_val <= 6.0, f"Expected max <= 6.0, got {max_val}" print(" PASS: multiply_noise() scale") def test_add_bsp_basic(): """Test basic add_bsp() operation""" # Create BSP bsp = mcrfpy.BSP(pos=(0, 0), size=(50, 50)) bsp.split_recursive(depth=3, min_size=(8, 8)) # Create heightmap and add BSP hmap = mcrfpy.HeightMap((50, 50), fill=0.0) result = hmap.add_bsp(bsp) assert result is hmap, "add_bsp() should return self" # Check that some values are non-zero (inside rooms) min_val, max_val = hmap.min_max() assert max_val > 0, "add_bsp() should add non-zero values inside BSP regions" print(" PASS: add_bsp() basic") def test_add_bsp_select_modes(): """Test add_bsp() with different select modes""" bsp = mcrfpy.BSP(pos=(0, 0), size=(60, 60)) bsp.split_recursive(depth=3, min_size=(10, 10)) # Test leaves hmap_leaves = mcrfpy.HeightMap((60, 60), fill=0.0) hmap_leaves.add_bsp(bsp, select="leaves") # Test all hmap_all = mcrfpy.HeightMap((60, 60), fill=0.0) hmap_all.add_bsp(bsp, select="all") # Test internal hmap_internal = mcrfpy.HeightMap((60, 60), fill=0.0) hmap_internal.add_bsp(bsp, select="internal") # All modes should produce some non-zero values for name, hmap in [("leaves", hmap_leaves), ("all", hmap_all), ("internal", hmap_internal)]: min_val, max_val = hmap.min_max() assert max_val > 0, f"select='{name}' should produce non-zero values" # "all" should cover more area than just leaves or internal count_leaves = hmap_leaves.count_in_range((0.5, 2.0)) count_all = hmap_all.count_in_range((0.5, 10.0)) # 'all' may overlap, so higher max count_internal = hmap_internal.count_in_range((0.5, 2.0)) assert count_all >= count_leaves, "'all' should cover at least as much as 'leaves'" print(" PASS: add_bsp() select modes") def test_add_bsp_shrink(): """Test add_bsp() with shrink parameter""" bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 80)) bsp.split_recursive(depth=2, min_size=(20, 20)) # Without shrink hmap1 = mcrfpy.HeightMap((80, 80), fill=0.0) hmap1.add_bsp(bsp, shrink=0) count1 = hmap1.count_in_range((0.5, 2.0)) # With shrink hmap2 = mcrfpy.HeightMap((80, 80), fill=0.0) hmap2.add_bsp(bsp, shrink=2) count2 = hmap2.count_in_range((0.5, 2.0)) # Shrunk version should have fewer cells assert count2 < count1, f"Shrink should reduce covered cells: {count2} vs {count1}" print(" PASS: add_bsp() shrink") def test_add_bsp_value(): """Test add_bsp() with custom value""" bsp = mcrfpy.BSP(pos=(0, 0), size=(40, 40)) bsp.split_recursive(depth=2, min_size=(10, 10)) hmap = mcrfpy.HeightMap((40, 40), fill=0.0) hmap.add_bsp(bsp, value=5.0) min_val, max_val = hmap.min_max() assert max_val == 5.0, f"Value parameter should set cell values to 5.0, got {max_val}" print(" PASS: add_bsp() value") def test_multiply_bsp_basic(): """Test basic multiply_bsp() operation (masking)""" bsp = mcrfpy.BSP(pos=(0, 0), size=(50, 50)) bsp.split_recursive(depth=3, min_size=(8, 8)) # Create heightmap with uniform value hmap = mcrfpy.HeightMap((50, 50), fill=10.0) result = hmap.multiply_bsp(bsp) assert result is hmap, "multiply_bsp() should return self" # Note: BSP leaves partition the ENTIRE space, so all cells are inside some leaf # To get "walls" between rooms, you need to use shrink > 0 # Without shrink, all cells should be preserved min_val, max_val = hmap.min_max() assert max_val == 10.0, f"Areas inside BSP should be 10.0, got max={max_val}" # Test with shrink to get actual masking (walls between rooms) hmap2 = mcrfpy.HeightMap((50, 50), fill=10.0) hmap2.multiply_bsp(bsp, shrink=2) # Leave 2-pixel walls min_val2, max_val2 = hmap2.min_max() assert min_val2 == 0.0, f"Areas between shrunken rooms should be 0, got min={min_val2}" assert max_val2 == 10.0, f"Areas inside shrunken rooms should be 10.0, got max={max_val2}" print(" PASS: multiply_bsp() basic (masking)") def test_multiply_bsp_with_noise(): """Test multiply_bsp() to mask noisy terrain""" noise = mcrfpy.NoiseSource(dimensions=2, seed=42) bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 80)) bsp.split_recursive(depth=3, min_size=(10, 10)) # Generate noisy terrain hmap = mcrfpy.HeightMap((80, 80), fill=0.0) hmap.add_noise(noise, mode="fbm", octaves=6, scale=1.0) hmap.normalize(0.0, 1.0) # Mask to BSP regions hmap.multiply_bsp(bsp, select="leaves", shrink=1) # Check that some values are 0 (outside rooms) and some are positive (inside) count_zero = hmap.count_in_range((-0.001, 0.001)) count_positive = hmap.count_in_range((0.1, 1.5)) assert count_zero > 0, "Should have zero values outside BSP" assert count_positive > 0, "Should have positive values inside BSP" print(" PASS: multiply_bsp() with noise (terrain masking)") def test_add_noise_requires_2d(): """Test that add_noise() requires 2D NoiseSource""" for dim in [1, 3, 4]: noise = mcrfpy.NoiseSource(dimensions=dim, seed=42) hmap = mcrfpy.HeightMap((20, 20), fill=0.0) try: hmap.add_noise(noise) print(f" FAIL: add_noise() should raise ValueError for {dim}D noise") sys.exit(1) except ValueError: pass print(" PASS: add_noise() requires 2D noise") def test_type_errors(): """Test type error handling""" hmap = mcrfpy.HeightMap((20, 20), fill=0.0) noise = mcrfpy.NoiseSource(dimensions=2, seed=42) # add_noise with non-NoiseSource try: hmap.add_noise("not a noise source") print(" FAIL: add_noise() should raise TypeError") sys.exit(1) except TypeError: pass # add_bsp with non-BSP try: hmap.add_bsp(noise) # passing NoiseSource instead of BSP print(" FAIL: add_bsp() should raise TypeError") sys.exit(1) except TypeError: pass print(" PASS: Type error handling") def test_invalid_select_mode(): """Test invalid select mode error""" bsp = mcrfpy.BSP(pos=(0, 0), size=(30, 30)) hmap = mcrfpy.HeightMap((30, 30), fill=0.0) try: hmap.add_bsp(bsp, select="invalid") print(" FAIL: add_bsp() should raise ValueError for invalid select") sys.exit(1) except ValueError: pass print(" PASS: Invalid select mode error") def test_method_chaining(): """Test method chaining with direct sampling""" noise = mcrfpy.NoiseSource(dimensions=2, seed=42) bsp = mcrfpy.BSP(pos=(0, 0), size=(60, 60)) bsp.split_recursive(depth=2, min_size=(15, 15)) hmap = mcrfpy.HeightMap((60, 60), fill=0.0) # Chain operations result = (hmap .add_noise(noise, mode="fbm", octaves=4) .normalize(0.0, 1.0) .multiply_bsp(bsp, select="leaves", shrink=1) .scale(10.0)) assert result is hmap, "Chained operations should return self" # Verify the result makes sense min_val, max_val = hmap.min_max() assert min_val == 0.0, "Masked areas should be 0" assert max_val > 0.0, "Unmasked areas should have positive values" print(" PASS: Method chaining") def run_tests(): """Run all HeightMap direct sampling tests""" print("Testing HeightMap direct sampling (Issue #209)...") test_add_noise_basic() test_add_noise_modes() test_add_noise_scale() test_add_noise_equivalence() test_multiply_noise_basic() test_multiply_noise_scale() test_add_bsp_basic() test_add_bsp_select_modes() test_add_bsp_shrink() test_add_bsp_value() test_multiply_bsp_basic() test_multiply_bsp_with_noise() test_add_noise_requires_2d() test_type_errors() test_invalid_select_mode() test_method_chaining() print("All HeightMap direct sampling tests PASSED!") return True if __name__ == "__main__": try: success = run_tests() sys.exit(0 if success else 1) except Exception as e: print(f"FAIL: Unexpected exception: {e}") import traceback traceback.print_exc() sys.exit(1)