Skip to content

Instantly share code, notes, and snippets.

@ssaurel
Created January 4, 2024 13:35

Revisions

  1. ssaurel created this gist Jan 4, 2024.
    255 changes: 255 additions & 0 deletions PongGame.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,255 @@
    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