44 Commits

Author SHA1 Message Date
5ddd1b8ec8 Cleanup 2026-05-11 21:51:18 +02:00
1b9372ff7c Restore dual terrain tilemap rendering 2026-05-10 23:04:48 +02:00
3a52db0071 Finish rewrite task list 2026-05-10 22:35:25 +02:00
5a186fb606 Split simulation systems 2026-05-10 18:49:24 +02:00
6c7fa070f6 cleanup code 2026-05-10 18:37:30 +02:00
d22c4a7528 Update rewrite docs and cleanup 2026-05-10 18:09:43 +02:00
7ffaa140a8 Introduce simulation engine facade 2026-05-10 18:08:03 +02:00
3c5fc60ffe Parameterize surface interactions 2026-05-10 18:07:16 +02:00
9cd9defc0b Unify junction props 2026-05-10 18:05:32 +02:00
1aa9734e08 Split simulation models 2026-05-10 18:03:46 +02:00
a0b10423ac Expand rule event coverage 2026-05-10 17:38:43 +02:00
cb28eee1dd Add branch-aware junction flow 2026-05-10 17:29:19 +02:00
b232c0319f Track rewrite tooling setup 2026-05-10 19:14:28 +02:00
30963a9bde Rework Win2D editor for design model 2026-05-10 18:59:00 +02:00
851f6d27e8 Rewrite simulation core for design model 2026-05-10 18:41:17 +02:00
ca41e009bd Add rewrite task tracker 2026-05-10 18:29:53 +02:00
79f3219a72 Condense reactor design spec 2026-05-10 14:33:09 +02:00
071e6a1d48 Finalize v1 design spec 2026-05-10 14:27:49 +02:00
810478ddee Refine leak and remediation design 2026-05-10 13:56:20 +02:00
bb8d1adb10 Finalize simulation design document 2026-05-10 13:28:33 +02:00
c8795d582c gitignore 2026-05-10 13:22:17 +02:00
2376edab0d Revise simulation design doc 2026-05-09 13:10:49 +02:00
c406bf9d73 Latest 2026-05-09 12:29:32 +02:00
4b581d60b5 Improve Win2D editor hover feedback 2026-05-09 03:01:26 +02:00
e90609bcee Improve Win2D editor UX 2026-05-09 02:41:05 +02:00
6e8766db3f Add generated Win2D sprites 2026-05-09 02:17:47 +02:00
fd5564e444 Fix tilemap asset loading 2026-05-09 00:48:57 +02:00
70adeb010f Render terrain from tilemap 2026-05-09 00:32:32 +02:00
1ca65eccf8 Render explicit dual terrain tiles 2026-05-08 22:30:18 +02:00
4827eefa9b Fix dual terrain arc placement 2026-05-08 22:22:55 +02:00
15fb522ac6 Rotate dual terrain arcs 2026-05-08 22:20:26 +02:00
5838214b36 Fix dual terrain corner rounding 2026-05-08 22:18:02 +02:00
5fb4265197 Round dual terrain tile corners 2026-05-08 22:10:57 +02:00
40038302de Refactor cells for dual tile rendering 2026-05-08 22:05:02 +02:00
9c7d661e8c Moved design to docs 2026-05-08 21:55:34 +02:00
c46b6664ed Organize simulation systems and balancing profiles 2026-05-08 21:45:43 +02:00
8018ebbabb Centralize simulation balancing values 2026-05-08 21:38:03 +02:00
8ec3c7847c Use primary constructor for simulation effects 2026-05-08 21:29:55 +02:00
1587395174 Cover configured simulation effects 2026-05-08 21:29:12 +02:00
637e9f7fbc Refactor simulation effects and forecast hazards 2026-05-08 21:26:19 +02:00
023d139281 docs updated 2026-05-08 21:12:49 +02:00
5a261f5fe2 Document code style and normalize files 2026-05-08 21:10:46 +02:00
2e813962c9 cleanup code 2026-05-08 20:47:09 +02:00
07d35a49a3 First batch 2026-05-08 20:46:17 +02:00
106 changed files with 5261 additions and 2 deletions

615
.editorconfig Normal file
View File

@@ -0,0 +1,615 @@
root = true
# Remove the line below if you want to inherit .editorconfig settings from higher directories
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = # 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
dotnet_style_qualification_for_property = false:suggestion
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:none
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:none
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
# Expression-level preferences
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Field preferences
dotnet_style_readonly_field = true
# Parameter preferences
dotnet_code_quality_unused_parameters = all
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = true:silent
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = true:silent
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_prefer_pattern_matching = true:silent
csharp_style_prefer_switch_expression = true:silent
# Null-checking preferences
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_prefer_static_local_function = true:suggestion
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion
# Code-block preferences
csharp_prefer_braces = when_multiline:silent
csharp_prefer_simple_using_statement = true:suggestion
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
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_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = false
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = warning
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.struct_should_be_begin_with_s.severity = warning
dotnet_naming_rule.struct_should_be_begin_with_s.symbols = struct
dotnet_naming_rule.struct_should_be_begin_with_s.style = begin_with_s
dotnet_naming_rule.property_should_be_pascal_case.severity = warning
dotnet_naming_rule.property_should_be_pascal_case.symbols = property
dotnet_naming_rule.property_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.constant_field_should_be_begins_with_c_.severity = warning
dotnet_naming_rule.constant_field_should_be_begins_with_c_.symbols = constant_field
dotnet_naming_rule.constant_field_should_be_begins_with_c_.style = begins_with_c_
dotnet_naming_rule.static_field_should_be_starts_with_s_.severity = warning
dotnet_naming_rule.static_field_should_be_starts_with_s_.symbols = static_field
dotnet_naming_rule.static_field_should_be_starts_with_s_.style = starts_with_s_
dotnet_naming_rule.private_or_internal_static_field_should_be_starts_with_s_.severity = warning
dotnet_naming_rule.private_or_internal_static_field_should_be_starts_with_s_.symbols = private_or_internal_static_field
dotnet_naming_rule.private_or_internal_static_field_should_be_starts_with_s_.style = starts_with_s_
dotnet_naming_rule.public_or_protected_field_should_be_starts_with_m_.severity = warning
dotnet_naming_rule.public_or_protected_field_should_be_starts_with_m_.symbols = public_or_protected_field
dotnet_naming_rule.public_or_protected_field_should_be_starts_with_m_.style = starts_with_m_
dotnet_naming_rule.private_or_internal_field_should_be_starts_with_m_.severity = warning
dotnet_naming_rule.private_or_internal_field_should_be_starts_with_m_.symbols = private_or_internal_field
dotnet_naming_rule.private_or_internal_field_should_be_starts_with_m_.style = starts_with_m_
dotnet_naming_rule.event_should_be_end_with_event.severity = warning
dotnet_naming_rule.event_should_be_end_with_event.symbols = event
dotnet_naming_rule.event_should_be_end_with_event.style = end_with_event
dotnet_naming_rule.delegate_should_be_end_with_delegate.severity = warning
dotnet_naming_rule.delegate_should_be_end_with_delegate.symbols = delegate
dotnet_naming_rule.delegate_should_be_end_with_delegate.style = end_with_delegate
dotnet_naming_rule.parameter_should_be_camelcase.severity = warning
dotnet_naming_rule.parameter_should_be_camelcase.symbols = parameter
dotnet_naming_rule.parameter_should_be_camelcase.style = camelcase
dotnet_naming_rule.local_should_be_camelcase.severity = warning
dotnet_naming_rule.local_should_be_camelcase.symbols = local
dotnet_naming_rule.local_should_be_camelcase.style = camelcase
# Symbol specifications
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.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.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.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.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.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.static_field.applicable_kinds = field
dotnet_naming_symbols.static_field.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
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_static_field.applicable_kinds = field
dotnet_naming_symbols.private_or_internal_static_field.applicable_accessibilities = internal, private, private_protected
dotnet_naming_symbols.private_or_internal_static_field.required_modifiers = static
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.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.parameter.applicable_kinds = parameter
dotnet_naming_symbols.parameter.applicable_accessibilities = *
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.constant_field.applicable_kinds = field
dotnet_naming_symbols.constant_field.applicable_accessibilities = *
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.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.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.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_.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_.capitalization = pascal_case
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.capitalization = pascal_case
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.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.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_.capitalization = pascal_case
# ReSharper properties
resharper_align_linq_query = true
resharper_align_multiline_argument = true
resharper_align_multiline_calls_chain = true
resharper_align_multiline_extends_list = true
resharper_align_multiline_for_stmt = true
resharper_align_multline_type_parameter_constrains = true
resharper_align_multline_type_parameter_list = true
resharper_align_tuple_components = true
resharper_allow_far_alignment = false
resharper_apply_auto_detected_rules = false
resharper_blank_lines_after_control_transfer_statements = 1
resharper_blank_lines_after_file_scoped_namespace_directive = 1
resharper_blank_lines_around_single_line_type = 0
resharper_blank_lines_before_multiline_statements = 0
resharper_braces_for_fixed = required
resharper_braces_for_lock = required
resharper_braces_for_using = required
resharper_csharp_align_multiline_parameter = true
resharper_csharp_align_multiple_declaration = true
resharper_csharp_allow_far_alignment = true
resharper_csharp_blank_lines_around_field = 0
resharper_csharp_extra_spaces = remove_all
resharper_csharp_insert_final_newline = false
resharper_csharp_keep_blank_lines_in_code = 1
resharper_csharp_keep_blank_lines_in_declarations = 1
resharper_csharp_max_line_length = 500
resharper_csharp_naming_rule.constants = c_ + AaBb
resharper_csharp_naming_rule.event = AaBb
resharper_csharp_naming_rule.interfaces = I + AaBb
resharper_csharp_naming_rule.local_constants = aaBb
resharper_csharp_naming_rule.method = AaBb
resharper_csharp_naming_rule.private_constants = c_ + AaBb
resharper_csharp_naming_rule.private_instance_fields = m_ + AaBb
resharper_csharp_naming_rule.private_static_fields = s_ + AaBb
resharper_csharp_naming_rule.private_static_readonly = s_ + AaBb
resharper_csharp_naming_rule.property = AaBb
resharper_csharp_naming_rule.public_fields = m_ + AaBb
resharper_csharp_naming_rule.static_readonly = s_ + AaBb
resharper_csharp_naming_rule.types_and_namespaces = AaBb
resharper_csharp_outdent_commas = true
resharper_csharp_outdent_dots = false
resharper_csharp_stick_comment = false
resharper_csharp_use_indent_from_vs = true
resharper_csharp_wrap_extends_list_style = chop_if_long
resharper_csharp_wrap_lines = false
resharper_csharp_wrap_parameters_style = chop_if_long
resharper_csharp_wrap_ternary_expr_style = wrap_if_long
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_int_align_switch_expressions = false
resharper_int_align_switch_sections = false
resharper_keep_existing_arrangement = true
resharper_keep_existing_attribute_arrangement = true
resharper_keep_existing_switch_expression_arrangement = true
resharper_max_attribute_length_for_same_line = 0
resharper_max_initializer_elements_on_line = 1
resharper_nested_ternary_style = compact
resharper_object_creation_when_type_not_evident = target_typed
resharper_outdent_binary_ops = true
resharper_place_constructor_initializer_on_same_line = false
resharper_place_method_attribute_on_same_line = if_owner_is_single_line
resharper_place_simple_case_statement_on_same_line = true
resharper_place_simple_embedded_statement_on_same_line = false
resharper_place_simple_initializer_on_single_line = true
resharper_place_simple_property_pattern_on_single_line = true
resharper_place_type_attribute_on_same_line = if_owner_is_single_line
resharper_use_heuristics_for_body_style = true
resharper_use_indent_from_vs = true
resharper_space_before_new_parentheses = false
resharper_use_roslyn_logic_for_evident_types = true
# IDE0090: Use 'new(...)'
dotnet_diagnostic.ide0090.severity = silent
dotnet_diagnostic.ide0110.severity = suggestion
dotnet_diagnostic.ide0058.severity = suggestion
dotnet_diagnostic.ide0059.severity = suggestion
csharp_style_namespace_declarations = file_scoped:none
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
csharp_style_prefer_extended_property_pattern = true:suggestion
# Microsoft .NET properties
dotnet_naming_rule.constants_rule.import_to_resharper = as_predefined
dotnet_naming_rule.constants_rule.severity = none
dotnet_naming_rule.constants_rule.style = begins_with_c_
dotnet_naming_rule.constants_rule.symbols = constants_symbols
dotnet_naming_rule.event_rule.import_to_resharper = as_predefined
dotnet_naming_rule.event_rule.severity = none
dotnet_naming_rule.event_rule.style = pascal_case
dotnet_naming_rule.event_rule.symbols = event_symbols
dotnet_naming_rule.interfaces_rule.import_to_resharper = as_predefined
dotnet_naming_rule.interfaces_rule.severity = none
dotnet_naming_rule.interfaces_rule.style = begins_with_i
dotnet_naming_rule.interfaces_rule.symbols = interfaces_symbols
dotnet_naming_rule.local_constants_rule.import_to_resharper = as_predefined
dotnet_naming_rule.local_constants_rule.severity = none
dotnet_naming_rule.local_constants_rule.style = camelcase
dotnet_naming_rule.local_constants_rule.symbols = local_constants_symbols
dotnet_naming_rule.local_should_be_camelcase.import_to_resharper = as_predefined
dotnet_naming_rule.method_rule.import_to_resharper = as_predefined
dotnet_naming_rule.method_rule.severity = none
dotnet_naming_rule.method_rule.style = pascal_case
dotnet_naming_rule.method_rule.symbols = method_symbols
dotnet_naming_rule.non_field_members_should_be_pascal_case.import_to_resharper = True
dotnet_naming_rule.non_field_members_should_be_pascal_case.resharper_description = non_field_members_should_be_pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.resharper_guid = 44ab0b2d-34cc-42db-a691-0c646821e2e4
dotnet_naming_rule.parameter_should_be_camelcase.import_to_resharper = as_predefined
dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined
dotnet_naming_rule.private_constants_rule.severity = none
dotnet_naming_rule.private_constants_rule.style = begins_with_c_
dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = as_predefined
dotnet_naming_rule.private_instance_fields_rule.severity = none
dotnet_naming_rule.private_instance_fields_rule.style = starts_with_m_
dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols
dotnet_naming_rule.private_static_fields_rule.import_to_resharper = as_predefined
dotnet_naming_rule.private_static_fields_rule.severity = none
dotnet_naming_rule.private_static_fields_rule.style = starts_with_s_
dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = as_predefined
dotnet_naming_rule.private_static_readonly_rule.severity = none
dotnet_naming_rule.private_static_readonly_rule.style = starts_with_s_
dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
dotnet_naming_rule.property_rule.import_to_resharper = as_predefined
dotnet_naming_rule.property_rule.severity = none
dotnet_naming_rule.property_rule.style = pascal_case
dotnet_naming_rule.property_rule.symbols = property_symbols
dotnet_naming_rule.public_fields_rule.import_to_resharper = as_predefined
dotnet_naming_rule.public_fields_rule.severity = none
dotnet_naming_rule.public_fields_rule.style = starts_with_m_
dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols
dotnet_naming_rule.static_readonly_rule.import_to_resharper = as_predefined
dotnet_naming_rule.static_readonly_rule.severity = none
dotnet_naming_rule.static_readonly_rule.style = starts_with_s_
dotnet_naming_rule.static_readonly_rule.symbols = static_readonly_symbols
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_kinds = field
dotnet_naming_symbols.constants_symbols.required_modifiers = const
dotnet_naming_symbols.event_symbols.applicable_accessibilities = *
dotnet_naming_symbols.event_symbols.applicable_kinds = event
dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities = *
dotnet_naming_symbols.interfaces_symbols.applicable_kinds = interface
dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities = *
dotnet_naming_symbols.local_constants_symbols.applicable_kinds = local
dotnet_naming_symbols.local_constants_symbols.required_modifiers = const
dotnet_naming_symbols.method_symbols.applicable_accessibilities = *
dotnet_naming_symbols.method_symbols.applicable_kinds = method
dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private
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.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_kinds = field
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.types_and_namespaces_symbols.applicable_accessibilities = *
dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds = namespace, class, struct, enum, delegate
# ReSharper inspection severities
resharper_arrange_accessor_owner_body_highlighting = suggestion
resharper_arrange_default_value_when_type_evident_highlighting = hint
resharper_arrange_object_creation_when_type_evident_highlighting = hint
resharper_arrange_redundant_parentheses_highlighting = hint
resharper_arrange_this_qualifier_highlighting = hint
resharper_arrange_type_member_modifiers_highlighting = hint
resharper_arrange_type_modifiers_highlighting = hint
resharper_built_in_type_reference_style_for_member_access_highlighting = hint
resharper_built_in_type_reference_style_highlighting = hint
resharper_comment_typo_highlighting = hint
resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting = none
resharper_identifier_typo_highlighting = hint
resharper_inconsistent_naming_highlighting = none
resharper_json_validation_failed_highlighting = warning
resharper_member_can_be_private_global_highlighting = hint
resharper_member_can_be_private_local_highlighting = hint
resharper_member_can_be_protected_global_highlighting = hint
resharper_member_can_be_protected_local_highlighting = hint
resharper_redundant_base_qualifier_highlighting = warning
resharper_remove_redundant_braces_highlighting = suggestion
resharper_string_literal_typo_highlighting = hint
resharper_suggest_var_or_type_built_in_types_highlighting = hint
resharper_suggest_var_or_type_elsewhere_highlighting = hint
resharper_suggest_var_or_type_simple_types_highlighting = hint
resharper_unused_auto_property_accessor_global_highlighting = hint
resharper_unused_auto_property_accessor_local_highlighting = hint
resharper_unused_member_global_highlighting = hint
csharp_style_prefer_method_group_conversion = true:suggestion
csharp_style_prefer_top_level_statements = true:silent
dotnet_diagnostic.VSTHRD001.severity = none
dotnet_diagnostic.VSTHRD105.severity = silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_prefer_system_threading_lock = true:suggestion
dotnet_diagnostic.VSTHRD012.severity = none
dotnet_diagnostic.VSTHRD003.severity = none
[*.{cs,vb}]
dotnet_diagnostic.ca1822.severity = silent
dotnet_diagnostic.ca1305.severity = suggestion
dotnet_diagnostic.IDE0079.severity = silent
dotnet_diagnostic.IDE0270.severity = silent
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_diagnostic.ca2215.severity = warning
dotnet_diagnostic.ca1001.severity = suggestion
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
end_of_line = crlf
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_style_readonly_field = true:suggestion
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
dotnet_style_allow_multiple_blank_lines_experimental = true:silent
dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
dotnet_code_quality_unused_parameters = all:suggestion
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}]
indent_style = space
indent_size = 4
tab_width = 4
[*.{appxmanifest,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cshtml,csproj,cu,cuh,cxx,dbml,discomap,dtd,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,jsproj,lsproj,mpp,mq4,mq5,mqh,njsproj,nuspec,proj,props,proto,razor,resw,resx,StyleCop,targets,tasks,tpp,usf,ush,vbproj,xml,xsd}]
indent_style = space
indent_size = 4
tab_width = 4
[*.{asax,ascx,aspx,axaml,cs,css,js,jsx,master,paml,skin,ts,tsx,vb,xaml,xamlx,xoml}]
indent_style = space
indent_size = 4
tab_width = 4
dotnet_diagnostic.CA1816.severity = silent
dotnet_diagnostic.HAA0501.severity = silent
dotnet_diagnostic.HAA0502.severity = silent
dotnet_style_prefer_collection_expression = true:suggestion
dotnet_diagnostic.RS2008.severity = none
[*.{json,resjson}]
indent_style = space
indent_size = 4
tab_width = 4

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.vs
bin
obj
.idea

3
AGENTS.linux.md Normal file
View File

@@ -0,0 +1,3 @@
# Linux-specific instructions
- After every iteration, run `dotnet jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.

30
AGENTS.md Normal file
View File

@@ -0,0 +1,30 @@
# Platform and documentation
If this is a linux environment, read `AGENTS.linux.md`.
If this is a windows environment, read `AGENTS.windows.md`.
Follow the guidelines laid out in `CODESTYLE.md`.
Also see the other related technical documentation in the docs folder.
## Rules
- Prefer extracting code to a shared helper to be reused instead of duplicating code. Always keep high maintainability standards.
- If a class is to be used only once, consider nesting it inside of another class. Otherwise place each newly created class into its own file. The file name must match the class name.
- When asked to begin working on a task, create a detailed implementation plan first, present the plan to the user, and ask for approval before beginning with the actual implementation.
- Don't make assumptions in the plan. If necessary, ask all clarifying questions before presenting the final plan.
- When an task is finished, perform a code review to evaluate if the change is clean and maintainable with high software engineering standards. Iterate on the code and repeat the review process until satisfied.
- If there's documnentation present, always keep it updated.
- After every iteration, evaluate if the test coverage would fall below 100%, and write tests if necessary.
### Git
- Never change the .gitignore file without consent.
- Keep changes small with minimal churn and commit often. If one iteration encompasses many smaller tasks with more than one commit, create a git branch and do the commits there. Let me review the branch before merging it back to master.
- When multiple commits are necessary, pause after every commit and ask the user to give a command to proceed.
- After every iteration, do a git commit with a brief summary of the changes as a commit message.
- If you find unexpected changes in the code (deletions, changes, diff results that were not communicated), never revert them and never restore the old state. Assume that those changes happened with intent.
- Never use `git restore`, `git checkout --`, reset commands, or equivalent rollback actions to discard local changes unless the user explicitly asks for that exact rollback.
### Dotnet CLI
- If you need a separate output directory, use a subfolder under `artifacts`, and clean it up afterwards.
- Avoid running `dotnet build` and `dotnet test` in parallel in this repo; that can cause file-lock failures in `obj\Debug\net10.0`.

4
AGENTS.windows.md Normal file
View File

@@ -0,0 +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 '$file1' '$file2' ...` for every C# file you touched.

51
CODESTYLE.md Normal file
View File

@@ -0,0 +1,51 @@
# Code Style
This repository follows the local `.editorconfig` and the style visible in the current local changes. Use these notes when creating new code.
## Naming
- Use PascalCase for namespaces, types, methods, properties, enum members, and non-field members.
- Prefix enum type names with `E`, for example `ECellKind`, `EPipeMedium`, `EFailureKind`, and `EEditorTool`.
- Prefix struct type names with `S` when creating new structs.
- Prefix interfaces with `I`.
- Use camelCase for parameters and local variables.
- Prefix private instance fields with `m_` and keep the remainder PascalCase, for example `m_Level` and `m_SelectedTool`.
- Prefix private static fields and static readonly fields with `s_`.
- Prefix constants with `c_`.
- Avoid `this.` unless it is needed for clarity or disambiguation.
- Always use folder-based namespaces when creating types and refactoring.
## Files And Types
- Use file-scoped namespaces.
- Keep one reusable top-level class per file, with the file name matching the class name.
- If a helper type is used only by one class, prefer nesting it inside that class.
- Keep small, cohesive files and extract shared helpers instead of duplicating logic.
## Braces And Blocks
- Use braces for multi-line bodies.
- If nesting a for-loop under another for-loop, always include curly braces in the parent for-loop.
- Omit braces for simple single-line embedded statements when readability stays clear.
- Nested control flow with multi-line bodies should use braces at every multi-line level.
- Keep opening braces on the next line for types, methods, properties, accessors, and control blocks.
- Compact object initializers, switch expressions, and `with` expressions may keep the opening brace on the same line when cleanup formats them that way.
## Blank Lines
- Use a blank line to separate members.
- Use a blank line after control-flow transfer clauses such as `return`, `continue`, `break`, and `throw` when more code follows in the same scope.
- Avoid extra blank lines inside short methods and between tightly related statements.
- Keep at most one blank line in code and declarations.
## Expressions And Formatting
- Prefer `var` when the type is apparent or not useful to repeat; use explicit built-in types such as `int`, `bool`, and `string`.
- Prefer target-typed `new()` when the type is evident.
- Prefer object and collection initializers, including collection expressions such as `[".json"]`.
- Prefer pattern matching for combined checks, for example `cell is { HasPipe: true, Pressure: > 7 }`.
- Prefer switch expressions for simple value selection.
- Prefer expression-bodied properties and accessors when they remain simple.
- Keep simple object initializers and property patterns on one line when they are short and readable.
- Keep long boolean expressions and interpolated status strings readable without introducing unnecessary blank lines.
- Keep using directives outside namespaces.

View File

@@ -1,2 +1,28 @@
# zfxaction26_2 # Reactor Maintenance
C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `docs/design.md`.
## Projects
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, validation, forecasts, simulation turns, versioned JSON serialization, and deterministic balancing defaults.
- `src/ReactorMaintenance.Win2D`: Win2D editor app for authoring terrain, underground fuel/coolant/electricity networks, props, explicit leak access faces, door edges, reactor consumer bindings, rule events, surface hazards, robot start, loading/saving levels, ending turns, interacting with props, and activating a ready reactor.
- `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior, validation, serialization, and editor operations.
## Editor Controls
- Left click selects or paints with the current tool. Right click clears the selected cell's prop, surface hazards, leaks, doors, and reactor control.
- Door authoring is explicit: select the Door tool, click the door cell, then click the adjacent floor cell that defines the blocked edge.
- Electricity wall leaks are explicit: select the Electricity Leak tool, click the wall network cell, then click the adjacent floor access face.
- Reactor bindings are explicit: select or place a reactor control, select a matching consumer cell, then use the Fuel, Coolant, or Electric binding action in the inspector.
- Rule event authoring is available from the inspector for next-turn warnings and selected-cell leak events; authored events are saved in the version 2 JSON schema.
## Commands
```powershell
dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj
dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64 -p:EnableWindowsTargeting=true
dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
```
The WinUI/XAML compiler is Windows-specific. On Linux, the simulation tests run normally, but the Win2D app build must be verified in a Windows-capable environment.

11
ReactorMaintenance.slnx Normal file
View File

@@ -0,0 +1,11 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/ReactorMaintenance.Simulation/ReactorMaintenance.Simulation.csproj"/>
<Project Path="src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj">
<Platform Project="x86"/>
</Project>
</Folder>
<Folder Name="/tests/">
<Project Path="tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj"/>
</Folder>
</Solution>

9
TASKS.md Normal file
View File

@@ -0,0 +1,9 @@
# Reactor Maintenance Rewrite Tasks
## Current State
## Completed Work
## Current Work
## Future Work

422
docs/design.md Normal file
View File

@@ -0,0 +1,422 @@
# 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:
- 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
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.
## Turn Structure
Each turn has three phases:
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.
The simulation advances once after the player spends or ends the turn action budget. Individual player actions do not tick the simulation.
## 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
When a reactor is ready, the level shows `REACTOR READY`. The player wins by spending an action at that 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
Consumer starvation blocks readiness but does not directly cause loss.
## Information
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
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.
The editor always sees and authors every layer.
Safe, caution, and critical labels are display and forecast bands derived from balance thresholds. Numeric simulation values remain authoritative.
## Grid And State
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
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`
Underground cells store carrier amount plus pressure for fuel/coolant or voltage for electricity. Same-carrier underground cells connect by inferred cardinal adjacency.
Surface floor cells store:
- 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.
## Level State
The derived level states are:
- `Stable`: no terminal path is near and required systems are not deteriorating.
- `Caution`: required service is missing, a consumer is starved or disabled, a hazard is growing, or reactor heat is concerning.
- `Critical`: forecast predicts loss without near-term intervention, or reactor heat is close to terminal.
- `Ready`: a reactor can be activated.
- `Lost`: terminal failure has occurred.
- `Won`: a reactor has been activated successfully.
## Props
Surface prop categories:
- 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.
### Flow Props
A flow prop is bound to fuel, coolant, or electricity. It can be `Enabled` or `Disabled`.
During network flow, an enabled flow prop injects source carrier amount and pressure or voltage into its connected underground network cell. A disabled flow prop injects nothing.
### Consumer Props
A consumer prop is bound to fuel, coolant, or electricity. It can be `Enabled` or `Disabled`.
An enabled consumer derives one service state after network propagation:
- `Supplied`: enough carrier and pressure or voltage reaches the bound 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.
### Reactor Control Props
A reactor control prop is the activation site for one reactor. Each reactor stores required consumer bindings by grid position:
- fuel consumer position
- coolant consumer position
- electricity consumer position
The level is invalid if any binding is missing, out of bounds, or points to the wrong prop type.
### Junction Props
A junction prop must be on a floor cell whose coordinate has exactly one underground carrier. That carrier is the regulated network.
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.
The gameplay UI exposes a single junction tool and cycles through balance-defined ratio presets for the inferred outgoing branch count.
Editor commands use a verb plus parameters, so carrier-specific choices such as fuel flow or coolant flow are UI presets over one `Flow` command instead of separate simulation concepts.
### Doors
A door stores one edge between two adjacent floor cells. Door states are `Open` and `Closed`.
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.
### Terminals And Supplies
An all-seeing-eye terminal unlocks underground inspection for the level.
Remedy supply props are single-use pickups:
- `FuelRemedySupply`
- `CoolantRemedySupply`
- `ElectricityRemedySupply`
- `HeatRemedySupply`
Each supply provides one matching inventory item and then becomes depleted.
## Leaks And Remedies
Each leak stores carrier type, underground coordinate, accessible floor coordinate, and repair state.
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
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
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.
The robot carries remedial consumables with balance-defined inventory capacity:
- `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.
For each carrier:
1. Clear transient carrier amount and pressure or voltage.
2. Start from every enabled flow prop connected to that carrier.
3. Walk through connected intact and leaking underground cells.
4. Stop at absent cells and disconnected topology.
5. Apply distance falloff.
6. Apply valid junction ratio weights.
7. Assign each reached cell its best incoming carrier amount and best incoming pressure or voltage.
8. Clamp final values.
Multiple non-ambiguous source paths may reach the same non-junction cell; the cell uses the best carrier amount and best pressure or voltage. Junction ambiguity is a validation error.
A consumer is supplied when carrier amount, pressure or voltage, and connectivity predicates pass.
## Surface Hazards
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
Injection magnitude is balance data and may depend on local carrier amount, pressure, or voltage.
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.
## 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`
The pair table maps projected bands to deterministic effects:
- `Hold`: no direct change.
The pair table maps projected bands to parameterized verbs:
- `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 | 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 | Flow | Flow |
| HeatCaution | | | | | | | | | | | Hold | Flow |
| HeatCritical | | | | | | | | | | | | Hold |
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
## Rule Events
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.
Predicate families:
- 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
Effects:
- 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
## Simulation Order
One completed turn resolves in this order:
1. Apply accepted player actions.
2. Validate runtime state.
3. Apply matching start-of-simulation rule events.
4. Propagate underground networks.
5. Resolve consumers and service production.
6. Inject leaks.
7. Evaluate same-cell surface interactions.
8. Evaluate adjacent floor interactions across unblocked edges.
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.
## Forecasts
Forecasts are deterministic simulations over copied state. Forecasting does not mutate the actual level.
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
The forecast horizon is balance data.
## Validation
The editor blocks run and save when validation errors exist. Warnings are visible and non-blocking.
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
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
## Editor And Schema
The editor authors:
- surface terrain
- underground fuel, coolant, and electricity cells
- flow props and consumer props
- reactor controls and explicit reactor consumer bindings
- junction props and balance-defined ratio mode index
- door props and explicit door edges
- all-seeing-eye terminals
- remedy supplies
- floor leaks and electricity wall leaks with authored access faces
- initial surface hazards and heat
- robot start position
- rule events
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 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.
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.

13
dotnet-tools.json Normal file
View File

@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2026.1.1",
"commands": [
"jb"
],
"rollForward": false
}
}
}

View File

@@ -0,0 +1,161 @@
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 };
}
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 ActionsPerTurn { get; }
public abstract int ForecastHorizon { get; }
public abstract float MinValue { get; }
public abstract float MaxValue { get; }
public abstract float FuelSafe { get; }
public abstract float FuelCaution { get; }
public abstract float FuelCritical { get; }
public abstract float CoolantSafe { get; }
public abstract float CoolantCaution { get; }
public abstract float CoolantCritical { get; }
public abstract float ElectricitySafe { get; }
public abstract float ElectricityCaution { get; }
public abstract float ElectricityCritical { get; }
public abstract float HeatSafe { get; }
public abstract float HeatCaution { get; }
public abstract float HeatCritical { get; }
public abstract float TerminalHeat { get; }
public abstract float RobotFuelSafetyThreshold { get; }
public abstract float RobotCoolantSafetyThreshold { get; }
public abstract float RobotElectricitySafetyThreshold { get; }
public abstract float RobotHeatSafetyThreshold { get; }
public abstract float SourceAmount { get; }
public abstract float SourceIntensity { get; }
public abstract float DistanceAmountFalloff { get; }
public abstract float DistanceIntensityFalloff { get; }
public abstract IReadOnlyList<JunctionRatioPreset> TwoOutflowJunctionRatios { get; }
public abstract IReadOnlyList<JunctionRatioPreset> ThreeOutflowJunctionRatios { get; }
public abstract float ConsumerRequiredAmount { get; }
public abstract float ConsumerRequiredIntensity { get; }
public abstract float LeakBaseAmount { get; }
public abstract float LeakAmountScale { get; }
public abstract float LeakIntensityScale { get; }
public abstract float FlowTransferRatio { get; }
public abstract float WarmCautionAmount { get; }
public abstract float WarmCriticalAmount { get; }
public abstract float QuenchCautionAmount { get; }
public abstract float QuenchCriticalAmount { get; }
public abstract float ShortCautionHeat { get; }
public abstract float ShortCautionDischarge { get; }
public abstract float ShortCriticalHeat { get; }
public abstract float ShortCriticalDischarge { get; }
public abstract float IgniteCautionHeat { get; }
public abstract float IgniteCautionFuelConsumption { get; }
public abstract float IgniteCriticalHeat { get; }
public abstract float IgniteCriticalFuelConsumption { get; }
public abstract int RemedyBlockTurns { get; }
public abstract int HeatShieldSteps { get; }
public abstract int InventoryCapacityPerRemedy { get; }
}

View File

@@ -0,0 +1,70 @@
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 ActionsPerTurn => 3;
public override int ForecastHorizon => 6;
public override float MinValue => 0;
public override float MaxValue => 10;
public override float FuelSafe => 1.5f;
public override float FuelCaution => 3.5f;
public override float FuelCritical => 6.5f;
public override float CoolantSafe => 1.5f;
public override float CoolantCaution => 3.5f;
public override float CoolantCritical => 6.5f;
public override float ElectricitySafe => 1.5f;
public override float ElectricityCaution => 3.5f;
public override float ElectricityCritical => 6.5f;
public override float HeatSafe => 2;
public override float HeatCaution => 5;
public override float HeatCritical => 8;
public override float TerminalHeat => 10;
public override float RobotFuelSafetyThreshold => 6.5f;
public override float RobotCoolantSafetyThreshold => 8;
public override float RobotElectricitySafetyThreshold => 6.5f;
public override float RobotHeatSafetyThreshold => 8;
public override float SourceAmount => 8;
public override float SourceIntensity => 8;
public override float DistanceAmountFalloff => 0.5f;
public override float DistanceIntensityFalloff => 0.4f;
public override IReadOnlyList<JunctionRatioPreset> TwoOutflowJunctionRatios { get; } = [
new("0/4", [0, 1]),
new("1/3", [0.25f, 0.75f]),
new("2/2", [0.5f, 0.5f]),
new("3/1", [0.75f, 0.25f]),
new("4/0", [1, 0])
];
public override IReadOnlyList<JunctionRatioPreset> ThreeOutflowJunctionRatios { get; } = [
new("0/3/3", [0, 0.5f, 0.5f]),
new("3/0/3", [0.5f, 0, 0.5f]),
new("3/3/0", [0.5f, 0.5f, 0]),
new("2/2/2", [1f / 3f, 1f / 3f, 1f / 3f])
];
public override float ConsumerRequiredAmount => 2.5f;
public override float ConsumerRequiredIntensity => 2.5f;
public override float LeakBaseAmount => 0.5f;
public override float LeakAmountScale => 0.15f;
public override float LeakIntensityScale => 0.1f;
public override float FlowTransferRatio => 0.05f;
public override float WarmCautionAmount => 0.5f;
public override float WarmCriticalAmount => 1.0f;
public override float QuenchCautionAmount => 0.6f;
public override float QuenchCriticalAmount => 1.2f;
public override float ShortCautionHeat => 0.8f;
public override float ShortCautionDischarge => 0.8f;
public override float ShortCriticalHeat => 1.6f;
public override float ShortCriticalDischarge => 1.5f;
public override float IgniteCautionHeat => 1.2f;
public override float IgniteCautionFuelConsumption => 0.4f;
public override float IgniteCriticalHeat => 2.4f;
public override float IgniteCriticalFuelConsumption => 0.8f;
public override int RemedyBlockTurns => 2;
public override int HeatShieldSteps => 3;
public override int InventoryCapacityPerRemedy => 3;
}

View 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
}

View 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; }
}

View 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);
}
}

View 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;
}

View 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;
}
}

View File

@@ -0,0 +1,3 @@
namespace ReactorMaintenance.Simulation;
public sealed record JunctionRatioPreset(string Label, float[] Weights);

View File

@@ -0,0 +1,168 @@
namespace ReactorMaintenance.Simulation;
public static class LevelEditor
{
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 => SetCarrierProp(level, position, EPropType.Consumer, command.Carrier),
EEditorTool.Junction => SetFloorProp(level, position, new() { Type = EPropType.Junction }),
EEditorTool.Door => SetFloorProp(level, position, new() { Type = EPropType.Door }),
EEditorTool.AllSeeingEyeTerminal => SetFloorProp(level, position, new() { Type = EPropType.AllSeeingEyeTerminal }),
EEditorTool.RemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = command.RemedyType }),
EEditorTool.ReactorControl => SetReactorControl(level, position),
EEditorTool.Leak => SetLeak(level, position, command.Carrier),
EEditorTool.SurfaceHazard => AddSurfaceHazard(level, position, command.Carrier),
EEditorTool.Heat => level.SetSurface(position, level.GetSurface(position) with { Heat = level.GetSurface(position).Heat + 1 }),
EEditorTool.Robot => level.IsFloor(position) ? level with { Robot = level.Robot with { Position = position } } : level,
_ => level
};
}
public static LevelState SetDoorEdge(LevelState level, GridPosition a, GridPosition b)
{
if (!level.IsFloor(a) || !level.IsFloor(b) || a.ManhattanDistance(b) != 1)
return level;
return level.SetProp(a, new() { Type = EPropType.Door }) with {
Doors = [
.. level.Doors.Where(door => !SameDoorEdge(door, a, b)),
new() { A = a, B = b, State = EDoorState.Closed }
]
};
}
public static LevelState SetLeak(LevelState level, GridPosition undergroundPosition, GridPosition accessPosition, ECarrierType carrier)
{
if (!level.InBounds(undergroundPosition) || !level.IsFloor(accessPosition))
return level;
if (carrier is ECarrierType.Fuel or ECarrierType.Coolant && undergroundPosition != accessPosition)
return level;
if (carrier == ECarrierType.Electricity && undergroundPosition.ManhattanDistance(accessPosition) != 1)
return level;
var next = level.SetUnderground(undergroundPosition, carrier, new() { State = EUndergroundState.Leaking });
return next with {
Leaks = [
.. next.Leaks.Where(leak => leak.UndergroundPosition != undergroundPosition || leak.Carrier != carrier),
new() {
Carrier = carrier,
UndergroundPosition = undergroundPosition,
AccessPosition = accessPosition
}
]
};
}
public static LevelState BindReactorConsumer(LevelState level, int reactorId, ECarrierType carrier, GridPosition consumerPosition)
{
if (!level.InBounds(consumerPosition) || level.GetProp(consumerPosition) is not { Type: EPropType.Consumer } prop || prop.Carrier != carrier)
return level;
var reactors = level.Reactors.Select(reactor => reactor.ReactorId == reactorId ? BindConsumer(reactor, carrier, consumerPosition) : reactor).ToArray();
return level with { Reactors = reactors };
}
public static LevelState AddRuleEvent(LevelState level, RuleEventState ruleEvent)
{
var id = string.IsNullOrWhiteSpace(ruleEvent.Id) ? NextRuleEventId(level) : ruleEvent.Id;
var authoredEvent = ruleEvent with { Id = id };
return level with {
RuleEvents = [.. level.RuleEvents.Where(existing => existing.Id != id), authoredEvent]
};
}
public static LevelState RemoveRuleEvent(LevelState level, string id)
{
return level with { RuleEvents = level.RuleEvents.Where(ruleEvent => ruleEvent.Id != id).ToArray() };
}
private static LevelState SetUnderground(LevelState level, GridPosition position, ECarrierType carrier)
{
return level.SetUnderground(position, carrier, new() { State = EUndergroundState.Intact });
}
private static LevelState SetCarrierProp(LevelState level, GridPosition position, EPropType type, ECarrierType carrier)
{
return SetFloorProp(level, position, new() { Type = type, Carrier = carrier, SwitchState = EPropSwitchState.Enabled });
}
private static LevelState AddSurfaceHazard(LevelState level, GridPosition position, ECarrierType carrier)
{
var surface = level.GetSurface(position);
return carrier switch {
ECarrierType.Fuel => level.SetSurface(position, surface with { Fuel = surface.Fuel + 1 }),
ECarrierType.Coolant => level.SetSurface(position, surface with { Coolant = surface.Coolant + 1 }),
ECarrierType.Electricity => level.SetSurface(position, surface with { Electricity = surface.Electricity + 1 }),
_ => level
};
}
private static LevelState SetFloorProp(LevelState level, GridPosition position, PropState prop)
{
return level.IsFloor(position) ? level.SetProp(position, prop) : level;
}
private static LevelState SetReactorControl(LevelState level, GridPosition position)
{
if (!level.IsFloor(position))
return level;
var id = level.Reactors.Count == 0 ? 1 : level.Reactors.Max(reactor => reactor.ReactorId) + 1;
var levelWithProp = level.SetProp(position, new() { Type = EPropType.ReactorControl, ReactorId = id });
return levelWithProp with {
Reactors = [
.. level.Reactors,
new() {
ReactorId = id,
ControlPosition = position,
FuelConsumerPosition = position,
CoolantConsumerPosition = position,
ElectricityConsumerPosition = position
}
]
};
}
private static LevelState SetLeak(LevelState level, GridPosition position, ECarrierType carrier)
{
if (!level.InBounds(position))
return level;
return SetLeak(level, position, position, carrier);
}
private static bool SameDoorEdge(DoorState door, GridPosition a, GridPosition b)
{
return (door.A == a && door.B == b) || (door.A == b && door.B == a);
}
private static ReactorBinding BindConsumer(ReactorBinding reactor, ECarrierType carrier, GridPosition consumerPosition)
{
return carrier switch {
ECarrierType.Fuel => reactor with { FuelConsumerPosition = consumerPosition },
ECarrierType.Coolant => reactor with { CoolantConsumerPosition = consumerPosition },
ECarrierType.Electricity => reactor with { ElectricityConsumerPosition = consumerPosition },
_ => reactor
};
}
private static string NextRuleEventId(LevelState level)
{
var next = level.RuleEvents.Count + 1;
while (level.RuleEvents.Any(ruleEvent => ruleEvent.Id == $"rule-{next}"))
next++;
return $"rule-{next}";
}
}

View File

@@ -0,0 +1,42 @@
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 = 2;
private static readonly JsonSerializerOptions s_Options = new() {
WriteIndented = true,
Converters = { new JsonStringEnumConverter() }
};
}

View File

@@ -0,0 +1,116 @@
namespace ReactorMaintenance.Simulation;
public static class LevelStateExtensions
{
public static bool InBounds(this LevelState level, GridPosition position)
{
return position.X >= 0 && position.Y >= 0 && position.X < level.Width && position.Y < level.Height;
}
public static int Index(this LevelState level, GridPosition position)
{
if (!level.InBounds(position))
throw new ArgumentOutOfRangeException(nameof(position), $"Position {position.X},{position.Y} is outside {level.Width}x{level.Height}.");
return (position.Y * level.Width) + position.X;
}
public static ECellTerrain GetTerrain(this LevelState level, GridPosition position)
{
return level.Terrain[level.Index(position)];
}
public static UndergroundCell GetUnderground(this LevelState level, GridPosition position, ECarrierType carrier)
{
return level.Layer(carrier)[level.Index(position)];
}
public static SurfaceState GetSurface(this LevelState level, GridPosition position)
{
return level.Surface[level.Index(position)];
}
public static PropState GetProp(this LevelState level, GridPosition position)
{
return level.Props[level.Index(position)];
}
public static bool IsFloor(this LevelState level, GridPosition position)
{
return level.InBounds(position) && level.GetTerrain(position) == ECellTerrain.Floor;
}
public static bool IsClosedDoorEdge(this LevelState level, GridPosition a, GridPosition b)
{
return level.Doors.Any(door => door.State == EDoorState.Closed && SameEdge(door.A, door.B, a, b));
}
public static LevelState SetTerrain(this LevelState level, GridPosition position, ECellTerrain terrain)
{
var next = level.Terrain.ToArray();
next[level.Index(position)] = terrain;
var updated = level with { Terrain = next };
return terrain == ECellTerrain.Wall ? updated.ClearFloorOnlyState(position) : updated;
}
public static LevelState SetUnderground(this LevelState level, GridPosition position, ECarrierType carrier, UndergroundCell cell)
{
var next = level.Layer(carrier).ToArray();
next[level.Index(position)] = cell;
return carrier switch {
ECarrierType.Fuel => level with { Fuel = next },
ECarrierType.Coolant => level with { Coolant = next },
ECarrierType.Electricity => level with { Electricity = next },
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
};
}
public static LevelState SetSurface(this LevelState level, GridPosition position, SurfaceState surface)
{
var next = level.Surface.ToArray();
next[level.Index(position)] = surface.Clamp();
return level with { Surface = next };
}
public static LevelState SetProp(this LevelState level, GridPosition position, PropState prop)
{
var next = level.Props.ToArray();
next[level.Index(position)] = prop;
return level with { Props = next };
}
public static LevelState WithRuntimeArrays(this LevelState level, UndergroundCell[] fuel, UndergroundCell[] coolant, UndergroundCell[] electricity, SurfaceState[] surface, PropState[] props)
{
return level with {
Fuel = fuel,
Coolant = coolant,
Electricity = electricity,
Surface = surface,
Props = props
};
}
public static IReadOnlyList<UndergroundCell> Layer(this LevelState level, ECarrierType carrier)
{
return carrier switch {
ECarrierType.Fuel => level.Fuel,
ECarrierType.Coolant => level.Coolant,
ECarrierType.Electricity => level.Electricity,
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
};
}
private static LevelState ClearFloorOnlyState(this LevelState level, GridPosition position)
{
return level.SetSurface(position, new())
.SetProp(position, new())
.SetUnderground(position, ECarrierType.Fuel, new())
.SetUnderground(position, ECarrierType.Coolant, new())
.SetUnderground(position, ECarrierType.Electricity, new());
}
private static bool SameEdge(GridPosition edgeA, GridPosition edgeB, GridPosition a, GridPosition b)
{
return (edgeA == a && edgeB == b) || (edgeA == b && edgeB == a);
}
}

View 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();
}
}

View 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);
}
}
}

View File

@@ -0,0 +1,284 @@
namespace ReactorMaintenance.Simulation;
public sealed class LevelValidator
{
public ValidationReport Validate(LevelState level)
{
var errors = new List<ValidationIssue>();
var warnings = new List<ValidationIssue>();
ValidateDimensions(level, errors);
ValidateRobot(level, errors);
ValidateCells(level, errors);
ValidateDoors(level, errors);
ValidateLeaks(level, errors);
ValidateReactors(level, errors, warnings);
ValidateJunctions(level, errors);
ValidateRuleEvents(level, errors);
ValidateWarnings(level, warnings);
return new() { Errors = errors, Warnings = warnings };
}
private static void ValidateDimensions(LevelState level, List<ValidationIssue> errors)
{
if (level.Width < Balancing.Current.MinimumLevelSize || level.Height < Balancing.Current.MinimumLevelSize)
errors.Add(new("Invalid level dimensions."));
var expected = level.Width * level.Height;
if (level.Terrain.Length != expected || level.Fuel.Length != expected || level.Coolant.Length != expected || level.Electricity.Length != expected || level.Surface.Length != expected || level.Props.Length != expected)
errors.Add(new("Cell array counts do not match level dimensions."));
}
private static void ValidateRobot(LevelState level, List<ValidationIssue> errors)
{
if (!level.IsFloor(level.Robot.Position))
errors.Add(new("Robot must be in bounds on a floor cell.", level.Robot.Position));
}
private static void ValidateCells(LevelState level, List<ValidationIssue> errors)
{
for (var y = 0; y < level.Height; y++)
{
for (var x = 0; x < level.Width; x++)
{
var position = new GridPosition(x, y);
var surface = level.GetSurface(position);
var prop = level.GetProp(position);
if (level.GetTerrain(position) == ECellTerrain.Wall)
{
if (surface.Fuel > 0 || surface.Coolant > 0 || surface.Electricity > 0 || surface.Heat > 0)
errors.Add(new("Wall cell cannot store surface hazards.", position));
if (prop.Type != EPropType.None)
errors.Add(new("Prop must be placed on floor terrain.", position));
}
}
}
}
private static void ValidateDoors(LevelState level, List<ValidationIssue> errors)
{
foreach (var door in level.Doors)
{
if (!level.IsFloor(door.A) || !level.IsFloor(door.B) || door.A.ManhattanDistance(door.B) != 1)
errors.Add(new("Door edge must connect two adjacent floor cells.", door.A));
}
}
private static void ValidateLeaks(LevelState level, List<ValidationIssue> errors)
{
foreach (var leak in level.Leaks)
{
if (!level.InBounds(leak.UndergroundPosition) || !level.IsFloor(leak.AccessPosition))
{
errors.Add(new("Leak must have valid floor access.", leak.AccessPosition));
continue;
}
var underground = level.GetUnderground(leak.UndergroundPosition, leak.Carrier);
if (!underground.IsPresent)
errors.Add(new("Leak target must point to an underground cell.", leak.UndergroundPosition));
if (leak.Carrier is ECarrierType.Fuel or ECarrierType.Coolant && leak.UndergroundPosition != leak.AccessPosition)
errors.Add(new("Fuel and coolant leaks must use their underground coordinate as access.", leak.AccessPosition));
if (leak.Carrier == ECarrierType.Electricity && leak.UndergroundPosition.ManhattanDistance(leak.AccessPosition) != 1)
errors.Add(new("Electricity leak access must be an adjacent floor face.", leak.AccessPosition));
}
}
private static void ValidateReactors(LevelState level, List<ValidationIssue> errors, List<ValidationIssue> warnings)
{
foreach (var reactor in level.Reactors)
{
if (!IsProp(level, reactor.ControlPosition, EPropType.ReactorControl))
errors.Add(new("Reactor binding control position must point to a reactor control prop.", reactor.ControlPosition));
ValidateConsumerBinding(level, reactor.FuelConsumerPosition, ECarrierType.Fuel, errors);
ValidateConsumerBinding(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant, errors);
ValidateConsumerBinding(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity, errors);
if (!reactor.Ready)
warnings.Add(new("Reactor is initially unready.", reactor.ControlPosition));
}
}
private static void ValidateConsumerBinding(LevelState level, GridPosition position, ECarrierType carrier, List<ValidationIssue> errors)
{
if (!level.InBounds(position) || level.GetProp(position) is not { Type: EPropType.Consumer } prop || prop.Carrier != carrier)
errors.Add(new($"Missing or invalid {carrier} consumer binding.", position));
}
private static void ValidateJunctions(LevelState level, List<ValidationIssue> errors)
{
foreach (var junction in JunctionFlowAnalyzer.Analyze(level))
errors.AddRange(junction.Errors.Select(error => new ValidationIssue(error, junction.Position)));
}
private static void ValidateRuleEvents(LevelState level, List<ValidationIssue> errors)
{
foreach (var ruleEvent in level.RuleEvents)
{
foreach (var predicate in ruleEvent.Predicates)
ValidateRulePredicate(level, predicate, errors);
foreach (var effect in ruleEvent.Effects)
ValidateRuleEffect(level, effect, errors);
}
}
private static void ValidateRulePredicate(LevelState level, RulePredicate predicate, List<ValidationIssue> errors)
{
switch (predicate.Kind)
{
case ERulePredicateKind.PropStateAt:
if (!level.InBounds(predicate.Position) || level.GetProp(predicate.Position).Type == EPropType.None)
errors.Add(new("Rule prop predicate must target a prop.", predicate.Position));
break;
case ERulePredicateKind.ConsumerStateAt:
if (!IsProp(level, predicate.Position, EPropType.Consumer))
errors.Add(new("Rule consumer predicate must target a consumer prop.", predicate.Position));
break;
case ERulePredicateKind.NetworkBandAt:
if (!level.InBounds(predicate.Position) || !level.GetUnderground(predicate.Position, predicate.Carrier).IsPresent)
errors.Add(new("Rule network predicate must target an underground cell.", predicate.Position));
break;
case ERulePredicateKind.SurfaceBandAt:
case ERulePredicateKind.RobotAt:
if (!level.IsFloor(predicate.Position))
errors.Add(new("Rule floor predicate must target a floor cell.", predicate.Position));
break;
case ERulePredicateKind.ReactorReadyIs:
case ERulePredicateKind.ReactorWonIs:
ValidateOptionalReactorId(level, predicate.ReactorId, predicate.Position, errors);
break;
}
}
private static void ValidateRuleEffect(LevelState level, RuleEffect effect, List<ValidationIssue> errors)
{
if (RequiresNonNegativeAmount(effect.Kind) && effect.Amount < 0)
errors.Add(new("Rule effect amount must be non-negative.", effect.Position));
switch (effect.Kind)
{
case ERuleEffectKind.StartLeak:
ValidateRuleLeakEffect(level, effect, errors);
break;
case ERuleEffectKind.WorsenLeak:
case ERuleEffectKind.RepairNetworkCell:
case ERuleEffectKind.DisableNetworkCell:
if (!level.InBounds(effect.Position) || !level.GetUnderground(effect.Position, effect.Carrier).IsPresent)
errors.Add(new("Rule network effect must target an underground cell.", effect.Position));
break;
case ERuleEffectKind.SetPropEnabled:
if (!level.InBounds(effect.Position) || level.GetProp(effect.Position).Type == EPropType.None)
errors.Add(new("Rule prop effect must target a prop.", effect.Position));
break;
case ERuleEffectKind.AddSurfaceHazard:
case ERuleEffectKind.RemoveSurfaceHazard:
case ERuleEffectKind.AddHeat:
case ERuleEffectKind.RemoveHeat:
if (!level.IsFloor(effect.Position))
errors.Add(new("Rule surface effect must target a floor cell.", effect.Position));
break;
}
}
private static bool RequiresNonNegativeAmount(ERuleEffectKind kind)
{
return kind is ERuleEffectKind.AddSurfaceHazard
or ERuleEffectKind.RemoveSurfaceHazard
or ERuleEffectKind.AddHeat
or ERuleEffectKind.RemoveHeat
or ERuleEffectKind.AddInventory
or ERuleEffectKind.RemoveInventory;
}
private static void ValidateRuleLeakEffect(LevelState level, RuleEffect effect, List<ValidationIssue> errors)
{
var accessPosition = effect.AccessPosition ?? effect.Position;
if (!level.InBounds(effect.Position) || !level.GetUnderground(effect.Position, effect.Carrier).IsPresent)
errors.Add(new("Rule leak effect must target an underground cell.", effect.Position));
if (!level.IsFloor(accessPosition))
{
errors.Add(new("Rule leak effect must have valid floor access.", accessPosition));
return;
}
if (effect.Carrier is ECarrierType.Fuel or ECarrierType.Coolant && effect.Position != accessPosition)
errors.Add(new("Rule fuel and coolant leak effects must use their underground coordinate as access.", accessPosition));
if (effect.Carrier == ECarrierType.Electricity && effect.Position.ManhattanDistance(accessPosition) != 1)
errors.Add(new("Rule electricity leak effect access must be an adjacent floor face.", accessPosition));
}
private static void ValidateOptionalReactorId(LevelState level, int reactorId, GridPosition position, List<ValidationIssue> errors)
{
if (reactorId > 0 && level.Reactors.All(reactor => reactor.ReactorId != reactorId))
errors.Add(new("Rule reactor predicate must reference an existing reactor.", position));
}
private static void ValidateWarnings(LevelState level, List<ValidationIssue> warnings)
{
foreach (var carrier in Enum.GetValues<ECarrierType>())
{
for (var y = 0; y < level.Height; y++)
{
for (var x = 0; x < level.Width; x++)
{
var position = new GridPosition(x, y);
if (level.GetUnderground(position, carrier).IsPresent && !HasSourcePath(level, position, carrier))
warnings.Add(new($"Underground {carrier} cell has no source path.", position));
}
}
}
for (var y = 0; y < level.Height; y++)
{
for (var x = 0; x < level.Width; x++)
{
var position = new GridPosition(x, y);
var prop = level.GetProp(position);
if (prop.Type == EPropType.Consumer && prop.SwitchState == EPropSwitchState.Enabled && !HasSourcePath(level, position, prop.Carrier))
warnings.Add(new("Enabled consumer is initially starved.", position));
}
}
}
private static bool HasSourcePath(LevelState level, GridPosition start, ECarrierType carrier)
{
if (!level.GetUnderground(start, carrier).CarriesFlow)
return false;
var visited = new HashSet<GridPosition>();
var open = new Queue<GridPosition>();
open.Enqueue(start);
visited.Add(start);
while (open.Count > 0)
{
var current = open.Dequeue();
if (level.GetProp(current) is { Type: EPropType.Flow, Carrier: var sourceCarrier, SwitchState: EPropSwitchState.Enabled } && sourceCarrier == carrier)
return true;
foreach (var next in current.Neighbors().Where(level.InBounds))
{
if (!visited.Add(next) || !level.GetUnderground(next, carrier).CarriesFlow)
continue;
open.Enqueue(next);
}
}
return false;
}
private static bool IsProp(LevelState level, GridPosition position, EPropType propType)
{
return level.InBounds(position) && level.GetProp(position).Type == propType;
}
}

View File

@@ -0,0 +1,8 @@
namespace ReactorMaintenance.Simulation;
public sealed record DoorState
{
public GridPosition A { get; init; } = new(0, 0);
public GridPosition B { get; init; } = new(0, 0);
public EDoorState State { get; init; } = EDoorState.Closed;
}

View File

@@ -0,0 +1,8 @@
namespace ReactorMaintenance.Simulation;
public enum EBand
{
Safe,
Caution,
Critical
}

View File

@@ -0,0 +1,8 @@
namespace ReactorMaintenance.Simulation;
public enum ECarrierType
{
Fuel,
Coolant,
Electricity
}

View File

@@ -0,0 +1,7 @@
namespace ReactorMaintenance.Simulation;
public enum ECellTerrain
{
Floor,
Wall
}

View File

@@ -0,0 +1,10 @@
namespace ReactorMaintenance.Simulation;
public enum EConsumerServiceState
{
Unknown,
Disabled,
Starved,
Supplied,
Producing
}

View File

@@ -0,0 +1,7 @@
namespace ReactorMaintenance.Simulation;
public enum EDoorState
{
Open,
Closed
}

View File

@@ -0,0 +1,10 @@
namespace ReactorMaintenance.Simulation;
public enum EForecastKind
{
TerminalLoss,
ReactorReady,
ConsumerStarved,
HazardGrowth,
RuleEvent
}

View File

@@ -0,0 +1,11 @@
namespace ReactorMaintenance.Simulation;
public enum ELevelState
{
Stable,
Caution,
Critical,
Ready,
Lost,
Won
}

View File

@@ -0,0 +1,7 @@
namespace ReactorMaintenance.Simulation;
public enum ENetworkValueKind
{
Amount,
Intensity
}

View File

@@ -0,0 +1,7 @@
namespace ReactorMaintenance.Simulation;
public enum EPropSwitchState
{
Disabled,
Enabled
}

View File

@@ -0,0 +1,13 @@
namespace ReactorMaintenance.Simulation;
public enum EPropType
{
None,
Flow,
Consumer,
Junction,
Door,
AllSeeingEyeTerminal,
RemedySupply,
ReactorControl
}

View File

@@ -0,0 +1,9 @@
namespace ReactorMaintenance.Simulation;
public enum ERemedyType
{
FuelNeutralizer,
CoolantNeutralizer,
ElectricityNeutralizer,
HeatShield
}

View File

@@ -0,0 +1,18 @@
namespace ReactorMaintenance.Simulation;
public enum ERuleEffectKind
{
StartLeak,
WorsenLeak,
RepairNetworkCell,
DisableNetworkCell,
SetPropEnabled,
AddSurfaceHazard,
RemoveSurfaceHazard,
AddHeat,
RemoveHeat,
AddInventory,
RemoveInventory,
MarkTerminalLoss,
EmitWarning
}

View File

@@ -0,0 +1,7 @@
namespace ReactorMaintenance.Simulation;
public enum ERuleEventPhase
{
StartOfSimulation,
EndOfTurn
}

View File

@@ -0,0 +1,17 @@
namespace ReactorMaintenance.Simulation;
public enum ERulePredicateKind
{
TurnAtLeast,
LevelStateIs,
ReactorReadyIs,
ReactorLostIs,
ReactorWonIs,
PropStateAt,
ConsumerStateAt,
NetworkBandAt,
SurfaceBandAt,
RobotAt,
RobotInventoryAtLeast,
AllSeeingEyeUnlocked
}

View File

@@ -0,0 +1,11 @@
namespace ReactorMaintenance.Simulation;
public enum ESurfaceInteractionVerb
{
Hold,
Flow,
Warm,
Quench,
Short,
Ignite
}

View File

@@ -0,0 +1,9 @@
namespace ReactorMaintenance.Simulation;
public enum ESurfaceQuantity
{
Fuel,
Coolant,
Electricity,
Heat
}

View File

@@ -0,0 +1,8 @@
namespace ReactorMaintenance.Simulation;
public enum EUndergroundState
{
Absent,
Intact,
Leaking
}

View File

@@ -0,0 +1,3 @@
namespace ReactorMaintenance.Simulation;
public sealed record Forecast(EForecastKind Kind, GridPosition? Position, int Turns, string Message);

View File

@@ -0,0 +1,12 @@
namespace ReactorMaintenance.Simulation;
public sealed record GlobalState
{
public int Turn { get; init; }
public int ActionsRemaining { get; init; } = Balancing.Current.ActionsPerTurn;
public ELevelState LevelState { get; init; } = ELevelState.Stable;
public string Status { get; init; } = "STABLE";
public bool AllSeeingEyeUnlocked { get; init; }
public bool TerminalLoss { get; init; }
public IReadOnlyList<string> Warnings { get; init; } = Array.Empty<string>();
}

View File

@@ -0,0 +1,3 @@
namespace ReactorMaintenance.Simulation;
public sealed record GridPosition(int X, int Y);

View 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; }
}

View File

@@ -0,0 +1,26 @@
namespace ReactorMaintenance.Simulation;
public sealed record LevelState
{
public static LevelState Create(string name, int width, int height)
{
return LevelStateFactory.Create(name, width, height);
}
public string Name { get; init; } = "New Reactor";
public int Width { get; init; } = Balancing.Current.DefaultLevelWidth;
public int Height { get; init; } = Balancing.Current.DefaultLevelHeight;
public ECellTerrain[] Terrain { get; init; } = LevelStateFactory.CreateTerrain(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
public UndergroundCell[] Fuel { get; init; } = LevelStateFactory.CreateUnderground(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
public UndergroundCell[] Coolant { get; init; } = LevelStateFactory.CreateUnderground(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
public UndergroundCell[] Electricity { get; init; } = LevelStateFactory.CreateUnderground(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
public SurfaceState[] Surface { get; init; } = LevelStateFactory.CreateSurface(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
public PropState[] Props { get; init; } = LevelStateFactory.CreateProps(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
public IReadOnlyList<DoorState> Doors { get; init; } = Array.Empty<DoorState>();
public IReadOnlyList<LeakState> Leaks { get; init; } = Array.Empty<LeakState>();
public IReadOnlyList<ReactorBinding> Reactors { get; init; } = Array.Empty<ReactorBinding>();
public IReadOnlyList<RuleEventState> RuleEvents { get; init; } = Array.Empty<RuleEventState>();
public RobotState Robot { get; init; } = new();
public GlobalState Global { get; init; } = new();
public IReadOnlyList<Forecast> Forecasts { get; init; } = Array.Empty<Forecast>();
}

View File

@@ -0,0 +1,15 @@
namespace ReactorMaintenance.Simulation;
public sealed record PropState
{
public EPropType Type { get; init; }
public ECarrierType Carrier { get; init; }
public EPropSwitchState SwitchState { get; init; } = EPropSwitchState.Enabled;
public EConsumerServiceState ServiceState { get; init; } = EConsumerServiceState.Unknown;
public int JunctionMode { get; init; }
public ERemedyType RemedyType { get; init; }
public bool Depleted { get; init; }
public int ReactorId { get; init; }
public bool IsEnabled => SwitchState == EPropSwitchState.Enabled;
}

View File

@@ -0,0 +1,12 @@
namespace ReactorMaintenance.Simulation;
public sealed record ReactorBinding
{
public int ReactorId { get; init; }
public GridPosition ControlPosition { get; init; } = new(0, 0);
public GridPosition FuelConsumerPosition { get; init; } = new(0, 0);
public GridPosition CoolantConsumerPosition { get; init; } = new(0, 0);
public GridPosition ElectricityConsumerPosition { get; init; } = new(0, 0);
public bool Ready { get; init; }
public bool Activated { get; init; }
}

View 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; }
}

View File

@@ -0,0 +1,13 @@
namespace ReactorMaintenance.Simulation;
public sealed record RuleEffect
{
public ERuleEffectKind Kind { get; init; }
public GridPosition Position { get; init; } = new(0, 0);
public GridPosition? AccessPosition { get; init; }
public ECarrierType Carrier { get; init; }
public ERemedyType Remedy { get; init; }
public float Amount { get; init; }
public EPropSwitchState PropSwitchState { get; init; }
public string Message { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,14 @@
namespace ReactorMaintenance.Simulation;
public sealed record RuleEventState
{
public string Id { get; init; } = string.Empty;
public bool Enabled { get; init; } = true;
public bool Repeat { get; init; }
public bool Triggered { get; init; }
public int Priority { get; init; }
public ERuleEventPhase Phase { get; init; }
public IReadOnlyList<RulePredicate> Predicates { get; init; } = Array.Empty<RulePredicate>();
public IReadOnlyList<RuleEffect> Effects { get; init; } = Array.Empty<RuleEffect>();
public string ForecastText { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,18 @@
namespace ReactorMaintenance.Simulation;
public sealed record RulePredicate
{
public ERulePredicateKind Kind { get; init; }
public GridPosition Position { get; init; } = new(0, 0);
public int ReactorId { get; init; }
public int Turn { get; init; }
public ELevelState LevelState { get; init; }
public EPropSwitchState PropSwitchState { get; init; }
public EConsumerServiceState ConsumerServiceState { get; init; }
public ECarrierType Carrier { get; init; }
public ENetworkValueKind NetworkValue { get; init; }
public ERemedyType Remedy { get; init; }
public EBand Band { get; init; }
public int InventoryCount { get; init; }
public bool BoolValue { get; init; }
}

View 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; }
}

View File

@@ -0,0 +1,11 @@
namespace ReactorMaintenance.Simulation;
public sealed record UndergroundCell
{
public EUndergroundState State { get; init; }
public float Amount { get; init; }
public float Intensity { get; init; }
public bool IsPresent => State != EUndergroundState.Absent;
public bool CarriesFlow => State is EUndergroundState.Intact or EUndergroundState.Leaking;
}

View File

@@ -0,0 +1,3 @@
namespace ReactorMaintenance.Simulation;
public sealed record ValidationIssue(string Message, GridPosition? Position = null);

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View 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);
}
}

View 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);
}
}

View File

@@ -0,0 +1,78 @@
namespace ReactorMaintenance.Simulation;
public sealed class SimulationEngine
{
public LevelState MoveRobot(LevelState level, GridPosition destination)
{
return PlayerActionSystem.MoveRobot(level, destination, SpendAction);
}
public LevelState InteractProp(LevelState level)
{
return PlayerActionSystem.InteractProp(level, SpendAction);
}
public LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy)
{
return PlayerActionSystem.InteractLeak(level, carrier, useRemedy, SpendAction);
}
public LevelState ApplyHeatShield(LevelState level)
{
return PlayerActionSystem.ApplyHeatShield(level, SpendAction);
}
public LevelState ActivateReactor(LevelState level)
{
return ReactorSystem.Activate(level);
}
public LevelState EndTurn(LevelState level)
{
return ResolveTurn(level with { Global = level.Global with { ActionsRemaining = 0 } });
}
public LevelState AdvanceTurn(LevelState level)
{
return ResolveTurn(level);
}
public IReadOnlyList<Forecast> Forecast(LevelState level)
{
return ForecastSystem.Forecast(level, simulated => ResolveTurn(simulated, false));
}
private LevelState SpendAction(LevelState level)
{
var actions = Math.Max(0, level.Global.ActionsRemaining - 1);
var next = level with { Global = level.Global with { ActionsRemaining = actions } };
return actions == 0 ? ResolveTurn(next) : next;
}
private LevelState ResolveTurn(LevelState level, bool refreshForecasts = true)
{
var report = m_Validator.Validate(level);
if (!report.IsValid)
return level with { Global = level.Global with { LevelState = ELevelState.Lost, Status = report.Errors[0].Message } };
var next = RuleEventSystem.Apply(level, ERuleEventPhase.StartOfSimulation);
next = NetworkPropagationSystem.Propagate(next);
next = ConsumerSystem.Resolve(next);
next = LeakSystem.Inject(next);
next = SurfaceInteractionSystem.Resolve(next);
next = RobotSafetySystem.Resolve(next);
next = ReactorSystem.DeriveState(next);
next = RuleEventSystem.Apply(next, ERuleEventPhase.EndOfTurn);
next = SurfaceInteractionSystem.AdvanceDurations(next);
next = next with {
Global = next.Global with {
Turn = next.Global.Turn + 1,
ActionsRemaining = Balancing.Current.ActionsPerTurn
}
};
return refreshForecasts ? next with { Forecasts = Forecast(next) } : next;
}
private readonly LevelValidator m_Validator = new();
}

View 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.")
};
}
}

View File

@@ -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; }
}

View 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.")
};
}
}

View File

@@ -0,0 +1,28 @@
namespace ReactorMaintenance.Simulation;
internal static class ConsumerSystem
{
public static LevelState Resolve(LevelState level)
{
var props = level.Props.ToArray();
foreach (var position in LevelTraversal.AllPositions(level))
{
var index = level.Index(position);
var prop = props[index];
if (prop.Type != EPropType.Consumer)
continue;
if (prop.SwitchState == EPropSwitchState.Disabled)
{
props[index] = prop with { ServiceState = EConsumerServiceState.Disabled };
continue;
}
var underground = level.GetUnderground(position, prop.Carrier);
var supplied = underground.Amount >= Balancing.Current.ConsumerRequiredAmount && underground.Intensity >= Balancing.Current.ConsumerRequiredIntensity;
props[index] = prop with { ServiceState = supplied ? EConsumerServiceState.Producing : EConsumerServiceState.Starved };
}
return level with { Props = props };
}
}

View File

@@ -0,0 +1,58 @@
namespace ReactorMaintenance.Simulation;
internal static class ForecastSystem
{
public static IReadOnlyList<Forecast> Forecast(LevelState level, Func<LevelState, LevelState> resolveTurn)
{
var forecasts = new List<Forecast>();
var simulated = CopyForForecast(level);
for (var turn = 0; turn <= Balancing.Current.ForecastHorizon; turn++)
{
AddForecasts(forecasts, simulated, turn);
if (simulated.Global.LevelState is ELevelState.Lost or ELevelState.Ready or ELevelState.Won)
break;
if (turn < Balancing.Current.ForecastHorizon)
simulated = resolveTurn(simulated);
}
return forecasts.DistinctBy(forecast => (forecast.Kind, forecast.Position, forecast.Message)).OrderBy(forecast => forecast.Turns).ThenBy(forecast => forecast.Message).ToArray();
}
private static LevelState CopyForForecast(LevelState level)
{
return level with {
Terrain = level.Terrain.ToArray(),
Fuel = level.Fuel.ToArray(),
Coolant = level.Coolant.ToArray(),
Electricity = level.Electricity.ToArray(),
Surface = level.Surface.ToArray(),
Props = level.Props.ToArray(),
Forecasts = Array.Empty<Forecast>()
};
}
private static void AddForecasts(List<Forecast> forecasts, LevelState level, int turn)
{
if (level.Global.LevelState == ELevelState.Lost)
forecasts.Add(new(EForecastKind.TerminalLoss, level.Robot.Position, turn, level.Global.Status));
if (level.Global.LevelState == ELevelState.Ready)
forecasts.Add(new(EForecastKind.ReactorReady, null, turn, "REACTOR READY"));
foreach (var position in LevelTraversal.AllPositions(level))
{
var prop = level.GetProp(position);
if (prop.Type == EPropType.Consumer && prop.ServiceState == EConsumerServiceState.Starved)
forecasts.Add(new(EForecastKind.ConsumerStarved, position, turn, $"{prop.Carrier} consumer starved"));
var surface = level.GetSurface(position);
if (SimulationBands.Fuel(surface.Fuel) == EBand.Critical || SimulationBands.Coolant(surface.Coolant) == EBand.Critical || SimulationBands.Electricity(surface.Electricity) == EBand.Critical || SimulationBands.Heat(surface.Heat) == EBand.Critical)
forecasts.Add(new(EForecastKind.HazardGrowth, position, turn, "Critical hazard"));
}
foreach (var ruleEvent in level.RuleEvents.Where(ruleEvent => ruleEvent.Enabled && !string.IsNullOrWhiteSpace(ruleEvent.ForecastText) && ruleEvent.Predicates.All(predicate => RuleEventSystem.PredicateMatches(level, predicate))))
forecasts.Add(new(EForecastKind.RuleEvent, null, turn, ruleEvent.ForecastText));
}
}

View 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() };
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,133 @@
namespace ReactorMaintenance.Simulation;
internal static class PlayerActionSystem
{
public static LevelState MoveRobot(LevelState level, GridPosition destination, Func<LevelState, LevelState> spendAction)
{
if (!CanSpendAction(level) || !level.IsFloor(destination) || level.Robot.Position.ManhattanDistance(destination) != 1)
return Refuse(level, "MOVE BLOCKED");
return spendAction(level with {
Robot = level.Robot with {
Position = destination,
HeatImmunitySteps = Math.Max(0, level.Robot.HeatImmunitySteps - 1)
}
});
}
public static LevelState InteractProp(LevelState level, Func<LevelState, LevelState> spendAction)
{
if (!CanSpendAction(level))
return Refuse(level, "NO ACTIONS");
var position = level.Robot.Position;
var prop = level.GetProp(position);
if (prop.Type == EPropType.None)
return Refuse(level, "NO PROP");
var next = prop.Type switch {
EPropType.Flow or EPropType.Consumer => ToggleProp(level, position, prop),
EPropType.Junction => CycleJunctionMode(level, position, prop),
EPropType.Door => ToggleDoor(level, position),
EPropType.AllSeeingEyeTerminal => level with { Global = level.Global with { AllSeeingEyeUnlocked = true, Status = "ALL-SEEING-EYE ONLINE" } },
EPropType.RemedySupply => PickUpRemedy(level, position, prop),
EPropType.ReactorControl => ReactorSystem.Activate(level),
_ => level
};
return spendAction(next);
}
public static LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy, Func<LevelState, LevelState> spendAction)
{
if (!CanSpendAction(level))
return Refuse(level, "NO ACTIONS");
var leakIndex = level.Leaks.ToList().FindIndex(leak => !leak.Repaired && leak.Carrier == carrier && leak.AccessPosition == level.Robot.Position);
if (leakIndex < 0)
return Refuse(level, "NO REACHABLE LEAK");
var leak = level.Leaks[leakIndex];
var next = useRemedy ? ApplyElementRemedy(level, leak) : RepairLeak(level, leakIndex, leak);
return spendAction(next);
}
public static LevelState ApplyHeatShield(LevelState level, Func<LevelState, LevelState> spendAction)
{
if (!CanSpendAction(level) || level.Robot.HeatShields <= 0)
return Refuse(level, "NO HEAT SHIELD");
return spendAction(level with {
Robot = level.Robot.Spend(ERemedyType.HeatShield) with { HeatImmunitySteps = Balancing.Current.HeatShieldSteps }
});
}
private static LevelState ToggleProp(LevelState level, GridPosition position, PropState prop)
{
var switchState = prop.SwitchState == EPropSwitchState.Enabled ? EPropSwitchState.Disabled : EPropSwitchState.Enabled;
return level.SetProp(position, prop with { SwitchState = switchState });
}
private static LevelState ToggleDoor(LevelState level, GridPosition position)
{
var doors = level.Doors.ToArray();
var index = Array.FindIndex(doors, door => door.A == position || door.B == position);
if (index < 0)
return level;
doors[index] = doors[index] with { State = doors[index].State == EDoorState.Open ? EDoorState.Closed : EDoorState.Open };
return level with { Doors = doors };
}
private static LevelState PickUpRemedy(LevelState level, GridPosition position, PropState prop)
{
if (prop.Depleted || level.Robot.Count(prop.RemedyType) >= Balancing.Current.InventoryCapacityPerRemedy)
return level;
return level.SetProp(position, prop with { Depleted = true }) with { Robot = level.Robot.Add(prop.RemedyType, 1) };
}
private static LevelState RepairLeak(LevelState level, int leakIndex, LeakState leak)
{
var leaks = level.Leaks.ToArray();
leaks[leakIndex] = leak with { Repaired = true };
return level.SetUnderground(leak.UndergroundPosition, leak.Carrier, level.GetUnderground(leak.UndergroundPosition, leak.Carrier) with { State = EUndergroundState.Intact }) with { Leaks = leaks };
}
private static LevelState ApplyElementRemedy(LevelState level, LeakState leak)
{
var remedy = leak.Carrier switch {
ECarrierType.Fuel => ERemedyType.FuelNeutralizer,
ECarrierType.Coolant => ERemedyType.CoolantNeutralizer,
ECarrierType.Electricity => ERemedyType.ElectricityNeutralizer,
_ => throw new ArgumentOutOfRangeException(nameof(leak), leak.Carrier, "Unsupported leak carrier.")
};
if (level.Robot.Count(remedy) <= 0)
return Refuse(level, "NO REMEDY");
var surface = SurfaceCarrierMath.RemoveCarrier(level.GetSurface(leak.AccessPosition), leak.Carrier);
return level.SetSurface(leak.AccessPosition, surface) with { Robot = level.Robot.Spend(remedy) };
}
private static LevelState CycleJunctionMode(LevelState level, GridPosition position, PropState prop)
{
var flow = JunctionFlowAnalyzer.Analyze(level).FirstOrDefault(junction => junction.Position == position);
var outflowCount = flow?.OutgoingBranches.Count ?? 2;
var ratios = Balancing.Current.JunctionRatios(outflowCount);
if (ratios.Count == 0)
return level;
return level.SetProp(position, prop with { JunctionMode = (prop.JunctionMode + 1) % ratios.Count });
}
private static bool CanSpendAction(LevelState level)
{
return level.Global.LevelState is not (ELevelState.Lost or ELevelState.Won) && level.Global.ActionsRemaining > 0;
}
private static LevelState Refuse(LevelState level, string message)
{
return level with { Global = level.Global with { Status = message } };
}
}

View File

@@ -0,0 +1,78 @@
namespace ReactorMaintenance.Simulation;
internal static class ReactorSystem
{
public static LevelState Activate(LevelState level)
{
var reactorIndex = level.Reactors.ToList().FindIndex(reactor => reactor.ControlPosition == level.Robot.Position);
if (reactorIndex < 0)
return Refuse(level, "NO REACTOR CONTROL");
var reactor = level.Reactors[reactorIndex];
if (!reactor.Ready)
return Refuse(level, "REACTOR NOT READY");
var reactors = level.Reactors.ToArray();
reactors[reactorIndex] = reactor with { Activated = true };
return level with {
Reactors = reactors,
Global = level.Global with { LevelState = ELevelState.Won, Status = "REACTOR ONLINE" }
};
}
public static LevelState DeriveState(LevelState level)
{
if (level.Global.LevelState is ELevelState.Lost or ELevelState.Won)
return level;
var reactors = level.Reactors.Select(reactor => reactor with { Ready = IsReady(level, reactor) }).ToArray();
if (reactors.Any(reactor => reactor.Ready))
return level with { Reactors = reactors, Global = level.Global with { LevelState = ELevelState.Ready, Status = "REACTOR READY" } };
var maxHeat = level.Surface.Where((_, index) => level.Terrain[index] == ECellTerrain.Floor).Select(surface => surface.Heat).DefaultIfEmpty(0).Max();
if (maxHeat >= Balancing.Current.TerminalHeat)
return level with { Reactors = reactors, Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "REACTOR HEAT TERMINAL" } };
var hasCritical = level.Surface.Any(surface => SimulationBands.Fuel(surface.Fuel) == EBand.Critical || SimulationBands.Coolant(surface.Coolant) == EBand.Critical || SimulationBands.Electricity(surface.Electricity) == EBand.Critical || SimulationBands.Heat(surface.Heat) == EBand.Critical);
var hasCaution = hasCritical || level.Props.Any(prop => prop.ServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled) || level.Leaks.Any(leak => !leak.Repaired);
var state = hasCritical ? ELevelState.Critical :
hasCaution ? ELevelState.Caution : ELevelState.Stable;
return level with { Reactors = reactors, Global = level.Global with { LevelState = state, Status = state.ToString().ToUpperInvariant() } };
}
public static bool MatchesReady(LevelState level, RulePredicate predicate)
{
return level.Reactors.Any(reactor => MatchesId(reactor, predicate.ReactorId) && reactor.Ready) == predicate.BoolValue;
}
public static bool MatchesWon(LevelState level, RulePredicate predicate)
{
var won = predicate.ReactorId > 0
? level.Reactors.Any(reactor => reactor.ReactorId == predicate.ReactorId && reactor.Activated)
: level.Global.LevelState == ELevelState.Won || level.Reactors.Any(reactor => reactor.Activated);
return won == predicate.BoolValue;
}
private static bool IsReady(LevelState level, ReactorBinding reactor)
{
return HasProducingConsumer(level, reactor.FuelConsumerPosition, ECarrierType.Fuel)
&& HasProducingConsumer(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant)
&& HasProducingConsumer(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity)
&& level.GetSurface(reactor.ControlPosition).Heat < Balancing.Current.TerminalHeat;
}
private static bool HasProducingConsumer(LevelState level, GridPosition position, ECarrierType carrier)
{
return level.InBounds(position) && level.GetProp(position) is { Type: EPropType.Consumer, Carrier: var consumerCarrier, ServiceState: EConsumerServiceState.Producing } && consumerCarrier == carrier;
}
private static bool MatchesId(ReactorBinding reactor, int reactorId)
{
return reactorId <= 0 || reactor.ReactorId == reactorId;
}
private static LevelState Refuse(LevelState level, string message)
{
return level with { Global = level.Global with { Status = message } };
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,73 @@
namespace ReactorMaintenance.Simulation;
internal static class RuleEventSystem
{
public static LevelState Apply(LevelState level, ERuleEventPhase phase)
{
var next = level;
var events = level.RuleEvents.Select((ruleEvent, index) => (Event: ruleEvent, Index: index)).Where(item => item.Event.Enabled && item.Event.Phase == phase && (item.Event.Repeat || !item.Event.Triggered)).OrderBy(item => item.Event.Priority).ToArray();
var ruleEvents = next.RuleEvents.ToArray();
foreach (var item in events)
{
if (!item.Event.Predicates.All(predicate => PredicateMatches(next, predicate)))
continue;
foreach (var effect in item.Event.Effects)
next = ApplyEffect(next, effect);
ruleEvents[item.Index] = item.Event with { Triggered = true };
}
return next with { RuleEvents = ruleEvents };
}
public static bool PredicateMatches(LevelState level, RulePredicate predicate)
{
return predicate.Kind switch {
ERulePredicateKind.TurnAtLeast => level.Global.Turn >= predicate.Turn,
ERulePredicateKind.LevelStateIs => level.Global.LevelState == predicate.LevelState,
ERulePredicateKind.ReactorReadyIs => ReactorSystem.MatchesReady(level, predicate),
ERulePredicateKind.ReactorLostIs => level.Global.LevelState == ELevelState.Lost == predicate.BoolValue,
ERulePredicateKind.ReactorWonIs => ReactorSystem.MatchesWon(level, predicate),
ERulePredicateKind.PropStateAt => level.InBounds(predicate.Position) && level.GetProp(predicate.Position).SwitchState == predicate.PropSwitchState,
ERulePredicateKind.ConsumerStateAt => level.InBounds(predicate.Position) && level.GetProp(predicate.Position).ServiceState == predicate.ConsumerServiceState,
ERulePredicateKind.NetworkBandAt => level.InBounds(predicate.Position) && SimulationBands.NetworkBand(level.GetUnderground(predicate.Position, predicate.Carrier), predicate.Carrier, predicate.NetworkValue) >= predicate.Band,
ERulePredicateKind.SurfaceBandAt => level.InBounds(predicate.Position) && SimulationBands.SurfaceBand(level.GetSurface(predicate.Position), predicate.Carrier) >= predicate.Band,
ERulePredicateKind.RobotAt => level.Robot.Position == predicate.Position,
ERulePredicateKind.RobotInventoryAtLeast => level.Robot.Count(predicate.Remedy) >= predicate.InventoryCount,
ERulePredicateKind.AllSeeingEyeUnlocked => level.Global.AllSeeingEyeUnlocked == predicate.BoolValue,
_ => false
};
}
private static LevelState ApplyEffect(LevelState level, RuleEffect effect)
{
return effect.Kind switch {
ERuleEffectKind.StartLeak => StartLeak(level, effect),
ERuleEffectKind.WorsenLeak => level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Leaking }),
ERuleEffectKind.RepairNetworkCell => level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Intact }),
ERuleEffectKind.DisableNetworkCell => level.SetUnderground(effect.Position, effect.Carrier, new()),
ERuleEffectKind.SetPropEnabled => level.SetProp(effect.Position, level.GetProp(effect.Position) with { SwitchState = effect.PropSwitchState }),
ERuleEffectKind.AddSurfaceHazard => level.SetSurface(effect.Position, SurfaceCarrierMath.AddCarrier(level.GetSurface(effect.Position), effect.Carrier, effect.Amount)),
ERuleEffectKind.RemoveSurfaceHazard => level.SetSurface(effect.Position, SurfaceCarrierMath.AddCarrier(level.GetSurface(effect.Position), effect.Carrier, -effect.Amount)),
ERuleEffectKind.AddHeat => level.SetSurface(effect.Position, level.GetSurface(effect.Position) with { Heat = level.GetSurface(effect.Position).Heat + effect.Amount }),
ERuleEffectKind.RemoveHeat => level.SetSurface(effect.Position, level.GetSurface(effect.Position) with { Heat = level.GetSurface(effect.Position).Heat - effect.Amount }),
ERuleEffectKind.AddInventory => level with { Robot = level.Robot.Add(effect.Remedy, (int)effect.Amount) },
ERuleEffectKind.RemoveInventory => level with { Robot = level.Robot.Add(effect.Remedy, -(int)effect.Amount) },
ERuleEffectKind.MarkTerminalLoss => level with { Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = string.IsNullOrWhiteSpace(effect.Message) ? "TERMINAL FAILURE" : effect.Message } },
ERuleEffectKind.EmitWarning => level with { Global = level.Global with { Warnings = [.. level.Global.Warnings, effect.Message] } },
_ => level
};
}
private static LevelState StartLeak(LevelState level, RuleEffect effect)
{
var leak = new LeakState {
Carrier = effect.Carrier,
UndergroundPosition = effect.Position,
AccessPosition = effect.AccessPosition ?? effect.Position
};
return level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Leaking }) with { Leaks = [.. level.Leaks, leak] };
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,5 @@
<Application
x:Class="ReactorMaintenance.Win2D.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
</Application>

View File

@@ -0,0 +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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

View File

@@ -0,0 +1,145 @@
<Window
x:Class="ReactorMaintenance.Win2D.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
Title="Reactor Maintenance">
<Grid Background="#16191D">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<CommandBar Grid.Row="0" DefaultLabelPosition="Right" Background="#20252A">
<AppBarButton Icon="Add" Label="New" Click="New_Click" />
<AppBarButton Icon="OpenFile" Label="Open" Click="Open_Click" />
<AppBarButton Icon="Save" Label="Save" Click="Save_Click" />
<AppBarSeparator />
<AppBarButton Icon="Play" Label="End Turn" Click="EndTurn_Click" />
<AppBarButton Label="Interact" Click="Interact_Click" />
<AppBarButton Label="Heat Shield" Click="HeatShield_Click" />
<AppBarButton Icon="Accept" Label="Activate" Click="Activate_Click" />
</CommandBar>
<Grid Grid.Row="1" ColumnSpacing="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="260" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="300" />
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Column="0" Background="#1C2126">
<StackPanel Padding="12" Spacing="10">
<TextBlock Text="Tools" FontSize="18" FontWeight="SemiBold" Foreground="#F4F1E8" />
<ItemsControl x:Name="ToolPicker">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid Orientation="Horizontal" MaximumRowsOrColumns="2" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ToggleButton IsChecked="{Binding IsSelected, Mode=TwoWay}"
Checked="ToolToggle_Checked" ToolTipService.ToolTip="{Binding Label}"
Width="112" MinHeight="46" Padding="6" Margin="0,0,8,8">
<TextBlock Text="{Binding Label}" TextWrapping="WrapWholeWords"
TextAlignment="Center"
FontSize="12" />
</ToggleButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Left click selects or paints. Right click clears surface prop and hazards."
Foreground="#9EA7AE" TextWrapping="Wrap" />
<TextBlock
Text="Door and wall electricity leaks use two clicks: choose the source cell, then the adjacent floor face."
Foreground="#9EA7AE"
TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>
<Grid Grid.Column="1" Background="#101215">
<canvas:CanvasControl
x:Name="LevelCanvas"
ClearColor="#101215"
CreateResources="LevelCanvas_CreateResources"
Draw="LevelCanvas_Draw"
PointerPressed="LevelCanvas_PointerPressed"
PointerMoved="LevelCanvas_PointerMoved"
PointerReleased="LevelCanvas_PointerReleased"
PointerExited="LevelCanvas_PointerExited"
PointerWheelChanged="LevelCanvas_PointerWheelChanged" />
</Grid>
<ScrollViewer Grid.Column="2" Background="#1C2126">
<StackPanel Padding="14" Spacing="12">
<TextBlock x:Name="LevelNameText" FontSize="20" FontWeight="SemiBold" Foreground="#F4F1E8"
TextWrapping="Wrap" />
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="Turn" Foreground="#9EA7AE" />
<TextBlock x:Name="TurnText" FontSize="22" Foreground="#F4F1E8" />
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Status" Foreground="#9EA7AE" />
<TextBlock x:Name="StatusText" FontSize="16" Foreground="#F4F1E8" TextWrapping="Wrap" />
</StackPanel>
</Grid>
<TextBlock Text="Global Systems" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<TextBlock x:Name="GlobalText" Foreground="#CCD4DA" TextWrapping="Wrap" />
<TextBlock Text="Selected Cell" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<TextBlock x:Name="CellText" Foreground="#CCD4DA" TextWrapping="Wrap" />
<TextBlock Text="Editor Workflow" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<TextBlock x:Name="WorkflowText" Foreground="#CCD4DA" TextWrapping="Wrap" />
<TextBlock Text="Reactor Binding" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<TextBlock x:Name="ReactorBindingText" Foreground="#CCD4DA" TextWrapping="Wrap" />
<Grid ColumnSpacing="8" RowSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Content="Fuel" Click="BindFuel_Click" HorizontalAlignment="Stretch" />
<Button Grid.Column="1" Content="Coolant" Click="BindCoolant_Click"
HorizontalAlignment="Stretch" />
<Button Grid.Column="2" Content="Electric" Click="BindElectricity_Click"
HorizontalAlignment="Stretch" />
</Grid>
<TextBlock Text="Rule Events" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<TextBlock x:Name="RuleEventText" Foreground="#CCD4DA" TextWrapping="Wrap" />
<Grid ColumnSpacing="8" RowSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Content="Warn Next Turn" Click="AddWarningRule_Click" HorizontalAlignment="Stretch" />
<Button Grid.Column="1" Content="Leak Next Turn" Click="AddLeakRule_Click"
HorizontalAlignment="Stretch" />
</Grid>
<Button Content="Remove Last Rule" Click="RemoveLastRule_Click" HorizontalAlignment="Stretch" />
<TextBlock Text="Forecasts" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<ItemsControl x:Name="ForecastList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#46515A" BorderThickness="1" Padding="8" Margin="0,0,0,8"
CornerRadius="3">
<TextBlock Text="{Binding Message}" Foreground="#F4F1E8" TextWrapping="Wrap" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Grid>
</Grid>
</Window>

Some files were not shown because too many files have changed in this diff Show More