Creating a visual Tic-Tac-Toe program
No 38
A common introductory programming problem is writing code for a Tic-Tac-Toe game, sometimes called Naughts and Crosses. As you probably know, you play the game in a 3 x 3 matrix, with two players taking turns writing X’s or O’s and seeking to get three cells in a row, column, or diagonal.
Many programmers skip the entire visual aspect of the game and just have the players enter the square number (say, 1-9) and maybe the X or O. This is really hard to keep track of, and they then sometimes print out an approximation of the matrix each time. Then they go on to solving the problem of checking rows, columns and diagonals for all three cells the same, sometimes in really baroque code.
In this article, we instead, treat this as a visual problem of representing data stored in a 3 x 3 matrix instead. We create a 3x3 grid using the tkinter toolkit and populate it with 9 buttons. If you are the X-player, when you click on one, it changes to display an ‘X’ and if you are the O-player, it displays an ’O.’
Now, the way we construct this game board is by creating nine buttons and placing them in a grid. The buttons are all derived from our popular DButton abstract class which contains its own abstract comd method, that we can then fill in when we derive buttons from it. Here’s that base class:
# abstract DButton class contains the comd method
class
DButton(tk.Button):
def
__init__(self, master=
None
, **kwargs):
super().__init__(master, command=self.comd, **kwargs)
def
comd(self):
pass
Our PlayButton class, derived from this one contains the x,y coordinates of the button as well as enable and disable methods:
# PlayButton for each of the 9 cells
class PlayButton(DButton):
def __init__(self, root, med, x: int, y: int):
super().__init__(root, width=2)
self.x = x # save the coordinates inside the button instance
self.y = y
self.med = med
self.config(font=('Helvetica bold', 26))
def disable(self):
self.configure(state='disabled')
def enable(self):
self.configure(state='normal')
def comd(self):
self.med.btClick(self)
self.disable() # disable each button once it is clicked
Note that when the PlayButton is clicked, it calls its own comd method which calls a btClick method in our Mediator class, which takes care of all interactions between GUI elements. Then it disables itself, so you can’t change the play afterwards.
We create all 9 buttons in a little loop like this, and keep them in a buttons array.
# create buttons and put in buttons 3x3 array
for
row
in
range(0,3):
for
col
in
range(0,3):
b= PlayButton(root, med, row,col)
b.grid(row =row,column=col, ipadx=15, ipady=15)
self.buttons[row][col]=b
The Board object
We also create a Board object, which contains a 3x3 array of the labels each button can have: space, X or O. When we start, all the elements are set to spaces. When a button is clicked, it becomes and X or an O and that is copied into that element of the board array. Then the player swaps to the other player automatically.
# this class represents the 3x3 array of plays as characters
# space, X or O
class Board:
def __init__(self):
self.createBoard()
# create blank board
def createBoard(self):
self.board = [ [' ',' ',' '],[' ',' ',' '],[' ',' ',' ']]
# set an element to an X or an O
def set(self, play, x, y):
self.board[x][y] = play
# check all rows for 3 matching plays
def checkRows(self):
pass
# check all columns
def checkCols(self):
pass
# check the left and right diagonal
def checkDiags(self):
pass
We’ll explain the checks for rows, columns and diagonals below. The reason to keep the separate array of these labels is that checking for three in a row is much simpler to code.
In the Mediator class, each button calls the same click event which copies the play into a Board array and uses the Board class to check to see if any row, column or diagonal contains all three of the same player’s marks (X’s or O’s),
# a button is clicked
def
btClick(self, butn):
butn['text'
] = self.player
# set button text to current player
# set board array element to an X or O
self.board.set(self.player, butn.x, butn.y)
won = False
found = self.board.checkRows() # check rows for winner
# pop up winner message box if a row is found
if
found >= 0:
self.mesgWin(self.player, "Row "
+ str(found) +
' wins'
)
won = True
The strategy for checking rows, columns or diagonals is to first check if the first of the three cells is not a space. If it isn’t, it is an X or O. Then you check to see if the other two cells contain the same player mark:
# check all rows for 3 matching plays
def
checkRows(self):
found = False
col = 1
r = 0
# if the first row element is not a space
# check to see if the other two match it
while
r <3
and not
found:
play = self.board[r][0]
if
play !=
' '
:
found = self.board[r][1]==play \
and
self.board[r][2]==play
if not
found:
r += 1
if
found:
return
r #return number of winning row
else
:
return
-1 #or return -1
The checkCols and checkDiags are completely analogous.
Finding the Winner
The program checks for three in a row, column or diagonal after every play (button click). If it finds a winner, it pops up a message box, and disables all the buttons, so play is ended.
To play again, you click on Reset to clear the board and the game begins anew.
Playing against the computer
This program allows two players to play against each other; it does not play against the user. In order for the computer to play against the user, a number of algorithms have been proposed. This article suggests using a MinMax algorithm or a Reinforcement Learning algorithm, and this article suggests another AI approach. And here is a simpler algorithm.
But in such a simple game, there are three steps to a successful player algorithm,
1. Find if the opponent has any rows with two cells already selected, and play the third cell.
2. Find if there is a row for you that has two cells selected. Play the third to win the game.
3. If #2 fails, look for an optimal play, such as selecting more than one corner, or playing in a row where you already have one cell, and play the second.
4. Otherwise use a random number to select one of the available cells.
The above is likely to work as well as any other.
All of the source code can be found in GitHub under jameswcooper/newsletter/Tictactoe