summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests/P54ProfessionDailyTests.fs
blob: 5d30921c3192e8a588e6ed28aaba7934a0e5e9d5 (plain)
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