Compare commits
28 Commits
79f3219a72
...
672f055a80
| 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 |
@@ -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,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.
|
||||
486
docs/design.md
486
docs/design.md
@@ -1,47 +1,60 @@
|
||||
# Reactor Maintenance Design
|
||||
# Reactor Maintenance Design
|
||||
|
||||
## Concept
|
||||
|
||||
The player controls a maintenance robot inside a failing reactor facility. The game is a deterministic, turn-based systems puzzle about reading a visible machine, forecasting failure, and choosing between local stabilization and longer-term network control.
|
||||
|
||||
The simulation uses a small formal core:
|
||||
The simulation core is built from:
|
||||
|
||||
- static floor and wall terrain
|
||||
- underground fuel, coolant, and electricity networks
|
||||
- surface props for controls, terminals, supplies, doors, and reactor activation
|
||||
- reachable leaks that project hazards onto floor cells
|
||||
- explicit reactor requirements bound to consumer props
|
||||
- deterministic rule events and forecasts
|
||||
- static floor and wall terrain,
|
||||
- underground fuel, coolant, and electricity networks,
|
||||
- surface props for controls, terminals, supplies, doors, and reactor activation,
|
||||
- consumers that consume whichever underground services exist under their cell,
|
||||
- reachable leaks that project hazards onto floor cells,
|
||||
- transport network structural integrity,
|
||||
- deterministic fixed simulation rules and forecasts.
|
||||
|
||||
The game should feel logical, tactical, readable, and systemic. It should avoid randomness, action pressure, and hidden information after the player earns the relevant diagnostic access.
|
||||
The game should feel logical, tactical, readable, and systemic. It should avoid randomness, action pressure, and hidden information once the player reaches an all-seeing-eye terminal.
|
||||
|
||||
## Turn Structure
|
||||
## Action Economy
|
||||
|
||||
Each turn has three phases:
|
||||
There is no per-turn action budget. Player choices are either quick or lengthy.
|
||||
|
||||
1. Player phase: the player spends the level action budget.
|
||||
2. Simulation phase: networks, consumers, leaks, hazards, doors, robot safety, and level state resolve.
|
||||
3. Event phase: rule events, remedy block durations, and heat immunity duration advance.
|
||||
Quick actions do not mutate the level state and do not advance the simulation:
|
||||
|
||||
The simulation advances once after the player spends or ends the turn action budget. Individual player actions do not tick the simulation.
|
||||
- `MoveRobot`: move the robot to an adjacent floor cell, reduce heat immunity movement steps if applicable, and reject movement into walls or out of bounds,
|
||||
- selection and inspection: change only UI selection state,
|
||||
- all-seeing-eye viewing: when the robot is at an all-seeing-eye terminal, allow the player to view every surface and underground layer.
|
||||
|
||||
Lengthy actions mutate level state and immediately advance one simulation step:
|
||||
|
||||
- `InteractProp`: toggle flow props, toggle consumers, cycle junction ratios, open or close doors, pick up remedy supplies, or activate all-seeing-eye viewing from a terminal,
|
||||
- `InteractLeak`: repair a reachable leak or apply a matching elemental remedy,
|
||||
- `ApplyHeatShield`: spend one heat shield and set heat immunity movement steps,
|
||||
- `ActivateReactor`: activate a ready reactor at the current reactor control prop,
|
||||
- `Wait`: advance one simulation step without applying another player mutation.
|
||||
|
||||
Invalid actions report refusal and do not mutate gameplay state.
|
||||
|
||||
## Goal And Failure
|
||||
|
||||
Each reactor starts offline. A reactor becomes ready when:
|
||||
|
||||
- its bound fuel consumer is enabled, supplied, and producing
|
||||
- its bound coolant consumer is enabled, supplied, and producing
|
||||
- its bound electricity consumer is enabled, supplied, and producing
|
||||
- reactor heat is below the terminal condition
|
||||
- every underground network present beneath the reactor control cell has positive amount and intensity,
|
||||
- the level has the required number of enabled, fed, producing fuel consumers,
|
||||
- the level has the required number of enabled, fed, producing coolant consumers,
|
||||
- the level has the required number of enabled, fed, producing electricity consumers,
|
||||
- reactor heat is below the terminal condition.
|
||||
|
||||
When a reactor is ready, the level shows `REACTOR READY`. The player wins by spending an action at that reactor control site.
|
||||
The required consumer counts are level properties. The reactor is not explicitly bound to any consumer positions.
|
||||
|
||||
When a reactor is ready, the level shows `REACTOR READY`. The player wins by activating the ready reactor at the reactor control site.
|
||||
|
||||
The level is lost when:
|
||||
|
||||
- reactor heat reaches the terminal threshold
|
||||
- the robot occupies an unsafe final hazard state without applicable protection
|
||||
- a rule event marks terminal failure
|
||||
- level-authored unrecoverable conditions are met
|
||||
- reactor heat reaches the terminal threshold,
|
||||
- the robot occupies an unsafe final hazard state without applicable protection,
|
||||
- fixed simulation rules mark terminal failure.
|
||||
|
||||
Consumer starvation blocks readiness but does not directly cause loss.
|
||||
|
||||
@@ -49,17 +62,17 @@ Consumer starvation blocks readiness but does not directly cause loss.
|
||||
|
||||
The player can always inspect:
|
||||
|
||||
- surface terrain
|
||||
- surface props and visible prop state
|
||||
- visible leaks and repair faces
|
||||
- visible surface hazards
|
||||
- door state
|
||||
- remedy inventory and supply props
|
||||
- consumer state: disabled, starved, supplied, producing, or unknown
|
||||
- level state
|
||||
- forecasted warnings the simulation can prove
|
||||
- surface terrain,
|
||||
- surface props and visible prop state,
|
||||
- visible leaks and repair faces,
|
||||
- visible surface hazards,
|
||||
- door state,
|
||||
- remedy inventory and supply props,
|
||||
- consumer state: disabled, starved, supplied, producing, or unknown,
|
||||
- level state,
|
||||
- forecasted warnings the simulation can prove.
|
||||
|
||||
Underground topology and numeric network values are hidden until all-seeing-eye terminal access is unlocked. With access, the player can inspect underground fuel, coolant, and electricity topology, network structural state, carrier amount, pressure or voltage, and source-to-consumer connectivity.
|
||||
Underground topology and numeric network values are available through all-seeing-eye viewing after the robot visits an all-seeing-eye terminal. There is no persistent lock or unlock state in the level data.
|
||||
|
||||
The editor always sees and authors every layer.
|
||||
|
||||
@@ -69,31 +82,41 @@ Safe, caution, and critical labels are display and forecast bands derived from b
|
||||
|
||||
Each map coordinate contains:
|
||||
|
||||
- one static surface terrain cell: `Floor` or `Wall`
|
||||
- zero or one underground fuel cell
|
||||
- zero or one underground coolant cell
|
||||
- zero or one underground electricity cell
|
||||
- zero or one surface prop
|
||||
- visible hazard amounts on floor cells
|
||||
- optionally the robot, only on a floor cell
|
||||
- one static surface terrain cell: `Floor` or `Wall`,
|
||||
- zero or one underground fuel cell,
|
||||
- zero or one underground coolant cell,
|
||||
- zero or one underground electricity cell,
|
||||
- zero or one surface prop,
|
||||
- visible hazard amounts on floor cells,
|
||||
- optionally the robot, only on a floor cell.
|
||||
|
||||
Terrain is authored and does not change during play. Wall cells are not walkable and do not store surface hazards.
|
||||
|
||||
Underground cells use one structural state:
|
||||
|
||||
- `Absent`
|
||||
- `Intact`
|
||||
- `Leaking`
|
||||
- `Absent`,
|
||||
- `Intact`,
|
||||
- `Leaking`.
|
||||
|
||||
Underground cells store carrier amount plus pressure for fuel/coolant or voltage for electricity. Same-carrier underground cells connect by inferred cardinal adjacency.
|
||||
Underground cells store carrier amount, pressure or voltage intensity, and structural integrity on a 0-10 scale. Max structural integrity supports the highest pressure. Non-max integrity under high pressure worsens proportionally to excess pressure. Low integrity with positive pressure creates a leak. Repairing a leak restores integrity to max.
|
||||
|
||||
Structural integrity is persistent authored and runtime state:
|
||||
|
||||
- absent cells have no meaningful structural integrity,
|
||||
- newly authored present cells start at max structural integrity,
|
||||
- network propagation updates amount and intensity but does not reset integrity,
|
||||
- structural integrity cannot exceed max and cannot fall below zero,
|
||||
- existing leaks keep their current integrity until repaired.
|
||||
|
||||
Same-carrier underground cells connect by inferred cardinal adjacency.
|
||||
|
||||
Surface floor cells store:
|
||||
|
||||
- leaked fuel
|
||||
- leaked coolant
|
||||
- leaked electricity
|
||||
- heat
|
||||
- active elemental remedy blocks
|
||||
- leaked fuel,
|
||||
- leaked coolant,
|
||||
- leaked electricity,
|
||||
- heat,
|
||||
- active elemental remedy blocks.
|
||||
|
||||
Simulation values use C# `float`. Runtime values are clamped and retain full float precision. UI shows visible values rounded to one decimal plus the safe/caution/critical band.
|
||||
|
||||
@@ -112,14 +135,13 @@ The derived level states are:
|
||||
|
||||
Surface prop categories:
|
||||
|
||||
- flow prop
|
||||
- consumer prop
|
||||
- T-junction prop
|
||||
- cross-junction prop
|
||||
- door prop
|
||||
- all-seeing-eye terminal prop
|
||||
- remedy supply prop
|
||||
- reactor control prop
|
||||
- flow prop,
|
||||
- consumer prop,
|
||||
- junction prop,
|
||||
- door prop,
|
||||
- all-seeing-eye terminal prop,
|
||||
- remedy supply prop,
|
||||
- reactor control prop.
|
||||
|
||||
Props exist on floor cells. Props do not directly participate in the surface hazard pair table.
|
||||
|
||||
@@ -131,65 +153,62 @@ During network flow, an enabled flow prop injects source carrier amount and pres
|
||||
|
||||
### Consumer Props
|
||||
|
||||
A consumer prop is bound to fuel, coolant, or electricity. It can be `Enabled` or `Disabled`.
|
||||
A consumer prop can be `Enabled` or `Disabled`.
|
||||
|
||||
An enabled consumer derives one service state after network propagation:
|
||||
An enabled consumer derives one service state per underground network present beneath it:
|
||||
|
||||
- `Supplied`: enough carrier and pressure or voltage reaches the bound underground cell.
|
||||
- `Supplied`: enough carrier and pressure or voltage reaches the underground cell.
|
||||
- `Starved`: supply predicates fail.
|
||||
- `Producing`: the consumer was supplied this simulation step and emits service.
|
||||
|
||||
A disabled consumer consumes nothing, produces nothing, and cannot satisfy reactor readiness.
|
||||
A disabled consumer consumes nothing, produces nothing, and cannot satisfy reactor readiness. A consumer on no underground layer is valid but produces no service and contributes no readiness requirement. A consumer on one underground layer consumes that service. A consumer on multiple underground layers consumes all present layers and can satisfy one requirement for each carrier that is producing.
|
||||
|
||||
The aggregate consumer prop still exposes a single switch state. Per-carrier service state is derived from the underground layers beneath the prop and is used by reactor readiness, forecasts, and inspection. If any consumed carrier is starved, the consumer contributes a warning for that carrier without blocking other carriers on the same prop from producing.
|
||||
|
||||
### Reactor Control Props
|
||||
|
||||
A reactor control prop is the activation site for one reactor. Each reactor stores required consumer bindings by grid position:
|
||||
A reactor control prop is the activation site for one reactor. Reactor readiness is derived from level-level consumer count requirements and the networks beneath the reactor control cell.
|
||||
|
||||
- fuel consumer position
|
||||
- coolant consumer position
|
||||
- electricity consumer position
|
||||
The reactor control prop itself is not bound to any individual consumer. It is considered a local consumer for any underground network present beneath its cell:
|
||||
|
||||
The level is invalid if any binding is missing, out of bounds, or points to the wrong prop type.
|
||||
- if no underground layer is present beneath the reactor, the local reactor feed requirement is satisfied,
|
||||
- if one or more underground layers are present, every present layer must have positive amount and positive intensity after network propagation,
|
||||
- a present but starved reactor-under-network blocks readiness but does not directly lose the level.
|
||||
|
||||
Level properties define `RequiredFuelConsumers`, `RequiredCoolantConsumers`, and `RequiredElectricityConsumers`. For each carrier, readiness requires at least that many enabled consumer props whose per-carrier service state is `Producing`.
|
||||
|
||||
### Junction Props
|
||||
|
||||
A junction prop must be on a floor cell whose coordinate has exactly one underground carrier. That carrier is the regulated network.
|
||||
|
||||
T-junction modes:
|
||||
|
||||
- `0/4`
|
||||
- `1/3`
|
||||
- `2/2`
|
||||
- `3/1`
|
||||
- `4/0`
|
||||
|
||||
Cross-junction modes:
|
||||
|
||||
- `0/3/3`
|
||||
- `3/0/3`
|
||||
- `3/3/0`
|
||||
- `2/2/2`
|
||||
|
||||
The engine infers incoming and outgoing branch directions from valid network topology and enabled source paths. Ambiguous junction flow is invalid. Ratio numbers are weights that divide carrier amount and pressure or voltage by balance-defined math. A zero-weight branch receives no intentional outflow.
|
||||
|
||||
The gameplay UI exposes ratio modes.
|
||||
The engine infers incoming and outgoing branch directions from valid network topology and enabled source paths. A valid junction has one incoming branch and either two or three outgoing branches. Ambiguous junction flow is invalid. Ratio numbers are balance-defined weights that divide carrier amount and pressure or voltage. A zero-weight branch receives no intentional outflow.
|
||||
|
||||
### Doors
|
||||
|
||||
A door stores one edge between two adjacent floor cells. Door states are `Open` and `Closed`.
|
||||
A door is a prop on one floor cell. Its orientation is derived from opposing wall cells:
|
||||
|
||||
Closed doors block fuel, coolant, electricity, and heat propagation across their edge. They do not block robot movement, underground network flow, source feeding, consumer supply, or hazards already present on either side.
|
||||
- north and south walls mean the door sits in an east-west corridor and blocks west/east propagation while closed,
|
||||
- west and east walls mean the door sits in a north-south corridor and blocks north/south propagation while closed.
|
||||
|
||||
A door must have exactly one valid opposing wall pair. Closed doors block fuel, coolant, electricity, and heat propagation across the corridor cell. They do not block robot movement, underground network flow, source feeding, consumer supply, or hazards already present on either side.
|
||||
|
||||
Door blocking is evaluated by the door cell and its inferred corridor axis:
|
||||
|
||||
- east-west corridor doors block surface interaction between the west neighbor, the door cell, and the east neighbor while closed,
|
||||
- north-south corridor doors block surface interaction between the north neighbor, the door cell, and the south neighbor while closed,
|
||||
- open doors do not block surface interaction,
|
||||
- door props on invalid terrain or with ambiguous opposing walls are validation errors.
|
||||
|
||||
### Terminals And Supplies
|
||||
|
||||
An all-seeing-eye terminal unlocks underground inspection for the level.
|
||||
An all-seeing-eye terminal allows full underground inspection when visited.
|
||||
|
||||
Remedy supply props are single-use pickups:
|
||||
|
||||
- `FuelRemedySupply`
|
||||
- `CoolantRemedySupply`
|
||||
- `ElectricityRemedySupply`
|
||||
- `HeatRemedySupply`
|
||||
- `FuelRemedySupply`,
|
||||
- `CoolantRemedySupply`,
|
||||
- `ElectricityRemedySupply`,
|
||||
- `HeatRemedySupply`.
|
||||
|
||||
Each supply provides one matching inventory item and then becomes depleted.
|
||||
|
||||
@@ -199,45 +218,30 @@ Each leak stores carrier type, underground coordinate, accessible floor coordina
|
||||
|
||||
Fuel and coolant leaks:
|
||||
|
||||
- occur under floor cells
|
||||
- use the same coordinate as their accessible floor coordinate
|
||||
- can be repaired or remediated by the robot standing on that floor cell
|
||||
- occur under floor cells,
|
||||
- use the same coordinate as their accessible floor coordinate,
|
||||
- can be repaired or remediated by the robot standing on that floor cell.
|
||||
|
||||
Electricity leaks:
|
||||
|
||||
- occur in wall cells
|
||||
- store exactly one adjacent floor cell as the emission face
|
||||
- can be repaired or remediated from that floor cell
|
||||
- emit only to that stored face
|
||||
- occur in wall cells,
|
||||
- store exactly one adjacent floor cell as the emission face,
|
||||
- can be repaired or remediated from that floor cell,
|
||||
- emit only to that stored face.
|
||||
|
||||
All leaks must have valid floor access. Repair changes the underground cell from `Leaking` to `Intact` and stops future injection. Repair does not clean existing surface hazards.
|
||||
All leaks must have valid floor access. Repair changes the underground cell from `Leaking` to `Intact`, restores structural integrity to max, and stops future injection. Repair does not clean existing surface hazards.
|
||||
|
||||
The robot carries remedial consumables with balance-defined inventory capacity:
|
||||
|
||||
- `FuelNeutralizer`
|
||||
- `CoolantNeutralizer`
|
||||
- `ElectricityNeutralizer`
|
||||
- `HeatShield`
|
||||
- `FuelNeutralizer`,
|
||||
- `CoolantNeutralizer`,
|
||||
- `ElectricityNeutralizer`,
|
||||
- `HeatShield`.
|
||||
|
||||
Element neutralizers remove the matching surface element from a target floor cell or reachable leak face, then apply a temporary same-element re-entry block. They do not remove other elements, reduce heat, or repair leaks.
|
||||
|
||||
Heat shield gives the robot heat immunity for a balance-defined number of movement steps. It does not remove heat, block heat propagation, or protect against fuel, coolant, or electricity hazards.
|
||||
|
||||
## Player Actions
|
||||
|
||||
Player actions:
|
||||
|
||||
- `MoveRobot`: move to an adjacent floor cell.
|
||||
- `InteractProp`: use the prop on the robot's current floor cell.
|
||||
- `InteractLeak`: repair a reachable leak or apply a matching elemental remedy.
|
||||
- `ApplyHeatShield`: activate a carried heat shield.
|
||||
- `ActivateReactor`: activate a ready reactor at the current reactor control prop.
|
||||
- `EndTurn`: forfeit remaining actions and run the simulation/event phases.
|
||||
|
||||
Each valid action costs one action unless balance or level data defines otherwise. Invalid actions report refusal and do not mutate gameplay state.
|
||||
|
||||
`InteractProp` covers flow toggles, consumer toggles, junction ratio changes, door toggles, terminal unlock, and remedy pickup.
|
||||
|
||||
## Network Flow
|
||||
|
||||
Network flow runs independently for fuel, coolant, and electricity.
|
||||
@@ -263,114 +267,127 @@ Leaking underground cells remain part of network propagation.
|
||||
|
||||
During leak injection:
|
||||
|
||||
- fuel leaks add leaked fuel to the accessible floor cell
|
||||
- coolant leaks add leaked coolant to the accessible floor cell
|
||||
- electricity leaks add leaked electricity to the stored floor emission face
|
||||
- active matching remedy blocks prevent matching element entry
|
||||
- fuel leaks add leaked fuel to the accessible floor cell,
|
||||
- coolant leaks add leaked coolant to the accessible floor cell,
|
||||
- electricity leaks add leaked electricity to the stored floor emission face,
|
||||
- active matching remedy blocks prevent matching element entry.
|
||||
|
||||
Injection magnitude is balance data and may depend on local carrier amount, pressure, or voltage.
|
||||
Injection magnitude is balance data and may depend on local carrier amount, pressure, or voltage. If the target floor cell has an active matching remedy block, that matching element is not injected into the cell for that step. Remedy blocks do not block other elements or heat.
|
||||
|
||||
After injection, the engine evaluates local interactions between leaked fuel, leaked coolant, leaked electricity, and heat on the same floor cell and across unblocked adjacent floor cells.
|
||||
|
||||
Surface interaction resolution is deterministic:
|
||||
|
||||
- same-cell interactions evaluate every unordered quantity pair on each floor cell,
|
||||
- adjacent interactions evaluate every unordered pair of adjacent floor cells once,
|
||||
- same-carrier leaked fuel, coolant, electricity, and heat equalize across adjacent floor cells using `Flow(amount)`,
|
||||
- wall cells never store surface hazards and do not participate,
|
||||
- closed doors and remedy blocks gate the interactions they explicitly block,
|
||||
- deltas accumulate during an interaction pass and are applied together before clamping.
|
||||
|
||||
## Hazard Bands And Pair Table
|
||||
|
||||
Balance thresholds project numeric values into safe, caution, and critical bands:
|
||||
|
||||
- `FuelSafe`, `FuelCaution`, `FuelCritical`
|
||||
- `CoolantSafe`, `CoolantCaution`, `CoolantCritical`
|
||||
- `ElectricitySafe`, `ElectricityCaution`, `ElectricityCritical`
|
||||
- `HeatSafe`, `HeatCaution`, `HeatCritical`
|
||||
- `FuelSafe`, `FuelCaution`, `FuelCritical`,
|
||||
- `CoolantSafe`, `CoolantCaution`, `CoolantCritical`,
|
||||
- `ElectricitySafe`, `ElectricityCaution`, `ElectricityCritical`,
|
||||
- `HeatSafe`, `HeatCaution`, `HeatCritical`.
|
||||
|
||||
The pair table maps projected bands to deterministic effects:
|
||||
The pair table maps projected bands to parameterized verbs:
|
||||
|
||||
- `Hold`: no direct change.
|
||||
- `FuelFlow`: equalize leaked fuel.
|
||||
- `CoolFlow`: equalize leaked coolant.
|
||||
- `ChargeFlow`: equalize leaked electricity.
|
||||
- `HeatFlow`: transfer heat.
|
||||
- `HeatFlow2`: stronger heat transfer.
|
||||
- `Warm1`: small heat increase from fuel exposure.
|
||||
- `Warm2`: larger heat increase from fuel exposure.
|
||||
- `Quench1`: small heat reduction from coolant exposure.
|
||||
- `Quench2`: larger heat reduction from coolant exposure.
|
||||
- `Short1`: electricity and coolant interaction causing heat and discharge.
|
||||
- `Short2`: stronger short.
|
||||
- `Ignite1`: fuel interaction with electricity or high heat.
|
||||
- `Ignite2`: stronger ignition.
|
||||
- `Hold`: no direct change,
|
||||
- `Flow(amount)`: equalize a surface quantity by a balance-defined transfer amount,
|
||||
- `Warm(amount)`: increase heat by a balance-defined amount,
|
||||
- `Quench(amount)`: reduce heat by a balance-defined amount,
|
||||
- `Short(heat, discharge)`: add heat and discharge electricity by balance-defined amounts,
|
||||
- `Ignite(heat, fuel)`: add heat and consume fuel by balance-defined amounts.
|
||||
|
||||
| Row\Col | FuelSafe | FuelCaution | FuelCritical | CoolantSafe | CoolantCaution | CoolantCritical | ElectricitySafe | ElectricityCaution | ElectricityCritical | HeatSafe | HeatCaution | HeatCritical |
|
||||
| ------- | -------- | ----------- | ------------ | ----------- | -------------- | --------------- | --------------- | ------------------ | ------------------- | -------- | ----------- | ------------ |
|
||||
| FuelSafe | Hold | FuelFlow | FuelFlow | Hold | Hold | Hold | Hold | Warm1 | Warm2 | Hold | Warm1 | Warm2 |
|
||||
| FuelCaution | | Hold | FuelFlow | Hold | Hold | Hold | Warm1 | Ignite1 | Ignite2 | Warm1 | Ignite1 | Ignite2 |
|
||||
| FuelCritical | | | Hold | Hold | Hold | Hold | Warm2 | Ignite2 | Ignite2 | Warm2 | Ignite2 | Ignite2 |
|
||||
| CoolantSafe | | | | Hold | CoolFlow | CoolFlow | Hold | Short1 | Short2 | Hold | Quench1 | Quench1 |
|
||||
| CoolantCaution | | | | | Hold | CoolFlow | Short1 | Short1 | Short2 | Hold | Quench1 | Quench2 |
|
||||
| CoolantCritical | | | | | | Hold | Short2 | Short2 | Short2 | Hold | Quench2 | Quench2 |
|
||||
| ElectricitySafe | | | | | | | Hold | ChargeFlow | ChargeFlow | Hold | Hold | Hold |
|
||||
| ElectricityCaution | | | | | | | | Hold | ChargeFlow | Hold | Hold | Hold |
|
||||
| FuelSafe | Hold | Flow | Flow | Hold | Hold | Hold | Hold | Warm | Ignite | Hold | Warm | Ignite |
|
||||
| FuelCaution | | Hold | Flow | Hold | Hold | Hold | Warm | Ignite | Ignite | Warm | Ignite | Ignite |
|
||||
| FuelCritical | | | Hold | Hold | Hold | Hold | Ignite | Ignite | Ignite | Ignite | Ignite | Ignite |
|
||||
| CoolantSafe | | | | Hold | Flow | Flow | Hold | Short | Short | Hold | Quench | Quench |
|
||||
| CoolantCaution | | | | | Hold | Flow | Short | Short | Short | Hold | Quench | Quench |
|
||||
| CoolantCritical | | | | | | Hold | Short | Short | Short | Hold | Quench | Quench |
|
||||
| ElectricitySafe | | | | | | | Hold | Flow | Flow | Hold | Hold | Hold |
|
||||
| ElectricityCaution | | | | | | | | Hold | Flow | Hold | Hold | Hold |
|
||||
| ElectricityCritical | | | | | | | | | Hold | Hold | Hold | Hold |
|
||||
| HeatSafe | | | | | | | | | | Hold | HeatFlow | HeatFlow2 |
|
||||
| HeatCaution | | | | | | | | | | | Hold | HeatFlow |
|
||||
| HeatSafe | | | | | | | | | | Hold | Flow | Flow |
|
||||
| HeatCaution | | | | | | | | | | | Hold | Flow |
|
||||
| HeatCritical | | | | | | | | | | | | Hold |
|
||||
|
||||
Blank lower-triangle entries mirror the corresponding upper-triangle entry. Fuel and coolant do not directly react with each other; leaked coolant plus leaked fuel is `Hold` unless another pair on that cell or an adjacent cell involves heat, electricity, or same-carrier flow.
|
||||
|
||||
Design rules:
|
||||
|
||||
- fuel becomes dangerous through electricity or heat
|
||||
- coolant becomes dangerous through electricity
|
||||
- coolant opposes heat
|
||||
- heat equalizes between neighboring floor cells
|
||||
- same-carrier leaked surface amounts equalize between neighboring floor cells
|
||||
- doors and remedy blocks gate local interactions
|
||||
- fuel becomes dangerous through electricity or heat,
|
||||
- coolant becomes dangerous through electricity,
|
||||
- coolant opposes heat,
|
||||
- heat equalizes between neighboring floor cells,
|
||||
- same-carrier leaked surface amounts equalize between neighboring floor cells,
|
||||
- doors and remedy blocks gate local interactions.
|
||||
|
||||
## Rule Events
|
||||
## Structural Integrity
|
||||
|
||||
Rule events are deterministic and forecastable. Each event stores enabled state, once/repeat behavior, priority, predicates, effects, and optional forecast text. All predicates must pass for the event to trigger.
|
||||
Structural integrity is resolved after network propagation and before leak injection. It is deterministic and uses balancing values:
|
||||
|
||||
Predicate families:
|
||||
- `MaxStructuralIntegrity`: the maximum integrity value, `10` for the approved scale,
|
||||
- `StructuralIntegrityHighIntensityThreshold`: the pressure or voltage threshold above which a weakened cell degrades,
|
||||
- `StructuralIntegrityDamageScale`: difficulty-specific multiplier, initially `0.25` in Normal difficulty,
|
||||
- `StructuralIntegrityLeakThreshold`: integrity at or below this value can become a leak when positive intensity exists.
|
||||
|
||||
- turn comparisons
|
||||
- level state comparisons
|
||||
- reactor readiness/loss/win comparisons
|
||||
- prop type or state at position
|
||||
- consumer service state at position
|
||||
- network value band at position
|
||||
- surface hazard band at position
|
||||
- robot position or inventory condition
|
||||
- all-seeing-eye unlocked state
|
||||
For every present underground cell:
|
||||
|
||||
Effects:
|
||||
1. If the cell is already max integrity, high intensity does not weaken it during that step.
|
||||
2. If integrity is below max and intensity is greater than the high threshold, integrity is reduced by `(intensity - threshold) * StructuralIntegrityDamageScale`.
|
||||
3. Integrity is clamped to the 0-10 range.
|
||||
4. If the final integrity is at or below the leak threshold and intensity is positive, the cell becomes `Leaking` and a `LeakState` is created if one does not already exist.
|
||||
|
||||
- start a valid leak
|
||||
- worsen an existing leak
|
||||
- repair or disable a network cell
|
||||
- enable or disable a prop
|
||||
- add or remove surface hazard
|
||||
- add or remove heat
|
||||
- add or remove inventory
|
||||
- mark terminal loss
|
||||
- emit warning text
|
||||
Automatic leak access follows the same rules as authored leaks:
|
||||
|
||||
- fuel and coolant leaks use the underground cell as their floor access and can only auto-start under floor cells,
|
||||
- electricity leaks require one adjacent floor access face; if multiple valid faces exist, the deterministic order is north, east, south, west,
|
||||
- if no valid floor access exists, the weakened cell remains damaged but no reachable leak state is created.
|
||||
|
||||
Repairing a leak sets the underground cell to `Intact`, sets structural integrity to max, marks the leak repaired, and leaves existing surface hazards unchanged.
|
||||
|
||||
## Fixed Rule Systems
|
||||
|
||||
Data-driven rule predicates and effects are not part of level data. Effects happen through fixed systems:
|
||||
|
||||
- player-issued lengthy interactions toggle props, cycle junctions, use inventory, open or close doors, repair leaks, and activate reactors,
|
||||
- network propagation clears transient amount and intensity, then recomputes flow from enabled sources,
|
||||
- consumer resolution derives per-carrier service states from present underground layers,
|
||||
- structural integrity resolution weakens damaged high-pressure cells and creates leaks from low-integrity positive-pressure cells,
|
||||
- leak injection adds carrier hazards to valid floor access cells,
|
||||
- surface interaction resolution spreads and reacts hazards according to the hazard pair table,
|
||||
- robot safety resolves terminal loss from unsafe final hazard states after surface interactions,
|
||||
- reactor state derives readiness or terminal heat loss,
|
||||
- duration advancement reduces remedy blocks and heat immunity counters,
|
||||
- forecast refresh simulates copied state over the configured horizon.
|
||||
|
||||
Warnings are generated by fixed forecast and status systems when conditions can be proven.
|
||||
|
||||
## Simulation Order
|
||||
|
||||
One completed turn resolves in this order:
|
||||
One lengthy interaction resolves in this order:
|
||||
|
||||
1. Apply accepted player actions.
|
||||
1. Apply the accepted player mutation.
|
||||
2. Validate runtime state.
|
||||
3. Apply matching start-of-simulation rule events.
|
||||
4. Propagate underground networks.
|
||||
5. Resolve consumers and service production.
|
||||
3. Propagate underground networks.
|
||||
4. Resolve consumers and service production.
|
||||
5. Resolve structural integrity and automatic leak creation.
|
||||
6. Inject leaks.
|
||||
7. Evaluate same-cell surface interactions.
|
||||
8. Evaluate adjacent floor interactions across unblocked edges.
|
||||
8. Evaluate adjacent floor interactions across unblocked door cells.
|
||||
9. Accumulate and apply deltas in deterministic priority order.
|
||||
10. Clamp values.
|
||||
11. Resolve robot safety.
|
||||
12. Derive reactor readiness and level state.
|
||||
13. Apply matching end-of-turn rule events.
|
||||
14. Advance remedy blocks and heat immunity.
|
||||
15. Refresh forecasts.
|
||||
|
||||
If multiple events modify the same value in one step, deltas accumulate and then clamp. Event priority may consume, reserve, or block a value first.
|
||||
13. Advance remedy blocks and heat immunity.
|
||||
14. Refresh forecasts.
|
||||
|
||||
## Forecasts
|
||||
|
||||
@@ -378,11 +395,11 @@ Forecasts are deterministic simulations over copied state. Forecasting does not
|
||||
|
||||
Forecast output includes:
|
||||
|
||||
- terminal loss forecasts
|
||||
- reactor ready forecasts
|
||||
- starved required consumer warnings
|
||||
- growing hazard warnings when values cross caution or critical bands
|
||||
- rule event warnings when predicates can be proven within the forecast horizon
|
||||
- terminal loss forecasts,
|
||||
- reactor ready forecasts,
|
||||
- starved required consumer warnings,
|
||||
- growing hazard warnings when values cross caution or critical bands,
|
||||
- structural integrity leak warnings when weakened cells are expected to leak.
|
||||
|
||||
The forecast horizon is balance data.
|
||||
|
||||
@@ -392,51 +409,60 @@ The editor blocks run and save when validation errors exist. Warnings are visibl
|
||||
|
||||
Validation errors:
|
||||
|
||||
- invalid dimensions or cell counts
|
||||
- robot out of bounds or not on floor
|
||||
- wall cell with surface hazards
|
||||
- prop on invalid terrain
|
||||
- missing or invalid reactor consumer binding
|
||||
- invalid door edge
|
||||
- invalid leak access
|
||||
- invalid rule target
|
||||
- junction without exactly one underground carrier
|
||||
- ambiguous junction flow
|
||||
- network loop or equal-source ambiguity at a junction
|
||||
- malformed required data
|
||||
- invalid dimensions or cell counts,
|
||||
- robot out of bounds or not on floor,
|
||||
- wall cell with surface hazards,
|
||||
- prop on invalid terrain,
|
||||
- invalid required consumer counts,
|
||||
- invalid door cell,
|
||||
- invalid leak access,
|
||||
- junction without exactly one underground carrier,
|
||||
- ambiguous junction flow,
|
||||
- network loop or equal-source ambiguity at a junction,
|
||||
- malformed required data.
|
||||
|
||||
Validation warnings:
|
||||
|
||||
- unreachable non-required consumer
|
||||
- underground cell with no source path
|
||||
- initially starved required consumer
|
||||
- initially unready reactor
|
||||
- unused remedy supply
|
||||
- visible hazard with no detectable nearby remedy or route
|
||||
- unreachable non-required consumer,
|
||||
- underground cell with no source path,
|
||||
- initially starved required consumer,
|
||||
- initially unready reactor,
|
||||
- unused remedy supply,
|
||||
- visible hazard with no detectable nearby remedy or route.
|
||||
|
||||
## Editor And Schema
|
||||
|
||||
The editor authors:
|
||||
|
||||
- surface terrain
|
||||
- underground fuel, coolant, and electricity cells
|
||||
- flow props and consumer props
|
||||
- reactor controls and reactor consumer bindings
|
||||
- junction props and ratio modes
|
||||
- door props and door edges
|
||||
- all-seeing-eye terminals
|
||||
- remedy supplies
|
||||
- floor leaks and electricity wall leaks
|
||||
- initial surface hazards and heat
|
||||
- robot start position
|
||||
- rule events
|
||||
- surface terrain,
|
||||
- underground fuel, coolant, and electricity cells,
|
||||
- flow props,
|
||||
- multi-service consumer props,
|
||||
- required fuel, coolant, and electricity consumer counts,
|
||||
- junction props and balance-defined ratio mode index,
|
||||
- door props,
|
||||
- all-seeing-eye terminals,
|
||||
- remedy supplies,
|
||||
- floor leaks and electricity wall leaks with authored access faces,
|
||||
- initial surface hazards and heat,
|
||||
- robot start position.
|
||||
|
||||
The serialized level schema stores level metadata, dimensions, terrain, underground layers, props and prop state, reactor bindings, leaks, doors, robot state, inventory, rule events, all-seeing-eye state, and dynamic state when saving active play.
|
||||
The editor includes layer selection for Surface, Electricity, Fuel, and Coolant:
|
||||
|
||||
- Surface active: surface is full opacity, all underground layers are 25% opacity.
|
||||
- Underground active: surface is 50% opacity, inactive underground layers are 25% opacity, active underground layer is full opacity.
|
||||
- Coolant renders blue, fuel red, electricity yellow.
|
||||
- Networks render as thick lines connecting adjacent cell centers; sources render as large centered dots.
|
||||
- Tools are layer-aware. Cursor is always available. Surface terrain, props, consumers, hazards, doors, and heat tools are available only on Surface. Network painting and sources are available only on their matching underground layer.
|
||||
|
||||
Editor tool badges and drag previews use stable semantic image keys when assets are available. Assets may be added under `Images/Badges` or `Images/Elements` with filenames such as `tool-door.png`, `prop-reactor.png`, `carrier-fuel-source.png`, `leak-electricity.png`, or `robot.png`; missing assets fall back to compact procedural badges and text labels.
|
||||
|
||||
The serialized level schema stores level metadata, dimensions, terrain, underground layers including structural integrity, props and prop state, required reactor consumer counts, leaks, robot state, inventory, forecasts, and dynamic state when saving active play.
|
||||
|
||||
The loader accepts only schema-valid level data and returns clear errors for malformed data.
|
||||
|
||||
## Balancing And Tests
|
||||
|
||||
Balancing defines source strengths, falloff, ratio math, consumer predicates, leak magnitudes, interaction magnitudes, display thresholds, robot safety thresholds, terminal heat thresholds, inventory capacity, remedy duration, heat immunity duration, action costs, and forecast horizon.
|
||||
Balancing defines source strengths, falloff, ratio math, consumer predicates, leak magnitudes, structural integrity thresholds and damage scale, interaction magnitudes, display thresholds, robot safety thresholds, terminal heat thresholds, inventory capacity, remedy duration, heat immunity duration, and forecast horizon.
|
||||
|
||||
Tests assert behavior against configured balance values and bands. Coverage includes validation, inferred connectivity, junction effects, consumer states, reactor readiness and activation, terminal loss, robot hazard loss, heat immunity, leak access, remedies, door blocking, rule events, forecasts, and serialization round trips.
|
||||
Tests assert behavior against configured balance values and bands. Coverage includes validation, inferred connectivity, junction effects, multi-service consumer states, reactor readiness and activation, terminal loss, robot hazard loss, heat immunity, structural integrity degradation and leak creation, leak access, remedies, door blocking, forecasts, and serialization round trips.
|
||||
|
||||
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