A Simple Introduction to Pygame with Tic-Tac-Toe

Lhamu Tsering
8 min readNov 9, 2020

--

Growing up Tic Tac Toe has been a much loved game. The rules are simple and very easy to learn. So, I thought about whether the game could be written with Python. I looked it up and yes! it could be done. I looked at the codes that were used and saw that a Python module called Pygame can be used. I coded along with the instructions and hope to explain in this blog what I learned about how to Tic-Tac-Toe.

Pygame is a Python module designed to write video games. It has a nice SDL (Simple DirectMedia Layer) library. All the codes used in this blog to create Tic-Tac-Toe came from here. The codes worked for me running it on jupyter notebook, but you can also run it as a py script from the command line. With the codes in this blog you should be able to code along and try it out yourself.

Breaking down the things we need to do build our Tic-Tac-Toe game:

  1. Import the required libraries and set up the required global variables.
  2. Design the game display function
  3. Write the win and draw algorithm
  4. Set up player click functionality to display ‘X’ and ‘O’ properly
  5. Running an infinite loop

Importing libraries and setting up global variables

This part is fairly straight forward. The libraries imported are modules that will be used to build the game. The global variables will be used to build the structure of the game.

# importing the required libraries 
import pygame as pg
import sys
import time
from pygame.locals import *
# declaring the global variables# for storing the 'x' or 'o' value as character
XO = 'x'
# storing the winner's value at any instant of code
winner = None
# to check if the game is a draw
draw = None
# to set width of the game window
width = 400
# to set height of the game window
height = 400
# to set background color of the game window
white = (255, 255, 255)
# color of the straight lines on that white game board, dividing board into 9 parts
line_color = (0, 0, 0)
# setting up a 3 * 3 board in canvas
board = [[None]*3, [None]*3, [None]*3]

Design the game display function

pg.init() initializes the pygame window on which the game will be played. fps or frames per second will give the frame rate or refresh rate of the algorithm. pg.display.set_mode sets the display window based on the width, the height, depth and fps given in the method arguments. The other codes explain themselves. “modified_cover.png” is just an image of the tic-tac-toe game board that will be used as the cover of the pygame window. The ‘X_modified.png’ and the ‘o_modified.png’ are images of ‘x’ and ‘o’ respectively that will be used to mark the player positions on the game board.

# initializing the pygame window 
pg.init()
# setting fps manually
fps = 30
# this is used to track time
CLOCK = pg.time.Clock()
# this method is used to build the infrastructure of the display
screen = pg.display.set_mode((width, height + 100), 0, 32)
# setting up a nametag for the game window
pg.display.set_caption("My Tic Tac Toe")
# loading the images as python object
initiating_window = pg.image.load("modified_cover.png")
x_img = pg.image.load("X_modified.png")
y_img = pg.image.load("o_modified.png")
# resizing images
initiating_window = pg.transform.scale(initiating_window, (width, height + 100))
x_img = pg.transform.scale(x_img, (80, 80))
o_img = pg.transform.scale(y_img, (80, 80))

To initiate the game window we create a function that will use display related global variables set above. .blit() here will enable to display the initiating_window(see above) images over the screen or the main display. pg.display.update() when called will update the window display. The horizontal and vertical lines will create the game board by dividing the window into 9 parts.

def game_initiating_window(): 

# displaying over the screen
screen.blit(initiating_window, (0, 0))

# updating the display
pg.display.update()
time.sleep(3)
screen.fill(white)
# drawing vertical lines
pg.draw.line(screen, line_color, (width / 3, 0), (width / 3, height), 7)
pg.draw.line(screen, line_color, (width / 3 * 2, 0), (width / 3 * 2, height), 7)
# drawing horizontal lines
pg.draw.line(screen, line_color, (0, height / 3), (width, height / 3), 7)
pg.draw.line(screen, line_color, (0, height / 3 * 2), (width, height / 3 * 2), 7)
draw_status()
Intial blank display board

When the display update command is called at the end of a game, the game status of win, draw or lose, must be announced at each update. The next function draw_status() will set how and where to display the game status message at each display update.

def draw_status(): 

# getting the global variable draw
# into action
global draw

if winner is None:
message = XO.upper() + "'s Turn"
else:
message = winner.upper() + " won !"
if draw:
message = "Game Draw !"
# setting a font object
font = pg.font.Font(None, 30)

# setting the font properties like color and width of the text
text = font.render(message, 1, (255, 255, 255))
# copy the rendered message onto the board creating a small block at # the bottom of the main display
screen.fill ((0, 0, 0), (0, 400, 500, 100))
text_rect = text.get_rect(center =(width / 2, 500-50))
screen.blit(text, text_rect)
pg.display.update()

The Win and Draw Algorithm

So based on the rules of the game, to make a win you need to have all ‘X’s or all ‘O’s either across, or down, or diagonally. Here we just use a simple “for loop” function with ‘if’ condition to do the checking of a win in rows, win in column or win in diagonal. If there is a win, a line will be drawn to show the win based on the pg.draw.line() function.

def check_win(): 
global board, winner, draw
# checking for winning rows
for row in range(0, 3):
if((board[row][0] == board[row][1] == board[row][2]) and (board [row][0] is not None)):
winner = board[row][0]
pg.draw.line(screen, (250, 0, 0),
(0, ((row + 1)*height / 3) - (height / 6)),
(width, ((row + 1)*height / 3) - (height / 6)),
4)
break
# checking for winning columns
for col in range(0, 3):
if((board[0][col] == board[1][col] == board[2][col]) and (board[0][col] is not None)):
winner = board[0][col]
pg.draw.line (screen, (250, 0, 0), ((col + 1)* width / 3 - width / 6, 0), (((col + 1)* width / 3) - (width / 6), height), 4)
break
# check for diagonal winners
if (board[0][0] == board[1][1] == board[2][2]) and (board[0][0] is not None):
# game won diagonally left to right
winner = board[0][0]
pg.draw.line (screen, (250, 70, 70), (50, 50), (350, 350), 4)

if (board[0][2] == board[1][1] == board[2][0]) and (board[0][2] is not None):
# game won diagonally right to left
winner = board[0][2]
pg.draw.line (screen, (250, 70, 70), (350, 50), (50, 350), 4)
if(all([all(row) for row in board]) and winner is None ):
draw = True
draw_status()

Set up player click functionality to display ‘X’ and ‘O’ properly

At this point, we have our game window set up and the game rules set up. Next we set up the positions to place the ‘x_img’ and ‘o_img’ objects when a player clicks on the board square, using geometric positioning of x & y coordinates.

def drawXO(row, col): 
global board, XO

# for the first row, the image should be pasted at a x coordinate # of 30 from the left margin
if row == 1:
posx = 30

# for the second row, the image should be pasted at a x coordinate
# of 30 from the game line
if row == 2:
# margin or width / 3 + 30 from the left margin of the window
posx = width / 3 + 30

if row == 3:
posx = width / 3 * 2 + 30
if col == 1:
posy = 30

if col == 2:
posy = height / 3 + 30

if col == 3:
posy = height / 3 * 2 + 30

# setting up the required board value to display
board[row-1][col-1] = XO

if(XO == 'x'):
# pasting x_img over the screen at a coordinate position of
# (pos_y, posx) defined in the above code
screen.blit(x_img, (posy, posx))
XO = 'o'
else:
screen.blit(o_img, (posy, posx))
XO = 'x'

pg.display.update()

The board’s and the rules are set up. The marker positions have been dictated. Next thing is create a function that takes input from the player from a mouse click. pg.mouse.get_pos() will get the x-coord and the y-coord of the mouse click and return a tuple. Based on the coordinates in the tuple, we identify the row and column the player clicked on. The row and column are then passed as arguments to the function drawXO(row, col) we just created above, to display the x and o images appropriately on the game board.

def user_click(): 
# get coordinates of mouse click
x, y = pg.mouse.get_pos()
# get column of mouse click (1-3)
if(x<width / 3):
col = 1

elif (x<width / 3 * 2):
col = 2

elif(x<width):
col = 3

else:
col = None
# get row of mouse click (1-3)
if(y<height / 3):
row = 1

elif (y<height / 3 * 2):
row = 2

elif(y<height):
row = 3

else:
row = None

# after getting the row and col, we need to draw the images at
# the desired positions
if(row and col and board[row-1][col-1] is None):
global XO

drawXO(row, col)
check_win()
X won!

Running an infinite loop

At this final step we create an infinite loop so that the game keeps running and updating the display at each end of game, until the player clicks the exit button to close out the program. At each update the global variables need to be refreshed and reset to its original values. We do this by creating a reset_game() function, which empties all values on the board and initializes the global parameters.

The ending ‘while loop’ is explained much better at the source site.

** “In the game development, every action by the player is an event. Whether he clicks on the window or clicks on the exit/close icon. To get these events as an object, pygame has a built-in method used as pg.event.get(). If the event type is “QUIT”, we use the sys library of Python to exit the game. But if the mouse is pressed, the event.get() will return “MOUSEBUTTONDOWN” and our call to user_click() happens to know the exact coordinate of the board where the user has clicked.”**

def reset_game(): 
global board, winner, XO, draw
time.sleep(3)
XO = 'x'
draw = False
game_initiating_window()
winner = None
board = [[None]*3, [None]*3, [None]*3]
game_initiating_window()while(True):
for event in pg.event.get():
if event.type == QUIT:
pg.quit()
sys.exit()
elif event.type == MOUSEBUTTONDOWN:
user_click()
if(winner or draw):
reset_game()
pg.display.update()
CLOCK.tick(fps)

To re-iterate the source site, I got the complete code from geeksforgeeks. when I initially copied the code, there were some typos that prevented it from running. But, once I cleaned that up and got to see it work it was exciting. This has been a fun exercise checking out Pygame, reading the code, understanding what each code does and seeing how it works. I don’t presume to know how to use Pygame fully just because I understand how this game code works, but I can say I got introduced to a fun library!

To learn more about Pygame, this link will take you to the documentation site. Another good resource to learn and play is here.

--

--

Lhamu Tsering
Lhamu Tsering

Responses (2)