Categories
Coding

Paddle Pong

Splash screen for the game

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

  1. 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.
  2. Players move paddles appropriately using the specified keys to hit the ball.
  3. If ball collides with a paddle, the ball changes direction going towards the opposite player’s paddle.
  4. The score increases by increments of 10 of the player whose paddle hit the ball.
  5. Game continues until ball hits the right or left side of the screen.
  6. 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.
  7. 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!

Leave a Reply

Your email address will not be published. Required fields are marked *