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")
|