CI / build-test (push) Successful in 1m14s
Introduce a WorldGen module under Simulation with reusable helpers for seed-based terrain heightmaps: PerlinNoise (deterministic 2D gradient noise), FractalNoise (fBm over octaves), Falloff (island edge masks), Heightmap (row-major grid with min-max normalize) and HeightmapGenerator (settings to normalized heightmap). All deterministic from an integer seed, allocation-free per sample, covered by xUnit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
125 lines
3.4 KiB
C#
125 lines
3.4 KiB
C#
using MrGameEng.WorldGen;
|
|
using Xunit;
|
|
|
|
namespace MrGameEng.WorldGen.Tests;
|
|
|
|
public class WorldGenTests
|
|
{
|
|
private static HeightmapSettings Settings(int seed) =>
|
|
HeightmapSettings.Default with
|
|
{
|
|
Seed = seed,
|
|
};
|
|
|
|
[Fact]
|
|
public void Generate_SameSeed_ProducesIdenticalMap()
|
|
{
|
|
var a = new HeightmapGenerator(Settings(1234)).Generate(64, 48);
|
|
var b = new HeightmapGenerator(Settings(1234)).Generate(64, 48);
|
|
|
|
Assert.Equal(a.Values, b.Values);
|
|
}
|
|
|
|
[Fact]
|
|
public void Generate_DifferentSeeds_ProduceDifferentMaps()
|
|
{
|
|
var a = new HeightmapGenerator(Settings(1)).Generate(64, 48);
|
|
var b = new HeightmapGenerator(Settings(2)).Generate(64, 48);
|
|
|
|
Assert.NotEqual(a.Values, b.Values);
|
|
}
|
|
|
|
[Fact]
|
|
public void Generate_AfterNormalize_ValuesSpanUnitRange()
|
|
{
|
|
var map = new HeightmapGenerator(Settings(7)).Generate(80, 60);
|
|
|
|
var min = float.MaxValue;
|
|
var max = float.MinValue;
|
|
foreach (var v in map.Values)
|
|
{
|
|
Assert.InRange(v, 0f, 1f);
|
|
min = MathF.Min(min, v);
|
|
max = MathF.Max(max, v);
|
|
}
|
|
|
|
Assert.Equal(0f, min, 3);
|
|
Assert.Equal(1f, max, 3);
|
|
}
|
|
|
|
[Fact]
|
|
public void Generate_WithIsland_EdgesAreLowerThanCentre()
|
|
{
|
|
var settings = HeightmapSettings.Default with { Seed = 42, IslandStrength = 1f };
|
|
var map = new HeightmapGenerator(settings).Generate(64, 64);
|
|
|
|
// Average the border ring vs. a centred block.
|
|
var edge = 0f;
|
|
var edgeCount = 0;
|
|
for (var x = 0; x < map.Width; x++)
|
|
{
|
|
edge += map[x, 0] + map[x, map.Height - 1];
|
|
edgeCount += 2;
|
|
}
|
|
|
|
var centre = 0f;
|
|
var centreCount = 0;
|
|
for (var y = map.Height / 2 - 4; y < map.Height / 2 + 4; y++)
|
|
{
|
|
for (var x = map.Width / 2 - 4; x < map.Width / 2 + 4; x++)
|
|
{
|
|
centre += map[x, y];
|
|
centreCount++;
|
|
}
|
|
}
|
|
|
|
Assert.True(
|
|
edge / edgeCount < centre / centreCount,
|
|
"island edges should sit below centre"
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void PerlinSample_StaysWithinUnitRange()
|
|
{
|
|
var noise = new PerlinNoise(99);
|
|
for (var i = 0; i < 2000; i++)
|
|
{
|
|
var v = noise.Sample(i * 0.37f, i * 0.19f);
|
|
Assert.InRange(v, -1f, 1f);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void PerlinSample_IsContinuousBetweenNeighbours()
|
|
{
|
|
var noise = new PerlinNoise(5);
|
|
var previous = noise.Sample(0f, 0f);
|
|
for (var i = 1; i < 500; i++)
|
|
{
|
|
var current = noise.Sample(i * 0.01f, 0f);
|
|
Assert.True(
|
|
MathF.Abs(current - previous) < 0.2f,
|
|
"noise jumped between adjacent samples"
|
|
);
|
|
previous = current;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void PerlinSample_AtIntegerLattice_IsZero()
|
|
{
|
|
var noise = new PerlinNoise(3);
|
|
Assert.Equal(0f, noise.Sample(4f, 7f), 5);
|
|
}
|
|
|
|
[Fact]
|
|
public void IslandFalloff_CentreIsHigherThanEdge()
|
|
{
|
|
Assert.True(Falloff.Island(0f, 0f) > Falloff.Island(1f, 0f));
|
|
Assert.True(Falloff.Island(0f, 0f) > Falloff.Island(0f, 1f));
|
|
Assert.Equal(1f, Falloff.Island(0f, 0f), 5);
|
|
Assert.Equal(0f, Falloff.Island(1f, 1f), 5);
|
|
}
|
|
}
|