Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
New to Python and Pygame
#1
Hello all. I'm new to Python and Pygame. Did some programming as a kid on Atari 8-bist BASIC, and in college on Pascal and C++, so, it's been a while. Didn't get too deep into any of those languages at the time.

Can I ask for a code review of my first attempt at Pygame with a Pong clone? I wrote it procedural style, with thoughts of refactoring to OOP later.

How to post code here? (about 250-300 lines)
Reply
#2
I think I figured it out?
# PONG by JM V1.0

import pygame
import math

pygame.init()
pygame.mixer.init()
screen_width, screen_height = 480,270
scale_factor = 2
display_width, display_height = screen_width * scale_factor,\
     screen_height * scale_factor
display= pygame.display.set_mode((display_width, display_height))
screen = pygame.Surface((screen_width, screen_height))
clock = pygame.time.Clock()
FPS = 70

# game states
pause = False
menu = True
game_over = False
level_up = False
levelup_time = 0
game_over_time = 0
game_over_duration = 5000

# init sounds
score1_sound = pygame.mixer.Sound('score1.wav')
score2_sound = pygame.mixer.Sound('score2.wav')
paddle1_sound = pygame.mixer.Sound('paddle1_hit.wav')
paddle2_sound = pygame.mixer.Sound('paddle2_hit.wav')
score1_sound.set_volume(0.5)
score2_sound.set_volume(0.5)
paddle1_sound.set_volume(0.5)
paddle2_sound.set_volume(0.5)

BG_COLOR = (0,50,0)
BORDER_COLOR = NET_COLOR = BALL_COLOR = PADDLE_COLOR = (255,255,255)
TEXT_COLOR = (68,255,30)
BALL_INIT_SPEED = 150
PADDLE1_INIT_SPEED = 0
MAX_BALL_SPEED = 600
font = pygame.font.Font("pong_font.ttf", 16)
screen_ctrx, screen_ctry = screen_width//2,screen_height//2
score1 = 0
score2 = 0
level_data = {1:(100,0.5,44),
              2:(150,0.5,44),
              3:(200,0.25,44),
              4:(250,0.25,44),
              5: (300,0,44),
              6: (300,0,34),
              7: (300,0,24),
              8: (300,0,14),
              9: (300,0,6),
              }
current_level = 1
leveled_paddle2_speed, leveled_player2_lookahead, leveled_paddle1_height = level_data[current_level]

# Init ball1
ball_size= 6
ball_speed= BALL_INIT_SPEED
ball_x, ball_y = screen_ctrx, screen_ctry
ball_dirx= 1
ball_diry= 1
ball_angle = 35
ball1_rect = pygame.rect.FRect(screen_ctrx - ball_size //2,screen_ctry - 
                              ball_size //2,ball_size, ball_size)

# Init Paddles
paddle_offset = 4
paddle_width = 6
paddle_height = leveled_paddle1_height
paddle1_speed = PADDLE1_INIT_SPEED
paddle2_speed = leveled_paddle2_speed
paddle_accel = 35
player1_auto = False
paddle1_rect = pygame.rect.FRect(paddle_offset,screen_ctry-
                                (paddle_height//2),paddle_width,paddle_height)
paddle2_rect = pygame.rect.FRect(screen_width-paddle_width-paddle_offset,
                                screen_ctry-(paddle_height//2),
                                paddle_width,paddle_height)

running = True
while running:
    dt = clock.tick(FPS) / 1000.0  # Time since last frame in seconds
# process input
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.WINDOWMOVED:
            pause = True  # Set flag when window is moved
        
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
            if event.key == pygame.K_SPACE and not menu:
                pause = not pause
            if event.key == pygame.K_RETURN and menu:
                menu = not menu
            
    if not player1_auto and not pause and not game_over and not level_up and not menu:
        keys = pygame.key.get_pressed()
        if keys[pygame.K_UP] and paddle1_rect.top>0:
            paddle1_speed += paddle_accel
            paddle1_rect.centery -= paddle1_speed*dt
           
        if keys[pygame.K_DOWN] and paddle1_rect.bottom < screen_height:
            paddle1_speed += paddle_accel
            paddle1_rect.centery += paddle1_speed*dt
           
        if not keys[pygame.K_UP] and not keys[pygame.K_DOWN]:
            paddle1_speed = 0
            
        if paddle1_speed > 300:
            paddle1_speed = 300

    if not pause and not menu and not game_over and not level_up:         
# move AI paddle
        if player1_auto:
            if ball1_rect.centerx < screen_ctrx:
                if paddle1_rect.centery> ball1_rect.centery and \
                    paddle1_rect.top>0:
                    paddle1_rect.centery -= paddle1_speed*dt
                if paddle1_rect.centery<ball1_rect.centery and \
                    paddle1_rect.bottom<screen_height:
                    paddle1_rect.centery += paddle1_speed*dt
        if ball1_rect.centerx > screen_width * leveled_player2_lookahead:
            if paddle2_rect.centery>ball1_rect.centery and \
                    paddle2_rect.top>0:
                paddle2_rect.centery -= paddle2_speed*dt
            if paddle2_rect.centery<ball1_rect.centery and \
                    paddle2_rect.bottom<screen_height:
                paddle2_rect.centery += paddle2_speed*dt
                
# handle ball wall bounce
        if ball1_rect.top <=0: 
            ball1_rect.top = 1
            ball_diry *=-1
        if ball1_rect.bottom>=screen_height:
            ball1_rect.bottom = screen_height-1
            ball_diry *=-1

# handle ball and paddle collisions
        if ball1_rect.colliderect(paddle1_rect):
            paddle1_sound.play()
            if ball1_rect.bottom <= paddle1_rect.top + 2 or ball1_rect.top >= paddle1_rect.bottom - 2:
                ball_diry *= -1
            else:
                
                ball_x = paddle1_rect.right+ball_size/2 + 1
                ball_angle = (abs(paddle1_rect.centery - 
                              ball1_rect.centery) /0.5) + 5
                ball_dirx *=-1
                ball_speed *=1.05
                if ball_speed > MAX_BALL_SPEED:
                    ball_speed = MAX_BALL_SPEED
        if ball1_rect.colliderect(paddle2_rect):
            paddle2_sound.play()
            if ball1_rect.bottom <= paddle2_rect.top + 2 or ball1_rect.top >= paddle2_rect.bottom - 2:
                ball_diry *= -1
            else:
                ball_x = paddle2_rect.left-ball_size/2 - 1
                ball_angle = (abs(paddle2_rect.centery - 
                                ball1_rect.centery) /0.5) + 5
                ball_dirx *=-1
                ball_speed *=1.05
                if ball_speed > MAX_BALL_SPEED:
                    ball_speed = MAX_BALL_SPEED
# score/reset ball
        if ball1_rect.centerx > screen_width:
            #reset ball
            score1_sound.play()
            score1+=1
            ball_speed = BALL_INIT_SPEED
            ball_dirx *=-1
            ball_angle = 35
            ball_x = screen_ctrx
            ball_y = screen_ctry
        if ball1_rect.centerx < 0:
            #reset ball
            score2_sound.play()
            score2+=1
            ball_speed = BALL_INIT_SPEED
            ball_dirx *=-1
            ball_angle = 35
            ball_x = screen_ctrx
            ball_y = screen_ctry
# check for win
        if score1 ==11 or score1-score2==3:
            level_up = True
            levelup_time = pygame.time.get_ticks()  # Record time of game over

        if score2 ==11 or score2-score1==3:
            game_over = True
            game_over_time = pygame.time.get_ticks()  # Record time of game over

# calculate dx,dy - the ball incremental move per frame
        '''ball x increment = cos( angle) * speed *  x direction
        ball y increment = sin( angle) * speed * x direction
        COS(angle) * ball speed keeps the ball speed the same at any angle
        Ball direction is - or + depending on up/down/left/right
        This setting makes it easy to change the ball direction 
        after IF statement'''
        ball_dx = math.cos(ball_angle/180*math.pi)*(ball_speed*dt)*ball_dirx
        ball_dy = math.sin(ball_angle/180*math.pi)*(ball_speed*dt)*ball_diry 
        ball_x +=ball_dx
        ball_y +=ball_dy
# move the ball in x,y
        ball1_rect.centerx = ball_x
        ball1_rect.centery = ball_y
# draw objects
    screen.fill((BG_COLOR))
    score1_text = font.render(str(score1), False, TEXT_COLOR)  
    score1_text_rect = score1_text.get_rect(center=(screen_width//4, 15))  
    score2_text = font.render(str(score2), False, TEXT_COLOR)
    score2_text_rect = score2_text.get_rect(center=(screen_width*3//4, 15))   
    screen.blit(score1_text, score1_text_rect)
    screen.blit(score2_text, score2_text_rect)
    pygame.draw.rect(screen,BORDER_COLOR,(0,0,480,270),1)
    for netlines in range(8):
        pygame.draw.line(screen,NET_COLOR,(screen_width//2,netlines * 35 + 5),(screen_width//2,netlines*35 + 20))
    pygame.draw.rect(screen,BALL_COLOR,ball1_rect)
    pygame.draw.rect(screen,BALL_COLOR,paddle1_rect)
    pygame.draw.rect(screen,BALL_COLOR,paddle2_rect)

# Game State logic
    if menu:
        menu_text = font.render("Pong by JM", False, 
                                 TEXT_COLOR)  
        menu_text2 = font.render("Press ENTER to start", False, 
                                 TEXT_COLOR)  
        menu_text3 = font.render("Press ESC anytime to QUIT. W and S move the paddle", False, 
                                 TEXT_COLOR)         
        menu_text_rect = menu_text.get_rect(center=(screen_width//2, 30))    
        menu_text_rect2 = menu_text2.get_rect(center=(screen_width//2, 67))
        menu_text_rect3 = menu_text3.get_rect(center=(screen_width//2, 102))    
        screen.blit(menu_text, menu_text_rect)
        screen.blit(menu_text2, menu_text_rect2)
        screen.blit(menu_text3, menu_text_rect3)



    if pause:
        pause_text = font.render("PAUSED - Press SPACE to resume", False, 
                                 TEXT_COLOR)  
        pause_text_rect = pause_text.get_rect(center=(screen_width//2, 67))    
        screen.blit(pause_text, pause_text_rect)
    
    if game_over:
        game_over_text = font.render("GAME OVER", False, 
                                     TEXT_COLOR)  
        game_over_text_rect = game_over_text.get_rect(center=(screen_width//2, 99))    
        screen.blit(game_over_text, game_over_text_rect)
        if pygame.time.get_ticks() - game_over_time >= game_over_duration:
            game_over = False
            menu = True
            score1 = 0
            score2 = 0
            current_level = 1
            leveled_paddle2_speed, leveled_player2_lookahead, leveled_paddle1_height = level_data[current_level]
            paddle2_speed = leveled_paddle2_speed
            paddle1_rect.height = leveled_paddle1_height
            paddle1_rect.centery = screen_height//2
            paddle2_rect.centery = screen_height//2

    if level_up:
        level_up_text = font.render(f"Leveling up to level {current_level+1}...", False, 
                                     TEXT_COLOR)  
        level_up_text_rect = level_up_text.get_rect(center=(screen_width//2, 99))    
        screen.blit(level_up_text, level_up_text_rect)
        paddle1_rect.centery = screen_height//2
        paddle2_rect.centery = screen_height//2
        if pygame.time.get_ticks() - levelup_time >= game_over_duration:
            
            score1 = 0
            score2 = 0
            current_level +=1
            if current_level >9:
                current_level = 9
            leveled_paddle2_speed, leveled_player2_lookahead, leveled_paddle1_height = level_data[current_level]
            paddle2_speed = leveled_paddle2_speed
            paddle1_rect.height = leveled_paddle1_height
            paddle1_rect.centery = screen_height//2
            paddle2_rect.centery = screen_height//2
            level_up = False
            
    scaled_screen = pygame.transform.scale(screen, (display_width, 
                                                    display_height))
    display.blit(scaled_screen, (0, 0))

    pygame.display.flip()
     
pygame.quit()
Reply
#3
You should read up about sprites.
Reply
#4
(Sep-11-2025, 03:10 AM)deanhystad Wrote: You should read up about sprites.

Agreed. Planning to try coding with sprites for a platformer or a Pitfall! clone. I thought using "colliderect" for Pong was good enough to start learning.

Any bad coding habits you can see so far in what I'm doing (besides repeating code where functions or OOP can be used)?
Reply
#5
The event loop is getting large. I would implement level up, menu and pause as function calls, maybe each having their own event loop. That would make it easier to add new features to your game.

I like it. It plays well
Reply
#6
Thanks for reviewing deanhystad!
Planning on refactoring using functions and/or OOP, taking your suggestions.
Reply
#7
An example that uses customized sprite classes to demonstrate inheritance. Using type notation, docstrings, pygame math and properties. Not a real game.
"""Pong like game demo using sprites."""
import pygame
import math
import random

screen_size = 600, 400
screen_center = screen_size[0] // 2, screen_size[1] // 2
frame_rate = 70
court_color = "#3C638E"


class EnhancedSprite(pygame.sprite.Sprite):
    """High res position sprite.

    Saves sprite position as floats instead of
    int to prevent quantization when moving slowly
    or at shallow angles.
    """

    def __init__(
            self,
            image: pygame.Surface, 
            pos: pygame.math.Vector2 | tuple[float, float]
    ) -> None:
        """Initalize sprite.

        Arguments:
            image: Pyagem Surface drawn in the sprite location.
            pos: (x, y) corrdinates for ball center.
        """
        super().__init__()
        self.image = image
        self.rect = self.image.get_rect()
        self.pos = pos

    @property
    def pos(self) -> pygame.math.Vector2:
        """Return (x, y) of sprite center."""
        return self._pos

    @pos.setter
    def pos(self, pos: pygame.math.Vector2 | tuple[float, float]) -> None:
        """Set sprite center to pos(x, y).
        
        Arguments:
            pos: Sprite position as a vector.
        """
        if isinstance(pos, pygame.math.Vector2):
            self._pos = pos
        else:
            self._pos = pygame.math.Vector2(pos)
        self.rect.centerx, self.rect.centery = self.pos

    def draw(self, screen: pygame.Surface) -> None:
        """Draw self on screen."""
        screen.blit(self.image, self.rect)


class Ball(EnhancedSprite):
    """Ball sprite.  Knows how to move the ball."""

    def __init__(
            self,
            pos: tuple[float, float],
            radius: int = 6,
            speed: float = 2,
            max_speed: float = 10,
            angle: float = 35
    ) -> None:
        """Initalize sprite.

        Arguments:
            pos: (x, y) corrdinates for ball center.
            radius: Radius of circle (in pixels) of ball.
            speed: Speed the ball travels.
            angle: Initial angle (in degrees) that ball travels.
        """
        image = pygame.Surface([radius*2, radius*2])
        image.fill("black")
        image.set_colorkey("black")
        pygame.draw.circle(image, "white", (radius, radius), radius, 0)
        super().__init__(image, pos)
        self.heading = pygame.math.Vector2(1, 0)
        self.speed = speed
        self.max_speed = max_speed
        self.angle = angle

    @property
    def angle(self) -> float:
        """Return angle of ball travel.

        Returns:
            Angle measured in degrees starting at 3 o'clock.
        """
        return pygame.math.Vector2(1, 0).angle_to(self.heading)

    @angle.setter
    def angle(self, angle) -> None:
        """Set angle of ball travel.

        Arguments:
            angle: Angle measured in degrees starting at 3 o'clock.
        """
        self.heading = pygame.math.Vector2(1, 0).rotate(angle)

    def speed_up(self, percent: float) -> None:
        """Increase the ball speed."""
        self.speed = min(self.max_speed, self.speed * (1 + percent / 100))

    def update(self) -> None:
        """Update position of ball."""
        self.pos = self.pos + self.heading * self.speed


class Paddle(EnhancedSprite):
    """A base class for paddles

    Paddles are sprites that move up/down controlled
    by key presses or tracked by the computer.  Paddle
    speed and acceleration are limited.

    When the paddle collides with the ball, the ball
    bounces off the paddle and increases speed.  The
    angle the ball bounces can be altered by striking
    the ball off center on the paddle.
    """

    def __init__(
            self,
            x: int,
            max_y: int,
            width: int = 10,
            height: int = 50,
            speed: float = 3,
            accel: float = 1
    ) -> None:
        """Initialize instance.

        Arguments:
            x: X location of paddle.
            max_y: Bottom of paddle range.
            width: Paddle width.
            height: Paddle height.
            speed: Max paddle speed.
            accel: Speed ramp rate.
        """
        image = pygame.Surface((width, height))
        image.fill("white")
        super().__init__(image, (x, max_y // 2))
        self.max_y = max_y
        self.max_speed = speed
        self.accel = accel
        self.speed = 0

    def update(self, ball: Ball) -> None:
        """Test for collision with ball."""
        if not ball.rect.colliderect(self.rect):
            return
        # Using cludgy heuristic to compute an aim adjustment to the
        # ball's reflection if it doesn't hit center of the paddle
        dy = ball.rect.centery - self.rect.centery
        aim = math.degrees(math.atan2(dy / self.rect.height, 2))
        angle = ball.angle
        if ball.heading.x > 0:
            # Right paddle
            ball.rect.right = self.rect.x
            incidence = angle
            reflection = max(-50, min(50, -incidence - aim))
            ball.angle = 180 + reflection
        else:
            # Left paddle
            ball.rect.x = self.rect.right
            incidence = angle + 180 if angle < 0 else angle - 180
            reflection = max(-50, min(50, -incidence + aim))
            ball.angle = reflection
        ball.speed_up(5)

    def move(self, direction: int) -> None:
        """Move paddle up (-1) or down(1) or stop (0).

        Arguments:
            direction: Direction to move paddle.
        """
        if direction == 0:
            self.speed = 0
        else:
            speed = self.speed + self.accel * direction
            self.speed = max(-self.max_speed, min(self.max_speed, speed))
        x, y = self.rect.center
        self.pos = (x, max(0, min(self.max_y, y + self.speed)))


class PlayerPaddle(Paddle):
    """A paddle that is player controlled using key_up and key_down"""
    def __init__(
            self,
            x: int,
            max_y: int,
            width: int = 10,
            height: int = 50,
            speed: float = 3,
            accel: float = 1,
            key_up: int = pygame.K_UP,
            key_down: int = pygame.K_DOWN
    ) -> None:
        """Initialize instance.

        Arguments:
            key_up: Key press to move paddle up.
            key_down: Key press to move paddle down.
        """
        super().__init__(x, max_y, width, height, speed, accel)
        self.key_up = key_up
        self.key_down = key_down

    def update(self, ball) -> None:
        """Update paddle position and check for ball collision."""
        keys = pygame.key.get_pressed()
        if keys[self.key_up]:
            self.move(-1)
        elif keys[self.key_down]:
            self.move(1)
        else:
            self.move(0)
        super().update(ball)


class ComputerPaddle(Paddle):
    """Computer controlled paddle."""

    def update(self, ball) -> None:
        """Update paddle position and check for ball collision."""

        x, y = self.pos
        bx, by = ball.pos
        move = 1 if by > y else -1
        # Only track ball as it approaches.
        if (x - bx) * ball.heading.x < 0:
            move = 0
        self.move(move)
        super().update(ball)


def text(
        screen,
        text: str,
        pos: tuple[int, int],
        font: pygame.font.Font | None = None,
        color="white"
) -> None:
    """Draw text on screen.
    
    Arguments:
        screen: Screen where text is drawn.
        text: Text to display.
        pos: (x, y) position of text center.
        font: Font used to draw text.
        color: Text color.
    """
    if font is None:
        font = default_font
    image = font.render(text, False, color)
    screen.blit(image, image.get_rect(center=pos))


def play_pong() -> None:
    """Play pong game."""

    paddles = pygame.sprite.Group([
        PlayerPaddle(x=10, max_y=screen_size[1]),
        ComputerPaddle(x=screen_size[0]-10, max_y=screen_size[1], accel=0.5)
    ])
    ball = Ball(pos=screen_center, angle=random.randint(-45, 45))
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return
        ball.update()
        if ball.rect.x > screen_size[0] or ball.rect.right < 0:
            ball = Ball(screen_center, angle=random.randint(-45, 45))
        elif ball.rect.y <= 0 or ball.rect.bottom >= screen_size[1]:
            ball.heading.y = -ball.heading.y
        paddles.update(ball)

        screen.fill(court_color)
        paddles.draw(screen)
        ball.draw(screen)
        display.blit(
            pygame.transform.scale(screen, display.get_size()), (0, 0)
        )
        pygame.display.flip()
        clock.tick(frame_rate)


def splash_screen() -> None:
    """Display splash screen."""
    screen.fill("black")
    text(
        screen,
        "Welcome to Pong",
        (screen_center[0], screen_center[1] - 100),
        pygame.font.Font(None, 64)
    )
    text(
        screen,
        "Press any key to continue",
        (screen_center[0], screen_center[1] + 100)
    )
    display.blit(pygame.transform.scale(screen, display.get_size()), (0, 0))
    pygame.display.flip()
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False
            if event.type == pygame.KEYDOWN:
                return True


pygame.init()
default_font = pygame.font.Font(None, 32)
display = pygame.display.set_mode(screen_size, pygame.RESIZABLE)
screen = pygame.Surface(screen_size)
clock = pygame.time.Clock()
if splash_screen():
    play_pong()
pygame.quit()
Reply
#8
Thanks for the example...lot of stuff in there I don't know yet. Smile
Reply
#9
Little late. @deanhystad. Why are you saying __init__ method is return None. It always return an object.
99 percent of computer problems exists between chair and keyboard.
Reply
#10
@Windspar

Like all methods, __init__() returns a value, but that value has to be None.

When you create an instance of an object you do this:
instance = some_class(args)
Behind closed doors, python does this:
instance = some_class.__new__()
instance.__init__(args)
return instance
The value returned by __init__ is ignored. Do you expect __init__ to automatically return self? It does not.
class A:
    def __init__(self):
        pass

class B(A):
    def __init__(self):
        print("Value returned by A.__init__()", super().__init__())

b = B()
Output:
Value returned by A.__init__() None
If you write __init__() to return a value other than None, Python flags an error.
class A:
    def __init__(self):
        return self

a = A()
Error:
Exception has occurred: TypeError __init__() should return None, not 'A' File "...test.py", line 5, in <module> a = A() ^^^ TypeError: __init__() should return None, not 'A'
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020