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);
}
///
/// A system is now a cell on the map rather than the whole sky, so one launched off the edge can drift
/// past without ever being felt. A freshly seeded pool has to be standing over the map, not beside it.
///
[Fact]
public void A_seeded_pool_is_actually_over_the_map()
{
var overhead = 0;
for (var seed = 0; seed < 30; seed++)
{
using var world = new EcsWorld();
WeatherSystem.Seed(world.Ecs, ClimateCatalog.HotDesert, 30.05, (ulong)seed, Summer);
Span systems = stackalloc PressureSystem[WeatherSystem.MaxSystems];
var count = WeatherSystem.CopySystems(world.Ecs, systems);
for (var i = 0; i < count; i++)
{
var system = systems[i];
if (MathF.Abs(system.X - 0.5f) < 0.8f && MathF.Abs(system.Y - 0.5f) < 0.8f) overhead++;
}
}
// Two systems per desert world over thirty worlds: the great majority must be within reach.
Assert.True(overhead > 45, $"Only {overhead}/60 seeded systems were anywhere near the map.");
}
[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);
}
}