-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathBaseNSudoku.py
217 lines (186 loc) · 8.54 KB
/
BaseNSudoku.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
BASE = 9
BLOCK_HEIGHT = 3
BLOCK_WIDTH = 3
field = [
[1, 2, 3, 4, 5, 6, 7, 8, 9],
[4, 5, 6, 7, 8, 9, 1, 2, 3],
[7, 8, 9, 1, 2, 3, 4, 5, 6],
[2, 3, 4, 5, 6, 7, 8, 9, 1],
[5, 6, 7, 8, 9, 1, 2, 3, 4],
[8, 9, 1, 2, 3, 4, 5, 6, 7],
[3, 4, 5, 6, 7, 8, 9, 1, 2],
[6, 7, 8, 9, 1, 2, 3, 4, 5],
[9, 1, 2, 3, 4, 5, 6, 7, 8]
]
def precheck():
# Step 0: Pre-solve consistency checks
global BASE, BLOCK_HEIGHT, BLOCK_WIDTH, field
try:
BASE = int(BASE)
except Exception as err:
err.message = "Failed to convert BASE ({}) to int: ".format(BASE) + err.message
raise
global BASE_RANGE
BASE_RANGE = range(1, BASE+1)
try:
BLOCK_HEIGHT = int(BLOCK_HEIGHT)
except Exception as err:
err.message = "Failed to convert BLOCK_HEIGHT ({}) to int: ".format(BLOCK_HEIGHT) + err.message
raise
try:
BLOCK_WIDTH = int(BLOCK_WIDTH)
except Exception as err:
err.message = "Failed to convert BLOCK_WIDTH ({}) to int: ".format(BLOCK_WIDTH) + err.message
raise
if BLOCK_HEIGHT * BLOCK_WIDTH != BASE:
raise ValueError("Block size ({} by {}) does not match number base ({})".format(BLOCK_WIDTH, BLOCK_HEIGHT, BASE))
try:
field = list(field)
except Exception as err:
err.message = "Failed to convert field ({}) to list: ".format(field) + err.message
raise
if len(field) != BASE:
raise ValueError("Field height ({}) is not equal to number base ({})".format(len(field), BASE))
if len(field) % BLOCK_HEIGHT != 0:
raise ValueError("Field height ({}) is not divisible by block height ({})".format(len(field), BLOCK_HEIGHT))
for y, row in enumerate(field):
try:
field[y] = list(row)
except Exception as err:
err.message = "Failed to convert row {} ({}) to list: ".format(y, row) + err.message
if len(row) != BASE:
raise ValueError("Width of row {} ({}) is not equal to number base ({})".format(i, len(row), BASE))
if len(row) % BLOCK_HEIGHT != 0:
raise ValueError("Width of row {} ({}) is not divisible by block height ({})".format(i, len(row), BLOCK_HEIGHT))
for x, cell in enumerate(row):
try:
field[y][x] = int(cell)
except Exception as err:
err.message = "Failed to parse cell {} in row {} ({}) as int: ".format(x, y, cell) + err.message
raise
if not 0 <= cell <= BASE:
raise ValueError("Cell {} in row {} ({}) must be greater than 0 and less than {}".format(x, y, cell, BASE))
for n in BASE_RANGE:
if row.count(n) > 1:
raise ValueError("Number {} appears more than once in row {}".format(n, y))
for x, col in enumerate(zip(*field)):
for n in BASE_RANGE:
if col.count(n) > 1:
raise ValueError("Number {} appears more than once in column {}".format(n, x))
for y in range(BASE / BLOCK_HEIGHT):
rows = field[BLOCK_HEIGHT*y:BLOCK_HEIGHT*(y+1)]
for x in range(BASE / BLOCK_WIDTH):
block = []
for row in rows:
block += row[BLOCK_WIDTH*x:BLOCK_WIDTH*(x+1)]
for n in BASE_RANGE:
if block.count(n) > 1:
raise ValueError("Number {} appears more than once in block y={}, x={}".format(n, y, x))
print("Checks done, field appears to be a valid sudoku. Proceeding to solve...")
def solve():
global field
# the following loop is just to be able to use "continue", it terminates after one full loop
step = 1
while step >= 0:
# reread variables
zipfield = zip(*field)
blocks = []
for y in range(BASE / BLOCK_HEIGHT):
blocks.append([])
rows = field[BLOCK_HEIGHT*y:BLOCK_HEIGHT*(y+1)]
for x in range(BASE / BLOCK_WIDTH):
block = []
for row in rows:
block += row[BLOCK_WIDTH*x:BLOCK_WIDTH*(x+1)]
blocks[y].append(block)
if step == 0:
step = 1
elif step == 1:
# Step 1: Basic solving of single-possibility cells
print("Step 1...")
for y, row in enumerate(field):
for x, cell in enumerate(row):
if isinstance(cell, set) or cell == 0:
# cell is a list or nonzero number, i. e. unsolved
poss = set()
for n in BASE_RANGE:
if n not in row and n not in zipfield[x] and n not in blocks[y//BLOCK_HEIGHT][x//BLOCK_WIDTH]:
# n does not yet exist in row, column or block
poss.add(n)
if len(poss) == 1:
# single possibility, cell is solved
print("Cell {} in row {} must be {}".format(x, y, list(poss)[0]))
field[y][x] = list(poss)[0]
step = 0
elif len(poss) == 0:
# no possibilities, something went wrong
print("No possibilities for cell {} in row {}, this should never happen!".format(x, y))
retry = True
step = -1
else:
# more than one possibility, store for later
field[y][x] = poss
if step <= 0:
break
if step <= 0:
break
if step == 1:
step = 2
elif step == 2:
# Step 2: Analyze mutually exclusive possibilities
print("Step 2...")
for y, row in enumerate(field):
for x, cell in enumerate(row):
if isinstance(cell, set):
poss = set()
# Step 2.1: Correlate with other possibilities in same row
oposs = set()
for ocell in row:
if isinstance(ocell, set) and ocell is not cell:
oposs.update(ocell)
for n in cell:
if n not in oposs:
# if n cannot go elsewhere, it must go here
poss.add(n)
# Step 2.2: Correlate with other possibilities in same column
oposs = set()
for ocell in zipfield[x]:
if isinstance(ocell, set) and ocell is not cell:
oposs.update(ocell)
for n in cell:
if n not in oposs:
# if n cannot go elsewhere, it must go here
poss.add(n)
# Step 2.3: Correlate with other possibilities in same block
oposs = set()
for ocell in blocks[y//BLOCK_HEIGHT][x//BLOCK_WIDTH]:
if isinstance(ocell, set) and ocell is not cell:
oposs.update(ocell)
for n in cell:
if n not in oposs:
# if n cannot go elsewhere, it must go here
poss.add(n)
if len(poss) == 1:
# single possibility, cell is solved
print("Cell {} in row {} must be {}".format(x, y, list(poss)[0]))
field[y][x] = list(poss)[0]
step = 0
elif len(poss) == 0:
# no possibilities, simply ignore
pass
else:
# more than one possibility, something went wrong
print("More than one possibility ({}) in step 2 for cell {} in row {}, this should not happen!".format(poss, x, y))
if step <= 0:
break
if step <= 0:
break
if step == 2:
step = -1
print("Final result: [")
for row in field:
print(str(row) + ",")
print("]")
if __name__ == "__main__":
precheck()
solve()