using Arch.Core; using TheLivingWorld.Core.Ecs; using TheLivingWorld.Core.Simulation; namespace TheLivingWorld.Tests; public sealed class WeatherSystemTests { private const double Warsaw = 52.23; /// Midsummer, so the seasonal snow depth is zero everywhere and cannot skew a comparison. private static readonly DateTime Summer = new(2012, 7, 15, 12, 0, 0, DateTimeKind.Unspecified); [Fact] public void Seed_creates_one_system_per_point_of_storminess() { var climate = ClimateCatalog.CentralEuropean; using var world = new EcsWorld(); WeatherSystem.Seed(world.Ecs, climate, Warsaw, seed: 42, Summer); Span systems = stackalloc PressureSystem[WeatherSystem.MaxSystems]; Assert.Equal(climate.Storminess, WeatherSystem.CopySystems(world.Ecs, systems)); } [Fact] public void Seed_starts_the_pool_mid_life_so_the_sky_is_never_empty() { using var world = new EcsWorld(); WeatherSystem.Seed(world.Ecs, ClimateCatalog.Oceanic, Warsaw, seed: 7, Summer); Span systems = stackalloc PressureSystem[WeatherSystem.MaxSystems]; var count = WeatherSystem.CopySystems(world.Ecs, systems); // At least one system has faded in far enough to actually be felt at the middle of the map. var anomaly = WeatherModel.SampleField(systems[..count], 0.5f, 0.5f).Anomaly; Assert.NotEqual(0f, anomaly); } [Fact] public void The_same_seed_produces_the_same_sky() { using var first = new EcsWorld(); using var second = new EcsWorld(); WeatherSystem.Seed(first.Ecs, ClimateCatalog.Siberian, 62.03, seed: 12345, Summer); WeatherSystem.Seed(second.Ecs, ClimateCatalog.Siberian, 62.03, seed: 12345, Summer); Span a = stackalloc PressureSystem[WeatherSystem.MaxSystems]; Span b = stackalloc PressureSystem[WeatherSystem.MaxSystems]; var countA = WeatherSystem.CopySystems(first.Ecs, a); var countB = WeatherSystem.CopySystems(second.Ecs, b); Assert.Equal(countA, countB); for (var i = 0; i < countA; i++) Assert.Equal(a[i], b[i]); } [Fact] public void Systems_drift_across_the_map_and_age_as_they_go() { using var world = new EcsWorld(); WeatherSystem.Seed(world.Ecs, ClimateCatalog.CentralEuropean, Warsaw, seed: 3, Summer); Span before = stackalloc PressureSystem[WeatherSystem.MaxSystems]; var count = WeatherSystem.CopySystems(world.Ecs, before); var firstBefore = before[0]; WeatherSystem.Execute(world.Ecs, ClimateCatalog.CentralEuropean, Warsaw, elapsedGameHours: 2f); Span after = stackalloc PressureSystem[WeatherSystem.MaxSystems]; WeatherSystem.CopySystems(world.Ecs, after); Assert.Equal(count, WeatherSystem.CopySystems(world.Ecs, after)); Assert.Equal(firstBefore.AgeHours + 2f, after[0].AgeHours, 3); Assert.NotEqual(firstBefore.X, after[0].X); } [Fact] public void An_expired_system_is_recycled_rather_than_destroyed() { var climate = ClimateCatalog.Oceanic; using var world = new EcsWorld(); WeatherSystem.Seed(world.Ecs, climate, Warsaw, seed: 99, Summer); // Well past every possible lifetime, so the whole pool turns over. WeatherSystem.Execute(world.Ecs, climate, Warsaw, elapsedGameHours: 500f); Span systems = stackalloc PressureSystem[WeatherSystem.MaxSystems]; var count = WeatherSystem.CopySystems(world.Ecs, systems); Assert.Equal(climate.Storminess, count); for (var i = 0; i < count; i++) { Assert.Equal(0f, systems[i].AgeHours); Assert.True(systems[i].LifetimeHours > 0f); } } [Fact] public void Execute_ignores_a_non_positive_step() { using var world = new EcsWorld(); WeatherSystem.Seed(world.Ecs, ClimateCatalog.Savanna, 13.75, seed: 5, Summer); Span before = stackalloc PressureSystem[WeatherSystem.MaxSystems]; var count = WeatherSystem.CopySystems(world.Ecs, before); WeatherSystem.Execute(world.Ecs, ClimateCatalog.Savanna, 13.75, elapsedGameHours: 0f); Span after = stackalloc PressureSystem[WeatherSystem.MaxSystems]; WeatherSystem.CopySystems(world.Ecs, after); for (var i = 0; i < count; i++) Assert.Equal(before[i], after[i]); } [Fact] public void Reseed_rolls_a_new_sky_without_changing_the_pool_size() { var climate = ClimateCatalog.Mediterranean; using var world = new EcsWorld(); WeatherSystem.Seed(world.Ecs, climate, 37.98, seed: 1, Summer); Span before = stackalloc PressureSystem[WeatherSystem.MaxSystems]; var count = WeatherSystem.CopySystems(world.Ecs, before); var firstBefore = before[0]; WeatherSystem.Reseed(world.Ecs, climate, 37.98, Summer); Span after = stackalloc PressureSystem[WeatherSystem.MaxSystems]; Assert.Equal(count, WeatherSystem.CopySystems(world.Ecs, after)); Assert.NotEqual(firstBefore, after[0]); } [Fact] public void Restore_brings_a_persisted_sky_back_verbatim() { using var source = new EcsWorld(); WeatherSystem.Seed(source.Ecs, ClimateCatalog.Tundra, 78.22, seed: 64, Summer); WeatherSystem.Execute(source.Ecs, ClimateCatalog.Tundra, 78.22, elapsedGameHours: 9f); Span saved = stackalloc PressureSystem[WeatherSystem.MaxSystems]; var count = WeatherSystem.CopySystems(source.Ecs, saved); var rng = WeatherSystem.RngState(source.Ecs); var snow = WeatherSystem.SnowDepthMm(source.Ecs); using var restored = new EcsWorld(); WeatherSystem.Restore( restored.Ecs, ClimateCatalog.Tundra, 78.22, 0, Summer, rng, snow, saved[..count]); Span read = stackalloc PressureSystem[WeatherSystem.MaxSystems]; Assert.Equal(count, WeatherSystem.CopySystems(restored.Ecs, read)); // Compared as a set: the ECS makes no promise about the order a query hands entities back, and the // field is a sum over all of them, so only membership matters. Assert.Equal( saved[..count].ToArray().OrderBy(static system => system.X).ToArray(), read[..count].ToArray().OrderBy(static system => system.X).ToArray()); Assert.Equal(rng, WeatherSystem.RngState(restored.Ecs)); Assert.Equal(snow, WeatherSystem.SnowDepthMm(restored.Ecs)); } [Fact] public void Restore_falls_back_to_a_fresh_seed_when_nothing_was_stored() { var climate = ClimateCatalog.HotDesert; using var world = new EcsWorld(); WeatherSystem.Restore( world.Ecs, climate, 30.05, fallbackSeed: 8, gameTime: Summer, rngState: 0, snowDepthMm: 0, systems: []); Span systems = stackalloc PressureSystem[WeatherSystem.MaxSystems]; Assert.Equal(climate.Storminess, WeatherSystem.CopySystems(world.Ecs, systems)); } [Fact] public void Tropical_systems_run_west_and_temperate_ones_run_east() { using var tropics = new EcsWorld(); using var temperate = new EcsWorld(); WeatherSystem.Seed(tropics.Ecs, ClimateCatalog.Equatorial, latitude: 5, seed: 21, Summer); WeatherSystem.Seed(temperate.Ecs, ClimateCatalog.Oceanic, latitude: 51.51, seed: 21, Summer); Span trade = stackalloc PressureSystem[WeatherSystem.MaxSystems]; Span westerly = stackalloc PressureSystem[WeatherSystem.MaxSystems]; var tradeCount = WeatherSystem.CopySystems(tropics.Ecs, trade); var westerlyCount = WeatherSystem.CopySystems(temperate.Ecs, westerly); for (var i = 0; i < tradeCount; i++) Assert.True(trade[i].VelocityX < 0f); for (var i = 0; i < westerlyCount; i++) Assert.True(westerly[i].VelocityX > 0f); } /// Owns an Arch world so a failing assert cannot leak it out of the static world registry. private sealed class EcsWorld : IDisposable { public World Ecs { get; } = World.Create(); public void Dispose() => World.Destroy(Ecs); } }