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