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 }); }