As games go, Dragon Drop is rudimentary…it’s more about presenting some starter code for your own project ideas, in a medium-size program. I was surprised how well CircuitPython could handle game animation on the little OLED screen.

CircuitPython 7.0.0 alpha 6 fixes some “scratchy” audio issues. If using an earlier version, it’s worth upgrading!

Text Editor

If you just want to try out the game, skip ahead to the “Download the Project Bundle” section below.

If you plan to get into the source code to customize gameplay or learn more about it, Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.

Alternatively, you can use any text editor that saves simple text files.

Download the Project Bundle

Your project will use a specific set of CircuitPython libraries and the code.py file, along with a folder of graphics and sound files. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.

Make backups of any files on the board you want to keep, then drag the contents of the uncompressed bundle directory onto your MACROPAD board's CIRCUITPY drive, replacing any existing files or directories with the same names.

# SPDX-FileCopyrightText: 2021 Phillip Burgess for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
Dragon Drop: a simple game for Adafruit MACROPAD. Uses OLED display in
portrait (vertical) orientation. Tap one of four keys across a row to
catch falling eggs before they hit the ground. Avoid fireballs.
"""

# pylint: disable=import-error, unused-import
import gc
import random
import time
import displayio
import adafruit_imageload
from adafruit_macropad import MacroPad
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
from adafruit_progressbar.progressbar import HorizontalProgressBar
import board      # These three can be removed
import audiocore  # if/when MacroPad library
import audiopwmio # adds background audio


# CONFIGURABLES ------------------------

MAX_EGGS = 7          # Max count of all projectiles; some are fireballs
PATH = '/dragondrop/' # Location of graphics, fonts, WAVs, etc.


# UTILITY FUNCTIONS AND CLASSES --------

def background_sound(filename):
    """ Start a WAV file playing in the background (non-blocking). This
        func can be removed if/when MacroPad lib gets background audio. """
    # pylint: disable=protected-access
    macropad._speaker_enable.value = True
    audio.play(audiocore.WaveFile(open(PATH + filename, 'rb')))

def show_screen(group):
    """ Activate a given displayio group, pause until keypress. """
    macropad.display.show(group)
    macropad.display.refresh()
    # Purge any queued up key events...
    while macropad.keys.events.get():
        pass
    while True: # ...then wait for first new key press event
        key_event = macropad.keys.events.get()
        if key_event and key_event.pressed:
            return

# pylint: disable=too-few-public-methods
class Sprite:
    """ Class holds sprite (eggs, fireballs) state information. """
    def __init__(self, col, start_time):
        self.column = col                       # 0-3
        self.is_fire = (random.random() < 0.25) # 1/4 chance of fireballs
        self.start_time = start_time            # For drop physics
        self.paused = False


# ONE-TIME INITIALIZATION --------------

macropad = MacroPad(rotation=90)
macropad.display.auto_refresh = False
macropad.pixels.auto_write = False
macropad.pixels.brightness = 0.5
audio = audiopwmio.PWMAudioOut(board.SPEAKER) # For background audio

font = bitmap_font.load_font(PATH + 'cursive-smart.pcf')

# Create 3 displayio groups -- one each for the title, play and end screens.

title_group = displayio.Group()
title_bitmap, title_palette = adafruit_imageload.load(PATH + 'title.bmp',
                                                      bitmap=displayio.Bitmap,
                                                      palette=displayio.Palette)
title_group.append(displayio.TileGrid(title_bitmap, pixel_shader=title_palette,
                                      width=1, height=1,
                                      tile_width=title_bitmap.width,
                                      tile_height=title_bitmap.height))

# Bitmap containing eggs, hatchling and fireballs
sprite_bitmap, sprite_palette = adafruit_imageload.load(
    PATH + 'sprites.bmp', bitmap=displayio.Bitmap, palette=displayio.Palette)
sprite_palette.make_transparent(0)

play_group = displayio.Group()
# Bitmap containing five shadow tiles ('no shadow' through 'max shadow')
shadow_bitmap, shadow_palette = adafruit_imageload.load(
    PATH + 'shadow.bmp', bitmap=displayio.Bitmap, palette=displayio.Palette)
# Tilegrid with four shadow tiles; one per column
shadow = displayio.TileGrid(shadow_bitmap, pixel_shader=shadow_palette,
                            width=4, height=1, tile_width=16,
                            tile_height=shadow_bitmap.height, x=0,
                            y=macropad.display.height - shadow_bitmap.height)
play_group.append(shadow)
shadow_scale = 5 / (macropad.display.height - 20) # For picking shadow sprite
life_bar = HorizontalProgressBar((0, 0), (macropad.display.width, 7),
                                 value=100, min_value=0, max_value=100,
                                 bar_color=0xFFFFFF, outline_color=0xFFFFFF,
                                 fill_color=0, margin_size=1)
play_group.append(life_bar)
# Score is last object in play_group, can be indexed as -1
play_group.append(label.Label(font, text='0', color=0xFFFFFF,
                              anchor_point=(0.5, 0.0),
                              anchored_position=(macropad.display.width // 2,
                                                 10)))

end_group = displayio.Group()
end_bitmap, end_palette = adafruit_imageload.load(
    PATH + 'gameover.bmp', bitmap=displayio.Bitmap, palette=displayio.Palette)
end_group.append(displayio.TileGrid(end_bitmap, pixel_shader=end_palette,
                                    width=1, height=1,
                                    tile_width=end_bitmap.width,
                                    tile_height=end_bitmap.height))
end_group.append(label.Label(font, text='0', color=0xFFFFFF,
                             anchor_point=(0.5, 0.0),
                             anchored_position=(macropad.display.width // 2,
                                                90)))


# MAIN LOOP -- alternates play and end-game screens --------

show_screen(title_group) # Just do this once on startup

while True:

    # NEW GAME -------------------------

    sprites = []
    score = 0
    play_group[-1].text = '0' # Score text
    life_bar.value = 100
    audio.stop()
    macropad.display.show(play_group)
    macropad.display.refresh()
    start = time.monotonic()

    # PLAY UNTIL LIFE BAR DEPLETED -----

    while life_bar.value > 0:
        now = time.monotonic()
        speed = 10 + (now - start) / 30   # Gradually speed up
        fire_sprite = 3 + int((now * 6) % 2.0) # For animating fire

        # Coalese any/all queued-up keypress events per column
        column_pressed = [False] * 4
        while True:
            event = macropad.keys.events.get()
            if not event:
                break
            if event.pressed:
                column_pressed[event.key_number % 4] = True

        # For determining upper/lower extents of active egg sprites per column
        column_min = [macropad.display.height] * 4
        column_max = [0] * 4

        # Traverse sprite list backwards so we can pop() without index problems
        for i in range(len(sprites) - 1, -1, -1):
            sprite = sprites[i]
            tile = play_group[i + 1] # Corresponding 1x1 TileGrid for sprite
            column = sprite.column
            elapsed = now - sprite.start_time # Time since add or pause event

            if sprite.is_fire:
                tile[0] = fire_sprite # Animate all flame sprites

            if sprite.paused:                # Sprite at bottom of screen
                if elapsed > 0.75:           # Hold position for 3/4 second,
                    for x in range(0, 9, 4): # then LEDs off,
                        macropad.pixels[x + sprite.column] = (0, 0, 0)
                    sprites.pop(i)           # and delete Sprite object and
                    play_group.pop(i + 1)    # element from displayio group
                    continue
                if not sprite.is_fire:
                    column_max[column] = max(column_max[column],
                                             macropad.display.height - 22)
            else: # Sprite in motion
                y = speed * elapsed * elapsed - 16
                # Track top of all sprites, bottom of eggs only
                column_min[column] = min(column_min[column], y)
                if not sprite.is_fire:
                    column_max[column] = max(column_max[column], y)
                tile.y = int(y) # Sprite's vertical pos. in play_group

                # Handle various catch or off-bottom actions...
                if sprite.is_fire:
                    if y >= macropad.display.height: # Off bottom of screen,
                        sprites.pop(i)               # remove fireball sprite
                        play_group.pop(i + 1)
                        continue
                    if y >= macropad.display.height - 40:
                        if column_pressed[column]:
                            # Fireball caught, ouch!
                            background_sound('sizzle.wav') # I smell bacon
                            sprite.paused = True
                            sprite.start_time = now
                            tile.y = macropad.display.height - 20
                            life_bar.value = max(0, life_bar.value - 5)
                            for x in range(0, 9, 4):
                                macropad.pixels[x + sprite.column] = (255, 0, 0)
                else: # Is egg...
                    if y >= macropad.display.height - 22:
                        # Egg hit ground
                        background_sound('splat.wav')
                        sprite.paused = True
                        sprite.start_time = now
                        tile.y = macropad.display.height - 22
                        tile[0] = 1 # Change sprite to broken egg
                        life_bar.value = max(0, life_bar.value - 5)
                        macropad.pixels[8 + sprite.column] = (255, 255, 0)
                    elif column_pressed[column]:
                        if y >= macropad.display.height - 40:
                            # Egg caught at right time
                            background_sound('rawr.wav')
                            sprite.paused = True
                            sprite.start_time = now
                            tile.y = macropad.display.height - 22
                            tile[0] = 2 # Hatchling
                            macropad.pixels[4 + sprite.column] = (0, 255, 0)
                            score += 10
                            play_group[-1].text = str(score)
                        elif y >= macropad.display.height - 58:
                            # Egg caught too early
                            background_sound('splat.wav')
                            sprite.paused = True
                            sprite.start_time = now
                            tile.y = macropad.display.height - 40
                            tile[0] = 1 # Broken egg
                            life_bar.value = max(0, life_bar.value - 5)
                            macropad.pixels[sprite.column] = (255, 255, 0)

        # Select shadow bitmaps based on each column's lowest egg
        for i in range(4):
            shadow[i] = min(4, int(column_max[i] * shadow_scale))

        # Time to introduce a new sprite? 1/20 chance each frame, if space
        if (len(sprites) < MAX_EGGS and random.random() < 0.05 and
                max(column_min) > 16):
            # Pick a column randomly...if it's occupied, keep trying...
            while True:
                column = random.randint(0, 3)
                if column_min[column] > 16:
                    # Found a clear spot. Add sprite and break loop
                    sprites.append(Sprite(column, now))
                    play_group.insert(-2, displayio.TileGrid(sprite_bitmap,
                                                             pixel_shader=sprite_palette,
                                                             width=1, height=1,
                                                             tile_width=16,
                                                             tile_height=sprite_bitmap.height,
                                                             x=column * 16,
                                                             y=-16))
                    break

        macropad.display.refresh()
        macropad.pixels.show()
        if not audio.playing:
            # pylint: disable=protected-access
            macropad._speaker_enable.value = False
        gc.collect()

        # Encoder button pauses/resumes game.
        macropad.encoder_switch_debounced.update()
        if macropad.encoder_switch_debounced.pressed:
            for n in (True, False, True): # Press, release, press
                while n == macropad.encoder_switch_debounced.pressed:
                    macropad.encoder_switch_debounced.update()
            # Sprite start times must be offset by pause duration
            # because time.monotonic() is used for drop physics.
            now = time.monotonic() - now # Pause duration
            for sprite in sprites:
                sprite.start_time += now

    # GAME OVER ------------------------

    time.sleep(1.5) # Pause display for a moment
    macropad.pixels.fill(0)
    macropad.pixels.show()
    # pylint: disable=protected-access
    macropad._speaker_enable.value = False
    # Pop any sprites from play_group (other elements remain, and sprites[]
    # list is cleared at start of next game).
    for _ in sprites:
        play_group.pop(1)
    end_group[-1].text = str(score)
    show_screen(end_group)

This guide was first published on Aug 04, 2021. It was last updated on Aug 04, 2021.

This page (Project Code) was last updated on Jun 08, 2023.

Text editor powered by tinymce.