summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel.Tests/TradeTests.fs
blob: fa40ac392ffab1538665bd3f197659fc1d7fd717 (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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
namespace LivingVillage.Kernel.Tests

open Microsoft.VisualStudio.TestTools.UnitTesting
open LivingVillage.Kernel
open LivingVillage.Kernel.Sim

module private TradeHarness =

    let private personality =
        { Drive = 0.5f
          Aggression = 0.0f
          Extraversion = 0.5f
          Honesty = 0.5f
          Greed = 0.5f }

    let private needs money hunger =
        { Hunger = hunger
          Energy = 100.0f
          Social = 100.0f
          Money = money }

    let private inventory food = Map.ofList [ Food, food ]

    let private npc id money hunger food memory personalityOverride =
        { Id = NpcId id
          Pos = { X = float32 (id * 10); Y = 0.0f }
          Inventory = inventory food
          Mind =
            { Needs = needs money hunger
              Personality = defaultArg personalityOverride personality
              Action = Wander
              Target = actionTarget Wander
              ActionAge = 0L
              EffectDone = false
              HungerFlagged = false
              Memory = memory } }

    let world buyerMoney buyerHunger buyerFood sellerMoney sellerHunger sellerFood sellerMemory =
        { Tick = 10L
          Time = 0.0
          Rng = Rng.ofSeed 7UL
          Avatar =
              { Pos = { X = 0.0f; Y = 0.0f }
                Mind =
                    { Needs = needs 50.0f 100.0f
                      Personality = personality
                      Action = Wander
                      Target = actionTarget Wander
                      ActionAge = 0L
                      EffectDone = false
                      HungerFlagged = false
                      Memory = [] } }
          NoHost = { Reserved = 0UL }
          Npcs =
              [| npc 0 buyerMoney buyerHunger buyerFood [] None
                 npc 1 sellerMoney sellerHunger sellerFood sellerMemory None |]
          Events = []
          Rumors = []
          RumorCount = 0
          RumorOldestDay = System.Int64.MaxValue
          Annals = [] }

    let request quantity =
        { Buyer = NpcId 0
          Seller = NpcId 1
          Item = Food
          Quantity = quantity }

    let withSellerGreed greed world =
        let seller = world.Npcs.[1]
        { world with
            Npcs =
                [| world.Npcs.[0]
                   { seller with
                       Mind =
                         { seller.Mind with
                             Personality = { seller.Mind.Personality with Greed = greed } } } |] }

    let succeeded result =
        match result with
        | TradeSucceeded world -> world
        | TradeRejected (failure, _) -> Assert.Fail($"expected success, got {failure}"); Unchecked.defaultof<World>

[<TestClass>]
type TradeTests () =

    [<TestMethod>]
    member _.SuccessfulTradeTransfersItemMoneyAndMemoryAtomically () =
        let before = TradeHarness.world 50.0f 30.0f 0 10.0f 80.0f 3 []
        let request = TradeHarness.request 2
        let unitPrice = Sim.quotePrice request before |> Option.get
        let after = Sim.trade request before |> TradeHarness.succeeded
        let total = unitPrice * 2.0f

        Assert.IsTrue(after.Npcs.[0].Inventory.[Food] = 2)
        Assert.IsTrue(after.Npcs.[1].Inventory.[Food] = 1)
        Assert.IsTrue(abs (after.Npcs.[0].Mind.Needs.Money - (50.0f - total)) < 1e-5f)
        Assert.IsTrue(abs (after.Npcs.[1].Mind.Needs.Money - (10.0f + total)) < 1e-5f)
        Assert.IsTrue(abs ((after.Npcs.[0].Mind.Needs.Money + after.Npcs.[1].Mind.Needs.Money) - 60.0f) < 1e-5f)
        Assert.IsTrue(after.Npcs.[0].Mind.Memory |> List.exists (fun e ->
            match e.Kind with
            | Bought (NpcId seller, Food, quantity, price) -> seller = 1 && quantity = 2 && price = unitPrice
            | _ -> false))
        Assert.IsTrue(after.Npcs.[1].Mind.Memory |> List.exists (fun e ->
            match e.Kind with
            | Sold (NpcId buyer, Food, quantity, price) -> buyer = 0 && quantity = 2 && price = unitPrice
            | _ -> false))
        Assert.IsTrue(after.Events |> List.exists (fun event ->
            match event.Kind with
            | TradeEvent (NpcId buyer, NpcId seller, Food, quantity, price) ->
                buyer = 0 && seller = 1 && quantity = 2 && price = unitPrice
            | _ -> false))
        Assert.IsTrue(before.Npcs.[0].Inventory.[Food] = 0, "trade must not mutate the input world")
        Assert.IsTrue(before.Npcs.[1].Inventory.[Food] = 3, "trade must not mutate the input world")

    [<TestMethod>]
    member _.RejectedTradesLeaveWorldUnchanged () =
        let before = TradeHarness.world 50.0f 30.0f 0 10.0f 80.0f 3 []
        let cases =
            [ TradeHarness.request 0, InvalidQuantity
              TradeHarness.request 4, OutOfStock
              { TradeHarness.request 1 with Seller = NpcId 0 }, SameParticipant
              TradeHarness.request 1, InsufficientFunds ]
        for request, expected in cases do
            let candidate =
                if expected = InsufficientFunds then
                    TradeHarness.world 1.0f 30.0f 0 10.0f 80.0f 3 []
                else before
            match Sim.trade request candidate with
            | TradeRejected (actual, unchanged) ->
                if actual <> expected then Assert.Fail($"expected {expected}, got {actual}")
                if unchanged <> candidate then Assert.Fail("rejected trade changed the world")
            | TradeSucceeded _ -> Assert.Fail($"expected rejection {expected}")

    [<TestMethod>]
    member _.UnknownParticipantsAreRejectedWithoutChangingWorld () =
        let before = TradeHarness.world 50.0f 30.0f 0 10.0f 80.0f 3 []
        let missingBuyer = { TradeHarness.request 1 with Buyer = NpcId 9 }
        let missingSeller = { TradeHarness.request 1 with Seller = NpcId 9 }
        match Sim.trade missingBuyer before with
        | TradeRejected (BuyerNotFound, unchanged) ->
            if unchanged <> before then Assert.Fail("missing buyer changed the world")
        | other -> Assert.Fail($"expected BuyerNotFound, got {other}")
        match Sim.trade missingSeller before with
        | TradeRejected (SellerNotFound, unchanged) ->
            if unchanged <> before then Assert.Fail("missing seller changed the world")
        | other -> Assert.Fail($"expected SellerNotFound, got {other}")

    [<TestMethod>]
    member _.QuoteIsDeterministicAndRespondsToNeedStockPersonalityAndRecentTrades () =
        let request = TradeHarness.request 1
        let baseWorld = TradeHarness.world 50.0f 100.0f 0 50.0f 100.0f 20 []
        let urgentWorld = TradeHarness.world 50.0f 0.0f 0 50.0f 100.0f 20 []
        let scarceWorld = TradeHarness.world 50.0f 100.0f 0 50.0f 100.0f 1 []
        let greedySeller = TradeHarness.withSellerGreed 1.0f baseWorld
        let recentMemory = [ { Tick = 9L; Kind = Sold(NpcId 0, Food, 1, 40.0f); Valence = 0.2f } ]
        let recentWorld = TradeHarness.world 50.0f 100.0f 0 50.0f 100.0f 20 recentMemory
        let p0 = Sim.quotePrice request baseWorld |> Option.get
        let p1 = Sim.quotePrice request baseWorld |> Option.get
        let urgent = Sim.quotePrice request urgentWorld |> Option.get
        let scarce = Sim.quotePrice request scarceWorld |> Option.get
        let greedy = Sim.quotePrice request greedySeller |> Option.get
        let recent = Sim.quotePrice request recentWorld |> Option.get
        if p0 <> p1 then Assert.Fail("same world and request must produce the same quote")
        if urgent <= p0 then Assert.Fail("urgent buyer need must raise the quote")
        if scarce <= p0 then Assert.Fail("scarce seller stock must raise the quote")
        if greedy <= p0 then Assert.Fail("seller greed must affect the quote")
        if recent <= p0 then Assert.Fail("recent transaction prices must affect the quote")