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 [] type TradeTests () = [] 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") [] 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}") [] 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}") [] 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")