namespace ReactorMaintenance.Simulation; public sealed class SimulationEngine(IEnumerable effects, IEnumerable areaEffects, IEnumerable 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(LevelState level) { var forecasts = new List(); var seen = new HashSet(); var forecastLevel = level with { Cells = level.Cells.ToArray(), Forecasts = Array.Empty() }; if (forecastLevel.Global.Lost) AddHazardForecasts(forecasts, seen, forecastLevel, Balancing.CurrentForecastTurn); AddReactorReadyForecast(forecasts, seen, forecastLevel, Balancing.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.TurnIncrement; step <= Balancing.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.FirstGridCoordinate; y < level.Height; y++) { for (var x = Balancing.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.TurnIncrement } }; return updateForecasts ? next with { Forecasts = Forecast(next) } : next; } private void AddHazardForecasts(List forecasts, HashSet 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 forecasts, HashSet seen, LevelState level, int turns) { if (IsReactorReady(level)) AddForecast(forecasts, seen, new(EFailureKind.ReactorReady, null, turns, "REACTOR READY")); } private static void AddForecast(List forecasts, HashSet 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.Kind == ECellKind.Reactor).Select(c => c.Hazards.Heat).DefaultIfEmpty(level.Global.CoreHeat).Max(); var poweredGenerators = cells.Count(c => c is { Kind: ECellKind.Generator, Powered: true, Hazards.Fire: false }); var poweredPumps = cells.Count(c => c is { Kind: ECellKind.CoolingPump, Powered: true, Hazards.Fire: false }); var damagedCriticalCells = cells.Count(c => c.Kind is ECellKind.Reactor or ECellKind.Generator or ECellKind.CoolingPump && c.Hazards.Stability <= Balancing.CriticalCellStabilityThreshold); var stability = Rules.Clamp(level.Global.FacilityStability - damagedCriticalCells); var lost = reactorHeat >= Balancing.MeltdownCoreHeatThreshold || stability <= Balancing.StabilityCollapseThreshold; var status = lost ? reactorHeat >= Balancing.MeltdownCoreHeatThreshold ? "CORE MELTDOWN" : "FACILITY STABILITY COLLAPSE" : "STABILIZE SYSTEMS"; var global = level.Global with { CoreHeat = Rules.Clamp(reactorHeat - poweredPumps), Power = Rules.Clamp(poweredGenerators * Balancing.GeneratorPowerOutput), Cooling = Rules.Clamp(poweredPumps * Balancing.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.Kind == ECellKind.Reactor); var hasStablePower = level.Global.Power >= Balancing.ReactorReadyPowerThreshold || level.Cells.Any(c => c is { Kind: ECellKind.Generator, Powered: true, Hazards.Fire: false }); var hasCooling = level.Global.Cooling >= Balancing.ReactorReadyCoolingThreshold || level.Cells.Any(c => c is { Kind: ECellKind.CoolingPump, Powered: true, Hazards.Fire: false }); var reactorStable = level.Global.CoreHeat < Balancing.ReactorReadyCoreHeatThreshold; return hasReactor && hasStablePower && hasCooling && reactorStable && !level.Global.Lost; } private readonly IReadOnlyList m_AreaEffects = areaEffects.ToArray(); private readonly IReadOnlyList m_Effects = effects.ToArray(); private readonly IReadOnlyList m_Hazards = hazards.ToArray(); }