From 9724663602b4b1eb755bf334c475dc2c80e4b386 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Sun, 20 Sep 2026 15:14:23 +0800 Subject: feat(m6a): add save speed and day-night controls --- .../LivingVillage.Kernel.Tests.fsproj | 1 + src/LivingVillage.Kernel.Tests/M6aTests.fs | 99 ++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/LivingVillage.Kernel.Tests/M6aTests.fs (limited to 'src/LivingVillage.Kernel.Tests') diff --git a/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj b/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj index 2a3ba3c..b30fc8f 100644 --- a/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj +++ b/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj @@ -11,6 +11,7 @@ + diff --git a/src/LivingVillage.Kernel.Tests/M6aTests.fs b/src/LivingVillage.Kernel.Tests/M6aTests.fs new file mode 100644 index 0000000..8a318be --- /dev/null +++ b/src/LivingVillage.Kernel.Tests/M6aTests.fs @@ -0,0 +1,99 @@ +namespace LivingVillage.Kernel.Tests + +open Microsoft.VisualStudio.TestTools.UnitTesting +open LivingVillage.Kernel +open LivingVillage.Kernel.Sim + +module private M6aHarness = + + 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 withDialogue (world: World) : World = + match Sim.chooseDialogue (NpcId 0) SmallTalk (atNpc 0 world) with + | DialogueSucceeded (_, next) -> next + | DialogueRejected (failure, _) -> Assert.Fail($"dialogue rejected: {failure}"); Unchecked.defaultof + + let withChat (world: World) : World = + match Sim.chat { Narrator = NpcId 0; Receiver = NpcId 1 } (readyForChat world) with + | ChatSucceeded (Some _, next) -> next + | ChatSucceeded (None, _) -> Assert.Fail("chat should create a rumor"); Unchecked.defaultof + | ChatRejected (failure, _) -> Assert.Fail($"chat rejected: {failure}"); Unchecked.defaultof + + let withTrade (world: World) : World = + match Sim.trade { Buyer = NpcId 0; Seller = NpcId 1; Item = Food; Quantity = 1 } world with + | TradeSucceeded next -> next + | TradeRejected (failure, _) -> Assert.Fail($"trade rejected: {failure}"); Unchecked.defaultof + + let advance (inputs: Input list) (world: World) : World = + inputs |> List.fold (fun current input -> Sim.step { Input = input } current) world + +[] +type M6aTests () = + + [] + member _.SaveLoadPreservesWorldAndDeterministicContinuation () = + let world = + Sim.initialWorldN 7UL 4 + |> M6aHarness.withDialogue + |> M6aHarness.withChat + |> M6aHarness.withTrade + let encoded = WorldSave.save world + + match WorldSave.load encoded with + | Error failure -> Assert.Fail($"save should load: {failure}") + | Ok restored -> + Assert.AreEqual(encoded, WorldSave.save restored) + let inputs = + [ { MoveX = 0.5f; MoveY = -0.25f } + { MoveX = 0.0f; MoveY = 0.0f } + { MoveX = -1.0f; MoveY = 0.75f } + { MoveX = 0.25f; MoveY = 0.0f } ] + let continued = M6aHarness.advance inputs world + let restoredContinued = M6aHarness.advance inputs restored + Assert.AreEqual(WorldSave.save continued, WorldSave.save restoredContinued) + Assert.AreEqual(world.Tick, restored.Tick) + Assert.AreEqual(world.Rng.State, restored.Rng.State) + Assert.IsFalse(restored.Events.IsEmpty) + Assert.IsFalse(restored.Rumors.IsEmpty) + Assert.IsFalse(restored.Annals.IsEmpty) + Assert.AreEqual(world.Avatar, restored.Avatar) + CollectionAssert.AreEqual(world.Npcs, restored.Npcs) + + [] + member _.SimulationClockSupportsPauseAndRequestedSpeeds () = + let initial = SimulationControl.initial + Assert.AreEqual(1, SimulationControl.stepsPerFrame initial) + Assert.AreEqual(2, SimulationControl.stepsPerFrame (SimulationControl.setSpeed TwoX initial)) + Assert.AreEqual(5, SimulationControl.stepsPerFrame (SimulationControl.setSpeed FiveX initial)) + let paused = SimulationControl.togglePause initial + Assert.IsTrue(paused.Paused) + Assert.AreEqual(0, SimulationControl.stepsPerFrame paused) + let resumed = SimulationControl.togglePause paused + Assert.IsFalse(resumed.Paused) + Assert.AreEqual(OneX, resumed.Speed) + + [] + member _.ConfiguredLegacyStepCountsRemainExactUntilManualSpeedSelection () = + let control, legacySteps = SimulationControl.initialForConfiguredSteps 7 + Assert.AreEqual(Some 7, legacySteps) + Assert.AreEqual(7, SimulationControl.stepsPerFrameWithLegacy legacySteps control) + Assert.AreEqual(0, SimulationControl.stepsPerFrameWithLegacy legacySteps (SimulationControl.togglePause control)) + let manuallySelected = SimulationControl.setSpeed TwoX control + Assert.AreEqual(2, SimulationControl.stepsPerFrameWithLegacy None manuallySelected) -- cgit v1.2.3