Passed
Push — master ( 5c6057...e1c816 )
by Ken M.
01:12
created

magic_domino.combine_columns()   A

Complexity

Conditions 4

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nop 3
dl 0
loc 15
rs 9.8
c 0
b 0
f 0
1
import operator as op
2
from functools import reduce
3
from itertools import chain, combinations, islice, permutations, product
4
from random import shuffle
5
6
7
flatten = lambda x: [j for i in x for j in i]
8
9
10
def dominos():
11
    return [(i, j) for i in range(7) for j in range(i, 7)]
12
13
14
def tile_combinations(tiles, size, number):
15
    return [i for i in combinations(tiles, size // 2) if sum(map(sum, i)) == number]
16
17
18
def combine_columns(columns, size, exception=set()):
19
    if size == 0:
20
        yield []
21
    for i, v in enumerate(columns):
22
        new_exception = set(v) | exception
23
        sub_columns = [
24
            item
25
            for item in columns[i + 1 :]
26
            if not any([tile in new_exception for tile in item])
27
        ]
28
        for j in combine_columns(sub_columns, size - 1, new_exception):
29
            # print(v)
30
            ret = [v] + j
31
            # print(ret)
32
            yield ret
33
34
35
def row_sum_check(columns, number, size):
36
    for i in range(size):
37
        item_index, sub_item_index = i // 2, i % 2
38
        if sum([i[item_index][sub_item_index] for i in columns]) != number:
39
            return False
40
    return True
41
42
43
def diagonal_check(columns, number, size):
44
    diagonal_indexes = []
45
    reversed_diagonal_indexes = []
46
    for i in range(size):
47
        diagonal_indexes.append((i // 2, i % 2))
48
        reversed_diagonal_indexes.append(((size - 1 - i) // 2, (size - 1 - i) % 2))
49
50
    for i in permutations(columns):
51
        if (
52
            sum(
53
                [
54
                    value[diagonal_indexes[row][0]][diagonal_indexes[row][1]]
55
                    for row, value in enumerate(i)
56
                ]
57
            )
58
            == number
59
            and sum(
60
                [
61
                    value[reversed_diagonal_indexes[row][0]][
62
                        reversed_diagonal_indexes[row][1]
63
                    ]
64
                    for row, value in enumerate(i)
65
                ]
66
            )
67
            == number
68
        ):
69
            return i
70
    return False
71
72
73
def brute_force(domino, number, size):
74
    shuffled_cols = []
75
    for column in domino:
76
        element_list = []
77
        for element in column:
78
            element_list.append(list(set([element, element[::-1]])))
79
80
        # find out all possible permutations of a column
81
        col_possibilities = []
82
        for i in permutations(element_list, len(element_list)):
83
            col_possibilities.append(product(*i))
84
        col_possibilities = chain(*col_possibilities)
85
        shuffled_cols.append(col_possibilities)
86
87
    for i in product(*shuffled_cols):
88
        if row_sum_check(i, number, size):
89
            ret = diagonal_check(i, number, size)
90
            if ret:
91
                return ret
92
93
94
def magic_domino(size, number):
95
    domino_tiles = dominos()
96
    columns = tile_combinations(domino_tiles, size, number)
97
    for i in combine_columns(columns, size):
98
        ret = brute_force(i, number, size)
99
        if ret:
100
            return list(zip(*[flatten(j) for j in ret]))
101
102
103
if __name__ == '__main__':
104
    # These "asserts" using only for self-checking and not necessary for auto-testing
105
    import itertools
106
107
    def check_data(size, number, user_result):
108
109
        # check types
110
        def check_container_type(o):
111
            return any(map(lambda t: isinstance(o, t), (list, tuple)))
112
113
        def check_cell_type(i):
114
            return isinstance(i, int)
115
116
        if not (
117
            check_container_type(user_result)
118
            and all(map(check_container_type, user_result))
119
            and all(map(lambda row: all(map(check_cell_type, row)), user_result))
120
        ):
121
            raise Exception(
122
                "You should return a list/tuple of lists/tuples with integers."
123
            )
124
125
        # check sizes
126
        def check_size(o):
127
            return len(o) == size
128
129
        if not (check_size(user_result) and all(map(check_size, user_result))):
130
            raise Exception("Wrong size of answer.")
131
132
        # check is it a possible numbers (from 0 to 6 inclusive)
133
        if not all(
134
            map(lambda x: 0 <= x <= 6, itertools.chain.from_iterable(user_result))
135
        ):
136
            raise Exception("Wrong matrix integers (can't be domino tiles)")
137
138
        # check is it a magic square
139
        def seq_sum_check(seq):
140
            return sum(seq) == number
141
142
        diagonals_indexes = zip(
143
            *map(lambda i: ((i, i), (i, size - i - 1)), range(size))
144
        )
145
146
        def values_from_indexes(inds):
147
            return itertools.starmap(lambda x, y: user_result[y][x], inds)
148
149
        if not (
150
            all(map(seq_sum_check, user_result))
151
            and all(map(seq_sum_check, zip(*user_result)))  # rows
152
            and all(  # columns
153
                map(seq_sum_check, map(values_from_indexes, diagonals_indexes))
154
            )
155
        ):  # diagonals
156
            raise Exception("It's not a magic square.")
157
158
        # check is it domino square
159
        tiles = set()
160
        for x, y in itertools.product(range(size), range(0, size, 2)):
161
            tile = tuple(sorted((user_result[y][x], user_result[y + 1][x])))
162
            if tile in tiles:
163
                raise Exception("It's not a domino magic square.")
164
            tiles.add(tile)
165
166
    check_data(4, 5, magic_domino(4, 5))
167