sandpypi/Sand.Tests/SandSimulationTests.cs

921 lines
33 KiB
C#

using FluentAssertions;
using Sand.Core;
namespace Sand.Tests;
public sealed class SandSimulationTests
{
private readonly ParticleLibrary _library = ParticleLibraryLoader.LoadFromDirectory(Path.Combine(AppContext.BaseDirectory, "Content", "part"));
[Fact]
public void SandFallsIntoEmptySpace()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(2, 2, "sand");
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
simulation.GetTypeIdAtCell(2, 3).Should().Be(_library.GetTypeId("sand"));
}
[Fact]
public void WaterAndSandCreateWetSand()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(3, 3, "sand");
simulation.CreateParticleAtPixel(2, 3, "water");
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
simulation.GetTypeIdAtCell(3, 3).Should().Be(_library.GetTypeId("wsand"));
}
[Fact]
public void LoaderParsesExtendedMetadataFields()
{
var burningWood = _library.GetDefinition(_library.GetTypeId("burning_wood"));
var energy = _library.GetDefinition(_library.GetTypeId("energy"));
var wind = _library.GetDefinition(_library.GetTypeId("wind"));
var stone = _library.GetDefinition(_library.GetTypeId("stone"));
var ultratanium = _library.GetDefinition(_library.GetTypeId("ultratanium"));
burningWood.Produces.Should().Be("smoke");
burningWood.HeatEmission.Should().Be(50f);
energy.EnergyTransfer.Should().BeGreaterThan(0f);
wind.Radius.Should().BeGreaterThan(0f);
wind.Affects.Should().Contain("steam");
stone.Hardness.Should().Be(0.7f);
stone.Durability.Should().Be(100f);
stone.Broken.Should().Be("brkstone");
ultratanium.Explosive.Should().BeTrue();
ultratanium.ExplosionRadius.Should().Be(15);
}
[Fact]
public void LavaAndWaterCreateStoneAndSteam()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(2, 2, "lava");
simulation.CreateParticleAtPixel(3, 2, "water");
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
simulation.GetTypeIdAtCell(2, 2).Should().Be(_library.GetTypeId("stone"));
simulation.GetTypeIdAtCell(3, 2).Should().Be(_library.GetTypeId("steam"));
}
[Fact]
public void BuildRgbFrameReflectsParticleColor()
{
var simulation = CreateSimulation();
var sand = _library.GetDefinition(_library.GetTypeId("sand"));
simulation.CreateParticleAtPixel(0, 0, "sand");
var frame = simulation.BuildRgbFrame().ToArray();
frame[0].Should().Be(sand.Color.R);
frame[1].Should().Be(sand.Color.G);
frame[2].Should().Be(sand.Color.B);
}
[Fact]
public void SparkSpreadsToDiagonalConductors()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(4, 4, "iron");
simulation.CreateParticleAtPixel(5, 5, "iron");
simulation.CreateParticleAtPixel(4, 4, "spark");
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
simulation.SparkTime[5, 5].Should().BeGreaterThan((short)0);
}
[Fact]
public void OuterWallPreventsPaintingBoundaryCells()
{
var simulation = new SandSimulation(10, 10, 1, _library, new SimulationSettings { PauseSim = true, OuterWall = true });
simulation.RefreshSettingsState();
simulation.CreateParticleAtPixel(0, 0, "sand");
simulation.GetTypeIdAtCell(0, 0).Should().Be(_library.GetTypeId("wall"));
}
[Fact]
public void TogglingOuterWallOffRemovesBoundaryWallsFromParticleCount()
{
var settings = new SimulationSettings { PauseSim = true, OuterWall = true };
var simulation = new SandSimulation(10, 10, 1, _library, settings);
simulation.RefreshSettingsState();
simulation.ParticleCount.Should().BeGreaterThan(0);
settings.OuterWall = false;
simulation.RefreshSettingsState();
simulation.ParticleCount.Should().Be(0);
simulation.GetTypeIdAtCell(0, 0).Should().Be(0);
simulation.GetTypeIdAtCell(9, 9).Should().Be(0);
}
[Fact]
public void WrapParticlesMovesAcrossEdge()
{
var simulation = new SandSimulation(5, 5, 1, _library, new SimulationSettings { PauseSim = false, WrapParticles = true });
simulation.CreateParticleAtPixel(4, 0, "steam");
simulation.Step(1f / 60f);
simulation.ParticleCount.Should().Be(1);
}
[Fact]
public void ParticleCountRemainsStableAcrossMovementSteps()
{
var simulation = new SandSimulation(12, 12, 1, _library, new SimulationSettings { PauseSim = false });
simulation.CreateParticleAtPixel(6, 2, "sand");
for (var i = 0; i < 6; i++)
{
simulation.Step(1f / 60f);
simulation.ParticleCount.Should().Be(1);
}
}
[Fact]
public void SymmetricSandPileDoesNotDevelopStrongRightBias()
{
var simulation = new SandSimulation(60, 40, 1, _library, new SimulationSettings { PauseSim = false });
for (var x = 0; x < simulation.Width; x++)
{
simulation.CreateParticleAtPixel(x, simulation.Height - 1, "wall");
}
for (var x = 24; x <= 36; x++)
{
for (var y = 4; y <= 16; y++)
{
simulation.CreateParticleAtPixel(x, y, "sand");
}
}
var startCenter = FindAverageX(simulation, "sand");
for (var i = 0; i < 120; i++)
{
simulation.Step(1f / 60f);
}
var finalCenter = FindAverageX(simulation, "sand");
Math.Abs(finalCenter - startCenter).Should().BeLessThanOrEqualTo(1.5);
}
[Fact]
public void NeutralFireCanMoveLeftWhenOnlyLeftEscapePathIsAvailable()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(5, 5, "fire");
simulation.CreateParticleAtPixel(5, 4, "wall");
simulation.CreateParticleAtPixel(6, 4, "wall");
simulation.CreateParticleAtPixel(6, 5, "wall");
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
var fireId = _library.GetTypeId("fire");
(simulation.GetTypeIdAtCell(4, 4) == fireId || simulation.GetTypeIdAtCell(4, 5) == fireId).Should().BeTrue();
}
[Fact]
public void LazyOccupiedBoundsShrinkEventuallyAfterEdgeRemoval()
{
var simulation = new SandSimulation(30, 30, 1, _library, new SimulationSettings { PauseSim = false });
simulation.CreateParticleAtPixel(2, 2, "wall");
simulation.CreateParticleAtPixel(20, 20, "wall");
simulation.Step(1f / 60f);
simulation.ClearParticleCircle(2, 2, 0);
for (var i = 0; i < 10; i++)
{
simulation.Step(1f / 60f);
}
simulation.FrameStats.ParticleCount.Should().Be(1);
simulation.FrameStats.MinActiveX.Should().BeLessThanOrEqualTo(20);
simulation.FrameStats.MaxActiveX.Should().BeGreaterThanOrEqualTo(20);
simulation.FrameStats.MaxActiveX.Should().BeLessThanOrEqualTo(21);
simulation.FrameStats.MinActiveY.Should().BeLessThanOrEqualTo(20);
simulation.FrameStats.MaxActiveY.Should().BeGreaterThanOrEqualTo(20);
simulation.FrameStats.MaxActiveY.Should().BeLessThanOrEqualTo(21);
}
[Fact]
public void EmptySimulationClearsActiveBoundsStats()
{
var simulation = new SandSimulation(20, 20, 1, _library, new SimulationSettings { PauseSim = false });
simulation.CreateParticleAtPixel(10, 10, "sand");
simulation.Step(1f / 60f);
simulation.Clear();
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
simulation.FrameStats.ProcessedCells.Should().Be(0);
simulation.FrameStats.ParticleCount.Should().Be(0);
simulation.FrameStats.MinActiveX.Should().Be(0);
simulation.FrameStats.MinActiveY.Should().Be(0);
simulation.FrameStats.MaxActiveX.Should().Be(0);
simulation.FrameStats.MaxActiveY.Should().Be(0);
}
[Fact]
public void BuildRgbaFrameWritesExpectedLength()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(0, 0, "sand");
var buffer = new byte[10 * 10 * 4];
simulation.BuildRgbaFrame(buffer);
buffer.Length.Should().Be(400);
buffer[3].Should().Be(255);
}
[Fact]
public void BuildRgbaFrameUpdatesReusedBufferAfterCellChanges()
{
var simulation = CreateSimulation();
var buffer = new byte[10 * 10 * 4];
var sand = _library.GetDefinition(_library.GetTypeId("sand"));
simulation.CreateParticleAtPixel(2, 2, "sand");
simulation.BuildRgbaFrame(buffer);
var sandIndex = ((2 * simulation.Width) + 2) * 4;
buffer[sandIndex].Should().Be(sand.Color.R);
simulation.ClearParticleCircle(2, 2, 0);
simulation.BuildRgbaFrame(buffer);
buffer[sandIndex].Should().Be(0);
buffer[sandIndex + 1].Should().Be(0);
buffer[sandIndex + 2].Should().Be(0);
buffer[sandIndex + 3].Should().Be(255);
}
[Fact]
public void WindBrushWritesLocalWindField()
{
var simulation = CreateSimulation();
simulation.ApplyWindBrushAtPixel(4, 4, 2, 1f, 0f);
var wind = simulation.GetWindAtCell(4, 4);
wind.X.Should().BeGreaterThan(0f);
MathF.Abs(wind.Y).Should().BeLessThan(0.2f);
}
[Fact]
public void AirBrushWritesPressureField()
{
var simulation = CreateSimulation();
simulation.ApplyAirBrushAtPixel(4, 4, 2, 1f, 0f);
simulation.GetAirPressureAtCell(4, 4).Should().BeGreaterThan(0f);
}
[Fact]
public void FieldDecayContinuesWithoutParticles()
{
var simulation = CreateSimulation();
simulation.Settings.PauseSim = false;
simulation.ApplyAirBrushAtPixel(4, 4, 2, 1f, 0f);
var initialPressure = simulation.GetAirPressureAtCell(4, 4);
simulation.Step(1f / 60f);
simulation.GetAirPressureAtCell(4, 4).Should().BeLessThan(initialPressure);
simulation.ParticleCount.Should().Be(0);
}
[Fact]
public void FreshlySpawnedFallingSandDoesNotImmediatelyCreateVisiblePressure()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(4, 1, "sand");
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
simulation.Step(1f / 60f);
MathF.Abs(simulation.GetAirPressureAtCell(4, 3)).Should().BeLessThan(0.08f);
MathF.Abs(simulation.GetAirPressureAtCell(3, 3)).Should().BeLessThan(0.08f);
MathF.Abs(simulation.GetAirPressureAtCell(5, 3)).Should().BeLessThan(0.08f);
}
[Fact]
public void GasRespondsMoreThanLiquidToSamePressureField()
{
var steamSimulation = new SandSimulation(12, 12, 1, _library, new SimulationSettings { PauseSim = false });
var waterSimulation = new SandSimulation(12, 12, 1, _library, new SimulationSettings { PauseSim = false });
steamSimulation.CreateParticleAtPixel(6, 6, "steam");
waterSimulation.CreateParticleAtPixel(6, 6, "water");
for (var i = 0; i < 6; i++)
{
steamSimulation.ApplyAirBrushAtPixel(3, 6, 3, 1f, 0f);
waterSimulation.ApplyAirBrushAtPixel(3, 6, 3, 1f, 0f);
steamSimulation.Step(1f / 60f);
waterSimulation.Step(1f / 60f);
}
Math.Abs(FindParticleX(steamSimulation, "steam") - 6).Should().BeGreaterThan(0);
Math.Abs(FindParticleX(waterSimulation, "water") - 6).Should().BeLessThan(Math.Abs(FindParticleX(steamSimulation, "steam") - 6));
}
[Fact]
public void NewlySpawnedParticleClearsLocalForceAndPressureFields()
{
var simulation = CreateSimulation();
simulation.ApplyWindBrushAtPixel(4, 4, 2, 1f, 0f);
simulation.ApplyAirBrushAtPixel(4, 4, 2, 1f, 0f);
simulation.GetAirPressureAtCell(4, 4).Should().BeGreaterThan(0f);
simulation.GetWindAtCell(4, 4).X.Should().BeGreaterThan(0f);
simulation.CreateParticleAtPixel(4, 4, "sand");
simulation.GetAirPressureAtCell(4, 4).Should().Be(0f);
simulation.GetWindAtCell(4, 4).X.Should().Be(0f);
simulation.GetWindAtCell(4, 4).Y.Should().Be(0f);
simulation.GetForceAtCell(4, 4).X.Should().Be(0f);
simulation.GetForceAtCell(4, 4).Y.Should().Be(0f);
}
[Fact]
public void PourPaintingAddsLimitedParticlesInsteadOfFillingEntireBrush()
{
var simulation = CreateSimulation();
simulation.CreateParticlePourAtPixel(8, 8, 3, "sand", 4, 1234);
simulation.ParticleCount.Should().Be(4);
}
[Fact]
public void FireTouchingWaterIsExtinguished()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(2, 2, "fire");
simulation.CreateParticleAtPixel(3, 2, "water");
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
simulation.GetTypeIdAtCell(2, 2).Should().Be(0);
var neighbor = simulation.GetTypeIdAtCell(3, 2);
neighbor.Should().BeOneOf((ushort)0, _library.GetTypeId("water"), _library.GetTypeId("steam"));
}
[Fact]
public void FireRisesInsteadOfBehavingLikeSolid()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(5, 5, "fire");
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
simulation.GetTypeIdAtCell(5, 4).Should().Be(_library.GetTypeId("fire"));
}
[Fact]
public void FireEventuallyProducesSmoke()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(5, 7, "fire");
simulation.Settings.PauseSim = false;
for (var i = 0; i < 12; i++)
{
simulation.Step(1f / 60f);
}
CountParticles(simulation, "smoke").Should().BeGreaterThan(0);
}
[Fact]
public void SandAndWaterDoNotSpontaneouslyGenerateSmokeOrEmbers()
{
var simulation = new SandSimulation(16, 16, 1, _library, new SimulationSettings { PauseSim = false });
simulation.CreateParticleAtPixel(6, 6, "sand");
simulation.CreateParticleAtPixel(9, 6, "water");
for (var i = 0; i < 20; i++)
{
simulation.Step(1f / 60f);
}
CountParticles(simulation, "smoke").Should().Be(0);
CountParticles(simulation, "ember").Should().Be(0);
}
[Fact]
public void BurningWoodStaysMostlyInPlaceAndProducesEffectParticles()
{
var simulation = new SandSimulation(16, 16, 1, _library, new SimulationSettings { PauseSim = false });
simulation.CreateParticleAtPixel(8, 8, "burning_wood");
for (var i = 0; i < 24; i++)
{
simulation.Step(1f / 60f);
}
simulation.GetTypeIdAtCell(8, 8).Should().Be(_library.GetTypeId("burning_wood"));
(CountParticles(simulation, "smoke") + CountParticles(simulation, "ember")).Should().BeGreaterThan(0);
}
[Fact]
public void PlasmaTransfersEnergyToConductiveNeighbors()
{
var simulation = new SandSimulation(16, 16, 1, _library, new SimulationSettings { PauseSim = false });
simulation.CreateParticleAtPixel(8, 8, "plasma");
simulation.CreateParticleAtPixel(8, 7, "wall");
simulation.CreateParticleAtPixel(7, 8, "iron");
simulation.CreateParticleAtPixel(9, 8, "iron");
simulation.CreateParticleAtPixel(7, 7, "wall");
simulation.CreateParticleAtPixel(9, 7, "wall");
simulation.CreateParticleAtPixel(8, 9, "wall");
for (var i = 0; i < 12; i++)
{
simulation.Step(1f / 60f);
}
(simulation.SparkTime[7, 8] != 0 || simulation.SparkTime[9, 8] != 0).Should().BeTrue();
}
[Fact]
public void SteamDoesNotImmediatelyCondenseAfterCreation()
{
var simulation = new SandSimulation(12, 12, 1, _library, new SimulationSettings { PauseSim = false, AmbientTemperature = 22f });
simulation.CreateParticleAtPixel(6, 6, "steam");
simulation.CreateParticleAtPixel(5, 5, "wall");
simulation.CreateParticleAtPixel(6, 5, "wall");
simulation.CreateParticleAtPixel(7, 5, "wall");
simulation.CreateParticleAtPixel(5, 6, "wall");
simulation.CreateParticleAtPixel(7, 6, "wall");
simulation.CreateParticleAtPixel(5, 7, "wall");
simulation.CreateParticleAtPixel(6, 7, "wall");
simulation.CreateParticleAtPixel(7, 7, "wall");
for (var i = 0; i < 6; i++)
{
simulation.Step(1f / 60f);
}
CountParticles(simulation, "steam").Should().BeGreaterThan(0);
}
[Fact]
public void CooledSteamCondensesBackIntoWaterInsteadOfDisappearing()
{
var simulation = new SandSimulation(12, 12, 1, _library, new SimulationSettings { PauseSim = false, AmbientTemperature = 22f });
simulation.CreateParticleAtPixel(6, 6, "steam");
simulation.CreateParticleAtPixel(5, 5, "wall");
simulation.CreateParticleAtPixel(6, 5, "wall");
simulation.CreateParticleAtPixel(7, 5, "wall");
simulation.CreateParticleAtPixel(5, 6, "wall");
simulation.CreateParticleAtPixel(7, 6, "wall");
simulation.CreateParticleAtPixel(5, 7, "wall");
simulation.CreateParticleAtPixel(6, 7, "wall");
simulation.CreateParticleAtPixel(7, 7, "wall");
for (var i = 0; i < 80; i++)
{
simulation.Step(1f / 60f);
}
simulation.GetTypeIdAtCell(6, 6).Should().Be(_library.GetTypeId("water"));
}
[Fact]
public void SteamRisesFasterThanSmokeBecauseOfVelocity()
{
var steamSimulation = new SandSimulation(12, 12, 1, _library, new SimulationSettings { PauseSim = false });
var smokeSimulation = new SandSimulation(12, 12, 1, _library, new SimulationSettings { PauseSim = false });
steamSimulation.CreateParticleAtPixel(6, 9, "steam");
smokeSimulation.CreateParticleAtPixel(6, 9, "smoke");
for (var i = 0; i < 8; i++)
{
steamSimulation.Step(1f / 60f);
smokeSimulation.Step(1f / 60f);
}
FindParticleY(steamSimulation, "steam").Should().BeLessThan(FindParticleY(smokeSimulation, "smoke"));
}
[Fact]
public void LavaDoesNotCoolImmediatelyAtAmbient()
{
var simulation = new SandSimulation(12, 12, 1, _library, new SimulationSettings { PauseSim = false, AmbientTemperature = 22f });
simulation.CreateParticleAtPixel(6, 6, "lava");
simulation.CreateParticleAtPixel(5, 5, "wall");
simulation.CreateParticleAtPixel(6, 5, "wall");
simulation.CreateParticleAtPixel(7, 5, "wall");
simulation.CreateParticleAtPixel(5, 7, "wall");
simulation.CreateParticleAtPixel(6, 7, "wall");
simulation.CreateParticleAtPixel(7, 7, "wall");
simulation.CreateParticleAtPixel(5, 6, "wall");
simulation.CreateParticleAtPixel(7, 6, "wall");
simulation.Temperature[6, 6] = 820f;
for (var i = 0; i < 8; i++)
{
simulation.Step(1f / 60f);
}
simulation.GetTypeIdAtCell(6, 6).Should().Be(_library.GetTypeId("lava"));
}
[Fact]
public void LavaDoesNotSelfDestructFromHeatAlone()
{
var simulation = new SandSimulation(12, 12, 1, _library, new SimulationSettings { PauseSim = false, AmbientTemperature = 22f });
simulation.CreateParticleAtPixel(6, 6, "lava");
simulation.CreateParticleAtPixel(5, 5, "wall");
simulation.CreateParticleAtPixel(6, 5, "wall");
simulation.CreateParticleAtPixel(7, 5, "wall");
simulation.CreateParticleAtPixel(5, 6, "wall");
simulation.CreateParticleAtPixel(7, 6, "wall");
simulation.CreateParticleAtPixel(5, 7, "wall");
simulation.CreateParticleAtPixel(6, 7, "wall");
simulation.CreateParticleAtPixel(7, 7, "wall");
for (var i = 0; i < 45; i++)
{
simulation.Step(1f / 60f);
}
simulation.GetTypeIdAtCell(6, 6).Should().Be(_library.GetTypeId("lava"));
}
[Fact]
public void MoltenGoldDoesNotSpreadFasterThanWaterInRuntimeTuning()
{
var waterSimulation = new SandSimulation(16, 16, 1, _library, new SimulationSettings { PauseSim = false });
var moltenGoldSimulation = new SandSimulation(16, 16, 1, _library, new SimulationSettings { PauseSim = false });
PrepareFloor(waterSimulation, 6);
PrepareFloor(moltenGoldSimulation, 6);
waterSimulation.CreateParticleAtPixel(8, 5, "water");
moltenGoldSimulation.CreateParticleAtPixel(8, 5, "molten_gold");
for (var i = 0; i < 8; i++)
{
waterSimulation.Step(1f / 60f);
moltenGoldSimulation.Step(1f / 60f);
}
var waterX = FindParticleX(waterSimulation, "water");
var moltenGoldX = FindParticleX(moltenGoldSimulation, "molten_gold");
Math.Abs(moltenGoldX - 8).Should().BeLessThanOrEqualTo(Math.Abs(waterX - 8));
}
[Fact]
public void LavaRespondsLessThanWaterToLateralForce()
{
var waterSimulation = new SandSimulation(16, 16, 1, _library, new SimulationSettings { PauseSim = false });
var lavaSimulation = new SandSimulation(16, 16, 1, _library, new SimulationSettings { PauseSim = false, AmbientTemperature = 900f });
PrepareFloor(waterSimulation, 6);
PrepareFloor(lavaSimulation, 6);
waterSimulation.CreateParticleAtPixel(8, 5, "water");
lavaSimulation.CreateParticleAtPixel(8, 5, "lava");
for (var i = 0; i < 10; i++)
{
waterSimulation.ApplyWindBrushAtPixel(8, 5, 3, 1f, 0f);
lavaSimulation.ApplyWindBrushAtPixel(8, 5, 3, 1f, 0f);
waterSimulation.Step(1f / 60f);
lavaSimulation.Step(1f / 60f);
}
var waterX = FindParticleX(waterSimulation, "water");
var lavaX = FindParticleX(lavaSimulation, "lava");
Math.Abs(lavaX - 8).Should().BeLessThan(Math.Abs(waterX - 8));
}
[Fact]
public void MoltenGoldDoesNotSelfDestructFromHeatAlone()
{
var simulation = new SandSimulation(12, 12, 1, _library, new SimulationSettings { PauseSim = false, AmbientTemperature = 22f });
simulation.CreateParticleAtPixel(6, 6, "molten_gold");
simulation.CreateParticleAtPixel(5, 5, "wall");
simulation.CreateParticleAtPixel(6, 5, "wall");
simulation.CreateParticleAtPixel(7, 5, "wall");
simulation.CreateParticleAtPixel(5, 6, "wall");
simulation.CreateParticleAtPixel(7, 6, "wall");
simulation.CreateParticleAtPixel(5, 7, "wall");
simulation.CreateParticleAtPixel(6, 7, "wall");
simulation.CreateParticleAtPixel(7, 7, "wall");
for (var i = 0; i < 35; i++)
{
simulation.Step(1f / 60f);
}
simulation.GetTypeIdAtCell(6, 6).Should().Be(_library.GetTypeId("molten_gold"));
}
[Fact]
public void IceAndSnowDoNotImmediatelyMeltAtThresholdTemperatures()
{
var simulation = new SandSimulation(12, 12, 1, _library, new SimulationSettings { PauseSim = false, AmbientTemperature = 22f });
simulation.CreateParticleAtPixel(4, 6, "ice");
simulation.CreateParticleAtPixel(8, 6, "snow");
simulation.CreateParticleAtPixel(3, 6, "wall");
simulation.CreateParticleAtPixel(5, 6, "wall");
simulation.CreateParticleAtPixel(3, 7, "wall");
simulation.CreateParticleAtPixel(4, 7, "wall");
simulation.CreateParticleAtPixel(5, 7, "wall");
simulation.CreateParticleAtPixel(7, 6, "wall");
simulation.CreateParticleAtPixel(9, 6, "wall");
simulation.CreateParticleAtPixel(7, 7, "wall");
simulation.CreateParticleAtPixel(8, 7, "wall");
simulation.CreateParticleAtPixel(9, 7, "wall");
simulation.Temperature[4, 6] = 8f;
simulation.Temperature[8, 6] = 10f;
for (var i = 0; i < 6; i++)
{
simulation.Step(1f / 60f);
}
simulation.GetTypeIdAtCell(4, 6).Should().Be(_library.GetTypeId("ice"));
simulation.GetTypeIdAtCell(8, 6).Should().Be(_library.GetTypeId("snow"));
}
[Fact]
public void ExplosiveParticleDetonatesUnderSustainedPressure()
{
var simulation = new SandSimulation(32, 32, 1, _library, new SimulationSettings { PauseSim = false });
simulation.CreateParticleAtPixel(16, 16, "ultratanium");
for (var i = 0; i < 8; i++)
{
simulation.ApplyAirBrushAtPixel(16, 16, 6, 1f, 0f);
simulation.Step(1f / 60f);
}
simulation.GetTypeIdAtCell(16, 16).Should().Be(0);
(CountParticles(simulation, "smoke") + CountParticles(simulation, "fire")).Should().BeGreaterThanOrEqualTo(0);
}
[Fact]
public void AdjacentExplosivesDoNotRecursivelyOverflowWhenDetonated()
{
var simulation = new SandSimulation(32, 32, 1, _library, new SimulationSettings { PauseSim = false });
for (var x = 12; x <= 18; x++)
{
for (var y = 12; y <= 18; y++)
{
simulation.CreateParticleAtPixel(x, y, "ultratanium");
}
}
for (var i = 0; i < 10; i++)
{
simulation.ApplyAirBrushAtPixel(15, 15, 8, 1f, 0f);
simulation.Step(1f / 60f);
}
simulation.ParticleCount.Should().BeGreaterThanOrEqualTo(0);
}
[Fact]
public void SustainedPressureBreaksStoneIntoBrokenStone()
{
var simulation = new SandSimulation(20, 20, 1, _library, new SimulationSettings { PauseSim = false });
var brokenStone = _library.GetTypeId("brkstone");
simulation.CreateParticleAtPixel(10, 10, "stone");
simulation.CreateParticleAtPixel(9, 9, "wall");
simulation.CreateParticleAtPixel(10, 9, "wall");
simulation.CreateParticleAtPixel(11, 9, "wall");
simulation.CreateParticleAtPixel(9, 10, "wall");
simulation.CreateParticleAtPixel(11, 10, "wall");
simulation.CreateParticleAtPixel(9, 11, "wall");
simulation.CreateParticleAtPixel(10, 11, "wall");
simulation.CreateParticleAtPixel(11, 11, "wall");
var broke = false;
for (var i = 0; i < 28; i++)
{
simulation.ApplyAirBrushAtPixel(10, 10, 6, 1f, 0f);
simulation.Step(1f / 60f);
if (simulation.GetTypeIdAtCell(10, 10) == brokenStone)
{
broke = true;
break;
}
}
broke.Should().BeTrue();
}
[Fact]
public void WindBiasPushesGasSidewaysWhenUpwardPathIsBlocked()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(4, 4, "steam");
simulation.CreateParticleAtPixel(4, 3, "wall");
simulation.CreateParticleAtPixel(3, 3, "wall");
simulation.CreateParticleAtPixel(5, 3, "wall");
simulation.ApplyWindBrushAtPixel(4, 4, 2, 1f, 0f);
simulation.ApplyWindBrushAtPixel(4, 4, 2, 1f, 0f);
simulation.ApplyWindBrushAtPixel(4, 4, 2, 1f, 0f);
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
simulation.GetTypeIdAtCell(5, 4).Should().Be(_library.GetTypeId("steam"));
}
[Fact]
public void WindToolDoesNotPushWaterWhenMetadataExcludesIt()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(4, 4, "water");
simulation.ApplyWindBrushAtPixel(4, 4, 3, 1f, 0f);
simulation.ApplyWindBrushAtPixel(4, 4, 3, 1f, 0f);
simulation.ApplyWindBrushAtPixel(4, 4, 3, 1f, 0f);
var wind = simulation.GetWindAtCell(4, 4);
MathF.Abs(wind.X).Should().BeLessThan(0.01f);
MathF.Abs(wind.Y).Should().BeLessThan(0.01f);
}
[Fact]
public void GravityWellPullsGasSidewaysWhenUpwardPathIsBlocked()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(5, 4, "steam");
simulation.CreateParticleAtPixel(5, 3, "wall");
simulation.CreateParticleAtPixel(4, 3, "wall");
simulation.CreateParticleAtPixel(6, 3, "wall");
simulation.ApplyGravityBrushAtPixel(3, 4, 3, 6f);
simulation.ApplyGravityBrushAtPixel(3, 4, 3, 6f);
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
simulation.GetTypeIdAtCell(4, 4).Should().Be(_library.GetTypeId("steam"));
}
[Fact]
public void RepulsorPushesLiquidSidewaysWhenUpwardPathIsBlocked()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(4, 4, "water");
simulation.CreateParticleAtPixel(4, 5, "wall");
simulation.CreateParticleAtPixel(3, 5, "wall");
simulation.CreateParticleAtPixel(5, 5, "wall");
simulation.ApplyRepulsorBrushAtPixel(3, 4, 3, 6f);
simulation.ApplyRepulsorBrushAtPixel(3, 4, 3, 6f);
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
simulation.GetTypeIdAtCell(5, 4).Should().Be(_library.GetTypeId("water"));
}
[Fact]
public void SolidDoesNotDiagonalSwapIntoLiquid()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(4, 4, "sand");
simulation.CreateParticleAtPixel(4, 5, "wall");
simulation.CreateParticleAtPixel(3, 5, "wall");
simulation.CreateParticleAtPixel(5, 5, "water");
simulation.CreateParticleAtPixel(5, 6, "wall");
simulation.CreateParticleAtPixel(4, 6, "wall");
simulation.CreateParticleAtPixel(6, 6, "wall");
simulation.CreateParticleAtPixel(6, 5, "wall");
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
simulation.GetTypeIdAtCell(4, 4).Should().Be(_library.GetTypeId("sand"));
simulation.GetTypeIdAtCell(5, 5).Should().Be(_library.GetTypeId("water"));
}
[Fact]
public void MoltenLiquidCanSinkThroughLighterLiquidBelow()
{
var simulation = CreateSimulation();
simulation.CreateParticleAtPixel(3, 3, "molten_gold");
simulation.CreateParticleAtPixel(3, 4, "water");
simulation.CreateParticleAtPixel(2, 3, "wall");
simulation.CreateParticleAtPixel(4, 3, "wall");
simulation.CreateParticleAtPixel(2, 4, "wall");
simulation.CreateParticleAtPixel(4, 4, "wall");
simulation.CreateParticleAtPixel(3, 5, "wall");
simulation.CreateParticleAtPixel(2, 5, "wall");
simulation.CreateParticleAtPixel(4, 5, "wall");
simulation.Temperature[3, 3] = 2000f;
simulation.Settings.PauseSim = false;
simulation.Step(1f / 60f);
simulation.GetTypeIdAtCell(3, 4).Should().Be(_library.GetTypeId("molten_gold"));
simulation.GetTypeIdAtCell(3, 3).Should().Be(_library.GetTypeId("water"));
}
private int CountParticles(SandSimulation simulation, string particleId)
{
var typeId = _library.GetTypeId(particleId);
var count = 0;
for (var y = 0; y < simulation.Height; y++)
{
for (var x = 0; x < simulation.Width; x++)
{
if (simulation.GetTypeIdAtCell(x, y) == typeId)
{
count++;
}
}
}
return count;
}
private int FindParticleX(SandSimulation simulation, string particleId)
{
var typeId = _library.GetTypeId(particleId);
for (var y = 0; y < simulation.Height; y++)
{
for (var x = 0; x < simulation.Width; x++)
{
if (simulation.GetTypeIdAtCell(x, y) == typeId)
{
return x;
}
}
}
return -1;
}
private int FindParticleY(SandSimulation simulation, string particleId)
{
var typeId = _library.GetTypeId(particleId);
for (var y = 0; y < simulation.Height; y++)
{
for (var x = 0; x < simulation.Width; x++)
{
if (simulation.GetTypeIdAtCell(x, y) == typeId)
{
return y;
}
}
}
return int.MaxValue;
}
private double FindAverageX(SandSimulation simulation, string particleId)
{
var typeId = _library.GetTypeId(particleId);
double total = 0;
var count = 0;
for (var y = 0; y < simulation.Height; y++)
{
for (var x = 0; x < simulation.Width; x++)
{
if (simulation.GetTypeIdAtCell(x, y) == typeId)
{
total += x;
count++;
}
}
}
return count == 0 ? -1 : total / count;
}
private void PrepareFloor(SandSimulation simulation, int y)
{
for (var x = 0; x < simulation.Width; x++)
{
simulation.CreateParticleAtPixel(x, y, "wall");
}
}
private SandSimulation CreateSimulation() => new(10, 10, 1, _library, new SimulationSettings { PauseSim = true });
}