using Theriapolis.Core; using Theriapolis.Core.World; using Theriapolis.Core.World.Generation; using Xunit; namespace Theriapolis.Tests.Worldgen; /// /// Linear feature exclusion (Addendum A §2): no parallel river+rail or rail+road /// on the same non-settlement tile. Road+river is allowed — it represents a /// bridge crossing (see ValidationPassStage.CheckLinearExclusion). /// public sealed class LinearFeatureTests : IClassFixture { private readonly WorldCache _cache; public LinearFeatureTests(WorldCache cache) => _cache = cache; [Theory] [InlineData(0xCAFEBABEUL)] [InlineData(0x11223344UL)] [InlineData(0xDEADBEEFUL)] [InlineData(0xFEEDFACEUL)] [InlineData(0xABCDEF00UL)] [InlineData(0x00112233UL)] [InlineData(0x99AABBCCUL)] [InlineData(0x12345678UL)] [InlineData(0x87654321UL)] [InlineData(0x0DEADC0DUL)] public void NoParallelLinearFeatures_OnNonSettlementTiles(ulong seed) { var ctx = _cache.Get(seed); var world = ctx.World; int W = C.WORLD_WIDTH_TILES; int H = C.WORLD_HEIGHT_TILES; int violations = 0; for (int y = 0; y < H; y++) for (int x = 0; x < W; x++) { ref var tile = ref world.TileAt(x, y); if ((tile.Features & FeatureFlags.IsSettlement) != 0) continue; bool hasRiver = (tile.Features & FeatureFlags.HasRiver) != 0; bool hasRail = (tile.Features & FeatureFlags.HasRail) != 0; bool hasRoad = (tile.Features & FeatureFlags.HasRoad) != 0; // River + Rail parallel if (hasRiver && hasRail && tile.RiverFlowDir != Theriapolis.Core.Util.Dir.None && tile.RailDir != Theriapolis.Core.Util.Dir.None && Theriapolis.Core.Util.Dir.IsParallel(tile.RiverFlowDir, tile.RailDir)) violations++; // Rail + Road (always a violation outside settlements) if (hasRail && hasRoad) violations++; } Assert.Equal(0, violations); } [Fact] public void RoadNetwork_HasAtLeastOneRoad() { var ctx = _cache.Get(0xCAFEBABEUL); Assert.NotEmpty(ctx.World.Roads); } [Fact] public void RailNetwork_HasAtLeastOneRailLine() { if (!C.ENABLE_RAIL) return; // rail subsystem disabled; nothing to assert var ctx = _cache.Get(0xCAFEBABEUL); Assert.NotEmpty(ctx.World.Rails); } [Fact] public void Roads_AreInWorldPixelSpace() { var ctx = _cache.Get(0xCAFEBABEUL); float maxCoord = C.WORLD_WIDTH_TILES * C.WORLD_TILE_PIXELS; foreach (var road in ctx.World.Roads) foreach (var pt in road.Points) { Assert.InRange(pt.X, 0f, maxCoord); Assert.InRange(pt.Y, 0f, maxCoord); } } [Fact] public void ValidationPassHash_RecordsZeroViolations() { var ctx = _cache.Get(0xCAFEBABEUL); if (!ctx.World.StageHashes.TryGetValue("ValidationPass", out ulong vhash)) return; // stage didn't record hash — skip int violations = (int)(vhash / 1000); Assert.Equal(0, violations); } }