Compare commits
23 Commits
4b581d60b5
...
design-rew
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ddd1b8ec8 | |||
| 1b9372ff7c | |||
| 3a52db0071 | |||
| 5a186fb606 | |||
| 6c7fa070f6 | |||
| d22c4a7528 | |||
| 7ffaa140a8 | |||
| 3c5fc60ffe | |||
| 9cd9defc0b | |||
| 1aa9734e08 | |||
| a0b10423ac | |||
| cb28eee1dd | |||
| b232c0319f | |||
| 30963a9bde | |||
| 851f6d27e8 | |||
| ca41e009bd | |||
| 79f3219a72 | |||
| 071e6a1d48 | |||
| 810478ddee | |||
| bb8d1adb10 | |||
| c8795d582c | |||
| 2376edab0d | |||
| c406bf9d73 |
1229
.editorconfig
1229
.editorconfig
File diff suppressed because it is too large
Load Diff
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
.idea
|
||||
@@ -1,3 +1,3 @@
|
||||
# Linux-specific instructions
|
||||
|
||||
# Linux-specific instructions
|
||||
|
||||
- After every iteration, run `dotnet jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
|
||||
60
AGENTS.md
60
AGENTS.md
@@ -1,30 +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`.
|
||||
Follow the guidelines laid out in `CODESTYLE.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.
|
||||
|
||||
### 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`.
|
||||
# Platform and documentation
|
||||
|
||||
If this is a linux environment, read `AGENTS.linux.md`.
|
||||
If this is a windows environment, read `AGENTS.windows.md`.
|
||||
Follow the guidelines laid out in `CODESTYLE.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.
|
||||
|
||||
### 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,4 +1,4 @@
|
||||
# Windows-specific instructions
|
||||
|
||||
- After the implementation is finished, run `python D:\Code\crlf.py $file1 $file2 ...` for changed files you recognize, in order to normalize all line endings of all touched files to CRLF.
|
||||
- After every iteration, run `jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
|
||||
# Windows-specific instructions
|
||||
|
||||
- After the implementation is finished, run `python D:\Code\crlf.py $file1 $file2 ...` for changed files you recognize, in order to normalize all line endings of all touched files to CRLF.
|
||||
- After every iteration, run `jb cleanupcode '$file1' '$file2' ...` for every C# file you touched.
|
||||
102
CODESTYLE.md
102
CODESTYLE.md
@@ -1,51 +1,51 @@
|
||||
# Code Style
|
||||
|
||||
This repository follows the local `.editorconfig` and the style visible in the current local changes. Use these notes when creating new code.
|
||||
|
||||
## Naming
|
||||
|
||||
- Use PascalCase for namespaces, types, methods, properties, enum members, and non-field members.
|
||||
- Prefix enum type names with `E`, for example `ECellKind`, `EPipeMedium`, `EFailureKind`, and `EEditorTool`.
|
||||
- Prefix struct type names with `S` when creating new structs.
|
||||
- Prefix interfaces with `I`.
|
||||
- Use camelCase for parameters and local variables.
|
||||
- Prefix private instance fields with `m_` and keep the remainder PascalCase, for example `m_Level` and `m_SelectedTool`.
|
||||
- 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
|
||||
|
||||
- Use file-scoped namespaces.
|
||||
- Keep one reusable top-level class per file, with the file name matching the class name.
|
||||
- If a helper type is used only by one class, prefer nesting it inside that class.
|
||||
- Keep small, cohesive files and extract shared helpers instead of duplicating logic.
|
||||
|
||||
## Braces And Blocks
|
||||
|
||||
- Use braces for multi-line bodies.
|
||||
- If nesting a for-loop under another for-loop, always include curly braces in the parent for-loop.
|
||||
- Omit braces for simple single-line embedded statements when readability stays clear.
|
||||
- Nested control flow with multi-line bodies should use braces at every multi-line level.
|
||||
- Keep opening braces on the next line for types, methods, properties, accessors, and control blocks.
|
||||
- Compact object initializers, switch expressions, and `with` expressions may keep the opening brace on the same line when cleanup formats them that way.
|
||||
|
||||
## Blank Lines
|
||||
|
||||
- Use a blank line to separate members.
|
||||
- Use a blank line after control-flow transfer clauses such as `return`, `continue`, `break`, and `throw` when more code follows in the same scope.
|
||||
- Avoid extra blank lines inside short methods and between tightly related statements.
|
||||
- Keep at most one blank line in code and declarations.
|
||||
|
||||
## Expressions And Formatting
|
||||
|
||||
- Prefer `var` when the type is apparent or not useful to repeat; use explicit built-in types such as `int`, `bool`, and `string`.
|
||||
- Prefer target-typed `new()` when the type is evident.
|
||||
- Prefer object and collection initializers, including collection expressions such as `[".json"]`.
|
||||
- Prefer pattern matching for combined checks, for example `cell is { HasPipe: true, Pressure: > 7 }`.
|
||||
- Prefer switch expressions for simple value selection.
|
||||
- Prefer expression-bodied properties and accessors when they remain simple.
|
||||
- Keep simple object initializers and property patterns on one line when they are short and readable.
|
||||
- Keep long boolean expressions and interpolated status strings readable without introducing unnecessary blank lines.
|
||||
- Keep using directives outside namespaces.
|
||||
# Code Style
|
||||
|
||||
This repository follows the local `.editorconfig` and the style visible in the current local changes. Use these notes when creating new code.
|
||||
|
||||
## Naming
|
||||
|
||||
- Use PascalCase for namespaces, types, methods, properties, enum members, and non-field members.
|
||||
- Prefix enum type names with `E`, for example `ECellKind`, `EPipeMedium`, `EFailureKind`, and `EEditorTool`.
|
||||
- Prefix struct type names with `S` when creating new structs.
|
||||
- Prefix interfaces with `I`.
|
||||
- Use camelCase for parameters and local variables.
|
||||
- Prefix private instance fields with `m_` and keep the remainder PascalCase, for example `m_Level` and `m_SelectedTool`.
|
||||
- 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
|
||||
|
||||
- Use file-scoped namespaces.
|
||||
- Keep one reusable top-level class per file, with the file name matching the class name.
|
||||
- If a helper type is used only by one class, prefer nesting it inside that class.
|
||||
- Keep small, cohesive files and extract shared helpers instead of duplicating logic.
|
||||
|
||||
## Braces And Blocks
|
||||
|
||||
- Use braces for multi-line bodies.
|
||||
- If nesting a for-loop under another for-loop, always include curly braces in the parent for-loop.
|
||||
- Omit braces for simple single-line embedded statements when readability stays clear.
|
||||
- Nested control flow with multi-line bodies should use braces at every multi-line level.
|
||||
- Keep opening braces on the next line for types, methods, properties, accessors, and control blocks.
|
||||
- Compact object initializers, switch expressions, and `with` expressions may keep the opening brace on the same line when cleanup formats them that way.
|
||||
|
||||
## Blank Lines
|
||||
|
||||
- Use a blank line to separate members.
|
||||
- Use a blank line after control-flow transfer clauses such as `return`, `continue`, `break`, and `throw` when more code follows in the same scope.
|
||||
- Avoid extra blank lines inside short methods and between tightly related statements.
|
||||
- Keep at most one blank line in code and declarations.
|
||||
|
||||
## Expressions And Formatting
|
||||
|
||||
- Prefer `var` when the type is apparent or not useful to repeat; use explicit built-in types such as `int`, `bool`, and `string`.
|
||||
- Prefer target-typed `new()` when the type is evident.
|
||||
- Prefer object and collection initializers, including collection expressions such as `[".json"]`.
|
||||
- Prefer pattern matching for combined checks, for example `cell is { HasPipe: true, Pressure: > 7 }`.
|
||||
- Prefer switch expressions for simple value selection.
|
||||
- Prefer expression-bodied properties and accessors when they remain simple.
|
||||
- Keep simple object initializers and property patterns on one line when they are short and readable.
|
||||
- Keep long boolean expressions and interpolated status strings readable without introducing unnecessary blank lines.
|
||||
- Keep using directives outside namespaces.
|
||||
|
||||
46
README.md
46
README.md
@@ -1,18 +1,28 @@
|
||||
# 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, versioned JSON serialization, and swappable difficulty balancing profiles.
|
||||
- `src/ReactorMaintenance.Win2D`: Win2D editor app for painting floor/wall terrain plus cell props, 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
|
||||
```
|
||||
|
||||
# Reactor Maintenance
|
||||
|
||||
C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `docs/design.md`.
|
||||
|
||||
## Projects
|
||||
|
||||
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, validation, forecasts, simulation turns, versioned JSON serialization, and deterministic balancing defaults.
|
||||
- `src/ReactorMaintenance.Win2D`: Win2D editor app for authoring terrain, underground fuel/coolant/electricity networks, props, explicit leak access faces, door edges, reactor consumer bindings, rule events, surface hazards, robot start, loading/saving levels, ending turns, interacting with props, and activating a ready reactor.
|
||||
- `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior, validation, serialization, and editor operations.
|
||||
|
||||
## Editor Controls
|
||||
|
||||
- Left click selects or paints with the current tool. Right click clears the selected cell's prop, surface hazards, leaks, doors, and reactor control.
|
||||
- Door authoring is explicit: select the Door tool, click the door cell, then click the adjacent floor cell that defines the blocked edge.
|
||||
- Electricity wall leaks are explicit: select the Electricity Leak tool, click the wall network cell, then click the adjacent floor access face.
|
||||
- Reactor bindings are explicit: select or place a reactor control, select a matching consumer cell, then use the Fuel, Coolant, or Electric binding action in the inspector.
|
||||
- Rule event authoring is available from the inspector for next-turn warnings and selected-cell leak events; authored events are saved in the version 2 JSON schema.
|
||||
|
||||
## Commands
|
||||
|
||||
```powershell
|
||||
dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj
|
||||
dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64 -p:EnableWindowsTargeting=true
|
||||
dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
|
||||
```
|
||||
|
||||
The WinUI/XAML compiler is Windows-specific. On Linux, the simulation tests run normally, but the Win2D app build must be verified in a Windows-capable environment.
|
||||
|
||||
|
||||
@@ -1,11 +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>
|
||||
<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>
|
||||
|
||||
9
TASKS.md
Normal file
9
TASKS.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Reactor Maintenance Rewrite Tasks
|
||||
|
||||
## Current State
|
||||
|
||||
## Completed Work
|
||||
|
||||
## Current Work
|
||||
|
||||
## Future Work
|
||||
1486
docs/design.md
1486
docs/design.md
File diff suppressed because it is too large
Load Diff
13
dotnet-tools.json
Normal file
13
dotnet-tools.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"jetbrains.resharper.globaltools": {
|
||||
"version": "2026.1.1",
|
||||
"commands": [
|
||||
"jb"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,75 +4,158 @@ namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public abstract class Balancing
|
||||
{
|
||||
public float ClampValue(float value)
|
||||
{
|
||||
return Math.Clamp(value, MinValue, MaxValue);
|
||||
}
|
||||
|
||||
public EBand Band(float value, float caution, float critical)
|
||||
{
|
||||
if (value >= critical)
|
||||
return EBand.Critical;
|
||||
|
||||
return value >= caution ? EBand.Caution : EBand.Safe;
|
||||
}
|
||||
|
||||
public IReadOnlyList<JunctionRatioPreset> JunctionRatios(int outflowCount)
|
||||
{
|
||||
return outflowCount switch {
|
||||
2 => TwoOutflowJunctionRatios,
|
||||
3 => ThreeOutflowJunctionRatios,
|
||||
_ => Array.Empty<JunctionRatioPreset>()
|
||||
};
|
||||
}
|
||||
|
||||
public float[] JunctionWeights(int outflowCount, int mode)
|
||||
{
|
||||
var ratios = JunctionRatios(outflowCount);
|
||||
if (ratios.Count == 0)
|
||||
return Array.Empty<float>();
|
||||
|
||||
return ratios[Math.Clamp(mode, 0, ratios.Count - 1)].Weights;
|
||||
}
|
||||
|
||||
public SurfaceInteractionEffect SameCellInteraction(ECarrierType? rowCarrier, EBand rowBand, ECarrierType? colCarrier, EBand colBand)
|
||||
{
|
||||
if (rowBand == EBand.Safe && colBand == EBand.Safe)
|
||||
return SurfaceInteractionEffect.Hold;
|
||||
|
||||
if (rowCarrier == ECarrierType.Fuel && colCarrier == ECarrierType.Electricity)
|
||||
return Ignite(rowBand, colBand);
|
||||
|
||||
if (rowCarrier == ECarrierType.Fuel && colCarrier is null)
|
||||
return rowBand == EBand.Critical || colBand == EBand.Critical ? Ignite(rowBand, colBand) : Warm(rowBand, colBand);
|
||||
|
||||
if (rowCarrier == ECarrierType.Coolant && colCarrier == ECarrierType.Electricity)
|
||||
return Short(rowBand, colBand);
|
||||
|
||||
if (rowCarrier == ECarrierType.Coolant && colCarrier is null)
|
||||
return Quench(rowBand, colBand);
|
||||
|
||||
return SurfaceInteractionEffect.Hold;
|
||||
}
|
||||
|
||||
public SurfaceInteractionEffect FlowInteraction(ESurfaceQuantity quantity)
|
||||
{
|
||||
return new() { Verb = ESurfaceInteractionVerb.Flow, Quantity = quantity, Amount = FlowTransferRatio };
|
||||
}
|
||||
|
||||
private SurfaceInteractionEffect Warm(EBand rowBand, EBand colBand)
|
||||
{
|
||||
return new() {
|
||||
Verb = ESurfaceInteractionVerb.Warm,
|
||||
Quantity = ESurfaceQuantity.Heat,
|
||||
Amount = Strongest(rowBand, colBand) == EBand.Critical ? WarmCriticalAmount : WarmCautionAmount
|
||||
};
|
||||
}
|
||||
|
||||
private SurfaceInteractionEffect Quench(EBand rowBand, EBand colBand)
|
||||
{
|
||||
return new() {
|
||||
Verb = ESurfaceInteractionVerb.Quench,
|
||||
Quantity = ESurfaceQuantity.Heat,
|
||||
Amount = Strongest(rowBand, colBand) == EBand.Critical ? QuenchCriticalAmount : QuenchCautionAmount
|
||||
};
|
||||
}
|
||||
|
||||
private SurfaceInteractionEffect Short(EBand rowBand, EBand colBand)
|
||||
{
|
||||
var critical = Strongest(rowBand, colBand) == EBand.Critical;
|
||||
return new() {
|
||||
Verb = ESurfaceInteractionVerb.Short,
|
||||
Quantity = ESurfaceQuantity.Electricity,
|
||||
Amount = critical ? ShortCriticalHeat : ShortCautionHeat,
|
||||
SecondaryAmount = critical ? ShortCriticalDischarge : ShortCautionDischarge
|
||||
};
|
||||
}
|
||||
|
||||
private SurfaceInteractionEffect Ignite(EBand rowBand, EBand colBand)
|
||||
{
|
||||
var critical = Strongest(rowBand, colBand) == EBand.Critical;
|
||||
return new() {
|
||||
Verb = ESurfaceInteractionVerb.Ignite,
|
||||
Quantity = ESurfaceQuantity.Fuel,
|
||||
Amount = critical ? IgniteCriticalHeat : IgniteCautionHeat,
|
||||
SecondaryAmount = critical ? IgniteCriticalFuelConsumption : IgniteCautionFuelConsumption
|
||||
};
|
||||
}
|
||||
|
||||
private static EBand Strongest(EBand a, EBand b)
|
||||
{
|
||||
return a > b ? a : b;
|
||||
}
|
||||
|
||||
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; }
|
||||
public abstract int MinimumLevelSize { get; }
|
||||
public abstract int ActionsPerTurn { get; }
|
||||
public abstract int ForecastHorizon { get; }
|
||||
public abstract float MinValue { get; }
|
||||
public abstract float MaxValue { get; }
|
||||
public abstract float FuelSafe { get; }
|
||||
public abstract float FuelCaution { get; }
|
||||
public abstract float FuelCritical { get; }
|
||||
public abstract float CoolantSafe { get; }
|
||||
public abstract float CoolantCaution { get; }
|
||||
public abstract float CoolantCritical { get; }
|
||||
public abstract float ElectricitySafe { get; }
|
||||
public abstract float ElectricityCaution { get; }
|
||||
public abstract float ElectricityCritical { get; }
|
||||
public abstract float HeatSafe { get; }
|
||||
public abstract float HeatCaution { get; }
|
||||
public abstract float HeatCritical { get; }
|
||||
public abstract float TerminalHeat { get; }
|
||||
public abstract float RobotFuelSafetyThreshold { get; }
|
||||
public abstract float RobotCoolantSafetyThreshold { get; }
|
||||
public abstract float RobotElectricitySafetyThreshold { get; }
|
||||
public abstract float RobotHeatSafetyThreshold { get; }
|
||||
public abstract float SourceAmount { get; }
|
||||
public abstract float SourceIntensity { get; }
|
||||
public abstract float DistanceAmountFalloff { get; }
|
||||
public abstract float DistanceIntensityFalloff { get; }
|
||||
public abstract IReadOnlyList<JunctionRatioPreset> TwoOutflowJunctionRatios { get; }
|
||||
public abstract IReadOnlyList<JunctionRatioPreset> ThreeOutflowJunctionRatios { get; }
|
||||
public abstract float ConsumerRequiredAmount { get; }
|
||||
public abstract float ConsumerRequiredIntensity { get; }
|
||||
public abstract float LeakBaseAmount { get; }
|
||||
public abstract float LeakAmountScale { get; }
|
||||
public abstract float LeakIntensityScale { get; }
|
||||
public abstract float FlowTransferRatio { get; }
|
||||
public abstract float WarmCautionAmount { get; }
|
||||
public abstract float WarmCriticalAmount { get; }
|
||||
public abstract float QuenchCautionAmount { get; }
|
||||
public abstract float QuenchCriticalAmount { get; }
|
||||
public abstract float ShortCautionHeat { get; }
|
||||
public abstract float ShortCautionDischarge { get; }
|
||||
public abstract float ShortCriticalHeat { get; }
|
||||
public abstract float ShortCriticalDischarge { get; }
|
||||
public abstract float IgniteCautionHeat { get; }
|
||||
public abstract float IgniteCautionFuelConsumption { get; }
|
||||
public abstract float IgniteCriticalHeat { get; }
|
||||
public abstract float IgniteCriticalFuelConsumption { get; }
|
||||
public abstract int RemedyBlockTurns { get; }
|
||||
public abstract int HeatShieldSteps { get; }
|
||||
public abstract int InventoryCapacityPerRemedy { get; }
|
||||
}
|
||||
@@ -1,76 +1,70 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Difficulties;
|
||||
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;
|
||||
public override int MinimumLevelSize => 4;
|
||||
public override int ActionsPerTurn => 3;
|
||||
public override int ForecastHorizon => 6;
|
||||
public override float MinValue => 0;
|
||||
public override float MaxValue => 10;
|
||||
public override float FuelSafe => 1.5f;
|
||||
public override float FuelCaution => 3.5f;
|
||||
public override float FuelCritical => 6.5f;
|
||||
public override float CoolantSafe => 1.5f;
|
||||
public override float CoolantCaution => 3.5f;
|
||||
public override float CoolantCritical => 6.5f;
|
||||
public override float ElectricitySafe => 1.5f;
|
||||
public override float ElectricityCaution => 3.5f;
|
||||
public override float ElectricityCritical => 6.5f;
|
||||
public override float HeatSafe => 2;
|
||||
public override float HeatCaution => 5;
|
||||
public override float HeatCritical => 8;
|
||||
public override float TerminalHeat => 10;
|
||||
public override float RobotFuelSafetyThreshold => 6.5f;
|
||||
public override float RobotCoolantSafetyThreshold => 8;
|
||||
public override float RobotElectricitySafetyThreshold => 6.5f;
|
||||
public override float RobotHeatSafetyThreshold => 8;
|
||||
public override float SourceAmount => 8;
|
||||
public override float SourceIntensity => 8;
|
||||
public override float DistanceAmountFalloff => 0.5f;
|
||||
public override float DistanceIntensityFalloff => 0.4f;
|
||||
|
||||
public override IReadOnlyList<JunctionRatioPreset> TwoOutflowJunctionRatios { get; } = [
|
||||
new("0/4", [0, 1]),
|
||||
new("1/3", [0.25f, 0.75f]),
|
||||
new("2/2", [0.5f, 0.5f]),
|
||||
new("3/1", [0.75f, 0.25f]),
|
||||
new("4/0", [1, 0])
|
||||
];
|
||||
|
||||
public override IReadOnlyList<JunctionRatioPreset> ThreeOutflowJunctionRatios { get; } = [
|
||||
new("0/3/3", [0, 0.5f, 0.5f]),
|
||||
new("3/0/3", [0.5f, 0, 0.5f]),
|
||||
new("3/3/0", [0.5f, 0.5f, 0]),
|
||||
new("2/2/2", [1f / 3f, 1f / 3f, 1f / 3f])
|
||||
];
|
||||
|
||||
public override float ConsumerRequiredAmount => 2.5f;
|
||||
public override float ConsumerRequiredIntensity => 2.5f;
|
||||
public override float LeakBaseAmount => 0.5f;
|
||||
public override float LeakAmountScale => 0.15f;
|
||||
public override float LeakIntensityScale => 0.1f;
|
||||
public override float FlowTransferRatio => 0.05f;
|
||||
public override float WarmCautionAmount => 0.5f;
|
||||
public override float WarmCriticalAmount => 1.0f;
|
||||
public override float QuenchCautionAmount => 0.6f;
|
||||
public override float QuenchCriticalAmount => 1.2f;
|
||||
public override float ShortCautionHeat => 0.8f;
|
||||
public override float ShortCautionDischarge => 0.8f;
|
||||
public override float ShortCriticalHeat => 1.6f;
|
||||
public override float ShortCriticalDischarge => 1.5f;
|
||||
public override float IgniteCautionHeat => 1.2f;
|
||||
public override float IgniteCautionFuelConsumption => 0.4f;
|
||||
public override float IgniteCriticalHeat => 2.4f;
|
||||
public override float IgniteCriticalFuelConsumption => 0.8f;
|
||||
public override int RemedyBlockTurns => 2;
|
||||
public override int HeatShieldSteps => 3;
|
||||
public override int InventoryCapacityPerRemedy => 3;
|
||||
}
|
||||
20
src/ReactorMaintenance.Simulation/EEditorTool.cs
Normal file
20
src/ReactorMaintenance.Simulation/EEditorTool.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EEditorTool
|
||||
{
|
||||
Cursor,
|
||||
Floor,
|
||||
Wall,
|
||||
Underground,
|
||||
Flow,
|
||||
Consumer,
|
||||
Junction,
|
||||
Door,
|
||||
AllSeeingEyeTerminal,
|
||||
RemedySupply,
|
||||
ReactorControl,
|
||||
Leak,
|
||||
SurfaceHazard,
|
||||
Heat,
|
||||
Robot
|
||||
}
|
||||
8
src/ReactorMaintenance.Simulation/EditorToolCommand.cs
Normal file
8
src/ReactorMaintenance.Simulation/EditorToolCommand.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record EditorToolCommand
|
||||
{
|
||||
public EEditorTool Tool { get; init; }
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public ERemedyType RemedyType { get; init; }
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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 { Prop: ECellProp.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() };
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public interface IAreaSimulationEffect
|
||||
{
|
||||
CellState[] Apply(LevelState level, CellState[] cells);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public interface ISimulationEffect
|
||||
{
|
||||
CellState Apply(CellState cell);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public sealed class MachineEffect : ISimulationEffect
|
||||
{
|
||||
public CellState Apply(CellState cell)
|
||||
{
|
||||
var hazards = cell.Prop switch {
|
||||
ECellProp.Generator when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.GeneratorHeatIncrease },
|
||||
ECellProp.CoolingPump when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat - Balancing.Current.CoolingPumpHeatReduction },
|
||||
ECellProp.Reactor => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.ReactorHeatIncrease },
|
||||
_ => cell.Hazards
|
||||
};
|
||||
|
||||
return cell with { Hazards = hazards.Clamp() };
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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() };
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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.Current.FirstGridCoordinate; y < level.Height; y++)
|
||||
{
|
||||
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.Current.SmokeSpreadThreshold)
|
||||
continue;
|
||||
|
||||
SpreadToNeighbors(level, next, position);
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
private static void SpreadToNeighbors(LevelState level, CellState[] next, GridPosition position)
|
||||
{
|
||||
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 + Balancing.Current.SmokeSpreadIncrease) } };
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/ReactorMaintenance.Simulation/GridPositionExtensions.cs
Normal file
17
src/ReactorMaintenance.Simulation/GridPositionExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class GridPositionExtensions
|
||||
{
|
||||
public static IEnumerable<GridPosition> Neighbors(this GridPosition position)
|
||||
{
|
||||
yield return new(position.X, position.Y - 1);
|
||||
yield return new(position.X + 1, position.Y);
|
||||
yield return new(position.X, position.Y + 1);
|
||||
yield return new(position.X - 1, position.Y);
|
||||
}
|
||||
|
||||
public static int ManhattanDistance(this GridPosition position, GridPosition other)
|
||||
{
|
||||
return Math.Abs(position.X - other.X) + Math.Abs(position.Y - other.Y);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public abstract class Hazard
|
||||
{
|
||||
public abstract IEnumerable<Forecast> Predict(LevelState level, int turns);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public sealed class IgnitionHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public sealed class MeltdownHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
|
||||
{
|
||||
if (level.Global is { Lost: true, Status: "CORE MELTDOWN" })
|
||||
yield return new(EFailureKind.Meltdown, null, turns, "CORE MELTDOWN APPROACHING");
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public sealed class PipeBurstHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> 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 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public sealed class StabilityCollapseHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
|
||||
{
|
||||
if (level.Global is { Lost: true, Status: "FACILITY STABILITY COLLAPSE" })
|
||||
yield return new(EFailureKind.StabilityCollapse, null, turns, "FACILITY STABILITY COLLAPSE APPROACHING");
|
||||
}
|
||||
}
|
||||
34
src/ReactorMaintenance.Simulation/JunctionFlow.cs
Normal file
34
src/ReactorMaintenance.Simulation/JunctionFlow.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record JunctionFlow
|
||||
{
|
||||
public float WeightFor(GridPosition outgoingBranch)
|
||||
{
|
||||
var index = IndexOfOutgoingBranch(outgoingBranch);
|
||||
if (index < 0)
|
||||
return 0;
|
||||
|
||||
var weights = Balancing.Current.JunctionWeights(OutgoingBranches.Count, Prop.JunctionMode);
|
||||
return index < weights.Length ? weights[index] : 0;
|
||||
}
|
||||
|
||||
private int IndexOfOutgoingBranch(GridPosition outgoingBranch)
|
||||
{
|
||||
for (var i = 0; i < OutgoingBranches.Count; i++)
|
||||
{
|
||||
if (OutgoingBranches[i] == outgoingBranch)
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public GridPosition Position { get; init; } = new(0, 0);
|
||||
public PropState Prop { get; init; } = new();
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public IReadOnlyList<GridPosition> Branches { get; init; } = Array.Empty<GridPosition>();
|
||||
public GridPosition? IncomingBranch { get; init; }
|
||||
public IReadOnlyList<GridPosition> OutgoingBranches { get; init; } = Array.Empty<GridPosition>();
|
||||
public IReadOnlyList<string> Errors { get; init; } = Array.Empty<string>();
|
||||
public bool IsValid => Errors.Count == 0;
|
||||
}
|
||||
97
src/ReactorMaintenance.Simulation/JunctionFlowAnalyzer.cs
Normal file
97
src/ReactorMaintenance.Simulation/JunctionFlowAnalyzer.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class JunctionFlowAnalyzer
|
||||
{
|
||||
private sealed record SourceBranch(GridPosition Position, int? Distance);
|
||||
|
||||
public static IReadOnlyList<JunctionFlow> Analyze(LevelState level)
|
||||
{
|
||||
var flows = new List<JunctionFlow>();
|
||||
|
||||
for (var y = 0; y < level.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type != EPropType.Junction)
|
||||
continue;
|
||||
|
||||
flows.Add(AnalyzeJunction(level, position, prop));
|
||||
}
|
||||
}
|
||||
|
||||
return flows;
|
||||
}
|
||||
|
||||
private static JunctionFlow AnalyzeJunction(LevelState level, GridPosition position, PropState prop)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
var carriers = Enum.GetValues<ECarrierType>().Where(carrier => level.GetUnderground(position, carrier).IsPresent).ToArray();
|
||||
var carrier = carriers.FirstOrDefault();
|
||||
if (carriers.Length != 1)
|
||||
errors.Add("Junction must regulate exactly one underground carrier.");
|
||||
|
||||
var branches = carriers.Length == 1
|
||||
? position.Neighbors().Where(level.InBounds).Where(neighbor => level.GetUnderground(neighbor, carrier).CarriesFlow).ToArray()
|
||||
: Array.Empty<GridPosition>();
|
||||
|
||||
if (carriers.Length == 1 && branches.Length is not 3 and not 4)
|
||||
errors.Add("Junction must have one incoming branch and two or three outgoing branches.");
|
||||
|
||||
var sourceBranches = carriers.Length == 1
|
||||
? branches.Select(branch => new SourceBranch(branch, ShortestDistanceToSource(level, branch, position, carrier)))
|
||||
.Where(branch => branch.Distance.HasValue)
|
||||
.ToArray()
|
||||
: Array.Empty<SourceBranch>();
|
||||
|
||||
GridPosition? incomingBranch = null;
|
||||
if (sourceBranches.Length > 0)
|
||||
{
|
||||
var bestDistance = sourceBranches.Min(branch => branch.Distance!.Value);
|
||||
var bestBranches = sourceBranches.Where(branch => branch.Distance == bestDistance).ToArray();
|
||||
if (bestBranches.Length != 1 || sourceBranches.Length != 1)
|
||||
errors.Add("Ambiguous junction flow.");
|
||||
else
|
||||
incomingBranch = bestBranches[0].Position;
|
||||
}
|
||||
|
||||
var outgoingBranches = incomingBranch is null
|
||||
? Array.Empty<GridPosition>()
|
||||
: branches.Where(branch => branch != incomingBranch).ToArray();
|
||||
|
||||
return new() {
|
||||
Position = position,
|
||||
Prop = prop,
|
||||
Carrier = carrier,
|
||||
Branches = branches,
|
||||
IncomingBranch = incomingBranch,
|
||||
OutgoingBranches = outgoingBranches,
|
||||
Errors = errors
|
||||
};
|
||||
}
|
||||
|
||||
private static int? ShortestDistanceToSource(LevelState level, GridPosition start, GridPosition blocked, ECarrierType carrier)
|
||||
{
|
||||
var visited = new HashSet<GridPosition> { blocked, start };
|
||||
var open = new Queue<(GridPosition Position, int Distance)>();
|
||||
open.Enqueue((start, 0));
|
||||
|
||||
while (open.Count > 0)
|
||||
{
|
||||
var current = open.Dequeue();
|
||||
if (level.GetProp(current.Position) is { Type: EPropType.Flow, Carrier: var sourceCarrier, SwitchState: EPropSwitchState.Enabled } && sourceCarrier == carrier)
|
||||
return current.Distance;
|
||||
|
||||
foreach (var next in current.Position.Neighbors().Where(level.InBounds))
|
||||
{
|
||||
if (!visited.Add(next) || !level.GetUnderground(next, carrier).CarriesFlow)
|
||||
continue;
|
||||
|
||||
open.Enqueue((next, current.Distance + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
3
src/ReactorMaintenance.Simulation/JunctionRatioPreset.cs
Normal file
3
src/ReactorMaintenance.Simulation/JunctionRatioPreset.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record JunctionRatioPreset(string Label, float[] Weights);
|
||||
@@ -1,119 +1,168 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EEditorTool
|
||||
{
|
||||
Cursor,
|
||||
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, EEditorTool tool)
|
||||
public static LevelState Apply(LevelState level, GridPosition position, EditorToolCommand command)
|
||||
{
|
||||
if (!level.InBounds(position))
|
||||
return level;
|
||||
|
||||
if (tool == EEditorTool.Robot)
|
||||
return level.GetCell(position).IsWalkable ? level with { Robot = position } : level;
|
||||
|
||||
var cell = level.GetCell(position);
|
||||
cell = tool switch {
|
||||
EEditorTool.Cursor => cell,
|
||||
EEditorTool.Floor => cell with { Terrain = ECellTerrain.Floor },
|
||||
EEditorTool.Wall => cell with {
|
||||
Terrain = ECellTerrain.Wall,
|
||||
Prop = ECellProp.None,
|
||||
Pipe = EPipeMedium.None,
|
||||
Flow = Balancing.Current.MinHazardValue,
|
||||
Pressure = Balancing.Current.MinHazardValue,
|
||||
LeakRate = Balancing.Current.MinHazardValue,
|
||||
PipeOpen = false,
|
||||
Powered = false
|
||||
},
|
||||
EEditorTool.Reactor => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.Reactor },
|
||||
EEditorTool.CoolingPump => cell with {
|
||||
Terrain = ECellTerrain.Floor,
|
||||
Prop = ECellProp.CoolingPump,
|
||||
Powered = true
|
||||
},
|
||||
EEditorTool.Generator => cell with {
|
||||
Terrain = ECellTerrain.Floor,
|
||||
Prop = ECellProp.Generator,
|
||||
Powered = true
|
||||
},
|
||||
EEditorTool.PressureRegulator => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.PressureRegulator },
|
||||
EEditorTool.DiagnosticTerminal => cell with {
|
||||
Terrain = ECellTerrain.Floor,
|
||||
Prop = ECellProp.DiagnosticTerminal,
|
||||
Powered = true
|
||||
},
|
||||
EEditorTool.ControlTerminal => cell with {
|
||||
Terrain = ECellTerrain.Floor,
|
||||
Prop = ECellProp.ControlTerminal,
|
||||
Powered = true
|
||||
},
|
||||
EEditorTool.CoolantPipe => cell with {
|
||||
Pipe = EPipeMedium.Coolant,
|
||||
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.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.Current.DefaultPressurePipeFlow,
|
||||
Pressure = Balancing.Current.DefaultPressurePipePressure,
|
||||
Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
|
||||
PipeOpen = true
|
||||
},
|
||||
EEditorTool.Leak => cell with {
|
||||
LeakRate = Math.Max(Balancing.Current.MinimumLeakRate, cell.LeakRate),
|
||||
Integrity = Math.Min(cell.Integrity, Balancing.Current.DamagedPipeIntegrity)
|
||||
},
|
||||
EEditorTool.Repair => cell with {
|
||||
LeakRate = Balancing.Current.RepairedLeakRate,
|
||||
Integrity = Balancing.Current.DefaultCellIntegrity,
|
||||
Hazards = cell.Hazards with {
|
||||
Fire = false,
|
||||
ElectricalCharge = Balancing.Current.RepairedElectricalCharge
|
||||
}
|
||||
},
|
||||
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.Current.FireToolMinimumHeat),
|
||||
Smoke = Math.Max(cell.Hazards.Smoke, Balancing.Current.FireToolMinimumSmoke)
|
||||
}
|
||||
},
|
||||
_ => cell
|
||||
return command.Tool switch {
|
||||
EEditorTool.Cursor => level,
|
||||
EEditorTool.Floor => level.SetTerrain(position, ECellTerrain.Floor),
|
||||
EEditorTool.Wall => level.SetTerrain(position, ECellTerrain.Wall),
|
||||
EEditorTool.Underground => SetUnderground(level, position, command.Carrier),
|
||||
EEditorTool.Flow => SetCarrierProp(level, position, EPropType.Flow, command.Carrier),
|
||||
EEditorTool.Consumer => SetCarrierProp(level, position, EPropType.Consumer, command.Carrier),
|
||||
EEditorTool.Junction => SetFloorProp(level, position, new() { Type = EPropType.Junction }),
|
||||
EEditorTool.Door => SetFloorProp(level, position, new() { Type = EPropType.Door }),
|
||||
EEditorTool.AllSeeingEyeTerminal => SetFloorProp(level, position, new() { Type = EPropType.AllSeeingEyeTerminal }),
|
||||
EEditorTool.RemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = command.RemedyType }),
|
||||
EEditorTool.ReactorControl => SetReactorControl(level, position),
|
||||
EEditorTool.Leak => SetLeak(level, position, command.Carrier),
|
||||
EEditorTool.SurfaceHazard => AddSurfaceHazard(level, position, command.Carrier),
|
||||
EEditorTool.Heat => level.SetSurface(position, level.GetSurface(position) with { Heat = level.GetSurface(position).Heat + 1 }),
|
||||
EEditorTool.Robot => level.IsFloor(position) ? level with { Robot = level.Robot with { Position = position } } : level,
|
||||
_ => level
|
||||
};
|
||||
}
|
||||
|
||||
if (cell.Terrain == ECellTerrain.Wall)
|
||||
cell = cell with { Hazards = new() };
|
||||
public static LevelState SetDoorEdge(LevelState level, GridPosition a, GridPosition b)
|
||||
{
|
||||
if (!level.IsFloor(a) || !level.IsFloor(b) || a.ManhattanDistance(b) != 1)
|
||||
return level;
|
||||
|
||||
return level.SetCell(position, cell);
|
||||
return level.SetProp(a, new() { Type = EPropType.Door }) with {
|
||||
Doors = [
|
||||
.. level.Doors.Where(door => !SameDoorEdge(door, a, b)),
|
||||
new() { A = a, B = b, State = EDoorState.Closed }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
public static LevelState SetLeak(LevelState level, GridPosition undergroundPosition, GridPosition accessPosition, ECarrierType carrier)
|
||||
{
|
||||
if (!level.InBounds(undergroundPosition) || !level.IsFloor(accessPosition))
|
||||
return level;
|
||||
|
||||
if (carrier is ECarrierType.Fuel or ECarrierType.Coolant && undergroundPosition != accessPosition)
|
||||
return level;
|
||||
|
||||
if (carrier == ECarrierType.Electricity && undergroundPosition.ManhattanDistance(accessPosition) != 1)
|
||||
return level;
|
||||
|
||||
var next = level.SetUnderground(undergroundPosition, carrier, new() { State = EUndergroundState.Leaking });
|
||||
return next with {
|
||||
Leaks = [
|
||||
.. next.Leaks.Where(leak => leak.UndergroundPosition != undergroundPosition || leak.Carrier != carrier),
|
||||
new() {
|
||||
Carrier = carrier,
|
||||
UndergroundPosition = undergroundPosition,
|
||||
AccessPosition = accessPosition
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
public static LevelState BindReactorConsumer(LevelState level, int reactorId, ECarrierType carrier, GridPosition consumerPosition)
|
||||
{
|
||||
if (!level.InBounds(consumerPosition) || level.GetProp(consumerPosition) is not { Type: EPropType.Consumer } prop || prop.Carrier != carrier)
|
||||
return level;
|
||||
|
||||
var reactors = level.Reactors.Select(reactor => reactor.ReactorId == reactorId ? BindConsumer(reactor, carrier, consumerPosition) : reactor).ToArray();
|
||||
return level with { Reactors = reactors };
|
||||
}
|
||||
|
||||
public static LevelState AddRuleEvent(LevelState level, RuleEventState ruleEvent)
|
||||
{
|
||||
var id = string.IsNullOrWhiteSpace(ruleEvent.Id) ? NextRuleEventId(level) : ruleEvent.Id;
|
||||
var authoredEvent = ruleEvent with { Id = id };
|
||||
return level with {
|
||||
RuleEvents = [.. level.RuleEvents.Where(existing => existing.Id != id), authoredEvent]
|
||||
};
|
||||
}
|
||||
|
||||
public static LevelState RemoveRuleEvent(LevelState level, string id)
|
||||
{
|
||||
return level with { RuleEvents = level.RuleEvents.Where(ruleEvent => ruleEvent.Id != id).ToArray() };
|
||||
}
|
||||
|
||||
private static LevelState SetUnderground(LevelState level, GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
return level.SetUnderground(position, carrier, new() { State = EUndergroundState.Intact });
|
||||
}
|
||||
|
||||
private static LevelState SetCarrierProp(LevelState level, GridPosition position, EPropType type, ECarrierType carrier)
|
||||
{
|
||||
return SetFloorProp(level, position, new() { Type = type, Carrier = carrier, SwitchState = EPropSwitchState.Enabled });
|
||||
}
|
||||
|
||||
private static LevelState AddSurfaceHazard(LevelState level, GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
var surface = level.GetSurface(position);
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => level.SetSurface(position, surface with { Fuel = surface.Fuel + 1 }),
|
||||
ECarrierType.Coolant => level.SetSurface(position, surface with { Coolant = surface.Coolant + 1 }),
|
||||
ECarrierType.Electricity => level.SetSurface(position, surface with { Electricity = surface.Electricity + 1 }),
|
||||
_ => level
|
||||
};
|
||||
}
|
||||
|
||||
private static LevelState SetFloorProp(LevelState level, GridPosition position, PropState prop)
|
||||
{
|
||||
return level.IsFloor(position) ? level.SetProp(position, prop) : level;
|
||||
}
|
||||
|
||||
private static LevelState SetReactorControl(LevelState level, GridPosition position)
|
||||
{
|
||||
if (!level.IsFloor(position))
|
||||
return level;
|
||||
|
||||
var id = level.Reactors.Count == 0 ? 1 : level.Reactors.Max(reactor => reactor.ReactorId) + 1;
|
||||
var levelWithProp = level.SetProp(position, new() { Type = EPropType.ReactorControl, ReactorId = id });
|
||||
return levelWithProp with {
|
||||
Reactors = [
|
||||
.. level.Reactors,
|
||||
new() {
|
||||
ReactorId = id,
|
||||
ControlPosition = position,
|
||||
FuelConsumerPosition = position,
|
||||
CoolantConsumerPosition = position,
|
||||
ElectricityConsumerPosition = position
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
private static LevelState SetLeak(LevelState level, GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
if (!level.InBounds(position))
|
||||
return level;
|
||||
|
||||
return SetLeak(level, position, position, carrier);
|
||||
}
|
||||
|
||||
private static bool SameDoorEdge(DoorState door, GridPosition a, GridPosition b)
|
||||
{
|
||||
return (door.A == a && door.B == b) || (door.A == b && door.B == a);
|
||||
}
|
||||
|
||||
private static ReactorBinding BindConsumer(ReactorBinding reactor, ECarrierType carrier, GridPosition consumerPosition)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => reactor with { FuelConsumerPosition = consumerPosition },
|
||||
ECarrierType.Coolant => reactor with { CoolantConsumerPosition = consumerPosition },
|
||||
ECarrierType.Electricity => reactor with { ElectricityConsumerPosition = consumerPosition },
|
||||
_ => reactor
|
||||
};
|
||||
}
|
||||
|
||||
private static string NextRuleEventId(LevelState level)
|
||||
{
|
||||
var next = level.RuleEvents.Count + 1;
|
||||
while (level.RuleEvents.Any(ruleEvent => ruleEvent.Id == $"rule-{next}"))
|
||||
next++;
|
||||
|
||||
return $"rule-{next}";
|
||||
}
|
||||
}
|
||||
@@ -5,35 +5,38 @@ namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class LevelSerializer
|
||||
{
|
||||
private const int c_CurrentVersion = 1;
|
||||
private sealed record LevelFile
|
||||
{
|
||||
public int Version { get; init; }
|
||||
public LevelState? Level { get; init; }
|
||||
}
|
||||
|
||||
public static string Serialize(LevelState level)
|
||||
{
|
||||
return JsonSerializer.Serialize(new LevelFile {
|
||||
Version = c_CurrentVersion,
|
||||
Level = level
|
||||
}, Options);
|
||||
}, s_Options);
|
||||
}
|
||||
|
||||
public static LevelState Deserialize(string json)
|
||||
{
|
||||
var file = JsonSerializer.Deserialize<LevelFile>(json, Options) ?? throw new InvalidOperationException("Level file did not contain a level.");
|
||||
var level = file.Version switch {
|
||||
c_CurrentVersion => file.Level,
|
||||
_ => throw new InvalidOperationException($"Unsupported level file version {file.Version}.")
|
||||
};
|
||||
var file = JsonSerializer.Deserialize<LevelFile>(json, s_Options) ?? throw new InvalidOperationException("Level file did not contain a level.");
|
||||
if (file.Version != c_CurrentVersion)
|
||||
throw new InvalidOperationException($"Unsupported level file version {file.Version}. Expected {c_CurrentVersion}.");
|
||||
|
||||
return level.Cells.Length != level.Width * level.Height ? throw new InvalidOperationException("Level cell count does not match its dimensions.") : level;
|
||||
var level = file.Level ?? throw new InvalidOperationException("Level file did not contain a level.");
|
||||
var report = new LevelValidator().Validate(level);
|
||||
if (!report.IsValid)
|
||||
throw new InvalidOperationException(report.Errors[0].Message);
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions Options = new() {
|
||||
private const int c_CurrentVersion = 2;
|
||||
|
||||
private static readonly JsonSerializerOptions s_Options = new() {
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
private sealed record LevelFile
|
||||
{
|
||||
public int Version { get; init; }
|
||||
public LevelState Level { get; init; } = new();
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/ReactorMaintenance.Simulation/LevelStateExtensions.cs
Normal file
116
src/ReactorMaintenance.Simulation/LevelStateExtensions.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class LevelStateExtensions
|
||||
{
|
||||
public static bool InBounds(this LevelState level, GridPosition position)
|
||||
{
|
||||
return position.X >= 0 && position.Y >= 0 && position.X < level.Width && position.Y < level.Height;
|
||||
}
|
||||
|
||||
public static int Index(this LevelState level, GridPosition position)
|
||||
{
|
||||
if (!level.InBounds(position))
|
||||
throw new ArgumentOutOfRangeException(nameof(position), $"Position {position.X},{position.Y} is outside {level.Width}x{level.Height}.");
|
||||
|
||||
return (position.Y * level.Width) + position.X;
|
||||
}
|
||||
|
||||
public static ECellTerrain GetTerrain(this LevelState level, GridPosition position)
|
||||
{
|
||||
return level.Terrain[level.Index(position)];
|
||||
}
|
||||
|
||||
public static UndergroundCell GetUnderground(this LevelState level, GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
return level.Layer(carrier)[level.Index(position)];
|
||||
}
|
||||
|
||||
public static SurfaceState GetSurface(this LevelState level, GridPosition position)
|
||||
{
|
||||
return level.Surface[level.Index(position)];
|
||||
}
|
||||
|
||||
public static PropState GetProp(this LevelState level, GridPosition position)
|
||||
{
|
||||
return level.Props[level.Index(position)];
|
||||
}
|
||||
|
||||
public static bool IsFloor(this LevelState level, GridPosition position)
|
||||
{
|
||||
return level.InBounds(position) && level.GetTerrain(position) == ECellTerrain.Floor;
|
||||
}
|
||||
|
||||
public static bool IsClosedDoorEdge(this LevelState level, GridPosition a, GridPosition b)
|
||||
{
|
||||
return level.Doors.Any(door => door.State == EDoorState.Closed && SameEdge(door.A, door.B, a, b));
|
||||
}
|
||||
|
||||
public static LevelState SetTerrain(this LevelState level, GridPosition position, ECellTerrain terrain)
|
||||
{
|
||||
var next = level.Terrain.ToArray();
|
||||
next[level.Index(position)] = terrain;
|
||||
var updated = level with { Terrain = next };
|
||||
return terrain == ECellTerrain.Wall ? updated.ClearFloorOnlyState(position) : updated;
|
||||
}
|
||||
|
||||
public static LevelState SetUnderground(this LevelState level, GridPosition position, ECarrierType carrier, UndergroundCell cell)
|
||||
{
|
||||
var next = level.Layer(carrier).ToArray();
|
||||
next[level.Index(position)] = cell;
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => level with { Fuel = next },
|
||||
ECarrierType.Coolant => level with { Coolant = next },
|
||||
ECarrierType.Electricity => level with { Electricity = next },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
public static LevelState SetSurface(this LevelState level, GridPosition position, SurfaceState surface)
|
||||
{
|
||||
var next = level.Surface.ToArray();
|
||||
next[level.Index(position)] = surface.Clamp();
|
||||
return level with { Surface = next };
|
||||
}
|
||||
|
||||
public static LevelState SetProp(this LevelState level, GridPosition position, PropState prop)
|
||||
{
|
||||
var next = level.Props.ToArray();
|
||||
next[level.Index(position)] = prop;
|
||||
return level with { Props = next };
|
||||
}
|
||||
|
||||
public static LevelState WithRuntimeArrays(this LevelState level, UndergroundCell[] fuel, UndergroundCell[] coolant, UndergroundCell[] electricity, SurfaceState[] surface, PropState[] props)
|
||||
{
|
||||
return level with {
|
||||
Fuel = fuel,
|
||||
Coolant = coolant,
|
||||
Electricity = electricity,
|
||||
Surface = surface,
|
||||
Props = props
|
||||
};
|
||||
}
|
||||
|
||||
public static IReadOnlyList<UndergroundCell> Layer(this LevelState level, ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => level.Fuel,
|
||||
ECarrierType.Coolant => level.Coolant,
|
||||
ECarrierType.Electricity => level.Electricity,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
private static LevelState ClearFloorOnlyState(this LevelState level, GridPosition position)
|
||||
{
|
||||
return level.SetSurface(position, new())
|
||||
.SetProp(position, new())
|
||||
.SetUnderground(position, ECarrierType.Fuel, new())
|
||||
.SetUnderground(position, ECarrierType.Coolant, new())
|
||||
.SetUnderground(position, ECarrierType.Electricity, new());
|
||||
}
|
||||
|
||||
private static bool SameEdge(GridPosition edgeA, GridPosition edgeB, GridPosition a, GridPosition b)
|
||||
{
|
||||
return (edgeA == a && edgeB == b) || (edgeA == b && edgeB == a);
|
||||
}
|
||||
}
|
||||
54
src/ReactorMaintenance.Simulation/LevelStateFactory.cs
Normal file
54
src/ReactorMaintenance.Simulation/LevelStateFactory.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class LevelStateFactory
|
||||
{
|
||||
public static LevelState Create(string name, int width, int height)
|
||||
{
|
||||
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}.");
|
||||
|
||||
return new() {
|
||||
Name = name,
|
||||
Width = width,
|
||||
Height = height,
|
||||
Terrain = CreateTerrain(width, height),
|
||||
Fuel = CreateUnderground(width, height),
|
||||
Coolant = CreateUnderground(width, height),
|
||||
Electricity = CreateUnderground(width, height),
|
||||
Surface = CreateSurface(width, height),
|
||||
Props = CreateProps(width, height),
|
||||
Robot = new() { Position = new(1, 1) },
|
||||
Forecasts = Array.Empty<Forecast>()
|
||||
};
|
||||
}
|
||||
|
||||
public static ECellTerrain[] CreateTerrain(int width, int height)
|
||||
{
|
||||
var terrain = Enumerable.Repeat(ECellTerrain.Floor, width * height).ToArray();
|
||||
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)
|
||||
terrain[(y * width) + x] = ECellTerrain.Wall;
|
||||
}
|
||||
}
|
||||
|
||||
return terrain;
|
||||
}
|
||||
|
||||
public static UndergroundCell[] CreateUnderground(int width, int height)
|
||||
{
|
||||
return Enumerable.Range(0, width * height).Select(_ => new UndergroundCell()).ToArray();
|
||||
}
|
||||
|
||||
public static SurfaceState[] CreateSurface(int width, int height)
|
||||
{
|
||||
return Enumerable.Range(0, width * height).Select(_ => new SurfaceState()).ToArray();
|
||||
}
|
||||
|
||||
public static PropState[] CreateProps(int width, int height)
|
||||
{
|
||||
return Enumerable.Range(0, width * height).Select(_ => new PropState()).ToArray();
|
||||
}
|
||||
}
|
||||
13
src/ReactorMaintenance.Simulation/LevelTraversal.cs
Normal file
13
src/ReactorMaintenance.Simulation/LevelTraversal.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class LevelTraversal
|
||||
{
|
||||
public static IEnumerable<GridPosition> AllPositions(LevelState level)
|
||||
{
|
||||
for (var y = 0; y < level.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < level.Width; x++)
|
||||
yield return new(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
284
src/ReactorMaintenance.Simulation/LevelValidator.cs
Normal file
284
src/ReactorMaintenance.Simulation/LevelValidator.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed class LevelValidator
|
||||
{
|
||||
public ValidationReport Validate(LevelState level)
|
||||
{
|
||||
var errors = new List<ValidationIssue>();
|
||||
var warnings = new List<ValidationIssue>();
|
||||
|
||||
ValidateDimensions(level, errors);
|
||||
ValidateRobot(level, errors);
|
||||
ValidateCells(level, errors);
|
||||
ValidateDoors(level, errors);
|
||||
ValidateLeaks(level, errors);
|
||||
ValidateReactors(level, errors, warnings);
|
||||
ValidateJunctions(level, errors);
|
||||
ValidateRuleEvents(level, errors);
|
||||
ValidateWarnings(level, warnings);
|
||||
|
||||
return new() { Errors = errors, Warnings = warnings };
|
||||
}
|
||||
|
||||
private static void ValidateDimensions(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
if (level.Width < Balancing.Current.MinimumLevelSize || level.Height < Balancing.Current.MinimumLevelSize)
|
||||
errors.Add(new("Invalid level dimensions."));
|
||||
|
||||
var expected = level.Width * level.Height;
|
||||
if (level.Terrain.Length != expected || level.Fuel.Length != expected || level.Coolant.Length != expected || level.Electricity.Length != expected || level.Surface.Length != expected || level.Props.Length != expected)
|
||||
errors.Add(new("Cell array counts do not match level dimensions."));
|
||||
}
|
||||
|
||||
private static void ValidateRobot(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
if (!level.IsFloor(level.Robot.Position))
|
||||
errors.Add(new("Robot must be in bounds on a floor cell.", level.Robot.Position));
|
||||
}
|
||||
|
||||
private static void ValidateCells(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
for (var y = 0; y < level.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var surface = level.GetSurface(position);
|
||||
var prop = level.GetProp(position);
|
||||
|
||||
if (level.GetTerrain(position) == ECellTerrain.Wall)
|
||||
{
|
||||
if (surface.Fuel > 0 || surface.Coolant > 0 || surface.Electricity > 0 || surface.Heat > 0)
|
||||
errors.Add(new("Wall cell cannot store surface hazards.", position));
|
||||
|
||||
if (prop.Type != EPropType.None)
|
||||
errors.Add(new("Prop must be placed on floor terrain.", position));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateDoors(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
foreach (var door in level.Doors)
|
||||
{
|
||||
if (!level.IsFloor(door.A) || !level.IsFloor(door.B) || door.A.ManhattanDistance(door.B) != 1)
|
||||
errors.Add(new("Door edge must connect two adjacent floor cells.", door.A));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateLeaks(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
foreach (var leak in level.Leaks)
|
||||
{
|
||||
if (!level.InBounds(leak.UndergroundPosition) || !level.IsFloor(leak.AccessPosition))
|
||||
{
|
||||
errors.Add(new("Leak must have valid floor access.", leak.AccessPosition));
|
||||
continue;
|
||||
}
|
||||
|
||||
var underground = level.GetUnderground(leak.UndergroundPosition, leak.Carrier);
|
||||
if (!underground.IsPresent)
|
||||
errors.Add(new("Leak target must point to an underground cell.", leak.UndergroundPosition));
|
||||
|
||||
if (leak.Carrier is ECarrierType.Fuel or ECarrierType.Coolant && leak.UndergroundPosition != leak.AccessPosition)
|
||||
errors.Add(new("Fuel and coolant leaks must use their underground coordinate as access.", leak.AccessPosition));
|
||||
|
||||
if (leak.Carrier == ECarrierType.Electricity && leak.UndergroundPosition.ManhattanDistance(leak.AccessPosition) != 1)
|
||||
errors.Add(new("Electricity leak access must be an adjacent floor face.", leak.AccessPosition));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateReactors(LevelState level, List<ValidationIssue> errors, List<ValidationIssue> warnings)
|
||||
{
|
||||
foreach (var reactor in level.Reactors)
|
||||
{
|
||||
if (!IsProp(level, reactor.ControlPosition, EPropType.ReactorControl))
|
||||
errors.Add(new("Reactor binding control position must point to a reactor control prop.", reactor.ControlPosition));
|
||||
|
||||
ValidateConsumerBinding(level, reactor.FuelConsumerPosition, ECarrierType.Fuel, errors);
|
||||
ValidateConsumerBinding(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant, errors);
|
||||
ValidateConsumerBinding(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity, errors);
|
||||
|
||||
if (!reactor.Ready)
|
||||
warnings.Add(new("Reactor is initially unready.", reactor.ControlPosition));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateConsumerBinding(LevelState level, GridPosition position, ECarrierType carrier, List<ValidationIssue> errors)
|
||||
{
|
||||
if (!level.InBounds(position) || level.GetProp(position) is not { Type: EPropType.Consumer } prop || prop.Carrier != carrier)
|
||||
errors.Add(new($"Missing or invalid {carrier} consumer binding.", position));
|
||||
}
|
||||
|
||||
private static void ValidateJunctions(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
foreach (var junction in JunctionFlowAnalyzer.Analyze(level))
|
||||
errors.AddRange(junction.Errors.Select(error => new ValidationIssue(error, junction.Position)));
|
||||
}
|
||||
|
||||
private static void ValidateRuleEvents(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
foreach (var ruleEvent in level.RuleEvents)
|
||||
{
|
||||
foreach (var predicate in ruleEvent.Predicates)
|
||||
ValidateRulePredicate(level, predicate, errors);
|
||||
|
||||
foreach (var effect in ruleEvent.Effects)
|
||||
ValidateRuleEffect(level, effect, errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateRulePredicate(LevelState level, RulePredicate predicate, List<ValidationIssue> errors)
|
||||
{
|
||||
switch (predicate.Kind)
|
||||
{
|
||||
case ERulePredicateKind.PropStateAt:
|
||||
if (!level.InBounds(predicate.Position) || level.GetProp(predicate.Position).Type == EPropType.None)
|
||||
errors.Add(new("Rule prop predicate must target a prop.", predicate.Position));
|
||||
break;
|
||||
case ERulePredicateKind.ConsumerStateAt:
|
||||
if (!IsProp(level, predicate.Position, EPropType.Consumer))
|
||||
errors.Add(new("Rule consumer predicate must target a consumer prop.", predicate.Position));
|
||||
break;
|
||||
case ERulePredicateKind.NetworkBandAt:
|
||||
if (!level.InBounds(predicate.Position) || !level.GetUnderground(predicate.Position, predicate.Carrier).IsPresent)
|
||||
errors.Add(new("Rule network predicate must target an underground cell.", predicate.Position));
|
||||
break;
|
||||
case ERulePredicateKind.SurfaceBandAt:
|
||||
case ERulePredicateKind.RobotAt:
|
||||
if (!level.IsFloor(predicate.Position))
|
||||
errors.Add(new("Rule floor predicate must target a floor cell.", predicate.Position));
|
||||
break;
|
||||
case ERulePredicateKind.ReactorReadyIs:
|
||||
case ERulePredicateKind.ReactorWonIs:
|
||||
ValidateOptionalReactorId(level, predicate.ReactorId, predicate.Position, errors);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateRuleEffect(LevelState level, RuleEffect effect, List<ValidationIssue> errors)
|
||||
{
|
||||
if (RequiresNonNegativeAmount(effect.Kind) && effect.Amount < 0)
|
||||
errors.Add(new("Rule effect amount must be non-negative.", effect.Position));
|
||||
|
||||
switch (effect.Kind)
|
||||
{
|
||||
case ERuleEffectKind.StartLeak:
|
||||
ValidateRuleLeakEffect(level, effect, errors);
|
||||
break;
|
||||
case ERuleEffectKind.WorsenLeak:
|
||||
case ERuleEffectKind.RepairNetworkCell:
|
||||
case ERuleEffectKind.DisableNetworkCell:
|
||||
if (!level.InBounds(effect.Position) || !level.GetUnderground(effect.Position, effect.Carrier).IsPresent)
|
||||
errors.Add(new("Rule network effect must target an underground cell.", effect.Position));
|
||||
break;
|
||||
case ERuleEffectKind.SetPropEnabled:
|
||||
if (!level.InBounds(effect.Position) || level.GetProp(effect.Position).Type == EPropType.None)
|
||||
errors.Add(new("Rule prop effect must target a prop.", effect.Position));
|
||||
break;
|
||||
case ERuleEffectKind.AddSurfaceHazard:
|
||||
case ERuleEffectKind.RemoveSurfaceHazard:
|
||||
case ERuleEffectKind.AddHeat:
|
||||
case ERuleEffectKind.RemoveHeat:
|
||||
if (!level.IsFloor(effect.Position))
|
||||
errors.Add(new("Rule surface effect must target a floor cell.", effect.Position));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool RequiresNonNegativeAmount(ERuleEffectKind kind)
|
||||
{
|
||||
return kind is ERuleEffectKind.AddSurfaceHazard
|
||||
or ERuleEffectKind.RemoveSurfaceHazard
|
||||
or ERuleEffectKind.AddHeat
|
||||
or ERuleEffectKind.RemoveHeat
|
||||
or ERuleEffectKind.AddInventory
|
||||
or ERuleEffectKind.RemoveInventory;
|
||||
}
|
||||
|
||||
private static void ValidateRuleLeakEffect(LevelState level, RuleEffect effect, List<ValidationIssue> errors)
|
||||
{
|
||||
var accessPosition = effect.AccessPosition ?? effect.Position;
|
||||
if (!level.InBounds(effect.Position) || !level.GetUnderground(effect.Position, effect.Carrier).IsPresent)
|
||||
errors.Add(new("Rule leak effect must target an underground cell.", effect.Position));
|
||||
|
||||
if (!level.IsFloor(accessPosition))
|
||||
{
|
||||
errors.Add(new("Rule leak effect must have valid floor access.", accessPosition));
|
||||
return;
|
||||
}
|
||||
|
||||
if (effect.Carrier is ECarrierType.Fuel or ECarrierType.Coolant && effect.Position != accessPosition)
|
||||
errors.Add(new("Rule fuel and coolant leak effects must use their underground coordinate as access.", accessPosition));
|
||||
|
||||
if (effect.Carrier == ECarrierType.Electricity && effect.Position.ManhattanDistance(accessPosition) != 1)
|
||||
errors.Add(new("Rule electricity leak effect access must be an adjacent floor face.", accessPosition));
|
||||
}
|
||||
|
||||
private static void ValidateOptionalReactorId(LevelState level, int reactorId, GridPosition position, List<ValidationIssue> errors)
|
||||
{
|
||||
if (reactorId > 0 && level.Reactors.All(reactor => reactor.ReactorId != reactorId))
|
||||
errors.Add(new("Rule reactor predicate must reference an existing reactor.", position));
|
||||
}
|
||||
|
||||
private static void ValidateWarnings(LevelState level, List<ValidationIssue> warnings)
|
||||
{
|
||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||
{
|
||||
for (var y = 0; y < level.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
if (level.GetUnderground(position, carrier).IsPresent && !HasSourcePath(level, position, carrier))
|
||||
warnings.Add(new($"Underground {carrier} cell has no source path.", position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var y = 0; y < level.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type == EPropType.Consumer && prop.SwitchState == EPropSwitchState.Enabled && !HasSourcePath(level, position, prop.Carrier))
|
||||
warnings.Add(new("Enabled consumer is initially starved.", position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasSourcePath(LevelState level, GridPosition start, ECarrierType carrier)
|
||||
{
|
||||
if (!level.GetUnderground(start, carrier).CarriesFlow)
|
||||
return false;
|
||||
|
||||
var visited = new HashSet<GridPosition>();
|
||||
var open = new Queue<GridPosition>();
|
||||
open.Enqueue(start);
|
||||
visited.Add(start);
|
||||
|
||||
while (open.Count > 0)
|
||||
{
|
||||
var current = open.Dequeue();
|
||||
if (level.GetProp(current) is { Type: EPropType.Flow, Carrier: var sourceCarrier, SwitchState: EPropSwitchState.Enabled } && sourceCarrier == carrier)
|
||||
return true;
|
||||
|
||||
foreach (var next in current.Neighbors().Where(level.InBounds))
|
||||
{
|
||||
if (!visited.Add(next) || !level.GetUnderground(next, carrier).CarriesFlow)
|
||||
continue;
|
||||
|
||||
open.Enqueue(next);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsProp(LevelState level, GridPosition position, EPropType propType)
|
||||
{
|
||||
return level.InBounds(position) && level.GetProp(position).Type == propType;
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ECellTerrain
|
||||
{
|
||||
Floor,
|
||||
Wall
|
||||
}
|
||||
|
||||
public enum ECellProp
|
||||
{
|
||||
None,
|
||||
Reactor,
|
||||
CoolingPump,
|
||||
Generator,
|
||||
PressureRegulator,
|
||||
DiagnosticTerminal,
|
||||
ControlTerminal
|
||||
}
|
||||
|
||||
public enum EPipeMedium
|
||||
{
|
||||
None,
|
||||
Pressure,
|
||||
Coolant,
|
||||
Fuel
|
||||
}
|
||||
|
||||
public enum EFailureKind
|
||||
{
|
||||
PipeBurst,
|
||||
Ignition,
|
||||
Meltdown,
|
||||
StabilityCollapse,
|
||||
ReactorReady
|
||||
}
|
||||
|
||||
public sealed record GridPosition(int X, int Y)
|
||||
{
|
||||
public IEnumerable<GridPosition> Neighbors()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
public int LiquidFuel { get; init; }
|
||||
public int CoolantPooling { get; init; }
|
||||
public int ElectricalCharge { get; init; }
|
||||
public int Stability { get; init; } = Balancing.Current.DefaultHazardStability;
|
||||
public bool Fire { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CellState
|
||||
{
|
||||
public ECellTerrain Terrain { get; init; } = ECellTerrain.Floor;
|
||||
public ECellProp Prop { get; init; }
|
||||
public EPipeMedium Pipe { get; init; }
|
||||
public int Flow { get; init; }
|
||||
public int Pressure { get; init; }
|
||||
public int Integrity { get; init; } = Balancing.Current.DefaultCellIntegrity;
|
||||
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 => Terrain != ECellTerrain.Wall;
|
||||
public bool HasPipe => Pipe != EPipeMedium.None;
|
||||
}
|
||||
|
||||
public sealed record GlobalState
|
||||
{
|
||||
public int Turn { get; init; }
|
||||
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";
|
||||
}
|
||||
|
||||
public sealed record Forecast(EFailureKind Kind, GridPosition? Position, int Turns, string Message);
|
||||
|
||||
public sealed record LevelState
|
||||
{
|
||||
public static LevelState Create(string name, int width, int height)
|
||||
{
|
||||
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.Current.FirstGridCoordinate; y < height; y++)
|
||||
{
|
||||
for (var x = Balancing.Current.FirstGridCoordinate; x < width; x++)
|
||||
{
|
||||
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 { Terrain = ECellTerrain.Wall };
|
||||
}
|
||||
}
|
||||
|
||||
return new() {
|
||||
Name = name,
|
||||
Width = width,
|
||||
Height = height,
|
||||
Cells = cells,
|
||||
Robot = new(Balancing.Current.DefaultRobotCoordinate, Balancing.Current.DefaultRobotCoordinate)
|
||||
};
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return position.X >= Balancing.Current.FirstGridCoordinate && position.Y >= Balancing.Current.FirstGridCoordinate && position.X < Width && position.Y < Height;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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.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<Forecast> Forecasts { get; init; } = Array.Empty<Forecast>();
|
||||
}
|
||||
|
||||
internal static class Rules
|
||||
{
|
||||
public static int Clamp(int value)
|
||||
{
|
||||
return Math.Clamp(value, Balancing.Current.MinHazardValue, Balancing.Current.MaxHazardValue);
|
||||
}
|
||||
}
|
||||
8
src/ReactorMaintenance.Simulation/Models/DoorState.cs
Normal file
8
src/ReactorMaintenance.Simulation/Models/DoorState.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record DoorState
|
||||
{
|
||||
public GridPosition A { get; init; } = new(0, 0);
|
||||
public GridPosition B { get; init; } = new(0, 0);
|
||||
public EDoorState State { get; init; } = EDoorState.Closed;
|
||||
}
|
||||
8
src/ReactorMaintenance.Simulation/Models/EBand.cs
Normal file
8
src/ReactorMaintenance.Simulation/Models/EBand.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EBand
|
||||
{
|
||||
Safe,
|
||||
Caution,
|
||||
Critical
|
||||
}
|
||||
8
src/ReactorMaintenance.Simulation/Models/ECarrierType.cs
Normal file
8
src/ReactorMaintenance.Simulation/Models/ECarrierType.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ECarrierType
|
||||
{
|
||||
Fuel,
|
||||
Coolant,
|
||||
Electricity
|
||||
}
|
||||
7
src/ReactorMaintenance.Simulation/Models/ECellTerrain.cs
Normal file
7
src/ReactorMaintenance.Simulation/Models/ECellTerrain.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ECellTerrain
|
||||
{
|
||||
Floor,
|
||||
Wall
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EConsumerServiceState
|
||||
{
|
||||
Unknown,
|
||||
Disabled,
|
||||
Starved,
|
||||
Supplied,
|
||||
Producing
|
||||
}
|
||||
7
src/ReactorMaintenance.Simulation/Models/EDoorState.cs
Normal file
7
src/ReactorMaintenance.Simulation/Models/EDoorState.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EDoorState
|
||||
{
|
||||
Open,
|
||||
Closed
|
||||
}
|
||||
10
src/ReactorMaintenance.Simulation/Models/EForecastKind.cs
Normal file
10
src/ReactorMaintenance.Simulation/Models/EForecastKind.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EForecastKind
|
||||
{
|
||||
TerminalLoss,
|
||||
ReactorReady,
|
||||
ConsumerStarved,
|
||||
HazardGrowth,
|
||||
RuleEvent
|
||||
}
|
||||
11
src/ReactorMaintenance.Simulation/Models/ELevelState.cs
Normal file
11
src/ReactorMaintenance.Simulation/Models/ELevelState.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ELevelState
|
||||
{
|
||||
Stable,
|
||||
Caution,
|
||||
Critical,
|
||||
Ready,
|
||||
Lost,
|
||||
Won
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ENetworkValueKind
|
||||
{
|
||||
Amount,
|
||||
Intensity
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EPropSwitchState
|
||||
{
|
||||
Disabled,
|
||||
Enabled
|
||||
}
|
||||
13
src/ReactorMaintenance.Simulation/Models/EPropType.cs
Normal file
13
src/ReactorMaintenance.Simulation/Models/EPropType.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EPropType
|
||||
{
|
||||
None,
|
||||
Flow,
|
||||
Consumer,
|
||||
Junction,
|
||||
Door,
|
||||
AllSeeingEyeTerminal,
|
||||
RemedySupply,
|
||||
ReactorControl
|
||||
}
|
||||
9
src/ReactorMaintenance.Simulation/Models/ERemedyType.cs
Normal file
9
src/ReactorMaintenance.Simulation/Models/ERemedyType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ERemedyType
|
||||
{
|
||||
FuelNeutralizer,
|
||||
CoolantNeutralizer,
|
||||
ElectricityNeutralizer,
|
||||
HeatShield
|
||||
}
|
||||
18
src/ReactorMaintenance.Simulation/Models/ERuleEffectKind.cs
Normal file
18
src/ReactorMaintenance.Simulation/Models/ERuleEffectKind.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ERuleEffectKind
|
||||
{
|
||||
StartLeak,
|
||||
WorsenLeak,
|
||||
RepairNetworkCell,
|
||||
DisableNetworkCell,
|
||||
SetPropEnabled,
|
||||
AddSurfaceHazard,
|
||||
RemoveSurfaceHazard,
|
||||
AddHeat,
|
||||
RemoveHeat,
|
||||
AddInventory,
|
||||
RemoveInventory,
|
||||
MarkTerminalLoss,
|
||||
EmitWarning
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ERuleEventPhase
|
||||
{
|
||||
StartOfSimulation,
|
||||
EndOfTurn
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ERulePredicateKind
|
||||
{
|
||||
TurnAtLeast,
|
||||
LevelStateIs,
|
||||
ReactorReadyIs,
|
||||
ReactorLostIs,
|
||||
ReactorWonIs,
|
||||
PropStateAt,
|
||||
ConsumerStateAt,
|
||||
NetworkBandAt,
|
||||
SurfaceBandAt,
|
||||
RobotAt,
|
||||
RobotInventoryAtLeast,
|
||||
AllSeeingEyeUnlocked
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ESurfaceInteractionVerb
|
||||
{
|
||||
Hold,
|
||||
Flow,
|
||||
Warm,
|
||||
Quench,
|
||||
Short,
|
||||
Ignite
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ESurfaceQuantity
|
||||
{
|
||||
Fuel,
|
||||
Coolant,
|
||||
Electricity,
|
||||
Heat
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EUndergroundState
|
||||
{
|
||||
Absent,
|
||||
Intact,
|
||||
Leaking
|
||||
}
|
||||
3
src/ReactorMaintenance.Simulation/Models/Forecast.cs
Normal file
3
src/ReactorMaintenance.Simulation/Models/Forecast.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record Forecast(EForecastKind Kind, GridPosition? Position, int Turns, string Message);
|
||||
12
src/ReactorMaintenance.Simulation/Models/GlobalState.cs
Normal file
12
src/ReactorMaintenance.Simulation/Models/GlobalState.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record GlobalState
|
||||
{
|
||||
public int Turn { get; init; }
|
||||
public int ActionsRemaining { get; init; } = Balancing.Current.ActionsPerTurn;
|
||||
public ELevelState LevelState { get; init; } = ELevelState.Stable;
|
||||
public string Status { get; init; } = "STABLE";
|
||||
public bool AllSeeingEyeUnlocked { get; init; }
|
||||
public bool TerminalLoss { get; init; }
|
||||
public IReadOnlyList<string> Warnings { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
3
src/ReactorMaintenance.Simulation/Models/GridPosition.cs
Normal file
3
src/ReactorMaintenance.Simulation/Models/GridPosition.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record GridPosition(int X, int Y);
|
||||
9
src/ReactorMaintenance.Simulation/Models/LeakState.cs
Normal file
9
src/ReactorMaintenance.Simulation/Models/LeakState.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record LeakState
|
||||
{
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public GridPosition UndergroundPosition { get; init; } = new(0, 0);
|
||||
public GridPosition AccessPosition { get; init; } = new(0, 0);
|
||||
public bool Repaired { get; init; }
|
||||
}
|
||||
26
src/ReactorMaintenance.Simulation/Models/LevelState.cs
Normal file
26
src/ReactorMaintenance.Simulation/Models/LevelState.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record LevelState
|
||||
{
|
||||
public static LevelState Create(string name, int width, int height)
|
||||
{
|
||||
return LevelStateFactory.Create(name, width, height);
|
||||
}
|
||||
|
||||
public string Name { get; init; } = "New Reactor";
|
||||
public int Width { get; init; } = Balancing.Current.DefaultLevelWidth;
|
||||
public int Height { get; init; } = Balancing.Current.DefaultLevelHeight;
|
||||
public ECellTerrain[] Terrain { get; init; } = LevelStateFactory.CreateTerrain(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||
public UndergroundCell[] Fuel { get; init; } = LevelStateFactory.CreateUnderground(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||
public UndergroundCell[] Coolant { get; init; } = LevelStateFactory.CreateUnderground(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||
public UndergroundCell[] Electricity { get; init; } = LevelStateFactory.CreateUnderground(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||
public SurfaceState[] Surface { get; init; } = LevelStateFactory.CreateSurface(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||
public PropState[] Props { get; init; } = LevelStateFactory.CreateProps(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||
public IReadOnlyList<DoorState> Doors { get; init; } = Array.Empty<DoorState>();
|
||||
public IReadOnlyList<LeakState> Leaks { get; init; } = Array.Empty<LeakState>();
|
||||
public IReadOnlyList<ReactorBinding> Reactors { get; init; } = Array.Empty<ReactorBinding>();
|
||||
public IReadOnlyList<RuleEventState> RuleEvents { get; init; } = Array.Empty<RuleEventState>();
|
||||
public RobotState Robot { get; init; } = new();
|
||||
public GlobalState Global { get; init; } = new();
|
||||
public IReadOnlyList<Forecast> Forecasts { get; init; } = Array.Empty<Forecast>();
|
||||
}
|
||||
15
src/ReactorMaintenance.Simulation/Models/PropState.cs
Normal file
15
src/ReactorMaintenance.Simulation/Models/PropState.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record PropState
|
||||
{
|
||||
public EPropType Type { get; init; }
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public EPropSwitchState SwitchState { get; init; } = EPropSwitchState.Enabled;
|
||||
public EConsumerServiceState ServiceState { get; init; } = EConsumerServiceState.Unknown;
|
||||
public int JunctionMode { get; init; }
|
||||
public ERemedyType RemedyType { get; init; }
|
||||
public bool Depleted { get; init; }
|
||||
public int ReactorId { get; init; }
|
||||
|
||||
public bool IsEnabled => SwitchState == EPropSwitchState.Enabled;
|
||||
}
|
||||
12
src/ReactorMaintenance.Simulation/Models/ReactorBinding.cs
Normal file
12
src/ReactorMaintenance.Simulation/Models/ReactorBinding.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record ReactorBinding
|
||||
{
|
||||
public int ReactorId { get; init; }
|
||||
public GridPosition ControlPosition { get; init; } = new(0, 0);
|
||||
public GridPosition FuelConsumerPosition { get; init; } = new(0, 0);
|
||||
public GridPosition CoolantConsumerPosition { get; init; } = new(0, 0);
|
||||
public GridPosition ElectricityConsumerPosition { get; init; } = new(0, 0);
|
||||
public bool Ready { get; init; }
|
||||
public bool Activated { get; init; }
|
||||
}
|
||||
11
src/ReactorMaintenance.Simulation/Models/RobotState.cs
Normal file
11
src/ReactorMaintenance.Simulation/Models/RobotState.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record RobotState
|
||||
{
|
||||
public GridPosition Position { get; init; } = new(1, 1);
|
||||
public int FuelNeutralizers { get; init; }
|
||||
public int CoolantNeutralizers { get; init; }
|
||||
public int ElectricityNeutralizers { get; init; }
|
||||
public int HeatShields { get; init; }
|
||||
public int HeatImmunitySteps { get; init; }
|
||||
}
|
||||
13
src/ReactorMaintenance.Simulation/Models/RuleEffect.cs
Normal file
13
src/ReactorMaintenance.Simulation/Models/RuleEffect.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record RuleEffect
|
||||
{
|
||||
public ERuleEffectKind Kind { get; init; }
|
||||
public GridPosition Position { get; init; } = new(0, 0);
|
||||
public GridPosition? AccessPosition { get; init; }
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public ERemedyType Remedy { get; init; }
|
||||
public float Amount { get; init; }
|
||||
public EPropSwitchState PropSwitchState { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
}
|
||||
14
src/ReactorMaintenance.Simulation/Models/RuleEventState.cs
Normal file
14
src/ReactorMaintenance.Simulation/Models/RuleEventState.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record RuleEventState
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
public bool Enabled { get; init; } = true;
|
||||
public bool Repeat { get; init; }
|
||||
public bool Triggered { get; init; }
|
||||
public int Priority { get; init; }
|
||||
public ERuleEventPhase Phase { get; init; }
|
||||
public IReadOnlyList<RulePredicate> Predicates { get; init; } = Array.Empty<RulePredicate>();
|
||||
public IReadOnlyList<RuleEffect> Effects { get; init; } = Array.Empty<RuleEffect>();
|
||||
public string ForecastText { get; init; } = string.Empty;
|
||||
}
|
||||
18
src/ReactorMaintenance.Simulation/Models/RulePredicate.cs
Normal file
18
src/ReactorMaintenance.Simulation/Models/RulePredicate.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record RulePredicate
|
||||
{
|
||||
public ERulePredicateKind Kind { get; init; }
|
||||
public GridPosition Position { get; init; } = new(0, 0);
|
||||
public int ReactorId { get; init; }
|
||||
public int Turn { get; init; }
|
||||
public ELevelState LevelState { get; init; }
|
||||
public EPropSwitchState PropSwitchState { get; init; }
|
||||
public EConsumerServiceState ConsumerServiceState { get; init; }
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public ENetworkValueKind NetworkValue { get; init; }
|
||||
public ERemedyType Remedy { get; init; }
|
||||
public EBand Band { get; init; }
|
||||
public int InventoryCount { get; init; }
|
||||
public bool BoolValue { get; init; }
|
||||
}
|
||||
12
src/ReactorMaintenance.Simulation/Models/SurfaceState.cs
Normal file
12
src/ReactorMaintenance.Simulation/Models/SurfaceState.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record SurfaceState
|
||||
{
|
||||
public float Fuel { get; init; }
|
||||
public float Coolant { get; init; }
|
||||
public float Electricity { get; init; }
|
||||
public float Heat { get; init; }
|
||||
public int FuelBlockTurns { get; init; }
|
||||
public int CoolantBlockTurns { get; init; }
|
||||
public int ElectricityBlockTurns { get; init; }
|
||||
}
|
||||
11
src/ReactorMaintenance.Simulation/Models/UndergroundCell.cs
Normal file
11
src/ReactorMaintenance.Simulation/Models/UndergroundCell.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record UndergroundCell
|
||||
{
|
||||
public EUndergroundState State { get; init; }
|
||||
public float Amount { get; init; }
|
||||
public float Intensity { get; init; }
|
||||
|
||||
public bool IsPresent => State != EUndergroundState.Absent;
|
||||
public bool CarriesFlow => State is EUndergroundState.Intact or EUndergroundState.Leaking;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record ValidationIssue(string Message, GridPosition? Position = null);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record ValidationReport
|
||||
{
|
||||
public IReadOnlyList<ValidationIssue> Errors { get; init; } = Array.Empty<ValidationIssue>();
|
||||
public IReadOnlyList<ValidationIssue> Warnings { get; init; } = Array.Empty<ValidationIssue>();
|
||||
public bool IsValid => Errors.Count == 0;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
36
src/ReactorMaintenance.Simulation/RobotStateExtensions.cs
Normal file
36
src/ReactorMaintenance.Simulation/RobotStateExtensions.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class RobotStateExtensions
|
||||
{
|
||||
public static int Count(this RobotState robot, ERemedyType remedy)
|
||||
{
|
||||
return remedy switch {
|
||||
ERemedyType.FuelNeutralizer => robot.FuelNeutralizers,
|
||||
ERemedyType.CoolantNeutralizer => robot.CoolantNeutralizers,
|
||||
ERemedyType.ElectricityNeutralizer => robot.ElectricityNeutralizers,
|
||||
ERemedyType.HeatShield => robot.HeatShields,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(remedy), remedy, "Unsupported remedy.")
|
||||
};
|
||||
}
|
||||
|
||||
public static RobotState Add(this RobotState robot, ERemedyType remedy, int amount)
|
||||
{
|
||||
return remedy switch {
|
||||
ERemedyType.FuelNeutralizer => robot with { FuelNeutralizers = ClampInventory(robot.FuelNeutralizers + amount) },
|
||||
ERemedyType.CoolantNeutralizer => robot with { CoolantNeutralizers = ClampInventory(robot.CoolantNeutralizers + amount) },
|
||||
ERemedyType.ElectricityNeutralizer => robot with { ElectricityNeutralizers = ClampInventory(robot.ElectricityNeutralizers + amount) },
|
||||
ERemedyType.HeatShield => robot with { HeatShields = ClampInventory(robot.HeatShields + amount) },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(remedy), remedy, "Unsupported remedy.")
|
||||
};
|
||||
}
|
||||
|
||||
public static RobotState Spend(this RobotState robot, ERemedyType remedy)
|
||||
{
|
||||
return robot.Count(remedy) <= 0 ? robot : robot.Add(remedy, -1);
|
||||
}
|
||||
|
||||
private static int ClampInventory(int value)
|
||||
{
|
||||
return Math.Clamp(value, 0, Balancing.Current.InventoryCapacityPerRemedy);
|
||||
}
|
||||
}
|
||||
45
src/ReactorMaintenance.Simulation/SimulationBands.cs
Normal file
45
src/ReactorMaintenance.Simulation/SimulationBands.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class SimulationBands
|
||||
{
|
||||
public static EBand SurfaceBand(SurfaceState surface, ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => Fuel(surface.Fuel),
|
||||
ECarrierType.Coolant => Coolant(surface.Coolant),
|
||||
ECarrierType.Electricity => Electricity(surface.Electricity),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
public static EBand NetworkBand(UndergroundCell underground, ECarrierType carrier, ENetworkValueKind valueKind)
|
||||
{
|
||||
var value = valueKind == ENetworkValueKind.Amount ? underground.Amount : underground.Intensity;
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => Fuel(value),
|
||||
ECarrierType.Coolant => Coolant(value),
|
||||
ECarrierType.Electricity => Electricity(value),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
public static EBand Fuel(float value)
|
||||
{
|
||||
return Balancing.Current.Band(value, Balancing.Current.FuelCaution, Balancing.Current.FuelCritical);
|
||||
}
|
||||
|
||||
public static EBand Coolant(float value)
|
||||
{
|
||||
return Balancing.Current.Band(value, Balancing.Current.CoolantCaution, Balancing.Current.CoolantCritical);
|
||||
}
|
||||
|
||||
public static EBand Electricity(float value)
|
||||
{
|
||||
return Balancing.Current.Band(value, Balancing.Current.ElectricityCaution, Balancing.Current.ElectricityCritical);
|
||||
}
|
||||
|
||||
public static EBand Heat(float value)
|
||||
{
|
||||
return Balancing.Current.Band(value, Balancing.Current.HeatCaution, Balancing.Current.HeatCritical);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +1,78 @@
|
||||
using ReactorMaintenance.Simulation.Effects;
|
||||
using ReactorMaintenance.Simulation.Hazards;
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed class SimulationEngine(IEnumerable<ISimulationEffect> effects, IEnumerable<IAreaSimulationEffect> areaEffects, IEnumerable<Hazard> hazards)
|
||||
public sealed class SimulationEngine
|
||||
{
|
||||
private sealed record ForecastKey(EFailureKind Kind, GridPosition? Position);
|
||||
|
||||
public SimulationEngine()
|
||||
: this(
|
||||
[new PipeLeakEffect(), new MachineEffect(), new FireAndElectricalHazardEffect(), new CellIntegrityEffect()],
|
||||
[new SmokeSpreadEffect()],
|
||||
[new PipeBurstHazard(), new IgnitionHazard(), new MeltdownHazard(), new StabilityCollapseHazard()])
|
||||
public LevelState MoveRobot(LevelState level, GridPosition destination)
|
||||
{
|
||||
return PlayerActionSystem.MoveRobot(level, destination, SpendAction);
|
||||
}
|
||||
|
||||
public LevelState AdvanceTurn(LevelState level)
|
||||
public LevelState InteractProp(LevelState level)
|
||||
{
|
||||
return AdvanceTurn(level, true);
|
||||
return PlayerActionSystem.InteractProp(level, SpendAction);
|
||||
}
|
||||
|
||||
public IReadOnlyList<Forecast> Forecast(LevelState level)
|
||||
public LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy)
|
||||
{
|
||||
var forecasts = new List<Forecast>();
|
||||
var seen = new HashSet<ForecastKey>();
|
||||
var forecastLevel = level with { Cells = level.Cells.ToArray(), Forecasts = Array.Empty<Forecast>() };
|
||||
if (forecastLevel.Global.Lost)
|
||||
AddHazardForecasts(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
|
||||
return PlayerActionSystem.InteractLeak(level, carrier, useRemedy, SpendAction);
|
||||
}
|
||||
|
||||
AddReactorReadyForecast(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
|
||||
|
||||
if (IsReactorReady(forecastLevel) || forecastLevel.Global.Lost || forecastLevel.Global.ReactorActivated)
|
||||
return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
|
||||
|
||||
for (var step = Balancing.Current.TurnIncrement; step <= Balancing.Current.MaxForecastStepCount; step++)
|
||||
{
|
||||
forecastLevel = AdvanceTurn(forecastLevel, false);
|
||||
AddHazardForecasts(forecasts, seen, forecastLevel, step);
|
||||
AddReactorReadyForecast(forecasts, seen, forecastLevel, step);
|
||||
|
||||
if (forecastLevel.Global.Lost || IsReactorReady(forecastLevel) || forecastLevel.Global.ReactorActivated)
|
||||
break;
|
||||
}
|
||||
|
||||
return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
|
||||
public LevelState ApplyHeatShield(LevelState level)
|
||||
{
|
||||
return PlayerActionSystem.ApplyHeatShield(level, SpendAction);
|
||||
}
|
||||
|
||||
public LevelState ActivateReactor(LevelState level)
|
||||
{
|
||||
if (!IsReactorReady(level))
|
||||
return level with { Global = level.Global with { Status = "REACTOR NOT READY" } };
|
||||
return ReactorSystem.Activate(level);
|
||||
}
|
||||
|
||||
return level with {
|
||||
Global = level.Global with {
|
||||
ReactorActivated = true,
|
||||
Status = "REACTOR ONLINE"
|
||||
public LevelState EndTurn(LevelState level)
|
||||
{
|
||||
return ResolveTurn(level with { Global = level.Global with { ActionsRemaining = 0 } });
|
||||
}
|
||||
|
||||
public LevelState AdvanceTurn(LevelState level)
|
||||
{
|
||||
return ResolveTurn(level);
|
||||
}
|
||||
|
||||
public IReadOnlyList<Forecast> Forecast(LevelState level)
|
||||
{
|
||||
return ForecastSystem.Forecast(level, simulated => ResolveTurn(simulated, false));
|
||||
}
|
||||
|
||||
private LevelState SpendAction(LevelState level)
|
||||
{
|
||||
var actions = Math.Max(0, level.Global.ActionsRemaining - 1);
|
||||
var next = level with { Global = level.Global with { ActionsRemaining = actions } };
|
||||
return actions == 0 ? ResolveTurn(next) : next;
|
||||
}
|
||||
|
||||
private LevelState ResolveTurn(LevelState level, bool refreshForecasts = true)
|
||||
{
|
||||
var report = m_Validator.Validate(level);
|
||||
if (!report.IsValid)
|
||||
return level with { Global = level.Global with { LevelState = ELevelState.Lost, Status = report.Errors[0].Message } };
|
||||
|
||||
var next = RuleEventSystem.Apply(level, ERuleEventPhase.StartOfSimulation);
|
||||
next = NetworkPropagationSystem.Propagate(next);
|
||||
next = ConsumerSystem.Resolve(next);
|
||||
next = LeakSystem.Inject(next);
|
||||
next = SurfaceInteractionSystem.Resolve(next);
|
||||
next = RobotSafetySystem.Resolve(next);
|
||||
next = ReactorSystem.DeriveState(next);
|
||||
next = RuleEventSystem.Apply(next, ERuleEventPhase.EndOfTurn);
|
||||
next = SurfaceInteractionSystem.AdvanceDurations(next);
|
||||
next = next with {
|
||||
Global = next.Global with {
|
||||
Turn = next.Global.Turn + 1,
|
||||
ActionsRemaining = Balancing.Current.ActionsPerTurn
|
||||
}
|
||||
};
|
||||
|
||||
return refreshForecasts ? next with { Forecasts = Forecast(next) } : next;
|
||||
}
|
||||
|
||||
private LevelState AdvanceTurn(LevelState level, bool updateForecasts)
|
||||
{
|
||||
var cells = level.Cells.ToArray();
|
||||
|
||||
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
|
||||
{
|
||||
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var index = level.Index(position);
|
||||
var cell = cells[index];
|
||||
|
||||
if (!cell.IsWalkable)
|
||||
continue;
|
||||
|
||||
foreach (var effect in m_Effects)
|
||||
cell = effect.Apply(cell);
|
||||
|
||||
cells[index] = cell with { Hazards = cell.Hazards.Clamp() };
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var areaEffect in m_AreaEffects)
|
||||
cells = areaEffect.Apply(level, cells);
|
||||
|
||||
var global = UpdateGlobal(level, cells);
|
||||
var next = level with {
|
||||
Cells = cells,
|
||||
Global = global with { Turn = level.Global.Turn + Balancing.Current.TurnIncrement }
|
||||
};
|
||||
|
||||
return updateForecasts ? next with { Forecasts = Forecast(next) } : next;
|
||||
}
|
||||
|
||||
private void AddHazardForecasts(List<Forecast> forecasts, HashSet<ForecastKey> seen, LevelState level, int turns)
|
||||
{
|
||||
foreach (var hazard in m_Hazards)
|
||||
{
|
||||
foreach (var forecast in hazard.Predict(level, turns))
|
||||
AddForecast(forecasts, seen, forecast);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddReactorReadyForecast(List<Forecast> forecasts, HashSet<ForecastKey> seen, LevelState level, int turns)
|
||||
{
|
||||
if (IsReactorReady(level))
|
||||
AddForecast(forecasts, seen, new(EFailureKind.ReactorReady, null, turns, "REACTOR READY"));
|
||||
}
|
||||
|
||||
private static void AddForecast(List<Forecast> forecasts, HashSet<ForecastKey> seen, Forecast forecast)
|
||||
{
|
||||
if (seen.Add(new(forecast.Kind, forecast.Position)))
|
||||
forecasts.Add(forecast);
|
||||
}
|
||||
|
||||
private static GlobalState UpdateGlobal(LevelState level, CellState[] cells)
|
||||
{
|
||||
var reactorHeat = cells.Where(c => c.Prop == ECellProp.Reactor).Select(c => c.Hazards.Heat).DefaultIfEmpty(level.Global.CoreHeat).Max();
|
||||
var poweredGenerators = cells.Count(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
|
||||
var poweredPumps = cells.Count(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
|
||||
var damagedCriticalCells = cells.Count(c => c.Prop is ECellProp.Reactor or ECellProp.Generator or ECellProp.CoolingPump && c.Hazards.Stability <= Balancing.Current.CriticalCellStabilityThreshold);
|
||||
var stability = Rules.Clamp(level.Global.FacilityStability - damagedCriticalCells);
|
||||
var lost = reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold || stability <= Balancing.Current.StabilityCollapseThreshold;
|
||||
var status = lost ? reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold ? "CORE MELTDOWN" : "FACILITY STABILITY COLLAPSE" : "STABILIZE SYSTEMS";
|
||||
var global = level.Global with {
|
||||
CoreHeat = Rules.Clamp(reactorHeat - poweredPumps),
|
||||
Power = Rules.Clamp(poweredGenerators * Balancing.Current.GeneratorPowerOutput),
|
||||
Cooling = Rules.Clamp(poweredPumps * Balancing.Current.CoolingPumpOutput),
|
||||
FacilityStability = stability,
|
||||
Lost = lost,
|
||||
Status = status
|
||||
};
|
||||
|
||||
return IsReactorReady(level with { Cells = cells, Global = global }) ? global with { Status = "REACTOR READY" } : global;
|
||||
}
|
||||
|
||||
private static bool IsReactorReady(LevelState level)
|
||||
{
|
||||
var hasReactor = level.Cells.Any(c => c.Prop == ECellProp.Reactor);
|
||||
var hasStablePower = level.Global.Power >= Balancing.Current.ReactorReadyPowerThreshold || level.Cells.Any(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
|
||||
var hasCooling = level.Global.Cooling >= Balancing.Current.ReactorReadyCoolingThreshold || level.Cells.Any(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
|
||||
var reactorStable = level.Global.CoreHeat < Balancing.Current.ReactorReadyCoreHeatThreshold;
|
||||
return hasReactor && hasStablePower && hasCooling && reactorStable && !level.Global.Lost;
|
||||
}
|
||||
|
||||
private readonly IReadOnlyList<IAreaSimulationEffect> m_AreaEffects = areaEffects.ToArray();
|
||||
private readonly IReadOnlyList<ISimulationEffect> m_Effects = effects.ToArray();
|
||||
private readonly IReadOnlyList<Hazard> m_Hazards = hazards.ToArray();
|
||||
private readonly LevelValidator m_Validator = new();
|
||||
}
|
||||
24
src/ReactorMaintenance.Simulation/SurfaceCarrierMath.cs
Normal file
24
src/ReactorMaintenance.Simulation/SurfaceCarrierMath.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class SurfaceCarrierMath
|
||||
{
|
||||
public static SurfaceState AddCarrier(SurfaceState surface, ECarrierType carrier, float amount)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => surface with { Fuel = surface.Fuel + amount },
|
||||
ECarrierType.Coolant => surface with { Coolant = surface.Coolant + amount },
|
||||
ECarrierType.Electricity => surface with { Electricity = surface.Electricity + amount },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
public static SurfaceState RemoveCarrier(SurfaceState surface, ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => surface with { Fuel = 0, FuelBlockTurns = Balancing.Current.RemedyBlockTurns },
|
||||
ECarrierType.Coolant => surface with { Coolant = 0, CoolantBlockTurns = Balancing.Current.RemedyBlockTurns },
|
||||
ECarrierType.Electricity => surface with { Electricity = 0, ElectricityBlockTurns = Balancing.Current.RemedyBlockTurns },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record SurfaceInteractionEffect
|
||||
{
|
||||
public static SurfaceInteractionEffect Hold { get; } = new();
|
||||
|
||||
public ESurfaceInteractionVerb Verb { get; init; }
|
||||
public ESurfaceQuantity Quantity { get; init; }
|
||||
public float Amount { get; init; }
|
||||
public float SecondaryAmount { get; init; }
|
||||
}
|
||||
28
src/ReactorMaintenance.Simulation/SurfaceStateExtensions.cs
Normal file
28
src/ReactorMaintenance.Simulation/SurfaceStateExtensions.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class SurfaceStateExtensions
|
||||
{
|
||||
public static SurfaceState Clamp(this SurfaceState surface)
|
||||
{
|
||||
var balancing = Balancing.Current;
|
||||
return surface with {
|
||||
Fuel = balancing.ClampValue(surface.Fuel),
|
||||
Coolant = balancing.ClampValue(surface.Coolant),
|
||||
Electricity = balancing.ClampValue(surface.Electricity),
|
||||
Heat = balancing.ClampValue(surface.Heat),
|
||||
FuelBlockTurns = Math.Max(0, surface.FuelBlockTurns),
|
||||
CoolantBlockTurns = Math.Max(0, surface.CoolantBlockTurns),
|
||||
ElectricityBlockTurns = Math.Max(0, surface.ElectricityBlockTurns)
|
||||
};
|
||||
}
|
||||
|
||||
public static bool Blocks(this SurfaceState surface, ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => surface.FuelBlockTurns > 0,
|
||||
ECarrierType.Coolant => surface.CoolantBlockTurns > 0,
|
||||
ECarrierType.Electricity => surface.ElectricityBlockTurns > 0,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
}
|
||||
28
src/ReactorMaintenance.Simulation/Systems/ConsumerSystem.cs
Normal file
28
src/ReactorMaintenance.Simulation/Systems/ConsumerSystem.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class ConsumerSystem
|
||||
{
|
||||
public static LevelState Resolve(LevelState level)
|
||||
{
|
||||
var props = level.Props.ToArray();
|
||||
foreach (var position in LevelTraversal.AllPositions(level))
|
||||
{
|
||||
var index = level.Index(position);
|
||||
var prop = props[index];
|
||||
if (prop.Type != EPropType.Consumer)
|
||||
continue;
|
||||
|
||||
if (prop.SwitchState == EPropSwitchState.Disabled)
|
||||
{
|
||||
props[index] = prop with { ServiceState = EConsumerServiceState.Disabled };
|
||||
continue;
|
||||
}
|
||||
|
||||
var underground = level.GetUnderground(position, prop.Carrier);
|
||||
var supplied = underground.Amount >= Balancing.Current.ConsumerRequiredAmount && underground.Intensity >= Balancing.Current.ConsumerRequiredIntensity;
|
||||
props[index] = prop with { ServiceState = supplied ? EConsumerServiceState.Producing : EConsumerServiceState.Starved };
|
||||
}
|
||||
|
||||
return level with { Props = props };
|
||||
}
|
||||
}
|
||||
58
src/ReactorMaintenance.Simulation/Systems/ForecastSystem.cs
Normal file
58
src/ReactorMaintenance.Simulation/Systems/ForecastSystem.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class ForecastSystem
|
||||
{
|
||||
public static IReadOnlyList<Forecast> Forecast(LevelState level, Func<LevelState, LevelState> resolveTurn)
|
||||
{
|
||||
var forecasts = new List<Forecast>();
|
||||
var simulated = CopyForForecast(level);
|
||||
|
||||
for (var turn = 0; turn <= Balancing.Current.ForecastHorizon; turn++)
|
||||
{
|
||||
AddForecasts(forecasts, simulated, turn);
|
||||
if (simulated.Global.LevelState is ELevelState.Lost or ELevelState.Ready or ELevelState.Won)
|
||||
break;
|
||||
|
||||
if (turn < Balancing.Current.ForecastHorizon)
|
||||
simulated = resolveTurn(simulated);
|
||||
}
|
||||
|
||||
return forecasts.DistinctBy(forecast => (forecast.Kind, forecast.Position, forecast.Message)).OrderBy(forecast => forecast.Turns).ThenBy(forecast => forecast.Message).ToArray();
|
||||
}
|
||||
|
||||
private static LevelState CopyForForecast(LevelState level)
|
||||
{
|
||||
return level with {
|
||||
Terrain = level.Terrain.ToArray(),
|
||||
Fuel = level.Fuel.ToArray(),
|
||||
Coolant = level.Coolant.ToArray(),
|
||||
Electricity = level.Electricity.ToArray(),
|
||||
Surface = level.Surface.ToArray(),
|
||||
Props = level.Props.ToArray(),
|
||||
Forecasts = Array.Empty<Forecast>()
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddForecasts(List<Forecast> forecasts, LevelState level, int turn)
|
||||
{
|
||||
if (level.Global.LevelState == ELevelState.Lost)
|
||||
forecasts.Add(new(EForecastKind.TerminalLoss, level.Robot.Position, turn, level.Global.Status));
|
||||
|
||||
if (level.Global.LevelState == ELevelState.Ready)
|
||||
forecasts.Add(new(EForecastKind.ReactorReady, null, turn, "REACTOR READY"));
|
||||
|
||||
foreach (var position in LevelTraversal.AllPositions(level))
|
||||
{
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type == EPropType.Consumer && prop.ServiceState == EConsumerServiceState.Starved)
|
||||
forecasts.Add(new(EForecastKind.ConsumerStarved, position, turn, $"{prop.Carrier} consumer starved"));
|
||||
|
||||
var surface = level.GetSurface(position);
|
||||
if (SimulationBands.Fuel(surface.Fuel) == EBand.Critical || SimulationBands.Coolant(surface.Coolant) == EBand.Critical || SimulationBands.Electricity(surface.Electricity) == EBand.Critical || SimulationBands.Heat(surface.Heat) == EBand.Critical)
|
||||
forecasts.Add(new(EForecastKind.HazardGrowth, position, turn, "Critical hazard"));
|
||||
}
|
||||
|
||||
foreach (var ruleEvent in level.RuleEvents.Where(ruleEvent => ruleEvent.Enabled && !string.IsNullOrWhiteSpace(ruleEvent.ForecastText) && ruleEvent.Predicates.All(predicate => RuleEventSystem.PredicateMatches(level, predicate))))
|
||||
forecasts.Add(new(EForecastKind.RuleEvent, null, turn, ruleEvent.ForecastText));
|
||||
}
|
||||
}
|
||||
24
src/ReactorMaintenance.Simulation/Systems/LeakSystem.cs
Normal file
24
src/ReactorMaintenance.Simulation/Systems/LeakSystem.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class LeakSystem
|
||||
{
|
||||
public static LevelState Inject(LevelState level)
|
||||
{
|
||||
var surface = level.Surface.ToArray();
|
||||
foreach (var leak in level.Leaks.Where(leak => !leak.Repaired))
|
||||
{
|
||||
var underground = level.GetUnderground(leak.UndergroundPosition, leak.Carrier);
|
||||
if (underground.State != EUndergroundState.Leaking)
|
||||
continue;
|
||||
|
||||
var accessIndex = level.Index(leak.AccessPosition);
|
||||
if (surface[accessIndex].Blocks(leak.Carrier))
|
||||
continue;
|
||||
|
||||
var amount = Balancing.Current.LeakBaseAmount + (underground.Amount * Balancing.Current.LeakAmountScale) + (underground.Intensity * Balancing.Current.LeakIntensityScale);
|
||||
surface[accessIndex] = SurfaceCarrierMath.AddCarrier(surface[accessIndex], leak.Carrier, amount);
|
||||
}
|
||||
|
||||
return level with { Surface = surface.Select(cell => cell.Clamp()).ToArray() };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class NetworkPropagationSystem
|
||||
{
|
||||
public static LevelState Propagate(LevelState level)
|
||||
{
|
||||
var fuel = ClearTransient(level.Fuel);
|
||||
var coolant = ClearTransient(level.Coolant);
|
||||
var electricity = ClearTransient(level.Electricity);
|
||||
var next = level.WithRuntimeArrays(fuel, coolant, electricity, level.Surface.ToArray(), level.Props.ToArray());
|
||||
|
||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||
next = PropagateCarrier(next, carrier);
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
private static UndergroundCell[] ClearTransient(IReadOnlyList<UndergroundCell> layer)
|
||||
{
|
||||
return layer.Select(cell => cell with { Amount = 0, Intensity = 0 }).ToArray();
|
||||
}
|
||||
|
||||
private static LevelState PropagateCarrier(LevelState level, ECarrierType carrier)
|
||||
{
|
||||
var layer = level.Layer(carrier).ToArray();
|
||||
var sources = LevelTraversal.AllPositions(level).Where(position => level.GetProp(position) is { Type: EPropType.Flow, SwitchState: EPropSwitchState.Enabled, Carrier: var sourceCarrier } && sourceCarrier == carrier && level.GetUnderground(position, carrier).CarriesFlow).ToArray();
|
||||
var junctions = JunctionFlowAnalyzer.Analyze(level).Where(junction => junction.IsValid && junction.Carrier == carrier).ToDictionary(junction => junction.Position);
|
||||
|
||||
foreach (var source in sources)
|
||||
ApplySourceFlow(level, layer, source, carrier, junctions);
|
||||
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => level with { Fuel = layer },
|
||||
ECarrierType.Coolant => level with { Coolant = layer },
|
||||
ECarrierType.Electricity => level with { Electricity = layer },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
private static void ApplySourceFlow(LevelState level, UndergroundCell[] layer, GridPosition source, ECarrierType carrier, IReadOnlyDictionary<GridPosition, JunctionFlow> junctions)
|
||||
{
|
||||
var open = new Queue<(GridPosition Position, int Distance, float AmountFactor, float IntensityFactor)>();
|
||||
var best = new Dictionary<GridPosition, float>();
|
||||
open.Enqueue((source, 0, 1, 1));
|
||||
best[source] = 1;
|
||||
|
||||
while (open.Count > 0)
|
||||
{
|
||||
var current = open.Dequeue();
|
||||
var amount = Balancing.Current.ClampValue((Balancing.Current.SourceAmount * current.AmountFactor) - (current.Distance * Balancing.Current.DistanceAmountFalloff));
|
||||
var intensity = Balancing.Current.ClampValue((Balancing.Current.SourceIntensity * current.IntensityFactor) - (current.Distance * Balancing.Current.DistanceIntensityFalloff));
|
||||
var index = level.Index(current.Position);
|
||||
layer[index] = layer[index] with {
|
||||
Amount = Math.Max(layer[index].Amount, amount),
|
||||
Intensity = Math.Max(layer[index].Intensity, intensity)
|
||||
};
|
||||
|
||||
foreach (var next in current.Position.Neighbors().Where(level.InBounds))
|
||||
{
|
||||
if (!level.GetUnderground(next, carrier).CarriesFlow)
|
||||
continue;
|
||||
|
||||
var weights = BranchWeights(current.Position, next, junctions);
|
||||
var amountFactor = current.AmountFactor * weights.Amount;
|
||||
var intensityFactor = current.IntensityFactor * weights.Intensity;
|
||||
if (amountFactor <= 0 || intensityFactor <= 0)
|
||||
continue;
|
||||
|
||||
if (best.TryGetValue(next, out var oldBest) && oldBest >= amountFactor)
|
||||
continue;
|
||||
|
||||
best[next] = amountFactor;
|
||||
open.Enqueue((next, current.Distance + 1, amountFactor, intensityFactor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static (float Amount, float Intensity) BranchWeights(GridPosition from, GridPosition to, IReadOnlyDictionary<GridPosition, JunctionFlow> junctions)
|
||||
{
|
||||
if (!junctions.TryGetValue(from, out var junction))
|
||||
return (1, 1);
|
||||
|
||||
var weight = junction.WeightFor(to);
|
||||
return (weight, weight);
|
||||
}
|
||||
}
|
||||
133
src/ReactorMaintenance.Simulation/Systems/PlayerActionSystem.cs
Normal file
133
src/ReactorMaintenance.Simulation/Systems/PlayerActionSystem.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class PlayerActionSystem
|
||||
{
|
||||
public static LevelState MoveRobot(LevelState level, GridPosition destination, Func<LevelState, LevelState> spendAction)
|
||||
{
|
||||
if (!CanSpendAction(level) || !level.IsFloor(destination) || level.Robot.Position.ManhattanDistance(destination) != 1)
|
||||
return Refuse(level, "MOVE BLOCKED");
|
||||
|
||||
return spendAction(level with {
|
||||
Robot = level.Robot with {
|
||||
Position = destination,
|
||||
HeatImmunitySteps = Math.Max(0, level.Robot.HeatImmunitySteps - 1)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static LevelState InteractProp(LevelState level, Func<LevelState, LevelState> spendAction)
|
||||
{
|
||||
if (!CanSpendAction(level))
|
||||
return Refuse(level, "NO ACTIONS");
|
||||
|
||||
var position = level.Robot.Position;
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type == EPropType.None)
|
||||
return Refuse(level, "NO PROP");
|
||||
|
||||
var next = prop.Type switch {
|
||||
EPropType.Flow or EPropType.Consumer => ToggleProp(level, position, prop),
|
||||
EPropType.Junction => CycleJunctionMode(level, position, prop),
|
||||
EPropType.Door => ToggleDoor(level, position),
|
||||
EPropType.AllSeeingEyeTerminal => level with { Global = level.Global with { AllSeeingEyeUnlocked = true, Status = "ALL-SEEING-EYE ONLINE" } },
|
||||
EPropType.RemedySupply => PickUpRemedy(level, position, prop),
|
||||
EPropType.ReactorControl => ReactorSystem.Activate(level),
|
||||
_ => level
|
||||
};
|
||||
|
||||
return spendAction(next);
|
||||
}
|
||||
|
||||
public static LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy, Func<LevelState, LevelState> spendAction)
|
||||
{
|
||||
if (!CanSpendAction(level))
|
||||
return Refuse(level, "NO ACTIONS");
|
||||
|
||||
var leakIndex = level.Leaks.ToList().FindIndex(leak => !leak.Repaired && leak.Carrier == carrier && leak.AccessPosition == level.Robot.Position);
|
||||
if (leakIndex < 0)
|
||||
return Refuse(level, "NO REACHABLE LEAK");
|
||||
|
||||
var leak = level.Leaks[leakIndex];
|
||||
var next = useRemedy ? ApplyElementRemedy(level, leak) : RepairLeak(level, leakIndex, leak);
|
||||
return spendAction(next);
|
||||
}
|
||||
|
||||
public static LevelState ApplyHeatShield(LevelState level, Func<LevelState, LevelState> spendAction)
|
||||
{
|
||||
if (!CanSpendAction(level) || level.Robot.HeatShields <= 0)
|
||||
return Refuse(level, "NO HEAT SHIELD");
|
||||
|
||||
return spendAction(level with {
|
||||
Robot = level.Robot.Spend(ERemedyType.HeatShield) with { HeatImmunitySteps = Balancing.Current.HeatShieldSteps }
|
||||
});
|
||||
}
|
||||
|
||||
private static LevelState ToggleProp(LevelState level, GridPosition position, PropState prop)
|
||||
{
|
||||
var switchState = prop.SwitchState == EPropSwitchState.Enabled ? EPropSwitchState.Disabled : EPropSwitchState.Enabled;
|
||||
return level.SetProp(position, prop with { SwitchState = switchState });
|
||||
}
|
||||
|
||||
private static LevelState ToggleDoor(LevelState level, GridPosition position)
|
||||
{
|
||||
var doors = level.Doors.ToArray();
|
||||
var index = Array.FindIndex(doors, door => door.A == position || door.B == position);
|
||||
if (index < 0)
|
||||
return level;
|
||||
|
||||
doors[index] = doors[index] with { State = doors[index].State == EDoorState.Open ? EDoorState.Closed : EDoorState.Open };
|
||||
return level with { Doors = doors };
|
||||
}
|
||||
|
||||
private static LevelState PickUpRemedy(LevelState level, GridPosition position, PropState prop)
|
||||
{
|
||||
if (prop.Depleted || level.Robot.Count(prop.RemedyType) >= Balancing.Current.InventoryCapacityPerRemedy)
|
||||
return level;
|
||||
|
||||
return level.SetProp(position, prop with { Depleted = true }) with { Robot = level.Robot.Add(prop.RemedyType, 1) };
|
||||
}
|
||||
|
||||
private static LevelState RepairLeak(LevelState level, int leakIndex, LeakState leak)
|
||||
{
|
||||
var leaks = level.Leaks.ToArray();
|
||||
leaks[leakIndex] = leak with { Repaired = true };
|
||||
return level.SetUnderground(leak.UndergroundPosition, leak.Carrier, level.GetUnderground(leak.UndergroundPosition, leak.Carrier) with { State = EUndergroundState.Intact }) with { Leaks = leaks };
|
||||
}
|
||||
|
||||
private static LevelState ApplyElementRemedy(LevelState level, LeakState leak)
|
||||
{
|
||||
var remedy = leak.Carrier switch {
|
||||
ECarrierType.Fuel => ERemedyType.FuelNeutralizer,
|
||||
ECarrierType.Coolant => ERemedyType.CoolantNeutralizer,
|
||||
ECarrierType.Electricity => ERemedyType.ElectricityNeutralizer,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(leak), leak.Carrier, "Unsupported leak carrier.")
|
||||
};
|
||||
|
||||
if (level.Robot.Count(remedy) <= 0)
|
||||
return Refuse(level, "NO REMEDY");
|
||||
|
||||
var surface = SurfaceCarrierMath.RemoveCarrier(level.GetSurface(leak.AccessPosition), leak.Carrier);
|
||||
return level.SetSurface(leak.AccessPosition, surface) with { Robot = level.Robot.Spend(remedy) };
|
||||
}
|
||||
|
||||
private static LevelState CycleJunctionMode(LevelState level, GridPosition position, PropState prop)
|
||||
{
|
||||
var flow = JunctionFlowAnalyzer.Analyze(level).FirstOrDefault(junction => junction.Position == position);
|
||||
var outflowCount = flow?.OutgoingBranches.Count ?? 2;
|
||||
var ratios = Balancing.Current.JunctionRatios(outflowCount);
|
||||
if (ratios.Count == 0)
|
||||
return level;
|
||||
|
||||
return level.SetProp(position, prop with { JunctionMode = (prop.JunctionMode + 1) % ratios.Count });
|
||||
}
|
||||
|
||||
private static bool CanSpendAction(LevelState level)
|
||||
{
|
||||
return level.Global.LevelState is not (ELevelState.Lost or ELevelState.Won) && level.Global.ActionsRemaining > 0;
|
||||
}
|
||||
|
||||
private static LevelState Refuse(LevelState level, string message)
|
||||
{
|
||||
return level with { Global = level.Global with { Status = message } };
|
||||
}
|
||||
}
|
||||
78
src/ReactorMaintenance.Simulation/Systems/ReactorSystem.cs
Normal file
78
src/ReactorMaintenance.Simulation/Systems/ReactorSystem.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class ReactorSystem
|
||||
{
|
||||
public static LevelState Activate(LevelState level)
|
||||
{
|
||||
var reactorIndex = level.Reactors.ToList().FindIndex(reactor => reactor.ControlPosition == level.Robot.Position);
|
||||
if (reactorIndex < 0)
|
||||
return Refuse(level, "NO REACTOR CONTROL");
|
||||
|
||||
var reactor = level.Reactors[reactorIndex];
|
||||
if (!reactor.Ready)
|
||||
return Refuse(level, "REACTOR NOT READY");
|
||||
|
||||
var reactors = level.Reactors.ToArray();
|
||||
reactors[reactorIndex] = reactor with { Activated = true };
|
||||
return level with {
|
||||
Reactors = reactors,
|
||||
Global = level.Global with { LevelState = ELevelState.Won, Status = "REACTOR ONLINE" }
|
||||
};
|
||||
}
|
||||
|
||||
public static LevelState DeriveState(LevelState level)
|
||||
{
|
||||
if (level.Global.LevelState is ELevelState.Lost or ELevelState.Won)
|
||||
return level;
|
||||
|
||||
var reactors = level.Reactors.Select(reactor => reactor with { Ready = IsReady(level, reactor) }).ToArray();
|
||||
if (reactors.Any(reactor => reactor.Ready))
|
||||
return level with { Reactors = reactors, Global = level.Global with { LevelState = ELevelState.Ready, Status = "REACTOR READY" } };
|
||||
|
||||
var maxHeat = level.Surface.Where((_, index) => level.Terrain[index] == ECellTerrain.Floor).Select(surface => surface.Heat).DefaultIfEmpty(0).Max();
|
||||
if (maxHeat >= Balancing.Current.TerminalHeat)
|
||||
return level with { Reactors = reactors, Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "REACTOR HEAT TERMINAL" } };
|
||||
|
||||
var hasCritical = level.Surface.Any(surface => SimulationBands.Fuel(surface.Fuel) == EBand.Critical || SimulationBands.Coolant(surface.Coolant) == EBand.Critical || SimulationBands.Electricity(surface.Electricity) == EBand.Critical || SimulationBands.Heat(surface.Heat) == EBand.Critical);
|
||||
var hasCaution = hasCritical || level.Props.Any(prop => prop.ServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled) || level.Leaks.Any(leak => !leak.Repaired);
|
||||
var state = hasCritical ? ELevelState.Critical :
|
||||
hasCaution ? ELevelState.Caution : ELevelState.Stable;
|
||||
return level with { Reactors = reactors, Global = level.Global with { LevelState = state, Status = state.ToString().ToUpperInvariant() } };
|
||||
}
|
||||
|
||||
public static bool MatchesReady(LevelState level, RulePredicate predicate)
|
||||
{
|
||||
return level.Reactors.Any(reactor => MatchesId(reactor, predicate.ReactorId) && reactor.Ready) == predicate.BoolValue;
|
||||
}
|
||||
|
||||
public static bool MatchesWon(LevelState level, RulePredicate predicate)
|
||||
{
|
||||
var won = predicate.ReactorId > 0
|
||||
? level.Reactors.Any(reactor => reactor.ReactorId == predicate.ReactorId && reactor.Activated)
|
||||
: level.Global.LevelState == ELevelState.Won || level.Reactors.Any(reactor => reactor.Activated);
|
||||
return won == predicate.BoolValue;
|
||||
}
|
||||
|
||||
private static bool IsReady(LevelState level, ReactorBinding reactor)
|
||||
{
|
||||
return HasProducingConsumer(level, reactor.FuelConsumerPosition, ECarrierType.Fuel)
|
||||
&& HasProducingConsumer(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant)
|
||||
&& HasProducingConsumer(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity)
|
||||
&& level.GetSurface(reactor.ControlPosition).Heat < Balancing.Current.TerminalHeat;
|
||||
}
|
||||
|
||||
private static bool HasProducingConsumer(LevelState level, GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
return level.InBounds(position) && level.GetProp(position) is { Type: EPropType.Consumer, Carrier: var consumerCarrier, ServiceState: EConsumerServiceState.Producing } && consumerCarrier == carrier;
|
||||
}
|
||||
|
||||
private static bool MatchesId(ReactorBinding reactor, int reactorId)
|
||||
{
|
||||
return reactorId <= 0 || reactor.ReactorId == reactorId;
|
||||
}
|
||||
|
||||
private static LevelState Refuse(LevelState level, string message)
|
||||
{
|
||||
return level with { Global = level.Global with { Status = message } };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class RobotSafetySystem
|
||||
{
|
||||
public static LevelState Resolve(LevelState level)
|
||||
{
|
||||
var surface = level.GetSurface(level.Robot.Position);
|
||||
var unsafeElement = surface.Fuel >= Balancing.Current.RobotFuelSafetyThreshold || surface.Coolant >= Balancing.Current.RobotCoolantSafetyThreshold || surface.Electricity >= Balancing.Current.RobotElectricitySafetyThreshold;
|
||||
var unsafeHeat = surface.Heat >= Balancing.Current.RobotHeatSafetyThreshold && level.Robot.HeatImmunitySteps <= 0;
|
||||
return unsafeElement || unsafeHeat
|
||||
? level with { Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "ROBOT LOST" } }
|
||||
: level;
|
||||
}
|
||||
}
|
||||
73
src/ReactorMaintenance.Simulation/Systems/RuleEventSystem.cs
Normal file
73
src/ReactorMaintenance.Simulation/Systems/RuleEventSystem.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class RuleEventSystem
|
||||
{
|
||||
public static LevelState Apply(LevelState level, ERuleEventPhase phase)
|
||||
{
|
||||
var next = level;
|
||||
var events = level.RuleEvents.Select((ruleEvent, index) => (Event: ruleEvent, Index: index)).Where(item => item.Event.Enabled && item.Event.Phase == phase && (item.Event.Repeat || !item.Event.Triggered)).OrderBy(item => item.Event.Priority).ToArray();
|
||||
var ruleEvents = next.RuleEvents.ToArray();
|
||||
|
||||
foreach (var item in events)
|
||||
{
|
||||
if (!item.Event.Predicates.All(predicate => PredicateMatches(next, predicate)))
|
||||
continue;
|
||||
|
||||
foreach (var effect in item.Event.Effects)
|
||||
next = ApplyEffect(next, effect);
|
||||
|
||||
ruleEvents[item.Index] = item.Event with { Triggered = true };
|
||||
}
|
||||
|
||||
return next with { RuleEvents = ruleEvents };
|
||||
}
|
||||
|
||||
public static bool PredicateMatches(LevelState level, RulePredicate predicate)
|
||||
{
|
||||
return predicate.Kind switch {
|
||||
ERulePredicateKind.TurnAtLeast => level.Global.Turn >= predicate.Turn,
|
||||
ERulePredicateKind.LevelStateIs => level.Global.LevelState == predicate.LevelState,
|
||||
ERulePredicateKind.ReactorReadyIs => ReactorSystem.MatchesReady(level, predicate),
|
||||
ERulePredicateKind.ReactorLostIs => level.Global.LevelState == ELevelState.Lost == predicate.BoolValue,
|
||||
ERulePredicateKind.ReactorWonIs => ReactorSystem.MatchesWon(level, predicate),
|
||||
ERulePredicateKind.PropStateAt => level.InBounds(predicate.Position) && level.GetProp(predicate.Position).SwitchState == predicate.PropSwitchState,
|
||||
ERulePredicateKind.ConsumerStateAt => level.InBounds(predicate.Position) && level.GetProp(predicate.Position).ServiceState == predicate.ConsumerServiceState,
|
||||
ERulePredicateKind.NetworkBandAt => level.InBounds(predicate.Position) && SimulationBands.NetworkBand(level.GetUnderground(predicate.Position, predicate.Carrier), predicate.Carrier, predicate.NetworkValue) >= predicate.Band,
|
||||
ERulePredicateKind.SurfaceBandAt => level.InBounds(predicate.Position) && SimulationBands.SurfaceBand(level.GetSurface(predicate.Position), predicate.Carrier) >= predicate.Band,
|
||||
ERulePredicateKind.RobotAt => level.Robot.Position == predicate.Position,
|
||||
ERulePredicateKind.RobotInventoryAtLeast => level.Robot.Count(predicate.Remedy) >= predicate.InventoryCount,
|
||||
ERulePredicateKind.AllSeeingEyeUnlocked => level.Global.AllSeeingEyeUnlocked == predicate.BoolValue,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static LevelState ApplyEffect(LevelState level, RuleEffect effect)
|
||||
{
|
||||
return effect.Kind switch {
|
||||
ERuleEffectKind.StartLeak => StartLeak(level, effect),
|
||||
ERuleEffectKind.WorsenLeak => level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Leaking }),
|
||||
ERuleEffectKind.RepairNetworkCell => level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Intact }),
|
||||
ERuleEffectKind.DisableNetworkCell => level.SetUnderground(effect.Position, effect.Carrier, new()),
|
||||
ERuleEffectKind.SetPropEnabled => level.SetProp(effect.Position, level.GetProp(effect.Position) with { SwitchState = effect.PropSwitchState }),
|
||||
ERuleEffectKind.AddSurfaceHazard => level.SetSurface(effect.Position, SurfaceCarrierMath.AddCarrier(level.GetSurface(effect.Position), effect.Carrier, effect.Amount)),
|
||||
ERuleEffectKind.RemoveSurfaceHazard => level.SetSurface(effect.Position, SurfaceCarrierMath.AddCarrier(level.GetSurface(effect.Position), effect.Carrier, -effect.Amount)),
|
||||
ERuleEffectKind.AddHeat => level.SetSurface(effect.Position, level.GetSurface(effect.Position) with { Heat = level.GetSurface(effect.Position).Heat + effect.Amount }),
|
||||
ERuleEffectKind.RemoveHeat => level.SetSurface(effect.Position, level.GetSurface(effect.Position) with { Heat = level.GetSurface(effect.Position).Heat - effect.Amount }),
|
||||
ERuleEffectKind.AddInventory => level with { Robot = level.Robot.Add(effect.Remedy, (int)effect.Amount) },
|
||||
ERuleEffectKind.RemoveInventory => level with { Robot = level.Robot.Add(effect.Remedy, -(int)effect.Amount) },
|
||||
ERuleEffectKind.MarkTerminalLoss => level with { Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = string.IsNullOrWhiteSpace(effect.Message) ? "TERMINAL FAILURE" : effect.Message } },
|
||||
ERuleEffectKind.EmitWarning => level with { Global = level.Global with { Warnings = [.. level.Global.Warnings, effect.Message] } },
|
||||
_ => level
|
||||
};
|
||||
}
|
||||
|
||||
private static LevelState StartLeak(LevelState level, RuleEffect effect)
|
||||
{
|
||||
var leak = new LeakState {
|
||||
Carrier = effect.Carrier,
|
||||
UndergroundPosition = effect.Position,
|
||||
AccessPosition = effect.AccessPosition ?? effect.Position
|
||||
};
|
||||
return level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Leaking }) with { Leaks = [.. level.Leaks, leak] };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class SurfaceInteractionSystem
|
||||
{
|
||||
private sealed class SurfaceDelta
|
||||
{
|
||||
public SurfaceState Apply(SurfaceState surface)
|
||||
{
|
||||
return surface with {
|
||||
Fuel = surface.Fuel + Fuel,
|
||||
Coolant = surface.Coolant + Coolant,
|
||||
Electricity = surface.Electricity + Electricity,
|
||||
Heat = surface.Heat + Heat
|
||||
};
|
||||
}
|
||||
|
||||
public float Fuel { get; set; }
|
||||
public float Coolant { get; set; }
|
||||
public float Electricity { get; set; }
|
||||
public float Heat { get; set; }
|
||||
}
|
||||
|
||||
public static LevelState Resolve(LevelState level)
|
||||
{
|
||||
var deltas = Enumerable.Range(0, level.Width * level.Height).Select(_ => new SurfaceDelta()).ToArray();
|
||||
foreach (var position in LevelTraversal.AllPositions(level).Where(level.IsFloor))
|
||||
ApplySameCellInteractions(level, position, deltas);
|
||||
|
||||
foreach (var position in LevelTraversal.AllPositions(level).Where(level.IsFloor))
|
||||
{
|
||||
foreach (var neighbor in position.Neighbors().Where(level.IsFloor))
|
||||
{
|
||||
if (level.Index(position) >= level.Index(neighbor) || level.IsClosedDoorEdge(position, neighbor))
|
||||
continue;
|
||||
|
||||
ApplyAdjacentInteractions(level, position, neighbor, deltas);
|
||||
}
|
||||
}
|
||||
|
||||
var surface = level.Surface.ToArray();
|
||||
for (var i = 0; i < surface.Length; i++)
|
||||
surface[i] = deltas[i].Apply(surface[i]).Clamp();
|
||||
|
||||
return level with { Surface = surface };
|
||||
}
|
||||
|
||||
public static LevelState AdvanceDurations(LevelState level)
|
||||
{
|
||||
var surface = level.Surface.Select(cell => cell with {
|
||||
FuelBlockTurns = Math.Max(0, cell.FuelBlockTurns - 1),
|
||||
CoolantBlockTurns = Math.Max(0, cell.CoolantBlockTurns - 1),
|
||||
ElectricityBlockTurns = Math.Max(0, cell.ElectricityBlockTurns - 1)
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return level with { Surface = surface };
|
||||
}
|
||||
|
||||
private static void ApplySameCellInteractions(LevelState level, GridPosition position, SurfaceDelta[] deltas)
|
||||
{
|
||||
var surface = level.GetSurface(position);
|
||||
ApplyPair(level, position, ECarrierType.Fuel, SimulationBands.Fuel(surface.Fuel), ECarrierType.Electricity, SimulationBands.Electricity(surface.Electricity), deltas);
|
||||
ApplyPair(level, position, ECarrierType.Fuel, SimulationBands.Fuel(surface.Fuel), null, SimulationBands.Heat(surface.Heat), deltas);
|
||||
ApplyPair(level, position, ECarrierType.Coolant, SimulationBands.Coolant(surface.Coolant), ECarrierType.Electricity, SimulationBands.Electricity(surface.Electricity), deltas);
|
||||
ApplyPair(level, position, ECarrierType.Coolant, SimulationBands.Coolant(surface.Coolant), null, SimulationBands.Heat(surface.Heat), deltas);
|
||||
}
|
||||
|
||||
private static void ApplyAdjacentInteractions(LevelState level, GridPosition a, GridPosition b, SurfaceDelta[] deltas)
|
||||
{
|
||||
var surfaceA = level.GetSurface(a);
|
||||
var surfaceB = level.GetSurface(b);
|
||||
FlowBetween(level, a, b, surfaceA.Fuel, surfaceB.Fuel, Balancing.Current.FlowInteraction(ESurfaceQuantity.Fuel), deltas);
|
||||
FlowBetween(level, a, b, surfaceA.Coolant, surfaceB.Coolant, Balancing.Current.FlowInteraction(ESurfaceQuantity.Coolant), deltas);
|
||||
FlowBetween(level, a, b, surfaceA.Electricity, surfaceB.Electricity, Balancing.Current.FlowInteraction(ESurfaceQuantity.Electricity), deltas);
|
||||
FlowBetween(level, a, b, surfaceA.Heat, surfaceB.Heat, Balancing.Current.FlowInteraction(ESurfaceQuantity.Heat), deltas);
|
||||
}
|
||||
|
||||
private static void ApplyPair(LevelState level, GridPosition position, ECarrierType? rowCarrier, EBand rowBand, ECarrierType? colCarrier, EBand colBand, SurfaceDelta[] deltas)
|
||||
{
|
||||
ApplyEffect(level, position, Balancing.Current.SameCellInteraction(rowCarrier, rowBand, colCarrier, colBand), deltas);
|
||||
}
|
||||
|
||||
private static void ApplyEffect(LevelState level, GridPosition position, SurfaceInteractionEffect effect, SurfaceDelta[] deltas)
|
||||
{
|
||||
var index = level.Index(position);
|
||||
switch (effect.Verb)
|
||||
{
|
||||
case ESurfaceInteractionVerb.Warm:
|
||||
deltas[index].Heat += effect.Amount;
|
||||
break;
|
||||
case ESurfaceInteractionVerb.Quench:
|
||||
deltas[index].Heat -= effect.Amount;
|
||||
break;
|
||||
case ESurfaceInteractionVerb.Short:
|
||||
deltas[index].Heat += effect.Amount;
|
||||
deltas[index].Electricity -= effect.SecondaryAmount;
|
||||
break;
|
||||
case ESurfaceInteractionVerb.Ignite:
|
||||
deltas[index].Heat += effect.Amount;
|
||||
deltas[index].Fuel -= effect.SecondaryAmount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void FlowBetween(LevelState level, GridPosition a, GridPosition b, float valueA, float valueB, SurfaceInteractionEffect effect, SurfaceDelta[] deltas)
|
||||
{
|
||||
var difference = valueA - valueB;
|
||||
if (Math.Abs(difference) < 0.01f)
|
||||
return;
|
||||
|
||||
var amount = difference * effect.Amount;
|
||||
var indexA = level.Index(a);
|
||||
var indexB = level.Index(b);
|
||||
|
||||
switch (effect.Quantity)
|
||||
{
|
||||
case ESurfaceQuantity.Fuel:
|
||||
deltas[indexA].Fuel -= amount;
|
||||
deltas[indexB].Fuel += amount;
|
||||
break;
|
||||
case ESurfaceQuantity.Coolant:
|
||||
deltas[indexA].Coolant -= amount;
|
||||
deltas[indexB].Coolant += amount;
|
||||
break;
|
||||
case ESurfaceQuantity.Electricity:
|
||||
deltas[indexA].Electricity -= amount;
|
||||
deltas[indexB].Electricity += amount;
|
||||
break;
|
||||
case ESurfaceQuantity.Heat:
|
||||
deltas[indexA].Heat -= amount;
|
||||
deltas[indexB].Heat += amount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +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
|
||||
x:Class="ReactorMaintenance.Win2D.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
</Application>
|
||||
@@ -1,115 +1,145 @@
|
||||
<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="260" />
|
||||
<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" />
|
||||
<ItemsControl x:Name="ToolPicker">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<ItemsWrapGrid Orientation="Horizontal" MaximumRowsOrColumns="2" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ToggleButton IsChecked="{Binding IsSelected, Mode=TwoWay}"
|
||||
Checked="ToolToggle_Checked" ToolTipService.ToolTip="{Binding Label}"
|
||||
Padding="5" Margin="0,0,8,8">
|
||||
<Image Width="96" Height="96" Source="{Binding Icon}" Stretch="Uniform" />
|
||||
</ToggleButton>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<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"
|
||||
CreateResources="LevelCanvas_CreateResources"
|
||||
Draw="LevelCanvas_Draw"
|
||||
PointerPressed="LevelCanvas_PointerPressed"
|
||||
PointerMoved="LevelCanvas_PointerMoved"
|
||||
PointerReleased="LevelCanvas_PointerReleased"
|
||||
PointerExited="LevelCanvas_PointerExited"
|
||||
PointerWheelChanged="LevelCanvas_PointerWheelChanged" />
|
||||
</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">
|
||||
<Grid ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image Width="28" Height="28" Source="{Binding Icon}" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding Message}" Foreground="#F4F1E8"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
<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="End Turn" Click="EndTurn_Click" />
|
||||
<AppBarButton Label="Interact" Click="Interact_Click" />
|
||||
<AppBarButton Label="Heat Shield" Click="HeatShield_Click" />
|
||||
<AppBarButton Icon="Accept" Label="Activate" Click="Activate_Click" />
|
||||
</CommandBar>
|
||||
|
||||
<Grid Grid.Row="1" ColumnSpacing="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="260" />
|
||||
<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" />
|
||||
<ItemsControl x:Name="ToolPicker">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<ItemsWrapGrid Orientation="Horizontal" MaximumRowsOrColumns="2" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ToggleButton IsChecked="{Binding IsSelected, Mode=TwoWay}"
|
||||
Checked="ToolToggle_Checked" ToolTipService.ToolTip="{Binding Label}"
|
||||
Width="112" MinHeight="46" Padding="6" Margin="0,0,8,8">
|
||||
<TextBlock Text="{Binding Label}" TextWrapping="WrapWholeWords"
|
||||
TextAlignment="Center"
|
||||
FontSize="12" />
|
||||
</ToggleButton>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Left click selects or paints. Right click clears surface prop and hazards."
|
||||
Foreground="#9EA7AE" TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
Text="Door and wall electricity leaks use two clicks: choose the source cell, then the adjacent floor face."
|
||||
Foreground="#9EA7AE"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid Grid.Column="1" Background="#101215">
|
||||
<canvas:CanvasControl
|
||||
x:Name="LevelCanvas"
|
||||
ClearColor="#101215"
|
||||
CreateResources="LevelCanvas_CreateResources"
|
||||
Draw="LevelCanvas_Draw"
|
||||
PointerPressed="LevelCanvas_PointerPressed"
|
||||
PointerMoved="LevelCanvas_PointerMoved"
|
||||
PointerReleased="LevelCanvas_PointerReleased"
|
||||
PointerExited="LevelCanvas_PointerExited"
|
||||
PointerWheelChanged="LevelCanvas_PointerWheelChanged" />
|
||||
</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="Editor Workflow" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="WorkflowText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock Text="Reactor Binding" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="ReactorBindingText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||
<Grid ColumnSpacing="8" RowSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Content="Fuel" Click="BindFuel_Click" HorizontalAlignment="Stretch" />
|
||||
<Button Grid.Column="1" Content="Coolant" Click="BindCoolant_Click"
|
||||
HorizontalAlignment="Stretch" />
|
||||
<Button Grid.Column="2" Content="Electric" Click="BindElectricity_Click"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Rule Events" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="RuleEventText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||
<Grid ColumnSpacing="8" RowSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Content="Warn Next Turn" Click="AddWarningRule_Click" HorizontalAlignment="Stretch" />
|
||||
<Button Grid.Column="1" Content="Leak Next Turn" Click="AddLeakRule_Click"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Grid>
|
||||
<Button Content="Remove Last Rule" Click="RemoveLastRule_Click" HorizontalAlignment="Stretch" />
|
||||
|
||||
<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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,33 +1,34 @@
|
||||
<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>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</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>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Images\**\*.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.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>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
</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>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Images\**\*.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,10 +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>
|
||||
<?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,76 @@
|
||||
namespace ReactorMaintenance.Simulation.Tests;
|
||||
|
||||
public sealed class LevelEditorTests
|
||||
{
|
||||
[Fact]
|
||||
public void DoorToolRequiresExplicitAdjacentEdgeSelection()
|
||||
{
|
||||
var level = LevelState.Create("Door editor", 6, 6);
|
||||
|
||||
var withDoorProp = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Door });
|
||||
var withDoorEdge = LevelEditor.SetDoorEdge(withDoorProp, new(2, 2), new(3, 2));
|
||||
var rejected = LevelEditor.SetDoorEdge(withDoorEdge, new(2, 2), new(4, 2));
|
||||
|
||||
Assert.Equal(EPropType.Door, withDoorProp.GetProp(new(2, 2)).Type);
|
||||
Assert.Empty(withDoorProp.Doors);
|
||||
Assert.Single(withDoorEdge.Doors);
|
||||
Assert.Equal(withDoorEdge.Doors, rejected.Doors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ElectricityLeakUsesAuthoredWallAccessFace()
|
||||
{
|
||||
var level = LevelState.Create("Electricity leak editor", 6, 6);
|
||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
||||
|
||||
var next = LevelEditor.SetLeak(level, new(2, 2), new(2, 3), ECarrierType.Electricity);
|
||||
var rejected = LevelEditor.SetLeak(next, new(2, 2), new(4, 4), ECarrierType.Electricity);
|
||||
|
||||
Assert.Single(next.Leaks);
|
||||
Assert.Equal(new(2, 3), next.Leaks[0].AccessPosition);
|
||||
Assert.Equal(EUndergroundState.Leaking, next.GetUnderground(new(2, 2), ECarrierType.Electricity).State);
|
||||
Assert.Equal(next.Leaks, rejected.Leaks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReactorBindingUpdatesOnlyMatchingCarrierConsumer()
|
||||
{
|
||||
var level = LevelState.Create("Binding editor", 8, 6);
|
||||
level = level.SetProp(new(1, 1), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
|
||||
level = level.SetProp(new(2, 1), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
|
||||
level = level.SetProp(new(3, 1), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant }) with {
|
||||
Reactors = [
|
||||
new() {
|
||||
ReactorId = 1,
|
||||
ControlPosition = new(1, 1),
|
||||
FuelConsumerPosition = new(1, 1),
|
||||
CoolantConsumerPosition = new(1, 1),
|
||||
ElectricityConsumerPosition = new(1, 1)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var bound = LevelEditor.BindReactorConsumer(level, 1, ECarrierType.Fuel, new(2, 1));
|
||||
var rejected = LevelEditor.BindReactorConsumer(bound, 1, ECarrierType.Fuel, new(3, 1));
|
||||
|
||||
Assert.Equal(new(2, 1), bound.Reactors[0].FuelConsumerPosition);
|
||||
Assert.Equal(bound.Reactors[0].FuelConsumerPosition, rejected.Reactors[0].FuelConsumerPosition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuleEventEditorAssignsStableIdsAndCanRemoveEvents()
|
||||
{
|
||||
var level = LevelState.Create("Rule editor", 6, 6);
|
||||
|
||||
var withRule = LevelEditor.AddRuleEvent(level, new() {
|
||||
Phase = ERuleEventPhase.EndOfTurn,
|
||||
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 1 }],
|
||||
Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "authored" }]
|
||||
});
|
||||
var removed = LevelEditor.RemoveRuleEvent(withRule, "rule-1");
|
||||
|
||||
Assert.Single(withRule.RuleEvents);
|
||||
Assert.Equal("rule-1", withRule.RuleEvents[0].Id);
|
||||
Assert.Empty(removed.RuleEvents);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +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>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.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>
|
||||
|
||||
@@ -1,298 +1,447 @@
|
||||
using ReactorMaintenance.Simulation.Difficulties;
|
||||
using ReactorMaintenance.Simulation.Effects;
|
||||
using ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Tests;
|
||||
namespace ReactorMaintenance.Simulation.Tests;
|
||||
|
||||
public sealed class SimulationEngineTests
|
||||
{
|
||||
[Fact]
|
||||
public void FuelLeakNearPoweredGeneratorCreatesIgnitionForecast()
|
||||
public void NetworkPropagationSuppliesBoundConsumersAndReadiesReactor()
|
||||
{
|
||||
var level = LevelState.Create("Fuel leak", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Prop = ECellProp.Generator,
|
||||
Pipe = EPipeMedium.Fuel,
|
||||
LeakRate = Balancing.Current.FuelVaporFireThreshold,
|
||||
Pressure = Balancing.Current.PressurizedFuelLeakPressureThreshold + Balancing.Current.NeighborDistance,
|
||||
Integrity = Balancing.Current.DefaultEditedPipeIntegrity,
|
||||
Powered = true
|
||||
});
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Ignition && forecast.Position == new GridPosition(2, 2) && forecast.Turns == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoolantLeakOnPoweredCellRaisesElectricalCharge()
|
||||
{
|
||||
var level = LevelState.Create("Wet cable", 6, 6)
|
||||
.SetCell(new(3, 3), new() {
|
||||
Pipe = EPipeMedium.Coolant,
|
||||
LeakRate = Balancing.Current.ElectrifiedCoolantPoolingThreshold,
|
||||
Powered = true
|
||||
});
|
||||
var level = BuildReadyLevel();
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.True(next.GetCell(new(3, 3)).Hazards.ElectricalCharge >= Balancing.Current.ElectricalChargeIncrease);
|
||||
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 2)).ServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 3)).ServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 4)).ServiceState);
|
||||
Assert.Equal(ELevelState.Ready, next.Global.LevelState);
|
||||
Assert.Contains(next.Forecasts, forecast => forecast.Kind == EForecastKind.ReactorReady);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveFireSpreadsSmokeToOpenNeighbors()
|
||||
public void ReactorActivatesOnlyAtReadyControl()
|
||||
{
|
||||
var level = LevelState.Create("Smoke", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Hazards = new() {
|
||||
Fire = true,
|
||||
Smoke = Balancing.Current.SmokeSpreadThreshold
|
||||
}
|
||||
});
|
||||
var level = m_Engine.AdvanceTurn(BuildReadyLevel()) with {
|
||||
Robot = new() { Position = new(5, 3) }
|
||||
};
|
||||
|
||||
var activated = m_Engine.ActivateReactor(level);
|
||||
|
||||
Assert.Equal(ELevelState.Won, activated.Global.LevelState);
|
||||
Assert.True(activated.Reactors[0].Activated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeakingUndergroundCellInjectsMatchingSurfaceHazard()
|
||||
{
|
||||
var level = LevelState.Create("Leak", 6, 6);
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Leaking, Amount = 5, Intensity = 5 }) with {
|
||||
Leaks = [new() { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }]
|
||||
};
|
||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.True(next.GetCell(new(2, 3)).Hazards.Smoke > 0);
|
||||
Assert.True(next.GetSurface(new(2, 2)).Fuel > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvanceTurnRunsConfiguredCellEffects()
|
||||
public void ElementRemedyClearsHazardAndBlocksImmediateReentry()
|
||||
{
|
||||
var engine = new SimulationEngine([new TestCellEffect()], [], []);
|
||||
var level = LevelState.Create("Custom effect", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Hazards = new() { Heat = 1 }
|
||||
});
|
||||
var level = LevelState.Create("Remedy", 6, 6);
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Leaking, Amount = 5, Intensity = 5 });
|
||||
level = level.SetSurface(new(2, 2), new() { Fuel = 5 }) with {
|
||||
Robot = new() { Position = new(2, 2), FuelNeutralizers = 1 },
|
||||
Leaks = [new() { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }]
|
||||
};
|
||||
|
||||
var next = engine.AdvanceTurn(level);
|
||||
var next = m_Engine.InteractLeak(level, ECarrierType.Fuel, true);
|
||||
|
||||
Assert.Equal(5, next.GetCell(new(2, 2)).Hazards.Heat);
|
||||
Assert.Equal(0, next.GetSurface(new(2, 2)).Fuel);
|
||||
Assert.True(next.GetSurface(new(2, 2)).FuelBlockTurns > 0);
|
||||
Assert.Equal(0, next.Robot.FuelNeutralizers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvanceTurnRunsConfiguredAreaEffects()
|
||||
public void ClosedDoorBlocksAdjacentHeatFlow()
|
||||
{
|
||||
var engine = new SimulationEngine([], [new TestAreaEffect()], []);
|
||||
var level = LevelState.Create("Custom area effect", 6, 6);
|
||||
var level = LevelState.Create("Door", 6, 6);
|
||||
level = level.SetSurface(new(2, 2), new() { Heat = 8 }) with {
|
||||
Doors = [new() { A = new(2, 2), B = new(3, 2), State = EDoorState.Closed }]
|
||||
};
|
||||
|
||||
var next = engine.AdvanceTurn(level);
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Equal(7, next.GetCell(new(2, 2)).Hazards.Smoke);
|
||||
Assert.Equal(0, next.GetSurface(new(3, 2)).Heat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverpressurePredictsPipeBurst()
|
||||
public void HeatShieldPreventsRobotHeatLoss()
|
||||
{
|
||||
var level = LevelState.Create("Pressure", 6, 6)
|
||||
.SetCell(new(1, 2), new() {
|
||||
Pipe = EPipeMedium.Pressure,
|
||||
Pressure = 10,
|
||||
Integrity = 6
|
||||
});
|
||||
var level = LevelState.Create("Heat shield", 6, 6);
|
||||
level = level.SetSurface(new(2, 2), new() { Heat = Balancing.Current.RobotHeatSafetyThreshold }) with {
|
||||
Robot = new() { Position = new(2, 2), HeatImmunitySteps = 1 }
|
||||
};
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.PipeBurst && forecast.Turns == 2);
|
||||
Assert.NotEqual(ELevelState.Lost, next.Global.LevelState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastCapsFutureSimulationWhenNoTerminalConditionOccurs()
|
||||
public void JunctionRatioSplitsFlowAcrossInferredOutgoingBranches()
|
||||
{
|
||||
var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
|
||||
var level = LevelState.Create("Stable", 6, 6);
|
||||
var level = BuildJunctionLevel(2);
|
||||
|
||||
var forecasts = engine.Forecast(level);
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
|
||||
Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Max(forecast => forecast.Turns));
|
||||
Assert.True(next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount > 0);
|
||||
Assert.Equal(next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount, next.GetUnderground(new(3, 3), ECarrierType.Fuel).Amount);
|
||||
Assert.True(next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount < next.GetUnderground(new(2, 3), ECarrierType.Fuel).Amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastUsesCurrentBalancingProfile()
|
||||
public void JunctionZeroWeightBranchReceivesNoIntentionalOutflow()
|
||||
{
|
||||
var previous = Balancing.Current;
|
||||
try
|
||||
{
|
||||
Balancing.Current = new TestBalancing();
|
||||
var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
|
||||
var level = LevelState.Create("Stable", 6, 6);
|
||||
var level = BuildJunctionLevel(0);
|
||||
|
||||
var forecasts = engine.Forecast(level);
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Balancing.Current = previous;
|
||||
}
|
||||
Assert.Equal(0, next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount);
|
||||
Assert.True(next.GetUnderground(new(3, 3), ECarrierType.Fuel).Amount > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastPredictsMeltdownFromFutureSimulation()
|
||||
public void ValidatorRejectsAmbiguousJunctionSourceBranches()
|
||||
{
|
||||
var level = LevelState.Create("Meltdown", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Prop = ECellProp.Reactor,
|
||||
Hazards = new() { Heat = Balancing.Current.MeltdownCoreHeatThreshold - Balancing.Current.ReactorHeatIncrease }
|
||||
});
|
||||
var level = BuildJunctionLevel(2);
|
||||
level = level.SetProp(new(3, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
var report = new LevelValidator().Validate(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 1);
|
||||
Assert.False(report.IsValid);
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Ambiguous junction flow", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastReportsAlreadyLostLevelAtCurrentTurn()
|
||||
public void ValidatorRejectsJunctionWithoutTwoOrThreeOutflows()
|
||||
{
|
||||
var level = LevelState.Create("Lost", 6, 6) with {
|
||||
Global = new() {
|
||||
CoreHeat = Balancing.Current.MeltdownCoreHeatThreshold,
|
||||
Lost = true,
|
||||
Status = "CORE MELTDOWN"
|
||||
}
|
||||
var level = BuildJunctionLevel(2);
|
||||
level = level.SetUnderground(new(3, 3), ECarrierType.Fuel, new());
|
||||
|
||||
var report = new LevelValidator().Validate(level);
|
||||
|
||||
Assert.False(report.IsValid);
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("one incoming branch and two or three outgoing branches", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonJunctionCellsAcceptBestFlowFromMultipleSourcePaths()
|
||||
{
|
||||
var level = LevelState.Create("Best path", 7, 7);
|
||||
level = AddHorizontalUnderground(level, ECarrierType.Fuel, 1, 5, 3);
|
||||
level = level.SetProp(new(1, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
level = level.SetProp(new(5, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
|
||||
|
||||
var report = new LevelValidator().Validate(level);
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.True(report.IsValid);
|
||||
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 3)).ServiceState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RobotLosesOnUnsafeElementHazard()
|
||||
{
|
||||
var level = LevelState.Create("Unsafe", 6, 6);
|
||||
level = level.SetSurface(new(2, 2), new() { Electricity = Balancing.Current.MaxValue }) with {
|
||||
Robot = new() { Position = new(2, 2) }
|
||||
};
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Equal(ELevelState.Lost, next.Global.LevelState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuleEventCanCreateTerminalLossForecast()
|
||||
{
|
||||
var level = LevelState.Create("Rule", 6, 6) with {
|
||||
RuleEvents = [
|
||||
new() {
|
||||
Phase = ERuleEventPhase.EndOfTurn,
|
||||
ForecastText = "containment failure",
|
||||
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }],
|
||||
Effects = [new() { Kind = ERuleEffectKind.MarkTerminalLoss, Message = "CONTAINMENT FAILURE" }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 0);
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EForecastKind.RuleEvent && forecast.Message == "containment failure");
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EForecastKind.TerminalLoss);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastPredictsStabilityCollapseFromFutureSimulation()
|
||||
public void RuleEventCanTriggerFromNetworkBand()
|
||||
{
|
||||
var level = LevelState.Create("Collapse", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Prop = ECellProp.Generator,
|
||||
Hazards = new() { Stability = Balancing.Current.CriticalCellStabilityThreshold }
|
||||
}) with {
|
||||
Global = new() { FacilityStability = Balancing.Current.FireStabilityDamage }
|
||||
var level = LevelState.Create("Network rule", 6, 6);
|
||||
level = AddLine(level, ECarrierType.Fuel, new(2, 2), new(3, 2));
|
||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }) with {
|
||||
RuleEvents = [
|
||||
new() {
|
||||
Phase = ERuleEventPhase.EndOfTurn,
|
||||
Predicates = [new() { Kind = ERulePredicateKind.NetworkBandAt, Position = new(3, 2), Carrier = ECarrierType.Fuel, NetworkValue = ENetworkValueKind.Amount, Band = EBand.Critical }],
|
||||
Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "fuel pressure high" }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.StabilityCollapse && forecast.Turns == 1);
|
||||
Assert.Contains("fuel pressure high", next.Global.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StableReactorWithPowerAndCoolingCanActivate()
|
||||
public void RuleEventCanTriggerFromReactorReadiness()
|
||||
{
|
||||
var level = LevelState.Create("Ready", 8, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Prop = ECellProp.Reactor,
|
||||
Hazards = new() { Heat = 3 }
|
||||
})
|
||||
.SetCell(new(3, 2), new() {
|
||||
Prop = ECellProp.Generator,
|
||||
Powered = true
|
||||
})
|
||||
.SetCell(new(4, 2), new() {
|
||||
Prop = ECellProp.CoolingPump,
|
||||
Powered = true
|
||||
});
|
||||
var level = BuildReadyLevel() with {
|
||||
RuleEvents = [
|
||||
new() {
|
||||
Phase = ERuleEventPhase.EndOfTurn,
|
||||
Predicates = [new() { Kind = ERulePredicateKind.ReactorReadyIs, ReactorId = 1, BoolValue = true }],
|
||||
Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "reactor ready rule" }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
var activated = m_Engine.ActivateReactor(next);
|
||||
|
||||
Assert.Equal("REACTOR ONLINE", activated.Global.Status);
|
||||
Assert.True(activated.Global.ReactorActivated);
|
||||
Assert.Contains("reactor ready rule", next.Global.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LevelSerializationRoundTripsEditableState()
|
||||
public void RuleEventCanTriggerFromRobotInventory()
|
||||
{
|
||||
var level = LevelState.Create("Round trip", 5, 5);
|
||||
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Reactor);
|
||||
level = LevelEditor.Apply(level, new(1, 2), EEditorTool.CoolantPipe);
|
||||
level = LevelEditor.Apply(level, new(1, 2), EEditorTool.Leak);
|
||||
var level = LevelState.Create("Inventory rule", 6, 6) with {
|
||||
Robot = new() { Position = new(1, 1), FuelNeutralizers = 1 },
|
||||
RuleEvents = [
|
||||
new() {
|
||||
Phase = ERuleEventPhase.StartOfSimulation,
|
||||
Predicates = [new() { Kind = ERulePredicateKind.RobotInventoryAtLeast, Remedy = ERemedyType.FuelNeutralizer, InventoryCount = 1 }],
|
||||
Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "fuel kit detected" }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Contains("fuel kit detected", next.Global.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuleEventCanRemoveHazardsHeatAndInventory()
|
||||
{
|
||||
var level = LevelState.Create("Remove rule", 6, 6);
|
||||
level = level.SetSurface(new(2, 2), new() { Fuel = 5, Heat = 5 }) with {
|
||||
Robot = new() { Position = new(1, 1), FuelNeutralizers = 2 },
|
||||
Doors = [
|
||||
new() { A = new(2, 2), B = new(1, 2), State = EDoorState.Closed },
|
||||
new() { A = new(2, 2), B = new(3, 2), State = EDoorState.Closed },
|
||||
new() { A = new(2, 2), B = new(2, 1), State = EDoorState.Closed },
|
||||
new() { A = new(2, 2), B = new(2, 3), State = EDoorState.Closed }
|
||||
],
|
||||
RuleEvents = [
|
||||
new() {
|
||||
Phase = ERuleEventPhase.StartOfSimulation,
|
||||
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }],
|
||||
Effects = [
|
||||
new() { Kind = ERuleEffectKind.RemoveSurfaceHazard, Position = new(2, 2), Carrier = ECarrierType.Fuel, Amount = 2 },
|
||||
new() { Kind = ERuleEffectKind.RemoveHeat, Position = new(2, 2), Amount = 3 },
|
||||
new() { Kind = ERuleEffectKind.RemoveInventory, Remedy = ERemedyType.FuelNeutralizer, Amount = 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Equal(3, next.GetSurface(new(2, 2)).Fuel);
|
||||
Assert.Equal(2, next.GetSurface(new(2, 2)).Heat);
|
||||
Assert.Equal(1, next.Robot.FuelNeutralizers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuleEventStartLeakUsesAuthoredElectricityAccessFace()
|
||||
{
|
||||
var level = LevelState.Create("Electricity leak rule", 6, 6);
|
||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Electricity, new() { State = EUndergroundState.Intact }) with {
|
||||
RuleEvents = [
|
||||
new() {
|
||||
Phase = ERuleEventPhase.StartOfSimulation,
|
||||
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }],
|
||||
Effects = [new() { Kind = ERuleEffectKind.StartLeak, Position = new(2, 2), AccessPosition = new(2, 3), Carrier = ECarrierType.Electricity }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Single(next.Leaks);
|
||||
Assert.Equal(new(2, 3), next.Leaks[0].AccessPosition);
|
||||
Assert.True(next.GetSurface(new(2, 3)).Electricity > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatorRejectsInvalidRuleTargets()
|
||||
{
|
||||
var level = LevelState.Create("Invalid rules", 6, 6);
|
||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall) with {
|
||||
RuleEvents = [
|
||||
new() {
|
||||
Predicates = [new() { Kind = ERulePredicateKind.PropStateAt, Position = new(1, 1) }],
|
||||
Effects = [
|
||||
new() { Kind = ERuleEffectKind.AddSurfaceHazard, Position = new(2, 2), Carrier = ECarrierType.Fuel, Amount = 1 },
|
||||
new() { Kind = ERuleEffectKind.RepairNetworkCell, Position = new(3, 3), Carrier = ECarrierType.Coolant }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var report = new LevelValidator().Validate(level);
|
||||
|
||||
Assert.False(report.IsValid);
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Rule prop predicate", StringComparison.Ordinal));
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Rule surface effect", StringComparison.Ordinal));
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Rule network effect", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatorRejectsWallHazardsAndInvalidReactorBinding()
|
||||
{
|
||||
var level = LevelState.Create("Invalid", 6, 6);
|
||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
||||
level = level with {
|
||||
Surface = level.Surface.ToArray(),
|
||||
Reactors = [new() { ControlPosition = new(3, 3), FuelConsumerPosition = new(1, 1), CoolantConsumerPosition = new(1, 1), ElectricityConsumerPosition = new(1, 1) }]
|
||||
};
|
||||
level.Surface[level.Index(new(2, 2))] = new() { Heat = 1 };
|
||||
|
||||
var report = new LevelValidator().Validate(level);
|
||||
|
||||
Assert.False(report.IsValid);
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Wall cell", StringComparison.Ordinal));
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Reactor binding", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LevelSerializationRoundTripsCurrentSchemaOnly()
|
||||
{
|
||||
var level = BuildReadyLevel();
|
||||
|
||||
var json = LevelSerializer.Serialize(level);
|
||||
var loaded = LevelSerializer.Deserialize(json);
|
||||
|
||||
Assert.Contains("\"Version\": 1", json);
|
||||
Assert.Contains("\"Version\": 2", json);
|
||||
Assert.Equal(level.Name, loaded.Name);
|
||||
Assert.Equal(ECellProp.Reactor, loaded.GetCell(new(2, 2)).Prop);
|
||||
Assert.Equal(EPipeMedium.Coolant, loaded.GetCell(new(1, 2)).Pipe);
|
||||
Assert.Equal(1, loaded.GetCell(new(1, 2)).LeakRate);
|
||||
Assert.Equal(EPropType.Flow, loaded.GetProp(new(2, 2)).Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LevelSerializationRejectsUnsupportedVersion()
|
||||
public void LevelSerializationRoundTripsRuleEventsDoorsAndElectricityLeakFaces()
|
||||
{
|
||||
var level = BuildReadyLevel();
|
||||
level = level.SetTerrain(new(6, 4), ECellTerrain.Wall);
|
||||
level = level.SetUnderground(new(6, 4), ECarrierType.Electricity, new() { State = EUndergroundState.Leaking }) with {
|
||||
Doors = [new() { A = new(5, 3), B = new(5, 4), State = EDoorState.Closed }],
|
||||
Leaks = [new() { Carrier = ECarrierType.Electricity, UndergroundPosition = new(6, 4), AccessPosition = new(6, 3) }],
|
||||
RuleEvents = [
|
||||
new() {
|
||||
Id = "authored",
|
||||
Phase = ERuleEventPhase.EndOfTurn,
|
||||
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 2 }],
|
||||
Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "serialized" }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var loaded = LevelSerializer.Deserialize(LevelSerializer.Serialize(level));
|
||||
|
||||
Assert.Single(loaded.Doors);
|
||||
Assert.Single(loaded.Leaks);
|
||||
Assert.Single(loaded.RuleEvents);
|
||||
Assert.Equal(new(6, 3), loaded.Leaks[0].AccessPosition);
|
||||
Assert.Equal("authored", loaded.RuleEvents[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LevelSerializationRejectsOldSchema()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"Version": 999,
|
||||
"Version": 1,
|
||||
"Level": {}
|
||||
}
|
||||
""";
|
||||
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => LevelSerializer.Deserialize(json));
|
||||
|
||||
Assert.Contains("Unsupported level file version 999", exception.Message);
|
||||
Assert.Contains("Unsupported level file version 1", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WallToolClearsCellPropsPipesAndHazards()
|
||||
private static LevelState BuildReadyLevel()
|
||||
{
|
||||
var level = LevelState.Create("Wall", 5, 5);
|
||||
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Generator);
|
||||
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.CoolantPipe);
|
||||
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Fire);
|
||||
|
||||
var edited = LevelEditor.Apply(level, new(2, 2), EEditorTool.Wall);
|
||||
var cell = edited.GetCell(new(2, 2));
|
||||
|
||||
Assert.Equal(ECellTerrain.Wall, cell.Terrain);
|
||||
Assert.Equal(ECellProp.None, cell.Prop);
|
||||
Assert.Equal(EPipeMedium.None, cell.Pipe);
|
||||
Assert.False(cell.Powered);
|
||||
Assert.False(cell.Hazards.Fire);
|
||||
var level = LevelState.Create("Ready", 8, 7);
|
||||
level = AddLine(level, ECarrierType.Fuel, new(2, 2), new(3, 2));
|
||||
level = AddLine(level, ECarrierType.Coolant, new(2, 3), new(3, 3));
|
||||
level = AddLine(level, ECarrierType.Electricity, new(2, 4), new(3, 4));
|
||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant });
|
||||
level = level.SetProp(new(2, 4), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
|
||||
level = level.SetProp(new(3, 2), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
|
||||
level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant });
|
||||
level = level.SetProp(new(3, 4), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Electricity });
|
||||
level = level.SetProp(new(5, 3), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
|
||||
return level with {
|
||||
Robot = new() { Position = new(5, 3) },
|
||||
Reactors = [
|
||||
new() {
|
||||
ReactorId = 1,
|
||||
ControlPosition = new(5, 3),
|
||||
FuelConsumerPosition = new(3, 2),
|
||||
CoolantConsumerPosition = new(3, 3),
|
||||
ElectricityConsumerPosition = new(3, 4)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PropToolsKeepFloorTerrain()
|
||||
private static LevelState AddLine(LevelState level, ECarrierType carrier, GridPosition a, GridPosition b)
|
||||
{
|
||||
var level = LevelState.Create("Prop", 5, 5);
|
||||
level = LevelEditor.Apply(level, new(1, 1), EEditorTool.Wall);
|
||||
level = level.SetUnderground(a, carrier, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetUnderground(b, carrier, new() { State = EUndergroundState.Intact });
|
||||
return level;
|
||||
}
|
||||
|
||||
var edited = LevelEditor.Apply(level, new(1, 1), EEditorTool.Reactor);
|
||||
var cell = edited.GetCell(new(1, 1));
|
||||
private static LevelState BuildJunctionLevel(int mode)
|
||||
{
|
||||
var level = LevelState.Create("Junction", 6, 6);
|
||||
level = level.SetUnderground(new(1, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetUnderground(new(2, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetUnderground(new(3, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetProp(new(1, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
return level.SetProp(new(2, 3), new() { Type = EPropType.Junction, Carrier = ECarrierType.Fuel, JunctionMode = mode });
|
||||
}
|
||||
|
||||
Assert.Equal(ECellTerrain.Floor, cell.Terrain);
|
||||
Assert.Equal(ECellProp.Reactor, cell.Prop);
|
||||
private static LevelState AddHorizontalUnderground(LevelState level, ECarrierType carrier, int startX, int endX, int y)
|
||||
{
|
||||
for (var x = startX; x <= endX; x++)
|
||||
level = level.SetUnderground(new(x, y), carrier, new() { State = EUndergroundState.Intact });
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
private readonly SimulationEngine m_Engine = new();
|
||||
|
||||
private sealed class StepCountingHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
|
||||
{
|
||||
yield return new(EFailureKind.PipeBurst, new(turns, 0), turns, $"STEP {turns}");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestBalancing : NormalBalancing
|
||||
{
|
||||
public override int MaxForecastStepCount => 2;
|
||||
}
|
||||
|
||||
private sealed class TestCellEffect : ISimulationEffect
|
||||
{
|
||||
public CellState Apply(CellState cell)
|
||||
{
|
||||
return cell with { Hazards = cell.Hazards with { Heat = 5 } };
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestAreaEffect : IAreaSimulationEffect
|
||||
{
|
||||
public CellState[] Apply(LevelState level, CellState[] cells)
|
||||
{
|
||||
var next = cells.ToArray();
|
||||
var position = new GridPosition(2, 2);
|
||||
var cell = next[level.Index(position)];
|
||||
next[level.Index(position)] = cell with { Hazards = cell.Hazards with { Smoke = 7 } };
|
||||
return next;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user