Files
the-living-world/tests/TheLivingWorld.Tests/WorldSimulationTests.cs
T

125 lines
4.2 KiB
C#

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<ArgumentOutOfRangeException>(() =>
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 Catch_up_skips_a_world_that_was_paused_when_the_host_went_down()
{
var summary = ReadySummary() with
{
Clock = WorldSimulation.DefaultClock() with { Paused = true },
LastTickedAt = DateTimeOffset.UtcNow - TimeSpan.FromHours(6),
};
using var simulation = WorldSimulation.Create(summary, catchUp: true);
Assert.Equal(GameTime.DefaultStart, simulation.SnapshotClock().GameTime);
// Unpausing must not replay the six hours spent paused offline either.
var resumed = simulation.Update(new UpdateClockRequest { Paused = false });
Assert.False(resumed.Paused);
Assert.InRange(resumed.GameTime, GameTime.DefaultStart, GameTime.DefaultStart.AddMinutes(5));
}
[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,
};
}