diff --git a/config.json b/config.json index befba58ab..c5f9cbe0b 100644 --- a/config.json +++ b/config.json @@ -2662,6 +2662,19 @@ "while-loops" ], "difficulty": 2 + }, + { + "slug": "killer-sudoku-helper", + "name": "Killer Sudoku Helper", + "uuid": "ef287b7e-50ca-4072-93e8-84efbbe77079", + "practices": [ + "arrays" + ], + "prerequisites": [ + "arrays", + "numbers" + ], + "difficulty": 7 } ], "foregone": [ diff --git a/exercises/Exercises.sln b/exercises/Exercises.sln index e74df7b04..4e5afa180 100644 --- a/exercises/Exercises.sln +++ b/exercises/Exercises.sln @@ -347,6 +347,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EliudsEggs", "practice\eliu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Knapsack", "practice\knapsack\Knapsack.csproj", "{90C70CAA-A225-4D66-8B42-6AC82AD1D5DC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KillerSudokuHelper", "practice\killer-sudoku-helper\KillerSudokuHelper.csproj", "{BEBBD420-075D-46F1-AE51-CC9A05FECE4A}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SquareRoot", "practice\square-root\SquareRoot.csproj", "{5C05051E-D46C-4544-9CF8-F2A748F63172}" EndProject Global @@ -1039,6 +1041,10 @@ Global {5C05051E-D46C-4544-9CF8-F2A748F63172}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C05051E-D46C-4544-9CF8-F2A748F63172}.Release|Any CPU.ActiveCfg = Release|Any CPU {5C05051E-D46C-4544-9CF8-F2A748F63172}.Release|Any CPU.Build.0 = Release|Any CPU + {BEBBD420-075D-46F1-AE51-CC9A05FECE4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BEBBD420-075D-46F1-AE51-CC9A05FECE4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEBBD420-075D-46F1-AE51-CC9A05FECE4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BEBBD420-075D-46F1-AE51-CC9A05FECE4A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1215,6 +1221,7 @@ Global {2F3B7384-F4CA-4925-B07C-E05DB3FAC01B} = {E276EF69-669A-43E0-88AC-8ABB17A9C026} {90C70CAA-A225-4D66-8B42-6AC82AD1D5DC} = {E276EF69-669A-43E0-88AC-8ABB17A9C026} {5C05051E-D46C-4544-9CF8-F2A748F63172} = {E276EF69-669A-43E0-88AC-8ABB17A9C026} + {BEBBD420-075D-46F1-AE51-CC9A05FECE4A} = {E276EF69-669A-43E0-88AC-8ABB17A9C026} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB4EA6C9-5461-4024-BDC7-2AE0C3A85CD1} diff --git a/exercises/practice/killer-sudoku-helper/.docs/instructions.md b/exercises/practice/killer-sudoku-helper/.docs/instructions.md new file mode 100644 index 000000000..fdafdca8f --- /dev/null +++ b/exercises/practice/killer-sudoku-helper/.docs/instructions.md @@ -0,0 +1,85 @@ +# Instructions + +A friend of yours is learning how to solve Killer Sudokus (rules below) but struggling to figure out which digits can go in a cage. +They ask you to help them out by writing a small program that lists all valid combinations for a given cage, and any constraints that affect the cage. + +To make the output of your program easy to read, the combinations it returns must be sorted. + +## Killer Sudoku Rules + +- [Standard Sudoku rules][sudoku-rules] apply. +- The digits in a cage, usually marked by a dotted line, add up to the small number given in the corner of the cage. +- A digit may only occur once in a cage. + +For a more detailed explanation, check out [this guide][killer-guide]. + +## Example 1: Cage with only 1 possible combination + +In a 3-digit cage with a sum of 7, there is only one valid combination: 124. + +- 1 + 2 + 4 = 7 +- Any other combination that adds up to 7, e.g. 232, would violate the rule of not repeating digits within a cage. + +![Sudoku grid, with three killer cages that are marked as grouped together. +The first killer cage is in the 3×3 box in the top left corner of the grid. +The middle column of that box forms the cage, with the followings cells from top to bottom: first cell contains a 1 and a pencil mark of 7, indicating a cage sum of 7, second cell contains a 2, third cell contains a 5. +The numbers are highlighted in red to indicate a mistake. +The second killer cage is in the central 3×3 box of the grid. +The middle column of that box forms the cage, with the followings cells from top to bottom: first cell contains a 1 and a pencil mark of 7, indicating a cage sum of 7, second cell contains a 2, third cell contains a 4. +None of the numbers in this cage are highlighted and therefore don't contain any mistakes. +The third killer cage follows the outside corner of the central 3×3 box of the grid. +It is made up of the following three cells: the top left cell of the cage contains a 2, highlighted in red, and a cage sum of 7. +The top right cell of the cage contains a 3. +The bottom right cell of the cage contains a 2, highlighted in red. All other cells are empty.][one-solution-img] + +## Example 2: Cage with several combinations + +In a 2-digit cage with a sum 10, there are 4 possible combinations: + +- 19 +- 28 +- 37 +- 46 + +![Sudoku grid, all squares empty except for the middle column, column 5, which has 8 rows filled. +Each continguous two rows form a killer cage and are marked as grouped together. +From top to bottom: first group is a cell with value 1 and a pencil mark indicating a cage sum of 10, cell with value 9. +Second group is a cell with value 2 and a pencil mark of 10, cell with value 8. +Third group is a cell with value 3 and a pencil mark of 10, cell with value 7. +Fourth group is a cell with value 4 and a pencil mark of 10, cell with value 6. +The last cell in the column is empty.][four-solutions-img] + +## Example 3: Cage with several combinations that is restricted + +In a 2-digit cage with a sum 10, where the column already contains a 1 and a 4, there are 2 possible combinations: + +- 28 +- 37 + +19 and 46 are not possible due to the 1 and 4 in the column according to standard Sudoku rules. + +![Sudoku grid, all squares empty except for the middle column, column 5, which has 8 rows filled. +The first row contains a 4, the second is empty, and the third contains a 1. +The 1 is highlighted in red to indicate a mistake. +The last 6 rows in the column form killer cages of two cells each. +From top to bottom: first group is a cell with value 2 and a pencil mark indicating a cage sum of 10, cell with value 8. +Second group is a cell with value 3 and a pencil mark of 10, cell with value 7. +Third group is a cell with value 1, highlighted in red, and a pencil mark of 10, cell with value 9.][not-possible-img] + +## Trying it yourself + +If you want to give an approachable Killer Sudoku a go, you can try out [this puzzle][clover-puzzle] by Clover, featured by [Mark Goodliffe on Cracking The Cryptic on the 21st of June 2021][goodliffe-video]. + +You can also find Killer Sudokus in varying difficulty in numerous newspapers, as well as Sudoku apps, books and websites. + +## Credit + +The screenshots above have been generated using [F-Puzzles.com](https://www.f-puzzles.com/), a Puzzle Setting Tool by Eric Fox. + +[sudoku-rules]: https://masteringsudoku.com/sudoku-rules-beginners/ +[killer-guide]: https://masteringsudoku.com/killer-sudoku/ +[one-solution-img]: https://assets.exercism.org/images/exercises/killer-sudoku-helper/example1.png +[four-solutions-img]: https://assets.exercism.org/images/exercises/killer-sudoku-helper/example2.png +[not-possible-img]: https://assets.exercism.org/images/exercises/killer-sudoku-helper/example3.png +[clover-puzzle]: https://app.crackingthecryptic.com/sudoku/HqTBn3Pr6R +[goodliffe-video]: https://youtu.be/c_NjEbFEeW0?t=1180 diff --git a/exercises/practice/killer-sudoku-helper/.editorconfig b/exercises/practice/killer-sudoku-helper/.editorconfig new file mode 100644 index 000000000..9c81abe0a --- /dev/null +++ b/exercises/practice/killer-sudoku-helper/.editorconfig @@ -0,0 +1,141 @@ +############################### +# Core EditorConfig Options # +############################### + +; This file is for unifying the coding style for different editors and IDEs. +; More information at: +; https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference?view=vs-2017 +; https://docs.microsoft.com/en-us/visualstudio/ide/create-portable-custom-editor-options?view=vs-2017 + +root = true + +[*] +indent_style = space + +[KillerSudokuHelper.cs] +indent_size = 4 + +############################### +# .NET Coding Conventions # +############################### + +# Organize usings +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = true + +# this. preferences +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 + +# 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 = never_if_unnecessary:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = always:suggestion +dotnet_style_readonly_field = true:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +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_auto_properties = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion + +############################### +# Naming Conventions # +############################### + +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const + +############################### +# C# Code Style Rules # +############################### + +# var preferences +csharp_style_var_for_built_in_types = true:none +csharp_style_var_when_type_is_apparent = true:none +csharp_style_var_elsewhere = true:none + +# Expression-bodied members +csharp_style_expression_bodied_methods = true:suggestion +csharp_style_expression_bodied_constructors = true:suggestion +csharp_style_expression_bodied_operators = true:suggestion +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_accessors = true:suggestion + +# Pattern-matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion + +# Expression-level preferences +csharp_prefer_braces = true:none +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +############################### +# C# Formatting Rules # +############################### + +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_members_in_anonymous_types = false +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true diff --git a/exercises/practice/killer-sudoku-helper/.meta/Example.cs b/exercises/practice/killer-sudoku-helper/.meta/Example.cs new file mode 100644 index 000000000..9794f0743 --- /dev/null +++ b/exercises/practice/killer-sudoku-helper/.meta/Example.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; + +public static class KillerSudokuHelper +{ + private static readonly int[] Digits = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + + public static IEnumerable Combinations(int sum, int size, int[] exclude) => + Digits.Except(exclude).ToArray() + .Combinations(size) + .Where(comb => comb.Sum() == sum); + + private static IEnumerable Combinations(this T[] items, int size) + { + if (size > items.Length) yield break; + + var pool = items.ToArray(); + var indices = Enumerable.Range(0, size).ToArray(); + + yield return indices.Select(x => pool[x]).ToArray(); + + while (true) + { + var i = indices.Length - 1; + while (i >= 0 && indices[i] == i + items.Length - size) + i -= 1; + + if (i < 0) yield break; + + indices[i] += 1; + + for (var j = i + 1; j < size; j += 1) + indices[j] = indices[j - 1] + 1; + + yield return indices.Select(x => pool[x]).ToArray(); + } + } +} \ No newline at end of file diff --git a/exercises/practice/killer-sudoku-helper/.meta/Generator.tpl b/exercises/practice/killer-sudoku-helper/.meta/Generator.tpl new file mode 100644 index 000000000..527e975c8 --- /dev/null +++ b/exercises/practice/killer-sudoku-helper/.meta/Generator.tpl @@ -0,0 +1,21 @@ +using Xunit; + +public class {{ testClass }} +{ + {{- for test in tests }} + [Fact{{ if !for.first }}(Skip = "Remove this Skip property to run this test"){{ end }}] + public void {{ test.testMethod }}() + { + {{- if test.expected | array.size > 1 }} + int[][] expected = [ + {{- for row in test.expected }} + {{ row }}{{- if !for.last }},{{ end -}} + {{ end }} + ]; + {{- else }} + int[][] expected = {{ test.expected }}; + {{ end -}} + Assert.Equal(expected, {{ testedClass }}.{{ test.testedMethod }}({{ test.input.cage.sum }}, {{ test.input.cage.size }}, {{ test.input.cage.exclude }})); + } + {{ end -}} +} diff --git a/exercises/practice/killer-sudoku-helper/.meta/config.json b/exercises/practice/killer-sudoku-helper/.meta/config.json new file mode 100644 index 000000000..00eb908f3 --- /dev/null +++ b/exercises/practice/killer-sudoku-helper/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "erikschierboom" + ], + "files": { + "solution": [ + "KillerSudokuHelper.cs" + ], + "test": [ + "KillerSudokuHelperTests.cs" + ], + "example": [ + ".meta/Example.cs" + ] + }, + "blurb": "Write a tool that makes it easier to solve Killer Sudokus", + "source": "Created by Sascha Mann, Jeremy Walker, and BethanyG for the Julia track on Exercism.", + "source_url": "https://github.com/exercism/julia/pull/413" +} diff --git a/exercises/practice/killer-sudoku-helper/.meta/tests.toml b/exercises/practice/killer-sudoku-helper/.meta/tests.toml new file mode 100644 index 000000000..19c23e8a9 --- /dev/null +++ b/exercises/practice/killer-sudoku-helper/.meta/tests.toml @@ -0,0 +1,49 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[2aaa8f13-11b5-4054-b95c-a906e4d79fb6] +description = "Trivial 1-digit cages -> 1" + +[4645da19-9fdd-4087-a910-a6ed66823563] +description = "Trivial 1-digit cages -> 2" + +[07cfc704-f8aa-41b2-8f9a-cbefb674cb48] +description = "Trivial 1-digit cages -> 3" + +[22b8b2ba-c4fd-40b3-b1bf-40aa5e7b5f24] +description = "Trivial 1-digit cages -> 4" + +[b75d16e2-ff9b-464d-8578-71f73094cea7] +description = "Trivial 1-digit cages -> 5" + +[bcbf5afc-4c89-4ff6-9357-07ab4d42788f] +description = "Trivial 1-digit cages -> 6" + +[511b3bf8-186f-4e35-844f-c804d86f4a7a] +description = "Trivial 1-digit cages -> 7" + +[bd09a60d-3aca-43bd-b6aa-6ccad01bedda] +description = "Trivial 1-digit cages -> 8" + +[9b539f27-44ea-4ff8-bd3d-c7e136bee677] +description = "Trivial 1-digit cages -> 9" + +[0a8b2078-b3a4-4dbd-be0d-b180f503d5c3] +description = "Cage with sum 45 contains all digits 1:9" + +[2635d7c9-c716-4da1-84f1-c96e03900142] +description = "Cage with only 1 possible combination" + +[a5bde743-e3a2-4a0c-8aac-e64fceea4228] +description = "Cage with several combinations" + +[dfbf411c-737d-465a-a873-ca556360c274] +description = "Cage with several combinations that is restricted" diff --git a/exercises/practice/killer-sudoku-helper/KillerSudokuHelper.cs b/exercises/practice/killer-sudoku-helper/KillerSudokuHelper.cs new file mode 100644 index 000000000..1413d51ed --- /dev/null +++ b/exercises/practice/killer-sudoku-helper/KillerSudokuHelper.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +public static class KillerSudokuHelper +{ + public static IEnumerable Combinations(int sum, int size, int[] exclude) + { + throw new NotImplementedException("You need to implement this method."); + } +} diff --git a/exercises/practice/killer-sudoku-helper/KillerSudokuHelper.csproj b/exercises/practice/killer-sudoku-helper/KillerSudokuHelper.csproj new file mode 100644 index 000000000..b5b065d5c --- /dev/null +++ b/exercises/practice/killer-sudoku-helper/KillerSudokuHelper.csproj @@ -0,0 +1,14 @@ + + + + net9.0 + + + + + + + + + + diff --git a/exercises/practice/killer-sudoku-helper/KillerSudokuHelperTests.cs b/exercises/practice/killer-sudoku-helper/KillerSudokuHelperTests.cs new file mode 100644 index 000000000..5df917d7c --- /dev/null +++ b/exercises/practice/killer-sudoku-helper/KillerSudokuHelperTests.cs @@ -0,0 +1,101 @@ +using Xunit; + +public class KillerSudokuHelperTests +{ + [Fact] + public void Trivial_1_digit_cages_1() + { + int[][] expected = [[1]]; + Assert.Equal(expected, KillerSudokuHelper.Combinations(1, 1, [])); + } + + [Fact(Skip = "Remove this Skip property to run this test")] + public void Trivial_1_digit_cages_2() + { + int[][] expected = [[2]]; + Assert.Equal(expected, KillerSudokuHelper.Combinations(2, 1, [])); + } + + [Fact(Skip = "Remove this Skip property to run this test")] + public void Trivial_1_digit_cages_3() + { + int[][] expected = [[3]]; + Assert.Equal(expected, KillerSudokuHelper.Combinations(3, 1, [])); + } + + [Fact(Skip = "Remove this Skip property to run this test")] + public void Trivial_1_digit_cages_4() + { + int[][] expected = [[4]]; + Assert.Equal(expected, KillerSudokuHelper.Combinations(4, 1, [])); + } + + [Fact(Skip = "Remove this Skip property to run this test")] + public void Trivial_1_digit_cages_5() + { + int[][] expected = [[5]]; + Assert.Equal(expected, KillerSudokuHelper.Combinations(5, 1, [])); + } + + [Fact(Skip = "Remove this Skip property to run this test")] + public void Trivial_1_digit_cages_6() + { + int[][] expected = [[6]]; + Assert.Equal(expected, KillerSudokuHelper.Combinations(6, 1, [])); + } + + [Fact(Skip = "Remove this Skip property to run this test")] + public void Trivial_1_digit_cages_7() + { + int[][] expected = [[7]]; + Assert.Equal(expected, KillerSudokuHelper.Combinations(7, 1, [])); + } + + [Fact(Skip = "Remove this Skip property to run this test")] + public void Trivial_1_digit_cages_8() + { + int[][] expected = [[8]]; + Assert.Equal(expected, KillerSudokuHelper.Combinations(8, 1, [])); + } + + [Fact(Skip = "Remove this Skip property to run this test")] + public void Trivial_1_digit_cages_9() + { + int[][] expected = [[9]]; + Assert.Equal(expected, KillerSudokuHelper.Combinations(9, 1, [])); + } + + [Fact(Skip = "Remove this Skip property to run this test")] + public void Cage_with_sum_45_contains_all_digits_1_9() + { + int[][] expected = [[1, 2, 3, 4, 5, 6, 7, 8, 9]]; + Assert.Equal(expected, KillerSudokuHelper.Combinations(45, 9, [])); + } + + [Fact(Skip = "Remove this Skip property to run this test")] + public void Cage_with_only_1_possible_combination() + { + int[][] expected = [[1, 2, 4]]; + Assert.Equal(expected, KillerSudokuHelper.Combinations(7, 3, [])); + } + + [Fact(Skip = "Remove this Skip property to run this test")] + public void Cage_with_several_combinations() + { + int[][] expected = [ + [1, 9], + [2, 8], + [3, 7], + [4, 6] + ]; Assert.Equal(expected, KillerSudokuHelper.Combinations(10, 2, [])); + } + + [Fact(Skip = "Remove this Skip property to run this test")] + public void Cage_with_several_combinations_that_is_restricted() + { + int[][] expected = [ + [2, 8], + [3, 7] + ]; Assert.Equal(expected, KillerSudokuHelper.Combinations(10, 2, [1, 4])); + } +}