Compare commits
28 Commits
1ca65eccf8
...
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 | |||
| 4b581d60b5 | |||
| e90609bcee | |||
| 6e8766db3f | |||
| fd5564e444 | |||
| 70adeb010f |
@@ -20,9 +20,10 @@ insert_final_newline = false
|
|||||||
# Organize usings
|
# Organize usings
|
||||||
dotnet_separate_import_directive_groups = false
|
dotnet_separate_import_directive_groups = false
|
||||||
dotnet_sort_system_directives_first = false
|
dotnet_sort_system_directives_first = false
|
||||||
file_header_template =
|
file_header_template = # this. and Me. preferences
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# this. and Me. preferences
|
|
||||||
dotnet_style_qualification_for_event = false:suggestion
|
dotnet_style_qualification_for_event = false:suggestion
|
||||||
dotnet_style_qualification_for_field = false:suggestion
|
dotnet_style_qualification_for_field = false:suggestion
|
||||||
dotnet_style_qualification_for_method = false:suggestion
|
dotnet_style_qualification_for_method = false:suggestion
|
||||||
@@ -125,7 +126,7 @@ csharp_new_line_before_else = true
|
|||||||
csharp_new_line_before_finally = true
|
csharp_new_line_before_finally = true
|
||||||
csharp_new_line_before_members_in_anonymous_types = true
|
csharp_new_line_before_members_in_anonymous_types = true
|
||||||
csharp_new_line_before_members_in_object_initializers = true
|
csharp_new_line_before_members_in_object_initializers = true
|
||||||
csharp_new_line_before_open_brace = accessors,anonymous_methods,control_blocks,events,indexers,local_functions,methods,properties,types
|
csharp_new_line_before_open_brace = accessors, anonymous_methods, control_blocks, events, indexers, local_functions, methods, properties, types
|
||||||
csharp_new_line_between_query_expression_clauses = true
|
csharp_new_line_between_query_expression_clauses = true
|
||||||
|
|
||||||
# Indentation preferences
|
# Indentation preferences
|
||||||
@@ -476,7 +477,7 @@ dotnet_naming_rule.types_and_namespaces_rule.import_to_resharper = as_predefined
|
|||||||
dotnet_naming_rule.types_and_namespaces_rule.severity = none
|
dotnet_naming_rule.types_and_namespaces_rule.severity = none
|
||||||
dotnet_naming_rule.types_and_namespaces_rule.style = pascal_case
|
dotnet_naming_rule.types_and_namespaces_rule.style = pascal_case
|
||||||
dotnet_naming_rule.types_and_namespaces_rule.symbols = types_and_namespaces_symbols
|
dotnet_naming_rule.types_and_namespaces_rule.symbols = types_and_namespaces_symbols
|
||||||
dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
|
dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
|
||||||
dotnet_naming_symbols.constants_symbols.applicable_kinds = field
|
dotnet_naming_symbols.constants_symbols.applicable_kinds = field
|
||||||
dotnet_naming_symbols.constants_symbols.required_modifiers = const
|
dotnet_naming_symbols.constants_symbols.required_modifiers = const
|
||||||
dotnet_naming_symbols.event_symbols.applicable_accessibilities = *
|
dotnet_naming_symbols.event_symbols.applicable_accessibilities = *
|
||||||
@@ -498,16 +499,16 @@ dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field
|
|||||||
dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
|
dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
|
||||||
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
|
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
|
||||||
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
|
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
|
||||||
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly
|
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly
|
||||||
dotnet_naming_symbols.property_symbols.applicable_accessibilities = *
|
dotnet_naming_symbols.property_symbols.applicable_accessibilities = *
|
||||||
dotnet_naming_symbols.property_symbols.applicable_kinds = property
|
dotnet_naming_symbols.property_symbols.applicable_kinds = property
|
||||||
dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
|
dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
|
||||||
dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field
|
dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field
|
||||||
dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
|
dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
|
||||||
dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field
|
dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field
|
||||||
dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static,readonly
|
dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static, readonly
|
||||||
dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities = *
|
dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities = *
|
||||||
dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds = namespace,class,struct,enum,delegate
|
dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds = namespace, class, struct, enum, delegate
|
||||||
|
|
||||||
# ReSharper inspection severities
|
# ReSharper inspection severities
|
||||||
resharper_arrange_accessor_owner_body_highlighting = suggestion
|
resharper_arrange_accessor_owner_body_highlighting = suggestion
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
.vs
|
.vs
|
||||||
bin
|
bin
|
||||||
obj
|
obj
|
||||||
|
.idea
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Windows-specific instructions
|
# 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 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.
|
- After every iteration, run `jb cleanupcode '$file1' '$file2' ...` for every C# file you touched.
|
||||||
@@ -25,6 +25,7 @@ This repository follows the local `.editorconfig` and the style visible in the c
|
|||||||
## Braces And Blocks
|
## Braces And Blocks
|
||||||
|
|
||||||
- Use braces for multi-line bodies.
|
- 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.
|
- 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.
|
- 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.
|
- Keep opening braces on the next line for types, methods, properties, accessors, and control blocks.
|
||||||
|
|||||||
20
README.md
@@ -1,18 +1,28 @@
|
|||||||
# Reactor Maintenance
|
# Reactor Maintenance
|
||||||
|
|
||||||
C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `design.md`.
|
C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `docs/design.md`.
|
||||||
|
|
||||||
## Projects
|
## Projects
|
||||||
|
|
||||||
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, versioned JSON serialization, and swappable difficulty balancing profiles.
|
- `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 painting floor/wall terrain plus cell props, loading/saving levels, advancing simulation turns, and activating the reactor.
|
- `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.
|
- `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
|
## Commands
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj
|
dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj
|
||||||
dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
|
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
|
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>
|
<Solution>
|
||||||
<Folder Name="/src/">
|
<Folder Name="/src/">
|
||||||
<Project Path="src/ReactorMaintenance.Simulation/ReactorMaintenance.Simulation.csproj" />
|
<Project Path="src/ReactorMaintenance.Simulation/ReactorMaintenance.Simulation.csproj"/>
|
||||||
<Project Path="src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj">
|
<Project Path="src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj">
|
||||||
<Platform Project="x86" />
|
<Platform Project="x86"/>
|
||||||
</Project>
|
</Project>
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj" />
|
<Project Path="tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj"/>
|
||||||
</Folder>
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
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
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 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 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 DefaultLevelWidth { get; }
|
||||||
public abstract int DefaultLevelHeight { get; }
|
public abstract int DefaultLevelHeight { get; }
|
||||||
public abstract int DefaultRobotCoordinate { get; }
|
public abstract int MinimumLevelSize { get; }
|
||||||
public abstract int DefaultPipeFlow { get; }
|
public abstract int ActionsPerTurn { get; }
|
||||||
public abstract int DefaultPipePressure { get; }
|
public abstract int ForecastHorizon { get; }
|
||||||
public abstract int DefaultPressurePipeFlow { get; }
|
public abstract float MinValue { get; }
|
||||||
public abstract int DefaultPressurePipePressure { get; }
|
public abstract float MaxValue { get; }
|
||||||
public abstract int DefaultEditedPipeIntegrity { get; }
|
public abstract float FuelSafe { get; }
|
||||||
public abstract int MinimumLeakRate { get; }
|
public abstract float FuelCaution { get; }
|
||||||
public abstract int DamagedPipeIntegrity { get; }
|
public abstract float FuelCritical { get; }
|
||||||
public abstract int RepairedLeakRate { get; }
|
public abstract float CoolantSafe { get; }
|
||||||
public abstract int RepairedElectricalCharge { get; }
|
public abstract float CoolantCaution { get; }
|
||||||
public abstract int HeatToolIncrease { get; }
|
public abstract float CoolantCritical { get; }
|
||||||
public abstract int FireToolMinimumHeat { get; }
|
public abstract float ElectricitySafe { get; }
|
||||||
public abstract int FireToolMinimumSmoke { get; }
|
public abstract float ElectricityCaution { get; }
|
||||||
public abstract int MaxForecastStepCount { get; }
|
public abstract float ElectricityCritical { get; }
|
||||||
public abstract int TurnIncrement { get; }
|
public abstract float HeatSafe { get; }
|
||||||
public abstract int OverpressureThreshold { get; }
|
public abstract float HeatCaution { get; }
|
||||||
public abstract int HeatIntegrityDamageThreshold { get; }
|
public abstract float HeatCritical { get; }
|
||||||
public abstract int PipeFireIntegrityDamage { get; }
|
public abstract float TerminalHeat { get; }
|
||||||
public abstract int FireStabilityDamage { get; }
|
public abstract float RobotFuelSafetyThreshold { get; }
|
||||||
public abstract int BurstLeakRate { get; }
|
public abstract float RobotCoolantSafetyThreshold { get; }
|
||||||
public abstract int BrokenPipeFlow { get; }
|
public abstract float RobotElectricitySafetyThreshold { get; }
|
||||||
public abstract int ElectrifiedCoolantPoolingThreshold { get; }
|
public abstract float RobotHeatSafetyThreshold { get; }
|
||||||
public abstract int ElectricalChargeIncrease { get; }
|
public abstract float SourceAmount { get; }
|
||||||
public abstract int FuelVaporFireThreshold { get; }
|
public abstract float SourceIntensity { get; }
|
||||||
public abstract int LiquidFuelFireThreshold { get; }
|
public abstract float DistanceAmountFalloff { get; }
|
||||||
public abstract int HeatIgnitionThreshold { get; }
|
public abstract float DistanceIntensityFalloff { get; }
|
||||||
public abstract int ElectricalIgnitionThreshold { get; }
|
public abstract IReadOnlyList<JunctionRatioPreset> TwoOutflowJunctionRatios { get; }
|
||||||
public abstract int FireHeatIncrease { get; }
|
public abstract IReadOnlyList<JunctionRatioPreset> ThreeOutflowJunctionRatios { get; }
|
||||||
public abstract int FireSmokeIncrease { get; }
|
public abstract float ConsumerRequiredAmount { get; }
|
||||||
public abstract int FireLiquidFuelConsumption { get; }
|
public abstract float ConsumerRequiredIntensity { get; }
|
||||||
public abstract int FireFuelVaporConsumption { get; }
|
public abstract float LeakBaseAmount { get; }
|
||||||
public abstract int SmokeDecay { get; }
|
public abstract float LeakAmountScale { get; }
|
||||||
public abstract int PressurizedFuelLeakPressureThreshold { get; }
|
public abstract float LeakIntensityScale { get; }
|
||||||
public abstract int PassiveFuelVaporHeatOffset { get; }
|
public abstract float FlowTransferRatio { get; }
|
||||||
public abstract int PassiveFuelVaporDivisor { get; }
|
public abstract float WarmCautionAmount { get; }
|
||||||
public abstract int MinimumCoolantHeatReduction { get; }
|
public abstract float WarmCriticalAmount { get; }
|
||||||
public abstract int CoolantHeatReductionDivisor { get; }
|
public abstract float QuenchCautionAmount { get; }
|
||||||
public abstract int CoolantSteamHeatThreshold { get; }
|
public abstract float QuenchCriticalAmount { get; }
|
||||||
public abstract int CoolantSteamSmokeIncrease { get; }
|
public abstract float ShortCautionHeat { get; }
|
||||||
public abstract int PressureLeakSmokeThreshold { get; }
|
public abstract float ShortCautionDischarge { get; }
|
||||||
public abstract int PressureLeakSmokeIncrease { get; }
|
public abstract float ShortCriticalHeat { get; }
|
||||||
public abstract int GeneratorHeatIncrease { get; }
|
public abstract float ShortCriticalDischarge { get; }
|
||||||
public abstract int CoolingPumpHeatReduction { get; }
|
public abstract float IgniteCautionHeat { get; }
|
||||||
public abstract int ReactorHeatIncrease { get; }
|
public abstract float IgniteCautionFuelConsumption { get; }
|
||||||
public abstract int SmokeSpreadThreshold { get; }
|
public abstract float IgniteCriticalHeat { get; }
|
||||||
public abstract int SmokeSpreadIncrease { get; }
|
public abstract float IgniteCriticalFuelConsumption { get; }
|
||||||
public abstract int CriticalCellStabilityThreshold { get; }
|
public abstract int RemedyBlockTurns { get; }
|
||||||
public abstract int MeltdownCoreHeatThreshold { get; }
|
public abstract int HeatShieldSteps { get; }
|
||||||
public abstract int StabilityCollapseThreshold { get; }
|
public abstract int InventoryCapacityPerRemedy { 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; }
|
|
||||||
}
|
}
|
||||||
@@ -1,76 +1,70 @@
|
|||||||
using ReactorMaintenance.Simulation;
|
namespace ReactorMaintenance.Simulation.Difficulties;
|
||||||
|
|
||||||
namespace ReactorMaintenance.Simulation.Difficulties;
|
|
||||||
|
|
||||||
public class NormalBalancing : Balancing
|
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 DefaultLevelWidth => 16;
|
||||||
public override int DefaultLevelHeight => 12;
|
public override int DefaultLevelHeight => 12;
|
||||||
public override int DefaultRobotCoordinate => 1;
|
public override int MinimumLevelSize => 4;
|
||||||
public override int DefaultPipeFlow => 4;
|
public override int ActionsPerTurn => 3;
|
||||||
public override int DefaultPipePressure => 4;
|
public override int ForecastHorizon => 6;
|
||||||
public override int DefaultPressurePipeFlow => 5;
|
public override float MinValue => 0;
|
||||||
public override int DefaultPressurePipePressure => 6;
|
public override float MaxValue => 10;
|
||||||
public override int DefaultEditedPipeIntegrity => 8;
|
public override float FuelSafe => 1.5f;
|
||||||
public override int MinimumLeakRate => 1;
|
public override float FuelCaution => 3.5f;
|
||||||
public override int DamagedPipeIntegrity => 4;
|
public override float FuelCritical => 6.5f;
|
||||||
public override int RepairedLeakRate => 0;
|
public override float CoolantSafe => 1.5f;
|
||||||
public override int RepairedElectricalCharge => 0;
|
public override float CoolantCaution => 3.5f;
|
||||||
public override int HeatToolIncrease => 2;
|
public override float CoolantCritical => 6.5f;
|
||||||
public override int FireToolMinimumHeat => 7;
|
public override float ElectricitySafe => 1.5f;
|
||||||
public override int FireToolMinimumSmoke => 3;
|
public override float ElectricityCaution => 3.5f;
|
||||||
public override int MaxForecastStepCount => 12;
|
public override float ElectricityCritical => 6.5f;
|
||||||
public override int TurnIncrement => 1;
|
public override float HeatSafe => 2;
|
||||||
public override int OverpressureThreshold => 7;
|
public override float HeatCaution => 5;
|
||||||
public override int HeatIntegrityDamageThreshold => 10;
|
public override float HeatCritical => 8;
|
||||||
public override int PipeFireIntegrityDamage => 1;
|
public override float TerminalHeat => 10;
|
||||||
public override int FireStabilityDamage => 1;
|
public override float RobotFuelSafetyThreshold => 6.5f;
|
||||||
public override int BurstLeakRate => 3;
|
public override float RobotCoolantSafetyThreshold => 8;
|
||||||
public override int BrokenPipeFlow => 0;
|
public override float RobotElectricitySafetyThreshold => 6.5f;
|
||||||
public override int ElectrifiedCoolantPoolingThreshold => 3;
|
public override float RobotHeatSafetyThreshold => 8;
|
||||||
public override int ElectricalChargeIncrease => 2;
|
public override float SourceAmount => 8;
|
||||||
public override int FuelVaporFireThreshold => 4;
|
public override float SourceIntensity => 8;
|
||||||
public override int LiquidFuelFireThreshold => 6;
|
public override float DistanceAmountFalloff => 0.5f;
|
||||||
public override int HeatIgnitionThreshold => 8;
|
public override float DistanceIntensityFalloff => 0.4f;
|
||||||
public override int ElectricalIgnitionThreshold => 4;
|
|
||||||
public override int FireHeatIncrease => 2;
|
public override IReadOnlyList<JunctionRatioPreset> TwoOutflowJunctionRatios { get; } = [
|
||||||
public override int FireSmokeIncrease => 2;
|
new("0/4", [0, 1]),
|
||||||
public override int FireLiquidFuelConsumption => 1;
|
new("1/3", [0.25f, 0.75f]),
|
||||||
public override int FireFuelVaporConsumption => 1;
|
new("2/2", [0.5f, 0.5f]),
|
||||||
public override int SmokeDecay => 1;
|
new("3/1", [0.75f, 0.25f]),
|
||||||
public override int PressurizedFuelLeakPressureThreshold => 7;
|
new("4/0", [1, 0])
|
||||||
public override int PassiveFuelVaporHeatOffset => 3;
|
];
|
||||||
public override int PassiveFuelVaporDivisor => 3;
|
|
||||||
public override int MinimumCoolantHeatReduction => 1;
|
public override IReadOnlyList<JunctionRatioPreset> ThreeOutflowJunctionRatios { get; } = [
|
||||||
public override int CoolantHeatReductionDivisor => 2;
|
new("0/3/3", [0, 0.5f, 0.5f]),
|
||||||
public override int CoolantSteamHeatThreshold => 7;
|
new("3/0/3", [0.5f, 0, 0.5f]),
|
||||||
public override int CoolantSteamSmokeIncrease => 2;
|
new("3/3/0", [0.5f, 0.5f, 0]),
|
||||||
public override int PressureLeakSmokeThreshold => 8;
|
new("2/2/2", [1f / 3f, 1f / 3f, 1f / 3f])
|
||||||
public override int PressureLeakSmokeIncrease => 1;
|
];
|
||||||
public override int GeneratorHeatIncrease => 1;
|
|
||||||
public override int CoolingPumpHeatReduction => 2;
|
public override float ConsumerRequiredAmount => 2.5f;
|
||||||
public override int ReactorHeatIncrease => 1;
|
public override float ConsumerRequiredIntensity => 2.5f;
|
||||||
public override int SmokeSpreadThreshold => 6;
|
public override float LeakBaseAmount => 0.5f;
|
||||||
public override int SmokeSpreadIncrease => 1;
|
public override float LeakAmountScale => 0.15f;
|
||||||
public override int CriticalCellStabilityThreshold => 3;
|
public override float LeakIntensityScale => 0.1f;
|
||||||
public override int MeltdownCoreHeatThreshold => 10;
|
public override float FlowTransferRatio => 0.05f;
|
||||||
public override int StabilityCollapseThreshold => 0;
|
public override float WarmCautionAmount => 0.5f;
|
||||||
public override int GeneratorPowerOutput => 3;
|
public override float WarmCriticalAmount => 1.0f;
|
||||||
public override int CoolingPumpOutput => 3;
|
public override float QuenchCautionAmount => 0.6f;
|
||||||
public override int ReactorReadyPowerThreshold => 3;
|
public override float QuenchCriticalAmount => 1.2f;
|
||||||
public override int ReactorReadyCoolingThreshold => 3;
|
public override float ShortCautionHeat => 0.8f;
|
||||||
public override int ReactorReadyCoreHeatThreshold => 8;
|
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
|
public sealed record JunctionRatioPreset(string Label, float[] Weights);
|
||||||
@@ -1,117 +1,168 @@
|
|||||||
namespace ReactorMaintenance.Simulation;
|
namespace ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
public enum EEditorTool
|
|
||||||
{
|
|
||||||
Floor,
|
|
||||||
Wall,
|
|
||||||
Reactor,
|
|
||||||
CoolingPump,
|
|
||||||
Generator,
|
|
||||||
PressureRegulator,
|
|
||||||
DiagnosticTerminal,
|
|
||||||
ControlTerminal,
|
|
||||||
CoolantPipe,
|
|
||||||
FuelPipe,
|
|
||||||
PressurePipe,
|
|
||||||
Leak,
|
|
||||||
Repair,
|
|
||||||
Heat,
|
|
||||||
Fire,
|
|
||||||
Robot
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LevelEditor
|
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))
|
if (!level.InBounds(position))
|
||||||
return level;
|
return level;
|
||||||
|
|
||||||
if (tool == EEditorTool.Robot)
|
return command.Tool switch {
|
||||||
return level.GetCell(position).IsWalkable ? level with { Robot = position } : level;
|
EEditorTool.Cursor => level,
|
||||||
|
EEditorTool.Floor => level.SetTerrain(position, ECellTerrain.Floor),
|
||||||
var cell = level.GetCell(position);
|
EEditorTool.Wall => level.SetTerrain(position, ECellTerrain.Wall),
|
||||||
cell = tool switch {
|
EEditorTool.Underground => SetUnderground(level, position, command.Carrier),
|
||||||
EEditorTool.Floor => cell with { Terrain = ECellTerrain.Floor },
|
EEditorTool.Flow => SetCarrierProp(level, position, EPropType.Flow, command.Carrier),
|
||||||
EEditorTool.Wall => cell with {
|
EEditorTool.Consumer => SetCarrierProp(level, position, EPropType.Consumer, command.Carrier),
|
||||||
Terrain = ECellTerrain.Wall,
|
EEditorTool.Junction => SetFloorProp(level, position, new() { Type = EPropType.Junction }),
|
||||||
Prop = ECellProp.None,
|
EEditorTool.Door => SetFloorProp(level, position, new() { Type = EPropType.Door }),
|
||||||
Pipe = EPipeMedium.None,
|
EEditorTool.AllSeeingEyeTerminal => SetFloorProp(level, position, new() { Type = EPropType.AllSeeingEyeTerminal }),
|
||||||
Flow = Balancing.Current.MinHazardValue,
|
EEditorTool.RemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = command.RemedyType }),
|
||||||
Pressure = Balancing.Current.MinHazardValue,
|
EEditorTool.ReactorControl => SetReactorControl(level, position),
|
||||||
LeakRate = Balancing.Current.MinHazardValue,
|
EEditorTool.Leak => SetLeak(level, position, command.Carrier),
|
||||||
PipeOpen = false,
|
EEditorTool.SurfaceHazard => AddSurfaceHazard(level, position, command.Carrier),
|
||||||
Powered = false
|
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,
|
||||||
EEditorTool.Reactor => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.Reactor },
|
_ => level
|
||||||
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
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (cell.Terrain == ECellTerrain.Wall)
|
public static LevelState SetDoorEdge(LevelState level, GridPosition a, GridPosition b)
|
||||||
cell = cell with { Hazards = new() };
|
{
|
||||||
|
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
|
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)
|
public static string Serialize(LevelState level)
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(new LevelFile {
|
return JsonSerializer.Serialize(new LevelFile {
|
||||||
Version = c_CurrentVersion,
|
Version = c_CurrentVersion,
|
||||||
Level = level
|
Level = level
|
||||||
}, Options);
|
}, s_Options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LevelState Deserialize(string json)
|
public static LevelState Deserialize(string json)
|
||||||
{
|
{
|
||||||
var file = JsonSerializer.Deserialize<LevelFile>(json, Options) ?? throw new InvalidOperationException("Level file did not contain a level.");
|
var file = JsonSerializer.Deserialize<LevelFile>(json, s_Options) ?? throw new InvalidOperationException("Level file did not contain a level.");
|
||||||
var level = file.Version switch {
|
if (file.Version != c_CurrentVersion)
|
||||||
c_CurrentVersion => file.Level,
|
throw new InvalidOperationException($"Unsupported level file version {file.Version}. Expected {c_CurrentVersion}.");
|
||||||
_ => throw new InvalidOperationException($"Unsupported level file version {file.Version}.")
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
WriteIndented = true,
|
||||||
Converters = { new JsonStringEnumConverter() }
|
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
|
public enum EBand
|
||||||
|
{
|
||||||
|
Safe,
|
||||||
|
Caution,
|
||||||
|
Critical
|
||||||
|
}
|
||||||
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
@@ -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
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
|
public enum EDoorState
|
||||||
|
{
|
||||||
|
Open,
|
||||||
|
Closed
|
||||||
|
}
|
||||||
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
@@ -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
@@ -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
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
|
public enum ERemedyType
|
||||||
|
{
|
||||||
|
FuelNeutralizer,
|
||||||
|
CoolantNeutralizer,
|
||||||
|
ElectricityNeutralizer,
|
||||||
|
HeatShield
|
||||||
|
}
|
||||||
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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
|
public sealed record GridPosition(int X, int Y);
|
||||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
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
@@ -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;
|
namespace ReactorMaintenance.Simulation;
|
||||||
using ReactorMaintenance.Simulation.Hazards;
|
|
||||||
|
|
||||||
namespace ReactorMaintenance.Simulation;
|
public sealed class SimulationEngine
|
||||||
|
|
||||||
public sealed class SimulationEngine(IEnumerable<ISimulationEffect> effects, IEnumerable<IAreaSimulationEffect> areaEffects, IEnumerable<Hazard> hazards)
|
|
||||||
{
|
{
|
||||||
private sealed record ForecastKey(EFailureKind Kind, GridPosition? Position);
|
public LevelState MoveRobot(LevelState level, GridPosition destination)
|
||||||
|
|
||||||
public SimulationEngine()
|
|
||||||
: this(
|
|
||||||
[new PipeLeakEffect(), new MachineEffect(), new FireAndElectricalHazardEffect(), new CellIntegrityEffect()],
|
|
||||||
[new SmokeSpreadEffect()],
|
|
||||||
[new PipeBurstHazard(), new IgnitionHazard(), new MeltdownHazard(), new StabilityCollapseHazard()])
|
|
||||||
{
|
{
|
||||||
|
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>();
|
return PlayerActionSystem.InteractLeak(level, carrier, useRemedy, SpendAction);
|
||||||
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);
|
|
||||||
|
|
||||||
AddReactorReadyForecast(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
|
public LevelState ApplyHeatShield(LevelState level)
|
||||||
|
{
|
||||||
if (IsReactorReady(forecastLevel) || forecastLevel.Global.Lost || forecastLevel.Global.ReactorActivated)
|
return PlayerActionSystem.ApplyHeatShield(level, SpendAction);
|
||||||
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 ActivateReactor(LevelState level)
|
public LevelState ActivateReactor(LevelState level)
|
||||||
{
|
{
|
||||||
if (!IsReactorReady(level))
|
return ReactorSystem.Activate(level);
|
||||||
return level with { Global = level.Global with { Status = "REACTOR NOT READY" } };
|
}
|
||||||
|
|
||||||
return level with {
|
public LevelState EndTurn(LevelState level)
|
||||||
Global = level.Global with {
|
{
|
||||||
ReactorActivated = true,
|
return ResolveTurn(level with { Global = level.Global with { ActionsRemaining = 0 } });
|
||||||
Status = "REACTOR ONLINE"
|
}
|
||||||
|
|
||||||
|
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)
|
private readonly LevelValidator m_Validator = new();
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
BIN
src/ReactorMaintenance.Win2D/Images/Pipes/pipe-fuel-tilemap.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
BIN
src/ReactorMaintenance.Win2D/Images/Props/control-terminal.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
src/ReactorMaintenance.Win2D/Images/Props/cooling-pump.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/ReactorMaintenance.Win2D/Images/Props/cursor.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
BIN
src/ReactorMaintenance.Win2D/Images/Props/fire.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
src/ReactorMaintenance.Win2D/Images/Props/floor.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/ReactorMaintenance.Win2D/Images/Props/generator.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |