Compare commits

16 Commits

36 changed files with 3873 additions and 1 deletions

614
.editorconfig Normal file
View File

@@ -0,0 +1,614 @@
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

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.vs
bin
obj

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 --build=False '$file1' '$file2' ...` for every C# file you touched.

50
CODESTYLE.md Normal file
View File

@@ -0,0 +1,50 @@
# 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.
- 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,18 @@
# zfxaction26_2 # Reactor Maintenance
C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `design.md`.
## Projects
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, versioned JSON serialization, and swappable difficulty balancing profiles.
- `src/ReactorMaintenance.Win2D`: Win2D editor app for painting floor/wall terrain plus cell props, loading/saving levels, advancing simulation turns, and activating the reactor.
- `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior.
## Commands
```powershell
dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj
dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
```

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>

1280
docs/design.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,78 @@
using ReactorMaintenance.Simulation.Difficulties;
namespace ReactorMaintenance.Simulation;
public abstract class Balancing
{
public static Balancing Current { get; set; } = new NormalBalancing();
public abstract int MinHazardValue { get; }
public abstract int MaxHazardValue { get; }
public abstract int DefaultHazardStability { get; }
public abstract int DefaultCellIntegrity { get; }
public abstract int DefaultActionsPerTurn { get; }
public abstract int DefaultCoreHeat { get; }
public abstract int DefaultFacilityStability { get; }
public abstract int DefaultPower { get; }
public abstract int DefaultCooling { get; }
public abstract int FirstGridCoordinate { get; }
public abstract int NeighborDistance { get; }
public abstract int CurrentForecastTurn { get; }
public abstract int MinimumLevelSize { get; }
public abstract int DefaultLevelWidth { get; }
public abstract int DefaultLevelHeight { get; }
public abstract int DefaultRobotCoordinate { get; }
public abstract int DefaultPipeFlow { get; }
public abstract int DefaultPipePressure { get; }
public abstract int DefaultPressurePipeFlow { get; }
public abstract int DefaultPressurePipePressure { get; }
public abstract int DefaultEditedPipeIntegrity { get; }
public abstract int MinimumLeakRate { get; }
public abstract int DamagedPipeIntegrity { get; }
public abstract int RepairedLeakRate { get; }
public abstract int RepairedElectricalCharge { get; }
public abstract int HeatToolIncrease { get; }
public abstract int FireToolMinimumHeat { get; }
public abstract int FireToolMinimumSmoke { get; }
public abstract int MaxForecastStepCount { get; }
public abstract int TurnIncrement { get; }
public abstract int OverpressureThreshold { get; }
public abstract int HeatIntegrityDamageThreshold { get; }
public abstract int PipeFireIntegrityDamage { get; }
public abstract int FireStabilityDamage { get; }
public abstract int BurstLeakRate { get; }
public abstract int BrokenPipeFlow { get; }
public abstract int ElectrifiedCoolantPoolingThreshold { get; }
public abstract int ElectricalChargeIncrease { get; }
public abstract int FuelVaporFireThreshold { get; }
public abstract int LiquidFuelFireThreshold { get; }
public abstract int HeatIgnitionThreshold { get; }
public abstract int ElectricalIgnitionThreshold { get; }
public abstract int FireHeatIncrease { get; }
public abstract int FireSmokeIncrease { get; }
public abstract int FireLiquidFuelConsumption { get; }
public abstract int FireFuelVaporConsumption { get; }
public abstract int SmokeDecay { get; }
public abstract int PressurizedFuelLeakPressureThreshold { get; }
public abstract int PassiveFuelVaporHeatOffset { get; }
public abstract int PassiveFuelVaporDivisor { get; }
public abstract int MinimumCoolantHeatReduction { get; }
public abstract int CoolantHeatReductionDivisor { get; }
public abstract int CoolantSteamHeatThreshold { get; }
public abstract int CoolantSteamSmokeIncrease { get; }
public abstract int PressureLeakSmokeThreshold { get; }
public abstract int PressureLeakSmokeIncrease { get; }
public abstract int GeneratorHeatIncrease { get; }
public abstract int CoolingPumpHeatReduction { get; }
public abstract int ReactorHeatIncrease { get; }
public abstract int SmokeSpreadThreshold { get; }
public abstract int SmokeSpreadIncrease { get; }
public abstract int CriticalCellStabilityThreshold { get; }
public abstract int MeltdownCoreHeatThreshold { get; }
public abstract int StabilityCollapseThreshold { get; }
public abstract int GeneratorPowerOutput { get; }
public abstract int CoolingPumpOutput { get; }
public abstract int ReactorReadyPowerThreshold { get; }
public abstract int ReactorReadyCoolingThreshold { get; }
public abstract int ReactorReadyCoreHeatThreshold { get; }
}

View File

@@ -0,0 +1,76 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation.Difficulties;
public class NormalBalancing : Balancing
{
public override int MinHazardValue => 0;
public override int MaxHazardValue => 10;
public override int DefaultHazardStability => 10;
public override int DefaultCellIntegrity => 10;
public override int DefaultActionsPerTurn => 3;
public override int DefaultCoreHeat => 5;
public override int DefaultFacilityStability => 10;
public override int DefaultPower => 5;
public override int DefaultCooling => 0;
public override int FirstGridCoordinate => 0;
public override int NeighborDistance => 1;
public override int CurrentForecastTurn => 0;
public override int MinimumLevelSize => 4;
public override int DefaultLevelWidth => 16;
public override int DefaultLevelHeight => 12;
public override int DefaultRobotCoordinate => 1;
public override int DefaultPipeFlow => 4;
public override int DefaultPipePressure => 4;
public override int DefaultPressurePipeFlow => 5;
public override int DefaultPressurePipePressure => 6;
public override int DefaultEditedPipeIntegrity => 8;
public override int MinimumLeakRate => 1;
public override int DamagedPipeIntegrity => 4;
public override int RepairedLeakRate => 0;
public override int RepairedElectricalCharge => 0;
public override int HeatToolIncrease => 2;
public override int FireToolMinimumHeat => 7;
public override int FireToolMinimumSmoke => 3;
public override int MaxForecastStepCount => 12;
public override int TurnIncrement => 1;
public override int OverpressureThreshold => 7;
public override int HeatIntegrityDamageThreshold => 10;
public override int PipeFireIntegrityDamage => 1;
public override int FireStabilityDamage => 1;
public override int BurstLeakRate => 3;
public override int BrokenPipeFlow => 0;
public override int ElectrifiedCoolantPoolingThreshold => 3;
public override int ElectricalChargeIncrease => 2;
public override int FuelVaporFireThreshold => 4;
public override int LiquidFuelFireThreshold => 6;
public override int HeatIgnitionThreshold => 8;
public override int ElectricalIgnitionThreshold => 4;
public override int FireHeatIncrease => 2;
public override int FireSmokeIncrease => 2;
public override int FireLiquidFuelConsumption => 1;
public override int FireFuelVaporConsumption => 1;
public override int SmokeDecay => 1;
public override int PressurizedFuelLeakPressureThreshold => 7;
public override int PassiveFuelVaporHeatOffset => 3;
public override int PassiveFuelVaporDivisor => 3;
public override int MinimumCoolantHeatReduction => 1;
public override int CoolantHeatReductionDivisor => 2;
public override int CoolantSteamHeatThreshold => 7;
public override int CoolantSteamSmokeIncrease => 2;
public override int PressureLeakSmokeThreshold => 8;
public override int PressureLeakSmokeIncrease => 1;
public override int GeneratorHeatIncrease => 1;
public override int CoolingPumpHeatReduction => 2;
public override int ReactorHeatIncrease => 1;
public override int SmokeSpreadThreshold => 6;
public override int SmokeSpreadIncrease => 1;
public override int CriticalCellStabilityThreshold => 3;
public override int MeltdownCoreHeatThreshold => 10;
public override int StabilityCollapseThreshold => 0;
public override int GeneratorPowerOutput => 3;
public override int CoolingPumpOutput => 3;
public override int ReactorReadyPowerThreshold => 3;
public override int ReactorReadyCoolingThreshold => 3;
public override int ReactorReadyCoreHeatThreshold => 8;
}

View File

@@ -0,0 +1,35 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation.Effects;
public sealed class CellIntegrityEffect : ISimulationEffect
{
public CellState Apply(CellState cell)
{
var integrity = cell.Integrity;
var hazards = cell.Hazards;
if (cell is { HasPipe: true } && cell.Pressure > Balancing.Current.OverpressureThreshold)
integrity -= cell.Pressure - Balancing.Current.OverpressureThreshold;
if (hazards.Heat >= Balancing.Current.HeatIntegrityDamageThreshold || hazards.Fire)
{
integrity -= cell.HasPipe ? Balancing.Current.PipeFireIntegrityDamage : Balancing.Current.MinHazardValue;
hazards = hazards with { Stability = hazards.Stability - Balancing.Current.FireStabilityDamage };
}
cell = cell with {
Integrity = Rules.Clamp(integrity),
Hazards = hazards.Clamp()
};
if (integrity > Balancing.Current.MinHazardValue || !cell.HasPipe)
return cell;
return cell with {
LeakRate = Math.Max(cell.LeakRate, Balancing.Current.BurstLeakRate),
Flow = Balancing.Current.BrokenPipeFlow,
PipeOpen = false
};
}
}

View File

@@ -0,0 +1,30 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation.Effects;
public sealed class FireAndElectricalHazardEffect : ISimulationEffect
{
public CellState Apply(CellState cell)
{
var hazards = cell.Hazards;
if (hazards.CoolantPooling >= Balancing.Current.ElectrifiedCoolantPoolingThreshold && cell.Powered)
hazards = hazards with { ElectricalCharge = hazards.ElectricalCharge + Balancing.Current.ElectricalChargeIncrease };
var hasFuel = hazards.FuelVapor >= Balancing.Current.FuelVaporFireThreshold || hazards.LiquidFuel >= Balancing.Current.LiquidFuelFireThreshold;
var hasIgnition = hazards.Heat >= Balancing.Current.HeatIgnitionThreshold || hazards.ElectricalCharge >= Balancing.Current.ElectricalIgnitionThreshold || cell is { Prop: ECellProp.Generator, Powered: true };
if ((hasFuel && hasIgnition) || hazards.Fire)
{
hazards = hazards with {
Fire = hasFuel || hazards.Fire,
Heat = hazards.Heat + Balancing.Current.FireHeatIncrease,
Smoke = hazards.Smoke + Balancing.Current.FireSmokeIncrease,
LiquidFuel = Math.Max(Balancing.Current.MinHazardValue, hazards.LiquidFuel - Balancing.Current.FireLiquidFuelConsumption),
FuelVapor = Math.Max(Balancing.Current.MinHazardValue, hazards.FuelVapor - Balancing.Current.FireFuelVaporConsumption)
};
}
else if (hazards.Smoke > Balancing.Current.MinHazardValue)
hazards = hazards with { Smoke = hazards.Smoke - Balancing.Current.SmokeDecay };
return cell with { Hazards = hazards.Clamp() };
}
}

View File

@@ -0,0 +1,8 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation.Effects;
public interface IAreaSimulationEffect
{
CellState[] Apply(LevelState level, CellState[] cells);
}

View File

@@ -0,0 +1,8 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation.Effects;
public interface ISimulationEffect
{
CellState Apply(CellState cell);
}

View File

@@ -0,0 +1,18 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation.Effects;
public sealed class MachineEffect : ISimulationEffect
{
public CellState Apply(CellState cell)
{
var hazards = cell.Prop switch {
ECellProp.Generator when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.GeneratorHeatIncrease },
ECellProp.CoolingPump when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat - Balancing.Current.CoolingPumpHeatReduction },
ECellProp.Reactor => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.ReactorHeatIncrease },
_ => cell.Hazards
};
return cell with { Hazards = hazards.Clamp() };
}
}

View File

@@ -0,0 +1,28 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation.Effects;
public sealed class PipeLeakEffect : ISimulationEffect
{
public CellState Apply(CellState cell)
{
if (!cell.HasPipe || cell.LeakRate <= Balancing.Current.MinHazardValue)
return cell;
var hazards = cell.Pipe switch {
EPipeMedium.Fuel => cell.Hazards with {
LiquidFuel = cell.Hazards.LiquidFuel + cell.LeakRate,
FuelVapor = cell.Hazards.FuelVapor + (cell.Pressure >= Balancing.Current.PressurizedFuelLeakPressureThreshold ? cell.LeakRate : Math.Max(Balancing.Current.MinHazardValue, cell.Hazards.Heat - Balancing.Current.PassiveFuelVaporHeatOffset) / Balancing.Current.PassiveFuelVaporDivisor)
},
EPipeMedium.Coolant => cell.Hazards with {
CoolantPooling = cell.Hazards.CoolantPooling + cell.LeakRate,
Heat = cell.Hazards.Heat - Math.Max(Balancing.Current.MinimumCoolantHeatReduction, cell.LeakRate / Balancing.Current.CoolantHeatReductionDivisor),
Smoke = cell.Hazards.Smoke + (cell.Hazards.Heat >= Balancing.Current.CoolantSteamHeatThreshold ? Balancing.Current.CoolantSteamSmokeIncrease : Balancing.Current.MinHazardValue)
},
EPipeMedium.Pressure => cell.Hazards with { Smoke = cell.Hazards.Smoke + (cell.Pressure >= Balancing.Current.PressureLeakSmokeThreshold ? Balancing.Current.PressureLeakSmokeIncrease : Balancing.Current.MinHazardValue) },
_ => cell.Hazards
};
return cell with { Hazards = hazards.Clamp() };
}
}

View File

@@ -0,0 +1,37 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation.Effects;
public sealed class SmokeSpreadEffect : IAreaSimulationEffect
{
public CellState[] Apply(LevelState level, CellState[] cells)
{
var next = cells.ToArray();
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
{
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
{
var position = new GridPosition(x, y);
var cell = cells[level.Index(position)];
if (cell.Hazards.Smoke < Balancing.Current.SmokeSpreadThreshold)
continue;
SpreadToNeighbors(level, next, position);
}
}
return next;
}
private static void SpreadToNeighbors(LevelState level, CellState[] next, GridPosition position)
{
foreach (var neighbor in position.Neighbors().Where(level.InBounds))
{
var neighborCell = next[level.Index(neighbor)];
if (!neighborCell.IsWalkable || neighborCell.DoorLocked)
continue;
next[level.Index(neighbor)] = neighborCell with { Hazards = neighborCell.Hazards with { Smoke = Rules.Clamp(neighborCell.Hazards.Smoke + Balancing.Current.SmokeSpreadIncrease) } };
}
}
}

View File

@@ -0,0 +1,8 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation.Hazards;
public abstract class Hazard
{
public abstract IEnumerable<Forecast> Predict(LevelState level, int turns);
}

View File

@@ -0,0 +1,20 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation.Hazards;
public sealed class IgnitionHazard : Hazard
{
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
{
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
{
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
{
var position = new GridPosition(x, y);
var cell = level.GetCell(position);
if (cell.Hazards.Fire)
yield return new(EFailureKind.Ignition, position, turns, turns == Balancing.Current.TurnIncrement ? $"FUEL IGNITION PREDICTED AT {x},{y} NEXT TURN" : $"FUEL IGNITION PREDICTED AT {x},{y} IN {turns} TURNS");
}
}
}
}

View File

@@ -0,0 +1,12 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation.Hazards;
public sealed class MeltdownHazard : Hazard
{
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
{
if (level.Global is { Lost: true, Status: "CORE MELTDOWN" })
yield return new(EFailureKind.Meltdown, null, turns, "CORE MELTDOWN APPROACHING");
}
}

View File

@@ -0,0 +1,20 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation.Hazards;
public sealed class PipeBurstHazard : Hazard
{
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
{
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
{
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
{
var position = new GridPosition(x, y);
var cell = level.GetCell(position);
if (cell is { HasPipe: true, PipeOpen: false } && cell.Flow == Balancing.Current.BrokenPipeFlow && cell.LeakRate >= Balancing.Current.BurstLeakRate)
yield return new(EFailureKind.PipeBurst, position, turns, $"PIPE BURST PREDICTED AT {x},{y} IN {turns} TURNS");
}
}
}
}

View File

@@ -0,0 +1,12 @@
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Simulation.Hazards;
public sealed class StabilityCollapseHazard : Hazard
{
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
{
if (level.Global is { Lost: true, Status: "FACILITY STABILITY COLLAPSE" })
yield return new(EFailureKind.StabilityCollapse, null, turns, "FACILITY STABILITY COLLAPSE APPROACHING");
}
}

View File

@@ -0,0 +1,117 @@
namespace ReactorMaintenance.Simulation;
public enum EEditorTool
{
Floor,
Wall,
Reactor,
CoolingPump,
Generator,
PressureRegulator,
DiagnosticTerminal,
ControlTerminal,
CoolantPipe,
FuelPipe,
PressurePipe,
Leak,
Repair,
Heat,
Fire,
Robot
}
public static class LevelEditor
{
public static LevelState Apply(LevelState level, GridPosition position, EEditorTool tool)
{
if (!level.InBounds(position))
return level;
if (tool == EEditorTool.Robot)
return level.GetCell(position).IsWalkable ? level with { Robot = position } : level;
var cell = level.GetCell(position);
cell = tool switch {
EEditorTool.Floor => cell with { Terrain = ECellTerrain.Floor },
EEditorTool.Wall => cell with {
Terrain = ECellTerrain.Wall,
Prop = ECellProp.None,
Pipe = EPipeMedium.None,
Flow = Balancing.Current.MinHazardValue,
Pressure = Balancing.Current.MinHazardValue,
LeakRate = Balancing.Current.MinHazardValue,
PipeOpen = false,
Powered = false
},
EEditorTool.Reactor => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.Reactor },
EEditorTool.CoolingPump => cell with {
Terrain = ECellTerrain.Floor,
Prop = ECellProp.CoolingPump,
Powered = true
},
EEditorTool.Generator => cell with {
Terrain = ECellTerrain.Floor,
Prop = ECellProp.Generator,
Powered = true
},
EEditorTool.PressureRegulator => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.PressureRegulator },
EEditorTool.DiagnosticTerminal => cell with {
Terrain = ECellTerrain.Floor,
Prop = ECellProp.DiagnosticTerminal,
Powered = true
},
EEditorTool.ControlTerminal => cell with {
Terrain = ECellTerrain.Floor,
Prop = ECellProp.ControlTerminal,
Powered = true
},
EEditorTool.CoolantPipe => cell with {
Pipe = EPipeMedium.Coolant,
Flow = Balancing.Current.DefaultPipeFlow,
Pressure = Balancing.Current.DefaultPipePressure,
Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
PipeOpen = true
},
EEditorTool.FuelPipe => cell with {
Pipe = EPipeMedium.Fuel,
Flow = Balancing.Current.DefaultPipeFlow,
Pressure = Balancing.Current.DefaultPipePressure,
Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
PipeOpen = true
},
EEditorTool.PressurePipe => cell with {
Pipe = EPipeMedium.Pressure,
Flow = Balancing.Current.DefaultPressurePipeFlow,
Pressure = Balancing.Current.DefaultPressurePipePressure,
Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
PipeOpen = true
},
EEditorTool.Leak => cell with {
LeakRate = Math.Max(Balancing.Current.MinimumLeakRate, cell.LeakRate),
Integrity = Math.Min(cell.Integrity, Balancing.Current.DamagedPipeIntegrity)
},
EEditorTool.Repair => cell with {
LeakRate = Balancing.Current.RepairedLeakRate,
Integrity = Balancing.Current.DefaultCellIntegrity,
Hazards = cell.Hazards with {
Fire = false,
ElectricalCharge = Balancing.Current.RepairedElectricalCharge
}
},
EEditorTool.Heat => cell with { Hazards = cell.Hazards with { Heat = Rules.Clamp(cell.Hazards.Heat + Balancing.Current.HeatToolIncrease) } },
EEditorTool.Fire => cell with {
Hazards = cell.Hazards with {
Fire = !cell.Hazards.Fire,
Heat = Math.Max(cell.Hazards.Heat, Balancing.Current.FireToolMinimumHeat),
Smoke = Math.Max(cell.Hazards.Smoke, Balancing.Current.FireToolMinimumSmoke)
}
},
_ => cell
};
if (cell.Terrain == ECellTerrain.Wall)
cell = cell with { Hazards = new() };
return level.SetCell(position, cell);
}
}

View File

@@ -0,0 +1,39 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ReactorMaintenance.Simulation;
public static class LevelSerializer
{
private const int c_CurrentVersion = 1;
public static string Serialize(LevelState level)
{
return JsonSerializer.Serialize(new LevelFile {
Version = c_CurrentVersion,
Level = level
}, Options);
}
public static LevelState Deserialize(string json)
{
var file = JsonSerializer.Deserialize<LevelFile>(json, Options) ?? throw new InvalidOperationException("Level file did not contain a level.");
var level = file.Version switch {
c_CurrentVersion => file.Level,
_ => throw new InvalidOperationException($"Unsupported level file version {file.Version}.")
};
return level.Cells.Length != level.Width * level.Height ? throw new InvalidOperationException("Level cell count does not match its dimensions.") : level;
}
private static readonly JsonSerializerOptions Options = new() {
WriteIndented = true,
Converters = { new JsonStringEnumConverter() }
};
private sealed record LevelFile
{
public int Version { get; init; }
public LevelState Level { get; init; } = new();
}
}

View File

@@ -0,0 +1,181 @@
namespace ReactorMaintenance.Simulation;
public enum ECellTerrain
{
Floor,
Wall
}
public enum ECellProp
{
None,
Reactor,
CoolingPump,
Generator,
PressureRegulator,
DiagnosticTerminal,
ControlTerminal
}
public enum EPipeMedium
{
None,
Pressure,
Coolant,
Fuel
}
public enum EFailureKind
{
PipeBurst,
Ignition,
Meltdown,
StabilityCollapse,
ReactorReady
}
public sealed record GridPosition(int X, int Y)
{
public IEnumerable<GridPosition> Neighbors()
{
yield return new(X - Balancing.Current.NeighborDistance, Y);
yield return new(X + Balancing.Current.NeighborDistance, Y);
yield return new(X, Y - Balancing.Current.NeighborDistance);
yield return new(X, Y + Balancing.Current.NeighborDistance);
}
}
public sealed record HazardState
{
public HazardState Clamp()
{
return this with {
Heat = Rules.Clamp(Heat),
Smoke = Rules.Clamp(Smoke),
FuelVapor = Rules.Clamp(FuelVapor),
LiquidFuel = Rules.Clamp(LiquidFuel),
CoolantPooling = Rules.Clamp(CoolantPooling),
ElectricalCharge = Rules.Clamp(ElectricalCharge),
Stability = Rules.Clamp(Stability)
};
}
public int Heat { get; init; }
public int Smoke { get; init; }
public int FuelVapor { get; init; }
public int LiquidFuel { get; init; }
public int CoolantPooling { get; init; }
public int ElectricalCharge { get; init; }
public int Stability { get; init; } = Balancing.Current.DefaultHazardStability;
public bool Fire { get; init; }
}
public sealed record CellState
{
public ECellTerrain Terrain { get; init; } = ECellTerrain.Floor;
public ECellProp Prop { get; init; }
public EPipeMedium Pipe { get; init; }
public int Flow { get; init; }
public int Pressure { get; init; }
public int Integrity { get; init; } = Balancing.Current.DefaultCellIntegrity;
public int LeakRate { get; init; }
public bool PipeOpen { get; init; } = true;
public bool Powered { get; init; }
public bool DoorLocked { get; init; }
public HazardState Hazards { get; init; } = new();
public bool IsWalkable => Terrain != ECellTerrain.Wall;
public bool HasPipe => Pipe != EPipeMedium.None;
}
public sealed record GlobalState
{
public int Turn { get; init; }
public int ActionsPerTurn { get; init; } = Balancing.Current.DefaultActionsPerTurn;
public int CoreHeat { get; init; } = Balancing.Current.DefaultCoreHeat;
public int FacilityStability { get; init; } = Balancing.Current.DefaultFacilityStability;
public int Power { get; init; } = Balancing.Current.DefaultPower;
public int Cooling { get; init; } = Balancing.Current.DefaultCooling;
public bool ReactorActivated { get; init; }
public bool Lost { get; init; }
public string Status { get; init; } = "STABILIZE SYSTEMS";
}
public sealed record Forecast(EFailureKind Kind, GridPosition? Position, int Turns, string Message);
public sealed record LevelState
{
public static LevelState Create(string name, int width, int height)
{
if (width < Balancing.Current.MinimumLevelSize || height < Balancing.Current.MinimumLevelSize)
throw new ArgumentOutOfRangeException(nameof(width), $"Levels must be at least {Balancing.Current.MinimumLevelSize}x{Balancing.Current.MinimumLevelSize}.");
var cells = CreateCells(width, height);
for (var y = Balancing.Current.FirstGridCoordinate; y < height; y++)
{
for (var x = Balancing.Current.FirstGridCoordinate; x < width; x++)
{
if (x == Balancing.Current.FirstGridCoordinate || y == Balancing.Current.FirstGridCoordinate || x == width - Balancing.Current.NeighborDistance || y == height - Balancing.Current.NeighborDistance)
cells[y * width + x] = cells[y * width + x] with { Terrain = ECellTerrain.Wall };
}
}
return new() {
Name = name,
Width = width,
Height = height,
Cells = cells,
Robot = new(Balancing.Current.DefaultRobotCoordinate, Balancing.Current.DefaultRobotCoordinate)
};
}
public CellState GetCell(GridPosition position)
{
EnsureInBounds(position);
return Cells[Index(position)];
}
public LevelState SetCell(GridPosition position, CellState cell)
{
EnsureInBounds(position);
var cells = Cells.ToArray();
cells[Index(position)] = cell;
return this with { Cells = cells };
}
public bool InBounds(GridPosition position)
{
return position.X >= Balancing.Current.FirstGridCoordinate && position.Y >= Balancing.Current.FirstGridCoordinate && position.X < Width && position.Y < Height;
}
public int Index(GridPosition position)
{
return position.Y * Width + position.X;
}
private void EnsureInBounds(GridPosition position)
{
if (!InBounds(position))
throw new ArgumentOutOfRangeException(nameof(position), $"Position {position.X},{position.Y} is outside {Width}x{Height}.");
}
private static CellState[] CreateCells(int width, int height)
{
return Enumerable.Range(Balancing.Current.FirstGridCoordinate, width * height).Select(_ => new CellState()).ToArray();
}
public string Name { get; init; } = "New Reactor";
public int Width { get; init; } = Balancing.Current.DefaultLevelWidth;
public int Height { get; init; } = Balancing.Current.DefaultLevelHeight;
public CellState[] Cells { get; init; } = CreateCells(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
public GridPosition Robot { get; init; } = new(Balancing.Current.DefaultRobotCoordinate, Balancing.Current.DefaultRobotCoordinate);
public GlobalState Global { get; init; } = new();
public IReadOnlyList<Forecast> Forecasts { get; init; } = Array.Empty<Forecast>();
}
internal static class Rules
{
public static int Clamp(int value)
{
return Math.Clamp(value, Balancing.Current.MinHazardValue, Balancing.Current.MaxHazardValue);
}
}

View File

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

View File

@@ -0,0 +1,150 @@
using ReactorMaintenance.Simulation.Effects;
using ReactorMaintenance.Simulation.Hazards;
namespace ReactorMaintenance.Simulation;
public sealed class SimulationEngine(IEnumerable<ISimulationEffect> effects, IEnumerable<IAreaSimulationEffect> areaEffects, IEnumerable<Hazard> hazards)
{
private sealed record ForecastKey(EFailureKind Kind, GridPosition? Position);
public SimulationEngine()
: this(
[new PipeLeakEffect(), new MachineEffect(), new FireAndElectricalHazardEffect(), new CellIntegrityEffect()],
[new SmokeSpreadEffect()],
[new PipeBurstHazard(), new IgnitionHazard(), new MeltdownHazard(), new StabilityCollapseHazard()])
{
}
public LevelState AdvanceTurn(LevelState level)
{
return AdvanceTurn(level, true);
}
public IReadOnlyList<Forecast> Forecast(LevelState level)
{
var forecasts = new List<Forecast>();
var seen = new HashSet<ForecastKey>();
var forecastLevel = level with { Cells = level.Cells.ToArray(), Forecasts = Array.Empty<Forecast>() };
if (forecastLevel.Global.Lost)
AddHazardForecasts(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
AddReactorReadyForecast(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
if (IsReactorReady(forecastLevel) || forecastLevel.Global.Lost || forecastLevel.Global.ReactorActivated)
return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
for (var step = Balancing.Current.TurnIncrement; step <= Balancing.Current.MaxForecastStepCount; step++)
{
forecastLevel = AdvanceTurn(forecastLevel, false);
AddHazardForecasts(forecasts, seen, forecastLevel, step);
AddReactorReadyForecast(forecasts, seen, forecastLevel, step);
if (forecastLevel.Global.Lost || IsReactorReady(forecastLevel) || forecastLevel.Global.ReactorActivated)
break;
}
return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
}
public LevelState ActivateReactor(LevelState level)
{
if (!IsReactorReady(level))
return level with { Global = level.Global with { Status = "REACTOR NOT READY" } };
return level with {
Global = level.Global with {
ReactorActivated = true,
Status = "REACTOR ONLINE"
}
};
}
private LevelState AdvanceTurn(LevelState level, bool updateForecasts)
{
var cells = level.Cells.ToArray();
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
{
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
{
var position = new GridPosition(x, y);
var index = level.Index(position);
var cell = cells[index];
if (!cell.IsWalkable)
continue;
foreach (var effect in m_Effects)
cell = effect.Apply(cell);
cells[index] = cell with { Hazards = cell.Hazards.Clamp() };
}
}
foreach (var areaEffect in m_AreaEffects)
cells = areaEffect.Apply(level, cells);
var global = UpdateGlobal(level, cells);
var next = level with {
Cells = cells,
Global = global with { Turn = level.Global.Turn + Balancing.Current.TurnIncrement }
};
return updateForecasts ? next with { Forecasts = Forecast(next) } : next;
}
private void AddHazardForecasts(List<Forecast> forecasts, HashSet<ForecastKey> seen, LevelState level, int turns)
{
foreach (var hazard in m_Hazards)
{
foreach (var forecast in hazard.Predict(level, turns))
AddForecast(forecasts, seen, forecast);
}
}
private static void AddReactorReadyForecast(List<Forecast> forecasts, HashSet<ForecastKey> seen, LevelState level, int turns)
{
if (IsReactorReady(level))
AddForecast(forecasts, seen, new(EFailureKind.ReactorReady, null, turns, "REACTOR READY"));
}
private static void AddForecast(List<Forecast> forecasts, HashSet<ForecastKey> seen, Forecast forecast)
{
if (seen.Add(new(forecast.Kind, forecast.Position)))
forecasts.Add(forecast);
}
private static GlobalState UpdateGlobal(LevelState level, CellState[] cells)
{
var reactorHeat = cells.Where(c => c.Prop == ECellProp.Reactor).Select(c => c.Hazards.Heat).DefaultIfEmpty(level.Global.CoreHeat).Max();
var poweredGenerators = cells.Count(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
var poweredPumps = cells.Count(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
var damagedCriticalCells = cells.Count(c => c.Prop is ECellProp.Reactor or ECellProp.Generator or ECellProp.CoolingPump && c.Hazards.Stability <= Balancing.Current.CriticalCellStabilityThreshold);
var stability = Rules.Clamp(level.Global.FacilityStability - damagedCriticalCells);
var lost = reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold || stability <= Balancing.Current.StabilityCollapseThreshold;
var status = lost ? reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold ? "CORE MELTDOWN" : "FACILITY STABILITY COLLAPSE" : "STABILIZE SYSTEMS";
var global = level.Global with {
CoreHeat = Rules.Clamp(reactorHeat - poweredPumps),
Power = Rules.Clamp(poweredGenerators * Balancing.Current.GeneratorPowerOutput),
Cooling = Rules.Clamp(poweredPumps * Balancing.Current.CoolingPumpOutput),
FacilityStability = stability,
Lost = lost,
Status = status
};
return IsReactorReady(level with { Cells = cells, Global = global }) ? global with { Status = "REACTOR READY" } : global;
}
private static bool IsReactorReady(LevelState level)
{
var hasReactor = level.Cells.Any(c => c.Prop == ECellProp.Reactor);
var hasStablePower = level.Global.Power >= Balancing.Current.ReactorReadyPowerThreshold || level.Cells.Any(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
var hasCooling = level.Global.Cooling >= Balancing.Current.ReactorReadyCoolingThreshold || level.Cells.Any(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
var reactorStable = level.Global.CoreHeat < Balancing.Current.ReactorReadyCoreHeatThreshold;
return hasReactor && hasStablePower && hasCooling && reactorStable && !level.Global.Lost;
}
private readonly IReadOnlyList<IAreaSimulationEffect> m_AreaEffects = areaEffects.ToArray();
private readonly IReadOnlyList<ISimulationEffect> m_Effects = effects.ToArray();
private readonly IReadOnlyList<Hazard> m_Hazards = hazards.ToArray();
}

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

View File

@@ -0,0 +1,90 @@
<Window
x:Class="ReactorMaintenance.Win2D.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
Title="Reactor Maintenance">
<Grid Background="#16191D">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<CommandBar Grid.Row="0" DefaultLabelPosition="Right" Background="#20252A">
<AppBarButton Icon="Add" Label="New" Click="New_Click" />
<AppBarButton Icon="OpenFile" Label="Open" Click="Open_Click" />
<AppBarButton Icon="Save" Label="Save" Click="Save_Click" />
<AppBarSeparator />
<AppBarButton Icon="Play" Label="Simulate" Click="Simulate_Click" />
<AppBarButton Icon="Accept" Label="Activate" Click="Activate_Click" />
</CommandBar>
<Grid Grid.Row="1" ColumnSpacing="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="220" />
<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" />
<ComboBox x:Name="ToolPicker" SelectionChanged="ToolPicker_SelectionChanged" />
<TextBlock Text="Brush applies to the selected cell." Foreground="#9EA7AE" TextWrapping="Wrap" />
<TextBlock Text="Left click paints. Use Robot to set the start position." Foreground="#9EA7AE"
TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>
<Grid Grid.Column="1" Background="#101215">
<canvas:CanvasControl
x:Name="LevelCanvas"
ClearColor="#101215"
Draw="LevelCanvas_Draw"
PointerPressed="LevelCanvas_PointerPressed"
PointerMoved="LevelCanvas_PointerMoved"
PointerReleased="LevelCanvas_PointerReleased" />
</Grid>
<ScrollViewer Grid.Column="2" Background="#1C2126">
<StackPanel Padding="14" Spacing="12">
<TextBlock x:Name="LevelNameText" FontSize="20" FontWeight="SemiBold" Foreground="#F4F1E8"
TextWrapping="Wrap" />
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="Turn" Foreground="#9EA7AE" />
<TextBlock x:Name="TurnText" FontSize="22" Foreground="#F4F1E8" />
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Status" Foreground="#9EA7AE" />
<TextBlock x:Name="StatusText" FontSize="16" Foreground="#F4F1E8" TextWrapping="Wrap" />
</StackPanel>
</Grid>
<TextBlock Text="Global Systems" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<TextBlock x:Name="GlobalText" Foreground="#CCD4DA" TextWrapping="Wrap" />
<TextBlock Text="Selected Cell" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<TextBlock x:Name="CellText" Foreground="#CCD4DA" TextWrapping="Wrap" />
<TextBlock Text="Forecasts" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<ItemsControl x:Name="ForecastList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#46515A" BorderThickness="1" Padding="8" Margin="0,0,0,8"
CornerRadius="3">
<TextBlock Text="{Binding Message}" Foreground="#F4F1E8" TextWrapping="Wrap" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,499 @@
using System.Numerics;
using Windows.Foundation;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.UI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using ReactorMaintenance.Simulation;
using System.Globalization;
using Windows.UI.Popups;
using WinRT.Interop;
namespace ReactorMaintenance.Win2D;
public sealed partial class MainWindow
{
private sealed record CanvasLayout(double CellSize, double OriginX, double OriginY)
{
public Rect CellRect(int x, int y)
{
return new(OriginX + x * CellSize, OriginY + y * CellSize, CellSize, CellSize);
}
public Rect DualTileRect(int x, int y)
{
return new(OriginX + (x - 0.5) * CellSize, OriginY + (y - 0.5) * CellSize, CellSize, CellSize);
}
}
public MainWindow()
{
InitializeComponent();
m_Level = BuildStarterLevel();
ToolPicker.ItemsSource = Enum.GetValues<EEditorTool>();
ToolPicker.SelectedItem = m_SelectedTool;
RefreshInspector();
}
private void ToolPicker_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ToolPicker.SelectedItem is EEditorTool tool)
m_SelectedTool = tool;
}
private void New_Click(object sender, RoutedEventArgs e)
{
m_Level = BuildStarterLevel();
m_CurrentFile = null;
m_SelectedCell = null;
RefreshInspector();
LevelCanvas.Invalidate();
}
private async void Open_Click(object sender, RoutedEventArgs args)
{
try
{
var picker = new FileOpenPicker();
InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this));
picker.FileTypeFilter.Add(".json");
var file = await picker.PickSingleFileAsync();
if (file is null)
return;
var json = await FileIO.ReadTextAsync(file);
m_Level = LevelSerializer.Deserialize(json);
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
m_CurrentFile = file;
m_SelectedCell = null;
RefreshInspector();
LevelCanvas.Invalidate();
}
catch (Exception e)
{
var messageDialog = new MessageDialog(e.Message);
_ = await messageDialog.ShowAsync();
}
}
private async void Save_Click(object sender, RoutedEventArgs args)
{
try
{
var file = m_CurrentFile;
if (file is null)
{
var picker = new FileSavePicker();
InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this));
picker.SuggestedFileName = m_Level.Name.Replace(' ', '-').ToLowerInvariant();
picker.FileTypeChoices.Add("Reactor level", [".json"]);
file = await picker.PickSaveFileAsync();
}
if (file is null)
return;
await FileIO.WriteTextAsync(file, LevelSerializer.Serialize(m_Level));
m_CurrentFile = file;
}
catch (Exception e)
{
var messageDialog = new MessageDialog(e.Message);
_ = await messageDialog.ShowAsync();
}
}
private void Simulate_Click(object sender, RoutedEventArgs e)
{
m_Level = m_Simulation.AdvanceTurn(m_Level);
RefreshInspector();
LevelCanvas.Invalidate();
}
private void Activate_Click(object sender, RoutedEventArgs e)
{
m_Level = m_Simulation.ActivateReactor(m_Level);
RefreshInspector();
LevelCanvas.Invalidate();
}
private void LevelCanvas_PointerPressed(object sender, PointerRoutedEventArgs e)
{
m_Painting = true;
_ = LevelCanvas.CapturePointer(e.Pointer);
PaintAt(e.GetCurrentPoint(LevelCanvas).Position);
}
private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
{
if (m_Painting)
PaintAt(e.GetCurrentPoint(LevelCanvas).Position);
}
private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
{
m_Painting = false;
LevelCanvas.ReleasePointerCapture(e.Pointer);
}
private void PaintAt(Point point)
{
if (!TryGetGridPosition(point, out var position))
return;
m_SelectedCell = position;
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
RefreshInspector();
LevelCanvas.Invalidate();
}
private void LevelCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
var drawing = args.DrawingSession;
var layout = GetLayout();
drawing.Clear(ColorHelper.FromArgb(255, 16, 18, 21));
DrawTerrain(drawing, layout);
DrawCellOverlays(drawing, layout);
DrawGrid(drawing, layout);
DrawRobot(drawing, layout);
}
private void DrawTerrain(CanvasDrawingSession drawing, CanvasLayout layout)
{
for (var y = 0; y <= m_Level.Height; y++)
for (var x = 0; x <= m_Level.Width; x++)
DrawDualTerrainTile(drawing, layout.DualTileRect(x, y), GetDualTileMask(x, y));
}
private void DrawCellOverlays(CanvasDrawingSession drawing, CanvasLayout layout)
{
for (var y = 0; y < m_Level.Height; y++)
for (var x = 0; x < m_Level.Width; x++)
{
var position = new GridPosition(x, y);
var cell = m_Level.GetCell(position);
var rect = layout.CellRect(x, y);
if (cell.HasPipe)
{
var center = new Vector2((float)(rect.X + rect.Width / 2), (float)(rect.Y + rect.Height / 2));
var pipeColor = cell.Pipe switch {
EPipeMedium.Coolant => Colors.DeepSkyBlue,
EPipeMedium.Fuel => Colors.Goldenrod,
EPipeMedium.Pressure => Colors.LightSteelBlue,
_ => Colors.Transparent
};
drawing.DrawLine(center with { X = (float)rect.X + 6 }, center with { X = (float)(rect.X + rect.Width - 6) }, pipeColor, Math.Max(3, (float)rect.Width / 7));
drawing.DrawLine(center with { Y = (float)rect.Y + 6 }, center with { Y = (float)(rect.Y + rect.Height - 6) }, pipeColor, Math.Max(3, (float)rect.Width / 7));
}
if (cell.LeakRate > 0)
drawing.DrawCircle(new((float)(rect.X + rect.Width - 10), (float)(rect.Y + 10)), 5, Colors.OrangeRed, 2);
if (cell.Hazards.Fire)
drawing.FillCircle(new((float)(rect.X + rect.Width * 0.5), (float)(rect.Y + rect.Height * 0.5)), (float)rect.Width * 0.24f, Colors.OrangeRed);
if (m_SelectedCell == position)
drawing.DrawRectangle(rect, Colors.White, 3);
DrawCellProp(drawing, cell, rect);
}
}
private void DrawDualTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask)
{
var wallColor = ColorHelper.FromArgb(255, 54, 61, 68);
var floorColor = ColorHelper.FromArgb(255, 31, 36, 40);
var wallMask = c_AllCorners ^ floorMask;
drawing.FillRectangle(rect, floorColor);
DrawDualTerrainTile(drawing, rect, wallColor, floorColor, GetDualTerrainTileId(wallMask));
}
private static int GetDualTerrainTileId(int wallMask)
{
return wallMask;
}
private static void DrawDualTerrainTile(CanvasDrawingSession drawing, Rect rect, Color wallColor, Color floorColor, int tileId)
{
if (tileId == 0)
return;
if (tileId == c_AllCorners)
{
drawing.FillRectangle(rect, wallColor);
return;
}
switch (tileId)
{
case c_TopLeftCorner:
case c_TopRightCorner:
case c_BottomLeftCorner:
case c_BottomRightCorner:
DrawTerrainCorner(drawing, rect, wallColor, tileId);
break;
case c_TopLeftCorner | c_TopRightCorner:
drawing.FillRectangle(new(rect.X, rect.Y, rect.Width, rect.Height / 2), wallColor);
break;
case c_BottomLeftCorner | c_BottomRightCorner:
drawing.FillRectangle(new(rect.X, rect.Y + rect.Height / 2, rect.Width, rect.Height / 2), wallColor);
break;
case c_TopLeftCorner | c_BottomLeftCorner:
drawing.FillRectangle(new(rect.X, rect.Y, rect.Width / 2, rect.Height), wallColor);
break;
case c_TopRightCorner | c_BottomRightCorner:
drawing.FillRectangle(new(rect.X + rect.Width / 2, rect.Y, rect.Width / 2, rect.Height), wallColor);
break;
case c_TopLeftCorner | c_BottomRightCorner:
DrawTerrainCorner(drawing, rect, wallColor, c_TopLeftCorner);
DrawTerrainCorner(drawing, rect, wallColor, c_BottomRightCorner);
break;
case c_TopRightCorner | c_BottomLeftCorner:
DrawTerrainCorner(drawing, rect, wallColor, c_TopRightCorner);
DrawTerrainCorner(drawing, rect, wallColor, c_BottomLeftCorner);
break;
default:
drawing.FillRectangle(rect, wallColor);
DrawTerrainCorner(drawing, rect, floorColor, c_AllCorners ^ tileId);
break;
}
}
private static void DrawTerrainCorner(CanvasDrawingSession drawing, Rect rect, Color color, int corner)
{
var radiusX = (float)rect.Width / 2;
var radiusY = (float)rect.Height / 2;
var center = corner switch {
c_TopLeftCorner => new Vector2((float)rect.X, (float)rect.Y),
c_TopRightCorner => new Vector2((float)(rect.X + rect.Width), (float)rect.Y),
c_BottomRightCorner => new Vector2((float)(rect.X + rect.Width), (float)(rect.Y + rect.Height)),
c_BottomLeftCorner => new Vector2((float)rect.X, (float)(rect.Y + rect.Height)),
_ => new Vector2((float)(rect.X + rect.Width / 2), (float)(rect.Y + rect.Height / 2))
};
var start = corner switch {
c_TopLeftCorner => new Vector2(center.X + radiusX, center.Y),
c_TopRightCorner => new Vector2(center.X, center.Y + radiusY),
c_BottomRightCorner => new Vector2(center.X - radiusX, center.Y),
c_BottomLeftCorner => new Vector2(center.X, center.Y - radiusY),
_ => center
};
var end = corner switch {
c_TopLeftCorner => new Vector2(center.X, center.Y + radiusY),
c_TopRightCorner => new Vector2(center.X - radiusX, center.Y),
c_BottomRightCorner => new Vector2(center.X, center.Y - radiusY),
c_BottomLeftCorner => new Vector2(center.X + radiusX, center.Y),
_ => center
};
using var builder = new CanvasPathBuilder(drawing);
builder.BeginFigure(center);
builder.AddLine(start);
builder.AddArc(end, radiusX, radiusY, 0, CanvasSweepDirection.Clockwise, CanvasArcSize.Small);
builder.EndFigure(CanvasFigureLoop.Closed);
using var geometry = CanvasGeometry.CreatePath(builder);
drawing.FillGeometry(geometry, color);
}
private int GetDualTileMask(int x, int y)
{
var mask = 0;
if (GetTerrainOrWall(x - 1, y - 1) == ECellTerrain.Floor)
mask |= c_TopLeftCorner;
if (GetTerrainOrWall(x, y - 1) == ECellTerrain.Floor)
mask |= c_TopRightCorner;
if (GetTerrainOrWall(x - 1, y) == ECellTerrain.Floor)
mask |= c_BottomLeftCorner;
if (GetTerrainOrWall(x, y) == ECellTerrain.Floor)
mask |= c_BottomRightCorner;
return mask;
}
private ECellTerrain GetTerrainOrWall(int x, int y)
{
var position = new GridPosition(x, y);
return m_Level.InBounds(position) ? m_Level.GetCell(position).Terrain : ECellTerrain.Wall;
}
private void DrawCellProp(CanvasDrawingSession drawing, CellState cell, Rect rect)
{
var text = cell.Prop switch {
ECellProp.Reactor => "R",
ECellProp.CoolingPump => "C",
ECellProp.Generator => "G",
ECellProp.PressureRegulator => "P",
ECellProp.DiagnosticTerminal => "D",
ECellProp.ControlTerminal => "T",
_ => string.Empty
};
if (string.IsNullOrEmpty(text))
return;
var propRect = new Rect(rect.X + rect.Width * 0.18, rect.Y + rect.Height * 0.18, rect.Width * 0.64, rect.Height * 0.64);
drawing.FillRoundedRectangle(propRect, 4, 4, PropColor(cell.Prop));
drawing.DrawRoundedRectangle(propRect, 4, 4, ColorHelper.FromArgb(210, 12, 14, 16), 2);
using var format = new CanvasTextFormat();
format.FontSize = Math.Max(14, (float)rect.Width * 0.34f);
format.HorizontalAlignment = CanvasHorizontalAlignment.Center;
format.VerticalAlignment = CanvasVerticalAlignment.Center;
drawing.DrawText(text, propRect, Colors.White, format);
}
private void DrawGrid(CanvasDrawingSession drawing, CanvasLayout layout)
{
for (var x = 0; x <= m_Level.Width; x++)
{
var xPos = (float)(layout.OriginX + x * layout.CellSize);
drawing.DrawLine(xPos, (float)layout.OriginY, xPos, (float)(layout.OriginY + m_Level.Height * layout.CellSize), ColorHelper.FromArgb(120, 91, 104, 115), 1);
}
for (var y = 0; y <= m_Level.Height; y++)
{
var yPos = (float)(layout.OriginY + y * layout.CellSize);
drawing.DrawLine((float)layout.OriginX, yPos, (float)(layout.OriginX + m_Level.Width * layout.CellSize), yPos, ColorHelper.FromArgb(120, 91, 104, 115), 1);
}
}
private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout)
{
var rect = layout.CellRect(m_Level.Robot.X, m_Level.Robot.Y);
var center = new Vector2((float)(rect.X + rect.Width / 2), (float)(rect.Y + rect.Height / 2));
drawing.FillCircle(center, (float)rect.Width * 0.28f, Colors.White);
drawing.DrawCircle(center, (float)rect.Width * 0.28f, Colors.Black, 2);
}
private bool TryGetGridPosition(Point point, out GridPosition position)
{
var layout = GetLayout();
var x = (int)((point.X - layout.OriginX) / layout.CellSize);
var y = (int)((point.Y - layout.OriginY) / layout.CellSize);
position = new(x, y);
return m_Level.InBounds(position);
}
private CanvasLayout GetLayout()
{
var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
var size = Math.Floor(Math.Min(availableWidth / m_Level.Width, availableHeight / m_Level.Height));
size = Math.Max(20, size);
var originX = Math.Max(0, (availableWidth - size * m_Level.Width) / 2);
var originY = Math.Max(0, (availableHeight - size * m_Level.Height) / 2);
return new(size, originX, originY);
}
private static Color PropColor(ECellProp prop)
{
return prop switch {
ECellProp.Reactor => ColorHelper.FromArgb(255, 61, 76, 82),
ECellProp.CoolingPump => ColorHelper.FromArgb(255, 25, 79, 96),
ECellProp.Generator => ColorHelper.FromArgb(255, 86, 75, 35),
ECellProp.PressureRegulator => ColorHelper.FromArgb(255, 70, 78, 98),
ECellProp.DiagnosticTerminal => ColorHelper.FromArgb(255, 39, 84, 62),
ECellProp.ControlTerminal => ColorHelper.FromArgb(255, 80, 61, 91),
_ => Colors.Transparent
};
}
private void RefreshInspector()
{
LevelNameText.Text = m_Level.Name;
TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture);
StatusText.Text = m_Level.Global.Status;
GlobalText.Text = $"Power: {m_Level.Global.Power}/10\n" + $"Cooling: {m_Level.Global.Cooling}/10\n" + $"Core Heat: {m_Level.Global.CoreHeat}/10\n" + $"Facility Stability: {m_Level.Global.FacilityStability}/10";
if (m_SelectedCell is { } position && m_Level.InBounds(position))
{
var cell = m_Level.GetCell(position);
CellText.Text = $"Position: {position.X},{position.Y}\n" + $"Terrain: {cell.Terrain}\n" + $"Prop: {cell.Prop}\n" + $"Pipe: {cell.Pipe}\n" + $"Flow: {cell.Flow}, Pressure: {cell.Pressure}\n" + $"Integrity: {cell.Integrity}, Leak: {cell.LeakRate}\n" + $"Heat: {cell.Hazards.Heat}, Smoke: {cell.Hazards.Smoke}\n" + $"Fuel Vapor: {cell.Hazards.FuelVapor}, Fuel: {cell.Hazards.LiquidFuel}\n" + $"Coolant: {cell.Hazards.CoolantPooling}, Charge: {cell.Hazards.ElectricalCharge}";
}
else
CellText.Text = "No cell selected.";
ForecastList.ItemsSource = m_Level.Forecasts;
}
private static LevelState BuildStarterLevel()
{
var level = LevelState.Create("Cooling Sector B", 16, 12);
level = level.SetCell(new(3, 5), new() {
Prop = ECellProp.CoolingPump,
Pipe = EPipeMedium.Coolant,
Flow = 5,
Pressure = 5,
Powered = true
});
level = level.SetCell(new(4, 5), new() {
Pipe = EPipeMedium.Coolant,
Flow = 5,
Pressure = 7
});
level = level.SetCell(new(5, 5), new() {
Pipe = EPipeMedium.Coolant,
Flow = 3,
Pressure = 8,
LeakRate = 2,
Integrity = 4
});
level = level.SetCell(new(6, 5), new() {
Pipe = EPipeMedium.Coolant,
Flow = 3,
Pressure = 7
});
level = level.SetCell(new(8, 5), new() {
Prop = ECellProp.Reactor,
Hazards = new() {
Heat = 6,
Stability = 8
}
});
level = level.SetCell(new(2, 8), new() {
Prop = ECellProp.Generator,
Pipe = EPipeMedium.Fuel,
Flow = 4,
Pressure = 6,
Powered = true
});
level = level.SetCell(new(11, 4), new() {
Prop = ECellProp.DiagnosticTerminal,
Powered = true
});
level = level.SetCell(new(12, 8), new() {
Prop = ECellProp.ControlTerminal,
Powered = true
});
return level with { Forecasts = new SimulationEngine().Forecast(level) };
}
private readonly SimulationEngine m_Simulation = new();
private const int c_TopLeftCorner = 1;
private const int c_TopRightCorner = 2;
private const int c_BottomLeftCorner = 4;
private const int c_BottomRightCorner = 8;
private const int c_AllCorners = c_TopLeftCorner | c_TopRightCorner | c_BottomLeftCorner | c_BottomRightCorner;
private StorageFile? m_CurrentFile;
private LevelState m_Level;
private bool m_Painting;
private GridPosition? m_SelectedCell;
private EEditorTool m_SelectedTool = EEditorTool.Floor;
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>ReactorMaintenance.Win2D</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;arm64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.4.0" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ReactorMaintenance.Simulation\ReactorMaintenance.Simulation.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="ReactorMaintenance.Win2D.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\ReactorMaintenance.Simulation\ReactorMaintenance.Simulation.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,298 @@
using ReactorMaintenance.Simulation.Difficulties;
using ReactorMaintenance.Simulation.Effects;
using ReactorMaintenance.Simulation.Hazards;
namespace ReactorMaintenance.Simulation.Tests;
public sealed class SimulationEngineTests
{
[Fact]
public void FuelLeakNearPoweredGeneratorCreatesIgnitionForecast()
{
var level = LevelState.Create("Fuel leak", 6, 6)
.SetCell(new(2, 2), new() {
Prop = ECellProp.Generator,
Pipe = EPipeMedium.Fuel,
LeakRate = Balancing.Current.FuelVaporFireThreshold,
Pressure = Balancing.Current.PressurizedFuelLeakPressureThreshold + Balancing.Current.NeighborDistance,
Integrity = Balancing.Current.DefaultEditedPipeIntegrity,
Powered = true
});
var forecasts = m_Engine.Forecast(level);
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Ignition && forecast.Position == new GridPosition(2, 2) && forecast.Turns == 1);
}
[Fact]
public void CoolantLeakOnPoweredCellRaisesElectricalCharge()
{
var level = LevelState.Create("Wet cable", 6, 6)
.SetCell(new(3, 3), new() {
Pipe = EPipeMedium.Coolant,
LeakRate = Balancing.Current.ElectrifiedCoolantPoolingThreshold,
Powered = true
});
var next = m_Engine.AdvanceTurn(level);
Assert.True(next.GetCell(new(3, 3)).Hazards.ElectricalCharge >= Balancing.Current.ElectricalChargeIncrease);
}
[Fact]
public void ActiveFireSpreadsSmokeToOpenNeighbors()
{
var level = LevelState.Create("Smoke", 6, 6)
.SetCell(new(2, 2), new() {
Hazards = new() {
Fire = true,
Smoke = Balancing.Current.SmokeSpreadThreshold
}
});
var next = m_Engine.AdvanceTurn(level);
Assert.True(next.GetCell(new(2, 3)).Hazards.Smoke > 0);
}
[Fact]
public void AdvanceTurnRunsConfiguredCellEffects()
{
var engine = new SimulationEngine([new TestCellEffect()], [], []);
var level = LevelState.Create("Custom effect", 6, 6)
.SetCell(new(2, 2), new() {
Hazards = new() { Heat = 1 }
});
var next = engine.AdvanceTurn(level);
Assert.Equal(5, next.GetCell(new(2, 2)).Hazards.Heat);
}
[Fact]
public void AdvanceTurnRunsConfiguredAreaEffects()
{
var engine = new SimulationEngine([], [new TestAreaEffect()], []);
var level = LevelState.Create("Custom area effect", 6, 6);
var next = engine.AdvanceTurn(level);
Assert.Equal(7, next.GetCell(new(2, 2)).Hazards.Smoke);
}
[Fact]
public void OverpressurePredictsPipeBurst()
{
var level = LevelState.Create("Pressure", 6, 6)
.SetCell(new(1, 2), new() {
Pipe = EPipeMedium.Pressure,
Pressure = 10,
Integrity = 6
});
var forecasts = m_Engine.Forecast(level);
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.PipeBurst && forecast.Turns == 2);
}
[Fact]
public void ForecastCapsFutureSimulationWhenNoTerminalConditionOccurs()
{
var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
var level = LevelState.Create("Stable", 6, 6);
var forecasts = engine.Forecast(level);
Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Max(forecast => forecast.Turns));
}
[Fact]
public void ForecastUsesCurrentBalancingProfile()
{
var previous = Balancing.Current;
try
{
Balancing.Current = new TestBalancing();
var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
var level = LevelState.Create("Stable", 6, 6);
var forecasts = engine.Forecast(level);
Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
}
finally
{
Balancing.Current = previous;
}
}
[Fact]
public void ForecastPredictsMeltdownFromFutureSimulation()
{
var level = LevelState.Create("Meltdown", 6, 6)
.SetCell(new(2, 2), new() {
Prop = ECellProp.Reactor,
Hazards = new() { Heat = Balancing.Current.MeltdownCoreHeatThreshold - Balancing.Current.ReactorHeatIncrease }
});
var forecasts = m_Engine.Forecast(level);
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 1);
}
[Fact]
public void ForecastReportsAlreadyLostLevelAtCurrentTurn()
{
var level = LevelState.Create("Lost", 6, 6) with {
Global = new() {
CoreHeat = Balancing.Current.MeltdownCoreHeatThreshold,
Lost = true,
Status = "CORE MELTDOWN"
}
};
var forecasts = m_Engine.Forecast(level);
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 0);
}
[Fact]
public void ForecastPredictsStabilityCollapseFromFutureSimulation()
{
var level = LevelState.Create("Collapse", 6, 6)
.SetCell(new(2, 2), new() {
Prop = ECellProp.Generator,
Hazards = new() { Stability = Balancing.Current.CriticalCellStabilityThreshold }
}) with {
Global = new() { FacilityStability = Balancing.Current.FireStabilityDamage }
};
var forecasts = m_Engine.Forecast(level);
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.StabilityCollapse && forecast.Turns == 1);
}
[Fact]
public void StableReactorWithPowerAndCoolingCanActivate()
{
var level = LevelState.Create("Ready", 8, 6)
.SetCell(new(2, 2), new() {
Prop = ECellProp.Reactor,
Hazards = new() { Heat = 3 }
})
.SetCell(new(3, 2), new() {
Prop = ECellProp.Generator,
Powered = true
})
.SetCell(new(4, 2), new() {
Prop = ECellProp.CoolingPump,
Powered = true
});
var next = m_Engine.AdvanceTurn(level);
var activated = m_Engine.ActivateReactor(next);
Assert.Equal("REACTOR ONLINE", activated.Global.Status);
Assert.True(activated.Global.ReactorActivated);
}
[Fact]
public void LevelSerializationRoundTripsEditableState()
{
var level = LevelState.Create("Round trip", 5, 5);
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Reactor);
level = LevelEditor.Apply(level, new(1, 2), EEditorTool.CoolantPipe);
level = LevelEditor.Apply(level, new(1, 2), EEditorTool.Leak);
var json = LevelSerializer.Serialize(level);
var loaded = LevelSerializer.Deserialize(json);
Assert.Contains("\"Version\": 1", json);
Assert.Equal(level.Name, loaded.Name);
Assert.Equal(ECellProp.Reactor, loaded.GetCell(new(2, 2)).Prop);
Assert.Equal(EPipeMedium.Coolant, loaded.GetCell(new(1, 2)).Pipe);
Assert.Equal(1, loaded.GetCell(new(1, 2)).LeakRate);
}
[Fact]
public void LevelSerializationRejectsUnsupportedVersion()
{
var json = """
{
"Version": 999,
"Level": {}
}
""";
var exception = Assert.Throws<InvalidOperationException>(() => LevelSerializer.Deserialize(json));
Assert.Contains("Unsupported level file version 999", exception.Message);
}
[Fact]
public void WallToolClearsCellPropsPipesAndHazards()
{
var level = LevelState.Create("Wall", 5, 5);
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Generator);
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.CoolantPipe);
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Fire);
var edited = LevelEditor.Apply(level, new(2, 2), EEditorTool.Wall);
var cell = edited.GetCell(new(2, 2));
Assert.Equal(ECellTerrain.Wall, cell.Terrain);
Assert.Equal(ECellProp.None, cell.Prop);
Assert.Equal(EPipeMedium.None, cell.Pipe);
Assert.False(cell.Powered);
Assert.False(cell.Hazards.Fire);
}
[Fact]
public void PropToolsKeepFloorTerrain()
{
var level = LevelState.Create("Prop", 5, 5);
level = LevelEditor.Apply(level, new(1, 1), EEditorTool.Wall);
var edited = LevelEditor.Apply(level, new(1, 1), EEditorTool.Reactor);
var cell = edited.GetCell(new(1, 1));
Assert.Equal(ECellTerrain.Floor, cell.Terrain);
Assert.Equal(ECellProp.Reactor, cell.Prop);
}
private readonly SimulationEngine m_Engine = new();
private sealed class StepCountingHazard : Hazard
{
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
{
yield return new(EFailureKind.PipeBurst, new(turns, 0), turns, $"STEP {turns}");
}
}
private sealed class TestBalancing : NormalBalancing
{
public override int MaxForecastStepCount => 2;
}
private sealed class TestCellEffect : ISimulationEffect
{
public CellState Apply(CellState cell)
{
return cell with { Hazards = cell.Hazards with { Heat = 5 } };
}
}
private sealed class TestAreaEffect : IAreaSimulationEffect
{
public CellState[] Apply(LevelState level, CellState[] cells)
{
var next = cells.ToArray();
var position = new GridPosition(2, 2);
var cell = next[level.Index(position)];
next[level.Index(position)] = cell with { Hazards = cell.Hazards with { Smoke = 7 } };
return next;
}
}
}