namespace LivingVillage.Kernel.Tests open Microsoft.VisualStudio.TestTools.UnitTesting open LivingVillage.Kernel open LivingVillage.Kernel.Sim module private M5Harness = let zeroInput = { Input = { MoveX = 0.0f; MoveY = 0.0f } } let atNpc (id: int) (world: World) : World = let npc = world.Npcs.[id] { world with Avatar = { world.Avatar with Pos = npc.Pos } } 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 atTick (tick: int64) (world: World) : World = { world with Tick = tick Time = float tick * dtSeconds } let chat (narrator: int) (receiver: int) (world: World) : World = match Sim.chat { Narrator = NpcId narrator; Receiver = NpcId receiver } world with | ChatSucceeded (_, next) -> next | ChatRejected (failure, _) -> Assert.Fail($"chat rejected: {failure}"); Unchecked.defaultof [] type M5Tests () = [] member _.AvatarNeedsDecayAndNeedsPanelUsesTheSameValues () = let before = Sim.initialWorld 42UL let after = Sim.step M5Harness.zeroInput before Assert.IsTrue(after.Avatar.Mind.Needs.Hunger < before.Avatar.Mind.Needs.Hunger) Assert.IsTrue(after.Avatar.Mind.Needs.Energy < before.Avatar.Mind.Needs.Energy) Assert.IsTrue(after.Avatar.Mind.Needs.Social < before.Avatar.Mind.Needs.Social) Assert.IsTrue(after.Avatar.Mind.Needs.Money < before.Avatar.Mind.Needs.Money) let panel = Sim.needsPanel after Assert.AreEqual(after.Avatar.Mind.Needs, panel.Needs) Assert.IsFalse(panel.Ranked.IsEmpty) [] member _.NearbyNpcOpensSixIntentDialogueMenu () = let world = Sim.initialWorldN 42UL 4 |> M5Harness.atNpc 0 match Sim.openDialogueMenu world with | Some menu -> Assert.AreEqual(NpcId 0, menu.Target) Assert.AreEqual(6, menu.Options.Length) Assert.IsTrue(menu.Options |> List.contains SmallTalk) Assert.IsTrue(menu.Options |> List.contains AskHelp) Assert.IsTrue(menu.Options |> List.contains OfferTrade) | None -> Assert.Fail("nearby NPC should open a dialogue menu") [] member _.SelectedIntentCreatesBilateralMemoryEventAndAnnal () = let world = Sim.initialWorldN 42UL 4 |> M5Harness.atNpc 0 match Sim.chooseDialogue (NpcId 0) SmallTalk world with | DialogueSucceeded (outcome, next) -> Assert.AreEqual(playerId, outcome.Actor) Assert.AreEqual(NpcId 0, outcome.Target) Assert.IsTrue(next.Avatar.Mind.Memory |> List.exists (fun e -> match e.Kind with | Dialogue (NpcId target, SmallTalk, response) -> target = 0 && response = outcome.Response | _ -> false)) Assert.IsTrue(next.Npcs.[0].Mind.Memory |> List.exists (fun e -> match e.Kind with | Dialogue (NpcId actor, SmallTalk, response) -> actor = -1 && response = outcome.Response | _ -> false)) Assert.IsTrue(next.Events |> List.exists (fun event -> match event.Kind with | DialogueEvent recorded -> recorded = outcome | _ -> false)) Assert.IsTrue(next.Annals |> List.exists (fun entry -> match entry.Kind with | DialogueAnnal recorded -> recorded = outcome | _ -> false)) Assert.IsTrue(Sim.annalText next |> fun text -> text.Contains("dialogue")) | DialogueRejected (failure, _) -> Assert.Fail($"dialogue rejected: {failure}") [] member _.PlayerSmallTalkPassesThroughThreeIntermediariesAndReturnsToPlayer () = let initial = Sim.initialWorldN 7UL 4 |> M5Harness.atNpc 0 |> M5Harness.readyForChat let afterPlayer = match Sim.chooseDialogue (NpcId 0) SmallTalk initial with | DialogueSucceeded (_, next) -> next | DialogueRejected (failure, _) -> Assert.Fail($"dialogue rejected: {failure}"); Unchecked.defaultof let first = afterPlayer |> M5Harness.atTick Sim.ticksPerDay |> M5Harness.readyForChat |> M5Harness.chat 0 1 let second = first |> M5Harness.atTick (2L * Sim.ticksPerDay) |> M5Harness.readyForChat |> M5Harness.chat 1 2 let third = second |> M5Harness.atTick (3L * Sim.ticksPerDay) |> M5Harness.readyForChat |> M5Harness.chat 2 -1 let finalRumor = third.Rumors |> List.sortBy (fun rumor -> rumor.Id) |> List.last let path = Sim.rumorPath third finalRumor.Id Assert.AreEqual(playerId, finalRumor.Source) Assert.AreEqual(playerId, finalRumor.Receiver) Assert.AreEqual(NpcId 2, finalRumor.Narrator) Assert.AreEqual(3, finalRumor.Depth) Assert.AreEqual(4, path.Length) Assert.AreEqual(playerId, path.Head.Source) Assert.AreEqual(playerId, path |> List.last |> fun rumor -> rumor.Receiver) [] member _.ObservationOnlyIncludesVisibleNpcsAndRanksUrgentNeeds () = let baseWorld = Sim.initialWorldN 42UL 2 let visibleNpc = { baseWorld.Npcs.[0] with Pos = { X = 10.0f; Y = 10.0f } Mind = { baseWorld.Npcs.[0].Mind with Needs = { baseWorld.Npcs.[0].Mind.Needs with Hunger = 5.0f; Money = 90.0f } } } let hiddenNpc = { baseWorld.Npcs.[1] with Pos = { X = 200.0f; Y = 200.0f } } let world = { baseWorld with Npcs = [| visibleNpc; hiddenNpc |] } let observations = Sim.observeVisible { Min = { X = 0.0f; Y = 0.0f } Max = { X = 100.0f; Y = 100.0f } } world Assert.AreEqual(1, observations.Length) Assert.AreEqual(NpcId 0, observations.Head.Id) Assert.AreEqual(HungerNeed, observations.Head.Ranked.Head.Kind)