namespace LivingVillage.Kernel.Tests open System open Microsoft.VisualStudio.TestTools.UnitTesting open LivingVillage.Kernel open LivingVillage.Kernel.Sim module private DriverHarness = let segmentTicks = 900L type Driver = { Rng: RngState Until: int64 Input: Input } let zeroInput = { MoveX = 0.0f; MoveY = 0.0f } let nextSegment (rng: RngState) (until: int64) : Driver = let a, r1 = Rng.nextUInt64 rng let m, r2 = Rng.nextUInt64 r1 let diag = 0.70710678f let dirX, dirY = match int (a &&& 7UL) with | 0 -> 1.0f, 0.0f | 1 -> diag, diag | 2 -> 0.0f, 1.0f | 3 -> -diag, diag | 4 -> -1.0f, 0.0f | 5 -> -diag, -diag | 6 -> 0.0f, -1.0f | _ -> diag, -diag let mag = match int (m % 3UL) with | 0 -> 0.4f | 1 -> 0.7f | _ -> 1.0f { Rng = r2 Until = until + segmentTicks Input = { MoveX = dirX * mag; MoveY = dirY * mag } } let run (seed: uint64) (ticks: int64) : World list = let mutable world = Sim.initialWorld seed let mutable driver = { Rng = Rng.ofSeed seed; Until = segmentTicks; Input = zeroInput } let acc = ResizeArray (int ticks + 1) acc.Add world for t in 1L .. ticks do let d, input = if t - 1L < driver.Until then driver, driver.Input else let d = nextSegment driver.Rng driver.Until d, d.Input driver <- d world <- Sim.step { Input = input } world acc.Add world List.ofSeq acc [] type DeterminismTests () = [] member _.SameSeedSameInputSequenceTracesAreTickByTickEqual () = let a = DriverHarness.run 42UL 20000L let b = DriverHarness.run 42UL 20000L Assert.IsTrue(20001 = List.length a) CollectionAssert.AreEqual(Array.ofList a, Array.ofList b) [] member _.DifferentSeedsProduceDivergentTraces () = let a = DriverHarness.run 42UL 20000L let b = DriverHarness.run 43UL 20000L let diverged = Seq.exists2 (fun (x: World) (y: World) -> x.Avatar.Pos <> y.Avatar.Pos) a b if not diverged then Assert.Fail("expected avatar positions to diverge across seeds") if a = b then Assert.Fail("expected whole traces to differ across seeds") [] member _.StepIsPureAndDoesNotMutateItsInput () = let w = Sim.initialWorld 7UL let ts = { Input = { MoveX = 1.0f; MoveY = -0.5f } } let w1 = Sim.step ts w let w1' = Sim.step ts w if w1 <> w1' then Assert.Fail("step must be deterministic for identical (world, input)") Assert.IsTrue(w.Tick = 0L) Assert.IsTrue(w.Time = 0.0) Assert.IsTrue(w1.Tick = 1L) Assert.IsTrue(abs (w1.Time - Sim.dtSeconds) < 1e-12) [] member _.WorldCarriesSeededRngState () = let a = Sim.initialWorld 42UL let b = Sim.initialWorld 43UL Assert.IsTrue(a.Rng.State = 42UL) Assert.IsTrue(b.Rng.State = 43UL) Assert.IsTrue(a <> b) [] member _.LongRunStaysFiniteBoundedAndTimeTracksTicks () = let trace = DriverHarness.run 42UL 120000L let maxX = float32 (Sim.mapWidthTiles * Sim.tilePixels - Sim.tilePixels) let maxY = float32 (Sim.mapHeightTiles * Sim.tilePixels - Sim.tilePixels) let mutable checkedTicks = 0 for w in trace do let p = w.Avatar.Pos if Single.IsNaN p.X then Assert.Fail($"NaN X at tick {w.Tick}") if Single.IsNaN p.Y then Assert.Fail($"NaN Y at tick {w.Tick}") if Single.IsInfinity p.X then Assert.Fail($"Infinity X at tick {w.Tick}") if Single.IsInfinity p.Y then Assert.Fail($"Infinity Y at tick {w.Tick}") if p.X < 0.0f || p.X > maxX then Assert.Fail($"X out of bounds at tick {w.Tick}: {p.X}") if p.Y < 0.0f || p.Y > maxY then Assert.Fail($"Y out of bounds at tick {w.Tick}: {p.Y}") if abs (float w.Tick * Sim.dtSeconds - w.Time) >= 1e-9 then Assert.Fail($"Time/Tick mismatch at tick {w.Tick}: {w.Time}") checkedTicks <- checkedTicks + 1 Assert.IsTrue(120001 = checkedTicks) [] member _.SplitMix64MatchesKnownVectors () = let expected : uint64 list = [ 0xbdd732262feb6e95UL 0x28efe333b266f103UL 0x47526757130f9f52UL 0x581ce1ff0e4ae394UL ] let mutable rng = Rng.ofSeed 42UL for e in expected do let v, rng' = Rng.nextUInt64 rng if v <> e then Assert.Fail($"expected {e}, got {v}") rng <- rng' [] member _.NextFloat32StaysInUnitInterval () = let mutable rng = Rng.ofSeed 1UL for _ in 1 .. 1000 do let f, rng' = Rng.nextFloat32 rng if f < 0.0f || f >= 1.0f then Assert.Fail($"out of range: {f}") rng <- rng'