Files
mrgameeng/tests/MrGameEng.Simulation.Tests/WorldGen/WorldGenTests.cs
T
Leonid PershinandClaude Opus 4.8 53659450a6
CI / build-test (push) Successful in 1m14s
Add MrGameEng.WorldGen: deterministic procedural world generation
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>
2026-06-12 09:23:45 +03:00

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);
}
}