summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel.Tests/M6aTests.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Kernel.Tests/M6aTests.fs')
-rw-r--r--src/LivingVillage.Kernel.Tests/M6aTests.fs113
1 files changed, 92 insertions, 21 deletions
diff --git a/src/LivingVillage.Kernel.Tests/M6aTests.fs b/src/LivingVillage.Kernel.Tests/M6aTests.fs
index 8a318be..91f0a10 100644
--- a/src/LivingVillage.Kernel.Tests/M6aTests.fs
+++ b/src/LivingVillage.Kernel.Tests/M6aTests.fs
@@ -1,5 +1,7 @@
namespace LivingVillage.Kernel.Tests
+open System
+open System.IO
open Microsoft.VisualStudio.TestTools.UnitTesting
open LivingVillage.Kernel
open LivingVillage.Kernel.Sim
@@ -44,6 +46,19 @@ module private M6aHarness =
let advance (inputs: Input list) (world: World) : World =
inputs |> List.fold (fun current input -> Sim.step { Input = input } current) world
+ let advanceTrace (inputs: Input list) (world: World) : World list =
+ inputs
+ |> List.scan (fun current input -> Sim.step { Input = input } current) world
+ |> List.tail
+
+ let withTempSave (action: string -> unit) =
+ let id = Guid.NewGuid().ToString("N")
+ let path = Path.Combine(Path.GetTempPath(), $"living-village-m6a-{id}.save")
+ try
+ action path
+ finally
+ if File.Exists path then File.Delete path
+
[<TestClass>]
type M6aTests () =
@@ -54,27 +69,83 @@ type M6aTests () =
|> 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<string>(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<string>(WorldSave.save continued, WorldSave.save restoredContinued)
- Assert.AreEqual<int64>(world.Tick, restored.Tick)
- Assert.AreEqual<uint64>(world.Rng.State, restored.Rng.State)
- Assert.IsFalse(restored.Events.IsEmpty)
- Assert.IsFalse(restored.Rumors.IsEmpty)
- Assert.IsFalse(restored.Annals.IsEmpty)
- Assert.AreEqual<Avatar>(world.Avatar, restored.Avatar)
- CollectionAssert.AreEqual(world.Npcs, restored.Npcs)
+ |> fun current ->
+ let tick = 22L * (Sim.ticksPerDay / 24L)
+ { current with Tick = tick; Time = float tick * Sim.dtSeconds }
+
+ M6aHarness.withTempSave (fun path ->
+ WorldSave.saveToFile path world
+
+ match WorldSave.loadFromFile path with
+ | Error failure -> Assert.Fail($"save should load: {failure}")
+ | Ok restored ->
+ Assert.AreEqual<string>(WorldSave.save world, WorldSave.save restored)
+ Assert.AreEqual<float>(world.Time, restored.Time)
+ Assert.IsTrue(Sim.isNightTick restored.Tick)
+ Assert.AreEqual<Needs>(world.Avatar.Mind.Needs, restored.Avatar.Mind.Needs)
+ Assert.AreEqual<float32>(world.Avatar.Mind.Needs.Money, restored.Avatar.Mind.Needs.Money)
+ CollectionAssert.AreEqual(world.Npcs, restored.Npcs)
+ Assert.AreEqual<Map<ItemKind, int>>(world.Npcs.[0].Inventory, restored.Npcs.[0].Inventory)
+ Assert.AreEqual<InteractionEvent list>(world.Events, restored.Events)
+ Assert.AreEqual<RumorEvent list>(world.Rumors, restored.Rumors)
+ Assert.AreEqual<AnnalEntry list>(world.Annals, restored.Annals)
+ Assert.AreEqual<uint64>(world.Rng.State, restored.Rng.State)
+
+ 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.advanceTrace inputs world
+ let restoredContinued = M6aHarness.advanceTrace inputs restored
+ Assert.AreEqual<string list>(continued |> List.map WorldSave.save, restoredContinued |> List.map WorldSave.save)
+ Assert.AreEqual<uint64 list>(continued |> List.map (fun next -> next.Rng.State), restoredContinued |> List.map (fun next -> next.Rng.State))
+ Assert.AreEqual<InteractionEvent list list>(continued |> List.map (fun next -> next.Events), restoredContinued |> List.map (fun next -> next.Events))
+ Assert.AreEqual<string>(WorldSave.save (List.last continued), WorldSave.save (List.last restoredContinued)))
+
+ [<TestMethod>]
+ member _.RejectedFilesDoNotReplaceCurrentWorld () =
+ let currentWorld = Sim.initialWorldN 99UL 2
+ let before = WorldSave.save currentWorld
+ let mutable current = currentWorld
+
+ let reject path =
+ match WorldSave.loadFromFile path with
+ | Ok loaded ->
+ current <- loaded
+ Assert.Fail("invalid save must not load")
+ | Error _ -> ()
+ Assert.AreEqual<string>(before, WorldSave.save current)
+
+ M6aHarness.withTempSave (fun path ->
+ reject (path + ".missing")
+
+ File.WriteAllText(path, "LV_WORLD_SAVE_V0")
+ reject path
+
+ File.WriteAllText(path, WorldSave.save currentWorld + "|trailing-corruption")
+ reject path)
+
+ [<TestMethod>]
+ member _.CorruptNumericAndInventoryValuesAreRejected () =
+ let world = Sim.initialWorldN 7UL 2
+
+ M6aHarness.withTempSave (fun path ->
+ let nonFinite = { world with Time = Double.NaN }
+ WorldSave.saveToFile path nonFinite
+ match WorldSave.loadFromFile path with
+ | Ok _ -> Assert.Fail("non-finite time must be rejected")
+ | Error _ -> ()
+
+ let invalidInventory =
+ { world with
+ Npcs =
+ [| { world.Npcs.[0] with Inventory = Map.ofList [ Food, -1 ] }
+ world.Npcs.[1] |] }
+ WorldSave.saveToFile path invalidInventory
+ match WorldSave.loadFromFile path with
+ | Ok _ -> Assert.Fail("negative inventory must be rejected")
+ | Error _ -> ())
[<TestMethod>]
member _.SimulationClockSupportsPauseAndRequestedSpeeds () =