diff options
Diffstat (limited to 'src/LivingVillage.Kernel.Tests')
3 files changed, 429 insertions, 0 deletions
diff --git a/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj b/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj index dea8314..1a36ea7 100644 --- a/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj +++ b/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj @@ -14,6 +14,8 @@ <Compile Include="M6aTests.fs" /> <Compile Include="DailyTaskTests.fs" /> <Compile Include="TradeBiasTests.fs" /> + <Compile Include="OccupationBiasTests.fs" /> + <Compile Include="OccupationStoryTests.fs" /> <Compile Include="PerformanceTests.fs" /> </ItemGroup> diff --git a/src/LivingVillage.Kernel.Tests/OccupationBiasTests.fs b/src/LivingVillage.Kernel.Tests/OccupationBiasTests.fs new file mode 100644 index 0000000..76e02f3 --- /dev/null +++ b/src/LivingVillage.Kernel.Tests/OccupationBiasTests.fs @@ -0,0 +1,146 @@ +namespace LivingVillage.Kernel.Tests + +open Microsoft.VisualStudio.TestTools.UnitTesting +open LivingVillage.Kernel +open LivingVillage.Kernel.Sim + +module private BiasHarness = + + /// 覆盖 responseFor 全部分支的两个人格样本: + /// 外向+诚实(E>=0.5, H>=0.4)与 内向+不诚实(E<0.5, H<0.4)。 + let outgoing = { Drive = 0.5f; Aggression = 0.2f; Extraversion = 0.8f; Honesty = 0.9f; Greed = 0.3f } + let withdrawn = { Drive = 0.5f; Aggression = 0.2f; Extraversion = 0.2f; Honesty = 0.1f; Greed = 0.3f } + let grumpy = { Drive = 0.5f; Aggression = 0.8f; Extraversion = 0.2f; Honesty = 0.1f; Greed = 0.3f } + + let atNpc (id: int) (world: World) : World = + let npc = world.Npcs.[id] + { world with Avatar = { world.Avatar with Pos = npc.Pos } } + +[<TestClass>] +type OccupationBiasTests () = + + // ---- 无职业 = 既有 responseFor 输出逐字节不变 ---- + + [<TestMethod>] + member _.NoOccupationReproducesTheHistoricResponsesForEveryIntent () = + for intent in [ SmallTalk; AskHelp; OfferTrade; Joke; Apologize; Provoke ] do + for personality in [ BiasHarness.outgoing; BiasHarness.withdrawn; BiasHarness.grumpy ] do + Assert.IsTrue( + Sim.responseFor intent personality = Occupation.biasResponse None intent personality, + $"no-occupation bias must be identity for {intent}") + + // ---- 农夫 AskHelp:Refused → Helpful(求助更友好)---- + + [<TestMethod>] + member _.FarmerAskHelpTurnsRefusalIntoHelpfulness () = + // 不诚实村民对常人 Refused,对农夫给 Helpful + Assert.AreEqual<DialogueResponse>(Refused, Sim.responseFor AskHelp BiasHarness.withdrawn) + Assert.AreEqual<DialogueResponse>(Helpful, Occupation.biasResponse (Some Occupation.Farmer) AskHelp BiasHarness.withdrawn) + // 本就 Helpful 的路径不变 + Assert.AreEqual<DialogueResponse>(Helpful, Sim.responseFor AskHelp BiasHarness.outgoing) + Assert.AreEqual<DialogueResponse>(Helpful, Occupation.biasResponse (Some Occupation.Farmer) AskHelp BiasHarness.outgoing) + + // ---- 渔夫 SmallTalk:Reserved → Friendly(聊天气鱼汛,冷场变热络)---- + + [<TestMethod>] + member _.FisherSmallTalkWarmsUpAReservedVillager () = + Assert.AreEqual<DialogueResponse>(Reserved, Sim.responseFor SmallTalk BiasHarness.withdrawn) + Assert.AreEqual<DialogueResponse>(Friendly, Occupation.biasResponse (Some Occupation.Fisher) SmallTalk BiasHarness.withdrawn) + Assert.AreEqual<DialogueResponse>(Friendly, Sim.responseFor SmallTalk BiasHarness.outgoing) + Assert.AreEqual<DialogueResponse>(Friendly, Occupation.biasResponse (Some Occupation.Fisher) SmallTalk BiasHarness.outgoing) + + // ---- 货郎 SmallTalk:Reserved → Bargaining(三句不离买卖,更易进讨价还价)---- + + [<TestMethod>] + member _.PeddlerSmallTalkSlidesIntoBargaining () = + Assert.AreEqual<DialogueResponse>(Reserved, Sim.responseFor SmallTalk BiasHarness.withdrawn) + Assert.AreEqual<DialogueResponse>(Bargaining, Occupation.biasResponse (Some Occupation.Peddler) SmallTalk BiasHarness.withdrawn) + // 外向村民本就 Friendly,不被货郎偏差改写 + Assert.AreEqual<DialogueResponse>(Friendly, Occupation.biasResponse (Some Occupation.Peddler) SmallTalk BiasHarness.outgoing) + + // ---- 书生 Joke:完全覆盖为 Reserved(与 personality 相反的确定性偏差)---- + // 语义说明:书生讲笑话走「完全覆盖」——无论村民性格如何,回应一律 Reserved。 + // 刻意不做概率偏向(禁随机);反差感即设计意图(design §5)。 + + [<TestMethod>] + member _.ScholarJokeIsAlwaysReservedRegardlessOfPersonality () = + Assert.AreEqual<DialogueResponse>(Amused, Sim.responseFor Joke BiasHarness.outgoing) + Assert.AreEqual<DialogueResponse>(Reserved, Sim.responseFor Joke BiasHarness.withdrawn) + for personality in [ BiasHarness.outgoing; BiasHarness.withdrawn; BiasHarness.grumpy ] do + Assert.AreEqual<DialogueResponse>( + Reserved, + Occupation.biasResponse (Some Occupation.Scholar) Joke personality) + + // ---- 偏差只作用于各自的签名意图,其余意图原样穿透 ---- + + [<TestMethod>] + member _.BiasOnlyAppliesToTheSignatureIntents () = + // 农夫:仅 AskHelp 受偏差 + for intent in [ SmallTalk; OfferTrade; Joke; Apologize; Provoke ] do + for personality in [ BiasHarness.outgoing; BiasHarness.withdrawn ] do + Assert.AreEqual<DialogueResponse>( + Sim.responseFor intent personality, + Occupation.biasResponse (Some Occupation.Farmer) intent personality) + // 渔夫:仅 SmallTalk 受偏差 + for intent in [ AskHelp; OfferTrade; Joke; Apologize; Provoke ] do + for personality in [ BiasHarness.outgoing; BiasHarness.withdrawn ] do + Assert.AreEqual<DialogueResponse>( + Sim.responseFor intent personality, + Occupation.biasResponse (Some Occupation.Fisher) intent personality) + // 货郎:仅 SmallTalk 受偏差 + for intent in [ AskHelp; OfferTrade; Joke; Apologize; Provoke ] do + for personality in [ BiasHarness.outgoing; BiasHarness.withdrawn ] do + Assert.AreEqual<DialogueResponse>( + Sim.responseFor intent personality, + Occupation.biasResponse (Some Occupation.Peddler) intent personality) + // 书生:仅 Joke 受偏差 + for intent in [ SmallTalk; AskHelp; OfferTrade; Apologize; Provoke ] do + for personality in [ BiasHarness.outgoing; BiasHarness.withdrawn ] do + Assert.AreEqual<DialogueResponse>( + Sim.responseFor intent personality, + Occupation.biasResponse (Some Occupation.Scholar) intent personality) + + // ---- 确定性:同输入两次调用结果一致 ---- + + [<TestMethod>] + member _.BiasIsDeterministicForTheSameInputs () = + for kind in Occupation.all do + for intent in [ SmallTalk; AskHelp; OfferTrade; Joke; Apologize; Provoke ] do + for personality in [ BiasHarness.outgoing; BiasHarness.withdrawn; BiasHarness.grumpy ] do + let first = Occupation.biasResponse (Some kind) intent personality + let second = Occupation.biasResponse (Some kind) intent personality + Assert.AreEqual<DialogueResponse>(first, second) + + // ---- 与 chooseDialogueWith 复合:偏差写进 DialogueOutcome/记忆/年鉴 ---- + + [<TestMethod>] + member _.ChooseDialogueWithIdentityMatchesTheHistoricPath () = + let world = Sim.initialWorldN 42UL 4 |> BiasHarness.atNpc 0 + let npc = world.Npcs.[0] + let identityOutcome = + match Sim.chooseDialogueWith Sim.responseFor npc.Id AskHelp world with + | DialogueSucceeded (outcome, _) -> outcome + | DialogueRejected (failure, _) -> Assert.Fail($"rejected: {failure}"); Unchecked.defaultof<DialogueOutcome> + Assert.AreEqual<DialogueResponse>(Sim.responseFor AskHelp npc.Mind.Personality, identityOutcome.Response) + + [<TestMethod>] + member _.ChooseDialogueWithBiasRecordsTheBiasedResponse () = + let world = Sim.initialWorldN 42UL 4 |> BiasHarness.atNpc 0 + let npc = world.Npcs.[0] + let biasedRespond = Occupation.biasResponse (Some Occupation.Farmer) + let response, outcomeWorld = + match Sim.chooseDialogueWith biasedRespond npc.Id AskHelp world with + | DialogueSucceeded (outcome, next) -> + Assert.AreEqual<DialogueResponse>( + Occupation.biasResponse (Some Occupation.Farmer) AskHelp npc.Mind.Personality, + outcome.Response) + outcome.Response, next + | DialogueRejected (failure, _) -> Assert.Fail($"rejected: {failure}"); Unchecked.defaultof<_>, Unchecked.defaultof<World> + // 目标村民记忆里记录的是偏差后的回应 + let remembered = + outcomeWorld.Npcs.[0].Mind.Memory + |> List.tryPick (fun memory -> + match memory.Kind with + | Dialogue (_, _, response) -> Some response + | _ -> None) + Assert.AreEqual<DialogueResponse option>(Some response, remembered) diff --git a/src/LivingVillage.Kernel.Tests/OccupationStoryTests.fs b/src/LivingVillage.Kernel.Tests/OccupationStoryTests.fs new file mode 100644 index 0000000..d204ca6 --- /dev/null +++ b/src/LivingVillage.Kernel.Tests/OccupationStoryTests.fs @@ -0,0 +1,281 @@ +namespace LivingVillage.Kernel.Tests + +open Microsoft.VisualStudio.TestTools.UnitTesting +open LivingVillage.Kernel +open LivingVillage.Kernel.Sim + +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 } + + let stateOf (kind: Occupation.Kind) (stage: int) : Occupation.State = + { Occupation.stateOf kind with StoryStage = stage } + + let dayTick (day: int64) : int64 = day * Sim.ticksPerDay + + let withMemory (valence: float32) (tick: int64) (world: World) : World = + { world with + Npcs = + world.Npcs + |> Array.mapi (fun index npc -> + if index = 0 then + { npc with + Mind = + { npc.Mind with + Memory = + [ { Tick = tick; Kind = Chatted(Sim.playerId); Valence = valence } ] } } + else npc) } + +[<TestClass>] +type OccupationStoryTests () = + + // ---- 命名:四条轻剧情线 + 四段进度 ---- + + [<TestMethod>] + member _.EachOccupationHasItsOwnStoryLineName () = + Assert.AreEqual<string>("雨水与年成", Occupation.Story.lineNameOf Occupation.Farmer) + Assert.AreEqual<string>("水位与收成", Occupation.Story.lineNameOf Occupation.Fisher) + Assert.AreEqual<string>("新货的路子", Occupation.Story.lineNameOf Occupation.Peddler) + Assert.AreEqual<string>("庙前的争论", Occupation.Story.lineNameOf Occupation.Scholar) + + [<TestMethod>] + member _.StageNamesCoverTheThreeSegmentArc () = + Assert.AreEqual<string>("未开始", Occupation.Story.stageNameOf 0) + Assert.AreEqual<string>("已接任务", Occupation.Story.stageNameOf 1) + Assert.AreEqual<string>("走访中", Occupation.Story.stageNameOf 2) + Assert.AreEqual<string>("已收束", Occupation.Story.stageNameOf 3) + + // ---- 开线日:hash(seed ^ storySeed) mod 3 + 2,纯函数、确定性 ---- + + [<TestMethod>] + member _.StartDayIsDeterministicAndInsideTwoToFour () = + for kind in Occupation.all do + for seed in [ 42UL; 7UL; 4242UL; 999UL ] do + let first = Occupation.Story.startDayOf seed kind + let second = Occupation.Story.startDayOf seed kind + Assert.AreEqual<int64>(first, second) + Assert.IsTrue(2L <= first && first <= 4L, $"{kind} start day {first} out of range") + + [<TestMethod>] + member _.StorySeedsDifferFromDailyOccupationSeeds () = + // 开线日与每日任务抽取互不串线(种子常量两两不同) + let storySeeds = Occupation.all |> List.map Occupation.Story.storySeedOf + let taskSeeds = Occupation.all |> List.map Occupation.occupationSeedOf + Assert.AreEqual<int>(storySeeds.Length, storySeeds |> Set.ofList |> Set.count) + for story in storySeeds do + Assert.IsFalse(taskSeeds |> List.contains story, "story seed must not collide with a task seed") + + // ---- 推进门控:开线日前的对话不动剧情线 ---- + + [<TestMethod>] + member _.DialogueBeforeStartDayDoesNotAdvanceTheStory () = + for kind in Occupation.all do + let state = StoryHarness.stateOf kind 0 + let world = StoryHarness.worldAtTick (StoryHarness.dayTick 1L) + let nextState, summary = Occupation.Story.onDialogue StoryHarness.seed state world + Assert.AreEqual<int>(0, nextState.StoryStage) + Assert.AreEqual<string option>(None, summary) + + // ---- 三段式推进:接任务 → 走访 → 收束,逐段一对话 ---- + + [<TestMethod>] + member _.StoryAdvancesOneStagePerDialogueAcrossThreeDays () = + for kind in Occupation.all do + let startDay = Occupation.Story.startDayOf StoryHarness.seed kind + let mutable state = StoryHarness.stateOf kind 0 + let mutable summaries = [] + for offset in 0L .. 2L do + let world = StoryHarness.worldAtTick (StoryHarness.dayTick (startDay + offset)) + let nextState, summary = Occupation.Story.onDialogue StoryHarness.seed state world + state <- nextState + 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.IsTrue( + summaries + |> List.choose id + |> List.exists (fun text -> text.Contains("有了结果")), + "the third dialogue must conclude the line") + // 终态后不再推进 + let world = StoryHarness.worldAtTick (StoryHarness.dayTick (startDay + 3L)) + let nextState, summary = Occupation.Story.onDialogue StoryHarness.seed state world + Assert.AreEqual<int>(3, nextState.StoryStage) + Assert.AreEqual<string option>(None, summary) + + [<TestMethod>] + member _.LateFirstDialogueStillAdvancesOneStageAtATime () = + let kind = Occupation.Fisher + let startDay = Occupation.Story.startDayOf StoryHarness.seed kind + let state = StoryHarness.stateOf kind 0 + let world = StoryHarness.worldAtTick (StoryHarness.dayTick (startDay + 5L)) + let stage1, summary1 = Occupation.Story.onDialogue StoryHarness.seed state world + Assert.AreEqual<int>(1, stage1.StoryStage) + Assert.IsTrue(summary1.IsSome) + let stage2, _ = Occupation.Story.onDialogue StoryHarness.seed stage1 world + Assert.AreEqual<int>(2, stage2.StoryStage) + let stage3, summary3 = Occupation.Story.onDialogue StoryHarness.seed stage2 world + Assert.AreEqual<int>(3, stage3.StoryStage) + Assert.IsTrue(summary3.Value.Contains("有了结果")) + + // ---- 关系均值:与 relationMatrix 同口径的衰减 valence,仅计认识玩家的人 ---- + + [<TestMethod>] + member _.RelationMeanAveragesOnlyAcquaintedNpcs () = + let baseWorld = StoryHarness.worldAtTick 0L + // 单个熟识村民:Helpful 口径 0.5 + let friendly = baseWorld |> StoryHarness.withMemory 0.5f 0L + Assert.AreEqual(0.5f, Occupation.Story.relationMeanToPlayer friendly, 1e-5f) + // 无记忆 = 0(中性,不取负) + Assert.AreEqual(0.0f, Occupation.Story.relationMeanToPlayer baseWorld, 1e-5f) + // 两名熟识村民(0.5 与 -0.4)均值 0.05 + let mixed = + { friendly with + Npcs = + friendly.Npcs + |> Array.mapi (fun index npc -> + if index = 1 then + { npc with + Mind = { npc.Mind with Memory = [ { Tick = 0L; Kind = Chatted(Sim.playerId); Valence = -0.4f } ] } } + else npc) } + Assert.AreEqual(0.05f, Occupation.Story.relationMeanToPlayer mixed, 1e-5f) + + [<TestMethod>] + member _.RelationMeanDecaysWithTheRelationHalfLife () = + let baseWorld = StoryHarness.worldAtTick Sim.ticksPerDay + let decayed = baseWorld |> StoryHarness.withMemory 0.5f 0L + // 半衰期 = 1 模拟日:一天后 0.5 → 0.25 + Assert.AreEqual(0.25f, Occupation.Story.relationMeanToPlayer decayed, 1e-5f) + + [<TestMethod>] + member _.DialogueMemoriesCountTowardTheRelationMean () = + let baseWorld = StoryHarness.worldAtTick 0L + let dialogued = + { baseWorld with + Npcs = + baseWorld.Npcs + |> Array.mapi (fun index npc -> + if index = 0 then + { npc with + Mind = + { npc.Mind with + Memory = + [ { Tick = 0L + Kind = Dialogue(Sim.playerId, SmallTalk, Friendly) + Valence = 0.4f } ] } } + else npc) } + Assert.AreEqual(0.4f, Occupation.Story.relationMeanToPlayer dialogued, 1e-5f) + + // ---- 三种结局:由当时关系均值确定性决定(三方对照)---- + + [<TestMethod>] + member _.EndingThresholdsSeparateThreeOutcomes () = + Assert.AreEqual<Occupation.Story.Ending>(Occupation.Story.Warm, Occupation.Story.endingOf 0.5f) + Assert.AreEqual<Occupation.Story.Ending>(Occupation.Story.Warm, Occupation.Story.endingOf 0.3f) + Assert.AreEqual<Occupation.Story.Ending>(Occupation.Story.Plain, Occupation.Story.endingOf 0.1f) + Assert.AreEqual<Occupation.Story.Ending>(Occupation.Story.Plain, Occupation.Story.endingOf 0.0f) + Assert.AreEqual<Occupation.Story.Ending>(Occupation.Story.Distant, Occupation.Story.endingOf -0.1f) + + [<TestMethod>] + member _.EachOccupationHasThreeDistinctEndingTexts () = + for kind in Occupation.all do + let warm = Occupation.Story.endingTextOf kind Occupation.Story.Warm + let plain = Occupation.Story.endingTextOf kind Occupation.Story.Plain + let distant = Occupation.Story.endingTextOf kind Occupation.Story.Distant + Assert.AreNotEqual(warm, plain, $"{kind} warm/plain must differ") + Assert.AreNotEqual(plain, distant, $"{kind} plain/distant must differ") + Assert.AreNotEqual(warm, distant, $"{kind} warm/distant must differ") + Assert.IsTrue(warm.Contains(Occupation.Story.lineNameOf kind)) + // 两次调用逐字节一致 + Assert.AreEqual<string>(warm, Occupation.Story.endingTextOf kind Occupation.Story.Warm) + + [<TestMethod>] + member _.ConcludingStageWritesTheEndingDeterminedByTheRelationMean () = + for kind in Occupation.all do + let startDay = Occupation.Story.startDayOf StoryHarness.seed kind + let conclusionTick = StoryHarness.dayTick (startDay + 2L) + // 热络结局:收束当天熟识村民 Helpful 口径 0.5 + let warmWorld = + StoryHarness.worldAtTick conclusionTick + |> StoryHarness.withMemory 0.5f conclusionTick + let state2 = StoryHarness.stateOf kind 2 + let warmState, warmSummary = Occupation.Story.onDialogue StoryHarness.seed state2 warmWorld + Assert.AreEqual<int>(3, warmState.StoryStage) + Assert.AreEqual<string>(Occupation.Story.endingTextOf kind Occupation.Story.Warm, warmSummary.Value) + // 淡漠结局:收束当天熟识村民 Offended 口径 -0.4 + let distantWorld = + StoryHarness.worldAtTick conclusionTick + |> StoryHarness.withMemory -0.4f conclusionTick + let distantState, distantSummary = Occupation.Story.onDialogue StoryHarness.seed state2 distantWorld + Assert.AreEqual<int>(3, distantState.StoryStage) + Assert.AreEqual<string>(Occupation.Story.endingTextOf kind Occupation.Story.Distant, distantSummary.Value) + // 平常结局:无关系记忆 → 均值 0 + let plainWorld = StoryHarness.worldAtTick conclusionTick + let plainState, plainSummary = Occupation.Story.onDialogue StoryHarness.seed state2 plainWorld + Assert.AreEqual<int>(3, plainState.StoryStage) + Assert.AreEqual<string>(Occupation.Story.endingTextOf kind Occupation.Story.Plain, plainSummary.Value) + + [<TestMethod>] + member _.OnDialogueIsDeterministicForTheSameInputs () = + let kind = Occupation.Fisher + let startDay = Occupation.Story.startDayOf StoryHarness.seed kind + let world = StoryHarness.worldAtTick (StoryHarness.dayTick startDay) |> StoryHarness.withMemory 0.5f 0L + let state = StoryHarness.stateOf kind 0 + let firstState, firstSummary = Occupation.Story.onDialogue StoryHarness.seed state world + 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|")) |
