summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests/P52ProfessionTradeTests.fs
blob: acae4831453f9e927d7536e378d1f882689697c9 (plain)
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)