Development Livestreams

Embedded above is an episode of the Deep Dive weekly live stream program where I worked on the development of the Octopus game. If you'd like to see more of the development process there is a playlist on YouTube that contains all of the live streams during which I was working on the Octopus Game.

Code.py

The code.py script is intentionally very small and non-complex. It sets up the display and hardware button inputs using the keypad module. It then initializes an OctopusGame object and calls tick() inside the main loop to process each frame of the game. Inside the main loop, it also polls the button inputs and calls the appropriate hardware input event functions when needed. 

Helper Objects

The code for this game utilizes 3 high-level helper objects that each control certain aspects of the game. The intention of using these objects is to keep all of the game logic and behaviors contained within them, and keep the interface that code.py uses from them small so that our code.py file will turn out relatively small and very easy to understand. The plan for these objects was discussed briefly on the Planning Process page, but here we will dive a little deeper and explain each of these classes in more detail. Writing the code this way would also make it very easy to port to a different device even if the button inputs need to be set up differently.

OctopusGame

This is the highest level class; it represents the entire game as a whole. It will contain the DiverPlayer and Octopus object instances, and manage how and when they interact with each other. The OctopusGame class extends displayio.Group so it is able to be appended to other groups and shown on the display. The functionality in this class can be further broken down into 4 main ideas: Variable declarations, display helper functions, game logic helper functions, and hardware event input functions.

Variables and Constants

The beginning of the OctopusGame class contains many constant variables used for various aspects of the game. Most of them are "internal" and not meant to be changed by the player. But if you're interested in poking around with them to learn more you can try tweaking them to observe the results. Most of these are relatively mundane, storing the fraction of a second that the game will wait in between some animation or action, or an x,y location on the screen to place a label. Lots of the constants are state variables that are used by the state machine logic to represent different behavior states that the game can be in. 

One variable which some people may want to have some fun with is this:

# "Cheat" variable to make diver invincible for testing
INVINCIBLE = False

This variable gave me flashbacks to playing with an NES Game Genie as a young child. INVINCIBLE is a boolean variable that defaults to False, and under normal circumstances will stay False. If you want to use it, you must explicitly change the value to True. When it's enabled, it will prevent the diver from ever being hit or captured by the Octopus. This was very helpful while developing the game, and can be a fun way to play if you find the normal game too challenging and want an easier mode.

OctopusGame Display Functions & Objects

These function and objects are all about making visual things happen on the display. Some of them are for hiding and showing certain game elements, and others are for running or stopping animations. Here is a list and brief description of the display helper functions and objects:

  • update_extra_lives() - Update the divers showing in the boat, which represent the current number of extra lives.
  • show_caught_diver() - Make the diver that is captured by the octopus visible, and start its flailing arms and legs animation.
  • hide_caught_diver() - Make the captured diver invisible and stop the flailing animation.
  • deposit_treasure() - Show the leftmost diver in the boat and run the "pulling treasure out of water" animation.
  • update_high_score_text() - Update the high score label to reflect the list of scores passed as an argument.
  • score - This property can be used for getting and setting the players score. It will update the score label on the display, as well as apply some logic to give the players extra lives when they reach high enough scores.
  • bg_tilegrid- Tilegrid object used to show the background
  • extra_life_tilegrid_1 & 2 - Two Tilegrid objects, each representing an extra life for the player.
  • caught_diver_tilegrid - Tilegrid object for the caught diver flailing animation
  • boat_diver_tilegrid - Tilegrid object for the leftmost diver in the boat, shown when player is pulling up treasure into the boat.
  • score_lbl - Label to show the current score.
  • mode_lbl- Label to show the current game mode.
  • high_score_lbl - Label to show the high score values.

Octopus Game Logic Helper Functions

Several functions are used to manage and carryout the game logic. These don't directly affect what is showing on the display, but rather things like score, high scores, and delay times for animations or actions.

  • tick() - This function will process one frame of the game logic. It will be called quickly in the main loop of the program. State machine logic decides what behaviors will occur. It will call the tick() functions on the Octopus and DiverPlayer as needed. 
  • reset() - Function responsible for resetting the game state back to a new game.
  • lose_life() - Function that will process a loss of life for the player changing to GAME_OVER state if needed.
  • game_mode_speed_adjustment  - Property to get the game speed adjustment value based on current mode
  • initialize_high_score() - Set up for high score storage, creates a json file on the SDCard, or saves the initial object into NVM based on high_score_type that was set when the Game object was initialized.
  • read_high_score_data() - Read the current high score data from NVM, or SDCard file.
  • write_high_score_data() - Write a new list of high scores to either NVM or SDCard based on high_score_type.
  • evaluate_high_score() - Check if the current score is high enough to deserve a spot in  the high score list. If so, add it in the appropriate spot and call write_high_score_data() to save it.

Hardware Event Input Functions

These functions will get called from code.py when hardware input events happen (i.e. player presses buttons). This allows us to separate the game logic from the actual hardware polling. That way we can easily swap to different input configurations if we want.

  • left_button_press() - Move the player diver one spot to the left. Retrieving treasure if you have some and are at the boat. This game uses the PyGamers select button for left movement.
  • right_button_press() - Move the player diver one spot to the right. Take treasure if you're at the treasure chest.
  • a_button_press() - Used to set game mode to mode A before the game begins.
  • b_button_press() - Used to set game mode to mode B before the game begins.

Octopus

This class encapsulates all of the sprites and behavior for the Octopus. The Game object will create an instance of this and call octopus.tick() as needed. Octopus extends displayio.Group which allows it to be added to other Groups and shown on a display.

Octopus manages extending or retracting state variables for each tentacle in a list variable. When the tentacles are fully extended or retracted, the state is changed to the opposite one. Tentacle 0 also manages which one of the two possible paths the tentacle is currently using.

Tentacle Segment Tilegrids

The Octopus holds several Tilegrid objects, one for each segment of the four visible tentacles, including different tentacle paths for tentacles 0 and 1.

This diagram shows the numbering used for the tentacles.

Within the tentacles, each segment is also numbered starting at 0 for the segments closest to the Octopus's main body, and incrementing by 1 for each segment further away.

The Octopus class has these functions and properties to manage its appearance and behavior:

  • tick() - Called from OctopusGame.tick(), it will extend and retract the tentacles by hiding and showing the appropriate segments. The animation speed gets calculated and stored in the variable _cur_tick_speed_delay, it takes into account the game mode and the current score to determine speed.
  • current_tentacle - Returns the index of the next tentacle that will extended or retracted when the appropriate timeout has passed.
  • hide_all_segments() - Hide all of the segments so none are showing on the display any more. The main body of the Octopus is inside of the background image, so it remains visible. 

DiverPlayer

This class represents the player. It extends TileGrid so it can be added to displayio.Groups and shown on the display. 

The Bitmap image used for this TileGrid is a sprite sheet containing sprites for each different diver state that it can appear as. There is a pair of sprites for each location on the map, one holding treasure, and one without treasure, there are also 3 sprites for the taking treasure animation.

The DiverPlayer class provides high-level interface functions to move the diver backward and forward. These get called by the Game object from the hardware input events. As it's moved, it automatically updates the sprite to the appropriate one for the new location, and plays the taking treasure animation when needed. The locations are stored as a list of tuples, each containing an X/Y coordinate on the display. The current location index is stored in a variable and changed as needed.

These functions are used to control the player:

  • tick() - Called from OctopusGame.tick(). The only thing it does is run the taking-treasure animation if we're in the appropriate state for it. The rest of the behavior is carried out as a reaction to user input, rather than automatically during the tick loop.
  • move_forward() - Move the diver forward one location. Change the state to TAKING_TREASURE if diver is next to the treasure chest
  • move_backward() - Move the diver backward one location. Update the state as needed when the diver drops off treasure at the boat.
  • update_location_and_sprite() - Set the current location and sprite index on the TileGrid to move it to the correct place on the display and show the proper sprite.

This guide was first published on Aug 31, 2022. It was last updated on Jun 24, 2024.

This page (Code Walk-Through) was last updated on Mar 08, 2024.

Text editor powered by tinymce.