229 lines
9.4 KiB
C#
229 lines
9.4 KiB
C#
using Arch.Core;
|
|
using TheLivingWorld.Core.Ecs;
|
|
using TheLivingWorld.Core.Simulation;
|
|
|
|
namespace TheLivingWorld.Tests;
|
|
|
|
public sealed class WeatherSystemTests
|
|
{
|
|
private const double Warsaw = 52.23;
|
|
|
|
/// <summary>Midsummer, so the seasonal snow depth is zero everywhere and cannot skew a comparison.</summary>
|
|
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<PressureSystem> 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<PressureSystem> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<PressureSystem> 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<PressureSystem> a = stackalloc PressureSystem[WeatherSystem.MaxSystems];
|
|
Span<PressureSystem> 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<PressureSystem> 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<PressureSystem> 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<PressureSystem> 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<PressureSystem> before = stackalloc PressureSystem[WeatherSystem.MaxSystems];
|
|
var count = WeatherSystem.CopySystems(world.Ecs, before);
|
|
|
|
WeatherSystem.Execute(world.Ecs, ClimateCatalog.Savanna, 13.75, elapsedGameHours: 0f);
|
|
|
|
Span<PressureSystem> 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<PressureSystem> 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<PressureSystem> 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<PressureSystem> 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<PressureSystem> 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<PressureSystem> 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<PressureSystem> trade = stackalloc PressureSystem[WeatherSystem.MaxSystems];
|
|
Span<PressureSystem> 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);
|
|
}
|
|
|
|
/// <summary>Owns an Arch world so a failing assert cannot leak it out of the static world registry.</summary>
|
|
private sealed class EcsWorld : IDisposable
|
|
{
|
|
public World Ecs { get; } = World.Create();
|
|
|
|
public void Dispose() => World.Destroy(Ecs);
|
|
}
|
|
}
|