Final Code

Here is PONG in all its full glory

from math import *
import pygame
import sys
import random


# ? This is the Player Object
class Player:
    def __init__(self, x: int) -> None:
        # ? We set the x and y position of the player. The value of the y position is at the center of the screen by default
        self.x = x
        self.h = screen_height*0.4
        self.y = (screen_height - self.h) / 2
        self.w = 20

        # ? This is the rectangle object of the player class
        self.rect = pygame.rect.Rect(self.x, self.y, self.w, self.h)

        # ? This is how fast the paddles move every frame
        self.move_speed = 15

        # ? This is a variable to basically tell if the paddle is moving
        # ? We do this because pygame doesn't have a built in key held down event type
        # ? Further explanation in the event loop
        self.moving = False
        self.moving_up = False


    # ? This is the update function that gets called every frame
    def update(self):
        # ? We check if we are moving first and then change the y position of the paddle's rectangle accordingly
        if self.moving:
            if self.moving_up:
                # ? We check if we have reached the top of the screen and then stop changing the y postion of the paddle's rectangle
                if self.rect.y > 0:
                    self.rect.y -= self.move_speed
                else:
                    self.rect.y = 0
            else:
                # ? We check if we have reached the bottom of the screen and then stop changing the y postion of the paddle's rectangle
                if self.rect.y < screen_height - self.h:
                    self.rect.y += self.move_speed
                else:
                    self.rect.y = screen_height - self.h

    # ? This just resets the paddle's location
    def reset(self):
        self.rect.y = (screen_height - self.h)/2
        self.moving = False

    # ? This shows the draws the paddle's rectangle object to the screen
    def show(self):
        pygame.draw.rect(display, foreground_color, self.rect)


# ? This is the ball object
class Ball:
    def __init__(self):
        # ? This is the size of the ball
        self.size = 20

        # ? This calculates the center position of the screen
        center_horizontal = (screen_width - self.size) / 2
        center_vertical = (screen_height - self.size) / 2

        # ? This is pygame rectangle object that gets drawn onto the screen
        self.rect = pygame.Rect(
            center_horizontal, center_vertical, self.size, self.size)

        # ? This chooses a random angle for velocity vector
        self.angle = random.choice(possible_angles)

        # ? This is the x component of the velocity vector
        self.x_component = cos(radians(self.angle))

        # ? This is the y component of the velocity vector
        self.y_component = sin(radians(self.angle))

        # ? This is the speed of the ball
        self.speed = 15

        # ? This is a variable that fixes a bug
        # ? Check the check_collisions method for more
        self.should_check = True

    def reset(self, left_paddle: Player, right_paddle: Player):
		self.angle = random.choice(possible_angles)

        # ? This just resets the ball and the paddles position and recalculates the x and y components of the velocity vector
        self.rect.x = (screen_width - self.size) / 2
        self.rect.y = (screen_height - self.size) / 2
        self.x_component = cos(radians(self.angle))
        self.y_component = sin(radians(self.angle))
        left_paddle.reset()
        right_paddle.reset()

    # ? This is kinda like a mini physics enginej
    def check_collisions(self, left_paddle: Player, right_paddle: Player):

        # ? We tell the interpreter that we are about to modify some global variables
        global left_score, right_score, wait_timer

        # ? Checking if the ball collides with the top part or the bottom part of the screen
        if self.rect.y <= 0 or self.rect.y + self.rect.h >= screen_height:
            # ? We just flip the value of the y component
            # ? So for example, if it was moving down it will then start moving up and vice versa
            self.y_component *= -1

        # ? Checking if the ball collides with the left paddle
        if self.rect.colliderect(left_paddle.rect) or self.rect.colliderect(right_paddle.rect):
            # ? So what does this should check variable do
            # ? This is just variable that just specifies if the ball collided with a paddle
            # ? We do this because we want to check the collisions once
            # ? Since when the ball collides with a paddle the x component is reversed
            # ? We only want to reverse the component once
            # ? So why? I bet that's what you are thinking
            # ? When we add the x component to ball's x position we could be inside a paddle
            # ? If we then just flip the vector when it has collided we could flip it multiple times
            # ? Since the should check variable is true by default the flip should only do once and then it should then set it to not check
            # ? Then we are not colliding a paddle anymore we then set the variable to true so when it collides with another paddle it flips the component once
            # ? And then rinse and repeat
            if self.should_check:
                self.x_component *= -1
                self.should_check = False
        else:
            self.should_check = True

        # ? Checking if the left side of the ball has touched the left side of the screen
        if self.rect.x <= 0 and not self.rect.colliderect(left_paddle.rect):

            # ? This increments the score for player 1
            right_score += 1

            # ? This resets the entire game
            self.reset(left_paddle, right_paddle)

            # ? waits for a 2 seconds before it resumes gameplay
            wait_timer = 120

        # ? Checking if the right side of the ball has touched the right side of the screen
        if self.rect.x + self.rect.w >= screen_width and not self.rect.colliderect(right_paddle.rect):
            # ? This increments the score for player 2
            left_score += 1
            self.reset(left_paddle, right_paddle)
            # ? waits for a 2 seconds before it resumes gameplay
            wait_timer = 120

    # ? This method runs every frame
    def update(self):
        # ? Adding the x and y components to the x and y position of the ball's rectangle object
        # ? We multiply the vector by a mangitude in order to make the ball move faster
        self.rect.x += self.x_component * self.speed
        self.rect.y += self.y_component * self.speed

    def show(self):
        # ? This just draws the ball's rectangle to the screen
        pygame.draw.rect(display, foreground_color, self.rect)


# ? These are the possible angles that the ball could be shot in
# ? The first 3 angles will shoot the ball to the right side of the screen
# ? The last 3 angles will shoot the ball to left side of the screen
possible_angles = [15, 45, 60, 120, 150, 135]

# ? This defines the screen size and set's the window to that size
screen_size = (screen_width, screen_height) = (1000, 400)
display = pygame.display.set_mode(screen_size)

# ? Definitions for some nice colors for our game :)
# ? I used hexadecimal to define the colors but you can use rgb
# ? You can just google hexadecimal color for more info
background_color = pygame.Color("#1b1b1e")
foreground_color = pygame.Color("#F2F3D9")

# ? We define the clock and then set the title of the window to "Pong"
clock = pygame.time.Clock()
pygame.display.set_caption("Pong")

#? We initalize pygame
pygame.init()

# ? Definition of some variables that will be used throughout the game
left_score = 0
right_score = 0
FRAMERATE = 60
wait_timer = 0
running = True

# ? We define a font object for showing the score and define the font size
font_size = screen_height * 0.1
font = pygame.font.SysFont(None, int(font_size))

# ? This creates the two paddles and ball object
left_paddle = Player(-10)
right_paddle = Player(screen_width-10)
ball = Ball()


# ? This function show the score to the screen
def show_score():
    # ? We define the left text object
    # ? We convert the left score to a string and display with a nice white color
    left_text = font.render(str(left_score), True, foreground_color)
    # ? This set the location of the text to be slightly to the left of the center of the screen
    left_text_location = ((screen_width / 2) - (font_size * 2), 20)
    # ? We draw the text to the display at the location calculated
    display.blit(left_text, left_text_location)
    # ? We define the right text object
    # ? We convert the right score to a string and display with a nice white color
    right_text = font.render(str(right_score), True, foreground_color)
    # ? This set the location of the text to be slightly to the right of the center of the screen
    right_text_location = ((screen_width / 2) + (font_size * 2), 20)
    # ? We draw the text to the display at the location calculated
    display.blit(right_text, right_text_location)



# ? This is basically the event loop
# ? The order by which things are put things here matter a lot
# ? For the event loop we perform the following things
# ? -> Check the quit event and for any keyboard events
# ? -> Draw the background color
# ? -> Draw the paddles and the ball
# ? -> Draw the left and right score of the players
# ? -> Update the ball's position and the paddle's position
# ? -> Then we check for collisions between the ball, the screen and any of the paddles
# ? And then we repeat forever and ever
while running:
    # ? We first get all the events that pygame is giving us
    for e in pygame.event.get():
        # ? We checked if the player wants to quit the game
        if e.type == pygame.QUIT:
            # ? This breaks out the event loop and then quits the game
            running = False
            pygame.quit()
            sys.exit()

        # ? This is where we do all of our input stuff for the game
        # ? This checks if the player has pressed a key down
        elif e.type == pygame.KEYDOWN:

            # ? We check if we pressed the w key and say that the left paddle is moving up
            if e.key == pygame.K_w:
                left_paddle.moving = True
                left_paddle.moving_up = True
            # ? We check if we pressed the up key and say that the right paddle is moving up
            elif e.key == pygame.K_UP:
                right_paddle.moving = True
                right_paddle.moving_up = True
            # ? We check if we pressed the s key and say that the left paddle is moving down
            elif e.key == pygame.K_s:
                left_paddle.moving = True
                left_paddle.moving_up = False
            # ? We check if we pressed the down key and say that the right paddle is moving down
            elif e.key == pygame.K_DOWN:
                right_paddle.moving = True
                right_paddle.moving_up = False
        # ? This checks if the player has stopped pressing the any keys
        elif e.type == pygame.KEYUP:
            # ? We stop moving the left paddle when we release the w key
            if e.key == pygame.K_w:
                left_paddle.moving = False
            # ? We stop moving the right paddle when we release the up key
            elif e.key == pygame.K_UP:
                right_paddle.moving = False
            # ? We stop moving the left paddle when we release the s key
            elif e.key == pygame.K_s:
                left_paddle.moving = False
            # ? We stop moving the right paddle when we release the down key
            elif e.key == pygame.K_DOWN:
                right_paddle.moving = False

    # ? We fill the display with black background color
    display.fill(background_color)
    # ? We then show the ball and the paddles
    ball.show()
    left_paddle.show()
    right_paddle.show()
    show_score()

    # ? Ah! The wait timer
    # ? This is a variable that we created when we defining some stuffs
    # ? This basically pauses the game for some time
    # ? We achieve this by not update the two paddles location and the ball locations and also by not checking collisions
    if wait_timer == 0:
        left_paddle.update()
        right_paddle.update()
        ball.check_collisions(left_paddle, right_paddle)
        ball.update()

    # ? If the timer is not zero it doesn't do anything at all
    else:
        # ? Then we count the decrement the timer by 1 every frame
        wait_timer -= 1

    # ? This updates the display to then show the ball and the paddles
    pygame.display.update()

    # ? This makes sure that the event loop runs at the specified frame rate
    clock.tick(FRAMERATE)