namespace LivingVillage.Kernel.Tests open Microsoft.VisualStudio.TestTools.UnitTesting open LivingVillage.Kernel open LivingVillage.Kernel.Sim module private RumorHarness = let private needs = { Hunger = 100.0f Energy = 100.0f Social = 100.0f Money = 50.0f } let private personality = { Drive = 0.5f Aggression = 0.0f Extraversion = 0.8f Honesty = 0.5f Greed = 0.5f } let private npc id = { Id = NpcId id Pos = { X = float32 (id * 10); Y = 0.0f } Inventory = Map.empty Mind = { Needs = needs Personality = personality Action = Wander Target = actionTarget Wander ActionAge = minActionTicks EffectDone = true HungerFlagged = false Memory = [] } } let world tick count = { Tick = tick Time = float tick * dtSeconds Rng = Rng.ofSeed 11UL Avatar = { Pos = { X = 0.0f; Y = 0.0f } Mind = { Needs = needs Personality = personality Action = Wander Target = actionTarget Wander ActionAge = 0L EffectDone = false HungerFlagged = false Memory = [] } } NoHost = { Reserved = 0UL } Npcs = Array.init count npc Events = [] Rumors = [] Annals = [] } let readyForChat (world: World) : World = { world with Npcs = world.Npcs |> Array.map (fun npc -> { npc with Mind = { npc.Mind with Action = Wander Target = actionTarget Wander ActionAge = minActionTicks EffectDone = true } }) } let chat narrator receiver world = match Sim.chat { Narrator = NpcId narrator; Receiver = NpcId receiver } world with | ChatSucceeded (_, next) -> next | ChatRejected (failure, _) -> Assert.Fail($"chat rejected: {failure}"); Unchecked.defaultof let rumorId (RumorId id) = id let orderedRumors (world: World) = world.Rumors |> List.sortBy (fun rumor -> rumorId rumor.Id) let traceText (world: World) = orderedRumors world |> List.map (fun rumor -> let parent = rumor.Parent |> Option.map rumorId |> Option.map string |> Option.defaultValue "-" sprintf "%d|%d|%d|%d|%d|%s|%d" (rumorId rumor.Id) rumor.Tick (match rumor.Source with NpcId id -> id) (match rumor.Narrator with NpcId id -> id) (match rumor.Receiver with NpcId id -> id) parent rumor.Depth) |> String.concat "\n" [] type RumorTests () = [] member _.IdleChatsCreateThreeHopTraceableRumorPathWithinTwoDays () = let initial = RumorHarness.world 0L 4 let first = RumorHarness.chat 0 1 initial let second = RumorHarness.chat 1 2 (RumorHarness.readyForChat { first with Tick = ticksPerDay }) let third = RumorHarness.chat 2 3 (RumorHarness.readyForChat { second with Tick = 2L * ticksPerDay }) let ordered = RumorHarness.orderedRumors third Assert.AreEqual(3, third.Rumors.Length) Assert.AreEqual(3, ordered.Length) let root = ordered.[0] let retelling = ordered.[1] let final = ordered.[2] Assert.AreEqual(NpcId 0, root.Source) Assert.AreEqual(NpcId 0, root.Narrator) Assert.AreEqual(NpcId 1, root.Receiver) Assert.IsTrue(root.Parent.IsNone) Assert.AreEqual(0, root.Depth) Assert.AreEqual(root.Id, retelling.Parent |> Option.get) Assert.AreEqual(root.Source, retelling.Source) Assert.AreEqual(NpcId 1, retelling.Narrator) Assert.AreEqual(NpcId 2, retelling.Receiver) Assert.AreEqual(1, retelling.Depth) Assert.AreEqual(retelling.Id, final.Parent |> Option.get) Assert.AreEqual(NpcId 2, final.Narrator) Assert.AreEqual(NpcId 3, final.Receiver) Assert.AreEqual(2, final.Depth) let path = Sim.rumorPath third final.Id CollectionAssert.AreEqual([| root; retelling; final |], List.toArray path) [] member _.DuplicateRetellingDoesNotCreateAnotherRumorNode () = let first = RumorHarness.chat 0 1 (RumorHarness.world 0L 3) let second = RumorHarness.chat 1 2 (RumorHarness.readyForChat first) let duplicate = RumorHarness.chat 1 2 (RumorHarness.readyForChat second) Assert.AreEqual(2, second.Rumors.Length) Assert.AreEqual(2, duplicate.Rumors.Length) Assert.AreEqual(RumorHarness.traceText second, RumorHarness.traceText duplicate) [] member _.StaleRumorIsNotUsedAsParentForAReTelling () = let first = RumorHarness.chat 0 1 (RumorHarness.world 0L 3) let stale = first |> RumorHarness.readyForChat |> fun world -> { world with Tick = rumorFreshnessTicks + 1L } let next = RumorHarness.chat 1 2 stale let latest = RumorHarness.orderedRumors next |> List.last Assert.IsTrue(latest.Parent.IsNone, "stale memory must not be reused as a parent") Assert.IsTrue(latest.Source = NpcId 1, "a stale rumor starts a new local source") Assert.IsTrue(Sim.rumorStrengthAt next.Tick (RumorHarness.orderedRumors next |> List.head) < rumorMinimumStrength) [] member _.InvalidParticipantsLeaveWorldUnchanged () = let before = RumorHarness.world 10L 2 let requests = [ { Narrator = NpcId 9; Receiver = NpcId 1 } { Narrator = NpcId 0; Receiver = NpcId 9 } { Narrator = NpcId 0; Receiver = NpcId 0 } ] for request in requests do match Sim.chat request before with | ChatRejected (_, unchanged) -> Assert.AreEqual(before, unchanged) | ChatSucceeded _ -> Assert.Fail($"expected invalid chat to be rejected: {request}") [] member _.SameSeedAndChatOperationsProduceIdenticalRumorTrace () = let run seed = let initial = { RumorHarness.world 0L 4 with Rng = Rng.ofSeed seed } let first = RumorHarness.chat 0 1 initial let second = RumorHarness.chat 1 2 (RumorHarness.readyForChat { first with Tick = ticksPerDay }) let third = RumorHarness.chat 2 3 (RumorHarness.readyForChat { second with Tick = 2L * ticksPerDay }) RumorHarness.traceText third Assert.AreEqual(run 4242UL, run 4242UL) [] member _.RumorTraceTextContainsStableMetadataAndReplayablePaths () = let initial = RumorHarness.world 0L 4 let first = RumorHarness.chat 0 1 initial let second = RumorHarness.chat 1 2 (RumorHarness.readyForChat { first with Tick = ticksPerDay }) let third = RumorHarness.chat 2 3 (RumorHarness.readyForChat { second with Tick = 2L * ticksPerDay }) let expected = String.concat "\n" [ sprintf "rumor_trace tick=%d count=3" (2L * ticksPerDay) "rumor id=0 tick=0 origin_tick=0 source=0 narrator=0 receiver=1 parent=- depth=0 strength=1.000000 path=0" sprintf "rumor id=1 tick=%d origin_tick=0 source=0 narrator=1 receiver=2 parent=0 depth=1 strength=0.500000 path=0,1" ticksPerDay sprintf "rumor id=2 tick=%d origin_tick=0 source=0 narrator=2 receiver=3 parent=1 depth=2 strength=0.250000 path=0,1,2" (2L * ticksPerDay) ] Assert.AreEqual(expected, Sim.rumorTraceText third)