From 2e813962c9d5be6ae55ee8e0aea004b829e78677 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Fri, 8 May 2026 20:47:09 +0200 Subject: [PATCH] cleanup code --- .../LevelEditor.cs | 95 ++++++-- .../LevelSerializer.cs | 32 +-- src/ReactorMaintenance.Simulation/Models.cs | 95 ++++---- .../SimulationEngine.cs | 185 ++++++--------- src/ReactorMaintenance.Win2D/App.xaml | 2 +- src/ReactorMaintenance.Win2D/App.xaml.cs | 6 +- src/ReactorMaintenance.Win2D/MainWindow.xaml | 11 +- .../MainWindow.xaml.cs | 224 +++++++++--------- .../SimulationEngineTests.cs | 92 +++---- 9 files changed, 387 insertions(+), 355 deletions(-) diff --git a/src/ReactorMaintenance.Simulation/LevelEditor.cs b/src/ReactorMaintenance.Simulation/LevelEditor.cs index bac6858..c40adc2 100644 --- a/src/ReactorMaintenance.Simulation/LevelEditor.cs +++ b/src/ReactorMaintenance.Simulation/LevelEditor.cs @@ -25,41 +25,98 @@ public static class LevelEditor public static LevelState Apply(LevelState level, GridPosition position, EditorTool tool) { if (!level.InBounds(position)) - { return level; - } if (tool == EditorTool.Robot) - { return level.GetCell(position).IsWalkable ? level with { Robot = position } : level; - } var cell = level.GetCell(position); cell = tool switch { EditorTool.Floor => cell with { Kind = CellKind.Floor }, - EditorTool.Wall => cell with { Kind = CellKind.Wall, Pipe = PipeMedium.None, Powered = false }, + EditorTool.Wall => cell with + { + Kind = CellKind.Wall, + Pipe = PipeMedium.None, + Powered = false + }, EditorTool.Reactor => cell with { Kind = CellKind.Reactor }, - EditorTool.CoolingPump => cell with { Kind = CellKind.CoolingPump, Powered = true }, - EditorTool.Generator => cell with { Kind = CellKind.Generator, Powered = true }, + EditorTool.CoolingPump => cell with + { + Kind = CellKind.CoolingPump, + Powered = true + }, + EditorTool.Generator => cell with + { + Kind = CellKind.Generator, + Powered = true + }, EditorTool.PressureRegulator => cell with { Kind = CellKind.PressureRegulator }, - EditorTool.DiagnosticTerminal => cell with { Kind = CellKind.DiagnosticTerminal, Powered = true }, - EditorTool.ControlTerminal => cell with { Kind = CellKind.ControlTerminal, Powered = true }, - EditorTool.CoolantPipe => cell with { Pipe = PipeMedium.Coolant, Flow = 4, Pressure = 4, Integrity = Math.Max(cell.Integrity, 8), PipeOpen = true }, - EditorTool.FuelPipe => cell with { Pipe = PipeMedium.Fuel, Flow = 4, Pressure = 4, Integrity = Math.Max(cell.Integrity, 8), PipeOpen = true }, - EditorTool.PressurePipe => cell with { Pipe = PipeMedium.Pressure, Flow = 5, Pressure = 6, Integrity = Math.Max(cell.Integrity, 8), PipeOpen = true }, - EditorTool.Leak => cell with { LeakRate = Math.Max(1, cell.LeakRate), Integrity = Math.Min(cell.Integrity, 4) }, - EditorTool.Repair => cell with { LeakRate = 0, Integrity = 10, Hazards = cell.Hazards with { Fire = false, ElectricalCharge = 0 } }, + EditorTool.DiagnosticTerminal => cell with + { + Kind = CellKind.DiagnosticTerminal, + Powered = true + }, + EditorTool.ControlTerminal => cell with + { + Kind = CellKind.ControlTerminal, + Powered = true + }, + EditorTool.CoolantPipe => cell with + { + Pipe = PipeMedium.Coolant, + Flow = 4, + Pressure = 4, + Integrity = Math.Max(cell.Integrity, 8), + PipeOpen = true + }, + EditorTool.FuelPipe => cell with + { + Pipe = PipeMedium.Fuel, + Flow = 4, + Pressure = 4, + Integrity = Math.Max(cell.Integrity, 8), + PipeOpen = true + }, + EditorTool.PressurePipe => cell with + { + Pipe = PipeMedium.Pressure, + Flow = 5, + Pressure = 6, + Integrity = Math.Max(cell.Integrity, 8), + PipeOpen = true + }, + EditorTool.Leak => cell with + { + LeakRate = Math.Max(1, cell.LeakRate), + Integrity = Math.Min(cell.Integrity, 4) + }, + EditorTool.Repair => cell with + { + LeakRate = 0, + Integrity = 10, + Hazards = cell.Hazards with + { + Fire = false, + ElectricalCharge = 0 + } + }, EditorTool.Heat => cell with { Hazards = cell.Hazards with { Heat = Rules.Clamp(cell.Hazards.Heat + 2) } }, - EditorTool.Fire => cell with { Hazards = cell.Hazards with { Fire = !cell.Hazards.Fire, Heat = Math.Max(cell.Hazards.Heat, 7), Smoke = Math.Max(cell.Hazards.Smoke, 3) } }, + EditorTool.Fire => cell with + { + Hazards = cell.Hazards with + { + Fire = !cell.Hazards.Fire, + Heat = Math.Max(cell.Hazards.Heat, 7), + Smoke = Math.Max(cell.Hazards.Smoke, 3) + } + }, _ => cell }; if (cell.Kind == CellKind.Wall) - { - cell = cell with { Hazards = new HazardState() }; - } + cell = cell with { Hazards = new() }; return level.SetCell(position, cell); } -} +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/LevelSerializer.cs b/src/ReactorMaintenance.Simulation/LevelSerializer.cs index 4006769..8d3fc64 100644 --- a/src/ReactorMaintenance.Simulation/LevelSerializer.cs +++ b/src/ReactorMaintenance.Simulation/LevelSerializer.cs @@ -5,24 +5,24 @@ namespace ReactorMaintenance.Simulation; public static class LevelSerializer { + public static string Serialize(LevelState level) + { + return JsonSerializer.Serialize(level, Options); + } + + public static LevelState Deserialize(string json) + { + var level = JsonSerializer.Deserialize(json, Options) ?? throw new InvalidOperationException("Level file did not contain a level."); + + if (level.Cells.Length != level.Width * level.Height) + throw new InvalidOperationException("Level cell count does not match its dimensions."); + + return level; + } + private static readonly JsonSerializerOptions Options = new() { WriteIndented = true, Converters = { new JsonStringEnumConverter() } }; - - public static string Serialize(LevelState level) => JsonSerializer.Serialize(level, Options); - - public static LevelState Deserialize(string json) - { - var level = JsonSerializer.Deserialize(json, Options) - ?? throw new InvalidOperationException("Level file did not contain a level."); - - if (level.Cells.Length != level.Width * level.Height) - { - throw new InvalidOperationException("Level cell count does not match its dimensions."); - } - - return level; - } -} +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/Models.cs b/src/ReactorMaintenance.Simulation/Models.cs index e7e45cb..64ad37e 100644 --- a/src/ReactorMaintenance.Simulation/Models.cs +++ b/src/ReactorMaintenance.Simulation/Models.cs @@ -34,15 +34,29 @@ public sealed record GridPosition(int X, int Y) { public IEnumerable Neighbors() { - yield return new GridPosition(X - 1, Y); - yield return new GridPosition(X + 1, Y); - yield return new GridPosition(X, Y - 1); - yield return new GridPosition(X, Y + 1); + yield return new(X - 1, Y); + yield return new(X + 1, Y); + yield return new(X, Y - 1); + yield return new(X, Y + 1); } } public sealed record HazardState { + public HazardState Clamp() + { + return this with + { + Heat = Rules.Clamp(Heat), + Smoke = Rules.Clamp(Smoke), + FuelVapor = Rules.Clamp(FuelVapor), + LiquidFuel = Rules.Clamp(LiquidFuel), + CoolantPooling = Rules.Clamp(CoolantPooling), + ElectricalCharge = Rules.Clamp(ElectricalCharge), + Stability = Rules.Clamp(Stability) + }; + } + public int Heat { get; init; } public int Smoke { get; init; } public int FuelVapor { get; init; } @@ -51,17 +65,6 @@ public sealed record HazardState public int ElectricalCharge { get; init; } public int Stability { get; init; } = 10; public bool Fire { get; init; } - - public HazardState Clamp() => this with - { - Heat = Rules.Clamp(Heat), - Smoke = Rules.Clamp(Smoke), - FuelVapor = Rules.Clamp(FuelVapor), - LiquidFuel = Rules.Clamp(LiquidFuel), - CoolantPooling = Rules.Clamp(CoolantPooling), - ElectricalCharge = Rules.Clamp(ElectricalCharge), - Stability = Rules.Clamp(Stability) - }; } public sealed record CellState @@ -98,40 +101,24 @@ public sealed record Forecast(FailureKind Kind, GridPosition? Position, int Turn public sealed record LevelState { - public string Name { get; init; } = "New Reactor"; - public int Width { get; init; } = 16; - public int Height { get; init; } = 12; - public CellState[] Cells { get; init; } = CreateCells(16, 12); - public GridPosition Robot { get; init; } = new(1, 1); - public GlobalState Global { get; init; } = new(); - public IReadOnlyList Forecasts { get; init; } = Array.Empty(); - public static LevelState Create(string name, int width, int height) { if (width < 4 || height < 4) - { throw new ArgumentOutOfRangeException(nameof(width), "Levels must be at least 4x4."); - } var cells = CreateCells(width, height); for (var y = 0; y < height; y++) - { - for (var x = 0; x < width; x++) - { - if (x == 0 || y == 0 || x == width - 1 || y == height - 1) - { - cells[y * width + x] = cells[y * width + x] with { Kind = CellKind.Wall }; - } - } - } + for (var x = 0; x < width; x++) + if (x == 0 || y == 0 || x == width - 1 || y == height - 1) + cells[y * width + x] = cells[y * width + x] with { Kind = CellKind.Wall }; - return new LevelState + return new() { Name = name, Width = width, Height = height, Cells = cells, - Robot = new GridPosition(1, 1) + Robot = new(1, 1) }; } @@ -149,26 +136,40 @@ public sealed record LevelState return this with { Cells = cells }; } - public bool InBounds(GridPosition position) => - position.X >= 0 && position.Y >= 0 && position.X < Width && position.Y < Height; + public bool InBounds(GridPosition position) + { + return position.X >= 0 && position.Y >= 0 && position.X < Width && position.Y < Height; + } - public int Index(GridPosition position) => position.Y * Width + position.X; + public int Index(GridPosition position) + { + return position.Y * Width + position.X; + } private void EnsureInBounds(GridPosition position) { if (!InBounds(position)) - { throw new ArgumentOutOfRangeException(nameof(position), $"Position {position.X},{position.Y} is outside {Width}x{Height}."); - } } - private static CellState[] CreateCells(int width, int height) => - Enumerable.Range(0, width * height) - .Select(_ => new CellState()) - .ToArray(); + private static CellState[] CreateCells(int width, int height) + { + return Enumerable.Range(0, width * height).Select(_ => new CellState()).ToArray(); + } + + public string Name { get; init; } = "New Reactor"; + public int Width { get; init; } = 16; + public int Height { get; init; } = 12; + public CellState[] Cells { get; init; } = CreateCells(16, 12); + public GridPosition Robot { get; init; } = new(1, 1); + public GlobalState Global { get; init; } = new(); + public IReadOnlyList Forecasts { get; init; } = Array.Empty(); } internal static class Rules { - public static int Clamp(int value) => Math.Clamp(value, 0, 10); -} + public static int Clamp(int value) + { + return Math.Clamp(value, 0, 10); + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/SimulationEngine.cs b/src/ReactorMaintenance.Simulation/SimulationEngine.cs index 00fb0a3..814fc42 100644 --- a/src/ReactorMaintenance.Simulation/SimulationEngine.cs +++ b/src/ReactorMaintenance.Simulation/SimulationEngine.cs @@ -7,51 +7,45 @@ public sealed class SimulationEngine var cells = level.Cells.ToArray(); for (var y = 0; y < level.Height; y++) + for (var x = 0; x < level.Width; x++) { - for (var x = 0; x < level.Width; x++) + var position = new GridPosition(x, y); + var index = level.Index(position); + var cell = cells[index]; + + if (!cell.IsWalkable) + continue; + + var hazards = ApplyPipeLeaks(cell); + hazards = ApplyMachineEffects(cell, hazards); + hazards = ApplyFireAndElectricalHazards(cell, hazards); + hazards = hazards.Clamp(); + + var integrity = cell.Integrity; + if (cell.HasPipe && cell.Pressure > 7) + integrity -= cell.Pressure - 7; + + if (hazards.Heat >= 10 || hazards.Fire) { - var position = new GridPosition(x, y); - var index = level.Index(position); - var cell = cells[index]; + integrity -= cell.HasPipe ? 1 : 0; + hazards = hazards with { Stability = hazards.Stability - 1 }; + } - if (!cell.IsWalkable) + if (integrity <= 0 && cell.HasPipe) + { + cell = cell with { - continue; - } - - var hazards = ApplyPipeLeaks(cell); - hazards = ApplyMachineEffects(cell, hazards); - hazards = ApplyFireAndElectricalHazards(cell, hazards); - hazards = hazards.Clamp(); - - var integrity = cell.Integrity; - if (cell.HasPipe && cell.Pressure > 7) - { - integrity -= cell.Pressure - 7; - } - - if (hazards.Heat >= 10 || hazards.Fire) - { - integrity -= cell.HasPipe ? 1 : 0; - hazards = hazards with { Stability = hazards.Stability - 1 }; - } - - if (integrity <= 0 && cell.HasPipe) - { - cell = cell with - { - LeakRate = Math.Max(cell.LeakRate, 3), - Flow = 0, - PipeOpen = false - }; - } - - cells[index] = cell with - { - Integrity = Rules.Clamp(integrity), - Hazards = hazards.Clamp() + LeakRate = Math.Max(cell.LeakRate, 3), + Flow = 0, + PipeOpen = false }; } + + cells[index] = cell with + { + Integrity = Rules.Clamp(integrity), + Hazards = hazards.Clamp() + }; } cells = SpreadSmoke(level, cells); @@ -71,43 +65,30 @@ public sealed class SimulationEngine var forecasts = new List(); for (var y = 0; y < level.Height; y++) + for (var x = 0; x < level.Width; x++) { - for (var x = 0; x < level.Width; x++) + var position = new GridPosition(x, y); + var cell = level.GetCell(position); + + if (cell.HasPipe && cell.Pressure > 7 && cell.Integrity > 0) { - var position = new GridPosition(x, y); - var cell = level.GetCell(position); - - if (cell.HasPipe && cell.Pressure > 7 && cell.Integrity > 0) - { - var damagePerTurn = Math.Max(1, cell.Pressure - 7); - var turns = (int)Math.Ceiling(cell.Integrity / (double)damagePerTurn); - if (turns <= 4) - { - forecasts.Add(new Forecast(FailureKind.PipeBurst, position, turns, $"PIPE BURST PREDICTED AT {x},{y} IN {turns} TURNS")); - } - } - - var fuelLeakNearIgnition = cell.Pipe == PipeMedium.Fuel && - cell.LeakRate > 0 && - (cell.Pressure >= 7 || cell.Kind == CellKind.Generator); - var ignitionRisk = (cell.Hazards.FuelVapor >= 4 || cell.Hazards.LiquidFuel >= 6 || fuelLeakNearIgnition) && - (cell.Hazards.Heat >= 8 || cell.Hazards.ElectricalCharge >= 4 || cell.Kind == CellKind.Generator); - if (ignitionRisk && !cell.Hazards.Fire) - { - forecasts.Add(new Forecast(FailureKind.Ignition, position, 1, $"FUEL IGNITION PREDICTED AT {x},{y} NEXT TURN")); - } + var damagePerTurn = Math.Max(1, cell.Pressure - 7); + var turns = (int)Math.Ceiling(cell.Integrity / (double)damagePerTurn); + if (turns <= 4) + forecasts.Add(new(FailureKind.PipeBurst, position, turns, $"PIPE BURST PREDICTED AT {x},{y} IN {turns} TURNS")); } + + var fuelLeakNearIgnition = cell.Pipe == PipeMedium.Fuel && cell.LeakRate > 0 && (cell.Pressure >= 7 || cell.Kind == CellKind.Generator); + var ignitionRisk = (cell.Hazards.FuelVapor >= 4 || cell.Hazards.LiquidFuel >= 6 || fuelLeakNearIgnition) && (cell.Hazards.Heat >= 8 || cell.Hazards.ElectricalCharge >= 4 || cell.Kind == CellKind.Generator); + if (ignitionRisk && !cell.Hazards.Fire) + forecasts.Add(new(FailureKind.Ignition, position, 1, $"FUEL IGNITION PREDICTED AT {x},{y} NEXT TURN")); } if (level.Global.CoreHeat >= 8) - { - forecasts.Add(new Forecast(FailureKind.Meltdown, null, Math.Max(1, 11 - level.Global.CoreHeat), "CORE MELTDOWN APPROACHING")); - } + forecasts.Add(new(FailureKind.Meltdown, null, Math.Max(1, 11 - level.Global.CoreHeat), "CORE MELTDOWN APPROACHING")); if (IsReactorReady(level)) - { - forecasts.Add(new Forecast(FailureKind.ReactorReady, null, 0, "REACTOR READY")); - } + forecasts.Add(new(FailureKind.ReactorReady, null, 0, "REACTOR READY")); return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray(); } @@ -115,9 +96,7 @@ public sealed class SimulationEngine public LevelState ActivateReactor(LevelState level) { if (!IsReactorReady(level)) - { return level with { Global = level.Global with { Status = "REACTOR NOT READY" } }; - } return level with { @@ -133,9 +112,7 @@ public sealed class SimulationEngine { var hazards = cell.Hazards; if (!cell.HasPipe || cell.LeakRate <= 0) - { return hazards; - } return cell.Pipe switch { @@ -150,11 +127,8 @@ public sealed class SimulationEngine Heat = hazards.Heat - Math.Max(1, cell.LeakRate / 2), Smoke = hazards.Smoke + (hazards.Heat >= 7 ? 2 : 0) }, - PipeMedium.Pressure => hazards with - { - Smoke = hazards.Smoke + (cell.Pressure >= 8 ? 1 : 0) - }, - _ => hazards + PipeMedium.Pressure => hazards with { Smoke = hazards.Smoke + (cell.Pressure >= 8 ? 1 : 0) }, + _ => hazards }; } @@ -162,19 +136,17 @@ public sealed class SimulationEngine { return cell.Kind switch { - CellKind.Generator when cell.Powered => hazards with { Heat = hazards.Heat + 1 }, + CellKind.Generator when cell.Powered => hazards with { Heat = hazards.Heat + 1 }, CellKind.CoolingPump when cell.Powered => hazards with { Heat = hazards.Heat - 2 }, - CellKind.Reactor => hazards with { Heat = hazards.Heat + 1 }, - _ => hazards + CellKind.Reactor => hazards with { Heat = hazards.Heat + 1 }, + _ => hazards }; } private static HazardState ApplyFireAndElectricalHazards(CellState cell, HazardState hazards) { if (hazards.CoolantPooling >= 3 && cell.Powered) - { hazards = hazards with { ElectricalCharge = hazards.ElectricalCharge + 2 }; - } var hasFuel = hazards.FuelVapor >= 4 || hazards.LiquidFuel >= 6; var hasIgnition = hazards.Heat >= 8 || hazards.ElectricalCharge >= 4 || (cell.Kind == CellKind.Generator && cell.Powered); @@ -190,9 +162,7 @@ public sealed class SimulationEngine }; } else if (hazards.Smoke > 0) - { hazards = hazards with { Smoke = hazards.Smoke - 1 }; - } return hazards; } @@ -201,29 +171,20 @@ public sealed class SimulationEngine { var next = cells.ToArray(); for (var y = 0; y < level.Height; y++) + for (var x = 0; x < level.Width; x++) { - for (var x = 0; x < level.Width; x++) + var position = new GridPosition(x, y); + var cell = cells[level.Index(position)]; + if (cell.Hazards.Smoke < 6) + continue; + + foreach (var neighbor in position.Neighbors().Where(level.InBounds)) { - var position = new GridPosition(x, y); - var cell = cells[level.Index(position)]; - if (cell.Hazards.Smoke < 6) - { + var neighborCell = next[level.Index(neighbor)]; + if (!neighborCell.IsWalkable || neighborCell.DoorLocked) continue; - } - foreach (var neighbor in position.Neighbors().Where(level.InBounds)) - { - var neighborCell = next[level.Index(neighbor)]; - if (!neighborCell.IsWalkable || neighborCell.DoorLocked) - { - continue; - } - - next[level.Index(neighbor)] = neighborCell with - { - Hazards = neighborCell.Hazards with { Smoke = Rules.Clamp(neighborCell.Hazards.Smoke + 1) } - }; - } + next[level.Index(neighbor)] = neighborCell with { Hazards = neighborCell.Hazards with { Smoke = Rules.Clamp(neighborCell.Hazards.Smoke + 1) } }; } } @@ -232,11 +193,7 @@ public sealed class SimulationEngine private static GlobalState UpdateGlobal(LevelState level, CellState[] cells) { - var reactorHeat = cells - .Where(c => c.Kind == CellKind.Reactor) - .Select(c => c.Hazards.Heat) - .DefaultIfEmpty(level.Global.CoreHeat) - .Max(); + var reactorHeat = cells.Where(c => c.Kind == CellKind.Reactor).Select(c => c.Hazards.Heat).DefaultIfEmpty(level.Global.CoreHeat).Max(); var poweredGenerators = cells.Count(c => c.Kind == CellKind.Generator && c.Powered && !c.Hazards.Fire); var poweredPumps = cells.Count(c => c.Kind == CellKind.CoolingPump && c.Powered && !c.Hazards.Fire); @@ -244,9 +201,7 @@ public sealed class SimulationEngine var stability = Rules.Clamp(level.Global.FacilityStability - damagedCriticalCells); var lost = reactorHeat >= 10 || stability <= 0; - var status = lost - ? (reactorHeat >= 10 ? "CORE MELTDOWN" : "FACILITY STABILITY COLLAPSE") - : "STABILIZE SYSTEMS"; + var status = lost ? reactorHeat >= 10 ? "CORE MELTDOWN" : "FACILITY STABILITY COLLAPSE" : "STABILIZE SYSTEMS"; var global = level.Global with { @@ -258,7 +213,11 @@ public sealed class SimulationEngine Status = status }; - return IsReactorReady(level with { Cells = cells, Global = global }) + return IsReactorReady(level with + { + Cells = cells, + Global = global + }) ? global with { Status = "REACTOR READY" } : global; } @@ -271,4 +230,4 @@ public sealed class SimulationEngine var reactorStable = level.Global.CoreHeat < 8; return hasReactor && hasStablePower && hasCooling && reactorStable && !level.Global.Lost; } -} +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Win2D/App.xaml b/src/ReactorMaintenance.Win2D/App.xaml index 4fb8631..3fc383f 100644 --- a/src/ReactorMaintenance.Win2D/App.xaml +++ b/src/ReactorMaintenance.Win2D/App.xaml @@ -2,4 +2,4 @@ x:Class="ReactorMaintenance.Win2D.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - + \ No newline at end of file diff --git a/src/ReactorMaintenance.Win2D/App.xaml.cs b/src/ReactorMaintenance.Win2D/App.xaml.cs index edf2043..3161abc 100644 --- a/src/ReactorMaintenance.Win2D/App.xaml.cs +++ b/src/ReactorMaintenance.Win2D/App.xaml.cs @@ -4,8 +4,6 @@ namespace ReactorMaintenance.Win2D; public partial class App : Application { - private Window? _window; - public App() { InitializeComponent(); @@ -16,4 +14,6 @@ public partial class App : Application _window = new MainWindow(); _window.Activate(); } -} + + private Window? _window; +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml b/src/ReactorMaintenance.Win2D/MainWindow.xaml index fcc1fbb..b9e56e1 100644 --- a/src/ReactorMaintenance.Win2D/MainWindow.xaml +++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml @@ -32,7 +32,8 @@ - + @@ -48,7 +49,8 @@ - + @@ -74,7 +76,8 @@ - + @@ -84,4 +87,4 @@ - + \ No newline at end of file diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs index 663df9f..e401434 100644 --- a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs +++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs @@ -1,4 +1,8 @@ using System.Numerics; +using Windows.Foundation; +using Windows.Storage; +using Windows.Storage.Pickers; +using Windows.UI; using Microsoft.Graphics.Canvas; using Microsoft.Graphics.Canvas.Text; using Microsoft.Graphics.Canvas.UI.Xaml; @@ -7,21 +11,19 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using ReactorMaintenance.Simulation; -using Windows.Foundation; -using Windows.Storage; -using Windows.Storage.Pickers; using WinRT.Interop; namespace ReactorMaintenance.Win2D; public sealed partial class MainWindow : Window { - private readonly SimulationEngine _simulation = new(); - private LevelState _level; - private EditorTool _selectedTool = EditorTool.Floor; - private GridPosition? _selectedCell; - private StorageFile? _currentFile; - private bool _painting; + private sealed record CanvasLayout(double CellSize, double OriginX, double OriginY) + { + public Rect CellRect(int x, int y) + { + return new(OriginX + x * CellSize, OriginY + y * CellSize, CellSize, CellSize); + } + } public MainWindow() { @@ -36,9 +38,7 @@ public sealed partial class MainWindow : Window private void ToolPicker_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (ToolPicker.SelectedItem is EditorTool tool) - { _selectedTool = tool; - } } private void New_Click(object sender, RoutedEventArgs e) @@ -58,9 +58,7 @@ public sealed partial class MainWindow : Window var file = await picker.PickSingleFileAsync(); if (file is null) - { return; - } var json = await FileIO.ReadTextAsync(file); _level = LevelSerializer.Deserialize(json); @@ -84,9 +82,7 @@ public sealed partial class MainWindow : Window } if (file is null) - { return; - } await FileIO.WriteTextAsync(file, LevelSerializer.Serialize(_level)); _currentFile = file; @@ -116,9 +112,7 @@ public sealed partial class MainWindow : Window private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e) { if (_painting) - { PaintAt(e.GetCurrentPoint(LevelCanvas).Position); - } } private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e) @@ -130,9 +124,7 @@ public sealed partial class MainWindow : Window private void PaintAt(Point point) { if (!TryGetGridPosition(point, out var position)) - { return; - } _selectedCell = position; _level = LevelEditor.Apply(_level, position, _selectedTool); @@ -155,46 +147,38 @@ public sealed partial class MainWindow : Window private void DrawCells(CanvasDrawingSession drawing, CanvasLayout layout) { for (var y = 0; y < _level.Height; y++) + for (var x = 0; x < _level.Width; x++) { - for (var x = 0; x < _level.Width; x++) + var position = new GridPosition(x, y); + var cell = _level.GetCell(position); + var rect = layout.CellRect(x, y); + + drawing.FillRectangle(rect, CellColor(cell)); + + if (cell.HasPipe) { - var position = new GridPosition(x, y); - var cell = _level.GetCell(position); - var rect = layout.CellRect(x, y); - - drawing.FillRectangle(rect, CellColor(cell)); - - if (cell.HasPipe) + var center = new Vector2((float)(rect.X + rect.Width / 2), (float)(rect.Y + rect.Height / 2)); + var pipeColor = cell.Pipe switch { - var center = new Vector2((float)(rect.X + rect.Width / 2), (float)(rect.Y + rect.Height / 2)); - var pipeColor = cell.Pipe switch - { - PipeMedium.Coolant => Colors.DeepSkyBlue, - PipeMedium.Fuel => Colors.Goldenrod, - PipeMedium.Pressure => Colors.LightSteelBlue, - _ => Colors.Transparent - }; - drawing.DrawLine(new Vector2((float)rect.X + 6, center.Y), new Vector2((float)(rect.X + rect.Width - 6), center.Y), pipeColor, Math.Max(3, (float)rect.Width / 7)); - drawing.DrawLine(new Vector2(center.X, (float)rect.Y + 6), new Vector2(center.X, (float)(rect.Y + rect.Height - 6)), pipeColor, Math.Max(3, (float)rect.Width / 7)); - } - - if (cell.LeakRate > 0) - { - drawing.DrawCircle(new Vector2((float)(rect.X + rect.Width - 10), (float)(rect.Y + 10)), 5, Colors.OrangeRed, 2); - } - - if (cell.Hazards.Fire) - { - drawing.FillCircle(new Vector2((float)(rect.X + rect.Width * 0.5), (float)(rect.Y + rect.Height * 0.5)), (float)rect.Width * 0.24f, Colors.OrangeRed); - } - - if (_selectedCell == position) - { - drawing.DrawRectangle(rect, Colors.White, 3); - } - - DrawCellGlyph(drawing, cell, rect); + PipeMedium.Coolant => Colors.DeepSkyBlue, + PipeMedium.Fuel => Colors.Goldenrod, + PipeMedium.Pressure => Colors.LightSteelBlue, + _ => Colors.Transparent + }; + drawing.DrawLine(new((float)rect.X + 6, center.Y), new((float)(rect.X + rect.Width - 6), center.Y), pipeColor, Math.Max(3, (float)rect.Width / 7)); + drawing.DrawLine(new(center.X, (float)rect.Y + 6), new(center.X, (float)(rect.Y + rect.Height - 6)), pipeColor, Math.Max(3, (float)rect.Width / 7)); } + + if (cell.LeakRate > 0) + drawing.DrawCircle(new((float)(rect.X + rect.Width - 10), (float)(rect.Y + 10)), 5, Colors.OrangeRed, 2); + + if (cell.Hazards.Fire) + drawing.FillCircle(new((float)(rect.X + rect.Width * 0.5), (float)(rect.Y + rect.Height * 0.5)), (float)rect.Width * 0.24f, Colors.OrangeRed); + + if (_selectedCell == position) + drawing.DrawRectangle(rect, Colors.White, 3); + + DrawCellGlyph(drawing, cell, rect); } } @@ -202,19 +186,17 @@ public sealed partial class MainWindow : Window { var text = cell.Kind switch { - CellKind.Reactor => "R", - CellKind.CoolingPump => "C", - CellKind.Generator => "G", - CellKind.PressureRegulator => "P", + CellKind.Reactor => "R", + CellKind.CoolingPump => "C", + CellKind.Generator => "G", + CellKind.PressureRegulator => "P", CellKind.DiagnosticTerminal => "D", - CellKind.ControlTerminal => "T", - _ => string.Empty + CellKind.ControlTerminal => "T", + _ => string.Empty }; if (string.IsNullOrEmpty(text)) - { return; - } using var format = new CanvasTextFormat { @@ -254,7 +236,7 @@ public sealed partial class MainWindow : Window var layout = GetLayout(); var x = (int)((point.X - layout.OriginX) / layout.CellSize); var y = (int)((point.Y - layout.OriginY) / layout.CellSize); - position = new GridPosition(x, y); + position = new(x, y); return _level.InBounds(position); } @@ -266,30 +248,26 @@ public sealed partial class MainWindow : Window size = Math.Max(20, size); var originX = Math.Max(0, (availableWidth - size * _level.Width) / 2); var originY = Math.Max(0, (availableHeight - size * _level.Height) / 2); - return new CanvasLayout(size, originX, originY); + return new(size, originX, originY); } - private static Windows.UI.Color CellColor(CellState cell) + private static Color CellColor(CellState cell) { if (cell.Kind == CellKind.Wall) - { return ColorHelper.FromArgb(255, 54, 61, 68); - } if (cell.Hazards.Fire) - { return ColorHelper.FromArgb(255, 91, 39, 30); - } return cell.Kind switch { - CellKind.Reactor => ColorHelper.FromArgb(255, 61, 76, 82), - CellKind.CoolingPump => ColorHelper.FromArgb(255, 25, 79, 96), - CellKind.Generator => ColorHelper.FromArgb(255, 86, 75, 35), - CellKind.PressureRegulator => ColorHelper.FromArgb(255, 70, 78, 98), + CellKind.Reactor => ColorHelper.FromArgb(255, 61, 76, 82), + CellKind.CoolingPump => ColorHelper.FromArgb(255, 25, 79, 96), + CellKind.Generator => ColorHelper.FromArgb(255, 86, 75, 35), + CellKind.PressureRegulator => ColorHelper.FromArgb(255, 70, 78, 98), CellKind.DiagnosticTerminal => ColorHelper.FromArgb(255, 39, 84, 62), - CellKind.ControlTerminal => ColorHelper.FromArgb(255, 80, 61, 91), - _ => ColorHelper.FromArgb(255, 31, 36, 40) + CellKind.ControlTerminal => ColorHelper.FromArgb(255, 80, 61, 91), + _ => ColorHelper.FromArgb(255, 31, 36, 40) }; } @@ -298,29 +276,15 @@ public sealed partial class MainWindow : Window LevelNameText.Text = _level.Name; TurnText.Text = _level.Global.Turn.ToString(); StatusText.Text = _level.Global.Status; - GlobalText.Text = - $"Power: {_level.Global.Power}/10\n" + - $"Cooling: {_level.Global.Cooling}/10\n" + - $"Core Heat: {_level.Global.CoreHeat}/10\n" + - $"Facility Stability: {_level.Global.FacilityStability}/10"; + GlobalText.Text = $"Power: {_level.Global.Power}/10\n" + $"Cooling: {_level.Global.Cooling}/10\n" + $"Core Heat: {_level.Global.CoreHeat}/10\n" + $"Facility Stability: {_level.Global.FacilityStability}/10"; if (_selectedCell is { } position && _level.InBounds(position)) { var cell = _level.GetCell(position); - CellText.Text = - $"Position: {position.X},{position.Y}\n" + - $"Kind: {cell.Kind}\n" + - $"Pipe: {cell.Pipe}\n" + - $"Flow: {cell.Flow}, Pressure: {cell.Pressure}\n" + - $"Integrity: {cell.Integrity}, Leak: {cell.LeakRate}\n" + - $"Heat: {cell.Hazards.Heat}, Smoke: {cell.Hazards.Smoke}\n" + - $"Fuel Vapor: {cell.Hazards.FuelVapor}, Fuel: {cell.Hazards.LiquidFuel}\n" + - $"Coolant: {cell.Hazards.CoolantPooling}, Charge: {cell.Hazards.ElectricalCharge}"; + CellText.Text = $"Position: {position.X},{position.Y}\n" + $"Kind: {cell.Kind}\n" + $"Pipe: {cell.Pipe}\n" + $"Flow: {cell.Flow}, Pressure: {cell.Pressure}\n" + $"Integrity: {cell.Integrity}, Leak: {cell.LeakRate}\n" + $"Heat: {cell.Hazards.Heat}, Smoke: {cell.Hazards.Smoke}\n" + $"Fuel Vapor: {cell.Hazards.FuelVapor}, Fuel: {cell.Hazards.LiquidFuel}\n" + $"Coolant: {cell.Hazards.CoolantPooling}, Charge: {cell.Hazards.ElectricalCharge}"; } else - { CellText.Text = "No cell selected."; - } ForecastList.ItemsSource = _level.Forecasts; } @@ -328,20 +292,68 @@ public sealed partial class MainWindow : Window private static LevelState BuildStarterLevel() { var level = LevelState.Create("Cooling Sector B", 16, 12); - level = level.SetCell(new GridPosition(3, 5), new CellState { Kind = CellKind.CoolingPump, Pipe = PipeMedium.Coolant, Flow = 5, Pressure = 5, Powered = true }); - level = level.SetCell(new GridPosition(4, 5), new CellState { Pipe = PipeMedium.Coolant, Flow = 5, Pressure = 7 }); - level = level.SetCell(new GridPosition(5, 5), new CellState { Pipe = PipeMedium.Coolant, Flow = 3, Pressure = 8, LeakRate = 2, Integrity = 4 }); - level = level.SetCell(new GridPosition(6, 5), new CellState { Pipe = PipeMedium.Coolant, Flow = 3, Pressure = 7 }); - level = level.SetCell(new GridPosition(8, 5), new CellState { Kind = CellKind.Reactor, Hazards = new HazardState { Heat = 6, Stability = 8 } }); - level = level.SetCell(new GridPosition(2, 8), new CellState { Kind = CellKind.Generator, Pipe = PipeMedium.Fuel, Flow = 4, Pressure = 6, Powered = true }); - level = level.SetCell(new GridPosition(11, 4), new CellState { Kind = CellKind.DiagnosticTerminal, Powered = true }); - level = level.SetCell(new GridPosition(12, 8), new CellState { Kind = CellKind.ControlTerminal, Powered = true }); + level = level.SetCell(new(3, 5), new() + { + Kind = CellKind.CoolingPump, + Pipe = PipeMedium.Coolant, + Flow = 5, + Pressure = 5, + Powered = true + }); + level = level.SetCell(new(4, 5), new() + { + Pipe = PipeMedium.Coolant, + Flow = 5, + Pressure = 7 + }); + level = level.SetCell(new(5, 5), new() + { + Pipe = PipeMedium.Coolant, + Flow = 3, + Pressure = 8, + LeakRate = 2, + Integrity = 4 + }); + level = level.SetCell(new(6, 5), new() + { + Pipe = PipeMedium.Coolant, + Flow = 3, + Pressure = 7 + }); + level = level.SetCell(new(8, 5), new() + { + Kind = CellKind.Reactor, + Hazards = new() + { + Heat = 6, + Stability = 8 + } + }); + level = level.SetCell(new(2, 8), new() + { + Kind = CellKind.Generator, + Pipe = PipeMedium.Fuel, + Flow = 4, + Pressure = 6, + Powered = true + }); + level = level.SetCell(new(11, 4), new() + { + Kind = CellKind.DiagnosticTerminal, + Powered = true + }); + level = level.SetCell(new(12, 8), new() + { + Kind = CellKind.ControlTerminal, + Powered = true + }); return level with { Forecasts = new SimulationEngine().Forecast(level) }; } - private sealed record CanvasLayout(double CellSize, double OriginX, double OriginY) - { - public Rect CellRect(int x, int y) => - new(OriginX + x * CellSize, OriginY + y * CellSize, CellSize, CellSize); - } -} + private readonly SimulationEngine _simulation = new(); + private StorageFile? _currentFile; + private LevelState _level; + private bool _painting; + private GridPosition? _selectedCell; + private EditorTool _selectedTool = EditorTool.Floor; +} \ No newline at end of file diff --git a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs index 730c786..a46cf0d 100644 --- a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs +++ b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs @@ -1,73 +1,71 @@ -using ReactorMaintenance.Simulation; - namespace ReactorMaintenance.Simulation.Tests; public sealed class SimulationEngineTests { - private readonly SimulationEngine _engine = new(); - [Fact] public void FuelLeakNearPoweredGeneratorCreatesIgnitionForecast() { - var level = LevelState.Create("Fuel leak", 6, 6) - .SetCell(new GridPosition(2, 2), new CellState - { - Kind = CellKind.Generator, - Pipe = PipeMedium.Fuel, - LeakRate = 4, - Pressure = 8, - Integrity = 8, - Powered = true - }); + var level = LevelState.Create("Fuel leak", 6, 6).SetCell(new(2, 2), new() + { + Kind = CellKind.Generator, + Pipe = PipeMedium.Fuel, + LeakRate = 4, + Pressure = 8, + Integrity = 8, + Powered = true + }); var forecasts = _engine.Forecast(level); - Assert.Contains(forecasts, forecast => - forecast.Kind == FailureKind.Ignition && - forecast.Position == new GridPosition(2, 2)); + Assert.Contains(forecasts, forecast => forecast.Kind == FailureKind.Ignition && forecast.Position == new GridPosition(2, 2)); } [Fact] public void CoolantLeakOnPoweredCellRaisesElectricalCharge() { - var level = LevelState.Create("Wet cable", 6, 6) - .SetCell(new GridPosition(3, 3), new CellState - { - Pipe = PipeMedium.Coolant, - LeakRate = 3, - Powered = true - }); + var level = LevelState.Create("Wet cable", 6, 6).SetCell(new(3, 3), new() + { + Pipe = PipeMedium.Coolant, + LeakRate = 3, + Powered = true + }); var next = _engine.AdvanceTurn(level); - Assert.True(next.GetCell(new GridPosition(3, 3)).Hazards.ElectricalCharge >= 2); + Assert.True(next.GetCell(new(3, 3)).Hazards.ElectricalCharge >= 2); } [Fact] public void OverpressurePredictsPipeBurst() { - var level = LevelState.Create("Pressure", 6, 6) - .SetCell(new GridPosition(1, 2), new CellState - { - Pipe = PipeMedium.Pressure, - Pressure = 10, - Integrity = 6 - }); + var level = LevelState.Create("Pressure", 6, 6).SetCell(new(1, 2), new() + { + Pipe = PipeMedium.Pressure, + Pressure = 10, + Integrity = 6 + }); var forecasts = _engine.Forecast(level); - Assert.Contains(forecasts, forecast => - forecast.Kind == FailureKind.PipeBurst && - forecast.Turns == 2); + Assert.Contains(forecasts, forecast => forecast.Kind == FailureKind.PipeBurst && forecast.Turns == 2); } [Fact] public void StableReactorWithPowerAndCoolingCanActivate() { - var level = LevelState.Create("Ready", 8, 6) - .SetCell(new GridPosition(2, 2), new CellState { Kind = CellKind.Reactor, Hazards = new HazardState { Heat = 3 } }) - .SetCell(new GridPosition(3, 2), new CellState { Kind = CellKind.Generator, Powered = true }) - .SetCell(new GridPosition(4, 2), new CellState { Kind = CellKind.CoolingPump, Powered = true }); + var level = LevelState.Create("Ready", 8, 6).SetCell(new(2, 2), new() + { + Kind = CellKind.Reactor, + Hazards = new() { Heat = 3 } + }).SetCell(new(3, 2), new() + { + Kind = CellKind.Generator, + Powered = true + }).SetCell(new(4, 2), new() + { + Kind = CellKind.CoolingPump, + Powered = true + }); var next = _engine.AdvanceTurn(level); var activated = _engine.ActivateReactor(next); @@ -80,16 +78,18 @@ public sealed class SimulationEngineTests public void LevelSerializationRoundTripsEditableState() { var level = LevelState.Create("Round trip", 5, 5); - level = LevelEditor.Apply(level, new GridPosition(2, 2), EditorTool.Reactor); - level = LevelEditor.Apply(level, new GridPosition(1, 2), EditorTool.CoolantPipe); - level = LevelEditor.Apply(level, new GridPosition(1, 2), EditorTool.Leak); + level = LevelEditor.Apply(level, new(2, 2), EditorTool.Reactor); + level = LevelEditor.Apply(level, new(1, 2), EditorTool.CoolantPipe); + level = LevelEditor.Apply(level, new(1, 2), EditorTool.Leak); var json = LevelSerializer.Serialize(level); var loaded = LevelSerializer.Deserialize(json); Assert.Equal(level.Name, loaded.Name); - Assert.Equal(CellKind.Reactor, loaded.GetCell(new GridPosition(2, 2)).Kind); - Assert.Equal(PipeMedium.Coolant, loaded.GetCell(new GridPosition(1, 2)).Pipe); - Assert.Equal(1, loaded.GetCell(new GridPosition(1, 2)).LeakRate); + Assert.Equal(CellKind.Reactor, loaded.GetCell(new(2, 2)).Kind); + Assert.Equal(PipeMedium.Coolant, loaded.GetCell(new(1, 2)).Pipe); + Assert.Equal(1, loaded.GetCell(new(1, 2)).LeakRate); } -} + + private readonly SimulationEngine _engine = new(); +} \ No newline at end of file