#!/usr/bin/env python3 """Unit tests for VoxelGrid mesh generation (Milestone 10) Tests: - Single voxel produces 36 vertices (6 faces x 6 verts) - Two adjacent voxels share a face (60 verts instead of 72) - Hollow cube only has outer faces - fill_box works correctly - Mesh dirty flag triggers rebuild - Vertex positions are in correct local space """ import sys # 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_single_voxel(): """Single voxel should produce 6 faces = 36 vertices""" import mcrfpy vg = mcrfpy.VoxelGrid(size=(8, 8, 8)) stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128)) # Initially no vertices (empty grid) test("Single voxel: initial vertex_count is 0", vg.vertex_count == 0) # Add one voxel vg.set(4, 4, 4, stone) vg.rebuild_mesh() # One voxel = 6 faces, each face = 2 triangles = 6 vertices expected = 6 * 6 test("Single voxel: produces 36 vertices", vg.vertex_count == expected, f"got {vg.vertex_count}, expected {expected}") def test_two_adjacent(): """Two adjacent voxels should share a face, producing 60 vertices instead of 72""" import mcrfpy vg = mcrfpy.VoxelGrid(size=(8, 8, 8)) stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128)) # Add two adjacent voxels (share one face) vg.set(4, 4, 4, stone) vg.set(5, 4, 4, stone) # Adjacent in X vg.rebuild_mesh() # Two separate voxels would be 72 vertices # Shared face is culled: 2 * 36 - 2 * 6 = 72 - 12 = 60 expected = 60 test("Two adjacent: shared face culled", vg.vertex_count == expected, f"got {vg.vertex_count}, expected {expected}") def test_hollow_cube(): """Hollow 3x3x3 cube should have much fewer vertices than solid""" import mcrfpy vg = mcrfpy.VoxelGrid(size=(8, 8, 8)) stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128)) # Create hollow 3x3x3 cube (only shell voxels) # Solid 3x3x3 = 27 voxels, Hollow = 26 voxels (remove center) for x in range(3): for y in range(3): for z in range(3): # Skip center voxel if x == 1 and y == 1 and z == 1: continue vg.set(x, y, z, stone) test("Hollow cube: 26 voxels placed", vg.count_non_air() == 26) vg.rebuild_mesh() # The hollow center creates inner faces facing the air void # Outer surface = 6 sides * 9 faces = 54 faces # Inner surface = 6 faces touching the center void # Total = 60 faces = 360 vertices expected = 360 test("Hollow cube: outer + inner void faces", vg.vertex_count == expected, f"got {vg.vertex_count}, expected {expected}") def test_fill_box(): """fill_box should fill a rectangular region""" import mcrfpy vg = mcrfpy.VoxelGrid(size=(16, 8, 16)) stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128)) # Fill a 4x3x5 box vg.fill_box((2, 1, 3), (5, 3, 7), stone) # Count: (5-2+1) * (3-1+1) * (7-3+1) = 4 * 3 * 5 = 60 expected = 60 test("fill_box: correct voxel count", vg.count_non_air() == expected, f"got {vg.count_non_air()}, expected {expected}") # Verify specific cells test("fill_box: corner (2,1,3) is filled", vg.get(2, 1, 3) == stone) test("fill_box: corner (5,3,7) is filled", vg.get(5, 3, 7) == stone) test("fill_box: outside (1,1,3) is empty", vg.get(1, 1, 3) == 0) test("fill_box: outside (6,1,3) is empty", vg.get(6, 1, 3) == 0) def test_fill_box_reversed(): """fill_box should handle reversed coordinates""" import mcrfpy vg = mcrfpy.VoxelGrid(size=(16, 8, 16)) stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128)) # Fill with reversed coordinates (max before min) vg.fill_box((5, 3, 7), (2, 1, 3), stone) # Should still fill 4x3x5 = 60 voxels expected = 60 test("fill_box reversed: correct voxel count", vg.count_non_air() == expected, f"got {vg.count_non_air()}, expected {expected}") def test_fill_box_clamping(): """fill_box should clamp to grid bounds""" import mcrfpy vg = mcrfpy.VoxelGrid(size=(8, 8, 8)) stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128)) # Fill beyond grid bounds vg.fill_box((-5, -5, -5), (100, 100, 100), stone) # Should fill entire 8x8x8 grid = 512 voxels expected = 512 test("fill_box clamping: fills entire grid", vg.count_non_air() == expected, f"got {vg.count_non_air()}, expected {expected}") def test_mesh_dirty(): """Modifying voxels should mark mesh dirty; rebuild_mesh updates vertex count""" import mcrfpy vg = mcrfpy.VoxelGrid(size=(8, 8, 8)) stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128)) # Initial state vg.set(4, 4, 4, stone) vg.rebuild_mesh() initial_count = vg.vertex_count test("Mesh dirty: initial vertex count correct", initial_count == 36) # Modify voxel - marks dirty but doesn't auto-rebuild vg.set(4, 4, 5, stone) # vertex_count doesn't auto-trigger rebuild (returns stale value) stale_count = vg.vertex_count test("Mesh dirty: vertex_count before rebuild is stale", stale_count == 36) # Explicit rebuild updates the mesh vg.rebuild_mesh() new_count = vg.vertex_count # Two adjacent voxels = 60 vertices test("Mesh dirty: rebuilt after explicit rebuild_mesh()", new_count == 60, f"got {new_count}, expected 60") def test_vertex_positions(): """Vertices should be in correct local space positions""" import mcrfpy vg = mcrfpy.VoxelGrid(size=(8, 8, 8), cell_size=2.0) stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128)) # Place voxel at (0,0,0) vg.set(0, 0, 0, stone) vg.rebuild_mesh() # With cell_size=2.0, the voxel center is at (1, 1, 1) # Vertices should be at corners: (0,0,0) to (2,2,2) # The vertex_count should still be 36 test("Vertex positions: correct vertex count", vg.vertex_count == 36) def test_empty_grid(): """Empty grid should produce no vertices""" import mcrfpy vg = mcrfpy.VoxelGrid(size=(8, 8, 8)) vg.rebuild_mesh() test("Empty grid: zero vertices", vg.vertex_count == 0) def test_all_air(): """Grid filled with air produces no vertices""" import mcrfpy vg = mcrfpy.VoxelGrid(size=(8, 8, 8)) stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128)) # Fill with stone, then fill with air vg.fill(stone) vg.fill(0) # Air vg.rebuild_mesh() test("All air: zero vertices", vg.vertex_count == 0) def test_large_solid_cube(): """Large solid cube should have face culling efficiency""" import mcrfpy vg = mcrfpy.VoxelGrid(size=(8, 8, 8)) stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128)) # Fill entire grid vg.fill(stone) vg.rebuild_mesh() # Without culling: 512 voxels * 6 faces * 6 verts = 18432 # With culling: only outer shell faces # 6 faces of cube, each 8x8 = 64 faces per side = 384 faces # 384 * 6 verts = 2304 vertices expected = 2304 test("Large solid cube: face culling efficiency", vg.vertex_count == expected, f"got {vg.vertex_count}, expected {expected}") # Verify massive reduction no_cull = 512 * 6 * 6 reduction = (no_cull - vg.vertex_count) / no_cull * 100 test("Large solid cube: >85% vertex reduction", reduction > 85, f"got {reduction:.1f}% reduction") def test_transparent_material(): """Faces between solid and transparent materials should be generated""" import mcrfpy vg = mcrfpy.VoxelGrid(size=(8, 8, 8)) stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128)) glass = vg.add_material("glass", color=mcrfpy.Color(200, 200, 255, 128), transparent=True) # Place stone with glass neighbor vg.set(4, 4, 4, stone) vg.set(5, 4, 4, glass) vg.rebuild_mesh() # Stone has 6 faces (all exposed - glass is transparent) # Glass has 5 faces (face towards stone not generated - stone is solid) # Total = 36 + 30 = 66 vertices expected = 66 test("Transparent material: correct face culling", vg.vertex_count == expected, f"got {vg.vertex_count}, expected {expected}") def main(): """Run all mesh generation tests""" print("=" * 60) print("VoxelGrid Mesh Generation Tests (Milestone 10)") print("=" * 60) print() test_single_voxel() print() test_two_adjacent() print() test_hollow_cube() print() test_fill_box() print() test_fill_box_reversed() print() test_fill_box_clamping() print() test_mesh_dirty() print() test_vertex_positions() print() test_empty_grid() print() test_all_air() print() test_large_solid_cube() print() test_transparent_material() 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())