Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
breakout clone
#21
Dear deanhystad,

thanks a lot for your effort!
You helped me so much to get this game nearly finished.
May I ask for a last help?
My original idea was to dedicate the game to some people by drawing their individual name with the bricks.
I've got lists with the coordinates of the bricks - each name is stored in a brick_coord_list.
I changed the following lines (307-313) to implement the brick_coord_list - but I failed to get it running.

Is it possible to use just 2 sets of damaged bricks randomly?
I've got a green brick set and a blue brick set.
I changed the following lines (18, 128, 132, 133, 140-144).

(I noticed that the score doesn't work properly...)

It would be great if you could help me again...

Have a nice next week!

Greetings,

flash77
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))

# Define some image files
BALL_IMAGE = "ball.png"
PADDLE_IMAGE = "paddle.png"
BRICK_FILES = ("brick0.png", "brick1.png", "brick2.png")
BRICK_FILESB = ("bbrick0.png", "bbrick1.png", "bbrick2.png")


SCREEN_WIDTH = 800
SCREEN_HEIGHT = 800
SCORE_POSITION = (SCREEN_WIDTH - 150, SCREEN_HEIGHT - 30)


def create_image(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(image_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(image_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()
    IMAGES = {}  # Dictionary of images.  Similar colored bricks share images

    def __init__(self, x, y, color=None, hits=None):
        self.color = color #or BRICK_COLORS[row % len(BRICK_COLORS)]
        hits = hits or random.choice((1, 1, 1, 2, 2, 3))
        self.value = self.hits = max(1, min(3, hits))
        super().__init__(self.get_image(), self.group)
        #self.at(col * self.rect.width, row * self.rect.height * 2 + 60)
        self.at(x, y)

    def get_image(self):
        """Return an image based on my color and number of hits."""
        images = self.IMAGES.get(self.color, None)
        if images is None:
            # Make brick images for this color
            r = random.randint(1, 2)
            if r == 1:
                images = [create_image(image_file, self.color) for image_file in BRICK_FILES]
            else:
                images = [create_image(image_file, self.color) for image_file in BRICK_FILESB]
            self.IMAGES[self.color] = images
        return images[self.hits - 1]

    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 score based on being hit.
        """
        self.hits -= 1
        if self.hits > 0:
            self.image = self.get_image()
            return 0
        self.kill()
        return self.hits


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.ymax:
            # The got past the paddle
            self.lives.kill()
            self.active = False
        elif pygame.sprite.spritecollide(self, self.paddle.group, False):
            # 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:
            # here I would like to draw a name
            # below it is just a test (I've got lists with full coordinates)
            brick_coord_list = [
                32, 64, 32, 96, 32, 128, 32, 160
                    ]
            i = 0
            while i < len(brick_coord_list):
            #for r, c in product(range(3 + level), range(10)):
                Brick(brick_coord_list[i], brick_coord_list[i+1])
            allsprites.add(Brick.group)

            while len(lives) > 0 and len(Brick.group) > 0:
                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()

            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
#22
This is a colored rectangle brick that can have text.
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()
    WIDTH = 50
    HEIGHT = 20
 
    def __init__(self, row, col, color=None, hits=None, text=None, text_color=(255, 255, 255), font=24):
        super().__init__(pygame.Surface((self.WIDTH-2, self.HEIGHT-2)), self.group)
        self.image.fill(color or BRICK_COLORS[row % len(BRICK_COLORS)])
        if text is not None:
            text = pygame.font.Font(None, font).render(text, 1, text_color)
            x = (self.WIDTH - text.get_width()) // 2
            y = (self.HEIGHT - text.get_height()) // 2
            self.image.blit(text, (x, y))
        hits = hits or random.choice((1, 1, 1, 2, 2, 3))
        self.value = self.hits = max(1, min(3, hits))
        self.at(col * self.WIDTH, row * self.HEIGHT * 2 + 60)
 
    def __len__(self):
        """Return how many bricks remaining"""
        return len(self.group)
 
    def hit(self, score):
        """I was hit!  Return score based on being hit. """
        self.hits -= 1
        if self.hits <= 0:
            self.kill()
        return self.value if self.hits <= 0 else 0
It would be easy to use an image file and blit the text on the image. HOWEVER, you cannot share image files like I did before. Just as each color required a different image, each text requires a different image.
Reply
#23
Dear deanhystad,

thanks a lot for your patience!
May I ask again a few questions?

I found a possibility to draw names (dedication) using the bricks.
Just copy/paste the full code and you will see the name "Marco" drawn with the bricks.

I just want to use png files for the bricks (that is easier for me to understand in opposite to color the bricks).
I've got 2 sets of png files:
A green set (3 pngs) and a blue set (3 pngs).
Each set contains the different appearence of the bricks (because of the hits).
I randomnly use the green and the blue bricks.
To achieve this I created a new class "BrickB" and 3 new pngs (bbrick0.png, bbrick1.png, bbrick2.png).

I wasn't able to merge Brick.group and BrickB.group - you can see what I did in the code.

Would you please be so kind and help me to clean up the code?

a) Just use pngs for the bricks and not to color them

b) How to merge Brick.group and BrickB.group

That would be great...

Greetings,

flash77

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))

# Define some image files
BALL_IMAGE = "ball.png"
PADDLE_IMAGE = "paddle.png"
BRICK_FILES = ("brick0.png", "brick1.png", "brick2.png")
BRICK_FILESB = ("bbrick0.png", "bbrick1.png", "bbrick2.png")

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 800
SCORE_POSITION = (SCREEN_WIDTH - 150, SCREEN_HEIGHT - 30)


def create_image(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(image_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(image_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()
    IMAGES = {}  # Dictionary of images.  Similar colored bricks share images

    def __init__(self, x, y, color=None, hits=None):
        self.color = color  # or BRICK_COLORS[row % len(BRICK_COLORS)]
        hits = hits or random.choice((1, 1, 1, 2, 2, 3))
        self.value = self.hits = max(1, min(3, hits))
        super().__init__(self.get_image(), self.group)
        # self.at(col * self.rect.width, row * self.rect.height * 2 + 60)
        self.at(x, y)

    def get_image(self):
        """Return an image based on my color and number of hits."""
        images = self.IMAGES.get(self.color, None)
        if images is None:
            images = [create_image(image_file, self.color) for image_file in BRICK_FILES]
            self.IMAGES[self.color] = images
            # Make brick images for this color
        return images[self.hits - 1]

    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 score based on being hit.
        """
        self.hits -= 1
        if self.hits > 0:
            self.image = self.get_image()
            return 0
        self.kill()
        return self.hits


class BrickB(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()
    IMAGES = {}  # Dictionary of images.  Similar colored bricks share images

    def __init__(self, x, y, color=None, hits=None):
        self.color = color  # or BRICK_COLORS[row % len(BRICK_COLORS)]
        hits = hits or random.choice((1, 1, 1, 2, 2, 3))
        self.value = self.hits = max(1, min(3, hits))
        super().__init__(self.get_image(), self.group)
        # self.at(col * self.rect.width, row * self.rect.height * 2 + 60)
        self.at(x, y)

    def get_image(self):
        """Return an image based on my color and number of hits."""
        images = self.IMAGES.get(self.color, None)
        if images is None:
            images = [create_image(image_file, self.color) for image_file in BRICK_FILESB]
            self.IMAGES[self.color] = images
            # Make brick images for this color
        return images[self.hits - 1]

    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 score based on being hit.
        """
        self.hits -= 1
        if self.hits > 0:
            self.image = self.get_image()
            return 0
        self.kill()
        return self.hits


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)) or (xhits := pygame.sprite.spritecollide(self.at(x2, y1), BrickB.group, False)):
            self.dx = -self.dx
        if (yhits := pygame.sprite.spritecollide(self.at(x1, y2), Brick.group, False)) or (yhits := pygame.sprite.spritecollide(self.at(x1, y2), BrickB.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.ymax:
            # The got past the paddle
            self.lives.kill()
            self.active = False
        elif pygame.sprite.spritecollide(self, self.paddle.group, False):
            # 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:
            # here I draw a name with the bricks
            brick_coord_list = [
                32, 32, 32, 64, 32, 96, 32, 128, 32, 160, 32, 192, 64, 64, 96, 96, 128, 64, 160,
                32, 160, 64, 160, 96, 160, 128, 160, 160, 160, 192, 224, 32, 224, 64, 224, 96, 224,
                128, 224, 160, 224, 192, 256, 32, 256, 128, 288, 32, 288, 64, 288, 96, 288, 128,
                288, 160, 288, 192, 352, 32, 352, 64, 352, 96, 352, 128, 352, 160, 352, 192, 384,
                32, 384, 96, 384, 128, 416, 32, 416, 64, 416, 96, 416, 160, 448, 192, 512, 32, 512,
                64, 512, 96, 512, 128, 512, 160, 512, 192, 544, 32, 544, 192, 576, 32, 576, 192, 640,
                32, 640, 64, 640, 96, 640, 128, 640, 160, 640, 192, 672, 32, 672, 192, 704, 32, 704,
                192, 736, 32, 736, 64, 736, 96, 736, 128, 736, 160, 736, 192
                            ]
            i = 0
            while i < len(brick_coord_list):
                r = random.randint(1, 2)
                if r == 1:
                    Brick(brick_coord_list[i], brick_coord_list[i + 1])
                else:
                    BrickB(brick_coord_list[i], brick_coord_list[i + 1])
                i = i + 2
            allsprites.add(Brick.group)
            allsprites.add(BrickB.group)

            while len(lives) > 0 and len(Brick.group) or len(lives) > 0 and len(BrickB.group) > 0:
                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()

            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
#24
Bricks and BricksB are identical.

I would change BRICKS_FILES to be this:
BRICK_FILES = (("brick0.png", "brick1.png", "brick2.png"), ("bbrick0.png", "bbrick1.png", "bbrick2.png"))
Bricks can randomly pick a group of image files.

With all the image files there's not much reason for an image dictionary. I would modify the code so each Brick has their own images. This will also make it easy to write text on the bricks.
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.
    image
    """
    group = pygame.sprite.Group()

    def __init__(self, x, y, color=None, hits=None, image_files=None):
        color = color or random.choice(BRICK_COLORS)
        hits = hits or random.choice((1, 1, 1, 2, 2, 3))
        hits = max(0, min(len(image_files, hits)))
        self.value = self.hits = hits
        image_files = image_files or random.choice(BRICK_FILES)
        self.images = [create_image(image_file, color) for image_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):
        """
        I was hit!  Update my appearance or die based on my hit total.
        Return score based on being hit.
        """
        self.hits -= 1
        if self.hits > 0:
            self.image = self.images[self.hits-1]
            return 0
        self.kill()
        return self.value
This simplifies the brick creation. The main function doesn't do anything special for bricks.
        while len(lives) > 0:
            for coords in BRICK_COORDS:
                 Brick(*coords)
            allsprites.add(Brick.group)
And I modified the coordinates too.
BRICK_COORDS = (
    (32, 32), (32, 64), (32, 96), (32, 128), (32, 160), (32, 192), (64, 64), (96, 96),
    (128, 64), (160, 32), (160, 64), (160, 96), (160, 128), (160, 160), (160, 192), (224, 32), 
    (224, 64), (224, 96), (224, 128), (224, 160), (224, 192), (256, 32), (256, 128), (288, 32), 
    (288, 64), (288, 96), (288, 128), (288, 160), (288, 192), (352, 32), (352, 64), (352, 96), 
    (352, 128), (352, 160), (352, 192), (384, 32), (384, 96), (384, 128), (416, 32), (416, 64), 
    (416, 96), (416, 160), (448, 192), (512, 32), (512, 64), (512, 96), (512, 128), (512, 160), 
    (512, 192), (544, 32), (544, 192), (576, 32), (576, 192), (640, 32), (640, 64), (640, 96), 
    (640, 128), (640, 160), (640, 192), (672, 32), (672, 192), (704, 32), (704, 192), (736, 32), 
    (736, 64), (736, 96), (736, 128), (736, 160), (736, 192))
You could, if you wanted, make a different brick coords list for each level. This will be cleaner if the list is at the top of the file instead of buried inside a method down near the bottom.

Note: I have not run any of this code.

I thought you wanted to put names on the bricks. Do you still need help with that? I noticed your code does not have the create_image() function that lets you write text on the sprite.
Reply
#25
Seems like a lot of work making brick images just so you can put names on the bricks. In an earlier example I wrote text on rectangular bricks. In this example I do the same thing, but on brick images.

I also fixed a problem with the ball going through the paddle when it is moving really fast. This example also writes words on some of the bricks and makes random brick layouts.
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 = [(x*50, y*20+40) for y in range(6) for x in range(10)]
BRICK_WORDS = "I am very pleased with the way this game turned out now that I fixed the a paddle problem".split()
 
# Define some image files
BALL_IMAGE = "breakout_ball.png"
PADDLE_IMAGE = "breakout_paddle.png"
BRICK_FILES = ("breakout_brick.png", "breakout_brick2.png", "breakout_brick3.png")
 
SCREEN_WIDTH = 500
SCREEN_HEIGHT = 600
SCORE_POSITION = (SCREEN_WIDTH-150, SCREEN_HEIGHT-30)
 
def create_image(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(image_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(image_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
 
    def set_text(self, text, font=18, color=TEXT_COLOR):
        """Draw text on the sprite"""
        text = pygame.font.Font(None, font).render(text, 1, color)
        x = (self.WIDTH - text.get_width()) // 2
        y = (self.HEIGHT - text.get_height()) // 2
        self.image.blit(text, (x, y))

    # 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 at random.
    """
    group = pygame.sprite.Group()
    WIDTH = 50
    HEIGHT = 20
 
    def __init__(self, x, y, color=None, images=None, hits=None):
        color = color or random.choice(BRICK_COLORS)
        images = images or BRICK_FILES
        hits = hits or random.choice((1, 1, 1, 2, 2, 3))
        self.value = self.hits = max(1, min(3, hits))
        self.images = [create_image(image, color)  for image in images]
        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 score based on being hit.
        """
        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
 
    def move(self, x):
        """Move to follow the cursor"""
        self.centerx = 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):
            sprite = 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=8):
        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.ymax:
            # 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:
            random.shuffle(BRICK_COORDS)
            for coords in BRICK_COORDS[:30]:
                Brick(*coords)
            for brick, word in zip(Brick.group.sprites(), BRICK_WORDS):
                brick.set_text(word)

            allsprites.add(Brick.group)
 
            while len(lives) > 0 and len(Brick.group) > 0:
                clock.tick(40)
                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()
 
            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
#26
Dear deanhystad,

thanks a lot for you answer!
I prefer to draw the names of persons with the bricks - I made an Excel-Macro, which generates the coordinates for me. I've attached the macro code (if you are interested)...

I've got an error, which I can't fix (because I'm not experienced in it)...
BRICK_COORDS = (
    (32, 32), (32, 64), (32, 96), (32, 128), (32, 160), (32, 192), (64, 64), (96, 96),
    (128, 64), (160, 32), (160, 64), (160, 96), (160, 128), (160, 160), (160, 192), (224, 32),
    (224, 64), (224, 96), (224, 128), (224, 160), (224, 192), (256, 32), (256, 128), (288, 32),
    (288, 64), (288, 96), (288, 128), (288, 160), (288, 192), (352, 32), (352, 64), (352, 96),
    (352, 128), (352, 160), (352, 192), (384, 32), (384, 96), (384, 128), (416, 32), (416, 64),
    (416, 96), (416, 160), (448, 192), (512, 32), (512, 64), (512, 96), (512, 128), (512, 160),
    (512, 192), (544, 32), (544, 192), (576, 32), (576, 192), (640, 32), (640, 64), (640, 96),
    (640, 128), (640, 160), (640, 192), (672, 32), (672, 192), (704, 32), (704, 192), (736, 32),
    (736, 64), (736, 96), (736, 128), (736, 160), (736, 192))
while len(lives) > 0:
            random.shuffle(BRICK_COORDS)
            for coords in BRICK_COORDS[:30]:
                Brick(*coords)
Traceback (most recent call last):
  File "D:\Daten\Breakout neu 2\main.py", line 345, in <module>
    main()
  File "D:\Daten\Breakout neu 2\main.py", line 310, in main
    random.shuffle(BRICK_COORDS)
  File "C:\Users\...\AppData\Local\Programs\Python\Python310\lib\random.py", line 394, in shuffle
    x[i], x[j] = x[j], x[i]
TypeError: 'tuple' object does not support item assignment
Please help me out...

Have a nice weekend!

Attached Files

Thumbnail(s)
       
Reply
#27
You made BRICK_COORDS a tuple. Tuples are immutable. Make it a list instead
Reply
#28
Dear deanhystad,

I'm sorry for asking again...
I don't want to jangle your nerves...

In post 24 you said:
I would change BRICKS_FILES to be this:
BRICK_FILES = (("brick0.png", "brick1.png", "brick2.png"), ("bbrick0.png", "bbrick1.png", "bbrick2.png"))
Bricks can randomly pick a group of image files.

I tried very long but I can't get along...

So I made the BRICK_COORDS as a list (as you said in post 27) and get back to an old state
for the Brick creation (line 307 - 321), because it is easier for me to understand.

What I'm trying to achieve is:

I've got 2 types of bricks:

BRICK_FILES is the green set.
BRICK_FILES = ("brick0.png", "brick1.png", "brick2.png")

BRICK_FILESB is the blue set.
BRICK_FILESB = ("bbrick0.png", "bbrick1.png", "bbrick2.png")

The numbers 0 to 2 (e.g. brick0.png", "brick1.png", "brick2.png) represent the state of the brick damage
- the brick looks more and more damaged if it takes hits.

The pictures should appear in sequence for the green set: brick0.png, brick1.png, brick2.png
The pictures should appear in sequence for the blue set: bbrick0.png, bbrick1.png, bbrick2.png

I would prefer to have 1 BRICK_FILES (as mentioned below) but I couldn't do it.
BRICK_FILES = (("brick0.png", "brick1.png", "brick2.png"), ("bbrick0.png", "bbrick1.png", "bbrick2.png"))
I tried to use r = random.int(1,2) to differentiate what type of brick (blue or green) should be used.

Please be so kind and have a look at the code...
Thanks for your patience...

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
BALL_IMAGE = "ball.png"
PADDLE_IMAGE = "paddle.png"
BRICK_FILES = ("brick0.png", "brick1.png", "brick2.png")
BRICK_FILESB = ("bbrick0.png", "bbrick1.png", "bbrick2.png")

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 800
SCORE_POSITION = (SCREEN_WIDTH - 150, SCREEN_HEIGHT - 30)


def create_image(image_file, r, 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(image_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(image_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, r, color=None, hits=None):
        self.color = color  # or BRICK_COLORS[row % len(BRICK_COLORS)]
        hits = hits or random.choice((1, 1, 1, 2, 2, 3))
        self.value = self.hits = max(1, min(3, hits))
        super().__init__(self.get_image(r), self.group)
        # self.at(col * self.rect.width, row * self.rect.height * 2 + 60)
        self.at(x, y)

    def get_image(self, r):
        """Return an image based on my color and number of hits."""
        if r == 1:
            images = [create_image(image_file, r, self.color) for image_file in BRICK_FILES]
        else:
            images = [create_image(image_file, r, self.color) for image_file in BRICK_FILESB]
        # Make brick images for this color
        return images[self.hits - 1]

    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 score based on being hit.
        """
        self.hits -= 1
        if self.hits > 0:
            self.image = self.get_image()
            return 0
        self.kill()
        return self.hits


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:
            # here I draw a name with the bricks
            BRICK_COORDS = [
                32, 32, 32, 64, 32, 96, 32, 128, 32, 160, 32, 192, 64, 64, 96, 96, 128, 64, 160,
                32, 160, 64, 160, 96, 160, 128, 160, 160, 160, 192, 224, 32, 224, 64, 224, 96, 224,
                128, 224, 160, 224, 192, 256, 32, 256, 128, 288, 32, 288, 64, 288, 96, 288, 128,
                288, 160, 288, 192, 352, 32, 352, 64, 352, 96, 352, 128, 352, 160, 352, 192, 384,
                32, 384, 96, 384, 128, 416, 32, 416, 64, 416, 96, 416, 160, 448, 192, 512, 32, 512,
                64, 512, 96, 512, 128, 512, 160, 512, 192, 544, 32, 544, 192, 576, 32, 576, 192, 640,
                32, 640, 64, 640, 96, 640, 128, 640, 160, 640, 192, 672, 32, 672, 192, 704, 32, 704,
                192, 736, 32, 736, 64, 736, 96, 736, 128, 736, 160, 736, 192
            ]
            i = 0
            while i < len(BRICK_COORDS):
                r = random.randint(1, 2)
                Brick(BRICK_COORDS[i], BRICK_COORDS[i + 1], r)
                i = i + 2
            allsprites.add(Brick.group)

            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(r)
                screen.fill(BACKGROUND)
                displayText(f"Score: {ball.score}", font=score_font, pos=SCORE_POSITION)
                allsprites.draw(screen)
                pygame.display.flip()

            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()
 File "D:\Daten\Breakout neu 2\main.py", line 355, in <module>
    main()
  File "D:\Daten\Breakout neu 2\main.py", line 335, in main
    ball.move(r)
TypeError: Ball.move() takes 1 positional argument but 2 were given
t sRGB profile
libpng warning: iCCP: cHRM chunk does not match sRGB
libpng warning: iCCP: known incorrect sRGB profile
Reply
#29
The way you do the BRICK_COORDS makes them hard to work with. When you couldn't shuffle the BRICK_COORDS it was because BRICK_COORDS was a tuple, not because it contained tuples.
BRICK_COORDS = [(x1, y1), (x2, y2) ... (xN, yN)]  # You can shuffle a list of tuples
Then you could replace this:
            i = 0
            while i < len(BRICK_COORDS):
                r = random.randint(1, 2)
                Brick(BRICK_COORDS[i], BRICK_COORDS[i + 1], r)
                i = i + 2
with this:
for coord in BRICK_COORDS:
    Brick(*coord)
Brick.__init__() is gets to randomly choose image files if none are specified:
    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)  # Randomly picking files
        self.images = [create_image(file, color for file in image_files]  # Making the images  here
        super().__init__(self.get_image(self.hits-1), self.group).at(x, y)
Brick.get_image() goes away and Brick.hit() changes to:
    def hit(self, score):
        """
        I was hit!  Update my appearance or die based on my hit total.
        Return score based on being hit.
        """
        self.hits -= 1
        if self.hits > 0:
            self.image = self.images[self.hits-1]  # Images are not shared anymore.  Each brick has their own.
            return 0
        self.kill()
        return self.value  # This was hits, but hits is 0
And BRICK_FILES is defined as:
BRICK_FILES = (("brick0.png", "brick1.png", "brick2.png"), ("bbrick0.png", "bbrick1.png", "bbrick2.png")
Do not bury BRICK_COORDS down in main, and you really don't want to put it inside a while loop. What you really, really don't want is to have a list of BRICK_COORDS at the top of the program and another in a while loop inside the main() function.
Reply
#30
Dear deanhystad,

I'm sorry for asking again.

I made the changes you mentioned.

You said "Brick.get_image() goes away":

In Brick class in line 139 I found a "self.get_image(self.hits - 1)"...
How should I replace it?

I would prefer to use just the Brick-pngs, the paddle-png and the ball-png, because the error below occurs and
I think that is easier to understand instead of specify a color.

Please be so kind and have a look at the code...

(I'm sorry for annoying again.)
I wish you a pleasant sunday...
Traceback (most recent call last):
  File "C:\Users\...\AppData\Local\Programs\Python\Python310\lib\site-packages\PIL\Image.py", line 2957, in open
    fp.seek(0)
AttributeError: 'tuple' object has no attribute 'seek'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "D:\Daten\Breakout neu 2\main.py", line 338, in <module>
    main()
  File "D:\Daten\Breakout neu 2\main.py", line 305, in main
    Brick(*coord)
  File "D:\Daten\Breakout neu 2\main.py", line 138, in __init__
    self.images = [create_image(file, color) for file in BRICK_FILES]  # Making the images  here
  File "D:\Daten\Breakout neu 2\main.py", line 138, in <listcomp>
    self.images = [create_image(file, color) for file in BRICK_FILES]  # Making the images  here
  File "D:\Daten\Breakout neu 2\main.py", line 41, in create_image
    image = Image.open(file).convert("RGB")
  File "C:\Users\...\AppData\Local\Programs\Python\Python310\lib\site-packages\PIL\Image.py", line 2959, in open
    fp = io.BytesIO(fp.read())
AttributeError: 'tuple' object has no attribute 'read'
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
BALL_IMAGE = "ball.png"
PADDLE_IMAGE = "paddle.png"
BRICK_FILES = (("brick0.png", "brick1.png", "brick2.png"), ("bbrick0.png", "bbrick1.png", "bbrick2.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)  # Randomly picking files
        self.images = [create_image(file, color) for file in BRICK_FILES]  # Making the images  here
        super().__init__(self.get_image(self.hits - 1), self.group).at(x, y)

    #def get_image(self):
        #"""Return an image based on my color and number of hits."""
        #images = [create_image(image_file, self.color) for image_file in BRICK_FILES]

        # Make brick images for this color
        #return images[self.hits - 1]

    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 score based on being hit.
        """
        self.hits -= 1
        if self.hits > 0:
            self.image = self.images[self.hits - 1]  # Images are not shared anymore.  Each brick has their own.
            return 0
        self.kill()
        return self.value  # This was hits, but hits is 0


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)
        for coord in BRICK_COORDS:
            Brick(*coord)
        allsprites.add(Brick.group)

        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()

        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


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