""" Main file for the entire hardware. Controls everything except for GPIO input. """ from collections import deque import importlib.util import json import os from pathlib import Path import pocket_friends import pygame from pygame.locals import * from ..hardware.gpio_handler import Constants, GPIOHandler # FPS for the entire game to run at. game_fps = 16 # FPS that animations run at animation_fps = 2 # The resolution the game is rendered at. game_res = 80 # Gets the directory of the script for importing and the save directory script_dir = os.path.dirname(os.path.abspath(__file__)) save_dir = os.path.join(Path.home(), '.pocket_friends') # Tries to make the save directory. Does nothing if it already exists. try: os.mkdir(save_dir) except FileExistsError: pass class SaveHandler: """ Class that handles the hardware attributes and save files. """ def __init__(self): # Attributes that are saved to a file to recover upon startup. self.attributes = { 'version': pocket_friends.__version__, 'time_elapsed': 0, 'bloop': '', 'age': 0, 'health': 0, 'hunger': 0, 'happiness': 0, 'missed_care': 0, 'adult': 0, 'evolution_stage': -1, } def write_save(self): """ Writes attributes of class to "save.json" file. """ with open(save_dir + '/save.json', 'w') as save_file: json.dump(self.attributes, save_file) save_file.close() def read_save(self): """ Reads from "save.json" and inserts into attributes dictionary. Creates file if it does not exist. """ # Open up the save file and read it into self.attributes. try: with open(save_dir + '/save.json', 'r') as save_file: self.attributes = json.load(save_file) save_file.close() # If there is no save file, write one with the defaults. except FileNotFoundError: self.write_save() class PlaygroundFriend(pygame.sprite.Sprite): """ Class for the sprite of the creature on the main playground. """ def __init__(self, save_handler): pygame.sprite.Sprite.__init__(self) # All attributes of the bloops self.bloop = save_handler.attributes['bloop'] self.age = save_handler.attributes['age'] self.health = save_handler.attributes['health'] self.hunger = save_handler.attributes['hunger'] self.happiness = save_handler.attributes['happiness'] self.evolution_stage = save_handler.attributes['evolution_stage'] self.missed_care = save_handler.attributes['missed_care'] self.adult = save_handler.attributes['adult'] self.direction = 0 # Draw the correct bloop depending on the stage if self.evolution_stage == 0: image_directory = script_dir + '/resources/images/bloops/{0}/egg_images'.format(self.bloop) elif self.evolution_stage == 1: image_directory = script_dir + '/resources/images/bloops/{0}/baby_images'.format(self.bloop) elif self.evolution_stage == 2: image_directory = script_dir + '/resources/images/bloops/{0}/teen_images'.format(self.bloop) else: # Draw the correct adult based on care if self.adult == 0: image_directory = script_dir + '/resources/images/bloops/{0}/adult_images/good'.format(self.bloop) elif self.adult == 1: image_directory = script_dir + '/resources/images/bloops/{0}/adult_images/neutral'.format(self.bloop) else: image_directory = script_dir + '/resources/images/bloops/{0}/adult_images/bad'.format(self.bloop) self.images = [] for filename in os.listdir(image_directory): self.images.append(pygame.image.load(image_directory + '/' + filename)) self.rect = self.images[0].get_rect() self.rect.x = (game_res / 2) - (self.rect.width / 2) self.rect.y = (game_res / 2) - (self.rect.height / 2) self.index = 0 self.image = self.images[self.index] self.animation_frames = game_fps / animation_fps self.current_frame = 0 def update(self): """ Takes the images loaded and animates it, spacing it out equally for the framerate. """ margins = 9 movement_amount = 4 self.current_frame += 1 if self.current_frame >= self.animation_frames: self.current_frame = 0 self.index = (self.index + 1) % len(self.images) self.image = self.images[self.index] # Move the sprite so long as it is not in the egg stage if self.evolution_stage != 0: if self.rect.x < margins: self.direction = 1 elif self.rect.x > game_res - margins - self.rect.width: self.direction = 0 if self.direction == 0: self.rect.x -= movement_amount else: self.rect.x += movement_amount self.age += 1 class SelectionEgg(pygame.sprite.Sprite): """ Class for the eggs on the egg selection screen. """ def __init__(self, egg_color): pygame.sprite.Sprite.__init__(self) self.egg_color = egg_color # Loads the JSON file of the egg to read in data. with open(script_dir + '/resources/data/bloop_info/{0}.json'.format(egg_color), 'r') as save_file: json_file = json.load(save_file) save_file.close() image_directory = script_dir + '/resources/images/bloops/{0}/egg_images'.format(egg_color) # Gets the description off the egg from the JSON file. self.description = json_file.get('description') # Load the egg from the given color and get the bounding rectangle for the image. self.images = [] for filename in os.listdir(image_directory): self.images.append(pygame.image.load(image_directory + '/' + filename)) # Get the rectangle from the first image in the list self.rect = self.images[0].get_rect() self.index = 0 self.image = self.images[self.index] self.animation_frames = game_fps / animation_fps self.current_frame = 0 def update_frame_dependent(self): """ Takes the images loaded and animates it, spacing it out equally for the framerate. """ self.current_frame += 1 if self.current_frame >= self.animation_frames: self.current_frame = 0 self.index = (self.index + 1) % len(self.images) self.image = self.images[self.index] def update(self): """ Updates the sprite object. """ self.update_frame_dependent() class InfoText: """ Class for drawing large amounts of text on the screen at a time """ def __init__(self, font, text='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam commodo tempor ' 'aliquet. Suspendisse placerat accumsan neque, nec volutpat nunc porta ut.'): self.font = font self.text = [] # Text broken up into a list according to how it will fit on screen. self.max_lines = 6 # Max number of lines to be shown on screen at a time. self.offset = 0 # Arrow icons to indicate scrolling self.up_arrow = pygame.image.load(script_dir + '/resources/images/gui/up_arrow.png').convert_alpha() self.down_arrow = pygame.image.load(script_dir + '/resources/images/gui/down_arrow.png').convert_alpha() raw_text = text # Copy the text to a different variable to be cut up. margins = 4.5 max_line_width = game_res - (margins * 2) # The maximum pixel width that drawn text can be. cut_chars = '.,! ' # Characters that will be considered "cuts" aka when a line break can occur. # Prevents freezing if the end of the string does not end in a cut character # Will fix eventually more elegantly if raw_text[-1:] not in cut_chars: raw_text += ' ' # Calculating line breaks. while len(raw_text) > 0: index = 0 test_text = '' # Chunk of text to pseudo-render and test the width of. # Loops until the testing text has reached the size limit. while True: # Break if the current index is larger than the remaining text. if index + 1 > len(raw_text): index -= 1 break # Add one character to the testing text from the raw text. test_text += raw_text[index] # Get the width of the pseudo-rendered text. text_width = font.size(test_text)[0] # Break if the text is larger than the defined max width. if text_width > max_line_width: break index += 1 pass # Gets the chunk of text to be added to the list. text_chunk = raw_text[0:index + 1] # Determines if the chunk of text has any break characters. has_breaks = any(cut_chars in text_chunk for cut_chars in cut_chars) # If the text has break characters, start with the last character and go backwards until # one has been found, decreasing the index each time. if has_breaks: while raw_text[index] not in cut_chars: index -= 1 text_chunk = raw_text[0:index + 1] # If there are no break characters in the chunk, simply decrease the index by one and insert # a dash at the end of the line to indicate the word continues. else: index -= 1 text_chunk = raw_text[0:index + 1] text_chunk += '-' # Append the text chunk to the list of text to draw. self.text.append(text_chunk) # Cut the text to repeat the process with the new cut string. raw_text = raw_text[index + 1:] def draw(self, surface): """ Draws the text on a given surface. :param surface: The surface for the text to be drawn on. """ # Constants to help draw the text line_separation = 7 left_margin = 3 top_margin = 25 bottom_margin = 10 # Draw the lines on the screen for i in range(min(len(self.text), self.max_lines)): text = self.font.render(self.text[i + self.offset], False, (64, 64, 64)) surface.blit(text, (left_margin, top_margin + (i * line_separation))) # Draw the arrows if there is more text than is on screen. if self.offset != 0: surface.blit(self.up_arrow, ((game_res / 2) - (self.up_arrow.get_rect().width / 2), top_margin - 3)) if len(self.text) - (self.offset + 1) >= self.max_lines: surface.blit(self.down_arrow, ((game_res / 2) - (self.down_arrow.get_rect().width / 2), game_res - bottom_margin)) def scroll_down(self): """ Scrolls the text on the screen down. """ # Ensures that the offset cannot be too big as to try to render non-existent lines. if len(self.text) - (self.offset + 1) >= self.max_lines: self.offset += 1 def scroll_up(self): """ Scrolls the text on the screen up. """ if self.offset > 0: # Ensures a non-zero offset is not possible. self.offset -= 1 # Makes Pygame draw on the display of the RPi. os.environ["SDL_FBDEV"] = "/dev/fb1" # Useful for debugging on the PC. Imports a fake RPi.GPIO library if one is not found (which it can't # be on a PC, RPi.GPIO cannot be installed outside of a Raspberry Pi. try: importlib.util.find_spec('RPi.GPIO') import RPi.GPIO as GPIO except ImportError: import pocket_friends.development.FakeGPIO as GPIO def game(): """ Starts the hardware. """ pygame.init() # Hide the cursor for the Pi display. pygame.mouse.set_visible(False) # The hardware is normally rendered at 80 pixels and upscaled from there. If changing displays, change the # screen_size to reflect what the resolution of the new display is. screen_size = 320 window = pygame.display.set_mode((screen_size, screen_size)) surface = pygame.Surface((game_res, game_res)) # Only really useful for PCs. Does nothing on the Raspberry Pi. pygame.display.set_caption('Pocket Friends {0}'.format(pocket_friends.__version__)) # Add an icon to the pygame window. icon = pygame.image.load(script_dir + '/resources/images/icon/icon.png').convert_alpha() pygame.display.set_icon(icon) clock = pygame.time.Clock() # Font used for small text in the hardware. Bigger text is usually image files. small_font = pygame.font.Font(script_dir + '/resources/fonts/5Pts5.ttf', 10) # Default hardware state when the hardware first starts. game_state = 'title' running = True save_handler = SaveHandler() # A group of all the sprites on screen. Used to update all sprites at onc all_sprites = pygame.sprite.Group() # Start the GPIO handler to take in buttons from the RPi HAT. GPIOHandler.setup() # Dev code used to exit the hardware. Default Down, Down, Up, Up, Down, Down, Up, Up, A, A, B dev_code = deque() for button in [Constants.buttons.get('j_d'), Constants.buttons.get('j_d'), Constants.buttons.get('j_u'), Constants.buttons.get('j_u'), Constants.buttons.get('j_d'), Constants.buttons.get('j_d'), Constants.buttons.get('j_u'), Constants.buttons.get('j_u'), Constants.buttons.get('a'), Constants.buttons.get('a'), Constants.buttons.get('b')]: dev_code.append(button) # Log of the inputs. input_log = deque() # Time since last input. Used to help regulate double presses of buttons. last_input_tick = 0 def draw(): """ Draws the main pygame display. """ # Draws all the sprites on screen and scales the screen to the correct size from the rendered size. all_sprites.update() all_sprites.draw(surface) frame = pygame.transform.scale(surface, (screen_size, screen_size)) window.blit(frame, frame.get_rect()) # Update the entire display. pygame.display.flip() def draw_bg(): """ Draws the main hardware background image onto a given surface. """ bg_image = pygame.image.load(script_dir + '/resources/images/bg.png').convert() surface.blit(bg_image, (0, 0)) def log_button(pressed_button): """ Logs the button presses to register the dev code. :param pressed_button: The button code to be logged """ input_log.append(pressed_button) if len(input_log) > len(dev_code): input_log.popleft() def create_event(pressed_button): """ Creates a pygame event with a given keyboard code :param pressed_button: """ nonlocal last_input_tick # Register a button click so long as the last button click happened no less than two frames ago if pygame.time.get_ticks() - last_input_tick > clock.get_time() * 2: pygame.event.post(pygame.event.Event(KEYDOWN, {'key': pressed_button})) pygame.event.post(pygame.event.Event(KEYUP, {'key': pressed_button})) log_button(pressed_button) last_input_tick = pygame.time.get_ticks() def check_dev_code(): """ Checks if the dev code has been entered. If it has, stop the program. """ nonlocal running if dev_code == input_log: running = False def handle_gpio(): """ Handles getting GPIO button presses and making a pygame event when a press is detected. """ for pressed_button in Constants.buttons: code = Constants.buttons.get(pressed_button) # Check if a button has been pressed. If it has, create a pygame event for it. if GPIOHandler.get_press(code): create_event(code) def keyboard_handler(): """ Simulates key presses to GPIO button presses. Also handles quitting the hardware. """ nonlocal running # Checks if a corresponding keyboard key has been pressed. If it has, emulate a button press. for keyboard_event in pygame.event.get(): if keyboard_event.type == pygame.QUIT: running = False if keyboard_event.type == pygame.KEYDOWN: if keyboard_event.key == pygame.K_a: create_event(Constants.buttons.get('a')) if keyboard_event.key == pygame.K_b: create_event(Constants.buttons.get('b')) if keyboard_event.key == pygame.K_PERIOD: create_event(Constants.buttons.get('j_i')) if keyboard_event.key == pygame.K_RIGHT: create_event(Constants.buttons.get('j_r')) if keyboard_event.key == pygame.K_LEFT: create_event(Constants.buttons.get('j_l')) if keyboard_event.key == pygame.K_DOWN: create_event(Constants.buttons.get('j_d')) if keyboard_event.key == pygame.K_UP: create_event(Constants.buttons.get('j_u')) if keyboard_event.key == pygame.K_ESCAPE: running = False def pre_handler(): """ Runs at the beginning of each loop, handles drawing the background, controlling hardware speed, and controlling the GPIO button inputs and keyboard handler """ # Regulate the speed of the hardware. clock.tick(game_fps) # Handle all inputs for both debugging and real GPIO button presses. keyboard_handler() handle_gpio() check_dev_code() # Draw the background. draw_bg() while running: if game_state == 'title': all_sprites.empty() pre_handler() # Draw the title image in the middle of the screen. title_image = pygame.image.load(script_dir + '/resources/images/title.png').convert_alpha() surface.blit(title_image, (0, 0)) draw() # Show the title for 1 second then move on to the initialization phase of the hardware. pygame.time.wait(1000) game_state = 'init' elif game_state == 'playground': all_sprites.empty() bloop = PlaygroundFriend(save_handler) all_sprites.add(bloop) while running and game_state == 'playground': pre_handler() draw() elif game_state == 'init': all_sprites.empty() pre_handler() draw() # Read the save file. save_handler.read_save() # Determines if it is a new hardware or not by looking at the evolution stage. If it is -1, the egg has # not been created yet, and the hardware sends you to the egg selection screen. If not, the hardware sends # you to the playground. if save_handler.attributes['bloop'] == '': game_state = 'egg_select' else: game_state = 'playground' elif game_state == 'egg_select': # Submenu used within the egg selection menu. submenu = 'main' selected = 0 selected_color = "" while running and game_state == 'egg_select': all_sprites.empty() if submenu == 'main': # Creates and holds the egg objects in a list. eggs = [SelectionEgg('dev_egg'), SelectionEgg('blue'), SelectionEgg('rainbow')] # How many eggs per row should be displayed. eggs_per_row = 3 distance_between_eggs = 36 / eggs_per_row # Count the total rows. total_rows = -(-len(eggs) // eggs_per_row) distance_between_rows = 32 / eggs_per_row # Determine the location of each egg. for egg in eggs: current_row = eggs.index(egg) // eggs_per_row rows_after = total_rows - (current_row + 1) egg_in_row = eggs.index(egg) % eggs_per_row eggs_after = min(len(eggs) - (current_row * eggs_per_row), eggs_per_row) - (egg_in_row + 1) x_offset = 32 y_offset = 30 # The x coordinate of an egg is determined by which egg in the row it is, and how many eggs # are in that row. If there is only 1 egg in a row, it is in the middle of the screen. If # there are two, they're on equal halves and so on. x = x_offset - (eggs_after * distance_between_eggs) + (egg_in_row * distance_between_eggs) y = y_offset - (rows_after * distance_between_rows) + (current_row * distance_between_rows) egg.rect.x = x egg.rect.y = y # Add the egg to the sprite list. all_sprites.add(egg) def get_cursor_coords(selection): """ Gets the coordinates of an egg on the selection screen by index and returns it as a tuple :param selection: index of the egg to be selected :return: tuple of the coordinates of the selected egg """ cursor_x_offset = -2 cursor_y_offset = -2 return eggs[selection].rect.x + cursor_x_offset, eggs[selection].rect.y + cursor_y_offset def sel_left(): """ Select the egg to the left with constraints. """ nonlocal selected if selected % eggs_per_row != 0: selected -= 1 def sel_right(): """ Select the egg to the right with constraints. """ nonlocal selected row = selected // eggs_per_row eggs_in_row = min(len(eggs) - (row * eggs_per_row), eggs_per_row) if selected % eggs_per_row != eggs_in_row - 1: selected += 1 def sel_up(): """ Select the egg above with constraints. """ nonlocal selected if selected // eggs_per_row != 0: selected -= eggs_per_row def sel_down(): """ Select the egg below with constraints. """ nonlocal selected if selected // eggs_per_row != total_rows - 1: selected += eggs_per_row while running and game_state == 'egg_select' and submenu == 'main': pre_handler() for event in pygame.event.get(): if event.type == pygame.KEYDOWN: if event.key == Constants.buttons.get('j_r'): sel_right() if event.key == Constants.buttons.get('j_l'): sel_left() if event.key == Constants.buttons.get('j_d'): sel_down() if event.key == Constants.buttons.get('j_u'): sel_up() if event.key == Constants.buttons.get('a'): # Advance to the egg info screen for the selected egg. submenu = 'bloop_info' # Draws the cursor on screen. cursor = pygame.image.load(script_dir + '/resources/images/clock_selector.png').convert_alpha() surface.blit(cursor, get_cursor_coords(selected)) selected_color = eggs[selected].egg_color draw() elif submenu == 'bloop_info': # Draw the selected egg on screen egg = SelectionEgg(selected_color) egg.rect.x = 32 egg.rect.y = 3 all_sprites.add(egg) # Info screen for the eggs. info = InfoText(small_font, egg.description) while running and game_state == 'egg_select' and submenu == 'bloop_info': pre_handler() for event in pygame.event.get(): if event.type == pygame.KEYDOWN: if event.key == Constants.buttons.get('j_d'): # Scroll down on the info screen. info.scroll_down() if event.key == Constants.buttons.get('j_u'): # Scroll up on the info screen. info.scroll_up() if event.key == Constants.buttons.get('a'): # Write save file with new attributes save_handler.attributes['bloop'] = egg.egg_color save_handler.attributes['health'] = 10 save_handler.attributes['hunger'] = 10 save_handler.attributes['happiness'] = 10 save_handler.attributes['evolution_stage'] = 0 save_handler.write_save() # Go to playground game_state = 'playground' if event.key == Constants.buttons.get('b'): # Go back to the egg selection screen. submenu = 'main' # Draw the info screen. info.draw(surface) draw() else: # Go to the error state if an invalid state is set. game_state = None else: # Error screen. This appears when an invalid hardware state has been selected. all_sprites.empty() frames_passed = 0 # Counter for frames, helps ensure the hardware isn't frozen. while running and game_state != 'title': pre_handler() # Draw the error screen error_screen = pygame.image.load(script_dir + '/resources/images/debug/invalid.png').convert_alpha() surface.blit(error_screen, (0, -8)) # Counts the frames passed. Resets every second. frames_passed += 1 if frames_passed >= game_fps: frames_passed = 0 # Draws the frame counter. frame_counter = small_font.render('frames: {0}'.format(frames_passed), False, (64, 64, 64)) surface.blit(frame_counter, (1, game_res - 10)) for event in pygame.event.get(): if event.type == pygame.KEYDOWN: if event.key == Constants.buttons.get('b'): # Reset back to the title screen. game_state = 'title' draw() def main(): """ Calls the hardware() function to start the hardware. """ game() GPIOHandler.teardown() pygame.quit()