summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel.Tests
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 16:33:03 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 16:33:03 +0800
commit9821b54ff2a77539898b62471d79dc4685b59cad (patch)
treeffd5fb63883dd05b80fca5729af74be41df3a04d /src/LivingVillage.Kernel.Tests
parent0539bba17c260f6a4b51d3cefddac0ff448c19bc (diff)
downloadliving-village-9821b54ff2a77539898b62471d79dc4685b59cad.tar.gz
feat(kernel,desktop): P31 step3 story annals + identity entry + glyph atlas
- Sim.AnnalKind gains StoryAnnal (summary lives in the existing Summary field); WorldSave v3 serializes it with the new 'story' token. v1/v2 saves never contain the token and stay byte-identical; v3 save -> load -> save roundtrips byte-for-byte (tested). - Occupation.Story.identitySummaryOf: plain chronicle-style identity line written to the yearbook at game start (design section 3), e.g. yi-da-yu-wei-sheng. - Desktop: story summaries render in the chronicle panel; StartNewGame appends the identity annal; chooseIntent now routes responses through Occupation.biasResponse and advances the storyline (one stage per player dialogue) with the summary appended as a StoryAnnal. - M5View gains Seed (world seed source for the story start-day hash). - Chinese copy synced into requiredUiLabels; cjk-glyphs.txt + CjkGlyphAtlas.characterTable extended 238 -> 307 glyphs (384x480), atlas regenerated with the deterministic script. - 4 new kernel tests (115 -> 119); performance digest unchanged.
Diffstat (limited to 'src/LivingVillage.Kernel.Tests')
-rw-r--r--src/LivingVillage.Kernel.Tests/OccupationStoryTests.fs53
1 files changed, 52 insertions, 1 deletions
diff --git a/src/LivingVillage.Kernel.Tests/OccupationStoryTests.fs b/src/LivingVillage.Kernel.Tests/OccupationStoryTests.fs
index 80bc9a4..d204ca6 100644
--- a/src/LivingVillage.Kernel.Tests/OccupationStoryTests.fs
+++ b/src/LivingVillage.Kernel.Tests/OccupationStoryTests.fs
@@ -8,6 +8,14 @@ module private StoryHarness =
let seed = 42UL
+ let withTempSave (action: string -> unit) =
+ let id = System.Guid.NewGuid().ToString("N")
+ let path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"living-village-p31-{id}.save")
+ try
+ action path
+ finally
+ if System.IO.File.Exists path then System.IO.File.Delete path
+
let worldAtTick (tick: int64) : World =
{ Sim.initialWorldN 4UL 4 with Tick = tick; Time = float tick * Sim.dtSeconds }
@@ -94,7 +102,7 @@ type OccupationStoryTests () =
summaries <- summary :: summaries
Assert.AreEqual<int>(3, state.StoryStage)
Assert.AreEqual<int>(3, summaries.Length)
- Assert.AreEqual<string option>(Some "走访了几户人家,改日再问。", summaries |> List.rev |> List.item 1)
+ Assert.AreEqual<string option>(Some "走访了几户人家,改日再问", summaries |> List.rev |> List.item 1)
Assert.IsTrue(
summaries
|> List.choose id
@@ -228,3 +236,46 @@ type OccupationStoryTests () =
let secondState, secondSummary = Occupation.Story.onDialogue StoryHarness.seed state world
Assert.AreEqual<Occupation.State>(firstState, secondState)
Assert.AreEqual<string option>(firstSummary, secondSummary)
+
+ // ---- §3 身份条目 + 存档持久化 ----
+
+ [<TestMethod>]
+ member _.IdentitySummariesUsePlainChronicleStyle () =
+ Assert.AreEqual<string>("以耕田为生", Occupation.Story.identitySummaryOf Occupation.Farmer)
+ Assert.AreEqual<string>("以打渔为生", Occupation.Story.identitySummaryOf Occupation.Fisher)
+ Assert.AreEqual<string>("以贩货为生", Occupation.Story.identitySummaryOf Occupation.Peddler)
+ Assert.AreEqual<string>("以读书为生", Occupation.Story.identitySummaryOf Occupation.Scholar)
+
+ [<TestMethod>]
+ member _.StoryAnnalRoundTripsThroughTheV3Save () =
+ let kind = Occupation.Fisher
+ let startDay = Occupation.Story.startDayOf StoryHarness.seed kind
+ let world = StoryHarness.worldAtTick (StoryHarness.dayTick (startDay + 2L))
+ let _, summary = Occupation.Story.onDialogue StoryHarness.seed (StoryHarness.stateOf kind 2) world
+ let entry = { Tick = world.Tick; Kind = StoryAnnal; Summary = summary.Value }
+ let worldWithAnnal = Sim.appendAnnal entry world
+ let text = WorldSave.saveWith (Some (StoryHarness.stateOf kind 3)) worldWithAnnal
+ match WorldSave.load text with
+ | Ok loaded ->
+ Assert.AreEqual(worldWithAnnal.Annals, loaded.Annals)
+ // 存 → 读 → 存 逐字节一致
+ Assert.AreEqual<string>(text, WorldSave.saveWith (Some (StoryHarness.stateOf kind 3)) loaded)
+ | Error message -> Assert.Fail(message)
+
+ [<TestMethod>]
+ member _.StoryStageRestoresFromTheV3OccupationSegment () =
+ StoryHarness.withTempSave (fun path ->
+ let state = StoryHarness.stateOf Occupation.Peddler 2
+ WorldSave.saveToFileWith (Some state) path (StoryHarness.worldAtTick 0L)
+ match WorldSave.loadFromFileWith path with
+ | Ok (_, Some loaded) -> Assert.AreEqual<int>(2, loaded.StoryStage)
+ | Ok (_, None) -> Assert.Fail("v3 occupation state expected")
+ | Error message -> Assert.Fail(message))
+
+ [<TestMethod>]
+ member _.NoOccupationSaveStaysFreeOfStoryTokens () =
+ // 无职业 v2 存档永不产生 story token,v1/v2 字节口径不变
+ let world = StoryHarness.worldAtTick 0L
+ let text = WorldSave.save world
+ Assert.IsFalse(text.Contains("story"))
+ Assert.IsTrue(text.StartsWith("LV_WORLD_SAVE_V2|"))