summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests/P65RumorInquiryTests.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop.Tests/P65RumorInquiryTests.fs')
-rw-r--r--src/LivingVillage.Desktop.Tests/P65RumorInquiryTests.fs166
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