
Keep your eyes out for the yellow ball of pong!
As part of trying to master Python by creating fun games using Pygame, I set of to make my own version of the classic game: Pong.
The genre of Py-Pong is a two dimensional, arcade sports game featuring two player competitive table tennis gameplay.
History
Pong is one of the first computer games that ever created, this simple “tennis like” game features two paddles and a ball. It is a 1972 game developed and published by Atari, Inc. for arcades. It was created by Allan Alcorn as a training exercise assigned to him by Atari co-founder Nolan Bushnell. The game was very surprising that Atari released it as an official game, making it the first commercially successful video game.
Py-Pong
I wanted to capture the simplicistic gameplay of the original, but also wanted to make it look more colourful and bright. The name squishes ‘Py’ from Pygame and adds it to the name ‘Pong’.
Pygame has been my go to platform for the development of video games using Python. I have been using it for all my gamey projects: Caver Drone and Tic Tac Toe.
Description
Py-Pong is a two player arcade style game that utilizes Python programming language and its library, Pygame. Each player controls a paddle on opposite sides of the screen, and they must not let the paddle miss the ball. The objective is to rally the ball as many times as possible to achieve a high score. The players can also track their individual player scores, that adds up by increment of 10, which can create a new high score. The high score is also written onto a .txt file. The game includes movement, collision detection, and score tracking and focuses on timing, coordination, and competitive play between the users.
Requirements
The game requirements include:
- Implementation of player controlled paddles, with smooth movement using keyboard input.
- Logical movement behaviour of the ball across the screen.
- Collision detection between the ball, paddles, and screen boundaries for realistic gameplay.
- Scoring system that tracks player scores in each game, identifies the high score and stores it in an external text file.
- The game should display individual player scores and high scores clearly on the screen.
Additionally, it should have basic requirements of smooth and logical gameplay, interactivity (music, sound effects, visual elements) and user satisfaction.
Design Phase
Gameplay
The objective is to hit the ball back and forth without missing for as long as possible. The game involves a two payer configuration where each player has their own paddle that is placed at opposite sides (left and right). The paddles can be moved using the keyboards keys and each successful hit increases the shots score by an increment of 10. So, a high score can be achieved by any player, if they don’t miss the ball. The game ends when the ball misses the paddle and collides onto the right or left wall.
User Interface (UI)
The game starts with a dark blue screen with height of 600 and width of 800. This is the surface on which the game is executed and played. The initial position of the paddles when the game starts is in the centre of the height. The ball randomly shoots to left or right from the top centre. The player can use the keyboard keys to help move the paddles in the y axis (up and down) to hit the ball. The ball can be seen moving between the paddles and occasionally bouncing from the top and bottom part of the screen. There is a black rectangular score bar at the bottom with texts showing the Player A score, Top Score and Player B score. The game UI is minimalistic, with geometrical shapes, colours and text.
Controls
The game is played using keyboard inputs. Player A (player left) uses the W key to move the paddle up and S key to move it down. Similarly, Player B (player right) uses the Up and Down Arrows. Only these keys are functionable, the other keys in the keyboard are disabled. This makes the game easy to play with minimal training required. If the player does not press the appropriate keys, the paddle is set to be still. The movement of the paddles are limited within the screen boundary, so the keys don’t work when the paddles reach the end (when y = 0 at the top, or y = 800 at the bottom).
Core Gameplay Loop
- Game starts with the ball moving towards left or right in a random direction with increments of 10 in both x and y coordinates. This causes a gradient movement.
- Players move paddles appropriately using the specified keys to hit the ball.
- If ball collides with a paddle, the ball changes direction going towards the opposite player’s paddle.
- The score increases by increments of 10 of the player whose paddle hit the ball.
- Game continues until ball hits the right or left side of the screen.
- Checks which score is higher in the list (player A score, high score or player B score?) and saves the high score to txt file.
- If it’s the first time ever the game is played and no score.txt file exists, create one to store the high score.
MAIN CODE
### GAME: PY-PONG
### created by Ahla 30/03/26
versionText = "10.8" # version number gets displayed on the game title bar
import pygame # For handling graphics, sounds, and game logic
import sys # Provides access to system-specific functions
import random # Allows generation of random numbers
import time # Module for implementing sleep
import json # Module for handling dictionary as json format
import datetime # Module for implementing time stamp
pygame.init() # Initialization of graphics UI
##VARIABLES##
screenWidthX = 800 # Screen dimension maximum X width
screenHeightY = 600 # Screen dimension maximum Y height
scoreBarHeightY = 100 # Screen dimension for the score bar, maximum Y height
scoreBarWidthX = screenWidthX # Screen dimension for the score bar, maximum X height
numberFPS = 30 # 30 frames per second for animation
listScore = [0, 0, 0]
# Define a font for score with a custom variable name
scoreFont = pygame.font.SysFont("Courier New", 22, bold=True) # (font_name, font_size, font_weight)
winnerFont = pygame.font.SysFont("Courier New", 24, bold=True) # (font_name, font_size, font_weight)
goodbyeFont = pygame.font.SysFont("Courier New", 48) # (font_name, font_size)
# Colors in (Red, Green, Blue) format
colourBackground = (85, 51, 153) #0x553399
colourWhite = (255, 255, 255) #0xFFFFFF
colourYellow = (255, 255, 0) #0xFFFF00
colourRed = (255, 0, 0) #0xFF0000
colourBlack = (0,0,0) #0x000000
screen = pygame.display.set_mode((screenWidthX, screenHeightY + scoreBarHeightY)) # Graphics window dimensions, with score bar included
pygame.display.set_caption("Game by Ahla | Ver." + versionText) # Title for the Graphics window
screen.fill(colourBackground) # Dark background for Graphics window
clockFrame = pygame.time.Clock() # Creates a Clock object to help control the frame rate
paddleWidth = 20 #width of the paddle
paddleHeight = 100 #height of the paddle
paddleGap = 5 #gap between the paddle and the edge of screen
paddleVerticalSpeed = 15 # Number of pixels moved vertically by a single key press
ballRadius = 10 # Radius of the circle (ball)
ballDiameter = ballRadius * 2 # Diameter of the circle (ball)
ballInitialX = screenWidthX / 2 # starting position of the ball at the middle of the screen
ballInitialY = 0 # starting position of the ball at the top of the screen
ballDynamicX = ballInitialX # dynamic variable for ball position X
ballDynamicY = ballInitialY # dynamic variable for ball position Y
# Boolean variable for horizontal ball direction
ballRightDirection = random.choice([True, False]) # bollean True or False is randomly generated so at the start of the game, ball will either go left or right
ballDownDirection = True # Boolean variable for vertical ball direction
ballIncrement = 10 # Increment value for the ball position (both X and Y)
# Get Rally with Highest Shots, if available
try:
with open("assets/score.txt", "r") as scoreFile: # Open score.txt in read mode
listScore[1] = int(scoreFile.read()) # Read the integer value stored there
except FileNotFoundError: # If no text file exists (so this is the first run)
# If file doesn't exist, assume 0
listScore[1] = 0
# Get json file with User Acceptance Testing (UAT) stats, if available
try:
with open("assets/gameStats.json", "r") as statsFile: # Open gameStats.json in read mode
dictionaryGameStats = json.load(statsFile) # Read the game stats stored there
except FileNotFoundError: # If no json file exists (so this is the first run)
# If file doesn't exist, assume 0
dictionaryGameStats = {"PlayerA": [],"PlayerB": []} # Initialize a dictionary with an empty list
# get the game ID to be saved under the Key "id" of the dictionary
if len(dictionaryGameStats["PlayerA"]) == 0:
newId = 1
else:
newId = dictionaryGameStats["PlayerA"][-1]["id"] + 1
dictionaryGameStats["PlayerA"].append({"id": newId, "winner": "", "timeStamps": []})
dictionaryGameStats["PlayerB"].append({"id": newId, "winner": "", "timeStamps": []})
# Sounds
collisionSFX = pygame.mixer.Sound("assets/ball_collision.mp3") # Sound effect for ball colliding (hit) with the paddles
ballBounceSFX = pygame.mixer.Sound("assets/ball_bounce.mp3") # Sound effect for ball bounding of the top or bottom of the screen
gameOverSFX = pygame.mixer.Sound("assets/game_over.mp3") # Sound effect for when paddle hits left or right side of the screen
# Sound settings
collisionSFX.set_volume(0.4) # 40% volume for collision
ballBounceSFX.set_volume(100) # 100% volume for ball bounce
gameOverSFX.set_volume(0.5) # 50% volume for game over
# Prepare two trasparent surface layers for the paddles
paddleSurfaceLeft = pygame.Surface((paddleWidth, paddleHeight), pygame.SRCALPHA) # transparent paddle surface
paddleSurfaceRight = pygame.Surface((paddleWidth, paddleHeight), pygame.SRCALPHA) # transparent paddle surface
scoreSurface = pygame.Surface((scoreBarWidthX, scoreBarHeightY)) # score bar surface
ballSurface = pygame.Surface((ballDiameter, ballDiameter), pygame.SRCALPHA) # transparent paddle surface
pygame.draw.circle(ballSurface, colourYellow,(ballRadius, ballRadius) , ballRadius) # circle(surface, color, center, radius) -> Rect
# Draw the paddles on the surfaces
# pygame draw rect(surface, color, rect) where, rect(left(x), top(y), width, height)
pygame.draw.rect(paddleSurfaceLeft, colourWhite, (0, 0, paddleWidth, paddleHeight)) # defines left rectangle for playerLeft
pygame.draw.rect(paddleSurfaceRight, colourWhite, (0, 0, paddleWidth, paddleHeight)) # defines right rectangle for playerLeft
pygame.draw.rect(scoreSurface, colourBlack, (0, screenHeightY, screenWidthX, scoreBarHeightY)) # defines bottom rectangle for scoreSurface
# Now the surfaces with paddles need to be placed on dynamic X and Y positions based on game logic
# Default location for both paddles are at the top
paddleLeftX = paddleGap #fixed X position of the left paddle, as it does not move left and right on x-axis
paddleLeftY = (screenHeightY / 2) - (paddleHeight / 2) #moving variable --> w,s keys
paddleRightX = screenWidthX-paddleGap-paddleWidth #fixed X position of the right paddle, as it does not move left and right on x-axis
paddleRightY = (screenHeightY / 2) - (paddleHeight / 2) #moving variable --> up,down arrow keys
## FUNCTION definitions ##
def functionExitGame (): # Click Close [X] on the graphics window to exit the game
for event in pygame.event.get(): # Waiting for someone to click close (X) on Graphics window
if event.type == pygame.QUIT:
pygame.quit() # Terminating graphics UI
sys.exit() # Exits the script completely
def functionSplashScreen(): # Function to define Splash screen
# Load intro image of splash screen
introImage = pygame.image.load("assets/splashScreen.jpg")
screen.blit(introImage, (0, 0))
pygame.display.flip() # Display the graphics
time.sleep(3) # Shows splash screen for 3 seconds, before starting the game
return
"""
functionDrawScene() handles all the blitting or rendering for the surfaces for the score band, ball and left and right paddles.
The inputs are: global variables like positions and scores. The function draws score bar, paddles, and ball using blit(), renders text using scoreFont.render(...) and displays current scores from listScore. The function is mainly used to separate the graphics logic from game logic, this improving modularity of the whole code.
"""
def functionDrawScene(): # Function to draw the scene showing paddles, ball and scoreboard
# Blit the transparent surface layers for the score bar
screen.blit(scoreSurface, (0, screenHeightY + ballRadius)) # draws and renders screenSurface onto the screen for space to add score values
# Blit the transparent surface layers for the paddles
# destination_surface.blit(source_surface, position), where the position can be (x, y)
screen.blit(paddleSurfaceLeft, (paddleLeftX, paddleLeftY)) # draws and renders paddleSurface onto the screen for playerLeft paddle
screen.blit(paddleSurfaceRight, (paddleRightX, paddleRightY)) # draws and renders paddleSurface onto the screen for playerRight paddle
# Blit the transparent surface layers for the ball
# destination_surface.blit(source_surface, position), where the position can be (x, y)
screen.blit(ballSurface, (ballDynamicX, ballDynamicY)) # starting position of the ball
# Prepare the score text to display
scoreText = scoreFont.render(f" Player A: {listScore[0]} shots | Record: {listScore[1]} shots | Player B: {listScore[2]} shots", True, colourWhite) # White color text, anti-aliasing ON
# Blit the score text at position (0, 645)
screen.blit(scoreText, (0, screenHeightY + 45))
return
"""
functionPaddleShots
The inputs are: boolRight (ball direction) and playerScore (list containing scores).
The function handles the creating of rectangles (pygame.Rect) for ball and paddles, using of .colliderect() to detect collision,
updates direction of the ball to change and increments correct score and records timestamp into dictionary.
The output is: return (boolRight, playerScore).
"""
def functionPaddleShots(boolRight, playerScore):
# Define boundaries for collision detection between ball and paddle
ballBoundary = pygame.Rect(ballDynamicX, ballDynamicY, ballDiameter, ballDiameter) # Define boundary for the ball as a square, using the current position
paddleLeftBoundary = pygame.Rect(paddleLeftX, paddleLeftY, paddleWidth, paddleHeight) # Define boundary for the left paddle as a rectangle, using the current position
paddleRightBoundary = pygame.Rect(paddleRightX, paddleRightY, paddleWidth, paddleHeight) # Define boundary for the right paddle as a rectangle, using the current position
# check for collision with paddles using "colliderect" in built function
if ballBoundary.colliderect(paddleRightBoundary): # if collided with the right paddle, function returns True
# recording the timestamp of the shot
shotTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
dictionaryGameStats["PlayerB"][-1]["timeStamps"].append(shotTime)
collisionSFX.play() # Play collision sound
boolRight = False # now the ball will move to the left
playerScore[2] = playerScore[2] + 1 # increment current shots score
elif ballBoundary.colliderect(paddleLeftBoundary): # if collided with the left paddle, function returns True
# recording the timestamp of the shot
shotTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
dictionaryGameStats["PlayerA"][-1]["timeStamps"].append(shotTime)
collisionSFX.play() # Play collision sound
boolRight = True # now the ball will move to the right
playerScore[0] = playerScore[0] + 1 # increment current shots score
return (boolRight, playerScore)
def functionWinnerDisplay(winnerName):
winnerText = winnerFont.render(f"{winnerName} wins.", True, colourYellow) # White color bold text, anti-aliasing ON
# Display the "Game Over!" message when the ball crashes onto the walls
endText = goodbyeFont.render("GAME OVER!", True, colourWhite) # White color bold text, anti-aliasing ON
# Blit the message text
screen.blit(winnerText, (260, 260)) # announce the winner
screen.blit(endText, (260, 300)) # announce game over
pygame.display.flip() # Display the text
return
## ~~~~~~~~~~~~ PROGRAM STARTS ~~~~~~~~~~~~ ##
# Declarations are over; code execution starts from here
functionSplashScreen() # at the beginning of the code excution, shows splash screen for 3 seconds
#################### MAIN LOOP #######################
while True: # Infinite loop
functionExitGame () # Click Close [X] on the graphics window to exit the game
# Redraw the background with solid color
screen.fill(colourBackground) # Dark blue background for Graphics window
# Call the function to draw the scene showing paddles, ball and scoreboard
functionDrawScene()
# Display the scene drawn to the screen
pygame.display.flip()
ballRightDirection, listScore = functionPaddleShots(ballRightDirection, listScore)
# Ball movement logic #
# X movement
if ballRightDirection == True: # check whether the ball is moving to the right
ballDynamicX = ballDynamicX + ballIncrement # if True, increment the dynamic X position
else: # ball is moving to the left
ballDynamicX = ballDynamicX - ballIncrement # decrement the dynamic X position
if ballDynamicX > screenWidthX - ballDiameter: # Has the ball touched the right side of the window?
winnerName = "Player A"
dictionaryGameStats["PlayerA"][-1]["winner"] = True
dictionaryGameStats["PlayerB"][-1]["winner"] = False
break
if ballDynamicX < 0: # Has the ball touched the left side of the window?
winnerName = "Player B"
dictionaryGameStats["PlayerA"][-1]["winner"] = False
dictionaryGameStats["PlayerB"][-1]["winner"] = True
break
# Y movement
if ballDownDirection == True: # check whether the ball is moving down
ballDynamicY = ballDynamicY + ballIncrement # if True, increment the dynamic Y position
else: # ball is moving up
ballDynamicY = ballDynamicY - ballIncrement # decrement the dynamic Y position
if ballDynamicY > screenHeightY - ballDiameter: # Has the ball touched the bottom side of the window?
ballBounceSFX.play() # Play ball bounce sound when ball hits bottom of screen
ballDownDirection = False # If yes, change the direction of the ball upwards
if ballDynamicY < 0: # Has the ball touched the top side of the window?
ballBounceSFX.play() # Play ball bounce sound when ball hits top of screen
ballDownDirection = True # If yes, change the direction of the ball downwards
# Now, find the next coordinates of the paddle based on user input
##player right key press logic
keyPress = pygame.key.get_pressed() # Read the key press from keyboard
if keyPress[pygame.K_UP]: # Up arrow key press detected
if paddleRightY > paddleGap: # Constrain the top left corner of the paddle at Y cordinate = 5 (paddleGap)
paddleRightY = paddleRightY - paddleVerticalSpeed # Y value is decreased so that paddle goes up by the speed factor
elif keyPress[pygame.K_DOWN]: # Down arrow key press detected
if paddleRightY < screenHeightY - paddleGap - paddleHeight:
paddleRightY = paddleRightY + paddleVerticalSpeed # Y value is increased so that paddle goes up by the speed factor
if keyPress[pygame.K_w]: # 'W' key press detected (up)
if paddleLeftY > paddleGap: # Constrain the top left corner of the paddle at Y cordinate = 5 (paddleGap)
paddleLeftY = paddleLeftY - paddleVerticalSpeed # Y value is decreased so that paddle goes up by the speed factor
elif keyPress[pygame.K_s]: # 's' arrow key press detected (down)
if paddleLeftY < screenHeightY - paddleGap - paddleHeight:
paddleLeftY = paddleLeftY + paddleVerticalSpeed # Y value is increased so that paddle goes up by the speed factor
clockFrame.tick(numberFPS) # Creates a Clock object to help control the frame rate
# Last line for infinite loop
#################### END OF MAIN LOOP #######################
# 'break' from the current main infinite while loop comes here
gameOverSFX.play() # Play game over sound
# If the current score is the new high score, record it before exit
with open("assets/score.txt", "w") as updatedScoreFile: # open the text file in write mode
updatedScoreFile.write(str(max(listScore))) # write the new high shots score to the text file
with open("assets/gameStats.json", "w") as updatedStatsFile: # open the json file in write mode
json.dump(dictionaryGameStats, updatedStatsFile) # write the new high shots score to the text file
functionWinnerDisplay(winnerName) # Function to announce the winner
while True: # Infinite loop for exiting the game -- waiting for the user to click close button
functionExitGame()
NOTE: You do need some assets and packages, for the game to function.
Packages needed:
- import pygame # For handling graphics, sounds, and game logic
- import sys # Provides access to system-specific functions
- import random # Allows generation of random numbers
- import time # Module for implementing sleep
- import json # Module for handling dictionary as json format
- import datetime # Module for implementing time stamp
It is recommended to open gameStats.json file in Notepad++:
To view the information in the json file properly: have JSON Viewer pluggin implanted in Notepad++
After adding plugging to view json formal properly do —-> Plugins —-> JSON Viewer —-> Format JSON
Conclusion
I am very happy with the game that I developed. The final game design met most of the targets outlined. The main goal was to create a functional two player game that had controllable paddles and a moving ball that could be hit with the paddle. Collision detection between the ball, paddle and screen boundaries were implemented. I was also able to create a txt file to save the high scores. The user interface was simple but effective, with a clear gameplay area and a dedicated score bar. While the game meets its primary goals, there is still room for improvement in visual polish and additional features like menu or best of three scoring. The requirements I wanted to fulfill were achieved, but there is room for perfecting and making the gameplay smoother.
I would like to further improve the game in the future!
