Files
TheriapolisV3/Theriapolis.Tests/Worldgen/HydrologyTests.cs
T

105 lines
3.6 KiB
C#
Raw Normal View History

using Theriapolis.Core;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Generation;
using Theriapolis.Core.World.Polylines;
using Xunit;
namespace Theriapolis.Tests.Worldgen;
/// <summary>
/// Hydrology correctness: rivers must be generated, endpoints must reach water.
/// </summary>
public sealed class HydrologyTests : IClassFixture<WorldCache>
{
private const ulong TestSeed = 0xCAFEBABEUL;
// HydrologyGen is stage 10 → 0-based index 9 (fast-path for hydrology-only tests).
private const int HydrologyStageIndex = 9;
private readonly WorldCache _cache;
public HydrologyTests(WorldCache cache) => _cache = cache;
[Fact]
public void Pipeline_GeneratesAtLeastOneRiver()
{
var ctx = _cache.GetThrough(TestSeed, HydrologyStageIndex);
Assert.NotEmpty(ctx.World.Rivers);
}
[Fact]
public void AllRivers_HaveAtLeastTwoPoints()
{
var ctx = _cache.GetThrough(TestSeed, HydrologyStageIndex);
foreach (var river in ctx.World.Rivers)
Assert.True(river.Points.Count >= 2,
$"River {river.Id} has fewer than 2 points");
}
[Fact]
public void RiverPolylines_AreInWorldPixelSpace()
{
var ctx = _cache.GetThrough(TestSeed, HydrologyStageIndex);
float maxCoord = C.WORLD_WIDTH_TILES * C.WORLD_TILE_PIXELS;
foreach (var river in ctx.World.Rivers)
foreach (var pt in river.Points)
{
Assert.True(pt.X >= 0 && pt.X <= maxCoord,
$"River {river.Id} point X={pt.X} out of world-pixel range");
Assert.True(pt.Y >= 0 && pt.Y <= maxCoord,
$"River {river.Id} point Y={pt.Y} out of world-pixel range");
}
}
[Theory]
[InlineData(0xCAFEBABEUL)]
[InlineData(0x12345678UL)]
[InlineData(0xDEADBEEFUL)]
public void RiverEndpoints_DrainToWaterOrWorldEdge(ulong seed)
{
var ctx = _cache.GetThrough(seed, HydrologyStageIndex);
var world = ctx.World;
int W = C.WORLD_WIDTH_TILES;
int H = C.WORLD_HEIGHT_TILES;
int nonDrainingCount = 0;
foreach (var river in world.Rivers)
{
if (river.Points.Count < 2) continue;
var last = river.Points[^1];
int lx = Math.Clamp((int)(last.X / C.WORLD_TILE_PIXELS), 0, W - 1);
int ly = Math.Clamp((int)(last.Y / C.WORLD_TILE_PIXELS), 0, H - 1);
var biome = world.Tiles[lx, ly].Biome;
bool atWater = biome == BiomeId.Ocean || biome == BiomeId.Wetland ||
(world.Tiles[lx, ly].Features & FeatureFlags.HasRiver) != 0;
bool atEdge = lx == 0 || ly == 0 || lx == W - 1 || ly == H - 1;
if (!atWater && !atEdge)
nonDrainingCount++;
}
// Allow up to 10% non-draining rivers (noise from complex terrain)
double failRate = world.Rivers.Count > 0
? (double)nonDrainingCount / world.Rivers.Count
: 0.0;
Assert.True(failRate <= 0.10,
$"Seed {seed:X}: {nonDrainingCount}/{world.Rivers.Count} rivers do not drain to water ({failRate:P0})");
}
[Fact]
public void EncounterDensityMap_IsPopulated()
{
var ctx = _cache.Get(TestSeed);
Assert.NotNull(ctx.World.EncounterDensity);
float sum = 0;
int W = C.WORLD_WIDTH_TILES;
int H = C.WORLD_HEIGHT_TILES;
for (int y = 0; y < H; y++)
for (int x = 0; x < W; x++)
sum += ctx.World.EncounterDensity![x, y];
Assert.True(sum > 0, "EncounterDensity map is all zeros");
}
}