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()
asked Feb 28, 2016 at 4:11
\$\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

answered Feb 28, 2016 at 23:07
\$\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.

answered Feb 28, 2016 at 21:52
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.