diff options
Diffstat (limited to 'src/LivingVillage.Desktop.Tests/P65RumorInquiryTests.fs')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/P65RumorInquiryTests.fs | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop.Tests/P65RumorInquiryTests.fs b/src/LivingVillage.Desktop.Tests/P65RumorInquiryTests.fs new file mode 100644 index 0000000..21ad47b --- /dev/null +++ b/src/LivingVillage.Desktop.Tests/P65RumorInquiryTests.fs @@ -0,0 +1,166 @@ +namespace LivingVillage.Desktop.Tests + +open System +open System.IO +open Microsoft.VisualStudio.TestTools.UnitTesting +open LivingVillage.Kernel +open LivingVillage.Kernel.Sim +open LivingVillage.Desktop + +/// P65 职业切片五:货郎「打听行情」(OfferTrade 意图)读取既有 Kernel `World.Rumors` 池, +/// 产出中文行情短答。全部为纯判定 + 文案,不新增状态、不改 `Sim.fs` 数值路径。 +/// 红先用例:`Occupation.marketInquiryText` / `freshReliableRumors` / `inquiryRumor` 尚未实现。 +[<TestClass>] +type P65RumorInquiryTests () = + + let readyNpc (npc: Npc) : Npc = + { npc with + Mind = + { npc.Mind with + Action = Wander + Target = actionTarget Wander + ActionAge = minActionTicks + EffectDone = true } } + + let rumor (id: int64) (tick: int64) (depth: int) (strength: float32) : RumorEvent = + { Id = RumorId id + Tick = tick + DayIndex = tick / ticksPerDay + OriginTick = tick + Source = NpcId 0 + Narrator = NpcId 0 + Receiver = NpcId 1 + Parent = None + Depth = depth + Strength = strength } + + let worldWithRumors (tick: int64) (rumors: RumorEvent list) : World = + let baseWorld = initialWorldN 42UL 4 + { baseWorld with + Tick = tick + Time = float tick * dtSeconds + Npcs = baseWorld.Npcs |> Array.map readyNpc + Rumors = rumors + RumorCount = rumors.Length + RumorOldestDay = + rumors + |> List.fold (fun acc entry -> min acc entry.DayIndex) Int64.MaxValue } + + let avatarAtNpc (world: World) : World = + { world with Avatar = { world.Avatar with Pos = world.Npcs.[0].Pos } } + + let peddlerAnswer (world: World) : string option = + Occupation.marketInquiryText (Some Occupation.Peddler) OfferTrade world + + [<TestMethod>] + member _.PeddlerOfferTradeReadsExistingRumorPoolAndAnswersInChinese () = + let tick = ticksPerDay + let world = worldWithRumors tick [ rumor 0L tick 0 1.0f ] + + Assert.AreEqual<int>(1, Occupation.freshReliableRumors world |> List.length) + Assert.IsTrue(Occupation.inquiryRumor world |> Option.isSome, "应命中一条新鲜可用谣言") + + match peddlerAnswer world with + | None -> Assert.Fail("货郎提议交易应产出行情短答") + | Some text -> + Assert.IsTrue(text.Length > 0, "行情短答不得为空") + Assert.IsTrue( + text |> Seq.forall CjkGlyphAtlas.contains, + sprintf "行情短答含缺字:%s" text) + + // 穷举全部地点/货品(id 索引 mod 6)与涨落分档(强度阈值)组合,断言无缺字。 + let strengths = [ 1.0f; 0.6f; 0.3f; 0.13f ] + for id in 0L .. 5L do + for depth in 0 .. 1 do + for strength in strengths do + match peddlerAnswer (worldWithRumors tick [ rumor id tick depth strength ]) with + | None -> Assert.Fail(sprintf "id=%d depth=%d strength=%.1f 应有短答" id depth strength) + | Some answer -> + Assert.IsTrue( + answer |> Seq.forall CjkGlyphAtlas.contains, + sprintf "行情短答含缺字:%s" answer) + + [<TestMethod>] + member _.SameInquiryInputProducesIdenticalText () = + let tick = ticksPerDay + let first = peddlerAnswer (worldWithRumors tick [ rumor 3L tick 1 0.6f ]) + let second = peddlerAnswer (worldWithRumors tick [ rumor 3L tick 1 0.6f ]) + let repeated = peddlerAnswer (worldWithRumors tick [ rumor 3L tick 1 0.6f ]) + + Assert.IsTrue(first.IsSome) + Assert.AreEqual<string option>(first, second) + Assert.AreEqual<string option>(first, repeated) + + [<TestMethod>] + member _.InquiryIgnoresStaleOrWeakOrMissingRumors () = + let tick = ticksPerDay * 10L + let stale = worldWithRumors tick [ rumor 0L (tick - rumorFreshnessTicks - 1L) 0 1.0f ] + let weak = worldWithRumors tick [ rumor 0L tick 0 0.01f ] + let empty = worldWithRumors tick [] + + for world in [ stale; weak; empty ] do + Assert.AreEqual<string option>(None, peddlerAnswer world) + + [<TestMethod>] + member _.OnlyPeddlerOfferTradeProducesInquiry () = + let tick = ticksPerDay + let world = worldWithRumors tick [ rumor 0L tick 0 1.0f ] + + Assert.AreEqual<string option>(None, Occupation.marketInquiryText (Some Occupation.Farmer) OfferTrade world) + Assert.AreEqual<string option>(None, Occupation.marketInquiryText (Some Occupation.Fisher) OfferTrade world) + Assert.AreEqual<string option>(None, Occupation.marketInquiryText (Some Occupation.Scholar) OfferTrade world) + Assert.AreEqual<string option>(None, Occupation.marketInquiryText None OfferTrade world) + Assert.AreEqual<string option>(None, Occupation.marketInquiryText (Some Occupation.Peddler) SmallTalk world) + Assert.AreEqual<string option>(None, Occupation.marketInquiryText (Some Occupation.Peddler) AskHelp world) + + [<TestMethod>] + member _.NonPeddlerDialogueStatusStaysByteIdenticalWhilePeddlerShowsInquiry () = + let tick = ticksPerDay + let baseWorld = worldWithRumors tick [ rumor 0L tick 0 1.0f ] |> avatarAtNpc + + let openAndAsk (task: Occupation.State option) = + let view = { M5Interaction.initial with Task = task; Seed = 42UL } + let afterInteract, opened = M5Interaction.apply M5Command.Interact baseWorld view + Assert.AreEqual<M5Panel>(DialoguePanel, opened.Panel) + let _, answered = M5Interaction.apply M5Command.Intent3 afterInteract opened + answered + + // 无职业 / 非货郎:状态逐字节等于既有 OfficeTrade 文案(旧行为回归)。 + Assert.AreEqual<string>( + "dialogue intent=OfferTrade response=Bargaining", + (openAndAsk None).Status) + Assert.AreEqual<string>( + "dialogue intent=OfferTrade response=Bargaining", + (openAndAsk (Some(Occupation.stateOf Occupation.Farmer))).Status) + + // 货郎:状态改为 inquiry|<中文行情短答>,并原样透出到情境状态条。 + let peddler = openAndAsk (Some(Occupation.stateOf Occupation.Peddler)) + Assert.IsTrue(peddler.Status.StartsWith("inquiry|"), peddler.Status) + let answer = peddler.Status.Substring("inquiry|".Length) + Assert.IsTrue(answer.Length > 0) + Assert.IsTrue(answer |> Seq.forall CjkGlyphAtlas.contains, answer) + Assert.AreEqual<string>(answer, M5Interaction.statusText peddler) + + [<TestMethod>] + member _.SaveV2HeaderUnchangedAndV3RoundTripStable () = + let tick = ticksPerDay + let world = worldWithRumors tick [ rumor 0L tick 0 1.0f ] + + let v2 = WorldSave.save world + Assert.IsTrue(v2.StartsWith("LV_WORLD_SAVE_V2")) + Assert.AreEqual<string>(v2, WorldSave.saveWith None world) + + let peddler = Occupation.stateOf Occupation.Peddler + let v3 = WorldSave.saveWith (Some peddler) world + Assert.IsTrue(v3.StartsWith("LV_WORLD_SAVE_V3")) + + let path = Path.Combine(Path.GetTempPath(), sprintf "lv-p65-%s.sav" (Guid.NewGuid().ToString("N"))) + try + WorldSave.saveToFileWith (Some peddler) path world + match WorldSave.loadFromFileWith path with + | Ok (_, Some loaded) -> + Assert.AreEqual<string>("peddler", Occupation.saveToken loaded.Profile.Kind) + Assert.AreEqual<int>(peddler.Backpack.Length, loaded.Backpack.Length) + | other -> Assert.Fail(sprintf "v3 round-trip failed: %A" other) + finally + if File.Exists path then File.Delete path |
