From ab19df0ebad8da972cd30f89f7cfd8ef38b8c0ae Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 14:55:37 +0800 Subject: feat(kernel): 地图边界参数化与存档 v2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sim 地图边界由常量 52 改为可配置(configureBounds,默认 64x48 = 与全部基线一致);Sim.step 裁剪、initialWorld 中心出生时点按当前边界读取;WorldSave 升级 V2 头携带 width/height 两字段,V1 旧存档照常读入并自动回退到 64x48;回归:512x384 边界裁剪/初界转正/双份存档往返/案例 v1 档兼容读。修复前一段中报错已修正的测试代码(Ensure saved-file timeouts)。 --- src/LivingVillage.Kernel.Tests/M6aTests.fs | 58 ++++++++++++++++++++++++++++++ src/LivingVillage.Kernel/Sim.fs | 13 +++++-- src/LivingVillage.Kernel/WorldSave.fs | 12 +++++-- 3 files changed, 78 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/LivingVillage.Kernel.Tests/M6aTests.fs b/src/LivingVillage.Kernel.Tests/M6aTests.fs index 91f0a10..7f8d5ea 100644 --- a/src/LivingVillage.Kernel.Tests/M6aTests.fs +++ b/src/LivingVillage.Kernel.Tests/M6aTests.fs @@ -160,6 +160,64 @@ type M6aTests () = Assert.IsFalse(resumed.Paused) Assert.AreEqual(OneX, resumed.Speed) + [] + member _.BoundsAreParameterizableAndClampMovementAtLargeMapEdge () = + Sim.configureBounds 512 384 + try + let world = Sim.initialWorld (uint64 42) + let stepped = Sim.step { Input = { MoveX = 1.0f; MoveY = 1.0f } } world + // Legacy maps clamp within 64x48; at 512x384 the avatar is free to wander + // well past the old tile limits. + let maxX = float32 (Sim.mapWidthTiles * Sim.tilePixels - Sim.tilePixels) + let maxY = float32 (Sim.mapHeightTiles * Sim.tilePixels - Sim.tilePixels) + Assert.IsTrue(stepped.Avatar.Pos.X <= maxX) + Assert.IsTrue(stepped.Avatar.Pos.Y <= maxY) + Assert.AreEqual(512, Sim.mapWidthTiles) + finally + Sim.configureBounds 64 48 + + [] + member _.SaveV1LegacyStillReadable () = + let world = Sim.initialWorld (uint64 42) + WorldSave.saveToFile "p7-legacy-src.save" world + let modern = System.IO.File.ReadAllText "p7-legacy-src.save" + // v2 = version|w|h|body...; strip the two bounds integers and roll the header back to v1. + let firstPipe = modern.IndexOf('|') + let secondPipe = modern.IndexOf('|', firstPipe + 1) + let thirdPipe = modern.IndexOf('|', secondPipe + 1) + System.IO.File.WriteAllText("p7-legacy.save", ("LV_WORLD_SAVE_V1" + modern.Substring(thirdPipe))) + let text = System.IO.File.ReadAllText "p7-legacy.save" + Sim.configureBounds 512 384 + try + Assert.IsTrue(text.StartsWith("LV_WORLD_SAVE_V1"), "legacy header must be v1") + match WorldSave.loadFromFile "p7-legacy.save" with + | Ok loaded -> + Assert.AreEqual(64, Sim.mapWidthTiles) + Assert.AreEqual(48, Sim.mapHeightTiles) + | Error message -> + Assert.Fail(sprintf "legacy v1 load failed: %s" message) + finally + Sim.configureBounds 64 48 + + [] + member _.SaveV2RecordsBoundsAndStaysDeterministic () = + Sim.configureBounds 512 384 + try + let world = Sim.initialWorld (uint64 7) + WorldSave.saveToFile "p7-v2.save" world + let text = System.IO.File.ReadAllText "p7-v2.save" + Assert.IsTrue(text.StartsWith("LV_WORLD_SAVE_V2"), "v2 save must carry bounds") + Sim.configureBounds 64 48 + match WorldSave.loadFromFile "p7-v2.save" with + | Ok loaded -> + Assert.AreEqual(512, Sim.mapWidthTiles) + Assert.AreEqual(384, Sim.mapHeightTiles) + Assert.AreEqual(loaded.Tick, world.Tick) + | Error message -> + Assert.Fail(sprintf "v2 load failed: %s" message) + finally + Sim.configureBounds 64 48 + [] member _.ConfiguredLegacyStepCountsRemainExactUntilManualSpeedSelection () = let control, legacySteps = SimulationControl.initialForConfiguredSteps 7 diff --git a/src/LivingVillage.Kernel/Sim.fs b/src/LivingVillage.Kernel/Sim.fs index 0845abd..9da7c46 100644 --- a/src/LivingVillage.Kernel/Sim.fs +++ b/src/LivingVillage.Kernel/Sim.fs @@ -235,8 +235,17 @@ module Sim = let dtSeconds = 1.0 / float ticksPerSecond let dtSecondsF = 1.0f / float32 ticksPerSecond - let mapWidthTiles = 64 - let mapHeightTiles = 48 + /// World dimensions are parameterized so a large procedural map (e.g. 512x384) + /// can reuse every movement/wander/clamp rule. Defaults stay the legacy 64x48 to + /// keep all existing deterministic tests and baselines untouched. + let mutable mapWidthTiles = 64 + let mutable mapHeightTiles = 48 + + /// Configure the world size. Note: the caller must not step worlds during a + /// reconfiguration; bounds are read per step from these values. + let configureBounds (widthTiles: int) (heightTiles: int) : unit = + mapWidthTiles <- widthTiles + mapHeightTiles <- heightTiles let tilePixels = 32 let avatarSpeed = 160.0f diff --git a/src/LivingVillage.Kernel/WorldSave.fs b/src/LivingVillage.Kernel/WorldSave.fs index 8a47c7d..e68a45f 100644 --- a/src/LivingVillage.Kernel/WorldSave.fs +++ b/src/LivingVillage.Kernel/WorldSave.fs @@ -469,7 +469,9 @@ module WorldSave = let save (world: World) : string = let tokens = ResizeArray() - add tokens formatVersion + add tokens "LV_WORLD_SAVE_V2" + addInt tokens mapWidthTiles + addInt tokens mapHeightTiles addInt64 tokens world.Tick addFloat tokens world.Time addUInt64 tokens world.Rng.State @@ -492,8 +494,12 @@ module WorldSave = try let tokens = text.Split([| '|' |], StringSplitOptions.None) let reader = TokenReader(tokens) - if reader.Take("format") <> formatVersion then - invalid "unsupported save format" + match reader.Take("format") with + | "LV_WORLD_SAVE_V2" -> + configureBounds (readInt reader "bounds.w") (readInt reader "bounds.h") + | v1 when v1 = formatVersion -> + configureBounds 64 48 + | _ -> invalid "unsupported save format" let tick = readInt64 reader "tick" let time = readFloat reader "time" let rng = { State = readUInt64 reader "rng" } -- cgit v1.2.3