using TheLivingWorld.Api.Simulation; using TheLivingWorld.Core.Contracts; using TheLivingWorld.Core.Simulation; namespace TheLivingWorld.Tests; public sealed class WorldSimulationTests { [Fact] public void Create_starts_at_default_morning_and_ticks_at_x1() { using var simulation = WorldSimulation.Create(ReadySummary(), catchUp: false); var before = simulation.SnapshotClock(); Assert.Equal(GameTime.DefaultStart, before.GameTime); Assert.Equal(1, before.TimeScale); Assert.False(before.Paused); simulation.Tick(TimeSpan.FromSeconds(2)); var after = simulation.SnapshotClock(); Assert.Equal(GameTime.DefaultStart.AddMinutes(10), after.GameTime); Assert.True(simulation.IsDirty); } [Fact] public void Tick_does_not_advance_while_paused() { var summary = ReadySummary() with { Clock = WorldSimulation.DefaultClock() with { Paused = true }, }; using var simulation = WorldSimulation.Create(summary, catchUp: false); simulation.Tick(TimeSpan.FromSeconds(5)); Assert.Equal(GameTime.DefaultStart, simulation.SnapshotClock().GameTime); Assert.False(simulation.IsDirty); } [Fact] public void Update_bakes_elapsed_time_before_changing_scale() { using var simulation = WorldSimulation.Create(ReadySummary(), catchUp: false); simulation.Tick(TimeSpan.FromSeconds(1)); var clock = simulation.Update(new UpdateClockRequest { TimeScale = 2 }); Assert.Equal(2, clock.TimeScale); // Tick advanced 5 game minutes; Update may bake a few extra wall-clock milliseconds. Assert.InRange( clock.GameTime, GameTime.DefaultStart.AddMinutes(5), GameTime.DefaultStart.AddMinutes(5).AddSeconds(30)); Assert.True(simulation.IsDirty); } [Fact] public void Update_rejects_invalid_time_scale() { using var simulation = WorldSimulation.Create(ReadySummary(), catchUp: false); Assert.Throws(() => simulation.Update(new UpdateClockRequest { TimeScale = 5 })); } [Fact] public void Catch_up_advances_from_last_ticked_at() { var lastTickedAt = DateTimeOffset.UtcNow - TimeSpan.FromSeconds(3); var summary = ReadySummary() with { LastTickedAt = lastTickedAt }; using var simulation = WorldSimulation.Create(summary, catchUp: true); // ~3 real seconds at x1 → ~15 game minutes (allow a little wall-clock drift). var actual = simulation.SnapshotClock().GameTime; Assert.InRange( actual, GameTime.DefaultStart.AddMinutes(14), GameTime.DefaultStart.AddMinutes(17)); } [Fact] public void OverlayForApi_strips_last_ticked_at() { using var simulation = WorldSimulation.Create(ReadySummary(), catchUp: false); var overlaid = simulation.OverlayForApi(ReadySummary()); Assert.NotNull(overlaid.Clock); Assert.Null(overlaid.LastTickedAt); } private static WorldSummaryDto ReadySummary() => new() { Id = "town-aaaaaaaa", Name = "Town", Latitude = 31.9, Longitude = -100.5, SizeMeters = 10_000, Status = WorldStatus.Ready, CreatedAt = DateTimeOffset.UtcNow, Clock = WorldSimulation.DefaultClock(), LastTickedAt = DateTimeOffset.UtcNow, }; }