From 8654557d9ba1c98208b67f5909f7dd7917e10d50 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Tue, 22 Sep 2026 04:37:51 +0800 Subject: test(kernel): 512x384 参数化边界存档 round-trip 与 NPC 边界回归(P23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sim.configureBounds 512 384 下 v2 存档 round-trip 且同 world 两次保存逐字节一致, 读回再保存仍与原文一致;v1 的 64x48 兼容由既有用例覆盖。 - DriverHarness 长跑 20000 tick 断言 avatar 与全部 NPC 有限且留在 [0,max] 内。 - Kernel 测试 90 -> 92。 --- src/LivingVillage.Kernel.Tests/DeterminismTests.fs | 24 ++++++++++++++++++++ src/LivingVillage.Kernel.Tests/M6aTests.fs | 26 ++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/LivingVillage.Kernel.Tests/DeterminismTests.fs b/src/LivingVillage.Kernel.Tests/DeterminismTests.fs index 52cae89..c4b4154 100644 --- a/src/LivingVillage.Kernel.Tests/DeterminismTests.fs +++ b/src/LivingVillage.Kernel.Tests/DeterminismTests.fs @@ -113,6 +113,30 @@ type DeterminismTests () = checkedTicks <- checkedTicks + 1 Assert.IsTrue(120001 = checkedTicks) + /// P23:512x384 参数化边界下,avatar 与全部 NPC 在长跑中仍留在 [0, max] 且数值有限。 + [] + member _.LargeMapBoundsClampAvatarAndNpcs () = + Sim.configureBounds 512 384 + try + let trace = DriverHarness.run 4242UL 20000L + let maxX = float32 (Sim.mapWidthTiles * Sim.tilePixels - Sim.tilePixels) + let maxY = float32 (Sim.mapHeightTiles * Sim.tilePixels - Sim.tilePixels) + Assert.IsTrue(maxX > 64.0f * float32 Sim.tilePixels, "512 map must extend past legacy width") + for w in trace do + let p = w.Avatar.Pos + if Single.IsNaN p.X || Single.IsNaN p.Y then Assert.Fail($"NaN avatar at tick {w.Tick}") + if p.X < 0.0f || p.X > maxX then Assert.Fail($"avatar X out of bounds at tick {w.Tick}: {p.X}") + if p.Y < 0.0f || p.Y > maxY then Assert.Fail($"avatar Y out of bounds at tick {w.Tick}: {p.Y}") + for npc in w.Npcs do + if Single.IsNaN npc.Pos.X || Single.IsNaN npc.Pos.Y then + Assert.Fail($"NaN npc {npc.Id} at tick {w.Tick}") + if npc.Pos.X < 0.0f || npc.Pos.X > maxX then + Assert.Fail($"npc {npc.Id} X out of bounds at tick {w.Tick}: {npc.Pos.X}") + if npc.Pos.Y < 0.0f || npc.Pos.Y > maxY then + Assert.Fail($"npc {npc.Id} Y out of bounds at tick {w.Tick}: {npc.Pos.Y}") + finally + Sim.configureBounds 64 48 + [] member _.SplitMix64MatchesKnownVectors () = let expected : uint64 list = diff --git a/src/LivingVillage.Kernel.Tests/M6aTests.fs b/src/LivingVillage.Kernel.Tests/M6aTests.fs index 9348b0f..94341a1 100644 --- a/src/LivingVillage.Kernel.Tests/M6aTests.fs +++ b/src/LivingVillage.Kernel.Tests/M6aTests.fs @@ -314,3 +314,29 @@ type M6aTests () = finally Sim.configureBounds 64 48 if File.Exists path then File.Delete path + + /// P23:512x384 参数化边界在存档 v2 中 round-trip;同 world 两次保存逐字节一致, + /// 读回后再次保存仍与原文一致(v1 的 64x48 兼容由 SaveV1LegacyStillReadable 覆盖)。 + [] + member _.SaveRoundTrips512x384BoundsAndIsByteStable () = + let path = "p23-bounds-512-v2.save" + let path2 = path + ".2" + try + Sim.configureBounds 512 384 + let world = Sim.initialWorld (uint64 4242) + WorldSave.saveToFile path world + WorldSave.saveToFile path2 world + let text = File.ReadAllText path + Assert.IsTrue(text.StartsWith("LV_WORLD_SAVE_V2"), "bounds save must stay v2") + Assert.AreEqual(text, File.ReadAllText path2) + Sim.configureBounds 64 48 + match WorldSave.loadFromFile path with + | Error failure -> Assert.Fail(sprintf "512x384 bounds save should load: %s" failure) + | Ok restored -> + Assert.AreEqual(512, Sim.mapWidthTiles) + Assert.AreEqual(384, Sim.mapHeightTiles) + Assert.AreEqual(text, WorldSave.save restored) + finally + Sim.configureBounds 64 48 + if File.Exists path then File.Delete path + if File.Exists path2 then File.Delete path2 -- cgit v1.2.3