Esempio n. 1
0
    def __init__(self, size: tuple = (720, 720)):
        """Bootstrap the application."""

        self.scale = calculate_scale(size)

        self.screen_size = size
        self.screen = pygame.display.set_mode(self.screen_size, depth=32)

        pygame.display.set_icon(self.ICON)
        pygame.display.set_caption(self.NAME)

        # Load the image in a temp variable,
        # then assign it to the attribute (omit long one liner)
        main_background = pygame.image.load(asset("background_menu.jpg"))
        self.main_background = pygame.transform.scale(main_background,
                                                      self.screen_size)

        # Bootstrap the notifications.
        self.notification = Notification(self.scale[0])

        # Audio
        self.mixer = Mixer(self.notification)

        # Init game as None
        self.game = None
Esempio n. 2
0
    def __init__(self, start: tuple, scale: tuple, *containers: sprite.Group):
        """MacGyver default constructor."""

        # Override default containers
        if containers:
            self.containers = containers

        # Call the parent constructor.
        super().__init__(self.containers)

        # The original graphic representation of MacGyver.
        self.original_img = pygame.image.load(asset(self.MAC_GYVER))

        # Scaled graphic representation of MacGyver.
        self.image = pygame.transform.scale(self.original_img, scale)

        # Represent the hitbox and the position of MacGyver.
        self.rect = self.image.get_rect()

        # Place MacGyver on the starting point.
        self.rect.topleft = scale_position(start, scale)

        # Scale the moves directions of MacGyver.
        self._set_scale_moves(scale)
        self._old_coordinates = (0, 0)

        # Represent the inventory of MacGyver where the items will be stored.
        # This inventory must be filled with the three needed items in order
        # to put the guardian in sleep.
        self.inventory = sprite.Group()
Esempio n. 3
0
    def set_music(self, music_slug, loops=-1):
        """Set a music to play."""
        # Fallback with the main theme.
        pygame.mixer.music.load(
            self.musics.get(music_slug,
                            asset('sounds/macgyver_theme_song.ogg')))
        pygame.mixer.music.set_volume(self._musicVolume)

        if self.is_muted:
            return
        else:
            pygame.mixer.music.play(loops)
Esempio n. 4
0
def defeat_screen(screen: pygame.Surface, mixer: Mixer):
    """Draw the defeat screen."""
    pygame.display.set_caption('MacGyver Maze - Defeat')

    mixer.set_music('defeat')

    banner = pygame.transform.scale(
        pygame.image.load(asset('defeat_banner.png')), get_screen_size())

    next_action = loop_menu(screen, banner, 'defeat', mixer)

    if next_action != 'quit':
        mixer.set_music('main')

    return next_action
Esempio n. 5
0
    def __init__(self, x, y, scale):
        super().__init__(self.containers)

        # image property is reserved for the scaled image.
        self.original_img = pygame.image.load(asset('floor.png'))

        # Scaled image must be assigned to the image attribute.
        self.image = pygame.transform.scale(self.original_img, scale)

        # Unpack the scale values.
        self.width, self.height = scale

        # Retrieve the rect of the image.
        self.rect = self.image.get_rect()

        # Assign the coordinates of the floor.
        self.rect.topleft = (x * self.width, y * self.height)
Esempio n. 6
0
    def __init__(self, pos: tuple, scale: tuple, *containers: sprite.Group):
        """Needle default constructor.

        :param pos A given position where the item will be placed.
        """

        # Override default containers if provided.
        if containers:
            self.containers = containers

        # Call the parent constructor.
        super().__init__(self.containers)

        # The original graphic representation of the Needle.
        self.original_img = pygame.image.load(asset(self.item_file))

        # Scaled graphic representation of the Needle.
        self.image = pygame.transform.scale(self.original_img, scale)

        # Represent the hitbox and the position of the Needle.
        self.rect = self.image.get_rect()

        # Place the item at the given coordinates.
        self.rect.topleft = scale_position(pos, scale)
Esempio n. 7
0
    def execute(self):
        # Load buttons images.
        buttons = {
            "play": pygame.image.load(asset('buttons/play.png')),
            "help": pygame.image.load(asset('buttons/help.png')),
            "quit": pygame.image.load(asset('buttons/quit.png'))
        }

        # Add a bit of transparency to the buttons,
        # we'll make them more opaque when hovering it.
        for button in buttons.values():
            button.fill((255, 255, 255, 192), None, pygame.BLEND_RGBA_MULT)

        # Get the rect of each buttons
        btns_rect = {
            button: buttons.get(button).get_rect()
            for button in buttons
        }

        # Set the x, y coordinates of the buttons rect.
        for i, rect in enumerate(list(btns_rect.values())):
            if i == 0:
                rect.topleft = (40, get_screen_height() - 450)
            else:
                precedent = list(btns_rect.values())[i - 1]
                rect.topleft = (40, (precedent.y + precedent.height + 10))

        # X and Y coordinates of the user mouse cursor at the precedent frame.
        precedent_x, precedent_y = None, None

        # This is the main menu.
        while True:
            self.screen.fill(BLACK)
            self.screen.blit(self.main_background, (0, 0))

            # Display buttons to the screen
            for button, rect in zip(buttons.values(), btns_rect.values()):
                self.screen.blit(button, rect.topleft)

            # X and Y coordinates of the user mouse cursor.
            mouse_x, mouse_y = pygame.mouse.get_pos()

            # The button that the mouse is hovering.
            # When the user switch to key navigation,
            # we'll focus the button where the mouse is hovering.
            mouse_hovering = None

            # If the mouse perform an action (move or click)
            # then leave the keyboard navigation
            if self.mouse or mouse_x != precedent_x or mouse_y != precedent_y:
                self.key_nav = None

            # PLAY button
            if (self.key_nav == 0 or (btns_rect.get('play').collidepoint(
                (mouse_x, mouse_y)) and self.key_nav is None)):

                if self.key_nav is None:
                    mouse_hovering = 0

                # Make the button more opaque when we're hovering it.
                hovered(self.screen, buttons['play'], btns_rect['play'])

                if self.mouse or (self.key_nav is not None and self.enter_key):
                    # Initialize the game.
                    if self.game is None:
                        self.game = Game(self.screen, self.mixer,
                                         self.notification, self.scale)

                    # Then execute the game loop.
                    remove_game = self.game.execute()

                    # remove_game determines if we cleanup the resources
                    # that the game instance use.
                    if remove_game:
                        self.game = None

                    # Reset the standard caption.
                    pygame.display.set_caption(self.NAME)

            # HELP button
            if (self.key_nav == 1 or (btns_rect.get('help').collidepoint(
                (mouse_x, mouse_y)) and self.key_nav is None)):

                if self.key_nav is None:
                    mouse_hovering = 1

                hovered(self.screen, buttons['help'], btns_rect['help'])

                if self.mouse or self.enter_key:
                    help_menu(self.screen, self.mixer)
                    pygame.display.set_caption(self.NAME)

            # QUIT button
            if (self.key_nav == 2 or (btns_rect.get('quit').collidepoint(
                (mouse_x, mouse_y)) and self.key_nav is None)):

                if self.key_nav is None:
                    mouse_hovering = 2

                hovered(self.screen, buttons['quit'], btns_rect['quit'])

                if self.mouse or self.enter_key:
                    exit_app()

            # Reset the click and enter_key attributes to false for each frame.
            self.mouse = False
            self.enter_key = False

            # Assign the current mouse position. These values will
            # determines if the mouse cursor has moved or not.
            precedent_x, precedent_y = mouse_x, mouse_y

            for event in pygame.event.get():

                if event.type == KEYDOWN:
                    keys = pygame.key.get_pressed()

                    if keys[K_DOWN]:
                        # Start key navigation
                        if self.key_nav is None:
                            if mouse_hovering is None:
                                self.key_nav = 0
                            else:
                                self.key_nav = mouse_hovering

                        # Stay on key navigation...
                        else:
                            self.key_nav = (0 if self.key_nav == 2 else
                                            self.key_nav + 1)

                    if keys[K_UP]:
                        if self.key_nav is None:
                            if mouse_hovering is None:
                                self.key_nav = 0
                            else:
                                self.key_nav = mouse_hovering

                        else:
                            self.key_nav = (2 if self.key_nav == 0 else
                                            self.key_nav - 1)

                    # If the user press the enter key
                    if keys[K_RETURN] or keys[K_KP_ENTER]:
                        self.enter_key = True

                    # Handle keys which interact with the audio
                    self.mixer.keys_interaction(keys)

                if event.type == QUIT:
                    exit_app()

                # If the user click somewhere.
                if event.type == MOUSEBUTTONDOWN:
                    self.mouse = True

            if self.notification.is_active:
                self.notification.render(self.screen)

            # Update the display
            pygame.display.update()
Esempio n. 8
0
class App:
    """Represent the entire application."""

    # The name of the application.
    NAME = "MacGyver Maze"

    # The slug name of the application
    SLUG_NAME = "macgyver-maze"

    # The version of the application.
    VERSION = '1.0.2'

    # The icon of the application.
    ICON = pygame.image.load(asset('tile-crusader-logo.png'))

    # Determines if the user click or not.
    mouse = False

    # The index of the object where the user is.
    # If set to None, this mean's that the user is using
    # his mouse to navigate throught the menus.
    key_nav = None

    # Determines if the user press an enter key or not.
    enter_key = False

    # Scale must be dynamic if resizable is available.
    # A specific event handler will manage that.
    scale = None

    # Screen attributes
    screen = None
    screen_size = None

    def __init__(self, size: tuple = (720, 720)):
        """Bootstrap the application."""

        self.scale = calculate_scale(size)

        self.screen_size = size
        self.screen = pygame.display.set_mode(self.screen_size, depth=32)

        pygame.display.set_icon(self.ICON)
        pygame.display.set_caption(self.NAME)

        # Load the image in a temp variable,
        # then assign it to the attribute (omit long one liner)
        main_background = pygame.image.load(asset("background_menu.jpg"))
        self.main_background = pygame.transform.scale(main_background,
                                                      self.screen_size)

        # Bootstrap the notifications.
        self.notification = Notification(self.scale[0])

        # Audio
        self.mixer = Mixer(self.notification)

        # Init game as None
        self.game = None

    def execute(self):
        # Load buttons images.
        buttons = {
            "play": pygame.image.load(asset('buttons/play.png')),
            "help": pygame.image.load(asset('buttons/help.png')),
            "quit": pygame.image.load(asset('buttons/quit.png'))
        }

        # Add a bit of transparency to the buttons,
        # we'll make them more opaque when hovering it.
        for button in buttons.values():
            button.fill((255, 255, 255, 192), None, pygame.BLEND_RGBA_MULT)

        # Get the rect of each buttons
        btns_rect = {
            button: buttons.get(button).get_rect()
            for button in buttons
        }

        # Set the x, y coordinates of the buttons rect.
        for i, rect in enumerate(list(btns_rect.values())):
            if i == 0:
                rect.topleft = (40, get_screen_height() - 450)
            else:
                precedent = list(btns_rect.values())[i - 1]
                rect.topleft = (40, (precedent.y + precedent.height + 10))

        # X and Y coordinates of the user mouse cursor at the precedent frame.
        precedent_x, precedent_y = None, None

        # This is the main menu.
        while True:
            self.screen.fill(BLACK)
            self.screen.blit(self.main_background, (0, 0))

            # Display buttons to the screen
            for button, rect in zip(buttons.values(), btns_rect.values()):
                self.screen.blit(button, rect.topleft)

            # X and Y coordinates of the user mouse cursor.
            mouse_x, mouse_y = pygame.mouse.get_pos()

            # The button that the mouse is hovering.
            # When the user switch to key navigation,
            # we'll focus the button where the mouse is hovering.
            mouse_hovering = None

            # If the mouse perform an action (move or click)
            # then leave the keyboard navigation
            if self.mouse or mouse_x != precedent_x or mouse_y != precedent_y:
                self.key_nav = None

            # PLAY button
            if (self.key_nav == 0 or (btns_rect.get('play').collidepoint(
                (mouse_x, mouse_y)) and self.key_nav is None)):

                if self.key_nav is None:
                    mouse_hovering = 0

                # Make the button more opaque when we're hovering it.
                hovered(self.screen, buttons['play'], btns_rect['play'])

                if self.mouse or (self.key_nav is not None and self.enter_key):
                    # Initialize the game.
                    if self.game is None:
                        self.game = Game(self.screen, self.mixer,
                                         self.notification, self.scale)

                    # Then execute the game loop.
                    remove_game = self.game.execute()

                    # remove_game determines if we cleanup the resources
                    # that the game instance use.
                    if remove_game:
                        self.game = None

                    # Reset the standard caption.
                    pygame.display.set_caption(self.NAME)

            # HELP button
            if (self.key_nav == 1 or (btns_rect.get('help').collidepoint(
                (mouse_x, mouse_y)) and self.key_nav is None)):

                if self.key_nav is None:
                    mouse_hovering = 1

                hovered(self.screen, buttons['help'], btns_rect['help'])

                if self.mouse or self.enter_key:
                    help_menu(self.screen, self.mixer)
                    pygame.display.set_caption(self.NAME)

            # QUIT button
            if (self.key_nav == 2 or (btns_rect.get('quit').collidepoint(
                (mouse_x, mouse_y)) and self.key_nav is None)):

                if self.key_nav is None:
                    mouse_hovering = 2

                hovered(self.screen, buttons['quit'], btns_rect['quit'])

                if self.mouse or self.enter_key:
                    exit_app()

            # Reset the click and enter_key attributes to false for each frame.
            self.mouse = False
            self.enter_key = False

            # Assign the current mouse position. These values will
            # determines if the mouse cursor has moved or not.
            precedent_x, precedent_y = mouse_x, mouse_y

            for event in pygame.event.get():

                if event.type == KEYDOWN:
                    keys = pygame.key.get_pressed()

                    if keys[K_DOWN]:
                        # Start key navigation
                        if self.key_nav is None:
                            if mouse_hovering is None:
                                self.key_nav = 0
                            else:
                                self.key_nav = mouse_hovering

                        # Stay on key navigation...
                        else:
                            self.key_nav = (0 if self.key_nav == 2 else
                                            self.key_nav + 1)

                    if keys[K_UP]:
                        if self.key_nav is None:
                            if mouse_hovering is None:
                                self.key_nav = 0
                            else:
                                self.key_nav = mouse_hovering

                        else:
                            self.key_nav = (2 if self.key_nav == 0 else
                                            self.key_nav - 1)

                    # If the user press the enter key
                    if keys[K_RETURN] or keys[K_KP_ENTER]:
                        self.enter_key = True

                    # Handle keys which interact with the audio
                    self.mixer.keys_interaction(keys)

                if event.type == QUIT:
                    exit_app()

                # If the user click somewhere.
                if event.type == MOUSEBUTTONDOWN:
                    self.mouse = True

            if self.notification.is_active:
                self.notification.render(self.screen)

            # Update the display
            pygame.display.update()
Esempio n. 9
0
class Mixer:
    """Class which manage all audios of the application."""

    # Section of the config file which this class manipulate.
    SECTION = 'Audio'

    # Determines if the audio is muted or not
    __is_muted = bool(settings('Audio', 'muted'))

    # Volume of the musics.
    _musicVolume = settings('Audio', 'music_volume')

    # TODO:
    #  replace by # settings('Audio', 'sound_volume') when sound is handled.
    # Volume of the sounds.
    _soundVolume = settings('Audio', 'music_volume')

    # Dict which contains every musics path of the game.
    musics = {
        'main': asset("sounds/macgyver_theme_song.ogg"),
        'victory': asset("sounds/victory_fanfare.ogg"),
        'defeat': asset("sounds/defeat_theme.ogg"),
    }

    # Dict which contains every sounds of the game.
    sounds = {
        'dummy_sound': asset("sounds/dummy_sound.ogg"),
        'wilhelm_scream': asset("sounds/wilhelm_scream.ogg"),
    }

    @property
    def is_muted(self):
        """Return the boolean which determine if the audio is muted or not."""
        return self.__is_muted

    @is_muted.setter
    def is_muted(self, value: int):
        """Set a new state for the mute status and save it to the config."""
        self.__is_muted = bool(value)
        write_config(section=self.SECTION, key='muted', value=value)

    @property
    def music_volume(self):
        """Retrieve the volume of musics."""
        return self._musicVolume

    @music_volume.setter
    def music_volume(self, value):
        """Increment/decrement the volume of musics."""
        new_volume = self._musicVolume + value

        if self.__is_audio_value(new_volume):
            self._musicVolume += value
        elif new_volume > 1:
            self._musicVolume = 1
        else:
            self._musicVolume = 0.00

        write_config(self.SECTION, key='music_volume', value=self._musicVolume)

        pygame.mixer.music.set_volume(self.music_volume)

    @property
    def sound_volume(self):
        """Increment/decrement the volume of sounds."""
        return self._soundVolume

    @sound_volume.setter
    def sound_volume(self, value):
        """Set a new volume for sounds."""
        if self.__is_audio_value(self._musicVolume + value):
            self._soundVolume += value
            write_config(self.SECTION, 'sound_volume', self._soundVolume)

    def __init__(self, notification: Notification = None):
        """Initialize the game mixer."""

        pygame.mixer.pre_init(
            # The audio frequency.
            frequency=44100,

            # How many bits used for audio samples.
            # negative value means that the signed sample values will be used.
            size=-1,

            # 1 for mono, 2 for stereo.
            channels=2,

            # A low buffer makes sounds play (essentially) immediatly.
            buffer=512)

        # Initialize sounds after the pre initialization of the mixer.
        for path in self.sounds.keys():
            self.sounds[path] = pygame.mixer.Sound(self.sounds[path])

        self.notification = notification

        # Music of the main menu.
        self.set_music('main')

    def set_music(self, music_slug, loops=-1):
        """Set a music to play."""
        # Fallback with the main theme.
        pygame.mixer.music.load(
            self.musics.get(music_slug,
                            asset('sounds/macgyver_theme_song.ogg')))
        pygame.mixer.music.set_volume(self._musicVolume)

        if self.is_muted:
            return
        else:
            pygame.mixer.music.play(loops)

    def play_sound(self, sound_slug):
        """Play a sound. Fallback with a dummy sound if given doesn't exist."""
        if self.is_muted:
            return

        # Retrieve the sound
        sound = self.sounds.get(sound_slug, self.sounds['dummy_sound'])
        # Set the volume based on the local settings.
        sound.set_volume(self.sound_volume)
        # Play the sound
        sound.play()

    def toggle(self):
        """Toggle the mute state."""
        if self.is_muted:
            self.__unmute()
        else:
            self.__mute()

    def keys_interaction(self, keys):
        """Handle keys which interact with the audio"""
        if keys[K_F1]:
            self.toggle()

            # String representation of mute status
            mute_status = 'muted' if self.is_muted else 'unmuted'

            self.notification.active('volume', mute_status).set_timer(1)

        if keys[K_KP_PLUS] or keys[K_KP_MINUS]:
            step = 0.1 if keys[K_KP_PLUS] else -0.1

            # Decrement/increment the volume by 0.1 (10%)
            self.music_volume = step

            # A volume "human readable" representation as %
            percent = int(self.music_volume * 100)

            self.notification.active('volume', percent).set_timer(1)

    def __mute(self):
        """Mute audio."""
        pygame.mixer.music.fadeout(0)
        self.is_muted = 1

    def __unmute(self):
        """Unmute the audio."""
        pygame.mixer.music.play()
        self.is_muted = 0

    @staticmethod
    def __is_audio_value(value):
        """Verify if the given value is a correct value for an audio level.

        A valid value is greater or equal than 0 and less or equal than 1.
        """
        return 1 >= value >= 0
Esempio n. 10
0
def loop_menu(screen: Surface, banner: Surface, menu_type: str, mixer: Mixer):
    """Handle events of the victory/defeat menus."""
    running = True
    next_action = None

    # near to the bottom of the screen.
    offset_buttons_y = get_screen_height() * 0.80
    buttons_margin = 20

    # Initialize buttons
    original_back_button = pygame.image.load(asset('buttons/back.png'))
    original_exit_button = pygame.image.load(asset('buttons/exit_game.png'))

    # Scale size of buttons
    back_button = pygame.transform.scale(
        original_back_button,
        (int(get_screen_width() / 4), original_back_button.get_height()))
    exit_button = pygame.transform.scale(
        original_exit_button,
        (int(get_screen_width() / 4), original_exit_button.get_height()))

    # Initialize retry button variables
    retry_button = None
    retry_rect = None

    exit_button.fill((255, 255, 255, 192), None, pygame.BLEND_RGBA_MULT)
    back_button.fill((255, 255, 255, 192), None, pygame.BLEND_RGBA_MULT)

    back_rect = back_button.get_rect()
    exit_rect = exit_button.get_rect()

    if menu_type == 'defeat':
        # We'll display the retry button only when the user doesn't win.
        original_retry_button = pygame.image.load(asset('buttons/retry.png'))
        retry_button = pygame.transform.scale(
            original_retry_button, (int(get_screen_width() / 4), 70))
        retry_button.fill((255, 255, 255, 192), None, pygame.BLEND_RGBA_MULT)
        retry_rect = retry_button.get_rect()
        retry_rect.topleft = (get_screen_width() / 2 - (retry_rect.width / 2),
                              offset_buttons_y)

    back_rect.topleft = (buttons_margin, offset_buttons_y)

    exit_rect.topleft = (get_screen_width() - exit_rect.width - buttons_margin,
                         offset_buttons_y)

    # Initialize click at False
    click = False

    while running:
        # X and Y coordinates of the user mouse cursor.
        mouse_x, mouse_y = pygame.mouse.get_pos()

        screen.blit(banner, (0, 0))
        screen.blit(back_button, back_rect.topleft)
        screen.blit(exit_button, exit_rect.topleft)

        # Draw the retry button
        if retry_rect is not None:
            screen.blit(retry_button, retry_rect.topleft)

            if retry_rect.collidepoint((mouse_x, mouse_y)):
                hovered(screen, retry_button, retry_rect)
                if click:
                    running = False
                    next_action = 'retry'

        if back_rect.collidepoint((mouse_x, mouse_y)):
            hovered(screen, back_button, back_rect)
            if click:
                running = False
                next_action = 'back'

        if exit_rect.collidepoint((mouse_x, mouse_y)):
            hovered(screen, exit_button, exit_rect)
            if click:
                running = False
                next_action = 'quit'

        if running:
            click, running, next_action = handle_keys_event(mixer, next_action)

        if mixer.notification.is_active:
            mixer.notification.render(screen)

        pygame.display.update()

    return next_action
Esempio n. 11
0
def help_menu(screen: pygame.Surface, mixer: Mixer):
    running = True

    # Create the background of the menu (transparent black)
    background = pygame.Surface((get_screen_width(), get_screen_height()))
    background.set_alpha(240)
    background.fill(BLACK)
    screen.blit(background, (0, 0))

    pygame.display.set_caption('MacGyver Maze - Options')

    font = pygame.font.SysFont(None, 50)

    back_button = pygame.image.load(asset('buttons/back.png'))
    back_button.fill((255, 255, 255, 192), None, BLEND_RGBA_MULT)
    back_button_rect = back_button.get_rect()
    back_button_rect.topleft = (get_screen_width() / 2 -
                                (back_button_rect.width / 2),
                                get_screen_height() - back_button_rect.height -
                                MARGIN)

    # Y offset for lines
    offset_line1 = LINE_SPACING
    offset_line2 = offset_line1 + LINE_SPACING
    offset_line3 = offset_line2 + LINE_SPACING
    offset_line4 = offset_line3 + LINE_SPACING

    # Initialize each key image and associate coordinates for each image.
    # Line 1 - Start with arrow keys.
    # These will be displayed with the same layout as on a standard keyboard.
    key_left = pygame.image.load(asset('keys/key_left.png'))
    key_left_rect = key_left.get_rect()
    key_left_rect.topleft = (MARGIN, offset_line1)

    key_down = pygame.image.load(asset('keys/key_down.png'))
    key_down_rect = key_down.get_rect()
    key_down_rect.topleft = (key_left_rect.right, offset_line1)

    key_up = pygame.image.load(asset('keys/key_up.png'))
    key_up_rect = key_up.get_rect()
    key_up_rect.topleft = (key_down_rect.left,
                           (offset_line1 - key_down_rect.height))

    key_right = pygame.image.load(asset('keys/key_right.png'))
    key_right_rect = key_right.get_rect()
    key_right_rect.topleft = (key_down_rect.right, offset_line1)

    # Line 2 - C key
    key_c = pygame.image.load(asset('keys/key_c.png'))
    key_c_rect = key_c.get_rect()
    key_c_rect.topleft = (MARGIN, offset_line2)

    # Line 3 - F1 key
    key_f1 = pygame.image.load(asset('keys/key_f1.png'))
    key_f1_rect = key_f1.get_rect()
    key_f1_rect.topleft = (MARGIN, offset_line3)

    # Line 4 - numeric keys - & +
    key_minus = pygame.image.load(asset('keys/key_n_minus.png'))
    key_minus_rect = key_minus.get_rect()
    key_minus_rect.topleft = (MARGIN, offset_line4)

    key_plus = pygame.image.load(asset('keys/key_n_plus.png'))
    key_plus_rect = key_plus.get_rect()
    key_plus_rect.topleft = (key_minus_rect.right, offset_line4)

    # Now, create a caption for each key
    # Arrow keys caption
    arrow_keys_caption = font.render("Move MacGyver", True, WHITE)
    arrow_keys_caption_rect = arrow_keys_caption.get_rect()
    arrow_keys_caption_rect.topleft = (key_right_rect.right + MARGIN,
                                       offset_line1),

    # Get the X axis of the first caption,
    # we'll align every captions based on this one.
    offset_caption_x = arrow_keys_caption_rect.left

    # C key caption
    key_c_caption = font.render("Craft the syringe", True, WHITE)
    key_c_caption_rect = key_c_caption.get_rect()
    key_c_caption_rect.topleft = (offset_caption_x, offset_line2)

    # F1 key caption
    f1_key_caption = font.render("Mute/unmute the volume", True, WHITE)
    f1_key_caption_rect = f1_key_caption.get_rect()
    f1_key_caption_rect.topleft = (offset_caption_x, offset_line3)

    # -/+ key caption
    volume_key_caption = font.render("Increase/decrease volume", True, WHITE)
    volume_key_caption_rect = volume_key_caption.get_rect()
    volume_key_caption_rect.topleft = (offset_caption_x, offset_line4)

    # Initialize click variable which
    # will determines if the user click or not.
    click = False

    while running:
        # Blit each images to the screen.
        screen.blits([
            # Back button
            (back_button, back_button_rect.topleft),

            # Arrow key images
            (key_up, key_up_rect.topleft),
            (key_left, key_left_rect.topleft),
            (key_down, key_down_rect.topleft),
            (key_right, key_right_rect.topleft),

            # other keys (C, F1, etc...)
            (key_c, key_c_rect.topleft),
            (key_f1, key_f1_rect.topleft),
            (key_minus, key_minus_rect),
            (key_plus, key_plus_rect),

            # captions
            (arrow_keys_caption, arrow_keys_caption_rect.topleft),
            (key_c_caption, key_c_caption_rect.topleft),
            (f1_key_caption, f1_key_caption_rect),
            (volume_key_caption, volume_key_caption_rect),
        ])

        # X and Y coordinates of the user mouse cursor.
        mouse_x, mouse_y = pygame.mouse.get_pos()

        if back_button_rect.collidepoint((mouse_x, mouse_y)):
            # Make the button more opaque when we're hovering it.
            hovered(screen, back_button, back_button_rect)

            if click:
                # We'll leave the menu
                running = False

        if running:
            click, running, _ = handle_keys_event(mixer)

        pygame.display.update()

    return