diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 16:33:03 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 16:33:03 +0800 |
| commit | 9821b54ff2a77539898b62471d79dc4685b59cad (patch) | |
| tree | ffd5fb63883dd05b80fca5729af74be41df3a04d /src/LivingVillage.Kernel.Tests | |
| parent | 0539bba17c260f6a4b51d3cefddac0ff448c19bc (diff) | |
| download | living-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.fs | 53 |
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|")) |
