Skip to content

Sudoku solver #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vscode/extensions.json
SudokuSolver/__pycache__/sudoku.cpython-38.pyc
27 changes: 27 additions & 0 deletions SudokuSolver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Sudoku Solver
## Description
A minimalist Python application which solves a given Sudoku problem in a *.csv file* following certain *formatting rules*.

### Formatting rules for .txt Sudokus
- The **first row** of the *.csv file* must be the following: `a,b,c,d,e,f,g,h`.
- The following rows will be the **numbers** of the sudoku given. The number **zero (0)** represents an empty cell or *naked cell*.
- The numbers will be separatted by commas, formatting the **columns**.
- Example of *.csv file* can be found under the folder **examples**.

## Requirements

This projects uses the following external libraries:
- **NumPy**
- **Pandas**

To be able to run this project, execute: `pip install -r requirements.txt`

## Steps To Execution

- Under the folder *SudokuSolver*, run: `python3 main.py`
- 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**.
- Example of input: **examples/ex2**

## TODOS
- Implement a simple **GUI** by using *tkinter* library.
- In the above mentioned **GUI**, implement an speed regulator to be able to see how the algorithm tests, fails, and backtracks.
10 changes: 10 additions & 0 deletions SudokuSolver/examples/ex1.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
a,b,c,d,e,f,g,h,i
0,0,4,0,0,2,6,5,0
0,0,0,0,0,8,0,9,0
7,0,6,0,0,0,8,0,4
0,5,0,0,4,9,0,0,0
0,7,0,0,0,0,5,2,3
0,0,8,5,7,0,0,0,0
8,0,0,3,2,0,0,0,6
0,6,0,9,5,0,0,8,0
0,0,0,0,0,6,7,4,0
10 changes: 10 additions & 0 deletions SudokuSolver/examples/ex2.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
a,b,c,d,e,f,g,h,i
0,0,0,7,0,8,0,0,0
0,0,7,0,4,0,0,5,0
0,3,0,0,0,2,8,0,0
3,0,4,0,8,0,0,0,2
5,7,0,2,0,0,0,0,3
0,2,0,0,7,3,0,6,5
0,0,0,6,5,0,0,2,0
8,0,0,0,3,0,6,0,9
0,0,0,8,0,0,0,7,4
10 changes: 10 additions & 0 deletions SudokuSolver/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from read_sudoku import import_sudoku
from sudoku_solver import solve_sudoku

def main() -> None:

sudoku_to_be_solved = import_sudoku()
solve_sudoku(sudoku_to_be_solved)

if __name__ == '__main__':
main()
7 changes: 7 additions & 0 deletions SudokuSolver/move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Move:

def __init__(self, number: int, row: int, column: int) -> None:

self.number = number
self.row = row
self.column = column
45 changes: 45 additions & 0 deletions SudokuSolver/read_sudoku.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import numpy as np
import pandas as pd
from sudoku import Sudoku

MINIMUM_NUMBERS_FOR_UNIQUE_SOLUTION = 17
NUMBER_REGIONS = 9

def import_sudoku() -> Sudoku:

filename = input('Enter the filename\'s path of the sudoku to be solved: ') + '.csv'

read_sudoku = pd.read_csv(filename).to_numpy()

sudoku = Sudoku(read_sudoku)

if is_solvable(sudoku):
return sudoku

def is_solvable(sudoku: Sudoku) -> bool:

# Check there are at least 17 non 0 numbers for unique solution

if sudoku.grid[sudoku.grid != 0].size < MINIMUM_NUMBERS_FOR_UNIQUE_SOLUTION:
return False

# Check rows' legality
for row in sudoku.grid:
row = row[row != 0]
if np.unique(row).size != row.size:
return False

# Check columns' legality
for column in np.nditer(sudoku.grid, flags = ['external_loop'], order='C'):
column = column[column != 0]
if np.unique(column).size != column.size:
return False

# Check regions' legality
for region_number in range(NUMBER_REGIONS):
region = getattr(sudoku, "{}{}".format('region',region_number+1))
region = region[region != 0]
if np.unique(region).size != region.size:
return False

return True
69 changes: 69 additions & 0 deletions SudokuSolver/sudoku.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import numpy as np
from move import Move

class Sudoku:

def __init__(self, grid: np.ndarray) -> None:

self.grid = grid
self.region1 = self.grid[:3, :3]
self.region2 = self.grid[:3, 3:6]
self.region3 = self.grid[:3, 6:9]
self.region4 = self.grid[3:6, :3]
self.region5 = self.grid[3:6, 3:6]
self.region6 = self.grid[3:6, 6:9]
self.region7 = self.grid[6:9, :3]
self.region8 = self.grid[6:9, 3:6]
self.region9 = self.grid[6:9, 6:9]

def put_number(self, move: Move) -> None:

self.grid[move.row, move.column] = move.number

def is_legal_state(self, new_move: Move) -> bool:

# Make the movement, take copies of the grid and remove the movement

self.put_number(new_move)

row_to_check = self.grid[new_move.row, :].copy()
column_to_check = self.grid[:, new_move.column].copy()
region_to_check = self.get_region_from_move(new_move).copy()

reset_move = Move(0, new_move.row, new_move.column)
self.put_number(reset_move)

# Remove 0's from the areas to be checked

row_to_check = row_to_check[row_to_check != 0]
column_to_check = column_to_check[column_to_check != 0]
region_to_check = region_to_check[region_to_check != 0]

# Check if there are repeated numbers after the move

row_check = np.unique(row_to_check).size == row_to_check.size
column_check = np.unique(column_to_check).size == column_to_check.size
region_check = np.unique(region_to_check).size == region_to_check.size

return row_check and column_check and region_check

def get_region_from_move(self, move: Move) -> np.ndarray :

if move.row <= 2 and move.column <= 2:
return self.region1
elif move.row <= 2 and move.column <= 5 and move.column > 2:
return self.region2
elif move.row <= 2 and move.column <= 9 and move.column > 5:
return self.region3
elif move.row <= 5 and move.row > 2 and move.column <= 2:
return self.region4
elif move.row <= 5 and move.row > 2 and move.column <= 5 and move.column > 2:
return self.region5
elif move.row <= 5 and move.row > 2 and move.column <= 9 and move.column > 5:
return self.region6
elif move.row <= 9 and move.row > 5 and move.column <= 2:
return self.region7
elif move.row <= 9 and move.row > 5 and move.column <= 5 and move.column > 2:
return self.region8
else:
return self.region9
46 changes: 46 additions & 0 deletions SudokuSolver/sudoku_solver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import numpy as np
from move import Move
from sudoku import Sudoku

LEGIT_DIGITS = list(range(1, 10))
SUDOKU_DIMENSION = 9


def find_naked_cell(sudoku: Sudoku) -> tuple:

'''
Finds the first 0 labeled cell in the sudoku
'''

naked_cells_indexes = np.where(sudoku.grid == 0)
if naked_cells_indexes[0].size > 0:
if naked_cells_indexes[0][0].size > 0 and naked_cells_indexes[1][0].size > 0:
return (naked_cells_indexes[0][0], naked_cells_indexes[1][0])

return naked_cells_indexes

def solve_sudoku(sudoku: Sudoku,) -> bool:
'''
Solve a given legal sudoku by appliying a
backtracking strategy.
'''
naked_cell = find_naked_cell(sudoku)

if(not naked_cell[0].size > 0):
print("The solution to the proposed Sudoku is: \n", sudoku.grid)
return True

move = Move(0, naked_cell[0], naked_cell[1])

for digit in LEGIT_DIGITS:
move.number = digit
if sudoku.is_legal_state(move):
sudoku.put_number(move)
print(sudoku.grid)
if(solve_sudoku(sudoku)):
return True

move.number = 0
sudoku.put_number(move)

return False