Latest
This commit is contained in:
@@ -1,150 +1,150 @@
|
||||
using ReactorMaintenance.Simulation.Effects;
|
||||
using ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed class SimulationEngine(IEnumerable<ISimulationEffect> effects, IEnumerable<IAreaSimulationEffect> areaEffects, IEnumerable<Hazard> hazards)
|
||||
{
|
||||
private sealed record ForecastKey(EFailureKind Kind, GridPosition? Position);
|
||||
|
||||
public SimulationEngine()
|
||||
: this(
|
||||
[new PipeLeakEffect(), new MachineEffect(), new FireAndElectricalHazardEffect(), new CellIntegrityEffect()],
|
||||
[new SmokeSpreadEffect()],
|
||||
[new PipeBurstHazard(), new IgnitionHazard(), new MeltdownHazard(), new StabilityCollapseHazard()])
|
||||
{
|
||||
}
|
||||
|
||||
public LevelState AdvanceTurn(LevelState level)
|
||||
{
|
||||
return AdvanceTurn(level, true);
|
||||
}
|
||||
|
||||
public IReadOnlyList<Forecast> Forecast(LevelState level)
|
||||
{
|
||||
var forecasts = new List<Forecast>();
|
||||
var seen = new HashSet<ForecastKey>();
|
||||
var forecastLevel = level with { Cells = level.Cells.ToArray(), Forecasts = Array.Empty<Forecast>() };
|
||||
if (forecastLevel.Global.Lost)
|
||||
AddHazardForecasts(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
|
||||
|
||||
AddReactorReadyForecast(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
|
||||
|
||||
if (IsReactorReady(forecastLevel) || forecastLevel.Global.Lost || forecastLevel.Global.ReactorActivated)
|
||||
return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
|
||||
|
||||
for (var step = Balancing.Current.TurnIncrement; step <= Balancing.Current.MaxForecastStepCount; step++)
|
||||
{
|
||||
forecastLevel = AdvanceTurn(forecastLevel, false);
|
||||
AddHazardForecasts(forecasts, seen, forecastLevel, step);
|
||||
AddReactorReadyForecast(forecasts, seen, forecastLevel, step);
|
||||
|
||||
if (forecastLevel.Global.Lost || IsReactorReady(forecastLevel) || forecastLevel.Global.ReactorActivated)
|
||||
break;
|
||||
}
|
||||
|
||||
return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
|
||||
}
|
||||
|
||||
public LevelState ActivateReactor(LevelState level)
|
||||
{
|
||||
if (!IsReactorReady(level))
|
||||
return level with { Global = level.Global with { Status = "REACTOR NOT READY" } };
|
||||
|
||||
return level with {
|
||||
Global = level.Global with {
|
||||
ReactorActivated = true,
|
||||
Status = "REACTOR ONLINE"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private LevelState AdvanceTurn(LevelState level, bool updateForecasts)
|
||||
{
|
||||
var cells = level.Cells.ToArray();
|
||||
|
||||
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
|
||||
{
|
||||
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var index = level.Index(position);
|
||||
var cell = cells[index];
|
||||
|
||||
if (!cell.IsWalkable)
|
||||
continue;
|
||||
|
||||
foreach (var effect in m_Effects)
|
||||
cell = effect.Apply(cell);
|
||||
|
||||
cells[index] = cell with { Hazards = cell.Hazards.Clamp() };
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var areaEffect in m_AreaEffects)
|
||||
cells = areaEffect.Apply(level, cells);
|
||||
|
||||
var global = UpdateGlobal(level, cells);
|
||||
var next = level with {
|
||||
Cells = cells,
|
||||
Global = global with { Turn = level.Global.Turn + Balancing.Current.TurnIncrement }
|
||||
};
|
||||
|
||||
return updateForecasts ? next with { Forecasts = Forecast(next) } : next;
|
||||
}
|
||||
|
||||
private void AddHazardForecasts(List<Forecast> forecasts, HashSet<ForecastKey> seen, LevelState level, int turns)
|
||||
{
|
||||
foreach (var hazard in m_Hazards)
|
||||
{
|
||||
foreach (var forecast in hazard.Predict(level, turns))
|
||||
AddForecast(forecasts, seen, forecast);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddReactorReadyForecast(List<Forecast> forecasts, HashSet<ForecastKey> seen, LevelState level, int turns)
|
||||
{
|
||||
if (IsReactorReady(level))
|
||||
AddForecast(forecasts, seen, new(EFailureKind.ReactorReady, null, turns, "REACTOR READY"));
|
||||
}
|
||||
|
||||
private static void AddForecast(List<Forecast> forecasts, HashSet<ForecastKey> seen, Forecast forecast)
|
||||
{
|
||||
if (seen.Add(new(forecast.Kind, forecast.Position)))
|
||||
forecasts.Add(forecast);
|
||||
}
|
||||
|
||||
private static GlobalState UpdateGlobal(LevelState level, CellState[] cells)
|
||||
{
|
||||
var reactorHeat = cells.Where(c => c.Prop == ECellProp.Reactor).Select(c => c.Hazards.Heat).DefaultIfEmpty(level.Global.CoreHeat).Max();
|
||||
var poweredGenerators = cells.Count(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
|
||||
var poweredPumps = cells.Count(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
|
||||
var damagedCriticalCells = cells.Count(c => c.Prop is ECellProp.Reactor or ECellProp.Generator or ECellProp.CoolingPump && c.Hazards.Stability <= Balancing.Current.CriticalCellStabilityThreshold);
|
||||
var stability = Rules.Clamp(level.Global.FacilityStability - damagedCriticalCells);
|
||||
var lost = reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold || stability <= Balancing.Current.StabilityCollapseThreshold;
|
||||
var status = lost ? reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold ? "CORE MELTDOWN" : "FACILITY STABILITY COLLAPSE" : "STABILIZE SYSTEMS";
|
||||
var global = level.Global with {
|
||||
CoreHeat = Rules.Clamp(reactorHeat - poweredPumps),
|
||||
Power = Rules.Clamp(poweredGenerators * Balancing.Current.GeneratorPowerOutput),
|
||||
Cooling = Rules.Clamp(poweredPumps * Balancing.Current.CoolingPumpOutput),
|
||||
FacilityStability = stability,
|
||||
Lost = lost,
|
||||
Status = status
|
||||
};
|
||||
|
||||
return IsReactorReady(level with { Cells = cells, Global = global }) ? global with { Status = "REACTOR READY" } : global;
|
||||
}
|
||||
|
||||
private static bool IsReactorReady(LevelState level)
|
||||
{
|
||||
var hasReactor = level.Cells.Any(c => c.Prop == ECellProp.Reactor);
|
||||
var hasStablePower = level.Global.Power >= Balancing.Current.ReactorReadyPowerThreshold || level.Cells.Any(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
|
||||
var hasCooling = level.Global.Cooling >= Balancing.Current.ReactorReadyCoolingThreshold || level.Cells.Any(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
|
||||
var reactorStable = level.Global.CoreHeat < Balancing.Current.ReactorReadyCoreHeatThreshold;
|
||||
return hasReactor && hasStablePower && hasCooling && reactorStable && !level.Global.Lost;
|
||||
}
|
||||
|
||||
private readonly IReadOnlyList<IAreaSimulationEffect> m_AreaEffects = areaEffects.ToArray();
|
||||
private readonly IReadOnlyList<ISimulationEffect> m_Effects = effects.ToArray();
|
||||
private readonly IReadOnlyList<Hazard> m_Hazards = hazards.ToArray();
|
||||
using ReactorMaintenance.Simulation.Effects;
|
||||
using ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed class SimulationEngine(IEnumerable<ISimulationEffect> effects, IEnumerable<IAreaSimulationEffect> areaEffects, IEnumerable<Hazard> hazards)
|
||||
{
|
||||
private sealed record ForecastKey(EFailureKind Kind, GridPosition? Position);
|
||||
|
||||
public SimulationEngine()
|
||||
: this(
|
||||
[new PipeLeakEffect(), new MachineEffect(), new FireAndElectricalHazardEffect(), new CellIntegrityEffect()],
|
||||
[new SmokeSpreadEffect()],
|
||||
[new PipeBurstHazard(), new IgnitionHazard(), new MeltdownHazard(), new StabilityCollapseHazard()])
|
||||
{
|
||||
}
|
||||
|
||||
public LevelState AdvanceTurn(LevelState level)
|
||||
{
|
||||
return AdvanceTurn(level, true);
|
||||
}
|
||||
|
||||
public IReadOnlyList<Forecast> Forecast(LevelState level)
|
||||
{
|
||||
var forecasts = new List<Forecast>();
|
||||
var seen = new HashSet<ForecastKey>();
|
||||
var forecastLevel = level with { Cells = level.Cells.ToArray(), Forecasts = Array.Empty<Forecast>() };
|
||||
if (forecastLevel.Global.Lost)
|
||||
AddHazardForecasts(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
|
||||
|
||||
AddReactorReadyForecast(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
|
||||
|
||||
if (IsReactorReady(forecastLevel) || forecastLevel.Global.Lost || forecastLevel.Global.ReactorActivated)
|
||||
return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
|
||||
|
||||
for (var step = Balancing.Current.TurnIncrement; step <= Balancing.Current.MaxForecastStepCount; step++)
|
||||
{
|
||||
forecastLevel = AdvanceTurn(forecastLevel, false);
|
||||
AddHazardForecasts(forecasts, seen, forecastLevel, step);
|
||||
AddReactorReadyForecast(forecasts, seen, forecastLevel, step);
|
||||
|
||||
if (forecastLevel.Global.Lost || IsReactorReady(forecastLevel) || forecastLevel.Global.ReactorActivated)
|
||||
break;
|
||||
}
|
||||
|
||||
return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
|
||||
}
|
||||
|
||||
public LevelState ActivateReactor(LevelState level)
|
||||
{
|
||||
if (!IsReactorReady(level))
|
||||
return level with { Global = level.Global with { Status = "REACTOR NOT READY" } };
|
||||
|
||||
return level with {
|
||||
Global = level.Global with {
|
||||
ReactorActivated = true,
|
||||
Status = "REACTOR ONLINE"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private LevelState AdvanceTurn(LevelState level, bool updateForecasts)
|
||||
{
|
||||
var cells = level.Cells.ToArray();
|
||||
|
||||
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
|
||||
{
|
||||
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var index = level.Index(position);
|
||||
var cell = cells[index];
|
||||
|
||||
if (!cell.IsWalkable)
|
||||
continue;
|
||||
|
||||
foreach (var effect in m_Effects)
|
||||
cell = effect.Apply(cell);
|
||||
|
||||
cells[index] = cell with { Hazards = cell.Hazards.Clamp() };
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var areaEffect in m_AreaEffects)
|
||||
cells = areaEffect.Apply(level, cells);
|
||||
|
||||
var global = UpdateGlobal(level, cells);
|
||||
var next = level with {
|
||||
Cells = cells,
|
||||
Global = global with { Turn = level.Global.Turn + Balancing.Current.TurnIncrement }
|
||||
};
|
||||
|
||||
return updateForecasts ? next with { Forecasts = Forecast(next) } : next;
|
||||
}
|
||||
|
||||
private void AddHazardForecasts(List<Forecast> forecasts, HashSet<ForecastKey> seen, LevelState level, int turns)
|
||||
{
|
||||
foreach (var hazard in m_Hazards)
|
||||
{
|
||||
foreach (var forecast in hazard.Predict(level, turns))
|
||||
AddForecast(forecasts, seen, forecast);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddReactorReadyForecast(List<Forecast> forecasts, HashSet<ForecastKey> seen, LevelState level, int turns)
|
||||
{
|
||||
if (IsReactorReady(level))
|
||||
AddForecast(forecasts, seen, new(EFailureKind.ReactorReady, null, turns, "REACTOR READY"));
|
||||
}
|
||||
|
||||
private static void AddForecast(List<Forecast> forecasts, HashSet<ForecastKey> seen, Forecast forecast)
|
||||
{
|
||||
if (seen.Add(new(forecast.Kind, forecast.Position)))
|
||||
forecasts.Add(forecast);
|
||||
}
|
||||
|
||||
private static GlobalState UpdateGlobal(LevelState level, CellState[] cells)
|
||||
{
|
||||
var reactorHeat = cells.Where(c => c.Prop == ECellProp.Reactor).Select(c => c.Hazards.Heat).DefaultIfEmpty(level.Global.CoreHeat).Max();
|
||||
var poweredGenerators = cells.Count(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
|
||||
var poweredPumps = cells.Count(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
|
||||
var damagedCriticalCells = cells.Count(c => c.Prop is ECellProp.Reactor or ECellProp.Generator or ECellProp.CoolingPump && c.Hazards.Stability <= Balancing.Current.CriticalCellStabilityThreshold);
|
||||
var stability = Rules.Clamp(level.Global.FacilityStability - damagedCriticalCells);
|
||||
var lost = reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold || stability <= Balancing.Current.StabilityCollapseThreshold;
|
||||
var status = lost ? reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold ? "CORE MELTDOWN" : "FACILITY STABILITY COLLAPSE" : "STABILIZE SYSTEMS";
|
||||
var global = level.Global with {
|
||||
CoreHeat = Rules.Clamp(reactorHeat - poweredPumps),
|
||||
Power = Rules.Clamp(poweredGenerators * Balancing.Current.GeneratorPowerOutput),
|
||||
Cooling = Rules.Clamp(poweredPumps * Balancing.Current.CoolingPumpOutput),
|
||||
FacilityStability = stability,
|
||||
Lost = lost,
|
||||
Status = status
|
||||
};
|
||||
|
||||
return IsReactorReady(level with { Cells = cells, Global = global }) ? global with { Status = "REACTOR READY" } : global;
|
||||
}
|
||||
|
||||
private static bool IsReactorReady(LevelState level)
|
||||
{
|
||||
var hasReactor = level.Cells.Any(c => c.Prop == ECellProp.Reactor);
|
||||
var hasStablePower = level.Global.Power >= Balancing.Current.ReactorReadyPowerThreshold || level.Cells.Any(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
|
||||
var hasCooling = level.Global.Cooling >= Balancing.Current.ReactorReadyCoolingThreshold || level.Cells.Any(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
|
||||
var reactorStable = level.Global.CoreHeat < Balancing.Current.ReactorReadyCoreHeatThreshold;
|
||||
return hasReactor && hasStablePower && hasCooling && reactorStable && !level.Global.Lost;
|
||||
}
|
||||
|
||||
private readonly IReadOnlyList<IAreaSimulationEffect> m_AreaEffects = areaEffects.ToArray();
|
||||
private readonly IReadOnlyList<ISimulationEffect> m_Effects = effects.ToArray();
|
||||
private readonly IReadOnlyList<Hazard> m_Hazards = hazards.ToArray();
|
||||
}
|
||||
Reference in New Issue
Block a user