2
\$\begingroup\$

I've been working on learning Pygame and I made a Breakout clone. I'm looking for stylistic advice and overall suggestions for improvement.

Controls are simple: - and to move.

# Breakout/Arkanoid clone

import pygame, sys
from pygame.locals import *
import math

# General game attributes
FPS = 100
WINDOWWIDTH = 480
WINDOWHEIGHT = 600
MAXLEVEL = 6

# Ball attributes
BALLSTARTINGX = 0
BALLSTARTINGY = 250
BALLSPEED = 6 # pixels per tick
STARTINGANGLE = 60

# Paddle attributes
PADDLEWIDTH = 100
PADDLEHEIGHT = 10
PADDLESTARTINGX = (WINDOWWIDTH-PADDLEWIDTH)/2
PADDLEY = WINDOWHEIGHT-PADDLEHEIGHT
PADDLESPEED = 6
# Brick attributes
BRICKWIDTH = 50
BRICKHEIGHT = 25
SIDEBUFFER = 15
TOPBUFFER = 220

# Colors
WHITE       = (255, 255, 255)
NAVYBLUE    = ( 60,  60, 100)
RED         = (200,   0,   0)
DARKRED     = (100,   0,   0)
ORANGE      = (200, 100,   0)
DARKORANGE  = (100,  50,   0)
YELLOW      = (200, 200,   0)
DARKYELLOW  = (100, 100,   0)
GREEN       = (  0, 200,   0)
DARKGREEN   = (  0, 100,   0)
BLUE        = (  0,   0, 200)
DARKBLUE    = (  0,   0, 100)
PURPLE      = (200,   0, 200)
DARKPURPLE  = (100,   0, 100)

RAINBOW = [(RED, DARKRED),(ORANGE,DARKORANGE),(YELLOW,DARKYELLOW),
           (GREEN,DARKGREEN),(BLUE,DARKBLUE),(PURPLE,DARKPURPLE)]

def main():
    global FPSCLOCK, DISPLAYSURF, FONT
    pygame.init()

    # Globals
    FPSCLOCK = pygame.time.Clock()
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))

    # Caption
    pygame.display.set_caption("Bricky")

    # Game objects
    ball = Ball(DISPLAYSURF)
    paddle = Paddle(DISPLAYSURF)
    game_board = Game_board(DISPLAYSURF)

    new_level = True

    # Font
    FONT = pygame.font.Font('freesansbold.ttf', 18)

    # Text objects
    text_newlevel_surf = FONT.render("New level!", True, WHITE, NAVYBLUE)
    text_newlevel = text_newlevel_surf.get_rect()
    text_newlevel.center = (WINDOWWIDTH/2, WINDOWHEIGHT/2)

    text_youwin_surf = FONT.render("YOU WIN!", True, WHITE, NAVYBLUE)
    text_youwin = text_newlevel_surf.get_rect()
    text_youwin.center = (WINDOWWIDTH/2, WINDOWHEIGHT/2)

    text_gamestart_surf = FONT.render("Game start!", True, WHITE, NAVYBLUE)
    text_gamestart = text_newlevel_surf.get_rect()
    text_gamestart.center = (WINDOWWIDTH/2, WINDOWHEIGHT/2)

    text_youlose_surf = FONT.render("YOU LOSE!", True, WHITE, NAVYBLUE)
    text_youlose = text_newlevel_surf.get_rect()
    text_youlose.center = (WINDOWWIDTH/2, WINDOWHEIGHT/2)

    # Main loop
    while True:
        DISPLAYSURF.fill(NAVYBLUE)
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == KEYDOWN:
                if(event.key == K_LEFT):
                    paddle.direction -= paddle.speed
                elif(event.key == K_RIGHT):
                    paddle.direction += paddle.speed
            elif event.type == KEYUP:
                if(event.key == K_LEFT):
                    paddle.direction += paddle.speed
                elif(event.key == K_RIGHT):
                    paddle.direction -= paddle.speed

        if new_level:
            game_board.new_board()
            ball.posx, ball.posy = BALLSTARTINGX, BALLSTARTINGY
            ball.angle = STARTINGANGLE
            new_level = False
            paddle.posx = PADDLESTARTINGX

        bricks_cleared = len(game_board.brickarray) == 0
        if bricks_cleared:
            if game_board.roundnum == MAXLEVEL:
                DISPLAYSURF.fill(NAVYBLUE)
                DISPLAYSURF.blit(text_youwin_surf, text_youwin)
                pygame.display.update()
                pygame.time.wait(1000)
            else:
                new_level = True
                DISPLAYSURF.fill(NAVYBLUE)
                if game_board.roundnum == 0:
                    DISPLAYSURF.blit(text_gamestart_surf, text_gamestart)
                else:
                    DISPLAYSURF.blit(text_newlevel_surf, text_newlevel)
                pygame.display.update()
                pygame.time.wait(1000)
                game_board.roundnum += 1
        elif ball.ballsremaining == -1:
            DISPLAYSURF.fill(NAVYBLUE)
            DISPLAYSURF.blit(text_youlose_surf, text_youlose)
            pygame.display.update()
            pygame.time.wait(2000)
            new_level = True
            game_board.roundnum = 0
            ball.ballsremaining = 3



        else:
            paddle.update()
            ball.update(paddle.posx, game_board.brickarray) # Handle ball collisions
            game_board.update(ball.brickarray) # Destroy bricks colliding with ball

        pygame.display.update()
        FPSCLOCK.tick(FPS)


class Game_board(object):
    def __init__(self, surface):
        self.surf = surface
        self.roundnum = 6
    def draw_brick(self, x, y, color):
        pygame.draw.rect(self.surf, color[0], (x,y,BRICKWIDTH, BRICKHEIGHT))
        pygame.draw.rect(self.surf, color[1], (x,y,BRICKWIDTH,BRICKHEIGHT), 3)
    def draw_board(self):
        for brick in self.brickarray:
            self.draw_brick(brick[1][0], brick[1][1], RAINBOW[brick[0][1]])
    def new_board(self):
        self.brick_indexes = zip(range(9)*self.roundnum, [i for i in range(self.roundnum) for _ in range(9)])
        self.brick_positions = [(SIDEBUFFER + x * BRICKWIDTH, (TOPBUFFER-(y*BRICKHEIGHT*1.5))) for x,y in self.brick_indexes]
        self.brickarray = zip(self.brick_indexes, self.brick_positions)
    def update(self, brickarray):
        self.brickarray = brickarray
        self.draw_board()


class Paddle(Game_board):
    def __init__(self, surf):
        """
        Initialize paddle. Paddle starts in the middle. Speed is constant.
        """
        self.surf = surf
        self.posx, self.posy = PADDLESTARTINGX, PADDLEY
        self.speed = PADDLESPEED
        self.direction = 0
        pygame.draw.rect(self.surf, WHITE,(self.posx, self.posy, PADDLEWIDTH, PADDLEHEIGHT))
    def update(self):
        """
        Move paddle if the left or right key is held down. Do not allow
        paddle to move out of bounds.
        """
        #############################
        # Handling movement of paddle
        #############################
        self.posx += self.direction
        ##############################################
        # Making sure paddle does not go out of bounds
        ##############################################
        if self.posx < 0:
            self.posx = 0
        if self.posx > WINDOWWIDTH-PADDLEWIDTH:
            self.posx = WINDOWWIDTH-PADDLEWIDTH
        # Drawing paddle
        pygame.draw.rect(self.surf, WHITE,(self.posx, self.posy, PADDLEWIDTH, PADDLEHEIGHT))


class Ball(object):
    """
    Ball object for Bricky game. Handles collision detection and bouncing.
    """
    def __init__(self, surf):
        """
        Initialize ball. Ball always starts in same position with same
        angle. Speed is constant throughout the game.
        """
        self.posx,self.posy = BALLSTARTINGX,BALLSTARTINGY
        self.speed = BALLSPEED
        self.angle = STARTINGANGLE
        self.surf = surf
        self.ballsremaining = 3

        pygame.draw.circle(self.surf, WHITE, (self.posx,self.posy), 5, 0)

    def update(self, paddlex, brickarray):
        self.brickarray = brickarray
        """
        Calculate position of ball at next tick. If no collision occurs,
        ball's position will change based on current angle and speed.

        If ball bounces off of a brick or a wall, position will not
        change this tick but angle will update to reflect the direction
        the ball is facing, based on its angle before the collision.

        If ball bounces off of the paddle, again position will not
        change but angle will update based off of the location on the
        paddle that the ball hit: the closer to the center the ball hits,
        the closer to a vertical angle the ball will have. Hitting towards
        the edges will give the ball a steeper angle closer to
        0 or 180.
        """
        ############################
        # Displaying remaining balls
        ############################
        text_ballsremaining_surf = FONT.render("Balls left: {}".format(self.ballsremaining), True, WHITE, NAVYBLUE)
        text_ballsremaining = text_ballsremaining_surf.get_rect()
        text_ballsremaining.center = (55, 10)
        self.surf.blit(text_ballsremaining_surf, text_ballsremaining)
        ######################################
        # Next position if no collision occurs
        ######################################
        # Next position depends on current angle and speed
        xdif = int(math.cos(self.angle*(math.pi/180)) * self.speed)
        ydif = int(math.sin(self.angle*(math.pi/180)) * self.speed)

        ########################
        # Bouncing off of bricks
        ########################
        ycollission = [ y1 < (self.posy + ydif) < y2 for y1, y2 in [ (y, y+BRICKHEIGHT) for x,y in [ b[1] for b in self.brickarray ] ]]
        xcollission = [ x1 < (self.posx + xdif) < x2 for x1, x2 in [ (x, x+BRICKWIDTH) for x,y in [ b[1] for b in self.brickarray ] ]]

        collissions = [ xcollission[i] and ycollission[i] for i in range(len([ b[1] for b in self.brickarray ]))]

        if any(collissions):
            collided = self.brickarray.pop([ i for i in range(len(collissions)) if collissions[i] ][0])[1]
            if (self.posx <= collided[0]) and (collided[0] <= (self.posx + xdif) <= collided[0]+BRICKWIDTH):
                if self.angle < 90:
                    adj = (90 - (self.angle%90))*2 # Change angle clockwise
                else:
                    adj = -(self.angle%90)*2 # change angle counter-clockwise
            elif (self.posx >= (collided[0]+BRICKWIDTH)) and (collided[0] <= (self.posx + xdif) <= collided[0]+BRICKWIDTH):
                if self.angle < 180:
                    adj = -(self.angle%90)*2 # Change angle counter-clockwise
                else:
                    adj = (90 - (self.angle%90))*2 # Change angle clockwise
            elif (self.posy <= collided[1]) and (collided[1] <= (self.posy + ydif) <= collided[1]+BRICKHEIGHT):
                if self.angle < 90:
                    adj = -(self.angle%90)*2 # Change angle counter-clockwise
                else:
                    adj = (90 - (self.angle%90))*2 # Change angle clockwise
            elif (self.posy >= collided[1]+BRICKHEIGHT) and (collided[1] <= (self.posy + ydif) <= collided[1]+BRICKHEIGHT):
                if self.angle < 270:
                    adj = -(self.angle%90)*2 # Change angle counter-clockwise
                else:
                    adj = (90 - (self.angle%90))*2 # Change angle clockwise

            self.angle = (self.angle + adj) % 360


        ##########################################
        # Bouncing against window borders
        ##########################################
        if not (0 < (self.posx + xdif) < WINDOWWIDTH): #Bounce off wall
            if (90 <= self.angle <= 180) or (270 <= self.angle <= 360):
                adj = -(self.angle%90)*2 # Change angle counterclockwise
            else:
                adj = (90 - (self.angle%90))*2 # Change angle clockwise
            self.angle  = (self.angle + adj) % 360

        elif self.posy + ydif < 0: # Bounce off ceiling
            if (90 <= self.angle <= 180) or (270 <= self.angle <= 360):
                adj = (90 - (self.angle%90))*2 # Change angle clockwise
            else:
                adj = -(self.angle%90)*2 # Change angle counterclockwise
            self.angle = (self.angle + adj) % 360
        ##############################################
        # Bouncing off paddle or falling out of bounds
        ##############################################
        elif WINDOWHEIGHT > self.posy + ydif > WINDOWHEIGHT-PADDLEHEIGHT: # Bounce off paddle or fall out of bounds
            if paddlex < self.posx < paddlex+PADDLEWIDTH:# Bounce off paddle
                if (270 <= self.angle <= 360):
                    adj = (90 - (self.angle%90))*2 # Change angle clockwise
                else:
                    adj = -(self.angle%90)*2 # Change angle counterclockwise
                #####
                # Angle changes based off of how far from the middle the ball has bounced
                #####
                paddleangle = (paddlex - self.posx) + PADDLEWIDTH/2
                paddleangle *= 130.0/PADDLEWIDTH
                self.angle = 270 - paddleangle
            else: # Ball falls out of bounds
                self.posx += xdif
                self.posy += ydif
                pygame.draw.circle(self.surf, WHITE, (self.posx,self.posy), 5, 0)




        ########################################
        # Movement if ball does not hit anything
        ########################################
        else:
            self.posx += xdif
            self.posy += ydif
            pygame.draw.circle(self.surf, WHITE, (self.posx,self.posy), 5, 0)

            if self.posy > WINDOWHEIGHT + 300:
                self.posx,self.posy = BALLSTARTINGX, BALLSTARTINGY
                self.angle = STARTINGANGLE
                self.ballsremaining -= 1



if __name__ == "__main__":
    main()
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

Avoid repetition

You have repetition that screams to become a function:

    adj = (90 - (self.angle%90))*2 # Change angle clockwise

    adj = -(self.angle%90)*2 # change angle counter-clockwise

Each one of this is repeated 6 times identical

Just write two easy functions:

def rotate_clockwise(angle):
    return (90 - (self.angle%90))*2

def rotate_counter_clockwise(angle):
    return -(self.angle%90)*2

And use those.

This also makes the code self-documenting removing the need for the # Change angle clockwise and # Change angle counter-clockwise comments

\$\endgroup\$
1
\$\begingroup\$

One suggestion is to avoid wildcard imports.
You can find some information and alternatives here.

Also in Ball.update you have a command before the docstring which is confusing.

Lastly you might want to use a tool like PyLint if you want to enforce some style guidelines. For example I see that several lines in your code exceed 80 characters and at some parts you have more than 3 empty lines which is not recommended.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.