b451f83174
Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
105 lines
3.6 KiB
C#
105 lines
3.6 KiB
C#
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");
|
|
}
|
|
}
|