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

173 lines
6.3 KiB
C#
Raw Normal View History

using Theriapolis.Core;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Generation;
using Xunit;
namespace Theriapolis.Tests.Worldgen;
/// <summary>
/// Settlement placement correctness: narrative anchors, tier counts, distances,
/// reachability, and no overlaps.
/// </summary>
public sealed class SettlementTests : IClassFixture<WorldCache>
{
private const ulong TestSeed = 0xCAFEBABEUL;
private readonly WorldCache _cache;
public SettlementTests(WorldCache cache) => _cache = cache;
// ── Narrative anchors ─────────────────────────────────────────────────────
[Fact]
public void SanctumFidelis_IsPresent()
{
var ctx = _cache.Get(TestSeed);
var capital = ctx.World.Settlements
.FirstOrDefault(s => s.Anchor == NarrativeAnchor.SanctumFidelis);
Assert.NotNull(capital);
Assert.Equal(1, capital!.Tier);
}
[Fact]
public void AllNarrativeAnchors_ArePlaced()
{
var ctx = _cache.Get(TestSeed);
var anchors = ctx.World.Settlements
.Where(s => s.Anchor.HasValue)
.Select(s => s.Anchor!.Value)
.ToHashSet();
foreach (NarrativeAnchor anchor in Enum.GetValues<NarrativeAnchor>())
Assert.Contains(anchor, anchors);
}
[Theory]
[InlineData(0xCAFEBABEUL)]
[InlineData(0x11111111UL)]
[InlineData(0x99887766UL)]
[InlineData(0xABCDEF01UL)]
[InlineData(0x00000042UL)]
public void NarrativeAnchors_PlacedAcrossMultipleSeeds(ulong seed)
{
var ctx = _cache.Get(seed);
var capital = ctx.World.Settlements
.FirstOrDefault(s => s.Anchor == NarrativeAnchor.SanctumFidelis);
Assert.NotNull(capital);
}
// ── Tier counts ───────────────────────────────────────────────────────────
[Fact]
public void TierCounts_MeetMinimums()
{
var ctx = _cache.Get(TestSeed);
var ss = ctx.World.Settlements.Where(s => !s.IsPoi).ToList();
int tier1 = ss.Count(s => s.Tier == 1);
int tier2 = ss.Count(s => s.Tier == 2);
int tier3 = ss.Count(s => s.Tier == 3);
int tier4 = ss.Count(s => s.Tier == 4);
Assert.Equal(1, tier1); // exactly one capital
Assert.InRange(tier2, C.SETTLE_TIER2_MIN, C.SETTLE_TIER2_MAX);
Assert.InRange(tier3, C.SETTLE_TIER3_MIN, C.SETTLE_TIER3_MAX);
Assert.True(tier4 >= C.SETTLE_TIER4_MIN,
$"Only {tier4} tier-4 settlements, need at least {C.SETTLE_TIER4_MIN}");
}
[Fact]
public void PoICount_MeetsMinimum()
{
var ctx = _cache.Get(TestSeed);
int pois = ctx.World.Settlements.Count(s => s.IsPoi);
Assert.True(pois >= C.SETTLE_TIER5_MIN,
$"Only {pois} PoIs, need at least {C.SETTLE_TIER5_MIN}");
}
// ── No overlapping settlements ─────────────────────────────────────────────
[Fact]
public void NoSettlements_ShareTheSameTile()
{
var ctx = _cache.Get(TestSeed);
var positions = new HashSet<(int, int)>();
foreach (var s in ctx.World.Settlements)
{
bool added = positions.Add((s.TileX, s.TileY));
Assert.True(added,
$"Two settlements share tile ({s.TileX},{s.TileY})");
}
}
// ── Minimum separation ─────────────────────────────────────────────────────
[Fact]
public void Tier1And2Settlements_MeetMinimumDistance()
{
var ctx = _cache.Get(TestSeed);
var high = ctx.World.Settlements.Where(s => s.Tier <= 2 && !s.IsPoi).ToList();
int minSq = C.SETTLE_MIN_DIST_TIER2 * C.SETTLE_MIN_DIST_TIER2;
for (int i = 0; i < high.Count; i++)
for (int j = i + 1; j < high.Count; j++)
{
int dx = high[i].TileX - high[j].TileX;
int dy = high[i].TileY - high[j].TileY;
Assert.True(dx * dx + dy * dy >= minSq,
$"{high[i].Name} and {high[j].Name} are too close ({Math.Sqrt(dx*dx+dy*dy):F0} tiles, min {C.SETTLE_MIN_DIST_TIER2})");
}
}
// ── Settlement attributes ─────────────────────────────────────────────────
[Fact]
public void AllSettlements_HaveNames()
{
var ctx = _cache.Get(TestSeed);
foreach (var s in ctx.World.Settlements.Where(s => !s.IsPoi))
Assert.False(string.IsNullOrWhiteSpace(s.Name),
$"Settlement at ({s.TileX},{s.TileY}) Tier {s.Tier} has no name");
}
[Fact]
public void AllSettlements_HaveValidTileCoordinates()
{
var ctx = _cache.Get(TestSeed);
int W = C.WORLD_WIDTH_TILES;
int H = C.WORLD_HEIGHT_TILES;
foreach (var s in ctx.World.Settlements)
{
Assert.InRange(s.TileX, 0, W - 1);
Assert.InRange(s.TileY, 0, H - 1);
}
}
[Fact]
public void NoSettlement_PlacedOnOcean()
{
var ctx = _cache.Get(TestSeed);
foreach (var s in ctx.World.Settlements)
{
var biome = ctx.World.Tiles[s.TileX, s.TileY].Biome;
Assert.NotEqual(BiomeId.Ocean, biome);
}
}
// ── Faction influence ─────────────────────────────────────────────────────
[Fact]
public void FactionInfluence_IsComputed()
{
var ctx = _cache.Get(TestSeed);
Assert.NotNull(ctx.World.FactionInfluence);
// Capital area should have high Enforcer influence
var capital = ctx.World.Settlements
.First(s => s.Anchor == NarrativeAnchor.SanctumFidelis);
float enforcer = ctx.World.FactionInfluence!
.Get((int)FactionId.CovenantEnforcers, capital.TileX, capital.TileY);
Assert.True(enforcer > 0.5f,
$"Enforcer influence at capital is only {enforcer:F3}");
}
}