diff --git a/config.json b/config.json index 1597373e132..db7f530ca48 100644 --- a/config.json +++ b/config.json @@ -1116,17 +1116,28 @@ ] }, { - "uuid": "55c818d6-04a8-2480-de83-9f85fd0d2eafa71935f", - "slug": "pov", - "core": false, - "unlocked_by": null, - "difficulty": 9, - "topics": [ - "graphs", - "recursion", - "searching", - "trees" - ] + "uuid": "4354f631-0cf5-9980-75e7-86d1c3da1f0d3f5e619", + "slug": "dominoes", + "core": false, + "unlocked_by": null, + "difficulty": 7, + "topics": [ + "tuples", + "lists" + ] + }, + { + "uuid": "55c818d6-04a8-2480-de83-9f85fd0d2eafa71935f", + "slug": "pov", + "core": false, + "unlocked_by": null, + "difficulty": 9, + "topics": [ + "graphs", + "recursion", + "searching", + "trees" + ] }, { "uuid": "e7351e8e-d3ff-4621-b818-cd55cf05bffd", diff --git a/exercises/dominoes/README.md b/exercises/dominoes/README.md new file mode 100644 index 00000000000..a858a78c0ab --- /dev/null +++ b/exercises/dominoes/README.md @@ -0,0 +1,28 @@ +# Dominoes + +Make a chain of dominoes. + +Compute a way to order a given set of dominoes in such a way that they form a +correct domino chain (the dots on one half of a stone match the dots on the +neighbouring half of an adjacent stone) and that dots on the halfs of the stones +which don't have a neighbour (the first and last stone) match each other. + +For example given the stones `[2|1]`, `[2|3]` and `[1|3]` you should compute something +like `[1|2] [2|3] [3|1]` or `[3|2] [2|1] [1|3]` or `[1|3] [3|2] [2|1]` etc, where the first and last numbers are the same. + +For stones `[1|2]`, `[4|1]` and `[2|3]` the resulting chain is not valid: `[4|1] [1|2] [2|3]`'s first and last numbers are not the same. 4 != 3 + +Some test cases may use duplicate stones in a chain solution, assume that multiple Domino sets are being used. + +## Submitting Exercises + +Note that, when trying to submit an exercise, make sure the solution is in the `exercism/python/` directory. + +For example, if you're submitting `bob.py` for the Bob exercise, the submit command would be something like `exercism submit /python/bob/bob.py`. + + +For more detailed information about running tests, code style and linting, +please see the [help page](http://exercism.io/languages/python). + +## Submitting Incomplete Solutions +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/exercises/dominoes/dominoes.py b/exercises/dominoes/dominoes.py new file mode 100644 index 00000000000..12b0c055ff7 --- /dev/null +++ b/exercises/dominoes/dominoes.py @@ -0,0 +1,2 @@ +def chain(dominoes): + pass diff --git a/exercises/dominoes/dominoes_test.py b/exercises/dominoes/dominoes_test.py new file mode 100644 index 00000000000..c0b9fe0b11a --- /dev/null +++ b/exercises/dominoes/dominoes_test.py @@ -0,0 +1,114 @@ +import unittest + +from dominoes import chain + + +# Tests adapted from `problem-specifications//canonical-data.json` @ v2.0.0 + +class DominoesTest(unittest.TestCase): + def test_empty_input_empty_output(self): + input_dominoes = [] + output_chain = chain(input_dominoes) + self.assert_correct_chain(input_dominoes, output_chain) + + def test_singleton_input_singleton_output(self): + input_dominoes = [(1, 1)] + output_chain = chain(input_dominoes) + self.assert_correct_chain(input_dominoes, output_chain) + + def test_singleton_cant_be_chained(self): + input_dominoes = [(1, 2)] + output_chain = chain(input_dominoes) + self.refute_correct_chain(input_dominoes, output_chain) + + def test_three_elements(self): + input_dominoes = [(1, 2), (3, 1), (2, 3)] + output_chain = chain(input_dominoes) + self.assert_correct_chain(input_dominoes, output_chain) + + def test_can_reverse_dominoes(self): + input_dominoes = [(1, 2), (1, 3), (2, 3)] + output_chain = chain(input_dominoes) + self.assert_correct_chain(input_dominoes, output_chain) + + def test_cant_be_chained(self): + input_dominoes = [(1, 2), (4, 1), (2, 3)] + output_chain = chain(input_dominoes) + self.refute_correct_chain(input_dominoes, output_chain) + + def test_disconnected_simple(self): + input_dominoes = [(1, 1), (2, 2)] + output_chain = chain(input_dominoes) + self.refute_correct_chain(input_dominoes, output_chain) + + def test_disconnected_double_loop(self): + input_dominoes = [(1, 2), (2, 1), (3, 4), (4, 3)] + output_chain = chain(input_dominoes) + self.refute_correct_chain(input_dominoes, output_chain) + + def test_disconnected_single_isolated(self): + input_dominoes = [(1, 2), (2, 3), (3, 1), (4, 4)] + output_chain = chain(input_dominoes) + self.refute_correct_chain(input_dominoes, output_chain) + + def test_need_backtrack(self): + input_dominoes = [(1, 2), (2, 3), (3, 1), (2, 4), (2, 4)] + output_chain = chain(input_dominoes) + self.assert_correct_chain(input_dominoes, output_chain) + + def test_separate_loops(self): + input_dominoes = [(1, 2), (2, 3), (3, 1), (1, 1), (2, 2), (3, 3)] + output_chain = chain(input_dominoes) + self.assert_correct_chain(input_dominoes, output_chain) + + def test_nine_elements(self): + input_dominoes = [(1, 2), (5, 3), (3, 1), (1, 2), (2, 4), (1, 6), + (2, 3), (3, 4), (5, 6)] + output_chain = chain(input_dominoes) + self.assert_correct_chain(input_dominoes, output_chain) + + # Utility methods + + def normalize_dominoes(self, dominoes): + return list(sorted(tuple(sorted(domino)) for domino in dominoes)) + + def assert_same_dominoes(self, input_dominoes, output_chain): + msg = ('Dominoes used in the output must be the same ' + 'as the ones given in the input') + input_normal = self.normalize_dominoes(input_dominoes) + output_normal = self.normalize_dominoes(output_chain) + self.assertEqual(input_normal, output_normal, msg) + + def assert_consecutive_dominoes_match(self, output_chain): + for i in range(len(output_chain) - 1): + msg = ("In chain {}, right end of domino {} ({}) " + "and left end of domino {} ({}) must match") + msg = msg.format(output_chain, + i, + output_chain[i], + i + 1, + output_chain[i + 1]) + self.assertEqual(output_chain[i][1], output_chain[i + 1][0], msg) + + def assert_dominoes_at_ends_match(self, output_chain): + msg = ("In chain {}, left end of first domino ({}) and " + "right end of last domino ({}) must match") + msg = msg.format(output_chain, output_chain[0], output_chain[-1]) + self.assertEqual(output_chain[0][0], output_chain[-1][1], msg) + + def assert_correct_chain(self, input_dominoes, output_chain): + msg = 'There should be a chain for {}'.format(input_dominoes) + self.assertIsNotNone(output_chain, msg) + self.assert_same_dominoes(input_dominoes, output_chain) + if not any(output_chain): + return + self.assert_consecutive_dominoes_match(output_chain) + self.assert_dominoes_at_ends_match(output_chain) + + def refute_correct_chain(self, input_dominoes, output_chain): + msg = 'There should be no valid chain for {}'.format(input_dominoes) + self.assertIsNone(output_chain, msg) + + +if __name__ == '__main__': + unittest.main() diff --git a/exercises/dominoes/example.py b/exercises/dominoes/example.py new file mode 100644 index 00000000000..031034f28ce --- /dev/null +++ b/exercises/dominoes/example.py @@ -0,0 +1,30 @@ +from itertools import permutations +from functools import reduce + + +def swap(a, b): + return (b, a) + + +def build_chain(chain, domino): + if chain is not None: + last = chain[-1] + if len(chain) == 1 and last[0] == domino[0]: + return [swap(*last), domino] + elif len(chain) == 1 and last[0] == domino[1]: + return [swap(*last), swap(*domino)] + elif last[1] == domino[0]: + return chain + [domino] + elif last[1] == domino[1]: + return chain + [swap(*domino)] + return None + + +def chain(dominoes): + if not any(dominoes): + return [] + for perm in permutations(dominoes): + chain = reduce(build_chain, perm[1:], [perm[0]]) + if chain is not None and chain[0][0] == chain[-1][1]: + return chain + return None