From 07f2e072a14cc74133b187bb8715758790cdfe67 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 28 Sep 2026 23:06:20 +0800 Subject: p52: 玩家交易通道——买/卖经 Sim.quotePrice→Occupation.biasedQuote,背包侧车/钱财增减 + v3 round-trip + HUD 短反馈(design §2.4-2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../P52ProfessionTradeTests.fs | 125 +++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/LivingVillage.Desktop.Tests/P52ProfessionTradeTests.fs (limited to 'src/LivingVillage.Desktop.Tests/P52ProfessionTradeTests.fs') diff --git a/src/LivingVillage.Desktop.Tests/P52ProfessionTradeTests.fs b/src/LivingVillage.Desktop.Tests/P52ProfessionTradeTests.fs new file mode 100644 index 0000000..acae483 --- /dev/null +++ b/src/LivingVillage.Desktop.Tests/P52ProfessionTradeTests.fs @@ -0,0 +1,125 @@ +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) -- cgit v1.2.3