Mar-06-2022, 04:53 PM
import random, math, pygame from PIL import Image from itertools import product # Define colors used by the game. TEXT_COLOR = (255, 255, 255) BACKGROUND = (0, 0, 200) FOREGROUND = (0, 0, 0) # Recolor image pixels that are this color TRANSPARENT = (255, 255, 255) # Make image pixels this color transparent BALL_COLOR = (220, 220, 220) PADDLE_COLOR = (255, 255, 0) BRICK_COLORS = ((255, 0, 0), (255, 50, 0), (255, 100, 0), (255, 150, 0), (255, 200, 0), (255, 255, 0)) BRICK_COORDS = [ (32, 32), (64, 32), (96, 32), (160, 32), (288, 32), (320, 32), (352, 32), (416, 32), (448, 32), (480, 32), (576, 32), (608, 32), (640, 32), (32, 64), (160, 64), (288, 64), (352, 64), (416, 64), (480, 64), (576, 64), (640, 64), (32, 96), (160, 96), (288, 96), (352, 96), (416, 96), (480, 96), (576, 96), (640, 96), (32, 128), (64, 128), (96, 128), (160, 128), (288, 128), (352, 128), (416, 128), (448, 128), (480, 128), (576, 128), (608, 128), (640, 128), (32, 160), (160, 160), (288, 160), (352, 160), (416, 160), (448, 160), (576, 160), (640, 160), (32, 192), (160, 192), (288, 192), (352, 192), (416, 192), (480, 192), (576, 192), (640, 192), (32, 224), (160, 224), (192, 224), (224, 224), (288, 224), (320, 224), (352, 224), (416, 224), (512, 224), (576, 224), (640, 224)] # Define some image files. These are not your files BALL_IMAGE = "breakout_ball.png" PADDLE_IMAGE = "breakout_paddle.png" BRICK_FILES = ( ("breakout_brick.png", "breakout_brick2.png", "breakout_brick2.png"), ("breakout_brick.png", "breakout_brick2.png", "breakout_brick3.png")) SCREEN_WIDTH = 800 SCREEN_HEIGHT = 800 SCORE_POSITION = (SCREEN_WIDTH - 150, SCREEN_HEIGHT - 30) def create_image(file, color=None): """ Create image from a file. If color is specified, replace all FOREGROUND pixels with color pixels. Modify image so TRANSPARENT colored pixels are transparent. """ if color: # Recolor the image image = Image.open(file).convert("RGB") for xy in product(range(image.width), range(image.height)): if image.getpixel(xy) == FOREGROUND: image.putpixel(xy, color) image = pygame.image.fromstring(image.tobytes(), image.size, "RGB") else: image = pygame.image.load(file) image.set_colorkey(TRANSPARENT) return image.convert() class EnhancedSprite(pygame.sprite.Sprite): """ Sprite with image and rectangle. I expose some of my rectangle's properties. """ def __init__(self, image, group=None, **kwargs): super().__init__(**kwargs) self.image = image self.rect = image.get_rect() if group is not None: group.add(self) def at(self, x, y): """Convenience method for setting my position""" self.x = x self.y = y return self # Properties below expose properties of my rectangle so you can use # self.x = 10 or self.centery = 30 instead of self.rect.x = 10 @property def x(self): return self.rect.x @x.setter def x(self, value): self.rect.x = value @property def y(self): return self.rect.y @y.setter def y(self, value): self.rect.y = value @property def centerx(self): return self.rect.centerx @centerx.setter def centerx(self, value): self.rect.centerx = value @property def centery(self): return self.rect.centery @centery.setter def centery(self, value): self.rect.centery = value @property def right(self): return self.rect.right @right.setter def right(self, value): self.rect.right = value @property def bottom(self): return self.rect.bottom @bottom.setter def bottom(self, value): self.rect.bottom = value class Brick(EnhancedSprite): """ A target for the ball. After I take some number of hits I die. Number of hits I can take is in range 1 to 3. Hits is randomly selected if not specified. Specify brick color using (R, G, B) format. If color not specified a color is selected based on the row. """ group = pygame.sprite.Group() def __init__(self, x, y, image_files=None, color=None, hits=None): color = color or random.choice(BRICK_COLORS) hits = hits or random.choice((1, 1, 1, 2, 2, 3)) self.value = self.hits = max(1, min(3, hits)) image_files = image_files or random.choice(BRICK_FILES) self.images = [create_image(file, color) for file in image_files] super().__init__(self.images[self.hits-1], self.group) self.at(x, y) def __len__(self): """Return how many bricks remaining""" return len(self.group) def hit(self, score): """ I was hit! Update my appearance or die based on my hit total. Return my value if I was killed. """ self.hits -= 1 if self.hits > 0: self.image = self.images[self.hits - 1] return 0 self.kill() return self.value class Paddle(EnhancedSprite): """The sprite the player moves around to redirect the ball""" group = pygame.sprite.Group() def __init__(self, bottom): super().__init__(create_image(PADDLE_IMAGE, PADDLE_COLOR), self.group) self.bottom = bottom self.xmin = self.rect.width // 2 # Compute paddle x range. self.xmax = SCREEN_WIDTH - self.xmin def move(self, x): """Move to follow the cursor. Clamp to window bounds""" self.centerx = max(self.xmin, min(self.xmax, x)) class LifeCounter(): """Keep track of lives count. Display lives remaining using ball image""" def __init__(self, x, y, count=5): self.x, self.y = x, y self.image = create_image(BALL_IMAGE, BALL_COLOR) self.spacing = self.image.get_width() + 5 self.group = pygame.sprite.Group() self.reset(count) def reset(self, count): """Reset number of lives""" self.count = count for c in range(count - 1): EnhancedSprite(self.image, self.group).at(self.x + c * self.spacing, self.y) def __len__(self): """Return number of lives remaining""" return self.count def kill(self): """Reduce number of lives""" if self.count > 1: self.group.sprites()[-1].kill() self.count = max(0, self.count - 1) class Ball(EnhancedSprite): """Ball bounces around colliding with walls, paddles and bricks""" group = pygame.sprite.Group() def __init__(self, paddle, lives, speed=10): super().__init__(create_image(BALL_IMAGE, BALL_COLOR), self.group) self.paddle = paddle self.lives = lives self.speed = speed self.dx = self.dy = 0 self.xmax = SCREEN_WIDTH - self.rect.width self.ymax = self.paddle.bottom - self.rect.height self.reset(0) def reset(self, score=None): """Reset for a new game""" self.active = False if score is not None: self.score = score def start(self): """Start moving the ball in a random direction""" angle = random.random() - 0.5 # Launch angle limited to about +/-60 degrees self.dx = int(self.speed * math.sin(angle)) self.dy = -int(self.speed * math.cos(angle)) self.active = True def move(self): """Update the ball position. Check for collisions with bricks, walls and the paddle""" if not self.active: # Sit on top of the paddle self.centerx = self.paddle.centerx self.bottom = self.paddle.y - 2 return self # Did I hit some bricks? Update the bricks and the score x1, y1 = self.x, self.y x2, y2 = x1 + self.dx, y1 + self.dy if (xhits := pygame.sprite.spritecollide(self.at(x2, y1), Brick.group, False)): self.dx = -self.dx if (yhits := pygame.sprite.spritecollide(self.at(x1, y2), Brick.group, False)): self.dy = -self.dy if (hits := set(xhits) or set(yhits)): for brick in hits: self.score += brick.hit(self.score) # Did I hit a wall? if x2 <= 0 or x2 >= self.xmax: self.dx = -self.dx hits = True if y2 <= 0: self.dy = abs(self.dy) hits = True # Did I hit or get past the paddle? if y2 >= self.paddle.y: # Did it get past the paddle? if self.x > self.paddle.right or self.right < self.paddle.x: self.lives.kill() self.active = False else: # I hit the paddle. Compute angle of reflection bangle = math.atan2(-self.dx, self.dy) # Ball angle of approach pangle = math.atan2(self.centerx - self.paddle.centerx, 30) # Paddle angle rangle = (pangle - bangle) / 2 # Angle of reflection self.dx = math.sin(rangle) * self.speed self.dy = -math.cos(rangle) * self.speed hits = True if hits: self.at(x1, y1) else: self.at(x2, y2) def main(): """Play game until out of lives or out of bricks""" def displayText(text, font, pos=None, color=TEXT_COLOR): text = font.render(text, 1, color) if pos is None: pos = ((SCREEN_WIDTH - text.get_width()) // 2, (SCREEN_HEIGHT - text.get_height()) // 2) screen.blit(text, pos) pygame.init() screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Breakout") clock = pygame.time.Clock() allsprites = pygame.sprite.Group() score_font = pygame.font.Font(None, 34) try: level = 1 lives = LifeCounter(10, SCREEN_HEIGHT - 30) paddle = Paddle(bottom=SCREEN_HEIGHT - 40) ball = Ball(paddle, lives) allsprites.add(paddle.group, lives.group, ball.group) while len(lives) > 0: # Start new boeard. Could have different layouts for each level for coord in BRICK_COORDS: Brick(*coord) allsprites.add(Brick.group) # Play until out of bricks or lives while len(lives) > 0 and len(Brick.group): clock.tick(60) for event in pygame.event.get(): if event.type == pygame.QUIT: raise SystemExit elif event.type == pygame.MOUSEMOTION: paddle.move(event.pos[0]) elif event.type == pygame.MOUSEBUTTONUP: if not ball.active: ball.start() ball.move() screen.fill(BACKGROUND) displayText(f"Score: {ball.score}", font=score_font, pos=SCORE_POSITION) allsprites.draw(screen) pygame.display.flip() # Display results if len(lives) == 0: displayText("Game over", font=pygame.font.Font(None, 74)) elif len(Brick.group) == 0: level += 1 displayText(f"Level {level}", font=pygame.font.Font(None, 74)) ball.speed *= 1.25 ball.reset(ball.score) pygame.display.flip() pygame.time.wait(3000) finally: pygame.quit() if __name__ == "__main__": main()