Skip to content

Commit bd7536a

Browse files
authored
Merge pull request adafruit#1676 from PaintYourDragon/main
Add Macropad_Dragon_Drop game
2 parents d83d646 + 103cd10 commit bd7536a

File tree

10 files changed

+282
-0
lines changed

10 files changed

+282
-0
lines changed

Macropad_Dragon_Drop/code.py

+271
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
"""
2+
Dragon Drop: a simple game for Adafruit MACROPAD. Uses OLED display in
3+
portrait (vertical) orientation. Tap one of four keys across a row to
4+
catch falling eggs before they hit the ground. Avoid fireballs.
5+
"""
6+
7+
# pylint: disable=import-error, unused-import
8+
import gc
9+
import random
10+
import time
11+
import displayio
12+
import adafruit_imageload
13+
from adafruit_macropad import MacroPad
14+
from adafruit_bitmap_font import bitmap_font
15+
from adafruit_display_text import label
16+
from adafruit_progressbar.progressbar import HorizontalProgressBar
17+
import board # These three can be removed
18+
import audiocore # if/when MacroPad library
19+
import audiopwmio # adds background audio
20+
21+
22+
# CONFIGURABLES ------------------------
23+
24+
MAX_EGGS = 7 # Max count of all projectiles; some are fireballs
25+
PATH = '/dragondrop/' # Location of graphics, fonts, WAVs, etc.
26+
27+
28+
# UTILITY FUNCTIONS AND CLASSES --------
29+
30+
def background_sound(filename):
31+
""" Start a WAV file playing in the background (non-blocking). This
32+
func can be removed if/when MacroPad lib gets background audio. """
33+
# pylint: disable=protected-access
34+
MACROPAD._speaker_enable.value = True
35+
AUDIO.play(audiocore.WaveFile(open(PATH + filename, 'rb')))
36+
37+
def show_screen(group):
38+
""" Activate a given displayio group, pause until keypress. """
39+
MACROPAD.display.show(group)
40+
MACROPAD.display.refresh()
41+
# Purge any queued up key events...
42+
while MACROPAD.keys.events.get():
43+
pass
44+
while True: # ...then wait for first new key press event
45+
event = MACROPAD.keys.events.get()
46+
if event and event.pressed:
47+
return
48+
49+
# pylint: disable=too-few-public-methods
50+
class Sprite:
51+
""" Class holds sprite (eggs, fireballs) state information. """
52+
def __init__(self, col, start_time):
53+
self.column = col # 0-3
54+
self.is_fire = (random.random() < 0.25) # 1/4 chance of fireballs
55+
self.start_time = start_time # For drop physics
56+
self.paused = False
57+
58+
59+
# ONE-TIME INITIALIZATION --------------
60+
61+
MACROPAD = MacroPad(rotation=90)
62+
MACROPAD.display.auto_refresh = False
63+
MACROPAD.pixels.auto_write = False
64+
MACROPAD.pixels.brightness = 0.5
65+
AUDIO = audiopwmio.PWMAudioOut(board.SPEAKER) # For background audio
66+
67+
FONT = bitmap_font.load_font(PATH + 'cursive-smart.pcf')
68+
69+
# Create 3 displayio groups -- one each for the title, play and end screens.
70+
71+
TITLE_GROUP = displayio.Group(max_size=1)
72+
TITLE_BITMAP, TITLE_PALETTE = adafruit_imageload.load(PATH + 'title.bmp',
73+
bitmap=displayio.Bitmap,
74+
palette=displayio.Palette)
75+
TITLE_GROUP.append(displayio.TileGrid(TITLE_BITMAP, pixel_shader=TITLE_PALETTE,
76+
width=1, height=1,
77+
tile_width=TITLE_BITMAP.width,
78+
tile_height=TITLE_BITMAP.height))
79+
80+
# Bitmap containing eggs, hatchling and fireballs
81+
SPRITE_BITMAP, SPRITE_PALETTE = adafruit_imageload.load(
82+
PATH + 'sprites.bmp', bitmap=displayio.Bitmap, palette=displayio.Palette)
83+
SPRITE_PALETTE.make_transparent(0)
84+
85+
PLAY_GROUP = displayio.Group(max_size=MAX_EGGS + 10)
86+
# Bitmap containing five shadow tiles ('no shadow' through 'max shadow')
87+
SHADOW_BITMAP, SHADOW_PALETTE = adafruit_imageload.load(
88+
PATH + 'shadow.bmp', bitmap=displayio.Bitmap, palette=displayio.Palette)
89+
# Tilegrid with four shadow tiles; one per column
90+
SHADOW = displayio.TileGrid(SHADOW_BITMAP, pixel_shader=SHADOW_PALETTE,
91+
width=4, height=1, tile_width=16,
92+
tile_height=SHADOW_BITMAP.height, x=0,
93+
y=MACROPAD.display.height - SHADOW_BITMAP.height)
94+
PLAY_GROUP.append(SHADOW)
95+
SHADOW_SCALE = 5 / (MACROPAD.display.height - 20) # For picking shadow sprite
96+
LIFE_BAR = HorizontalProgressBar((0, 0), (MACROPAD.display.width, 7),
97+
value=100, min_value=0, max_value=100,
98+
bar_color=0xFFFFFF, outline_color=0xFFFFFF,
99+
fill_color=0, margin_size=1)
100+
PLAY_GROUP.append(LIFE_BAR)
101+
# Score is last object in PLAY_GROUP, can be indexed as -1
102+
PLAY_GROUP.append(label.Label(FONT, text='0', max_glyphs=10, color=0xFFFFFF,
103+
anchor_point=(0.5, 0.0),
104+
anchored_position=(MACROPAD.display.width // 2,
105+
10)))
106+
107+
END_GROUP = displayio.Group(max_size=1)
108+
END_BITMAP, END_PALETTE = adafruit_imageload.load(
109+
PATH + 'gameover.bmp', bitmap=displayio.Bitmap, palette=displayio.Palette)
110+
END_GROUP.append(displayio.TileGrid(END_BITMAP, pixel_shader=END_PALETTE,
111+
width=1, height=1,
112+
tile_width=END_BITMAP.width,
113+
tile_height=END_BITMAP.height))
114+
END_GROUP.append(label.Label(FONT, text='0', max_glyphs=10, color=0xFFFFFF,
115+
anchor_point=(0.5, 0.0),
116+
anchored_position=(MACROPAD.display.width // 2,
117+
90)))
118+
119+
120+
# MAIN LOOP -- alternates play and end-game screens --------
121+
122+
show_screen(TITLE_GROUP) # Just do this once on startup
123+
124+
while True:
125+
126+
# NEW GAME -------------------------
127+
128+
SPRITES = []
129+
SCORE = 0
130+
PLAY_GROUP[-1].text = '0' # Score text
131+
LIFE_BAR.value = 100
132+
AUDIO.stop()
133+
MACROPAD.display.show(PLAY_GROUP)
134+
MACROPAD.display.refresh()
135+
START_TIME = time.monotonic()
136+
137+
# PLAY UNTIL LIFE BAR DEPLETED -----
138+
139+
while LIFE_BAR.value > 0:
140+
NOW = time.monotonic()
141+
SPEED = 10 + (NOW - START_TIME) / 30 # Gradually speed up
142+
FIRE_SPRITE = 3 + int((NOW * 6) % 2.0) # For animating fire
143+
144+
# Coalese any/all queued-up keypress events per column
145+
COLUMN_PRESSED = [False] * 4
146+
while True:
147+
EVENT = MACROPAD.keys.events.get()
148+
if not EVENT:
149+
break
150+
if EVENT.pressed:
151+
COLUMN_PRESSED[EVENT.key_number % 4] = True
152+
153+
# For determining upper/lower extents of active egg sprites per column
154+
COLUMN_MIN = [MACROPAD.display.height] * 4
155+
COLUMN_MAX = [0] * 4
156+
157+
# Traverse sprite list backwards so we can pop() without index problems
158+
for i in range(len(SPRITES) - 1, -1, -1):
159+
sprite = SPRITES[i]
160+
tile = PLAY_GROUP[i + 1] # Corresponding 1x1 TileGrid for sprite
161+
column = sprite.column
162+
elapsed = NOW - sprite.start_time # Time since add or pause event
163+
164+
if sprite.is_fire:
165+
tile[0] = FIRE_SPRITE # Animate all flame sprites
166+
167+
if sprite.paused: # Sprite at bottom of screen
168+
if elapsed > 0.75: # Hold position for 3/4 second,
169+
for x in range(0, 9, 4): # then LEDs off,
170+
MACROPAD.pixels[x + sprite.column] = (0, 0, 0)
171+
SPRITES.pop(i) # and delete Sprite object and
172+
PLAY_GROUP.pop(i + 1) # element from displayio group
173+
continue
174+
if not sprite.is_fire:
175+
COLUMN_MAX[column] = max(COLUMN_MAX[column],
176+
MACROPAD.display.height - 22)
177+
else: # Sprite in motion
178+
y = SPEED * elapsed * elapsed - 16
179+
# Track top of all sprites, bottom of eggs only
180+
COLUMN_MIN[column] = min(COLUMN_MIN[column], y)
181+
if not sprite.is_fire:
182+
COLUMN_MAX[column] = max(COLUMN_MAX[column], y)
183+
tile.y = int(y) # Sprite's vertical pos. in PLAY_GROUP
184+
185+
# Handle various catch or off-bottom actions...
186+
if sprite.is_fire:
187+
if y >= MACROPAD.display.height: # Off bottom of screen,
188+
SPRITES.pop(i) # remove fireball sprite
189+
PLAY_GROUP.pop(i + 1)
190+
continue
191+
elif y >= MACROPAD.display.height - 40:
192+
if COLUMN_PRESSED[column]:
193+
# Fireball caught, ouch!
194+
background_sound('sizzle.wav') # I smell bacon
195+
sprite.paused = True
196+
sprite.start_time = NOW
197+
tile.y = MACROPAD.display.height - 20
198+
LIFE_BAR.value = max(0, LIFE_BAR.value - 5)
199+
for x in range(0, 9, 4):
200+
MACROPAD.pixels[x + sprite.column] = (255, 0, 0)
201+
else: # Is egg...
202+
if y >= MACROPAD.display.height - 22:
203+
# Egg hit ground
204+
background_sound('splat.wav')
205+
sprite.paused = True
206+
sprite.start_time = NOW
207+
tile.y = MACROPAD.display.height - 22
208+
tile[0] = 1 # Change sprite to broken egg
209+
LIFE_BAR.value = max(0, LIFE_BAR.value - 5)
210+
MACROPAD.pixels[8 + sprite.column] = (255, 255, 0)
211+
elif COLUMN_PRESSED[column]:
212+
if y >= MACROPAD.display.height - 40:
213+
# Egg caught at right time
214+
background_sound('rawr.wav')
215+
sprite.paused = True
216+
sprite.start_time = NOW
217+
tile.y = MACROPAD.display.height - 22
218+
tile[0] = 2 # Hatchling
219+
MACROPAD.pixels[4 + sprite.column] = (0, 255, 0)
220+
SCORE += 10
221+
PLAY_GROUP[-1].text = str(SCORE)
222+
elif y >= MACROPAD.display.height - 58:
223+
# Egg caught too early
224+
background_sound('splat.wav')
225+
sprite.paused = True
226+
sprite.start_time = NOW
227+
tile.y = MACROPAD.display.height - 40
228+
tile[0] = 1 # Broken egg
229+
LIFE_BAR.value = max(0, LIFE_BAR.value - 5)
230+
MACROPAD.pixels[sprite.column] = (255, 255, 0)
231+
232+
# Select shadow bitmaps based on each column's lowest egg
233+
for i in range(4):
234+
SHADOW[i] = min(4, int(COLUMN_MAX[i] * SHADOW_SCALE))
235+
236+
# Time to introduce a new sprite? 1/20 chance each frame, if space
237+
if (len(SPRITES) < MAX_EGGS and random.random() < 0.05 and
238+
max(COLUMN_MIN) > 16):
239+
# Pick a column randomly...if it's occupied, keep trying...
240+
while True:
241+
COLUMN = random.randint(0, 3)
242+
if COLUMN_MIN[COLUMN] > 16:
243+
# Found a clear spot. Add sprite and break loop
244+
SPRITES.append(Sprite(COLUMN, NOW))
245+
PLAY_GROUP.insert(-2, displayio.TileGrid(SPRITE_BITMAP,
246+
pixel_shader=SPRITE_PALETTE,
247+
width=1, height=1,
248+
tile_width=16,
249+
tile_height=SPRITE_BITMAP.height,
250+
x=COLUMN * 16,
251+
y=-16))
252+
break
253+
254+
MACROPAD.display.refresh()
255+
MACROPAD.pixels.show()
256+
if not AUDIO.playing:
257+
# pylint: disable=protected-access
258+
MACROPAD._speaker_enable.value = False
259+
gc.collect()
260+
261+
# GAME OVER ------------------------
262+
263+
time.sleep(1.5) # Pause display for a moment
264+
MACROPAD.pixels.fill(0)
265+
MACROPAD.pixels.show()
266+
# Pop any sprites from PLAY_GROUP (other elements remain, and SPRITES[]
267+
# list is cleared at start of next game).
268+
for _ in SPRITES:
269+
PLAY_GROUP.pop(1)
270+
END_GROUP[-1].text = str(SCORE)
271+
show_screen(END_GROUP)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Copyright (c) 1999, Thomas A. Fine
2+
3+
License to copy and distribute for both commercial and
4+
non-commercial use is herby granted, provided this notice
5+
is preserved.
6+
7+
8+
http://hea-www.harvard.edu/~fine/
9+
10+
Produced with bdfedit, a tcl/tk font editing program
11+
written by Thomas A. Fine
Binary file not shown.
1.06 KB
Binary file not shown.
27.4 KB
Binary file not shown.
208 Bytes
Binary file not shown.
22.7 KB
Binary file not shown.
12.6 KB
Binary file not shown.
828 Bytes
Binary file not shown.
1.06 KB
Binary file not shown.

0 commit comments

Comments
 (0)