# SPDX-FileCopyrightText: 2022 Tim C, written for Adafruit Industries # # SPDX-License-Identifier: MIT # pylint: disable=too-many-lines, import-outside-toplevel, too-many-statements, too-many-branches import os import time import json import board import adafruit_imageload import terminalio from displayio import TileGrid, Group, OnDiskBitmap from adafruit_display_text.bitmap_label import Label try: import foamyguy_nvm_helper as nvm_helper except ImportError: nvm_helper = None print( "Warning: missing foamyguy_nvm_helper, will not be able to use NVM for highscore" ) class OctopusGame(Group): """ This class will orchestrate and manage the entire game. High level functions are provided for hardware input events to come in via. It extends `displayio.Group` so it can be added to other Groups or shown directly on the display. """ # Seconds between flailing diver animation frames CAUGHT_DIVER_ANIMATION_DELAY = 0.3 # Seconds between treasure pulling animation frames DEPOSIT_TREASURE_ANIMATION_DELAY = 0.3 # How many frames to run the treasure pulling animation for DEPOSIT_TREASURE_ANIMATION_FRAMES = 3 # Game mode state variables GAME_MODE_A = 0 GAME_MODE_B = 1 # Vertical position of game mode labels (near bottom left) GAME_MODE_A_LBL_Y = 128 - 14 GAME_MODE_B_LBL_Y = 128 - 7 # speed adjustment values. Value in seconds will be subtracted from game speed. # larger values will equate to faster game speed. GAME_MODE_A_SPEED_ADJUSTMENT = 0.0 GAME_MODE_B_SPEED_ADJUSTMENT = 0.2 # Highscore type contants HIGH_SCORE_DISABLED = 0 HIGH_SCORE_SDCARD = 1 HIGH_SCORE_NVM = 2 # -- State machine constant variables -- STATE_WAITING_TO_PLAY = -1 # Before game begins STATE_NORMAL_GAMEPLAY = 0 # Standard game behavior STATE_CAUGHT_ANIMATION = 1 # Diver has been caught STATE_DEPOSIT_TREASURE_ANIMATION = 2 # Depositing treasure at the boat STATE_GAME_OVER = 3 # Diver has been caught and no lives remain # -- End of State machine constants -- # Seconds to play the caught diver flailing animations CAUGHT_DIVER_LENGTH = 4.0 # "Cheat" variable to make diver invincible for testing / development INVINCIBLE = False # Seconds between game Octopus movements, will get faster as score increases SCORE_SPEED_FACTOR = 0.5 # name of the file on the SDCard where high score data will be stored SDCARD_HIGH_SCORE_FILE = "octopus_high_score.json" def __init__(self, display=None, high_score_type=HIGH_SCORE_DISABLED): #pylint: disable=too-many-statements super().__init__() # if user did not pass display argument try to use built-in display. if not display and "DISPLAY" in dir(board): display = board.DISPLAY # current score variable self._score = 0 # timestamp when the diver was caught self._diver_caught_time = 0 # main state machine current state self.current_state = OctopusGame.STATE_WAITING_TO_PLAY # current extra lives available self.extra_lives = 2 # speed of the octopus movements, goes faster as score increases self.score_speed_factor = OctopusGame.SCORE_SPEED_FACTOR # current frame index for depositing treasure animation self.current_deposit_treasure_animation_frame = 0 # game mode variable self.current_game_mode = OctopusGame.GAME_MODE_A # Set up Background self.bg_with_sadow = True self.bg_bmp = OnDiskBitmap("octopus_game_sprites/bg_with_shadow.bmp") self.bg_tilegrid = TileGrid(self.bg_bmp, pixel_shader=self.bg_bmp.pixel_shader) self.append(self.bg_tilegrid) # Set up Extra Life indicator images self.extra_life_bmp = OnDiskBitmap("octopus_game_sprites/diver_extra_life.bmp") self.extra_life_bmp.pixel_shader.make_transparent(0) self.extra_life_tilegrid_1 = TileGrid( self.extra_life_bmp, pixel_shader=self.extra_life_bmp.pixel_shader ) self.extra_life_tilegrid_2 = TileGrid( self.extra_life_bmp, pixel_shader=self.extra_life_bmp.pixel_shader ) self.extra_life_tilegrid_1.x = 33 self.extra_life_tilegrid_2.x = 46 self.extra_life_tilegrid_1.y = 4 self.extra_life_tilegrid_2.y = 4 self.append(self.extra_life_tilegrid_1) self.append(self.extra_life_tilegrid_2) # Set up Player Character self.player = DiverPlayer() self.player.hidden = True self.append(self.player) # Set up Octopus object self.octopus = Octopus() self.append(self.octopus) # Set up caught flailing diver self.caught_diver_bmp, self.caught_diver_palette = adafruit_imageload.load( "octopus_game_sprites/diver_caught_small.bmp" ) self.caught_diver_tilegrid = TileGrid( self.caught_diver_bmp, pixel_shader=self.caught_diver_palette, height=1, width=1, tile_width=31, tile_height=40, ) self.caught_diver_palette.make_transparent(0) self.caught_diver_tilegrid.y = 46 self.caught_diver_tilegrid.x = 82 self.append(self.caught_diver_tilegrid) self.caught_diver_tilegrid.hidden = True self.caught_diver_last_anim_time = 0 # Set up treasure depositing boat diver sprite self.boat_diver_bmp, self.boat_diver_palette = adafruit_imageload.load( "octopus_game_sprites/diver_boat_small.bmp" ) self.boat_diver_tilegrid = TileGrid( self.boat_diver_bmp, pixel_shader=self.boat_diver_palette, width=1, height=1, tile_width=21, tile_height=16, default_tile=1, ) self.boat_diver_palette.make_transparent(0) self.boat_diver_tilegrid.x = 11 self.boat_diver_tilegrid.y = 5 self.boat_diver_last_anim_time = 0 self.append(self.boat_diver_tilegrid) # hide the caught diver to begin self.hide_caught_diver() # Set up label to show the current score self.score_lbl = Label(font=terminalio.FONT, text="0000", color=0x0) self.score_lbl.x = 90 self.score_lbl.y = 5 self.append(self.score_lbl) # Set up game mode label self.mode_lbl = Label(font=terminalio.FONT, text="", color=0x0) self.mode_lbl.x = 1 self.mode_lbl.y = OctopusGame.GAME_MODE_A_LBL_Y self.append(self.mode_lbl) # Treasure that the diver had before moving self._before_move_treasure_count = 0 # store the high score type on self to access later self.high_score_type = high_score_type # if we're using any highscore system if self.high_score_type: # set up the high score label to show scores to user self.high_score_label = Label( font=terminalio.FONT, scale=2, background_color=0xF9E8C2, anchor_point=(0.5, 0.5), color=0x000000, anchored_position=(display.width // 2, display.height // 2), padding_left=16, padding_right=16, line_spacing=0.75, ) self.high_score_label.hidden = True self.append(self.high_score_label) # if we're using SDCard high score system if self.high_score_type == OctopusGame.HIGH_SCORE_SDCARD: # setup the high score system with SDCard data import sdcardio import storage sd = sdcardio.SDCard(board.SPI(), board.SD_CS) vfs = storage.VfsFat(sd) storage.mount(vfs, "/sd") elif self.high_score_type == OctopusGame.HIGH_SCORE_NVM: if not nvm_helper: raise ImportError( "Cannot use NVM High Score system without foamyguy_nvm_helper library." ) @property def score(self): """ Current game score :return: The current score """ return self._score @score.setter def score(self, new_score): """ Set the score variable and update the visual score label :param new_score: new score value :return: None """ if new_score in (200, 500): self.extra_lives = 2 self.update_extra_lives() self._score = new_score self.score_speed_factor = self._score / 500 self.score_lbl.text = str(self.score) def update_extra_lives(self): """ Hide / Show the appropriate extra live indicator images :return: None """ if self.extra_lives == 2: self.extra_life_tilegrid_1.hidden = False self.extra_life_tilegrid_2.hidden = False if self.extra_lives == 1: self.extra_life_tilegrid_2.hidden = True if self.extra_lives == 0: self.extra_life_tilegrid_1.hidden = True def show_caught_diver(self): """ Show the caught diver and begin the flailing animation :return: None """ # player loses currently held treasure self.player.treasure_count = 0 # set the player state self.player.CUR_STATE = DiverPlayer.STATE_NO_TREASURE # current timestamp now = time.monotonic() # save the timestamp so we can compare to know when we're done self._diver_caught_time = now # hide the player character self.player.hidden = True # Show the appropriate tentacle segments self.octopus.t1as2_tilegrid.hidden = True self.octopus.t1as3_tilegrid.hidden = True self.octopus.t1as4_tilegrid.hidden = True # set the appropriate tentacle index segment for tentacle 1 self.octopus.tentacle_cur_indexes[1] = 1 # Hide the appropriate tentacle segments. self.octopus.t1bs2_tilegrid.hidden = False self.octopus.t1s1_tilegrid.hidden = False self.octopus.t1s0_tilegrid.hidden = False self.caught_diver_tilegrid.hidden = False def hide_caught_diver(self): """ Hide the caught diver and stop the flailing animation :return: None """ self.caught_diver_tilegrid.hidden = True self.octopus.t1bs2_tilegrid.hidden = True self.boat_diver_tilegrid.hidden = False self.player.CUR_LOCATION_INDEX = 0 self.player.CUR_SPRITE_INDEX = 0 self.player.update_location_and_sprite() def left_button_press(self): """ Left movement button action function. code.py will poll the hardware and call this. :return: None """ # check which state we're in to determine appropriate action(s) if ( self.current_state not in (OctopusGame.STATE_GAME_OVER, OctopusGame.STATE_WAITING_TO_PLAY) ): # if player just moved from the last spot in the water to the boat if self.player.CUR_LOCATION_INDEX == 0: # empty treasure from the player if self.player.treasure_count > 0: # hide the player character self.player.hidden = True # set the boat diver sprite self.boat_diver_tilegrid[0, 0] = 1 # store a timestamp for animation self.boat_diver_last_anim_time = time.monotonic() # show the boat diver self.boat_diver_tilegrid.hidden = False # Start the depositing treasure animation self.deposit_treasure() else: # player did not just move out of the water into the boat if not self.player.hidden: # tell the player object to move and let it handle the specific details self.player.move_backward() else: # we're in game over, or waiting to play state # swap to the alternate background image if not self.bg_with_sadow: self.remove(self.bg_tilegrid) self.bg_bmp = OnDiskBitmap("octopus_game_sprites/bg_with_shadow.bmp") self.bg_tilegrid = TileGrid( self.bg_bmp, pixel_shader=self.bg_bmp.pixel_shader ) self.insert(0, self.bg_tilegrid) else: self.remove(self.bg_tilegrid) self.bg_bmp = OnDiskBitmap("octopus_game_sprites/bg.bmp") self.bg_tilegrid = TileGrid( self.bg_bmp, pixel_shader=self.bg_bmp.pixel_shader ) self.insert(0, self.bg_tilegrid) self.bg_with_sadow = not self.bg_with_sadow def right_button_press(self): """ Right movement button action function. code.py will poll the hardware and call this. :return: None """ # check which state we're in to determine the appropriate action(s) if self.current_state not in ( OctopusGame.STATE_GAME_OVER, OctopusGame.STATE_WAITING_TO_PLAY, ): # if the boat diver is currently showing if not self.boat_diver_tilegrid.hidden: # hide the boat diver self.boat_diver_tilegrid.hidden = True # show the player character self.player.hidden = False else: # boat diver isn't currently showing if not self.player.hidden: # store treasure count before moving self._before_move_treasure_count = self.player.treasure_count # tell player to move forward and let it handle the details self.player.move_forward() # if treasure count changed then we know that we got one if self.player.treasure_count != self._before_move_treasure_count: # increment the score self.score += 1 else: # we're in game over, or waiting to play state # if high score is enabled if self.high_score_type: # get the current high score data self.read_high_score_data() # toggle the high score visibility self.high_score_label.hidden = not self.high_score_label.hidden def reset(self): """ Reset the game back to beginning state. :return: None """ self.score = 0 self.extra_lives = 2 self.update_extra_lives() # hide the high score label try: self.high_score_label.hidden = True except AttributeError: # high score is disabled pass def a_button_press(self): """ (A) Button action function. code.py will poll hardware and call this as needed :return: None """ # if we're in game over, or waiting to play state if self.current_state in ( OctopusGame.STATE_GAME_OVER, OctopusGame.STATE_WAITING_TO_PLAY, ): # reset the game self.reset() # set the mode to A self.current_game_mode = OctopusGame.GAME_MODE_A # update the mode label self.mode_lbl.text = "GAMEA" self.mode_lbl.y = OctopusGame.GAME_MODE_A_LBL_Y # set the current state to playing for the state machine self.current_state = OctopusGame.STATE_NORMAL_GAMEPLAY def b_button_press(self): """ (B) Button action function. code.py will poll hardware and call this as needed :return: None """ # if we're in game over or waiting to play state if self.current_state in ( OctopusGame.STATE_GAME_OVER, OctopusGame.STATE_WAITING_TO_PLAY, ): # reset the game self.reset() # set the mode to B self.current_game_mode = OctopusGame.GAME_MODE_B # update the mode label self.mode_lbl.text = "GAMEB" self.mode_lbl.y = OctopusGame.GAME_MODE_B_LBL_Y # set the current state to playing for the state machine. self.current_state = OctopusGame.STATE_NORMAL_GAMEPLAY def deposit_treasure(self): """ Show the deposit treasure animation :return: None """ # set the current state for the game state machine self.current_state = OctopusGame.STATE_DEPOSIT_TREASURE_ANIMATION # set the state for the player state machine self.player.CUR_STATE = DiverPlayer.STATE_NO_TREASURE # loop for animation frames for _ in range(3): # increment score self.score += 1 # wait until next animation frame time.sleep(0.15) # empty player treasure self.player.treasure_count = 0 # show the correct location and sprite for player character self.player.update_location_and_sprite() def lose_life(self): """ Process a lost life for the player. Called when octopus catches diver :return: None """ # decrement lives self.extra_lives -= 1 # check for game over if self.extra_lives <= -1: # set current state to game over self.current_state = OctopusGame.STATE_GAME_OVER if self.high_score_type: print("need to evaluate high score") self.evaluate_high_score() # update the extra lives indicators self.update_extra_lives() # show the caught diver and start the flailing animation self.show_caught_diver() def tick(self): """ Main game.tick() function, will be called once per iteration in the main loop. Will process behaviors based on current state. Will call tick() on Player and Octopus as needed. :return: None """ #pylint: disable=too-many-branches # store a timestamp to reference now = time.monotonic() # if current state is normal game playing if self.current_state == OctopusGame.STATE_NORMAL_GAMEPLAY: # if the caught diver / flail animation is not showing if self.caught_diver_tilegrid.hidden: # call tick on octopus, it will decide it's time to hide or show a tentacle segment self.octopus.tick(self) # only check for player being caught if the invincibility cheat is off. if not self.INVINCIBLE: # check if the player is within reach of the last tentacle segments # call lose_life() if so to process it if self.player.CUR_LOCATION_INDEX == 0 and not self.player.hidden: if not self.octopus.t0as2_tilegrid.hidden: self.lose_life() if self.player.CUR_LOCATION_INDEX == 1 and not self.player.hidden: if not self.octopus.t0bs3_tilegrid.hidden: self.lose_life() if self.player.CUR_LOCATION_INDEX == 2 and not self.player.hidden: if not self.octopus.t1as4_tilegrid.hidden: self.lose_life() if self.player.CUR_LOCATION_INDEX == 3 and not self.player.hidden: if not self.octopus.t2s3_tilegrid.hidden: self.lose_life() if self.player.CUR_LOCATION_INDEX == 4 and not self.player.hidden: if not self.octopus.t3s2_tilegrid.hidden: self.lose_life() # if the caught diver / flail animation is showing if not self.caught_diver_tilegrid.hidden: # if the total duration has not elapsed yet if now <= self._diver_caught_time + OctopusGame.CAUGHT_DIVER_LENGTH: # if it's been long enough since the previously shown frame if ( now >= OctopusGame.CAUGHT_DIVER_ANIMATION_DELAY + self.caught_diver_last_anim_time ): # show the next animation frame by swaping indexes in the spritesheet self.caught_diver_tilegrid[0, 0] = ( 0 if self.caught_diver_tilegrid[0, 0] == 1 else 1 ) # store the timestamp to compare with next time self.caught_diver_last_anim_time = now else: # the total duration has elapsed # stop the animation and hide the caught diver self.hide_caught_diver() # call player tick, it will manage the taking treasure animation as needed self.player.tick() # if current state is depositing treasure animation elif self.current_state == OctopusGame.STATE_DEPOSIT_TREASURE_ANIMATION: # if enough time has passed since the last animation frame shown if ( now >= OctopusGame.DEPOSIT_TREASURE_ANIMATION_DELAY + self.boat_diver_last_anim_time ): # if we haven't shown all of the frames yet if ( self.current_deposit_treasure_animation_frame < OctopusGame.DEPOSIT_TREASURE_ANIMATION_FRAMES ): # increment the frame count self.current_deposit_treasure_animation_frame += 1 # swap the sprite index to change to the other tile in the spritesheet self.boat_diver_tilegrid[0, 0] = ( 1 if self.boat_diver_tilegrid[0, 0] == 0 else 0 ) # store the timestamp to comapre with next time self.boat_diver_last_anim_time = now else: # We have shown all of the frames # set the sprite index to the one without the treasure bag self.boat_diver_tilegrid[0, 0] = 1 # set the current state to normal game playing self.current_state = OctopusGame.STATE_NORMAL_GAMEPLAY # set the frame count to zero for next time we need to show it self.current_deposit_treasure_animation_frame = 0 # if current state is game over elif self.current_state == OctopusGame.STATE_GAME_OVER: # if enough time has passed since the previous flailing diver animation frame if ( now >= OctopusGame.CAUGHT_DIVER_ANIMATION_DELAY + self.caught_diver_last_anim_time ): # swap the sprite index to show the other tile in the flailing animation spritesheet self.caught_diver_tilegrid[0, 0] = ( 0 if self.caught_diver_tilegrid[0, 0] == 1 else 1 ) # store the timestamp to comapre with next time self.caught_diver_last_anim_time = now def initialize_high_score(self): """ Check if the high score file or NVM object exists, and create it with an empty list if it doesn't. :return: None """ if self.high_score_type == OctopusGame.HIGH_SCORE_SDCARD: if OctopusGame.SDCARD_HIGH_SCORE_FILE not in os.listdir("/sd/"): f = open(f"/sd/{OctopusGame.SDCARD_HIGH_SCORE_FILE}", "w") f.write(json.dumps({"highscore_list": []})) f.close() elif self.high_score_type == OctopusGame.HIGH_SCORE_NVM: try: read_data = nvm_helper.read_data() except EOFError: read_data = None if not read_data or "highscore_list" not in read_data.keys(): nvm_helper.save_data({"highscore_list": []}, test_run=False) def read_high_score_data(self): """ Read the high score data object from the SDCard or NVM and populate the high score label with the data. :return: Dictionary object with highscore_list key containing list of high score values. """ self.initialize_high_score() if self.high_score_type == OctopusGame.HIGH_SCORE_SDCARD: f = open(f"/sd/{OctopusGame.SDCARD_HIGH_SCORE_FILE}", "r") data_obj = json.loads(f.read()) f.close() self.update_high_score_text(data_obj) return data_obj elif self.high_score_type == OctopusGame.HIGH_SCORE_NVM: try: read_data = nvm_helper.read_data() if "highscore_list" in read_data.keys(): self.update_high_score_text(read_data) return read_data except EOFError: # no high score data stored yet pass return None def write_high_score_data(self, new_data_obj): """ write the given high score object into the storage system, either SDcard or NVM. :param new_data_obj: the data :return: None """ self.initialize_high_score() if self.high_score_type == OctopusGame.HIGH_SCORE_SDCARD: f = open(f"/sd/{OctopusGame.SDCARD_HIGH_SCORE_FILE}", "w") f.write(json.dumps(new_data_obj)) f.close() elif self.high_score_type == OctopusGame.HIGH_SCORE_NVM: nvm_helper.save_data(new_data_obj, test_run=False) def update_high_score_text(self, data_obj): """ update the high score text label to show the high score values currently stored in the data file or NVM. :param data_obj: the dictionary data object to write. Should contain "highscore_list" key with list of highscore values. :return: None """ if self.high_score_type: self.high_score_label.text = "\n".join(data_obj["highscore_list"]) def evaluate_high_score(self): """ Check if the current score is higher than the existing high scores in the list, if it is then insert the current score into the list in the appropriate position. :return: None """ print("inside evaluate high score") saved_score_data = self.read_high_score_data() added_score = False if len(saved_score_data["highscore_list"]) > 0: for i, score in enumerate(saved_score_data["highscore_list"]): if ( int(score) < self.score and str(self.score) not in saved_score_data["highscore_list"] ): added_score = True saved_score_data["highscore_list"].insert(i, str(self.score)) if len(saved_score_data["highscore_list"]) > 4: saved_score_data["highscore_list"].pop() self.write_high_score_data(saved_score_data) break if ( not added_score and len(saved_score_data["highscore_list"]) < 4 and str(self.score) not in saved_score_data["highscore_list"] ): saved_score_data["highscore_list"].append(str(self.score)) self.write_high_score_data(saved_score_data) else: saved_score_data["highscore_list"].append(str(self.score)) self.write_high_score_data(saved_score_data) # pylint: disable=inconsistent-return-statements @property def game_mode_speed_adjustment(self): if self.current_game_mode == OctopusGame.GAME_MODE_A: return OctopusGame.GAME_MODE_A_SPEED_ADJUSTMENT elif self.current_game_mode == OctopusGame.GAME_MODE_B: return OctopusGame.GAME_MODE_B_SPEED_ADJUSTMENT class Octopus(Group): """ This class will contain all of the graphics and behavior for the Octopus tentacle segments. A tick() method will be called during the game.tick() and it will hide and show segments as needed to make the tentacles appear to extend and retract. It extends Group so it can be added to other Group's and shown on the display. """ # direction constant variables TENTACLE_DIRECTION_EXTENDING = 0 TENTACLE_DIRECTION_RETRACTING = 1 # tentacle 0 path option variables TENTACLE_0_PATH_A = 0 TENTACLE_0_PATH_B = 1 # action speed starting point, will be modified by score # larger value means slower speed BASE_TICK_DELAY = 0.75 # maximum action speed delay value # larger value means slower speed MAX_TICK_SPEED = BASE_TICK_DELAY - 0.35 # seconds # order that the tentacles will take actions TENTACLE_ORDER = [0, 2, 1, 3] def __init__(self): super().__init__() # timestamp of most recent action self.last_action_time = 0 # index of the tentacle currently moving self.current_tentacle_index = 0 # --- Set up tentacle segment images --- self.t0s0_bmp, self.t0s0_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_0_seg_0.bmp" ) self.t0s0_tilegrid = TileGrid(self.t0s0_bmp, pixel_shader=self.t0s0_palette) self.t0s0_palette.make_transparent(0) self.t0s0_tilegrid.x = 57 self.t0s0_tilegrid.y = 40 self.t0as1_bmp, self.t0as1_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_0a_seg_1.bmp" ) self.t0as1_tilegrid = TileGrid(self.t0as1_bmp, pixel_shader=self.t0as1_palette) self.t0as1_palette.make_transparent(0) self.t0as1_tilegrid.x = 47 self.t0as1_tilegrid.y = 43 self.t0as2_bmp, self.t0as2_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_0a_seg_2.bmp" ) self.t0as2_tilegrid = TileGrid(self.t0as2_bmp, pixel_shader=self.t0as2_palette) self.t0as2_palette.make_transparent(0) self.t0as2_tilegrid.x = 33 self.t0as2_tilegrid.y = 36 self.t0bs1_bmp, self.t0bs1_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_0b_seg_1.bmp" ) self.t0bs1_tilegrid = TileGrid(self.t0bs1_bmp, pixel_shader=self.t0bs1_palette) self.t0bs1_palette.make_transparent(0) self.t0bs1_tilegrid.x = 53 self.t0bs1_tilegrid.y = 50 self.t0bs2_bmp, self.t0bs2_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_0b_seg_2.bmp" ) self.t0bs2_tilegrid = TileGrid(self.t0bs2_bmp, pixel_shader=self.t0bs2_palette) self.t0bs2_palette.make_transparent(0) self.t0bs2_tilegrid.x = 49 self.t0bs2_tilegrid.y = 56 self.t0bs3_bmp, self.t0bs3_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_0b_seg_3.bmp" ) self.t0bs3_tilegrid = TileGrid(self.t0bs3_bmp, pixel_shader=self.t0bs3_palette) self.t0bs3_palette.make_transparent(0) self.t0bs3_tilegrid.x = 36 self.t0bs3_tilegrid.y = 69 self.t1s0_bmp, self.t1s0_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_1_seg_0.bmp" ) self.t1s0_tilegrid = TileGrid(self.t1s0_bmp, pixel_shader=self.t1s0_palette) self.t1s0_palette.make_transparent(0) self.t1s0_tilegrid.x = 72 self.t1s0_tilegrid.y = 51 self.t1s1_bmp, self.t1s1_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_1_seg_1.bmp" ) self.t1s1_tilegrid = TileGrid(self.t1s1_bmp, pixel_shader=self.t1s1_palette) self.t1s1_palette.make_transparent(0) self.t1s1_tilegrid.x = 71 self.t1s1_tilegrid.y = 61 self.t1as2_bmp, self.t1as2_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_1a_seg_2.bmp" ) self.t1as2_tilegrid = TileGrid(self.t1as2_bmp, pixel_shader=self.t1as2_palette) self.t1as2_palette.make_transparent(0) self.t1as2_tilegrid.x = 70 self.t1as2_tilegrid.y = 69 self.t1as3_bmp, self.t1as3_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_1a_seg_3.bmp" ) self.t1as3_tilegrid = TileGrid(self.t1as3_bmp, pixel_shader=self.t1as3_palette) self.t1as3_palette.make_transparent(0) self.t1as3_tilegrid.x = 70 self.t1as3_tilegrid.y = 78 self.t1as4_bmp, self.t1as4_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_1a_seg_4.bmp" ) self.t1as4_tilegrid = TileGrid(self.t1as4_bmp, pixel_shader=self.t1as4_palette) self.t1as4_palette.make_transparent(0) self.t1as4_tilegrid.x = 65 self.t1as4_tilegrid.y = 87 self.t1bs2_bmp, self.t1bs2_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_1b_seg_2.bmp" ) self.t1bs2_tilegrid = TileGrid(self.t1bs2_bmp, pixel_shader=self.t1bs2_palette) self.t1bs2_palette.make_transparent(0) self.t1bs2_tilegrid.x = 79 self.t1bs2_tilegrid.y = 71 self.t2s0_bmp, self.t2s0_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_2_seg_0.bmp" ) self.t2s0_tilegrid = TileGrid(self.t2s0_bmp, pixel_shader=self.t2s0_palette) self.t2s0_palette.make_transparent(0) self.t2s0_tilegrid.x = 94 self.t2s0_tilegrid.y = 66 self.t2s1_bmp, self.t2s1_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_2_seg_1.bmp" ) self.t2s1_tilegrid = TileGrid(self.t2s1_bmp, pixel_shader=self.t2s1_palette) self.t2s1_palette.make_transparent(0) self.t2s1_tilegrid.x = 95 self.t2s1_tilegrid.y = 75 self.t2s2_bmp, self.t2s2_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_2_seg_2.bmp" ) self.t2s2_tilegrid = TileGrid(self.t2s2_bmp, pixel_shader=self.t2s2_palette) self.t2s2_palette.make_transparent(0) self.t2s2_tilegrid.x = 98 self.t2s2_tilegrid.y = 80 self.t2s3_bmp, self.t2s3_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_2_seg_3.bmp" ) self.t2s3_tilegrid = TileGrid(self.t2s3_bmp, pixel_shader=self.t2s3_palette) self.t2s3_palette.make_transparent(0) self.t2s3_tilegrid.x = 99 self.t2s3_tilegrid.y = 88 self.t3s0_bmp, self.t3s0_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_3_seg_0.bmp" ) self.t3s0_tilegrid = TileGrid(self.t3s0_bmp, pixel_shader=self.t3s0_palette) self.t3s0_palette.make_transparent(0) self.t3s0_tilegrid.x = 119 self.t3s0_tilegrid.y = 72 self.t3s1_bmp, self.t3s1_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_3_seg_1.bmp" ) self.t3s1_tilegrid = TileGrid(self.t3s1_bmp, pixel_shader=self.t3s1_palette) self.t3s1_palette.make_transparent(0) self.t3s1_tilegrid.x = 119 self.t3s1_tilegrid.y = 80 self.t3s2_bmp, self.t3s2_palette = adafruit_imageload.load( "octopus_game_sprites/tentacle_3_seg_2.bmp" ) self.t3s2_tilegrid = TileGrid(self.t3s2_bmp, pixel_shader=self.t3s2_palette) self.t3s2_palette.make_transparent(0) self.t3s2_tilegrid.x = 120 self.t3s2_tilegrid.y = 87 self.append(self.t0s0_tilegrid) self.append(self.t0as1_tilegrid) self.append(self.t0as2_tilegrid) self.append(self.t0bs1_tilegrid) self.append(self.t0bs2_tilegrid) self.append(self.t0bs3_tilegrid) self.append(self.t1s0_tilegrid) self.append(self.t1s1_tilegrid) self.append(self.t1as2_tilegrid) self.append(self.t1as3_tilegrid) self.append(self.t1as4_tilegrid) self.append(self.t1bs2_tilegrid) self.append(self.t2s0_tilegrid) self.append(self.t2s1_tilegrid) self.append(self.t2s2_tilegrid) self.append(self.t2s3_tilegrid) self.append(self.t3s0_tilegrid) self.append(self.t3s1_tilegrid) self.append(self.t3s2_tilegrid) # --- End of tentacle segment initializations --- # Lists of segments for each tentacle self.tentacle_0a_list = [ self.t0s0_tilegrid, self.t0as1_tilegrid, self.t0as2_tilegrid, ] self.tentacle_0b_list = [ self.t0s0_tilegrid, self.t0bs1_tilegrid, self.t0bs2_tilegrid, self.t0bs3_tilegrid, ] self.tentacle_1_list = [ self.t1s0_tilegrid, self.t1s1_tilegrid, self.t1as2_tilegrid, self.t1as3_tilegrid, self.t1as4_tilegrid, ] self.tentacle_2_list = [ self.t2s0_tilegrid, self.t2s1_tilegrid, self.t2s2_tilegrid, self.t2s3_tilegrid, ] self.tentacle_3_list = [ self.t3s0_tilegrid, self.t3s1_tilegrid, self.t3s2_tilegrid, ] # list of the tentacles self.tentacles = [ self.tentacle_0a_list, self.tentacle_1_list, self.tentacle_2_list, self.tentacle_3_list, ] # list of directions for each tentacle self.tentacle_directions = [] # initialize all of the directions to extending self.tentacle_directions[:] = [Octopus.TENTACLE_DIRECTION_EXTENDING] * 4 # tentacle 0 path variable self.tentacle_0_path = Octopus.TENTACLE_0_PATH_A # list of current segment indexes for each tentacle self.tentacle_cur_indexes = [] # initialize segment indexes to -1 self.tentacle_cur_indexes[:] = [-1] * 4 # hide all of the segments to start with self.hide_all_segments() @property def current_tentacle(self): """ The tentacle that will move next :return: index of the current tentacle to move """ return Octopus.TENTACLE_ORDER[self.current_tentacle_index] def hide_all_segments(self): """ Hide all of the tentacle segments :return: None """ # loop over all segments in every tentacle for segment in ( self.tentacle_0a_list + self.tentacle_0b_list + self.tentacle_1_list + self.tentacle_2_list + self.tentacle_3_list ): # hide the current segment segment.hidden = True # reset all tentacle current indexes to -1 self.tentacle_cur_indexes[:] = [-1] * 4 # reset all tentacle directions to extending self.tentacle_directions[:] = [Octopus.TENTACLE_DIRECTION_EXTENDING] * 4 def tick(self, game_obj): """ Octopus tick() function called during game.tick(). Take turns extending and retracting each tentacle in the sequence dictated by ORDER. :param game_obj: The game object with context data and variables :return: None """ # timestamp to determine if it's time for an action to occur now = time.monotonic() _cur_tick_speed_delay = ( Octopus.BASE_TICK_DELAY - min(game_obj.score_speed_factor, Octopus.MAX_TICK_SPEED) ) - game_obj.game_mode_speed_adjustment #print( # f"cur tick speed: {_cur_tick_speed_delay} ajd: {game_obj.game_mode_speed_adjustment}" # f" mode: {game_obj.current_game_mode}" #) # Check if it's time for an action if self.last_action_time + _cur_tick_speed_delay <= now: # store the timestamp to compare against next iteration self.last_action_time = now # if we're moving tentacle 0 if self.current_tentacle == 0: # if tentacle 0 is extending if self.tentacle_directions[0] == Octopus.TENTACLE_DIRECTION_EXTENDING: # increment segment index self.tentacle_cur_indexes[0] += 1 # if we're on path A if self.tentacle_0_path == Octopus.TENTACLE_0_PATH_A: # if we're on the last segment if ( self.tentacle_cur_indexes[0] >= len(self.tentacle_0a_list) - 1 ): # change directions to retracting self.tentacle_directions[ 0 ] = Octopus.TENTACLE_DIRECTION_RETRACTING # show the current segment self.tentacle_0a_list[ self.tentacle_cur_indexes[0] ].hidden = False # if we're on path B elif self.tentacle_0_path == Octopus.TENTACLE_0_PATH_B: # if we're on the last segment if ( self.tentacle_cur_indexes[0] >= len(self.tentacle_0b_list) - 1 ): # change direction to retracting self.tentacle_directions[ 0 ] = Octopus.TENTACLE_DIRECTION_RETRACTING # show the current segment self.tentacle_0b_list[ self.tentacle_cur_indexes[0] ].hidden = False # if tentacle 0 is retracting elif ( self.tentacle_directions[0] == Octopus.TENTACLE_DIRECTION_RETRACTING ): # decrement the current segment index self.tentacle_cur_indexes[0] -= 1 # if the we're done with the first segment if self.tentacle_cur_indexes[0] < -1: # reset the index to the first segment self.tentacle_cur_indexes[0] += 1 # set the direction to extending self.tentacle_directions[ 0 ] = Octopus.TENTACLE_DIRECTION_EXTENDING # if we are currently on path A if self.tentacle_0_path == Octopus.TENTACLE_0_PATH_A: # change to path B self.tentacle_0_path = Octopus.TENTACLE_0_PATH_B else: # we are currently on path B # change to path A self.tentacle_0_path = Octopus.TENTACLE_0_PATH_A # if we're on path A if self.tentacle_0_path == Octopus.TENTACLE_0_PATH_A: # hide the current segment self.tentacle_0a_list[ self.tentacle_cur_indexes[0] + 1 ].hidden = True # if we're on path B elif self.tentacle_0_path == Octopus.TENTACLE_0_PATH_B: # hide the current segment self.tentacle_0b_list[ self.tentacle_cur_indexes[0] + 1 ].hidden = True else: # we're moving tentacle 1, 2, or 3 not tentacle 0 # current tentacle list that we're processing action for _cur_tentacle_list = self.tentacles[self.current_tentacle] # index of current tentacle _cur_tentacle_index = self.tentacle_cur_indexes[self.current_tentacle] # direction of this tentacle _cur_tentacle_direction = self.tentacle_directions[ self.current_tentacle ] # if the tentacle is extending if _cur_tentacle_direction == Octopus.TENTACLE_DIRECTION_EXTENDING: # increment the index of current segment self.tentacle_cur_indexes[self.current_tentacle] += 1 # if it's the last segment in the tentacle if ( self.tentacle_cur_indexes[self.current_tentacle] >= len(_cur_tentacle_list) - 1 ): # change the direction to retracting self.tentacle_directions[ self.current_tentacle ] = Octopus.TENTACLE_DIRECTION_RETRACTING # show the current segment _cur_tentacle_list[ self.tentacle_cur_indexes[self.current_tentacle] ].hidden = False # if the tentacle is retracting elif _cur_tentacle_direction == Octopus.TENTACLE_DIRECTION_RETRACTING: # decrement the segment index self.tentacle_cur_indexes[self.current_tentacle] -= 1 # if all segments have been retracted if self.tentacle_cur_indexes[self.current_tentacle] <= -1: # change direction to extending self.tentacle_directions[ self.current_tentacle ] = Octopus.TENTACLE_DIRECTION_EXTENDING # hide the current segment _cur_tentacle_list[ self.tentacle_cur_indexes[self.current_tentacle] + 1 ].hidden = True # increment tentacle index so we process the next segment next time self.current_tentacle_index += 1 # if this was the final tentacle if self.current_tentacle_index > 3: # reset the index back to the beginning self.current_tentacle_index = 0 class DiverPlayer(TileGrid): """ This class will contain the sprites and behavior for the player character. A spritesheet contains sprites for each state of the diver. The states change when for each location and based on whether It extends TileGrid so it can be added to a Group and shown on the display. """ # list of X,Y coordinate locations that the diver can move to on the map. DIVER_LOCATIONS = [(9, 29), (9, 73), (44, 91), (72, 90), (102, 94)] # Sprite indexes within the sprite sheet for non-treasure divers SPRITE_INDEXES_NO_TREASURE = [0, 2, 4, 6, 8] # Sprite indexes within the sprite sheet for divers with treasure SPRITE_INDEXES_WITH_TREASURE = [1, 3, 5, 7, 10] # Sprite indexes within the sprite sheet for the diver taking treasure SPRITE_INDEXES_TAKING_TREASURE = [10, 9, 11] # State machine index variables STATE_NO_TREASURE = 0 STATE_HAVE_TREASURE = 1 STATE_TAKING_TREASURE = 2 # Taking treasure animation delay TREASURE_ANIMATION_DELAY = 0.3 # seconds def __init__(self): # set up diver sprite sheet self._sprite_sheet_bmp, self._sprite_sheet_palette = adafruit_imageload.load( "octopus_game_sprites/diver_sprite_sheet_v2.bmp" ) # initialize super instance of TileGrid super().__init__( self._sprite_sheet_bmp, pixel_shader=self._sprite_sheet_palette, height=1, width=1, tile_width=29, tile_height=28, ) # set the transparent color index self._sprite_sheet_palette.make_transparent(0) # index of the sprite currently showing self.CUR_SPRITE_INDEX = 0 # index of the current location coordinate point self.CUR_LOCATION_INDEX = 0 # state machine current state variable self.CUR_STATE = DiverPlayer.STATE_NO_TREASURE # timestamp of last time the an animation sprite frame was shown self.last_treasure_animation_time = 0 # how much treasure the player is holding self.treasure_count = 0 # set the initial location and sprite self.update_location_and_sprite() def update_location_and_sprite(self): """ Update the current location and sprite of the diver based on the current indexe values. :return: None """ # set the x and y location of the diver. self.x = self.DIVER_LOCATIONS[self.CUR_LOCATION_INDEX][0] self.y = self.DIVER_LOCATIONS[self.CUR_LOCATION_INDEX][1] # check which state we're in currently if self.CUR_STATE == DiverPlayer.STATE_NO_TREASURE: # set the sprite index from the no treasure sprites self[0, 0] = DiverPlayer.SPRITE_INDEXES_NO_TREASURE[self.CUR_SPRITE_INDEX] elif self.CUR_STATE == DiverPlayer.STATE_HAVE_TREASURE: # set the sprite index from the sprites with treasure self[0, 0] = DiverPlayer.SPRITE_INDEXES_WITH_TREASURE[self.CUR_SPRITE_INDEX] elif self.CUR_STATE == DiverPlayer.STATE_TAKING_TREASURE: # set the sprite index for the start of the taking treasure animation print(f"CUR_SPRITE_INDEX: {self.CUR_SPRITE_INDEX}") print( f"TILE INDEX: {DiverPlayer.SPRITE_INDEXES_TAKING_TREASURE[self.CUR_SPRITE_INDEX]}" ) self[0, 0] = DiverPlayer.SPRITE_INDEXES_TAKING_TREASURE[ self.CUR_SPRITE_INDEX ] def move_forward(self): """ Move the diver forward one location on the map. If they're at the treasure chest then take a treasure from it and play the animation. :return: None """ # if we are note currently taking treasure if self.CUR_STATE != DiverPlayer.STATE_TAKING_TREASURE: # if we're not at the spot next to the treasure chest if self.CUR_LOCATION_INDEX <= 3: # increment location and sprite indexes self.CUR_SPRITE_INDEX += 1 self.CUR_LOCATION_INDEX += 1 # if we are at the spot next to the treasure elif self.CUR_LOCATION_INDEX == 4: # set the state machine current state variable self.CUR_STATE = DiverPlayer.STATE_TAKING_TREASURE # set the sprite index self.CUR_SPRITE_INDEX = 0 # increment treasure self.treasure_count += 1 # set the position and sprite based on the new indexes self.update_location_and_sprite() def move_backward(self): """ Move the player backward one location on the map. :return: None """ # if the current state is not taking treasure if self.CUR_STATE != DiverPlayer.STATE_TAKING_TREASURE: # if current location is next to the treasure chest if self.CUR_LOCATION_INDEX == 4: # decrement the location index self.CUR_LOCATION_INDEX -= 1 # Set the state according to whether the diver has treasure or not self.CUR_STATE = ( DiverPlayer.STATE_HAVE_TREASURE if self.treasure_count >= 0 else DiverPlayer.STATE_NO_TREASUREm ) # set the sprite index self.CUR_SPRITE_INDEX = 3 # if current location is not next to the boat elif self.CUR_LOCATION_INDEX > 0: # decrement location and sprite indexes self.CUR_LOCATION_INDEX -= 1 self.CUR_SPRITE_INDEX -= 1 # if current location is next to the boat else: # drop off treasure at boat print("drop off: {}".format(self.treasure_count)) # set the state to no treasure self.CUR_STATE = DiverPlayer.STATE_NO_TREASURE # set the position and sprite based on the new indexes self.update_location_and_sprite() def tick(self): """ Player tick function, called from game tick. Will process the taking treasure animation when needed. :return: None """ # if we're in the taking treasure state if self.CUR_STATE == DiverPlayer.STATE_TAKING_TREASURE: # store a timestamp to compare with now = time.monotonic() # if it's been long enough since the last animation frame if ( now >= self.last_treasure_animation_time + DiverPlayer.TREASURE_ANIMATION_DELAY ): # increment the sprite index self.CUR_SPRITE_INDEX += 1 # if we've shown all of the animation sprites if self.CUR_SPRITE_INDEX == len( DiverPlayer.SPRITE_INDEXES_TAKING_TREASURE ): # set the state to have treasure self.CUR_STATE = DiverPlayer.STATE_HAVE_TREASURE # set the sprite index to the correct one for this location with treasure self.CUR_SPRITE_INDEX = 4 # set the position and sprite based on the new indexes self.update_location_and_sprite() # store the timestamp to compare with next time self.last_treasure_animation_time = now