Adafruit_Learning_System_Gu.../CircuitPython_GameAndWatch_Octopus/octopus_game_helpers.py

1429 lines
53 KiB
Python

# 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