namespace LivingVillage.Desktop.Tests open System open System.IO open Microsoft.VisualStudio.TestTools.UnitTesting open LivingVillage.Kernel open LivingVillage.Kernel.Sim open LivingVillage.Desktop open LivingVillage.Desktop.ChineseText /// P37 职业系统首切片验收回归(docs/design/profession-system.md §4): /// 渔夫首项钓鱼相关、职业任务生成确定性、开局属性差异、旧档兼容、中文 UI 选择。 [] type P37ProfessionTests () = let withTempSave (action: string -> unit) = let id = Guid.NewGuid().ToString("N") let path = Path.Combine(Path.GetTempPath(), $"living-village-p37-{id}.save") try action path finally if File.Exists path then File.Delete path let fisherPool : Occupation.TaskTemplateId list = [ Occupation.NightCatch; Occupation.SellFish; Occupation.MarketInquiry ] let sequence (seed: uint64) (kind: Occupation.Kind) (days: int64 list) : Occupation.TaskTemplateId list = days |> List.map (fun day -> (Occupation.dailyTaskOf seed (Occupation.occupationSeedOf kind) day kind).Value.TemplateId) [] member _.FisherFirstTaskIsFishingRelated () = let state = WorldBootstrap.occupationStateFor 42UL Occupation.Fisher 0L match state.Today with | Some task -> Assert.IsTrue(fisherPool |> List.contains task.TemplateId, sprintf "渔夫当日任务越池:%A" task.TemplateId) Assert.IsTrue( [ "夜捕"; "卖渔"; "询价" ] |> List.contains (Occupation.taskNameOf task.TemplateId), "渔夫首项必须是钓鱼相关任务") | None -> Assert.Fail("渔夫开局必须生成当日任务") [] member _.SameProfessionAndSeedProduceIdenticalTaskSequences () = let days = [ 0L .. 29L ] for kind in Occupation.all do let once = sequence 42UL kind days let twice = sequence 42UL kind days Assert.AreEqual(once, twice) for day in days do let task = (Occupation.dailyTaskOf 42UL (Occupation.occupationSeedOf kind) day kind).Value Assert.IsTrue(Occupation.taskPoolOf kind |> List.contains task.TemplateId, sprintf "day %d 越池" day) [] member _.DifferentProfessionsProduceDifferentTaskSequences () = let days = [ 0L .. 29L ] let fisher = sequence 42UL Occupation.Fisher days for kind in [ Occupation.Farmer; Occupation.Peddler; Occupation.Scholar ] do Assert.IsTrue(fisher <> sequence 42UL kind days, sprintf "渔夫与 %A 的任务序列不应相同" kind) [] member _.OpeningMoneyAndEnergyDifferPerProfession () = let needsOf (kind: Occupation.Kind) = let world = WorldBootstrap.initialWorldWithOccupation false 42UL 4 (Some(Occupation.stateOf kind)) world.Avatar.Mind.Needs Assert.AreEqual(60.0f, (needsOf Occupation.Farmer).Money) Assert.AreEqual(45.0f, (needsOf Occupation.Fisher).Money) Assert.AreEqual(90.0f, (needsOf Occupation.Peddler).Money) Assert.AreEqual(70.0f, (needsOf Occupation.Scholar).Money) Assert.AreEqual(95.0f, (needsOf Occupation.Fisher).Energy) for kind in [ Occupation.Farmer; Occupation.Peddler; Occupation.Scholar ] do Assert.AreEqual(100.0f, (needsOf kind).Energy) [] member _.OpeningInventoryBundleIsDefinedPerProfession () = Assert.AreEqual<(Sim.ItemKind * int) list>([ Sim.Food, 12 ], (Occupation.profileOf Occupation.Farmer).InitialInventory) Assert.AreEqual<(Sim.ItemKind * int) list>( [ Sim.Food, 8; Sim.Fish, 3 ], (Occupation.profileOf Occupation.Fisher).InitialInventory) Assert.AreEqual<(Sim.ItemKind * int) list>( [ Sim.Food, 6; Sim.Spice, 4 ], (Occupation.profileOf Occupation.Peddler).InitialInventory) Assert.AreEqual<(Sim.ItemKind * int) list>( [ Sim.Food, 10; Sim.Scroll, 2 ], (Occupation.profileOf Occupation.Scholar).InitialInventory) [] member _.LegacyAndNoProfessionSavesLoadAsNoOccupation () = let world = Sim.initialWorldN 42UL 4 withTempSave (fun path -> File.WriteAllText(path, WorldSave.save world) let v2Text = File.ReadAllText path Assert.IsTrue(v2Text.StartsWith("LV_WORLD_SAVE_V2"), "无职业存档应保持 v2") match WorldSave.loadFromFileWith path with | Ok (_, occupation) -> Assert.AreEqual(None, occupation) | Error message -> Assert.Fail(sprintf "v2 读取失败:%s" message) let firstPipe = v2Text.IndexOf('|') let secondPipe = v2Text.IndexOf('|', firstPipe + 1) let thirdPipe = v2Text.IndexOf('|', secondPipe + 1) File.WriteAllText(path, "LV_WORLD_SAVE_V1" + v2Text.Substring(thirdPipe)) match WorldSave.loadFromFileWith path with | Ok (_, occupation) -> Assert.AreEqual(None, occupation) | Error message -> Assert.Fail(sprintf "v1 读取失败:%s" message)) [] member _.OccupationSelectPageNavigatesAndEmitsChosenKind () = // 默认选中「暂不选择」(index 4);两次 Down 到「渔夫」(index 1),Enter 确认。 let page = MenuState.openOccupationSelect (MenuState.create SimulationControl.initial false) Assert.AreEqual(OccupationSelect, page.Page) Assert.AreEqual(None, MenuState.selectedOccupation page) let fisher = MenuState.move Down (MenuState.move Down page) Assert.AreEqual(Some Occupation.Fisher, MenuState.selectedOccupation fisher) let _, command = MenuState.update Confirm fisher Assert.AreEqual(StartNewGameWith(Some Occupation.Fisher), command) [] member _.OccupationUiLabelsAreAtlasBacked () = let labels = [ "选择职业"; "暂不选择"; Occupation.nameOf Occupation.Farmer; Occupation.nameOf Occupation.Fisher Occupation.nameOf Occupation.Peddler; Occupation.nameOf Occupation.Scholar ] for label in labels do Assert.IsTrue(requiredUiLabels |> List.contains label, sprintf "缺少必备用字登记:%s" label) Assert.IsTrue(PixelText.isRenderable label, sprintf "文案不可渲染(缺字):%s" label)