2
\$\begingroup\$

I made a game in Python where you play Pong against an AI. As I am quite new to pygame, I would be grateful to hear any possible improvements.

This is my code:

import random import pygame from pygame.locals import ( K_UP, K_DOWN, K_w, K_s, QUIT ) BG_COLOR = (255,) * 3 FG_COLOR = (0,) * 3 SCREEN_WIDTH = 800 SCREEN_HEIGHT = 500 PADDLE_X = SCREEN_WIDTH / 15 PADDLE_Y = SCREEN_HEIGHT / 2 PADDLE_WIDTH = SCREEN_WIDTH / 30 PADDLE_HEIGHT = SCREEN_HEIGHT / 5 SPEED_MULTIPLIER = 3 class Paddle(pygame.sprite.Sprite): def __init__(self, x, y, width, height, *groups): super().__init__(*groups) self.image = pygame.Surface((width, height)) self.image.fill(FG_COLOR) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y def update(self): if self.rect.collidepoint(self.rect.x, 0): self.rect.y += SPEED_MULTIPLIER elif self.rect.collidepoint(self.rect.x, SCREEN_HEIGHT): self.rect.y -= SPEED_MULTIPLIER class Ball(pygame.sprite.Sprite): def __init__(self, x, y, r, *groups): super().__init__(*groups) self.image = pygame.Surface((r * 2,) * 2, pygame.SRCALPHA) self.image = self.image.convert_alpha() pygame.draw.circle(self.image, FG_COLOR, (r, r), r) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.dx = random.choice((-1, 1)) self.dy = random.choice((random.uniform(1, -0.7), random.uniform(0.7, 1))) self.radius = r def update(self): self.rect.x += self.dx * SPEED_MULTIPLIER self.rect.y += self.dy * SPEED_MULTIPLIER pygame.init() screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) clock = pygame.time.Clock() sprites = pygame.sprite.Group() computer_paddle = Paddle( PADDLE_X, PADDLE_Y, PADDLE_WIDTH, PADDLE_HEIGHT, sprites ) human_paddle = Paddle( SCREEN_WIDTH - 2 * PADDLE_X, PADDLE_Y, PADDLE_WIDTH, PADDLE_HEIGHT, sprites ) ball = Ball( SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, SCREEN_WIDTH / 40, sprites ) while True: if any([event.type == QUIT for event in pygame.event.get()]): break screen.fill(BG_COLOR) keys_pressed = pygame.key.get_pressed() # right paddle controls if keys_pressed[K_UP] or keys_pressed[K_w]: human_paddle.rect.y -= SPEED_MULTIPLIER if keys_pressed[K_DOWN] or keys_pressed[K_s]: human_paddle.rect.y += SPEED_MULTIPLIER # computer logic if ball.rect.centerx < SCREEN_WIDTH / 2: if ball.rect.centery > computer_paddle.rect.centery: computer_paddle.rect.centery += SPEED_MULTIPLIER else: computer_paddle.rect.centery -= SPEED_MULTIPLIER elif abs(computer_paddle.rect.centery - SCREEN_HEIGHT / 2) > SCREEN_HEIGHT / 10: if computer_paddle.rect.centery > SCREEN_HEIGHT / 2: computer_paddle.rect.centery -= SPEED_MULTIPLIER else: computer_paddle.rect.centery += SPEED_MULTIPLIER # move the ball if ball.rect.colliderect(computer_paddle.rect) \ or ball.rect.colliderect(human_paddle.rect): ball.dx *= -1 # while ball isn't touching either paddle while not (ball.rect.colliderect(computer_paddle.rect) or ball.rect.colliderect(human_paddle.rect)): ball.rect.x += ball.dx ball.dy += random.uniform(-0.2, 0.2) # random bounces if not (0 < ball.rect.y and ball.rect.y + ball.rect.height < SCREEN_HEIGHT): ball.dy *= random.uniform(-1.2, -0.8) # random bounces # check if game is over if not 0 < ball.rect.x < SCREEN_WIDTH: break sprites.update() sprites.draw(screen) pygame.display.flip() clock.tick(120) pygame.quit() 

Game window:
Game window

\$\endgroup\$
1
  • \$\begingroup\$@kimi, I removed the pygame link - that's something for the tag wiki rather than for an individual question.\$\endgroup\$CommentedOct 28, 2023 at 19:59

1 Answer 1

3
\$\begingroup\$

Nice effort, this is some good looking code!


init bug

 self.dy = random.choice((random.uniform(1, -0.7), random.uniform(0.7, 1))) 

That doesn't make sense to me. During play we observe the initial vector is sometimes nearly horizontal. It appears you wanted

 self.dy = random.choice((random.uniform(-1, -0.7), random.uniform(0.7, 1))) 

Such a "give magic number twice" bug would have been unlikely if you'd instead phrased it

 self.dy = random.choice((-1, 1)) * random.uniform(0.7, 1) 

symmetry

Some of your magic numbers are just fine as-is. For example, starting the ball at center of screen seems a perfectly natural choice.

This expression seems weird:

human_paddle = Paddle( SCREEN_WIDTH - 2 * PADDLE_X, ... 

I would have expected SCREEN_WIDTH - PADDLE_X - PADDLE_WIDTH, for symmetry with computer_paddle.


extract helper

There's not a lot to DRY up here, but to make the logic more readable I do recommend you define this predicate:

def is_colliding() -> bool: return (ball.rect.colliderect(computer_paddle.rect) or ball.rect.colliderect(human_paddle.rect)) 

Consider defining an is_vertically_in_bounds predicate, just to improve legibility of not is_vertically_in_bounds(), as the current code doesn't exactly read like an English sentence. No biggie. Or consider applying De Morgan to the bounds check. Why? The computer doesn't care which way you phrase it, but humans do better when reasoning about the positive instead of the negative.


bounds check

OTOH, the horizontally in bounds test appears to be buggy. On the left, it's fine. But on the right you want to pay the same careful attention to detail as when you were doing the vertical check. That is, the ball.rect.x + ball.rect.width right-hand side is what matters, rather than the left-hand side that you currently check.

During play we occasionally see glitches where ball bounces behind the human's paddle and re-enters play.

The ball's horizontal movement usually happens, very nicely, in this way:

 def update(self): self.rect.x += self.dx * SPEED_MULTIPLIER 

I don't understand this business:

 # move the ball if is_colliding(): ball.dx *= -1 # while ball isn't touching either paddle while not is_colliding(): ball.rect.x += ball.dx 

(I paraphrased slightly, to simplify.)

I assume the goal was to ensure that we are not is_colliding() when we emerge from that code. So the negation of while not doesn't make sense -- surely we wanted the positive instead?

You would better reveal Author's Intent if you appended

 assert not is_colliding() 

after that code. Plus, if it turns out there are glitches, that would help you chase them down.


refactor

The main loop is a little on the long side -- we have to scroll vertically to read all of it.

Consider breaking out helpers:

  • def read_keyboard(),
  • def move_paddles(), and maybe even
  • def adjust_ball_speed().

This code achieves most of its design goals.

I would be willing to delegate or accept maintenance tasks on it.

\$\endgroup\$

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.