Compare commits
34 Commits
c406bf9d73
...
design-ite
| Author | SHA1 | Date | |
|---|---|---|---|
| 672f055a80 | |||
| adf1475fc0 | |||
| 06d37aac10 | |||
| 99482c7011 | |||
| fbb7c0490c | |||
| dfe0cb3b6a | |||
| 884cc4503f | |||
| 0651603fd2 | |||
| 69ed79ce86 | |||
| e1ac56d201 | |||
| 3d406179bf | |||
| 787f1e5e85 | |||
| 5ddd1b8ec8 | |||
| 1b9372ff7c | |||
| 3a52db0071 | |||
| 5a186fb606 | |||
| 6c7fa070f6 | |||
| d22c4a7528 | |||
| 7ffaa140a8 | |||
| 3c5fc60ffe | |||
| 9cd9defc0b | |||
| 1aa9734e08 | |||
| a0b10423ac | |||
| cb28eee1dd | |||
| b232c0319f | |||
| 30963a9bde | |||
| 851f6d27e8 | |||
| ca41e009bd | |||
| 79f3219a72 | |||
| 071e6a1d48 | |||
| 810478ddee | |||
| bb8d1adb10 | |||
| c8795d582c | |||
| 2376edab0d |
@@ -20,9 +20,10 @@ insert_final_newline = false
|
||||
# Organize usings
|
||||
dotnet_separate_import_directive_groups = 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_field = 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_members_in_anonymous_types = 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
|
||||
|
||||
# Indentation preferences
|
||||
@@ -228,27 +229,27 @@ dotnet_naming_rule.local_should_be_camelcase.style = camelcase
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.struct.applicable_kinds = struct
|
||||
dotnet_naming_symbols.struct.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.struct.required_modifiers =
|
||||
dotnet_naming_symbols.struct.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.delegate.applicable_kinds = delegate
|
||||
dotnet_naming_symbols.delegate.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.delegate.required_modifiers =
|
||||
dotnet_naming_symbols.delegate.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.event.applicable_kinds = event
|
||||
dotnet_naming_symbols.event.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.event.required_modifiers =
|
||||
dotnet_naming_symbols.event.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.property.applicable_kinds = property
|
||||
dotnet_naming_symbols.property.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.property.required_modifiers =
|
||||
dotnet_naming_symbols.property.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.public_or_protected_field.applicable_kinds = field
|
||||
dotnet_naming_symbols.public_or_protected_field.applicable_accessibilities = public, protected
|
||||
dotnet_naming_symbols.public_or_protected_field.required_modifiers =
|
||||
dotnet_naming_symbols.public_or_protected_field.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.static_field.applicable_kinds = field
|
||||
dotnet_naming_symbols.static_field.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
@@ -256,7 +257,7 @@ dotnet_naming_symbols.static_field.required_modifiers = static
|
||||
|
||||
dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
|
||||
dotnet_naming_symbols.private_or_internal_field.required_modifiers =
|
||||
dotnet_naming_symbols.private_or_internal_field.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.private_or_internal_static_field.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_or_internal_static_field.applicable_accessibilities = internal, private, private_protected
|
||||
@@ -264,19 +265,19 @@ dotnet_naming_symbols.private_or_internal_static_field.required_modifiers = stat
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.parameter.applicable_kinds = parameter
|
||||
dotnet_naming_symbols.parameter.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.parameter.required_modifiers =
|
||||
dotnet_naming_symbols.parameter.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.local.applicable_kinds = local
|
||||
dotnet_naming_symbols.local.applicable_accessibilities = local
|
||||
dotnet_naming_symbols.local.required_modifiers =
|
||||
dotnet_naming_symbols.local.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.constant_field.applicable_kinds = field
|
||||
dotnet_naming_symbols.constant_field.applicable_accessibilities = *
|
||||
@@ -284,49 +285,49 @@ dotnet_naming_symbols.constant_field.required_modifiers = const
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.begin_with_s.required_prefix = S
|
||||
dotnet_naming_style.begin_with_s.required_suffix =
|
||||
dotnet_naming_style.begin_with_s.word_separator =
|
||||
dotnet_naming_style.begin_with_s.required_suffix =
|
||||
dotnet_naming_style.begin_with_s.word_separator =
|
||||
dotnet_naming_style.begin_with_s.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.starts_with_m_.required_prefix = m_
|
||||
dotnet_naming_style.starts_with_m_.required_suffix =
|
||||
dotnet_naming_style.starts_with_m_.word_separator =
|
||||
dotnet_naming_style.starts_with_m_.required_suffix =
|
||||
dotnet_naming_style.starts_with_m_.word_separator =
|
||||
dotnet_naming_style.starts_with_m_.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.starts_with_s_.required_prefix = s_
|
||||
dotnet_naming_style.starts_with_s_.required_suffix =
|
||||
dotnet_naming_style.starts_with_s_.word_separator =
|
||||
dotnet_naming_style.starts_with_s_.required_suffix =
|
||||
dotnet_naming_style.starts_with_s_.word_separator =
|
||||
dotnet_naming_style.starts_with_s_.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.end_with_delegate.required_prefix =
|
||||
dotnet_naming_style.end_with_delegate.required_prefix =
|
||||
dotnet_naming_style.end_with_delegate.required_suffix = Delegate
|
||||
dotnet_naming_style.end_with_delegate.word_separator =
|
||||
dotnet_naming_style.end_with_delegate.word_separator =
|
||||
dotnet_naming_style.end_with_delegate.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.end_with_event.required_prefix =
|
||||
dotnet_naming_style.end_with_event.required_prefix =
|
||||
dotnet_naming_style.end_with_event.required_suffix = Event
|
||||
dotnet_naming_style.end_with_event.word_separator =
|
||||
dotnet_naming_style.end_with_event.word_separator =
|
||||
dotnet_naming_style.end_with_event.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.camelcase.required_prefix =
|
||||
dotnet_naming_style.camelcase.required_suffix =
|
||||
dotnet_naming_style.camelcase.word_separator =
|
||||
dotnet_naming_style.camelcase.required_prefix =
|
||||
dotnet_naming_style.camelcase.required_suffix =
|
||||
dotnet_naming_style.camelcase.word_separator =
|
||||
dotnet_naming_style.camelcase.capitalization = camel_case
|
||||
|
||||
dotnet_naming_style.begins_with_c_.required_prefix = c_
|
||||
dotnet_naming_style.begins_with_c_.required_suffix =
|
||||
dotnet_naming_style.begins_with_c_.word_separator =
|
||||
dotnet_naming_style.begins_with_c_.required_suffix =
|
||||
dotnet_naming_style.begins_with_c_.word_separator =
|
||||
dotnet_naming_style.begins_with_c_.capitalization = pascal_case
|
||||
|
||||
# ReSharper properties
|
||||
@@ -381,7 +382,7 @@ resharper_csharp_wrap_chained_method_calls = chop_if_long
|
||||
resharper_extra_spaces = remove_all
|
||||
resharper_for_built_in_types = use_var
|
||||
resharper_indent_preprocessor_region = no_indent
|
||||
resharper_instance_members_qualify_declared_in =
|
||||
resharper_instance_members_qualify_declared_in =
|
||||
resharper_int_align_switch_expressions = false
|
||||
resharper_int_align_switch_sections = false
|
||||
resharper_keep_existing_arrangement = true
|
||||
@@ -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.style = pascal_case
|
||||
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.required_modifiers = const
|
||||
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_readonly_symbols.applicable_accessibilities = private
|
||||
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_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.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.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_kinds = namespace,class,struct,enum,delegate
|
||||
dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds = namespace, class, struct, enum, delegate
|
||||
|
||||
# ReSharper inspection severities
|
||||
resharper_arrange_accessor_owner_body_highlighting = suggestion
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
.idea
|
||||
@@ -1,3 +1,3 @@
|
||||
# Linux-specific instructions
|
||||
|
||||
- After every iteration, run `dotnet jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
|
||||
- After every iteration, run `dotnet jb cleanupcode --verbosity:ERROR ./ReactorMaintenance.slnx`.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Windows-specific instructions
|
||||
|
||||
- After the implementation is finished, run `python D:\Code\crlf.py $file1 $file2 ...` for changed files you recognize, in order to normalize all line endings of all touched files to CRLF.
|
||||
- After every iteration, run `jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
|
||||
- After every iteration, run `jb cleanupcode --verbosity:ERROR ReactorMaintenance.slnx`.
|
||||
20
README.md
20
README.md
@@ -1,18 +1,28 @@
|
||||
# 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
|
||||
|
||||
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, versioned JSON serialization, and swappable difficulty balancing profiles.
|
||||
- `src/ReactorMaintenance.Win2D`: Win2D editor app for painting floor/wall terrain plus cell props, loading/saving levels, advancing simulation turns, and activating the reactor.
|
||||
- `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior.
|
||||
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, validation, forecasts, simulation turns, versioned JSON serialization, and deterministic balancing defaults.
|
||||
- `src/ReactorMaintenance.Win2D`: Win2D editor app for authoring terrain, underground fuel/coolant/electricity networks, props, explicit leak access faces, door edges, reactor consumer bindings, rule events, surface hazards, robot start, loading/saving levels, ending turns, interacting with props, and activating a ready reactor.
|
||||
- `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior, validation, serialization, and editor operations.
|
||||
|
||||
## Editor Controls
|
||||
|
||||
- Left click selects or paints with the current tool. Right click clears the selected cell's prop, surface hazards, leaks, doors, and reactor control.
|
||||
- Door authoring is explicit: select the Door tool, click the door cell, then click the adjacent floor cell that defines the blocked edge.
|
||||
- Electricity wall leaks are explicit: select the Electricity Leak tool, click the wall network cell, then click the adjacent floor access face.
|
||||
- Reactor bindings are explicit: select or place a reactor control, select a matching consumer cell, then use the Fuel, Coolant, or Electric binding action in the inspector.
|
||||
- Rule event authoring is available from the inspector for next-turn warnings and selected-cell leak events; authored events are saved in the version 2 JSON schema.
|
||||
|
||||
## Commands
|
||||
|
||||
```powershell
|
||||
dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj
|
||||
dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
|
||||
dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64 -p:EnableWindowsTargeting=true
|
||||
dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
|
||||
```
|
||||
|
||||
The WinUI/XAML compiler is Windows-specific. On Linux, the simulation tests run normally, but the Win2D app build must be verified in a Windows-capable environment.
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/ReactorMaintenance.Simulation/ReactorMaintenance.Simulation.csproj" />
|
||||
<Project Path="src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj">
|
||||
<Platform Project="x86" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/ReactorMaintenance.Simulation/ReactorMaintenance.Simulation.csproj"/>
|
||||
<Project Path="src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj">
|
||||
<Platform Project="x86"/>
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj"/>
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
84
TASKS.md
Normal file
84
TASKS.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Reactor Maintenance Rewrite Tasks
|
||||
|
||||
## Current State
|
||||
- Approved design iteration targets the simulation model, rule removal, action economy, reactor requirements, and editor layer workflow.
|
||||
- Work is proceeding on branch `design-iteration-structural-editor` in methodical commits.
|
||||
- Completed commits:
|
||||
- `787f1e5` Document approved design iteration.
|
||||
- `3d40617` Restore complete design system documentation.
|
||||
- `e1ac56d` Rework simulation rules.
|
||||
- Design documentation must preserve every existing system-level rule unless a change explicitly supersedes it. Superseded sections must document the replacement behavior with equal detail.
|
||||
- The next implementation iteration is the Win2D editor overhaul.
|
||||
|
||||
## Completed Work
|
||||
- Created the approved implementation plan for:
|
||||
- single multi-service consumers,
|
||||
- count-based reactor requirements,
|
||||
- cell-derived doors,
|
||||
- 0-10 structural integrity,
|
||||
- fixed automatic rule systems,
|
||||
- quick/lengthy action economy,
|
||||
- all-seeing-eye viewing without persistent unlocking,
|
||||
- layer-aware editor visualization and tools.
|
||||
- Repaired the design documentation after the hazard-interaction regression:
|
||||
- restored the complete surface hazard interaction matrix,
|
||||
- documented that leaked coolant plus leaked fuel directly holds unless mediated by heat/electricity,
|
||||
- expanded structural integrity, consumer, reactor, all-seeing-eye, and action-economy details.
|
||||
- Reworked simulation state and systems:
|
||||
- removed data-driven rule predicates/effects/events from runtime state, validation, forecasts, serialization, and tests,
|
||||
- replaced explicit reactor consumer bindings with unbound reactor controls plus required fuel/coolant/electricity consumer counts,
|
||||
- made consumer props carrier-agnostic with per-carrier service state derived from networks beneath the cell,
|
||||
- moved doors from explicit edge state to door props on floor cells with orientation inferred from opposing wall cells,
|
||||
- added underground structural integrity, high-pressure degradation, automatic leak creation, structural forecasts, and repair-to-max behavior,
|
||||
- removed action budgets and made movement quick while mutating interactions resolve one simulation step,
|
||||
- removed persistent all-seeing-eye unlocking from simulation state,
|
||||
- bumped serialized level schema to version 3.
|
||||
- Updated simulation tests for the replacement systems:
|
||||
- consumer derivation and disabled consumer service state,
|
||||
- count-based reactor readiness and reactor-under-network positive-flow requirement,
|
||||
- quick movement versus lengthy door interaction,
|
||||
- inferred door blocking,
|
||||
- structural degradation, automatic leaks, repair integrity reset,
|
||||
- schema version 3 round-tripping and old-schema rejection.
|
||||
- Kept the Win2D project compiling after the simulation model changes with narrow compatibility edits. Full editor workflow/rendering work remains outstanding.
|
||||
- Verified after the simulation rework:
|
||||
- `dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj` passed with 23 tests,
|
||||
- `dotnet build ReactorMaintenance.slnx` passed with 0 warnings.
|
||||
- Reworked the Win2D editor workflow:
|
||||
- added the Surface/Electricity/Fuel/Coolant layer combobox,
|
||||
- filtered tools by active layer and fixed exclusive tool selection,
|
||||
- rendered underground networks as carrier-colored centerline networks with source dots and layer opacity rules,
|
||||
- removed Rule Events, Reactor Binding, and pending workflow panels from the editor UI,
|
||||
- replaced two-click electricity leak authoring with electric-layer leak access cycling,
|
||||
- made Shift+left drag pan in all tools and Cursor drag move the robot or props.
|
||||
- Added editor-helper tests for electricity leak access cycling and cursor drag movement behavior.
|
||||
- Verified after the editor overhaul:
|
||||
- `dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj` passed with 26 tests,
|
||||
- `dotnet build ReactorMaintenance.slnx` passed with 0 warnings.
|
||||
|
||||
## Current Work
|
||||
- Editor overhaul implementation is complete; commit is pending.
|
||||
|
||||
## Editor Overhaul Requirements
|
||||
- Add a layer combobox with Surface, Electricity, Fuel, and Coolant.
|
||||
- When Surface is active, draw the surface layer at full opacity and all underground layers at 25% opacity.
|
||||
- When an underground layer is active, draw the surface layer at 50% opacity, other underground layers at 25% opacity, and the active underground layer at full opacity.
|
||||
- Render coolant blue, fuel red, and electricity yellow.
|
||||
- Render networks as thick lines connecting adjacent cell centers; render sources as large centered dots.
|
||||
- Make tools layer-aware:
|
||||
- Cursor is always available.
|
||||
- Heat, Floor, Walls, Props, Consumers, Hazards, and Doors are only available for Surface.
|
||||
- Network painting and Sources are only available on their respective underground layers.
|
||||
- Selecting a tool must deselect all other tools. The current two-way binding can leave multiple tools selected.
|
||||
- Shift+LMB should pan the view in all tools, including Cursor mode.
|
||||
- Cursor LMB drag should move any prop or robot from one cell to another.
|
||||
- Remove Rule Events UI.
|
||||
- Remove Reactor Binding UI.
|
||||
- Remove editor workflow and pending actions.
|
||||
- Door cells are redesigned as single prop cells.
|
||||
- Electricity leak neighbour should be toggled by using the electric leak tool on an existing electric leak cell.
|
||||
|
||||
## Future Work
|
||||
- Add authored sample levels once the new schema stabilizes.
|
||||
- Tune structural integrity balancing after playtesting.
|
||||
- Extend UI affordances for inspecting per-carrier consumer service state.
|
||||
1748
docs/design.md
1748
docs/design.md
File diff suppressed because it is too large
Load Diff
13
dotnet-tools.json
Normal file
13
dotnet-tools.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"jetbrains.resharper.globaltools": {
|
||||
"version": "2026.1.1",
|
||||
"commands": [
|
||||
"jb"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +1,173 @@
|
||||
using ReactorMaintenance.Simulation.Difficulties;
|
||||
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public abstract class Balancing
|
||||
{
|
||||
public static Balancing Current { get; set; } = new NormalBalancing();
|
||||
|
||||
public abstract int MinHazardValue { get; }
|
||||
public abstract int MaxHazardValue { get; }
|
||||
public abstract int DefaultHazardStability { get; }
|
||||
public abstract int DefaultCellIntegrity { get; }
|
||||
public abstract int DefaultActionsPerTurn { get; }
|
||||
public abstract int DefaultCoreHeat { get; }
|
||||
public abstract int DefaultFacilityStability { get; }
|
||||
public abstract int DefaultPower { get; }
|
||||
public abstract int DefaultCooling { get; }
|
||||
public abstract int FirstGridCoordinate { get; }
|
||||
public abstract int NeighborDistance { get; }
|
||||
public abstract int CurrentForecastTurn { get; }
|
||||
public abstract int MinimumLevelSize { get; }
|
||||
public abstract int DefaultLevelWidth { get; }
|
||||
public abstract int DefaultLevelHeight { get; }
|
||||
public abstract int DefaultRobotCoordinate { get; }
|
||||
public abstract int DefaultPipeFlow { get; }
|
||||
public abstract int DefaultPipePressure { get; }
|
||||
public abstract int DefaultPressurePipeFlow { get; }
|
||||
public abstract int DefaultPressurePipePressure { get; }
|
||||
public abstract int DefaultEditedPipeIntegrity { get; }
|
||||
public abstract int MinimumLeakRate { get; }
|
||||
public abstract int DamagedPipeIntegrity { get; }
|
||||
public abstract int RepairedLeakRate { get; }
|
||||
public abstract int RepairedElectricalCharge { get; }
|
||||
public abstract int HeatToolIncrease { get; }
|
||||
public abstract int FireToolMinimumHeat { get; }
|
||||
public abstract int FireToolMinimumSmoke { get; }
|
||||
public abstract int MaxForecastStepCount { get; }
|
||||
public abstract int TurnIncrement { get; }
|
||||
public abstract int OverpressureThreshold { get; }
|
||||
public abstract int HeatIntegrityDamageThreshold { get; }
|
||||
public abstract int PipeFireIntegrityDamage { get; }
|
||||
public abstract int FireStabilityDamage { get; }
|
||||
public abstract int BurstLeakRate { get; }
|
||||
public abstract int BrokenPipeFlow { get; }
|
||||
public abstract int ElectrifiedCoolantPoolingThreshold { get; }
|
||||
public abstract int ElectricalChargeIncrease { get; }
|
||||
public abstract int FuelVaporFireThreshold { get; }
|
||||
public abstract int LiquidFuelFireThreshold { get; }
|
||||
public abstract int HeatIgnitionThreshold { get; }
|
||||
public abstract int ElectricalIgnitionThreshold { get; }
|
||||
public abstract int FireHeatIncrease { get; }
|
||||
public abstract int FireSmokeIncrease { get; }
|
||||
public abstract int FireLiquidFuelConsumption { get; }
|
||||
public abstract int FireFuelVaporConsumption { get; }
|
||||
public abstract int SmokeDecay { get; }
|
||||
public abstract int PressurizedFuelLeakPressureThreshold { get; }
|
||||
public abstract int PassiveFuelVaporHeatOffset { get; }
|
||||
public abstract int PassiveFuelVaporDivisor { get; }
|
||||
public abstract int MinimumCoolantHeatReduction { get; }
|
||||
public abstract int CoolantHeatReductionDivisor { get; }
|
||||
public abstract int CoolantSteamHeatThreshold { get; }
|
||||
public abstract int CoolantSteamSmokeIncrease { get; }
|
||||
public abstract int PressureLeakSmokeThreshold { get; }
|
||||
public abstract int PressureLeakSmokeIncrease { get; }
|
||||
public abstract int GeneratorHeatIncrease { get; }
|
||||
public abstract int CoolingPumpHeatReduction { get; }
|
||||
public abstract int ReactorHeatIncrease { get; }
|
||||
public abstract int SmokeSpreadThreshold { get; }
|
||||
public abstract int SmokeSpreadIncrease { get; }
|
||||
public abstract int CriticalCellStabilityThreshold { get; }
|
||||
public abstract int MeltdownCoreHeatThreshold { get; }
|
||||
public abstract int StabilityCollapseThreshold { get; }
|
||||
public abstract int GeneratorPowerOutput { get; }
|
||||
public abstract int CoolingPumpOutput { get; }
|
||||
public abstract int ReactorReadyPowerThreshold { get; }
|
||||
public abstract int ReactorReadyCoolingThreshold { get; }
|
||||
public abstract int ReactorReadyCoreHeatThreshold { get; }
|
||||
using ReactorMaintenance.Simulation.Difficulties;
|
||||
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public abstract class Balancing
|
||||
{
|
||||
public float ClampValue(float value)
|
||||
{
|
||||
return Math.Clamp(value, MinValue, MaxValue);
|
||||
}
|
||||
|
||||
public EBand Band(float value, float caution, float critical)
|
||||
{
|
||||
if (value >= critical)
|
||||
return EBand.Critical;
|
||||
|
||||
return value >= caution ? EBand.Caution : EBand.Safe;
|
||||
}
|
||||
|
||||
public IReadOnlyList<JunctionRatioPreset> JunctionRatios(int outflowCount)
|
||||
{
|
||||
return outflowCount switch {
|
||||
2 => TwoOutflowJunctionRatios,
|
||||
3 => ThreeOutflowJunctionRatios,
|
||||
_ => Array.Empty<JunctionRatioPreset>()
|
||||
};
|
||||
}
|
||||
|
||||
public float[] JunctionWeights(int outflowCount, int mode)
|
||||
{
|
||||
var ratios = JunctionRatios(outflowCount);
|
||||
if (ratios.Count == 0)
|
||||
return Array.Empty<float>();
|
||||
|
||||
return ratios[Math.Clamp(mode, 0, ratios.Count - 1)].Weights;
|
||||
}
|
||||
|
||||
public SurfaceInteractionEffect SameCellInteraction(ECarrierType? rowCarrier, EBand rowBand, ECarrierType? colCarrier, EBand colBand)
|
||||
{
|
||||
if (rowBand == EBand.Safe && colBand == EBand.Safe)
|
||||
return SurfaceInteractionEffect.Hold;
|
||||
|
||||
if (rowCarrier == ECarrierType.Fuel && colCarrier == ECarrierType.Electricity)
|
||||
return Ignite(rowBand, colBand);
|
||||
|
||||
if (rowCarrier == ECarrierType.Fuel && colCarrier is null)
|
||||
return rowBand == EBand.Critical || colBand == EBand.Critical ? Ignite(rowBand, colBand) : Warm(rowBand, colBand);
|
||||
|
||||
if (rowCarrier == ECarrierType.Coolant && colCarrier == ECarrierType.Electricity)
|
||||
return Short(rowBand, colBand);
|
||||
|
||||
if (rowCarrier == ECarrierType.Coolant && colCarrier is null)
|
||||
return Quench(rowBand, colBand);
|
||||
|
||||
return SurfaceInteractionEffect.Hold;
|
||||
}
|
||||
|
||||
public SurfaceInteractionEffect FlowInteraction(ESurfaceQuantity quantity)
|
||||
{
|
||||
return new() { Verb = ESurfaceInteractionVerb.Flow, Quantity = quantity, Amount = FlowTransferRatio };
|
||||
}
|
||||
|
||||
public float StructuralPressureThreshold(ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => FuelCritical,
|
||||
ECarrierType.Coolant => CoolantCritical,
|
||||
ECarrierType.Electricity => ElectricityCritical,
|
||||
_ => MaxValue
|
||||
};
|
||||
}
|
||||
|
||||
private SurfaceInteractionEffect Warm(EBand rowBand, EBand colBand)
|
||||
{
|
||||
return new() {
|
||||
Verb = ESurfaceInteractionVerb.Warm,
|
||||
Quantity = ESurfaceQuantity.Heat,
|
||||
Amount = Strongest(rowBand, colBand) == EBand.Critical ? WarmCriticalAmount : WarmCautionAmount
|
||||
};
|
||||
}
|
||||
|
||||
private SurfaceInteractionEffect Quench(EBand rowBand, EBand colBand)
|
||||
{
|
||||
return new() {
|
||||
Verb = ESurfaceInteractionVerb.Quench,
|
||||
Quantity = ESurfaceQuantity.Heat,
|
||||
Amount = Strongest(rowBand, colBand) == EBand.Critical ? QuenchCriticalAmount : QuenchCautionAmount
|
||||
};
|
||||
}
|
||||
|
||||
private SurfaceInteractionEffect Short(EBand rowBand, EBand colBand)
|
||||
{
|
||||
var critical = Strongest(rowBand, colBand) == EBand.Critical;
|
||||
return new() {
|
||||
Verb = ESurfaceInteractionVerb.Short,
|
||||
Quantity = ESurfaceQuantity.Electricity,
|
||||
Amount = critical ? ShortCriticalHeat : ShortCautionHeat,
|
||||
SecondaryAmount = critical ? ShortCriticalDischarge : ShortCautionDischarge
|
||||
};
|
||||
}
|
||||
|
||||
private SurfaceInteractionEffect Ignite(EBand rowBand, EBand colBand)
|
||||
{
|
||||
var critical = Strongest(rowBand, colBand) == EBand.Critical;
|
||||
return new() {
|
||||
Verb = ESurfaceInteractionVerb.Ignite,
|
||||
Quantity = ESurfaceQuantity.Fuel,
|
||||
Amount = critical ? IgniteCriticalHeat : IgniteCautionHeat,
|
||||
SecondaryAmount = critical ? IgniteCriticalFuelConsumption : IgniteCautionFuelConsumption
|
||||
};
|
||||
}
|
||||
|
||||
private static EBand Strongest(EBand a, EBand b)
|
||||
{
|
||||
return a > b ? a : b;
|
||||
}
|
||||
|
||||
public static Balancing Current { get; set; } = new NormalBalancing();
|
||||
|
||||
public abstract int DefaultLevelWidth { get; }
|
||||
public abstract int DefaultLevelHeight { get; }
|
||||
public abstract int MinimumLevelSize { get; }
|
||||
public abstract int ForecastHorizon { get; }
|
||||
public abstract float MinValue { get; }
|
||||
public abstract float MaxValue { get; }
|
||||
public abstract float FuelSafe { get; }
|
||||
public abstract float FuelCaution { get; }
|
||||
public abstract float FuelCritical { get; }
|
||||
public abstract float CoolantSafe { get; }
|
||||
public abstract float CoolantCaution { get; }
|
||||
public abstract float CoolantCritical { get; }
|
||||
public abstract float ElectricitySafe { get; }
|
||||
public abstract float ElectricityCaution { get; }
|
||||
public abstract float ElectricityCritical { get; }
|
||||
public abstract float HeatSafe { get; }
|
||||
public abstract float HeatCaution { get; }
|
||||
public abstract float HeatCritical { get; }
|
||||
public abstract float TerminalHeat { get; }
|
||||
public abstract float RobotFuelSafetyThreshold { get; }
|
||||
public abstract float RobotCoolantSafetyThreshold { get; }
|
||||
public abstract float RobotElectricitySafetyThreshold { get; }
|
||||
public abstract float RobotHeatSafetyThreshold { get; }
|
||||
public abstract float SourceAmount { get; }
|
||||
public abstract float SourceIntensity { get; }
|
||||
public abstract float DistanceAmountFalloff { get; }
|
||||
public abstract float DistanceIntensityFalloff { get; }
|
||||
public abstract IReadOnlyList<JunctionRatioPreset> TwoOutflowJunctionRatios { get; }
|
||||
public abstract IReadOnlyList<JunctionRatioPreset> ThreeOutflowJunctionRatios { get; }
|
||||
public abstract float ConsumerRequiredAmount { get; }
|
||||
public abstract float ConsumerRequiredIntensity { get; }
|
||||
public abstract int MaxStructuralIntegrity { get; }
|
||||
public abstract int StructuralIntegrityLeakThreshold { get; }
|
||||
public abstract float StructuralIntegrityDamageScale { get; }
|
||||
public abstract float LeakBaseAmount { get; }
|
||||
public abstract float LeakAmountScale { get; }
|
||||
public abstract float LeakIntensityScale { get; }
|
||||
public abstract float FlowTransferRatio { get; }
|
||||
public abstract float WarmCautionAmount { get; }
|
||||
public abstract float WarmCriticalAmount { get; }
|
||||
public abstract float QuenchCautionAmount { get; }
|
||||
public abstract float QuenchCriticalAmount { get; }
|
||||
public abstract float ShortCautionHeat { get; }
|
||||
public abstract float ShortCautionDischarge { get; }
|
||||
public abstract float ShortCriticalHeat { get; }
|
||||
public abstract float ShortCriticalDischarge { get; }
|
||||
public abstract float IgniteCautionHeat { get; }
|
||||
public abstract float IgniteCautionFuelConsumption { get; }
|
||||
public abstract float IgniteCriticalHeat { get; }
|
||||
public abstract float IgniteCriticalFuelConsumption { get; }
|
||||
public abstract int RemedyBlockTurns { get; }
|
||||
public abstract int HeatShieldSteps { get; }
|
||||
public abstract int InventoryCapacityPerRemedy { get; }
|
||||
}
|
||||
@@ -1,76 +1,72 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Difficulties;
|
||||
|
||||
public class NormalBalancing : Balancing
|
||||
{
|
||||
public override int MinHazardValue => 0;
|
||||
public override int MaxHazardValue => 10;
|
||||
public override int DefaultHazardStability => 10;
|
||||
public override int DefaultCellIntegrity => 10;
|
||||
public override int DefaultActionsPerTurn => 3;
|
||||
public override int DefaultCoreHeat => 5;
|
||||
public override int DefaultFacilityStability => 10;
|
||||
public override int DefaultPower => 5;
|
||||
public override int DefaultCooling => 0;
|
||||
public override int FirstGridCoordinate => 0;
|
||||
public override int NeighborDistance => 1;
|
||||
public override int CurrentForecastTurn => 0;
|
||||
public override int MinimumLevelSize => 4;
|
||||
public override int DefaultLevelWidth => 16;
|
||||
public override int DefaultLevelHeight => 12;
|
||||
public override int DefaultRobotCoordinate => 1;
|
||||
public override int DefaultPipeFlow => 4;
|
||||
public override int DefaultPipePressure => 4;
|
||||
public override int DefaultPressurePipeFlow => 5;
|
||||
public override int DefaultPressurePipePressure => 6;
|
||||
public override int DefaultEditedPipeIntegrity => 8;
|
||||
public override int MinimumLeakRate => 1;
|
||||
public override int DamagedPipeIntegrity => 4;
|
||||
public override int RepairedLeakRate => 0;
|
||||
public override int RepairedElectricalCharge => 0;
|
||||
public override int HeatToolIncrease => 2;
|
||||
public override int FireToolMinimumHeat => 7;
|
||||
public override int FireToolMinimumSmoke => 3;
|
||||
public override int MaxForecastStepCount => 12;
|
||||
public override int TurnIncrement => 1;
|
||||
public override int OverpressureThreshold => 7;
|
||||
public override int HeatIntegrityDamageThreshold => 10;
|
||||
public override int PipeFireIntegrityDamage => 1;
|
||||
public override int FireStabilityDamage => 1;
|
||||
public override int BurstLeakRate => 3;
|
||||
public override int BrokenPipeFlow => 0;
|
||||
public override int ElectrifiedCoolantPoolingThreshold => 3;
|
||||
public override int ElectricalChargeIncrease => 2;
|
||||
public override int FuelVaporFireThreshold => 4;
|
||||
public override int LiquidFuelFireThreshold => 6;
|
||||
public override int HeatIgnitionThreshold => 8;
|
||||
public override int ElectricalIgnitionThreshold => 4;
|
||||
public override int FireHeatIncrease => 2;
|
||||
public override int FireSmokeIncrease => 2;
|
||||
public override int FireLiquidFuelConsumption => 1;
|
||||
public override int FireFuelVaporConsumption => 1;
|
||||
public override int SmokeDecay => 1;
|
||||
public override int PressurizedFuelLeakPressureThreshold => 7;
|
||||
public override int PassiveFuelVaporHeatOffset => 3;
|
||||
public override int PassiveFuelVaporDivisor => 3;
|
||||
public override int MinimumCoolantHeatReduction => 1;
|
||||
public override int CoolantHeatReductionDivisor => 2;
|
||||
public override int CoolantSteamHeatThreshold => 7;
|
||||
public override int CoolantSteamSmokeIncrease => 2;
|
||||
public override int PressureLeakSmokeThreshold => 8;
|
||||
public override int PressureLeakSmokeIncrease => 1;
|
||||
public override int GeneratorHeatIncrease => 1;
|
||||
public override int CoolingPumpHeatReduction => 2;
|
||||
public override int ReactorHeatIncrease => 1;
|
||||
public override int SmokeSpreadThreshold => 6;
|
||||
public override int SmokeSpreadIncrease => 1;
|
||||
public override int CriticalCellStabilityThreshold => 3;
|
||||
public override int MeltdownCoreHeatThreshold => 10;
|
||||
public override int StabilityCollapseThreshold => 0;
|
||||
public override int GeneratorPowerOutput => 3;
|
||||
public override int CoolingPumpOutput => 3;
|
||||
public override int ReactorReadyPowerThreshold => 3;
|
||||
public override int ReactorReadyCoolingThreshold => 3;
|
||||
public override int ReactorReadyCoreHeatThreshold => 8;
|
||||
namespace ReactorMaintenance.Simulation.Difficulties;
|
||||
|
||||
public class NormalBalancing : Balancing
|
||||
{
|
||||
public override int DefaultLevelWidth => 16;
|
||||
public override int DefaultLevelHeight => 12;
|
||||
public override int MinimumLevelSize => 4;
|
||||
public override int ForecastHorizon => 6;
|
||||
public override float MinValue => 0;
|
||||
public override float MaxValue => 10;
|
||||
public override float FuelSafe => 1.5f;
|
||||
public override float FuelCaution => 3.5f;
|
||||
public override float FuelCritical => 6.5f;
|
||||
public override float CoolantSafe => 1.5f;
|
||||
public override float CoolantCaution => 3.5f;
|
||||
public override float CoolantCritical => 6.5f;
|
||||
public override float ElectricitySafe => 1.5f;
|
||||
public override float ElectricityCaution => 3.5f;
|
||||
public override float ElectricityCritical => 6.5f;
|
||||
public override float HeatSafe => 2;
|
||||
public override float HeatCaution => 5;
|
||||
public override float HeatCritical => 8;
|
||||
public override float TerminalHeat => 10;
|
||||
public override float RobotFuelSafetyThreshold => 6.5f;
|
||||
public override float RobotCoolantSafetyThreshold => 8;
|
||||
public override float RobotElectricitySafetyThreshold => 6.5f;
|
||||
public override float RobotHeatSafetyThreshold => 8;
|
||||
public override float SourceAmount => 8;
|
||||
public override float SourceIntensity => 8;
|
||||
public override float DistanceAmountFalloff => 0.5f;
|
||||
public override float DistanceIntensityFalloff => 0.4f;
|
||||
|
||||
public override IReadOnlyList<JunctionRatioPreset> TwoOutflowJunctionRatios { get; } = [
|
||||
new("0/4", [0, 1]),
|
||||
new("1/3", [0.25f, 0.75f]),
|
||||
new("2/2", [0.5f, 0.5f]),
|
||||
new("3/1", [0.75f, 0.25f]),
|
||||
new("4/0", [1, 0])
|
||||
];
|
||||
|
||||
public override IReadOnlyList<JunctionRatioPreset> ThreeOutflowJunctionRatios { get; } = [
|
||||
new("0/3/3", [0, 0.5f, 0.5f]),
|
||||
new("3/0/3", [0.5f, 0, 0.5f]),
|
||||
new("3/3/0", [0.5f, 0.5f, 0]),
|
||||
new("2/2/2", [1f / 3f, 1f / 3f, 1f / 3f])
|
||||
];
|
||||
|
||||
public override float ConsumerRequiredAmount => 2.5f;
|
||||
public override float ConsumerRequiredIntensity => 2.5f;
|
||||
public override int MaxStructuralIntegrity => 10;
|
||||
public override int StructuralIntegrityLeakThreshold => 2;
|
||||
public override float StructuralIntegrityDamageScale => 0.35f;
|
||||
public override float LeakBaseAmount => 0.5f;
|
||||
public override float LeakAmountScale => 0.15f;
|
||||
public override float LeakIntensityScale => 0.1f;
|
||||
public override float FlowTransferRatio => 0.05f;
|
||||
public override float WarmCautionAmount => 0.5f;
|
||||
public override float WarmCriticalAmount => 1.0f;
|
||||
public override float QuenchCautionAmount => 0.6f;
|
||||
public override float QuenchCriticalAmount => 1.2f;
|
||||
public override float ShortCautionHeat => 0.8f;
|
||||
public override float ShortCautionDischarge => 0.8f;
|
||||
public override float ShortCriticalHeat => 1.6f;
|
||||
public override float ShortCriticalDischarge => 1.5f;
|
||||
public override float IgniteCautionHeat => 1.2f;
|
||||
public override float IgniteCautionFuelConsumption => 0.4f;
|
||||
public override float IgniteCriticalHeat => 2.4f;
|
||||
public override float IgniteCriticalFuelConsumption => 0.8f;
|
||||
public override int RemedyBlockTurns => 2;
|
||||
public override int HeatShieldSteps => 3;
|
||||
public override int InventoryCapacityPerRemedy => 3;
|
||||
}
|
||||
20
src/ReactorMaintenance.Simulation/EEditorTool.cs
Normal file
20
src/ReactorMaintenance.Simulation/EEditorTool.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EEditorTool
|
||||
{
|
||||
Cursor,
|
||||
Floor,
|
||||
Wall,
|
||||
Underground,
|
||||
Flow,
|
||||
Consumer,
|
||||
Junction,
|
||||
Door,
|
||||
AllSeeingEyeTerminal,
|
||||
RemedySupply,
|
||||
ReactorControl,
|
||||
Leak,
|
||||
SurfaceHazard,
|
||||
Heat,
|
||||
Robot
|
||||
}
|
||||
8
src/ReactorMaintenance.Simulation/EditorToolCommand.cs
Normal file
8
src/ReactorMaintenance.Simulation/EditorToolCommand.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record EditorToolCommand
|
||||
{
|
||||
public EEditorTool Tool { get; init; }
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public ERemedyType RemedyType { get; init; }
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public sealed class CellIntegrityEffect : ISimulationEffect
|
||||
{
|
||||
public CellState Apply(CellState cell)
|
||||
{
|
||||
var integrity = cell.Integrity;
|
||||
var hazards = cell.Hazards;
|
||||
|
||||
if (cell is { HasPipe: true } && cell.Pressure > Balancing.Current.OverpressureThreshold)
|
||||
integrity -= cell.Pressure - Balancing.Current.OverpressureThreshold;
|
||||
|
||||
if (hazards.Heat >= Balancing.Current.HeatIntegrityDamageThreshold || hazards.Fire)
|
||||
{
|
||||
integrity -= cell.HasPipe ? Balancing.Current.PipeFireIntegrityDamage : Balancing.Current.MinHazardValue;
|
||||
hazards = hazards with { Stability = hazards.Stability - Balancing.Current.FireStabilityDamage };
|
||||
}
|
||||
|
||||
cell = cell with {
|
||||
Integrity = Rules.Clamp(integrity),
|
||||
Hazards = hazards.Clamp()
|
||||
};
|
||||
|
||||
if (integrity > Balancing.Current.MinHazardValue || !cell.HasPipe)
|
||||
return cell;
|
||||
|
||||
return cell with {
|
||||
LeakRate = Math.Max(cell.LeakRate, Balancing.Current.BurstLeakRate),
|
||||
Flow = Balancing.Current.BrokenPipeFlow,
|
||||
PipeOpen = false
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public sealed class FireAndElectricalHazardEffect : ISimulationEffect
|
||||
{
|
||||
public CellState Apply(CellState cell)
|
||||
{
|
||||
var hazards = cell.Hazards;
|
||||
if (hazards.CoolantPooling >= Balancing.Current.ElectrifiedCoolantPoolingThreshold && cell.Powered)
|
||||
hazards = hazards with { ElectricalCharge = hazards.ElectricalCharge + Balancing.Current.ElectricalChargeIncrease };
|
||||
|
||||
var hasFuel = hazards.FuelVapor >= Balancing.Current.FuelVaporFireThreshold || hazards.LiquidFuel >= Balancing.Current.LiquidFuelFireThreshold;
|
||||
var hasIgnition = hazards.Heat >= Balancing.Current.HeatIgnitionThreshold || hazards.ElectricalCharge >= Balancing.Current.ElectricalIgnitionThreshold || cell is { Prop: ECellProp.Generator, Powered: true };
|
||||
if ((hasFuel && hasIgnition) || hazards.Fire)
|
||||
{
|
||||
hazards = hazards with {
|
||||
Fire = hasFuel || hazards.Fire,
|
||||
Heat = hazards.Heat + Balancing.Current.FireHeatIncrease,
|
||||
Smoke = hazards.Smoke + Balancing.Current.FireSmokeIncrease,
|
||||
LiquidFuel = Math.Max(Balancing.Current.MinHazardValue, hazards.LiquidFuel - Balancing.Current.FireLiquidFuelConsumption),
|
||||
FuelVapor = Math.Max(Balancing.Current.MinHazardValue, hazards.FuelVapor - Balancing.Current.FireFuelVaporConsumption)
|
||||
};
|
||||
}
|
||||
else if (hazards.Smoke > Balancing.Current.MinHazardValue)
|
||||
hazards = hazards with { Smoke = hazards.Smoke - Balancing.Current.SmokeDecay };
|
||||
|
||||
return cell with { Hazards = hazards.Clamp() };
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public interface IAreaSimulationEffect
|
||||
{
|
||||
CellState[] Apply(LevelState level, CellState[] cells);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public interface ISimulationEffect
|
||||
{
|
||||
CellState Apply(CellState cell);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public sealed class MachineEffect : ISimulationEffect
|
||||
{
|
||||
public CellState Apply(CellState cell)
|
||||
{
|
||||
var hazards = cell.Prop switch {
|
||||
ECellProp.Generator when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.GeneratorHeatIncrease },
|
||||
ECellProp.CoolingPump when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat - Balancing.Current.CoolingPumpHeatReduction },
|
||||
ECellProp.Reactor => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.ReactorHeatIncrease },
|
||||
_ => cell.Hazards
|
||||
};
|
||||
|
||||
return cell with { Hazards = hazards.Clamp() };
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public sealed class PipeLeakEffect : ISimulationEffect
|
||||
{
|
||||
public CellState Apply(CellState cell)
|
||||
{
|
||||
if (!cell.HasPipe || cell.LeakRate <= Balancing.Current.MinHazardValue)
|
||||
return cell;
|
||||
|
||||
var hazards = cell.Pipe switch {
|
||||
EPipeMedium.Fuel => cell.Hazards with {
|
||||
LiquidFuel = cell.Hazards.LiquidFuel + cell.LeakRate,
|
||||
FuelVapor = cell.Hazards.FuelVapor + (cell.Pressure >= Balancing.Current.PressurizedFuelLeakPressureThreshold ? cell.LeakRate : Math.Max(Balancing.Current.MinHazardValue, cell.Hazards.Heat - Balancing.Current.PassiveFuelVaporHeatOffset) / Balancing.Current.PassiveFuelVaporDivisor)
|
||||
},
|
||||
EPipeMedium.Coolant => cell.Hazards with {
|
||||
CoolantPooling = cell.Hazards.CoolantPooling + cell.LeakRate,
|
||||
Heat = cell.Hazards.Heat - Math.Max(Balancing.Current.MinimumCoolantHeatReduction, cell.LeakRate / Balancing.Current.CoolantHeatReductionDivisor),
|
||||
Smoke = cell.Hazards.Smoke + (cell.Hazards.Heat >= Balancing.Current.CoolantSteamHeatThreshold ? Balancing.Current.CoolantSteamSmokeIncrease : Balancing.Current.MinHazardValue)
|
||||
},
|
||||
EPipeMedium.Pressure => cell.Hazards with { Smoke = cell.Hazards.Smoke + (cell.Pressure >= Balancing.Current.PressureLeakSmokeThreshold ? Balancing.Current.PressureLeakSmokeIncrease : Balancing.Current.MinHazardValue) },
|
||||
_ => cell.Hazards
|
||||
};
|
||||
|
||||
return cell with { Hazards = hazards.Clamp() };
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public sealed class SmokeSpreadEffect : IAreaSimulationEffect
|
||||
{
|
||||
public CellState[] Apply(LevelState level, CellState[] cells)
|
||||
{
|
||||
var next = cells.ToArray();
|
||||
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
|
||||
{
|
||||
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var cell = cells[level.Index(position)];
|
||||
if (cell.Hazards.Smoke < Balancing.Current.SmokeSpreadThreshold)
|
||||
continue;
|
||||
|
||||
SpreadToNeighbors(level, next, position);
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
private static void SpreadToNeighbors(LevelState level, CellState[] next, GridPosition position)
|
||||
{
|
||||
foreach (var neighbor in position.Neighbors().Where(level.InBounds))
|
||||
{
|
||||
var neighborCell = next[level.Index(neighbor)];
|
||||
if (!neighborCell.IsWalkable || neighborCell.DoorLocked)
|
||||
continue;
|
||||
|
||||
next[level.Index(neighbor)] = neighborCell with { Hazards = neighborCell.Hazards with { Smoke = Rules.Clamp(neighborCell.Hazards.Smoke + Balancing.Current.SmokeSpreadIncrease) } };
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/ReactorMaintenance.Simulation/GridPositionExtensions.cs
Normal file
17
src/ReactorMaintenance.Simulation/GridPositionExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class GridPositionExtensions
|
||||
{
|
||||
public static IEnumerable<GridPosition> Neighbors(this GridPosition position)
|
||||
{
|
||||
yield return new(position.X, position.Y - 1);
|
||||
yield return new(position.X + 1, position.Y);
|
||||
yield return new(position.X, position.Y + 1);
|
||||
yield return new(position.X - 1, position.Y);
|
||||
}
|
||||
|
||||
public static int ManhattanDistance(this GridPosition position, GridPosition other)
|
||||
{
|
||||
return Math.Abs(position.X - other.X) + Math.Abs(position.Y - other.Y);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public abstract class Hazard
|
||||
{
|
||||
public abstract IEnumerable<Forecast> Predict(LevelState level, int turns);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public sealed class IgnitionHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
|
||||
{
|
||||
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
|
||||
{
|
||||
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var cell = level.GetCell(position);
|
||||
if (cell.Hazards.Fire)
|
||||
yield return new(EFailureKind.Ignition, position, turns, turns == Balancing.Current.TurnIncrement ? $"FUEL IGNITION PREDICTED AT {x},{y} NEXT TURN" : $"FUEL IGNITION PREDICTED AT {x},{y} IN {turns} TURNS");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public sealed class MeltdownHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
|
||||
{
|
||||
if (level.Global is { Lost: true, Status: "CORE MELTDOWN" })
|
||||
yield return new(EFailureKind.Meltdown, null, turns, "CORE MELTDOWN APPROACHING");
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public sealed class PipeBurstHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
|
||||
{
|
||||
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
|
||||
{
|
||||
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var cell = level.GetCell(position);
|
||||
if (cell is { HasPipe: true, PipeOpen: false } && cell.Flow == Balancing.Current.BrokenPipeFlow && cell.LeakRate >= Balancing.Current.BurstLeakRate)
|
||||
yield return new(EFailureKind.PipeBurst, position, turns, $"PIPE BURST PREDICTED AT {x},{y} IN {turns} TURNS");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public sealed class StabilityCollapseHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
|
||||
{
|
||||
if (level.Global is { Lost: true, Status: "FACILITY STABILITY COLLAPSE" })
|
||||
yield return new(EFailureKind.StabilityCollapse, null, turns, "FACILITY STABILITY COLLAPSE APPROACHING");
|
||||
}
|
||||
}
|
||||
34
src/ReactorMaintenance.Simulation/JunctionFlow.cs
Normal file
34
src/ReactorMaintenance.Simulation/JunctionFlow.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record JunctionFlow
|
||||
{
|
||||
public float WeightFor(GridPosition outgoingBranch)
|
||||
{
|
||||
var index = IndexOfOutgoingBranch(outgoingBranch);
|
||||
if (index < 0)
|
||||
return 0;
|
||||
|
||||
var weights = Balancing.Current.JunctionWeights(OutgoingBranches.Count, Prop.JunctionMode);
|
||||
return index < weights.Length ? weights[index] : 0;
|
||||
}
|
||||
|
||||
private int IndexOfOutgoingBranch(GridPosition outgoingBranch)
|
||||
{
|
||||
for (var i = 0; i < OutgoingBranches.Count; i++)
|
||||
{
|
||||
if (OutgoingBranches[i] == outgoingBranch)
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public GridPosition Position { get; init; } = new(0, 0);
|
||||
public PropState Prop { get; init; } = new();
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public IReadOnlyList<GridPosition> Branches { get; init; } = Array.Empty<GridPosition>();
|
||||
public GridPosition? IncomingBranch { get; init; }
|
||||
public IReadOnlyList<GridPosition> OutgoingBranches { get; init; } = Array.Empty<GridPosition>();
|
||||
public IReadOnlyList<string> Errors { get; init; } = Array.Empty<string>();
|
||||
public bool IsValid => Errors.Count == 0;
|
||||
}
|
||||
97
src/ReactorMaintenance.Simulation/JunctionFlowAnalyzer.cs
Normal file
97
src/ReactorMaintenance.Simulation/JunctionFlowAnalyzer.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class JunctionFlowAnalyzer
|
||||
{
|
||||
private sealed record SourceBranch(GridPosition Position, int? Distance);
|
||||
|
||||
public static IReadOnlyList<JunctionFlow> Analyze(LevelState level)
|
||||
{
|
||||
var flows = new List<JunctionFlow>();
|
||||
|
||||
for (var y = 0; y < level.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type != EPropType.Junction)
|
||||
continue;
|
||||
|
||||
flows.Add(AnalyzeJunction(level, position, prop));
|
||||
}
|
||||
}
|
||||
|
||||
return flows;
|
||||
}
|
||||
|
||||
private static JunctionFlow AnalyzeJunction(LevelState level, GridPosition position, PropState prop)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
var carriers = Enum.GetValues<ECarrierType>().Where(carrier => level.GetUnderground(position, carrier).IsPresent).ToArray();
|
||||
var carrier = carriers.FirstOrDefault();
|
||||
if (carriers.Length != 1)
|
||||
errors.Add("Junction must regulate exactly one underground carrier.");
|
||||
|
||||
var branches = carriers.Length == 1
|
||||
? position.Neighbors().Where(level.InBounds).Where(neighbor => level.GetUnderground(neighbor, carrier).CarriesFlow).ToArray()
|
||||
: Array.Empty<GridPosition>();
|
||||
|
||||
if (carriers.Length == 1 && branches.Length is not 3 and not 4)
|
||||
errors.Add("Junction must have one incoming branch and two or three outgoing branches.");
|
||||
|
||||
var sourceBranches = carriers.Length == 1
|
||||
? branches.Select(branch => new SourceBranch(branch, ShortestDistanceToSource(level, branch, position, carrier)))
|
||||
.Where(branch => branch.Distance.HasValue)
|
||||
.ToArray()
|
||||
: Array.Empty<SourceBranch>();
|
||||
|
||||
GridPosition? incomingBranch = null;
|
||||
if (sourceBranches.Length > 0)
|
||||
{
|
||||
var bestDistance = sourceBranches.Min(branch => branch.Distance!.Value);
|
||||
var bestBranches = sourceBranches.Where(branch => branch.Distance == bestDistance).ToArray();
|
||||
if (bestBranches.Length != 1 || sourceBranches.Length != 1)
|
||||
errors.Add("Ambiguous junction flow.");
|
||||
else
|
||||
incomingBranch = bestBranches[0].Position;
|
||||
}
|
||||
|
||||
var outgoingBranches = incomingBranch is null
|
||||
? Array.Empty<GridPosition>()
|
||||
: branches.Where(branch => branch != incomingBranch).ToArray();
|
||||
|
||||
return new() {
|
||||
Position = position,
|
||||
Prop = prop,
|
||||
Carrier = carrier,
|
||||
Branches = branches,
|
||||
IncomingBranch = incomingBranch,
|
||||
OutgoingBranches = outgoingBranches,
|
||||
Errors = errors
|
||||
};
|
||||
}
|
||||
|
||||
private static int? ShortestDistanceToSource(LevelState level, GridPosition start, GridPosition blocked, ECarrierType carrier)
|
||||
{
|
||||
var visited = new HashSet<GridPosition> { blocked, start };
|
||||
var open = new Queue<(GridPosition Position, int Distance)>();
|
||||
open.Enqueue((start, 0));
|
||||
|
||||
while (open.Count > 0)
|
||||
{
|
||||
var current = open.Dequeue();
|
||||
if (level.GetProp(current.Position) is { Type: EPropType.Flow, Carrier: var sourceCarrier, SwitchState: EPropSwitchState.Enabled } && sourceCarrier == carrier)
|
||||
return current.Distance;
|
||||
|
||||
foreach (var next in current.Position.Neighbors().Where(level.InBounds))
|
||||
{
|
||||
if (!visited.Add(next) || !level.GetUnderground(next, carrier).CarriesFlow)
|
||||
continue;
|
||||
|
||||
open.Enqueue((next, current.Distance + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
3
src/ReactorMaintenance.Simulation/JunctionRatioPreset.cs
Normal file
3
src/ReactorMaintenance.Simulation/JunctionRatioPreset.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record JunctionRatioPreset(string Label, float[] Weights);
|
||||
@@ -1,119 +1,252 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EEditorTool
|
||||
{
|
||||
Cursor,
|
||||
Floor,
|
||||
Wall,
|
||||
Reactor,
|
||||
CoolingPump,
|
||||
Generator,
|
||||
PressureRegulator,
|
||||
DiagnosticTerminal,
|
||||
ControlTerminal,
|
||||
CoolantPipe,
|
||||
FuelPipe,
|
||||
PressurePipe,
|
||||
Leak,
|
||||
Repair,
|
||||
Heat,
|
||||
Fire,
|
||||
Robot
|
||||
}
|
||||
|
||||
public static class LevelEditor
|
||||
{
|
||||
public static LevelState Apply(LevelState level, GridPosition position, EEditorTool tool)
|
||||
{
|
||||
if (!level.InBounds(position))
|
||||
return level;
|
||||
|
||||
if (tool == EEditorTool.Robot)
|
||||
return level.GetCell(position).IsWalkable ? level with { Robot = position } : level;
|
||||
|
||||
var cell = level.GetCell(position);
|
||||
cell = tool switch {
|
||||
EEditorTool.Cursor => cell,
|
||||
EEditorTool.Floor => cell with { Terrain = ECellTerrain.Floor },
|
||||
EEditorTool.Wall => cell with {
|
||||
Terrain = ECellTerrain.Wall,
|
||||
Prop = ECellProp.None,
|
||||
Pipe = EPipeMedium.None,
|
||||
Flow = Balancing.Current.MinHazardValue,
|
||||
Pressure = Balancing.Current.MinHazardValue,
|
||||
LeakRate = Balancing.Current.MinHazardValue,
|
||||
PipeOpen = false,
|
||||
Powered = false
|
||||
},
|
||||
EEditorTool.Reactor => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.Reactor },
|
||||
EEditorTool.CoolingPump => cell with {
|
||||
Terrain = ECellTerrain.Floor,
|
||||
Prop = ECellProp.CoolingPump,
|
||||
Powered = true
|
||||
},
|
||||
EEditorTool.Generator => cell with {
|
||||
Terrain = ECellTerrain.Floor,
|
||||
Prop = ECellProp.Generator,
|
||||
Powered = true
|
||||
},
|
||||
EEditorTool.PressureRegulator => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.PressureRegulator },
|
||||
EEditorTool.DiagnosticTerminal => cell with {
|
||||
Terrain = ECellTerrain.Floor,
|
||||
Prop = ECellProp.DiagnosticTerminal,
|
||||
Powered = true
|
||||
},
|
||||
EEditorTool.ControlTerminal => cell with {
|
||||
Terrain = ECellTerrain.Floor,
|
||||
Prop = ECellProp.ControlTerminal,
|
||||
Powered = true
|
||||
},
|
||||
EEditorTool.CoolantPipe => cell with {
|
||||
Pipe = EPipeMedium.Coolant,
|
||||
Flow = Balancing.Current.DefaultPipeFlow,
|
||||
Pressure = Balancing.Current.DefaultPipePressure,
|
||||
Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
|
||||
PipeOpen = true
|
||||
},
|
||||
EEditorTool.FuelPipe => cell with {
|
||||
Pipe = EPipeMedium.Fuel,
|
||||
Flow = Balancing.Current.DefaultPipeFlow,
|
||||
Pressure = Balancing.Current.DefaultPipePressure,
|
||||
Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
|
||||
PipeOpen = true
|
||||
},
|
||||
EEditorTool.PressurePipe => cell with {
|
||||
Pipe = EPipeMedium.Pressure,
|
||||
Flow = Balancing.Current.DefaultPressurePipeFlow,
|
||||
Pressure = Balancing.Current.DefaultPressurePipePressure,
|
||||
Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
|
||||
PipeOpen = true
|
||||
},
|
||||
EEditorTool.Leak => cell with {
|
||||
LeakRate = Math.Max(Balancing.Current.MinimumLeakRate, cell.LeakRate),
|
||||
Integrity = Math.Min(cell.Integrity, Balancing.Current.DamagedPipeIntegrity)
|
||||
},
|
||||
EEditorTool.Repair => cell with {
|
||||
LeakRate = Balancing.Current.RepairedLeakRate,
|
||||
Integrity = Balancing.Current.DefaultCellIntegrity,
|
||||
Hazards = cell.Hazards with {
|
||||
Fire = false,
|
||||
ElectricalCharge = Balancing.Current.RepairedElectricalCharge
|
||||
}
|
||||
},
|
||||
EEditorTool.Heat => cell with { Hazards = cell.Hazards with { Heat = Rules.Clamp(cell.Hazards.Heat + Balancing.Current.HeatToolIncrease) } },
|
||||
EEditorTool.Fire => cell with {
|
||||
Hazards = cell.Hazards with {
|
||||
Fire = !cell.Hazards.Fire,
|
||||
Heat = Math.Max(cell.Hazards.Heat, Balancing.Current.FireToolMinimumHeat),
|
||||
Smoke = Math.Max(cell.Hazards.Smoke, Balancing.Current.FireToolMinimumSmoke)
|
||||
}
|
||||
},
|
||||
_ => cell
|
||||
};
|
||||
|
||||
if (cell.Terrain == ECellTerrain.Wall)
|
||||
cell = cell with { Hazards = new() };
|
||||
|
||||
return level.SetCell(position, cell);
|
||||
}
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class LevelEditor
|
||||
{
|
||||
public sealed record MoveOccupantResult(bool Success, LevelState Level, string Reason)
|
||||
{
|
||||
public static MoveOccupantResult Succeeded(LevelState level)
|
||||
{
|
||||
return new(true, level, string.Empty);
|
||||
}
|
||||
|
||||
public static MoveOccupantResult Failed(LevelState level, string reason)
|
||||
{
|
||||
return new(false, level, reason);
|
||||
}
|
||||
}
|
||||
|
||||
public static LevelState MoveOccupant(LevelState level, GridPosition source, GridPosition destination)
|
||||
{
|
||||
return TryMoveOccupant(level, source, destination).Level;
|
||||
}
|
||||
|
||||
public static MoveOccupantResult TryMoveOccupant(LevelState level, GridPosition source, GridPosition destination)
|
||||
{
|
||||
if (!level.InBounds(source))
|
||||
return MoveOccupantResult.Failed(level, "Drag start is outside the level.");
|
||||
|
||||
if (!level.InBounds(destination))
|
||||
return MoveOccupantResult.Failed(level, "Drop target is outside the level.");
|
||||
|
||||
if (source == destination)
|
||||
return MoveOccupantResult.Failed(level, "Drop target is the same cell.");
|
||||
|
||||
var prop = level.GetProp(source);
|
||||
if (prop.Type != EPropType.None)
|
||||
return TryMoveProp(level, source, destination, prop);
|
||||
|
||||
var leak = level.Leaks.FirstOrDefault(leak => !leak.Repaired && (leak.AccessPosition == source || leak.UndergroundPosition == source));
|
||||
if (leak is not null)
|
||||
return TryMoveLeak(level, leak, destination);
|
||||
|
||||
return level.Robot.Position == source
|
||||
? TryMoveRobot(level, destination)
|
||||
: MoveOccupantResult.Failed(level, "No movable robot, prop, source, or leak starts here.");
|
||||
}
|
||||
|
||||
private static MoveOccupantResult TryMoveRobot(LevelState level, GridPosition destination)
|
||||
{
|
||||
if (!level.IsFloor(destination))
|
||||
return MoveOccupantResult.Failed(level, "Robot destination must be a floor cell.");
|
||||
|
||||
return MoveOccupantResult.Succeeded(level with { Robot = level.Robot with { Position = destination } });
|
||||
}
|
||||
|
||||
private static MoveOccupantResult TryMoveProp(LevelState level, GridPosition source, GridPosition destination, PropState prop)
|
||||
{
|
||||
if (!level.IsFloor(destination))
|
||||
return MoveOccupantResult.Failed(level, "Prop destination must be a floor cell.");
|
||||
|
||||
if (level.GetProp(destination).Type != EPropType.None)
|
||||
return MoveOccupantResult.Failed(level, "Prop destination is already occupied.");
|
||||
|
||||
var next = level.SetProp(source, new()).SetProp(destination, prop);
|
||||
if (prop.Type != EPropType.ReactorControl)
|
||||
return MoveOccupantResult.Succeeded(next);
|
||||
|
||||
return MoveOccupantResult.Succeeded(next with {
|
||||
Reactors = next.Reactors
|
||||
.Select(reactor => reactor.ControlPosition == source ? reactor with { ControlPosition = destination } : reactor)
|
||||
.ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
private static MoveOccupantResult TryMoveLeak(LevelState level, LeakState leak, GridPosition destination)
|
||||
{
|
||||
if (leak.Carrier is ECarrierType.Fuel or ECarrierType.Coolant)
|
||||
{
|
||||
if (!level.IsFloor(destination))
|
||||
return MoveOccupantResult.Failed(level, "Fuel and coolant leaks must move to a floor cell.");
|
||||
|
||||
var next = ClearLeak(level, leak)
|
||||
.SetUnderground(leak.UndergroundPosition, leak.Carrier, new());
|
||||
return MoveOccupantResult.Succeeded(SetLeak(next, destination, destination, leak.Carrier));
|
||||
}
|
||||
|
||||
if (leak.Carrier == ECarrierType.Electricity)
|
||||
{
|
||||
if (!level.IsFloor(destination))
|
||||
return MoveOccupantResult.Failed(level, "Electric leak destination must be an adjacent floor access cell.");
|
||||
|
||||
var undergroundPosition = leak.UndergroundPosition;
|
||||
if (undergroundPosition.ManhattanDistance(destination) != 1)
|
||||
return MoveOccupantResult.Failed(level, "Electric leak destination must stay adjacent to its underground wall cell.");
|
||||
|
||||
return MoveOccupantResult.Succeeded(SetLeak(ClearLeak(level, leak), undergroundPosition, destination, leak.Carrier));
|
||||
}
|
||||
|
||||
return MoveOccupantResult.Failed(level, "Unsupported leak carrier.");
|
||||
}
|
||||
|
||||
private static LevelState ClearLeak(LevelState level, LeakState leak)
|
||||
{
|
||||
return level with {
|
||||
Leaks = level.Leaks.Where(candidate => candidate != leak).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public static LevelState CycleElectricityLeakAccess(LevelState level, GridPosition undergroundPosition)
|
||||
{
|
||||
if (!level.InBounds(undergroundPosition))
|
||||
return level;
|
||||
|
||||
if (!level.GetUnderground(undergroundPosition, ECarrierType.Electricity).IsPresent)
|
||||
return level;
|
||||
|
||||
var accessPositions = undergroundPosition.Neighbors().Where(level.IsFloor).ToArray();
|
||||
if (accessPositions.Length == 0)
|
||||
return level;
|
||||
|
||||
var existingLeak = level.Leaks.FirstOrDefault(leak => leak.Carrier == ECarrierType.Electricity && leak.UndergroundPosition == undergroundPosition);
|
||||
var nextAccessPosition = accessPositions[0];
|
||||
if (existingLeak is not null)
|
||||
{
|
||||
var index = Array.IndexOf(accessPositions, existingLeak.AccessPosition);
|
||||
nextAccessPosition = accessPositions[(index + 1) % accessPositions.Length];
|
||||
}
|
||||
|
||||
return SetLeak(level, undergroundPosition, nextAccessPosition, ECarrierType.Electricity);
|
||||
}
|
||||
|
||||
public static LevelState Apply(LevelState level, GridPosition position, EditorToolCommand command)
|
||||
{
|
||||
if (!level.InBounds(position))
|
||||
return level;
|
||||
|
||||
return command.Tool switch {
|
||||
EEditorTool.Cursor => level,
|
||||
EEditorTool.Floor => level.SetTerrain(position, ECellTerrain.Floor),
|
||||
EEditorTool.Wall => level.SetTerrain(position, ECellTerrain.Wall),
|
||||
EEditorTool.Underground => SetUnderground(level, position, command.Carrier),
|
||||
EEditorTool.Flow => SetCarrierProp(level, position, EPropType.Flow, command.Carrier),
|
||||
EEditorTool.Consumer => SetFloorProp(level, position, new() { Type = EPropType.Consumer, SwitchState = EPropSwitchState.Enabled }),
|
||||
EEditorTool.Junction => SetFloorProp(level, position, new() { Type = EPropType.Junction }),
|
||||
EEditorTool.Door => ToggleOrSetDoor(level, position),
|
||||
EEditorTool.AllSeeingEyeTerminal => SetFloorProp(level, position, new() { Type = EPropType.AllSeeingEyeTerminal }),
|
||||
EEditorTool.RemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = command.RemedyType }),
|
||||
EEditorTool.ReactorControl => SetReactorControl(level, position),
|
||||
EEditorTool.Leak => SetLeak(level, position, command.Carrier),
|
||||
EEditorTool.SurfaceHazard => AddSurfaceHazard(level, position, command.Carrier),
|
||||
EEditorTool.Heat => level.SetSurface(position, level.GetSurface(position) with { Heat = level.GetSurface(position).Heat + 1 }),
|
||||
EEditorTool.Robot => level.IsFloor(position) ? level with { Robot = level.Robot with { Position = position } } : level,
|
||||
_ => level
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
StructuralIntegrity = Balancing.Current.StructuralIntegrityLeakThreshold
|
||||
});
|
||||
return next with {
|
||||
Leaks = [
|
||||
.. next.Leaks.Where(leak => leak.UndergroundPosition != undergroundPosition || leak.Carrier != carrier),
|
||||
new() {
|
||||
Carrier = carrier,
|
||||
UndergroundPosition = undergroundPosition,
|
||||
AccessPosition = accessPosition
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
private static LevelState SetUnderground(LevelState level, GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
return level.SetUnderground(position, carrier, new() {
|
||||
State = EUndergroundState.Intact,
|
||||
StructuralIntegrity = Balancing.Current.MaxStructuralIntegrity
|
||||
});
|
||||
}
|
||||
|
||||
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 ToggleOrSetDoor(LevelState level, GridPosition position)
|
||||
{
|
||||
if (!level.IsFloor(position))
|
||||
return level;
|
||||
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type == EPropType.Door)
|
||||
{
|
||||
var nextState = prop.DoorState == EDoorState.Open ? EDoorState.Closed : EDoorState.Open;
|
||||
return level.SetProp(position, prop with { DoorState = nextState });
|
||||
}
|
||||
|
||||
return level.SetProp(position, new() { Type = EPropType.Door, DoorState = EDoorState.Closed });
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
private static LevelState SetLeak(LevelState level, GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
if (!level.InBounds(position))
|
||||
return level;
|
||||
|
||||
return SetLeak(level, position, position, carrier);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,42 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class LevelSerializer
|
||||
{
|
||||
private const int c_CurrentVersion = 1;
|
||||
|
||||
public static string Serialize(LevelState level)
|
||||
{
|
||||
return JsonSerializer.Serialize(new LevelFile {
|
||||
Version = c_CurrentVersion,
|
||||
Level = level
|
||||
}, Options);
|
||||
}
|
||||
|
||||
public static LevelState Deserialize(string json)
|
||||
{
|
||||
var file = JsonSerializer.Deserialize<LevelFile>(json, Options) ?? throw new InvalidOperationException("Level file did not contain a level.");
|
||||
var level = file.Version switch {
|
||||
c_CurrentVersion => file.Level,
|
||||
_ => throw new InvalidOperationException($"Unsupported level file version {file.Version}.")
|
||||
};
|
||||
|
||||
return level.Cells.Length != level.Width * level.Height ? throw new InvalidOperationException("Level cell count does not match its dimensions.") : level;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions Options = new() {
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
private sealed record LevelFile
|
||||
{
|
||||
public int Version { get; init; }
|
||||
public LevelState Level { get; init; } = new();
|
||||
}
|
||||
}
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class LevelSerializer
|
||||
{
|
||||
private sealed record LevelFile
|
||||
{
|
||||
public int Version { get; init; }
|
||||
public LevelState? Level { get; init; }
|
||||
}
|
||||
|
||||
public static string Serialize(LevelState level)
|
||||
{
|
||||
return JsonSerializer.Serialize(new LevelFile {
|
||||
Version = c_CurrentVersion,
|
||||
Level = level
|
||||
}, s_Options);
|
||||
}
|
||||
|
||||
public static LevelState Deserialize(string json)
|
||||
{
|
||||
var file = JsonSerializer.Deserialize<LevelFile>(json, s_Options) ?? throw new InvalidOperationException("Level file did not contain a level.");
|
||||
if (file.Version != c_CurrentVersion)
|
||||
throw new InvalidOperationException($"Unsupported level file version {file.Version}. Expected {c_CurrentVersion}.");
|
||||
|
||||
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 const int c_CurrentVersion = 3;
|
||||
|
||||
private static readonly JsonSerializerOptions s_Options = new() {
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
}
|
||||
136
src/ReactorMaintenance.Simulation/LevelStateExtensions.cs
Normal file
136
src/ReactorMaintenance.Simulation/LevelStateExtensions.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
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 DoorBlocksEdge(level, a, b) || DoorBlocksEdge(level, b, a);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
private static bool DoorBlocksEdge(LevelState level, GridPosition doorPosition, GridPosition neighbor)
|
||||
{
|
||||
if (!level.InBounds(doorPosition) || !level.InBounds(neighbor))
|
||||
return false;
|
||||
|
||||
var prop = level.GetProp(doorPosition);
|
||||
if (prop is not { Type: EPropType.Door, DoorState: EDoorState.Closed } || doorPosition.ManhattanDistance(neighbor) != 1)
|
||||
return false;
|
||||
|
||||
var north = new GridPosition(doorPosition.X, doorPosition.Y - 1);
|
||||
var south = new GridPosition(doorPosition.X, doorPosition.Y + 1);
|
||||
var west = new GridPosition(doorPosition.X - 1, doorPosition.Y);
|
||||
var east = new GridPosition(doorPosition.X + 1, doorPosition.Y);
|
||||
|
||||
if (IsWall(level, north) && IsWall(level, south))
|
||||
return neighbor.Y == doorPosition.Y;
|
||||
|
||||
if (IsWall(level, west) && IsWall(level, east))
|
||||
return neighbor.X == doorPosition.X;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsWall(LevelState level, GridPosition position)
|
||||
{
|
||||
return level.InBounds(position) && level.GetTerrain(position) == ECellTerrain.Wall;
|
||||
}
|
||||
}
|
||||
54
src/ReactorMaintenance.Simulation/LevelStateFactory.cs
Normal file
54
src/ReactorMaintenance.Simulation/LevelStateFactory.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class LevelStateFactory
|
||||
{
|
||||
public static LevelState Create(string name, int width, int height)
|
||||
{
|
||||
if (width < Balancing.Current.MinimumLevelSize || height < Balancing.Current.MinimumLevelSize)
|
||||
throw new ArgumentOutOfRangeException(nameof(width), $"Levels must be at least {Balancing.Current.MinimumLevelSize}x{Balancing.Current.MinimumLevelSize}.");
|
||||
|
||||
return new() {
|
||||
Name = name,
|
||||
Width = width,
|
||||
Height = height,
|
||||
Terrain = CreateTerrain(width, height),
|
||||
Fuel = CreateUnderground(width, height),
|
||||
Coolant = CreateUnderground(width, height),
|
||||
Electricity = CreateUnderground(width, height),
|
||||
Surface = CreateSurface(width, height),
|
||||
Props = CreateProps(width, height),
|
||||
Robot = new() { Position = new(1, 1) },
|
||||
Forecasts = Array.Empty<Forecast>()
|
||||
};
|
||||
}
|
||||
|
||||
public static ECellTerrain[] CreateTerrain(int width, int height)
|
||||
{
|
||||
var terrain = Enumerable.Repeat(ECellTerrain.Floor, width * height).ToArray();
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
for (var x = 0; x < width; x++)
|
||||
{
|
||||
if (x == 0 || y == 0 || x == width - 1 || y == height - 1)
|
||||
terrain[(y * width) + x] = ECellTerrain.Wall;
|
||||
}
|
||||
}
|
||||
|
||||
return terrain;
|
||||
}
|
||||
|
||||
public static UndergroundCell[] CreateUnderground(int width, int height)
|
||||
{
|
||||
return Enumerable.Range(0, width * height).Select(_ => new UndergroundCell()).ToArray();
|
||||
}
|
||||
|
||||
public static SurfaceState[] CreateSurface(int width, int height)
|
||||
{
|
||||
return Enumerable.Range(0, width * height).Select(_ => new SurfaceState()).ToArray();
|
||||
}
|
||||
|
||||
public static PropState[] CreateProps(int width, int height)
|
||||
{
|
||||
return Enumerable.Range(0, width * height).Select(_ => new PropState()).ToArray();
|
||||
}
|
||||
}
|
||||
13
src/ReactorMaintenance.Simulation/LevelTraversal.cs
Normal file
13
src/ReactorMaintenance.Simulation/LevelTraversal.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class LevelTraversal
|
||||
{
|
||||
public static IEnumerable<GridPosition> AllPositions(LevelState level)
|
||||
{
|
||||
for (var y = 0; y < level.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < level.Width; x++)
|
||||
yield return new(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
201
src/ReactorMaintenance.Simulation/LevelValidator.cs
Normal file
201
src/ReactorMaintenance.Simulation/LevelValidator.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
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);
|
||||
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 position in LevelTraversal.AllPositions(level))
|
||||
{
|
||||
if (level.GetProp(position).Type != EPropType.Door)
|
||||
continue;
|
||||
|
||||
if (!level.IsFloor(position))
|
||||
{
|
||||
errors.Add(new("Door prop must be placed on a floor cell.", position));
|
||||
continue;
|
||||
}
|
||||
|
||||
var northSouthWalls = IsWall(level, new(position.X, position.Y - 1)) && IsWall(level, new(position.X, position.Y + 1));
|
||||
var westEastWalls = IsWall(level, new(position.X - 1, position.Y)) && IsWall(level, new(position.X + 1, position.Y));
|
||||
if (northSouthWalls == westEastWalls)
|
||||
errors.Add(new("Door must be surrounded by one opposing pair of wall cells.", position));
|
||||
}
|
||||
}
|
||||
|
||||
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 control position must point to a reactor control prop.", reactor.ControlPosition));
|
||||
|
||||
if (!reactor.Ready)
|
||||
warnings.Add(new("Reactor is initially unready.", reactor.ControlPosition));
|
||||
}
|
||||
|
||||
if (level.RequiredFuelConsumers < 0 || level.RequiredCoolantConsumers < 0 || level.RequiredElectricityConsumers < 0)
|
||||
errors.Add(new("Required consumer counts cannot be negative."));
|
||||
}
|
||||
|
||||
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 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)
|
||||
continue;
|
||||
|
||||
var hasPresentNetwork = false;
|
||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||
{
|
||||
if (!level.GetUnderground(position, carrier).IsPresent)
|
||||
continue;
|
||||
|
||||
hasPresentNetwork = true;
|
||||
if (!HasSourcePath(level, position, carrier))
|
||||
warnings.Add(new($"Enabled consumer has no {carrier} source path.", position));
|
||||
}
|
||||
|
||||
if (!hasPresentNetwork)
|
||||
warnings.Add(new("Enabled consumer has no underground network beneath it.", position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsWall(LevelState level, GridPosition position)
|
||||
{
|
||||
return level.InBounds(position) && level.GetTerrain(position) == ECellTerrain.Wall;
|
||||
}
|
||||
|
||||
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/EBand.cs
Normal file
8
src/ReactorMaintenance.Simulation/Models/EBand.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EBand
|
||||
{
|
||||
Safe,
|
||||
Caution,
|
||||
Critical
|
||||
}
|
||||
8
src/ReactorMaintenance.Simulation/Models/ECarrierType.cs
Normal file
8
src/ReactorMaintenance.Simulation/Models/ECarrierType.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ECarrierType
|
||||
{
|
||||
Fuel,
|
||||
Coolant,
|
||||
Electricity
|
||||
}
|
||||
7
src/ReactorMaintenance.Simulation/Models/ECellTerrain.cs
Normal file
7
src/ReactorMaintenance.Simulation/Models/ECellTerrain.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ECellTerrain
|
||||
{
|
||||
Floor,
|
||||
Wall
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EConsumerServiceState
|
||||
{
|
||||
Unknown,
|
||||
Disabled,
|
||||
Starved,
|
||||
Supplied,
|
||||
Producing
|
||||
}
|
||||
7
src/ReactorMaintenance.Simulation/Models/EDoorState.cs
Normal file
7
src/ReactorMaintenance.Simulation/Models/EDoorState.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EDoorState
|
||||
{
|
||||
Open,
|
||||
Closed
|
||||
}
|
||||
10
src/ReactorMaintenance.Simulation/Models/EForecastKind.cs
Normal file
10
src/ReactorMaintenance.Simulation/Models/EForecastKind.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EForecastKind
|
||||
{
|
||||
TerminalLoss,
|
||||
ReactorReady,
|
||||
ConsumerStarved,
|
||||
HazardGrowth,
|
||||
StructuralIntegrity
|
||||
}
|
||||
11
src/ReactorMaintenance.Simulation/Models/ELevelState.cs
Normal file
11
src/ReactorMaintenance.Simulation/Models/ELevelState.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ELevelState
|
||||
{
|
||||
Stable,
|
||||
Caution,
|
||||
Critical,
|
||||
Ready,
|
||||
Lost,
|
||||
Won
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ENetworkValueKind
|
||||
{
|
||||
Amount,
|
||||
Intensity
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EPropSwitchState
|
||||
{
|
||||
Disabled,
|
||||
Enabled
|
||||
}
|
||||
13
src/ReactorMaintenance.Simulation/Models/EPropType.cs
Normal file
13
src/ReactorMaintenance.Simulation/Models/EPropType.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EPropType
|
||||
{
|
||||
None,
|
||||
Flow,
|
||||
Consumer,
|
||||
Junction,
|
||||
Door,
|
||||
AllSeeingEyeTerminal,
|
||||
RemedySupply,
|
||||
ReactorControl
|
||||
}
|
||||
9
src/ReactorMaintenance.Simulation/Models/ERemedyType.cs
Normal file
9
src/ReactorMaintenance.Simulation/Models/ERemedyType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ERemedyType
|
||||
{
|
||||
FuelNeutralizer,
|
||||
CoolantNeutralizer,
|
||||
ElectricityNeutralizer,
|
||||
HeatShield
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ESurfaceInteractionVerb
|
||||
{
|
||||
Hold,
|
||||
Flow,
|
||||
Warm,
|
||||
Quench,
|
||||
Short,
|
||||
Ignite
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ESurfaceQuantity
|
||||
{
|
||||
Fuel,
|
||||
Coolant,
|
||||
Electricity,
|
||||
Heat
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EUndergroundState
|
||||
{
|
||||
Absent,
|
||||
Intact,
|
||||
Leaking
|
||||
}
|
||||
3
src/ReactorMaintenance.Simulation/Models/Forecast.cs
Normal file
3
src/ReactorMaintenance.Simulation/Models/Forecast.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record Forecast(EForecastKind Kind, GridPosition? Position, int Turns, string Message);
|
||||
10
src/ReactorMaintenance.Simulation/Models/GlobalState.cs
Normal file
10
src/ReactorMaintenance.Simulation/Models/GlobalState.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record GlobalState
|
||||
{
|
||||
public int Turn { get; init; }
|
||||
public ELevelState LevelState { get; init; } = ELevelState.Stable;
|
||||
public string Status { get; init; } = "STABLE";
|
||||
public bool TerminalLoss { get; init; }
|
||||
public IReadOnlyList<string> Warnings { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
3
src/ReactorMaintenance.Simulation/Models/GridPosition.cs
Normal file
3
src/ReactorMaintenance.Simulation/Models/GridPosition.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record GridPosition(int X, int Y);
|
||||
9
src/ReactorMaintenance.Simulation/Models/LeakState.cs
Normal file
9
src/ReactorMaintenance.Simulation/Models/LeakState.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record LeakState
|
||||
{
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public GridPosition UndergroundPosition { get; init; } = new(0, 0);
|
||||
public GridPosition AccessPosition { get; init; } = new(0, 0);
|
||||
public bool Repaired { get; init; }
|
||||
}
|
||||
27
src/ReactorMaintenance.Simulation/Models/LevelState.cs
Normal file
27
src/ReactorMaintenance.Simulation/Models/LevelState.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
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<LeakState> Leaks { get; init; } = Array.Empty<LeakState>();
|
||||
public IReadOnlyList<ReactorState> Reactors { get; init; } = Array.Empty<ReactorState>();
|
||||
public int RequiredFuelConsumers { get; init; } = 1;
|
||||
public int RequiredCoolantConsumers { get; init; } = 1;
|
||||
public int RequiredElectricityConsumers { get; init; } = 1;
|
||||
public RobotState Robot { get; init; } = new();
|
||||
public GlobalState Global { get; init; } = new();
|
||||
public IReadOnlyList<Forecast> Forecasts { get; init; } = Array.Empty<Forecast>();
|
||||
}
|
||||
29
src/ReactorMaintenance.Simulation/Models/PropState.cs
Normal file
29
src/ReactorMaintenance.Simulation/Models/PropState.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record PropState
|
||||
{
|
||||
public EConsumerServiceState ServiceStateFor(ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => FuelServiceState,
|
||||
ECarrierType.Coolant => CoolantServiceState,
|
||||
ECarrierType.Electricity => ElectricityServiceState,
|
||||
_ => EConsumerServiceState.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
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 EConsumerServiceState FuelServiceState { get; init; } = EConsumerServiceState.Unknown;
|
||||
public EConsumerServiceState CoolantServiceState { get; init; } = EConsumerServiceState.Unknown;
|
||||
public EConsumerServiceState ElectricityServiceState { 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 EDoorState DoorState { get; init; } = EDoorState.Closed;
|
||||
|
||||
public bool IsEnabled => SwitchState == EPropSwitchState.Enabled;
|
||||
}
|
||||
9
src/ReactorMaintenance.Simulation/Models/ReactorState.cs
Normal file
9
src/ReactorMaintenance.Simulation/Models/ReactorState.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record ReactorState
|
||||
{
|
||||
public int ReactorId { get; init; }
|
||||
public GridPosition ControlPosition { get; init; } = new(0, 0);
|
||||
public bool Ready { get; init; }
|
||||
public bool Activated { get; init; }
|
||||
}
|
||||
11
src/ReactorMaintenance.Simulation/Models/RobotState.cs
Normal file
11
src/ReactorMaintenance.Simulation/Models/RobotState.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record RobotState
|
||||
{
|
||||
public GridPosition Position { get; init; } = new(1, 1);
|
||||
public int FuelNeutralizers { get; init; }
|
||||
public int CoolantNeutralizers { get; init; }
|
||||
public int ElectricityNeutralizers { get; init; }
|
||||
public int HeatShields { get; init; }
|
||||
public int HeatImmunitySteps { get; init; }
|
||||
}
|
||||
12
src/ReactorMaintenance.Simulation/Models/SurfaceState.cs
Normal file
12
src/ReactorMaintenance.Simulation/Models/SurfaceState.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record SurfaceState
|
||||
{
|
||||
public float Fuel { get; init; }
|
||||
public float Coolant { get; init; }
|
||||
public float Electricity { get; init; }
|
||||
public float Heat { get; init; }
|
||||
public int FuelBlockTurns { get; init; }
|
||||
public int CoolantBlockTurns { get; init; }
|
||||
public int ElectricityBlockTurns { get; init; }
|
||||
}
|
||||
12
src/ReactorMaintenance.Simulation/Models/UndergroundCell.cs
Normal file
12
src/ReactorMaintenance.Simulation/Models/UndergroundCell.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record UndergroundCell
|
||||
{
|
||||
public EUndergroundState State { get; init; }
|
||||
public float Amount { get; init; }
|
||||
public float Intensity { get; init; }
|
||||
public int StructuralIntegrity { get; init; } = Balancing.Current.MaxStructuralIntegrity;
|
||||
|
||||
public bool IsPresent => State != EUndergroundState.Absent;
|
||||
public bool CarriesFlow => State is EUndergroundState.Intact or EUndergroundState.Leaking;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record ValidationIssue(string Message, GridPosition? Position = null);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record ValidationReport
|
||||
{
|
||||
public IReadOnlyList<ValidationIssue> Errors { get; init; } = Array.Empty<ValidationIssue>();
|
||||
public IReadOnlyList<ValidationIssue> Warnings { get; init; } = Array.Empty<ValidationIssue>();
|
||||
public bool IsValid => Errors.Count == 0;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
36
src/ReactorMaintenance.Simulation/RobotStateExtensions.cs
Normal file
36
src/ReactorMaintenance.Simulation/RobotStateExtensions.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class RobotStateExtensions
|
||||
{
|
||||
public static int Count(this RobotState robot, ERemedyType remedy)
|
||||
{
|
||||
return remedy switch {
|
||||
ERemedyType.FuelNeutralizer => robot.FuelNeutralizers,
|
||||
ERemedyType.CoolantNeutralizer => robot.CoolantNeutralizers,
|
||||
ERemedyType.ElectricityNeutralizer => robot.ElectricityNeutralizers,
|
||||
ERemedyType.HeatShield => robot.HeatShields,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(remedy), remedy, "Unsupported remedy.")
|
||||
};
|
||||
}
|
||||
|
||||
public static RobotState Add(this RobotState robot, ERemedyType remedy, int amount)
|
||||
{
|
||||
return remedy switch {
|
||||
ERemedyType.FuelNeutralizer => robot with { FuelNeutralizers = ClampInventory(robot.FuelNeutralizers + amount) },
|
||||
ERemedyType.CoolantNeutralizer => robot with { CoolantNeutralizers = ClampInventory(robot.CoolantNeutralizers + amount) },
|
||||
ERemedyType.ElectricityNeutralizer => robot with { ElectricityNeutralizers = ClampInventory(robot.ElectricityNeutralizers + amount) },
|
||||
ERemedyType.HeatShield => robot with { HeatShields = ClampInventory(robot.HeatShields + amount) },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(remedy), remedy, "Unsupported remedy.")
|
||||
};
|
||||
}
|
||||
|
||||
public static RobotState Spend(this RobotState robot, ERemedyType remedy)
|
||||
{
|
||||
return robot.Count(remedy) <= 0 ? robot : robot.Add(remedy, -1);
|
||||
}
|
||||
|
||||
private static int ClampInventory(int value)
|
||||
{
|
||||
return Math.Clamp(value, 0, Balancing.Current.InventoryCapacityPerRemedy);
|
||||
}
|
||||
}
|
||||
45
src/ReactorMaintenance.Simulation/SimulationBands.cs
Normal file
45
src/ReactorMaintenance.Simulation/SimulationBands.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class SimulationBands
|
||||
{
|
||||
public static EBand SurfaceBand(SurfaceState surface, ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => Fuel(surface.Fuel),
|
||||
ECarrierType.Coolant => Coolant(surface.Coolant),
|
||||
ECarrierType.Electricity => Electricity(surface.Electricity),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
public static EBand NetworkBand(UndergroundCell underground, ECarrierType carrier, ENetworkValueKind valueKind)
|
||||
{
|
||||
var value = valueKind == ENetworkValueKind.Amount ? underground.Amount : underground.Intensity;
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => Fuel(value),
|
||||
ECarrierType.Coolant => Coolant(value),
|
||||
ECarrierType.Electricity => Electricity(value),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
public static EBand Fuel(float value)
|
||||
{
|
||||
return Balancing.Current.Band(value, Balancing.Current.FuelCaution, Balancing.Current.FuelCritical);
|
||||
}
|
||||
|
||||
public static EBand Coolant(float value)
|
||||
{
|
||||
return Balancing.Current.Band(value, Balancing.Current.CoolantCaution, Balancing.Current.CoolantCritical);
|
||||
}
|
||||
|
||||
public static EBand Electricity(float value)
|
||||
{
|
||||
return Balancing.Current.Band(value, Balancing.Current.ElectricityCaution, Balancing.Current.ElectricityCritical);
|
||||
}
|
||||
|
||||
public static EBand Heat(float value)
|
||||
{
|
||||
return Balancing.Current.Band(value, Balancing.Current.HeatCaution, Balancing.Current.HeatCritical);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +1,75 @@
|
||||
using ReactorMaintenance.Simulation.Effects;
|
||||
using ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed class SimulationEngine(IEnumerable<ISimulationEffect> effects, IEnumerable<IAreaSimulationEffect> areaEffects, IEnumerable<Hazard> hazards)
|
||||
{
|
||||
private sealed record ForecastKey(EFailureKind Kind, GridPosition? Position);
|
||||
|
||||
public SimulationEngine()
|
||||
: this(
|
||||
[new PipeLeakEffect(), new MachineEffect(), new FireAndElectricalHazardEffect(), new CellIntegrityEffect()],
|
||||
[new SmokeSpreadEffect()],
|
||||
[new PipeBurstHazard(), new IgnitionHazard(), new MeltdownHazard(), new StabilityCollapseHazard()])
|
||||
{
|
||||
}
|
||||
|
||||
public LevelState AdvanceTurn(LevelState level)
|
||||
{
|
||||
return AdvanceTurn(level, true);
|
||||
}
|
||||
|
||||
public IReadOnlyList<Forecast> Forecast(LevelState level)
|
||||
{
|
||||
var forecasts = new List<Forecast>();
|
||||
var seen = new HashSet<ForecastKey>();
|
||||
var forecastLevel = level with { Cells = level.Cells.ToArray(), Forecasts = Array.Empty<Forecast>() };
|
||||
if (forecastLevel.Global.Lost)
|
||||
AddHazardForecasts(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
|
||||
|
||||
AddReactorReadyForecast(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
|
||||
|
||||
if (IsReactorReady(forecastLevel) || forecastLevel.Global.Lost || forecastLevel.Global.ReactorActivated)
|
||||
return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
|
||||
|
||||
for (var step = Balancing.Current.TurnIncrement; step <= Balancing.Current.MaxForecastStepCount; step++)
|
||||
{
|
||||
forecastLevel = AdvanceTurn(forecastLevel, false);
|
||||
AddHazardForecasts(forecasts, seen, forecastLevel, step);
|
||||
AddReactorReadyForecast(forecasts, seen, forecastLevel, step);
|
||||
|
||||
if (forecastLevel.Global.Lost || IsReactorReady(forecastLevel) || forecastLevel.Global.ReactorActivated)
|
||||
break;
|
||||
}
|
||||
|
||||
return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
|
||||
}
|
||||
|
||||
public LevelState ActivateReactor(LevelState level)
|
||||
{
|
||||
if (!IsReactorReady(level))
|
||||
return level with { Global = level.Global with { Status = "REACTOR NOT READY" } };
|
||||
|
||||
return level with {
|
||||
Global = level.Global with {
|
||||
ReactorActivated = true,
|
||||
Status = "REACTOR ONLINE"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private LevelState AdvanceTurn(LevelState level, bool updateForecasts)
|
||||
{
|
||||
var cells = level.Cells.ToArray();
|
||||
|
||||
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
|
||||
{
|
||||
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var index = level.Index(position);
|
||||
var cell = cells[index];
|
||||
|
||||
if (!cell.IsWalkable)
|
||||
continue;
|
||||
|
||||
foreach (var effect in m_Effects)
|
||||
cell = effect.Apply(cell);
|
||||
|
||||
cells[index] = cell with { Hazards = cell.Hazards.Clamp() };
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var areaEffect in m_AreaEffects)
|
||||
cells = areaEffect.Apply(level, cells);
|
||||
|
||||
var global = UpdateGlobal(level, cells);
|
||||
var next = level with {
|
||||
Cells = cells,
|
||||
Global = global with { Turn = level.Global.Turn + Balancing.Current.TurnIncrement }
|
||||
};
|
||||
|
||||
return updateForecasts ? next with { Forecasts = Forecast(next) } : next;
|
||||
}
|
||||
|
||||
private void AddHazardForecasts(List<Forecast> forecasts, HashSet<ForecastKey> seen, LevelState level, int turns)
|
||||
{
|
||||
foreach (var hazard in m_Hazards)
|
||||
{
|
||||
foreach (var forecast in hazard.Predict(level, turns))
|
||||
AddForecast(forecasts, seen, forecast);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddReactorReadyForecast(List<Forecast> forecasts, HashSet<ForecastKey> seen, LevelState level, int turns)
|
||||
{
|
||||
if (IsReactorReady(level))
|
||||
AddForecast(forecasts, seen, new(EFailureKind.ReactorReady, null, turns, "REACTOR READY"));
|
||||
}
|
||||
|
||||
private static void AddForecast(List<Forecast> forecasts, HashSet<ForecastKey> seen, Forecast forecast)
|
||||
{
|
||||
if (seen.Add(new(forecast.Kind, forecast.Position)))
|
||||
forecasts.Add(forecast);
|
||||
}
|
||||
|
||||
private static GlobalState UpdateGlobal(LevelState level, CellState[] cells)
|
||||
{
|
||||
var reactorHeat = cells.Where(c => c.Prop == ECellProp.Reactor).Select(c => c.Hazards.Heat).DefaultIfEmpty(level.Global.CoreHeat).Max();
|
||||
var poweredGenerators = cells.Count(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
|
||||
var poweredPumps = cells.Count(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
|
||||
var damagedCriticalCells = cells.Count(c => c.Prop is ECellProp.Reactor or ECellProp.Generator or ECellProp.CoolingPump && c.Hazards.Stability <= Balancing.Current.CriticalCellStabilityThreshold);
|
||||
var stability = Rules.Clamp(level.Global.FacilityStability - damagedCriticalCells);
|
||||
var lost = reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold || stability <= Balancing.Current.StabilityCollapseThreshold;
|
||||
var status = lost ? reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold ? "CORE MELTDOWN" : "FACILITY STABILITY COLLAPSE" : "STABILIZE SYSTEMS";
|
||||
var global = level.Global with {
|
||||
CoreHeat = Rules.Clamp(reactorHeat - poweredPumps),
|
||||
Power = Rules.Clamp(poweredGenerators * Balancing.Current.GeneratorPowerOutput),
|
||||
Cooling = Rules.Clamp(poweredPumps * Balancing.Current.CoolingPumpOutput),
|
||||
FacilityStability = stability,
|
||||
Lost = lost,
|
||||
Status = status
|
||||
};
|
||||
|
||||
return IsReactorReady(level with { Cells = cells, Global = global }) ? global with { Status = "REACTOR READY" } : global;
|
||||
}
|
||||
|
||||
private static bool IsReactorReady(LevelState level)
|
||||
{
|
||||
var hasReactor = level.Cells.Any(c => c.Prop == ECellProp.Reactor);
|
||||
var hasStablePower = level.Global.Power >= Balancing.Current.ReactorReadyPowerThreshold || level.Cells.Any(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
|
||||
var hasCooling = level.Global.Cooling >= Balancing.Current.ReactorReadyCoolingThreshold || level.Cells.Any(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
|
||||
var reactorStable = level.Global.CoreHeat < Balancing.Current.ReactorReadyCoreHeatThreshold;
|
||||
return hasReactor && hasStablePower && hasCooling && reactorStable && !level.Global.Lost;
|
||||
}
|
||||
|
||||
private readonly IReadOnlyList<IAreaSimulationEffect> m_AreaEffects = areaEffects.ToArray();
|
||||
private readonly IReadOnlyList<ISimulationEffect> m_Effects = effects.ToArray();
|
||||
private readonly IReadOnlyList<Hazard> m_Hazards = hazards.ToArray();
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed class SimulationEngine
|
||||
{
|
||||
public LevelState MoveRobot(LevelState level, GridPosition destination)
|
||||
{
|
||||
return PlayerActionSystem.MoveRobot(level, destination);
|
||||
}
|
||||
|
||||
public LevelState InteractProp(LevelState level)
|
||||
{
|
||||
return PlayerActionSystem.InteractProp(level, ResolveStep);
|
||||
}
|
||||
|
||||
public LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy)
|
||||
{
|
||||
return PlayerActionSystem.InteractLeak(level, carrier, useRemedy, ResolveStep);
|
||||
}
|
||||
|
||||
public LevelState ApplyHeatShield(LevelState level)
|
||||
{
|
||||
return PlayerActionSystem.ApplyHeatShield(level, ResolveStep);
|
||||
}
|
||||
|
||||
private LevelState ResolveStep(LevelState level)
|
||||
{
|
||||
return ResolveStep(level, true);
|
||||
}
|
||||
|
||||
public LevelState ActivateReactor(LevelState level)
|
||||
{
|
||||
return ReactorSystem.Activate(level);
|
||||
}
|
||||
|
||||
public LevelState EndTurn(LevelState level)
|
||||
{
|
||||
return ResolveStep(level);
|
||||
}
|
||||
|
||||
public LevelState AdvanceTurn(LevelState level)
|
||||
{
|
||||
return ResolveStep(level);
|
||||
}
|
||||
|
||||
public IReadOnlyList<Forecast> Forecast(LevelState level)
|
||||
{
|
||||
return ForecastSystem.Forecast(level, simulated => ResolveStep(simulated, false));
|
||||
}
|
||||
|
||||
private LevelState ResolveStep(LevelState level, bool refreshForecasts)
|
||||
{
|
||||
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 = level;
|
||||
next = NetworkPropagationSystem.Propagate(next);
|
||||
next = ConsumerSystem.Resolve(next);
|
||||
next = StructuralIntegritySystem.Resolve(next);
|
||||
next = LeakSystem.Inject(next);
|
||||
next = SurfaceInteractionSystem.Resolve(next);
|
||||
next = RobotSafetySystem.Resolve(next);
|
||||
next = ReactorSystem.DeriveState(next);
|
||||
next = SurfaceInteractionSystem.AdvanceDurations(next);
|
||||
next = next with {
|
||||
Global = next.Global with {
|
||||
Turn = next.Global.Turn + 1
|
||||
}
|
||||
};
|
||||
|
||||
return refreshForecasts ? next with { Forecasts = Forecast(next) } : next;
|
||||
}
|
||||
|
||||
private readonly LevelValidator m_Validator = new();
|
||||
}
|
||||
24
src/ReactorMaintenance.Simulation/SurfaceCarrierMath.cs
Normal file
24
src/ReactorMaintenance.Simulation/SurfaceCarrierMath.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class SurfaceCarrierMath
|
||||
{
|
||||
public static SurfaceState AddCarrier(SurfaceState surface, ECarrierType carrier, float amount)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => surface with { Fuel = surface.Fuel + amount },
|
||||
ECarrierType.Coolant => surface with { Coolant = surface.Coolant + amount },
|
||||
ECarrierType.Electricity => surface with { Electricity = surface.Electricity + amount },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
public static SurfaceState RemoveCarrier(SurfaceState surface, ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => surface with { Fuel = 0, FuelBlockTurns = Balancing.Current.RemedyBlockTurns },
|
||||
ECarrierType.Coolant => surface with { Coolant = 0, CoolantBlockTurns = Balancing.Current.RemedyBlockTurns },
|
||||
ECarrierType.Electricity => surface with { Electricity = 0, ElectricityBlockTurns = Balancing.Current.RemedyBlockTurns },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed record SurfaceInteractionEffect
|
||||
{
|
||||
public static SurfaceInteractionEffect Hold { get; } = new();
|
||||
|
||||
public ESurfaceInteractionVerb Verb { get; init; }
|
||||
public ESurfaceQuantity Quantity { get; init; }
|
||||
public float Amount { get; init; }
|
||||
public float SecondaryAmount { get; init; }
|
||||
}
|
||||
28
src/ReactorMaintenance.Simulation/SurfaceStateExtensions.cs
Normal file
28
src/ReactorMaintenance.Simulation/SurfaceStateExtensions.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class SurfaceStateExtensions
|
||||
{
|
||||
public static SurfaceState Clamp(this SurfaceState surface)
|
||||
{
|
||||
var balancing = Balancing.Current;
|
||||
return surface with {
|
||||
Fuel = balancing.ClampValue(surface.Fuel),
|
||||
Coolant = balancing.ClampValue(surface.Coolant),
|
||||
Electricity = balancing.ClampValue(surface.Electricity),
|
||||
Heat = balancing.ClampValue(surface.Heat),
|
||||
FuelBlockTurns = Math.Max(0, surface.FuelBlockTurns),
|
||||
CoolantBlockTurns = Math.Max(0, surface.CoolantBlockTurns),
|
||||
ElectricityBlockTurns = Math.Max(0, surface.ElectricityBlockTurns)
|
||||
};
|
||||
}
|
||||
|
||||
public static bool Blocks(this SurfaceState surface, ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => surface.FuelBlockTurns > 0,
|
||||
ECarrierType.Coolant => surface.CoolantBlockTurns > 0,
|
||||
ECarrierType.Electricity => surface.ElectricityBlockTurns > 0,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
}
|
||||
72
src/ReactorMaintenance.Simulation/Systems/ConsumerSystem.cs
Normal file
72
src/ReactorMaintenance.Simulation/Systems/ConsumerSystem.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
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)
|
||||
{
|
||||
var disabledFuel = DisabledServiceStateFor(level, position, ECarrierType.Fuel);
|
||||
var disabledCoolant = DisabledServiceStateFor(level, position, ECarrierType.Coolant);
|
||||
var disabledElectricity = DisabledServiceStateFor(level, position, ECarrierType.Electricity);
|
||||
props[index] = prop with {
|
||||
ServiceState = Aggregate(disabledFuel, disabledCoolant, disabledElectricity),
|
||||
FuelServiceState = disabledFuel,
|
||||
CoolantServiceState = disabledCoolant,
|
||||
ElectricityServiceState = disabledElectricity
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
var fuel = ServiceStateFor(level, position, ECarrierType.Fuel);
|
||||
var coolant = ServiceStateFor(level, position, ECarrierType.Coolant);
|
||||
var electricity = ServiceStateFor(level, position, ECarrierType.Electricity);
|
||||
props[index] = prop with {
|
||||
ServiceState = Aggregate(fuel, coolant, electricity),
|
||||
FuelServiceState = fuel,
|
||||
CoolantServiceState = coolant,
|
||||
ElectricityServiceState = electricity
|
||||
};
|
||||
}
|
||||
|
||||
return level with { Props = props };
|
||||
}
|
||||
|
||||
private static EConsumerServiceState ServiceStateFor(LevelState level, GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
var underground = level.GetUnderground(position, carrier);
|
||||
if (!underground.IsPresent)
|
||||
return EConsumerServiceState.Unknown;
|
||||
|
||||
var supplied = underground.Amount >= Balancing.Current.ConsumerRequiredAmount && underground.Intensity >= Balancing.Current.ConsumerRequiredIntensity;
|
||||
return supplied ? EConsumerServiceState.Producing : EConsumerServiceState.Starved;
|
||||
}
|
||||
|
||||
private static EConsumerServiceState DisabledServiceStateFor(LevelState level, GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
return level.GetUnderground(position, carrier).IsPresent ? EConsumerServiceState.Disabled : EConsumerServiceState.Unknown;
|
||||
}
|
||||
|
||||
private static EConsumerServiceState Aggregate(params EConsumerServiceState[] states)
|
||||
{
|
||||
var participating = states.Where(state => state != EConsumerServiceState.Unknown).ToArray();
|
||||
if (participating.Length == 0)
|
||||
return EConsumerServiceState.Unknown;
|
||||
|
||||
if (participating.Any(state => state == EConsumerServiceState.Starved))
|
||||
return EConsumerServiceState.Starved;
|
||||
|
||||
if (participating.Any(state => state == EConsumerServiceState.Disabled))
|
||||
return EConsumerServiceState.Disabled;
|
||||
|
||||
return EConsumerServiceState.Producing;
|
||||
}
|
||||
}
|
||||
68
src/ReactorMaintenance.Simulation/Systems/ForecastSystem.cs
Normal file
68
src/ReactorMaintenance.Simulation/Systems/ForecastSystem.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
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)
|
||||
{
|
||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||
{
|
||||
if (prop.ServiceStateFor(carrier) == EConsumerServiceState.Starved)
|
||||
forecasts.Add(new(EForecastKind.ConsumerStarved, position, turn, $"{carrier} consumer starved"));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||
{
|
||||
var underground = level.GetUnderground(position, carrier);
|
||||
if (underground.IsPresent && underground.StructuralIntegrity <= Balancing.Current.StructuralIntegrityLeakThreshold)
|
||||
forecasts.Add(new(EForecastKind.StructuralIntegrity, position, turn, $"{carrier} structural integrity failing"));
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/ReactorMaintenance.Simulation/Systems/LeakSystem.cs
Normal file
24
src/ReactorMaintenance.Simulation/Systems/LeakSystem.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class LeakSystem
|
||||
{
|
||||
public static LevelState Inject(LevelState level)
|
||||
{
|
||||
var surface = level.Surface.ToArray();
|
||||
foreach (var leak in level.Leaks.Where(leak => !leak.Repaired))
|
||||
{
|
||||
var underground = level.GetUnderground(leak.UndergroundPosition, leak.Carrier);
|
||||
if (underground.State != EUndergroundState.Leaking)
|
||||
continue;
|
||||
|
||||
var accessIndex = level.Index(leak.AccessPosition);
|
||||
if (surface[accessIndex].Blocks(leak.Carrier))
|
||||
continue;
|
||||
|
||||
var amount = Balancing.Current.LeakBaseAmount + (underground.Amount * Balancing.Current.LeakAmountScale) + (underground.Intensity * Balancing.Current.LeakIntensityScale);
|
||||
surface[accessIndex] = SurfaceCarrierMath.AddCarrier(surface[accessIndex], leak.Carrier, amount);
|
||||
}
|
||||
|
||||
return level with { Surface = surface.Select(cell => cell.Clamp()).ToArray() };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class NetworkPropagationSystem
|
||||
{
|
||||
public static LevelState Propagate(LevelState level)
|
||||
{
|
||||
var fuel = ClearTransient(level.Fuel);
|
||||
var coolant = ClearTransient(level.Coolant);
|
||||
var electricity = ClearTransient(level.Electricity);
|
||||
var next = level.WithRuntimeArrays(fuel, coolant, electricity, level.Surface.ToArray(), level.Props.ToArray());
|
||||
|
||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||
next = PropagateCarrier(next, carrier);
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
private static UndergroundCell[] ClearTransient(IReadOnlyList<UndergroundCell> layer)
|
||||
{
|
||||
return layer.Select(cell => cell with { Amount = 0, Intensity = 0 }).ToArray();
|
||||
}
|
||||
|
||||
private static LevelState PropagateCarrier(LevelState level, ECarrierType carrier)
|
||||
{
|
||||
var layer = level.Layer(carrier).ToArray();
|
||||
var sources = LevelTraversal.AllPositions(level).Where(position => level.GetProp(position) is { Type: EPropType.Flow, SwitchState: EPropSwitchState.Enabled, Carrier: var sourceCarrier } && sourceCarrier == carrier && level.GetUnderground(position, carrier).CarriesFlow).ToArray();
|
||||
var junctions = JunctionFlowAnalyzer.Analyze(level).Where(junction => junction.IsValid && junction.Carrier == carrier).ToDictionary(junction => junction.Position);
|
||||
|
||||
foreach (var source in sources)
|
||||
ApplySourceFlow(level, layer, source, carrier, junctions);
|
||||
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => level with { Fuel = layer },
|
||||
ECarrierType.Coolant => level with { Coolant = layer },
|
||||
ECarrierType.Electricity => level with { Electricity = layer },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
private static void ApplySourceFlow(LevelState level, UndergroundCell[] layer, GridPosition source, ECarrierType carrier, IReadOnlyDictionary<GridPosition, JunctionFlow> junctions)
|
||||
{
|
||||
var open = new Queue<(GridPosition Position, int Distance, float AmountFactor, float IntensityFactor)>();
|
||||
var best = new Dictionary<GridPosition, float>();
|
||||
open.Enqueue((source, 0, 1, 1));
|
||||
best[source] = 1;
|
||||
|
||||
while (open.Count > 0)
|
||||
{
|
||||
var current = open.Dequeue();
|
||||
var amount = Balancing.Current.ClampValue((Balancing.Current.SourceAmount * current.AmountFactor) - (current.Distance * Balancing.Current.DistanceAmountFalloff));
|
||||
var intensity = Balancing.Current.ClampValue((Balancing.Current.SourceIntensity * current.IntensityFactor) - (current.Distance * Balancing.Current.DistanceIntensityFalloff));
|
||||
var index = level.Index(current.Position);
|
||||
layer[index] = layer[index] with {
|
||||
Amount = Math.Max(layer[index].Amount, amount),
|
||||
Intensity = Math.Max(layer[index].Intensity, intensity)
|
||||
};
|
||||
|
||||
foreach (var next in current.Position.Neighbors().Where(level.InBounds))
|
||||
{
|
||||
if (!level.GetUnderground(next, carrier).CarriesFlow)
|
||||
continue;
|
||||
|
||||
var weights = BranchWeights(current.Position, next, junctions);
|
||||
var amountFactor = current.AmountFactor * weights.Amount;
|
||||
var intensityFactor = current.IntensityFactor * weights.Intensity;
|
||||
if (amountFactor <= 0 || intensityFactor <= 0)
|
||||
continue;
|
||||
|
||||
if (best.TryGetValue(next, out var oldBest) && oldBest >= amountFactor)
|
||||
continue;
|
||||
|
||||
best[next] = amountFactor;
|
||||
open.Enqueue((next, current.Distance + 1, amountFactor, intensityFactor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static (float Amount, float Intensity) BranchWeights(GridPosition from, GridPosition to, IReadOnlyDictionary<GridPosition, JunctionFlow> junctions)
|
||||
{
|
||||
if (!junctions.TryGetValue(from, out var junction))
|
||||
return (1, 1);
|
||||
|
||||
var weight = junction.WeightFor(to);
|
||||
return (weight, weight);
|
||||
}
|
||||
}
|
||||
133
src/ReactorMaintenance.Simulation/Systems/PlayerActionSystem.cs
Normal file
133
src/ReactorMaintenance.Simulation/Systems/PlayerActionSystem.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class PlayerActionSystem
|
||||
{
|
||||
public static LevelState MoveRobot(LevelState level, GridPosition destination)
|
||||
{
|
||||
if (!CanAct(level) || !level.IsFloor(destination) || level.Robot.Position.ManhattanDistance(destination) != 1)
|
||||
return Refuse(level, "MOVE BLOCKED");
|
||||
|
||||
return 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> resolveLengthyAction)
|
||||
{
|
||||
if (!CanAct(level))
|
||||
return Refuse(level, "NO CONTROL");
|
||||
|
||||
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, prop),
|
||||
EPropType.AllSeeingEyeTerminal => level with { Global = level.Global with { Status = "ALL-SEEING-EYE AVAILABLE" } },
|
||||
EPropType.RemedySupply => PickUpRemedy(level, position, prop),
|
||||
EPropType.ReactorControl => ReactorSystem.Activate(level),
|
||||
_ => level
|
||||
};
|
||||
|
||||
return prop.Type == EPropType.AllSeeingEyeTerminal || prop.Type == EPropType.ReactorControl ? next : resolveLengthyAction(next);
|
||||
}
|
||||
|
||||
public static LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy, Func<LevelState, LevelState> resolveLengthyAction)
|
||||
{
|
||||
if (!CanAct(level))
|
||||
return Refuse(level, "NO CONTROL");
|
||||
|
||||
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 resolveLengthyAction(next);
|
||||
}
|
||||
|
||||
public static LevelState ApplyHeatShield(LevelState level, Func<LevelState, LevelState> resolveLengthyAction)
|
||||
{
|
||||
if (!CanAct(level) || level.Robot.HeatShields <= 0)
|
||||
return Refuse(level, "NO HEAT SHIELD");
|
||||
|
||||
return resolveLengthyAction(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, PropState prop)
|
||||
{
|
||||
var doorState = prop.DoorState == EDoorState.Open ? EDoorState.Closed : EDoorState.Open;
|
||||
return level.SetProp(position, prop with { DoorState = doorState });
|
||||
}
|
||||
|
||||
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,
|
||||
StructuralIntegrity = Balancing.Current.MaxStructuralIntegrity
|
||||
}) 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 CanAct(LevelState level)
|
||||
{
|
||||
return level.Global.LevelState is not (ELevelState.Lost or ELevelState.Won);
|
||||
}
|
||||
|
||||
private static LevelState Refuse(LevelState level, string message)
|
||||
{
|
||||
return level with { Global = level.Global with { Status = message } };
|
||||
}
|
||||
}
|
||||
91
src/ReactorMaintenance.Simulation/Systems/ReactorSystem.cs
Normal file
91
src/ReactorMaintenance.Simulation/Systems/ReactorSystem.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
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
|
||||
|| !HasRequiredConsumers(level)
|
||||
|| level.Props.Any(prop => prop.Type == EPropType.Consumer && HasConsumerTrouble(prop))
|
||||
|| 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() } };
|
||||
}
|
||||
|
||||
private static bool IsReady(LevelState level, ReactorState reactor)
|
||||
{
|
||||
return ReactorFeedsPresentAndProducing(level, reactor.ControlPosition)
|
||||
&& ProducingConsumerCount(level, ECarrierType.Fuel) >= level.RequiredFuelConsumers
|
||||
&& ProducingConsumerCount(level, ECarrierType.Coolant) >= level.RequiredCoolantConsumers
|
||||
&& ProducingConsumerCount(level, ECarrierType.Electricity) >= level.RequiredElectricityConsumers
|
||||
&& level.GetSurface(reactor.ControlPosition).Heat < Balancing.Current.TerminalHeat;
|
||||
}
|
||||
|
||||
private static bool ReactorFeedsPresentAndProducing(LevelState level, GridPosition position)
|
||||
{
|
||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||
{
|
||||
var underground = level.GetUnderground(position, carrier);
|
||||
if (underground.IsPresent && (underground.Amount <= 0 || underground.Intensity <= 0))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int ProducingConsumerCount(LevelState level, ECarrierType carrier)
|
||||
{
|
||||
return LevelTraversal.AllPositions(level)
|
||||
.Count(position => level.GetProp(position) is { Type: EPropType.Consumer } prop && prop.ServiceStateFor(carrier) == EConsumerServiceState.Producing);
|
||||
}
|
||||
|
||||
private static bool HasRequiredConsumers(LevelState level)
|
||||
{
|
||||
return ProducingConsumerCount(level, ECarrierType.Fuel) >= level.RequiredFuelConsumers
|
||||
&& ProducingConsumerCount(level, ECarrierType.Coolant) >= level.RequiredCoolantConsumers
|
||||
&& ProducingConsumerCount(level, ECarrierType.Electricity) >= level.RequiredElectricityConsumers;
|
||||
}
|
||||
|
||||
private static bool HasConsumerTrouble(PropState prop)
|
||||
{
|
||||
return prop.FuelServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled
|
||||
|| prop.CoolantServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled
|
||||
|| prop.ElectricityServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class StructuralIntegritySystem
|
||||
{
|
||||
public static LevelState Resolve(LevelState level)
|
||||
{
|
||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||
level = ResolveCarrier(level, carrier);
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
private static LevelState ResolveCarrier(LevelState level, ECarrierType carrier)
|
||||
{
|
||||
var layer = level.Layer(carrier).ToArray();
|
||||
var leaks = level.Leaks.ToList();
|
||||
|
||||
foreach (var position in LevelTraversal.AllPositions(level))
|
||||
{
|
||||
var index = level.Index(position);
|
||||
var cell = layer[index];
|
||||
if (!cell.IsPresent)
|
||||
continue;
|
||||
|
||||
var integrity = DegradeIntegrity(cell, carrier);
|
||||
var state = cell.State;
|
||||
if (state != EUndergroundState.Leaking && integrity <= Balancing.Current.StructuralIntegrityLeakThreshold && cell.Intensity > 0)
|
||||
{
|
||||
state = EUndergroundState.Leaking;
|
||||
leaks = UpsertLeak(leaks, level, position, carrier);
|
||||
}
|
||||
|
||||
layer[index] = cell with { State = state, StructuralIntegrity = integrity };
|
||||
}
|
||||
|
||||
var next = 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.")
|
||||
};
|
||||
|
||||
return next with { Leaks = leaks.ToArray() };
|
||||
}
|
||||
|
||||
private static int DegradeIntegrity(UndergroundCell cell, ECarrierType carrier)
|
||||
{
|
||||
if (cell.StructuralIntegrity >= Balancing.Current.MaxStructuralIntegrity && cell.Intensity <= Balancing.Current.StructuralPressureThreshold(carrier))
|
||||
return Balancing.Current.MaxStructuralIntegrity;
|
||||
|
||||
var overPressure = Math.Max(0, cell.Intensity - Balancing.Current.StructuralPressureThreshold(carrier));
|
||||
if (overPressure <= 0 || cell.StructuralIntegrity >= Balancing.Current.MaxStructuralIntegrity)
|
||||
return Math.Clamp(cell.StructuralIntegrity, 0, Balancing.Current.MaxStructuralIntegrity);
|
||||
|
||||
var damage = Math.Max(1, (int)Math.Ceiling(overPressure * Balancing.Current.StructuralIntegrityDamageScale));
|
||||
return Math.Clamp(cell.StructuralIntegrity - damage, 0, Balancing.Current.MaxStructuralIntegrity);
|
||||
}
|
||||
|
||||
private static List<LeakState> UpsertLeak(List<LeakState> leaks, LevelState level, GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
var accessPosition = carrier == ECarrierType.Electricity
|
||||
? position.Neighbors().FirstOrDefault(level.IsFloor)
|
||||
: position;
|
||||
if (accessPosition is null || !level.IsFloor(accessPosition))
|
||||
return leaks;
|
||||
|
||||
var index = leaks.FindIndex(leak => leak.UndergroundPosition == position && leak.Carrier == carrier);
|
||||
var leakState = new LeakState {
|
||||
Carrier = carrier,
|
||||
UndergroundPosition = position,
|
||||
AccessPosition = accessPosition,
|
||||
Repaired = false
|
||||
};
|
||||
|
||||
if (index >= 0)
|
||||
leaks[index] = leakState;
|
||||
else
|
||||
leaks.Add(leakState);
|
||||
|
||||
return leaks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
internal static class SurfaceInteractionSystem
|
||||
{
|
||||
private sealed class SurfaceDelta
|
||||
{
|
||||
public SurfaceState Apply(SurfaceState surface)
|
||||
{
|
||||
return surface with {
|
||||
Fuel = surface.Fuel + Fuel,
|
||||
Coolant = surface.Coolant + Coolant,
|
||||
Electricity = surface.Electricity + Electricity,
|
||||
Heat = surface.Heat + Heat
|
||||
};
|
||||
}
|
||||
|
||||
public float Fuel { get; set; }
|
||||
public float Coolant { get; set; }
|
||||
public float Electricity { get; set; }
|
||||
public float Heat { get; set; }
|
||||
}
|
||||
|
||||
public static LevelState Resolve(LevelState level)
|
||||
{
|
||||
var deltas = Enumerable.Range(0, level.Width * level.Height).Select(_ => new SurfaceDelta()).ToArray();
|
||||
foreach (var position in LevelTraversal.AllPositions(level).Where(level.IsFloor))
|
||||
ApplySameCellInteractions(level, position, deltas);
|
||||
|
||||
foreach (var position in LevelTraversal.AllPositions(level).Where(level.IsFloor))
|
||||
{
|
||||
foreach (var neighbor in position.Neighbors().Where(level.IsFloor))
|
||||
{
|
||||
if (level.Index(position) >= level.Index(neighbor) || level.IsClosedDoorEdge(position, neighbor))
|
||||
continue;
|
||||
|
||||
ApplyAdjacentInteractions(level, position, neighbor, deltas);
|
||||
}
|
||||
}
|
||||
|
||||
var surface = level.Surface.ToArray();
|
||||
for (var i = 0; i < surface.Length; i++)
|
||||
surface[i] = deltas[i].Apply(surface[i]).Clamp();
|
||||
|
||||
return level with { Surface = surface };
|
||||
}
|
||||
|
||||
public static LevelState AdvanceDurations(LevelState level)
|
||||
{
|
||||
var surface = level.Surface.Select(cell => cell with {
|
||||
FuelBlockTurns = Math.Max(0, cell.FuelBlockTurns - 1),
|
||||
CoolantBlockTurns = Math.Max(0, cell.CoolantBlockTurns - 1),
|
||||
ElectricityBlockTurns = Math.Max(0, cell.ElectricityBlockTurns - 1)
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return level with { Surface = surface };
|
||||
}
|
||||
|
||||
private static void ApplySameCellInteractions(LevelState level, GridPosition position, SurfaceDelta[] deltas)
|
||||
{
|
||||
var surface = level.GetSurface(position);
|
||||
ApplyPair(level, position, ECarrierType.Fuel, SimulationBands.Fuel(surface.Fuel), ECarrierType.Electricity, SimulationBands.Electricity(surface.Electricity), deltas);
|
||||
ApplyPair(level, position, ECarrierType.Fuel, SimulationBands.Fuel(surface.Fuel), null, SimulationBands.Heat(surface.Heat), deltas);
|
||||
ApplyPair(level, position, ECarrierType.Coolant, SimulationBands.Coolant(surface.Coolant), ECarrierType.Electricity, SimulationBands.Electricity(surface.Electricity), deltas);
|
||||
ApplyPair(level, position, ECarrierType.Coolant, SimulationBands.Coolant(surface.Coolant), null, SimulationBands.Heat(surface.Heat), deltas);
|
||||
}
|
||||
|
||||
private static void ApplyAdjacentInteractions(LevelState level, GridPosition a, GridPosition b, SurfaceDelta[] deltas)
|
||||
{
|
||||
var surfaceA = level.GetSurface(a);
|
||||
var surfaceB = level.GetSurface(b);
|
||||
FlowBetween(level, a, b, surfaceA.Fuel, surfaceB.Fuel, Balancing.Current.FlowInteraction(ESurfaceQuantity.Fuel), deltas);
|
||||
FlowBetween(level, a, b, surfaceA.Coolant, surfaceB.Coolant, Balancing.Current.FlowInteraction(ESurfaceQuantity.Coolant), deltas);
|
||||
FlowBetween(level, a, b, surfaceA.Electricity, surfaceB.Electricity, Balancing.Current.FlowInteraction(ESurfaceQuantity.Electricity), deltas);
|
||||
FlowBetween(level, a, b, surfaceA.Heat, surfaceB.Heat, Balancing.Current.FlowInteraction(ESurfaceQuantity.Heat), deltas);
|
||||
}
|
||||
|
||||
private static void ApplyPair(LevelState level, GridPosition position, ECarrierType? rowCarrier, EBand rowBand, ECarrierType? colCarrier, EBand colBand, SurfaceDelta[] deltas)
|
||||
{
|
||||
ApplyEffect(level, position, Balancing.Current.SameCellInteraction(rowCarrier, rowBand, colCarrier, colBand), deltas);
|
||||
}
|
||||
|
||||
private static void ApplyEffect(LevelState level, GridPosition position, SurfaceInteractionEffect effect, SurfaceDelta[] deltas)
|
||||
{
|
||||
var index = level.Index(position);
|
||||
switch (effect.Verb)
|
||||
{
|
||||
case ESurfaceInteractionVerb.Warm:
|
||||
deltas[index].Heat += effect.Amount;
|
||||
break;
|
||||
case ESurfaceInteractionVerb.Quench:
|
||||
deltas[index].Heat -= effect.Amount;
|
||||
break;
|
||||
case ESurfaceInteractionVerb.Short:
|
||||
deltas[index].Heat += effect.Amount;
|
||||
deltas[index].Electricity -= effect.SecondaryAmount;
|
||||
break;
|
||||
case ESurfaceInteractionVerb.Ignite:
|
||||
deltas[index].Heat += effect.Amount;
|
||||
deltas[index].Fuel -= effect.SecondaryAmount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void FlowBetween(LevelState level, GridPosition a, GridPosition b, float valueA, float valueB, SurfaceInteractionEffect effect, SurfaceDelta[] deltas)
|
||||
{
|
||||
var difference = valueA - valueB;
|
||||
if (Math.Abs(difference) < 0.01f)
|
||||
return;
|
||||
|
||||
var amount = difference * effect.Amount;
|
||||
var indexA = level.Index(a);
|
||||
var indexB = level.Index(b);
|
||||
|
||||
switch (effect.Quantity)
|
||||
{
|
||||
case ESurfaceQuantity.Fuel:
|
||||
deltas[indexA].Fuel -= amount;
|
||||
deltas[indexB].Fuel += amount;
|
||||
break;
|
||||
case ESurfaceQuantity.Coolant:
|
||||
deltas[indexA].Coolant -= amount;
|
||||
deltas[indexB].Coolant += amount;
|
||||
break;
|
||||
case ESurfaceQuantity.Electricity:
|
||||
deltas[indexA].Electricity -= amount;
|
||||
deltas[indexB].Electricity += amount;
|
||||
break;
|
||||
case ESurfaceQuantity.Heat:
|
||||
deltas[indexA].Heat -= amount;
|
||||
deltas[indexB].Heat += amount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace ReactorMaintenance.Win2D;
|
||||
|
||||
public partial class App
|
||||
{
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
m_Window = new MainWindow();
|
||||
m_Window.Activate();
|
||||
}
|
||||
|
||||
private Window? m_Window;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace ReactorMaintenance.Win2D;
|
||||
|
||||
public partial class App
|
||||
{
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
m_Window = new MainWindow();
|
||||
m_Window.Activate();
|
||||
}
|
||||
|
||||
private Window? m_Window;
|
||||
}
|
||||
64
src/ReactorMaintenance.Win2D/EditorImageRegistry.cs
Normal file
64
src/ReactorMaintenance.Win2D/EditorImageRegistry.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||
|
||||
namespace ReactorMaintenance.Win2D;
|
||||
|
||||
public sealed class EditorImageRegistry
|
||||
{
|
||||
public async Task LoadAsync(CanvasControl sender)
|
||||
{
|
||||
m_Images.Clear();
|
||||
await LoadFolderAsync(sender, "Images", "Props");
|
||||
await LoadFolderAsync(sender, "Images", "Pipes");
|
||||
await LoadFolderAsync(sender, "Images", "Badges");
|
||||
await LoadFolderAsync(sender, "Images", "Elements");
|
||||
AddAlias("tool-floor", "floor");
|
||||
AddAlias("tool-wall", "wall");
|
||||
AddAlias("tool-heat", "heat");
|
||||
AddAlias("tool-robot", "robot");
|
||||
AddAlias("robot", "robot");
|
||||
AddAlias("prop-reactor", "reactor");
|
||||
AddAlias("prop-consumer", "generator");
|
||||
AddAlias("prop-flow", "generator");
|
||||
AddAlias("carrier-fuel-source", "generator");
|
||||
AddAlias("carrier-coolant-source", "cooling-pump");
|
||||
AddAlias("carrier-electricity-source", "generator");
|
||||
AddAlias("prop-junction", "pressure-regulator");
|
||||
AddAlias("prop-door", "wall");
|
||||
AddAlias("prop-eye-terminal", "diagnostic-terminal");
|
||||
AddAlias("prop-remedy", "repair");
|
||||
AddAlias("leak-fuel", "leak");
|
||||
AddAlias("leak-coolant", "leak");
|
||||
AddAlias("leak-electricity", "leak");
|
||||
AddAlias("hazard-heat", "heat");
|
||||
AddAlias("hazard-fuel", "fire");
|
||||
AddAlias("hazard-coolant", "leak");
|
||||
AddAlias("hazard-electricity", "heat");
|
||||
}
|
||||
|
||||
public CanvasBitmap? Get(string key)
|
||||
{
|
||||
return m_Images.GetValueOrDefault(key);
|
||||
}
|
||||
|
||||
private async Task LoadFolderAsync(CanvasControl sender, params string[] pathParts)
|
||||
{
|
||||
var folder = Path.Combine([AppContext.BaseDirectory, .. pathParts]);
|
||||
if (!Directory.Exists(folder))
|
||||
return;
|
||||
|
||||
foreach (var path in Directory.EnumerateFiles(folder, "*.png"))
|
||||
{
|
||||
var key = Path.GetFileNameWithoutExtension(path);
|
||||
m_Images[key] = await CanvasBitmap.LoadAsync(sender, path);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddAlias(string alias, string key)
|
||||
{
|
||||
if (!m_Images.ContainsKey(alias) && m_Images.TryGetValue(key, out var image))
|
||||
m_Images[alias] = image;
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, CanvasBitmap> m_Images = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -15,7 +15,10 @@
|
||||
<AppBarButton Icon="OpenFile" Label="Open" Click="Open_Click" />
|
||||
<AppBarButton Icon="Save" Label="Save" Click="Save_Click" />
|
||||
<AppBarSeparator />
|
||||
<AppBarButton Icon="Play" Label="Simulate" Click="Simulate_Click" />
|
||||
<AppBarButton x:Name="PlayPauseButton" Icon="Play" Label="Play" Click="PlayPause_Click" />
|
||||
<AppBarButton Icon="Forward" Label="End Turn" Click="EndTurn_Click" />
|
||||
<AppBarButton Label="Interact" Click="Interact_Click" />
|
||||
<AppBarButton Label="Heat Shield" Click="HeatShield_Click" />
|
||||
<AppBarButton Icon="Accept" Label="Activate" Click="Activate_Click" />
|
||||
</CommandBar>
|
||||
|
||||
@@ -28,6 +31,10 @@
|
||||
|
||||
<ScrollViewer Grid.Column="0" Background="#1C2126">
|
||||
<StackPanel Padding="12" Spacing="10">
|
||||
<TextBlock Text="Layer" FontSize="18" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ComboBox x:Name="LayerPicker"
|
||||
SelectionChanged="LayerPicker_SelectionChanged"
|
||||
HorizontalAlignment="Stretch" />
|
||||
<TextBlock Text="Tools" FontSize="18" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="ToolPicker">
|
||||
<ItemsControl.ItemsPanel>
|
||||
@@ -38,15 +45,19 @@
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ToggleButton IsChecked="{Binding IsSelected, Mode=TwoWay}"
|
||||
Checked="ToolToggle_Checked" ToolTipService.ToolTip="{Binding Label}"
|
||||
Padding="5" Margin="0,0,8,8">
|
||||
<Image Width="96" Height="96" Source="{Binding Icon}" Stretch="Uniform" />
|
||||
Checked="ToolToggle_Checked" ToolTipService.ToolTip="{Binding Label}"
|
||||
Width="112" MinHeight="46" Padding="6" Margin="0,0,8,8">
|
||||
<TextBlock Text="{Binding Label}" TextWrapping="WrapWholeWords"
|
||||
TextAlignment="Center"
|
||||
FontSize="12" />
|
||||
</ToggleButton>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Brush applies to the selected cell." Foreground="#9EA7AE" TextWrapping="Wrap" />
|
||||
<TextBlock Text="Left click paints. Use Robot to set the start position." Foreground="#9EA7AE"
|
||||
<TextBlock Text="Left click selects or paints. Right click clears surface prop and hazards."
|
||||
Foreground="#9EA7AE" TextWrapping="Wrap" />
|
||||
<TextBlock Text="Shift+left drag pans. Cursor drag moves the robot or a prop."
|
||||
Foreground="#9EA7AE"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
@@ -83,11 +94,193 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Global Systems" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="GlobalText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||
<TextBlock Text="Inventory" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="InventoryGrid">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid MinWidth="55" Margin="0,0,4,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{Binding Label}" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Row="1" Text="{Binding Value}" Foreground="#F4F1E8" FontSize="18" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Selected Cell" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="CellText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||
<TextBlock Text="Required" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="RequiredGrid">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Width="84" Margin="0,0,4,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{Binding Label}" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Row="1" Text="{Binding Value}" Foreground="#F4F1E8"
|
||||
FontSize="18" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock x:Name="HoveredCellText" Text="Hovered Cell:" FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="SelectedCellText" Text="Selected Cell:" FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="#F4F1E8" />
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Grid.Row="0" Text="Terrain" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Column="0" Grid.Row="1" x:Name="TerrainText" Foreground="#F4F1E8" FontSize="16"
|
||||
Margin="0,0,10,0" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Text="Prop" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="1" x:Name="PropText" Foreground="#F4F1E8" FontSize="16" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Consumers:" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="ConsumersGrid">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Width="96" Margin="0,0,4,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{Binding Label}" Foreground="#9EA7AE" FontSize="11"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock Grid.Row="1" Text="{Binding Value}" Foreground="#F4F1E8" FontSize="16" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Services:" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="ServicesGrid">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Width="96" Margin="0,0,4,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{Binding Label}" Foreground="#9EA7AE" FontSize="11"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock Grid.Row="1" Text="{Binding Value}" Foreground="#F4F1E8" FontSize="16" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Leaks:" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="LeaksGrid">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,0,4,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="{Binding Label}" Foreground="#F4F1E8" FontSize="16"
|
||||
Margin="0,0,10,0" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding Value}" Foreground="#F4F1E8"
|
||||
FontSize="16" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Surface" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="SurfaceGrid">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Width="64" Margin="0,0,4,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{Binding Label}" Foreground="#9EA7AE" FontSize="11"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock Grid.Row="1" Text="{Binding Value}" Foreground="#F4F1E8" FontSize="16" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Network" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<Grid ColumnSpacing="6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="64" />
|
||||
<ColumnDefinition Width="60" />
|
||||
<ColumnDefinition Width="34" />
|
||||
<ColumnDefinition Width="34" />
|
||||
<ColumnDefinition Width="34" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="Carrier" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Column="1" Text="State" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Column="2" Text="Amt" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Column="3" Text="Int" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Column="4" Text="HP" Foreground="#9EA7AE" FontSize="11" />
|
||||
</Grid>
|
||||
<ItemsControl x:Name="NetworkGrid">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,0,0,8" ColumnSpacing="6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="64" />
|
||||
<ColumnDefinition Width="60" />
|
||||
<ColumnDefinition Width="34" />
|
||||
<ColumnDefinition Width="34" />
|
||||
<ColumnDefinition Width="34" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="{Binding Carrier}" Foreground="#F4F1E8" FontSize="12"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding State}" Foreground="#9EA7AE"
|
||||
FontSize="12" TextWrapping="Wrap" />
|
||||
<TextBlock Grid.Column="2" Text="{Binding Amount}" Foreground="#F4F1E8"
|
||||
FontSize="12" />
|
||||
<TextBlock Grid.Column="3" Text="{Binding Intensity}" Foreground="#F4F1E8"
|
||||
FontSize="12" />
|
||||
<TextBlock Grid.Column="4" Text="{Binding Integrity}" Foreground="#F4F1E8"
|
||||
FontSize="12" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Forecasts" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="ForecastList">
|
||||
@@ -95,15 +288,7 @@
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="#46515A" BorderThickness="1" Padding="8" Margin="0,0,0,8"
|
||||
CornerRadius="3">
|
||||
<Grid ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image Width="28" Height="28" Source="{Binding Icon}" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding Message}" Foreground="#F4F1E8"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding Message}" Foreground="#F4F1E8" TextWrapping="Wrap" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
@@ -112,4 +297,4 @@
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
</Window>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>ReactorMaintenance.Win2D</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
@@ -13,16 +13,17 @@
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.4.0" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
|
||||
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.4.0"/>
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839"/>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ReactorMaintenance.Simulation\ReactorMaintenance.Simulation.csproj" />
|
||||
<ProjectReference Include="..\ReactorMaintenance.Simulation\ReactorMaintenance.Simulation.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="ReactorMaintenance.Win2D.app"/>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
<assemblyIdentity version="1.0.0.0" name="ReactorMaintenance.Win2D.app"/>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
|
||||
201
tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs
Normal file
201
tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
namespace ReactorMaintenance.Simulation.Tests;
|
||||
|
||||
public sealed class LevelEditorTests
|
||||
{
|
||||
[Fact]
|
||||
public void DoorToolPlacesSingleFloorDoorProp()
|
||||
{
|
||||
var level = LevelState.Create("Door editor", 6, 6);
|
||||
|
||||
var next = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Door });
|
||||
|
||||
Assert.Equal(EPropType.Door, next.GetProp(new(2, 2)).Type);
|
||||
Assert.Equal(EDoorState.Closed, next.GetProp(new(2, 2)).DoorState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoorToolTogglesExistingDoorState()
|
||||
{
|
||||
var level = LevelState.Create("Door editor", 6, 6);
|
||||
level = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Door });
|
||||
|
||||
var opened = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Door });
|
||||
var closed = LevelEditor.Apply(opened, new(2, 2), new() { Tool = EEditorTool.Door });
|
||||
|
||||
Assert.Equal(EDoorState.Open, opened.GetProp(new(2, 2)).DoorState);
|
||||
Assert.Equal(EDoorState.Closed, closed.GetProp(new(2, 2)).DoorState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WallToolPreservesUndergroundNetworks()
|
||||
{
|
||||
var level = LevelState.Create("Wall editor", 6, 6);
|
||||
var position = new GridPosition(2, 2);
|
||||
level = LevelEditor.Apply(level, position, new() { Tool = EEditorTool.Underground, Carrier = ECarrierType.Fuel });
|
||||
level = LevelEditor.Apply(level, position, new() { Tool = EEditorTool.Underground, Carrier = ECarrierType.Coolant });
|
||||
level = LevelEditor.Apply(level, position, new() { Tool = EEditorTool.Underground, Carrier = ECarrierType.Electricity });
|
||||
|
||||
var next = LevelEditor.Apply(level, position, new() { Tool = EEditorTool.Wall });
|
||||
|
||||
Assert.Equal(EUndergroundState.Intact, next.GetUnderground(position, ECarrierType.Fuel).State);
|
||||
Assert.Equal(EUndergroundState.Intact, next.GetUnderground(position, ECarrierType.Coolant).State);
|
||||
Assert.Equal(EUndergroundState.Intact, next.GetUnderground(position, ECarrierType.Electricity).State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UndergroundToolCanPaintAdjacentCellsRepeatedly()
|
||||
{
|
||||
var level = LevelState.Create("Network editor", 6, 6);
|
||||
var command = new EditorToolCommand { Tool = EEditorTool.Underground, Carrier = ECarrierType.Fuel };
|
||||
|
||||
level = LevelEditor.Apply(level, new(1, 1), command);
|
||||
level = LevelEditor.Apply(level, new(2, 1), command);
|
||||
level = LevelEditor.Apply(level, new(3, 1), command);
|
||||
|
||||
Assert.Equal(EUndergroundState.Intact, level.GetUnderground(new(1, 1), ECarrierType.Fuel).State);
|
||||
Assert.Equal(EUndergroundState.Intact, level.GetUnderground(new(2, 1), ECarrierType.Fuel).State);
|
||||
Assert.Equal(EUndergroundState.Intact, level.GetUnderground(new(3, 1), ECarrierType.Fuel).State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConsumerToolPlacesCarrierAgnosticConsumer()
|
||||
{
|
||||
var level = LevelState.Create("Consumer editor", 6, 6);
|
||||
|
||||
var next = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Consumer, Carrier = ECarrierType.Fuel });
|
||||
|
||||
Assert.Equal(EPropType.Consumer, next.GetProp(new(2, 2)).Type);
|
||||
Assert.Equal(EConsumerServiceState.Unknown, next.GetProp(new(2, 2)).FuelServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Unknown, next.GetProp(new(2, 2)).CoolantServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Unknown, next.GetProp(new(2, 2)).ElectricityServiceState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ElectricityLeakUsesAuthoredWallAccessFace()
|
||||
{
|
||||
var level = LevelState.Create("Electricity leak editor", 6, 6);
|
||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
||||
|
||||
var next = LevelEditor.SetLeak(level, new(2, 2), new(2, 3), ECarrierType.Electricity);
|
||||
var rejected = LevelEditor.SetLeak(next, new(2, 2), new(4, 4), ECarrierType.Electricity);
|
||||
|
||||
Assert.Single(next.Leaks);
|
||||
Assert.Equal(new(2, 3), next.Leaks[0].AccessPosition);
|
||||
Assert.Equal(EUndergroundState.Leaking, next.GetUnderground(new(2, 2), ECarrierType.Electricity).State);
|
||||
Assert.Equal(next.Leaks, rejected.Leaks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ElectricityLeakAccessCyclesAcrossAdjacentFloorFaces()
|
||||
{
|
||||
var level = LevelState.Create("Electricity leak editor", 6, 6);
|
||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
||||
level = level.SetTerrain(new(2, 1), ECellTerrain.Wall);
|
||||
level = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Underground, Carrier = ECarrierType.Electricity });
|
||||
|
||||
var first = LevelEditor.CycleElectricityLeakAccess(level, new(2, 2));
|
||||
var second = LevelEditor.CycleElectricityLeakAccess(first, new(2, 2));
|
||||
|
||||
Assert.Single(first.Leaks);
|
||||
Assert.Equal(new(3, 2), first.Leaks[0].AccessPosition);
|
||||
Assert.Single(second.Leaks);
|
||||
Assert.Equal(new(2, 3), second.Leaks[0].AccessPosition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReactorControlToolCreatesUnboundReactorState()
|
||||
{
|
||||
var level = LevelState.Create("Reactor editor", 6, 6);
|
||||
|
||||
var next = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.ReactorControl });
|
||||
|
||||
Assert.Single(next.Reactors);
|
||||
Assert.Equal(new(2, 2), next.Reactors[0].ControlPosition);
|
||||
Assert.Equal(1, next.Reactors[0].ReactorId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveOccupantMovesRobotToFloorDestination()
|
||||
{
|
||||
var level = LevelState.Create("Move editor", 6, 6) with { Robot = new() { Position = new(1, 1) } };
|
||||
|
||||
var next = LevelEditor.MoveOccupant(level, new(1, 1), new(3, 3));
|
||||
|
||||
Assert.Equal(new(3, 3), next.Robot.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveOccupantMovesPropAndUpdatesReactorControlPosition()
|
||||
{
|
||||
var level = LevelState.Create("Move editor", 6, 6);
|
||||
level = LevelEditor.Apply(level, new(1, 1), new() { Tool = EEditorTool.ReactorControl });
|
||||
|
||||
var next = LevelEditor.MoveOccupant(level, new(1, 1), new(3, 3));
|
||||
|
||||
Assert.Equal(EPropType.None, next.GetProp(new(1, 1)).Type);
|
||||
Assert.Equal(EPropType.ReactorControl, next.GetProp(new(3, 3)).Type);
|
||||
Assert.Equal(new(3, 3), next.Reactors[0].ControlPosition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveOccupantMovesSourcesAsProps()
|
||||
{
|
||||
var level = LevelState.Create("Move editor", 6, 6);
|
||||
level = LevelEditor.Apply(level, new(1, 1), new() { Tool = EEditorTool.Flow, Carrier = ECarrierType.Fuel });
|
||||
|
||||
var result = LevelEditor.TryMoveOccupant(level, new(1, 1), new(3, 3));
|
||||
|
||||
Assert.True(result.Success, result.Reason);
|
||||
Assert.Equal(EPropType.None, result.Level.GetProp(new(1, 1)).Type);
|
||||
Assert.Equal(EPropType.Flow, result.Level.GetProp(new(3, 3)).Type);
|
||||
Assert.Equal(ECarrierType.Fuel, result.Level.GetProp(new(3, 3)).Carrier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveOccupantMovesFuelLeakToFloorDestination()
|
||||
{
|
||||
var level = LevelState.Create("Move editor", 6, 6);
|
||||
level = LevelEditor.SetLeak(level, new(1, 1), new(1, 1), ECarrierType.Fuel);
|
||||
|
||||
var result = LevelEditor.TryMoveOccupant(level, new(1, 1), new(3, 3));
|
||||
|
||||
Assert.True(result.Success, result.Reason);
|
||||
Assert.Single(result.Level.Leaks);
|
||||
Assert.Equal(new(3, 3), result.Level.Leaks[0].UndergroundPosition);
|
||||
Assert.Equal(new(3, 3), result.Level.Leaks[0].AccessPosition);
|
||||
Assert.Equal(EUndergroundState.Leaking, result.Level.GetUnderground(new(3, 3), ECarrierType.Fuel).State);
|
||||
Assert.Equal(EUndergroundState.Absent, result.Level.GetUnderground(new(1, 1), ECarrierType.Fuel).State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveOccupantMovesElectricityLeakAccessFace()
|
||||
{
|
||||
var level = LevelState.Create("Move editor", 6, 6);
|
||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
||||
level = LevelEditor.SetLeak(level, new(2, 2), new(2, 3), ECarrierType.Electricity);
|
||||
|
||||
var result = LevelEditor.TryMoveOccupant(level, new(2, 3), new(3, 2));
|
||||
|
||||
Assert.True(result.Success, result.Reason);
|
||||
Assert.Single(result.Level.Leaks);
|
||||
Assert.Equal(new(2, 2), result.Level.Leaks[0].UndergroundPosition);
|
||||
Assert.Equal(new(3, 2), result.Level.Leaks[0].AccessPosition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveOccupantReportsInvalidStartAndDestinationReasons()
|
||||
{
|
||||
var level = LevelState.Create("Move editor", 6, 6);
|
||||
level = level.SetTerrain(new(3, 3), ECellTerrain.Wall);
|
||||
level = level.SetProp(new(1, 1), new() { Type = EPropType.Consumer });
|
||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Consumer });
|
||||
|
||||
var invalidStart = LevelEditor.TryMoveOccupant(level, new(4, 4), new(5, 5));
|
||||
var invalidDestination = LevelEditor.TryMoveOccupant(level, new(1, 1), new(2, 2));
|
||||
|
||||
Assert.False(invalidStart.Success);
|
||||
Assert.Contains("No movable", invalidStart.Reason);
|
||||
Assert.False(invalidDestination.Success);
|
||||
Assert.Contains("occupied", invalidDestination.Reason);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
|
||||
<PackageReference Include="xunit" Version="2.5.3"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ReactorMaintenance.Simulation\ReactorMaintenance.Simulation.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ReactorMaintenance.Simulation\ReactorMaintenance.Simulation.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,298 +1,324 @@
|
||||
using ReactorMaintenance.Simulation.Difficulties;
|
||||
using ReactorMaintenance.Simulation.Effects;
|
||||
using ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Tests;
|
||||
|
||||
public sealed class SimulationEngineTests
|
||||
{
|
||||
[Fact]
|
||||
public void FuelLeakNearPoweredGeneratorCreatesIgnitionForecast()
|
||||
{
|
||||
var level = LevelState.Create("Fuel leak", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Prop = ECellProp.Generator,
|
||||
Pipe = EPipeMedium.Fuel,
|
||||
LeakRate = Balancing.Current.FuelVaporFireThreshold,
|
||||
Pressure = Balancing.Current.PressurizedFuelLeakPressureThreshold + Balancing.Current.NeighborDistance,
|
||||
Integrity = Balancing.Current.DefaultEditedPipeIntegrity,
|
||||
Powered = true
|
||||
});
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Ignition && forecast.Position == new GridPosition(2, 2) && forecast.Turns == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoolantLeakOnPoweredCellRaisesElectricalCharge()
|
||||
{
|
||||
var level = LevelState.Create("Wet cable", 6, 6)
|
||||
.SetCell(new(3, 3), new() {
|
||||
Pipe = EPipeMedium.Coolant,
|
||||
LeakRate = Balancing.Current.ElectrifiedCoolantPoolingThreshold,
|
||||
Powered = true
|
||||
});
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.True(next.GetCell(new(3, 3)).Hazards.ElectricalCharge >= Balancing.Current.ElectricalChargeIncrease);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveFireSpreadsSmokeToOpenNeighbors()
|
||||
{
|
||||
var level = LevelState.Create("Smoke", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Hazards = new() {
|
||||
Fire = true,
|
||||
Smoke = Balancing.Current.SmokeSpreadThreshold
|
||||
}
|
||||
});
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.True(next.GetCell(new(2, 3)).Hazards.Smoke > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvanceTurnRunsConfiguredCellEffects()
|
||||
{
|
||||
var engine = new SimulationEngine([new TestCellEffect()], [], []);
|
||||
var level = LevelState.Create("Custom effect", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Hazards = new() { Heat = 1 }
|
||||
});
|
||||
|
||||
var next = engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Equal(5, next.GetCell(new(2, 2)).Hazards.Heat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvanceTurnRunsConfiguredAreaEffects()
|
||||
{
|
||||
var engine = new SimulationEngine([], [new TestAreaEffect()], []);
|
||||
var level = LevelState.Create("Custom area effect", 6, 6);
|
||||
|
||||
var next = engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Equal(7, next.GetCell(new(2, 2)).Hazards.Smoke);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverpressurePredictsPipeBurst()
|
||||
{
|
||||
var level = LevelState.Create("Pressure", 6, 6)
|
||||
.SetCell(new(1, 2), new() {
|
||||
Pipe = EPipeMedium.Pressure,
|
||||
Pressure = 10,
|
||||
Integrity = 6
|
||||
});
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.PipeBurst && forecast.Turns == 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastCapsFutureSimulationWhenNoTerminalConditionOccurs()
|
||||
{
|
||||
var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
|
||||
var level = LevelState.Create("Stable", 6, 6);
|
||||
|
||||
var forecasts = engine.Forecast(level);
|
||||
|
||||
Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
|
||||
Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Max(forecast => forecast.Turns));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastUsesCurrentBalancingProfile()
|
||||
{
|
||||
var previous = Balancing.Current;
|
||||
try
|
||||
{
|
||||
Balancing.Current = new TestBalancing();
|
||||
var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
|
||||
var level = LevelState.Create("Stable", 6, 6);
|
||||
|
||||
var forecasts = engine.Forecast(level);
|
||||
|
||||
Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Balancing.Current = previous;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastPredictsMeltdownFromFutureSimulation()
|
||||
{
|
||||
var level = LevelState.Create("Meltdown", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Prop = ECellProp.Reactor,
|
||||
Hazards = new() { Heat = Balancing.Current.MeltdownCoreHeatThreshold - Balancing.Current.ReactorHeatIncrease }
|
||||
});
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastReportsAlreadyLostLevelAtCurrentTurn()
|
||||
{
|
||||
var level = LevelState.Create("Lost", 6, 6) with {
|
||||
Global = new() {
|
||||
CoreHeat = Balancing.Current.MeltdownCoreHeatThreshold,
|
||||
Lost = true,
|
||||
Status = "CORE MELTDOWN"
|
||||
}
|
||||
};
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastPredictsStabilityCollapseFromFutureSimulation()
|
||||
{
|
||||
var level = LevelState.Create("Collapse", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Prop = ECellProp.Generator,
|
||||
Hazards = new() { Stability = Balancing.Current.CriticalCellStabilityThreshold }
|
||||
}) with {
|
||||
Global = new() { FacilityStability = Balancing.Current.FireStabilityDamage }
|
||||
};
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.StabilityCollapse && forecast.Turns == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StableReactorWithPowerAndCoolingCanActivate()
|
||||
{
|
||||
var level = LevelState.Create("Ready", 8, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Prop = ECellProp.Reactor,
|
||||
Hazards = new() { Heat = 3 }
|
||||
})
|
||||
.SetCell(new(3, 2), new() {
|
||||
Prop = ECellProp.Generator,
|
||||
Powered = true
|
||||
})
|
||||
.SetCell(new(4, 2), new() {
|
||||
Prop = ECellProp.CoolingPump,
|
||||
Powered = true
|
||||
});
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
var activated = m_Engine.ActivateReactor(next);
|
||||
|
||||
Assert.Equal("REACTOR ONLINE", activated.Global.Status);
|
||||
Assert.True(activated.Global.ReactorActivated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LevelSerializationRoundTripsEditableState()
|
||||
{
|
||||
var level = LevelState.Create("Round trip", 5, 5);
|
||||
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Reactor);
|
||||
level = LevelEditor.Apply(level, new(1, 2), EEditorTool.CoolantPipe);
|
||||
level = LevelEditor.Apply(level, new(1, 2), EEditorTool.Leak);
|
||||
|
||||
var json = LevelSerializer.Serialize(level);
|
||||
var loaded = LevelSerializer.Deserialize(json);
|
||||
|
||||
Assert.Contains("\"Version\": 1", json);
|
||||
Assert.Equal(level.Name, loaded.Name);
|
||||
Assert.Equal(ECellProp.Reactor, loaded.GetCell(new(2, 2)).Prop);
|
||||
Assert.Equal(EPipeMedium.Coolant, loaded.GetCell(new(1, 2)).Pipe);
|
||||
Assert.Equal(1, loaded.GetCell(new(1, 2)).LeakRate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LevelSerializationRejectsUnsupportedVersion()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"Version": 999,
|
||||
"Level": {}
|
||||
}
|
||||
""";
|
||||
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => LevelSerializer.Deserialize(json));
|
||||
|
||||
Assert.Contains("Unsupported level file version 999", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WallToolClearsCellPropsPipesAndHazards()
|
||||
{
|
||||
var level = LevelState.Create("Wall", 5, 5);
|
||||
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Generator);
|
||||
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.CoolantPipe);
|
||||
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Fire);
|
||||
|
||||
var edited = LevelEditor.Apply(level, new(2, 2), EEditorTool.Wall);
|
||||
var cell = edited.GetCell(new(2, 2));
|
||||
|
||||
Assert.Equal(ECellTerrain.Wall, cell.Terrain);
|
||||
Assert.Equal(ECellProp.None, cell.Prop);
|
||||
Assert.Equal(EPipeMedium.None, cell.Pipe);
|
||||
Assert.False(cell.Powered);
|
||||
Assert.False(cell.Hazards.Fire);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PropToolsKeepFloorTerrain()
|
||||
{
|
||||
var level = LevelState.Create("Prop", 5, 5);
|
||||
level = LevelEditor.Apply(level, new(1, 1), EEditorTool.Wall);
|
||||
|
||||
var edited = LevelEditor.Apply(level, new(1, 1), EEditorTool.Reactor);
|
||||
var cell = edited.GetCell(new(1, 1));
|
||||
|
||||
Assert.Equal(ECellTerrain.Floor, cell.Terrain);
|
||||
Assert.Equal(ECellProp.Reactor, cell.Prop);
|
||||
}
|
||||
|
||||
private readonly SimulationEngine m_Engine = new();
|
||||
|
||||
private sealed class StepCountingHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
|
||||
{
|
||||
yield return new(EFailureKind.PipeBurst, new(turns, 0), turns, $"STEP {turns}");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestBalancing : NormalBalancing
|
||||
{
|
||||
public override int MaxForecastStepCount => 2;
|
||||
}
|
||||
|
||||
private sealed class TestCellEffect : ISimulationEffect
|
||||
{
|
||||
public CellState Apply(CellState cell)
|
||||
{
|
||||
return cell with { Hazards = cell.Hazards with { Heat = 5 } };
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestAreaEffect : IAreaSimulationEffect
|
||||
{
|
||||
public CellState[] Apply(LevelState level, CellState[] cells)
|
||||
{
|
||||
var next = cells.ToArray();
|
||||
var position = new GridPosition(2, 2);
|
||||
var cell = next[level.Index(position)];
|
||||
next[level.Index(position)] = cell with { Hazards = cell.Hazards with { Smoke = 7 } };
|
||||
return next;
|
||||
}
|
||||
}
|
||||
namespace ReactorMaintenance.Simulation.Tests;
|
||||
|
||||
public sealed class SimulationEngineTests
|
||||
{
|
||||
[Fact]
|
||||
public void NetworkPropagationSuppliesConsumerServicesAndReadiesReactor()
|
||||
{
|
||||
var level = BuildReadyLevel();
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
var consumer = next.GetProp(new(3, 3));
|
||||
|
||||
Assert.Equal(EConsumerServiceState.Producing, consumer.FuelServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Producing, consumer.CoolantServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Producing, consumer.ElectricityServiceState);
|
||||
Assert.Equal(ELevelState.Ready, next.Global.LevelState);
|
||||
Assert.Contains(next.Forecasts, forecast => forecast.Kind == EForecastKind.ReactorReady);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReactorNeedsPositiveFlowOnlyForNetworksBeneathControl()
|
||||
{
|
||||
var level = BuildReadyLevel();
|
||||
level = level.SetUnderground(new(5, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.NotEqual(ELevelState.Ready, next.Global.LevelState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReactorActivatesOnlyAtReadyControl()
|
||||
{
|
||||
var level = m_Engine.AdvanceTurn(BuildReadyLevel()) with {
|
||||
Robot = new() { Position = new(5, 3) }
|
||||
};
|
||||
|
||||
var activated = m_Engine.ActivateReactor(level);
|
||||
|
||||
Assert.Equal(ELevelState.Won, activated.Global.LevelState);
|
||||
Assert.True(activated.Reactors[0].Activated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisabledConsumerReportsDisabledOnlyForNetworksBeneathIt()
|
||||
{
|
||||
var level = LevelState.Create("Disabled", 6, 6);
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Consumer, SwitchState = EPropSwitchState.Disabled });
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
var consumer = next.GetProp(new(2, 2));
|
||||
|
||||
Assert.Equal(EConsumerServiceState.Disabled, consumer.FuelServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Unknown, consumer.CoolantServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Unknown, consumer.ElectricityServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Disabled, consumer.ServiceState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovementIsQuickAndDoesNotResolveSimulationStep()
|
||||
{
|
||||
var level = LevelState.Create("Quick", 6, 6) with {
|
||||
Robot = new() { Position = new(1, 1) }
|
||||
};
|
||||
|
||||
var next = m_Engine.MoveRobot(level, new(2, 1));
|
||||
|
||||
Assert.Equal(new(2, 1), next.Robot.Position);
|
||||
Assert.Equal(0, next.Global.Turn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoorInteractionIsLengthyAndResolvesSimulationStep()
|
||||
{
|
||||
var level = DoorLevel();
|
||||
level = level with { Robot = new() { Position = new(3, 2) } };
|
||||
|
||||
var next = m_Engine.InteractProp(level);
|
||||
|
||||
Assert.Equal(EDoorState.Open, next.GetProp(new(3, 2)).DoorState);
|
||||
Assert.Equal(1, next.Global.Turn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClosedInferredDoorBlocksAdjacentHeatFlow()
|
||||
{
|
||||
var level = DoorLevel();
|
||||
level = level.SetSurface(new(3, 2), new() { Heat = 8 });
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Equal(0, next.GetSurface(new(4, 2)).Heat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StructuralIntegrityCreatesLeakWhenWeakCellHasPositivePressure()
|
||||
{
|
||||
var level = LevelState.Create("Integrity leak", 6, 6);
|
||||
level = AddLine(level, ECarrierType.Fuel, new(1, 2), new(2, 2));
|
||||
level = level.SetProp(new(1, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, level.GetUnderground(new(2, 2), ECarrierType.Fuel) with {
|
||||
StructuralIntegrity = Balancing.Current.StructuralIntegrityLeakThreshold
|
||||
});
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Equal(EUndergroundState.Leaking, next.GetUnderground(new(2, 2), ECarrierType.Fuel).State);
|
||||
Assert.Contains(next.Leaks, leak => leak.Carrier == ECarrierType.Fuel && leak.UndergroundPosition == new GridPosition(2, 2));
|
||||
Assert.True(next.GetSurface(new(2, 2)).Fuel > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HighPressureWorsensNonMaxStructuralIntegrity()
|
||||
{
|
||||
var level = LevelState.Create("Integrity damage", 6, 6);
|
||||
level = AddLine(level, ECarrierType.Fuel, new(1, 2), new(2, 2));
|
||||
level = level.SetProp(new(1, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, level.GetUnderground(new(2, 2), ECarrierType.Fuel) with {
|
||||
StructuralIntegrity = Balancing.Current.MaxStructuralIntegrity - 1
|
||||
});
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.True(next.GetUnderground(new(2, 2), ECarrierType.Fuel).StructuralIntegrity < Balancing.Current.MaxStructuralIntegrity - 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RepairingLeakRestoresStructuralIntegrity()
|
||||
{
|
||||
var level = LevelState.Create("Repair", 6, 6);
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() {
|
||||
State = EUndergroundState.Leaking,
|
||||
Amount = 5,
|
||||
Intensity = 5,
|
||||
StructuralIntegrity = 0
|
||||
}) with {
|
||||
Robot = new() { Position = new(2, 2) },
|
||||
Leaks = [new() { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }]
|
||||
};
|
||||
|
||||
var next = m_Engine.InteractLeak(level, ECarrierType.Fuel, false);
|
||||
|
||||
Assert.True(next.Leaks[0].Repaired);
|
||||
Assert.Equal(EUndergroundState.Intact, next.GetUnderground(new(2, 2), ECarrierType.Fuel).State);
|
||||
Assert.Equal(Balancing.Current.MaxStructuralIntegrity, next.GetUnderground(new(2, 2), ECarrierType.Fuel).StructuralIntegrity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ElementRemedyClearsHazardAndBlocksImmediateReentry()
|
||||
{
|
||||
var level = LevelState.Create("Remedy", 6, 6);
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Leaking, Amount = 5, Intensity = 5 });
|
||||
level = level.SetSurface(new(2, 2), new() { Fuel = 5 }) with {
|
||||
Robot = new() { Position = new(2, 2), FuelNeutralizers = 1 },
|
||||
Leaks = [new() { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }]
|
||||
};
|
||||
|
||||
var next = m_Engine.InteractLeak(level, ECarrierType.Fuel, true);
|
||||
|
||||
Assert.Equal(0, next.GetSurface(new(2, 2)).Fuel);
|
||||
Assert.True(next.GetSurface(new(2, 2)).FuelBlockTurns > 0);
|
||||
Assert.Equal(0, next.Robot.FuelNeutralizers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HeatShieldPreventsRobotHeatLoss()
|
||||
{
|
||||
var level = LevelState.Create("Heat shield", 6, 6);
|
||||
level = level.SetSurface(new(2, 2), new() { Heat = Balancing.Current.RobotHeatSafetyThreshold }) with {
|
||||
Robot = new() { Position = new(2, 2), HeatImmunitySteps = 1 }
|
||||
};
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.NotEqual(ELevelState.Lost, next.Global.LevelState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JunctionRatioSplitsFlowAcrossInferredOutgoingBranches()
|
||||
{
|
||||
var level = BuildJunctionLevel(2);
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.True(next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount > 0);
|
||||
Assert.Equal(next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount, next.GetUnderground(new(3, 3), ECarrierType.Fuel).Amount);
|
||||
Assert.True(next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount < next.GetUnderground(new(2, 3), ECarrierType.Fuel).Amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JunctionZeroWeightBranchReceivesNoIntentionalOutflow()
|
||||
{
|
||||
var level = BuildJunctionLevel(0);
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Equal(0, next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount);
|
||||
Assert.True(next.GetUnderground(new(3, 3), ECarrierType.Fuel).Amount > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatorRejectsAmbiguousJunctionSourceBranches()
|
||||
{
|
||||
var level = BuildJunctionLevel(2);
|
||||
level = level.SetProp(new(3, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
|
||||
var report = new LevelValidator().Validate(level);
|
||||
|
||||
Assert.False(report.IsValid);
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Ambiguous junction flow", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatorRejectsInvalidDoorGeometryAndWallHazards()
|
||||
{
|
||||
var level = LevelState.Create("Invalid", 6, 6);
|
||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Door });
|
||||
level = level.SetTerrain(new(4, 4), ECellTerrain.Wall);
|
||||
level = level with { Surface = level.Surface.ToArray() };
|
||||
level.Surface[level.Index(new(4, 4))] = new() { Heat = 1 };
|
||||
|
||||
var report = new LevelValidator().Validate(level);
|
||||
|
||||
Assert.False(report.IsValid);
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Door must be surrounded", StringComparison.Ordinal));
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Wall cell", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LevelSerializationRoundTripsCurrentSchemaOnly()
|
||||
{
|
||||
var level = BuildReadyLevel();
|
||||
|
||||
var json = LevelSerializer.Serialize(level);
|
||||
var loaded = LevelSerializer.Deserialize(json);
|
||||
|
||||
Assert.Contains("\"Version\": 3", json);
|
||||
Assert.Equal(level.Name, loaded.Name);
|
||||
Assert.Equal(EPropType.Consumer, loaded.GetProp(new(3, 3)).Type);
|
||||
Assert.Equal(level.RequiredFuelConsumers, loaded.RequiredFuelConsumers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LevelSerializationRoundTripsPropDoorsAndElectricityLeakFaces()
|
||||
{
|
||||
var level = BuildReadyLevel();
|
||||
level = level.SetTerrain(new(6, 4), ECellTerrain.Wall);
|
||||
level = level.SetUnderground(new(6, 4), ECarrierType.Electricity, new() { State = EUndergroundState.Leaking }) with {
|
||||
Leaks = [new() { Carrier = ECarrierType.Electricity, UndergroundPosition = new(6, 4), AccessPosition = new(6, 3) }]
|
||||
};
|
||||
level = DoorLevel(level);
|
||||
|
||||
var loaded = LevelSerializer.Deserialize(LevelSerializer.Serialize(level));
|
||||
|
||||
Assert.Equal(EPropType.Door, loaded.GetProp(new(3, 2)).Type);
|
||||
Assert.Single(loaded.Leaks);
|
||||
Assert.Equal(new(6, 3), loaded.Leaks[0].AccessPosition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LevelSerializationRejectsOldSchema()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"Version": 2,
|
||||
"Level": {}
|
||||
}
|
||||
""";
|
||||
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => LevelSerializer.Deserialize(json));
|
||||
|
||||
Assert.Contains("Unsupported level file version 2", exception.Message);
|
||||
}
|
||||
|
||||
private static LevelState BuildReadyLevel()
|
||||
{
|
||||
var level = LevelState.Create("Ready", 8, 7);
|
||||
level = AddLine(level, ECarrierType.Fuel, new(2, 3), new(3, 3));
|
||||
level = AddLine(level, ECarrierType.Coolant, new(2, 3), new(3, 3));
|
||||
level = AddLine(level, ECarrierType.Electricity, new(2, 3), new(3, 3));
|
||||
level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant });
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Coolant, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetUnderground(new(2, 3), ECarrierType.Coolant, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetProp(new(2, 4), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
|
||||
level = level.SetUnderground(new(2, 4), ECarrierType.Electricity, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetUnderground(new(2, 3), ECarrierType.Electricity, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer });
|
||||
level = level.SetProp(new(5, 3), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
|
||||
return level with {
|
||||
Robot = new() { Position = new(5, 3) },
|
||||
Reactors = [new() { ReactorId = 1, ControlPosition = new(5, 3) }]
|
||||
};
|
||||
}
|
||||
|
||||
private static LevelState DoorLevel(LevelState? seed = null)
|
||||
{
|
||||
var level = seed ?? LevelState.Create("Door", 6, 6);
|
||||
level = level.SetTerrain(new(3, 1), ECellTerrain.Wall);
|
||||
level = level.SetTerrain(new(3, 3), ECellTerrain.Wall);
|
||||
return level.SetProp(new(3, 2), new() { Type = EPropType.Door, DoorState = EDoorState.Closed });
|
||||
}
|
||||
|
||||
private static LevelState AddLine(LevelState level, ECarrierType carrier, GridPosition a, GridPosition b)
|
||||
{
|
||||
level = level.SetUnderground(a, carrier, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetUnderground(b, carrier, new() { State = EUndergroundState.Intact });
|
||||
return level;
|
||||
}
|
||||
|
||||
private static LevelState BuildJunctionLevel(int mode)
|
||||
{
|
||||
var level = LevelState.Create("Junction", 6, 6);
|
||||
level = level.SetUnderground(new(1, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetUnderground(new(2, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetUnderground(new(3, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetProp(new(1, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
return level.SetProp(new(2, 3), new() { Type = EPropType.Junction, Carrier = ECarrierType.Fuel, JunctionMode = mode });
|
||||
}
|
||||
|
||||
private readonly SimulationEngine m_Engine = new();
|
||||
}
|
||||
Reference in New Issue
Block a user