DEV Community

Cover image for Creating the Classical Pong Game in Python with a Tkinter UI
Sylvain Saurel
Sylvain Saurel

Posted on • Originally published at ssaurel.com

1

Creating the Classical Pong Game in Python with a Tkinter UI

To kick off the year 2024 on SSaurel’s Blog, what better way than to code the classic game of Pong in Python?

That’s what I suggest you do in this step-by-step tutorial, using the Tkinter library for the user interface. This will show you how to program a simple game in Python and manage user interaction.


Let’s start by designing the Ball

To design our ball, we need to define its position and velocity. We’ll then need to define a method for modifying the position of the rectangle representing this ball as its coordinates change.

When the ball touches one of the two horizontal edges, we’ll need to restart the ball from the center of the screen. This will be done in a restart method, and we’ll use the random method in Python’s Random library to randomly choose an x and y direction.

Finally, the bulk of the work remains to be done in the ball’s move method.

We’ll have to deal with several scenarios. The first is when the ball touches the outer edges at height. In this case, we’ll need to reverse the direction of the ball in ordinate. This is done by multiplying the current y velocity of the ball by -1.

The second is when the ball comes into contact with the right or left paddles. In this case, you need to invert the current velocity on the x-axis. This is done by multiplying the velocity at x by -1.

In the third and fourth cases, points are scored for the player on the left and the player on the right. If the ball touches the edge on the left, the player on the right scores 1 point. If the ball touches the edge on the right, the player on the left scores 1 point.

Finally, we play the x and y positions of the ball, adding the current velocity.

This gives us the following for the Ball object:

# First, we design the Ball Object
class Ball:
def __init__(self, canvas, width, velocity, boardwidth, boardheight):
self.width = width
self.boardwidth = boardwidth
self.boardheight = boardheight
# we center the ball on the board
self.topx = boardwidth / 2 - width / 2
self.topy = boardheight / 2 - width / 2
self.velocity = velocity
self.vx = velocity
self.vy = velocity
self.canvas = canvas
self.id = self.canvas.create_rectangle(self.topx, self.topy, self.topx + self.width, self.topy + self.width, fill = 'white')
# we define a method to draw the ball on the canvas
def draw(self):
self.canvas.coords(self.id, self.topx, self.topy, self.topx + self.width, self.topy + self.width)
# we define a restart method for restarting the ball move
def restart(self):
self.topx = self.boardwidth / 2 - self.width / 2
self.topy = self.boardheight / 2 - self.width / 2
# we define a random direction for the ball when restarting
self.vx = (-1, 1)[rand.random() > 0.5] * self.velocity
self.vy = (-1, 1)[rand.random() > 0.5] * self.velocity
# Move the ball
# we need to pass the pong game instance and the paddles in the move method of the Ball. You can improve this by yourself later ;)
def move(self, pong, paddleright, paddleleft):
# if the ball touches the top or the bottom of the board, we invert direction y
if self.topy <= 0 or (self.topy + self.width) >= self.boardheight:
self.vy = self.vy * -1
# if the ball touches one of both paddles, we invert direction x
if paddleright.collideright(self) or paddleleft.collideleft(self):
self.vx = self.vx * -1
# if the ball touches the right or the left of the board, we update paddle points and we return True
if (self.topx + self.width) >= self.boardwidth:
pong.leftpoints = pong.leftpoints + 1
return True
if self.topx <= 0:
pong.rightpoints = pong.rightpoints + 1
return True
# we update ball position
self.topx = self.topx + self.vx
self.topy = self.topy + self.vy
return False
view raw Ball.py hosted with ❤ by GitHub

Let’s move on to designing the Paddle

To model a paddle, we define a Paddle object.

This Paddle object will have x and y coordinates, as well as a width and height. Each paddle is assigned a score initialized to 0. We also initially create the rectangle representing the paddle and store its id returned by the Tkinter library’s create_rectangle method. This id will then be used to modify the paddle’s coordinates.

The draw method is used to modify the paddle’s coordinates using this id.

The top and down methods move the paddle up and down respectively. The VELOCITY constant is used for displacement. This constant will also be used whenever ball positions are modified.

Finally, we use the collideright and collideleft methods to test whether the paddle and ball collide to the right and left respectively. In the event of a collision, we return True, which will then be used in the Pong class we’ll define for the Pong game.

This gives us the following code for our Paddle object:

# Now, it is time to design the Paddle for our Pong Game
class Paddle:
def __init__(self, canvas, topx, topy, width, height, boardheight):
self.topx = topx
self.topy = topy
self.width = width
self.height = height
self.boardheight = boardheight
self.score = 0
self.canvas = canvas
# draw this paddle according positions passed in parameter
self.id = self.canvas.create_rectangle(self.topx, self.topy, self.topx + self.width, self.topy + self.height, fill = 'white')
# we update coords of this paddle
def draw(self):
self.canvas.coords(self.id, self.topx, self.topy, self.topx + self.width, self.topy + self.height)
# now, we need to manage down event then top event for the current paddle object
def top(self):
if self.topy - VELOCITY > 0:
self.topy = self.topy - VELOCITY
def down(self):
if (self.topy + self.height + VELOCITY) < self.boardheight:
self.topy = self.topy + VELOCITY
# use both methods to collide paddle right or left. As an exercise, you can improve this to make one generic method ;)
def collideright(self, ball):
if (ball.topx + ball.width) >= self.topx and (ball.topy >= self.topy or (ball.topy + ball.width) >= self.topy) and ((ball.topy + ball.width) <= (self.topy + self.height) or ball.topy <= (self.topy + self.height)):
return True
return False
def collideleft(self, ball):
if ball.topx <= (self.topx + self.width) and (ball.topy >= self.topy or (ball.topy + ball.width) >= self.topy) and ((ball.topy + ball.width) <= (self.topy + self.height) or ball.topy <= (self.topy + self.height)):
return True
return False
view raw Paddle.py hosted with ❤ by GitHub

Time to design the Pong Game using Ball and Paddle objects

With our two objects modeled above, we can move on to modeling the Pong object.

The Pong object will store the scores of the two Paddles, as well as the objects drawn on the canvas to represent these scores. These will be deleted before each rewrite when the scores are updated. This avoids having to delete the entire canvas each time the scores are updated.

A Boolean is set to True when the game is rendering.

Four Booleans are used to define whether the up/down keys chosen for each player are pressed or not. If they are pressed, the paddles are activated. The Paddle and Ball objects are created in the Pong object constructor. We call the canvas’s pack method and then call the drawmiddlelines, drawbord, and move methods respectively to start the game.

The drawmiddlelines method draws the mid-screen line vertically.

The drawboard method draws all game elements. Note the small workaround used with the _exit method of the os library to ensure that the application stops when the Tkinter game window is closed.

The drawpoints method is used to draw the scores of two players on the screen.

The move method will be called every X milliseconds to update the state of our Pong game. We’ll use a Timer object from Python’s Threading library to handle this repetitive call. In the method, we check whether it’s necessary to move the paddles. If so, we act accordingly.

After calling the ball’s move method, we retrieve the returned state. If the state is True, this means that the ball must be restarted from the center of the board. To do this, we call the restart method of the Pong object, within which we call only the restart method of the Ball object. So why a separate method? To improve the game in the future and add more behaviors to restart. You could choose to emit a sound at this point…

As for the keypress and keyrelease methods, they are associated with the keyboard events and . This makes it possible to manage several keyboard keys at the same time. We choose the z / s keys for the left paddle and the o / l keys for the right paddle.

Finally, the killtimer method will be used to end the rendering phase and cancel the timer if it’s running.

This gives us the following code for the Pong object:

# Now, we can define the Pong Game Object
class Pong:
def __init__(self, root, width, height, margin):
paddlewidth = width / 50
paddleheight = height / 12
self.leftpoints = 0
self.lefttxt = None
self.rightpoints = 0
self.righttxt = None
self.render = True # True when we need to render the game on the canvas
# we manage left up / down for moving the left paddle
self.leftup = False
self.leftdown = False
# same for right paddle
self.rightup = False
self.rightdown = False
self.width = width
self.height = height
self.margin = margin
self.root = root
self.root.title("Pong Game - SSaurel's Blog")
self.root.geometry(str(width) + "x" + str(height))
# we create the canvas
self.canvas = tk.Canvas(self.root, width = width, height = height, bg = 'black')
self.paddleleft = Paddle(self.canvas, margin, height / 2 - paddleheight / 2, paddlewidth, paddleheight, height)
self.paddleright = Paddle(self.canvas, (width - margin) - paddlewidth, height / 2 - paddleheight / 2, paddlewidth, paddleheight, height)
self.ball = Ball(self.canvas, paddlewidth, VELOCITY, width, height)
self.canvas.pack()
self.drawmiddlelines()
self.drawboard()
self.move()
# we move draw middle lines for the board
def drawmiddlelines(self):
leftx = self.width / 2 - self.paddleleft.width / 2
for y in range(0, self.height, int(self.paddleleft.height + self.margin * 2)):
self.canvas.create_rectangle(leftx, y, leftx + self.paddleleft.width, y + self.paddleleft.height, fill = 'grey')
def drawboard(self):
try:
# draw the paddles
self.paddleleft.draw()
self.paddleright.draw()
# draw points
self.drawpoints()
# draw the ball
self.ball.draw()
except:
# some strange exception occur here when we quit the game. We need to call explicitly exit!
os._exit(0)
def drawpoints(self):
# we delete the previous score for the left paddle
if self.lefttxt != None:
self.canvas.delete(self.lefttxt)
# we write the new score
self.lefttxt = self.canvas.create_text(self.width / 2 - 50, 50, text = str(self.leftpoints), fill = 'grey', font = ("Helvetica 35 bold"))
# the same thing for the right paddle
if self.righttxt != None:
self.canvas.delete(self.righttxt)
# we write the new score
self.righttxt = self.canvas.create_text(self.width / 2 + 50, 50, text = str(self.rightpoints), fill = 'grey', font = ("Helvetica 35 bold"))
# we define the move method to update the game elements
def move(self):
if self.render:
# use a timer to call this method each X milliseconds
self.timer = threading.Timer(0.05, self.move)
self.timer.start()
# we manage touch events
if self.leftup:
self.paddleleft.top()
if self.leftdown:
self.paddleleft.down()
if self.rightup:
self.paddleright.top()
if self.rightdown:
self.paddleright.down()
# True if the Ball touched one of both sides of the board
state = self.ball.move(self, self.paddleright, self.paddleleft)
if state:
self.restart() # we need to restart the ball
self.drawboard()
def restart(self):
self.ball.restart()
# Time to manage keyboards event from users
# We need to make this special code to detect several keys used at the same time on the keyboard
# z / s for the left paddle - o / l for the right paddle
def keypress(self, event):
match event.char:
case 'z':
self.leftup = True
case 's':
self.leftdown = True
case 'o':
self.rightup = True
case 'l':
self.rightdown = True
def keyrelease(self, event):
match event.char:
case 'z':
self.leftup = False
case 's':
self.leftdown = False
case 'o':
self.rightup = False
case 'l':
self.rightdown = False
# last method: we define a method to kill the timer and stop the rendering of the game
def killtimer(self):
self.render = False
self.timer.cancel()
self.root.destroy()
view raw Pong.py hosted with ❤ by GitHub

Assembling objects for the Pong Game

The final part of the program simply consists of creating the root of the UI by calling the TK method of the tk object. The instance of the Pong game is created by passing the board size and margin as parameters. These are defined as constants at the start of the program.

We then bind the and events to the UI root and call the keypress and keyrelease methods of our Pong object instance respectively. Finally, don’t forget to listen for the WM_DELETE_WINDOW event to react to the click on the cross in our Tkinter window. This ensures that the timer is properly killed and that the process associated with our Pong game is stopped.

This gives us the following complete code for our Pong game in Python with a Tkinter UI:

import tkinter as tk
import random as rand
import threading
import os
#define some constants
WIDTH = 1000
HEIGHT = 700
MARGIN = 15
VELOCITY = 15
# First, we design the Ball Object
class Ball:
def __init__(self, canvas, width, velocity, boardwidth, boardheight):
self.width = width
self.boardwidth = boardwidth
self.boardheight = boardheight
# we center the ball on the board
self.topx = boardwidth / 2 - width / 2
self.topy = boardheight / 2 - width / 2
self.velocity = velocity
self.vx = velocity
self.vy = velocity
self.canvas = canvas
self.id = self.canvas.create_rectangle(self.topx, self.topy, self.topx + self.width, self.topy + self.width, fill = 'white')
# we define a method to draw the ball on the canvas
def draw(self):
self.canvas.coords(self.id, self.topx, self.topy, self.topx + self.width, self.topy + self.width)
# we define a restart method for restarting the ball move
def restart(self):
self.topx = self.boardwidth / 2 - self.width / 2
self.topy = self.boardheight / 2 - self.width / 2
# we define a random direction for the ball when restarting
self.vx = (-1, 1)[rand.random() > 0.5] * self.velocity
self.vy = (-1, 1)[rand.random() > 0.5] * self.velocity
# Move the ball
# we need to pass the pong game instance and the paddles in the move method of the Ball. You can improve this by yourself later ;)
def move(self, pong, paddleright, paddleleft):
# if the ball touches the top or the bottom of the board, we invert direction y
if self.topy <= 0 or (self.topy + self.width) >= self.boardheight:
self.vy = self.vy * -1
# if the ball touches one of both paddles, we invert direction x
if paddleright.collideright(self) or paddleleft.collideleft(self):
self.vx = self.vx * -1
# if the ball touches the right or the left of the board, we update paddle points and we return True
if (self.topx + self.width) >= self.boardwidth:
pong.leftpoints = pong.leftpoints + 1
return True
if self.topx <= 0:
pong.rightpoints = pong.rightpoints + 1
return True
# we update ball position
self.topx = self.topx + self.vx
self.topy = self.topy + self.vy
return False
# Now, it is time to design the Paddle for our Pong Game
class Paddle:
def __init__(self, canvas, topx, topy, width, height, boardheight):
self.topx = topx
self.topy = topy
self.width = width
self.height = height
self.boardheight = boardheight
self.score = 0
self.canvas = canvas
# draw this paddle according positions passed in parameter
self.id = self.canvas.create_rectangle(self.topx, self.topy, self.topx + self.width, self.topy + self.height, fill = 'white')
# we update coords of this paddle
def draw(self):
self.canvas.coords(self.id, self.topx, self.topy, self.topx + self.width, self.topy + self.height)
# now, we need to manage down event then top event for the current paddle object
def top(self):
if self.topy - VELOCITY > 0:
self.topy = self.topy - VELOCITY
def down(self):
if (self.topy + self.height + VELOCITY) < self.boardheight:
self.topy = self.topy + VELOCITY
# use both methods to collide paddle right or left. As an exercise, you can improve this to make one generic method ;)
def collideright(self, ball):
if (ball.topx + ball.width) >= self.topx and (ball.topy >= self.topy or (ball.topy + ball.width) >= self.topy) and ((ball.topy + ball.width) <= (self.topy + self.height) or ball.topy <= (self.topy + self.height)):
return True
return False
def collideleft(self, ball):
if ball.topx <= (self.topx + self.width) and (ball.topy >= self.topy or (ball.topy + ball.width) >= self.topy) and ((ball.topy + ball.width) <= (self.topy + self.height) or ball.topy <= (self.topy + self.height)):
return True
return False
# Now, we can define the Pong Game Object
class Pong:
def __init__(self, root, width, height, margin):
paddlewidth = width / 50
paddleheight = height / 12
self.leftpoints = 0
self.lefttxt = None
self.rightpoints = 0
self.righttxt = None
self.render = True # True when we need to render the game on the canvas
# we manage left up / down for moving the left paddle
self.leftup = False
self.leftdown = False
# same for right paddle
self.rightup = False
self.rightdown = False
self.width = width
self.height = height
self.margin = margin
self.root = root
self.root.title("Pong Game - SSaurel's Blog")
self.root.geometry(str(width) + "x" + str(height))
# we create the canvas
self.canvas = tk.Canvas(self.root, width = width, height = height, bg = 'black')
self.paddleleft = Paddle(self.canvas, margin, height / 2 - paddleheight / 2, paddlewidth, paddleheight, height)
self.paddleright = Paddle(self.canvas, (width - margin) - paddlewidth, height / 2 - paddleheight / 2, paddlewidth, paddleheight, height)
self.ball = Ball(self.canvas, paddlewidth, VELOCITY, width, height)
self.canvas.pack()
self.drawmiddlelines()
self.drawboard()
self.move()
# we move draw middle lines for the board
def drawmiddlelines(self):
leftx = self.width / 2 - self.paddleleft.width / 2
for y in range(0, self.height, int(self.paddleleft.height + self.margin * 2)):
self.canvas.create_rectangle(leftx, y, leftx + self.paddleleft.width, y + self.paddleleft.height, fill = 'grey')
def drawboard(self):
try:
# draw the paddles
self.paddleleft.draw()
self.paddleright.draw()
# draw points
self.drawpoints()
# draw the ball
self.ball.draw()
except:
# some strange exception occur here when we quit the game. We need to call explicitly exit!
os._exit(0)
def drawpoints(self):
# we delete the previous score for the left paddle
if self.lefttxt != None:
self.canvas.delete(self.lefttxt)
# we write the new score
self.lefttxt = self.canvas.create_text(self.width / 2 - 50, 50, text = str(self.leftpoints), fill = 'grey', font = ("Helvetica 35 bold"))
# the same thing for the right paddle
if self.righttxt != None:
self.canvas.delete(self.righttxt)
# we write the new score
self.righttxt = self.canvas.create_text(self.width / 2 + 50, 50, text = str(self.rightpoints), fill = 'grey', font = ("Helvetica 35 bold"))
# we define the move method to update the game elements
def move(self):
if self.render:
# use a timer to call this method each X milliseconds
self.timer = threading.Timer(0.05, self.move)
self.timer.start()
# we manage touch events
if self.leftup:
self.paddleleft.top()
if self.leftdown:
self.paddleleft.down()
if self.rightup:
self.paddleright.top()
if self.rightdown:
self.paddleright.down()
# True if the Ball touched one of both sides of the board
state = self.ball.move(self, self.paddleright, self.paddleleft)
if state:
self.restart() # we need to restart the ball
self.drawboard()
def restart(self):
self.ball.restart()
# Time to manage keyboards event from users
# We need to make this special code to detect several keys used at the same time on the keyboard
# z / s for the left paddle - o / l for the right paddle
def keypress(self, event):
match event.char:
case 'z':
self.leftup = True
case 's':
self.leftdown = True
case 'o':
self.rightup = True
case 'l':
self.rightdown = True
def keyrelease(self, event):
match event.char:
case 'z':
self.leftup = False
case 's':
self.leftdown = False
case 'o':
self.rightup = False
case 'l':
self.rightdown = False
# last method: we define a method to kill the timer and stop the rendering of the game
def killtimer(self):
self.render = False
self.timer.cancel()
self.root.destroy()
# we can assemble the pong game elements!
root = tk.Tk()
pong = Pong(root, WIDTH, HEIGHT, MARGIN)
# we bind key press and key release events to our Pong Game Object
root.bind("<KeyPress>", pong.keypress)
root.bind("<KeyRelease>", pong.keyrelease)
# we listen to WM_DELETE_WINDOW event to kill the timer ...
root.wm_protocol("WM_DELETE_WINDOW", pong.killtimer)
root.mainloop()
# Time to try our Pong Game in Python with Tkinter
# Improvement you can make from here :
# - Make a limit points to end the game. For the moment, it is an infinite Pong Game :D
# - You can also imagine to increase the velocity of the ball each 10 points scored.
# - You can implement an AI for the second paddle
# - You can improve the design of the objects as stated in the code
# --> It is your turn to code :D
view raw PongGame.py hosted with ❤ by GitHub

You can see the result of our Pong Game below:

Our Pong Game in Python in Action


You can also watch this tutorial on YouTube on the SSaurel channel here:


At the end of the program code on Github, I’ve also suggested some ideas for improvements, such as defining a limit at which the game is over. You can also define an AI for the second paddle. Finally, there’s inevitably some work to be done to improve the dependencies between each of the objects. This will allow you to code your own Python Pong game.

It’s up to you to code.

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more