Skip to content

Commit d5e3927

Browse files
authored
Merge pull request larymak#63 from RgrMz/sudoku-solver
Sudoku solver
2 parents e747969 + 06617a5 commit d5e3927

File tree

9 files changed

+226
-0
lines changed

9 files changed

+226
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.vscode/extensions.json
2+
SudokuSolver/__pycache__/sudoku.cpython-38.pyc

SudokuSolver/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Sudoku Solver
2+
## Description
3+
A minimalist Python application which solves a given Sudoku problem in a *.csv file* following certain *formatting rules*.
4+
5+
### Formatting rules for .txt Sudokus
6+
- The **first row** of the *.csv file* must be the following: `a,b,c,d,e,f,g,h`.
7+
- The following rows will be the **numbers** of the sudoku given. The number **zero (0)** represents an empty cell or *naked cell*.
8+
- The numbers will be separatted by commas, formatting the **columns**.
9+
- Example of *.csv file* can be found under the folder **examples**.
10+
11+
## Requirements
12+
13+
This projects uses the following external libraries:
14+
- **NumPy**
15+
- **Pandas**
16+
17+
To be able to run this project, execute: `pip install -r requirements.txt`
18+
19+
## Steps To Execution
20+
21+
- Under the folder *SudokuSolver*, run: `python3 main.py`
22+
- The path for a *.csv file* following the above formatting rules will be required. **YOU SHOULDN'T TYPE THE .CSV EXTENSION ALONG THE NAME OF THE FILE**.
23+
- Example of input: **examples/ex2**
24+
25+
## TODOS
26+
- Implement a simple **GUI** by using *tkinter* library.
27+
- In the above mentioned **GUI**, implement an speed regulator to be able to see how the algorithm tests, fails, and backtracks.

SudokuSolver/examples/ex1.csv

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
a,b,c,d,e,f,g,h,i
2+
0,0,4,0,0,2,6,5,0
3+
0,0,0,0,0,8,0,9,0
4+
7,0,6,0,0,0,8,0,4
5+
0,5,0,0,4,9,0,0,0
6+
0,7,0,0,0,0,5,2,3
7+
0,0,8,5,7,0,0,0,0
8+
8,0,0,3,2,0,0,0,6
9+
0,6,0,9,5,0,0,8,0
10+
0,0,0,0,0,6,7,4,0

SudokuSolver/examples/ex2.csv

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
a,b,c,d,e,f,g,h,i
2+
0,0,0,7,0,8,0,0,0
3+
0,0,7,0,4,0,0,5,0
4+
0,3,0,0,0,2,8,0,0
5+
3,0,4,0,8,0,0,0,2
6+
5,7,0,2,0,0,0,0,3
7+
0,2,0,0,7,3,0,6,5
8+
0,0,0,6,5,0,0,2,0
9+
8,0,0,0,3,0,6,0,9
10+
0,0,0,8,0,0,0,7,4

SudokuSolver/main.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from read_sudoku import import_sudoku
2+
from sudoku_solver import solve_sudoku
3+
4+
def main() -> None:
5+
6+
sudoku_to_be_solved = import_sudoku()
7+
solve_sudoku(sudoku_to_be_solved)
8+
9+
if __name__ == '__main__':
10+
main()

SudokuSolver/move.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class Move:
2+
3+
def __init__(self, number: int, row: int, column: int) -> None:
4+
5+
self.number = number
6+
self.row = row
7+
self.column = column

SudokuSolver/read_sudoku.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import numpy as np
2+
import pandas as pd
3+
from sudoku import Sudoku
4+
5+
MINIMUM_NUMBERS_FOR_UNIQUE_SOLUTION = 17
6+
NUMBER_REGIONS = 9
7+
8+
def import_sudoku() -> Sudoku:
9+
10+
filename = input('Enter the filename\'s path of the sudoku to be solved: ') + '.csv'
11+
12+
read_sudoku = pd.read_csv(filename).to_numpy()
13+
14+
sudoku = Sudoku(read_sudoku)
15+
16+
if is_solvable(sudoku):
17+
return sudoku
18+
19+
def is_solvable(sudoku: Sudoku) -> bool:
20+
21+
# Check there are at least 17 non 0 numbers for unique solution
22+
23+
if sudoku.grid[sudoku.grid != 0].size < MINIMUM_NUMBERS_FOR_UNIQUE_SOLUTION:
24+
return False
25+
26+
# Check rows' legality
27+
for row in sudoku.grid:
28+
row = row[row != 0]
29+
if np.unique(row).size != row.size:
30+
return False
31+
32+
# Check columns' legality
33+
for column in np.nditer(sudoku.grid, flags = ['external_loop'], order='C'):
34+
column = column[column != 0]
35+
if np.unique(column).size != column.size:
36+
return False
37+
38+
# Check regions' legality
39+
for region_number in range(NUMBER_REGIONS):
40+
region = getattr(sudoku, "{}{}".format('region',region_number+1))
41+
region = region[region != 0]
42+
if np.unique(region).size != region.size:
43+
return False
44+
45+
return True

SudokuSolver/sudoku.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import numpy as np
2+
from move import Move
3+
4+
class Sudoku:
5+
6+
def __init__(self, grid: np.ndarray) -> None:
7+
8+
self.grid = grid
9+
self.region1 = self.grid[:3, :3]
10+
self.region2 = self.grid[:3, 3:6]
11+
self.region3 = self.grid[:3, 6:9]
12+
self.region4 = self.grid[3:6, :3]
13+
self.region5 = self.grid[3:6, 3:6]
14+
self.region6 = self.grid[3:6, 6:9]
15+
self.region7 = self.grid[6:9, :3]
16+
self.region8 = self.grid[6:9, 3:6]
17+
self.region9 = self.grid[6:9, 6:9]
18+
19+
def put_number(self, move: Move) -> None:
20+
21+
self.grid[move.row, move.column] = move.number
22+
23+
def is_legal_state(self, new_move: Move) -> bool:
24+
25+
# Make the movement, take copies of the grid and remove the movement
26+
27+
self.put_number(new_move)
28+
29+
row_to_check = self.grid[new_move.row, :].copy()
30+
column_to_check = self.grid[:, new_move.column].copy()
31+
region_to_check = self.get_region_from_move(new_move).copy()
32+
33+
reset_move = Move(0, new_move.row, new_move.column)
34+
self.put_number(reset_move)
35+
36+
# Remove 0's from the areas to be checked
37+
38+
row_to_check = row_to_check[row_to_check != 0]
39+
column_to_check = column_to_check[column_to_check != 0]
40+
region_to_check = region_to_check[region_to_check != 0]
41+
42+
# Check if there are repeated numbers after the move
43+
44+
row_check = np.unique(row_to_check).size == row_to_check.size
45+
column_check = np.unique(column_to_check).size == column_to_check.size
46+
region_check = np.unique(region_to_check).size == region_to_check.size
47+
48+
return row_check and column_check and region_check
49+
50+
def get_region_from_move(self, move: Move) -> np.ndarray :
51+
52+
if move.row <= 2 and move.column <= 2:
53+
return self.region1
54+
elif move.row <= 2 and move.column <= 5 and move.column > 2:
55+
return self.region2
56+
elif move.row <= 2 and move.column <= 9 and move.column > 5:
57+
return self.region3
58+
elif move.row <= 5 and move.row > 2 and move.column <= 2:
59+
return self.region4
60+
elif move.row <= 5 and move.row > 2 and move.column <= 5 and move.column > 2:
61+
return self.region5
62+
elif move.row <= 5 and move.row > 2 and move.column <= 9 and move.column > 5:
63+
return self.region6
64+
elif move.row <= 9 and move.row > 5 and move.column <= 2:
65+
return self.region7
66+
elif move.row <= 9 and move.row > 5 and move.column <= 5 and move.column > 2:
67+
return self.region8
68+
else:
69+
return self.region9

SudokuSolver/sudoku_solver.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import numpy as np
2+
from move import Move
3+
from sudoku import Sudoku
4+
5+
LEGIT_DIGITS = list(range(1, 10))
6+
SUDOKU_DIMENSION = 9
7+
8+
9+
def find_naked_cell(sudoku: Sudoku) -> tuple:
10+
11+
'''
12+
Finds the first 0 labeled cell in the sudoku
13+
'''
14+
15+
naked_cells_indexes = np.where(sudoku.grid == 0)
16+
if naked_cells_indexes[0].size > 0:
17+
if naked_cells_indexes[0][0].size > 0 and naked_cells_indexes[1][0].size > 0:
18+
return (naked_cells_indexes[0][0], naked_cells_indexes[1][0])
19+
20+
return naked_cells_indexes
21+
22+
def solve_sudoku(sudoku: Sudoku,) -> bool:
23+
'''
24+
Solve a given legal sudoku by appliying a
25+
backtracking strategy.
26+
'''
27+
naked_cell = find_naked_cell(sudoku)
28+
29+
if(not naked_cell[0].size > 0):
30+
print("The solution to the proposed Sudoku is: \n", sudoku.grid)
31+
return True
32+
33+
move = Move(0, naked_cell[0], naked_cell[1])
34+
35+
for digit in LEGIT_DIGITS:
36+
move.number = digit
37+
if sudoku.is_legal_state(move):
38+
sudoku.put_number(move)
39+
print(sudoku.grid)
40+
if(solve_sudoku(sudoku)):
41+
return True
42+
43+
move.number = 0
44+
sudoku.put_number(move)
45+
46+
return False

0 commit comments

Comments
 (0)