From c46b6664edd6b33e94ee17ef2413172dfca772d9 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Fri, 8 May 2026 21:45:43 +0200 Subject: [PATCH] Organize simulation systems and balancing profiles --- CODESTYLE.md | 1 + README.md | 2 +- .../Balancing.cs | 146 +++++++++--------- .../CellIntegrityEffect.cs | 33 ---- .../Difficulties/NormalBalancing.cs | 76 +++++++++ .../Effects/CellIntegrityEffect.cs | 35 +++++ .../Effects/FireAndElectricalHazardEffect.cs | 30 ++++ .../{ => Effects}/IAreaSimulationEffect.cs | 4 +- .../Effects/ISimulationEffect.cs | 8 + .../{ => Effects}/MachineEffect.cs | 10 +- .../Effects/PipeLeakEffect.cs | 28 ++++ .../{ => Effects}/SmokeSpreadEffect.cs | 12 +- .../FireAndElectricalHazardEffect.cs | 28 ---- .../{ => Hazards}/Hazard.cs | 4 +- .../Hazards/IgnitionHazard.cs | 20 +++ .../{ => Hazards}/MeltdownHazard.cs | 4 +- .../{ => Hazards}/PipeBurstHazard.cs | 10 +- .../{ => Hazards}/StabilityCollapseHazard.cs | 4 +- .../ISimulationEffect.cs | 6 - .../IgnitionHazard.cs | 18 --- .../LevelEditor.cs | 34 ++-- src/ReactorMaintenance.Simulation/Models.cs | 48 +++--- .../PipeLeakEffect.cs | 26 ---- .../SimulationEngine.cs | 33 ++-- .../SimulationEngineTests.cs | 55 +++++-- 25 files changed, 406 insertions(+), 269 deletions(-) delete mode 100644 src/ReactorMaintenance.Simulation/CellIntegrityEffect.cs create mode 100644 src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs create mode 100644 src/ReactorMaintenance.Simulation/Effects/CellIntegrityEffect.cs create mode 100644 src/ReactorMaintenance.Simulation/Effects/FireAndElectricalHazardEffect.cs rename src/ReactorMaintenance.Simulation/{ => Effects}/IAreaSimulationEffect.cs (53%) create mode 100644 src/ReactorMaintenance.Simulation/Effects/ISimulationEffect.cs rename src/ReactorMaintenance.Simulation/{ => Effects}/MachineEffect.cs (58%) create mode 100644 src/ReactorMaintenance.Simulation/Effects/PipeLeakEffect.cs rename src/ReactorMaintenance.Simulation/{ => Effects}/SmokeSpreadEffect.cs (70%) delete mode 100644 src/ReactorMaintenance.Simulation/FireAndElectricalHazardEffect.cs rename src/ReactorMaintenance.Simulation/{ => Hazards}/Hazard.cs (55%) create mode 100644 src/ReactorMaintenance.Simulation/Hazards/IgnitionHazard.cs rename src/ReactorMaintenance.Simulation/{ => Hazards}/MeltdownHazard.cs (77%) rename src/ReactorMaintenance.Simulation/{ => Hazards}/PipeBurstHazard.cs (57%) rename src/ReactorMaintenance.Simulation/{ => Hazards}/StabilityCollapseHazard.cs (79%) delete mode 100644 src/ReactorMaintenance.Simulation/ISimulationEffect.cs delete mode 100644 src/ReactorMaintenance.Simulation/IgnitionHazard.cs delete mode 100644 src/ReactorMaintenance.Simulation/PipeLeakEffect.cs diff --git a/CODESTYLE.md b/CODESTYLE.md index ec5431c..eeb4e9d 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -13,6 +13,7 @@ This repository follows the local `.editorconfig` and the style visible in the c - Prefix private static fields and static readonly fields with `s_`. - Prefix constants with `c_`. - Avoid `this.` unless it is needed for clarity or disambiguation. +- Always use folder-based namespaces when creating types and refactoring. ## Files And Types diff --git a/README.md b/README.md index 1780cb7..555ba01 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ C# WinUI 3 + Win2D level editor for the deterministic grid simulation described ## Projects -- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, JSON serialization, and `Balancing.cs` tuning values. +- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, JSON serialization, and swappable difficulty balancing profiles. - `src/ReactorMaintenance.Win2D`: Win2D editor app for painting square grid cells, loading/saving levels, advancing simulation turns, and activating the reactor. - `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior. diff --git a/src/ReactorMaintenance.Simulation/Balancing.cs b/src/ReactorMaintenance.Simulation/Balancing.cs index e615a12..890bb46 100644 --- a/src/ReactorMaintenance.Simulation/Balancing.cs +++ b/src/ReactorMaintenance.Simulation/Balancing.cs @@ -1,74 +1,78 @@ -namespace ReactorMaintenance.Simulation; +using ReactorMaintenance.Simulation.Difficulties; -public static class Balancing +namespace ReactorMaintenance.Simulation; + +public abstract class Balancing { - public static int MinHazardValue => 0; - public static int MaxHazardValue => 10; - public static int DefaultHazardStability => 10; - public static int DefaultCellIntegrity => 10; - public static int DefaultActionsPerTurn => 3; - public static int DefaultCoreHeat => 5; - public static int DefaultFacilityStability => 10; - public static int DefaultPower => 5; - public static int DefaultCooling => 0; - public static int FirstGridCoordinate => 0; - public static int NeighborDistance => 1; - public static int CurrentForecastTurn => 0; - public static int MinimumLevelSize => 4; - public static int DefaultLevelWidth => 16; - public static int DefaultLevelHeight => 12; - public static int DefaultRobotCoordinate => 1; - public static int DefaultPipeFlow => 4; - public static int DefaultPipePressure => 4; - public static int DefaultPressurePipeFlow => 5; - public static int DefaultPressurePipePressure => 6; - public static int DefaultEditedPipeIntegrity => 8; - public static int MinimumLeakRate => 1; - public static int DamagedPipeIntegrity => 4; - public static int RepairedLeakRate => 0; - public static int RepairedElectricalCharge => 0; - public static int HeatToolIncrease => 2; - public static int FireToolMinimumHeat => 7; - public static int FireToolMinimumSmoke => 3; - public static int MaxForecastStepCount => 12; - public static int TurnIncrement => 1; - public static int OverpressureThreshold => 7; - public static int HeatIntegrityDamageThreshold => 10; - public static int PipeFireIntegrityDamage => 1; - public static int FireStabilityDamage => 1; - public static int BurstLeakRate => 3; - public static int BrokenPipeFlow => 0; - public static int ElectrifiedCoolantPoolingThreshold => 3; - public static int ElectricalChargeIncrease => 2; - public static int FuelVaporFireThreshold => 4; - public static int LiquidFuelFireThreshold => 6; - public static int HeatIgnitionThreshold => 8; - public static int ElectricalIgnitionThreshold => 4; - public static int FireHeatIncrease => 2; - public static int FireSmokeIncrease => 2; - public static int FireLiquidFuelConsumption => 1; - public static int FireFuelVaporConsumption => 1; - public static int SmokeDecay => 1; - public static int PressurizedFuelLeakPressureThreshold => 7; - public static int PassiveFuelVaporHeatOffset => 3; - public static int PassiveFuelVaporDivisor => 3; - public static int MinimumCoolantHeatReduction => 1; - public static int CoolantHeatReductionDivisor => 2; - public static int CoolantSteamHeatThreshold => 7; - public static int CoolantSteamSmokeIncrease => 2; - public static int PressureLeakSmokeThreshold => 8; - public static int PressureLeakSmokeIncrease => 1; - public static int GeneratorHeatIncrease => 1; - public static int CoolingPumpHeatReduction => 2; - public static int ReactorHeatIncrease => 1; - public static int SmokeSpreadThreshold => 6; - public static int SmokeSpreadIncrease => 1; - public static int CriticalCellStabilityThreshold => 3; - public static int MeltdownCoreHeatThreshold => 10; - public static int StabilityCollapseThreshold => 0; - public static int GeneratorPowerOutput => 3; - public static int CoolingPumpOutput => 3; - public static int ReactorReadyPowerThreshold => 3; - public static int ReactorReadyCoolingThreshold => 3; - public static int ReactorReadyCoreHeatThreshold => 8; + public static Balancing Current { get; set; } = new NormalBalancing(); + + public abstract int MinHazardValue { get; } + public abstract int MaxHazardValue { get; } + public abstract int DefaultHazardStability { get; } + public abstract int DefaultCellIntegrity { get; } + public abstract int DefaultActionsPerTurn { get; } + public abstract int DefaultCoreHeat { get; } + public abstract int DefaultFacilityStability { get; } + public abstract int DefaultPower { get; } + public abstract int DefaultCooling { get; } + public abstract int FirstGridCoordinate { get; } + public abstract int NeighborDistance { get; } + public abstract int CurrentForecastTurn { get; } + public abstract int MinimumLevelSize { get; } + public abstract int DefaultLevelWidth { get; } + public abstract int DefaultLevelHeight { get; } + public abstract int DefaultRobotCoordinate { get; } + public abstract int DefaultPipeFlow { get; } + public abstract int DefaultPipePressure { get; } + public abstract int DefaultPressurePipeFlow { get; } + public abstract int DefaultPressurePipePressure { get; } + public abstract int DefaultEditedPipeIntegrity { get; } + public abstract int MinimumLeakRate { get; } + public abstract int DamagedPipeIntegrity { get; } + public abstract int RepairedLeakRate { get; } + public abstract int RepairedElectricalCharge { get; } + public abstract int HeatToolIncrease { get; } + public abstract int FireToolMinimumHeat { get; } + public abstract int FireToolMinimumSmoke { get; } + public abstract int MaxForecastStepCount { get; } + public abstract int TurnIncrement { get; } + public abstract int OverpressureThreshold { get; } + public abstract int HeatIntegrityDamageThreshold { get; } + public abstract int PipeFireIntegrityDamage { get; } + public abstract int FireStabilityDamage { get; } + public abstract int BurstLeakRate { get; } + public abstract int BrokenPipeFlow { get; } + public abstract int ElectrifiedCoolantPoolingThreshold { get; } + public abstract int ElectricalChargeIncrease { get; } + public abstract int FuelVaporFireThreshold { get; } + public abstract int LiquidFuelFireThreshold { get; } + public abstract int HeatIgnitionThreshold { get; } + public abstract int ElectricalIgnitionThreshold { get; } + public abstract int FireHeatIncrease { get; } + public abstract int FireSmokeIncrease { get; } + public abstract int FireLiquidFuelConsumption { get; } + public abstract int FireFuelVaporConsumption { get; } + public abstract int SmokeDecay { get; } + public abstract int PressurizedFuelLeakPressureThreshold { get; } + public abstract int PassiveFuelVaporHeatOffset { get; } + public abstract int PassiveFuelVaporDivisor { get; } + public abstract int MinimumCoolantHeatReduction { get; } + public abstract int CoolantHeatReductionDivisor { get; } + public abstract int CoolantSteamHeatThreshold { get; } + public abstract int CoolantSteamSmokeIncrease { get; } + public abstract int PressureLeakSmokeThreshold { get; } + public abstract int PressureLeakSmokeIncrease { get; } + public abstract int GeneratorHeatIncrease { get; } + public abstract int CoolingPumpHeatReduction { get; } + public abstract int ReactorHeatIncrease { get; } + public abstract int SmokeSpreadThreshold { get; } + public abstract int SmokeSpreadIncrease { get; } + public abstract int CriticalCellStabilityThreshold { get; } + public abstract int MeltdownCoreHeatThreshold { get; } + public abstract int StabilityCollapseThreshold { get; } + public abstract int GeneratorPowerOutput { get; } + public abstract int CoolingPumpOutput { get; } + public abstract int ReactorReadyPowerThreshold { get; } + public abstract int ReactorReadyCoolingThreshold { get; } + public abstract int ReactorReadyCoreHeatThreshold { get; } } \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/CellIntegrityEffect.cs b/src/ReactorMaintenance.Simulation/CellIntegrityEffect.cs deleted file mode 100644 index c611632..0000000 --- a/src/ReactorMaintenance.Simulation/CellIntegrityEffect.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -public sealed class CellIntegrityEffect : ISimulationEffect -{ - public CellState Apply(CellState cell) - { - var integrity = cell.Integrity; - var hazards = cell.Hazards; - - if (cell is { HasPipe: true } && cell.Pressure > Balancing.OverpressureThreshold) - integrity -= cell.Pressure - Balancing.OverpressureThreshold; - - if (hazards.Heat >= Balancing.HeatIntegrityDamageThreshold || hazards.Fire) - { - integrity -= cell.HasPipe ? Balancing.PipeFireIntegrityDamage : Balancing.MinHazardValue; - hazards = hazards with { Stability = hazards.Stability - Balancing.FireStabilityDamage }; - } - - cell = cell with { - Integrity = Rules.Clamp(integrity), - Hazards = hazards.Clamp() - }; - - if (integrity > Balancing.MinHazardValue || !cell.HasPipe) - return cell; - - return cell with { - LeakRate = Math.Max(cell.LeakRate, Balancing.BurstLeakRate), - Flow = Balancing.BrokenPipeFlow, - PipeOpen = false - }; - } -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs b/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs new file mode 100644 index 0000000..40572cb --- /dev/null +++ b/src/ReactorMaintenance.Simulation/Difficulties/NormalBalancing.cs @@ -0,0 +1,76 @@ +using ReactorMaintenance.Simulation; + +namespace ReactorMaintenance.Simulation.Difficulties; + +public class NormalBalancing : Balancing +{ + public override int MinHazardValue => 0; + public override int MaxHazardValue => 10; + public override int DefaultHazardStability => 10; + public override int DefaultCellIntegrity => 10; + public override int DefaultActionsPerTurn => 3; + public override int DefaultCoreHeat => 5; + public override int DefaultFacilityStability => 10; + public override int DefaultPower => 5; + public override int DefaultCooling => 0; + public override int FirstGridCoordinate => 0; + public override int NeighborDistance => 1; + public override int CurrentForecastTurn => 0; + public override int MinimumLevelSize => 4; + public override int DefaultLevelWidth => 16; + public override int DefaultLevelHeight => 12; + public override int DefaultRobotCoordinate => 1; + public override int DefaultPipeFlow => 4; + public override int DefaultPipePressure => 4; + public override int DefaultPressurePipeFlow => 5; + public override int DefaultPressurePipePressure => 6; + public override int DefaultEditedPipeIntegrity => 8; + public override int MinimumLeakRate => 1; + public override int DamagedPipeIntegrity => 4; + public override int RepairedLeakRate => 0; + public override int RepairedElectricalCharge => 0; + public override int HeatToolIncrease => 2; + public override int FireToolMinimumHeat => 7; + public override int FireToolMinimumSmoke => 3; + public override int MaxForecastStepCount => 12; + public override int TurnIncrement => 1; + public override int OverpressureThreshold => 7; + public override int HeatIntegrityDamageThreshold => 10; + public override int PipeFireIntegrityDamage => 1; + public override int FireStabilityDamage => 1; + public override int BurstLeakRate => 3; + public override int BrokenPipeFlow => 0; + public override int ElectrifiedCoolantPoolingThreshold => 3; + public override int ElectricalChargeIncrease => 2; + public override int FuelVaporFireThreshold => 4; + public override int LiquidFuelFireThreshold => 6; + public override int HeatIgnitionThreshold => 8; + public override int ElectricalIgnitionThreshold => 4; + public override int FireHeatIncrease => 2; + public override int FireSmokeIncrease => 2; + public override int FireLiquidFuelConsumption => 1; + public override int FireFuelVaporConsumption => 1; + public override int SmokeDecay => 1; + public override int PressurizedFuelLeakPressureThreshold => 7; + public override int PassiveFuelVaporHeatOffset => 3; + public override int PassiveFuelVaporDivisor => 3; + public override int MinimumCoolantHeatReduction => 1; + public override int CoolantHeatReductionDivisor => 2; + public override int CoolantSteamHeatThreshold => 7; + public override int CoolantSteamSmokeIncrease => 2; + public override int PressureLeakSmokeThreshold => 8; + public override int PressureLeakSmokeIncrease => 1; + public override int GeneratorHeatIncrease => 1; + public override int CoolingPumpHeatReduction => 2; + public override int ReactorHeatIncrease => 1; + public override int SmokeSpreadThreshold => 6; + public override int SmokeSpreadIncrease => 1; + public override int CriticalCellStabilityThreshold => 3; + public override int MeltdownCoreHeatThreshold => 10; + public override int StabilityCollapseThreshold => 0; + public override int GeneratorPowerOutput => 3; + public override int CoolingPumpOutput => 3; + public override int ReactorReadyPowerThreshold => 3; + public override int ReactorReadyCoolingThreshold => 3; + public override int ReactorReadyCoreHeatThreshold => 8; +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Effects/CellIntegrityEffect.cs b/src/ReactorMaintenance.Simulation/Effects/CellIntegrityEffect.cs new file mode 100644 index 0000000..d32ad13 --- /dev/null +++ b/src/ReactorMaintenance.Simulation/Effects/CellIntegrityEffect.cs @@ -0,0 +1,35 @@ +using ReactorMaintenance.Simulation; + +namespace ReactorMaintenance.Simulation.Effects; + +public sealed class CellIntegrityEffect : ISimulationEffect +{ + public CellState Apply(CellState cell) + { + var integrity = cell.Integrity; + var hazards = cell.Hazards; + + if (cell is { HasPipe: true } && cell.Pressure > Balancing.Current.OverpressureThreshold) + integrity -= cell.Pressure - Balancing.Current.OverpressureThreshold; + + if (hazards.Heat >= Balancing.Current.HeatIntegrityDamageThreshold || hazards.Fire) + { + integrity -= cell.HasPipe ? Balancing.Current.PipeFireIntegrityDamage : Balancing.Current.MinHazardValue; + hazards = hazards with { Stability = hazards.Stability - Balancing.Current.FireStabilityDamage }; + } + + cell = cell with { + Integrity = Rules.Clamp(integrity), + Hazards = hazards.Clamp() + }; + + if (integrity > Balancing.Current.MinHazardValue || !cell.HasPipe) + return cell; + + return cell with { + LeakRate = Math.Max(cell.LeakRate, Balancing.Current.BurstLeakRate), + Flow = Balancing.Current.BrokenPipeFlow, + PipeOpen = false + }; + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Effects/FireAndElectricalHazardEffect.cs b/src/ReactorMaintenance.Simulation/Effects/FireAndElectricalHazardEffect.cs new file mode 100644 index 0000000..99be29f --- /dev/null +++ b/src/ReactorMaintenance.Simulation/Effects/FireAndElectricalHazardEffect.cs @@ -0,0 +1,30 @@ +using ReactorMaintenance.Simulation; + +namespace ReactorMaintenance.Simulation.Effects; + +public sealed class FireAndElectricalHazardEffect : ISimulationEffect +{ + public CellState Apply(CellState cell) + { + var hazards = cell.Hazards; + if (hazards.CoolantPooling >= Balancing.Current.ElectrifiedCoolantPoolingThreshold && cell.Powered) + hazards = hazards with { ElectricalCharge = hazards.ElectricalCharge + Balancing.Current.ElectricalChargeIncrease }; + + var hasFuel = hazards.FuelVapor >= Balancing.Current.FuelVaporFireThreshold || hazards.LiquidFuel >= Balancing.Current.LiquidFuelFireThreshold; + var hasIgnition = hazards.Heat >= Balancing.Current.HeatIgnitionThreshold || hazards.ElectricalCharge >= Balancing.Current.ElectricalIgnitionThreshold || cell is { Kind: ECellKind.Generator, Powered: true }; + if ((hasFuel && hasIgnition) || hazards.Fire) + { + hazards = hazards with { + Fire = hasFuel || hazards.Fire, + Heat = hazards.Heat + Balancing.Current.FireHeatIncrease, + Smoke = hazards.Smoke + Balancing.Current.FireSmokeIncrease, + LiquidFuel = Math.Max(Balancing.Current.MinHazardValue, hazards.LiquidFuel - Balancing.Current.FireLiquidFuelConsumption), + FuelVapor = Math.Max(Balancing.Current.MinHazardValue, hazards.FuelVapor - Balancing.Current.FireFuelVaporConsumption) + }; + } + else if (hazards.Smoke > Balancing.Current.MinHazardValue) + hazards = hazards with { Smoke = hazards.Smoke - Balancing.Current.SmokeDecay }; + + return cell with { Hazards = hazards.Clamp() }; + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/IAreaSimulationEffect.cs b/src/ReactorMaintenance.Simulation/Effects/IAreaSimulationEffect.cs similarity index 53% rename from src/ReactorMaintenance.Simulation/IAreaSimulationEffect.cs rename to src/ReactorMaintenance.Simulation/Effects/IAreaSimulationEffect.cs index 059edda..4bba0cc 100644 --- a/src/ReactorMaintenance.Simulation/IAreaSimulationEffect.cs +++ b/src/ReactorMaintenance.Simulation/Effects/IAreaSimulationEffect.cs @@ -1,4 +1,6 @@ -namespace ReactorMaintenance.Simulation; +using ReactorMaintenance.Simulation; + +namespace ReactorMaintenance.Simulation.Effects; public interface IAreaSimulationEffect { diff --git a/src/ReactorMaintenance.Simulation/Effects/ISimulationEffect.cs b/src/ReactorMaintenance.Simulation/Effects/ISimulationEffect.cs new file mode 100644 index 0000000..4d6a1b5 --- /dev/null +++ b/src/ReactorMaintenance.Simulation/Effects/ISimulationEffect.cs @@ -0,0 +1,8 @@ +using ReactorMaintenance.Simulation; + +namespace ReactorMaintenance.Simulation.Effects; + +public interface ISimulationEffect +{ + CellState Apply(CellState cell); +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/MachineEffect.cs b/src/ReactorMaintenance.Simulation/Effects/MachineEffect.cs similarity index 58% rename from src/ReactorMaintenance.Simulation/MachineEffect.cs rename to src/ReactorMaintenance.Simulation/Effects/MachineEffect.cs index e879604..97389a8 100644 --- a/src/ReactorMaintenance.Simulation/MachineEffect.cs +++ b/src/ReactorMaintenance.Simulation/Effects/MachineEffect.cs @@ -1,13 +1,15 @@ -namespace ReactorMaintenance.Simulation; +using ReactorMaintenance.Simulation; + +namespace ReactorMaintenance.Simulation.Effects; public sealed class MachineEffect : ISimulationEffect { public CellState Apply(CellState cell) { var hazards = cell.Kind switch { - ECellKind.Generator when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.GeneratorHeatIncrease }, - ECellKind.CoolingPump when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat - Balancing.CoolingPumpHeatReduction }, - ECellKind.Reactor => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.ReactorHeatIncrease }, + ECellKind.Generator when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.GeneratorHeatIncrease }, + ECellKind.CoolingPump when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat - Balancing.Current.CoolingPumpHeatReduction }, + ECellKind.Reactor => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.ReactorHeatIncrease }, _ => cell.Hazards }; diff --git a/src/ReactorMaintenance.Simulation/Effects/PipeLeakEffect.cs b/src/ReactorMaintenance.Simulation/Effects/PipeLeakEffect.cs new file mode 100644 index 0000000..458f24a --- /dev/null +++ b/src/ReactorMaintenance.Simulation/Effects/PipeLeakEffect.cs @@ -0,0 +1,28 @@ +using ReactorMaintenance.Simulation; + +namespace ReactorMaintenance.Simulation.Effects; + +public sealed class PipeLeakEffect : ISimulationEffect +{ + public CellState Apply(CellState cell) + { + if (!cell.HasPipe || cell.LeakRate <= Balancing.Current.MinHazardValue) + return cell; + + var hazards = cell.Pipe switch { + EPipeMedium.Fuel => cell.Hazards with { + LiquidFuel = cell.Hazards.LiquidFuel + cell.LeakRate, + FuelVapor = cell.Hazards.FuelVapor + (cell.Pressure >= Balancing.Current.PressurizedFuelLeakPressureThreshold ? cell.LeakRate : Math.Max(Balancing.Current.MinHazardValue, cell.Hazards.Heat - Balancing.Current.PassiveFuelVaporHeatOffset) / Balancing.Current.PassiveFuelVaporDivisor) + }, + EPipeMedium.Coolant => cell.Hazards with { + CoolantPooling = cell.Hazards.CoolantPooling + cell.LeakRate, + Heat = cell.Hazards.Heat - Math.Max(Balancing.Current.MinimumCoolantHeatReduction, cell.LeakRate / Balancing.Current.CoolantHeatReductionDivisor), + Smoke = cell.Hazards.Smoke + (cell.Hazards.Heat >= Balancing.Current.CoolantSteamHeatThreshold ? Balancing.Current.CoolantSteamSmokeIncrease : Balancing.Current.MinHazardValue) + }, + EPipeMedium.Pressure => cell.Hazards with { Smoke = cell.Hazards.Smoke + (cell.Pressure >= Balancing.Current.PressureLeakSmokeThreshold ? Balancing.Current.PressureLeakSmokeIncrease : Balancing.Current.MinHazardValue) }, + _ => cell.Hazards + }; + + return cell with { Hazards = hazards.Clamp() }; + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/SmokeSpreadEffect.cs b/src/ReactorMaintenance.Simulation/Effects/SmokeSpreadEffect.cs similarity index 70% rename from src/ReactorMaintenance.Simulation/SmokeSpreadEffect.cs rename to src/ReactorMaintenance.Simulation/Effects/SmokeSpreadEffect.cs index 7dab7b2..432d9f1 100644 --- a/src/ReactorMaintenance.Simulation/SmokeSpreadEffect.cs +++ b/src/ReactorMaintenance.Simulation/Effects/SmokeSpreadEffect.cs @@ -1,17 +1,19 @@ -namespace ReactorMaintenance.Simulation; +using ReactorMaintenance.Simulation; + +namespace ReactorMaintenance.Simulation.Effects; public sealed class SmokeSpreadEffect : IAreaSimulationEffect { public CellState[] Apply(LevelState level, CellState[] cells) { var next = cells.ToArray(); - for (var y = Balancing.FirstGridCoordinate; y < level.Height; y++) + for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++) { - for (var x = Balancing.FirstGridCoordinate; x < level.Width; x++) + for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++) { var position = new GridPosition(x, y); var cell = cells[level.Index(position)]; - if (cell.Hazards.Smoke < Balancing.SmokeSpreadThreshold) + if (cell.Hazards.Smoke < Balancing.Current.SmokeSpreadThreshold) continue; SpreadToNeighbors(level, next, position); @@ -29,7 +31,7 @@ public sealed class SmokeSpreadEffect : IAreaSimulationEffect if (!neighborCell.IsWalkable || neighborCell.DoorLocked) continue; - next[level.Index(neighbor)] = neighborCell with { Hazards = neighborCell.Hazards with { Smoke = Rules.Clamp(neighborCell.Hazards.Smoke + Balancing.SmokeSpreadIncrease) } }; + next[level.Index(neighbor)] = neighborCell with { Hazards = neighborCell.Hazards with { Smoke = Rules.Clamp(neighborCell.Hazards.Smoke + Balancing.Current.SmokeSpreadIncrease) } }; } } } \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/FireAndElectricalHazardEffect.cs b/src/ReactorMaintenance.Simulation/FireAndElectricalHazardEffect.cs deleted file mode 100644 index c64cc32..0000000 --- a/src/ReactorMaintenance.Simulation/FireAndElectricalHazardEffect.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -public sealed class FireAndElectricalHazardEffect : ISimulationEffect -{ - public CellState Apply(CellState cell) - { - var hazards = cell.Hazards; - if (hazards.CoolantPooling >= Balancing.ElectrifiedCoolantPoolingThreshold && cell.Powered) - hazards = hazards with { ElectricalCharge = hazards.ElectricalCharge + Balancing.ElectricalChargeIncrease }; - - var hasFuel = hazards.FuelVapor >= Balancing.FuelVaporFireThreshold || hazards.LiquidFuel >= Balancing.LiquidFuelFireThreshold; - var hasIgnition = hazards.Heat >= Balancing.HeatIgnitionThreshold || hazards.ElectricalCharge >= Balancing.ElectricalIgnitionThreshold || cell is { Kind: ECellKind.Generator, Powered: true }; - if ((hasFuel && hasIgnition) || hazards.Fire) - { - hazards = hazards with { - Fire = hasFuel || hazards.Fire, - Heat = hazards.Heat + Balancing.FireHeatIncrease, - Smoke = hazards.Smoke + Balancing.FireSmokeIncrease, - LiquidFuel = Math.Max(Balancing.MinHazardValue, hazards.LiquidFuel - Balancing.FireLiquidFuelConsumption), - FuelVapor = Math.Max(Balancing.MinHazardValue, hazards.FuelVapor - Balancing.FireFuelVaporConsumption) - }; - } - else if (hazards.Smoke > Balancing.MinHazardValue) - hazards = hazards with { Smoke = hazards.Smoke - Balancing.SmokeDecay }; - - return cell with { Hazards = hazards.Clamp() }; - } -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Hazard.cs b/src/ReactorMaintenance.Simulation/Hazards/Hazard.cs similarity index 55% rename from src/ReactorMaintenance.Simulation/Hazard.cs rename to src/ReactorMaintenance.Simulation/Hazards/Hazard.cs index d4f35ab..d6a7814 100644 --- a/src/ReactorMaintenance.Simulation/Hazard.cs +++ b/src/ReactorMaintenance.Simulation/Hazards/Hazard.cs @@ -1,4 +1,6 @@ -namespace ReactorMaintenance.Simulation; +using ReactorMaintenance.Simulation; + +namespace ReactorMaintenance.Simulation.Hazards; public abstract class Hazard { diff --git a/src/ReactorMaintenance.Simulation/Hazards/IgnitionHazard.cs b/src/ReactorMaintenance.Simulation/Hazards/IgnitionHazard.cs new file mode 100644 index 0000000..0d320cc --- /dev/null +++ b/src/ReactorMaintenance.Simulation/Hazards/IgnitionHazard.cs @@ -0,0 +1,20 @@ +using ReactorMaintenance.Simulation; + +namespace ReactorMaintenance.Simulation.Hazards; + +public sealed class IgnitionHazard : Hazard +{ + public override IEnumerable Predict(LevelState level, int turns) + { + 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 cell = level.GetCell(position); + if (cell.Hazards.Fire) + yield return new(EFailureKind.Ignition, position, turns, turns == Balancing.Current.TurnIncrement ? $"FUEL IGNITION PREDICTED AT {x},{y} NEXT TURN" : $"FUEL IGNITION PREDICTED AT {x},{y} IN {turns} TURNS"); + } + } + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/MeltdownHazard.cs b/src/ReactorMaintenance.Simulation/Hazards/MeltdownHazard.cs similarity index 77% rename from src/ReactorMaintenance.Simulation/MeltdownHazard.cs rename to src/ReactorMaintenance.Simulation/Hazards/MeltdownHazard.cs index f2e121e..28e1452 100644 --- a/src/ReactorMaintenance.Simulation/MeltdownHazard.cs +++ b/src/ReactorMaintenance.Simulation/Hazards/MeltdownHazard.cs @@ -1,4 +1,6 @@ -namespace ReactorMaintenance.Simulation; +using ReactorMaintenance.Simulation; + +namespace ReactorMaintenance.Simulation.Hazards; public sealed class MeltdownHazard : Hazard { diff --git a/src/ReactorMaintenance.Simulation/PipeBurstHazard.cs b/src/ReactorMaintenance.Simulation/Hazards/PipeBurstHazard.cs similarity index 57% rename from src/ReactorMaintenance.Simulation/PipeBurstHazard.cs rename to src/ReactorMaintenance.Simulation/Hazards/PipeBurstHazard.cs index c578f7b..8c41a9b 100644 --- a/src/ReactorMaintenance.Simulation/PipeBurstHazard.cs +++ b/src/ReactorMaintenance.Simulation/Hazards/PipeBurstHazard.cs @@ -1,16 +1,18 @@ -namespace ReactorMaintenance.Simulation; +using ReactorMaintenance.Simulation; + +namespace ReactorMaintenance.Simulation.Hazards; public sealed class PipeBurstHazard : Hazard { public override IEnumerable Predict(LevelState level, int turns) { - for (var y = Balancing.FirstGridCoordinate; y < level.Height; y++) + for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++) { - for (var x = Balancing.FirstGridCoordinate; x < level.Width; x++) + for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++) { var position = new GridPosition(x, y); var cell = level.GetCell(position); - if (cell is { HasPipe: true, PipeOpen: false } && cell.Flow == Balancing.BrokenPipeFlow && cell.LeakRate >= Balancing.BurstLeakRate) + if (cell is { HasPipe: true, PipeOpen: false } && cell.Flow == Balancing.Current.BrokenPipeFlow && cell.LeakRate >= Balancing.Current.BurstLeakRate) yield return new(EFailureKind.PipeBurst, position, turns, $"PIPE BURST PREDICTED AT {x},{y} IN {turns} TURNS"); } } diff --git a/src/ReactorMaintenance.Simulation/StabilityCollapseHazard.cs b/src/ReactorMaintenance.Simulation/Hazards/StabilityCollapseHazard.cs similarity index 79% rename from src/ReactorMaintenance.Simulation/StabilityCollapseHazard.cs rename to src/ReactorMaintenance.Simulation/Hazards/StabilityCollapseHazard.cs index 2cd8c38..6510b40 100644 --- a/src/ReactorMaintenance.Simulation/StabilityCollapseHazard.cs +++ b/src/ReactorMaintenance.Simulation/Hazards/StabilityCollapseHazard.cs @@ -1,4 +1,6 @@ -namespace ReactorMaintenance.Simulation; +using ReactorMaintenance.Simulation; + +namespace ReactorMaintenance.Simulation.Hazards; public sealed class StabilityCollapseHazard : Hazard { diff --git a/src/ReactorMaintenance.Simulation/ISimulationEffect.cs b/src/ReactorMaintenance.Simulation/ISimulationEffect.cs deleted file mode 100644 index 3508659..0000000 --- a/src/ReactorMaintenance.Simulation/ISimulationEffect.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -public interface ISimulationEffect -{ - CellState Apply(CellState cell); -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/IgnitionHazard.cs b/src/ReactorMaintenance.Simulation/IgnitionHazard.cs deleted file mode 100644 index 51b855f..0000000 --- a/src/ReactorMaintenance.Simulation/IgnitionHazard.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -public sealed class IgnitionHazard : Hazard -{ - public override IEnumerable Predict(LevelState level, int turns) - { - 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 cell = level.GetCell(position); - if (cell.Hazards.Fire) - yield return new(EFailureKind.Ignition, position, turns, turns == Balancing.TurnIncrement ? $"FUEL IGNITION PREDICTED AT {x},{y} NEXT TURN" : $"FUEL IGNITION PREDICTED AT {x},{y} IN {turns} TURNS"); - } - } - } -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/LevelEditor.cs b/src/ReactorMaintenance.Simulation/LevelEditor.cs index a9e09fc..8d60e9a 100644 --- a/src/ReactorMaintenance.Simulation/LevelEditor.cs +++ b/src/ReactorMaintenance.Simulation/LevelEditor.cs @@ -58,43 +58,43 @@ public static class LevelEditor }, EEditorTool.CoolantPipe => cell with { Pipe = EPipeMedium.Coolant, - Flow = Balancing.DefaultPipeFlow, - Pressure = Balancing.DefaultPipePressure, - Integrity = Math.Max(cell.Integrity, Balancing.DefaultEditedPipeIntegrity), + Flow = Balancing.Current.DefaultPipeFlow, + Pressure = Balancing.Current.DefaultPipePressure, + Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity), PipeOpen = true }, EEditorTool.FuelPipe => cell with { Pipe = EPipeMedium.Fuel, - Flow = Balancing.DefaultPipeFlow, - Pressure = Balancing.DefaultPipePressure, - Integrity = Math.Max(cell.Integrity, Balancing.DefaultEditedPipeIntegrity), + Flow = Balancing.Current.DefaultPipeFlow, + Pressure = Balancing.Current.DefaultPipePressure, + Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity), PipeOpen = true }, EEditorTool.PressurePipe => cell with { Pipe = EPipeMedium.Pressure, - Flow = Balancing.DefaultPressurePipeFlow, - Pressure = Balancing.DefaultPressurePipePressure, - Integrity = Math.Max(cell.Integrity, Balancing.DefaultEditedPipeIntegrity), + Flow = Balancing.Current.DefaultPressurePipeFlow, + Pressure = Balancing.Current.DefaultPressurePipePressure, + Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity), PipeOpen = true }, EEditorTool.Leak => cell with { - LeakRate = Math.Max(Balancing.MinimumLeakRate, cell.LeakRate), - Integrity = Math.Min(cell.Integrity, Balancing.DamagedPipeIntegrity) + LeakRate = Math.Max(Balancing.Current.MinimumLeakRate, cell.LeakRate), + Integrity = Math.Min(cell.Integrity, Balancing.Current.DamagedPipeIntegrity) }, EEditorTool.Repair => cell with { - LeakRate = Balancing.RepairedLeakRate, - Integrity = Balancing.DefaultCellIntegrity, + LeakRate = Balancing.Current.RepairedLeakRate, + Integrity = Balancing.Current.DefaultCellIntegrity, Hazards = cell.Hazards with { Fire = false, - ElectricalCharge = Balancing.RepairedElectricalCharge + ElectricalCharge = Balancing.Current.RepairedElectricalCharge } }, - EEditorTool.Heat => cell with { Hazards = cell.Hazards with { Heat = Rules.Clamp(cell.Hazards.Heat + Balancing.HeatToolIncrease) } }, + EEditorTool.Heat => cell with { Hazards = cell.Hazards with { Heat = Rules.Clamp(cell.Hazards.Heat + Balancing.Current.HeatToolIncrease) } }, EEditorTool.Fire => cell with { Hazards = cell.Hazards with { Fire = !cell.Hazards.Fire, - Heat = Math.Max(cell.Hazards.Heat, Balancing.FireToolMinimumHeat), - Smoke = Math.Max(cell.Hazards.Smoke, Balancing.FireToolMinimumSmoke) + Heat = Math.Max(cell.Hazards.Heat, Balancing.Current.FireToolMinimumHeat), + Smoke = Math.Max(cell.Hazards.Smoke, Balancing.Current.FireToolMinimumSmoke) } }, _ => cell diff --git a/src/ReactorMaintenance.Simulation/Models.cs b/src/ReactorMaintenance.Simulation/Models.cs index 32a0042..1a3e5d6 100644 --- a/src/ReactorMaintenance.Simulation/Models.cs +++ b/src/ReactorMaintenance.Simulation/Models.cs @@ -34,10 +34,10 @@ public sealed record GridPosition(int X, int Y) { public IEnumerable Neighbors() { - yield return new(X - Balancing.NeighborDistance, Y); - yield return new(X + Balancing.NeighborDistance, Y); - yield return new(X, Y - Balancing.NeighborDistance); - yield return new(X, Y + Balancing.NeighborDistance); + yield return new(X - Balancing.Current.NeighborDistance, Y); + yield return new(X + Balancing.Current.NeighborDistance, Y); + yield return new(X, Y - Balancing.Current.NeighborDistance); + yield return new(X, Y + Balancing.Current.NeighborDistance); } } @@ -62,7 +62,7 @@ public sealed record HazardState public int LiquidFuel { get; init; } public int CoolantPooling { get; init; } public int ElectricalCharge { get; init; } - public int Stability { get; init; } = Balancing.DefaultHazardStability; + public int Stability { get; init; } = Balancing.Current.DefaultHazardStability; public bool Fire { get; init; } } @@ -72,7 +72,7 @@ public sealed record CellState public EPipeMedium Pipe { get; init; } public int Flow { get; init; } public int Pressure { get; init; } - public int Integrity { get; init; } = Balancing.DefaultCellIntegrity; + public int Integrity { get; init; } = Balancing.Current.DefaultCellIntegrity; public int LeakRate { get; init; } public bool PipeOpen { get; init; } = true; public bool Powered { get; init; } @@ -85,11 +85,11 @@ public sealed record CellState public sealed record GlobalState { public int Turn { get; init; } - public int ActionsPerTurn { get; init; } = Balancing.DefaultActionsPerTurn; - public int CoreHeat { get; init; } = Balancing.DefaultCoreHeat; - public int FacilityStability { get; init; } = Balancing.DefaultFacilityStability; - public int Power { get; init; } = Balancing.DefaultPower; - public int Cooling { get; init; } = Balancing.DefaultCooling; + public int ActionsPerTurn { get; init; } = Balancing.Current.DefaultActionsPerTurn; + public int CoreHeat { get; init; } = Balancing.Current.DefaultCoreHeat; + public int FacilityStability { get; init; } = Balancing.Current.DefaultFacilityStability; + public int Power { get; init; } = Balancing.Current.DefaultPower; + public int Cooling { get; init; } = Balancing.Current.DefaultCooling; public bool ReactorActivated { get; init; } public bool Lost { get; init; } public string Status { get; init; } = "STABILIZE SYSTEMS"; @@ -101,15 +101,15 @@ public sealed record LevelState { public static LevelState Create(string name, int width, int height) { - if (width < Balancing.MinimumLevelSize || height < Balancing.MinimumLevelSize) - throw new ArgumentOutOfRangeException(nameof(width), $"Levels must be at least {Balancing.MinimumLevelSize}x{Balancing.MinimumLevelSize}."); + if (width < Balancing.Current.MinimumLevelSize || height < Balancing.Current.MinimumLevelSize) + throw new ArgumentOutOfRangeException(nameof(width), $"Levels must be at least {Balancing.Current.MinimumLevelSize}x{Balancing.Current.MinimumLevelSize}."); var cells = CreateCells(width, height); - for (var y = Balancing.FirstGridCoordinate; y < height; y++) + for (var y = Balancing.Current.FirstGridCoordinate; y < height; y++) { - for (var x = Balancing.FirstGridCoordinate; x < width; x++) + for (var x = Balancing.Current.FirstGridCoordinate; x < width; x++) { - if (x == Balancing.FirstGridCoordinate || y == Balancing.FirstGridCoordinate || x == width - Balancing.NeighborDistance || y == height - Balancing.NeighborDistance) + if (x == Balancing.Current.FirstGridCoordinate || y == Balancing.Current.FirstGridCoordinate || x == width - Balancing.Current.NeighborDistance || y == height - Balancing.Current.NeighborDistance) cells[y * width + x] = cells[y * width + x] with { Kind = ECellKind.Wall }; } } @@ -119,7 +119,7 @@ public sealed record LevelState Width = width, Height = height, Cells = cells, - Robot = new(Balancing.DefaultRobotCoordinate, Balancing.DefaultRobotCoordinate) + Robot = new(Balancing.Current.DefaultRobotCoordinate, Balancing.Current.DefaultRobotCoordinate) }; } @@ -139,7 +139,7 @@ public sealed record LevelState public bool InBounds(GridPosition position) { - return position.X >= Balancing.FirstGridCoordinate && position.Y >= Balancing.FirstGridCoordinate && position.X < Width && position.Y < Height; + return position.X >= Balancing.Current.FirstGridCoordinate && position.Y >= Balancing.Current.FirstGridCoordinate && position.X < Width && position.Y < Height; } public int Index(GridPosition position) @@ -155,14 +155,14 @@ public sealed record LevelState private static CellState[] CreateCells(int width, int height) { - return Enumerable.Range(Balancing.FirstGridCoordinate, width * height).Select(_ => new CellState()).ToArray(); + return Enumerable.Range(Balancing.Current.FirstGridCoordinate, width * height).Select(_ => new CellState()).ToArray(); } public string Name { get; init; } = "New Reactor"; - public int Width { get; init; } = Balancing.DefaultLevelWidth; - public int Height { get; init; } = Balancing.DefaultLevelHeight; - public CellState[] Cells { get; init; } = CreateCells(Balancing.DefaultLevelWidth, Balancing.DefaultLevelHeight); - public GridPosition Robot { get; init; } = new(Balancing.DefaultRobotCoordinate, Balancing.DefaultRobotCoordinate); + public int Width { get; init; } = Balancing.Current.DefaultLevelWidth; + public int Height { get; init; } = Balancing.Current.DefaultLevelHeight; + public CellState[] Cells { get; init; } = CreateCells(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight); + public GridPosition Robot { get; init; } = new(Balancing.Current.DefaultRobotCoordinate, Balancing.Current.DefaultRobotCoordinate); public GlobalState Global { get; init; } = new(); public IReadOnlyList Forecasts { get; init; } = Array.Empty(); } @@ -171,6 +171,6 @@ internal static class Rules { public static int Clamp(int value) { - return Math.Clamp(value, Balancing.MinHazardValue, Balancing.MaxHazardValue); + return Math.Clamp(value, Balancing.Current.MinHazardValue, Balancing.Current.MaxHazardValue); } } \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/PipeLeakEffect.cs b/src/ReactorMaintenance.Simulation/PipeLeakEffect.cs deleted file mode 100644 index 9792bb4..0000000 --- a/src/ReactorMaintenance.Simulation/PipeLeakEffect.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace ReactorMaintenance.Simulation; - -public sealed class PipeLeakEffect : ISimulationEffect -{ - public CellState Apply(CellState cell) - { - if (!cell.HasPipe || cell.LeakRate <= Balancing.MinHazardValue) - return cell; - - var hazards = cell.Pipe switch { - EPipeMedium.Fuel => cell.Hazards with { - LiquidFuel = cell.Hazards.LiquidFuel + cell.LeakRate, - FuelVapor = cell.Hazards.FuelVapor + (cell.Pressure >= Balancing.PressurizedFuelLeakPressureThreshold ? cell.LeakRate : Math.Max(Balancing.MinHazardValue, cell.Hazards.Heat - Balancing.PassiveFuelVaporHeatOffset) / Balancing.PassiveFuelVaporDivisor) - }, - EPipeMedium.Coolant => cell.Hazards with { - CoolantPooling = cell.Hazards.CoolantPooling + cell.LeakRate, - Heat = cell.Hazards.Heat - Math.Max(Balancing.MinimumCoolantHeatReduction, cell.LeakRate / Balancing.CoolantHeatReductionDivisor), - Smoke = cell.Hazards.Smoke + (cell.Hazards.Heat >= Balancing.CoolantSteamHeatThreshold ? Balancing.CoolantSteamSmokeIncrease : Balancing.MinHazardValue) - }, - EPipeMedium.Pressure => cell.Hazards with { Smoke = cell.Hazards.Smoke + (cell.Pressure >= Balancing.PressureLeakSmokeThreshold ? Balancing.PressureLeakSmokeIncrease : Balancing.MinHazardValue) }, - _ => cell.Hazards - }; - - return cell with { Hazards = hazards.Clamp() }; - } -} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/SimulationEngine.cs b/src/ReactorMaintenance.Simulation/SimulationEngine.cs index e5accdb..1bca146 100644 --- a/src/ReactorMaintenance.Simulation/SimulationEngine.cs +++ b/src/ReactorMaintenance.Simulation/SimulationEngine.cs @@ -1,4 +1,7 @@ -namespace ReactorMaintenance.Simulation; +using ReactorMaintenance.Simulation.Effects; +using ReactorMaintenance.Simulation.Hazards; + +namespace ReactorMaintenance.Simulation; public sealed class SimulationEngine(IEnumerable effects, IEnumerable areaEffects, IEnumerable hazards) { @@ -23,14 +26,14 @@ public sealed class SimulationEngine(IEnumerable effects, IEn 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); + AddHazardForecasts(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn); - AddReactorReadyForecast(forecasts, seen, forecastLevel, Balancing.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.TurnIncrement; step <= Balancing.MaxForecastStepCount; step++) + for (var step = Balancing.Current.TurnIncrement; step <= Balancing.Current.MaxForecastStepCount; step++) { forecastLevel = AdvanceTurn(forecastLevel, false); AddHazardForecasts(forecasts, seen, forecastLevel, step); @@ -60,9 +63,9 @@ public sealed class SimulationEngine(IEnumerable effects, IEn { var cells = level.Cells.ToArray(); - for (var y = Balancing.FirstGridCoordinate; y < level.Height; y++) + for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++) { - for (var x = Balancing.FirstGridCoordinate; x < level.Width; x++) + for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++) { var position = new GridPosition(x, y); var index = level.Index(position); @@ -84,7 +87,7 @@ public sealed class SimulationEngine(IEnumerable effects, IEn var global = UpdateGlobal(level, cells); var next = level with { Cells = cells, - Global = global with { Turn = level.Global.Turn + Balancing.TurnIncrement } + Global = global with { Turn = level.Global.Turn + Balancing.Current.TurnIncrement } }; return updateForecasts ? next with { Forecasts = Forecast(next) } : next; @@ -116,14 +119,14 @@ public sealed class SimulationEngine(IEnumerable effects, IEn 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 damagedCriticalCells = cells.Count(c => c.Kind is ECellKind.Reactor or ECellKind.Generator or ECellKind.CoolingPump && c.Hazards.Stability <= Balancing.Current.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 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.GeneratorPowerOutput), - Cooling = Rules.Clamp(poweredPumps * Balancing.CoolingPumpOutput), + Power = Rules.Clamp(poweredGenerators * Balancing.Current.GeneratorPowerOutput), + Cooling = Rules.Clamp(poweredPumps * Balancing.Current.CoolingPumpOutput), FacilityStability = stability, Lost = lost, Status = status @@ -135,9 +138,9 @@ public sealed class SimulationEngine(IEnumerable effects, IEn 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; + var hasStablePower = level.Global.Power >= Balancing.Current.ReactorReadyPowerThreshold || level.Cells.Any(c => c is { Kind: ECellKind.Generator, Powered: true, Hazards.Fire: false }); + var hasCooling = level.Global.Cooling >= Balancing.Current.ReactorReadyCoolingThreshold || level.Cells.Any(c => c is { Kind: ECellKind.CoolingPump, Powered: true, Hazards.Fire: false }); + var reactorStable = level.Global.CoreHeat < Balancing.Current.ReactorReadyCoreHeatThreshold; return hasReactor && hasStablePower && hasCooling && reactorStable && !level.Global.Lost; } diff --git a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs index 6c729d3..df7ecfd 100644 --- a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs +++ b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs @@ -1,4 +1,8 @@ -namespace ReactorMaintenance.Simulation.Tests; +using ReactorMaintenance.Simulation.Difficulties; +using ReactorMaintenance.Simulation.Effects; +using ReactorMaintenance.Simulation.Hazards; + +namespace ReactorMaintenance.Simulation.Tests; public sealed class SimulationEngineTests { @@ -9,9 +13,9 @@ public sealed class SimulationEngineTests .SetCell(new(2, 2), new() { Kind = ECellKind.Generator, Pipe = EPipeMedium.Fuel, - LeakRate = Balancing.FuelVaporFireThreshold, - Pressure = Balancing.PressurizedFuelLeakPressureThreshold + Balancing.NeighborDistance, - Integrity = Balancing.DefaultEditedPipeIntegrity, + LeakRate = Balancing.Current.FuelVaporFireThreshold, + Pressure = Balancing.Current.PressurizedFuelLeakPressureThreshold + Balancing.Current.NeighborDistance, + Integrity = Balancing.Current.DefaultEditedPipeIntegrity, Powered = true }); @@ -26,13 +30,13 @@ public sealed class SimulationEngineTests var level = LevelState.Create("Wet cable", 6, 6) .SetCell(new(3, 3), new() { Pipe = EPipeMedium.Coolant, - LeakRate = Balancing.ElectrifiedCoolantPoolingThreshold, + LeakRate = Balancing.Current.ElectrifiedCoolantPoolingThreshold, Powered = true }); var next = m_Engine.AdvanceTurn(level); - Assert.True(next.GetCell(new(3, 3)).Hazards.ElectricalCharge >= Balancing.ElectricalChargeIncrease); + Assert.True(next.GetCell(new(3, 3)).Hazards.ElectricalCharge >= Balancing.Current.ElectricalChargeIncrease); } [Fact] @@ -42,7 +46,7 @@ public sealed class SimulationEngineTests .SetCell(new(2, 2), new() { Hazards = new() { Fire = true, - Smoke = Balancing.SmokeSpreadThreshold + Smoke = Balancing.Current.SmokeSpreadThreshold } }); @@ -99,8 +103,28 @@ public sealed class SimulationEngineTests var forecasts = engine.Forecast(level); - Assert.Equal(Balancing.MaxForecastStepCount, forecasts.Count); - Assert.Equal(Balancing.MaxForecastStepCount, forecasts.Max(forecast => forecast.Turns)); + Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count); + Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Max(forecast => forecast.Turns)); + } + + [Fact] + public void ForecastUsesCurrentBalancingProfile() + { + var previous = Balancing.Current; + try + { + Balancing.Current = new TestBalancing(); + var engine = new SimulationEngine([], [], [new StepCountingHazard()]); + var level = LevelState.Create("Stable", 6, 6); + + var forecasts = engine.Forecast(level); + + Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count); + } + finally + { + Balancing.Current = previous; + } } [Fact] @@ -109,7 +133,7 @@ public sealed class SimulationEngineTests var level = LevelState.Create("Meltdown", 6, 6) .SetCell(new(2, 2), new() { Kind = ECellKind.Reactor, - Hazards = new() { Heat = Balancing.MeltdownCoreHeatThreshold - Balancing.ReactorHeatIncrease } + Hazards = new() { Heat = Balancing.Current.MeltdownCoreHeatThreshold - Balancing.Current.ReactorHeatIncrease } }); var forecasts = m_Engine.Forecast(level); @@ -122,7 +146,7 @@ public sealed class SimulationEngineTests { var level = LevelState.Create("Lost", 6, 6) with { Global = new() { - CoreHeat = Balancing.MeltdownCoreHeatThreshold, + CoreHeat = Balancing.Current.MeltdownCoreHeatThreshold, Lost = true, Status = "CORE MELTDOWN" } @@ -139,9 +163,9 @@ public sealed class SimulationEngineTests var level = LevelState.Create("Collapse", 6, 6) .SetCell(new(2, 2), new() { Kind = ECellKind.Generator, - Hazards = new() { Stability = Balancing.CriticalCellStabilityThreshold } + Hazards = new() { Stability = Balancing.Current.CriticalCellStabilityThreshold } }) with { - Global = new() { FacilityStability = Balancing.FireStabilityDamage } + Global = new() { FacilityStability = Balancing.Current.FireStabilityDamage } }; var forecasts = m_Engine.Forecast(level); @@ -200,6 +224,11 @@ public sealed class SimulationEngineTests } } + private sealed class TestBalancing : NormalBalancing + { + public override int MaxForecastStepCount => 2; + } + private sealed class TestCellEffect : ISimulationEffect { public CellState Apply(CellState cell)