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)
1 Answer 1
Looks to me that you want to have a box with various buttons that allows you to select, say, easy, medium, or hard. Return that and then say
if AIChoose == "Easy":
getEasyAIMove()
elif AIChoose == "Medium":
GetMedAIMove()
elif AIChoose == "Hard":
GetHardAIMove()
Now, that's obviously the easy part, but a Tic-Tac-Toe AI is actually not at all hard:
def GetHardAIMove():
# first check if you're about to win. If you are, move there
# now check if your opponent is about to win. If he is, block him
# check if any of the corners are free. If they are, go to a random one of them
# check if the middle is free. If it is, go to it
#otherwise, go to a random open spot
def GetMedAIMove():
# here's where we cheat. generate a random number between 1 and 3. if its <= 2, GetHardAIMove. otherwise, go easy.
def GetEasyAIMove():
# If you're about to win, go there
# If you're about to lose, go there
# Otherwise, go to a random spot.
See? This is a quick and painless AI that can beat decent players who don't know how it's made about 50-60% of the time.
EDIT: Since Tic-Tac-Toe can always be won or tied by the player that moves first, you may wish to add this into the AI by doing something like this:
def GetHardAIMove():
# if you move first:
# after making sure you or your opponent isn't about to win:
# move to a corner
# then move to the corner opposite that
# then move to a random corner
# at this point, you have either won or tied
# first check if you're about to win. If you are, move there
# now check if your opponent is about to win. If he is, block him
# check if any of the corners are free. If they are, go to a random one of them
# check if the middle is free. If it is, go to it
#otherwise, go to a random open spot
TicTacToeBoard.draw
. \$\endgroup\$collections.namedtuple
does not work like you think it does, please check docs. \$\endgroup\$