| 
 | 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)  | 
0 commit comments