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>
173 lines
6.3 KiB
C#
173 lines
6.3 KiB
C#
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}");
|
|
}
|
|
}
|