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.root_group = 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.root_group = 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)
Page last edited January 22, 2025
Text editor powered by tinymce.