Python Forum
background music in the gameloop
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
background music in the gameloop
#1
Dear community,

I've got a problem with game development/tkinter...

I generated a form which let the user choose some tracks.
These tracks should be played randomly in the game loop.

I printed playlist after function "ischecked" is processed - and it contains, as expected,
the randomly sorted values of "listl".

But in main() the list "playlist" is emtpy...

What I am doing wrong?

Thanks a lot for your help!

import random, math, pygame, tkinter
from tkinter import *
from PIL import Image
from itertools import product
#from PyQt6.QtWidgets import QLabel, QRadioButton, QVBoxLayout, QApplication, QWidget
#from PyQt6.QtGui import *
pygame.init()
pygame.mixer.init()
pygame.mixer.music.set_volume(0.7)
master = Tk()
master.geometry('500x400')

# Define colors used by the game.
TEXT_COLOR = (255, 255, 255)
FOREGROUND = (0, 0, 0)  # Recolor image pixels that are this color
TRANSPARENT = (255, 255, 255)  # Make image pixels this color transparent
BALL_COLOR = (255, 255, 255)
PADDLE_COLOR = (255, 255, 255)
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 = "ball.png"
PADDLE_IMAGE = "paddle.png"
BRICK_FILES = (("brick0.png", "brick1.png", "brick2.png"), ("bbrick0.png", "bbrick1.png", "bbrick2.png"))
BACKGROUND_IMAGE = pygame.image.load("Hintergrund.png")
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 578
SCORE_POSITION = (SCREEN_WIDTH - 150, SCREEN_HEIGHT - 30)

listl = []
playlist = []

def insert_into_listl(listl, track):
    # Adding songs file in our listl
    listl.append(track)


def isChecked():
    if cb1.get() is True:
        insert_into_listl(listl, "1.mp3")
    if cb2.get() is True:
        insert_into_listl(listl, "2.mp3")
    if cb3.get() is True:
        insert_into_listl(listl, "3.mp3")
    if cb4.get() is True:
        insert_into_listl(listl, "4.mp3")
    if cb5.get() is True:
        insert_into_listl(listl, "5.mp3")
    if cb6.get() is True:
        insert_into_listl(listl, "6.mp3")
    if cb7.get() is True:
        insert_into_listl(listl, "7.mp3")
    if cb8.get() is True:
        insert_into_listl(listl, "8.mp3")
    if cb9.get() is True:
        insert_into_listl(listl, "9.mp3")
    if cb10.get() is True:
        insert_into_listl(listl, "10.mp3")
    playlist = random.sample(listl, len(listl))
    return playlist


def play(bnr):
    if bnr == 1:
        pygame.mixer.music.load(str(bnr) + ".mp3")
        pygame.mixer.music.play()
    elif bnr == 2:
        pygame.mixer.music.load(str(bnr) + ".mp3")
        pygame.mixer.music.play()
    elif bnr == 3:
        pygame.mixer.music.load(str(bnr) + ".mp3")
        pygame.mixer.music.play()
    elif bnr == 4:
        pygame.mixer.music.load(str(bnr) + ".mp3")
        pygame.mixer.music.play()
    elif bnr == 5:
        pygame.mixer.music.load(str(bnr) + ".mp3")
        pygame.mixer.music.play()
    elif bnr == 6:
        pygame.mixer.music.load(str(bnr) + ".mp3")
        pygame.mixer.music.play()
    elif bnr == 7:
        pygame.mixer.music.load(str(bnr) + ".mp3")
        pygame.mixer.music.play()
    elif bnr == 8:
        pygame.mixer.music.load(str(bnr) + ".mp3")
        pygame.mixer.music.play()
    elif bnr == 9:
        pygame.mixer.music.load(str(bnr) + ".mp3")
        pygame.mixer.music.play()
    elif bnr == 10:
        pygame.mixer.music.load(str(bnr) + ".mp3")
        pygame.mixer.music.play()


l = Label(master, text="Welche tracks sollen zufällig gespielt werden (mit playlist)?", font=('arial', 12, 'bold', 'italic'))
l.place(x=0, y=0)
button1 = tkinter.Button(master, text="reinhören", command=lambda: play(1))
button1.place(x=0, y=25)
button2 = tkinter.Button(master, text="reinhören", command=lambda: play(2))
button2.place(x=0, y=50)
button3 = tkinter.Button(master, text="reinhören", command=lambda: play(3))
button3.place(x=0, y=75)
button4 = tkinter.Button(master, text="reinhören", command=lambda: play(4))
button4.place(x=0, y=100)
button5 = tkinter.Button(master, text="reinhören", command=lambda: play(5))
button5.place(x=0, y=125)
button6 = tkinter.Button(master, text="reinhören", command=lambda: play(6))
button6.place(x=0, y=150)
button7 = tkinter.Button(master, text="reinhören", command=lambda: play(7))
button7.place(x=0, y=175)
button8 = tkinter.Button(master, text="reinhören", command=lambda: play(8))
button8.place(x=0, y=200)
button9 = tkinter.Button(master, text="reinhören", command=lambda: play(9))
button9.place(x=0, y=225)
button10 = tkinter.Button(master, text="reinhören", command=lambda: play(10))
button10.place(x=0, y=250)

cb1=BooleanVar()
cb2=BooleanVar()
cb3=BooleanVar()
cb4=BooleanVar()
cb5=BooleanVar()
cb6=BooleanVar()
cb7=BooleanVar()
cb8=BooleanVar()
cb9=BooleanVar()
cb10=BooleanVar()

Checkbutton1 = Checkbutton(master, text="Adeline Yeo (HP) - Kite Fly High", variable=cb1)
Checkbutton1.place(x=75, y=25)
Checkbutton2 = Checkbutton(master, text="Nul Tiel Records - Fireflies", variable=cb2)
Checkbutton2.place(x=75, y=50)
Checkbutton3 = Checkbutton(master, text="cryptic scenery - Endzeit Endlos", variable=cb3)
Checkbutton3.place(x=75, y=75)
Checkbutton4 = Checkbutton(master, text="cryptic scenery - Helix Spire", variable=cb4)
Checkbutton4.place(x=75, y=100)
Checkbutton5 = Checkbutton(master, text="cryptic scenery - Minsk Metro", variable=cb5)
Checkbutton5.place(x=75, y=125)
Checkbutton6 = Checkbutton(master, text="cryptic scenery - Stazione Termini", variable=cb6)
Checkbutton6.place(x=75, y=150)
Checkbutton7 = Checkbutton(master, text="cryptic scenery - The Future was Japanese", variable=cb7)
Checkbutton7.place(x=75, y=175)
Checkbutton8 = Checkbutton(master, text="Ketsa - Holding The Line", variable=cb8)
Checkbutton8.place(x=75, y=200)
Checkbutton9 = Checkbutton(master, text="Maarten Schellekens - Salt Lake Swerve", variable=cb9)
Checkbutton9.place(x=75, y=225)
Checkbutton10 = Checkbutton(master, text="Strobotone - Dance Track", variable=cb10)
Checkbutton10.place(x=75, y=250)
button11 = tkinter.Button(master, text="ausgewählte tracks zu playlist hinzufügen", command=isChecked)
button11.place(x=75, y=275)


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=5):
        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 = 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 start_playlist(playlist):
    # Loading first audio file into our player
    pygame.mixer.music.load(playlist[0])
    # Playing our music
    pygame.mixer.music.play()
    if len(playlist) > 1:
        playlist.pop(0)
        pygame.mixer.music.queue(playlist[0])
    # setting up an end event which host an event
    # after the end of every song
    pygame.mixer.music.set_endevent(pygame.USEREVENT)
    # checking if any event has been
    # hosted at time of playing
    for event in pygame.event.get():
        # A event will be hosted
        # after the end of every song
        if event.type == pygame.USEREVENT:
            if len(playlist) > 0:
                pygame.mixer.music.queue(playlist[0])
                playlist.pop(0)
        if not pygame.mixer.music.get_busy():
            break


def main(playlist):
    """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)

    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 board.  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):
                print(playlist)
                start_playlist(playlist)
                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.blit(BACKGROUND_IMAGE, (0, 0))
                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__":
    mainloop()
    main(playlist)
Reply
#2
You assign playlist = [] in line 40 and never modify the list or assign a different list to playlist. You have a different variable named playlist that is assigned a value in line 68, but this playlist is a local variable that only exists inside the function isChecked().

Why are you writing this horrible code with all the cbx, buttonx and Checkbuttonx variables and a hard coded list of songs? I mentioned in response to another post that any time you have multiple related things that you should use lists, tuples or dictionaries instead of individual variables. Your player code should look like this:
import tkinter as tk

songs = (
    "Adeline Yeo (HP) - Kite Fly High",
    "Nul Tiel Records - Fireflies",
    "cryptic scenery - Endzeit Endlos",
    "cryptic scenery - Helix Spire",
    "cryptic scenery - Minsk Metro",
    "cryptic scenery - Stazione Termini",
    "cryptic scenery - The Future was Japanese",
    "Ketsa - Holding The Line",
    "Maarten Schellekens - Salt Lake Swerve",
    "Strobotone - Dance Track")
song_selections = []
playlist = []

def select_song():
    playlist.clear()
    for var in song_selections:
        if var.get():
            playlist.append(f"{var.song}.mp3")

def play_song(song):
    pygame.mixer.music.load(f"{song}.mp3")
    pygame.mixer.music.play()

root = tk.Tk()
tk.Label(root, text="Welche tracks sollen zufällig gespielt werden (mit playlist)?", font=('arial', 12, 'bold', 'italic')) \
    .grid(row=0, column=0, columnspan=2)

for index, song in enumerate(songs):
    tk.Button(root, text="reinhören", command=lambda arg=song: play_song(arg)) \
        .grid(row=index+1, column=0)
    var = tk.BooleanVar()
    var.song = song
    song_selections.append(var)
    tk.Checkbutton(root, text=song, variable=var, command=select_song) \
        .grid(row=index+1, column=1)
And I don't like this code much because of the hard coded play list.

You should not use place() anywhere in a tkinter program. Tkinter has layout managers that do the widget layout for you. Use grid() or pack() instead.
I used your playlist code to make a standalone music player that could be used to edit a playlist for your game. Making a standalone player makes it easier to understand and debug the code. Since I wrote it, it does not have a hardcoded song list, but instead gets the song titles from mp3 files in a music subfolder.
import pygame, pathlib, random
import tkinter as tk

class MusicPlayer(tk.Tk):
    """
    A simple music player that uses the pygame music mixer.  I maintain
    a class "playlist" so I can be used to select music for a pygame.
    """
    music_folder = pathlib.Path('.') / 'music'  # Put mp3 files in subfolder named "music"
    playlist = []  # Class variable so can be accessed after player shuts down.

    def __init__(self, *args, **kvargs):
        super().__init__(*args, **kvargs)
        self.title("Music")
        tk.Label(
            self,
            text="Music to game by",
            font=('arial', 12, 'bold', 'italic')).grid(row=0, column=0, columnspan=2)

        # Make controls for constructing playlist and playing songs
        self.song_selections = []
        for index, song in enumerate(self.music_folder.glob("*.mp3")):  # Get all mp3 files in music folder
            # Checkboxes are used to select songs for playlist
            songtitle = pathlib.Path(song).name[:-4]  # Use filename sans extension
            var = tk.BooleanVar()
            var.song = song  # Adding song file as attribute to var
            self.song_selections.append(var)
            tk.Checkbutton(self, text=songtitle, variable=var, command=self.update_playlist) \
                .grid(row=index+1, column=0, sticky="w")

            # Button is used to immediately play song
            tk.Button(self, text="Play", command=lambda arg=song: self.play_song(arg)) \
                .grid(row=index+1, column=1, padx=5, pady=2)

        # Button to play the playlist
        tk.Button(self, text="Play Selected", command=self.play_playlist) \
            .grid(row=index+1, column=0, columnspan=2, sticky="ew")

    def update_playlist(self):
        """Update the playlist to contain selected songs"""
        self.playlist.clear()
        for var in self.song_selections:
            if var.get():
                self.playlist.append(var.song)

    def play_playlist(self):
        """Play songs in playlist"""
        pygame.mixer.music.stop()
        if len(self.playlist) > 0:
            songs = random.sample(self.playlist, k=len(self.playlist))
            pygame.mixer.music.load(songs[0])
            for song in songs[1:]:
                pygame.mixer.music.queue(song)
            pygame.mixer.music.play()

    @staticmethod
    def play_song(song):
        """Play song now"""
        pygame.mixer.music.load(song)
        pygame.mixer.music.play()

pygame.init()
pygame.mixer.init()
pygame.mixer.music.set_volume(0.7)
MusicPlayer().mainloop()
print("This is the playlist:", MusicPlayer.playlist)  # Get the playlist after player shuts down
Reply
#3
Dear deanhystad,

I'm very sorry for causing annoyance...

One of my friends is hindered and she can better deal with a hardcoded playlist - that's the reason for the hard coded playlist.

In main() (in the game loop) I added some code for randomly playing the playlist.
I also added "return playlist" in line 57.

I noticed that the ball is bouncing inside the paddle, not on the edge.

But I'm thinking that it is to complicated to find the reason (and I don't want to annoy).

Now I think the gamplay is finished....

Thanks for your patience and effort!

I followed your instructions and the code now looks like:
import random, math, pygame
import tkinter as tk
from PIL import Image
from itertools import product
from random import sample

pygame.init()
pygame.mixer.init()
pygame.mixer.music.set_volume(0.7)

# Define colors used by the game.
TEXT_COLOR = (255, 255, 255)
FOREGROUND = (0, 0, 0)  # Recolor image pixels that are this color
TRANSPARENT = (255, 255, 255)  # Make image pixels this color transparent
BALL_COLOR = (255, 255, 255)
PADDLE_COLOR = (255, 255, 255)
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 = "ball.png"
PADDLE_IMAGE = "paddle.png"
BRICK_FILES = (("brick0.png", "brick1.png", "brick2.png"), ("bbrick0.png", "bbrick1.png", "bbrick2.png"))
BACKGROUND_IMAGE = pygame.image.load("Hintergrund.png")
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 578
SCORE_POSITION = (SCREEN_WIDTH - 150, SCREEN_HEIGHT - 30)

songs = (
    "Adeline Yeo (HP) - Kite Fly High",
    "Nul Tiel Records - Fireflies",
    "cryptic scenery - Endzeit Endlos",
    "cryptic scenery - Helix Spire",
    "cryptic scenery - Minsk Metro",
    "cryptic scenery - Stazione Termini",
    "cryptic scenery - The Future was Japanese",
    "Ketsa - Holding The Line",
    "Maarten Schellekens - Salt Lake Swerve",
    "Strobotone - Dance Track")
song_selections = []
playlist = []


def select_song():
    playlist.clear()
    for var in song_selections:
        if var.get():
            playlist.append(f"{var.song}.mp3")
    return playlist


def play_song(song):
    pygame.mixer.music.load(f"{song}.mp3")
    pygame.mixer.music.play()


root = tk.Tk()
root.geometry('500x400')
tk.Label(root, text="Welche tracks sollen zufällig gespielt werden?",
         font=('arial', 12, 'bold', 'italic')) \
    .grid(row=0, column=0, columnspan=2)

for index, song in enumerate(songs):
    tk.Button(root, text="reinhören", command=lambda arg=song: play_song(arg)) \
        .grid(row=index + 1, column=0)
    var = tk.BooleanVar()
    var.song = song
    song_selections.append(var)
    tk.Checkbutton(root, text=song, variable=var, command=select_song) \
        .grid(row=index + 1, column=1)
root.mainloop()

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=5):
        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 = 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)

    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:
        # to stop the music if he has listened shortly at the tkinter form
        pygame.mixer.music.stop()
        playlist = select_song()
        track = sample(playlist, 1)
        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 board.  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)
                pygame.mixer.music.load(track[0])
                pygame.mixer.music.play()
                pygame.mixer.music.set_endevent(pygame.USEREVENT)
                music_running = True
                while music_running:
                    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()
                        elif event.type == pygame.USEREVENT:
                            track = random.sample(playlist, 1)
                        if not pygame.mixer.music.get_busy():
                            music_running = False
                            break

                    ball.move()
                    screen.blit(BACKGROUND_IMAGE, (0, 0))
                    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
#4
I don't think I explained the file search very well. I will try to do a better job.

You have the mp3 files in the same folder as the game (I think there should be a subfolder for all the image and mp3 files). All you need to do is replace the hardcoded song list with a one line file search for mp3 files. Results will be the same except the order, and order doesn't matter because you randomize the playback. Players don't have to do anything special and will not notice any difference. However, players now have the option of replacing music files if they want. I am listening to "Dark Side of the Moon" while playing.

I took the standalone music player and modified it for use by the breakout game. Only required a few small changes. If I can't talk you into using a subfolder for all the game related files you just need to remove the "/ 'music'" from line 9. This makes the music player look for mp3 files in the current folder.
import pygame, pathlib, random
import tkinter as tk

class MusicPlayer(tk.Tk):
    """
    A simple music player that uses the pygame music mixer.  I maintain
    a class "playlist" so I can be used to select music for a pygame.
    """
    music_folder = pathlib.Path('.') / 'music'  # Put mp3 files in subfolder named "music"
    playlist = []  # Class variable so can be accessed after player shuts down.

    def __init__(self, *args, title="Music Player", playmsg=None, **kvargs):
        super().__init__(*args, **kvargs)
        self.title("music")
        tk.Label(
            self,
            text=title,
            font=('arial', 12, 'bold', 'italic')).grid(row=0, column=0, columnspan=2)

        # Make controls for constructing playlist and playing songs
        self.song_selections = []
        for index, song in enumerate(self.music_folder.glob("*.mp3")):
            # Checkboxes are used to select songs for playlist
            songtitle = pathlib.Path(song).name[:-4]
            var = tk.BooleanVar()
            var.song = song
            self.song_selections.append(var)
            tk.Checkbutton(self, text=songtitle, variable=var, command=self.update_playlist) \
                .grid(row=index+1, column=0, sticky="w")

            # Button is used to immediately play song
            tk.Button(self, text="Play", command=lambda arg=song: self.play_song(arg)) \
                .grid(row=index+1, column=1, padx=5, pady=2)

        # Play the playlist
        if playmsg is None:
            tk.Button(self, text="Play Selected", command=self.play_playlist) \
                .grid(row=index+1, column=0, columnspan=2, sticky="ew")
        else:
            tk.Button(self, text=playmsg, command=self.destroy) \
                .grid(row=index+1, column=0, columnspan=2, sticky="ew")

    def update_playlist(self):
        """Update the playlist to contain selected songs"""
        self.playlist.clear()
        for var in self.song_selections:
            if var.get():
                self.playlist.append(var.song)

    def play_playlist(self):
        """Play songs in playlist"""
        pygame.mixer.music.stop()
        if len(self.playlist) > 0:
            songs = random.sample(self.playlist, k=len(self.playlist))
            pygame.mixer.music.load(songs[0])
            for song in songs[1:]:
                pygame.mixer.music.queue(song)
            pygame.mixer.music.play()

    @staticmethod
    def play_song(song):
        """Play song now"""
        pygame.mixer.music.load(song)
        pygame.mixer.music.play()

if __name__ == "__main__":
    pygame.init()
    pygame.mixer.init()
    pygame.mixer.music.set_volume(0.7)
    MusicPlayer().mainloop()
Then I modified your breakout game to import the module and use it to get a playlist.
import random, math, pygame
import tkinter as tk
from musicplayer import MusicPlayer
from PIL import Image
from itertools import product

# Define colors used by the game.
TEXT_COLOR = (255, 255, 255)
FOREGROUND = (0, 0, 0)  # Recolor image pixels that are this color
TRANSPARENT = (255, 255, 255)  # Make image pixels this color transparent
BALL_COLOR = (255, 255, 255)
PADDLE_COLOR = (255, 255, 255)
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"))
BACKGROUND_FILE = "Hintergrund.png"
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 578
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=5):
        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 = 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(playlist):

    def play_music():
        if playlist and len(playlist) > 0:
            pygame.mixer.music.load(random.choice(playlist))
            pygame.mixer.music.play()

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

    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)
    background = pygame.image.load(BACKGROUND_FILE)

    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 board.  Could have different layouts for each level
            for coord in BRICK_COORDS:
                Brick(*coord)
            allsprites.add(Brick.group)
            play_music()
            pygame.mixer.music.set_endevent(pygame.USEREVENT)

            # 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()
                    elif event.type == pygame.USEREVENT:
                        if not pygame.mixer.music.get_busy():
                            play_music()

                ball.move()
                screen.fill((0, 0, 180))
                # screen.blit(background, (0, 0))   # I don't have a background image
                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__":
    pygame.init()
    pygame.mixer.init()
    pygame.mixer.music.set_volume(0.7)
    MusicPlayer(title="Select Music", playmsg="Play Breakout").mainloop()
    main(MusicPlayer.playlist)
My image files aren't right for your game. I think my bricks are too big and the ball and paddle don't show up because I don't have your background file and my ball and paddle end up being black on black. I commented out the line that draws the background image and replaced it with one that fills the screen with blue. I also removed the music loop and instead wait for events from the music mixer. You were kind of doing this, but not quite. The event works pretty well for me.
Reply
#5
Dear deanhystad,

thanks a lot for the excellent work you did!!

I moved the music in the music subfolder and implemented the MusicPlayer class...

I've got a little last question concerning the game:

Do you have an idea why the ball is bouncing inside the paddle (and not on the edge)?

I don't want to cause inconvenience again, because you made a great many of effort for the game and I don't want to overtax your helpfulness...

If fixing it demands to much work, just communicate it to me - and I can deal with it...

Please don't put to much effort in it, because the game works perfectly now...

Without your help I couldn't get the game this perfect own my own!!

Thanks very much for your patience and your great helpfulness...
Reply
#6
The ball is bouncing on the bottom of the paddle because Ball.ymax = bottom of paddle - height of ball. The ball does not check for collisions until the bottom of the ball reaches the bottom of the paddle. I was just about to post some new code that addresses this very problem and another that I had noticed for a while but never really looked at until I could play breakout while listening to Jethro Tull.

Paddle collisions:
I've tried a lot of things with paddle/ball collisions but I think I finally have it. See Ball.move() in the code below for details, but the cliff notes version is: Check if the ball got past the paddle. If not, check for ball/paddle collision only if ball is moving down (positive dy).

The other thing I noticed had to do with horizontal ball movement. Far too often there is none. Initially I was converting ball dx and dy to ints and I figured the vertical ball motion was from clipping all -1 < dx < 1 to zero. I also noticed there didn't seem to be a lot of variety in the angles either. Ball movement was experiencing "quantization". With ball speed set to 5, the ball angle off the paddle was limited to 9 possible values.

I changed dx and dy to be floats, but this changed nothing. Turned out the problem was not only with dx and dy being ints, but with x and y being ints also. If I ask Pygame to move the ball to 5.4, 14.8 it moves the ball to 5, 14. All coordinates are converted to int, and any decimal information is lost. A ball angle near vertical can have a dx in the range -1 < dx < 1. Because of the integer clipping any dx in this range is effectively zero. If you ask Pygame to move less than a pixel it doesn't move at all.

The solution is to have float x and y coordinates. Pygame won't do that, so I added float coordinates to the Ball class. Now when I tell the ball to go to 5.4, 14.8 it still moves the sprite to 5, 14, but it remembers the decimal part of the coordinates. The next time I move the ball the reported ball position is 5.4, 14.8. Now the ball can move at sub-pixel "speeds".
import random, math, pygame
import tkinter as tk
from musicplayer import MusicPlayer
from PIL import Image
from itertools import product

# Define colors used by the game.
TEXT_COLOR = (255, 255, 255)
FOREGROUND = (0, 0, 0)  # Recolor image pixels that are this color
TRANSPARENT = (255, 255, 255)  # Make image pixels this color transparent
BALL_COLOR = (255, 255, 255)
PADDLE_COLOR = (255, 255, 255)
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 = "ball.png"
PADDLE_IMAGE = "paddle.png"
BRICK_FILES = (("brick0.png", "brick1.png", "brick2.png"), ("bbrick0.png", "bbrick1.png", "bbrick2.png"))
BACKGROUND_FILE = "Hintergrund.png"
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 578
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

    @property
    def width(self):
        return self.rect.width

    @property
    def height(self):
        return self.rect.height



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=5):
        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.xfloat = self.yfloat = 0
        self.xmax = SCREEN_WIDTH - self.rect.width
        self.ymax = paddle.bottom - self.rect.height
        self.reset(0)

    def at(self, x, y):
        self.xfloat = x
        self.yfloat = y
        return super().at(x, y)

    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 = self.speed * math.sin(angle)
        self.dy = -self.speed * math.cos(angle)
        self.active = True

    def move(self):
        """Update the ball position.  Check for collisions with bricks, walls and the paddle"""
        hit_status = 0
        if not self.active:
            # Sit on top of the paddle
            self.at(self.paddle.centerx-self.width//2, self.paddle.y-self.height-2)
            return self

        # Did I hit some bricks?  Update the bricks and the score
        x1, y1 = self.xfloat, self.yfloat
        x2, y2 = x1 + self.dx, y1 + self.dy
        if (xhits := pygame.sprite.spritecollide(self.at(x2, y1), Brick.group, False)):
            self.dx = -self.dx
            hit_status += 1
        if (yhits := pygame.sprite.spritecollide(self.at(x1, y2), Brick.group, False)):
            self.dy = -self.dy
            hit_status += 2
        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
            hit_status += 4
        if y2 <= 0:
            self.dy = abs(self.dy)
            hit_status += 8

        # Did I get past the paddle?
        if (y2 >= self.paddle.y) and ((self.x > self.paddle.right) or (self.right < self.paddle.x)):
            self.lives.kill()
            self.active = False
        elif self.dy > 0 and pygame.Rect.colliderect(self.at(x2, y2).rect, self.paddle.rect):
            # 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
            hit_status += 16

        if hit_status > 0:
            self.at(x1, y1)
            print(hit_status, self.dx, self.dy, x1, y1, x2, y2)
        else:
            self.at(x2, y2)


def main(playlist):

    def play_music():
        """Play song from playlist"""
        if playlist and len(playlist) > 0:
            pygame.mixer.music.load(random.choice(playlist))
            pygame.mixer.music.play()

    def displayText(text, font, pos=None, color=TEXT_COLOR):
        """Draw text on screen"""
        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.display.set_caption("Breakout")
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    clock = pygame.time.Clock()
    allsprites = pygame.sprite.Group()
    score_font = pygame.font.Font(None, 34)
    background = pygame.image.load(BACKGROUND_FILE)

    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 board.  Could have different brick layouts for each level
            for coord in BRICK_COORDS:
                Brick(*coord)
            allsprites.add(Brick.group)
            play_music()

            # 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()
                    elif event.type == pygame.USEREVENT:
                        if not pygame.mixer.music.get_busy():
                            play_music()

                ball.move()
                screen.fill((0, 0, 180))   # Doing this because I don't have a background image
                # screen.blit(background, (0, 0))
                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__":
    pygame.init()
    pygame.mixer.init()
    pygame.mixer.music.set_volume(0.7)
    pygame.mixer.music.set_endevent(pygame.USEREVENT)
    MusicPlayer(title="Select Music", playmsg="Play Breakout").mainloop()
    main(MusicPlayer.playlist)
Reply
#7
Dear deanhystad,

like in my previous post I'm deeply grateful...

Now everything is perfect!!

Thanks for helping me complete this game for this long period!!

flash77
Reply


Forum Jump:

User Panel Messages

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