Walkthrough of writing the logic for our Sudoku Board.
When we eventually run the sudoku.py
script, we first need to create a Python representation of the Sudoku board that we pass in as an argument. We’ll start off with a class representing a Sudoku board in sudoku.py
:
1 2 3 4 | class SudokuBoard(object):
"""
Sudoku Board representation
"""
|
When a new board is created, e.g. new_board = SudokuBoard()
, it should initialize with the name of the .sudoku
file (which we will create towards the end of the tutorial) we pass in as an argument:
1 2 3 4 5 6 | class SudokuBoard(object):
"""
Sudoku Board representation
"""
def __init__(self, board_file):
self.board = board_file
|
Now we actually want to parse out the board_file
by making a matrix, or a list of lists. We can do this by creating a private function (denoted by two leading _
), and setting self.board
equal to that private function:
1 2 3 4 5 6 7 8 9 | class SudokuBoard(object):
"""
Sudoku Board representation
"""
def __init__(self, board_file):
self.board = __create_board(board_file)
def __create_board(self, board_file):
pass
|
With __create_board
, we will iterate over each line in the .sudoku
file, and each integer in the line, and create the matrix representing the Sudoku board.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class SudokuBoard(object):
"""
Sudoku Board representation
"""
def __init__(self, board_file):
self.board = self.__create_board(board_file)
def __create_board(self, board_file):
# create an initial matrix, or a list of a list
board = []
# iterate over each line
# then iterate over each character
# Raise an error if there are not 9 lines
# Return the constructed board
return board
|
Now let’s iterate over each line in the board file, adding each character to the board
variable, with the appropriate errors raised if the length of a line is not equal to 9, or the number of lines in the file is not equal to 9.
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 | class SudokuBoard(object):
"""
Sudoku Board representation
"""
def __init__(self, board_file):
self.board = self.__create_board(board_file)
def __create_board(self, board_file):
# create an initial matrix, or a list of a list
board = []
# iterate over each line
for line in board_file:
line = line.strip()
# raise error if line is longer or shorter than 9 characters
if len(line) != 9:
board = []
raise SudokuError(
"Each line in the sudoku puzzle must be 9 chars long."
)
# create a list for the line
board.append([])
# then iterate over each character
for c in line:
# Raise an error if the character is not an integer
if not c.isdigit():
raise SudokuError(
"Valid characters for a sudoku puzzle must be in 0-9"
)
# Add to the latest list for the line
board[-1].append(int(c))
# Raise an error if there are not 9 lines
if len(board) != 9:
raise SudokuError("Each sudoku puzzle must be 9 lines long")
# Return the constructed board
return board
|
OK so we initialized SudokuBoard
object with a board_file
(e.g. debug.sudoku
), and created a list of lists (a matrix) to represent the Sudoku board to solve.
Next, we’ll create an object representing the game itself, SudokuGame
.
We are representing the game as a Python class because we want to maintain the state of the game. Here, we maintain the state of the board (e.g. every time the user inputs a number), as well as check to see if the latest board state is actually a “win”.
SudokuGame
will be initialized with our board_file
to create the actual board for the game:
1 2 3 4 5 6 7 8 | class SudokuGame(object):
"""
A Sudoku game, in charge of storing the state of the board and checking
whether the puzzle is completed.
"""
def __init__(self, board_file):
self.board_file = board_file
self.start_puzzle = SudokuBoard(board_file).board
|
Next we’ll set up the puzzle for the user to play:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class SudokuGame(object):
"""
A Sudoku game, in charge of storing the state of the board and checking
whether the puzzle is completed.
"""
def __init__(self, board_file):
self.board_file = board_file
self.start_puzzle = SudokuBoard(board_file).board
def start(self):
self.game_over = False
self.puzzle = []
for i in xrange(9):
self.puzzle.append([])
for j in xrange(9):
self.puzzle[i].append(self.start_puzzle[i][j])
|
In start()
, we set a flag, self.game_over
, to False
. When the user plays the game and correctly solves it, we’ll set it to True
.
We create a copy of the puzzle for two reasons: to create the functionality of clearing the board when the user wants to start over, as well as to check the inputted answers against the start board.
self.puzzle
to self.start_puzzle
. If we were to, Python actually does not create a brand new object; the variable name self.puzzle
would just point to self.start_puzzle
. So any edits to self.start_puzzle
(e.g. when the user fills in numbers) it would change the self.puzzle
. We don’t want this – we want to preserve the start puzzle.
Now we will add the logic to actually checking the answers to see if the user has won the puzzle.
We’ll create a function called check_win
that will check the board’s rows, columns, and each 3x3 square:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class SudokuGame(object):
# <-- snip -->
def check_win(self):
for row in xrange(9):
if not self.__check_row(row):
return False
for column in xrange(9):
if not self.__check_column(column):
return False
for row in xrange(3):
for column in xrange(3):
if not self.__check_square(row, column):
return False
self.game_over = True
return True
|
You might have noticed that there are three helper functions we have yet to define, but basically, we are iterating over each row, each column, and each 3x3 square. If either the row, column, or square does not pass some logic (which we will implement next), we return False
.
However, if all of them do pass our logic, we set the game_over
flag to True
, and return True
. Later when we implement the user interface, we will be referring to the game_over
flag.
Now for each helper function that is the logic of checking the inputted numbers:
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 | class SudokuGame(object):
# <-- snip -->
def check_win(self):
for row in xrange(9):
if not self.__check_row(row):
return False
for column in xrange(9):
if not self.__check_column(column):
return False
for row in xrange(3):
for column in xrange(3):
if not self.__check_square(row, column):
return False
self.game_over = True
return True
def __check_block(self, block):
return set(block) == set(range(1, 10))
def __check_row(self, row):
return self.__check_block(self.puzzle[row])
def __check_column(self, column):
return self.__check_block(
[self.puzzle[row][column] for row in xrange(9)]
)
def __check_square(self, row, column):
return self.__check_block(
[
self.puzzle[r][c]
for r in xrange(row * 3, (row + 1) * 3)
for c in xrange(column * 3, (column + 1) * 3)
]
)
|
We have the main logic method, __check_block
. This returns True
if the block that we’ve passed in (either the row, column, or square) is equal to set(range(1,10))
. The set(range(1, 10))
means that only numbers 1 through 9 (the last number in range
in Python is excluded) are valid. If the block that is passed into the method, then False
is returned.
__check_row
and __check_column
iterates over each row/column of the puzzle with the user’s input, and passes it to __check_block
. The same with __check_square
, but rather than a row or a column, it pulls out a 3x3 square.
So that’s it for the SudokuGame
and SudokuBoard
objects! Here is the complete code for those two classes:
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 | class SudokuBoard(object):
"""
Sudoku Board representation
"""
def __init__(self, board_file):
self.board = self.__create_board(board_file)
def __create_board(self, board_file):
board = []
for line in board_file:
line = line.strip()
if len(line) != 9:
raise SudokuError(
"Each line in the sudoku puzzle must be 9 chars long."
)
board.append([])
for c in line:
if not c.isdigit():
raise SudokuError(
"Valid characters for a sudoku puzzle must be in 0-9"
)
board[-1].append(int(c))
if len(board) != 9:
raise SudokuError("Each sudoku puzzle must be 9 lines long")
return board
class SudokuGame(object):
"""
A Sudoku game, in charge of storing the state of the board and checking
whether the puzzle is completed.
"""
def __init__(self, board_file):
self.board_file = board_file
self.start_puzzle = SudokuBoard(board_file).board
def start(self):
self.game_over = False
self.puzzle = []
for i in xrange(9):
self.puzzle.append([])
for j in xrange(9):
self.puzzle[i].append(self.start_puzzle[i][j])
def check_win(self):
for row in xrange(9):
if not self.__check_row(row):
return False
for column in xrange(9):
if not self.__check_column(column):
return False
for row in xrange(3):
for column in xrange(3):
if not self.__check_square(row, column):
return False
self.game_over = True
return True
def __check_block(self, block):
return set(block) == set(range(1, 10))
def __check_row(self, row):
return self.__check_block(self.puzzle[row])
def __check_column(self, column):
return self.__check_block(
[self.puzzle[row][column] for row in xrange(9)]
)
def __check_square(self, row, column):
return self.__check_block(
[
self.puzzle[r][c]
for r in xrange(row * 3, (row + 1) * 3)
for c in xrange(column * 3, (column + 1) * 3)
]
)
|
Next up: adding the graphical user interface of the board.