Wrapping up the Sudoku tutorial by putting the finishing touches of the script.
We’d like to allow the user to pass in the desired board that he or she wants to solve. We’ll use Python’s argument parser here for that.
Remember, in Part 0 we defined a few global constant variables. One in particular was BOARDS
, which was set to a list of strings. This list will define the choices available to the user.
Let’s setup our argument parser:
1 2 3 4 5 6 7 | def parse_arguments():
"""
Parses arguments of the form:
sudoku.py <board name>
Where `board name` must be in the `BOARD` list
"""
arg_parser = argparse.ArgumentParser()
|
Here we instantiate ArgumentParser
from the argparse
library. Now let’s add an argument:
1 2 3 4 5 6 7 8 9 10 11 12 | def parse_arguments():
"""
Parses arguments of the form:
sudoku.py <board name>
Where `board name` must be in the `BOARD` list
"""
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("--board",
help="Desired board name",
type=str,
choices=BOARDS,
required=True)
|
The argument will have the flag --board
, so when the user runs the script, it will look like python sudoku.py --board BOARD_NAME
. The BOARD_NAME
will be limited to the available choices defined in BOARDS
.
We’re also telling the argument parser to expect a string input, and set it to be required (the argument parser will take care of erroring out for us in case the user does not supply a BOARD_NAME
or the --board
flag.
The help
attribute allows the user to run python sudoku.py -h
or python sudoku.py --help
, so the user will understand what the purpose of the --board
flag is.
Lastly, we parse out the values for the arguments. The vars()
built-in function creates a dictionary for us, where the key is the argument flag name (without the two leading dashes, board
), and the value is the user input. And we return the value of the board
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | def parse_arguments():
"""
Parses arguments of the form:
sudoku.py <board name>
Where `board name` must be in the `BOARD` list
"""
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("--board",
help="Desired board name",
type=str,
choices=BOARDS,
required=True)
# Creates a dictionary of keys = argument flag, and value = argument
args = arg_parser.parse_args()
return args['board']
|
Now in putting it all together, we’ll define the logic for when the user is running this script.
Like in earlier tutorials, we want to add the boilerplate code, if __name__ == '__main__'
to ensure this executes via the command line.
1 | if __name__ == '__main__':
|
The first thing we want our script to do when the user calls python sudoku.py ...
is to parse the arguments and return the board name for us:
1 2 | if __name__ == '__main__':
board_name = parse_arguments()
|
Next, with opening the Sudoku board file (the board that the user passed in as an argument), we initialize a game and call the start()
method:
1 2 3 4 5 6 | if __name__ == '__main__':
board_name = parse_arguments()
with open('%s.sudoku' % board_name, 'r') as boards_file:
game = SudokuGame(boards_file)
game.start()
|
Next, we’ll create a “root” widget, which is actually passed in on the next line as the the parent widget when instantiating SudokuUI
(no need to assign the newly instantiated SudokuUI
class since we will not be calling any methods directly; all methods will be called via the __initUI
method).
1 2 3 4 5 6 7 8 9 | if __name__ == '__main__':
board_name = parse_arguments()
with open('%s.sudoku' % board_name, 'r') as boards_file:
game = SudokuGame(boards_file)
game.start()
root = Tk()
SudokuUI(root, game)
|
We then draw the parent (root) widget a little bigger than the height and width of the puzzle:
1 2 3 4 5 6 7 8 9 10 | if __name__ == '__main__':
board_name = parse_arguments()
with open('%s.sudoku' % board_name, 'r') as boards_file:
game = SudokuGame(boards_file)
game.start()
root = Tk()
SudokuUI(root, game)
root.geometry("%dx%d" % (WIDTH, HEIGHT + 40))
|
Finally, we call mainloop
on the root
to start the Sudoku program (and in effect, launches the window/widget we built with the SudokuUI
):
1 2 3 4 5 6 7 8 9 10 11 | if __name__ == '__main__':
board_name = parse_arguments()
with open('%s.sudoku' % board_name, 'r') as boards_file:
game = SudokuGame(boards_file)
game.start()
root = Tk()
SudokuUI(root, game)
root.geometry("%dx%d" % (WIDTH, HEIGHT + 40))
root.mainloop()
|
The mainloop will shut down when the user closes the puzzle window.
Here’s the complete script:
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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | import argparse
from Tkinter import Tk, Canvas, Frame, Button, BOTH, TOP, BOTTOM
BOARDS = ['debug', 'n00b', 'l33t', 'error'] # Available sudoku boards
MARGIN = 20 # Pixels around the board
SIDE = 50 # Width of every board cell.
WIDTH = HEIGHT = MARGIN * 2 + SIDE * 9 # Width and height of the whole board
class SudokuError(Exception):
"""
An application specific error.
"""
pass
def parse_arguments():
"""
Parses arguments of the form:
sudoku.py <board name>
Where `board name` must be in the `BOARD` list
"""
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("--board",
help="Desired board name",
type=str,
choices=BOARDS,
required=True)
# Creates a dictionary of keys = argument flag, and value = argument
args = vars(arg_parser.parse_args())
return args['board']
class SudokuUI(Frame):
"""
The Tkinter UI, responsible for drawing the board and accepting user input.
"""
def __init__(self, parent, game):
self.game = game
Frame.__init__(self, parent)
self.parent = parent
self.row, self.col = -1, -1
self.__initUI()
def __initUI(self):
self.parent.title("Sudoku")
self.pack(fill=BOTH)
self.canvas = Canvas(self,
width=WIDTH,
height=HEIGHT)
self.canvas.pack(fill=BOTH, side=TOP)
clear_button = Button(self,
text="Clear answers",
command=self.__clear_answers)
clear_button.pack(fill=BOTH, side=BOTTOM)
self.__draw_grid()
self.__draw_puzzle()
self.canvas.bind("<Button-1>", self.__cell_clicked)
self.canvas.bind("<Key>", self.__key_pressed)
def __draw_grid(self):
"""
Draws grid divided with blue lines into 3x3 squares
"""
for i in xrange(10):
color = "blue" if i % 3 == 0 else "gray"
x0 = MARGIN + i * SIDE
y0 = MARGIN
x1 = MARGIN + i * SIDE
y1 = HEIGHT - MARGIN
self.canvas.create_line(x0, y0, x1, y1, fill=color)
x0 = MARGIN
y0 = MARGIN + i * SIDE
x1 = WIDTH - MARGIN
y1 = MARGIN + i * SIDE
self.canvas.create_line(x0, y0, x1, y1, fill=color)
def __draw_puzzle(self):
self.canvas.delete("numbers")
for i in xrange(9):
for j in xrange(9):
answer = self.game.puzzle[i][j]
if answer != 0:
x = MARGIN + j * SIDE + SIDE / 2
y = MARGIN + i * SIDE + SIDE / 2
original = self.game.start_puzzle[i][j]
color = "black" if answer == original else "sea green"
self.canvas.create_text(
x, y, text=answer, tags="numbers", fill=color
)
def __draw_cursor(self):
self.canvas.delete("cursor")
if self.row >= 0 and self.col >= 0:
x0 = MARGIN + self.col * SIDE + 1
y0 = MARGIN + self.row * SIDE + 1
x1 = MARGIN + (self.col + 1) * SIDE - 1
y1 = MARGIN + (self.row + 1) * SIDE - 1
self.canvas.create_rectangle(
x0, y0, x1, y1,
outline="red", tags="cursor"
)
def __draw_victory(self):
# create a oval (which will be a circle)
x0 = y0 = MARGIN + SIDE * 2
x1 = y1 = MARGIN + SIDE * 7
self.canvas.create_oval(
x0, y0, x1, y1,
tags="victory", fill="dark orange", outline="orange"
)
# create text
x = y = MARGIN + 4 * SIDE + SIDE / 2
self.canvas.create_text(
x, y,
text="You win!", tags="victory",
fill="white", font=("Arial", 32)
)
def __cell_clicked(self, event):
if self.game.game_over:
return
x, y = event.x, event.y
if (MARGIN < x < WIDTH - MARGIN and MARGIN < y < HEIGHT - MARGIN):
self.canvas.focus_set()
# get row and col numbers from x,y coordinates
row, col = (y - MARGIN) / SIDE, (x - MARGIN) / SIDE
# if cell was selected already - deselect it
if (row, col) == (self.row, self.col):
self.row, self.col = -1, -1
elif self.game.puzzle[row][col] == 0:
self.row, self.col = row, col
else:
self.row, self.col = -1, -1
self.__draw_cursor()
def __key_pressed(self, event):
if self.game.game_over:
return
if self.row >= 0 and self.col >= 0 and event.char in "1234567890":
self.game.puzzle[self.row][self.col] = int(event.char)
self.col, self.row = -1, -1
self.__draw_puzzle()
self.__draw_cursor()
if self.game.check_win():
self.__draw_victory()
def __clear_answers(self):
self.game.start()
self.canvas.delete("victory")
self.__draw_puzzle()
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)
]
)
if __name__ == '__main__':
board_name = parse_arguments()
with open('%s.sudoku' % board_name, 'r') as boards_file:
game = SudokuGame(boards_file)
game.start()
root = Tk()
SudokuUI(root, game)
root.geometry("%dx%d" % (WIDTH, HEIGHT + 40))
root.mainloop()
|
Now let’s make a the Sudoku boards that the user can pass in. Save all of these files within the same directory as your sudoku.py
file with the extension .sudoku
:
debug.sudoku
:
217385469
385469712
496721835
524816973
639547281
871293546
762158394
953674128
148932650
error.sudoku
:
809200000
200980160
030007008
008600500
400000002
003008400
300400050
045032006
n00b.sudoku
:
210000400
380400702
000720000
024806900
000000000
001203540
000058000
903004028
008000057
l33t.sudoku
:
809200000
200980160
030007008
008600500
400000002
003008400
300400050
045032006
000006205
If you were to go back to your terminal, within the gui
directory, you would see following files:
1 2 | $ ls
debug.sudoku error.sudoku l33t.sudoku n00b.sudoku sudoku.py
|
Now let’s try it out! Within your terminal, within the same gui
directory:
1 | $ python sudoku.py --board debug
|
When you’re ready, try it with n00b
, l33t
, and error
boards.
Was that so bad? I hope not!