CircuitPython Setup for the PyBadge and PyGamer

First, to make sure you're running the most recent version of CircuitPython for the PyBadge or PyGamer:

 

Or

If you have an Edge Badge the process is the same as PyBadge, but use the button below to download the firmware file.

If you have a Pew Pew M4 the process is very similar as the others, but there is no button for the reset pin. Short the R  (reset) and - (GND) pins with a wire twice in quick succession to access the BOOT drive. Use the button below to download the firmware file.

Or

CircuitPython Libraries

You'll need a few CircuitPython libraries in the lib folder on the CIRCUITPY drive of your device for the code to work. Head to https://circuitpython.org/libraries to download the latest library bundle matching the major version of CircuitPython now on your board (5 for CircuitPython 5.x, etc.). The procedure is available in the Grand Central M4 Express guide.

Once you've downloaded the libraries bundle, add these library directories to the lib folder on the CIRCUITPY drive:

  • adafruit_display_text
  • adafruit_imageload

Tilegame Assets

Included with this project is a directory called tilegame_assets. You must copy this directory onto the CIRCUITPY drive. This contains the artwork, maps, and other modules needed for the game. 

With everything in place you drive should have these files on it:

It's okay if your device has other files as well, but it must have these ones at a minimum.

Project Files

To get all the code and files for this project, click Download: Project Zip in the code box below. Unzip them and place a copy of them on the CIRCUITPY drive of your device in the directories noted above.

# SPDX-FileCopyrightText: 2020 FoamyGuy for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import time
import random
import gc
import board
import displayio
import adafruit_imageload
import ugame
import terminalio
from adafruit_display_text import label
from tilegame_assets.tiles import TILES
from tilegame_assets.states import (
    STATE_PLAYING,
    STATE_MAPWIN,
    STATE_WAITING,
    STATE_LOST_SPARKY,
    STATE_MINERVA,
)
from tilegame_assets.fun_facts import FACTS
from tilegame_assets.text_helper import wrap_nicely

# pylint: disable=bad-continuation

# Direction constants for comparison
UP = 0
DOWN = 1
RIGHT = 2
LEFT = 3

# how long to wait between rendering frames
FPS_DELAY = 1 / 60

# how many tiles can fit on thes screen. Tiles are 16x16 pixels
SCREEN_HEIGHT_TILES = 8
SCREEN_WIDTH_TILES = 10

# list of maps in order they should be played
MAPS = ["map0.csv", "map1.csv"]

GAME_STATE = {
    # hold the map state as it came out of the csv. Only holds non-entities.
    "ORIGINAL_MAP": {},
    # hold the current map state as it changes. Only holds non-entities.
    "CURRENT_MAP": {},
    # Dictionary with touple keys that map to lists of entity objects.
    # Each one has the index of the sprite in the ENTITY_SPRITES list
    # and the tile type string
    "ENTITY_SPRITES_DICT": {},
    # hold the location of the player in tile coordinates
    "PLAYER_LOC": (0, 0),
    # list of items the player has in inventory
    "INVENTORY": [],
    # how many hearts there are in this map level
    "TOTAL_HEARTS": 0,
    # sprite object to draw for the player
    "PLAYER_SPRITE": None,
    # size of the map
    "MAP_WIDTH": 0,
    "MAP_HEIGHT": 0,
    # which map level within MAPS we are currently playing
    "MAP_INDEX": 0,
    # current state of the state machine
    "STATE": STATE_PLAYING,
}

# dictionary with tuple keys that map to tile type values
# e.x. {(0,0): "left_wall", (1,1): "floor"}
CAMERA_VIEW = {}

# how far offset the camera is from the GAME_STATE['CURRENT_MAP']
# used to determine where things are at in the camera view vs. the MAP
CAMERA_OFFSET_X = 0
CAMERA_OFFSET_Y = 0

# list of sprite objects, one for each entity
ENTITY_SPRITES = []

# list of entities that need to be on the screen currently based on the camera view
NEED_TO_DRAW_ENTITIES = []


def get_tile(coords):
    """
    :param coords: (x, y) tuple
    :return: tile name of the tile at the given coords from GAME_STATE['CURRENT_MAP']
    """
    return GAME_STATE["CURRENT_MAP"][coords[0], coords[1]]


def get_tile_obj(coords):
    """
    :param coords: (x, y) tuple
    :return: tile object with stats and behavior for the tile at the given coords.
    """
    return TILES[GAME_STATE["CURRENT_MAP"][coords[0], coords[1]]]


#
def is_tile_moveable(tile_coords):
    """
    Check the can_walk property of the tile at the given coordinates
    :param tile_coords: (x, y) tuple
    :return: True if the player can walk on this tile. False otherwise.
    """
    return TILES[GAME_STATE["CURRENT_MAP"][tile_coords[0], tile_coords[1]]]["can_walk"]


print("after funcs {}".format(gc.mem_free()))

# display object variable
display = board.DISPLAY

# Load the sprite sheet (bitmap)
sprite_sheet, palette = adafruit_imageload.load(
    "tilegame_assets/sprite_sheet.bmp",
    bitmap=displayio.Bitmap,
    palette=displayio.Palette,
)

# make green be transparent so entities can be drawn on top of map tiles
palette.make_transparent(0)

# Create the castle TileGrid
castle = displayio.TileGrid(
    sprite_sheet,
    pixel_shader=palette,
    width=10,
    height=8,
    tile_width=16,
    tile_height=16,
)

# Create a Group to hold the sprite and castle
group = displayio.Group()

# Add castle to the group
group.append(castle)


def load_map(file_name):
    # pylint: disable=global-statement,too-many-statements,too-many-nested-blocks,too-many-branches
    global ENTITY_SPRITES, CAMERA_VIEW

    # empty the sprites from the group
    for cur_s in ENTITY_SPRITES:
        group.remove(cur_s)
    # remove player sprite
    try:
        group.remove(GAME_STATE["PLAYER_SPRITE"])
    except ValueError:
        pass

    # reset map and other game state objects
    GAME_STATE["ORIGINAL_MAP"] = {}
    GAME_STATE["CURRENT_MAP"] = {}
    ENTITY_SPRITES = []
    GAME_STATE["ENTITY_SPRITES_DICT"] = {}
    CAMERA_VIEW = {}
    GAME_STATE["INVENTORY"] = []
    GAME_STATE["TOTAL_HEARTS"] = 0

    # Open and read raw string from the map csv file
    f = open("tilegame_assets/{}".format(file_name), "r")
    map_csv_str = f.read()
    f.close()

    # split the raw string into lines
    map_csv_lines = map_csv_str.replace("\r", "").split("\n")

    # set the WIDTH and HEIGHT variables.
    # this assumes the map is rectangular.
    GAME_STATE["MAP_HEIGHT"] = len(map_csv_lines)
    GAME_STATE["MAP_WIDTH"] = len(map_csv_lines[0].split(","))

    # loop over each line storing index in y variable
    for y, line in enumerate(map_csv_lines):
        # ignore empty line
        if line != "":
            # loop over each tile type separated by commas, storing index in x variable
            for x, tile_name in enumerate(line.split(",")):
                print("%s '%s'" % (len(tile_name), str(tile_name)))

                # if the tile exists in our main dictionary
                if tile_name in TILES.keys():

                    # if the tile is an entity
                    if (
                        "entity" in TILES[tile_name].keys()
                        and TILES[tile_name]["entity"]
                    ):
                        # set the map tiles to floor
                        GAME_STATE["ORIGINAL_MAP"][x, y] = "floor"
                        GAME_STATE["CURRENT_MAP"][x, y] = "floor"

                        if tile_name == "heart":
                            GAME_STATE["TOTAL_HEARTS"] += 1

                        # if it's the player
                        if tile_name == "player":
                            # Create the sprite TileGrid
                            GAME_STATE["PLAYER_SPRITE"] = displayio.TileGrid(
                                sprite_sheet,
                                pixel_shader=palette,
                                width=1,
                                height=1,
                                tile_width=16,
                                tile_height=16,
                                default_tile=TILES[tile_name]["sprite_index"],
                            )

                            # set the position of sprite on screen
                            GAME_STATE["PLAYER_SPRITE"].x = x * 16
                            GAME_STATE["PLAYER_SPRITE"].y = y * 16

                            # set position in x,y tile coords for reference later
                            GAME_STATE["PLAYER_LOC"] = (x, y)

                            # add sprite to the group
                            group.append(GAME_STATE["PLAYER_SPRITE"])
                        else:  # not the player
                            # Create the sprite TileGrid
                            entity_srite = displayio.TileGrid(
                                sprite_sheet,
                                pixel_shader=palette,
                                width=1,
                                height=1,
                                tile_width=16,
                                tile_height=16,
                                default_tile=TILES[tile_name]["sprite_index"],
                            )
                            # set the position of sprite on screen
                            # default to off the edge
                            entity_srite.x = -16
                            entity_srite.y = -16

                            # add the sprite object to ENTITY_SPRITES list
                            ENTITY_SPRITES.append(entity_srite)
                            # print("setting GAME_STATE['ENTITY_SPRITES_DICT'][%s,%s]" % (x,y))

                            # create an entity obj
                            _entity_obj = {
                                "entity_sprite_index": len(ENTITY_SPRITES) - 1,
                                "map_tile_name": tile_name,
                            }

                            # if there are no entities at this location yet
                            if (x, y) not in GAME_STATE["ENTITY_SPRITES_DICT"]:
                                # create a list and add it to the dictionary at the x,y location
                                GAME_STATE["ENTITY_SPRITES_DICT"][x, y] = [_entity_obj]
                            else:
                                # append the entity to the existing list in the dictionary
                                GAME_STATE["ENTITY_SPRITES_DICT"][x, y].append(
                                    _entity_obj
                                )

                    else:  # tile is not entity
                        # set the tile_name into MAP dictionaries
                        GAME_STATE["ORIGINAL_MAP"][x, y] = tile_name
                        GAME_STATE["CURRENT_MAP"][x, y] = tile_name

                else:  # tile type wasn't found in dict
                    print("tile: %s not found in TILES dict" % tile_name)
    # add all entity sprites to the group
    print("appending {} sprites".format(len(ENTITY_SPRITES)))
    for entity in ENTITY_SPRITES:
        group.append(entity)


print("loading map")
load_map(MAPS[GAME_STATE["MAP_INDEX"]])

# Add the Group to the Display
display.show(group)

# variables to store previous value of button state
prev_up = False
prev_down = False
prev_left = False
prev_right = False


# helper function returns true if player is allowed to move given direction
# based on can_walk property of the tiles next to the player
def can_player_move(direction):
    try:
        if direction == UP:
            tile_above_coords = (
                GAME_STATE["PLAYER_LOC"][0],
                GAME_STATE["PLAYER_LOC"][1] - 1,
            )

            return TILES[
                GAME_STATE["CURRENT_MAP"][tile_above_coords[0], tile_above_coords[1]]
            ]["can_walk"]

        if direction == DOWN:
            tile_below_coords = (
                GAME_STATE["PLAYER_LOC"][0],
                GAME_STATE["PLAYER_LOC"][1] + 1,
            )
            return TILES[
                GAME_STATE["CURRENT_MAP"][tile_below_coords[0], tile_below_coords[1]]
            ]["can_walk"]

        if direction == LEFT:
            tile_left_of_coords = (
                GAME_STATE["PLAYER_LOC"][0] - 1,
                GAME_STATE["PLAYER_LOC"][1],
            )
            return TILES[
                GAME_STATE["CURRENT_MAP"][
                    tile_left_of_coords[0], tile_left_of_coords[1]
                ]
            ]["can_walk"]

        if direction == RIGHT:
            tile_right_of_coords = (
                GAME_STATE["PLAYER_LOC"][0] + 1,
                GAME_STATE["PLAYER_LOC"][1],
            )
            return TILES[
                GAME_STATE["CURRENT_MAP"][
                    tile_right_of_coords[0], tile_right_of_coords[1]
                ]
            ]["can_walk"]
    except KeyError:
        return False

    return None


# set the appropriate tiles into the CAMERA_VIEW dictionary
# based on given starting coords and size
def set_camera_view(startX, startY, width, height):
    # pylint: disable=global-statement
    global CAMERA_OFFSET_X
    global CAMERA_OFFSET_Y
    # set the offset variables for use in other parts of the code
    CAMERA_OFFSET_X = startX
    CAMERA_OFFSET_Y = startY

    # loop over the rows and indexes in the desired size section
    for y_index, y in enumerate(range(startY, startY + height)):
        # loop over columns and indexes in the desired size section
        for x_index, x in enumerate(range(startX, startX + width)):
            # print("setting camera_view[%s,%s]" % (x_index,y_index))
            try:
                # set the tile at the current coordinate of the MAP into the CAMERA_VIEW
                CAMERA_VIEW[x_index, y_index] = GAME_STATE["CURRENT_MAP"][x, y]
            except KeyError:
                # if coordinate is out of bounds set it to floor by default
                CAMERA_VIEW[x_index, y_index] = "floor"


# draw the current CAMERA_VIEW dictionary and the GAME_STATE['ENTITY_SPRITES_DICT']
def draw_camera_view():
    # list that will hold all entities that have been drawn based on their MAP location
    # any entities not in this list should get moved off the screen
    drew_entities = []
    # print(CAMERA_VIEW)
    # pylint: disable=too-many-nested-blocks

    # loop over y tile coordinates
    for y in range(0, SCREEN_HEIGHT_TILES):
        # loop over x tile coordinates
        for x in range(0, SCREEN_WIDTH_TILES):
            # tile name at this location
            tile_name = CAMERA_VIEW[x, y]

            # if tile exists in the main dictionary
            if tile_name in TILES.keys():
                # if there are entity(s) at this location
                if (x + CAMERA_OFFSET_X, y + CAMERA_OFFSET_Y) in GAME_STATE[
                    "ENTITY_SPRITES_DICT"
                ]:
                    # default background for entities is floor
                    castle[x, y] = TILES["floor"]["sprite_index"]

                    # if it's not the player
                    if tile_name != "player":
                        # loop over all entities at this location
                        for entity_obj_at_tile in GAME_STATE["ENTITY_SPRITES_DICT"][
                            x + CAMERA_OFFSET_X, y + CAMERA_OFFSET_Y
                        ]:
                            # set appropriate x,y screen coordinates
                            # based on tile coordinates
                            ENTITY_SPRITES[
                                int(entity_obj_at_tile["entity_sprite_index"])
                            ].x = (x * 16)
                            ENTITY_SPRITES[
                                int(entity_obj_at_tile["entity_sprite_index"])
                            ].y = (y * 16)

                            # add the index of the entity sprite to the draw_entities
                            # list so we know not to hide it later.
                            drew_entities.append(
                                entity_obj_at_tile["entity_sprite_index"]
                            )

                else:  # no entities at this location
                    # set the sprite index of this tile into the CASTLE dictionary
                    castle[x, y] = TILES[tile_name]["sprite_index"]

            else:  # tile type not found in main dictionary
                # default to floor tile
                castle[x, y] = TILES["floor"]["sprite_index"]

            # if the player is at this x,y tile coordinate accounting for camera offset
            if GAME_STATE["PLAYER_LOC"] == ((x + CAMERA_OFFSET_X, y + CAMERA_OFFSET_Y)):
                # set player sprite screen coordinates
                GAME_STATE["PLAYER_SPRITE"].x = x * 16
                GAME_STATE["PLAYER_SPRITE"].y = y * 16

    # loop over all entity sprites
    for index in range(0, len(ENTITY_SPRITES)):
        # if the sprite wasn't drawn then it's outside the camera view
        if index not in drew_entities:
            # hide the sprite by moving it off screen
            ENTITY_SPRITES[index].x = int(-16)
            ENTITY_SPRITES[index].y = int(-16)


# variable to store timestamp of last drawn frame
last_update_time = 0

# variables to store movement offset values
x_offset = 0
y_offset = 0


def show_splash(new_text, color, vertical_offset=18):
    text_area.text = ""
    text_area.text = new_text
    text_area.anchor_point = (0, 0)
    text_area.anchored_position = (0, vertical_offset)
    text_area.color = color
    group.append(splash)


# Make the splash context
splash = displayio.Group()

# CircuitPython 6 & 7 compatible

# game message background bmp file
game_message_background = open("tilegame_assets/game_message_background.bmp", "rb")
odb = displayio.OnDiskBitmap(game_message_background)
bg_grid = displayio.TileGrid(odb, pixel_shader=getattr(odb, 'pixel_shader', displayio.ColorConverter()))

# # CircuitPython 7+ compatible
# game message background bmp file
# odb = displayio.OnDiskBitmap("tilegame_assets/game_message_background.bmp")
# bg_grid = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)

splash.append(bg_grid)

# Text for the message
text_group = displayio.Group(x=14, y=8)
text_area = label.Label(terminalio.FONT, text=" " * 180, color=0xD39AE5)
text_group.append(text_area)
splash.append(text_group)

# main loop
while True:
    # set the current button values into variables
    cur_btn_vals = ugame.buttons.get_pressed()
    cur_up = cur_btn_vals & ugame.K_UP
    cur_down = cur_btn_vals & ugame.K_DOWN
    cur_right = cur_btn_vals & ugame.K_RIGHT
    cur_left = cur_btn_vals & ugame.K_LEFT
    cur_a = cur_btn_vals & ugame.K_O or cur_btn_vals & ugame.K_X

    if GAME_STATE["STATE"] == STATE_WAITING:
        print(cur_a)
        if cur_a:
            GAME_STATE["STATE"] = STATE_PLAYING
            group.remove(splash)

    if GAME_STATE["STATE"] == STATE_PLAYING:
        # check for up button press / release
        if not cur_up and prev_up:
            if can_player_move(UP):
                x_offset = 0
                y_offset = -1

        # check for down button press / release
        if not cur_down and prev_down:
            if can_player_move(DOWN):
                x_offset = 0
                y_offset = 1

        # check for right button press / release
        if not cur_right and prev_right:
            if can_player_move(RIGHT):
                x_offset = 1
                y_offset = 0

        # check for left button press / release
        if not cur_left and prev_left:
            if can_player_move(LEFT):
                x_offset = -1
                y_offset = 0

        # if any offset is not zero then we need to process player movement
        if x_offset != 0 or y_offset != 0:
            # variable to store if player is allowed to move
            can_move = False

            # coordinates the player is moving to
            moving_to_coords = (
                GAME_STATE["PLAYER_LOC"][0] + x_offset,
                GAME_STATE["PLAYER_LOC"][1] + y_offset,
            )

            # tile name of the spot player is moving to
            moving_to_tile_name = GAME_STATE["CURRENT_MAP"][
                moving_to_coords[0], moving_to_coords[1]
            ]

            # if there are entity(s) at spot the player is moving to
            if moving_to_coords in GAME_STATE["ENTITY_SPRITES_DICT"]:
                print("found entity(s) where we are moving to")

                # loop over all entities at the location player is moving to
                for entity_obj in GAME_STATE["ENTITY_SPRITES_DICT"][
                    moving_to_coords
                ]:
                    print("checking entity %s" % entity_obj["map_tile_name"])
                    # if the entity has a before_move behavior function
                    if "before_move" in TILES[entity_obj["map_tile_name"]].keys():
                        print(
                            "calling before_move %s, %s, %s"
                            % (
                                moving_to_coords,
                                GAME_STATE["PLAYER_LOC"],
                                entity_obj,
                            )
                        )
                        # call the before_move behavior function act upon it's result
                        if TILES[entity_obj["map_tile_name"]]["before_move"](
                            moving_to_coords,
                            GAME_STATE["PLAYER_LOC"],
                            entity_obj,
                            GAME_STATE,
                        ):
                            # all the movement if it returned true
                            can_move = True
                        else:
                            # break and don't allow movement if it returned false
                            break
                    else:  # entity does not have a before_move function
                        # allow movement
                        can_move = True
                if can_move:
                    # set the player loc variable to the new coords
                    GAME_STATE["PLAYER_LOC"] = moving_to_coords

            else:  # no entities at the location player is moving to
                # set player loc variable to new coords
                GAME_STATE["PLAYER_LOC"] = moving_to_coords

        # reset movement offset variables
        y_offset = 0
        x_offset = 0

        # set previous button values for next iteration
        prev_up = cur_up
        prev_down = cur_down
        prev_right = cur_right
        prev_left = cur_left

        # current time
        now = time.monotonic()

        # if it has been long enough based on FPS delay
        if now > last_update_time + FPS_DELAY:
            # Set camera to 10x8 centered on the player
            # Clamped to (0, MAP_WIDTH) and (0, MAP_HEIGHT)
            set_camera_view(
                max(
                    min(
                        GAME_STATE["PLAYER_LOC"][0] - 4,
                        GAME_STATE["MAP_WIDTH"] - SCREEN_WIDTH_TILES,
                    ),
                    0,
                ),
                max(
                    min(
                        GAME_STATE["PLAYER_LOC"][1] - 3,
                        GAME_STATE["MAP_HEIGHT"] - SCREEN_HEIGHT_TILES,
                    ),
                    0,
                ),
                10,
                8,
            )
            # draw the camera
            draw_camera_view()
        # if player beat this map
        if GAME_STATE["STATE"] == STATE_MAPWIN:
            GAME_STATE["MAP_INDEX"] += 1
            # if player has beaten all maps
            if GAME_STATE["MAP_INDEX"] >= len(MAPS):
                GAME_STATE["MAP_INDEX"] = 0
                GAME_STATE["STATE"] = STATE_WAITING
                load_map(MAPS[GAME_STATE["MAP_INDEX"]])
                show_splash(
                    "You Win \n =D \nCongratulations. \nStart Over?", 0x29C1CF
                )
            else:
                # prompt to start next
                GAME_STATE["STATE"] = STATE_WAITING
                load_map(MAPS[GAME_STATE["MAP_INDEX"]])
                show_splash(
                    "You beat this level\n =D \nCongratulations. \nStart Next?",
                    0x29C1CF,
                )
        # game over from sparky
        elif GAME_STATE["STATE"] == STATE_LOST_SPARKY:
            GAME_STATE["MAP_INDEX"] = 0
            GAME_STATE["STATE"] = STATE_WAITING
            game_over_text = (
                "Be careful not to \ntouch Sparky unless \n"
                "you've collected \nenough Mho's.\nStarting Over"
            )
            load_map(MAPS[GAME_STATE["MAP_INDEX"]])
            show_splash(game_over_text, 0x25AFBB)

        # talking to minerva
        elif GAME_STATE["STATE"] == STATE_MINERVA:
            GAME_STATE["STATE"] = STATE_WAITING
            random_fact = random.choice(FACTS)
            minerva_txt = wrap_nicely("Minerva: {}".format(random_fact), 23)
            show_splash(minerva_txt, 0xD39AE5, 0)

        # store the last update time
        last_update_time = now

This guide was first published on Jun 10, 2020. It was last updated on Jun 10, 2020.

This page (CircuitPython Setup) was last updated on Oct 22, 2021.

Text editor powered by tinymce.