Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
breakout clone
#31
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()
Reply
#32
Dear deanhystad,

thanks a lot for your help!!

Now the gameplay from the game is perfect.

I couldn't have done this without your help...

Thanks for your tireless and patient support!!
Reply
#33
Your BrickCoords inspired me to make a class that creates custom brick layouts to spell words with bricks. Still working on it, but it I get a readable pattern if I do this:
for coord in words_to_bricks("BREAK"):
    Brick(coord)
Now I need to make patterns for all the other letters and add support for multiple words and multiple lines. I'm also thinking of having a HOLLOW/FILL mode. In HOLLOW mode there are only bricks for the outline of the letters. In FILL mode all rows and columns are filled across the board, and the brick color is changed to mimic foreground/background. Then I could easily make all kinds of different brick layouts. Maybe starting with "LEVEL 1\nBEGIN", "LEVEL 2\nBEGIN" etc,,,
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  breakout clone pygame flash77 2 1,729 Feb-06-2022, 06:36 PM
Last Post: flash77
  [PyGame] arkanoid / breakout clone flash77 2 4,037 Feb-04-2022, 05:42 PM
Last Post: flash77

Forum Jump:

User Panel Messages

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