diff options
Diffstat (limited to 'src/LivingVillage.Kernel.Tests')
4 files changed, 160 insertions, 0 deletions
diff --git a/src/LivingVillage.Kernel.Tests/DeterminismTests.fs b/src/LivingVillage.Kernel.Tests/DeterminismTests.fs index 972ce19..c37be67 100644 --- a/src/LivingVillage.Kernel.Tests/DeterminismTests.fs +++ b/src/LivingVillage.Kernel.Tests/DeterminismTests.fs @@ -233,6 +233,9 @@ type DeterminismTests () = | Rest -> 0.4f | Pay -> 0.2f | Hungry -> -0.4f + | Chatted _ -> Sim.chatValence + | Bought _ -> 0.3f + | Sold _ -> 0.3f if e.Valence <> expected then Assert.Fail($"valence/kind mismatch: {e.Kind} -> {e.Valence}") if e.Valence < -1.0f || e.Valence > 1.0f then diff --git a/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj b/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj index 65e50db..d805d73 100644 --- a/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj +++ b/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj @@ -8,6 +8,7 @@ <ItemGroup> <Compile Include="DeterminismTests.fs" /> <Compile Include="RelationTests.fs" /> + <Compile Include="TradeTests.fs" /> </ItemGroup> <ItemGroup> diff --git a/src/LivingVillage.Kernel.Tests/RelationTests.fs b/src/LivingVillage.Kernel.Tests/RelationTests.fs index dc67e6d..dc433a7 100644 --- a/src/LivingVillage.Kernel.Tests/RelationTests.fs +++ b/src/LivingVillage.Kernel.Tests/RelationTests.fs @@ -19,6 +19,7 @@ module RelationHarness = let mkNpc id mem = { Id = NpcId id Pos = { X = 0.0f; Y = 0.0f } + Inventory = Map.empty Mind = { Needs = needs Personality = personalityOfSeed 1UL @@ -163,6 +164,7 @@ type RelationTests () = let npc id pos action age effectDone memory = { Id = NpcId id Pos = pos + Inventory = Map.empty Mind = { Needs = needs Personality = personality diff --git a/src/LivingVillage.Kernel.Tests/TradeTests.fs b/src/LivingVillage.Kernel.Tests/TradeTests.fs new file mode 100644 index 0000000..c7c8ef4 --- /dev/null +++ b/src/LivingVillage.Kernel.Tests/TradeTests.fs @@ -0,0 +1,154 @@ +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 } } + NoHost = { Reserved = 0UL } + Npcs = + [| npc 0 buyerMoney buyerHunger buyerFood [] None + npc 1 sellerMoney sellerHunger sellerFood sellerMemory None |] + Events = [] } + + 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") |
