Initial commit: Theriapolis baseline at port/godot branch point
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>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
using Theriapolis.Core;
|
||||
using Theriapolis.Core.Tactical;
|
||||
using Theriapolis.Core.Util;
|
||||
using Xunit;
|
||||
|
||||
namespace Theriapolis.Tests.Tactical;
|
||||
|
||||
/// <summary>
|
||||
/// Streamer-level invariants: caching, eviction, and delta round-trip.
|
||||
/// </summary>
|
||||
public sealed class ChunkStreamerTests : IClassFixture<WorldCache>
|
||||
{
|
||||
private const ulong TestSeed = 0xCAFEBABEUL;
|
||||
private readonly WorldCache _cache;
|
||||
public ChunkStreamerTests(WorldCache c) => _cache = c;
|
||||
|
||||
[Fact]
|
||||
public void Get_CachesSubsequentCalls()
|
||||
{
|
||||
var w = _cache.Get(TestSeed).World;
|
||||
var streamer = new ChunkStreamer(TestSeed, w, new InMemoryChunkDeltaStore());
|
||||
var cc = new ChunkCoord(3, 3);
|
||||
var first = streamer.Get(cc);
|
||||
var second = streamer.Get(cc);
|
||||
Assert.Same(first, second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureLoaded_PopulatesCacheNearPlayer()
|
||||
{
|
||||
var w = _cache.Get(TestSeed).World;
|
||||
var streamer = new ChunkStreamer(TestSeed, w, new InMemoryChunkDeltaStore());
|
||||
// Centre on world tile (50, 50) → tactical-pixel (1600, 1600).
|
||||
var pos = new Vec2(50 * C.WORLD_TILE_PIXELS, 50 * C.WORLD_TILE_PIXELS);
|
||||
streamer.EnsureLoadedAround(pos, worldTileRadius: C.TACTICAL_WINDOW_WORLD_TILES);
|
||||
Assert.NotEmpty(streamer.Loaded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeltaRoundtrip_PreservesTileEdits()
|
||||
{
|
||||
var w = _cache.Get(TestSeed).World;
|
||||
var deltas = new InMemoryChunkDeltaStore();
|
||||
var streamer = new ChunkStreamer(TestSeed, w, deltas);
|
||||
var cc = new ChunkCoord(8, 8);
|
||||
|
||||
var chunk = streamer.Get(cc);
|
||||
// Pick a known tile, edit it, mark the chunk dirty.
|
||||
ref var t = ref chunk.Tiles[10, 10];
|
||||
var origSurface = t.Surface;
|
||||
var newSurface = origSurface == TacticalSurface.Cobble ? TacticalSurface.Sand : TacticalSurface.Cobble;
|
||||
t.Surface = newSurface;
|
||||
t.Deco = TacticalDeco.None;
|
||||
chunk.HasDelta = true;
|
||||
|
||||
// Force the streamer above the cache cap so the chunk gets evicted
|
||||
// (and its delta flushed). Easiest way: load enough other chunks.
|
||||
for (int i = 0; i < C.CHUNK_CACHE_SOFT_MAX + 2; i++)
|
||||
streamer.Get(new ChunkCoord(100 + i, 100));
|
||||
streamer.EnsureLoadedAround(new Vec2(100 * C.WORLD_TILE_PIXELS, 100 * C.WORLD_TILE_PIXELS),
|
||||
worldTileRadius: 1);
|
||||
|
||||
// Now reload our edited chunk and verify the delta was reapplied.
|
||||
var reloaded = streamer.Get(cc);
|
||||
Assert.Equal(newSurface, reloaded.Tiles[10, 10].Surface);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FlushAll_PersistsModifiedChunks()
|
||||
{
|
||||
var w = _cache.Get(TestSeed).World;
|
||||
var deltas = new InMemoryChunkDeltaStore();
|
||||
var streamer = new ChunkStreamer(TestSeed, w, deltas);
|
||||
var cc = new ChunkCoord(2, 2);
|
||||
var chunk = streamer.Get(cc);
|
||||
chunk.Tiles[5, 5].Deco = TacticalDeco.Boulder;
|
||||
chunk.HasDelta = true;
|
||||
|
||||
streamer.FlushAll();
|
||||
Assert.NotNull(deltas.Get(cc));
|
||||
Assert.NotEmpty(deltas.Get(cc)!.TileMods);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using Theriapolis.Core;
|
||||
using Theriapolis.Core.Tactical;
|
||||
using Xunit;
|
||||
|
||||
namespace Theriapolis.Tests.Tactical;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public sealed class TacticalChunkDeterminismTests : IClassFixture<WorldCache>
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user