203 lines
6.7 KiB
C#
203 lines
6.7 KiB
C#
using OpenTK.Mathematics;
|
|
using VoxelEngine.Core;
|
|
using VoxelEngine.World.Blocks;
|
|
using VoxelEngine.World.Chunk;
|
|
using VoxelEngine.World.WorldGen;
|
|
using Xunit;
|
|
|
|
namespace VoxelEngine.Tests.WorldGen;
|
|
|
|
public class CaveCarverTests
|
|
{
|
|
private static readonly BlockRegistry Blocks = new();
|
|
private static readonly Configuration Cfg = new();
|
|
|
|
private static Chunk MakeStoneChunk()
|
|
{
|
|
Chunk c = new();
|
|
for (int x = 0; x < Chunk.Size; x++)
|
|
for (int y = 0; y < Chunk.Size; y++)
|
|
for (int z = 0; z < Chunk.Size; z++)
|
|
c[x, y, z] = Blocks.Stone.Id;
|
|
return c;
|
|
}
|
|
|
|
[Fact]
|
|
public void Carve_IsNoOpOnAirChunk()
|
|
{
|
|
var carver = new CaveCarver(seed: 42, Blocks, Cfg);
|
|
Chunk c = new(); // all-air by default
|
|
|
|
carver.Carve(c, new Vector3i(0, 4, 0));
|
|
|
|
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(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?");
|
|
}
|
|
|
|
[Fact]
|
|
public void WormCaves_IsDeterministic_ForSameSeedAndChunkPos()
|
|
{
|
|
var c1 = new CaveCarver(seed: 999, Blocks, Cfg);
|
|
var c2 = new CaveCarver(seed: 999, Blocks, Cfg);
|
|
Chunk a = MakeStoneChunk();
|
|
Chunk b = MakeStoneChunk();
|
|
|
|
c1.CarveWormCaves(a, new Vector3i(0, 4, 0));
|
|
c2.CarveWormCaves(b, new Vector3i(0, 4, 0));
|
|
|
|
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 WormCaves_OnlyConvertsSolidToAir_NeverWater()
|
|
{
|
|
var carver = new CaveCarver(seed: 999, Blocks, Cfg);
|
|
Chunk c = MakeStoneChunk();
|
|
// Set every voxel along a diagonal to water — must survive.
|
|
for (int i = 0; i < Chunk.Size; i++)
|
|
c[i, i, i] = Blocks.Water.Id;
|
|
|
|
carver.CarveWormCaves(c, new Vector3i(0, 4, 0));
|
|
|
|
for (int i = 0; i < Chunk.Size; i++)
|
|
Assert.Equal(Blocks.Water.Id, c[i, i, i]);
|
|
}
|
|
|
|
[Fact]
|
|
public void WormCaves_RespectsBedrockFloor()
|
|
{
|
|
var carver = new CaveCarver(seed: 999, Blocks, Cfg);
|
|
Chunk c = MakeStoneChunk();
|
|
|
|
carver.CarveWormCaves(c, new Vector3i(0, 0, 0)); // worldY = 0..15
|
|
|
|
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]); // bedrock untouched
|
|
}
|
|
|
|
[Fact]
|
|
public void WormCaves_SeamContinuity_VoxelAtSharedWorldCoord_IsSame()
|
|
{
|
|
var carver = new CaveCarver(seed: 999, Blocks, Cfg);
|
|
Chunk c1 = MakeStoneChunk();
|
|
Chunk c2 = MakeStoneChunk();
|
|
|
|
carver.CarveWormCaves(c1, new Vector3i(0, 4, 0));
|
|
carver.CarveWormCaves(c2, new Vector3i(1, 4, 0));
|
|
|
|
int airC1Seam = 0, airC2Seam = 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) airC1Seam++;
|
|
if (c2[0, y, z] == Blocks.Air.Id) airC2Seam++;
|
|
}
|
|
bool bothEmpty = airC1Seam == 0 && airC2Seam == 0;
|
|
bool bothCarved = airC1Seam > 0 && airC2Seam > 0;
|
|
Assert.True(bothEmpty || bothCarved,
|
|
$"seam mismatch: c1={airC1Seam}, c2={airC2Seam}");
|
|
}
|
|
|
|
[Fact]
|
|
public void Carve_RespectsMinCaveY_NoCarvingAboveCeiling()
|
|
{
|
|
var cfg = new Configuration { MinCaveY = 20 };
|
|
var carver = new CaveCarver(seed: 42, Blocks, cfg);
|
|
Chunk c = MakeStoneChunk();
|
|
|
|
// Chunk Y=2 → worldY 32..47, all above MinCaveY=20 → must stay solid.
|
|
carver.Carve(c, new Vector3i(0, 2, 0));
|
|
|
|
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(Blocks.Stone.Id, c[x, y, z]);
|
|
}
|
|
}
|