Implement surface safety and powered props

This commit is contained in:
2026-05-14 10:19:21 +02:00
parent 2ad7feef96
commit 830c7aef14
9 changed files with 267 additions and 29 deletions

View File

@@ -7,14 +7,14 @@ public sealed class SimulationEngineTests
{
var level = BuildReadyLevel();
var next = m_Engine.AdvanceTurn(level);
var next = m_Engine.AdvancePulseForDebug(level);
var consumer = next.GetProp(new(3, 3));
Assert.Equal(EConsumerServiceState.Producing, consumer.FuelServiceState);
Assert.Equal(EConsumerServiceState.Producing, consumer.WaterServiceState);
Assert.Equal(EConsumerServiceState.Producing, consumer.ElectricityServiceState);
Assert.Equal(ELevelState.Ready, next.Global.LevelState);
Assert.Contains(next.Forecasts, forecast => forecast.Kind == EForecastKind.ReactorReady);
Assert.Empty(next.Forecasts);
}
[Fact]
@@ -23,7 +23,7 @@ public sealed class SimulationEngineTests
var level = BuildReadyLevel();
level = level.SetUnderground(new(5, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
var next = m_Engine.AdvanceTurn(level);
var next = m_Engine.AdvancePulseForDebug(level);
Assert.NotEqual(ELevelState.Ready, next.Global.LevelState);
}
@@ -31,7 +31,7 @@ public sealed class SimulationEngineTests
[Fact]
public void ReactorActivatesOnlyAtReadyControl()
{
var level = m_Engine.AdvanceTurn(BuildReadyLevel()) with {
var level = m_Engine.AdvancePulseForDebug(BuildReadyLevel()) with {
Robot = new() { Position = new(5, 3) }
};
@@ -48,7 +48,7 @@ public sealed class SimulationEngineTests
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
level = level.SetProp(new(2, 2), new() { Type = EPropType.Consumer, SwitchState = EPropSwitchState.Disabled });
var next = m_Engine.AdvanceTurn(level);
var next = m_Engine.AdvancePulseForDebug(level);
var consumer = next.GetProp(new(2, 2));
Assert.Equal(EConsumerServiceState.Disabled, consumer.FuelServiceState);
@@ -83,6 +83,42 @@ public sealed class SimulationEngineTests
Assert.Equal(Balancing.Current.StepsPerPulse, next.Global.Step);
}
[Fact]
public void UnpoweredDoorInteractionChangesNoDoorStateButStillPulses()
{
var level = DoorLevel(powered: false) with { Robot = new() { Position = new(3, 2) } };
var next = m_Engine.InteractProp(level);
Assert.Equal(EDoorState.Closed, next.GetProp(new(3, 2)).DoorState);
Assert.Equal(1, next.Global.Pulse);
}
[Fact]
public void PoweredTerminalInteractionEnablesLocalForecastsUntilRobotLeaves()
{
var level = TerminalLevel();
var activated = m_Engine.InteractProp(level);
var moved = m_Engine.MoveRobot(activated, new(2, 1));
Assert.True(activated.GetProp(new(1, 1)).Active);
Assert.NotEmpty(activated.Forecasts);
Assert.Empty(moved.Forecasts);
}
[Fact]
public void UnpoweredTerminalInteractionRevealsNothingButStillPulses()
{
var level = TerminalLevel(false);
var next = m_Engine.InteractProp(level);
Assert.False(next.GetProp(new(1, 1)).Active);
Assert.Empty(next.Forecasts);
Assert.Equal(1, next.Global.Pulse);
}
[Fact]
public void EveryAcceptedLengthyActionAdvancesOneFixedPulse()
{
@@ -138,7 +174,7 @@ public sealed class SimulationEngineTests
var level = DoorLevel();
level = level.SetSurface(new(3, 2), new() { Heat = 8 });
var next = m_Engine.AdvanceTurn(level);
var next = m_Engine.AdvancePulseForDebug(level);
Assert.Equal(0, next.GetSurface(new(4, 2)).Heat);
}
@@ -153,7 +189,7 @@ public sealed class SimulationEngineTests
StructuralIntegrity = Balancing.Current.StructuralIntegrityLeakThreshold
});
var next = m_Engine.AdvanceTurn(level);
var next = m_Engine.AdvancePulseForDebug(level);
Assert.Equal(EUndergroundState.Leaking, next.GetUnderground(new(2, 2), ECarrierType.Fuel).State);
Assert.Contains(next.Leaks, leak => leak.Carrier == ECarrierType.Fuel && leak.UndergroundPosition == new GridPosition(2, 2));
@@ -170,7 +206,7 @@ public sealed class SimulationEngineTests
StructuralIntegrity = Balancing.Current.MaxStructuralIntegrity - 1
});
var next = m_Engine.AdvanceTurn(level);
var next = m_Engine.AdvancePulseForDebug(level);
Assert.True(next.GetUnderground(new(2, 2), ECarrierType.Fuel).StructuralIntegrity < Balancing.Current.MaxStructuralIntegrity - 1);
}
@@ -221,17 +257,107 @@ public sealed class SimulationEngineTests
Robot = new() { Position = new(2, 2), HeatImmunitySteps = 1 }
};
var next = m_Engine.AdvanceTurn(level);
var next = m_Engine.AdvancePulseForDebug(level);
Assert.NotEqual(ELevelState.Lost, next.Global.LevelState);
}
[Fact]
public void UnsafeEntryLossOnlyHappensWhenMovingIntoUnsafeDestination()
{
var level = LevelState.Create("Unsafe", 5, 5) with { Robot = new() { Position = new(1, 1) } };
level = level.SetSurface(new(2, 1), new() { Heat = Balancing.Current.RobotHeatSafetyThreshold });
var next = m_Engine.MoveRobot(level, new(2, 1));
Assert.Equal(ELevelState.Lost, next.Global.LevelState);
Assert.Equal("UNSAFE ENTRY LOSS", next.Global.Status);
}
[Fact]
public void FuelOnlyAndWaterOnlyCellsAreSafeToEnter()
{
var fuel = LevelState.Create("Fuel safe", 5, 5) with { Robot = new() { Position = new(1, 1) } };
fuel = fuel.SetSurface(new(2, 1), new() { Fuel = Balancing.Current.MaxValue });
var water = LevelState.Create("Water safe", 5, 5) with { Robot = new() { Position = new(1, 1) } };
water = water.SetSurface(new(2, 1), new() { Water = Balancing.Current.MaxValue });
Assert.NotEqual(ELevelState.Lost, m_Engine.MoveRobot(fuel, new(2, 1)).Global.LevelState);
Assert.NotEqual(ELevelState.Lost, m_Engine.MoveRobot(water, new(2, 1)).Global.LevelState);
}
[Fact]
public void PulseDoesNotLoseStationaryRobotWhenCellBecomesUnsafe()
{
var level = LevelState.Create("Stationary", 5, 5) with { Robot = new() { Position = new(2, 2) } };
level = level.SetSurface(new(2, 2), new() { Heat = Balancing.Current.RobotHeatSafetyThreshold });
var next = m_Engine.AdvancePulseForDebug(level);
Assert.NotEqual(ELevelState.Lost, next.Global.LevelState);
}
[Fact]
public void WaterDilutesFuelAndQuenchesHeatBeforeIgnition()
{
var level = LevelState.Create("Mitigation", 5, 5);
level = level.SetSurface(new(2, 2), new() {
Fuel = Balancing.Current.FuelCaution,
Water = Balancing.Current.WaterCritical,
Heat = Balancing.Current.HeatCaution
});
var next = m_Engine.AdvanceStepForDebug(level);
Assert.True(next.GetSurface(new(2, 2)).Fuel < level.GetSurface(new(2, 2)).Fuel);
Assert.True(next.GetSurface(new(2, 2)).Heat < level.GetSurface(new(2, 2)).Heat);
}
[Fact]
public void HotCellsEvaporateWaterFasterThanColdCells()
{
var level = LevelState.Create("Evaporation", 5, 5);
level = level.SetSurface(new(1, 1), new() { Water = 5 });
level = level.SetSurface(new(2, 1), new() { Water = 5, Heat = 6 });
var next = m_Engine.AdvanceStepForDebug(level);
Assert.True(next.GetSurface(new(2, 1)).Water < next.GetSurface(new(1, 1)).Water);
Assert.True(next.GetSurface(new(2, 1)).Heat < 6);
}
[Fact]
public void WetCellsConductElectricityFasterThanDryCells()
{
var wet = LevelState.Create("Wet conduct", 5, 5);
wet = wet.SetSurface(new(1, 1), new() { Electricity = 8, Water = Balancing.Current.WaterCaution });
var dry = LevelState.Create("Dry conduct", 5, 5);
dry = dry.SetSurface(new(1, 1), new() { Electricity = 8 });
var wetNext = m_Engine.AdvanceStepForDebug(wet);
var dryNext = m_Engine.AdvanceStepForDebug(dry);
Assert.True(wetNext.GetSurface(new(2, 1)).Electricity > dryNext.GetSurface(new(2, 1)).Electricity);
}
[Fact]
public void FuelAndElectricityCanIgniteIntoHeat()
{
var level = LevelState.Create("Ignite", 5, 5);
level = level.SetSurface(new(2, 2), new() { Fuel = Balancing.Current.FuelCritical, Electricity = Balancing.Current.ElectricityCritical });
var next = m_Engine.AdvanceStepForDebug(level);
Assert.True(next.GetSurface(new(2, 2)).Heat > 0);
Assert.True(next.GetSurface(new(2, 2)).Fuel < level.GetSurface(new(2, 2)).Fuel);
}
[Fact]
public void JunctionRatioSplitsFlowAcrossInferredOutgoingBranches()
{
var level = BuildJunctionLevel(2);
var next = m_Engine.AdvanceTurn(level);
var next = m_Engine.AdvancePulseForDebug(level);
Assert.True(next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount > 0);
Assert.Equal(next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount, next.GetUnderground(new(3, 3), ECarrierType.Fuel).Amount);
@@ -243,7 +369,7 @@ public sealed class SimulationEngineTests
{
var level = BuildJunctionLevel(0);
var next = m_Engine.AdvanceTurn(level);
var next = m_Engine.AdvancePulseForDebug(level);
Assert.Equal(0, next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount);
Assert.True(next.GetUnderground(new(3, 3), ECarrierType.Fuel).Amount > 0);
@@ -393,11 +519,17 @@ public sealed class SimulationEngineTests
};
}
private static LevelState DoorLevel(LevelState? seed = null)
private static LevelState DoorLevel(LevelState? seed = null, bool powered = true)
{
var level = seed ?? LevelState.Create("Door", 6, 6);
level = level.SetTerrain(new(3, 1), ECellTerrain.Wall);
level = level.SetTerrain(new(3, 3), ECellTerrain.Wall);
if (powered)
{
level = AddLine(level, ECarrierType.Electricity, new(2, 2), new(3, 2));
level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
}
return level.SetProp(new(3, 2), new() { Type = EPropType.Door, DoorState = EDoorState.Closed });
}
@@ -453,5 +585,19 @@ public sealed class SimulationEngineTests
};
}
private static LevelState TerminalLevel(bool powered = true)
{
var level = BuildReadyLevel();
level = level.SetProp(new(1, 1), new() { Type = EPropType.AllSeeingEyeTerminal });
level = level.SetUnderground(new(1, 1), ECarrierType.Electricity, new() { State = EUndergroundState.Intact });
if (powered)
{
level = level.SetUnderground(new(1, 2), ECarrierType.Electricity, new() { State = EUndergroundState.Intact });
level = level.SetProp(new(1, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
}
return level with { Robot = new() { Position = new(1, 1) } };
}
private readonly SimulationEngine m_Engine = new();
}