203 lines
8.2 KiB
C#
203 lines
8.2 KiB
C#
namespace ReactorMaintenance.Simulation.Tests;
|
|
|
|
public sealed class SimulationEngineTests
|
|
{
|
|
[Fact]
|
|
public void NetworkPropagationSuppliesBoundConsumersAndReadiesReactor()
|
|
{
|
|
var level = BuildReadyLevel();
|
|
|
|
var next = m_Engine.AdvanceTurn(level);
|
|
|
|
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 2)).ServiceState);
|
|
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 3)).ServiceState);
|
|
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 4)).ServiceState);
|
|
Assert.Equal(ELevelState.Ready, next.Global.LevelState);
|
|
Assert.Contains(next.Forecasts, forecast => forecast.Kind == EForecastKind.ReactorReady);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReactorActivatesOnlyAtReadyControl()
|
|
{
|
|
var level = m_Engine.AdvanceTurn(BuildReadyLevel()) with {
|
|
Robot = new() { Position = new(5, 3) }
|
|
};
|
|
|
|
var activated = m_Engine.ActivateReactor(level);
|
|
|
|
Assert.Equal(ELevelState.Won, activated.Global.LevelState);
|
|
Assert.True(activated.Reactors[0].Activated);
|
|
}
|
|
|
|
[Fact]
|
|
public void LeakingUndergroundCellInjectsMatchingSurfaceHazard()
|
|
{
|
|
var level = LevelState.Create("Leak", 6, 6);
|
|
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Leaking, Amount = 5, Intensity = 5 }) with {
|
|
Leaks = [new LeakState { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }]
|
|
};
|
|
level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
|
|
|
var next = m_Engine.AdvanceTurn(level);
|
|
|
|
Assert.True(next.GetSurface(new(2, 2)).Fuel > 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void ElementRemedyClearsHazardAndBlocksImmediateReentry()
|
|
{
|
|
var level = LevelState.Create("Remedy", 6, 6);
|
|
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Leaking, Amount = 5, Intensity = 5 });
|
|
level = level.SetSurface(new(2, 2), new() { Fuel = 5 }) with {
|
|
Robot = new() { Position = new(2, 2), FuelNeutralizers = 1 },
|
|
Leaks = [new LeakState { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }]
|
|
};
|
|
|
|
var next = m_Engine.InteractLeak(level, ECarrierType.Fuel, true);
|
|
|
|
Assert.Equal(0, next.GetSurface(new(2, 2)).Fuel);
|
|
Assert.True(next.GetSurface(new(2, 2)).FuelBlockTurns > 0);
|
|
Assert.Equal(0, next.Robot.FuelNeutralizers);
|
|
}
|
|
|
|
[Fact]
|
|
public void ClosedDoorBlocksAdjacentHeatFlow()
|
|
{
|
|
var level = LevelState.Create("Door", 6, 6);
|
|
level = level.SetSurface(new(2, 2), new() { Heat = 8 }) with {
|
|
Doors = [new DoorState { A = new(2, 2), B = new(3, 2), State = EDoorState.Closed }]
|
|
};
|
|
|
|
var next = m_Engine.AdvanceTurn(level);
|
|
|
|
Assert.Equal(0, next.GetSurface(new(3, 2)).Heat);
|
|
}
|
|
|
|
[Fact]
|
|
public void HeatShieldPreventsRobotHeatLoss()
|
|
{
|
|
var level = LevelState.Create("Heat shield", 6, 6);
|
|
level = level.SetSurface(new(2, 2), new() { Heat = Balancing.Current.RobotHeatSafetyThreshold }) with {
|
|
Robot = new() { Position = new(2, 2), HeatImmunitySteps = 1 }
|
|
};
|
|
|
|
var next = m_Engine.AdvanceTurn(level);
|
|
|
|
Assert.NotEqual(ELevelState.Lost, next.Global.LevelState);
|
|
}
|
|
|
|
[Fact]
|
|
public void RobotLosesOnUnsafeElementHazard()
|
|
{
|
|
var level = LevelState.Create("Unsafe", 6, 6);
|
|
level = level.SetSurface(new(2, 2), new() { Electricity = Balancing.Current.MaxValue }) with {
|
|
Robot = new() { Position = new(2, 2) }
|
|
};
|
|
|
|
var next = m_Engine.AdvanceTurn(level);
|
|
|
|
Assert.Equal(ELevelState.Lost, next.Global.LevelState);
|
|
}
|
|
|
|
[Fact]
|
|
public void RuleEventCanCreateTerminalLossForecast()
|
|
{
|
|
var level = LevelState.Create("Rule", 6, 6) with {
|
|
RuleEvents = [
|
|
new RuleEventState {
|
|
Phase = ERuleEventPhase.EndOfTurn,
|
|
ForecastText = "containment failure",
|
|
Predicates = [new RulePredicate { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }],
|
|
Effects = [new RuleEffect { Kind = ERuleEffectKind.MarkTerminalLoss, Message = "CONTAINMENT FAILURE" }]
|
|
}
|
|
]
|
|
};
|
|
|
|
var forecasts = m_Engine.Forecast(level);
|
|
|
|
Assert.Contains(forecasts, forecast => forecast.Kind == EForecastKind.RuleEvent && forecast.Message == "containment failure");
|
|
Assert.Contains(forecasts, forecast => forecast.Kind == EForecastKind.TerminalLoss);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidatorRejectsWallHazardsAndInvalidReactorBinding()
|
|
{
|
|
var level = LevelState.Create("Invalid", 6, 6);
|
|
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
|
level = level with {
|
|
Surface = level.Surface.ToArray(),
|
|
Reactors = [new ReactorBinding { ControlPosition = new(3, 3), FuelConsumerPosition = new(1, 1), CoolantConsumerPosition = new(1, 1), ElectricityConsumerPosition = new(1, 1) }]
|
|
};
|
|
level.Surface[level.Index(new(2, 2))] = new() { Heat = 1 };
|
|
|
|
var report = new LevelValidator().Validate(level);
|
|
|
|
Assert.False(report.IsValid);
|
|
Assert.Contains(report.Errors, error => error.Message.Contains("Wall cell", StringComparison.Ordinal));
|
|
Assert.Contains(report.Errors, error => error.Message.Contains("Reactor binding", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void LevelSerializationRoundTripsCurrentSchemaOnly()
|
|
{
|
|
var level = BuildReadyLevel();
|
|
|
|
var json = LevelSerializer.Serialize(level);
|
|
var loaded = LevelSerializer.Deserialize(json);
|
|
|
|
Assert.Contains("\"Version\": 2", json);
|
|
Assert.Equal(level.Name, loaded.Name);
|
|
Assert.Equal(EPropType.Flow, loaded.GetProp(new(2, 2)).Type);
|
|
}
|
|
|
|
[Fact]
|
|
public void LevelSerializationRejectsOldSchema()
|
|
{
|
|
var json = """
|
|
{
|
|
"Version": 1,
|
|
"Level": {}
|
|
}
|
|
""";
|
|
|
|
var exception = Assert.Throws<InvalidOperationException>(() => LevelSerializer.Deserialize(json));
|
|
|
|
Assert.Contains("Unsupported level file version 1", exception.Message);
|
|
}
|
|
|
|
private static LevelState BuildReadyLevel()
|
|
{
|
|
var level = LevelState.Create("Ready", 8, 7);
|
|
level = AddLine(level, ECarrierType.Fuel, new(2, 2), new(3, 2));
|
|
level = AddLine(level, ECarrierType.Coolant, new(2, 3), new(3, 3));
|
|
level = AddLine(level, ECarrierType.Electricity, new(2, 4), new(3, 4));
|
|
level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
|
level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant });
|
|
level = level.SetProp(new(2, 4), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
|
|
level = level.SetProp(new(3, 2), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
|
|
level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant });
|
|
level = level.SetProp(new(3, 4), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Electricity });
|
|
level = level.SetProp(new(5, 3), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
|
|
return level with {
|
|
Robot = new() { Position = new(5, 3) },
|
|
Reactors = [
|
|
new ReactorBinding {
|
|
ReactorId = 1,
|
|
ControlPosition = new(5, 3),
|
|
FuelConsumerPosition = new(3, 2),
|
|
CoolantConsumerPosition = new(3, 3),
|
|
ElectricityConsumerPosition = new(3, 4)
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
private static LevelState AddLine(LevelState level, ECarrierType carrier, GridPosition a, GridPosition b)
|
|
{
|
|
level = level.SetUnderground(a, carrier, new() { State = EUndergroundState.Intact });
|
|
level = level.SetUnderground(b, carrier, new() { State = EUndergroundState.Intact });
|
|
return level;
|
|
}
|
|
|
|
private readonly SimulationEngine m_Engine = new();
|
|
} |