diff options
Diffstat (limited to 'src/LivingVillage.Desktop.Tests/P54ProfessionDailyTests.fs')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/P54ProfessionDailyTests.fs | 217 |
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 |
