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。 [] 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) [] member _.AcceptOfferedTaskActivatesItWithFeedback () = let activated, message = TaskRuntime.accept (stateWith Occupation.HelpWork Occupation.Offered) Assert.AreEqual(Some Occupation.Active, taskStateOf activated) Assert.AreEqual(Some "已接受任务:帮工", message) // 已在进行/终态:无变化、无反馈。 let active, activeMessage = TaskRuntime.accept (stateWith Occupation.HelpWork Occupation.Active) Assert.AreEqual(Some Occupation.Active, taskStateOf active) Assert.AreEqual(None, activeMessage) let none, noneMessage = TaskRuntime.accept stateWithout Assert.AreEqual(None, taskStateOf none) Assert.AreEqual(None, noneMessage) [] 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(sprintf "任务完成:%s" (Occupation.taskNameOf template), message) [] member _.TradeEventsCompleteActiveTradeTasks () = let state = stateWith Occupation.Resell Occupation.Active let sold, soldMessage = TaskRuntime.advanceWithEvent (tradeEvent (NpcId 1) playerId Spice 1) state Assert.AreEqual(Some Occupation.Done, taskStateOf sold) Assert.AreEqual(Some "任务完成:转卖", soldMessage) let grain = stateWith Occupation.DeliverGrain Occupation.Active Assert.AreEqual( Some Occupation.Done, taskStateOf (fst (TaskRuntime.advanceWithEvent (tradeEvent (NpcId 1) playerId Food 2) grain))) let fish = stateWith Occupation.SellFish Occupation.Active Assert.AreEqual( Some Occupation.Done, taskStateOf (fst (TaskRuntime.advanceWithEvent (tradeEvent (NpcId 1) playerId Fish 1) fish))) let buy = stateWith Occupation.BuyGoods Occupation.Active Assert.AreEqual( Some Occupation.Done, taskStateOf (fst (TaskRuntime.advanceWithEvent (tradeEvent playerId (NpcId 1) Food 1) buy))) [] member _.EnvironmentSignalsOnlyFireOnRealChange () = // 无基线不发到达信号(避免开局即完成翻土)。 Assert.AreEqual([], TaskRuntime.environmentSignals false false None (5, 5)) // 跨瓦片 → 到达瓦片。 Assert.AreEqual( [ Occupation.ArrivedAt(6, 5) ], TaskRuntime.environmentSignals false false (Some(5, 5)) (6, 5)) // 同瓦片不重复到达。 Assert.AreEqual([], TaskRuntime.environmentSignals false false (Some(6, 5)) (6, 5)) // 夜 + 水边 → 夜在水边。 Assert.AreEqual( [ Occupation.NightAtWater ], TaskRuntime.environmentSignals true true (Some(6, 5)) (6, 5)) [] member _.FoldedEnvironmentSignalsCompleteTheirTasks () = let arrived = stateWith Occupation.TillSoil Occupation.Active Assert.AreEqual( Some Occupation.Done, taskStateOf (fst (TaskRuntime.advanceWithSignal (Occupation.ArrivedAt(52, 38)) arrived))) let night = stateWith Occupation.NightCatch Occupation.Active Assert.AreEqual( Some Occupation.Done, taskStateOf (fst (TaskRuntime.advanceWithSignal Occupation.NightAtWater night))) let observe = stateWith Occupation.ObserveNotes Occupation.Active Assert.AreEqual( Some Occupation.Done, taskStateOf (fst (TaskRuntime.advanceWithSignal Occupation.Observed observe))) [] member _.OfferedTaskNeedsAcceptBeforeItCanComplete () = let offered = stateWith Occupation.HelpWork Occupation.Offered let stillOffered, missed = TaskRuntime.advanceWithEvent (dialogueEvent playerId (NpcId 4)) offered Assert.AreEqual(Some Occupation.Offered, taskStateOf stillOffered) Assert.AreEqual(None, missed) let active, _ = TaskRuntime.accept offered let done', message = TaskRuntime.advanceWithEvent (dialogueEvent playerId (NpcId 4)) active Assert.AreEqual(Some Occupation.Done, taskStateOf done') Assert.IsTrue(message.IsSome) [] member _.OnlyTheMatchingRealSignalCompletesAndNothingIsCalendarCounted () = // 非玩家事件(NPC↔NPC)不得推进。 let active = stateWith Occupation.HelpWork Occupation.Active let npcOnly, _ = TaskRuntime.advanceWithEvent (dialogueEvent (NpcId 1) (NpcId 2)) active Assert.AreEqual(Some Occupation.Active, taskStateOf npcOnly) let npcTrade, _ = TaskRuntime.advanceWithEvent (tradeEvent (NpcId 1) (NpcId 2) Food 1) (stateWith Occupation.Resell Occupation.Active) Assert.AreEqual(Some Occupation.Active, taskStateOf npcTrade) // 不匹配的信号不完成(观察任务不会被对话完成)。 let observe = stateWith Occupation.ObserveNotes Occupation.Active let afterDialogue, _ = TaskRuntime.advanceWithEvent (dialogueEvent playerId (NpcId 1)) observe Assert.AreEqual(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(before.OfferedTick, after.OfferedTick) Assert.AreEqual(before.DueTick, after.DueTick) [] 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(Some Occupation.Active, taskStateOf accepted.Task.Value) Assert.AreEqual("已接受任务:帮工", M5Interaction.statusText accepted) [] 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(Some Occupation.Done, taskStateOf observed.Task.Value) Assert.AreEqual("任务完成:观察记事", M5Interaction.statusText observed) [] 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(DialoguePanel, opened.Panel) let _, replied = M5Interaction.apply Intent1 placed opened Assert.AreEqual(Some Occupation.Done, taskStateOf replied.Task.Value) Assert.IsTrue((M5Interaction.statusText replied).StartsWith("任务完成")) [] 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(Some Occupation.Done, taskStateOf sell.Occupation.Value) [] 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(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(state.Today, restored.Today) Assert.AreEqual(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