3D / voxel unit tests

This commit is contained in:
John McCardle 2026-02-06 16:15:07 -05:00
commit 71cd2b9b41
22 changed files with 4705 additions and 0 deletions

View file

@ -0,0 +1,335 @@
#!/usr/bin/env python3
"""Unit tests for VoxelGrid bulk operations (Milestone 11)
Tests:
- fill_box_hollow: Verify shell only, interior empty
- fill_sphere: Volume roughly matches (4/3)πr³
- fill_cylinder: Volume roughly matches πr²h
- fill_noise: Higher threshold = fewer voxels
- copy_region/paste_region: Round-trip verification
- skip_air option for paste
"""
import sys
import math
# Track test results
passed = 0
failed = 0
def test(name, condition, detail=""):
"""Record test result"""
global passed, failed
if condition:
print(f"[PASS] {name}")
passed += 1
else:
print(f"[FAIL] {name}" + (f" - {detail}" if detail else ""))
failed += 1
def test_fill_box_hollow_basic():
"""fill_box_hollow creates correct shell"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(10, 10, 10))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
# Create hollow 6x6x6 box with thickness 1
vg.fill_box_hollow((2, 2, 2), (7, 7, 7), stone, thickness=1)
# Total box = 6x6x6 = 216
# Interior = 4x4x4 = 64
# Shell = 216 - 64 = 152
expected = 152
actual = vg.count_non_air()
test("Hollow box: shell has correct voxel count", actual == expected,
f"got {actual}, expected {expected}")
# Verify interior is empty (center should be air)
test("Hollow box: interior is air", vg.get(4, 4, 4) == 0)
test("Hollow box: interior is air (another point)", vg.get(5, 5, 5) == 0)
# Verify shell exists
test("Hollow box: corner is filled", vg.get(2, 2, 2) == stone)
test("Hollow box: edge is filled", vg.get(4, 2, 2) == stone)
def test_fill_box_hollow_thick():
"""fill_box_hollow with thickness > 1"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(12, 12, 12))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
# Create hollow 10x10x10 box with thickness 2
vg.fill_box_hollow((1, 1, 1), (10, 10, 10), stone, thickness=2)
# Total box = 10x10x10 = 1000
# Interior = 6x6x6 = 216
# Shell = 1000 - 216 = 784
expected = 784
actual = vg.count_non_air()
test("Thick hollow box: correct voxel count", actual == expected,
f"got {actual}, expected {expected}")
# Verify interior is empty
test("Thick hollow box: center is air", vg.get(5, 5, 5) == 0)
def test_fill_sphere_volume():
"""fill_sphere produces roughly spherical shape"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(30, 30, 30))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
# Fill sphere with radius 8
radius = 8
vg.fill_sphere((15, 15, 15), radius, stone)
# Expected volume ≈ (4/3)πr³
expected_vol = (4.0 / 3.0) * math.pi * (radius ** 3)
actual = vg.count_non_air()
# Voxel sphere should be within 20% of theoretical volume
ratio = actual / expected_vol
test("Sphere volume: within 20% of (4/3)πr³",
0.8 <= ratio <= 1.2,
f"got {actual}, expected ~{int(expected_vol)}, ratio={ratio:.2f}")
def test_fill_sphere_carve():
"""fill_sphere with material 0 carves out voxels"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(20, 20, 20))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
# Fill entire grid with stone
vg.fill(stone)
initial = vg.count_non_air()
test("Sphere carve: initial fill", initial == 8000) # 20x20x20
# Carve out a sphere (material 0)
vg.fill_sphere((10, 10, 10), 5, 0) # Air
final = vg.count_non_air()
test("Sphere carve: voxels removed", final < initial)
def test_fill_cylinder_volume():
"""fill_cylinder produces roughly cylindrical shape"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(30, 30, 30))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
# Fill cylinder with radius 5, height 10
radius = 5
height = 10
vg.fill_cylinder((15, 5, 15), radius, height, stone)
# Expected volume ≈ πr²h
expected_vol = math.pi * (radius ** 2) * height
actual = vg.count_non_air()
# Voxel cylinder should be within 20% of theoretical volume
ratio = actual / expected_vol
test("Cylinder volume: within 20% of πr²h",
0.8 <= ratio <= 1.2,
f"got {actual}, expected ~{int(expected_vol)}, ratio={ratio:.2f}")
def test_fill_cylinder_bounds():
"""fill_cylinder respects grid bounds"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(10, 10, 10))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
# Cylinder partially outside grid
vg.fill_cylinder((2, 0, 2), 3, 15, stone) # height extends beyond grid
# Should not crash, and have some voxels
count = vg.count_non_air()
test("Cylinder bounds: handles out-of-bounds gracefully", count > 0)
test("Cylinder bounds: limited by grid height", count < 3.14 * 9 * 15)
def test_fill_noise_threshold():
"""fill_noise: higher threshold = fewer voxels"""
import mcrfpy
vg1 = mcrfpy.VoxelGrid(size=(16, 16, 16))
vg2 = mcrfpy.VoxelGrid(size=(16, 16, 16))
stone = vg1.add_material("stone", color=mcrfpy.Color(128, 128, 128))
vg2.add_material("stone", color=mcrfpy.Color(128, 128, 128))
# Same seed, different thresholds
vg1.fill_noise((0, 0, 0), (15, 15, 15), stone, threshold=0.3, scale=0.15, seed=12345)
vg2.fill_noise((0, 0, 0), (15, 15, 15), stone, threshold=0.7, scale=0.15, seed=12345)
count1 = vg1.count_non_air()
count2 = vg2.count_non_air()
# Higher threshold should produce fewer voxels
test("Noise threshold: high threshold produces fewer voxels",
count2 < count1,
f"threshold=0.3 gave {count1}, threshold=0.7 gave {count2}")
def test_fill_noise_seed():
"""fill_noise: same seed produces same result"""
import mcrfpy
vg1 = mcrfpy.VoxelGrid(size=(16, 16, 16))
vg2 = mcrfpy.VoxelGrid(size=(16, 16, 16))
stone = vg1.add_material("stone", color=mcrfpy.Color(128, 128, 128))
vg2.add_material("stone", color=mcrfpy.Color(128, 128, 128))
# Same parameters
vg1.fill_noise((0, 0, 0), (15, 15, 15), stone, threshold=0.5, scale=0.1, seed=42)
vg2.fill_noise((0, 0, 0), (15, 15, 15), stone, threshold=0.5, scale=0.1, seed=42)
# Should produce identical results
count1 = vg1.count_non_air()
count2 = vg2.count_non_air()
test("Noise seed: same seed produces same count", count1 == count2,
f"got {count1} vs {count2}")
# Check a few sample points
same_values = True
for x, y, z in [(0, 0, 0), (8, 8, 8), (15, 15, 15), (3, 7, 11)]:
if vg1.get(x, y, z) != vg2.get(x, y, z):
same_values = False
break
test("Noise seed: same seed produces identical voxels", same_values)
def test_copy_paste_basic():
"""copy_region and paste_region round-trip"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(20, 10, 20))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
brick = vg.add_material("brick", color=mcrfpy.Color(165, 42, 42))
# Create a small structure
vg.fill_box((2, 0, 2), (5, 3, 5), stone)
vg.set(3, 1, 3, brick) # Add a different material
# Copy the region
prefab = vg.copy_region((2, 0, 2), (5, 3, 5))
# Verify VoxelRegion properties
test("Copy region: correct width", prefab.width == 4)
test("Copy region: correct height", prefab.height == 4)
test("Copy region: correct depth", prefab.depth == 4)
test("Copy region: size tuple", prefab.size == (4, 4, 4))
# Paste elsewhere
vg.paste_region(prefab, (10, 0, 10))
# Verify paste
test("Paste region: stone at corner", vg.get(10, 0, 10) == stone)
test("Paste region: brick inside", vg.get(11, 1, 11) == brick)
def test_copy_paste_skip_air():
"""paste_region with skip_air=True doesn't overwrite"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(20, 10, 20))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
gold = vg.add_material("gold", color=mcrfpy.Color(255, 215, 0))
# Create prefab with air gaps
vg.fill_box((0, 0, 0), (3, 3, 3), stone)
vg.set(1, 1, 1, 0) # Air hole
vg.set(2, 2, 2, 0) # Another air hole
# Copy it
prefab = vg.copy_region((0, 0, 0), (3, 3, 3))
# Place gold in destination
vg.set(11, 1, 11, gold) # Where air hole will paste
vg.set(12, 2, 12, gold) # Where another air hole will paste
# Paste with skip_air=True (default)
vg.paste_region(prefab, (10, 0, 10), skip_air=True)
# Gold should still be there (air didn't overwrite)
test("Skip air: preserves existing material", vg.get(11, 1, 11) == gold)
test("Skip air: preserves at other location", vg.get(12, 2, 12) == gold)
def test_copy_paste_overwrite():
"""paste_region with skip_air=False overwrites"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(20, 10, 20))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
gold = vg.add_material("gold", color=mcrfpy.Color(255, 215, 0))
# Create prefab with air gap
vg.fill_box((0, 0, 0), (3, 3, 3), stone)
vg.set(1, 1, 1, 0) # Air hole
# Copy it
prefab = vg.copy_region((0, 0, 0), (3, 3, 3))
# Clear and place gold in destination
vg.clear()
vg.set(11, 1, 11, gold)
# Paste with skip_air=False
vg.paste_region(prefab, (10, 0, 10), skip_air=False)
# Gold should be overwritten with air
test("Overwrite air: replaces existing material", vg.get(11, 1, 11) == 0)
def test_voxel_region_repr():
"""VoxelRegion has proper repr"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(10, 10, 10))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
vg.fill_box((0, 0, 0), (4, 4, 4), stone)
prefab = vg.copy_region((0, 0, 0), (4, 4, 4))
rep = repr(prefab)
test("VoxelRegion repr: contains dimensions", "5x5x5" in rep)
test("VoxelRegion repr: is VoxelRegion", "VoxelRegion" in rep)
def main():
"""Run all bulk operation tests"""
print("=" * 60)
print("VoxelGrid Bulk Operations Tests (Milestone 11)")
print("=" * 60)
print()
test_fill_box_hollow_basic()
print()
test_fill_box_hollow_thick()
print()
test_fill_sphere_volume()
print()
test_fill_sphere_carve()
print()
test_fill_cylinder_volume()
print()
test_fill_cylinder_bounds()
print()
test_fill_noise_threshold()
print()
test_fill_noise_seed()
print()
test_copy_paste_basic()
print()
test_copy_paste_skip_air()
print()
test_copy_paste_overwrite()
print()
test_voxel_region_repr()
print()
print("=" * 60)
print(f"Results: {passed} passed, {failed} failed")
print("=" * 60)
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())