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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
|
namespace LivingVillage.Desktop.Tests
open System
open System.IO
open Microsoft.VisualStudio.TestTools.UnitTesting
open LivingVillage.Kernel
open LivingVillage.Kernel.Sim
open LivingVillage.Desktop
/// P54 职业切片三:当日任务 → 玩家可见与可完成。
/// 完成度只由**真实世界事件/信号**驱动(`Occupation.applySignal`),不接受任何
/// 按日历索引硬编码的计数;Offered 须先接受为 Active。
[<TestClass>]
type P54ProfessionDailyTests () =
let mkTask template taskState : Occupation.DailyTask =
{ TemplateId = template
TargetNpc = None
TargetTile = None
OfferedTick = 0L
DueTick = Sim.ticksPerDay - 1L
State = taskState }
let stateWith template taskState : Occupation.State =
{ Occupation.stateOf Occupation.Farmer with Today = Some(mkTask template taskState) }
let stateWithout : Occupation.State =
{ Occupation.stateOf Occupation.Farmer with Today = None }
let dialogueEvent actor target : InteractionEvent =
{ Tick = 10L
Kind =
DialogueEvent
{ Tick = 10L
Actor = actor
Target = target
Intent = SmallTalk
Response = Friendly
Valence = 0.1f } }
let tradeEvent buyer seller item quantity : InteractionEvent =
{ Tick = 10L
Kind = TradeEvent(buyer, seller, item, quantity, 10.0f) }
let taskStateOf (state: Occupation.State) =
state.Today |> Option.map (fun task -> task.State)
[<TestMethod>]
member _.AcceptOfferedTaskActivatesItWithFeedback () =
let activated, message = TaskRuntime.accept (stateWith Occupation.HelpWork Occupation.Offered)
Assert.AreEqual<Occupation.TaskState option>(Some Occupation.Active, taskStateOf activated)
Assert.AreEqual<string option>(Some "已接受任务:帮工", message)
// 已在进行/终态:无变化、无反馈。
let active, activeMessage = TaskRuntime.accept (stateWith Occupation.HelpWork Occupation.Active)
Assert.AreEqual<Occupation.TaskState option>(Some Occupation.Active, taskStateOf active)
Assert.AreEqual<string option>(None, activeMessage)
let none, noneMessage = TaskRuntime.accept stateWithout
Assert.AreEqual<Occupation.TaskState option>(None, taskStateOf none)
Assert.AreEqual<string option>(None, noneMessage)
[<TestMethod>]
member _.DialogueEventsCompleteActiveDialogueTasks () =
let complete template =
let state = stateWith template Occupation.Active
let next, message = TaskRuntime.advanceWithEvent (dialogueEvent playerId (NpcId 3)) state
Assert.IsTrue((taskStateOf next = Some Occupation.Done), sprintf "%A 应完成" template)
Assert.IsTrue(message.IsSome, sprintf "%A 完成应有短反馈" template)
message.Value
for template in [ Occupation.HelpWork; Occupation.MarketInquiry; Occupation.ReasonDebate ] do
let message = complete template
Assert.AreEqual<string>(sprintf "任务完成:%s" (Occupation.taskNameOf template), message)
[<TestMethod>]
member _.TradeEventsCompleteActiveTradeTasks () =
let state = stateWith Occupation.Resell Occupation.Active
let sold, soldMessage = TaskRuntime.advanceWithEvent (tradeEvent (NpcId 1) playerId Spice 1) state
Assert.AreEqual<Occupation.TaskState option>(Some Occupation.Done, taskStateOf sold)
Assert.AreEqual<string option>(Some "任务完成:转卖", soldMessage)
let grain = stateWith Occupation.DeliverGrain Occupation.Active
Assert.AreEqual<Occupation.TaskState option>(
Some Occupation.Done,
taskStateOf (fst (TaskRuntime.advanceWithEvent (tradeEvent (NpcId 1) playerId Food 2) grain)))
let fish = stateWith Occupation.SellFish Occupation.Active
Assert.AreEqual<Occupation.TaskState option>(
Some Occupation.Done,
taskStateOf (fst (TaskRuntime.advanceWithEvent (tradeEvent (NpcId 1) playerId Fish 1) fish)))
let buy = stateWith Occupation.BuyGoods Occupation.Active
Assert.AreEqual<Occupation.TaskState option>(
Some Occupation.Done,
taskStateOf (fst (TaskRuntime.advanceWithEvent (tradeEvent playerId (NpcId 1) Food 1) buy)))
[<TestMethod>]
member _.EnvironmentSignalsOnlyFireOnRealChange () =
// 无基线不发到达信号(避免开局即完成翻土)。
Assert.AreEqual<Occupation.TaskSignal list>([], TaskRuntime.environmentSignals false false None (5, 5))
// 跨瓦片 → 到达瓦片。
Assert.AreEqual<Occupation.TaskSignal list>(
[ Occupation.ArrivedAt(6, 5) ],
TaskRuntime.environmentSignals false false (Some(5, 5)) (6, 5))
// 同瓦片不重复到达。
Assert.AreEqual<Occupation.TaskSignal list>([], TaskRuntime.environmentSignals false false (Some(6, 5)) (6, 5))
// 夜 + 水边 → 夜在水边。
Assert.AreEqual<Occupation.TaskSignal list>(
[ Occupation.NightAtWater ],
TaskRuntime.environmentSignals true true (Some(6, 5)) (6, 5))
[<TestMethod>]
member _.FoldedEnvironmentSignalsCompleteTheirTasks () =
let arrived = stateWith Occupation.TillSoil Occupation.Active
Assert.AreEqual<Occupation.TaskState option>(
Some Occupation.Done,
taskStateOf (fst (TaskRuntime.advanceWithSignal (Occupation.ArrivedAt(52, 38)) arrived)))
let night = stateWith Occupation.NightCatch Occupation.Active
Assert.AreEqual<Occupation.TaskState option>(
Some Occupation.Done,
taskStateOf (fst (TaskRuntime.advanceWithSignal Occupation.NightAtWater night)))
let observe = stateWith Occupation.ObserveNotes Occupation.Active
Assert.AreEqual<Occupation.TaskState option>(
Some Occupation.Done,
taskStateOf (fst (TaskRuntime.advanceWithSignal Occupation.Observed observe)))
[<TestMethod>]
member _.OfferedTaskNeedsAcceptBeforeItCanComplete () =
let offered = stateWith Occupation.HelpWork Occupation.Offered
let stillOffered, missed = TaskRuntime.advanceWithEvent (dialogueEvent playerId (NpcId 4)) offered
Assert.AreEqual<Occupation.TaskState option>(Some Occupation.Offered, taskStateOf stillOffered)
Assert.AreEqual<string option>(None, missed)
let active, _ = TaskRuntime.accept offered
let done', message = TaskRuntime.advanceWithEvent (dialogueEvent playerId (NpcId 4)) active
Assert.AreEqual<Occupation.TaskState option>(Some Occupation.Done, taskStateOf done')
Assert.IsTrue(message.IsSome)
[<TestMethod>]
member _.OnlyTheMatchingRealSignalCompletesAndNothingIsCalendarCounted () =
// 非玩家事件(NPC↔NPC)不得推进。
let active = stateWith Occupation.HelpWork Occupation.Active
let npcOnly, _ = TaskRuntime.advanceWithEvent (dialogueEvent (NpcId 1) (NpcId 2)) active
Assert.AreEqual<Occupation.TaskState option>(Some Occupation.Active, taskStateOf npcOnly)
let npcTrade, _ = TaskRuntime.advanceWithEvent (tradeEvent (NpcId 1) (NpcId 2) Food 1) (stateWith Occupation.Resell Occupation.Active)
Assert.AreEqual<Occupation.TaskState option>(Some Occupation.Active, taskStateOf npcTrade)
// 不匹配的信号不完成(观察任务不会被对话完成)。
let observe = stateWith Occupation.ObserveNotes Occupation.Active
let afterDialogue, _ = TaskRuntime.advanceWithEvent (dialogueEvent playerId (NpcId 1)) observe
Assert.AreEqual<Occupation.TaskState option>(Some Occupation.Active, taskStateOf afterDialogue)
// 反复推进也不改变 OfferedTick/DueTick(无日历计数副作用)。
let before = (stateWith Occupation.HelpWork Occupation.Active).Today.Value
let after = (fst (TaskRuntime.advanceWithEvent (dialogueEvent playerId (NpcId 1)) active)).Today.Value
Assert.AreEqual<int64>(before.OfferedTick, after.OfferedTick)
Assert.AreEqual<int64>(before.DueTick, after.DueTick)
[<TestMethod>]
member _.AcceptTaskCommandActivatesTheDailyTaskThroughTheUi () =
let world = Sim.initialWorldN 42UL 4
let view = { M5Interaction.initial with Panel = TaskPanel; Task = Some(stateWith Occupation.HelpWork Occupation.Offered) }
let _, accepted = M5Interaction.apply AcceptTask world view
Assert.AreEqual<Occupation.TaskState option>(Some Occupation.Active, taskStateOf accepted.Task.Value)
Assert.AreEqual<string>("已接受任务:帮工", M5Interaction.statusText accepted)
[<TestMethod>]
member _.ObserveCommandCompletesTheObserveNotesTask () =
let world = Sim.initialWorldN 42UL 4
let view = { M5Interaction.initial with Task = Some(stateWith Occupation.ObserveNotes Occupation.Active) }
let _, observed = M5Interaction.apply Observe world view
Assert.AreEqual<Occupation.TaskState option>(Some Occupation.Done, taskStateOf observed.Task.Value)
Assert.AreEqual<string>("任务完成:观察记事", M5Interaction.statusText observed)
[<TestMethod>]
member _.DialogueThroughTheMenuCompletesTheHelpWorkTask () =
let world = Sim.initialWorldN 42UL 4
let npc = world.Npcs.[0]
let placed = { world with Avatar = { world.Avatar with Pos = npc.Pos } }
let seeded = { M5Interaction.initial with Task = Some(stateWith Occupation.HelpWork Occupation.Active) }
let _, opened = M5Interaction.apply M5Command.Interact placed seeded
Assert.AreEqual<M5Panel>(DialoguePanel, opened.Panel)
let _, replied = M5Interaction.apply Intent1 placed opened
Assert.AreEqual<Occupation.TaskState option>(Some Occupation.Done, taskStateOf replied.Task.Value)
Assert.IsTrue((M5Interaction.statusText replied).StartsWith("任务完成"))
[<TestMethod>]
member _.PlayerTradeCompletesTheResellTask () =
let seed = 42UL
// 货郎背包含香料,才能卖出以完成「转卖」。
let state =
{ Occupation.stateOf Occupation.Peddler with
Today = Some(mkTask Occupation.Resell Occupation.Active) }
let world = WorldBootstrap.initialWorldWithOccupation false seed 4 (Some state)
let npc = world.Npcs.[0].Id
let sell = PlayerTrade.sell (Some state) npc Spice 1 world
Assert.IsTrue(sell.Succeeded)
Assert.AreEqual<Occupation.TaskState option>(Some Occupation.Done, taskStateOf sell.Occupation.Value)
[<TestMethod>]
member _.DailyTaskStatePersistsThroughV3RoundTripAndV2StaysCompatible () =
let world = Sim.initialWorldN 42UL 4
let state =
{ Occupation.stateOf Occupation.Peddler with
Today = Some(mkTask Occupation.Resell Occupation.Done) }
// v2(None)保持与旧体一致、无任务侧车。
let v2 = WorldSave.save world
Assert.IsTrue(v2.StartsWith("LV_WORLD_SAVE_V2"))
match WorldSave.load v2 with
| Ok restored -> Assert.AreEqual<string>(v2, WorldSave.save restored)
| Error message -> Assert.Fail(message)
// v3 round-trip:任务模板 + 终态读回一致。
let path = Path.Combine(Path.GetTempPath(), "living-village-p54-" + Guid.NewGuid().ToString("N") + ".save")
try
WorldSave.saveToFileWith (Some state) path world
match WorldSave.loadFromFileWith path with
| Ok (_, Some restored) ->
Assert.AreEqual<Occupation.DailyTask option>(state.Today, restored.Today)
Assert.AreEqual<Occupation.TaskState option>(Some Occupation.Done, taskStateOf restored)
| Ok (_, None) -> Assert.Fail("v3 must round-trip the occupation sidecar")
| Error message -> Assert.Fail(message)
finally
if File.Exists path then File.Delete path
|