This project is a displayio based CircuitPython port of a classic Nintendo Game & Watch handheld game titled Octopus. It's made to run on PyGamer and PyBadge devices (including PyBadge LC and EdgeBadge).

The game centers around 3 divers trying to retrieve as much treasure as they can from a sunken ship. The player controls one diver at a time, moving them forward and backward to dive into the water, move across the seabed, grab treasure from the chest, and then retrace their watery steps back to the boat to haul in their catch. The treasure is guarded by a giant octopus with several outstretched tentacles waiting to snatch any diver who gets too close at the wrong time. 

The guide is intended to cover the general process of porting low-complexity games to CircuitPython, and Octopus is the specific one I've done. But the techniques and process shown can be used to implement other basic games, whether they were originally Nintendo Game & Watch, or anything else!

The graphics in the game rely heavily upon the principals in displayio. If you don't have any experience with it yet, it's probably good to have a look through the main displayio learn guide, then come back here to work on the game.

The majority of the project code was developed during live streams. This YouTube playlist contains the videos of those streams in chronological order if you'd like a more in depth look at the development process.

Parts

Adafruit PyGamer Starter Kit with PCB, enclosure, buttons, and storage bag
Please note: you may get a royal blue or purple case with your starter kit (they're both lovely colors)What fits in your pocket, is fully Open...
Out of Stock
Angled shot of Adafruit PyGamer for MakeCode Arcade, CircuitPython or Arduino.
What fits in your pocket, is fully Open Source, and can run CircuitPython, MakeCode Arcade or Arduino games you write yourself? That's right, it's the Adafruit...
Out of Stock
Top view of Adafruit EdgeBadge - Display reads "Test the TensorFlow lite voice model. Press and hold A button, say YES or NO, and see if the machine learned!".
Machine learning has come to the 'edge' - small microcontrollers that can run a very miniature version of TensorFlow Lite to do ML computations. But you don't...
$35.95
In Stock
Angled shot of a Adafruit PyBadge for MakeCode Arcade, CircuitPython, or Arduino.
What's the size of a credit card and can run CircuitPython, MakeCode Arcade or Arduino? That's right, its the Adafruit PyBadge! We wanted to see how much we...
Out of Stock
Angled shot of Adafruit PyBadge - Low Cost.
What's the size of a credit card and can run CircuitPython, MakeCode Arcade or Arduino even when you're on a budget? That's right, it's the Adafruit...
Out of Stock

Before we are ready to jump straight into coding, there are some things we can think about and try to plan out without writing a single line of code. This page covers the thought process I went through while making the Octopus game for CircuitPython. If you're a visual person, you could be making sketches or other illustrations during this process to get a feel for what "moving parts" your game will have. 

Select a game

Perhaps this goes without saying, but you need to decide on which game you want to port to CircuitPython. Or, if you're feeling extra creative, you could invent a new one! I looked through the original classic Game & Watch games and Octopus was the first one that caught my attention for more than a quick glance. I may still go back and port more of the Game & Watch games to CircuitPython, but the following were the main reasons I went with this one for the learn guide:

  • It has relatively basic graphics -- most things that need to be drawn can be either a bitmap image file that is hidden or shown, or a single tile within a sprite sheet of evenly sized sprites.
  • Sprite assets were available online. They'll require some modification and format conversion, but I don't think I'll need to make anything from scratch.
  • The gameplay is not super complex -- the user has two main inputs: Left and Right. The only NPC entity is the Octopus with its extending and retracting tentacles.

If it's your first time porting a game or you don't have much experience with CircuitPython displayio yet, consider some of these things when deciding on the game to port. If you have more experience already, you could aim for something more advanced.

Assess the Assets

Graphics

CircuitPython displayio graphics are primarily based around Bitmaps. Now is a good time to take stock of what assets are available for your game and what size and format those graphics are. This will give you an idea of what preparations will be required for your assets. You can also start making a first guess about how the graphics could be implemented. The primary ways to show graphics with displayio are:

  • ImageLoad library to load a .bmp file into a Bitmap object in memory, then show it with a TileGrid. Loading the Bitmap into memory gives you the option to manipulate it before or as you show it. If you anticipate needing to change the pixels within your graphics from code, then this is the best option. However Bitmaps do consume precious RAM, so if you don't need this level of control, other options may be better. This is great for small graphics that need to be changed somewhat frequently or arbitrarily. For animations or different visual states you can use a sprite sheet with the TileGrid to change between different sprites easily.
  • OnDiskBitmap to load a .bmp file directly to the display without storing it in memory. This means you cannot manipulate the pixels within the Bitmap. It can still be shown on the display and it will not consume as much RAM, so you can load larger images and not risk running out! These are great for backgrounds and other larger images that will remain static or only change infrequently.
  • Basic shapes with vectorio or display_shapes library. If you need to draw circles, rectangles or other polygons, there are various shape helper classes in these modules. These are good for making borders or backgrounds of panels or other graphics. These are solid-fill only, so if you want gradients or anything more complex than single-color fill, you'll need to use a Bitmap instead.

Don't feel stuck with a particular choice at this point, because there is still time to make adjustments. Just try to get a first guess at which of these makes sense for the different graphics that your game will utilize. If you have lots of different graphical components and aren't sure how to display them, consider making smaller self-contained test scripts to render a small subset of the graphics. You can try out different methods to see which ones are best suited for the graphics in your game.

For the Octopus game, I knew I needed to show and hide lots of individual tentacle segments and I wasn't sure if I'd want to change pixels within them -- but I wanted to keep the option open if possible. I wanted to verify before I got too far along whether I would have enough RAM on the PyGamer for them to be loaded into memory. In order to check, I cut out one segment and converted it to a .bmp file. I made a test script to load the same segment several times over with the help of a for loop. I set it to load more than I would actually end up needing for the game, which ensured that there was plenty of room left over. I loaded the background and a few other sprites to represent different graphics for the game all in the same script to ensure everything would fit together. Once I was sure we'd have enough RAM with this plan, I moved on to processing the rest of the game assets.

My plan for graphics is as follows:

  • Background scene and octopus main body will all exist in a single bitmap file that is the same size as the target display, and will be shown with OnDiskBitmap. There are two versions of the background scene that we can allow the user to switch between.
  • Player's diver character will be loaded with Imageload library and will use a sprite sheet that has all of the different visual states needed.
  • Octopus tentacle segments will each be loaded individually with the Imageload library.
  • Extra life indicator divers on the boat will be OnDiskBitmaps hidden and shown as needed to show the current extra lives. 
  • Treasure-retrieving diver on the boat will be loaded with Imageload library and use a spritesheet with the different frames of the animation to pull treasure up.
  • Caught-flailing diver will be loaded with Imageload and will use a sprite sheet that has each frame of the flailing animation. 

Sounds

CircuitPython has support for .wav and low-bitrate .mp3 files. There is also RawSample which allows you to make beeps, boops and other synth-like sounds. Similarly to the graphics, it's a good idea to try to make a first guess about how your game's sounds can be played. If you're unsure, you can make proof-of-concept scripts to try out the different types, and get a feel for APIs.

Planning Classes

To make your code easy to understand and develop, it is best to break things down into classes that encapsulate the behavior and graphics for your game elements. The exact way that it will break down is going to depend a lot on your specific game and what graphics and behaviors it has. Remember as well that we are still in the planning stage -- we're not committed to any specific design yet, and as you develop the game, it may still make sense to break out additional classes, or even to collapse and combine some of the ones you initially planned. This process is a bit of an art form, and requires practice. Don't get discouraged if your classes don't end up being perfect, because you can always tweak and mold things over time to add different behaviors as needed. Ideally you'll want to break it down in such a way that you'll have high-level variables with descriptively-named functions that make it easy to understand and follow the logic of the game while reading the code. 

For the Octopus game, I had the following ideas for classes:

  • OctopusGame - A high-level object that orchestrates and manages the entire game. It has functions for each hardware input event that the player can cause. It holds instances of the other classes. The Game class should have a tick() function that will get called from the main loop. In my game, tick() is called very quickly over and over. The game object keeps track of time and has a built-in delay to decide when the next action should occur within the game. 
  • DiverPlayer - A spritesheet-based bitmap image that manages its own location on the screen and currently-showing sprite. It has simple action functions that the Game object will call when the user inputs actions, and these will control the behavior of the player character in my game.
  • Octopus - Holds several bitmap images representing the octopus tentacle segments. This class manages the hiding and showing of the tentacle segments over time as the game is being played.

These classes can cover the majority of the more complex behaviors in the Octopus game, but there are a few simpler elements that deserve honorable mention.

Basic Elements

These don't have very complex behaviors and therefore don't need custom classes created for them -- the built-in displayio objects will do just fine. These will get created and managed by the Game class. Any elements in your game that need to hide/show, move around, or change sprites (but nothing more complex) could be good candidates for basic elements.

  • background - An OnDiskBitmap to show the background scene. Could support different backgrounds if we want.
  • caught_diver - A TileGrid that uses a spritesheet with a few sprites that represent the animation of the diver that is flailing its arms and legs when caught by the octopus.
  • boat_diver - A TileGrid that uses a spritesheet with 2 sprites showing the two states of pulling treasure out of the water and into the boat.
  • extra_life - There are two extra_life TileGrids that each show a static BMP image. These are hidden and shown as needed to indicate the players remaining. They're positioned in the boat near the top left. 
  • score_label - A display_text Label to show the player's current score.

CircuitPython displayio requires indexed bitmap graphics. Depending on the assets available, you may need to convert images to this format as well as crop images down and combine them into equally-sized sprite sheets. In this page, I'll go over the commonly-needed graphics manipulations.

Many photo editing software applications can handle these manipulations. One such free tool is GIMP Image Editor, which is what I used to prepare the Octopus game graphics.

This YouTube video shows my process for preparing some of the graphics.

Cropping Sprites

I needed the tentacle segments to be cut out and placed into individual .bmp files. GIMP provides the rectangle select tool for this purpose. Select the rectangle select tool and then drag a box around the thing you want to cut out. Adjust the sides as needed to contain exactly the element you want to cut out. 

Once you've got your selection made, press ctrl-C to copy the selection.

Then press ctrl-shift-V or click Edit -> Paste As -> New Image to paste it into a new file.

Now you can make any required touch ups on this element then convert it to indexed and save it as in the .bmp file format. For the Octopus tentacle segments, I needed to erase a bit in the corner of a different segment. I also changed the background to my transparency green and scaled the image to fit the aspect ratio for my target display size.

Scaling Graphics

The graphics that I found were not quite the same size as the target display I'm making the game for, so I had to scale them to match the target display size. I started with the background, it was 240x160 pixels. The PyGamer's display is 160x128 pixels so we need to scale down some. To find out how much, I divided the pixel sizes to find the scale factor.

160/240 = 0.6666667  Horizontal Scale Factor

128/160 = 0.8  Vertical Scale Factor

So my graphics need to get scaled down to 66.6% size width, and 80% size height. 

After cropping the graphics, I multiplied the size of the cropped image by these scale factors to find the final size of each graphic that will get used in the game. Once you have calculated the new size, the resize tool in GIMP can be used to make the change.

Select the scale tool, then click anywhere in your graphic. In the scale dialog that appears, ensure that the height and width are not linked, and enter the new values for height and width that you calculated. In some cases, I had to round up or down to the closest whole number for my scaled graphics. Once the new values are entered, click the Scale button.

Making Sprite Sheets

In cases where you have multiple sprites of the same size that are used within the same element in the game, you can combine all of the graphics into a sprite sheet instead of having individual graphics broken out. In the Octopus game, the DiverPlayer character is a good candidate for a sprite sheet because there are several different sprites used at different times throughout gameplay. All of them are roughly the same size, so I combined them into a sprite sheet and use a TileGrid to manage which sprite from the sheet is showing at any given time.

In CircuitPython, all tiles in a sprite sheet must have the same size. The full sheet image should be a multiple of the height and width of the tile size. Note that height and width can be different from each other, they just need to be consistent across tiles, the width of all tiles must be equal.

For the DiverPlayer sprite sheet, the sprites are 29x28 pixels and the sprite sheet is laid out as 3 rows by 4 columns. So the total size of the sheet comes out to 116x84 pixels. There is one unused slot in this sprite sheet.

I like to paste each sprite on its own layer within the GIMP project. I save the GIMP project .xcf file with all of these layers broken out so they are easy to update independently from each other later on if needed. When you're ready to export, you can keep an extra copy of your .xcf project file, and then flatten the image to a single layer with Image -> Flatten Image. Then you'll be ready to export it as an indexed bitmap.

Converting to Indexed Bitmap

If the graphics you find are PNG or JPG, you'll need to convert them to indexed bitmaps. Open your image in GIMP and then click Image -> Mode -> Indexed.

The default configuration for the conversion is sufficient in most cases. If your image contains more than 255 different colors, then it will have to remove some of the colors and change them to the closest remaining color. If you've got less than 255 different colors, then it won't really look any different.

Transparency Color Index

Indexed BMPs do not support transparency in the same way that PNG images do. PNGs can contain transparent regions which are typically visually represented by a light and dark grey checkerboard pattern. For our indexed BMP graphics, we can select one or more colors in the palette to not get drawn when the display renders. This has the visual effect of making anything in the image that are those colors to be transparent from the users perspective. Any color(s) can be used; some game sprites use a bright pink for this purpose. I've chosen to use bright green for my bitmap graphics because I think of it similarly to green screen graphics. Try to choose a color that has easy visual contrast with the rest of the colors used by your asset; that will make it easier to imagine at a glance how your asset will appear when rendered in the game.

The pencil and paint bucket fill tools in GIMP are helpful for coloring in the necessary sections of your image with the chosen transparency color.

When you've got your transparent regions filled in with the selected transparency color, you can re-arrange the color map to move your transparency color the beginning of the list. That way, your code can remain the same from project to project.

To do so, click on Color -> Map -> Rearrange Colormap

Then drag the transparency colors all the way to the beginning of the list. In this graphic, there are only 2 colors used, but yours may have more.

In the dialog that appears, you can drag and drop the color swatches to change their position within the list. The index of the color is shown below the swatch.

The indexes do not update live as you drag things around. So even after you drag your transparent color(s) to the first index in the list, they'll still have a higher index number showing. This is fine, it's just how GIMP works. 

If you are ever unsure, you can click OK to save and close the dialog and then re-open it with the same process as before. When it opens, it will load the actual current index numbers.

Export As Bitmap

Once your graphics are prepared, you can export them as a bitmap file.

Click File -> Export As or press ctrl-shift-E

In the export dialog, ensure that "Select File Type (By Extension)" is checked. Navigate to the location you want to save your image. It's best to keep a copy of the project stored on your PC and then copy it from there to your CIRCUITPY drive. Enter a filename to save the image, and be sure to use .bmp as the extension at the end of your file.

Once you've chosen the directory and named your bitmap file, you can press the "Export" button.

Are you new to using CircuitPython? No worries, there is a full getting-started guide here.

Plug the PyGamer into your computer with a known good USB cable (not a charge-only cable). The PyGamer will appear to your computer as a flash drive named CIRCUITPY. If the drive does not appear, you can install CircuitPython on your PyGamer and then return here.

Download the project files with the Download Project Bundle button below. Unzip the file and copy/paste the code.py and other project files to your CIRCUITPY drive using File Explorer or Finder (depending on your operating system).

Drive Structure

After copying the files, your drive should look like the listing below. It can contain other files as well, but must contain these at a minimum:

Drive

High Score Configuration

The game supports different configurations for high score. It can be disabled entirely, or it can be enabled and store data with either NVM, or SDCard storage. To configure it uncomment the appropriate version of the constructor call with configuration argument.

# create instance of OctopusGame
octopus_game = OctopusGame()

# uncomment this instead, to use NVM highscore
#octopus_game = OctopusGame(high_score_type=OctopusGame.HIGH_SCORE_NVM)

# uncomment this instead, to use SDCard highscore
#octopus_game = OctopusGame(high_score_type=OctopusGame.HIGH_SCORE_SDCARD)

Code

The project code.py is shown below:

# SPDX-FileCopyrightText: 2022 Tim C, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import board
import keypad
from displayio import Group
from octopus_game_helpers import OctopusGame

# built-in display
display = board.DISPLAY

# display.brightness = 0.3

# main group that we'll show in the display
main_group = Group()

# create instance of OctopusGame, high score disabled
octopus_game = OctopusGame()

# uncomment this instead, to use NVM highscore
#octopus_game = OctopusGame(high_score_type=OctopusGame.HIGH_SCORE_NVM)

# uncomment this instead, to use SDCard highscore
#octopus_game = OctopusGame(high_score_type=OctopusGame.HIGH_SCORE_SDCARD)


# add octopus game to main group
main_group.append(octopus_game)

# initialize the shiftregister keys to read hardware buttons
buttons = keypad.ShiftRegisterKeys(
    clock=board.BUTTON_CLOCK,
    data=board.BUTTON_OUT,
    latch=board.BUTTON_LATCH,
    key_count=4,
    value_when_pressed=True,
)

# show the main group on the display
display.show(main_group)

# main loop
while True:

    # get event from hardware buttons
    event = buttons.events.get()

    # if anything is pressed
    if event:

        # if the event is for the start button
        if event.key_number == 2:
            # if it's a pressed event
            if event.pressed:
                # trigger the right button press action function
                octopus_game.right_button_press()

        # if the event is for the select button
        elif event.key_number == 3:
            # if it's a pressed event
            if event.pressed:
                # trigger the left button press action function
                octopus_game.left_button_press()

        # if the event is for the b button
        elif event.key_number == 0:
            # if it's a pressed event
            if event.pressed:
                # trigger the b button press action function
                octopus_game.b_button_press()

        # if the event is for the a button
        elif event.key_number == 1:
            # if it's a pressed event
            if event.pressed:
                # trigger the a button press action function
                octopus_game.a_button_press()

    # call the game tick function
    octopus_game.tick()

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 Jul 11, 2022.