Files
Christopher Wiebe b451f83174 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>
2026-04-30 20:40:51 -07:00

94 lines
3.4 KiB
C#

using Theriapolis.Core;
using Theriapolis.Core.Persistence;
using Theriapolis.Core.Tactical;
using Theriapolis.Core.Util;
using Theriapolis.Core.World;
using Xunit;
namespace Theriapolis.Tests.Persistence;
/// <summary>
/// End-to-end M5 smoke test:
/// 1. Generate a chunk via the streamer.
/// 2. Mutate a tile (chop a tree → set Deco=None).
/// 3. Flush → save → load.
/// 4. Re-stream the chunk and verify the mutation is still applied.
///
/// This is the single most important save-correctness test — exercises the
/// streamer/delta-store/codec/restore loop in one shot.
/// </summary>
public sealed class DeltaPersistenceTests : IClassFixture<WorldCache>
{
private const ulong TestSeed = 0xCAFEBABEUL;
private readonly WorldCache _cache;
public DeltaPersistenceTests(WorldCache c) => _cache = c;
[Fact]
public void ChopTree_PersistsAcrossSaveLoad()
{
var w = _cache.Get(TestSeed).World;
// Find a chunk + tile that the baseline generator put a tree on.
var (cc, lx, ly) = FindTreeTile(w);
var deltasA = new InMemoryChunkDeltaStore();
var streamerA = new ChunkStreamer(TestSeed, w, deltasA);
var chunk = streamerA.Get(cc);
Assert.Equal(TacticalDeco.Tree, chunk.Tiles[lx, ly].Deco);
// Chop it.
chunk.Tiles[lx, ly].Deco = TacticalDeco.None;
chunk.HasDelta = true;
// Flush + save.
streamerA.FlushAll();
var header = new SaveHeader { WorldSeedHex = $"0x{TestSeed:X}" };
var body = new SaveBody { Clock = new() { InGameSeconds = 0 } };
body.Player = new() { Name = "Tester" };
foreach (var kv in deltasA.All) body.ModifiedChunks[kv.Key] = kv.Value;
var bytes = SaveCodec.Serialize(header, body);
// ── Load on a fresh streamer ──
var (_, rb) = SaveCodec.Deserialize(bytes);
var deltasB = new InMemoryChunkDeltaStore();
foreach (var kv in rb.ModifiedChunks) deltasB.Put(kv.Key, kv.Value);
var streamerB = new ChunkStreamer(TestSeed, w, deltasB);
var reloaded = streamerB.Get(cc);
Assert.Equal(TacticalDeco.None, reloaded.Tiles[lx, ly].Deco);
}
[Fact]
public void UnmodifiedChunk_HasNoDelta()
{
var w = _cache.Get(TestSeed).World;
var deltas = new InMemoryChunkDeltaStore();
var streamer = new ChunkStreamer(TestSeed, w, deltas);
// Touch a chunk without modifying it.
var cc = new ChunkCoord(20, 20);
streamer.Get(cc);
streamer.FlushAll();
Assert.Null(deltas.Get(cc));
}
private static (ChunkCoord cc, int lx, int ly) FindTreeTile(WorldState w)
{
// Walk a band of chunks centred near a known land settlement so we
// don't waste time scanning ocean.
var anchor = w.Settlements.First(s => !s.IsPoi && s.Tier <= 3);
var centre = ChunkCoord.ForWorldTile(anchor.TileX, anchor.TileY);
for (int dy = -3; dy <= 3; dy++)
for (int dx = -3; dx <= 3; dx++)
{
var cc = new ChunkCoord(centre.X + dx, centre.Y + dy);
var chunk = TacticalChunkGen.Generate(0xCAFEBABEUL, cc, w);
for (int ly = 0; ly < C.TACTICAL_CHUNK_SIZE; ly++)
for (int lx = 0; lx < C.TACTICAL_CHUNK_SIZE; lx++)
if (chunk.Tiles[lx, ly].Deco == TacticalDeco.Tree)
return (cc, lx, ly);
}
throw new Xunit.Sdk.XunitException("no tree near settlement to chop in test");
}
}