Part 2: Sudoku Board Game Logic

Walkthrough of writing the logic for our Sudoku Board.

Creating the 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

classSudokuBoard(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:

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:

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

classSudokuBoard(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 listboard=[]# iterate over each line# then iterate over each character# Raise an error if there are not 9 lines# Return the constructed boardreturnboard

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.

classSudokuBoard(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 listboard=[]# iterate over each lineforlineinboard_file:line=line.strip()# raise error if line is longer or shorter than 9 charactersiflen(line)!=9:board=[]raiseSudokuError("Each line in the sudoku puzzle must be 9 chars long.")# create a list for the lineboard.append([])# then iterate over each characterforcinline:# Raise an error if the character is not an integerifnotc.isdigit():raiseSudokuError("Valid characters for a sudoku puzzle must be in 0-9")# Add to the latest list for the lineboard[-1].append(int(c))# Raise an error if there are not 9 linesiflen(board)!=9:raiseSudokuError("Each sudoku puzzle must be 9 lines long")# Return the constructed boardreturnboard

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.

Creating the Sudoku Game

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

classSudokuGame(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_fileself.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

classSudokuGame(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_fileself.start_puzzle=SudokuBoard(board_file).boarddefstart(self):self.game_over=Falseself.puzzle=[]foriinxrange(9):self.puzzle.append([])forjinxrange(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.

NOTE: We simply can not set 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:

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:

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:

classSudokuBoard(object):""" Sudoku Board representation """def__init__(self,board_file):self.board=self.__create_board(board_file)def__create_board(self,board_file):board=[]forlineinboard_file:line=line.strip()iflen(line)!=9:raiseSudokuError("Each line in the sudoku puzzle must be 9 chars long.")board.append([])forcinline:ifnotc.isdigit():raiseSudokuError("Valid characters for a sudoku puzzle must be in 0-9")board[-1].append(int(c))iflen(board)!=9:raiseSudokuError("Each sudoku puzzle must be 9 lines long")returnboardclassSudokuGame(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_fileself.start_puzzle=SudokuBoard(board_file).boarddefstart(self):self.game_over=Falseself.puzzle=[]foriinxrange(9):self.puzzle.append([])forjinxrange(9):self.puzzle[i].append(self.start_puzzle[i][j])defcheck_win(self):forrowinxrange(9):ifnotself.__check_row(row):returnFalseforcolumninxrange(9):ifnotself.__check_column(column):returnFalseforrowinxrange(3):forcolumninxrange(3):ifnotself.__check_square(row,column):returnFalseself.game_over=TruereturnTruedef__check_block(self,block):returnset(block)==set(range(1,10))def__check_row(self,row):returnself.__check_block(self.puzzle[row])def__check_column(self,column):returnself.__check_block([self.puzzle[row][column]forrowinxrange(9)])def__check_square(self,row,column):returnself.__check_block([self.puzzle[r][c]forrinxrange(row*3,(row+1)*3)forcinxrange(column*3,(column+1)*3)])