#!/usr/bin/env python3 # Launch PGFW games in the `games/` folder import argparse, os, pathlib, pygame, sys import lib.pgfw.pgfw as pgfw from games.ibitfit.electric_sieve.ElectricSieve import ElectricSieve # Import GPIO library if available try: import RPi.GPIO as GPIO except ImportError: pass class Playzing(pgfw.Game): # The GPIO pins corresponding to the buttons PIN_BUTTON_LEFT = 24 PIN_BUTTON_RIGHT = 23 def __init__(self, config_overrides=None): """ Create logo sprite, clear screen, and subscribe to events. """ pgfw.Game.__init__(self) # Member dict for tracking pin state changes. Start at 1 because high means unpressed. self.pin_states = { self.PIN_BUTTON_LEFT: 1, self.PIN_BUTTON_RIGHT: 1 } # Assign types to configuration values self.configuration.type_declarations.add_chart({ "display": { "int-list": "clear", "bool": "rotated" }, "logo": { "path": ["fire", "text"], "int": ["margin", "restart", "delay"], "bool": "trail" } }) # Merge config overrides from the command line self.configuration.merge_command_line() # Clear screen to black self.get_display_surface().fill(self.configuration.get("display", "clear")) # Initialize GPIO input and callbacks if GPIO library is loaded if "RPi.GPIO" in sys.modules: self.initialize_gpio() # Subscribe to PGFW command events self.subscribe(self.respond) # Load sprites for logo parts self.logo_text = pgfw.Sprite(self) self.logo_text.load_from_path(self.get_resource(self.configuration.get("logo", "text")), True) if self.rotated: self.logo_text.rotate() self.logo_fire = pgfw.Sprite(self) self.logo_fire.load_from_path(self.get_resource(self.configuration.get("logo", "fire")), True) if self.rotated: self.logo_fire.rotate() # Register logo animation functions and begin animation self.register(self.animate_logo) self.register(self.reveal_text, interval=40) self.play(self.animate_logo, play_once=True) # Don't let modifier keys trigger an any key event self.input.suppress_any_key_on_mods() def initialize_gpio(self): """ Set pin numbering mode to GPIO, initialize all buttons to input pullup. """ # Use GPIO numbering GPIO.setmode(GPIO.BCM) # Set button pins to pullup and attach to each a callback that runs on press or release for pin in self.PIN_BUTTON_LEFT, self.PIN_BUTTON_RIGHT: GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.add_event_detect(pin, GPIO.BOTH, self.gpio_input) def gpio_input(self, pin): """ Translate GPIO input into PGFW commands, so Raspberry Pi wired controllers be used for input. Compare the pin state to what is stored in memory. Only fire an event if there has been a change in state. A change from high to low triggers a press event. A change from low to high triggers a press cancel event. @param pin Raspberry Pi pin number as read by the RPi.GPIO library """ # Print the input state of each pin if debug is requested on the command line if "--debug" in sys.argv: pin_name = "left" if pin == self.PIN_BUTTON_LEFT else "right" left_pin_state = GPIO.input(self.PIN_BUTTON_LEFT) right_pin_state = GPIO.input(self.PIN_BUTTON_RIGHT) print(f"Received {pin} ({pin_name}) input. Left state is {left_pin_state}. Right state is {right_pin_state}") # If the saved state of the pin is the same, there hasn't been a real button press or release, so don't continue if self.pin_states[pin] != GPIO.input(pin): self.pin_states[pin] = GPIO.input(pin) # A high signal means the button is released, and a low signal means it is pressed cancel = not (GPIO.input(pin) == GPIO.LOW) if pin == self.PIN_BUTTON_LEFT: self.input.post_command("left", cancel=cancel) elif pin == self.PIN_BUTTON_RIGHT: self.input.post_command("right", cancel=cancel) self.input.post_any_command(id=pin, cancel=cancel) @property def rotated(self): return self.configuration.get("display", "rotated") def orient(self, obj): oriented = obj if self.rotated: if isinstance(obj, pygame.Rect): oriented = pygame.Rect(obj.y, self.get_display_surface().get_height() - obj.x + obj.w, obj.h, obj.w) return oriented def animate_logo(self): """ Reset the logo animation to its beginning and start animating. """ # Place the logo margin = self.configuration.get("logo", "margin") self.logo_text.location.center = self.get_display_surface().get_rect().center self.logo_text.move(-self.orient(self.logo_fire.location).w + margin / 2, rotated=self.rotated) # Clear the screen self.get_display_surface().fill(self.configuration.get("display", "clear")) # Place the fire at the left side of the logo text if not self.rotated: self.logo_fire.location.midleft = self.logo_text.location.midleft else: self.logo_fire.location.midbottom = self.logo_text.location.midbottom # Close the draw clip for the logo completely self.logo_text_clip = self.logo_text.location.copy() if not self.rotated: self.logo_text_clip.width = 0 else: self.logo_text_clip.height = 0 # Queue the text reveal to start self.play(self.reveal_text, delay=self.configuration.get("logo", "delay")) def reveal_text(self): """ Move the fire right, opening the logo text clip rect until it's big enough to show all the text. Queue the animation to restart once finished. """ self.logo_fire.move(10, rotated=self.rotated) halt = False if not self.rotated: self.logo_text_clip.width = self.logo_fire.location.left - self.logo_text.location.left limit = self.logo_text.location.right + self.configuration.get("logo", "margin") / 2 if self.logo_fire.location.left > limit: self.logo_fire.location.left = limit halt = True else: self.logo_text_clip.height = self.logo_text.location.bottom - self.logo_fire.location.bottom self.logo_text_clip.bottom = self.logo_text.location.bottom limit = self.logo_text.location.top - self.configuration.get("logo", "margin") / 2 if self.logo_fire.location.bottom < limit: self.logo_fire.location.bottom = limit halt = True if halt: self.halt(self.reveal_text) self.play(self.animate_logo, delay=self.configuration.get("logo", "restart"), play_once=True) def respond(self, event): """ Respond to all PGFW commands. If an any button is pressed, launch the iBitFit game. """ if self.delegate.compare(event, "any"): os.chdir(pathlib.Path("games/ibitfit")) print(f"current working dir is {os.getcwd()}") ibitfit = ElectricSieve(suppress_gpio_init=True, rotate=self.rotated) ibitfit.display.set_screen(dimensions=ibitfit.configuration.get("display", "dimensions")) ibitfit.run() os.chdir(pathlib.Path("../..")) self.display.set_screen(dimensions=self.configuration.get("display", "dimensions")) print(f"current working dir is {os.getcwd()}") def update(self): pgfw.Animation.update(self) # Temporary fix: this stops audio when the game is quit and prevents spawning multiple BGM when game is relaunched pygame.mixer.stop() # Erase the center of the screen if enabled if not self.configuration.get("logo", "trail"): erase_rect = self.logo_text.location.copy() if not self.rotated: erase_rect.width = self.logo_fire.location.right - self.logo_text.location.left else: erase_rect.height = self.logo_text.location.bottom - self.logo_fire.location.top erase_rect.bottom = self.logo_text.location.bottom self.get_display_surface().fill(self.configuration.get("display", "clear"), erase_rect) # Draw the logo text clipped to the current state of the animation, then remove the clip self.get_display_surface().set_clip(self.logo_text_clip) self.logo_text.update() self.get_display_surface().set_clip(None) # Draw the fire self.logo_fire.update() if __name__ == "__main__": # Parse command line arguments parser = argparse.ArgumentParser() parser.add_argument( "--keep-working-directory", action="store_true", help=""" Keep using the current directory as the working directory. The script expects the games to be in the games/ folder, so they can be imported as Python modules. Changing the directory to anything other than the launcher project's root folder will break it under normal circumstances, so use with care. """) parser.add_argument( "--kms", action="store_true", help=""" Use SDL 2's KMS video driver. For use on systems without a windowing system (like Linux in only console mode). See https://wiki.libsdl.org/SDL2/FAQUsingSDL#how_do_i_choose_a_specific_video_driver """) parser.add_argument( "--framebuffer", action="store_true", help=""" Use SDL 1.2's framebuffer video driver. For use on older systems without a windowing system that aren't using KMS. This won't work with SDL 2 or Pygame 2. See https://wiki.libsdl.org/SDL2/FAQUsingSDL#how_do_i_choose_a_specific_video_driver """) parser.add_argument( "--ignore-hangup", action="store_true", help=""" Ignore hangup signals. Enabling this may be necessary for running the launcher as a systemd service. See https://stackoverflow.com/questions/57205271/how-to-display-pygame-framebuffer-using-systemd-service """) arguments, unknown = parser.parse_known_args() # If not keeping the working directory, try to move into the same directory as where this program is stored. Use the location of the program # that launched this process to determine the path. if not arguments.keep_working_directory: target = os.path.dirname(sys.argv[0]) if not pathlib.Path(os.getcwd()).samefile(target): try: os.chdir(target) except: print(f""" Warning: detected that the working directory is not the same as the launcher project's root directory and could not change to to detected directory {target} """) if arguments.kms: # Use the KMS video driver. This works for newer versions of Raspberry Pi with the KMS overlay enabled, SDL 2, and Pygame 2. os.putenv("SDL_VIDEODRIVER", "kmsdrm") elif arguments.framebuffer: # Use the framebuffer display. This only works with Pygame 1.9.6 (and SDL 1.2). os.putenv("SDL_VIDEODRIVER", "fbcon") os.putenv("SDL_FBDEV", "/dev/fb0") # Run the launcher playzing = Playzing() playzing.run()