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
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.

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
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.

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:

 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.