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 = [] RumorCount = 0 RumorOldestDay = System.Int64.MaxValue 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" let rumor id tick dayIndex = { Id = RumorId id Tick = tick DayIndex = dayIndex OriginTick = tick Source = NpcId 0 Narrator = NpcId 0 Receiver = NpcId 1 Parent = None Depth = 0 Strength = 1.0f } let withRumorBudget capacity retentionDays action = let previousCapacity = rumorCapacity let previousRetention = rumorRetentionDays rumorCapacity <- capacity rumorRetentionDays <- retentionDays try action () finally rumorCapacity <- previousCapacity rumorRetentionDays <- previousRetention [] 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) [] member _.TrimWithinCapacityAndFreshWindowIsZeroCopy () = RumorHarness.withRumorBudget 4 3L (fun () -> let rumors = [ RumorHarness.rumor 3L (3L * ticksPerDay) 3L RumorHarness.rumor 2L (2L * ticksPerDay) 2L RumorHarness.rumor 1L (1L * ticksPerDay) 1L ] let trimmed = Sim.trimRumors (3L * ticksPerDay) rumors Assert.IsTrue(System.Object.ReferenceEquals(rumors, trimmed), "within budget the list must be returned as-is") Assert.AreEqual(rumors, trimmed)) [] member _.TrimCapsToNewestRumors () = RumorHarness.withRumorBudget 4 30L (fun () -> let rumors = List.init 6 (fun index -> RumorHarness.rumor (int64 (5 - index)) (10L * ticksPerDay) 10L) let trimmed = Sim.trimRumors (10L * ticksPerDay) rumors Assert.AreEqual(4, trimmed.Length) let keptIds = trimmed |> List.map (fun rumor -> RumorHarness.rumorId rumor.Id) CollectionAssert.AreEqual([| 5L; 4L; 3L; 2L |], List.toArray keptIds)) [] member _.TrimDropsRumorsOlderThanRetentionWindow () = RumorHarness.withRumorBudget 100 3L (fun () -> let rumors = [ RumorHarness.rumor 3L (10L * ticksPerDay) 10L RumorHarness.rumor 2L (9L * ticksPerDay) 9L RumorHarness.rumor 1L (6L * ticksPerDay) 6L RumorHarness.rumor 0L (5L * ticksPerDay) 5L ] let trimmed = Sim.trimRumors (10L * ticksPerDay) rumors let keptIds = trimmed |> List.map (fun rumor -> RumorHarness.rumorId rumor.Id) CollectionAssert.AreEqual([| 3L; 2L |], List.toArray keptIds) Assert.AreEqual(trimmed, Sim.trimRumors (10L * ticksPerDay) trimmed)) [] member _.ChatKeepsWorkingSetWithinCapacityAndIdsStayMonotonic () = RumorHarness.withRumorBudget 1 30L (fun () -> let first = RumorHarness.chat 0 1 (RumorHarness.world 0L 4) let second = RumorHarness.chat 1 2 (RumorHarness.readyForChat first) Assert.AreEqual(1, first.Rumors.Length) Assert.AreEqual(1, second.Rumors.Length) let head = second.Rumors |> List.head Assert.AreEqual(1L, RumorHarness.rumorId head.Id) Assert.AreEqual(Some(RumorId 0L), head.Parent)) [] member _.CappedRumorsWithinCapacitySaveByteIdenticallyToUncapped () = let run capacity = RumorHarness.withRumorBudget capacity 30L (fun () -> let first = RumorHarness.chat 0 1 (RumorHarness.world 0L 4) let second = RumorHarness.chat 1 2 (RumorHarness.readyForChat { first with Tick = ticksPerDay }) WorldSave.save second) Assert.AreEqual(run 8, run System.Int32.MaxValue) [] member _.RumorDayIndexMirrorsTickAndSurvivesSaveRoundTrip () = let world = RumorHarness.chat 0 1 (RumorHarness.world 0L 3) let rumor = world.Rumors |> List.head Assert.AreEqual(rumor.Tick / ticksPerDay, rumor.DayIndex) match WorldSave.load (WorldSave.save world) with | Error failure -> Assert.Fail($"round trip should load: {failure}") | Ok restored -> Assert.AreEqual(world.Rumors, restored.Rumors) Assert.AreEqual(rumor.DayIndex, (restored.Rumors |> List.head).DayIndex)