using Theriapolis.Core; using Theriapolis.Core.Tactical; using Xunit; namespace Theriapolis.Tests.Tactical; /// /// Phase 4 chunk-determinism contract: /// • Same (worldSeed, ChunkCoord) twice → byte-identical chunk hash. /// • Stream cycle: generate → evict → regenerate → identical hash. /// • Different chunk coords → different hashes (no chunk-coord collision). /// public sealed class TacticalChunkDeterminismTests : IClassFixture { private const ulong TestSeed = 0xCAFEBABEUL; private readonly WorldCache _cache; public TacticalChunkDeterminismTests(WorldCache c) => _cache = c; [Theory] [InlineData(0, 0)] [InlineData(5, 7)] [InlineData(20, 30)] [InlineData(60, 60)] public void SameChunk_GeneratesIdenticalBytes(int cx, int cy) { var w = _cache.Get(TestSeed).World; var a = TacticalChunkGen.Generate(TestSeed, new ChunkCoord(cx, cy), w); var b = TacticalChunkGen.Generate(TestSeed, new ChunkCoord(cx, cy), w); Assert.Equal(a.Hash(), b.Hash()); } [Fact] public void StreamCycle_RegenerateProducesSameHash() { // Use two independent worldgen runs to avoid sharing any cached state // accidentally — each Generate call is supposed to be a pure function. var wA = _cache.Get(TestSeed, variant: 0).World; var wB = _cache.Get(TestSeed, variant: 1).World; var cc = new ChunkCoord(15, 20); var first = TacticalChunkGen.Generate(TestSeed, cc, wA); var second = TacticalChunkGen.Generate(TestSeed, cc, wB); Assert.Equal(first.Hash(), second.Hash()); } [Fact] public void DifferentCoords_DifferentHashes() { // Pick chunks that overlap a known settlement footprint so we // guarantee non-trivial content rather than picking edges that may // both be all-ocean (identical hashes are then a true positive, // not a determinism bug). var w = _cache.Get(TestSeed).World; var s = w.Settlements.First(s => !s.IsPoi && s.Tier <= 3); var anchor = ChunkCoord.ForWorldTile(s.TileX, s.TileY); var a = TacticalChunkGen.Generate(TestSeed, anchor, w); var b = TacticalChunkGen.Generate(TestSeed, new ChunkCoord(anchor.X + 4, anchor.Y), w); var c = TacticalChunkGen.Generate(TestSeed, new ChunkCoord(anchor.X, anchor.Y + 4), w); Assert.NotEqual(a.Hash(), b.Hash()); Assert.NotEqual(a.Hash(), c.Hash()); Assert.NotEqual(b.Hash(), c.Hash()); } [Fact] public void DifferentSeeds_DifferentHashes() { var wA = _cache.Get(TestSeed).World; var wB = _cache.Get(TestSeed + 1).World; var sA = wA.Settlements.First(s => !s.IsPoi && s.Tier <= 3); var anchor = ChunkCoord.ForWorldTile(sA.TileX, sA.TileY); var a = TacticalChunkGen.Generate(TestSeed, anchor, wA); var b = TacticalChunkGen.Generate(TestSeed + 1, anchor, wB); Assert.NotEqual(a.Hash(), b.Hash()); } [Fact] public void Chunk_HasExpectedDimensions() { var w = _cache.Get(TestSeed).World; var chunk = TacticalChunkGen.Generate(TestSeed, new ChunkCoord(0, 0), w); Assert.Equal(C.TACTICAL_CHUNK_SIZE, chunk.Tiles.GetLength(0)); Assert.Equal(C.TACTICAL_CHUNK_SIZE, chunk.Tiles.GetLength(1)); } }