summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests/P52ProfessionTradeTests.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 23:06:20 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 23:06:20 +0800
commit07f2e072a14cc74133b187bb8715758790cdfe67 (patch)
tree715efbba87d3be156e927ed5c3ed857a274ea1ae /src/LivingVillage.Desktop.Tests/P52ProfessionTradeTests.fs
parenta26ba18e9710dcdac57d942d2d7ac1ce1e47ca1a (diff)
downloadliving-village-07f2e072a14cc74133b187bb8715758790cdfe67.tar.gz
p52: 玩家交易通道——买/卖经 Sim.quotePrice→Occupation.biasedQuote,背包侧车/钱财增减 + v3 round-trip + HUD 短反馈(design §2.4-2)
Diffstat (limited to 'src/LivingVillage.Desktop.Tests/P52ProfessionTradeTests.fs')
-rw-r--r--src/LivingVillage.Desktop.Tests/P52ProfessionTradeTests.fs125
1 files changed, 125 insertions, 0 deletions
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… 不变。
+[<TestClass>]
+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 ]
+
+ [<TestMethod>]
+ 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<float32>(
+ baseBuy.Value * Occupation.quoteBiasOf (Some kind) true,
+ (PlayerTrade.quote (Some state) true npc Sim.Food 1 world).Value)
+ Assert.AreEqual<float32>(
+ 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<float32 option>(
+ PlayerTrade.quoteBase None true npcNone Sim.Food 1 worldNone,
+ PlayerTrade.quote None true npcNone Sim.Food 1 worldNone)
+
+ [<TestMethod>]
+ 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<float32>(baseBuy * expectedBuy, (PlayerTrade.quote (Some state) true npc Sim.Food 1 world).Value)
+ Assert.AreEqual<float32>(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
+
+ [<TestMethod>]
+ 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<int>(food0 + 1, bag stateAfterBuy Sim.Food)
+ Assert.AreEqual<float32>(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<int>(food0, bag stateAfterSell Sim.Food)
+ Assert.AreEqual<float32>(moneyAfterBuy + unitSell, money sell.World)
+
+ [<TestMethod>]
+ 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)
+
+ [<TestMethod>]
+ 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<float32>(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
+
+ [<TestMethod>]
+ member _.SimBaselineDigestStaysPinned () =
+ let world = PerformanceProbe.runSteps PerformanceProbe.defaultConfiguration
+ Assert.AreEqual<string>(
+ "953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C",
+ PerformanceProbe.worldDigest world)