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