I recently read the book Clean Code and I also did some research on the SOLID principles. I'm looking for general feedback on if I was able to transpose the examples (written in Java) to Python while still maintaining a "Pythonic" program.
I wrote a simple TicTacToe game in Python using Tkinter as the GUI and I tried to strike a balance between readable, clean code and avoiding code cluttering with useless functions (get()
, set()
, isEmpty()
and other 1-liners)
Having a generic BoardGUI class specializing in TicTacToeBoardGUI
is my attempt at the open/close principle.
The 3 modules (models, views, game) implement a minimalistic MVC design where GameApplication controls the flow and events.
The part I don't know how to handle is using polymorphism to have different levels of AI (right now the AI only checks a random empty box)
http://www.filedropper.com/tictactoe (~200 lines total)
main.py
#!/usr/bin/env
import Tkinter as tk
from game import GameApplication
def convert_images():
"""Returns a dictionary with the images converted into PhotoImages Tkinter can use"""
img_dict = {}
img_dict.update({'cross': tk.PhotoImage(file='images/cross.gif')})
img_dict.update({'circle': tk.PhotoImage(file='images/circle.gif')})
return img_dict
def main():
root = tk.Tk()
root.title('Tic-Tac-Toe v1.0')
images = convert_images()
GameApplication(root, images)
root.mainloop()
if __name__ == '__main__':
main()
models.py:
#!/usr/bin/env
from collections import namedtuple
class TicTacToeBoard():
"""
Keeps track of the status of the boxes of a standard 3x3 TicTacToe game
I don't particularly like the victory, draw and _find_lines functions, any better solutions?
"""
ROWS = 3
COLUMNS = 3
EMPTY = 0
O = 1
X = 2
def __init__(self):
self.boxes = {(column, row): namedtuple('box', 'value') \
for row in range(TicTacToeBoard.ROWS) \
for column in range(TicTacToeBoard.COLUMNS)}
for __, items in self.boxes.items():
items.value = TicTacToeBoard.EMPTY
self._lines = self._find_all_lines()
def victory(self):
for line in self._lines:
if self.boxes[line[0]].value == self.boxes[line[1]].value == self.boxes[line[2]].value:
if self.boxes[line[0]].value != TicTacToeBoard.EMPTY:
return True
return False
def draw(self):
for __, box in self.boxes.items():
if box.value == TicTacToeBoard.EMPTY:
return False
return True
def _find_all_lines(self):
lines = []
for x in range(3):
lines.append(((0, x), (1, x), (2, x)))
lines.append(((x, 0), (x, 1), (x, 2)))
lines.append(((0, 0), (1, 1), (2, 2)))
lines.append(((0, 2), (1, 1), (2, 0)))
return lines
views.py:
#!/usr/bin/env
import Tkinter as tk
from math import ceil
class BoardGUI(tk.LabelFrame):
"""
Questions:
Does this class respects the open/close principle?
Am I reinventing the wheel by transforming the coordinates to row and
columns or Tkinter.canvas can do that already?
A generic board game of arbitrary size, number of columns and number of rows.
It can be inherited to serve as a point and click GUI for different board games.
The mouse coordinates should be carried by an event passed in parameter to the functions
Boxes are accessed with the tags kw from a Tkinter canvas
They are nothing but drawings; actual rows and columns are computed based on mouse position and size
Needs Tkinter compatible images to draw in boxes
Coordinates for the boxes are either in form of (box_column, box_row) or (box_top_left_x, box_top_left_y)
"""
def __init__(self, root, rows, columns, width, height, **options):
tk.LabelFrame.__init__(self, root, **options)
self._rows = rows
self._columns = columns
self._width = width
self._height = height
self.canvas = tk.Canvas(self, width=self._width, height=self._height)
self.canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.TRUE)
self._draw_boxes()
def get_box_coord(self, event):
return self._convert_pixels_to_box_coord(event.x, event.y)
def draw_image_on_box(self, coord, image):
(x, y) = self._convert_box_coord_to_pixels(coord)
self.canvas.create_image((x, y), image=image, anchor=tk.NW)
def draw_message_in_center(self, message):
self.canvas.create_text(self._find_board_center(), text=message, font=("Helvetica", 50, "bold"))
def _draw_boxes(self):
[self.canvas.create_rectangle(self._find_box_corners((column, row))) \
for row in range(self._rows) \
for column in range(self._columns)]
def _find_box_corners(self, coord):
boxWidth, boxHeight = self._find_box_dimensions()
column, row = coord[0], coord[1]
topLeftCorner = column * boxWidth
topRightCorner = (column * boxWidth) + boxWidth
bottomLeftCorner = row * boxHeight
bottomRightCorner = (row * boxHeight) + boxWidth
return (topLeftCorner, bottomLeftCorner, topRightCorner, bottomRightCorner)
def _find_board_center(self):
return ((self._width / 2), (self._height / 2))
def _find_box_dimensions(self):
return ((self._width / self._columns), (self._height / self._rows))
def _convert_box_coord_to_pixels(self, coord):
boxWidth, boxHeight = self._find_box_dimensions()
return ((coord[0] * boxWidth), (coord[1] * boxHeight))
def _convert_pixels_to_box_coord(self, x, y):
column = ceil(x / (self._height / self._columns))
row = ceil(y / (self._width / self._rows))
return (column, row)
class TicTacToeBoardGUI(BoardGUI):
"""
Allows you to draw circles or crosses on a 3x3 square gaming board
Tkinter needs to keep a reference of the images to loop. It's better to
initiate them elsewhere and pass them in parameters even if they seem like
they should be internal attributes.
"""
ROWS = 3
COLUMNS = 3
def __init__(self, root, width, height, images):
BoardGUI.__init__(self, root, TicTacToeBoardGUI.ROWS, TicTacToeBoardGUI.COLUMNS, width, height)
self.xSymbol = images['cross']
self.oSymbol = images['circle']
def draw_x_on_box(self, coord):
self.draw_image_on_box(coord, self.xSymbol)
def draw_o_on_box(self, coord):
self.draw_image_on_box(coord, self.oSymbol)
game.py:
#!/usr/bin/env
import Tkinter as tk
from views import TicTacToeBoardGUI
from models import TicTacToeBoard
from random import shuffle
class GameApplication(tk.Frame):
"""
Serves as a top level Tkinter frame to display the game and as a controller to connect
the models and the views.
I'm willingly giving this class 2 tasks because the controller as a single event to connect
and the purpose of this exercise isn't the MVC pattern but the SOLID and clean code principles
It simply instantiates the models and views necessary to play a TicTacToe game and interpret a left
mouse click as a move from the Human player
"""
CANVAS_WIDTH = 300
CANVAS_HEIGHT = 300
def __init__(self, root, images, **options):
tk.Frame.__init__(self, root, **options)
self._boardModel = TicTacToeBoard()
self._boardView = TicTacToeBoardGUI(root, GameApplication.CANVAS_WIDTH, GameApplication.CANVAS_HEIGHT, images)
self._boardView.pack()
self._connect_click_event()
if self._ai_goes_first():
self._ai_plays()
def _player_plays(self, event):
coord = self._boardView.get_box_coord(event)
if self._boardModel.boxes[coord].value != self._boardModel.EMPTY:
return
self._boardModel.boxes[coord].value = self._boardModel.X
self._boardView.draw_x_on_box(coord)
self._ai_plays()
def _ai_plays(self):
#Simply checks a random empty box on the board;
#The minmax algorithm could be used to never lose but that's no fun
for coord, box in self._boardModel.boxes.items():
if box.value == self._boardModel.EMPTY:
self._boardModel.boxes[coord].value = self._boardModel.O
self._boardView.draw_o_on_box(coord)
break
self._check_end_game_status()
def _check_end_game_status(self):
if self._boardModel.victory():
self._boardView.draw_message_in_center("Victory!")
elif self._boardModel.draw():
self._boardView.draw_message_in_center("Draw!")
def _ai_goes_first(self):
result = [True, False]
shuffle(result)
return result[0]
def _connect_click_event(self):
self._boardView.canvas.bind("<Button-1>", self._player_plays)
TicTacToeBoard.draw
. \$\endgroup\$collections.namedtuple
does not work like you think it does, please check docs. \$\endgroup\$