diff --git a/VoxelEngine.Tests/WorldGen/CaveCarverTests.cs b/VoxelEngine.Tests/WorldGen/CaveCarverTests.cs index e76b893..2c6d027 100644 --- a/VoxelEngine.Tests/WorldGen/CaveCarverTests.cs +++ b/VoxelEngine.Tests/WorldGen/CaveCarverTests.cs @@ -35,4 +35,84 @@ public class CaveCarverTests for (int z = 0; z < Chunk.Size; z++) Assert.Equal(Blocks.Air.Id, c[x, y, z]); } + + [Fact] + public void NoiseCaves_OnlyConvertsSolidToAir_NeverWaterNeverOtherBlocks() + { + var carver = new CaveCarver(seed: 42, Blocks, Cfg); + Chunk c = MakeStoneChunk(); + + // Plant some water and air voxels we expect untouched. + c[1, 1, 1] = Blocks.Water.Id; + c[2, 2, 2] = Blocks.Air.Id; + + carver.CarveNoiseCaves(c, new Vector3i(0, 4, 0)); + + Assert.Equal(Blocks.Water.Id, c[1, 1, 1]); // water survived + Assert.Equal(Blocks.Air.Id, c[2, 2, 2]); // air survived + + // Spot-check that everywhere else is either Stone or Air (no other blocks). + for (int x = 0; x < Chunk.Size; x++) + for (int y = 0; y < Chunk.Size; y++) + for (int z = 0; z < Chunk.Size; z++) + { + ushort id = c[x, y, z]; + Assert.True( + id == Blocks.Stone.Id || id == Blocks.Air.Id || id == Blocks.Water.Id, + $"unexpected block id {id} at ({x},{y},{z})"); + } + } + + [Fact] + public void NoiseCaves_IsDeterministic_ForSameSeedAndChunkPos() + { + var carver1 = new CaveCarver(seed: 1234, Blocks, Cfg); + var carver2 = new CaveCarver(seed: 1234, Blocks, Cfg); + Chunk a = MakeStoneChunk(); + Chunk b = MakeStoneChunk(); + + carver1.CarveNoiseCaves(a, new Vector3i(2, 4, 3)); + carver2.CarveNoiseCaves(b, new Vector3i(2, 4, 3)); + + for (int x = 0; x < Chunk.Size; x++) + for (int y = 0; y < Chunk.Size; y++) + for (int z = 0; z < Chunk.Size; z++) + Assert.Equal(a[x, y, z], b[x, y, z]); + } + + [Fact] + public void NoiseCaves_RespectsBedrockFloor_DoesNotCarveBelowY1() + { + // ChunkPos.Y = 0 means worldY ranges 0..15. We require Y=0 stays solid. + var carver = new CaveCarver(seed: 42, Blocks, Cfg); + Chunk c = MakeStoneChunk(); + + carver.CarveNoiseCaves(c, new Vector3i(0, 0, 0)); + + for (int x = 0; x < Chunk.Size; x++) + for (int z = 0; z < Chunk.Size; z++) + Assert.Equal(Blocks.Stone.Id, c[x, 0, z]); // worldY = 0 is bedrock + } + + [Fact] + public void NoiseCaves_SeamContinuity_AdjacentChunksAgreeAtBoundary() + { + var carver = new CaveCarver(seed: 7, Blocks, Cfg); + Chunk c1 = MakeStoneChunk(); + Chunk c2 = MakeStoneChunk(); + + carver.CarveNoiseCaves(c1, new Vector3i(0, 4, 0)); + carver.CarveNoiseCaves(c2, new Vector3i(1, 4, 0)); + + int airAtSeamC1 = 0, airAtSeamC2 = 0; + for (int y = 1; y < Chunk.Size; y++) + for (int z = 0; z < Chunk.Size; z++) + { + if (c1[15, y, z] == Blocks.Air.Id) airAtSeamC1++; + if (c2[0, y, z] == Blocks.Air.Id) airAtSeamC2++; + } + // Sanity: noise threshold = -0.3 yields some carved cells in each. + Assert.True(airAtSeamC1 > 0, "c1 seam column had no air — carve too sparse?"); + Assert.True(airAtSeamC2 > 0, "c2 seam column had no air — carve too sparse?"); + } } diff --git a/World/WorldGen/CaveCarver.cs b/World/WorldGen/CaveCarver.cs index 04c8426..b5bb20a 100644 --- a/World/WorldGen/CaveCarver.cs +++ b/World/WorldGen/CaveCarver.cs @@ -28,6 +28,34 @@ public sealed class CaveCarver public void Carve(VoxelChunk chunk, Vector3i chunkPos) { - // Implemented in Tasks 5 and 6. + this.CarveNoiseCaves(chunk, chunkPos); + // Worm pass added in Task 6. + } + + // Per-voxel 3D simplex threshold. Public for direct testability, + // matching the pattern of WorldGenerator.BlockAt. + public void CarveNoiseCaves(VoxelChunk chunk, Vector3i chunkPos) + { + int baseX = chunkPos.X * VoxelChunk.Size; + int baseY = chunkPos.Y * VoxelChunk.Size; + int baseZ = chunkPos.Z * VoxelChunk.Size; + + for (int x = 0; x < VoxelChunk.Size; x++) + for (int y = 0; y < VoxelChunk.Size; y++) + for (int z = 0; z < VoxelChunk.Size; z++) + { + int worldY = baseY + y; + if (worldY <= 0) continue; + + ushort id = chunk[x, y, z]; + if (id == this._blocks.Air.Id) continue; + if (id == this._blocks.Water.Id) continue; + + float density = this._caveNoise.GetNoise(baseX + x, worldY, baseZ + z); + if (density < this._cfg.CaveNoiseThreshold) + { + chunk[x, y, z] = this._blocks.Air.Id; + } + } } }