1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
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
[<TestClass>]
type P31StoryUiTests () =
// ---- 任务面板:剧情线名 + 进度段名 ----
[<TestMethod>]
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}")
[<TestMethod>]
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("剧情线")))
// ---- 对话接线:回应偏差进记忆 + 剧情线推进 + 年鉴写入 ----
[<TestMethod>]
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<int>(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")
[<TestMethod>]
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))
|