summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel.Tests
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 17:18:11 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 17:18:11 +0800
commitcf214889490a62ba4fe84fb0055bc2133776e8a2 (patch)
tree0408fbd82e95ed55f39f8da3ccd3d2259e999313 /src/LivingVillage.Kernel.Tests
parent87bdfe273555c5ae5f698b3b89fa6c39d84d0c33 (diff)
downloadliving-village-cf214889490a62ba4fe84fb0055bc2133776e8a2.tar.gz
feat(occupation): 职业系统第一片 — Kernel 结构 / bootstrap 注入 / 存档 v3
Occupation.fs(Kind/Profile/State + 农夫/渔夫/货郎/书生 中文名常量,按设计 §1 表给初始 Money/Energy/Inventory);ItemKind 扩出 Fish/Spice/Scroll(basePrice 分支补齐,Food 10.0f 数值路径不变 → 无职业 digest 不动);WorldBootstrap.initialWorldWithOccupation 默认 None = 旧行为(Money/Energy 覆盖,库存字段先留数据侧、物理挂接留交易单——取舍见 design §2-C 反对加字段);WorldSave: v3 头仅在有职业时写入(保存 saveToFileWith / loadFromFileWith,v1/v2/v3 无职业类型默认,token 顺序 version|w|h|kind|task|story|…),默认路径 save 输出逐字节等同现行 v2;P10 测试 5 例(§1 表值、v3 round-trip、v1/v2 不吞档、双 seed 逐字节、中文名);Kernel.Tests 引用 Desktop 以驱动 WorldBootstrap(取舍:不做测试专用 shim)。TaskToken/StoryStage 为 v3 占位(任务系统后续单实现)。
Diffstat (limited to 'src/LivingVillage.Kernel.Tests')
-rw-r--r--src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj2
-rw-r--r--src/LivingVillage.Kernel.Tests/M6aTests.fs66
2 files changed, 67 insertions, 1 deletions
diff --git a/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj b/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj
index f618dc9..f7a50d5 100644
--- a/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj
+++ b/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj
@@ -22,7 +22,7 @@
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\LivingVillage.Kernel\LivingVillage.Kernel.fsproj" />
+ <ProjectReference Include="..\LivingVillage.Desktop\LivingVillage.Desktop.fsproj" />
<ProjectReference Include="..\LivingVillage.Headless\LivingVillage.Headless.fsproj" />
</ItemGroup>
diff --git a/src/LivingVillage.Kernel.Tests/M6aTests.fs b/src/LivingVillage.Kernel.Tests/M6aTests.fs
index 7f8d5ea..7184a75 100644
--- a/src/LivingVillage.Kernel.Tests/M6aTests.fs
+++ b/src/LivingVillage.Kernel.Tests/M6aTests.fs
@@ -5,6 +5,7 @@ open System.IO
open Microsoft.VisualStudio.TestTools.UnitTesting
open LivingVillage.Kernel
open LivingVillage.Kernel.Sim
+open LivingVillage.Desktop
module private M6aHarness =
@@ -226,3 +227,68 @@ type M6aTests () =
Assert.AreEqual<int>(0, SimulationControl.stepsPerFrameWithLegacy legacySteps (SimulationControl.togglePause control))
let manuallySelected = SimulationControl.setSpeed TwoX control
Assert.AreEqual<int>(2, SimulationControl.stepsPerFrameWithLegacy None manuallySelected)
+
+ [<TestMethod>]
+ member _.OccupationProfilesMatchTheDesignTable () =
+ let farmer = LivingVillage.Kernel.Occupation.profileOf Occupation.Farmer
+ Assert.AreEqual<int>(60, farmer.InitialMoney)
+ Assert.AreEqual<float32>(100.0f, farmer.InitialEnergy)
+ Assert.AreEqual<int>(12, snd (List.head farmer.InitialInventory))
+ let fisher = Occupation.profileOf Occupation.Fisher
+ Assert.AreEqual<int>(45, fisher.InitialMoney)
+ Assert.AreEqual<float32>(95.0f, fisher.InitialEnergy)
+ Assert.AreEqual<int>(8, snd (List.head fisher.InitialInventory))
+ Assert.AreEqual<int>(3, snd (List.nth fisher.InitialInventory 1))
+ let peddler = Occupation.profileOf Occupation.Peddler
+ Assert.AreEqual<int>(90, peddler.InitialMoney)
+ Assert.AreEqual<int>(6, snd (List.head peddler.InitialInventory))
+ Assert.AreEqual<int>(4, snd (List.nth peddler.InitialInventory 1))
+ let scholar = Occupation.profileOf Occupation.Scholar
+ Assert.AreEqual<int>(70, scholar.InitialMoney)
+ Assert.AreEqual<int>(10, snd (List.head scholar.InitialInventory))
+ Assert.AreEqual<int>(2, snd (List.nth scholar.InitialInventory 1))
+
+ [<TestMethod>]
+ member _.WorldSaveV3RoundTripsOccupation () =
+ let world = Sim.initialWorld (uint64 11)
+ let state = Occupation.stateOf Occupation.Fisher
+ WorldSave.saveToFileWith (Some state) "p10-v3.save" world
+ let text = System.IO.File.ReadAllText "p10-v3.save"
+ Assert.IsTrue(text.StartsWith("LV_WORLD_SAVE_V3"), "v3 header expected")
+ match WorldSave.loadFromFileWith "p10-v3.save" with
+ | Ok (_loaded, restored) ->
+ match restored with
+ | Some st ->
+ Assert.AreEqual<int>(45, st.Profile.InitialMoney)
+ Assert.AreEqual<int>(0, st.TaskToken)
+ Assert.AreEqual<int>(0, st.StoryStage)
+ | None -> Assert.Fail("v3 round trip lost the occupation")
+ | Error message -> Assert.Fail(sprintf "v3 round trip failed: %s" message)
+
+ [<TestMethod>]
+ member _.WorldSaveLegacySavesStillReadWithoutOccupation () =
+ let world = Sim.initialWorld (uint64 42)
+ WorldSave.saveToFile "p10-v2.save" world
+ Assert.IsTrue((System.IO.File.ReadAllText "p10-v2.save").StartsWith("LV_WORLD_SAVE_V2"))
+ match WorldSave.loadFromFileWith "p10-v2.save" with
+ | Ok (_w, occ) -> Assert.AreEqual<obj>(null, occ |> Option.map (fun _ -> box 1) |> Option.defaultValue null)
+ | Error message -> Assert.Fail(sprintf "v2 read failed: %s" message)
+ Sim.configureBounds 64 48
+
+ [<TestMethod>]
+ member _.OccupationBootstrapSavesAreByteIdenticalForSameSeed () =
+ let seed = uint64 4242
+ let state1 = Some (Occupation.stateOf Occupation.Peddler)
+ let state2 = Some (Occupation.stateOf Occupation.Peddler)
+ let world1 = WorldBootstrap.initialWorldWithOccupation false seed 4 state1
+ let world2 = WorldBootstrap.initialWorldWithOccupation false seed 4 state2
+ let text1 = WorldSave.saveWith state1 world1
+ let text2 = WorldSave.saveWith state2 world2
+ Assert.AreEqual<string>(text1, text2)
+
+ [<TestMethod>]
+ member _.OccupationChineseNamesAreKernelSideConstants () =
+ Assert.AreEqual<string>("农夫", Occupation.nameOf Occupation.Farmer)
+ Assert.AreEqual<string>("渔夫", Occupation.nameOf Occupation.Fisher)
+ Assert.AreEqual<string>("货郎", Occupation.nameOf Occupation.Peddler)
+ Assert.AreEqual<string>("书生", Occupation.nameOf Occupation.Scholar)