namespace LivingVillage.Desktop.Tests open System open System.IO open Microsoft.VisualStudio.TestTools.UnitTesting open LivingVillage.Kernel open LivingVillage.Kernel.Sim open LivingVillage.Headless open LivingVillage.Desktop /// P52 职业切片二:物品侧车 → 玩家可用(docs/design-professions.md §2.4-2)。 /// (a) 玩家买/卖 = `Sim.quotePrice` → `Occupation.biasedQuote`,四职业偏置生效,背包/钱随之增减; /// (b) 交易后 v3 存档 round-trip 读回背包与钱一致; /// (c) 交易不进 `Sim.step` 数值路径,基线 digest 钉值 953775FA… 不变。 [] type P52ProfessionTradeTests () = let seed = 42UL let buildWorld (kind: Occupation.Kind) : World * Occupation.State = let state = WorldBootstrap.occupationStateFor seed kind 0L let world = WorldBootstrap.initialWorldWithOccupation false seed 4 (Some state) { world with Tick = 0L; Time = 0.0 }, state let npcId (world: World) = world.Npcs.[0].Id let money (world: World) = world.Avatar.Mind.Needs.Money let bag (state: Occupation.State) (item: ItemKind) = Occupation.backpackQuantity item state let allKinds = [ Occupation.Farmer; Occupation.Fisher; Occupation.Peddler; Occupation.Scholar ] [] member _.QuoteAppliesProfessionBiasOverSimQuotePrice () = for kind in allKinds do let world, state = buildWorld kind let npc = npcId world let baseBuy = PlayerTrade.quoteBase (Some state) true npc Sim.Food 1 world let baseSell = PlayerTrade.quoteBase (Some state) false npc Sim.Food 1 world Assert.IsTrue(baseBuy.IsSome, "base buy quote expected") Assert.IsTrue(baseSell.IsSome, "base sell quote expected") Assert.AreEqual( baseBuy.Value * Occupation.quoteBiasOf (Some kind) true, (PlayerTrade.quote (Some state) true npc Sim.Food 1 world).Value) Assert.AreEqual( baseSell.Value * Occupation.quoteBiasOf (Some kind) false, (PlayerTrade.quote (Some state) false npc Sim.Food 1 world).Value) // 无职业 = 恒等 1.0(与 TradeBiasTests 锁定的口径一致) let worldNone = WorldBootstrap.initialWorldWithOccupation false seed 4 None let npcNone = worldNone.Npcs.[0].Id Assert.AreEqual( PlayerTrade.quoteBase None true npcNone Sim.Food 1 worldNone, PlayerTrade.quote None true npcNone Sim.Food 1 worldNone) [] member _.PeddlerAndScholarBiasDirectionsMatchDesign () = let check kind expectedBuy expectedSell = let world, state = buildWorld kind let npc = npcId world let baseBuy = (PlayerTrade.quoteBase (Some state) true npc Sim.Food 1 world).Value let baseSell = (PlayerTrade.quoteBase (Some state) false npc Sim.Food 1 world).Value Assert.AreEqual(baseBuy * expectedBuy, (PlayerTrade.quote (Some state) true npc Sim.Food 1 world).Value) Assert.AreEqual(baseSell * expectedSell, (PlayerTrade.quote (Some state) false npc Sim.Food 1 world).Value) check Occupation.Peddler 0.97f 1.03f check Occupation.Scholar 0.99f 1.01f check Occupation.Farmer 1.0f 1.0f check Occupation.Fisher 1.0f 1.0f [] member _.BuyAndSellUpdateBackpackAndMoney () = for kind in allKinds do let world, state = buildWorld kind let npc = npcId world let unitBuy = (PlayerTrade.quote (Some state) true npc Sim.Food 1 world).Value let money0 = money world let food0 = bag state Sim.Food let buy = PlayerTrade.buy (Some state) npc Sim.Food 1 world Assert.IsTrue(buy.Succeeded, sprintf "%A buy should succeed" kind) let stateAfterBuy = buy.Occupation.Value Assert.AreEqual(food0 + 1, bag stateAfterBuy Sim.Food) Assert.AreEqual(money0 - unitBuy, money buy.World) let unitSell = (PlayerTrade.quote (Some stateAfterBuy) false npc Sim.Food 1 buy.World).Value let moneyAfterBuy = money buy.World let sell = PlayerTrade.sell (Some stateAfterBuy) npc Sim.Food 1 buy.World Assert.IsTrue(sell.Succeeded, sprintf "%A sell should succeed" kind) let stateAfterSell = sell.Occupation.Value Assert.AreEqual(food0, bag stateAfterSell Sim.Food) Assert.AreEqual(moneyAfterBuy + unitSell, money sell.World) [] member _.TradeRejectsWithoutProfessionOrFunds () = let worldNone = WorldBootstrap.initialWorldWithOccupation false seed 4 None let noProfession = PlayerTrade.buy None (worldNone.Npcs.[0].Id) Sim.Food 1 worldNone Assert.IsFalse(noProfession.Succeeded) let world, state = buildWorld Occupation.Farmer let npc = npcId world let tooMuch = PlayerTrade.buy (Some state) npc Sim.Food 999 world Assert.IsFalse(tooMuch.Succeeded) // Farmer 背包有食物,卖首件成功;空背包卖首件失败 Assert.IsTrue((PlayerTrade.sellFirst (Some state) npc world).Succeeded) let emptied = { Occupation.stateOf Occupation.Farmer with Backpack = [] } Assert.IsFalse((PlayerTrade.sellFirst (Some emptied) npc world).Succeeded) [] member _.TradeSurvivesSaveRoundTrip () = let world, state = buildWorld Occupation.Peddler let npc = npcId world let buy = PlayerTrade.buy (Some state) npc Sim.Food 2 world Assert.IsTrue(buy.Succeeded) let path = Path.Combine(Path.GetTempPath(), "living-village-p52-" + Guid.NewGuid().ToString("N") + ".save") try WorldSave.saveToFileWith buy.Occupation path buy.World match WorldSave.loadFromFileWith path with | Ok (restored, Some restoredState) -> Assert.AreEqual<(ItemKind * int) list>(buy.Occupation.Value.Backpack, restoredState.Backpack) Assert.AreEqual(money buy.World, money restored) | Ok (_, None) -> Assert.Fail("occupation sidecar must round-trip") | Error message -> Assert.Fail(message) finally if File.Exists path then File.Delete path [] member _.SimBaselineDigestStaysPinned () = let world = PerformanceProbe.runSteps PerformanceProbe.defaultConfiguration Assert.AreEqual( "953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C", PerformanceProbe.worldDigest world)