├── README.md
├── SudokuGenerator.py
└── SudokuSolver.py
/README.md:
--------------------------------------------------------------------------------
1 | # SudokuGenerator
2 |
3 | There are two files:
4 | * SudokuSolver.py is just a Solver, that finds all possible Solutions to a given Sudoku
5 | * SudokuGenerator.py generates Sudoku's with different difficulty levels (1 to 6). You can pass the difficulty level as an argument. For example:
6 | ```
7 | python3 SudokuGenerator.py 4
8 | ```
9 |
10 | ## Copyright
11 |
12 | Copyright ©️ 2023 Wilhelm Drehling, Heise Medien GmbH & Co. KG
13 |
14 | This program is free software: you can redistribute it and/or modify
15 | it under the terms of the GNU General Public License as published by
16 | the Free Software Foundation, either version 3 of the License, or
17 | (at your option) any later version.
18 |
19 | This program is distributed in the hope that it will be useful,
20 | but WITHOUT ANY WARRANTY; without even the implied warranty of
21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 | GNU General Public License for more details.
23 |
24 | You should have received a copy of the GNU General Public License
25 | along with this program. If not, see .
26 |
--------------------------------------------------------------------------------
/SudokuGenerator.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import random
4 | import sys
5 | import time
6 | from datetime import datetime
7 |
8 | class Sudoku:
9 | def __init__(self):
10 | self.reset()
11 |
12 | def reset(self):
13 | # create empty 9x9 board
14 | rows = 9
15 | columns = 9
16 | self.board = [[0 for j in range(columns)] for i in range(rows)]
17 |
18 |
19 | def toSVG(self):
20 | # Variables
21 | cell_size = 40
22 | line_color = "black"
23 |
24 | # creating a rectangle in white with the size of a 9x9-Sudoku
25 | svg = ''
46 | return svg
47 |
48 | def generate(self, difficulty):
49 | # fill diagonal squares
50 | for i in range(0, 9, 3):
51 | square = [1, 2, 3, 4, 5, 6, 7, 8, 9]
52 | random.shuffle(square)
53 | for r in range(3):
54 | for c in range(3):
55 | self.board[r + i][c + i] = square.pop()
56 |
57 | # fill rest
58 | for solutions in self.solve():
59 | break
60 |
61 | # difficulty
62 | empty_cells = self.evaluate(difficulty)
63 |
64 | # creating a list of coordinates to visit and shuffeling them
65 | unvisited = [(r, c) for r in range(9) for c in range(9)]
66 | random.shuffle(unvisited)
67 |
68 | # remove numbers
69 | while empty_cells > 0 and len(unvisited) > 0:
70 | # saving a copy of the number, just in case, if we cant remove it
71 | r, c = unvisited.pop()
72 | copy = self.board[r][c]
73 | self.board[r][c] = 0
74 |
75 | # checking how many solutions are in the board
76 | solutions = [solution for solution in self.solve()]
77 |
78 | # if there is more than one solution, we put the number back
79 | if len(solutions) > 1:
80 | self.board[r][c] = copy
81 | else:
82 | empty_cells -= 1
83 |
84 | # if unvisited is empty, but empty_cells not -> trying again
85 | if empty_cells > 0:
86 | print("No Sudoku found. Trying again.")
87 | return False
88 | else:
89 | return True
90 |
91 |
92 |
93 |
94 | def evaluate(self, difficulty):
95 | # 1 = really easy, 3 = middle, 6 = devilish (lowest number possible, takes a long time to calculate)
96 | empty_cells = [0, 25, 35, 45, 52, 58, 64]
97 | if difficulty < 1 or difficulty > len(empty_cells)-1:
98 | print("invalid difficulty", file=sys.stderr)
99 | return empty_cells[difficulty]
100 |
101 |
102 | # method to print the board in console
103 | def print(self):
104 | for i in range(9):
105 | print(" ".join([str(x)if x != 0 else "." for x in self.board[i]]))
106 |
107 |
108 | def number_is_valid(self, row, column, number):
109 | # check row and column
110 | for i in range(9):
111 | if self.board[row][i] == number or self.board[i][column] == number:
112 | return False
113 |
114 | # check square
115 | start_column = column // 3 * 3
116 | start_row = row // 3 * 3
117 | for i in range(3):
118 | for j in range(3):
119 | if self.board[i + start_row][j + start_column] == number:
120 | return False
121 | return True
122 |
123 |
124 | def solve(self):
125 | # generate random numbers from 1 to 9
126 | digits = list(range(1, 10))
127 |
128 | # find an empty cell
129 | for r in range(9):
130 | for c in range(9):
131 | if self.board[r][c] == 0:
132 | # for every empty cell fill a random valid number into it
133 | random.shuffle(digits)
134 | for n in digits:
135 | if self.number_is_valid(r, c, n):
136 | self.board[r][c] = n
137 | # is it solved?
138 | yield from self.solve()
139 | # backtrack
140 | self.board[r][c] = 0
141 | return
142 | yield True
143 |
144 |
145 | def main():
146 | # takes difficulty as an argument, if not provided the program removes half of the board (level 3)
147 | args = [int(x) if x.isdecimal() else x for x in sys.argv[1:]]
148 | difficulty = args[0] if len(args) > 0 else 3
149 |
150 | sudoku = Sudoku()
151 |
152 | # trying in Total for 10 mins to find a sudoku
153 | timeout = 600
154 | start_time = time.time()
155 | end_time = start_time + timeout
156 |
157 | while time.time() < end_time:
158 | if sudoku.generate(difficulty) == True:
159 | break
160 | else:
161 | sudoku.reset()
162 |
163 | # printing
164 | sudoku.print()
165 |
166 | # creating the .svg-File with crrent date, time and difficulty
167 | svg = sudoku.toSVG()
168 | now = datetime.now()
169 | name = f'sudoku-{now:%Y%m%dT%H%M%S}-{difficulty}.svg'
170 | with open(name, 'w') as f:
171 | f.write(svg)
172 |
173 |
174 | if __name__ == "__main__":
175 | main()
176 |
--------------------------------------------------------------------------------
/SudokuSolver.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | class Sudoku:
4 | def __init__(self, board):
5 | self.board = board
6 |
7 | # evaluates the difficulty of the sudoku by counting the empty spaces
8 | def evaluate(self):
9 | empty_cells = sum(self.board, []).count(0)
10 | if empty_cells <= 28:
11 | return "Pretty easy"
12 | elif empty_cells <= 39:
13 | return "Easy"
14 | elif empty_cells <= 53:
15 | return "Medium"
16 | elif empty_cells <= 64:
17 | return "Hard"
18 | elif empty_cells <= 71:
19 | return "Pretty hard"
20 | else:
21 | return "Diabolical"
22 |
23 |
24 | # method to print the board
25 | def print(self):
26 | for i in range(9):
27 | print(" ".join([str(x)if x != 0 else "." for x in self.board[i]]))
28 |
29 |
30 | def number_is_valid(self, row, column, number):
31 | # check row and column
32 | for i in range(9):
33 | if self.board[row][i] == number or self.board[i][column] == number:
34 | return False
35 |
36 | # check square
37 | start_column = column // 3 * 3
38 | start_row = row // 3 * 3
39 | for i in range(3):
40 | for j in range(3):
41 | if self.board[i + start_row][j + start_column] == number:
42 | return False
43 | return True
44 |
45 |
46 | def solve(self):
47 | # find an empty cell
48 | for r in range(9):
49 | for c in range(9):
50 | if self.board[r][c] == 0:
51 | # for every empty cell fill a valid number into it
52 | for n in range(1, 10):
53 | if self.number_is_valid(r, c, n):
54 | self.board[r][c] = n
55 | # is it solved?
56 | yield from self.solve()
57 | # backtrack
58 | self.board[r][c] = 0
59 | return False
60 | yield True
61 |
62 | def main():
63 | # example board, 4 possible Solutions
64 | board = [[0, 7, 6, 0, 1, 3, 0, 0, 0],
65 | [0, 4, 0, 0, 0, 0, 0, 0, 0],
66 | [0, 0, 8, 6, 9, 0, 7, 0, 0],
67 | [0, 5, 0, 0, 6, 9, 0, 3, 0],
68 | [0, 0, 0, 0, 0, 0, 5, 4, 0],
69 | [0, 8, 0, 7, 3, 0, 0, 0, 0],
70 | [5, 1, 0, 0, 2, 6, 8, 0, 0],
71 | [0, 0, 7, 1, 0, 0, 9, 0, 0],
72 | [0, 0, 0, 0, 4, 0, 0, 6, 0]]
73 |
74 | sudoku = Sudoku(board)
75 | print("Difficulty: " + sudoku.evaluate())
76 |
77 | # needed for multiple solutions
78 | counter = 0
79 | for solutions in sudoku.solve():
80 | print()
81 | sudoku.print()
82 | counter += 1
83 | print("Solutions: ", counter)
84 |
85 |
86 | if __name__ == "__main__":
87 | main()
88 |
--------------------------------------------------------------------------------