summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests/P54ProfessionDailyTests.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop.Tests/P54ProfessionDailyTests.fs')
-rw-r--r--src/LivingVillage.Desktop.Tests/P54ProfessionDailyTests.fs217
1 files changed, 217 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop.Tests/P54ProfessionDailyTests.fs b/src/LivingVillage.Desktop.Tests/P54ProfessionDailyTests.fs
new file mode 100644
index 0000000..5d30921
--- /dev/null
+++ b/src/LivingVillage.Desktop.Tests/P54ProfessionDailyTests.fs
@@ -0,0 +1,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