First batch
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.vs
|
||||||
|
bin
|
||||||
|
obj
|
||||||
1
AGENTS.linux.md
Normal file
1
AGENTS.linux.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Linux-specific instructions
|
||||||
30
AGENTS.md
Normal file
30
AGENTS.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Platform and documentation
|
||||||
|
|
||||||
|
If this is a linux environment, read `AGENTS.linux.md`.
|
||||||
|
If this is a windows environment, read `AGENTS.windows.md`.
|
||||||
|
Also see the other related technical documentation in the docs folder.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Prefer extracting code to a shared helper to be reused instead of duplicating code. Always keep high maintainability standards.
|
||||||
|
- If a class is to be used only once, consider nesting it inside of another class. Otherwise place each newly created class into its own file. The file name must match the class name.
|
||||||
|
- When asked to begin working on a task, create a detailed implementation plan first, present the plan to the user, and ask for approval before beginning with the actual implementation.
|
||||||
|
- Don't make assumptions in the plan. If necessary, ask all clarifying questions before presenting the final plan.
|
||||||
|
- When an task is finished, perform a code review to evaluate if the change is clean and maintainable with high software engineering standards. Iterate on the code and repeat the review process until satisfied.
|
||||||
|
- If there's documnentation present, always keep it updated.
|
||||||
|
- After every iteration, evaluate if the test coverage would fall below 100%, and write tests if necessary.
|
||||||
|
- After every iteration, run `dotnet jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
|
||||||
|
|
||||||
|
### Git
|
||||||
|
|
||||||
|
- Never change the .gitignore file without consent.
|
||||||
|
- Keep changes small with minimal churn and commit often. If one iteration encompasses many smaller tasks with more than one commit, create a git branch and do the commits there. Let me review the branch before merging it back to master.
|
||||||
|
- When multiple commits are necessary, pause after every commit and ask the user to give a command to proceed.
|
||||||
|
- After every iteration, do a git commit with a brief summary of the changes as a commit message.
|
||||||
|
- If you find unexpected changes in the code (deletions, changes, diff results that were not communicated), never revert them and never restore the old state. Assume that those changes happened with intent.
|
||||||
|
- Never use `git restore`, `git checkout --`, reset commands, or equivalent rollback actions to discard local changes unless the user explicitly asks for that exact rollback.
|
||||||
|
|
||||||
|
### Dotnet CLI
|
||||||
|
|
||||||
|
- If you need a separate output directory, use a subfolder under `artifacts`, and clean it up afterwards.
|
||||||
|
- Avoid running `dotnet build` and `dotnet test` in parallel in this repo; that can cause file-lock failures in `obj\Debug\net10.0`.
|
||||||
1
AGENTS.windows.md
Normal file
1
AGENTS.windows.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Windows-specific instructions
|
||||||
18
README.md
18
README.md
@@ -1,2 +1,18 @@
|
|||||||
# zfxaction26_2
|
# Reactor Maintenance
|
||||||
|
|
||||||
|
C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `design.md`.
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
|
||||||
|
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, and JSON serialization.
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj
|
||||||
|
dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
|
||||||
|
dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
11
ReactorMaintenance.slnx
Normal file
11
ReactorMaintenance.slnx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<Solution>
|
||||||
|
<Folder Name="/src/">
|
||||||
|
<Project Path="src/ReactorMaintenance.Simulation/ReactorMaintenance.Simulation.csproj" />
|
||||||
|
<Project Path="src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj">
|
||||||
|
<Platform Project="x86" />
|
||||||
|
</Project>
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/tests/">
|
||||||
|
<Project Path="tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj" />
|
||||||
|
</Folder>
|
||||||
|
</Solution>
|
||||||
65
src/ReactorMaintenance.Simulation/LevelEditor.cs
Normal file
65
src/ReactorMaintenance.Simulation/LevelEditor.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
namespace ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
|
public enum EditorTool
|
||||||
|
{
|
||||||
|
Floor,
|
||||||
|
Wall,
|
||||||
|
Reactor,
|
||||||
|
CoolingPump,
|
||||||
|
Generator,
|
||||||
|
PressureRegulator,
|
||||||
|
DiagnosticTerminal,
|
||||||
|
ControlTerminal,
|
||||||
|
CoolantPipe,
|
||||||
|
FuelPipe,
|
||||||
|
PressurePipe,
|
||||||
|
Leak,
|
||||||
|
Repair,
|
||||||
|
Heat,
|
||||||
|
Fire,
|
||||||
|
Robot
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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.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.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) } },
|
||||||
|
_ => cell
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cell.Kind == CellKind.Wall)
|
||||||
|
{
|
||||||
|
cell = cell with { Hazards = new HazardState() };
|
||||||
|
}
|
||||||
|
|
||||||
|
return level.SetCell(position, cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/ReactorMaintenance.Simulation/LevelSerializer.cs
Normal file
28
src/ReactorMaintenance.Simulation/LevelSerializer.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
|
public static class LevelSerializer
|
||||||
|
{
|
||||||
|
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<LevelState>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
174
src/ReactorMaintenance.Simulation/Models.cs
Normal file
174
src/ReactorMaintenance.Simulation/Models.cs
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
namespace ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
|
public enum CellKind
|
||||||
|
{
|
||||||
|
Empty,
|
||||||
|
Floor,
|
||||||
|
Wall,
|
||||||
|
Reactor,
|
||||||
|
CoolingPump,
|
||||||
|
Generator,
|
||||||
|
PressureRegulator,
|
||||||
|
DiagnosticTerminal,
|
||||||
|
ControlTerminal
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PipeMedium
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Pressure,
|
||||||
|
Coolant,
|
||||||
|
Fuel
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FailureKind
|
||||||
|
{
|
||||||
|
PipeBurst,
|
||||||
|
Ignition,
|
||||||
|
Meltdown,
|
||||||
|
StabilityCollapse,
|
||||||
|
ReactorReady
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record GridPosition(int X, int Y)
|
||||||
|
{
|
||||||
|
public IEnumerable<GridPosition> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record HazardState
|
||||||
|
{
|
||||||
|
public int Heat { get; init; }
|
||||||
|
public int Smoke { get; init; }
|
||||||
|
public int FuelVapor { get; init; }
|
||||||
|
public int LiquidFuel { get; init; }
|
||||||
|
public int CoolantPooling { get; init; }
|
||||||
|
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
|
||||||
|
{
|
||||||
|
public CellKind Kind { get; init; } = CellKind.Floor;
|
||||||
|
public PipeMedium Pipe { get; init; }
|
||||||
|
public int Flow { get; init; }
|
||||||
|
public int Pressure { get; init; }
|
||||||
|
public int Integrity { get; init; } = 10;
|
||||||
|
public int LeakRate { get; init; }
|
||||||
|
public bool PipeOpen { get; init; } = true;
|
||||||
|
public bool Powered { get; init; }
|
||||||
|
public bool DoorLocked { get; init; }
|
||||||
|
public HazardState Hazards { get; init; } = new();
|
||||||
|
|
||||||
|
public bool IsWalkable => Kind != CellKind.Wall && Kind != CellKind.Empty;
|
||||||
|
public bool HasPipe => Pipe != PipeMedium.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record GlobalState
|
||||||
|
{
|
||||||
|
public int Turn { get; init; }
|
||||||
|
public int ActionsPerTurn { get; init; } = 3;
|
||||||
|
public int CoreHeat { get; init; } = 5;
|
||||||
|
public int FacilityStability { get; init; } = 10;
|
||||||
|
public int Power { get; init; } = 5;
|
||||||
|
public int Cooling { get; init; } = 0;
|
||||||
|
public bool ReactorActivated { get; init; }
|
||||||
|
public bool Lost { get; init; }
|
||||||
|
public string Status { get; init; } = "STABILIZE SYSTEMS";
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record Forecast(FailureKind Kind, GridPosition? Position, int Turns, string Message);
|
||||||
|
|
||||||
|
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<Forecast> Forecasts { get; init; } = Array.Empty<Forecast>();
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LevelState
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Width = width,
|
||||||
|
Height = height,
|
||||||
|
Cells = cells,
|
||||||
|
Robot = new GridPosition(1, 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public CellState GetCell(GridPosition position)
|
||||||
|
{
|
||||||
|
EnsureInBounds(position);
|
||||||
|
return Cells[Index(position)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public LevelState SetCell(GridPosition position, CellState cell)
|
||||||
|
{
|
||||||
|
EnsureInBounds(position);
|
||||||
|
var cells = Cells.ToArray();
|
||||||
|
cells[Index(position)] = cell;
|
||||||
|
return this with { Cells = cells };
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool InBounds(GridPosition position) =>
|
||||||
|
position.X >= 0 && position.Y >= 0 && position.X < Width && position.Y < Height;
|
||||||
|
|
||||||
|
public int Index(GridPosition position) => 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class Rules
|
||||||
|
{
|
||||||
|
public static int Clamp(int value) => Math.Clamp(value, 0, 10);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
274
src/ReactorMaintenance.Simulation/SimulationEngine.cs
Normal file
274
src/ReactorMaintenance.Simulation/SimulationEngine.cs
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
namespace ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
|
public sealed class SimulationEngine
|
||||||
|
{
|
||||||
|
public LevelState AdvanceTurn(LevelState level)
|
||||||
|
{
|
||||||
|
var cells = level.Cells.ToArray();
|
||||||
|
|
||||||
|
for (var y = 0; y < level.Height; y++)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cells = SpreadSmoke(level, cells);
|
||||||
|
|
||||||
|
var global = UpdateGlobal(level, cells);
|
||||||
|
var next = level with
|
||||||
|
{
|
||||||
|
Cells = cells,
|
||||||
|
Global = global with { Turn = level.Global.Turn + 1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
return next with { Forecasts = Forecast(next) };
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<Forecast> Forecast(LevelState level)
|
||||||
|
{
|
||||||
|
var forecasts = new List<Forecast>();
|
||||||
|
|
||||||
|
for (var y = 0; y < level.Height; y++)
|
||||||
|
{
|
||||||
|
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 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level.Global.CoreHeat >= 8)
|
||||||
|
{
|
||||||
|
forecasts.Add(new Forecast(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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LevelState ActivateReactor(LevelState level)
|
||||||
|
{
|
||||||
|
if (!IsReactorReady(level))
|
||||||
|
{
|
||||||
|
return level with { Global = level.Global with { Status = "REACTOR NOT READY" } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return level with
|
||||||
|
{
|
||||||
|
Global = level.Global with
|
||||||
|
{
|
||||||
|
ReactorActivated = true,
|
||||||
|
Status = "REACTOR ONLINE"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HazardState ApplyPipeLeaks(CellState cell)
|
||||||
|
{
|
||||||
|
var hazards = cell.Hazards;
|
||||||
|
if (!cell.HasPipe || cell.LeakRate <= 0)
|
||||||
|
{
|
||||||
|
return hazards;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cell.Pipe switch
|
||||||
|
{
|
||||||
|
PipeMedium.Fuel => hazards with
|
||||||
|
{
|
||||||
|
LiquidFuel = hazards.LiquidFuel + cell.LeakRate,
|
||||||
|
FuelVapor = hazards.FuelVapor + (cell.Pressure >= 7 ? cell.LeakRate : Math.Max(0, hazards.Heat - 3) / 3)
|
||||||
|
},
|
||||||
|
PipeMedium.Coolant => hazards with
|
||||||
|
{
|
||||||
|
CoolantPooling = hazards.CoolantPooling + cell.LeakRate,
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HazardState ApplyMachineEffects(CellState cell, HazardState hazards)
|
||||||
|
{
|
||||||
|
return cell.Kind switch
|
||||||
|
{
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if ((hasFuel && hasIgnition) || hazards.Fire)
|
||||||
|
{
|
||||||
|
hazards = hazards with
|
||||||
|
{
|
||||||
|
Fire = hasFuel || hazards.Fire,
|
||||||
|
Heat = hazards.Heat + 2,
|
||||||
|
Smoke = hazards.Smoke + 2,
|
||||||
|
LiquidFuel = Math.Max(0, hazards.LiquidFuel - 1),
|
||||||
|
FuelVapor = Math.Max(0, hazards.FuelVapor - 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (hazards.Smoke > 0)
|
||||||
|
{
|
||||||
|
hazards = hazards with { Smoke = hazards.Smoke - 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return hazards;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CellState[] SpreadSmoke(LevelState level, CellState[] cells)
|
||||||
|
{
|
||||||
|
var next = cells.ToArray();
|
||||||
|
for (var y = 0; y < level.Height; y++)
|
||||||
|
{
|
||||||
|
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 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) }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
var damagedCriticalCells = cells.Count(c => c.Kind is CellKind.Reactor or CellKind.Generator or CellKind.CoolingPump && c.Hazards.Stability <= 3);
|
||||||
|
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 global = level.Global with
|
||||||
|
{
|
||||||
|
CoreHeat = Rules.Clamp(reactorHeat - poweredPumps),
|
||||||
|
Power = Rules.Clamp(poweredGenerators * 3),
|
||||||
|
Cooling = Rules.Clamp(poweredPumps * 3),
|
||||||
|
FacilityStability = stability,
|
||||||
|
Lost = lost,
|
||||||
|
Status = status
|
||||||
|
};
|
||||||
|
|
||||||
|
return IsReactorReady(level with { Cells = cells, Global = global })
|
||||||
|
? global with { Status = "REACTOR READY" }
|
||||||
|
: global;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsReactorReady(LevelState level)
|
||||||
|
{
|
||||||
|
var hasReactor = level.Cells.Any(c => c.Kind == CellKind.Reactor);
|
||||||
|
var hasStablePower = level.Global.Power >= 3 || level.Cells.Any(c => c.Kind == CellKind.Generator && c.Powered && !c.Hazards.Fire);
|
||||||
|
var hasCooling = level.Global.Cooling >= 3 || level.Cells.Any(c => c.Kind == CellKind.CoolingPump && c.Powered && !c.Hazards.Fire);
|
||||||
|
var reactorStable = level.Global.CoreHeat < 8;
|
||||||
|
return hasReactor && hasStablePower && hasCooling && reactorStable && !level.Global.Lost;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/ReactorMaintenance.Win2D/App.xaml
Normal file
5
src/ReactorMaintenance.Win2D/App.xaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<Application
|
||||||
|
x:Class="ReactorMaintenance.Win2D.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
</Application>
|
||||||
19
src/ReactorMaintenance.Win2D/App.xaml.cs
Normal file
19
src/ReactorMaintenance.Win2D/App.xaml.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
namespace ReactorMaintenance.Win2D;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
private Window? _window;
|
||||||
|
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||||
|
{
|
||||||
|
_window = new MainWindow();
|
||||||
|
_window.Activate();
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/ReactorMaintenance.Win2D/MainWindow.xaml
Normal file
87
src/ReactorMaintenance.Win2D/MainWindow.xaml
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<Window
|
||||||
|
x:Class="ReactorMaintenance.Win2D.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
|
||||||
|
Title="Reactor Maintenance">
|
||||||
|
|
||||||
|
<Grid Background="#16191D">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<CommandBar Grid.Row="0" DefaultLabelPosition="Right" Background="#20252A">
|
||||||
|
<AppBarButton Icon="Add" Label="New" Click="New_Click" />
|
||||||
|
<AppBarButton Icon="OpenFile" Label="Open" Click="Open_Click" />
|
||||||
|
<AppBarButton Icon="Save" Label="Save" Click="Save_Click" />
|
||||||
|
<AppBarSeparator />
|
||||||
|
<AppBarButton Icon="Play" Label="Simulate" Click="Simulate_Click" />
|
||||||
|
<AppBarButton Icon="Accept" Label="Activate" Click="Activate_Click" />
|
||||||
|
</CommandBar>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" ColumnSpacing="0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="220" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="300" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Column="0" Background="#1C2126">
|
||||||
|
<StackPanel Padding="12" Spacing="10">
|
||||||
|
<TextBlock Text="Tools" FontSize="18" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||||
|
<ComboBox x:Name="ToolPicker" SelectionChanged="ToolPicker_SelectionChanged" />
|
||||||
|
<TextBlock Text="Brush applies to the selected cell." Foreground="#9EA7AE" TextWrapping="Wrap" />
|
||||||
|
<TextBlock Text="Left click paints. Use Robot to set the start position." Foreground="#9EA7AE" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<Grid Grid.Column="1" Background="#101215">
|
||||||
|
<canvas:CanvasControl
|
||||||
|
x:Name="LevelCanvas"
|
||||||
|
ClearColor="#101215"
|
||||||
|
Draw="LevelCanvas_Draw"
|
||||||
|
PointerPressed="LevelCanvas_PointerPressed"
|
||||||
|
PointerMoved="LevelCanvas_PointerMoved"
|
||||||
|
PointerReleased="LevelCanvas_PointerReleased" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Column="2" Background="#1C2126">
|
||||||
|
<StackPanel Padding="14" Spacing="12">
|
||||||
|
<TextBlock x:Name="LevelNameText" FontSize="20" FontWeight="SemiBold" Foreground="#F4F1E8" TextWrapping="Wrap" />
|
||||||
|
<Grid ColumnSpacing="8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Turn" Foreground="#9EA7AE" />
|
||||||
|
<TextBlock x:Name="TurnText" FontSize="22" Foreground="#F4F1E8" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="1">
|
||||||
|
<TextBlock Text="Status" Foreground="#9EA7AE" />
|
||||||
|
<TextBlock x:Name="StatusText" FontSize="16" Foreground="#F4F1E8" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Text="Global Systems" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||||
|
<TextBlock x:Name="GlobalText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<TextBlock Text="Selected Cell" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||||
|
<TextBlock x:Name="CellText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<TextBlock Text="Forecasts" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||||
|
<ItemsControl x:Name="ForecastList">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border BorderBrush="#46515A" BorderThickness="1" Padding="8" Margin="0,0,0,8" CornerRadius="3">
|
||||||
|
<TextBlock Text="{Binding Message}" Foreground="#F4F1E8" TextWrapping="Wrap" />
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
347
src/ReactorMaintenance.Win2D/MainWindow.xaml.cs
Normal file
347
src/ReactorMaintenance.Win2D/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Microsoft.Graphics.Canvas;
|
||||||
|
using Microsoft.Graphics.Canvas.Text;
|
||||||
|
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||||
|
using Microsoft.UI;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
_level = BuildStarterLevel();
|
||||||
|
ToolPicker.ItemsSource = Enum.GetValues<EditorTool>();
|
||||||
|
ToolPicker.SelectedItem = _selectedTool;
|
||||||
|
RefreshInspector();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToolPicker_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ToolPicker.SelectedItem is EditorTool tool)
|
||||||
|
{
|
||||||
|
_selectedTool = tool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void New_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_level = BuildStarterLevel();
|
||||||
|
_currentFile = null;
|
||||||
|
_selectedCell = null;
|
||||||
|
RefreshInspector();
|
||||||
|
LevelCanvas.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Open_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var picker = new FileOpenPicker();
|
||||||
|
InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this));
|
||||||
|
picker.FileTypeFilter.Add(".json");
|
||||||
|
|
||||||
|
var file = await picker.PickSingleFileAsync();
|
||||||
|
if (file is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = await FileIO.ReadTextAsync(file);
|
||||||
|
_level = LevelSerializer.Deserialize(json);
|
||||||
|
_level = _level with { Forecasts = _simulation.Forecast(_level) };
|
||||||
|
_currentFile = file;
|
||||||
|
_selectedCell = null;
|
||||||
|
RefreshInspector();
|
||||||
|
LevelCanvas.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Save_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var file = _currentFile;
|
||||||
|
if (file is null)
|
||||||
|
{
|
||||||
|
var picker = new FileSavePicker();
|
||||||
|
InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this));
|
||||||
|
picker.SuggestedFileName = _level.Name.Replace(' ', '-').ToLowerInvariant();
|
||||||
|
picker.FileTypeChoices.Add("Reactor level", new List<string> { ".json" });
|
||||||
|
file = await picker.PickSaveFileAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await FileIO.WriteTextAsync(file, LevelSerializer.Serialize(_level));
|
||||||
|
_currentFile = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Simulate_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_level = _simulation.AdvanceTurn(_level);
|
||||||
|
RefreshInspector();
|
||||||
|
LevelCanvas.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Activate_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_level = _simulation.ActivateReactor(_level);
|
||||||
|
RefreshInspector();
|
||||||
|
LevelCanvas.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LevelCanvas_PointerPressed(object sender, PointerRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_painting = true;
|
||||||
|
LevelCanvas.CapturePointer(e.Pointer);
|
||||||
|
PaintAt(e.GetCurrentPoint(LevelCanvas).Position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_painting)
|
||||||
|
{
|
||||||
|
PaintAt(e.GetCurrentPoint(LevelCanvas).Position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_painting = false;
|
||||||
|
LevelCanvas.ReleasePointerCapture(e.Pointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PaintAt(Point point)
|
||||||
|
{
|
||||||
|
if (!TryGetGridPosition(point, out var position))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedCell = position;
|
||||||
|
_level = LevelEditor.Apply(_level, position, _selectedTool);
|
||||||
|
_level = _level with { Forecasts = _simulation.Forecast(_level) };
|
||||||
|
RefreshInspector();
|
||||||
|
LevelCanvas.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LevelCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
|
||||||
|
{
|
||||||
|
var drawing = args.DrawingSession;
|
||||||
|
var layout = GetLayout();
|
||||||
|
|
||||||
|
drawing.Clear(ColorHelper.FromArgb(255, 16, 18, 21));
|
||||||
|
DrawCells(drawing, layout);
|
||||||
|
DrawGrid(drawing, layout);
|
||||||
|
DrawRobot(drawing, layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCells(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||||
|
{
|
||||||
|
for (var y = 0; y < _level.Height; y++)
|
||||||
|
{
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCellGlyph(CanvasDrawingSession drawing, CellState cell, Rect rect)
|
||||||
|
{
|
||||||
|
var text = cell.Kind switch
|
||||||
|
{
|
||||||
|
CellKind.Reactor => "R",
|
||||||
|
CellKind.CoolingPump => "C",
|
||||||
|
CellKind.Generator => "G",
|
||||||
|
CellKind.PressureRegulator => "P",
|
||||||
|
CellKind.DiagnosticTerminal => "D",
|
||||||
|
CellKind.ControlTerminal => "T",
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var format = new CanvasTextFormat
|
||||||
|
{
|
||||||
|
FontSize = Math.Max(14, (float)rect.Width * 0.42f),
|
||||||
|
HorizontalAlignment = CanvasHorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = CanvasVerticalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
drawing.DrawText(text, rect, Colors.White, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawGrid(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||||
|
{
|
||||||
|
for (var x = 0; x <= _level.Width; x++)
|
||||||
|
{
|
||||||
|
var xPos = (float)(layout.OriginX + x * layout.CellSize);
|
||||||
|
drawing.DrawLine(xPos, (float)layout.OriginY, xPos, (float)(layout.OriginY + _level.Height * layout.CellSize), ColorHelper.FromArgb(120, 91, 104, 115), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var y = 0; y <= _level.Height; y++)
|
||||||
|
{
|
||||||
|
var yPos = (float)(layout.OriginY + y * layout.CellSize);
|
||||||
|
drawing.DrawLine((float)layout.OriginX, yPos, (float)(layout.OriginX + _level.Width * layout.CellSize), yPos, ColorHelper.FromArgb(120, 91, 104, 115), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||||
|
{
|
||||||
|
var rect = layout.CellRect(_level.Robot.X, _level.Robot.Y);
|
||||||
|
var center = new Vector2((float)(rect.X + rect.Width / 2), (float)(rect.Y + rect.Height / 2));
|
||||||
|
drawing.FillCircle(center, (float)rect.Width * 0.28f, Colors.White);
|
||||||
|
drawing.DrawCircle(center, (float)rect.Width * 0.28f, Colors.Black, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetGridPosition(Point point, out GridPosition position)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
return _level.InBounds(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CanvasLayout GetLayout()
|
||||||
|
{
|
||||||
|
var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
|
||||||
|
var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
|
||||||
|
var size = Math.Floor(Math.Min(availableWidth / _level.Width, availableHeight / _level.Height));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Windows.UI.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.DiagnosticTerminal => ColorHelper.FromArgb(255, 39, 84, 62),
|
||||||
|
CellKind.ControlTerminal => ColorHelper.FromArgb(255, 80, 61, 91),
|
||||||
|
_ => ColorHelper.FromArgb(255, 31, 36, 40)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshInspector()
|
||||||
|
{
|
||||||
|
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";
|
||||||
|
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CellText.Text = "No cell selected.";
|
||||||
|
}
|
||||||
|
|
||||||
|
ForecastList.ItemsSource = _level.Forecasts;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj
Normal file
26
src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||||
|
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||||
|
<RootNamespace>ReactorMaintenance.Win2D</RootNamespace>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<Platforms>x86;x64;arm64</Platforms>
|
||||||
|
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||||
|
<UseWinUI>true</UseWinUI>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<WindowsPackageType>None</WindowsPackageType>
|
||||||
|
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.4.0" />
|
||||||
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
|
||||||
|
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ReactorMaintenance.Simulation\ReactorMaintenance.Simulation.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
10
src/ReactorMaintenance.Win2D/app.manifest
Normal file
10
src/ReactorMaintenance.Win2D/app.manifest
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="ReactorMaintenance.Win2D.app"/>
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
</assembly>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.5.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\ReactorMaintenance.Simulation\ReactorMaintenance.Simulation.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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 forecasts = _engine.Forecast(level);
|
||||||
|
|
||||||
|
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 next = _engine.AdvanceTurn(level);
|
||||||
|
|
||||||
|
Assert.True(next.GetCell(new GridPosition(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 forecasts = _engine.Forecast(level);
|
||||||
|
|
||||||
|
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 next = _engine.AdvanceTurn(level);
|
||||||
|
var activated = _engine.ActivateReactor(next);
|
||||||
|
|
||||||
|
Assert.Equal("REACTOR ONLINE", activated.Global.Status);
|
||||||
|
Assert.True(activated.Global.ReactorActivated);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user