Skip to content

Commit 8fcac70

Browse files
committed
initial commit
0 parents  commit 8fcac70

File tree

1 file changed

+338
-0
lines changed

1 file changed

+338
-0
lines changed

py-sudoku.py

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
import random
2+
import numpy as np
3+
4+
5+
def read_puzzle(in_line):
6+
"""
7+
reads a sudoku puzzle from a single input line
8+
:param in_line: 9 space-separated 9-digit numbers ranging from 0-9
9+
:return: returns the numbers in a numpy array
10+
"""
11+
arr = np.zeros((9,9), dtype=int)
12+
for i, line in enumerate(in_line.split(' ')):
13+
for j, num in enumerate(list(line)):
14+
arr[i, j] = num
15+
16+
return arr
17+
18+
19+
def test_fit(x, y, n, board_state):
20+
"""
21+
tests whether n can be placed in x,y on the current board_state
22+
:param x: horizontal position on the board
23+
:param y: vertical position on the board
24+
:param n: the number to place
25+
:param board_state: the current board state as a numpy array
26+
:return: true if nothing would stop n from being placed in x,y on board_state, else returns false
27+
"""
28+
29+
# first test if something is already in that position
30+
if board_state[x, y] != 0:
31+
return False
32+
33+
# then test if n already exists in that column or row
34+
for i in range(9):
35+
if board_state[x, i] == n:
36+
return False
37+
elif board_state[i, y] == n:
38+
return False
39+
40+
# finally test if it fits in the block
41+
x_block = 0
42+
y_block = 0
43+
44+
if x < 3:
45+
x_block = 0
46+
elif x < 6:
47+
x_block = 1
48+
else:
49+
x_block = 2
50+
51+
if y < 3:
52+
y_block = 0
53+
elif y < 6:
54+
y_block = 1
55+
else:
56+
y_block = 2
57+
58+
for i in range(x_block * 3, x_block * 3 + 3):
59+
for j in range(y_block * 3, y_block * 3 + 3):
60+
if board_state[i, j] == n:
61+
return False
62+
63+
return True
64+
65+
66+
def generate_puzzle(difficulty='easy'):
67+
"""
68+
creates a sudoku puzzle
69+
:param difficulty: easy, medium, hard, impossible accepted
70+
:return: a 9x9 numpy array of valid sudoku
71+
"""
72+
73+
num_clues = 0
74+
75+
# define a random-ish amount of clues based on the difficulty
76+
if difficulty == 'easy':
77+
for i in range(9):
78+
num_clues += random.randint(3,5)
79+
elif difficulty == 'medium':
80+
for i in range(9):
81+
num_clues += random.randint(2,4)
82+
elif difficulty == 'hard':
83+
for i in range(9):
84+
num_clues += random.randint(1,3)
85+
elif difficulty == 'impossible':
86+
for i in range(9):
87+
num_clues += random.randint(0,2)
88+
89+
# create the playboard, or puzzle
90+
playboard = np.zeros((9,9), dtype=int)
91+
92+
# always make sure a number can be placed on the board
93+
while num_clues > 0:
94+
x = random.randint(0, 8)
95+
y = random.randint(0, 8)
96+
n = random.randint(1, 9)
97+
98+
if test_fit(x, y, n, playboard):
99+
playboard[x, y] = n
100+
num_clues -= 1
101+
102+
return playboard
103+
104+
105+
def narrow_solutions(x, y, board_state):
106+
"""
107+
tests all numbers 1-9 whether they could fit in spot x,y on the current board
108+
:param x: horizontal position
109+
:param y: vertical position
110+
:param board_state: current board as a 9x9 valid sudoku puzzle
111+
:return: returns a numpy array of bools indicating whether that index(+1) would fit in x,y
112+
"""
113+
solutions = np.array([True, True, True, True, True, True, True, True, True])
114+
115+
for i in range(1, 10): # numbers from 1 to 9
116+
# test for vertical and horizontal
117+
for j in range(9):
118+
if board_state[x, j] == i:
119+
solutions[i - 1] = False
120+
break
121+
elif board_state[j, y] == i:
122+
solutions[i - 1] = False
123+
break
124+
125+
# finally test if it exists in the block
126+
x_block = 0
127+
y_block = 0
128+
129+
if x < 3:
130+
x_block = 0
131+
elif x < 6:
132+
x_block = 1
133+
else:
134+
x_block = 2
135+
136+
if y < 3:
137+
y_block = 0
138+
elif y < 6:
139+
y_block = 1
140+
else:
141+
y_block = 2
142+
143+
for j in range(x_block * 3, x_block * 3 + 3):
144+
for k in range(y_block * 3, y_block * 3 + 3):
145+
if board_state[j, k] == i:
146+
solutions[i - 1] = False
147+
148+
return solutions
149+
150+
151+
def catch_error(list_like):
152+
"""
153+
if none of the options are true, something has gone wrong
154+
:param list_like: a list_like of bools
155+
:return: true if at least one is true, else returns false
156+
"""
157+
for b in list_like:
158+
if b:
159+
return True
160+
161+
return False
162+
163+
164+
def single_option(list_like):
165+
"""
166+
checks if a single option is true
167+
:param list_like: a list-like of bools
168+
:return: true if exactly 1 is true, else returns false
169+
"""
170+
trues = 0
171+
num = 0
172+
for i, b in enumerate(list_like):
173+
if b:
174+
trues += 1
175+
num = i + 1
176+
if trues > 1:
177+
return 0
178+
return num
179+
180+
181+
def iterate_through_block(n, x, y, state, solution_state):
182+
"""
183+
goes through a 3x3 block to check for n
184+
:param n:
185+
:param x:
186+
:param y:
187+
:param state:
188+
:param solution_state:
189+
:return:
190+
"""
191+
num_n = 0
192+
pos = (0, 0)
193+
194+
# go though each position in the block looking for n
195+
for i in range(x * 3, x * 3 + 3):
196+
for j in range(y * 3, y * 3 + 3):
197+
if state[i, j] == n + 1:
198+
return 0, (0, 0) # break all the way out of the block because the number already exists here
199+
200+
if state[i, j] != 0: # make sure nothing is here already
201+
continue
202+
203+
# this position is valid, check if it can be n
204+
if solution_state[i, j, n]:
205+
num_n += 1
206+
pos = (i, j)
207+
208+
return num_n, pos
209+
210+
211+
def check_area(current_state, current_solution_state):
212+
# first check for rows where only one box can have a number (n)
213+
for n in range(9): # current number we are working with (to access in current_solution_state)
214+
for i in range(9): # current row we are in
215+
num_n = 0
216+
pos = (0, 0)
217+
for j in range(9): # current index in the row
218+
if current_state[i, j] == n + 1:
219+
break # break all the way out of the row because the number already exists here
220+
221+
if current_state[i, j] != 0: # make sure nothing is here already
222+
continue
223+
224+
# this position is valid, check if it can be n
225+
if current_solution_state[i, j, n]:
226+
num_n += 1
227+
pos = (i, j)
228+
229+
# now we are done with that row, so check if there was one single solution
230+
if num_n == 1:
231+
current_state[pos[0], pos[1]] = n + 1 # if one solution existed, the state is updated and we return
232+
return current_state
233+
234+
# then do the same with columns
235+
for n in range(9): # current number we are working with (to access in current_solution_state)
236+
for i in range(9): # current column we are in
237+
num_n = 0
238+
pos = (0, 0)
239+
for j in range(9): # current index in the row
240+
if current_state[j, i] == n + 1:
241+
break # break all the way out of the row because the number already exists here
242+
243+
if current_state[j, i] != 0: # make sure nothing is here already
244+
continue
245+
246+
# this position is valid, check if it can be n
247+
if current_solution_state[j, i, n]:
248+
num_n += 1
249+
pos = (j, i)
250+
251+
# now we are done with that row, so check if there was one single solution
252+
if num_n == 1:
253+
current_state[pos[0], pos[1]] = n + 1 # if one solution existed, the state is updated and we return
254+
return current_state
255+
256+
# finally, check the blocks
257+
# this can be done by just iterating through them
258+
for n in range(9): # the number we are looking for
259+
for x in range(3): # the horizontal block from 0 to 2
260+
for y in range(3): # the vertical block from 0 to 2
261+
num_n, pos = iterate_through_block(n, x, y, current_state, current_solution_state)
262+
if num_n == 1: # there was exactly one square that could be n
263+
current_state[pos[0], pos[1]] = n + 1
264+
return current_state
265+
266+
return current_state # that was a fluke (but it should have worked)
267+
268+
269+
def sudoku_is_complete(state):
270+
for i in range(9):
271+
for j in range(9):
272+
if(state[i,j] == 0):
273+
return False
274+
275+
return True
276+
277+
278+
def solver(puzzle_input):
279+
puzzle_is_solved = False
280+
281+
# create a board of possible solutions of each unsolved square
282+
solutions_board = np.zeros((9,9,9))
283+
for i in range(9):
284+
for j in range(9):
285+
if puzzle_input[i, j] == 0:
286+
solutions_board[i, j] = np.array([True, True, True, True, True, True, True, True, True])
287+
288+
num_iterations = 0
289+
# iterate through these until the puzzle is solved
290+
while not puzzle_is_solved:
291+
did_something = False
292+
293+
for i in range(9):
294+
for j in range(9):
295+
if puzzle_input[i, j] != 0:
296+
continue
297+
# first, narrow down options in that spot based on row, column and block
298+
solutions_board[i, j] = narrow_solutions(i, j, puzzle_input)
299+
300+
# check for errors first
301+
if not catch_error(solutions_board[i, j]):
302+
print("There was an unsolvable error at", i, j)
303+
puzzle_input[i, j] = 69
304+
return puzzle_input
305+
306+
# now check if this position only has one option
307+
single_solution = single_option(solutions_board[i, j])
308+
if single_solution > 0:
309+
puzzle_input[i, j] = single_solution
310+
did_something = True
311+
312+
if not did_something: # only check areas if nothing happened last iteration since it is unnecessary,
313+
# resource intensive, and might break something by using outdated possibilities
314+
puzzle_input = check_area(puzzle, solutions_board)
315+
316+
num_iterations += 1
317+
318+
if sudoku_is_complete(puzzle_input):
319+
print("Sudoku complete")
320+
break
321+
322+
if num_iterations > 1000:
323+
print("Something might be wrong here, aborting")
324+
return puzzle_input
325+
326+
return puzzle_input
327+
328+
329+
# puzzle = generate_puzzle()
330+
331+
puzzle = read_puzzle(input("Please input a string equating a sudoku board: "
332+
"\n9 space-separated 9-long series of numbers from 0-9 where 0 indicates empty"
333+
"\n"))
334+
335+
print(puzzle)
336+
337+
solved = solver(puzzle)
338+
print(solved)

0 commit comments

Comments
 (0)