summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel.Tests
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Kernel.Tests')
-rw-r--r--src/LivingVillage.Kernel.Tests/DeterminismTests.fs19
-rw-r--r--src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj1
-rw-r--r--src/LivingVillage.Kernel.Tests/M5Tests.fs141
-rw-r--r--src/LivingVillage.Kernel.Tests/RelationTests.fs30
-rw-r--r--src/LivingVillage.Kernel.Tests/RumorTests.fs15
-rw-r--r--src/LivingVillage.Kernel.Tests/TradeTests.fs19
6 files changed, 211 insertions, 14 deletions
diff --git a/src/LivingVillage.Kernel.Tests/DeterminismTests.fs b/src/LivingVillage.Kernel.Tests/DeterminismTests.fs
index cc1944f..52cae89 100644
--- a/src/LivingVillage.Kernel.Tests/DeterminismTests.fs
+++ b/src/LivingVillage.Kernel.Tests/DeterminismTests.fs
@@ -233,10 +233,21 @@ type DeterminismTests () =
| Rest -> 0.4f
| Pay -> 0.2f
| Hungry -> -0.4f
- | Chatted _ -> Sim.chatValence
- | Rumor _ -> 0.1f
- | Bought _ -> 0.3f
- | Sold _ -> 0.3f
+ | Chatted _ -> Sim.chatValence
+ | Rumor _ -> 0.1f
+ | Bought _ -> 0.3f
+ | Sold _ -> 0.3f
+ | Dialogue (_, _, response) ->
+ match response with
+ | Friendly -> 0.4f
+ | Helpful -> 0.5f
+ | Bargaining -> 0.2f
+ | Amused -> 0.35f
+ | Forgiving -> 0.45f
+ | Hostile -> -0.5f
+ | Reserved -> 0.0f
+ | Refused -> -0.1f
+ | Offended -> -0.4f
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 f65200b..2a3ba3c 100644
--- a/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj
+++ b/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj
@@ -10,6 +10,7 @@
<Compile Include="RelationTests.fs" />
<Compile Include="RumorTests.fs" />
<Compile Include="TradeTests.fs" />
+ <Compile Include="M5Tests.fs" />
</ItemGroup>
<ItemGroup>
diff --git a/src/LivingVillage.Kernel.Tests/M5Tests.fs b/src/LivingVillage.Kernel.Tests/M5Tests.fs
new file mode 100644
index 0000000..5eb1501
--- /dev/null
+++ b/src/LivingVillage.Kernel.Tests/M5Tests.fs
@@ -0,0 +1,141 @@
+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<World>
+
+[<TestClass>]
+type M5Tests () =
+
+ [<TestMethod>]
+ 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<Needs>(after.Avatar.Mind.Needs, panel.Needs)
+ Assert.IsFalse(panel.Ranked.IsEmpty)
+
+ [<TestMethod>]
+ member _.NearbyNpcOpensSixIntentDialogueMenu () =
+ let world = Sim.initialWorldN 42UL 4 |> M5Harness.atNpc 0
+ match Sim.openDialogueMenu world with
+ | Some menu ->
+ Assert.AreEqual<NpcId>(NpcId 0, menu.Target)
+ Assert.AreEqual<int>(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")
+
+ [<TestMethod>]
+ member _.SelectedIntentCreatesBilateralMemoryEventAndAnnal () =
+ let world = Sim.initialWorldN 42UL 4 |> M5Harness.atNpc 0
+ match Sim.chooseDialogue (NpcId 0) SmallTalk world with
+ | DialogueSucceeded (outcome, next) ->
+ Assert.AreEqual<NpcId>(playerId, outcome.Actor)
+ Assert.AreEqual<NpcId>(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}")
+
+ [<TestMethod>]
+ 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<World>
+ 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<NpcId>(playerId, finalRumor.Source)
+ Assert.AreEqual<NpcId>(playerId, finalRumor.Receiver)
+ Assert.AreEqual<NpcId>(NpcId 2, finalRumor.Narrator)
+ Assert.AreEqual<int>(3, finalRumor.Depth)
+ Assert.AreEqual<int>(4, path.Length)
+ Assert.AreEqual<NpcId>(playerId, path.Head.Source)
+ Assert.AreEqual<NpcId>(playerId, path |> List.last |> fun rumor -> rumor.Receiver)
+
+ [<TestMethod>]
+ 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<int>(1, observations.Length)
+ Assert.AreEqual<NpcId>(NpcId 0, observations.Head.Id)
+ Assert.AreEqual<NeedKind>(HungerNeed, observations.Head.Ranked.Head.Kind)
diff --git a/src/LivingVillage.Kernel.Tests/RelationTests.fs b/src/LivingVillage.Kernel.Tests/RelationTests.fs
index 20331ce..b98252d 100644
--- a/src/LivingVillage.Kernel.Tests/RelationTests.fs
+++ b/src/LivingVillage.Kernel.Tests/RelationTests.fs
@@ -32,11 +32,22 @@ module RelationHarness =
{ Tick = tick
Time = 0.0
Rng = Rng.ofSeed 1UL
- Avatar = { Pos = { X = 0.0f; Y = 0.0f } }
+ Avatar =
+ { Pos = { X = 0.0f; Y = 0.0f }
+ Mind =
+ { Needs = needs
+ Personality = personalityOfSeed 1UL
+ Action = Wander
+ Target = actionTarget Wander
+ ActionAge = 0L
+ EffectDone = false
+ HungerFlagged = false
+ Memory = [] } }
NoHost = { Reserved = 0UL }
Npcs = [| mkNpc 0 mem0; mkNpc 1 mem1 |]
Events = []
- Rumors = [] }
+ Rumors = []
+ Annals = [] }
let chatted (tick: int64) (partner: int) : MemoryEvent =
{ Tick = tick; Kind = Chatted(NpcId partner); Valence = Sim.chatValence }
@@ -182,11 +193,22 @@ type RelationTests () =
{ Tick = 1L
Time = 0.0
Rng = Rng.ofSeed 55UL
- Avatar = { Pos = plaza }
+ Avatar =
+ { Pos = plaza
+ Mind =
+ { Needs = needs
+ Personality = personality
+ Action = Wander
+ Target = actionTarget Wander
+ ActionAge = 0L
+ EffectDone = false
+ HungerFlagged = false
+ Memory = [] } }
NoHost = { Reserved = 0UL }
Npcs = [| self; nearest; alternate |]
Events = []
- Rumors = [] }
+ Rumors = []
+ Annals = [] }
let next = Sim.step RelationHarness.zeroInput world
match List.tryHead next.Npcs.[0].Mind.Memory with
| Some entry ->
diff --git a/src/LivingVillage.Kernel.Tests/RumorTests.fs b/src/LivingVillage.Kernel.Tests/RumorTests.fs
index 3c9b267..f52fca9 100644
--- a/src/LivingVillage.Kernel.Tests/RumorTests.fs
+++ b/src/LivingVillage.Kernel.Tests/RumorTests.fs
@@ -37,11 +37,22 @@ module private RumorHarness =
{ Tick = tick
Time = float tick * dtSeconds
Rng = Rng.ofSeed 11UL
- Avatar = { Pos = { X = 0.0f; Y = 0.0f } }
+ 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 = [] }
+ Rumors = []
+ Annals = [] }
let readyForChat (world: World) : World =
{ world with
diff --git a/src/LivingVillage.Kernel.Tests/TradeTests.fs b/src/LivingVillage.Kernel.Tests/TradeTests.fs
index 5a0e724..77e5658 100644
--- a/src/LivingVillage.Kernel.Tests/TradeTests.fs
+++ b/src/LivingVillage.Kernel.Tests/TradeTests.fs
@@ -39,13 +39,24 @@ module private TradeHarness =
{ Tick = 10L
Time = 0.0
Rng = Rng.ofSeed 7UL
- Avatar = { Pos = { X = 0.0f; Y = 0.0f } }
+ 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 |]
+ [| npc 0 buyerMoney buyerHunger buyerFood [] None
+ npc 1 sellerMoney sellerHunger sellerFood sellerMemory None |]
Events = []
- Rumors = [] }
+ Rumors = []
+ Annals = [] }
let request quantity =
{ Buyer = NpcId 0