namespace LivingVillage.Desktop.Tests open System open Microsoft.VisualStudio.TestTools.UnitTesting open LivingVillage.Kernel open LivingVillage.Kernel.Sim open LivingVillage.Desktop module private P31Harness = let atNpc (id: int) (world: World) : World = let npc = world.Npcs.[id] { world with Avatar = { world.Avatar with Pos = npc.Pos } } let nearWorld () : World = Sim.initialWorldN 42UL 4 |> atNpc 0 let npcIndex (NpcId id) : int = id [] type P31StoryUiTests () = // ---- 任务面板:剧情线名 + 进度段名 ---- [] member _.TaskPanelShowsTheStorylineNameAndStage () = let world = Sim.initialWorldN 42UL 4 let state = { Occupation.stateOf Occupation.Fisher with StoryStage = 2 } let view = { M5Interaction.initial with Task = Some state } let _, taskView = M5Interaction.apply ShowTaskPanel world view let lines = M5Interaction.panelLines world taskView Assert.IsTrue(lines |> List.contains "剧情线:水位与收成,走访中", $"lines={lines}") [] member _.TaskPanelShowsConcludedStageAndNoOccupationStaysStoryFree () = let world = Sim.initialWorldN 42UL 4 let concluded = { Occupation.stateOf Occupation.Scholar with StoryStage = 3 } let view = { M5Interaction.initial with Task = Some concluded } let _, taskView = M5Interaction.apply ShowTaskPanel world view Assert.IsTrue( M5Interaction.panelLines world taskView |> List.contains "剧情线:庙前的争论,已收束") let _, noneView = M5Interaction.apply ShowTaskPanel world M5Interaction.initial Assert.IsFalse( M5Interaction.panelLines world noneView |> List.exists (fun line -> line.Contains("剧情线"))) // ---- 对话接线:回应偏差进记忆 + 剧情线推进 + 年鉴写入 ---- [] member _.DialogueThroughM5AppliesBiasAndAdvancesTheStory () = let world = { P31Harness.nearWorld () with Tick = 5L * Sim.ticksPerDay } let state = Occupation.stateOf Occupation.Fisher let view = { M5Interaction.initial with Task = Some state; Seed = 42UL } let personality = world.Npcs.[0].Mind.Personality let openedWorld, openedView = M5Interaction.apply Interact world view match openedView.Menu with | None -> Assert.Fail("dialogue menu should open next to a villager") | Some menu -> let finalWorld, finalView = M5Interaction.apply Intent1 openedWorld openedView // 剧情线推进一段(tick 第 5 日 ≥ 开线日) let task = finalView.Task.Value Assert.AreEqual(1, task.StoryStage) // 年鉴写入剧情条目 Assert.IsTrue( finalWorld.Annals |> List.exists (fun entry -> match entry.Kind with | StoryAnnal -> entry.Summary.Contains("接了件事") | _ -> false)) // 回应偏差可观测:偏差后的回应与纯函数口径一致 let biased = Occupation.biasResponse (Some Occupation.Fisher) SmallTalk personality let unbiased = Sim.responseFor SmallTalk personality if biased <> unbiased then Assert.IsTrue( finalWorld.Annals |> List.exists (fun entry -> match entry.Kind with | DialogueAnnal outcome -> outcome.Response = biased | _ -> false), "the biased response must be the one recorded") [] member _.NoOccupationDialogueKeepsTheHistoricResponsePath () = let world = { P31Harness.nearWorld () with Tick = 5L * Sim.ticksPerDay } let view = M5Interaction.initial let openedWorld, openedView = M5Interaction.apply Interact world view match openedView.Menu with | None -> Assert.Fail("dialogue menu should open next to a villager") | Some menu -> let personality = openedWorld.Npcs.[P31Harness.npcIndex menu.Target].Mind.Personality let finalWorld, _ = M5Interaction.apply Intent1 openedWorld openedView let expected = Sim.responseFor SmallTalk personality Assert.IsTrue( finalWorld.Npcs.[0].Mind.Memory |> List.exists (fun memory -> match memory.Kind with | Dialogue (_, SmallTalk, response) -> response = expected | _ -> false)) // 无职业不产生剧情条目,剧情线状态不存在 Assert.IsFalse( finalWorld.Annals |> List.exists (fun entry -> match entry.Kind with | StoryAnnal -> true | _ -> false))