1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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)
|